diff --git a/Dappi.Core/Models/MethodRouteEntry.cs b/Dappi.Core/Models/MethodRouteEntry.cs new file mode 100644 index 00000000..6dfe7a38 --- /dev/null +++ b/Dappi.Core/Models/MethodRouteEntry.cs @@ -0,0 +1,7 @@ +namespace Dappi.Core.Models; + +public class MethodRouteEntry +{ + public string MethodName { get; set; } = string.Empty; + public string HttpRoute { get; set; } = string.Empty; +} diff --git a/Dappi.HeadlessCms.UsersAndPermissions/Controllers/RolePermissionDto.cs b/Dappi.HeadlessCms.UsersAndPermissions/Controllers/RolePermissionDto.cs new file mode 100644 index 00000000..88ea7027 --- /dev/null +++ b/Dappi.HeadlessCms.UsersAndPermissions/Controllers/RolePermissionDto.cs @@ -0,0 +1,9 @@ +namespace Dappi.HeadlessCms.UsersAndPermissions.Controllers +{ + public class RolePermissionDto + { + public string PermissionName { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public bool Selected { get; set; } + } +} diff --git a/Dappi.HeadlessCms.UsersAndPermissions/Controllers/UsersAndPermissionsController.cs b/Dappi.HeadlessCms.UsersAndPermissions/Controllers/UsersAndPermissionsController.cs new file mode 100644 index 00000000..72bd7ced --- /dev/null +++ b/Dappi.HeadlessCms.UsersAndPermissions/Controllers/UsersAndPermissionsController.cs @@ -0,0 +1,75 @@ +using Dappi.HeadlessCms.UsersAndPermissions.Database; +using Dappi.HeadlessCms.UsersAndPermissions.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Dappi.HeadlessCms.UsersAndPermissions.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class UsersAndPermissionsController( + IDbContextAccessor usersAndPermissionsDb, + AvailablePermissionsRepository availablePermissionsRepository +) : ControllerBase +{ + private readonly UsersAndPermissionsDbContext _usersAndPermissionsDb = + usersAndPermissionsDb.DbContext; + + [HttpGet] + public async Task>>> GetRolePermissions( + string roleName, + CancellationToken cancellationToken + ) + { + if (string.IsNullOrWhiteSpace(roleName)) + return BadRequest("Role name is required."); + + var role = await _usersAndPermissionsDb + .AppRoles.Include(r => r.Permissions) + .FirstOrDefaultAsync(r => r.Name == roleName, cancellationToken); + + if (role is null) + return NotFound($"Role '{roleName}' not found."); + + var assignedPermissionNames = role + .Permissions.Select(p => p.Name) + .Where(n => !string.IsNullOrWhiteSpace(n)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + var availablePermissions = availablePermissionsRepository.GetAllPermissions(); + + var result = new Dictionary>( + StringComparer.OrdinalIgnoreCase + ); + + foreach (var perm in availablePermissions) + { + if (string.IsNullOrWhiteSpace(perm.Name)) + continue; + + var parts = perm.Name.Split(':', 2); + if (parts.Length < 2) + continue; + + var controllerName = parts[0]; + var methodName = parts[1]; + + if (!result.TryGetValue(controllerName, out var list)) + { + list = new List(); + result[controllerName] = list; + } + + list.Add( + new RolePermissionDto + { + PermissionName = methodName, + Description = perm.Description ?? string.Empty, + Selected = assignedPermissionNames.Contains(perm.Name), + } + ); + } + + return Ok(result); + } +} diff --git a/Dappi.HeadlessCms.UsersAndPermissions/Core/AppPermission.cs b/Dappi.HeadlessCms.UsersAndPermissions/Core/AppPermission.cs new file mode 100644 index 00000000..5eb4b027 --- /dev/null +++ b/Dappi.HeadlessCms.UsersAndPermissions/Core/AppPermission.cs @@ -0,0 +1,24 @@ +namespace Dappi.HeadlessCms.UsersAndPermissions.Core +{ + public class AppPermission + { + private AppPermission() { } // For EF Core + + public AppPermission(string name, string description) + { + Name = name; + Description = description; + } + + public int Id { get; private set; } + public string Name { get; private set; } + public string Description { get; private set; } + + public List Roles { get; private set; } = []; + + public override string ToString() + { + return Name; + } + } +} diff --git a/Dappi.HeadlessCms.UsersAndPermissions/Core/AppRole.cs b/Dappi.HeadlessCms.UsersAndPermissions/Core/AppRole.cs new file mode 100644 index 00000000..2d69f19f --- /dev/null +++ b/Dappi.HeadlessCms.UsersAndPermissions/Core/AppRole.cs @@ -0,0 +1,61 @@ +namespace Dappi.HeadlessCms.UsersAndPermissions.Core +{ + public class AppRole + { + public int Id { get; private set; } + public string Name { get; private set; } + + private readonly List _permissions = []; + public IEnumerable Users { get; set; } + + private AppRole() { } // For EF Core + + public IReadOnlyList Permissions => _permissions.AsReadOnly(); + + public bool IsDefaultForAuthenticatedUser { get; private set; } + + public AppRole(string name, IEnumerable permissions) + { + Name = name; + _permissions.AddRange(permissions); + } + + public bool HasPermission(string permissionName) => + _permissions.Any(p => p.Name == permissionName); + + public static AppRole CreateDefaultPublicUserRole(IEnumerable permissions) + { + return new AppRole(UsersAndPermissionsConstants.DefaultRoles.Public, permissions); + } + + public static AppRole CreateDefaultAuthenticatedUserRole( + IEnumerable permissions + ) + { + return new AppRole(UsersAndPermissionsConstants.DefaultRoles.Authenticated, permissions) + { + IsDefaultForAuthenticatedUser = true, + }; + } + + public void AddPermission(AppPermission permission) + { + if (_permissions.All(p => p.Name != permission.Name)) + _permissions.Add(permission); + } + + public void RemovePermission(AppPermission permission) + { + var existing = _permissions.FirstOrDefault(p => p.Name == permission.Name); + if (existing != null) + _permissions.Remove(existing); + } + + public void ClearPermissions() => _permissions.Clear(); + + public override string ToString() + { + return Name; + } + } +} diff --git a/Dappi.HeadlessCms.UsersAndPermissions/Core/AppRoleAndPermissionsBuilder.cs b/Dappi.HeadlessCms.UsersAndPermissions/Core/AppRoleAndPermissionsBuilder.cs new file mode 100644 index 00000000..044b09d1 --- /dev/null +++ b/Dappi.HeadlessCms.UsersAndPermissions/Core/AppRoleAndPermissionsBuilder.cs @@ -0,0 +1,170 @@ +using System.Reflection; + +namespace Dappi.HeadlessCms.UsersAndPermissions.Core +{ + public interface IRoleStage + where TUser : IAppUser + { + IControllerStage ForRole(string roleName, bool isDefaultForAuthenticated = false); + AppRole[] Build(); + } + + public interface IControllerStage + where TUser : IAppUser + { + IPermissionStage ForController(string controllerName); + } + + public interface IPermissionStage + where TUser : IAppUser + { + IPermissionStage Allow(string methodName); + IPermissionStage AllowAll(); + IPermissionStage Deny(string methodName); + IPermissionStage ForController(string controllerName); + IRoleStage And(); + } + + public class AppRoleAndPermissionsBuilder + : IRoleStage, + IControllerStage, + IPermissionStage + where TUser : IAppUser + { + private record RoleEntry( + string RoleName, + string Controller, + string Method, + bool IsDefaultForAuthenticated + ); + + private readonly List _entries = []; + private string? _currentRole; + private string? _currentController; + private bool _currentIsDefaultForAuthenticated; + + public static IRoleStage Create() => new AppRoleAndPermissionsBuilder(); + + public IControllerStage ForRole( + string roleName, + bool isDefaultForAuthenticated = false + ) + { + _currentRole = roleName ?? throw new ArgumentNullException(nameof(roleName)); + _currentIsDefaultForAuthenticated = isDefaultForAuthenticated; + return this; + } + + IPermissionStage IControllerStage.ForController(string controllerName) => + SetController(controllerName); + + IPermissionStage IPermissionStage.ForController(string controllerName) => + SetController(controllerName); + + public IPermissionStage Allow(string methodName) + { + AddEntry(methodName); + return this; + } + + public IPermissionStage Deny(string methodName) + { + _entries.RemoveAll(e => + e.RoleName == _currentRole + && e.Controller == _currentController + && e.Method == methodName + ); + return this; + } + + public IPermissionStage AllowAll() + { + if (_currentController == null) + throw new InvalidOperationException("Call ForController() before AllowAll()."); + + var controllerType = + AppDomain + .CurrentDomain.GetAssemblies() + .SelectMany(a => + { + try + { + return a.GetTypes(); + } + catch + { + return Array.Empty(); + } + }) + .FirstOrDefault(t => t.Name == _currentController) + ?? throw new InvalidOperationException( + $"Controller type '{_currentController}' not found." + ); + + var methods = controllerType + .GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + .Where(m => !m.IsSpecialName) + .Select(m => m.Name) + .Distinct(); + + foreach (var method in methods) + AddEntry(method); + + return this; + } + + public IRoleStage And() => this; + + public AppRole[] Build() + { + return _entries + .GroupBy(e => e.RoleName) + .Select(roleGroup => + { + var permissions = roleGroup + .Select(e => new AppPermission($"{e.Controller}:{e.Method}", "")) + .ToList(); + + var isDefault = roleGroup.First().IsDefaultForAuthenticated; + + return isDefault + ? AppRole.CreateDefaultAuthenticatedUserRole(permissions) + : new AppRole(roleGroup.Key, permissions); + }) + .ToArray(); + } + + private AppRoleAndPermissionsBuilder SetController(string controllerName) + { + _currentController = + controllerName ?? throw new ArgumentNullException(nameof(controllerName)); + return this; + } + + private void AddEntry(string methodName) + { + if (_currentRole == null) + throw new InvalidOperationException("No role selected."); + if (_currentController == null) + throw new InvalidOperationException("No controller selected."); + if (string.IsNullOrWhiteSpace(methodName)) + throw new ArgumentException("Method name required.", nameof(methodName)); + + var alreadyAdded = _entries.Any(e => + e.RoleName == _currentRole + && e.Controller == _currentController + && e.Method == methodName + ); + + if (!alreadyAdded) + _entries.Add( + new RoleEntry( + _currentRole, + _currentController, + methodName, + _currentIsDefaultForAuthenticated + ) + ); + } + } +} diff --git a/Dappi.HeadlessCms.UsersAndPermissions/Core/Constants.cs b/Dappi.HeadlessCms.UsersAndPermissions/Core/Constants.cs new file mode 100644 index 00000000..02951297 --- /dev/null +++ b/Dappi.HeadlessCms.UsersAndPermissions/Core/Constants.cs @@ -0,0 +1,11 @@ +namespace Dappi.HeadlessCms.UsersAndPermissions.Core +{ + public static class UsersAndPermissionsConstants + { + public static class DefaultRoles + { + public const string Authenticated = nameof(Authenticated); + public const string Public = nameof(Public); + } + } +} diff --git a/Dappi.HeadlessCms.UsersAndPermissions/Core/IAppUser.cs b/Dappi.HeadlessCms.UsersAndPermissions/Core/IAppUser.cs new file mode 100644 index 00000000..698c06f8 --- /dev/null +++ b/Dappi.HeadlessCms.UsersAndPermissions/Core/IAppUser.cs @@ -0,0 +1,8 @@ +namespace Dappi.HeadlessCms.UsersAndPermissions.Core +{ + public interface IAppUser + { + int RoleId { get; } + AppRole? Role { get; } + } +} diff --git a/Dappi.HeadlessCms.UsersAndPermissions/Dappi.HeadlessCms.UsersAndPermissions.csproj b/Dappi.HeadlessCms.UsersAndPermissions/Dappi.HeadlessCms.UsersAndPermissions.csproj new file mode 100644 index 00000000..54461a08 --- /dev/null +++ b/Dappi.HeadlessCms.UsersAndPermissions/Dappi.HeadlessCms.UsersAndPermissions.csproj @@ -0,0 +1,22 @@ + + + net9.0 + enable + enable + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/Dappi.HeadlessCms.UsersAndPermissions/Database/DbContextAccessor.cs b/Dappi.HeadlessCms.UsersAndPermissions/Database/DbContextAccessor.cs new file mode 100644 index 00000000..c8d8f630 --- /dev/null +++ b/Dappi.HeadlessCms.UsersAndPermissions/Database/DbContextAccessor.cs @@ -0,0 +1,15 @@ +namespace Dappi.HeadlessCms.UsersAndPermissions.Database +{ + public class DbContextAccessor : IDbContextAccessor + where TDbContext : UsersAndPermissionsDbContext + { + private readonly TDbContext _dbContext; + + public DbContextAccessor(TDbContext dbContext) + { + _dbContext = dbContext; + } + + public UsersAndPermissionsDbContext DbContext => _dbContext; + } +} diff --git a/Dappi.HeadlessCms.UsersAndPermissions/Database/UsersAndPermissionsDbContext.cs b/Dappi.HeadlessCms.UsersAndPermissions/Database/UsersAndPermissionsDbContext.cs new file mode 100644 index 00000000..ff62fba0 --- /dev/null +++ b/Dappi.HeadlessCms.UsersAndPermissions/Database/UsersAndPermissionsDbContext.cs @@ -0,0 +1,32 @@ +using Dappi.HeadlessCms.UsersAndPermissions.Core; +using Microsoft.EntityFrameworkCore; + +namespace Dappi.HeadlessCms.UsersAndPermissions.Database +{ + public abstract class UsersAndPermissionsDbContext(DbContextOptions options) + : DbContext(options) + { + public DbSet AppPermissions { get; set; } + public DbSet AppRoles { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasDefaultSchema("UsersAndPermissions"); + + modelBuilder.Entity(entity => + { + entity.HasKey(p => p.Id); + entity.Property(p => p.Name).IsRequired(); + entity.Property(p => p.Description).IsRequired(); + }); + modelBuilder.Entity(entity => + { + entity.HasKey(p => p.Id); + entity.Property(p => p.Name).IsRequired(); + entity.HasMany(r => r.Permissions); + }); + + base.OnModelCreating(modelBuilder); + } + } +} diff --git a/Dappi.HeadlessCms.UsersAndPermissions/Extensions.cs b/Dappi.HeadlessCms.UsersAndPermissions/Extensions.cs new file mode 100644 index 00000000..46675eec --- /dev/null +++ b/Dappi.HeadlessCms.UsersAndPermissions/Extensions.cs @@ -0,0 +1,123 @@ +using System.Text.Json.Serialization; +using Dappi.Core.Models; +using Dappi.HeadlessCms.UsersAndPermissions.Core; +using Dappi.HeadlessCms.UsersAndPermissions.Database; +using Dappi.HeadlessCms.UsersAndPermissions.Services; +using Microsoft.AspNetCore.Builder; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Npgsql; + +namespace Dappi.HeadlessCms.UsersAndPermissions +{ + public static class Extensions + { + public static IServiceCollection AddUsersAndPermissionsSystem( + this IServiceCollection services, + IReadOnlyDictionary> controllerRoutes, + IConfiguration configuration + ) + where TDbContext : UsersAndPermissionsDbContext + { + services.AddDbContext( + (_, options) => + { + var dataSourceBuilder = new NpgsqlDataSourceBuilder( + configuration.GetValue("Dappi:PostgresConnection") + ); + + options.UseNpgsql(dataSourceBuilder.Build()); + } + ); + services.AddScoped>(); + services.AddSingleton(new AvailablePermissionsRepository(controllerRoutes)); + + services + .AddControllers() + .AddApplicationPart( + typeof(Dappi.HeadlessCms.UsersAndPermissions.Controllers.UsersAndPermissionsController).Assembly + ) + .AddJsonOptions(options => + { + options.JsonSerializerOptions.PropertyNamingPolicy = null; + options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; + }); + return services; + } + + public static async Task UseUsersAndPermissionsSystem< + TDbContext, + TUser + >(this WebApplication app, AppRoleAndPermissionsBuilder appRoleAndPermissionsBuilder) + where TDbContext : UsersAndPermissionsDbContext + where TUser : class, IAppUser + { + using var scope = app.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var allPermissions = scope + .ServiceProvider.GetRequiredService() + .GetAllPermissions(); + + await db.Database.MigrateAsync(); + + db.AppPermissions.RemoveRange(db.AppPermissions); + await db.SaveChangesAsync(); + + await db.AppPermissions.AddRangeAsync(allPermissions); + await db.SaveChangesAsync(); + + var permissionsByName = await db.AppPermissions.ToDictionaryAsync(p => p.Name); + + var builtRoles = appRoleAndPermissionsBuilder.Build(); + + var existingRoles = await db.AppRoles.Include(r => r.Permissions).ToListAsync(); + + var existingRolesByName = existingRoles.ToDictionary(r => r.Name); + + var builtRoleNames = builtRoles.Select(r => r.Name).ToHashSet(); + + var orphanedRoles = await db + .AppRoles.Where(r => !builtRoleNames.Contains(r.Name)) + .Where(r => !db.Set().Any(u => u.RoleId == r.Id)) + .ToListAsync(); + + db.AppRoles.RemoveRange(orphanedRoles); + + foreach (var builtRole in builtRoles) + { + var resolvedPermissions = builtRole + .Permissions.Where(p => permissionsByName.ContainsKey(p.Name)) + .Select(p => permissionsByName[p.Name]) + .ToList(); + + if (existingRolesByName.TryGetValue(builtRole.Name, out var existingRole)) + { + existingRole.ClearPermissions(); + foreach (var perm in resolvedPermissions) + existingRole.AddPermission(perm); + } + else + { + var newRole = builtRole.Name switch + { + UsersAndPermissionsConstants.DefaultRoles.Public => + AppRole.CreateDefaultPublicUserRole(resolvedPermissions), + + UsersAndPermissionsConstants.DefaultRoles.Authenticated => + AppRole.CreateDefaultAuthenticatedUserRole(resolvedPermissions), + + _ => new AppRole(builtRole.Name, resolvedPermissions), + }; + + await db.AppRoles.AddAsync(newRole); + } + } + + await db.SaveChangesAsync(); + app.MapControllers(); + + return app; + } + } +} diff --git a/Dappi.HeadlessCms.UsersAndPermissions/Services/AvailablePermissionsRepository.cs b/Dappi.HeadlessCms.UsersAndPermissions/Services/AvailablePermissionsRepository.cs new file mode 100644 index 00000000..d4856194 --- /dev/null +++ b/Dappi.HeadlessCms.UsersAndPermissions/Services/AvailablePermissionsRepository.cs @@ -0,0 +1,28 @@ +using Dappi.Core.Models; +using Dappi.HeadlessCms.UsersAndPermissions.Core; + +namespace Dappi.HeadlessCms.UsersAndPermissions.Services +{ + public class AvailablePermissionsRepository( + IReadOnlyDictionary> controllerRoutes + ) + { + public IEnumerable GetAllPermissions() + { + var permissions = new List(); + foreach (var controller in controllerRoutes) + { + foreach (var methodRoute in controller.Value) + { + permissions.Add( + new AppPermission( + $"{controller.Key}:{methodRoute.MethodName}", + methodRoute.HttpRoute + ) + ); + } + } + return permissions; + } + } +} diff --git a/Dappi.HeadlessCms/ServiceExtensions.cs b/Dappi.HeadlessCms/ServiceExtensions.cs index c9218c64..93a807fc 100644 --- a/Dappi.HeadlessCms/ServiceExtensions.cs +++ b/Dappi.HeadlessCms/ServiceExtensions.cs @@ -7,7 +7,6 @@ using Dappi.HeadlessCms.Database; using Dappi.HeadlessCms.Database.Interceptors; using Dappi.HeadlessCms.Interfaces; -using Dappi.HeadlessCms.Models; using Dappi.HeadlessCms.Services; using Dappi.HeadlessCms.Services.Identity; using Dappi.HeadlessCms.Services.StorageServices; @@ -67,7 +66,6 @@ public static IServiceCollection AddDappi( services.AddScoped(); services.AddScoped(); - services.AddDappiSwaggerGen(); services.AddFluentValidationAutoValidation(); services.AddValidatorsFromAssemblyContaining(); @@ -264,7 +262,7 @@ public static IServiceCollection AddDappiAuthentication( return services; } - private static IServiceCollection AddDappiSwaggerGen(this IServiceCollection services) + public static IServiceCollection AddDappiSwaggerGen(this IServiceCollection services) { return services.AddSwaggerGen(c => { diff --git a/Dappi.SourceGenerator/Dappi.SourceGenerator.csproj b/Dappi.SourceGenerator/Dappi.SourceGenerator.csproj index d1ec6bf3..1ea64ab5 100644 --- a/Dappi.SourceGenerator/Dappi.SourceGenerator.csproj +++ b/Dappi.SourceGenerator/Dappi.SourceGenerator.csproj @@ -1,29 +1,34 @@  - - latest - netstandard2.0 - enable - true - - - Dappi.SourceGenerator - CodeChem - SourceGenerator for DAPPI - enable - - - - - - - - - - - - - - - - - \ No newline at end of file + + latest + netstandard2.0 + enable + true + + Dappi.SourceGenerator + CodeChem + SourceGenerator for DAPPI + enable + true + + + + + + + + + + + + + + + + + + diff --git a/Dappi.SourceGenerator/Generators/BaseSourceModelToSourceOutputGenerator.cs b/Dappi.SourceGenerator/Generators/BaseSourceModelToSourceOutputGenerator.cs index 498c28d1..e78cbd0e 100644 --- a/Dappi.SourceGenerator/Generators/BaseSourceModelToSourceOutputGenerator.cs +++ b/Dappi.SourceGenerator/Generators/BaseSourceModelToSourceOutputGenerator.cs @@ -26,7 +26,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var crudActions = classDeclaration.ExtractAllowedCrudActions(); - var authorizeAttributes = classSymbol.GetAttributes() + var authorizeAttributes = classSymbol + .GetAttributes() .Where(attr => attr.AttributeClass?.ToDisplayString() == authorizeAttributeName) .Select(attr => { @@ -40,37 +41,59 @@ public void Initialize(IncrementalGeneratorInitializationContext context) foreach (var namedArg in attr.NamedArguments) { - if (namedArg is { Key: nameof(DappiAuthorizeAttribute.Roles), Value.Kind: TypedConstantKind.Array }) + if ( + namedArg is + { + Key: nameof(DappiAuthorizeAttribute.Roles), + Value.Kind: TypedConstantKind.Array + } + ) { - roles = namedArg.Value.Values - .Select(v => v.Value?.ToString() ?? string.Empty) + roles = namedArg + .Value.Values.Select(v => v.Value?.ToString() ?? string.Empty) .ToList(); } - if (namedArg is { Key: nameof(DappiAuthorizeAttribute.Methods), Value.Kind: TypedConstantKind.Array }) + if ( + namedArg is + { + Key: nameof(DappiAuthorizeAttribute.Methods), + Value.Kind: TypedConstantKind.Array + } + ) { - methods = namedArg.Value.Values - .Select(v => + methods = namedArg + .Value.Values.Select(v => { - if (v is { Value: not null, Type: INamedTypeSymbol { TypeKind: TypeKind.Enum } enumType }) + if ( + v is + { + Value: not null, + Type: INamedTypeSymbol + { + TypeKind: TypeKind.Enum + } enumType + } + ) { - var enumMember = enumType.GetMembers() + var enumMember = enumType + .GetMembers() .OfType() - .FirstOrDefault(f => f.IsConst && Equals(f.ConstantValue, v.Value)); + .FirstOrDefault(f => + f.IsConst && Equals(f.ConstantValue, v.Value) + ); - return enumMember?.Name.ToUpperInvariant() ?? string.Empty; + return enumMember?.Name.ToUpperInvariant() + ?? string.Empty; } - return v.Value?.ToString()?.ToUpperInvariant() ?? string.Empty; + return v.Value?.ToString()?.ToUpperInvariant() + ?? string.Empty; }) .ToList(); } } - - return new DappiAuthorizeInfo - { - Roles = roles, - Methods = methods - }; + + return new DappiAuthorizeInfo { Roles = roles, Methods = methods }; }) .ToList(); @@ -81,14 +104,17 @@ public void Initialize(IncrementalGeneratorInitializationContext context) RootNamespace = classSymbol.ContainingNamespace.GetRootNamespace(), PropertiesInfos = GoThroughPropertiesAndGatherInfo(namedClassTypeSymbol), AuthorizeAttributes = authorizeAttributes, - CrudActions = crudActions.ToList() + CrudActions = crudActions.ToList(), }; - }); + } + ); var compilation = context.CompilationProvider.Combine(syntaxProvider.Collect()); context.RegisterSourceOutput(compilation, Execute); } - protected abstract void Execute(SourceProductionContext context, - (Compilation Compilation, ImmutableArray CollectedData) input); -} \ No newline at end of file + protected abstract void Execute( + SourceProductionContext context, + (Compilation Compilation, ImmutableArray CollectedData) input + ); +} diff --git a/Dappi.SourceGenerator/Generators/MethodAndPermissionsGenerator.cs b/Dappi.SourceGenerator/Generators/MethodAndPermissionsGenerator.cs new file mode 100644 index 00000000..f51e6f44 --- /dev/null +++ b/Dappi.SourceGenerator/Generators/MethodAndPermissionsGenerator.cs @@ -0,0 +1,218 @@ +using System.Collections.Immutable; +using Dappi.Core.Utils; +using Dappi.SourceGenerator.Models; +using Dappi.SourceGenerator.Utilities; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Scriban; + +namespace Dappi.SourceGenerator.Generators; + +[Generator] +public class MethodAndPermissionsGenerator : BaseSourceModelToSourceOutputGenerator +{ + private const string TemplateResourceName = + "Dappi.SourceGenerator.Templates.MethodsAndPermissionsTemplate.tpl"; + + private class MethodRouteEntry + { + public string MethodName { get; set; } = string.Empty; + public string HttpRoute { get; set; } = string.Empty; + } + + private class ControllerEntry + { + public string Controller_name { get; set; } = string.Empty; + public List Methods { get; set; } = new(); + } + + protected override void Execute( + SourceProductionContext context, + (Compilation Compilation, ImmutableArray CollectedData) input + ) + { + var (compilation, collectedData) = input; + + var httpMethodAttributesToVerb = new Dictionary + { + { "HttpGet", "GET" }, + { "HttpPost", "POST" }, + { "HttpPut", "PUT" }, + { "HttpDelete", "DELETE" }, + { "HttpPatch", "PATCH" }, + { "HttpHead", "HEAD" }, + { "HttpOptions", "OPTIONS" }, + }; + + // Aggregate all controllers with their methods + var controllers = new List(); + + foreach (var model in collectedData) + { + var controllerName = $"{model.ClassName}Controller"; + + var controllerEntry = new ControllerEntry + { + Controller_name = controllerName, + Methods = new List(), + }; + + var existingPartialController = FindPartialControllerForClass(compilation, model); + + if (existingPartialController is not null) + { + var methodsWithHttp = existingPartialController + .Members.OfType() + .Select(method => new + { + Method = method, + HttpAttr = method + .AttributeLists.SelectMany(al => al.Attributes) + .FirstOrDefault(IsHttpMethodAttribute), + }) + .Where(x => x.HttpAttr is not null) + .Select(x => + { + var routeArg = x + .HttpAttr?.ArgumentList?.Arguments.FirstOrDefault() + ?.ToFullString(); + var cleanedRoute = routeArg + ?.Replace("/", string.Empty) + .Replace("\"", string.Empty) + .Trim(); + + var verb = httpMethodAttributesToVerb[x.HttpAttr!.Name.ToString()!]; + + return new MethodRouteEntry + { + MethodName = x.Method.Identifier.Text, + HttpRoute = $"{verb}/{cleanedRoute}", + }; + }) + .ToList(); + + controllerEntry.Methods.AddRange(methodsWithHttp); + } + + var resolvedActions = model + .CrudActions.Select(action => + { + var actionName = action.ToString(); + + return actionName switch + { + "Get" => new MethodRouteEntry + { + MethodName = $"Get{model.ClassName.Pluralize()}", + HttpRoute = $"GET/{model.ClassName}", + }, + "GetOne" => new MethodRouteEntry + { + MethodName = $"Get{model.ClassName}", + HttpRoute = $"GET/{model.ClassName}/{{id}}", + }, + "GetAll" => new MethodRouteEntry + { + MethodName = $"GetAll{model.ClassName.Pluralize()}", + HttpRoute = $"GET/{model.ClassName}/get-all", + }, + "Create" => new MethodRouteEntry + { + MethodName = "Create", + HttpRoute = $"POST/{model.ClassName}", + }, + "Update" => new MethodRouteEntry + { + MethodName = "Update", + HttpRoute = $"PUT/{model.ClassName}/{{id}}", + }, + "Delete" => new MethodRouteEntry + { + MethodName = "Delete", + HttpRoute = $"DELETE/{model.ClassName}{{id}}", + }, + "Patch" => new MethodRouteEntry + { + MethodName = $"JsonPatch{model.ClassName}", + HttpRoute = $"PATCH/{model.ClassName}/{{id}}", + }, + _ => null, + }; + }) + .Where(x => x is not null)! + .ToList(); + + controllerEntry.Methods.AddRange(resolvedActions); + + if (controllerEntry.Methods.Count > 0) + { + controllers.Add(controllerEntry); + } + } + + var templateContent = EmbeddedResourceLoader.LoadEmbeddedTemplate(TemplateResourceName); + var template = Template.Parse(templateContent); + + if (template.HasErrors) + { + foreach (var message in template.Messages) + { + context.ReportDiagnostic( + Diagnostic.Create( + new DiagnosticDescriptor( + id: "DAPPMETHODS001", + title: "Methods & Permissions template error", + messageFormat: message.Message, + category: "SourceGenerator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true + ), + Location.None + ) + ); + } + return; + } + + var output = template.Render(new { controllers }); + + context.AddSource("MethodsAndPermissions.g.cs", output); + } + + private static ClassDeclarationSyntax? FindPartialControllerForClass( + Compilation compilation, + SourceModel model + ) + { + return compilation + .SyntaxTrees.SelectMany(t => t.GetRoot().DescendantNodes()) + .OfType() + .FirstOrDefault(cds => + cds.Identifier.Text == $"{model.ClassName}Controller" + && cds.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)) + ); + } + + private static bool IsHttpMethodAttribute(AttributeSyntax attr) + { + var httpMethodAttributes = new HashSet + { + "HttpGet", + "HttpPost", + "HttpPut", + "HttpDelete", + "HttpPatch", + "HttpHead", + "HttpOptions", + }; + + var name = attr.Name.ToString(); + return httpMethodAttributes.Any(h => + name == h + || name == $"{h}Attribute" + || name.EndsWith($".{h}") + || name.EndsWith($".{h}Attribute") + ); + } +} diff --git a/Dappi.SourceGenerator/Templates/MethodsAndPermissionsTemplate.tpl b/Dappi.SourceGenerator/Templates/MethodsAndPermissionsTemplate.tpl new file mode 100644 index 00000000..3733ce89 --- /dev/null +++ b/Dappi.SourceGenerator/Templates/MethodsAndPermissionsTemplate.tpl @@ -0,0 +1,29 @@ +// +using System.Collections.Generic; +using Dappi.Core.Models; + +namespace GeneratedPermissions +{ + public static partial class PermissionsMeta + { + public static readonly IReadOnlyDictionary> Controllers = + new Dictionary> + { + {{ for controller in controllers }} + { + "{{ controller.controller_name }}", + new List + { + {{ for method in controller.methods }} + new MethodRouteEntry + { + MethodName = "{{ method.method_name }}", + HttpRoute = "{{ method.http_route }}" + }{{ if !for.last }},{{ end }} + {{ end }} + } + }{{ if !for.last }},{{ end }} + {{ end }} + }; + } +} diff --git a/Dappi.SourceGenerator/Utilities/EmbeddedResourceLoader.cs b/Dappi.SourceGenerator/Utilities/EmbeddedResourceLoader.cs new file mode 100644 index 00000000..9409a60b --- /dev/null +++ b/Dappi.SourceGenerator/Utilities/EmbeddedResourceLoader.cs @@ -0,0 +1,21 @@ +using System.Reflection; + +namespace Dappi.SourceGenerator.Utilities; + +public static class EmbeddedResourceLoader +{ + public static string LoadEmbeddedTemplate(string embeddedResourceName) + { + var assembly = Assembly.GetExecutingAssembly(); + using var stream = assembly.GetManifestResourceStream(embeddedResourceName); + if (stream is null) + { + throw new InvalidOperationException( + $"Embedded resource '{embeddedResourceName}' not found." + ); + } + + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } +} diff --git a/Dappi.sln b/Dappi.sln index da6886fb..a37f0727 100644 --- a/Dappi.sln +++ b/Dappi.sln @@ -22,6 +22,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dappi.HeadlessCms.Tests", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dappi.TestEnv", "Dappi.TestEnv\Dappi.TestEnv.csproj", "{1A30AC9A-75AA-443D-AC97-988A0609C97C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dappi.HeadlessCms.UsersAndPermissions", "Dappi.HeadlessCms.UsersAndPermissions\Dappi.HeadlessCms.UsersAndPermissions.csproj", "{5DC893C6-CBBD-40F0-9F78-1711E19D525B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -116,6 +118,18 @@ Global {1A30AC9A-75AA-443D-AC97-988A0609C97C}.Release|x64.Build.0 = Release|Any CPU {1A30AC9A-75AA-443D-AC97-988A0609C97C}.Release|x86.ActiveCfg = Release|Any CPU {1A30AC9A-75AA-443D-AC97-988A0609C97C}.Release|x86.Build.0 = Release|Any CPU + {5DC893C6-CBBD-40F0-9F78-1711E19D525B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5DC893C6-CBBD-40F0-9F78-1711E19D525B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5DC893C6-CBBD-40F0-9F78-1711E19D525B}.Debug|x64.ActiveCfg = Debug|Any CPU + {5DC893C6-CBBD-40F0-9F78-1711E19D525B}.Debug|x64.Build.0 = Debug|Any CPU + {5DC893C6-CBBD-40F0-9F78-1711E19D525B}.Debug|x86.ActiveCfg = Debug|Any CPU + {5DC893C6-CBBD-40F0-9F78-1711E19D525B}.Debug|x86.Build.0 = Debug|Any CPU + {5DC893C6-CBBD-40F0-9F78-1711E19D525B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5DC893C6-CBBD-40F0-9F78-1711E19D525B}.Release|Any CPU.Build.0 = Release|Any CPU + {5DC893C6-CBBD-40F0-9F78-1711E19D525B}.Release|x64.ActiveCfg = Release|Any CPU + {5DC893C6-CBBD-40F0-9F78-1711E19D525B}.Release|x64.Build.0 = Release|Any CPU + {5DC893C6-CBBD-40F0-9F78-1711E19D525B}.Release|x86.ActiveCfg = Release|Any CPU + {5DC893C6-CBBD-40F0-9F78-1711E19D525B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/USERS_AND_PERMISSIONS_SETUP.md b/USERS_AND_PERMISSIONS_SETUP.md new file mode 100644 index 00000000..e0be5642 --- /dev/null +++ b/USERS_AND_PERMISSIONS_SETUP.md @@ -0,0 +1,509 @@ +# UsersAndPermissions System - Setup & Usage Guide + +## Overview + +The **UsersAndPermissions System** is a comprehensive role-based access control (RBAC) framework integrated into Dappi. It allows you to define roles, permissions, and control which authenticated users can access specific API endpoints and methods. + +--- + +## Key Components + +### 1. **AppUser Entity** (Your Application) +Implements the `IAppUser` interface and represents an authenticated user in your system. + +```csharp +public class AppUser : IAppUser +{ + public int Id { get; set; } + public int RoleId { get; set; } + public AppRole? Role { get; set; } +} +``` + +**Key Properties:** +- `Id`: Unique identifier for the user +- `RoleId`: Foreign key to the `AppRole` (defines user's role) +- `Role`: Navigation property to the user's assigned role + +### 2. **AppUsersAndPermissionsDbContext** (Your Application) +A custom DbContext that inherits from `UsersAndPermissionsDbContext`. + +```csharp +public class AppUsersAndPermissionsDbContext(DbContextOptions options) + : UsersAndPermissionsDbContext(options) +{ + public DbSet AppUsers { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .HasOne(u => u.Role) + .WithMany(r => (IEnumerable)r.Users); + } +} +``` + +**Inherited DbSets:** +- `AppPermissions`: Database representation of available permissions +- `AppRoles`: Database representation of roles and their assigned permissions + +### 3. **Core Models** (From Dappi.HeadlessCms.UsersAndPermissions) + +#### `AppRole` +Represents a role in your application with associated permissions. +- **Properties:** `Id`, `Name`, `Permissions` (collection), `Users` (collection), `IsDefaultForAuthenticatedUser` +- **Methods:** `AddPermission()`, `ClearPermissions()`, factory methods for default roles + +#### `AppPermission` +Represents an individual permission that can be granted to a role. +- **Properties:** `Id`, `Name`, `Description` + +#### `IAppUser` +Interface that your user entity must implement. + +```csharp +public interface IAppUser +{ + int RoleId { get; } + AppRole? Role { get; } +} +``` + +--- + +## Setup Instructions + +### Step 1: Implement IAppUser in Your User Entity + +Create an `AppUser` entity that implements `IAppUser`: + +```csharp +using Dappi.HeadlessCms.UsersAndPermissions.Core; + +public class AppUser : IAppUser +{ + public int Id { get; set; } + public int RoleId { get; set; } + public AppRole? Role { get; set; } +} +``` + +### Step 2: Create AppUsersAndPermissionsDbContext + +Create a custom DbContext that inherits from `UsersAndPermissionsDbContext`: + +```csharp +using Dappi.HeadlessCms.UsersAndPermissions.Database; +using Microsoft.EntityFrameworkCore; + +public class AppUsersAndPermissionsDbContext(DbContextOptions options) + : UsersAndPermissionsDbContext(options) +{ + public DbSet AppUsers { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .HasOne(u => u.Role) + .WithMany(r => (IEnumerable)r.Users); + } +} +``` + +**Important:** +- The base class configures the `UsersAndPermissions` schema +- It automatically manages `AppPermissions` and `AppRoles` DbSets +- Call `base.OnModelCreating()` to ensure proper configuration + +### Step 3: Create Database Migration + +Generate an EF Core migration for the UsersAndPermissions DbContext: + +```bash +dotnet ef migrations add UsersAndPermissions \ + --context AppUsersAndPermissionsDbContext \ + --output-dir Migrations/AppUsersAndPermissionsDb +``` + +This creates: +- Migration files in `Migrations/AppUsersAndPermissionsDb/` folder +- A separate migration path for the UsersAndPermissions schema + +### Step 4: Configure Services in Program.cs + +Add the UsersAndPermissions System to your dependency injection container: + +```csharp +using Dappi.HeadlessCms; +using Dappi.HeadlessCms.UsersAndPermissions; +using Dappi.HeadlessCms.UsersAndPermissions.Core; +using GeneratedPermissions; +using MyCompany.MyProject.WebApi.Data; +using MyCompany.MyProject.WebApi.Entities; + +var builder = WebApplication.CreateBuilder(args); + +// Add core Dappi services +builder.Services.AddDappi(builder.Configuration); +builder.Services.AddDappiAuthentication(builder.Configuration); + +// Add UsersAndPermissions System +builder.Services.AddUsersAndPermissionsSystem( + PermissionsMeta.Controllers, + builder.Configuration +); + +builder.Services.AddDappiSwaggerGen(); + +var app = builder.Build(); +``` + +**Key Parameters:** +- `AppUsersAndPermissionsDbContext`: Your custom DbContext +- `PermissionsMeta.Controllers`: Auto-generated metadata about all available controller methods +- `builder.Configuration`: Configuration object + +### Step 5: Define Roles and Permissions + +Before building the application, define which roles have access to which endpoints: + +```csharp +var permBuilder = new AppRoleAndPermissionsBuilder(); + +permBuilder + .ForRole(UsersAndPermissionsConstants.DefaultRoles.Authenticated) + .ForController(nameof(TestController)) + .Allow(nameof(TestController.GetAllTests)) + .Allow(nameof(TestController.TestCustomMethod)) + .And() + .ForRole("Admin") + .ForController(nameof(TestController)) + .AllowAll(); + +await app.UseUsersAndPermissionsSystem(permBuilder); +``` + +**Builder Methods:** +- `ForRole(string roleName, bool isDefaultForAuthenticated = false)`: Define a new role +- `ForController(string controllerName)`: Specify which controller to configure +- `Allow(string methodName)`: Grant permission for a specific method +- `AllowAll()`: Grant permission for all methods in the controller +- `Deny(string methodName)`: Explicitly deny a method (useful for AllowAll with exceptions) +- `And()`: Move to the next role definition + +**Default Roles:** +- `UsersAndPermissionsConstants.DefaultRoles.Public`: For unauthenticated users +- `UsersAndPermissionsConstants.DefaultRoles.Authenticated`: For authenticated users + +### Step 6: Apply Migrations and Run + +The `UseUsersAndPermissionsSystem` extension method automatically: +1. Runs pending migrations on the UsersAndPermissions database +2. Synchronizes permissions from controllers to the database +3. Creates/updates roles based on your builder configuration + +```csharp +await app.UseDappi(); +app.UseHttpsRedirection(); +app.MapControllers(); +app.Run(); +``` + +--- + +## Permission Synchronization + +### How It Works + +Every time your application starts, the permission system: + +1. **Scans all controllers** using `PermissionsMeta.Controllers` (generated by the source generator) +2. **Registers permissions** in the database for each controller method +3. **Synchronizes roles** based on your `AppRoleAndPermissionsBuilder` configuration +4. **Removes orphaned roles** that aren't referenced in your builder and have no users + +### Automatic Permission Detection + +The `PermissionsMeta.Controllers` is automatically generated and contains: +- All public controller methods +- HTTP method mappings +- Full route information + +No manual permission registration is needed! + +--- + +## Extension Methods + +### `AddUsersAndPermissionsSystem` +Configures dependency injection for the UsersAndPermissions System. + +**Parameters:** +- `services`: IServiceCollection +- `controllerRoutes`: IReadOnlyDictionary> (from PermissionsMeta) +- `configuration`: IConfiguration + +**What it does:** +- Registers the DbContext +- Configures PostgreSQL connection +- Registers `DbContextAccessor` +- Registers `AvailablePermissionsRepository` +- Adds controllers with proper JSON serialization settings + +### `UseUsersAndPermissionsSystem` +Initializes the UsersAndPermissions System at runtime. + +**Parameters:** +- `app`: WebApplication +- `appRoleAndPermissionsBuilder`: AppRoleAndPermissionsBuilder + +**What it does:** +1. Creates a scope and retrieves the DbContext +2. Runs all pending migrations +3. Clears and reloads permissions from controllers +4. Synchronizes roles with the builder configuration +5. Maps role controllers +6. Returns the app for method chaining + +--- + +## Database Schema + +### UsersAndPermissions Schema + +The system creates the following tables in the `UsersAndPermissions` schema: + +#### `AppPermissions` +``` +Id (int, PK) +Name (string, required) +Description (string, required) +``` + +#### `AppRoles` +``` +Id (int, PK) +Name (string, required) +IsDefaultForAuthenticatedUser (bool) +``` + +#### `AppPermissionAppRole` (Junction table) +``` +PermissionsId (int, FK -> AppPermissions) +RolesId (int, FK -> AppRoles) +``` + +#### `AppUsers` (In your schema) +``` +Id (int, PK) +RoleId (int, FK -> AppRoles.Id) +``` + +--- + +## Example: Complete Setup + +### 1. Define your AppUser +```csharp +// Entities/AppUser.cs +using Dappi.HeadlessCms.UsersAndPermissions.Core; + +public class AppUser : IAppUser +{ + public int Id { get; set; } + public int RoleId { get; set; } + public AppRole? Role { get; set; } +} +``` + +### 2. Create DbContext +```csharp +// Data/AppUsersAndPermissionsDbContext.cs +using Dappi.HeadlessCms.UsersAndPermissions.Database; +using Microsoft.EntityFrameworkCore; + +public class AppUsersAndPermissionsDbContext(DbContextOptions options) + : UsersAndPermissionsDbContext(options) +{ + public DbSet AppUsers { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .HasOne(u => u.Role) + .WithMany(r => (IEnumerable)r.Users); + } +} +``` + +### 3. Configure Program.cs +```csharp +// Program.cs +using Dappi.HeadlessCms; +using Dappi.HeadlessCms.UsersAndPermissions; +using Dappi.HeadlessCms.UsersAndPermissions.Core; +using GeneratedPermissions; +using MyCompany.MyProject.WebApi.Data; +using MyCompany.MyProject.WebApi.Entities; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDappi(builder.Configuration); +builder.Services.AddDappiAuthentication(builder.Configuration); +builder.Services.AddUsersAndPermissionsSystem( + PermissionsMeta.Controllers, + builder.Configuration +); +builder.Services.AddDappiSwaggerGen(); + +var app = builder.Build(); + +// Define roles and permissions +var permBuilder = new AppRoleAndPermissionsBuilder(); + +permBuilder + .ForRole(UsersAndPermissionsConstants.DefaultRoles.Authenticated) + .ForController(nameof(ProductController)) + .Allow(nameof(ProductController.GetAllProducts)) + .Allow(nameof(ProductController.GetProductById)) + .And() + .ForRole("Admin") + .ForController(nameof(ProductController)) + .AllowAll() + .And() + .ForRole("Editor") + .ForController(nameof(ProductController)) + .Allow(nameof(ProductController.CreateProduct)) + .Allow(nameof(ProductController.UpdateProduct)) + .Allow(nameof(ProductController.DeleteProduct)); + +// Initialize the system +await app.UseUsersAndPermissionsSystem(permBuilder); + +// Rest of configuration +await app.UseDappi(); +app.UseHttpsRedirection(); +app.MapControllers(); +app.Run(); +``` + +### 4. Create Migration +```bash +dotnet ef migrations add UsersAndPermissions \ + --context AppUsersAndPermissionsDbContext \ + --output-dir Migrations/AppUsersAndPermissionsDb +``` + +--- + +## Accessing the System at Runtime + +### DbContext Accessor +You can inject `IDbContextAccessor` into your services to access the UsersAndPermissions DbContext: + +```csharp +public class MyService +{ + private readonly IDbContextAccessor _dbContextAccessor; + + public MyService(IDbContextAccessor dbContextAccessor) + { + _dbContextAccessor = dbContextAccessor; + } + + public async Task> GetAllRoles() + { + var db = _dbContextAccessor.GetContext(); + return await db.AppRoles.ToListAsync(); + } +} +``` + +### Available Permissions Repository +Access the automatically-discovered permissions: + +```csharp +public class AuthorizationService +{ + private readonly AvailablePermissionsRepository _permissionsRepository; + + public AuthorizationService(AvailablePermissionsRepository permissionsRepository) + { + _permissionsRepository = permissionsRepository; + } + + public var allPermissions = _permissionsRepository.GetAllPermissions(); +} +``` + +--- + +## Best Practices + +### 1. **Inherit from UsersAndPermissionsDbContext** +Always inherit your custom DbContext from `UsersAndPermissionsDbContext` to get automatic table configuration. + +### 2. **Call `base.OnModelCreating()`** +Always call the base implementation in your `OnModelCreating` method to ensure the UsersAndPermissions schema is properly configured. + +### 3. **Use Separate Migration Folder** +Keep migrations for the UsersAndPermissions DbContext in a separate folder (`Migrations/AppUsersAndPermissionsDb/`) for better organization. + +### 4. **Define Roles at Startup** +Configure all roles and permissions in your `Program.cs` before calling `UseUsersAndPermissionsSystem`. This ensures consistency across deployments. + +### 5. **Use Default Roles** +Leverage the built-in default roles (`Public`, `Authenticated`) for common scenarios instead of creating custom ones. + +### 6. **Fluent Configuration** +Use the fluent builder pattern for clean, readable role configuration: + +```csharp +permBuilder + .ForRole("Admin") + .ForController("Users").AllowAll() + .And() + .ForRole("User") + .ForController("Products").Allow("GetAll") + .And(); +``` + +--- + +## Troubleshooting + +### Issue: Migrations not applied +**Solution:** Ensure `UseUsersAndPermissionsSystem` is called before `MapControllers()`. The extension method automatically applies migrations. + +### Issue: Permissions not syncing +**Solution:** Verify that `PermissionsMeta.Controllers` is properly generated. Rebuild the project if needed to trigger the source generator. + +### Issue: Role not found in database +**Solution:** Ensure the role is defined in the `AppRoleAndPermissionsBuilder` and the application has started at least once to synchronize the database. + +### Issue: User has multiple roles +**Solution:** The current design supports one role per user (via `RoleId`). Implement a role-assignment service to manage role changes. + +--- + +## Next Steps + +1. Implement authentication to populate `AppUser` entities +2. Create authorization policies that check user roles +3. Implement a UI for managing roles and permissions (optional) +4. Monitor permission changes across deployments + +--- + +## Related Documentation + +- [Dappi Core Documentation](./README.md) +- [UsersAndPermissions API Reference](./Dappi.HeadlessCms.UsersAndPermissions/) +- Entity Framework Core Migrations: https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/ +