FullStackHero implements permission-based authorization where access control is determined by specific permissions rather than roles. Roles and groups are containers for permissions.
Permissions are defined as constants in the Shared project:
IdentityPermissionConstants.cs
namespace FSH.Framework.Shared.Identity;public static class IdentityPermissionConstants{ public static class Users { public const string View = "Permissions.Users.View"; public const string Create = "Permissions.Users.Create"; public const string Update = "Permissions.Users.Update"; public const string Delete = "Permissions.Users.Delete"; public const string ManageRoles = "Permissions.Users.ManageRoles"; } public static class Roles { public const string View = "Permissions.Roles.View"; public const string Create = "Permissions.Roles.Create"; public const string Update = "Permissions.Roles.Update"; public const string Delete = "Permissions.Roles.Delete"; } public static class Groups { public const string View = "Permissions.Groups.View"; public const string Create = "Permissions.Groups.Create"; public const string Update = "Permissions.Groups.Update"; public const string Delete = "Permissions.Groups.Delete"; public const string ManageMembers = "Permissions.Groups.ManageMembers"; }}
Permission constants follow the pattern: Permissions.{Resource}.{Action}
The .RequirePermission() extension method adds metadata to endpoints:
EndpointExtensions.cs
using Microsoft.AspNetCore.Builder;namespace FSH.Framework.Shared.Identity.Authorization;public static class EndpointExtensions{ public static TBuilder RequirePermission<TBuilder>( this TBuilder endpointConventionBuilder, string requiredPermission, params string[] additionalRequiredPermissions) where TBuilder : IEndpointConventionBuilder { return endpointConventionBuilder.WithMetadata( new RequiredPermissionAttribute(requiredPermission, additionalRequiredPermissions)); }}
// In your seed data or admin UIvar adminRole = new FshRole{ Name = "Admin", Description = "Administrator role"};var permissions = new[]{ IdentityPermissionConstants.Users.View, IdentityPermissionConstants.Users.Create, IdentityPermissionConstants.Users.Update, IdentityPermissionConstants.Users.Delete, IdentityPermissionConstants.Groups.View, IdentityPermissionConstants.Groups.Create};foreach (var permission in permissions){ await roleManager.AddClaimAsync(adminRole, new Claim(ClaimTypes.Permission, permission));}
When checking permissions, the system resolves permissions from:
Direct user roles → Permissions
User groups → Group roles → Permissions
// Pseudo-code for permission resolutionvar userPermissions = new HashSet<string>();// Get permissions from user's direct rolesforeach (var role in user.Roles){ userPermissions.UnionWith(role.Claims.Where(c => c.Type == "Permission").Select(c => c.Value));}// Get permissions from user's groupsforeach (var group in user.Groups){ foreach (var role in group.Roles) { userPermissions.UnionWith(role.Claims.Where(c => c.Type == "Permission").Select(c => c.Value)); }}
public async ValueTask<GroupDto> Handle(DeleteGroupCommand command, CancellationToken cancellationToken){ var userId = _currentUser.GetUserId().ToString(); // Manual permission check if (!await _userService.HasPermissionAsync(userId, IdentityPermissionConstants.Groups.Delete, cancellationToken)) { throw new UnauthorizedAccessException("You do not have permission to delete groups."); } // ... rest of handler logic}