API em F #. Módulos de aplicativos baseados em funções de acesso

O ASP.NET Core, por padrão, oferece a configuração de acesso à API usando atributos, é possível restringir o acesso a usuários com uma reivindicação específica, você pode definir políticas e vincular-se a controladores, criando controladores para diferentes funções
Este sistema possui desvantagens, a maior delas, observando este atributo:


[Authorize(Roles = "Administrator")] public class AdministrationController : Controller { } 

Não recebemos informações sobre quais direitos o administrador possui.


Minha tarefa é exibir todos os usuários banidos para este mês (não basta ir ao banco de dados e filtrar, existem certas regras de contagem em algum lugar), faço CTRL + N no projeto e procuro BannedUserHandler ou IHasInfoAbounBannedUser ou GetBannedUsersForAdmin .


Acho que os controladores marcados com o atributo [Autorizar (funções = "Administrador")] , podem haver dois cenários:


Fazemos tudo no controlador


  [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 //... //... } 

Distribuímos em manipuladores


  [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); } 

A primeira abordagem não é ruim, pois sabemos o acesso a quais recursos o Admin tem, quais dependências ele pode usar. Eu usaria essa abordagem em pequenas aplicações, sem uma área de assunto complexa.


O segundo não é o que fala, todas as dependências são resolvidas nos manipuladores, não consigo olhar para o construtor e entender que tipo de dependência eu preciso; essa abordagem se justifica quando o aplicativo é complexo e os controladores incham, torna-se impossível apoiá-los. A solução clássica para esse problema é o particionamento da solução em pastas / projetos, os serviços necessários são colocados em cada um, são fáceis de encontrar e usar


Tudo isso tem uma grande desvantagem, o código não diz ao desenvolvedor o que fazer, faz você pensar => perda de tempo => erros de implementação


E quanto mais você pensa, mais erros são cometidos.


Introdução ao Roteamento Suave


E se o roteamento for construído assim :


 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") ] 

''> => '' - o que é? Essa coisa tem um nome, mas seu conhecimento não aproximará nem um grama o leitor de como ela funciona; portanto, não há sentido em trazê-la; é melhor considerar como tudo funciona.


O pipeline do Suave está escrito acima, o mesmo é usado no Giraffe (com uma assinatura de funções diferente), existe uma assinatura:


 type WebPart = HttpContext -> Async<HttpContext option> 

O assíncrono nesse caso não desempenha um papel especial (para entender como funciona), omita-o


 HttpContext -> HttpContext option 

Uma função com essa assinatura aceita um HttpContext , processa (desserializa o corpo, consulta cookies, solicita cabeçalhos), forma uma resposta e, se tudo correu bem, envolve-a em Some , se algo der errado, retorna None , por exemplo ( função da biblioteca ):


  //    async let OK s : WebPart = fun ctx -> { ctx with response = { ctx.response with status = HTTP_200.status; content = Bytes s }} |> Some |> async.Return 

Essa função não pode "encapsular o fluxo de execução da solicitação", sempre lança uma nova resposta ainda mais, com corpo e status 200, mas esta pode:


 let path (str:string) ctx = let path = ctx.request.rawPath if path.StartsWith str then ctx |> Some |> async.Return else async.Return None 

A última função que você precisa é escolher - ela obtém uma lista de funções diferentes e seleciona a que retorna 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 } 

Bem, a função de ligação mais importante (Async omitida):


 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 

Versão assíncrona
 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 } 

"> =>" aceita dois manipuladores nos lados esquerdo e direito e httpContext , quando a solicitação chega, o servidor forma um objeto HttpContext e o passa para a função "> =>" executa o primeiro manipulador (esquerdo) se retornar algum ctx , passa ctx para a entrada do segundo manipulador.


E por que podemos escrever assim (combinar várias funções)?


 GET >=> path "/api" >=> OK 

Como "> =>" aceita duas funções WebPart e retorna uma função que aceita o HttpContext e retorna Async <opção HttpContext> , e qual função aceita o contexto e retorna Async <opção HttpContext> ?
Webpart .


Acontece que "> =>" pega o WebPart para o manipulador e retorna o WebPart , para que possamos escrever vários combinadores em uma linha, e não apenas dois.
Detalhes sobre o trabalho dos combinadores podem ser encontrados aqui.


O que a função e a restrição de acesso têm a ver com isso?


Voltando ao início do artigo, como podemos indicar explicitamente ao programador quais recursos podem ser acessados ​​para uma função específica? É necessário inserir esses dados no pipeline para que os manipuladores tenham acesso aos recursos correspondentes, eu fiz assim:



O aplicativo é dividido em partes / módulos. As funções AdminPart e AccountPart permitem acesso a esses módulos de várias funções, todos os usuários têm acesso a AccountPart, apenas o administrador está acessando AdminPart, os dados são recebidos, preste atenção à função chooseP, preciso adicionar mais funções, porque as padrão estão anexadas aos tipos Suave e manipuladores dentro AdminPart e AccountPart agora têm assinaturas diferentes:


 // AdminPart AdminInfo * HttpContext -> Async<(AdminInfo * HttpContext) option> // AccountPart AccountInfo* HttpContext -> Async<(AccountInfo * HttpContext) option> 

No interior, os novos recursos são completamente idênticos aos originais.


Agora, o manipulador tem acesso imediato aos recursos de cada função, apenas o principal precisa ser adicionado lá, para que você possa navegar facilmente, por exemplo, em AccountPart, você pode adicionar um apelido, email, função de usuário e lista de amigos, se for uma rede social, mas há um problema: na maioria dos manipuladores, preciso de uma lista de amigos, mas, de resto, não preciso disso, o que devo fazer? Distribua esses manipuladores em diferentes módulos (de preferência) ou torne o acesso lento (agrupar na unidade -> lista de amigos ), a principal coisa é não colocar IQueryable <Friend> lá , porque esse não é um serviço - é um conjunto de dados que define a função


Coloquei no AdminInfo informações sobre usuários aprovados e banidos pelo administrador atual. No contexto do meu "aplicativo", isso define o papel do Administrador:


  type AdminInfo = { ActiveUsersEmails: string list BanUsersEmails : string list } type UserInfo = { Name:string Surname:string } 

Qual é a diferença de Claim ? É possível criar User.Claims no controlador e obter a mesma coisa?


Ao digitar e falar: modules, o desenvolvedor não precisa procurar exemplos de código em manipuladores no mesmo contexto, ele cria um manipulador e o adiciona ao roteamento e faz tudo compilar


 let AccountPart handler = let getUserInfo ctx = async.Return {Name="Al";Surname="Pacino"} permissionHandler [User;Admin] getUserInfo handler 

getUserInfo recebe dados para o módulo Account , tem acesso ao contexto para obter dados pessoais (este é exatamente esse usuário, administrador)


permissionHandler verifica o token jwt, descriptografa e verifica o acesso, retorna a WebPart original para manter a compatibilidade com o Suave


O código fonte completo pode ser encontrado no github


Obrigado pela atenção!

Source: https://habr.com/ru/post/pt461593/


All Articles