I have created myself a new AuthorizeClaim filter and attribute which works on my API. It looks like this:
public class AuthorizeClaimFilter: IAuthorizationFilter
{
private readonly string[] _claims;
public AuthorizeClaimFilter(string[] claims) => _claims = claims;
public void OnAuthorization(AuthorizationFilterContext context)
{
if (_claims.Any())
{
var user = context.HttpContext.User;
if (user.IsInRole(SituIdentityConstants.Roles.Administrator)) return;
var hasClaim = user.Claims.Any(c =>
c.Type == JwtClaimTypes.Role &&
_claims.Any(x => x.Equals(c.Value)));
if (hasClaim) return;
}
context.Result = new ForbidResult();
}
}
public class AuthorizeClaimAttribute: TypeFilterAttribute
{
public AuthorizeClaimAttribute(params string[] values) : base(typeof(AuthorizeClaimFilter))
{
Arguments = new object[] {values};
}
}
This works fine in our API, but in my infinite wisdom, a while ago (about a year ago) I created some generic controllers:
[ApiController]
public class GenericController<T> : GenericController<T, int> where T : BaseModel, IKey<int>
{
public GenericController(IMediator mediator) : base(mediator)
{
}
}
[ApiController]
public class GenericController<T, TKey> : ControllerBase where T: BaseModel, IKey<TKey>
{
public readonly IMediator Mediator;
public GenericController(IMediator mediator)
{
Mediator = mediator;
}
/// <summary>
/// Gets an entity by id
/// </summary>
/// <param name="id">The id of the entity</param>
/// <returns>The entity</returns>
[HttpGet("{id}")]
[ApiConventionMethod(typeof(AttemptApiConventions), nameof(AttemptApiConventions.AttemptGet))]
public virtual async Task<ActionResult<T>> GetAsync(TKey id) =>
Ok(await Mediator.Send(new GenericGet<T, TKey>(id)));
/// <summary>
/// Creates a new entity
/// </summary>
/// <param name="model">The entity to create</param>
/// <returns>The created entity</returns>
[HttpPost]
[ApiConventionMethod(typeof(AttemptApiConventions), nameof(AttemptApiConventions.AttemptPost))]
public virtual async Task<ActionResult<T>> CreateAsync(T model) =>
Ok(await Mediator.Send(new GenericCreate<T, TKey>(model, User)))
.WithMessage<T>(string.Format(Resources.EntityCreated, typeof(T).Name));
/// <summary>
/// Updates a new entity
/// </summary>
/// <param name="model">The entity to update</param>
/// <returns>The created entity</returns>
[HttpPut]
[ApiConventionMethod(typeof(AttemptApiConventions), nameof(AttemptApiConventions.AttemptPost))]
public virtual async Task<ActionResult<T>> UpdateAsync(T model) =>
Ok(await Mediator.Send(new GenericUpdate<T, TKey>(model, User)))
.WithMessage<T>(string.Format(Resources.EntityUpdated, typeof(T).Name));
/// <summary>
/// Deletes an entity
/// </summary>
/// <param name="id">The id of the entity</param>
/// <returns></returns>
[HttpDelete("{id}")]
[ApiConventionMethod(typeof(AttemptApiConventions), nameof(AttemptApiConventions.AttemptPost))]
public virtual async Task<ActionResult<bool>> DeleteAsync(TKey id) =>
Ok(await Mediator.Send(new GenericDelete<T, TKey>(id)))
.WithMessage<bool>(string.Format(Resources.EntityDeleted, typeof(T).Name));
}
These have been working absolutely fine and satisfied our needs, but now I have come to use different claims to access different endpoints (hence the new attribute filter). I now need to somehow pass the claims to the generic controllers.
I tried to do something like this:
[HttpGet("{id}")]
[AuthorizeClaim($"{typeof(T)}:read")]
[ApiConventionMethod(typeof(AttemptApiConventions), nameof(AttemptApiConventions.AttemptGet))]
public virtual async Task<ActionResult<T>> GetAsync(TKey id) =>
Ok(await Mediator.Send(new GenericGet<T, TKey>(id)));
But I get an error stating:
An attribute argument must be a constant expression, typeof expression or array creation expression of an attribute parameter type
which makes sense, so I tried to think of another solution.
I came up with the idea of registering a class that holds a list of my claims:
public class RequiredClaimHandler : IRequiredClaimHandler
{
public readonly List<RequiredClaim> Claims;
public RequiredClaimHandler(List<RequiredClaim> claims) => Claims = claims;
public string[] Get(HttpMethod action, Type type)
{
var claim = Claims?.SingleOrDefault(m => m.Action == action && m.Type == type);
return claim != null && claim.UseDefault ? GetDefault(claim) : claim?.Claims;
}
private static string[] GetDefault(RequiredClaim claim)
{
var action = claim.Action == HttpMethod.Get ? "read" : "write";
return new[] {$"{claim.Type.Name.ToLower()}:{action}"};
}
}
The idea was to then register it as a singleton and create a factory method like this:
public static class RequiredClaimHandlerFactory
{
public static IRequiredClaimHandler Create(List<RequiredClaim> claims) =>
new RequiredClaimHandler(claims);
}
I wrote the unit tests and then created a new attribute filter:
public class AuthorizeRequiredClaimFilter : IAuthorizationFilter
{
private readonly IRequiredClaimHandler _handler;
private readonly Type _type;
private readonly HttpMethod _method;
public AuthorizeRequiredClaimFilter(IRequiredClaimHandler handler, Type type, HttpMethod method)
{
_handler = handler;
_type = type;
_method = method;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
if (_handler.Get(_method, _type) != null) return;
context.Result = new ForbidResult();
}
}
public class AuthorizeRequiredClaimAttribute: TypeFilterAttribute
{
public AuthorizeRequiredClaimAttribute(HttpMethod method, Type type) : base(typeof(AuthorizeRequiredClaimFilter))
{
Arguments = new object[] {method, type};
}
}
Then I updated my generic method:
[HttpGet("{id}")]
[AuthorizeRequiredClaim(HttpMethod.Get, typeof(T))]
[ApiConventionMethod(typeof(AttemptApiConventions), nameof(AttemptApiConventions.AttemptGet))]
public virtual async Task<ActionResult<T>> GetAsync(TKey id) =>
Ok(await Mediator.Send(new GenericGet<T, TKey>(id)));
But of course, I am back where I started....
These parameters are not constants.
Does anyone know how I can get around this? I would prefer not to abandon my generic controllers as they are useful.
Any help would be appreciated.
question from:
https://stackoverflow.com/questions/65905123/c-sharp-using-custom-filter-attribute-on-a-generic-controller