diff --git a/HopFrame.Api/EncryptionManager.cs b/HopFrame.Api/EncryptionManager.cs
index 8b29a57..592ad5d 100644
--- a/HopFrame.Api/EncryptionManager.cs
+++ b/HopFrame.Api/EncryptionManager.cs
@@ -4,6 +4,13 @@ namespace HopFrame.Api;
public static class EncryptionManager {
+ ///
+ /// Encrypts the given string with the specified hash method
+ ///
+ /// The raw string that should be hashed
+ /// The "password" for the hash
+ /// The preferred hash method
+ ///
public static string Hash(string input, byte[] salt, KeyDerivationPrf method = KeyDerivationPrf.HMACSHA256) {
return Convert.ToBase64String(KeyDerivation.Pbkdf2(
password: input,
diff --git a/HopFrame.Api/Extensions/ServiceCollectionExtensions.cs b/HopFrame.Api/Extensions/ServiceCollectionExtensions.cs
index 6d0e3d2..568580e 100644
--- a/HopFrame.Api/Extensions/ServiceCollectionExtensions.cs
+++ b/HopFrame.Api/Extensions/ServiceCollectionExtensions.cs
@@ -7,6 +7,11 @@ namespace HopFrame.Api.Extensions;
public static class ServiceCollectionExtensions {
+ ///
+ /// Adds all HopFrame endpoints and the HopFrame security layer to the WebApplication
+ ///
+ /// The service provider to add the services to
+ /// The data source for all HopFrame entities
public static void AddHopFrame(this IServiceCollection services) where TDbContext : HopDbContextBase {
services.AddMvcCore().UseSpecificControllers(typeof(SecurityController));
services.AddHopFrameAuthentication();
diff --git a/HopFrame.Database/HopDbContextBase.cs b/HopFrame.Database/HopDbContextBase.cs
index 6dd41be..944ac54 100644
--- a/HopFrame.Database/HopDbContextBase.cs
+++ b/HopFrame.Database/HopDbContextBase.cs
@@ -3,6 +3,9 @@ using Microsoft.EntityFrameworkCore;
namespace HopFrame.Database;
+///
+/// This class includes the basic database structure in order for HopFrame to work
+///
public class HopDbContextBase : DbContext {
public HopDbContextBase() {}
diff --git a/HopFrame.Database/Models/ModelExtensions.cs b/HopFrame.Database/Models/ModelExtensions.cs
index b0cc7a6..e632367 100644
--- a/HopFrame.Database/Models/ModelExtensions.cs
+++ b/HopFrame.Database/Models/ModelExtensions.cs
@@ -4,6 +4,12 @@ namespace HopFrame.Database.Models;
public static class ModelExtensions {
+ ///
+ /// Converts the database model to a friendly user model
+ ///
+ /// the database model
+ /// the data source for the permissions and users
+ ///
public static User ToUserModel(this UserEntry entry, HopDbContextBase contextBase) {
var user = new User {
Id = Guid.Parse(entry.Id),
diff --git a/HopFrame.Security/Authentication/HopFrameAuthentication.cs b/HopFrame.Security/Authentication/HopFrameAuthentication.cs
index c4f0bfb..e0244ac 100644
--- a/HopFrame.Security/Authentication/HopFrameAuthentication.cs
+++ b/HopFrame.Security/Authentication/HopFrameAuthentication.cs
@@ -12,29 +12,29 @@ using Microsoft.Extensions.Options;
namespace HopFrame.Security.Authentication;
-public class HopFrameAuthentication : AuthenticationHandler where TDbContext : HopDbContextBase {
+public class HopFrameAuthentication(
+ IOptionsMonitor options,
+ ILoggerFactory logger,
+ UrlEncoder encoder,
+ ISystemClock clock,
+ TDbContext context)
+ : AuthenticationHandler(options, logger, encoder, clock)
+ where TDbContext : HopDbContextBase {
public const string SchemeName = "HopCore.Authentication";
public static readonly TimeSpan AccessTokenTime = new(0, 0, 5, 0);
public static readonly TimeSpan RefreshTokenTime = new(30, 0, 0, 0);
- private readonly TDbContext _context;
-
- public HopFrameAuthentication(IOptionsMonitor options, ILoggerFactory logger,
- UrlEncoder encoder, ISystemClock clock, TDbContext context) : base(options, logger, encoder, clock) {
- _context = context;
- }
-
protected override async Task HandleAuthenticateAsync() {
var accessToken = Request.Headers["Authorization"].ToString();
if (string.IsNullOrEmpty(accessToken)) return AuthenticateResult.Fail("No Access Token provided");
- var tokenEntry = await _context.Tokens.SingleOrDefaultAsync(token => token.Token == accessToken);
+ var tokenEntry = await context.Tokens.SingleOrDefaultAsync(token => token.Token == accessToken);
if (tokenEntry is null) return AuthenticateResult.Fail("The provided Access Token does not exist");
if (tokenEntry.CreatedAt + AccessTokenTime < DateTime.Now) return AuthenticateResult.Fail("The provided Access Token is expired");
- if (!(await _context.Users.AnyAsync(user => user.Id == tokenEntry.UserId)))
+ if (!(await context.Users.AnyAsync(user => user.Id == tokenEntry.UserId)))
return AuthenticateResult.Fail("The provided Access Token does not match any user");
var claims = new List {
@@ -42,7 +42,7 @@ public class HopFrameAuthentication : AuthenticationHandler perm.UserId == tokenEntry.UserId)
.Select(perm => perm.PermissionText)
.ToListAsync();
@@ -51,7 +51,7 @@ public class HopFrameAuthentication : AuthenticationHandler perm.StartsWith("group."))
.ToList();
- var groupPerms = await _context.Permissions
+ var groupPerms = await context.Permissions
.Where(perm => groups.Contains(perm.UserId))
.Select(perm => perm.PermissionText)
.ToListAsync();
diff --git a/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs b/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs
index 4b857d4..fed708c 100644
--- a/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs
+++ b/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs
@@ -1,5 +1,6 @@
using HopFrame.Database;
using HopFrame.Security.Claims;
+using HopFrame.Security.Services;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
@@ -9,9 +10,16 @@ namespace HopFrame.Security.Authentication;
public static class HopFrameAuthenticationExtensions {
+ ///
+ /// Configures the WebApplication to use the authentication and authorization of the HopFrame API
+ ///
+ /// The service provider to add the services to
+ /// The database object that saves all entities that are important for the security api
+ ///
public static AuthenticationBuilder AddHopFrameAuthentication(this IServiceCollection service) where TDbContext : HopDbContextBase {
service.TryAddSingleton();
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 0180b52..ad81973 100644
--- a/HopFrame.Security/Authorization/AuthorizedAttribute.cs
+++ b/HopFrame.Security/Authorization/AuthorizedAttribute.cs
@@ -3,7 +3,12 @@ using Microsoft.AspNetCore.Mvc;
namespace HopFrame.Security.Authorization;
public class AuthorizedAttribute : TypeFilterAttribute {
- public AuthorizedAttribute(params string[] permission) : base(typeof(AuthorizedFilter)) {
- Arguments = new object[] { permission };
+
+ ///
+ /// If this decorator is present, the endpoint is only accessible if the user provided a valid access token (is logged in)
+ ///
+ /// specifies the permissions the user needs to have in order to access this endpoint
+ public AuthorizedAttribute(params string[] permissions) : base(typeof(AuthorizedFilter)) {
+ Arguments = [permissions];
}
}
\ No newline at end of file
diff --git a/HopFrame.Security/Authorization/PermissionValidator.cs b/HopFrame.Security/Authorization/PermissionValidator.cs
index 70fd695..75e2869 100644
--- a/HopFrame.Security/Authorization/PermissionValidator.cs
+++ b/HopFrame.Security/Authorization/PermissionValidator.cs
@@ -1,18 +1,7 @@
namespace HopFrame.Security.Authorization;
internal static class PermissionValidator {
-
- ///
- /// 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
- /// All the permissions the user has (includes group permissions)
- ///
+
public static bool IncludesPermission(string permission, string[] permissions) {
if (permission == "*") return true;
if (permissions.Contains(permission)) return true;
diff --git a/HopFrame.Security/Claims/ITokenContext.cs b/HopFrame.Security/Claims/ITokenContext.cs
index 4a28c12..6b6e624 100644
--- a/HopFrame.Security/Claims/ITokenContext.cs
+++ b/HopFrame.Security/Claims/ITokenContext.cs
@@ -3,7 +3,19 @@ using HopFrame.Database.Models;
namespace HopFrame.Security.Claims;
public interface ITokenContext {
+
+ ///
+ /// This field specifies that a valid user is accessing the endpoint
+ ///
bool IsAuthenticated { get; }
+
+ ///
+ /// The user that is accessing the endpoint
+ ///
User User { get; }
+
+ ///
+ /// The access token the user provided
+ ///
Guid AccessToken { get; }
}
\ No newline at end of file
diff --git a/HopFrame.Security/Services/IPermissionService.cs b/HopFrame.Security/Services/IPermissionService.cs
new file mode 100644
index 0000000..646ba22
--- /dev/null
+++ b/HopFrame.Security/Services/IPermissionService.cs
@@ -0,0 +1,43 @@
+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);
+
+ ///
+ /// 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
+ /// The user who gets checked
+ /// rather the user has the permission or not
+ Task HasPermission(string permission, Guid user);
+}
\ No newline at end of file
diff --git a/HopFrame.Security/Services/PermissionService.cs b/HopFrame.Security/Services/PermissionService.cs
new file mode 100644
index 0000000..daf4f81
--- /dev/null
+++ b/HopFrame.Security/Services/PermissionService.cs
@@ -0,0 +1,60 @@
+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