Different API functionality for different roles - c#

I have API with asp.net core 2.1. Claims-based authentication. Is it possible to combine these two api function in one?
[Authorize(Roles = "Admin")]
[HttpPost("delete")]
public IActionResult Delete([FromBody]Item item)
{
_itemService.Delete(item.Id);
return Ok();
}
[Authorize]
[HttpPost("delete")]
public IActionResult Delete([FromBody]Item item)
{
var id = int.Parse(User.FindFirst(ClaimTypes.NameIdentifier).Value);
if (_itemService.IsAuthor(id))
{
_itemService.Delete(item.Id);
return Ok();
}
return Forbid();
}
Or should I just check the role inside method?

For checking the permission with whether the user is Admin or Author, you could implement multiple requirements as the doc from #user2884707bond.
For using the multiple requrements for your scenario.
You could follow steps below:
PermissionHandler.cs
public class PermissionHandler : IAuthorizationHandler
{
public Task HandleAsync(AuthorizationHandlerContext context)
{
var pendingRequirements = context.PendingRequirements.ToList();
foreach (var requirement in pendingRequirements)
{
if (requirement is ReadPermission)
{
if (IsOwner(context.User, context.Resource) ||
IsAdmin(context.User, context.Resource))
{
context.Succeed(requirement);
}
}
else if (requirement is EditPermission ||
requirement is DeletePermission)
{
if (IsOwner(context.User, context.Resource))
{
context.Succeed(requirement);
}
}
}
return Task.CompletedTask;
}
private bool IsAdmin(ClaimsPrincipal user, object resource)
{
if (user.IsInRole("Admin"))
{
return true;
}
return false;
}
private bool IsOwner(ClaimsPrincipal user, object resource)
{
// Code omitted for brevity
return true;
}
private bool IsSponsor(ClaimsPrincipal user, object resource)
{
// Code omitted for brevity
return true;
}
}
Requirements
public class ReadPermission : IAuthorizationRequirement
{
// Code omitted for brevity
}
public class EditPermission : IAuthorizationRequirement
{
// Code omitted for brevity
}
public class DeletePermission : IAuthorizationRequirement
{
// Code omitted for brevity
}
Register Requirement in Startup.cs
services.AddAuthorization(options =>
{
options.AddPolicy("Read", policy => policy.AddRequirements(new ReadPermission()));
});
services.AddSingleton<IAuthorizationHandler, PermissionHandler>();
Use
[Authorize(Policy = "Read")]
[HttpPost("delete")]
public IActionResult Delete([FromBody]Item item)
{
_itemService.Delete(item.Id);
return Ok();
}

Related

How to move custom authorization attribute policy implementation logic to startup?

I have implemented a custom [Authorize] attribute in a ASP.NET MVC Core 3.1 app. The main reason I have a custom one is because the app uses a lot of AJAX and I couldn't figure out how to get it to work with AJAX. The attribute also implements ActionFilterAttribute instead of IAuthorizationFilter due to issues I posted about here. Having this custom attribute means implementing my own logic to handle roles, which I did.
I've also implemented code to handle policies that group roles, but the code is inside the attribute itself. When a developer would think of changing policies within MVC framework, they'd probably think to go to Startup.cs.
How I could put the logic for policies in Startup.cs and have my custom attribute use it?
Custom attribute:
public class AuthorizeUser : ActionFilterAttribute
{
public Policies Policy { get; set; }
public AuthorizeUser(Policies policy)
{
Policy = policy;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
string signInPageUrl = "/UserAccess/Index";
string notAuthorizedUrl = "/UserAccess/NotAuthorized";
if (context.HttpContext.User.Identity.IsAuthenticated)
{
List<string> roles = GetRolesByPolicy(Policy);
bool userHasRole = false;
foreach (var role in roles)
{
if (context.HttpContext.User.IsInRole(role.ToUpper()))
{
userHasRole = true;
}
}
if (userHasRole == false)
{
if (context.HttpContext.Request.IsAjaxRequest())
{
context.HttpContext.Response.StatusCode = 401;
JsonResult jsonResult = new JsonResult(new { redirectUrl = notAuthorizedUrl });
context.Result = jsonResult;
}
else
{
context.Result = new RedirectResult(notAuthorizedUrl);
}
}
}
else
{
if (context.HttpContext.Request.IsAjaxRequest())
{
context.HttpContext.Response.StatusCode = 403;
JsonResult jsonResult = new JsonResult(new { redirectUrl = signInPageUrl });
context.Result = jsonResult;
}
else
{
context.Result = new RedirectResult(signInPageUrl);
}
}
}
private List<string> GetRolesByPolicy(Policies policy)
{
List<RoleModel.Roles> roleEnums = new List<RoleModel.Roles>();
roleEnums.Add(RoleModel.Roles.Admin); //admin is always allowed all actions
switch (policy)
{
case Policies.EditUsers:
roleEnums.AddRange(new List<RoleModel.Roles>
{
RoleModel.Roles.User,
RoleModel.Roles.Customer,
RoleModel.Roles.Developer,
RoleModel.Roles.Manager
});
break;
case Policies.ManageOrders:
roleEnums.AddRange(new List<RoleModel.Roles>
{
RoleModel.Roles.User,
RoleModel.Roles.Customer
});
break;
case Policies.SendOrders:
roleEnums.AddRange(new List<RoleModel.Roles>
{
RoleModel.Roles.User
});
break;
}
return roleEnums.ConvertAll(r => r.ToString());
}
}
Policies enum
public enum Policies
{
EditUsers,
ManageOrders,
SendOrders
}

