Reputation: 62213
I have a MVC project where we implemented security using a custom attribute that derives from System.Web.Mvc.AuthorizeAttribute
. This is placed on methods that we want to secure with custom authorization on our MVC Controllers that derive from System.Web.Mvc.Controller
. We also use Autofac for DI (IoC) to inject an autohrization service into our custom security filter.
We now are also adding Web API controllers to our project, that derive from System.Web.Http.ApiController
. The I am running into is that it seems you have to register each filter instance, or the entire controller, with Autofac DI using their fluent api when you want to include DI in your filters when using Web API. See the autofac documentation for more details.
For MVC controllers we only have to add the attribute, no additional registration necessary. Now for ApiControllers we need to register that attribute (filter) instance using a fluent approach, it is not required to apply this attribute to the method/class itself. I would rather keep everything consistent, not to mention easier to maintain, by applying these filters only as attributes and not using the fluent approach. This is a cleaner approach IMHO especially in a larger solution where there could be more than 100 security implementations.
I have been trying to create a single method which would register all instances of applied filters on the Web API conttrollers but it seems like the Autofac implementation does not lend itself to this. Can anyone either suggest how to get around this limitation or an alternate way to register the filters once at run time instead of having to write duplicate code for each filter instance?
ModuleAccessAuthorizationAttribute
- this is the custom authorization attribute that implements Autofac.Integration.WebApi.IAutofacAuthorizationFilter
MyController
- this is a controller that implements System.Web.Http.ApiController
This works
builder.Register(c => new ModuleAccessAuthorizationAttribute(c.Resolve<IAuthenticationFactory>())) // the constructor actually takes much more information which varies each time it is applied but that is out of the scope of this question
.AsWebApiAuthorizationFilterFor<MyController>(c => c.Get()) // Get() is one of the methods that has needs to have the filter applied to it
.InstancePerRequest();
I would like to change it to this
public class MyController : ApiController {
[ModuleAccessAuthorizationAttribute]
public IHttpActionResult Get()
}
public static void RegisterWebApiSecurityFilter(ContainerBuilder builder)
{
var securedMembers = typeof(DependencyInjectionConfig).Assembly.ExportedTypes
.Where(x => !x.IsAbstract // not abstract
&& x.Name.EndsWith("Controller", StringComparison.Ordinal) // name ends in controller
&& !x.GetCustomAttributes<ObsoleteAttribute>().Any(y => y.IsError) // not marked as obsolete
&& (x.IsSubclassOf(typeof(System.Web.Http.ApiController)))) // implements ApiController
.SelectMany(x => x.GetMembers(BindingFlags.Public | BindingFlags.Instance)) // public instance members
.Where(x => x.GetCustomAttributes<ModuleAccessAuthorizationAttribute>().Any()) // marked with my custom attribute
.Select(x => x)
.ToList();
foreach (var securedMember in securedMembers) // iterate over collection
{
var attribute = securedMember.GetCustomAttribute<ModuleAccessAuthorizationAttribute>(); // get the attribute instance
var declaringClass = securedMember.DeclaringType; // get the declaring (class) type
builder.Register(c => ModuleAccessAuthorizationAttribute(c.Resolve<IAuthenticationFactory>()))
.AsWebApiAuthorizationFilterFor() // this is where I need help, it seems this is generic only and I cannot explicitly pass in my reflected type declaringClass
.InstancePerRequest();
}
}
Upvotes: 2
Views: 1042
Reputation: 62213
Thank you Cyril Durand
for your excellent response. I do not wish to fork the repository because I do not want to maintain additional code. I have chosen to not use Autofac as the filter provider and instead rely on the default Web Api implementation for getting the filters and then using the dependency resolver in the code of the filter to get my dependency instances. Although this goes against the design principles behind using DI / IoC, in this specific case I personally find the benefit to outweigh the costs of having to fork the Autofac RegistrationExtensions
and maintain that.
Here is the code in case anyone is interested although it is fairly self explanatory I think on how to accomplish this.
public sealed class ModuleAccessAuthorizationAttribute : System.Web.Http.Filters.IAuthorizationFilter {
public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation) {
var resolver = actionContext.Request.GetDependencyScope();
var authenticationFactory = resolver.GetService<IAuthenticationFactory>();
// rest of implementation executing an authorization check
}
}
Upvotes: 1
Reputation: 16192
Unfortunately there no overload of AsWebApiAuthorizationFilterFor
have a controllerType parameter.
If you look at the source code of AsWebApiAuthorizationFilterFor
method you can see that the implementation is the following :
public static IRegistrationBuilder<object, IConcreteActivatorData, SingleRegistrationStyle>
AsWebApiAuthorizationFilterFor<TController>(this IRegistrationBuilder<object, IConcreteActivatorData, SingleRegistrationStyle> registration)
where TController : IHttpController
{
return AsFilterFor<IAutofacAuthorizationFilter, TController>(registration, AutofacWebApiFilterProvider.AuthorizationFilterMetadataKey);
}
And AsFilterFor
is :
static IRegistrationBuilder<object, IConcreteActivatorData, SingleRegistrationStyle>
AsFilterFor<TFilter, TController>(IRegistrationBuilder<object, IConcreteActivatorData, SingleRegistrationStyle> registration, string metadataKey)
where TController : IHttpController
{
if (registration == null) throw new ArgumentNullException("registration");
var limitType = registration.ActivatorData.Activator.LimitType;
if (!limitType.IsAssignableTo<TFilter>())
{
var message = string.Format(CultureInfo.CurrentCulture, RegistrationExtensionsResources.MustBeAssignableToFilterType,
limitType.FullName, typeof(TFilter).FullName);
throw new ArgumentException(message, "registration");
}
var metadata = new FilterMetadata
{
ControllerType = typeof(TController),
FilterScope = FilterScope.Controller,
MethodInfo = null
};
return registration.As<TFilter>().WithMetadata(metadataKey, metadata);
}
With these information you can easily fork the RegistrationExtensions
class and add new overload that will match your needs.
ie :
public static class MyRegistrationExtensions
{
public static IRegistrationBuilder<object, IConcreteActivatorData, SingleRegistrationStyle>
AsWebApiAuthorizationFilterFor(this IRegistrationBuilder<object, IConcreteActivatorData, SingleRegistrationStyle> registration, Type controllerType)
{
return AsFilterFor<IAutofacAuthorizationFilter>(registration, AutofacWebApiFilterProvider.AuthorizationFilterMetadataKey, controllerType);
}
static IRegistrationBuilder<object, IConcreteActivatorData, SingleRegistrationStyle>
AsFilterFor<TFilter>(IRegistrationBuilder<object, IConcreteActivatorData, SingleRegistrationStyle> registration, string metadataKey, Type controllerType)
{
if (registration == null) throw new ArgumentNullException("registration");
if (controllerType == null) throw new ArgumentNullException("controllerType");
if (!controllerType.IsAssignableTo<IHttpController>()) throw new ArgumentNullException("controllerType");
var limitType = registration.ActivatorData.Activator.LimitType;
if (!limitType.IsAssignableTo<TFilter>())
{
var message = string.Format(CultureInfo.CurrentCulture, RegistrationExtensionsResources.MustBeAssignableToFilterType,
limitType.FullName, typeof(TFilter).FullName);
throw new ArgumentException(message, "registration");
}
var metadata = new FilterMetadata
{
ControllerType = controllerType,
FilterScope = FilterScope.Controller,
MethodInfo = null
};
return registration.As<TFilter>().WithMetadata(metadataKey, metadata);
}
}
Upvotes: 1