I'm having trouble specifying two separate Authorization attributes on a class method: the user is to be allowed access if either of the two attributes are true.
The Athorization class looks like this:
[AttributeUsage(AttributeTargets.All, AllowMultiple = true)]
public class AuthAttribute : AuthorizeAttribute {
. . .
and the action:
[Auth(Roles = AuthRole.SuperAdministrator)]
[Auth(Roles = AuthRole.Administrator, Module = ModuleID.SomeModule)]
public ActionResult Index() {
return View(GetIndexViewModel());
}
Is there a way to solve this or do I need to rethink my approach?
This is to be run in MVC2.
There is a better way to do this in later versions of asp.net you can do both OR and AND on roles. This is done through convention, listing multiple roles in a single Authorize will perform an OR where adding Multiple Authorize Attributes will perform AND.
OR example
[Authorize(Roles = "PowerUser,ControlPanelUser")]
AND Example
[Authorize(Roles = "PowerUser")]
[Authorize(Roles = "ControlPanelUser")]
You can find more info on this at the following link
https://learn.microsoft.com/en-us/aspnet/core/security/authorization/roles
Multiple AuthorizeAttribute instances are processed by MVC as if they were joined with AND. If you want an OR behaviour you will need to implement your own logic for checks. Preferably implement AuthAttribute to take multiple roles and perform an own check with OR logic.
Another solution is to use standard AuthorizeAttribute and implement custom IPrincipal that will implement bool IsInRole(string role) method to provide 'OR' behaviour.
An example is here:
https://stackoverflow.com/a/10754108/449906
I've been using this solution in production environment for awhile now, using .NET Core 3.0. I wanted the OR behavior between a custom attribute and the native AuthorizeAttribute. To do so, I implemented the IAuthorizationEvaluator interface, which gets called as soon as all authorizers evaluate theirs results.
/// <summary>
/// Responsible for evaluating if authorization was successful or not, after execution of
/// authorization handler pipelines.
/// This class was implemented because MVC default behavior is to apply an AND behavior
/// with the result of each authorization handler. But to allow our API to have multiple
/// authorization handlers, in which the final authorization result is if ANY handlers return
/// true, the class <cref name="IAuthorizationEvaluator" /> had to be extended to add this
/// OR behavior.
/// </summary>
public class CustomAuthorizationEvaluator : IAuthorizationEvaluator
{
/// <summary>
/// Evaluates the results of all authorization handlers called in the pipeline.
/// Will fail if: at least ONE authorization handler calls context.Fail() OR none of
/// authorization handlers call context.Success().
/// Will succeed if: at least one authorization handler calls context.Success().
/// </summary>
/// <param name="context">Shared context among handlers.</param>
/// <returns>Authorization result.</returns>
public AuthorizationResult Evaluate(AuthorizationHandlerContext context)
{
// If context.Fail() got called in ANY of the authorization handlers:
if (context.HasFailed == true)
{
return AuthorizationResult.Failed(AuthorizationFailure.ExplicitFail());
}
// If none handler called context.Fail(), some of them could have called
// context.Success(). MVC treats the context.HasSucceeded with an AND behavior,
// meaning that if one of the custom authorization handlers have called
// context.Success() and others didn't, the property context.HasSucceeded will be
// false. Thus, this class is responsible for applying the OR behavior instead of
// the default AND.
bool success =
context.PendingRequirements.Count() < context.Requirements.Count();
return success == true
? AuthorizationResult.Success()
: AuthorizationResult.Failed(AuthorizationFailure.ExplicitFail());
}
}
This evaluator will only be called if added to .NET service collection (in your startup class) as follows:
services.AddSingleton<IAuthorizationEvaluator, CustomAuthorizationEvaluator>();
In the controller class, decorate each method with both attributes. In my case [Authorize] and [CustomAuthorize].
I'm not sure how others feel about this but I wanted an OR behavior too. In my AuthorizationHandlers I just called Succeed if any of them passed. Note this did NOT work with the built-in Authorize attribute that has no parameters.
public class LoggedInHandler : AuthorizationHandler<LoggedInAuthReq>
{
private readonly IHttpContextAccessor httpContextAccessor;
public LoggedInHandler(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LoggedInAuthReq requirement)
{
var httpContext = httpContextAccessor.HttpContext;
if (httpContext != null && requirement.IsLoggedIn())
{
context.Succeed(requirement);
foreach (var req in context.Requirements)
{
context.Succeed(req);
}
}
return Task.CompletedTask;
}
}
Supply your own LoggedInAuthReq. In startup inject these in services with
services.AddAuthorization(o => {
o.AddPolicy("AadLoggedIn", policy => policy.AddRequirements(new LoggedInAuthReq()));
... more here
});
services.AddSingleton<IAuthorizationHandler, LoggedInHandler>();
... more here
And in your controller method
[Authorize("FacebookLoggedIn")]
[Authorize("MsaLoggedIn")]
[Authorize("AadLoggedIn")]
[HttpGet("anyuser")]
public JsonResult AnyUser()
{
return new JsonResult(new { I = "did it with Any User!" })
{
StatusCode = (int)HttpStatusCode.OK,
};
}
This could probably also be accomplished with a single attribute and a bunch of if statements. It works for me in this scenario. asp.net core 2.2 as of this writing.
Related
I'm using ABP (aspnetboilerplate) 7.0 thru ASP.NET Zero 11 and I'm trying to get OData working. I've followed the article over at ABP and I've taken inspiration from their sample.
The response of OData routes (/odata and /odata/$metadata) should not be wrapped. ABP does provide an attribute to prevent wrapping called DontWrapResult. However, since these routes are not on controllers that I have direct access to, I can't set the attribute.
The same question has been asked here: Disable Wrapping of Controller Results
However, they wanted to disable wrapping altogether, which is not what I want to do.
The answer to that question is to use a ResultFilter to set the attribute's value. I have, however, found that setting the value thru the attribute also sets the value that comes from the injected IAbpAspNetCoreConfiguration.
For example:
public class ODataResultFilter : IResultFilter, ITransientDependency
{
private readonly IAbpAspNetCoreConfiguration _configuration;
public ODataResultFilter(IAbpAspNetCoreConfiguration configuration)
{
_configuration = configuration;
}
public void OnResultExecuting(ResultExecutingContext context)
{
var methodInfo = context.ActionDescriptor.GetMethodInfo();
var wrapResultAttribute =
GetSingleAttributeOfMemberOrDeclaringTypeOrDefault(
methodInfo,
_configuration.DefaultWrapResultAttribute,
false
);
if (context.HttpContext.Request.Path.Value.Equals("/odata/$metadata") ||
context.HttpContext.Request.Path.Value.Equals("/odata"))
{
wrapResultAttribute.WrapOnSuccess = false;
}
}
public void OnResultExecuted(ResultExecutedContext context)
{
// No action
}
private TAttribute GetSingleAttributeOfMemberOrDeclaringTypeOrDefault<TAttribute>(MemberInfo memberInfo, TAttribute defaultValue = default(TAttribute), bool inherit = true)
where TAttribute : class
{
return memberInfo.GetCustomAttributes(true).OfType<TAttribute>().FirstOrDefault()
?? memberInfo.DeclaringType?.GetTypeInfo().GetCustomAttributes(true).OfType<TAttribute>().FirstOrDefault()
?? defaultValue;
}
}
As soon as I hit wrapResultAttribute.WrapOnSuccess = false;, _configuration.DefaultWrapResultAttribute becomes false and every other request ends up not being wrapped. My front-end expects wrapped responses and thus the front-end stops working as soon as I hit an OData route once.
How can I manipulate this attribute and prevent wrapping for OData routes but leave the default + attribute-configured wrapping behavior for the other routes?
GetSingleAttributeOfMemberOrDeclaringTypeOrDefault method should work fine, except right now, since _configuration.DefaultWrapResultAttribute gets modified, a controller that doesn't explicitly set a WrapResult attribute will get the default, overridden by the last value set.
Implement IWrapResultFilter, which was introduced in ABP v6.5:
using Abp.Web.Results.Filters;
using System;
namespace AbpODataDemo.Web.Host.ResultWrapping
{
public class ODataWrapResultFilter : IWrapResultFilter
{
public bool HasFilterForWrapOnError(string url, out bool wrapOnError)
{
wrapOnError = false;
return new Uri(url).AbsolutePath.StartsWith("/odata", StringComparison.InvariantCultureIgnoreCase);
}
public bool HasFilterForWrapOnSuccess(string url, out bool wrapOnSuccess)
{
wrapOnSuccess = false;
return new Uri(url).AbsolutePath.StartsWith("/odata", StringComparison.InvariantCultureIgnoreCase);
}
}
}
Add it to WrapResultFilters in the PreInitialize method of your module:
Configuration.Modules.AbpWebCommon().WrapResultFilters.Add(new ODataWrapResultFilter());
Reference:
https://aspnetboilerplate.com/Pages/Documents/AspNet-Core#wrapresultfilters
https://github.com/aspnetboilerplate/sample-odata/pull/20
OData with ABP v7.1 and later
Abp.AspNetCore.OData implements AbpODataDontWrapResultFilter to disable result wrapping for paths that start with "/odata".
Add it to WrapResultFilters in the PreInitialize method of your module:
Configuration.Modules.AbpWebCommon().WrapResultFilters.Add(new AbpODataDontWrapResultFilter());
The rationale of letting library users configure this explicitly is to highlight this interaction between the default result wrapping and OData use case.
References:
https://aspnetboilerplate.com/Pages/Documents/OData-AspNetCore-Integration#result-wrapping
https://github.com/aspnetboilerplate/aspnetboilerplate/pull/6375
EDIT: This has been solved. Please see my EDIT below. For a more "default" solution where you do not need a "ignore the claim completely" override, see the accepted answer. At the time of writing, My EDIT below contains code that helps you support another scenario where you want to completely ignore the requirement instead of override it.
I have seen multiple posts on this issue but none really solve my problem.
One promising one sadly did not work for me.
I have some some policies. The default one requires the existence of a claim. But another policy requires that this claim does NOT exist. If the default one is applied on the controller, I can not apply the other one on the method. Instead of overriding my previous policy, the policies are all collected together and the first policy fails because the claim is not available.
A good example:
Startup:
services.AddAuthorization(options =>
{
// 99% of the methods require you to have this claim, so we set this as default.
options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.RequireClaim("UserId")
.Build();
options.AddPolicy("UnregisteredUsers",
new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser() // You DO need to be authorized, but that does not mean you already have an account!
.RequireAssertion(x => x.User.FindUserIdClaim() == null) // If you do not have a user ID, you can not access endpoints that require you to be registered until you have an ID
unregistered.
.Build());
// A more specific policy
options.AddPolicy("Administrator",
new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.RequireRole(Roles.Administrator)
.Build());
});
Controllers:
[Route("api/[controller]")]
[ApiController]
[Authorize] // You could also use MapControllers().RequireAuthorization() in Startup.cs instead of [Authorize]
public class BaseController : ControllerBase
{
}
public class UsersController : BaseController
{
// You can only access this endpoint IF you do not have an account.
[HttpPost]
[Authorize(Policy = "UnregisteredUsers")]
public async Task<IActionResult> CreateAccount()
{
// Code here
return Ok();
}
// This one uses the default policy, just like many other policies
[HttpGet]
public async Task<IActionResult> GetUsers()
{
// This would throw if unregistered users would access this endpoint because the claim is not set.
var userId = User.GetUserIdClaim();
// Other code here
return Ok();
}
}
As you can see, if you were to try to create an account, the basecontroller would already deny you access because you do not have that claim. But I require the claim to NOT exist, so this is not a useful comment. Basically, my 2nd policy is more important than the first one.
I hope you guys can help me out!
EDIT:
Thanks to #King King I have been able to fix this.
Their answer works, but I needed to make some changes for some specific scenario's:
The order is apparently not guaranteed.
The answer works when you want to "override" an existing policy by "turning it around". In this case, by default I require a user ID, but another policy requires that you DO not have it. But a scenario that is not supported out of the box is when you have a policy that does not care if you have user ID or not. I will post my FULL solution here but I am very grateful for #King King for his help!
// Note: Scope is REQUIRED!
services.AddScoped<IAuthorizationHandler, UserIdClaimRequirementHandler>();
services.AddAuthorization(options =>
{
// 99% of the methods require you to have this claim, so we set this as default.
options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.AddRequirements(new UserIdClaimRequirement(UserIdClaimSetting.ClaimMustExist))
.Build();
options.AddPolicy("UnregisteredUsers",
new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser() // You DO need to be authorized, but that does not mean you already have an account!
.AddRequirements(new UserIdClaimRequirement(UserIdClaimSetting.ClaimMustNotExist)) // If you do not have a user ID, it means you are not in the database yet, which means you are unregistered.
.Build());
// Registed users and Unregistered users can access these endpoints, but they DO need to be authenticated.
options.AddPolicy("AllUsers",
new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.AddRequirements(new UserIdClaimRequirement(UserIdClaimSetting.IgnoreClaim))
.Build());
// A more specific policy. This builds upon the default policy, so a user ID is required. Technically we could also omit the RequireAuthenticatedUser() call but I like that this is explicit.
options.AddPolicy("Administrator",
new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.RequireRole(Roles.Administrator)
.Build());
});
Requirement class:
/// <summary>
/// Can be used to configure the requirement of the existence of the UserId claim.
/// Either it must exist, it must NOT exist or it does not matter. <br/><br/>
///
/// Please note: Order is not guaranteed with policies. <br/>
/// In the case when you use a <see cref="UserIdClaimSetting.IgnoreClaim"/> or <see cref="UserIdClaimSetting.ClaimMustNotExist"/> and the default policy uses a <see cref="UserIdClaimSetting.ClaimMustExist"/>,
/// the handler should prioritize the result of the <see cref="UserIdClaimSetting.IgnoreClaim"/> and <see cref="UserIdClaimSetting.ClaimMustNotExist"/>
/// and just not evaluate <see cref="UserIdClaimSetting.ClaimMustExist"/> to prevent it returning 403 when that one runs last.
/// In order to do so, the handler MUST be registered as Scoped so it resets for the next reset.
/// </summary>
/// <remarks>
/// This is quite a silly solution!
/// The reason it is necessary is because policies are built on top of each other.
/// The default policy requires that the claim exists because this is true for 99% of the requests,
/// so it makes sense to make this the default to prevent having to explicitly setup authorization on each endpoint. <br/><br/>
/// The "UnregisteredUsers" policy requires that it does NOT exist.<br/><br/>
/// The "AllUsers" policy does not care if it exists or not.
/// Your first thought is probably that using this requirement would be unnecessary in that case,
/// but if this requirement is not used there, the default policy's requirement will require the existence of the claim which will break this policy.
/// </remarks>
public class UserIdClaimRequirement : IAuthorizationRequirement
{
public UserIdClaimSetting Setting { get; }
public UserIdClaimRequirement(UserIdClaimSetting setting)
{
Setting = setting;
}
}
public enum UserIdClaimSetting
{
/// <summary>
/// If the claim does <b>not</b> exist, authorization will fail
/// </summary>
ClaimMustExist,
/// <summary>
/// If the claim exists, authorization will fail
/// </summary>
ClaimMustNotExist,
/// <summary>
/// It does not matter if the claim exists. Either way, authorization will succeed.
/// </summary>
IgnoreClaim
}
Requirement Handler:
public class UserIdClaimRequirementHandler : AuthorizationHandler<UserIdClaimRequirement>
{
/// <summary>
/// Order is not guaranteed with policies.
/// In the case when you use a <see cref="UserIdClaimSetting.IgnoreClaim"/> or <see cref="UserIdClaimSetting.ClaimMustNotExist"/> and the default policy uses a <see cref="UserIdClaimSetting.ClaimMustExist"/>,
/// the handler should prioritize the result of the <see cref="UserIdClaimSetting.IgnoreClaim"/> and <see cref="UserIdClaimSetting.ClaimMustNotExist"/>
/// and just not evaluate <see cref="UserIdClaimSetting.ClaimMustExist"/> to prevent it returning 403 when that one runs last.
/// In order to do so, the handler MUST be registered as Scoped so it resets for the next reset.
/// </summary>
private bool _policyHasAlreadySucceeded = false;
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserIdClaimRequirement requirement)
{
if(_policyHasAlreadySucceeded)
{
return Task.CompletedTask;
}
var hasUserId = context.User.FindFirst("UserId") != null;
// If the claim must not exist but it does -> FAIL
// If the claim must exist but it does not -> FAIL
// If it doesn't matter if the claim exists -> SUCCEED
if ((requirement.Setting == UserIdClaimSetting.ClaimMustNotExist && hasUserId) ||
(requirement.Setting == UserIdClaimSetting.ClaimMustExist && !hasUserId) ||
(requirement.Setting == UserIdClaimSetting.IgnoreClaim && false))
{
context.Fail();
}
else
{
// This requirement has succeeded!
_policyHasAlreadySucceeded = requirement.Setting == UserIdClaimSetting.IgnoreClaim || requirement.Setting == UserIdClaimSetting.ClaimMustNotExist;
// Also, if there are other policy requirements that use the UserId claim, just set them to SUCCEEDED because this requirement is more important than those.
// Example: The default policy requires you to have a user id claim, while this requirement might be used by requiring the claim to NOT exist.
// In order to make this work, we have to override the "require user id claim" requirement by telling it that it succeeded even though it did not!
var otherUserIdClaimRequirements = context.Requirements.Where(e => e is UserIdClaimRequirement || e is ClaimsAuthorizationRequirement cu && cu.ClaimType == "UserId");
foreach (var r in otherUserIdClaimRequirements)
{
context.Succeed(r);
}
}
return Task.CompletedTask;
}
}
The authorization requirement handlers are ANDed. So if any failed, the whole will fail.
The authorization policies will be transformed into a set of authorization requirement handlers. Per my debugging, there are 2 authorization requirements transformed from the default policy (in your code) namely DenyAnonymousAuthorizationRequirement (corresponding to RequireAuthenticatedUser()) and ClaimsAuthorizationRequirement with ClaimType = "UserId" (corresponding to RequireClaim("UserId")).
I've found myself one way to override the result (or simply skip, I'm not so sure about this) of the handlers that handle those 2 requirements. That is by implement a custom requirement handler in which you have access to the AuthorizationHandlerContext which exposes all the authorization requirements that need to be handled (of course including the 2 those I mentioned above). By calling Succeed on them, they seem to be ignored (from being handled again or simply skipped). We can add our second custom authorization requirement handler to verify that but it's not important at all (so I did not do it).
Here is how you build your custom policy (UnregisteredUsers) using a custom authorization requirement handler instead of basing on RequireAssertion:
//the custom requirement class which must implement IAuthorizationRequirement
public class NoUserIdClaimRequirement : IAuthorizationRequirement
{
}
//the corresponding handler
public class NoUserIdClaimRequirementHandler : AuthorizationHandler<NoUserIdClaimRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, NoUserIdClaimRequirement requirement)
{
var hasUserId = context.User.FindFirst("UserId") != null;
if (hasUserId)
{
context.Fail();
} else
{
//NOTE: if you're sure about the extremely high priority of this requirement
//(so we can discard/ignore all the other requirements), just remove the .Where for a shorter code
foreach (var r in context.Requirements
.Where(e => e is NoUserIdClaimRequirement ||
e is ClaimsAuthorizationRequirement cu && cu.ClaimType == "UserId"))
{
//mark all as succeeded
context.Succeed(r);
}
}
return Task.CompletedTask;
}
}
In the code above, I understand that the user still need to be authenticated so we don't call Succeed for DenyAnonymousAuthorizationRequirement. If that's not true, you can include it in the filter above.
You need to register the authorization requirement handler type in ConfigureServices:
services.AddSingleton<IAuthorizationHandler, NoUserIdClaimRequirementHandler>();
Now instead of using RequireAssertion, you need to build your custom policy like this:
options.AddPolicy("UnregisteredUsers",
x => x.RequireAuthenticatedUser()
.AddRequirements(new NoUserIdClaimRequirement()));
I've tried a simple demo on my own, which works perfectly. But it may need some tweak from your own side. If any error occurs, please let me know. The code here is just to show the idea about a possible way to solve this issue. You can base on that to build a more complicated & general solution.
I'm actually migrating some parts of my previous WCF services to Web API.
I had used QueryInterceptor on my Machine entity which checks whether the current user has access to the desired data and returns all the data or a filtered set that they are allowed to see.
[QueryInterceptor("Machines")]
public Expression<Func<Machine, bool>> FilterMachines()
{
return CheckMachineAccess<Machine>(m => m.MachineRole==xyz && m.userHasPermission);
}
I'm finding it difficult to implement the same in Web API. I'm using odata v4, OWIN hosted web API.
Anyone has any suggestions regarding this? Thanks in advance :)
Edit:
I have followed this approach. Don't know if this is the right way to follow.
[HttpGet]
[ODataRoute("Machines")]
[EnableQuery]
public IQueryable<Machine> FilterMachines(ODataQueryOptions opts)
{
var expression = CheckMachineAccess<Machine>(m => m.MachineRole==xyz && m.userHasPermission);
var result = db.Machines.Where(expression);
return (IQueryable<Machine>)result;
}
OP you are on the right track, if that is working for you then I totally support it!
I'll address the Title of your question directly first.
While using middleware is a good way to intercept incoming requests for Authentication and Access control, it is not a great way to implement row level security or to manipulate the query used in your controller.
Why? To manipulate the query for the controller, before the request is passed to the controller your middleware code will need to know so much about the controller and the data context that a lot of code will be duplicated.
In OData services, a good replacement for the many QueryInterceptor implementations is to Inherit from the EnableQuery Attribute.
[AttributeUsage(validOn: AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class EnableQueryAttribute : System.Web.OData.EnableQueryAttribute
{
public EnableQueryAttribute()
{
// TODO: Reset default values
}
/// <summary>
/// Intercept before the query, here we can safely manipulate the URL before the WebAPI request has been processed so before the OData context has been resolved.
/// </summary>
/// <remarks>Simple implementation of common url replacement tasks in OData</remarks>
/// <param name="actionContext"></param>
public override void OnActionExecuting(HttpActionContext actionContext)
{
var tokens = HttpUtility.ParseQueryString(actionContext.Request.RequestUri.AbsoluteUri);
// If the caller requested oDataV2 $inlinecount then remove it!
if (tokens.AllKeys.Contains("$inlinecount"))
{
// CS: we don't care what value they requested, OData v4 will only return the allPages count
tokens["$count"] = "true";
tokens.Remove("$inlinecount");
}
// if caller forgot to ask for count and we are top'ing but paging hasn't been configured lets add the overall count for good measure
else if (String.IsNullOrEmpty(tokens["$count"])
&& !String.IsNullOrEmpty(tokens["$top"])
&& this.PageSize <= 0
)
{
// we want to add $count if it is not there
tokens["$count"] = "true";
}
var modifiedUrl = ParseUri(tokens);
// if we modified the url, reset it. Leaving this in a logic block to make an obvious point to extend the process, say to perform other clean up when we know we have modified the url
if (modifiedUrl != actionContext.Request.RequestUri.AbsoluteUri)
actionContext.Request.RequestUri = new Uri(modifiedUrl);
base.OnActionExecuting(actionContext);
}
/// <summary>
/// Simple validator that can fix common issues when converting NameValueCollection back to Uri when the collection has been modified.
/// </summary>
/// <param name="tokens"></param>
/// <returns></returns>
private static string ParseUri(System.Collections.Specialized.NameValueCollection tokens)
{
var query = tokens.ToHttpQuery().TrimStart('=');
if (!query.Contains('?')) query = query.Insert(query.IndexOf('&'), "?");
return query.Replace("?&", "?");
}
/// <summary>
/// Here we can intercept the IQueryable result AFTER the controller has processed the request and created the intial query.
/// </summary>
/// <remarks>
/// So you could append filter conditions to the query, but, like middleware you may need to know a lot about the controller
/// or you have to make a lot of assumptions to make effective use of this override. Stick to operations that modify the queryOptions
/// or that conditionally modify the properties on this EnableQuery attribute
/// </remarks>
/// <param name="queryable">The original queryable instance from the controller</param>
/// <param name="queryOptions">The System.Web.OData.Query.ODataQueryOptions instance constructed based on the incomming request</param>
public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
{
// I do not offer common examples of this override, because they would be specific to your business logic, but know that it is an available option
return base.ApplyQuery(queryable, queryOptions);
}
}
But how do we solve your issue of what is effectively an implementation of Row Level Security?
What you have implemented already is very similar to what I would have done. You are right, in your controller method you have enough information about the
context to be able to apply a filter to your query.
I had a similar idea in my projects and have a common base class for all my controllers that has a single method that all inheriting controllers must use to get the initial filtered query for their respective entity type:
The following are a cut down version of my base class methods for applying security style rules to a query
/// <summary>
/// Get the base table query for this entity, with user policy applied
/// </summary>
/// <returns>Default IQueryable reference to use in this controller</returns>
protected Task<IQueryable<TEntity>> GetQuery()
{
var dbQuery = this.GetEntityQuery();
return this.ApplyUserPolicy(dbQuery);
}
/// <summary>
/// Inheriting classes MUST override this method to include standard related tables to the DB query
/// </summary>
/// <returns></returns>
protected abstract DbQuery<TEntity> GetEntityQuery();
/// <summary>
/// Apply default user policy to the DBQuery that will be used by actions on this controller.
/// </summary>
/// <remarks>
/// Allow inheriting classes to implement or override the DBQuery before it is parsed to an IQueryable, note that you cannot easily add include statements once it is IQueryable
/// </remarks>
/// <param name="dataTable">DbQuery to parse</param>
/// <param name="tokenParameters">Security and Context Token variables that you can apply if you want to</param>
/// <returns></returns>
protected virtual IQueryable<TEntity> ApplyUserPolicy(DbQuery<TEntity> dataTable, System.Collections.Specialized.NameValueCollection tokenParameters)
{
// TODO: Implement default user policy filtering - like filter by tenant or customer.
return dataTable;
}
So now in your controller you would override the ApplyUserPolicy method to evaluate your security rules in the specific context of the Machine data, which would result in the following changes to your endpoint.
Note that I have also included additional endpoints to show how with this pattern ALL endpoints in your controller
should use GetQuery() to ensure they have the correct security rules applied.
The implication of this pattern though is that A single item Get will return not found instead of access denied if the item is not
found because it is out of scope for that user. I prefer this limitation because my user should not have any knowledge that the other data
that they are not allowed to access exists.
/// <summary>
/// Check that User has permission to view the rows and the required role level
/// </summary>
/// <remarks>This applies to all queries on this controller</remarks>
/// <param name="dataTable">Base DbQuery to parse</param>
/// <returns></returns>
protected override IQueryable<Machine> ApplyUserPolicy(DbQuery<Machine> dataTable)
{
// Apply base level policies, we only want to add further filtering conditions, we are not trying to circumvent base level security
var query = base.ApplyUserPolicy(dataTable, tokenParameters);
// I am faking your CheckMachineAccess code, as I don't know what your logic is
var role = GetUserRole();
query = query.Where(m => m.MachineRole == role);
// additional rule... prehaps user is associated to a specific plant or site and con only access machines at that plant
var plant = GetUserPlant();
if (plant != null) // Maybe plant is optional, so admin users might not return a plant, as they can access all machines
{
query = query.Where(m => m.PlantId == plant.PlantId);
}
return query;
}
[HttpGet]
[ODataRoute("Machines")]
[EnableQuery]
public IQueryable<Machine> FilterMachines(ODataQueryOptions opts)
{
// Get the default query with security applied
var expression = GetQuery();
// TODO: apply any additional queries specific to this endpoint, if there are any
return expression;
}
[HttpGet]
[ODataRoute("Machine")]
[EnableQuery] // so we can still apply $select and $expand
[HttpGet]
public SingleResult<Machine> GetMachine([FromODataUri] int key)
{
// Get the default query with security applied
var query = GetQuery();
// Now filter for just this item by id
query = query.Where(m => m.Id == key);
return SingleResult.Create(query);
}
[HttpGet]
[ODataRoute("MachinesThatNeedService")]
[EnableQuery]
internal IQueryable<Machine> GetMachinesServiceDue(ODataQueryOptions opts)
{
// Get the default query with security applied
var query = GetQuery();
// apply the specific filter for this endpoint
var lastValidServiceDate = DateTimeOffset.Now.Add(-TimeSpan.FromDays(60));
query = query.Where(m => m.LastService < lastValidServiceDate);
return query;
}
You can use OWIN middelware to enter in the pipe of the request.
You will have a function with HTTP request and you can decide to accept or reject the request.
Function to implement is like this:
public async override Task Invoke(IOwinContext context)
{
// here do your check!!
if(isValid)
{
await Next.Invoke(context);
}
Console.WriteLine("End Request");
}
I am trying to set up authorization in ASP.NET Core 1.0 (MVC 6) web app.
More restrictive approach - by default I want to restrict all controllers and action methods to users with Admin role. So, I am adding a global authorize attribute like:
AuthorizationPolicy requireAdminRole = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.RequireRole("Admin")
.Build();
services.AddMvc(options => { options.Filters.Add(new AuthorizeFilter(requireAdminRole));});
Then I want to allow users with specific roles to access concrete controllers. For example:
[Authorize(Roles="Admin,UserManager")]
public class UserControler : Controller{}
Which of course will not work, as the "global filter" will not allow the UserManager to access the controller as they are not "admins".
In MVC5, I was able to implement this by creating a custom authorize attribute and putting my logic there. Then using this custom attribute as a global. For example:
public class IsAdminOrAuthorizeAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
ActionDescriptor action = filterContext.ActionDescriptor;
if (action.IsDefined(typeof(AuthorizeAttribute), true) ||
action.ControllerDescriptor.IsDefined(typeof(AuthorizeAttribute), true))
{
return;
}
base.OnAuthorization(filterContext);
}
}
I tried to create a custom AuthorizeFilter, but no success. API seems to be different.
So my question is: Is it possible to set up default policy and then override it for specific controllers and actions. Or something similar.
I don't want to go with this
[Authorize(Roles="Admin,[OtherRoles]")]
on every controller/action, as this is a potential security problem. What will happen if I accidentally forget to put the Admin role.
You will need to play with the framework a bit since your global policy is more restrictive than the one you want to apply to specific controllers and actions:
By default only Admin users can access your application
Specific roles will also be granted access to some controllers (like UserManagers accessing the UsersController)
As you have already noticied, having a global filter means that only Admin users will have access to a controller. When you add the additional attribute on the UsersController, only users that are both Admin and UserManager will have access.
It is possible to use a similar approach to the MVC 5 one, but it works in a different way.
In MVC 6 the [Authorize] attribute does not contain the authorization logic.
Instead the AuthorizeFilter is the one that has an OnAuthorizeAsync method calling the authorization service to make sure policies are satisfied.
A specific IApplicationModelProvider is used to add an AuthorizeFilter for every controller and action that has an [Authorize] attribute.
One option could be to recreate your IsAdminOrAuthorizeAttribute, but this time as an AuthorizeFilter that you will then add as a global filter:
public class IsAdminOrAuthorizeFilter : AuthorizeFilter
{
public IsAdminOrAuthorizeFilter(AuthorizationPolicy policy): base(policy)
{
}
public override Task OnAuthorizationAsync(Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext context)
{
// If there is another authorize filter, do nothing
if (context.Filters.Any(item => item is IAsyncAuthorizationFilter && item != this))
{
return Task.FromResult(0);
}
//Otherwise apply this policy
return base.OnAuthorizationAsync(context);
}
}
services.AddMvc(opts =>
{
opts.Filters.Add(new IsAdminOrAuthorizeFilter(new AuthorizationPolicyBuilder().RequireRole("admin").Build()));
});
This would apply your global filter only when the controller/action doesn't have a specific [Authorize] attribute.
You could also avoid having a global filter by injecting yourself in the process that generates the filters to be applied for every controller and action. You can either add your own IApplicationModelProvider or your own IApplicationModelConvention. Both will let you add/remove specific controller and actions filters.
For example, you can define a default authorization policy and extra specific policies:
services.AddAuthorization(opts =>
{
opts.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().RequireRole("admin").Build();
opts.AddPolicy("Users", policy => policy.RequireAuthenticatedUser().RequireRole("admin", "users"));
});
Then you can create a new IApplicatioModelProvider that will add the default policy to every controller that doesn't have its own [Authorize] attribute (An application convention would be very similar and probably more aligned with the way the framework is intended to be extended. I just quickly used the existing AuthorizationApplicationModelProvider as a guide):
public class OverridableDefaultAuthorizationApplicationModelProvider : IApplicationModelProvider
{
private readonly AuthorizationOptions _authorizationOptions;
public OverridableDefaultAuthorizationApplicationModelProvider(IOptions<AuthorizationOptions> authorizationOptionsAccessor)
{
_authorizationOptions = authorizationOptionsAccessor.Value;
}
public int Order
{
//It will be executed after AuthorizationApplicationModelProvider, which has order -990
get { return 0; }
}
public void OnProvidersExecuted(ApplicationModelProviderContext context)
{
foreach (var controllerModel in context.Result.Controllers)
{
if (controllerModel.Filters.OfType<IAsyncAuthorizationFilter>().FirstOrDefault() == null)
{
//default policy only used when there is no authorize filter in the controller
controllerModel.Filters.Add(new AuthorizeFilter(_authorizationOptions.DefaultPolicy));
}
}
}
public void OnProvidersExecuting(ApplicationModelProviderContext context)
{
//empty
}
}
//Register in Startup.ConfigureServices
services.TryAddEnumerable(
ServiceDescriptor.Transient<IApplicationModelProvider, OverridableDefaultAuthorizationApplicationModelProvider>());
With this in place, the default policy will be used on these 2 controllers:
public class FooController : Controller
[Authorize]
public class BarController : Controller
And the specific Users policy will be used here:
[Authorize(Policy = "Users")]
public class UsersController : Controller
Notice that you still need to add the admin role to every policy, but at least all your policies will be declared in a single startup method. You could probably create your own methods for building policies that will always add the admin role.
Using #Daniel's solution I ran into the same issue mentioned by #TarkaDaal in the comment (there's 2 AuthorizeFilter in the context for each call...not quite sure where they are coming from).
So my way to solve it is as follow:
public class IsAdminOrAuthorizeFilter : AuthorizeFilter
{
public IsAdminOrAuthorizeFilter(AuthorizationPolicy policy): base(policy)
{
}
public override Task OnAuthorizationAsync(Microsoft.AspNet.Mvc.Filters.AuthorizationContext context)
{
if (context.Filters.Any(f =>
{
var filter = f as AuthorizeFilter;
//There's 2 default Authorize filter in the context for some reason...so we need to filter out the empty ones
return filter?.AuthorizeData != null && filter.AuthorizeData.Any() && f != this;
}))
{
return Task.FromResult(0);
}
//Otherwise apply this policy
return base.OnAuthorizationAsync(context);
}
}
services.AddMvc(opts =>
{
opts.Filters.Add(new IsAdminOrAuthorizeFilter(new AuthorizationPolicyBuilder().RequireRole("admin").Build()));
});
This is ugly but it works in this case because if you're only using the Authorize attribute with no arguments you're going to be handled by the new AuthorizationPolicyBuilder().RequireRole("admin").Build() filter anyway.
I detected a problem in the RequestFilter execution order.
The ValidationFeature in ServiceStack is a Plugin that just registers a Global Request Filter. The Order of Operations points out that Global Request Filters are executed after Filter Attributes with a Priority <0 and before Filter Attributes with a Priority >=0
My BasicAuth filter has -100 priority, and in fact everything goes well if the Service is annotated at class level, but it fails when the annotation is at method level, with the authentication filter being executed after.
I am using 3.9.70
Is there any quick fix for this? Thanks
When you add the annotation at method level then you are creating an Action Request Filter (because you are adding the annotation to an action method) which in the Order of Operations is operation 8, after the other filters have run.
5: Request Filter Attributes with Priority < 0 gets executed
6: Then any Global Request Filters get executed
7: Followed by Request Filter Attributes with Priority >= 0
8: Action Request Filters (New API only)
The best workaround I can suggest is to reconsider your service structure. I imagine you are having these difficulties because you are adding unauthenticated api methods alongside your secure api methods, and thus are using method level attributes to control authentication. So you are presumably doing something like this Your classes and attributes will be different, this is just exemplar:
public class MyService : Service
{
// Unauthenticated API method
public object Get(GetPublicData request)
{
return {};
}
// Secure API method
[MyBasicAuth] // <- Checks user has permission to run this method
public object Get(GetSecureData request)
{
return {};
}
}
I would do this differently, and separate your insecure and secure methods into 2 services. So I use this:
// Wrap in an outer class, then you can still register AppHost with `typeof(MyService).Assembly`
public partial class MyService
{
public class MyPublicService : Service
{
public object Get(GetPublicData request)
{
return {};
}
}
[MyBasicAuth] // <- Check is now class level, can run as expected before Validation
public class MySecureService : Service
{
public object Get(GetSecureData request)
{
return {};
}
}
}
Solution - Deferred Validation:
You can solve your execution order problem by creating your own custom validation feature, which will allow you to defer the validation process. I have created a fully functional self hosted ServiceStack v3 application that demonstrates this.
Full source code here.
Essentially instead of adding the standard ValidationFeature plugin we implement a slightly modified version:
public class MyValidationFeature : IPlugin
{
static readonly ILog Log = LogManager.GetLogger(typeof(MyValidationFeature));
public void Register(IAppHost appHost)
{
// Registers to use your custom validation filter instead of the standard one.
if(!appHost.RequestFilters.Contains(MyValidationFilters.RequestFilter))
appHost.RequestFilters.Add(MyValidationFilters.RequestFilter);
}
}
public static class MyValidationFilters
{
public static void RequestFilter(IHttpRequest req, IHttpResponse res, object requestDto)
{
// Determine if the Request DTO type has a MyRoleAttribute.
// If it does not, run the validation normally. Otherwise defer doing that, it will happen after MyRoleAttribute.
if(!requestDto.GetType().HasAttribute<MyRoleAttribute>()){
Console.WriteLine("Running Validation");
ValidationFilters.RequestFilter(req, res, requestDto);
return;
}
Console.WriteLine("Deferring Validation until Roles are checked");
}
}
Configure to use our plugin:
// Configure to use our custom Validation Feature (MyValidationFeature)
Plugins.Add(new MyValidationFeature());
Then we need to create our custom attribute. Your attribute will be different of course. The key thing you need to do is call ValidationFilters.RequestFilter(req, res, requestDto); if you are satisfied the user has the required role and meets your conditions.
public class MyRoleAttribute : RequestFilterAttribute
{
readonly string[] _roles;
public MyRoleAttribute(params string[] roles)
{
_roles = roles;
}
#region implemented abstract members of RequestFilterAttribute
public override void Execute(IHttpRequest req, IHttpResponse res, object requestDto)
{
Console.WriteLine("Checking for required role");
// Replace with your actual role checking code
var role = req.GetParam("role");
if(role == null || !_roles.Contains(role))
throw HttpError.Unauthorized("You don't have the correct role");
Console.WriteLine("Has required role");
// Perform the deferred validation
Console.WriteLine("Running Validation");
ValidationFilters.RequestFilter(req, res, requestDto);
}
#endregion
}
For this to work we need to apply our custom attribute on the DTO route not the action method. So this will be slightly different to how you are doing it now, but should still be flexible.
[Route("/HaveChristmas", "GET")]
[MyRole("Santa","Rudolph","MrsClaus")] // Notice our custom MyRole attribute.
public class HaveChristmasRequest {}
[Route("/EasterEgg", "GET")]
[MyRole("Easterbunny")]
public class GetEasterEggRequest {}
[Route("/EinsteinsBirthday", "GET")]
public class EinsteinsBirthdayRequest {}
Then your service would look something like this:
public class TestController : Service
{
// Roles: Santa, Rudolph, MrsClaus
public object Get(HaveChristmasRequest request)
{
return new { Presents = "Toy Car, Teddy Bear, Xbox" };
}
// Roles: Easterbunny
public object Get(GetEasterEggRequest request)
{
return new { EasterEgg = "Chocolate" };
}
// No roles required
public object Get(EinsteinsBirthdayRequest request)
{
return new { Birthdate = new DateTime(1879, 3, 14) };
}
}
So when we call the route /EinsteinsBirthday which does not have a MyRole attribute the validation will be called normally, as if using the standard ValidationFeature.
If we call the route /HaveChristmas?role=Santa then our validation plugin will determine that the DTO has our attribute and not run. Then our attribute filter triggers and it will trigger the validation to run. Thus the order is correct.