IdentityServer: Multiple Clients with different grant types, policies not working properly

I've got an identity server setup with the following 'look a like' configuration:
return new List<Client>
{
new Client
{
[...]
AllowedGrantTypes = GrantTypes.Implicit,
[....]
},
new Client
{
[...]
AllowedGrantTypes = GrantTypes.ClientCredentials,
[....]
}
};
and controlles annotated like this:
[Route("api/forms")]
[ApiController]
[Authorize(Policy = "user.api.portfolio.manager")]
[Authorize(Policy = "application.api.portfolio.manager")]
public class FormsController : ControllerBase
{
[...]
}
and a policy
private System.Action<AuthorizationOptions> AddJwtAuthorizationPolicyForRole()
{
return options => { options.AddPolicy("**POLICY_FOR_GRANT_IMPLICIT**", policy => {
policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
policy.RequireClaim(ClaimTypes.Role, "USER_ACCESSIBLE");
});
};
}
private System.Action<AuthorizationOptions> AddJwtAuthorizationPolicyForRole()
{
return options => { options.AddPolicy("**POLICY_FOR_CLIENT_CREDENTIALS**", policy => {
policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
});
};
}
so I want to achieve:
Clients using the GrantType.ClientCredentials can access the controller without any further needs.
Clients using the Implicit Schema must have role USER_ACCESSIBLE
If it's configured like shown above, both policies must apply -> Both grant types are failing.
How can I achieve the described behavior using IdentityServer, that each grant-types may have an independent policy so be applied?
Thanks in advance for your help.
The most simplest solution is adding another single policy for Implicit + ClientCredential to implement logics for OR conditions .
Or you can create a custom attribute like :
MultiplePolicysAuthorizeAttribute
public class MultiplePolicysAuthorizeAttribute : TypeFilterAttribute
{
public MultiplePolicysAuthorizeAttribute(string policys, bool isAnd = false) : base(typeof(MultiplePolicysAuthorizeFilter))
{
Arguments = new object[] { policys, isAnd };
}
}
MultiplePolicysAuthorizeFilter
public class MultiplePolicysAuthorizeFilter : IAsyncAuthorizationFilter
{
private readonly IAuthorizationService _authorization;
public string Policys { get; private set; }
public bool IsAnd { get; private set; }
public MultiplePolicysAuthorizeFilter(string policys, bool isAnd, IAuthorizationService authorization)
{
Policys = policys;
IsAnd = isAnd;
_authorization = authorization;
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var policys = Policys.Split(";").ToList();
if (IsAnd)
{
foreach (var policy in policys)
{
var authorized = await _authorization.AuthorizeAsync(context.HttpContext.User, policy);
if (!authorized.Succeeded)
{
context.Result = new ForbidResult();
return;
}
}
}
else
{
foreach (var policy in policys)
{
var authorized = await _authorization.AuthorizeAsync(context.HttpContext.User, policy);
if (authorized.Succeeded)
{
return;
}
}
context.Result = new ForbidResult();
return;
}
}
}
If actions require matching one of the policies(OR):
[MultiplePolicysAuthorize("POLICY_FOR_GRANT_IMPLICIT;POLICY_FOR_CLIENT_CREDENTIALS")]
If actions require matching all the policies(And) :
[MultiplePolicysAuthorize("POLICY_FOR_GRANT_IMPLICIT;POLICY_FOR_CLIENT_CREDENTIALS",true)]
Code sample reference : https://stackoverflow.com/a/52639938/5751404

Attribute Routing In Asp.net Core Web api

