Created
August 26, 2022 14:17
-
-
Save OFark/2a49e0a74a094ea02089d9aeaa414d1d to your computer and use it in GitHub Desktop.
Adding a matrix style authorization to FastEndpoints
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Flags] | |
public enum AccessLevel : uint | |
{ | |
None = 0, | |
Allowed = 1 << 0, | |
List = 1 << 1, | |
Update = 1 << 2, | |
Create = 1 << 3, | |
Delete = 1 << 4, | |
Grant = 1 << 5, | |
CRUD = ~(-1 << 5), | |
All = ~(-1 << 6) | |
} | |
public static class AccessLevelParser | |
{ | |
public static AccessLevel Parse(string accessTo) | |
{ | |
return Enum.Parse<AccessLevel>(accessTo); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// A Factory-Style class to manage AccessTo and AccessLevel to make the string generation consistent. | |
public record AccessRight : IAccessWriteTo | |
{ | |
private readonly HashSet<AccessLevel> ignoreFlags = new() { AccessLevel.None, AccessLevel.CRUD, AccessLevel.All }; // The access levels are not required in the JWT | |
public AccessRight(AccessTo accessTo, AccessLevel accessLevel = AccessLevel.Allowed, bool deny = false) | |
{ | |
AccessTo = accessTo; | |
AccessLevel = accessLevel; | |
Deny = deny; | |
} | |
public AccessTo AccessTo { get; init; } | |
public AccessLevel AccessLevel { get; init; } | |
public bool Deny { get; init; } | |
public override int GetHashCode() => ToString()?.GetHashCode() ?? 0; | |
public override string ToString() => $"{AccessTo} {(int)AccessLevel}"; | |
public IEnumerable<string> ToPermissions() | |
{ | |
return Enum.GetValues<AccessLevel>() | |
.Where(a => !ignoreFlags.Contains(a) && AccessLevel.HasFlag(a)) | |
.Select(a => $"{AccessTo} {(int)a}"); | |
} | |
public bool Has(AccessTo accessTo, AccessLevel accessLevel) => accessTo == AccessTo && (AccessLevel & accessLevel) == accessLevel; | |
public static AccessRight Parse(string accessRightString, bool deny = false) | |
{ | |
var accessSplit = accessRightString.Split(" "); | |
if(accessSplit.Length != 2) | |
{ | |
throw new ArgumentException($"{accessRightString} is not a valid permission claim"); | |
} | |
var accessTo = AccessToParser.Parse(accessSplit[0]); | |
var accessLevel = (AccessLevel)int.Parse(accessSplit[1]); | |
return new AccessRight(accessTo, accessLevel, deny); | |
} | |
public static AccessRight Allowed(AccessTo accessTo) => new(accessTo, AccessLevel.Allowed); | |
public static IAccessWriteTo With(AccessTo accessTo) => new AccessRight(accessTo); | |
public AccessRight With(AccessLevel accessLevel) => this with { AccessLevel = accessLevel }; | |
} | |
public interface IAccessWriteTo | |
{ | |
AccessRight With(AccessLevel accessLevel); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Define what you want access to here and the maximum possible access level required. | |
public enum AccessTo | |
{ | |
[EnumData(maxAccessLevel: AccessLevel.Grant)] | |
Logins, | |
[EnumData(maxAccessLevel: AccessLevel.Grant)] | |
Orders, | |
[EnumData(maxAccessLevel: AccessLevel.Grant)] | |
Accounts, | |
[EnumData(maxAccessLevel: AccessLevel.Grant)] | |
Roles, | |
[EnumData(maxAccessLevel: AccessLevel.Allowed)] | |
ChargeToAccount, | |
[EnumData(maxAccessLevel: AccessLevel.Allowed)] | |
Impersonate | |
} | |
public static class AccessToParser | |
{ | |
public static AccessTo Parse(string accessTo) | |
{ | |
return Enum.Parse<AccessTo>(accessTo); | |
} | |
public static AccessTo Parse(Guid accessToId) | |
{ | |
return Enum.GetValues<AccessTo>().FirstOrDefault(x => x.GetEnumGuid() == accessToId); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This file isn't necessary it just provides some handy Methods in a Endpoint Subclass | |
public abstract partial class APIEndpoint<TRequest, TResponse, TMapper> : APIEndpoint<TRequest, TResponse>, IHasMapper<TMapper>, IEndpoint where TRequest : notnull, new() where TResponse : notnull where TMapper : notnull, IMapper, new() | |
{ | |
/// <summary> | |
/// the entity mapper for the endpoint | |
/// <para>HINT: entity mappers are singletons for performance reasons. do not maintain state in the mappers.</para> | |
/// </summary> | |
public static TMapper Map { get; } = new(); | |
} | |
public abstract partial class APIEndpoint<TRequest, TResponse> : Endpoint<TRequest, TResponse>, IEndpoint where TRequest : notnull, new() where TResponse : notnull | |
{ | |
protected void Has(params AccessRight[] permissions) => Policies(permissions.Select(p => p.ToString()).ToArray()); | |
protected void Is(params InRole[] roles) => Roles(roles.Select(r => r.ToString()).ToArray()); | |
} | |
public abstract class APIEndpoint<TRequest> : APIEndpoint<TRequest, object>, IEndpoint where TRequest : notnull, new() { }; | |
public abstract class APIEndpointWithoutRequest : APIEndpointWithoutRequest<object> { } | |
public abstract class APIEndpointWithoutRequest<TResponse> : EndpointWithoutRequest<TResponse> where TResponse : notnull | |
{ | |
protected void Has(params AccessRight[] permissions) => Policies(permissions.Select(p => p.ToString()).ToArray()); | |
protected void Is(params InRole[] roles) => Roles(roles.Select(r => r.ToString()).ToArray()); | |
} | |
public abstract class APIEndpointWithoutRequest<TResponse, TMapper> : APIEndpointWithoutRequest<TResponse>, IHasMapper<TMapper> where TResponse : notnull where TMapper : notnull, IResponseMapper, new() | |
{ | |
/// <summary> | |
/// the entity mapper for the endpoint | |
/// <para>HINT: entity mappers are singletons for performance reasons. do not maintain state in the mappers.</para> | |
/// </summary> | |
public static TMapper Map { get; } = new(); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static class ClaimsPrincipalExtensions | |
{ | |
private const string permissionsClaimType = "permissions"; | |
/// <summary> | |
/// Tests the ClaimsPricipal for access to a permission as a certain access level | |
/// </summary> | |
/// <param name="user">The ClaimsPricipal</param> | |
/// <param name="accessTo">The Permission to test access for</param> | |
/// <param name="accessLevel">The level of access to be tested</param> | |
/// <param name="errorResponse">If Access is denied this is a ConditionalResponse returnable ErrorResponse</param> | |
/// <returns>True if the ClaimsPrincipal has the access level to the permission</returns> | |
public static bool Has(this ClaimsPrincipal user, AccessTo accessTo, AccessLevel accessLevel, [NotNullWhen(false)] out ErrorResponse? errorResponse) | |
{ | |
var accessRight = new AccessRight(accessTo, accessLevel); | |
var allowed = user.FindAll(permissionsClaimType).Any(c => AccessRight.Parse(c.Value).Has(accessTo, accessLevel)); | |
errorResponse = allowed ? null : new ErrorResponse($"{accessLevel} access to {accessTo} is denied", HttpStatusCode.Forbidden); | |
return allowed; | |
} | |
/// <summary> | |
/// Tests the ClaimsPricipal for access to a permission as a certain access level | |
/// </summary> | |
/// <param name="user">The ClaimsPricipal</param> | |
/// <param name="accessTo">The Permission to test access for</param> | |
/// <param name="accessLevel">Optional. The level of access to be tested (Default: Allowed)</param> | |
/// <returns>True if the ClaimsPrincipal has the access level to the permission</returns> | |
public static bool Has(this ClaimsPrincipal user, AccessTo accessTo, AccessLevel accessLevel = AccessLevel.Allowed) => Has(user, accessTo, accessLevel, out _); | |
/// <summary> | |
/// Tests the ClaimsPricipal for access to a permission at allowed level (minimum) | |
/// </summary> | |
/// <param name="user">The ClaimsPricipal</param> | |
/// <param name="accessTo">The Permission to test access for</param> | |
/// <param name="errorResponse"></param> | |
/// <returns>True if the ClaimsPrincipal has allowed level access to the permission</returns> | |
public static bool Has(this ClaimsPrincipal user, AccessTo accessTo, [NotNullWhen(false)] out ErrorResponse? errorResponse) => Has(user, accessTo, AccessLevel.Allowed, out errorResponse); | |
public static bool Is(this ClaimsPrincipal user, InRole role) => user.IsInRole(role.ToString()); | |
public static bool IsCustomer(this ClaimsPrincipal user) => user.IsInRole(InRole.Customer.ToString()); | |
public static bool IsStaff(this ClaimsPrincipal user) => user.IsInRole(InRole.MemberOfStaff.ToString()); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Socmething to hold data about the Enum Value | |
[AttributeUsage(AttributeTargets.Field)] | |
internal class EnumData : Attribute | |
{ | |
public AccessLevel MaxAccessLevel; | |
public EnumData(AccessLevel maxAccessLevel = AccessLevel.Allowed) | |
{ | |
MaxAccessLevel = maxAccessLevel; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//Adds some extensions to the AccessTo Enum for get the MaxAccessLevel | |
public static class EnumExtensions | |
{ | |
public static AccessLevel GetMaxAccessLevel(this AccessTo e) | |
{ | |
var memInfo = e.GetType().GetMember(e.ToString()); | |
if (memInfo != null && memInfo.Length > 0) | |
{ | |
var attrs = memInfo[0].GetCustomAttributes(typeof(EnumData), false); | |
if (attrs != null && attrs.Length > 0) | |
return ((EnumData)attrs[0]).MaxAccessLevel; | |
} | |
throw new ArgumentException("Enum " + e.ToString() + " has no EnumData defined!"); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public enum InRole | |
{ | |
SystemsAdministrator, | |
MemberOfStaff, | |
Customer | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// add this in the right place, this registers the policies | |
builder.Services.AddAuthorization(options => | |
{ | |
foreach(var policy in AuthorizationPolicies.AllPolicies) | |
{ | |
options.AddPolicy(policy.Key, policy.Value); | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment