From ba46147a74e7a7bbd12c0994cbea45f78a5a4b0a Mon Sep 17 00:00:00 2001 From: Leon Hoppe Date: Sat, 21 Dec 2024 16:09:55 +0100 Subject: [PATCH 1/4] Added API token functionality --- .idea/.idea.HopFrame/.idea/dataSources.xml | 2 +- HopFrame.sln.DotSettings.user | 1 + .../Logic/Implementation/AuthLogic.cs | 16 ++++++------ src/HopFrame.Database/HopDbContextBase.cs | 5 ++++ src/HopFrame.Database/Models/Permission.cs | 3 +++ src/HopFrame.Database/Models/Token.cs | 12 +++++++-- .../Repositories/ITokenRepository.cs | 1 + .../Implementation/PermissionRepository.cs | 19 ++++++++++++++ .../Implementation/TokenRepository.cs | 18 +++++++++++-- .../Authentication/HopFrameAuthentication.cs | 16 ++++++++++-- src/HopFrame.Web/AuthMiddleware.cs | 2 +- .../Services/Implementation/AuthService.cs | 10 +++---- tests/HopFrame.Tests.Api/AuthLogicTests.cs | 16 ++++++------ .../Repositories/TokenRepositoryTests.cs | 8 +++--- .../AuthenticationTests.cs | 14 +++++----- .../HopFrame.Tests.Web/AuthMiddlewareTests.cs | 4 +-- tests/HopFrame.Tests.Web/AuthServiceTests.cs | 26 +++++++++---------- 17 files changed, 118 insertions(+), 55 deletions(-) diff --git a/.idea/.idea.HopFrame/.idea/dataSources.xml b/.idea/.idea.HopFrame/.idea/dataSources.xml index ded00e9..3e820ee 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:$PROJECT_DIR$/test/RestApiTest/bin/Debug/net8.0/test.db + jdbc:sqlite:C:\Users\leon\Documents\Projekte\HopFrame\testing\HopFrame.Testing.Api\bin\Debug\net8.0\test.db diff --git a/HopFrame.sln.DotSettings.user b/HopFrame.sln.DotSettings.user index 1e30ef8..86d9f80 100644 --- a/HopFrame.sln.DotSettings.user +++ b/HopFrame.sln.DotSettings.user @@ -69,6 +69,7 @@ + \ No newline at end of file diff --git a/src/HopFrame.Api/Logic/Implementation/AuthLogic.cs b/src/HopFrame.Api/Logic/Implementation/AuthLogic.cs index acf8fb7..61e9681 100644 --- a/src/HopFrame.Api/Logic/Implementation/AuthLogic.cs +++ b/src/HopFrame.Api/Logic/Implementation/AuthLogic.cs @@ -23,18 +23,18 @@ internal class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenC var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user); var accessToken = await tokens.CreateToken(Token.AccessTokenType, user); - accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions { + accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.TokenId.ToString(), new CookieOptions { MaxAge = options.Value.RefreshTokenTime, HttpOnly = true, Secure = true }); - accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions { + accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions { MaxAge = options.Value.AccessTokenTime, HttpOnly = true, Secure = true }); - return LogicResult>.Ok(accessToken.Content.ToString()); + return LogicResult>.Ok(accessToken.TokenId.ToString()); } public async Task>> Register(UserRegister register) { @@ -54,18 +54,18 @@ internal class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenC var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user); var accessToken = await tokens.CreateToken(Token.AccessTokenType, user); - accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions { + accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.TokenId.ToString(), new CookieOptions { MaxAge = options.Value.RefreshTokenTime, HttpOnly = true, Secure = true }); - accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions { + accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions { MaxAge = options.Value.AccessTokenTime, HttpOnly = false, Secure = true }); - return LogicResult>.Ok(accessToken.Content.ToString()); + return LogicResult>.Ok(accessToken.TokenId.ToString()); } public async Task>> Authenticate() { @@ -87,13 +87,13 @@ internal class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenC var accessToken = await tokens.CreateToken(Token.AccessTokenType, token.Owner); - accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions { + accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions { MaxAge = options.Value.AccessTokenTime, HttpOnly = false, Secure = true }); - return LogicResult>.Ok(accessToken.Content.ToString()); + return LogicResult>.Ok(accessToken.TokenId.ToString()); } public async Task Logout() { diff --git a/src/HopFrame.Database/HopDbContextBase.cs b/src/HopFrame.Database/HopDbContextBase.cs index 21342ea..cd03860 100644 --- a/src/HopFrame.Database/HopDbContextBase.cs +++ b/src/HopFrame.Database/HopDbContextBase.cs @@ -30,5 +30,10 @@ public abstract class HopDbContextBase : DbContext { .HasMany(g => g.Permissions) .WithOne(p => p.Group) .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity() + .HasMany(t => t.Permissions) + .WithOne(t => t.Token) + .OnDelete(DeleteBehavior.Cascade); } } \ No newline at end of file diff --git a/src/HopFrame.Database/Models/Permission.cs b/src/HopFrame.Database/Models/Permission.cs index db111ba..658a90e 100644 --- a/src/HopFrame.Database/Models/Permission.cs +++ b/src/HopFrame.Database/Models/Permission.cs @@ -21,6 +21,9 @@ public class Permission { [ForeignKey("GroupName"), JsonIgnore] public virtual PermissionGroup Group { get; set; } + [ForeignKey("TokenId"), JsonIgnore] + public virtual Token Token { get; set; } + } public interface IPermissionOwner; diff --git a/src/HopFrame.Database/Models/Token.cs b/src/HopFrame.Database/Models/Token.cs index a42d367..b22bd21 100644 --- a/src/HopFrame.Database/Models/Token.cs +++ b/src/HopFrame.Database/Models/Token.cs @@ -4,24 +4,32 @@ using System.Text.Json.Serialization; namespace HopFrame.Database.Models; -public class Token { +public class Token : IPermissionOwner { public const int RefreshTokenType = 0; public const int AccessTokenType = 1; + public const int ApiTokenType = 2; /// /// Defines the Type of the stored Token /// 0: Refresh token /// 1: Access token + /// 2: Api token /// [Required, MinLength(1), MaxLength(1)] public int Type { get; set; } [Key, Required, MinLength(36), MaxLength(36)] - public Guid Content { get; set; } + public Guid TokenId { get; set; } + /// + /// Defines the creation date of the token + /// In case of an api token it defines the date it becomes invalid + /// [Required] public DateTime CreatedAt { get; set; } [ForeignKey("UserId"), JsonIgnore] public virtual User Owner { get; set; } + + public virtual List Permissions { get; set; } } \ No newline at end of file diff --git a/src/HopFrame.Database/Repositories/ITokenRepository.cs b/src/HopFrame.Database/Repositories/ITokenRepository.cs index bec3963..5f66769 100644 --- a/src/HopFrame.Database/Repositories/ITokenRepository.cs +++ b/src/HopFrame.Database/Repositories/ITokenRepository.cs @@ -6,4 +6,5 @@ public interface ITokenRepository { Task GetToken(string content); Task CreateToken(int type, User owner); Task DeleteUserTokens(User owner); + Task CreateApiToken(User owner, DateTime expirationDate); } \ No newline at end of file diff --git a/src/HopFrame.Database/Repositories/Implementation/PermissionRepository.cs b/src/HopFrame.Database/Repositories/Implementation/PermissionRepository.cs index 45bcfd8..f80b0b8 100644 --- a/src/HopFrame.Database/Repositories/Implementation/PermissionRepository.cs +++ b/src/HopFrame.Database/Repositories/Implementation/PermissionRepository.cs @@ -24,6 +24,10 @@ internal sealed class PermissionRepository(TDbContext context, IGrou entry.User = user; }else if (owner is PermissionGroup group) { entry.Group = group; + }else if (owner is Token token) { + if (token.Type != Token.ApiTokenType) + throw new ArgumentException("Only API tokens can have permissions!"); + entry.Token = token; } await context.Permissions.AddAsync(entry); @@ -48,6 +52,13 @@ internal sealed class PermissionRepository(TDbContext context, IGrou .Where(p =>p.Group.Name == group.Name) .Where(p => p.PermissionName == permission) .SingleOrDefaultAsync(); + }else if (owner is Token token) { + entry = await context.Permissions + .Include(p => p.Token) + .Where(p => p.Token != null) + .Where(p => p.Token.TokenId == token.TokenId) + .Where(p => p.PermissionName == permission) + .SingleOrDefaultAsync(); } if (entry is not null) { @@ -74,6 +85,14 @@ internal sealed class PermissionRepository(TDbContext context, IGrou .Where(p =>p.Group.Name == group.Name) .ToListAsync(); + permissions.AddRange(perms.Select(p => p.PermissionName)); + }else if (owner is Token token) { + var perms = await context.Permissions + .Include(p => p.Token) + .Where(p => p.Token != null) + .Where(p =>p.Token.TokenId == token.TokenId) + .ToListAsync(); + permissions.AddRange(perms.Select(p => p.PermissionName)); } diff --git a/src/HopFrame.Database/Repositories/Implementation/TokenRepository.cs b/src/HopFrame.Database/Repositories/Implementation/TokenRepository.cs index 70f727a..927d080 100644 --- a/src/HopFrame.Database/Repositories/Implementation/TokenRepository.cs +++ b/src/HopFrame.Database/Repositories/Implementation/TokenRepository.cs @@ -11,14 +11,14 @@ internal sealed class TokenRepository(TDbContext context) : ITokenRe return await context.Tokens .Include(t => t.Owner) - .Where(t => t.Content == guid) + .Where(t => t.TokenId == guid) .SingleOrDefaultAsync(); } public async Task CreateToken(int type, User owner) { var token = new Token { CreatedAt = DateTime.Now, - Content = Guid.NewGuid(), + TokenId = Guid.NewGuid(), Type = type, Owner = owner }; @@ -38,4 +38,18 @@ internal sealed class TokenRepository(TDbContext context) : ITokenRe context.Tokens.RemoveRange(tokens); await context.SaveChangesAsync(); } + + public async Task CreateApiToken(User owner, DateTime expirationDate) { + var token = new Token { + CreatedAt = expirationDate, + TokenId = Guid.NewGuid(), + Type = Token.ApiTokenType, + Owner = owner + }; + + await context.Tokens.AddAsync(token); + await context.SaveChangesAsync(); + + return token; + } } \ No newline at end of file diff --git a/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs b/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs index 8b0a3b1..9f9af47 100644 --- a/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs +++ b/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs @@ -1,5 +1,6 @@ using System.Security.Claims; using System.Text.Encodings.Web; +using HopFrame.Database.Models; using HopFrame.Database.Repositories; using HopFrame.Security.Claims; using Microsoft.AspNetCore.Authentication; @@ -33,7 +34,10 @@ public class HopFrameAuthentication( var tokenEntry = await tokens.GetToken(accessToken); if (tokenEntry is null) return AuthenticateResult.Fail("The provided Access Token does not exist"); - if (tokenEntry.CreatedAt + tokenOptions.Value.AccessTokenTime < DateTime.Now) return AuthenticateResult.Fail("The provided Access Token is expired"); + + if (tokenEntry.Type == Token.ApiTokenType) { + if (tokenEntry.CreatedAt < DateTime.Now) return AuthenticateResult.Fail("The provided API Token is expired"); + }else if (tokenEntry.CreatedAt + tokenOptions.Value.AccessTokenTime < DateTime.Now) return AuthenticateResult.Fail("The provided Access Token is expired"); if (tokenEntry.Owner is null) return AuthenticateResult.Fail("The provided Access Token does not match any user"); @@ -43,7 +47,15 @@ public class HopFrameAuthentication( new(HopFrameClaimTypes.UserId, tokenEntry.Owner.Id.ToString()) }; - var permissions = await perms.GetFullPermissions(tokenEntry.Owner); + IList permissions; + + if (tokenEntry.Type == Token.ApiTokenType) { + permissions = await perms.GetFullPermissions(tokenEntry); + } + else { + permissions = await perms.GetFullPermissions(tokenEntry.Owner); + } + claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm))); var principal = new ClaimsPrincipal(); diff --git a/src/HopFrame.Web/AuthMiddleware.cs b/src/HopFrame.Web/AuthMiddleware.cs index 33e2f52..ac5c954 100644 --- a/src/HopFrame.Web/AuthMiddleware.cs +++ b/src/HopFrame.Web/AuthMiddleware.cs @@ -22,7 +22,7 @@ public sealed class AuthMiddleware(IAuthService auth, IPermissionRepository perm } var claims = new List { - new(HopFrameClaimTypes.AccessTokenId, token.Content.ToString()), + new(HopFrameClaimTypes.AccessTokenId, token.TokenId.ToString()), new(HopFrameClaimTypes.UserId, token.Owner.Id.ToString()) }; diff --git a/src/HopFrame.Web/Services/Implementation/AuthService.cs b/src/HopFrame.Web/Services/Implementation/AuthService.cs index 6fca234..7bc38a4 100644 --- a/src/HopFrame.Web/Services/Implementation/AuthService.cs +++ b/src/HopFrame.Web/Services/Implementation/AuthService.cs @@ -28,12 +28,12 @@ internal class AuthService( var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user); var accessToken = await tokens.CreateToken(Token.AccessTokenType, user); - httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions { + httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.TokenId.ToString(), new CookieOptions { MaxAge = options.Value.RefreshTokenTime, HttpOnly = true, Secure = true }); - httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions { + httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions { MaxAge = options.Value.AccessTokenTime, HttpOnly = false, Secure = true @@ -49,12 +49,12 @@ internal class AuthService( var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user); var accessToken = await tokens.CreateToken(Token.AccessTokenType, user); - httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions { + httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.TokenId.ToString(), new CookieOptions { MaxAge = options.Value.RefreshTokenTime, HttpOnly = true, Secure = true }); - httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions { + httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions { MaxAge = options.Value.AccessTokenTime, HttpOnly = false, Secure = true @@ -83,7 +83,7 @@ internal class AuthService( var accessToken = await tokens.CreateToken(Token.AccessTokenType, token.Owner); - httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions { + httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions { MaxAge = options.Value.AccessTokenTime, HttpOnly = false, Secure = true diff --git a/tests/HopFrame.Tests.Api/AuthLogicTests.cs b/tests/HopFrame.Tests.Api/AuthLogicTests.cs index a5163d2..39975f5 100644 --- a/tests/HopFrame.Tests.Api/AuthLogicTests.cs +++ b/tests/HopFrame.Tests.Api/AuthLogicTests.cs @@ -58,13 +58,13 @@ public class AuthLogicTests { tokens .Setup(t => t.CreateToken(It.Is(t => t == Token.RefreshTokenType), It.IsAny())) .ReturnsAsync(new Token { - Content = _refreshToken, + TokenId = _refreshToken, Type = Token.RefreshTokenType }); tokens .Setup(t => t.CreateToken(It.Is(t => t == Token.AccessTokenType), It.IsAny())) .ReturnsAsync(new Token { - Content = _accessToken, + TokenId = _accessToken, Type = Token.AccessTokenType }); tokens @@ -229,11 +229,11 @@ public class AuthLogicTests { // Arrange var token = new Token { Type = Token.RefreshTokenType, - Content = _refreshToken, + TokenId = _refreshToken, CreatedAt = DateTime.Now, Owner = CreateDummyUser() }; - var (auth, context) = SetupEnvironment(true, token, token.Content.ToString()); + var (auth, context) = SetupEnvironment(true, token, token.TokenId.ToString()); // Act var result = await auth.Authenticate(); @@ -277,11 +277,11 @@ public class AuthLogicTests { // Arrange var token = new Token { Type = Token.AccessTokenType, - Content = _refreshToken, + TokenId = _refreshToken, CreatedAt = DateTime.Now, Owner = CreateDummyUser() }; - var (auth, context) = SetupEnvironment(true, token, token.Content.ToString()); + var (auth, context) = SetupEnvironment(true, token, token.TokenId.ToString()); // Act var result = await auth.Authenticate(); @@ -297,11 +297,11 @@ public class AuthLogicTests { // Arrange var token = new Token { Type = Token.RefreshTokenType, - Content = _refreshToken, + TokenId = _refreshToken, CreatedAt = DateTime.MinValue, Owner = CreateDummyUser() }; - var (auth, context) = SetupEnvironment(true, token, token.Content.ToString()); + var (auth, context) = SetupEnvironment(true, token, token.TokenId.ToString()); // Act var result = await auth.Authenticate(); diff --git a/tests/HopFrame.Tests.Database/Repositories/TokenRepositoryTests.cs b/tests/HopFrame.Tests.Database/Repositories/TokenRepositoryTests.cs index 83dc770..d37fde2 100644 --- a/tests/HopFrame.Tests.Database/Repositories/TokenRepositoryTests.cs +++ b/tests/HopFrame.Tests.Database/Repositories/TokenRepositoryTests.cs @@ -14,7 +14,7 @@ public class TokenRepositoryTests { for (int i = 0; i < count; i++) { await context.Tokens.AddAsync(new() { - Content = Guid.NewGuid(), + TokenId = Guid.NewGuid(), Owner = CreateTestUser(), Type = Token.AccessTokenType }); @@ -37,7 +37,7 @@ public class TokenRepositoryTests { var token = context.Tokens.First(); // Act - var result = await repo.GetToken(token.Content.ToString()); + var result = await repo.GetToken(token.TokenId.ToString()); // Assert Assert.Equal(token, result); @@ -64,12 +64,12 @@ public class TokenRepositoryTests { var user = CreateTestUser(); await context.Tokens.AddRangeAsync(new List { new() { - Content = Guid.NewGuid(), + TokenId = Guid.NewGuid(), Owner = user, Type = Token.AccessTokenType }, new() { - Content = Guid.NewGuid(), + TokenId = Guid.NewGuid(), Owner = user, Type = Token.RefreshTokenType } diff --git a/tests/HopFrame.Tests.Security/AuthenticationTests.cs b/tests/HopFrame.Tests.Security/AuthenticationTests.cs index 5cd6d44..5a00df9 100644 --- a/tests/HopFrame.Tests.Security/AuthenticationTests.cs +++ b/tests/HopFrame.Tests.Security/AuthenticationTests.cs @@ -30,7 +30,7 @@ public class AuthenticationTests { var provideCorrectToken = correctToken is null; correctToken ??= new Token { - Content = Guid.NewGuid(), + TokenId = Guid.NewGuid(), CreatedAt = DateTime.Now, Type = Token.AccessTokenType, Owner = new User { @@ -39,7 +39,7 @@ public class AuthenticationTests { }; tokens - .Setup(x => x.GetToken(It.Is(t => t == correctToken.Content.ToString()))) + .Setup(x => x.GetToken(It.Is(t => t == correctToken.TokenId.ToString()))) .ReturnsAsync(correctToken); perms @@ -49,7 +49,7 @@ public class AuthenticationTests { var auth = new HopFrameAuthentication(options.Object, logger.Object, encoder.Object, clock.Object, tokens.Object, perms.Object, new OptionsWrapper(new HopFrameAuthenticationOptions())); var context = new DefaultHttpContext(); if (provideCorrectToken) - context.HttpContext.Request.Headers.Append(HopFrameAuthentication.SchemeName, correctToken.Content.ToString()); + context.HttpContext.Request.Headers.Append(HopFrameAuthentication.SchemeName, correctToken.TokenId.ToString()); if (providedToken is not null) context.HttpContext.Request.Headers.Append(HopFrameAuthentication.SchemeName, providedToken); @@ -101,12 +101,12 @@ public class AuthenticationTests { public async Task Authentication_With_ExpiredToken_Should_Fail() { // Arrange var token = new Token { - Content = Guid.NewGuid(), + TokenId = Guid.NewGuid(), CreatedAt = DateTime.MinValue, Type = Token.AccessTokenType, Owner = new User() }; - var auth = await SetupEnvironment(token, token.Content.ToString()); + var auth = await SetupEnvironment(token, token.TokenId.ToString()); // Act var result = await auth.AuthenticateAsync(); @@ -121,12 +121,12 @@ public class AuthenticationTests { public async Task Authentication_With_UnownedToken_Should_Fail() { // Arrange var token = new Token { - Content = Guid.NewGuid(), + TokenId = Guid.NewGuid(), CreatedAt = DateTime.Now, Type = Token.AccessTokenType, Owner = null }; - var auth = await SetupEnvironment(token, token.Content.ToString()); + var auth = await SetupEnvironment(token, token.TokenId.ToString()); // Act var result = await auth.AuthenticateAsync(); diff --git a/tests/HopFrame.Tests.Web/AuthMiddlewareTests.cs b/tests/HopFrame.Tests.Web/AuthMiddlewareTests.cs index d9e136f..bada100 100644 --- a/tests/HopFrame.Tests.Web/AuthMiddlewareTests.cs +++ b/tests/HopFrame.Tests.Web/AuthMiddlewareTests.cs @@ -61,7 +61,7 @@ public class AuthMiddlewareTests { public async Task InvokeAsync_With_InvalidLoginValidToken_Should_Succeed() { // Arrange var token = new Token { - Content = Guid.NewGuid(), + TokenId = Guid.NewGuid(), CreatedAt = DateTime.Now, Type = Token.AccessTokenType, Owner = CreateDummyUser() @@ -74,7 +74,7 @@ public class AuthMiddlewareTests { // Assert Assert.Equal(token.Owner.Id.ToString(), context.User.FindFirstValue(HopFrameClaimTypes.UserId)); - Assert.Equal(token.Content.ToString(), context.User.FindFirstValue(HopFrameClaimTypes.AccessTokenId)); + Assert.Equal(token.TokenId.ToString(), context.User.FindFirstValue(HopFrameClaimTypes.AccessTokenId)); Assert.Equal(token.Owner.Permissions.First().PermissionName, context.User.FindFirstValue(HopFrameClaimTypes.Permission)); } diff --git a/tests/HopFrame.Tests.Web/AuthServiceTests.cs b/tests/HopFrame.Tests.Web/AuthServiceTests.cs index d5c5ad7..306a94b 100644 --- a/tests/HopFrame.Tests.Web/AuthServiceTests.cs +++ b/tests/HopFrame.Tests.Web/AuthServiceTests.cs @@ -47,13 +47,13 @@ public class AuthServiceTests { tokens .Setup(t => t.CreateToken(It.Is(t => t == Token.RefreshTokenType), It.IsAny())) .ReturnsAsync(new Token { - Content = _refreshToken, + TokenId = _refreshToken, Type = Token.RefreshTokenType }); tokens .Setup(t => t.CreateToken(It.Is(t => t == Token.AccessTokenType), It.IsAny())) .ReturnsAsync(new Token { - Content = _accessToken, + TokenId = _accessToken, Type = Token.AccessTokenType }); tokens @@ -171,18 +171,18 @@ public class AuthServiceTests { // Arrange var token = new Token { Type = Token.RefreshTokenType, - Content = _refreshToken, + TokenId = _refreshToken, CreatedAt = DateTime.Now, Owner = CreateDummyUser() }; - var (service, context) = SetupEnvironment(true, token, token.Content.ToString()); + var (service, context) = SetupEnvironment(true, token, token.TokenId.ToString()); // Act var result = await service.RefreshLogin(); // Assert Assert.NotNull(result); - Assert.Equal(_accessToken, result.Content); + Assert.Equal(_accessToken, result.TokenId); Assert.Equal(_accessToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.AccessTokenType)); } @@ -217,11 +217,11 @@ public class AuthServiceTests { // Arrange var token = new Token { Type = Token.AccessTokenType, - Content = _refreshToken, + TokenId = _refreshToken, CreatedAt = DateTime.Now, Owner = CreateDummyUser() }; - var (service, context) = SetupEnvironment(true, token, token.Content.ToString()); + var (service, context) = SetupEnvironment(true, token, token.TokenId.ToString()); // Act var result = await service.RefreshLogin(); @@ -236,11 +236,11 @@ public class AuthServiceTests { // Arrange var token = new Token { Type = Token.RefreshTokenType, - Content = _refreshToken, + TokenId = _refreshToken, CreatedAt = DateTime.MinValue, Owner = CreateDummyUser() }; - var (service, context) = SetupEnvironment(true, token, token.Content.ToString()); + var (service, context) = SetupEnvironment(true, token, token.TokenId.ToString()); // Act var result = await service.RefreshLogin(); @@ -255,7 +255,7 @@ public class AuthServiceTests { // Arrange var token = new Token { Type = Token.AccessTokenType, - Content = _accessToken, + TokenId = _accessToken, CreatedAt = DateTime.Now, Owner = CreateDummyUser() }; @@ -285,7 +285,7 @@ public class AuthServiceTests { // Arrange var token = new Token { Type = Token.RefreshTokenType, - Content = _accessToken, + TokenId = _accessToken, CreatedAt = DateTime.Now, Owner = CreateDummyUser() }; @@ -303,7 +303,7 @@ public class AuthServiceTests { // Arrange var token = new Token { Type = Token.AccessTokenType, - Content = _accessToken, + TokenId = _accessToken, CreatedAt = DateTime.MinValue, Owner = CreateDummyUser() }; @@ -321,7 +321,7 @@ public class AuthServiceTests { // Arrange var token = new Token { Type = Token.AccessTokenType, - Content = _accessToken, + TokenId = _accessToken, CreatedAt = DateTime.Now, Owner = null }; From 59c452ff73559cd40376fd2d9fa0887269eae0f7 Mon Sep 17 00:00:00 2001 From: Leon Hoppe Date: Sat, 21 Dec 2024 16:55:20 +0100 Subject: [PATCH 2/4] updated application to check for contextual permissions --- .idea/.idea.HopFrame/.idea/discord.xml | 14 ++++++++++++ HopFrame.sln.DotSettings.user | 1 + .../Repositories/ITokenRepository.cs | 1 + .../Implementation/PermissionRepository.cs | 8 +++++-- .../Implementation/TokenRepository.cs | 5 +++++ .../Authentication/HopFrameAuthentication.cs | 10 +-------- src/HopFrame.Security/Claims/ITokenContext.cs | 2 ++ .../Claims/TokenContextImplementor.cs | 4 +++- src/HopFrame.Web/AuthMiddleware.cs | 2 +- .../Administration/AdminPageModal.razor | 4 ++-- .../Pages/Administration/AdminPageList.razor | 4 ++-- .../Controllers/TestController.cs | 22 ++++++++++++++++--- testing/HopFrame.Testing.Api/Program.cs | 2 +- 13 files changed, 58 insertions(+), 21 deletions(-) create mode 100644 .idea/.idea.HopFrame/.idea/discord.xml diff --git a/.idea/.idea.HopFrame/.idea/discord.xml b/.idea/.idea.HopFrame/.idea/discord.xml new file mode 100644 index 0000000..912db82 --- /dev/null +++ b/.idea/.idea.HopFrame/.idea/discord.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/HopFrame.sln.DotSettings.user b/HopFrame.sln.DotSettings.user index 86d9f80..eab4e1d 100644 --- a/HopFrame.sln.DotSettings.user +++ b/HopFrame.sln.DotSettings.user @@ -70,6 +70,7 @@ + \ No newline at end of file diff --git a/src/HopFrame.Database/Repositories/ITokenRepository.cs b/src/HopFrame.Database/Repositories/ITokenRepository.cs index 5f66769..2c1192c 100644 --- a/src/HopFrame.Database/Repositories/ITokenRepository.cs +++ b/src/HopFrame.Database/Repositories/ITokenRepository.cs @@ -6,5 +6,6 @@ public interface ITokenRepository { Task GetToken(string content); Task CreateToken(int type, User owner); Task DeleteUserTokens(User owner); + Task DeleteToken(Token token); Task CreateApiToken(User owner, DateTime expirationDate); } \ No newline at end of file diff --git a/src/HopFrame.Database/Repositories/Implementation/PermissionRepository.cs b/src/HopFrame.Database/Repositories/Implementation/PermissionRepository.cs index f80b0b8..6d55bc0 100644 --- a/src/HopFrame.Database/Repositories/Implementation/PermissionRepository.cs +++ b/src/HopFrame.Database/Repositories/Implementation/PermissionRepository.cs @@ -69,6 +69,10 @@ internal sealed class PermissionRepository(TDbContext context, IGrou public async Task> GetFullPermissions(IPermissionOwner owner) { var permissions = new List(); + + if (owner is Token token && token.Type != Token.ApiTokenType) { + owner = token.Owner; + } if (owner is User user) { var perms = await context.Permissions @@ -86,11 +90,11 @@ internal sealed class PermissionRepository(TDbContext context, IGrou .ToListAsync(); permissions.AddRange(perms.Select(p => p.PermissionName)); - }else if (owner is Token token) { + }else if (owner is Token apiToken) { var perms = await context.Permissions .Include(p => p.Token) .Where(p => p.Token != null) - .Where(p =>p.Token.TokenId == token.TokenId) + .Where(p =>p.Token.TokenId == apiToken.TokenId) .ToListAsync(); permissions.AddRange(perms.Select(p => p.PermissionName)); diff --git a/src/HopFrame.Database/Repositories/Implementation/TokenRepository.cs b/src/HopFrame.Database/Repositories/Implementation/TokenRepository.cs index 927d080..b44dc43 100644 --- a/src/HopFrame.Database/Repositories/Implementation/TokenRepository.cs +++ b/src/HopFrame.Database/Repositories/Implementation/TokenRepository.cs @@ -39,6 +39,11 @@ internal sealed class TokenRepository(TDbContext context) : ITokenRe await context.SaveChangesAsync(); } + public async Task DeleteToken(Token token) { + context.Tokens.Remove(token); + await context.SaveChangesAsync(); + } + public async Task CreateApiToken(User owner, DateTime expirationDate) { var token = new Token { CreatedAt = expirationDate, diff --git a/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs b/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs index 9f9af47..88a95c1 100644 --- a/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs +++ b/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs @@ -47,15 +47,7 @@ public class HopFrameAuthentication( new(HopFrameClaimTypes.UserId, tokenEntry.Owner.Id.ToString()) }; - IList permissions; - - if (tokenEntry.Type == Token.ApiTokenType) { - permissions = await perms.GetFullPermissions(tokenEntry); - } - else { - permissions = await perms.GetFullPermissions(tokenEntry.Owner); - } - + var permissions = await perms.GetFullPermissions(tokenEntry); claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm))); var principal = new ClaimsPrincipal(); diff --git a/src/HopFrame.Security/Claims/ITokenContext.cs b/src/HopFrame.Security/Claims/ITokenContext.cs index 6b5a590..6b052bc 100644 --- a/src/HopFrame.Security/Claims/ITokenContext.cs +++ b/src/HopFrame.Security/Claims/ITokenContext.cs @@ -21,4 +21,6 @@ public interface ITokenContext { /// The access token the user provided /// Token AccessToken { get; } + + IList ContextualPermissions { get; } } \ No newline at end of file diff --git a/src/HopFrame.Security/Claims/TokenContextImplementor.cs b/src/HopFrame.Security/Claims/TokenContextImplementor.cs index dd50a08..47fce76 100644 --- a/src/HopFrame.Security/Claims/TokenContextImplementor.cs +++ b/src/HopFrame.Security/Claims/TokenContextImplementor.cs @@ -4,10 +4,12 @@ using Microsoft.AspNetCore.Http; namespace HopFrame.Security.Claims; -internal sealed class TokenContextImplementor(IHttpContextAccessor accessor, IUserRepository users, ITokenRepository tokens) : ITokenContext { +internal sealed class TokenContextImplementor(IHttpContextAccessor accessor, IUserRepository users, ITokenRepository tokens, IPermissionRepository permissions) : ITokenContext { public bool IsAuthenticated => !string.IsNullOrEmpty(accessor.HttpContext?.User.GetAccessTokenId()); public User User => users.GetUser(Guid.Parse(accessor.HttpContext?.User.GetUserId() ?? Guid.Empty.ToString())).GetAwaiter().GetResult(); public Token AccessToken => tokens.GetToken(accessor.HttpContext?.User.GetAccessTokenId()).GetAwaiter().GetResult(); + + public IList ContextualPermissions => permissions.GetFullPermissions(AccessToken).GetAwaiter().GetResult(); } \ No newline at end of file diff --git a/src/HopFrame.Web/AuthMiddleware.cs b/src/HopFrame.Web/AuthMiddleware.cs index ac5c954..b5fbc93 100644 --- a/src/HopFrame.Web/AuthMiddleware.cs +++ b/src/HopFrame.Web/AuthMiddleware.cs @@ -26,7 +26,7 @@ public sealed class AuthMiddleware(IAuthService auth, IPermissionRepository perm new(HopFrameClaimTypes.UserId, token.Owner.Id.ToString()) }; - var permissions = await perms.GetFullPermissions(token.Owner); + var permissions = await perms.GetFullPermissions(token); claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm))); context.User.AddIdentity(new ClaimsIdentity(claims, HopFrameAuthentication.SchemeName)); diff --git a/src/HopFrame.Web/Components/Administration/AdminPageModal.razor b/src/HopFrame.Web/Components/Administration/AdminPageModal.razor index 4876323..4e212b5 100644 --- a/src/HopFrame.Web/Components/Administration/AdminPageModal.razor +++ b/src/HopFrame.Web/Components/Administration/AdminPageModal.razor @@ -321,7 +321,7 @@ private async void Save() { if (_isEdit && _currentPage.Permissions.Update is not null) { - if (!await Permissions.HasPermission(Auth.User, _currentPage.Permissions.Update)) { + if (!await Permissions.HasPermission(Auth.AccessToken, _currentPage.Permissions.Update)) { await Alerts.FireAsync(new SweetAlertOptions { Title = "Unauthorized!", Text = "You don't have the required permissions to edit an entry!", @@ -330,7 +330,7 @@ return; } }else if (_currentPage.Permissions.Create is not null) { - if (!await Permissions.HasPermission(Auth.User, _currentPage.Permissions.Create)) { + if (!await Permissions.HasPermission(Auth.AccessToken, _currentPage.Permissions.Create)) { await Alerts.FireAsync(new SweetAlertOptions { Title = "Unauthorized!", Text = "You don't have the required permissions to add an entry!", diff --git a/src/HopFrame.Web/Pages/Administration/AdminPageList.razor b/src/HopFrame.Web/Pages/Administration/AdminPageList.razor index d796aeb..1086918 100644 --- a/src/HopFrame.Web/Pages/Administration/AdminPageList.razor +++ b/src/HopFrame.Web/Pages/Administration/AdminPageList.razor @@ -140,8 +140,8 @@ throw new ArgumentException($"AdminPage '{_pageData.Title}' does not specify a model repository!'"); _modelProvider = _pageData.LoadModelProvider(Provider); - _hasEditPermission = _pageData.Permissions.Update is null || await Permissions.HasPermission(Auth.User, _pageData.Permissions.Update); - _hasDeletePermission = _pageData.Permissions.Delete is null || await Permissions.HasPermission(Auth.User, _pageData.Permissions.Delete); + _hasEditPermission = _pageData.Permissions.Update is null || await Permissions.HasPermission(Auth.AccessToken, _pageData.Permissions.Update); + _hasDeletePermission = _pageData.Permissions.Delete is null || await Permissions.HasPermission(Auth.AccessToken, _pageData.Permissions.Delete); await Reload(); } diff --git a/testing/HopFrame.Testing.Api/Controllers/TestController.cs b/testing/HopFrame.Testing.Api/Controllers/TestController.cs index fb39666..d097592 100644 --- a/testing/HopFrame.Testing.Api/Controllers/TestController.cs +++ b/testing/HopFrame.Testing.Api/Controllers/TestController.cs @@ -1,5 +1,7 @@ using HopFrame.Api.Logic; +using HopFrame.Api.Models; using HopFrame.Database.Models; +using HopFrame.Database.Repositories; using HopFrame.Security.Authorization; using HopFrame.Security.Claims; using HopFrame.Testing.Api.Models; @@ -10,11 +12,11 @@ namespace HopFrame.Testing.Api.Controllers; [ApiController] [Route("test")] -public class TestController(ITokenContext userContext, DatabaseContext context) : ControllerBase { +public class TestController(ITokenContext userContext, DatabaseContext context, ITokenRepository tokens, IPermissionRepository permissions) : ControllerBase { [HttpGet("permissions"), Authorized] - public ActionResult> Permissions() { - return new ActionResult>(userContext.User.Permissions); + public ActionResult> Permissions() { + return new ActionResult>(userContext.ContextualPermissions); } [HttpGet("generate")] @@ -50,5 +52,19 @@ public class TestController(ITokenContext userContext, DatabaseContext context) public async Task>> GetAddresses() { return LogicResult>.Ok(await context.Addresses.Include(e => e.Employee).ToListAsync()); } + + [HttpGet("token"), Authorized] + public async Task>> GetApiToken() { + var token = await tokens.CreateApiToken(userContext.User, DateTime.MaxValue); + await permissions.AddPermission(token, "hopframe.admin"); + await permissions.AddPermission(token, "hopframe.admin.users.read"); + return LogicResult>.Ok(token.TokenId.ToString()); + } + + [HttpDelete("token/{tokenId}")] + public async Task DeleteToken(string tokenId) { + var token = await tokens.GetToken(tokenId); + await tokens.DeleteToken(token); + } } \ No newline at end of file diff --git a/testing/HopFrame.Testing.Api/Program.cs b/testing/HopFrame.Testing.Api/Program.cs index b728eb3..948be0d 100644 --- a/testing/HopFrame.Testing.Api/Program.cs +++ b/testing/HopFrame.Testing.Api/Program.cs @@ -18,7 +18,7 @@ builder.Services.AddSwaggerGen(c => { c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Description = @"JWT Authorization header using the Bearer scheme. \r\n\r\n Enter 'Bearer' [space] and then your token in the text input below.", - Name = "Authorization", + Name = "Token", In = ParameterLocation.Header, Type = SecuritySchemeType.ApiKey, Scheme = "Bearer" From e47d4917df393ce69519142d7241bc45de2426d8 Mon Sep 17 00:00:00 2001 From: Leon Hoppe Date: Sat, 21 Dec 2024 17:13:18 +0100 Subject: [PATCH 3/4] Added api key documentation + fixed tests --- HopFrame.sln.DotSettings.user | 2 ++ docs/authentication.md | 27 +++++++++++++++++-- docs/models.md | 4 ++- docs/repositories.md | 4 +++ .../AuthenticationTests.cs | 2 +- .../HopFrame.Tests.Web/AuthMiddlewareTests.cs | 2 +- 6 files changed, 36 insertions(+), 5 deletions(-) diff --git a/HopFrame.sln.DotSettings.user b/HopFrame.sln.DotSettings.user index eab4e1d..0d4f6c6 100644 --- a/HopFrame.sln.DotSettings.user +++ b/HopFrame.sln.DotSettings.user @@ -1,4 +1,5 @@  + ForceIncluded ForceIncluded ForceIncluded ForceIncluded @@ -71,6 +72,7 @@ + \ No newline at end of file diff --git a/docs/authentication.md b/docs/authentication.md index 469ceee..f79c51e 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -23,8 +23,6 @@ by configuring your configuration to load these. > `builder.Configuration.AddEnvironmentVariables();` to your startup configuration before you add the > custom configurations / HopFrame services. -### Example - You can specify `Seconds`, `Minutes`, `Hours` and `Days` for either of the two token types. These get combined to a single time span. @@ -49,3 +47,28 @@ HOPFRAME__AUTHENTICATION__ACCESSTOKEN__MINUTES=30 HOPFRAME__AUTHENTICATION__REFRESHTOKEN__DAYS=10 HOPFRAME__AUTHENTICATION__REFRESHTOKEN__HOURS=5 ``` + +## API tokens + +API tokens are useful to use in automation environments that need to access an endpoint or page of your application. +The HopFrame supports this natively and no further configuration is required in order to use them. + +### Create an api token + +You can create an api token via the `ITokenRepository`: +```csharp +tokens.CreateApiToken(user, DateTime.MaxValue); +``` + +This creates a new api token that is valid until the provided DateTime has passed. Note that in the database and the token +model the `CreatedAt` property represents the expiration date on an api token. For security reasons the api token by default +has no permissions. This allows you to create tokens that are just permitted to perform a single action. Note that a token +associated to a user can also have more permissions than the user itself so make sure to properly secure the creation process. + +### Add permissions to an api token + +You can add permissions to an api token like you would to a normal user or group: + +```csharp +permissions.AddPermission(apiToken, "token.permission"); +``` diff --git a/docs/models.md b/docs/models.md index 39ecc99..7f61e86 100644 --- a/docs/models.md +++ b/docs/models.md @@ -35,16 +35,18 @@ public class Permission { public DateTime GrantedAt { get; set; } public virtual User User { get; set; } public virtual PermissionGroup Group { get; set; } + public virtual Token Token { get; set; } } ``` ## Token ```csharp -public class Token { +public class Token : IPermissionOwner { public int Type { get; set; } public Guid Content { get; set; } public DateTime CreatedAt { get; set; } public virtual User Owner { get; set; } + public virtual List Permissions { get; set; } } ``` diff --git a/docs/repositories.md b/docs/repositories.md index 25cb4ac..f72d876 100644 --- a/docs/repositories.md +++ b/docs/repositories.md @@ -71,5 +71,9 @@ public interface ITokenRepository { Task CreateToken(int type, User owner); Task DeleteUserTokens(User owner); + + Task DeleteToken(Token token); + + Task CreateApiToken(User owner, DateTime expirationDate); } ``` diff --git a/tests/HopFrame.Tests.Security/AuthenticationTests.cs b/tests/HopFrame.Tests.Security/AuthenticationTests.cs index 5a00df9..17e3d1d 100644 --- a/tests/HopFrame.Tests.Security/AuthenticationTests.cs +++ b/tests/HopFrame.Tests.Security/AuthenticationTests.cs @@ -43,7 +43,7 @@ public class AuthenticationTests { .ReturnsAsync(correctToken); perms - .Setup(x => x.GetFullPermissions(It.IsAny())) + .Setup(x => x.GetFullPermissions(It.IsAny())) .ReturnsAsync(new List()); var auth = new HopFrameAuthentication(options.Object, logger.Object, encoder.Object, clock.Object, tokens.Object, perms.Object, new OptionsWrapper(new HopFrameAuthenticationOptions())); diff --git a/tests/HopFrame.Tests.Web/AuthMiddlewareTests.cs b/tests/HopFrame.Tests.Web/AuthMiddlewareTests.cs index bada100..685e588 100644 --- a/tests/HopFrame.Tests.Web/AuthMiddlewareTests.cs +++ b/tests/HopFrame.Tests.Web/AuthMiddlewareTests.cs @@ -23,7 +23,7 @@ public class AuthMiddlewareTests { var perms = new Mock(); perms - .Setup(p => p.GetFullPermissions(It.Is(u => newToken.Owner.Id == u.Id))) + .Setup(p => p.GetFullPermissions(It.Is(u => newToken.Owner.Id == u.Owner.Id))) .ReturnsAsync(CreateDummyUser().Permissions.Select(p => p.PermissionName).ToList); return new AuthMiddleware(auth.Object, perms.Object); From c6aca4baf6426b8620d5d76a7840ad4db67052c6 Mon Sep 17 00:00:00 2001 From: Leon Hoppe Date: Sat, 21 Dec 2024 17:35:11 +0100 Subject: [PATCH 4/4] secured api tokens against permission breaches --- docs/authentication.md | 4 ++-- src/HopFrame.Database/Repositories/ITokenRepository.cs | 2 +- .../Repositories/Implementation/PermissionRepository.cs | 6 ++++++ .../Repositories/Implementation/TokenRepository.cs | 7 ++++++- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/authentication.md b/docs/authentication.md index f79c51e..c3489d0 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -62,8 +62,8 @@ tokens.CreateApiToken(user, DateTime.MaxValue); This creates a new api token that is valid until the provided DateTime has passed. Note that in the database and the token model the `CreatedAt` property represents the expiration date on an api token. For security reasons the api token by default -has no permissions. This allows you to create tokens that are just permitted to perform a single action. Note that a token -associated to a user can also have more permissions than the user itself so make sure to properly secure the creation process. +has no permissions. This allows you to create tokens that are just permitted to perform a single action. Note that an api token +can **never** have more permissions than the user associated with it. ### Add permissions to an api token diff --git a/src/HopFrame.Database/Repositories/ITokenRepository.cs b/src/HopFrame.Database/Repositories/ITokenRepository.cs index 2c1192c..9447994 100644 --- a/src/HopFrame.Database/Repositories/ITokenRepository.cs +++ b/src/HopFrame.Database/Repositories/ITokenRepository.cs @@ -5,7 +5,7 @@ namespace HopFrame.Database.Repositories; public interface ITokenRepository { Task GetToken(string content); Task CreateToken(int type, User owner); - Task DeleteUserTokens(User owner); + Task DeleteUserTokens(User owner, bool includeApiTokens = false); Task DeleteToken(Token token); Task CreateApiToken(User owner, DateTime expirationDate); } \ No newline at end of file diff --git a/src/HopFrame.Database/Repositories/Implementation/PermissionRepository.cs b/src/HopFrame.Database/Repositories/Implementation/PermissionRepository.cs index 6d55bc0..3156361 100644 --- a/src/HopFrame.Database/Repositories/Implementation/PermissionRepository.cs +++ b/src/HopFrame.Database/Repositories/Implementation/PermissionRepository.cs @@ -5,6 +5,10 @@ namespace HopFrame.Database.Repositories.Implementation; internal sealed class PermissionRepository(TDbContext context, IGroupRepository groupRepository) : IPermissionRepository where TDbContext : HopDbContextBase { public async Task HasPermission(IPermissionOwner owner, params string[] permissions) { + if (owner is Token { Type: Token.ApiTokenType } token) { + if (!await HasPermission(token.Owner, permissions)) return false; + } + var perms = (await GetFullPermissions(owner)).ToArray(); foreach (var permission in permissions) { @@ -27,6 +31,8 @@ internal sealed class PermissionRepository(TDbContext context, IGrou }else if (owner is Token token) { if (token.Type != Token.ApiTokenType) throw new ArgumentException("Only API tokens can have permissions!"); + if (!await HasPermission(token.Owner, permission)) + throw new ArgumentException("An api token cannot have more permissions than the owner has!"); entry.Token = token; } diff --git a/src/HopFrame.Database/Repositories/Implementation/TokenRepository.cs b/src/HopFrame.Database/Repositories/Implementation/TokenRepository.cs index b44dc43..29deaab 100644 --- a/src/HopFrame.Database/Repositories/Implementation/TokenRepository.cs +++ b/src/HopFrame.Database/Repositories/Implementation/TokenRepository.cs @@ -29,11 +29,16 @@ internal sealed class TokenRepository(TDbContext context) : ITokenRe return token; } - public async Task DeleteUserTokens(User owner) { + public async Task DeleteUserTokens(User owner, bool includeApiTokens = false) { var tokens = await context.Tokens .Include(t => t.Owner) .Where(t => t.Owner.Id == owner.Id) .ToListAsync(); + + if (!includeApiTokens) + tokens = tokens + .Where(t => t.Type != Token.ApiTokenType) + .ToList(); context.Tokens.RemoveRange(tokens); await context.SaveChangesAsync();