I try to start asp.net core web api routing attribute as default route but when I access routing with parameter, I could not get any response
[Route("api/[controller]")]
[ApiController]
public class WarehousesController : ControllerBase
{
private readonly ApplicationDbContext _context;
public WarehousesController(ApplicationDbContext context)
{
_context = context;
}
//http://localhost:2394/api/Warehouses/Project/1 (Not working)
[HttpGet("Project/{projectId}")]
public async Task<IActionResult> GetWarehouseByProjectId([FromRoute] int projectId)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var warehouse = await _context.warehouses.Include(x => x.Projects).Where(y => y.Projects.FirstOrDefault().ProjectId == projectId).ToListAsync();
if (warehouse == null)
{
return NotFound();
}
return Ok(warehouse);
}
}
Try with this one .This works fine for me
[HttpGet("Project/{projectId}")]
public async Task<IActionResult> GetWarehouseByProjectId([FromRoute] int projectId)
{
if (!ModelState.IsValid)
{
return BadRequest("Invalid state");
}
var warehouse =await _context.warehouses.FindAsync(projectId);
if (warehouse == null)
{
return NotFound("not found");
}
return Ok(warehouse);
}

ASP.NET REST service routing

I'm trying to get a REST service up and running (I followed this tutorial), and was trying to extend it with a simple method to mark one of the ToDoItem as "Complete"; literally to pass an ID into a method which should mark it as "Complete".
However, I'm struggling to understand how the routing works.
This is the method provided by default, which works correctly via https://localhost:44388/api/values
If I add another GET operation, even with different [Route] attribute, then I end up with "AmbiguousActionException: Multiple actions matched"
[Route("api/values")]
[ApiController]
public class ValuesController : ControllerBase
{
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
I tried to specify a route prefix using the method below, so that I could add doesn't work; I get a 404 on https://localhost:44388/api/values and https://localhost:44388/api/values/getbyname
[RoutePrefix("api/values")]
[ApiController]
public class ValuesController : ControllerBase
{
// GET api/values
[Route("getbyname")]
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
I might be trying the wrong method, so I'm happy to take any advice. I just want to be able to create new REST calls and have them the appropriate actions. Do I need to create other controllers? Am I limited to one GET/POST/PUT etc per controller?
Edit: didn't provide enough info, here's more code:
[Route("api/ToDo")]
[ApiController]
public class ToDoController : ControllerBase
{
private readonly ToDoContext _context;
public ToDoController(ToDoContext toDoContext)
{
_context = toDoContext;
if (_context.ToDoItems.Count() == 0)
{
//collection is empty, so add a new item
ToDoItem item1 = new ToDoItem(1, "example 1");
ToDoItem item2 = new ToDoItem(2, "example 2");
_context.ToDoItems.Add(item1);
_context.ToDoItems.Add(item2);
_context.SaveChanges();
}
}
//GET: api/todo
[HttpGet]
public async Task<ActionResult<IEnumerable<ToDoItem>>> GetToDoItems()
{
return await _context.ToDoItems.ToListAsync();
}
//GET: api/todo/5
//[HttpGet(Name = "Get a ToDoItem")]
//[Route("get{id}")]
[HttpGet("{id}")]
public async Task<ActionResult<ToDoItem>> GetToDoItem(long id)
{
var todoitem = await _context.ToDoItems.FindAsync(id);
if (todoitem == null)
{
return NotFound();
}
return todoitem;
}
//POST: api/Todo
[HttpPost]
public async Task<ActionResult<ToDoItem>> PostToDoItem(ToDoItem todoItem)
{
_context.ToDoItems.Add(todoItem);
await _context.SaveChangesAsync();
//calls the "GetToDoItem" method above!
return CreatedAtAction("GetToDoItem", new { id = todoItem.ID }, todoItem);
}
//DELETE: api/todo/5
[HttpDelete("{id}")]
public async Task<ActionResult<ToDoItem>> DeleteToDoItem(long id)
{
var todoItem = await _context.ToDoItems.FindAsync(id);
if(todoItem == null)
{
return NotFound();
}
_context.ToDoItems.Remove(todoItem);
await _context.SaveChangesAsync();
return todoItem;
}
//* -. space to create a "MarkAsComplete" method
//GET: api/todo/5
[HttpGet(Name = "{name}")]
public async Task<ActionResult<ToDoItem>> MarkAsComplete(long id)
{
var todoitem = await _context.ToDoItems.FindAsync(id);
if (todoitem == null)
{
return NotFound();
}
else
{
todoitem.IsComplete = true;
}
return todoitem;
}
//*/
}
Mixing up different versions of the attributes. RoutePrefix is from a previous version.
Routes need to be unique per action to avoid route conflicts.
For example.
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase {
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get() {
return new string[] { "value1", "value2" };
}
// GET api/values/some_name
[HttpGet("{name}")]
public IActionResult GetByName(string name) {
return Ok();
}
}
Reference Routing to controller actions in ASP.NET Core
When building a REST API, it's rare that you will want to use [Route(...)] on an action method. It's better to use the more specific Http*Verb*Attributes to be precise about what your API supports. Clients of REST APIs are expected to know what paths and HTTP verbs map to specific logical operations.
Based on the additional details provided, that MarkAsComplete action should use HTTP PUT so signify that the model is being edited/updated.
For example
//* -. space to create a "MarkAsComplete" method
//PUT: api/todo/5
[HttpPut("{id:long}")]
public async Task<ActionResult<ToDoItem>> MarkAsComplete(long id) {
var todoitem = await _context.ToDoItems.FindAsync(id);
if (todoitem == null) {
return NotFound();
} else {
todoitem.IsComplete = true;
}
return todoitem;
}
//*/

Customizing .net Identity 2

I have an MVC5 application with .net 4.7.2 and I'm trying to have my customized version of .net Identity classes. I've followed the steps in this tutorial, with some changes here and there, because my project rely on a set of WCF services to communicate with the database, so I don't have direct connection to the database:
overview-of-custom-storage-providers-for-aspnet-identity
So mainly I've created my own MyUserStore which implements IUserStore and IUserRoleStore, and I've added this to Startup.cs:
public void ConfigureAuth(IAppBuilder app)
{
app.CreatePerOwinContext<MyUserManager>(MyUserManager.Create);
}
Now the problem that the IsInRole methods are not getting invoked, and also I had break point for methods like FindByIdAsync, FindByNameAsync and IsInRoleAsync and nothing got invoked, even though I've added
[Authorize(Roles ="TestRole")] attribute to couple actions and has tried to invoke this explicitly:
this.User.IsInRole("TestRole");
What I'm missing here to have it work in the proper way?
Edit #1 - adding the whole code:
Here under is the User Store related classes:
public class MyUserStore :
IUserStore<MyIdentityUser, int>,
IUserRoleStore<MyIdentityUser, int>,
IQueryableUserStore<MyIdentityUser, int>,
IDisposable
{
public Task CreateAsync(MyIdentityUser account)
{
// Add account to DB...
return Task.FromResult<object>(null);
}
public Task<MyIdentityUser> FindByIdAsync(int accountId)
{
// Get account from DB and return it
return Task.FromResult<MyIdentityUser>(account);
}
public Task<MyIdentityUser> FindByNameAsync(string userName)
{
// Get account from DB and return it
return Task.FromResult<MyIdentityUser>(account);
}
public Task UpdateAsync(MyIdentityUser account)
{
// Update account in DB
return Task.FromResult<object>(null);
}
public Task AddToRoleAsync(MyIdentityUser account, string roleName)
{
throw new NotImplementedException("UserStore.AddToRoleAsync");
return Task.FromResult<object>(null);
}
public Task<IList<string>> GetRolesAsync(MyIdentityUser account)
{
// TODO: Check if important to implement
throw new NotImplementedException("UserStore.GetRolesAsync");
}
public Task<bool> IsInRoleAsync(MyIdentityUser account, string task)
{
// Return true if has permission (not getting invoked)
return Task.FromResult<bool>(hasPermission);
}
public Task RemoveFromRoleAsync(MyIdentityUser account, string task)
{
throw new NotImplementedException("UserStore.RemoveFromRoleAsync");
}
public Task DeleteAsync(MyIdentityUser account)
{
// Delete user from DB
return Task.FromResult<Object>(null);
}
}
public class MyUserRole : IdentityUserRole<int> { }
public class MyUserClaim : IdentityUserClaim<int> { }
public class MyUserLogin : IdentityUserLogin<int> { }
public class MyIdentityUser : IdentityUser<int, MyUserLogin, MyUserRole, MyUserClaim>
{
public MyIdentityUser(int id)
{
Id = id;
}
// My extra account's properties
}
}
And here is the UserManager class:
public class MyUserManager : UserManager<MyIdentityUser, int>
{
public MyUserManager(IUserStore<MyIdentityUser, int> store)
: base(store) { }
public static MyUserManager Create(IdentityFactoryOptions<MyUserManager> options, IOwinContext context)
{
///Calling the non-default constructor of the UserStore class
var manager = new MyUserManager(new MyUserStore());
manager.UserValidator = new UserValidator<MyIdentityUser, int>(manager)
{
AllowOnlyAlphanumericUserNames = false,
RequireUniqueEmail = true
};
// Configure validation logic for passwords
manager.PasswordValidator = new PasswordValidator
{
RequiredLength = 6,
};
// Configure user lockout defaults
manager.UserLockoutEnabledByDefault = false;
manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
manager.MaxFailedAccessAttemptsBeforeLockout = 3;
// You can write your own provider and plug it in here.
return manager;
}
}
And maybe this is the most important part, the login controller:
var manager = HttpContext.GetOwinContext().GetUserManager<MyUserManager>();
//check for credentials before sign in ..
var result = manager.CheckPasswordAsync(vm.userName, vm.password, ref state);
if(result.Result)
{
FormsAuthentication.SetAuthCookie(vm.userName, vm.rememberMe);
return Redirect("~/");
}
Regards,

Categories

Resources