Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
179 views
in Technique[技术] by (71.8m points)

asp.net core - c# Using custom filter attribute on a generic controller

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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

You can stick to first option using AuthorizeClaimFilter/AuthorizeClaimAttribute with updated AuthorizeClaimAttribute

public class AuthorizeClaimAttribute : TypeFilterAttribute
{
    private string[] _claims;

    public string PrefixSeparator { get; set; } = ":";

    private object _prefix;
    public object Prefix
    {
        get => _prefix;
        set
        {
            _prefix = value;
            _claims = _claims.Select(a => $"{_prefix}{PrefixSeparator}{a}").ToArray();
            Arguments = new object[] { _claims }; ;
        }
    }

    public AuthorizeClaimAttribute(params string[] values) : base(typeof(AuthorizeClaimFilter))
    {
        _claims = values;
        Arguments = new object[] { _claims };
    }
}

And usage is

[HttpGet("{id}")]
[AuthorizeClaim("read", Prefix = 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)));

In this case case you will get claims prefixed with T fully qualified name. If you need only short name of the type you can modify AuthorizeClaimAttribute to accept Type prefix and prepend _prefix.Name to claims, or implement more sophisticated logic for this

public class AuthorizeClaimAttribute : TypeFilterAttribute
{
    private string[] _claims;

    public string PrefixSeparator { get; set; } = ":";

    private Type _prefix;
    public Type Prefix
    {
        get => _prefix;
        set
        {
            _prefix = value;
            _claims = _claims.Select(a => $"{_prefix.Name}{PrefixSeparator}{a}").ToArray();
            Arguments = new object[] { _claims }; ;
        }
    }

    public AuthorizeClaimAttribute(params string[] values) : base(typeof(AuthorizeClaimFilter))
    {
        _claims = values;
        Arguments = new object[] { _claims };
    }
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...