ASP.NET Core by standard offers to configure access to api using attributes, it is possible to restrict access to users with a certain claim, you can define policies and bind to controllers, creating controllers for different roles
This system has minuses, the biggest in that, looking at this attribute:
[Authorize(Roles = "Administrator")] public class AdministrationController : Controller { }
We do not receive any information about what rights the administrator has.
My task is to display all banned users for this month (not just go to the database and filter, there are certain counting rules that lie somewhere), I do CTRL + N on the project and look for BannedUserHandler or IHasInfoAbounBannedUser or GetBannedUsersForAdmin .
I find the controllers marked with the attribute [Authorize (Roles = "Administrator")] , there may be two scenarios:
We do everything in the controller
[Route("api/[controller]/[action]")] public class AdminInfoController1 : ControllerBase { private readonly IGetUserInfoService _getInfoAboutActiveUsers; private readonly ICanBanUserService _banUserService; private readonly ICanRemoveBanUserService _removeBanUserService; // action public AdminInfoController1( IGetUserInfoService infoAboutActiveUsers, ICanBanUserService banUserService, ICanRemoveBanUserService removeBanUserService) { _getInfoAboutActiveUsers = infoAboutActiveUsers; _banUserService = banUserService; _removeBanUserService = removeBanUserService; } // actions //... //... }
Spread by Handlers
[Route("api/[controller]/[action]")] public class AdminInfoController2 : ControllerBase { [HttpPatch("{id}")] public async Task<ActionResult<BanUserResult>> BanUser( [FromServices] IAsyncHandler<UserId, BanUserResult> handler, UserId userId) => await handler.Handle(userId, HttpContext.RequestAborted); [HttpPatch("{id}")] public async Task<ActionResult<RemoveBanUserResult>> RemoveBanUser( [FromServices] IAsyncHandler<UserId, RemoveBanUserResult> handler, UserId userId) => await handler.Handle(userId, HttpContext.RequestAborted); }
The first approach is not bad in that we know the access to which resources the Admin has, what dependencies it can use, I would use this approach in small applications, without a complex subject area
The second one is not so talking, all dependencies are resolved in handlers, I can’t look at the constructor and understand what kind of dependency I need, this approach justifies itself when the application is complex and the controllers swell, it becomes impossible to support them. The classic solution to this problem is the partitioning of the solution into folders / projects, the necessary services are put in each one, they are easy to find and use
All this has a big drawback, the code does not tell the developer what to do, it makes you think => waste of time => implementation errors
And the more you have to think, the more mistakes are made.
What if routing is built like this :
let webPart = choose [ path "/" >=> (OK "Home") path "/about" >=> (OK "About") path "/articles" >=> (OK "List of articles") path "/articles/browse" >=> (OK "Browse articles") path "/articles/details" >=> (OK "Content of an article") ]
''> => '' - what is it? This thing has a name, but its knowledge will not bring the reader closer to understanding how it works, so there’s no point in bringing it, it’s better to consider how it works
The pipeline from Suave is written above, the same is used in Giraffe (with a different signature of functions), there is a signature:
type WebPart = HttpContext -> Async<HttpContext option>
Async in this case does not play a special role (to understand how it works), omit it
HttpContext -> HttpContext option
A function with such a signature accepts an HttpContext , processes (deserializes the body, looks at cookies, request headers), forms a response, and if everything went well, wraps it in Some , if something goes wrong, returns None , for example ( library function ):
// async let OK s : WebPart = fun ctx -> { ctx with response = { ctx.response with status = HTTP_200.status; content = Bytes s }} |> Some |> async.Return
This function cannot "wrap the request execution flow", it always throws a new response further, with a body and status of 200, but this one can:
let path (str:string) ctx = let path = ctx.request.rawPath if path.StartsWith str then ctx |> Some |> async.Return else async.Return None
The last function you need is choose - it gets a list of different functions and selects the one that returns Some first:
let rec choose (webparts:(HttpContext) -> Async<HttpContext option>) list) context= async{ match webparts with | [head] -> return! head context | head::tail -> let! result = head context match result with | Some _-> return result | None -> return! choose tail context | [] -> return None }
Well, the most important binding function (Async omitted):
type WebPartWithoutAsync = HttpContext -> HttpContext option let (>=>) (h1:WebPartWithoutAsync ) (h2:WebPartWithoutAsync) ctx : HttpContext option = let result = h1 ctx match result with | Some ctx' -> h2 ctx' | None -> None
type WebPart = HttpContext -> Async<HttpContext option> let (>=>) (h1:WebPart ) (h2:WebPart ) ctx : Async<HttpContext option>= async{ let! result = h1 ctx match result with | Some ctx' -> return! h2 ctx' | None -> return None }
"> =>" accepts two handlers on the left and right sides and httpContext , when the request arrives, the server forms an HttpContext object and passes it to the function, "> =>" executes the first (left) handler if it returns Some ctx , passes ctx at the entrance of the second handler.
And why can we write like this (combine several functions)?
GET >=> path "/api" >=> OK
Because "> =>" accepts two WebPart functions and returns one function that accepts HttpContext and returns Async <HttpContext option> , and which function takes context and returns Async <HttpContext option> ?
Webpart .
It turns out that "> =>" takes WebPart for the handler and returns WebPart , so we can write several combinators in a row, and not just two.
Details about the work of combinators can be found here.
Let us return to the beginning of the article, how can we explicitly indicate to the programmer what resources can be accessed for a particular role? It is necessary to enter this data into the pipeline so that handlers have access to the corresponding resources, I did it like this:
The application is divided into parts / modules. In the AdminPart and AccountPart functions , access to these modules of various roles is allowed, all users have access to AccountPart, only admin is accessing AdminPart, data is received, pay attention to the chooseP function, I have to add more functions, because the standard ones are attached to Suave types, and handlers inside AdminPart and AccountPart now have different signatures:
// AdminPart AdminInfo * HttpContext -> Async<(AdminInfo * HttpContext) option> // AccountPart AccountInfo* HttpContext -> Async<(AccountInfo * HttpContext) option>
Inside, the new features are completely identical to the original ones.
Now the handler immediately has access to resources for each role, only the main thing needs to be added there so that you can easily navigate, for example, in AccountPart you can add a nickname, email, user role, friend list if it's a social network, but there is a problem: for one overwhelming most handlers I need a list of friends, but for the rest I don’t need it at all, what should I do? Either distribute these handlers into different modules (preferably), or make access lazy (wrap in unit -> friends list ), the main thing is not to put IQueryable <Friend> there , because this is not a service - it is a data set that defines the role
I put in AdminInfo information about approved and banned users by the current administrator, in the context of my "application" this defines the role of the Administrator:
type AdminInfo = { ActiveUsersEmails: string list BanUsersEmails : string list } type UserInfo = { Name:string Surname:string }
What is the difference from Claim ? Can I make User.Claims in the controller and get the same thing?
In typing and in talking: modules, the developer does not have to look for code examples on handlers in the same context, he creates a handler and adds it to the routing and makes it all compile
let AccountPart handler = let getUserInfo ctx = async.Return {Name="Al";Surname="Pacino"} permissionHandler [User;Admin] getUserInfo handler
getUserInfo receives data for the Account module, has access to the context to get personal data (this is exactly this user'a, admin'a)
permissionHandler checks for the presence of jwt token, decrypts it, and checks access, returns the original WebPart to maintain compatibility with Suave
Full source code can be found on github
Thanks for attention!
Source: https://habr.com/ru/post/461593/
All Articles