From 01978d30cecc45fc7cb8472b3c6c50d3aa1276b1 Mon Sep 17 00:00:00 2001 From: Leon Hoppe Date: Sun, 14 Jul 2024 12:17:49 +0200 Subject: [PATCH] Restructured projects and created Services for permissions and users --- .idea/.idea.HopFrame/.idea/dataSources.xml | 2 +- DatabaseTest/Controllers/TestController.cs | 5 +- HopFrame.Api/Controller/SecurityController.cs | 65 +++-------- HopFrame.Api/Models/UserPasswordValidation.cs | 5 + HopFrame.Database/HopDbContextBase.cs | 14 ++- HopFrame.Database/Models/ModelExtensions.cs | 29 ++++- HopFrame.Database/Models/Permission.cs | 10 ++ HopFrame.Database/Models/PermissionGroup.cs | 9 ++ HopFrame.Database/Models/User.cs | 6 +- .../Authentication/HopFrameAuthentication.cs | 21 +--- .../HopFrameAuthenticationExtensions.cs | 3 + .../Authorization/AuthorizedAttribute.cs | 5 + .../EncryptionManager.cs | 2 +- .../Models/UserRegister.cs | 2 +- .../Services/IPermissionService.cs | 52 ++++----- HopFrame.Security/Services/IUserService.cs | 27 +++++ .../Implementation/PermissionService.cs | 105 +++++++++++++++++ .../Services/Implementation/UserService.cs | 110 ++++++++++++++++++ .../Services/PermissionService.cs | 60 ---------- 19 files changed, 363 insertions(+), 169 deletions(-) create mode 100644 HopFrame.Api/Models/UserPasswordValidation.cs create mode 100644 HopFrame.Database/Models/Permission.cs create mode 100644 HopFrame.Database/Models/PermissionGroup.cs rename {HopFrame.Api => HopFrame.Security}/EncryptionManager.cs (96%) rename {HopFrame.Api => HopFrame.Security}/Models/UserRegister.cs (80%) create mode 100644 HopFrame.Security/Services/IUserService.cs create mode 100644 HopFrame.Security/Services/Implementation/PermissionService.cs create mode 100644 HopFrame.Security/Services/Implementation/UserService.cs delete mode 100644 HopFrame.Security/Services/PermissionService.cs diff --git a/.idea/.idea.HopFrame/.idea/dataSources.xml b/.idea/.idea.HopFrame/.idea/dataSources.xml index f9bef4b..81347d0 100644 --- a/.idea/.idea.HopFrame/.idea/dataSources.xml +++ b/.idea/.idea.HopFrame/.idea/dataSources.xml @@ -5,7 +5,7 @@ sqlite.xerial true org.sqlite.JDBC - jdbc:sqlite:C:\Users\Remote\Documents\Projekte\HopFrame\DatabaseTest\test.db + jdbc:sqlite:$PROJECT_DIR$/DatabaseTest/bin/Debug/net8.0/test.db diff --git a/DatabaseTest/Controllers/TestController.cs b/DatabaseTest/Controllers/TestController.cs index 1b78deb..4e74da0 100644 --- a/DatabaseTest/Controllers/TestController.cs +++ b/DatabaseTest/Controllers/TestController.cs @@ -1,3 +1,4 @@ +using HopFrame.Database.Models; using HopFrame.Security.Authorization; using HopFrame.Security.Claims; using Microsoft.AspNetCore.Mvc; @@ -9,8 +10,8 @@ namespace DatabaseTest.Controllers; public class TestController(ITokenContext userContext) : ControllerBase { [HttpGet("permissions"), Authorized] - public ActionResult> Permissions() { - return new ActionResult>(userContext.User.Permissions); + public ActionResult> Permissions() { + return new ActionResult>(userContext.User.Permissions); } } \ No newline at end of file diff --git a/HopFrame.Api/Controller/SecurityController.cs b/HopFrame.Api/Controller/SecurityController.cs index f9bcd9d..8405b95 100644 --- a/HopFrame.Api/Controller/SecurityController.cs +++ b/HopFrame.Api/Controller/SecurityController.cs @@ -4,9 +4,12 @@ using HopFrame.Api.Logic; using HopFrame.Api.Models; using HopFrame.Database; using HopFrame.Database.Models.Entries; +using HopFrame.Security; using HopFrame.Security.Authentication; using HopFrame.Security.Authorization; using HopFrame.Security.Claims; +using HopFrame.Security.Models; +using HopFrame.Security.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -15,32 +18,32 @@ namespace HopFrame.Api.Controller; [ApiController] [Route("authentication")] -public class SecurityController(TDbContext context) : ControllerBase where TDbContext : HopDbContextBase { +public class SecurityController(TDbContext context, IUserService users, ITokenContext tokenContext) : ControllerBase where TDbContext : HopDbContextBase { private const string RefreshTokenType = "HopFrame.Security.RefreshToken"; [HttpPut("login")] public async Task>> Login([FromBody] UserLogin login) { - var user = await context.Users.SingleOrDefaultAsync(user => user.Email == login.Email); + var user = await users.GetUserByEmail(login.Email); if (user is null) return LogicResult>.NotFound("The provided email address was not found"); var hashedPassword = EncryptionManager.Hash(login.Password, Encoding.Default.GetBytes(user.CreatedAt.ToString(CultureInfo.InvariantCulture))); - if (hashedPassword != user.Password) + if (hashedPassword != await users.GetUserPassword(user)) return LogicResult>.Forbidden("The provided password is not correct"); var refreshToken = new TokenEntry { CreatedAt = DateTime.Now, Token = Guid.NewGuid().ToString(), Type = TokenEntry.RefreshTokenType, - UserId = user.Id + UserId = user.Id.ToString() }; var accessToken = new TokenEntry { CreatedAt = DateTime.Now, Token = Guid.NewGuid().ToString(), Type = TokenEntry.AccessTokenType, - UserId = user.Id + UserId = user.Id.ToString() }; HttpContext.Response.Cookies.Append(RefreshTokenType, refreshToken.Token, new CookieOptions { @@ -59,42 +62,24 @@ public class SecurityController(TDbContext context) : ControllerBase public async Task>> Register([FromBody] UserRegister register) { if (register.Password.Length < 8) return LogicResult>.Conflict("Password needs to be at least 8 characters long"); - - if (await context.Users.AnyAsync(user => user.Username == register.Username || user.Email == register.Email)) + + var allUsers = await users.GetUsers(); + if (allUsers.Any(user => user.Username == register.Username || user.Email == register.Email)) return LogicResult>.Conflict("Username or Email is already registered"); - var user = new UserEntry { - CreatedAt = DateTime.Now, - Email = register.Email, - Username = register.Username, - Id = Guid.NewGuid().ToString() - }; - user.Password = EncryptionManager.Hash(register.Password, Encoding.Default.GetBytes(user.CreatedAt.ToString(CultureInfo.InvariantCulture))); - - await context.Users.AddAsync(user); - - var defaultGroups = await context.Groups - .Where(group => group.Default) - .Select(group => "group." + group.Name) - .ToListAsync(); - - await context.Permissions.AddRangeAsync(defaultGroups.Select(group => new PermissionEntry { - GrantedAt = DateTime.Now, - PermissionText = group, - UserId = user.Id - })); + var user = await users.AddUser(register); var refreshToken = new TokenEntry { CreatedAt = DateTime.Now, Token = Guid.NewGuid().ToString(), Type = TokenEntry.RefreshTokenType, - UserId = user.Id + UserId = user.Id.ToString() }; var accessToken = new TokenEntry { CreatedAt = DateTime.Now, Token = Guid.NewGuid().ToString(), Type = TokenEntry.AccessTokenType, - UserId = user.Id + UserId = user.Id.ToString() }; HttpContext.Response.Cookies.Append(RefreshTokenType, refreshToken.Token, new CookieOptions { @@ -163,26 +148,14 @@ public class SecurityController(TDbContext context) : ControllerBase } [HttpDelete("delete"), Authorized] - public async Task Delete([FromBody] UserLogin login) { - var token = HttpContext.User.GetAccessTokenId(); - var userId = (await context.Tokens.SingleOrDefaultAsync(t => t.Token == token && t.Type == TokenEntry.AccessTokenType))?.UserId; - - if (string.IsNullOrEmpty(userId)) - return LogicResult.NotFound("Access token does not match any user"); - - var user = await context.Users.SingleAsync(user => user.Id == userId); + public async Task Delete([FromBody] UserPasswordValidation validation) { + var user = tokenContext.User; - var password = EncryptionManager.Hash(login.Password, Encoding.Default.GetBytes(user.CreatedAt.ToString(CultureInfo.InvariantCulture))); - if (user.Password != password) + var password = EncryptionManager.Hash(validation.Password, Encoding.Default.GetBytes(user.CreatedAt.ToString(CultureInfo.InvariantCulture))); + if (await users.GetUserPassword(user) != password) return LogicResult.Forbidden("The provided password is not correct"); - var tokens = await context.Tokens.Where(t => t.UserId == userId).ToArrayAsync(); - var permissions = await context.Permissions.Where(perm => perm.UserId == userId).ToArrayAsync(); - - context.Tokens.RemoveRange(tokens); - context.Permissions.RemoveRange(permissions); - context.Users.Remove(user); - await context.SaveChangesAsync(); + await users.DeleteUser(user); HttpContext.Response.Cookies.Delete(RefreshTokenType); diff --git a/HopFrame.Api/Models/UserPasswordValidation.cs b/HopFrame.Api/Models/UserPasswordValidation.cs new file mode 100644 index 0000000..1b274cb --- /dev/null +++ b/HopFrame.Api/Models/UserPasswordValidation.cs @@ -0,0 +1,5 @@ +namespace HopFrame.Api.Models; + +public class UserPasswordValidation { + public string Password { get; set; } +} \ No newline at end of file diff --git a/HopFrame.Database/HopDbContextBase.cs b/HopFrame.Database/HopDbContextBase.cs index 944ac54..2ae1fe6 100644 --- a/HopFrame.Database/HopDbContextBase.cs +++ b/HopFrame.Database/HopDbContextBase.cs @@ -6,11 +6,7 @@ namespace HopFrame.Database; /// /// This class includes the basic database structure in order for HopFrame to work /// -public class HopDbContextBase : DbContext { - - public HopDbContextBase() {} - - public HopDbContextBase(DbContextOptions options) : base(options) {} +public abstract class HopDbContextBase : DbContext { public virtual DbSet Users { get; set; } public virtual DbSet Permissions { get; set; } @@ -25,4 +21,12 @@ public class HopDbContextBase : DbContext { modelBuilder.Entity(); modelBuilder.Entity(); } + + /// + /// Gets executed when a user is deleted through the IUserService from the + /// HopFrame.Security package. You can override this method to also delete + /// related user specific entries in the database + /// + /// + public virtual void OnUserDelete(UserEntry user) {} } \ No newline at end of file diff --git a/HopFrame.Database/Models/ModelExtensions.cs b/HopFrame.Database/Models/ModelExtensions.cs index e632367..4600afd 100644 --- a/HopFrame.Database/Models/ModelExtensions.cs +++ b/HopFrame.Database/Models/ModelExtensions.cs @@ -20,10 +20,37 @@ public static class ModelExtensions { user.Permissions = contextBase.Permissions .Where(perm => perm.UserId == entry.Id) - .Select(perm => perm.PermissionText) + .Select(perm => perm.ToPermissionModel()) .ToList(); return user; } + + public static Permission ToPermissionModel(this PermissionEntry entry) { + Guid.TryParse(entry.UserId, out var userId); + + return new Permission { + Owner = userId, + PermissionName = entry.PermissionText, + GrantedAt = entry.GrantedAt, + Id = entry.RecordId + }; + } + + public static PermissionGroup ToPermissionGroup(this GroupEntry entry, HopDbContextBase contextBase) { + var group = new PermissionGroup { + Name = entry.Name, + IsDefaultGroup = entry.Default, + Description = entry.Description, + CreatedAt = entry.CreatedAt + }; + + group.Permissions = contextBase.Permissions + .Where(perm => perm.UserId == group.Name) + .Select(perm => perm.ToPermissionModel()) + .ToList(); + + return group; + } } \ No newline at end of file diff --git a/HopFrame.Database/Models/Permission.cs b/HopFrame.Database/Models/Permission.cs new file mode 100644 index 0000000..e6fbe14 --- /dev/null +++ b/HopFrame.Database/Models/Permission.cs @@ -0,0 +1,10 @@ +namespace HopFrame.Database.Models; + +public sealed class Permission { + public long Id { get; init; } + public string PermissionName { get; set; } + public Guid Owner { get; set; } + public DateTime GrantedAt { get; set; } +} + +public interface IPermissionOwner {} diff --git a/HopFrame.Database/Models/PermissionGroup.cs b/HopFrame.Database/Models/PermissionGroup.cs new file mode 100644 index 0000000..6d151b1 --- /dev/null +++ b/HopFrame.Database/Models/PermissionGroup.cs @@ -0,0 +1,9 @@ +namespace HopFrame.Database.Models; + +public sealed class PermissionGroup : IPermissionOwner { + public string Name { get; init; } + public bool IsDefaultGroup { get; set; } + public string Description { get; set; } + public DateTime CreatedAt { get; set; } + public IList Permissions { get; set; } +} \ No newline at end of file diff --git a/HopFrame.Database/Models/User.cs b/HopFrame.Database/Models/User.cs index cbedd0c..e97d720 100644 --- a/HopFrame.Database/Models/User.cs +++ b/HopFrame.Database/Models/User.cs @@ -1,9 +1,9 @@ namespace HopFrame.Database.Models; -public class User { - public Guid Id { get; set; } +public sealed class User : IPermissionOwner { + public Guid Id { get; init; } public string Username { get; set; } public string Email { get; set; } public DateTime CreatedAt { get; set; } - public IList Permissions { get; set; } + public IList Permissions { get; set; } } \ No newline at end of file diff --git a/HopFrame.Security/Authentication/HopFrameAuthentication.cs b/HopFrame.Security/Authentication/HopFrameAuthentication.cs index e0244ac..5fc7e3b 100644 --- a/HopFrame.Security/Authentication/HopFrameAuthentication.cs +++ b/HopFrame.Security/Authentication/HopFrameAuthentication.cs @@ -2,6 +2,7 @@ using System.Security.Claims; using System.Text.Encodings.Web; using HopFrame.Database; using HopFrame.Security.Claims; +using HopFrame.Security.Services; using Microsoft.AspNetCore.Authentication; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -17,7 +18,8 @@ public class HopFrameAuthentication( ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, - TDbContext context) + TDbContext context, + IPermissionService perms) : AuthenticationHandler(options, logger, encoder, clock) where TDbContext : HopDbContextBase { @@ -42,22 +44,7 @@ public class HopFrameAuthentication( new(HopFrameClaimTypes.UserId, tokenEntry.UserId) }; - var permissions = await context.Permissions - .Where(perm => perm.UserId == tokenEntry.UserId) - .Select(perm => perm.PermissionText) - .ToListAsync(); - - var groups = permissions - .Where(perm => perm.StartsWith("group.")) - .ToList(); - - var groupPerms = await context.Permissions - .Where(perm => groups.Contains(perm.UserId)) - .Select(perm => perm.PermissionText) - .ToListAsync(); - - permissions.AddRange(groupPerms); - + var permissions = await perms.GetFullPermissions(tokenEntry.UserId); claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm))); var principal = new ClaimsPrincipal(); diff --git a/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs b/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs index fed708c..e6348ed 100644 --- a/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs +++ b/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs @@ -1,6 +1,7 @@ using HopFrame.Database; using HopFrame.Security.Claims; using HopFrame.Security.Services; +using HopFrame.Security.Services.Implementation; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -20,6 +21,8 @@ public static class HopFrameAuthenticationExtensions { service.TryAddSingleton(); service.AddScoped>(); service.AddScoped>(); + service.AddScoped>(); + return service.AddAuthentication(HopFrameAuthentication.SchemeName).AddScheme>(HopFrameAuthentication.SchemeName, _ => {}); } diff --git a/HopFrame.Security/Authorization/AuthorizedAttribute.cs b/HopFrame.Security/Authorization/AuthorizedAttribute.cs index ad81973..436881d 100644 --- a/HopFrame.Security/Authorization/AuthorizedAttribute.cs +++ b/HopFrame.Security/Authorization/AuthorizedAttribute.cs @@ -6,6 +6,11 @@ public class AuthorizedAttribute : TypeFilterAttribute { /// /// If this decorator is present, the endpoint is only accessible if the user provided a valid access token (is logged in) + /// permission system:
+ /// - "*" -> all rights
+ /// - "group.[name]" -> group member
+ /// - "[namespace].[name]" -> single permission
+ /// - "[namespace].*" -> all permissions in the namespace ///
/// specifies the permissions the user needs to have in order to access this endpoint public AuthorizedAttribute(params string[] permissions) : base(typeof(AuthorizedFilter)) { diff --git a/HopFrame.Api/EncryptionManager.cs b/HopFrame.Security/EncryptionManager.cs similarity index 96% rename from HopFrame.Api/EncryptionManager.cs rename to HopFrame.Security/EncryptionManager.cs index 592ad5d..8f5037b 100644 --- a/HopFrame.Api/EncryptionManager.cs +++ b/HopFrame.Security/EncryptionManager.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Cryptography.KeyDerivation; -namespace HopFrame.Api; +namespace HopFrame.Security; public static class EncryptionManager { diff --git a/HopFrame.Api/Models/UserRegister.cs b/HopFrame.Security/Models/UserRegister.cs similarity index 80% rename from HopFrame.Api/Models/UserRegister.cs rename to HopFrame.Security/Models/UserRegister.cs index aacafdc..3f560c1 100644 --- a/HopFrame.Api/Models/UserRegister.cs +++ b/HopFrame.Security/Models/UserRegister.cs @@ -1,4 +1,4 @@ -namespace HopFrame.Api.Models; +namespace HopFrame.Security.Models; public struct UserRegister { public string Username { get; set; } diff --git a/HopFrame.Security/Services/IPermissionService.cs b/HopFrame.Security/Services/IPermissionService.cs index 646ba22..1564aea 100644 --- a/HopFrame.Security/Services/IPermissionService.cs +++ b/HopFrame.Security/Services/IPermissionService.cs @@ -1,43 +1,31 @@ +using HopFrame.Database.Models; + namespace HopFrame.Security.Services; public interface IPermissionService { - - /// - /// Checks for the user to have the specified permission - /// Permission system:
- /// - "*" -> all rights
- /// - "group.[name]" -> group member
- /// - "[namespace].[name]" -> single permission
- /// - "[namespace].*" -> all permissions in the namespace - ///
- /// The permission the user needs - /// rather the user has the permission or not - Task HasPermission(string permission); - - /// - /// Checks if the user has all the specified permissions - /// - /// list of the permissions - /// rather the user has all the permissions or not - Task HasPermissions(params string[] permissions); - - /// - /// Checks if the user has any of the specified permissions - /// - /// list of the permissions - /// rather the user has any permission or not - Task HasAnyPermission(params string[] permissions); + + Task HasPermission(string permission, Guid user); + + Task GetPermissionGroup(string name); + + Task CreatePermissionGroup(string name, bool isDefault = false, string description = null); + + Task DeletePermissionGroup(PermissionGroup group); /// - /// Checks for the user to have the specified permission - /// Permission system:
+ /// permission system:
/// - "*" -> all rights
/// - "group.[name]" -> group member
/// - "[namespace].[name]" -> single permission
/// - "[namespace].*" -> all permissions in the namespace ///
- /// The permission the user needs - /// The user who gets checked - /// rather the user has the permission or not - Task HasPermission(string permission, Guid user); + /// + /// + /// + Task AddPermission(IPermissionOwner owner, string permission); + + Task DeletePermission(Permission permission); + + internal Task GetFullPermissions(string user); + } \ No newline at end of file diff --git a/HopFrame.Security/Services/IUserService.cs b/HopFrame.Security/Services/IUserService.cs new file mode 100644 index 0000000..786d83e --- /dev/null +++ b/HopFrame.Security/Services/IUserService.cs @@ -0,0 +1,27 @@ +using HopFrame.Database.Models; +using HopFrame.Security.Models; + +namespace HopFrame.Security.Services; + +public interface IUserService { + Task> GetUsers(); + + Task GetUser(Guid userId); + + Task GetUserByEmail(string email); + + Task GetUserByUsername(string username); + + Task AddUser(UserRegister user); + + /// + /// IMPORTANT:
+ /// This function does not add or remove any permissions to the user. + /// For that please use + ///
+ Task UpdateUser(User user); + + Task DeleteUser(User user); + + Task GetUserPassword(User user); +} \ No newline at end of file diff --git a/HopFrame.Security/Services/Implementation/PermissionService.cs b/HopFrame.Security/Services/Implementation/PermissionService.cs new file mode 100644 index 0000000..50dc3a2 --- /dev/null +++ b/HopFrame.Security/Services/Implementation/PermissionService.cs @@ -0,0 +1,105 @@ +using HopFrame.Database; +using HopFrame.Database.Models; +using HopFrame.Database.Models.Entries; +using HopFrame.Security.Authorization; +using HopFrame.Security.Claims; +using Microsoft.EntityFrameworkCore; + +namespace HopFrame.Security.Services.Implementation; + +internal sealed class PermissionService(TDbContext context, ITokenContext current) : IPermissionService where TDbContext : HopDbContextBase { + public async Task HasPermission(string permission) { + return await HasPermission(permission, current.User.Id); + } + + public async Task HasPermissions(params string[] permissions) { + var user = current.User.Id.ToString(); + var perms = await GetFullPermissions(user); + + foreach (var permission in permissions) { + if (!PermissionValidator.IncludesPermission(permission, perms)) return false; + } + + return true; + } + + public async Task HasAnyPermission(params string[] permissions) { + var user = current.User.Id.ToString(); + var perms = await GetFullPermissions(user); + + foreach (var permission in permissions) { + if (PermissionValidator.IncludesPermission(permission, perms)) return true; + } + + return false; + } + + public async Task HasPermission(string permission, Guid user) { + var permissions = await GetFullPermissions(user.ToString()); + + return PermissionValidator.IncludesPermission(permission, permissions); + } + + public Task GetPermissionGroup(string name) { + return context.Groups + .Where(group => group.Name == name) + .Select(group => group.ToPermissionGroup(context)) + .SingleOrDefaultAsync(); + } + + public async Task CreatePermissionGroup(string name, bool isDefault = false, string description = null) { + var group = new GroupEntry { + Name = name, + Description = description, + Default = isDefault, + CreatedAt = DateTime.Now + }; + + await context.Groups.AddAsync(group); + await context.SaveChangesAsync(); + } + + public async Task DeletePermissionGroup(PermissionGroup group) { + var entry = await context.Groups.SingleOrDefaultAsync(entry => entry.Name == group.Name); + context.Groups.Remove(entry); + await context.SaveChangesAsync(); + } + + public async Task AddPermission(IPermissionOwner owner, string permission) { + var userId = owner is User user ? user.Id.ToString() : (owner as PermissionGroup)?.Name; + + await context.Permissions.AddAsync(new PermissionEntry { + UserId = userId, + PermissionText = permission, + GrantedAt = DateTime.Now + }); + await context.SaveChangesAsync(); + } + + public async Task DeletePermission(Permission permission) { + var entry = await context.Permissions.SingleOrDefaultAsync(entry => entry.RecordId == permission.Id); + context.Permissions.Remove(entry); + await context.SaveChangesAsync(); + } + + public async Task GetFullPermissions(string user) { + var permissions = await context.Permissions + .Where(perm => perm.UserId == user) + .Select(perm => perm.PermissionText) + .ToListAsync(); + + var groups = permissions + .Where(perm => perm.StartsWith("group.")) + .ToList(); + + var groupPerms = new List(); + foreach (var group in groups) { + var perms = await GetFullPermissions(group); + groupPerms.AddRange(perms); + } + + permissions.AddRange(groupPerms); + + return permissions.ToArray(); + } +} \ No newline at end of file diff --git a/HopFrame.Security/Services/Implementation/UserService.cs b/HopFrame.Security/Services/Implementation/UserService.cs new file mode 100644 index 0000000..7d549d1 --- /dev/null +++ b/HopFrame.Security/Services/Implementation/UserService.cs @@ -0,0 +1,110 @@ +using System.Globalization; +using System.Text; +using HopFrame.Database; +using HopFrame.Database.Models; +using HopFrame.Database.Models.Entries; +using HopFrame.Security.Models; +using Microsoft.EntityFrameworkCore; + +namespace HopFrame.Security.Services.Implementation; + +internal sealed class UserService(TDbContext context) : IUserService where TDbContext : HopDbContextBase { + public async Task> GetUsers() { + return await context.Users + .Select(user => user.ToUserModel(context)) + .ToListAsync(); + } + + public Task GetUser(Guid userId) { + var id = userId.ToString(); + + return context.Users + .Where(user => user.Id == id) + .Select(user => user.ToUserModel(context)) + .SingleOrDefaultAsync(); + } + + public Task GetUserByEmail(string email) { + return context.Users + .Where(user => user.Email == email) + .Select(user => user.ToUserModel(context)) + .SingleOrDefaultAsync(); + } + + public Task GetUserByUsername(string username) { + return context.Users + .Where(user => user.Username == username) + .Select(user => user.ToUserModel(context)) + .SingleOrDefaultAsync(); + } + + public async Task AddUser(UserRegister user) { + var entry = new UserEntry { + Id = Guid.NewGuid().ToString(), + Email = user.Email, + Username = user.Username, + CreatedAt = DateTime.Now + }; + entry.Password = EncryptionManager.Hash(user.Password, Encoding.Default.GetBytes(entry.CreatedAt.ToString(CultureInfo.InvariantCulture))); + + await context.Users.AddAsync(entry); + + var defaultGroups = await context.Groups + .Where(group => group.Default) + .Select(group => "group." + group.Name) + .ToListAsync(); + + await context.Permissions.AddRangeAsync(defaultGroups.Select(group => new PermissionEntry { + GrantedAt = DateTime.Now, + PermissionText = group, + UserId = entry.Id + })); + + await context.SaveChangesAsync(); + return entry.ToUserModel(context); + } + + public async Task UpdateUser(User user) { + var id = user.Id.ToString(); + var entry = await context.Users + .SingleOrDefaultAsync(entry => entry.Id == id); + if (entry is null) return; + + entry.Email = user.Email; + entry.Username = user.Username; + + await context.SaveChangesAsync(); + } + + public async Task DeleteUser(User user) { + var id = user.Id.ToString(); + var entry = await context.Users + .SingleOrDefaultAsync(entry => entry.Id == id); + + if (entry is null) return; + + context.Users.Remove(entry); + + var userTokens = await context.Tokens + .Where(token => token.UserId == id) + .ToArrayAsync(); + context.Tokens.RemoveRange(userTokens); + + var userPermissions = await context.Permissions + .Where(perm => perm.UserId == id) + .ToArrayAsync(); + context.Permissions.RemoveRange(userPermissions); + + context.OnUserDelete(entry); + + await context.SaveChangesAsync(); + } + + public Task GetUserPassword(User user) { + var id = user.Id.ToString(); + return context.Users + .Where(entry => entry.Id == id) + .Select(entry => entry.Password) + .SingleOrDefaultAsync(); + } +} \ No newline at end of file diff --git a/HopFrame.Security/Services/PermissionService.cs b/HopFrame.Security/Services/PermissionService.cs deleted file mode 100644 index daf4f81..0000000 --- a/HopFrame.Security/Services/PermissionService.cs +++ /dev/null @@ -1,60 +0,0 @@ -using HopFrame.Database; -using HopFrame.Security.Authorization; -using HopFrame.Security.Claims; -using Microsoft.EntityFrameworkCore; - -namespace HopFrame.Security.Services; - -internal class PermissionService(TDbContext context, ITokenContext current) : IPermissionService where TDbContext : HopDbContextBase { - public async Task HasPermission(string permission) { - return await HasPermission(permission, current.User.Id); - } - - public async Task HasPermissions(params string[] permissions) { - var user = current.User.Id.ToString(); - var perms = await GetFullPermissions(user); - - foreach (var permission in permissions) { - if (!PermissionValidator.IncludesPermission(permission, perms)) return false; - } - - return true; - } - - public async Task HasAnyPermission(params string[] permissions) { - var user = current.User.Id.ToString(); - var perms = await GetFullPermissions(user); - - foreach (var permission in permissions) { - if (PermissionValidator.IncludesPermission(permission, perms)) return true; - } - - return false; - } - - public async Task HasPermission(string permission, Guid user) { - var permissions = await GetFullPermissions(user.ToString()); - - return PermissionValidator.IncludesPermission(permission, permissions); - } - - private async Task GetFullPermissions(string user) { - var permissions = await context.Permissions - .Where(perm => perm.UserId == user) - .Select(perm => perm.PermissionText) - .ToListAsync(); - - var groups = permissions - .Where(perm => perm.StartsWith("group.")) - .ToList(); - - var groupPerms = await context.Permissions - .Where(perm => groups.Contains(user)) - .Select(perm => perm.PermissionText) - .ToListAsync(); - - permissions.AddRange(groupPerms); - - return permissions.ToArray(); - } -} \ No newline at end of file