Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
259 changes: 245 additions & 14 deletions src/Microservices.sln

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// ---------------------------------------
// Email: quickapp@ebenmonney.com
// Templates: www.ebenmonney.com/templates
// (c) 2024 www.ebenmonney.com/mit-license
// ---------------------------------------

using System.Collections;
using System.ComponentModel.DataAnnotations;

namespace Identity.API.Attributes
{
[AttributeUsage(AttributeTargets.Property)]
public sealed class MinimumCountAttribute(int minCount, bool required = true, bool allowEmptyStringValues = false) :
ValidationAttribute("'{0}' must have at least {1} item.")
{
public MinimumCountAttribute() : this(1)
{

}

public override bool IsValid(object? value)
{
if (value == null)
return !required;

if (!allowEmptyStringValues && value is ICollection<string> stringList)
return stringList.Count(s => !string.IsNullOrWhiteSpace(s)) >= minCount;

if (value is ICollection list)
return list.Count >= minCount;

return false;
}

public override string FormatErrorMessage(string name)
{
return string.Format(ErrorMessageString, name, minCount);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// ---------------------------------------
// Email: quickapp@ebenmonney.com
// Templates: www.ebenmonney.com/templates
// (c) 2024 www.ebenmonney.com/mit-license
// ---------------------------------------

using Microsoft.AspNetCore.Mvc.Filters;

namespace Identity.API.Attributes
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class SanitizeModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
foreach (var arg in context.ActionArguments.Values)
{
if (arg is ISanitizeModel model)
{
model.SanitizeModel();
}
}
}
}

public interface ISanitizeModel
{
public void SanitizeModel();
}
}
29 changes: 29 additions & 0 deletions src/Services/Identity/Identity.API/Authorization/AuthPolicies.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// ---------------------------------------
// Email: quickapp@ebenmonney.com
// Templates: www.ebenmonney.com/templates
// (c) 2024 www.ebenmonney.com/mit-license
// ---------------------------------------

