Added PermissionService and added docs to all methods with potential user contact

This commit is contained in:
2024-07-13 23:04:20 +02:00
parent ec7982471e
commit 54ec3b4f52
11 changed files with 164 additions and 26 deletions

View File

@@ -4,6 +4,13 @@ namespace HopFrame.Api;
public static class EncryptionManager {
/// <summary>
/// Encrypts the given string with the specified hash method
/// </summary>
/// <param name="input">The raw string that should be hashed</param>
/// <param name="salt">The "password" for the hash</param>
/// <param name="method">The preferred hash method</param>
/// <returns></returns>
public static string Hash(string input, byte[] salt, KeyDerivationPrf method = KeyDerivationPrf.HMACSHA256) {
return Convert.ToBase64String(KeyDerivation.Pbkdf2(
password: input,

View File

@@ -7,6 +7,11 @@ namespace HopFrame.Api.Extensions;
public static class ServiceCollectionExtensions {
/// <summary>
/// Adds all HopFrame endpoints and the HopFrame security layer to the WebApplication
/// </summary>
/// <param name="services">The service provider to add the services to</param>
/// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam>
public static void AddHopFrame<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase {
services.AddMvcCore().UseSpecificControllers(typeof(SecurityController<TDbContext>));
services.AddHopFrameAuthentication<TDbContext>();

View File

@@ -3,6 +3,9 @@ using Microsoft.EntityFrameworkCore;
namespace HopFrame.Database;
/// <summary>
/// This class includes the basic database structure in order for HopFrame to work
/// </summary>
public class HopDbContextBase : DbContext {
public HopDbContextBase() {}

View File

@@ -4,6 +4,12 @@ namespace HopFrame.Database.Models;
public static class ModelExtensions {
/// <summary>
/// Converts the database model to a friendly user model
/// </summary>
/// <param name="entry">the database model</param>
/// <param name="contextBase">the data source for the permissions and users</param>
/// <returns></returns>
public static User ToUserModel(this UserEntry entry, HopDbContextBase contextBase) {
var user = new User {
Id = Guid.Parse(entry.Id),

View File

@@ -12,29 +12,29 @@ using Microsoft.Extensions.Options;
namespace HopFrame.Security.Authentication;
public class HopFrameAuthentication<TDbContext> : AuthenticationHandler<AuthenticationSchemeOptions> where TDbContext : HopDbContextBase {
public class HopFrameAuthentication<TDbContext>(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
TDbContext context)
: AuthenticationHandler<AuthenticationSchemeOptions>(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<AuthenticationSchemeOptions> options, ILoggerFactory logger,
UrlEncoder encoder, ISystemClock clock, TDbContext context) : base(options, logger, encoder, clock) {
_context = context;
}
protected override async Task<AuthenticateResult> 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<Claim> {
@@ -42,7 +42,7 @@ public class HopFrameAuthentication<TDbContext> : AuthenticationHandler<Authenti
new(HopFrameClaimTypes.UserId, tokenEntry.UserId)
};
var permissions = await _context.Permissions
var permissions = await context.Permissions
.Where(perm => perm.UserId == tokenEntry.UserId)
.Select(perm => perm.PermissionText)
.ToListAsync();
@@ -51,7 +51,7 @@ public class HopFrameAuthentication<TDbContext> : AuthenticationHandler<Authenti
.Where(perm => 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();

View File

@@ -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 {
/// <summary>
/// Configures the WebApplication to use the authentication and authorization of the HopFrame API
/// </summary>
/// <param name="service">The service provider to add the services to</param>
/// <typeparam name="TDbContext">The database object that saves all entities that are important for the security api</typeparam>
/// <returns></returns>
public static AuthenticationBuilder AddHopFrameAuthentication<TDbContext>(this IServiceCollection service) where TDbContext : HopDbContextBase {
service.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
service.AddScoped<ITokenContext, TokenContextImplementor<TDbContext>>();
service.AddScoped<IPermissionService, PermissionService<TDbContext>>();
return service.AddAuthentication(HopFrameAuthentication<TDbContext>.SchemeName).AddScheme<AuthenticationSchemeOptions, HopFrameAuthentication<TDbContext>>(HopFrameAuthentication<TDbContext>.SchemeName, _ => {});
}

View File

@@ -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 };
/// <summary>
/// If this decorator is present, the endpoint is only accessible if the user provided a valid access token (is logged in)
/// </summary>
/// <param name="permissions">specifies the permissions the user needs to have in order to access this endpoint</param>
public AuthorizedAttribute(params string[] permissions) : base(typeof(AuthorizedFilter)) {
Arguments = [permissions];
}
}

View File

@@ -2,17 +2,6 @@ namespace HopFrame.Security.Authorization;
internal static class PermissionValidator {
/// <summary>
/// Checks for the user to have the specified permission<br/>
/// Permission system:<br/>
/// - "*" -> all rights<br/>
/// - "group.[name]" -> group member<br/>
/// - "[namespace].[name]" -> single permission<br/>
/// - "[namespace].*" -> all permissions in the namespace
/// </summary>
/// <param name="permission">The permission the user needs</param>
/// <param name="permissions">All the permissions the user has (includes group permissions)</param>
/// <returns></returns>
public static bool IncludesPermission(string permission, string[] permissions) {
if (permission == "*") return true;
if (permissions.Contains(permission)) return true;

View File

@@ -3,7 +3,19 @@ using HopFrame.Database.Models;
namespace HopFrame.Security.Claims;
public interface ITokenContext {
/// <summary>
/// This field specifies that a valid user is accessing the endpoint
/// </summary>
bool IsAuthenticated { get; }
/// <summary>
/// The user that is accessing the endpoint
/// </summary>
User User { get; }
/// <summary>
/// The access token the user provided
/// </summary>
Guid AccessToken { get; }
}

View File

@@ -0,0 +1,43 @@
namespace HopFrame.Security.Services;
public interface IPermissionService {
/// <summary>
/// Checks for the user to have the specified permission
/// Permission system:<br/>
/// - "*" -> all rights<br/>
/// - "group.[name]" -> group member<br/>
/// - "[namespace].[name]" -> single permission<br/>
/// - "[namespace].*" -> all permissions in the namespace
/// </summary>
/// <param name="permission">The permission the user needs</param>
/// <returns>rather the user has the permission or not</returns>
Task<bool> HasPermission(string permission);
/// <summary>
/// Checks if the user has all the specified permissions
/// </summary>
/// <param name="permissions">list of the permissions</param>
/// <returns>rather the user has all the permissions or not</returns>
Task<bool> HasPermissions(params string[] permissions);
/// <summary>
/// Checks if the user has any of the specified permissions
/// </summary>
/// <param name="permissions">list of the permissions</param>
/// <returns>rather the user has any permission or not</returns>
Task<bool> HasAnyPermission(params string[] permissions);
/// <summary>
/// Checks for the user to have the specified permission
/// Permission system:<br/>
/// - "*" -> all rights<br/>
/// - "group.[name]" -> group member<br/>
/// - "[namespace].[name]" -> single permission<br/>
/// - "[namespace].*" -> all permissions in the namespace
/// </summary>
/// <param name="permission">The permission the user needs</param>
/// <param name="user">The user who gets checked</param>
/// <returns>rather the user has the permission or not</returns>
Task<bool> HasPermission(string permission, Guid user);
}

View File

@@ -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>(TDbContext context, ITokenContext current) : IPermissionService where TDbContext : HopDbContextBase {
public async Task<bool> HasPermission(string permission) {
return await HasPermission(permission, current.User.Id);
}
public async Task<bool> 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<bool> 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<bool> HasPermission(string permission, Guid user) {
var permissions = await GetFullPermissions(user.ToString());
return PermissionValidator.IncludesPermission(permission, permissions);
}
private async Task<string[]> 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();
}
}