namespace Identity.API.Authorization
{
public static class AuthPolicies
{
///<summary>Policy to allow viewing all user records.</summary>
public const string ViewAllUsersPolicy = "View All Users";

///<summary>Policy to allow adding, removing and updating all user records.</summary>
public const string ManageAllUsersPolicy = "Manage All Users";

/// <summary>Policy to allow viewing details of all roles.</summary>
public const string ViewAllRolesPolicy = "View All Roles";

/// <summary>Policy to allow viewing details of all or specific roles (Requires roleName as parameter).</summary>
public const string ViewRoleByRoleNamePolicy = "View Role by RoleName";

/// <summary>Policy to allow adding, removing and updating all roles.</summary>
public const string ManageAllRolesPolicy = "Manage All Roles";

/// <summary>Policy to allow assigning roles the user has access to (Requires new and current roles as parameter).</summary>
public const string AssignAllowedRolesPolicy = "Assign Allowed Roles";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// ---------------------------------------
// Email: quickapp@ebenmonney.com
// Templates: www.ebenmonney.com/templates
// (c) 2024 www.ebenmonney.com/mit-license
// ---------------------------------------

using Microsoft.AspNetCore.Authorization;
using Identity.Domain.Authorization;
using Identity.Domain.Exceptions;
using Identity.Domain.Interfaces;
using System.Security.Claims;

namespace Identity.API.Authorization.Requirements
{
public class AssignRolesAuthorizationRequirement : IAuthorizationRequirement
{

}

public class AssignRolesAuthorizationHandler :
AuthorizationHandler<AssignRolesAuthorizationRequirement, (string[] newRoles, string[] currentRoles)>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
AssignRolesAuthorizationRequirement requirement, (string[] newRoles, string[] currentRoles) roles)
{
if (!GetIsRolesChanged(roles.newRoles, roles.currentRoles))
{
context.Succeed(requirement);
}
else if (context.User.HasClaim(CustomClaims.Permission, ApplicationPermissions.AssignRoles))
{
// If user has ViewRoles permission, then he can assign any roles
if (context.User.HasClaim(CustomClaims.Permission, ApplicationPermissions.ViewRoles))
context.Succeed(requirement);

// Else user can only assign roles they're part of
else if (GetIsUserInAllAddedRoles(context.User, roles.newRoles, roles.currentRoles))
context.Succeed(requirement);
}

return Task.CompletedTask;
}

private static bool GetIsRolesChanged(string[] newRoles, string[] currentRoles)
{
newRoles ??= [];
currentRoles ??= [];

var roleAdded = newRoles.Except(currentRoles).Any();
var roleRemoved = currentRoles.Except(newRoles).Any();

return roleAdded || roleRemoved;
}

private static bool GetIsUserInAllAddedRoles(ClaimsPrincipal contextUser, string[] newRoles, string[] currentRoles)
{
newRoles ??= [];
currentRoles ??= [];

var addedRoles = newRoles.Except(currentRoles);

return addedRoles.All(contextUser.IsInRole);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// ---------------------------------------
// Email: quickapp@ebenmonney.com
// Templates: www.ebenmonney.com/templates
// (c) 2024 www.ebenmonney.com/mit-license
// ---------------------------------------

using Microsoft.AspNetCore.Authorization;
using Identity.Domain.Authorization;
using Identity.Domain.Exceptions;
using Identity.Domain.Interfaces;
using Identity.API.Services;
using System.Security.Claims;

namespace Identity.API.Authorization.Requirements
{
public class UserAccountAuthorizationRequirement(string operationName) : IAuthorizationRequirement
{
public string OperationName { get; private set; } = operationName;
}

public class ViewUserAuthorizationHandler : AuthorizationHandler<UserAccountAuthorizationRequirement, string>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context, UserAccountAuthorizationRequirement requirement, string targetUserId)
{
if (context.User == null || requirement.OperationName != UserAccountManagementOperations.ReadOperationName)
return Task.CompletedTask;

if (context.User.HasClaim(CustomClaims.Permission, ApplicationPermissions.ViewUsers)
|| GetIsSameUser(context.User, targetUserId))
context.Succeed(requirement);

return Task.CompletedTask;
}

private static bool GetIsSameUser(ClaimsPrincipal user, string targetUserId)
{
if (string.IsNullOrWhiteSpace(targetUserId))
return false;

return Utilities.GetUserId(user) == targetUserId;
}
}

public class ManageUserAuthorizationHandler : AuthorizationHandler<UserAccountAuthorizationRequirement, string>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context, UserAccountAuthorizationRequirement requirement, string targetUserId)
{
if (context.User == null ||
(requirement.OperationName != UserAccountManagementOperations.CreateOperationName &&
requirement.OperationName != UserAccountManagementOperations.UpdateOperationName &&
requirement.OperationName != UserAccountManagementOperations.DeleteOperationName))
return Task.CompletedTask;

if (context.User.HasClaim(CustomClaims.Permission, ApplicationPermissions.ManageUsers)
|| GetIsSameUser(context.User, targetUserId))
context.Succeed(requirement);

return Task.CompletedTask;
}

private static bool GetIsSameUser(ClaimsPrincipal user, string targetUserId)
{
if (string.IsNullOrWhiteSpace(targetUserId))
return false;

return Utilities.GetUserId(user) == targetUserId;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// ---------------------------------------
// Email: quickapp@ebenmonney.com
// Templates: www.ebenmonney.com/templates
// (c) 2024 www.ebenmonney.com/mit-license
// ---------------------------------------

using Microsoft.AspNetCore.Authorization;
using Identity.Domain.Authorization;
using Identity.Domain.Exceptions;
using Identity.Domain.Interfaces;

namespace Identity.API.Authorization.Requirements
{
public class ViewRoleAuthorizationRequirement : IAuthorizationRequirement
{

}

public class ViewRoleAuthorizationHandler : AuthorizationHandler<ViewRoleAuthorizationRequirement, string>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context, ViewRoleAuthorizationRequirement requirement, string roleName)
{
if (context.User == null)
return Task.CompletedTask;

if (context.User.HasClaim(CustomClaims.Permission, ApplicationPermissions.ViewRoles)
|| context.User.IsInRole(roleName))
context.Succeed(requirement);

return Task.CompletedTask;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// ---------------------------------------
// Email: quickapp@ebenmonney.com
// Templates: www.ebenmonney.com/templates
// (c) 2024 www.ebenmonney.com/mit-license
// ---------------------------------------

using Microsoft.AspNetCore.Authorization;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;

namespace Identity.API.Authorization
{
// Swagger IOperationFilter implementation that will decide which api action needs authorization
internal class SwaggerAuthorizeOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
// Check for authorize attribute
var hasAuthorize = context.MethodInfo.DeclaringType?.GetCustomAttributes(true)
.Union(context.MethodInfo.GetCustomAttributes(true))
.OfType<AuthorizeAttribute>()
.Any();

if (hasAuthorize == true)
{
operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });

var oAuthScheme = new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" }
};

operation.Security =
[
new() { [oAuthScheme] = [] }
];
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// ---------------------------------------
// Email: quickapp@ebenmonney.com
// Templates: www.ebenmonney.com/templates
// (c) 2024 www.ebenmonney.com/mit-license
// ---------------------------------------

using Identity.API.Authorization.Requirements;

namespace Identity.API.Authorization
{
/// <summary>
/// Operation Policy to allow adding, viewing, updating and deleting general or specific user records.
/// </summary>
public static class UserAccountManagementOperations
{
public const string CreateOperationName = "Create";
public const string ReadOperationName = "Read";
public const string UpdateOperationName = "Update";
public const string DeleteOperationName = "Delete";

public static readonly UserAccountAuthorizationRequirement CreateOperationRequirement = new(CreateOperationName);
public static readonly UserAccountAuthorizationRequirement ReadOperationRequirement = new(ReadOperationName);
public static readonly UserAccountAuthorizationRequirement UpdateOperationRequirement = new(UpdateOperationName);
public static readonly UserAccountAuthorizationRequirement DeleteOperationRequirement = new(DeleteOperationName);
}
}
53 changes: 53 additions & 0 deletions src/Services/Identity/Identity.API/Configuration/MappingProfile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// ---------------------------------------
// Email: quickapp@ebenmonney.com
// Templates: www.ebenmonney.com/templates
// (c) 2024 www.ebenmonney.com/mit-license
// ---------------------------------------

using AutoMapper;
using Microsoft.AspNetCore.Identity;
using Identity.Domain.Authorization;
using Identity.Domain.Entities;
using Identity.API.ViewModels.Account;

namespace Identity.API.Configuration
{
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<ApplicationUser, UserVM>()
.ForMember(d => d.Roles, map => map.Ignore());
CreateMap<UserVM, ApplicationUser>()
.ForMember(d => d.Roles, map => map.Ignore())
.ForMember(d => d.Id, map => map.Condition(src => src.Id != null));

CreateMap<ApplicationUser, UserEditVM>()
.ForMember(d => d.Roles, map => map.Ignore());
CreateMap<UserEditVM, ApplicationUser>()
.ForMember(d => d.Roles, map => map.Ignore())
.ForMember(d => d.Id, map => map.Condition(src => src.Id != null));

CreateMap<ApplicationUser, UserPatchVM>()
.ReverseMap();

CreateMap<ApplicationRole, RoleVM>()
.ForMember(d => d.Permissions, map => map.MapFrom(s => s.Claims))
.ForMember(d => d.UsersCount, map => map.MapFrom(s => s.Users != null ? s.Users.Count : 0))
.ReverseMap();
CreateMap<RoleVM, ApplicationRole>()
.ForMember(d => d.Id, map => map.Condition(src => src.Id != null));

CreateMap<IdentityRoleClaim<string>, ClaimVM>()
.ForMember(d => d.Type, map => map.MapFrom(s => s.ClaimType))
.ForMember(d => d.Value, map => map.MapFrom(s => s.ClaimValue))
.ReverseMap();

CreateMap<ApplicationPermission, PermissionVM>()
.ReverseMap();

CreateMap<IdentityRoleClaim<string>, PermissionVM>()
.ConvertUsing(s => ((PermissionVM)ApplicationPermissions.GetPermissionByValue(s.ClaimValue))!);
}
}
}
Loading