Angular - Como verificar a autorização com base na função e nos estados da entidade

Hoje em dia é muito comum encontrá-lo no estado de construção de autenticação e autorização em seu aplicativo, é fácil procurar na Web bibliotecas e técnicas de autorização sempre para encontrar soluções que oferecem apenas autorização de base de função que impede apenas o acesso a uma página. quase sempre ocorre que você precisa de outra coisa, a autorização do estado da entidade.

Para o TL; DR, aqui está a demonstração e o código usado na demonstração.

Créditos de imagem

O que diabos é a autorização do estado da entidade?

Bem, acho que não havia nada melhor na minha cabeça para nomear isso enquanto escrevia este post. No entanto, mais ou menos o que estou tentando dizer está relacionado às situações em que você precisa conceder ou não conceder ao usuário atual a capacidade de aplicar uma ação específica se o estado atual da entidade se basear no estado X e em uma habilidade diferente quando a entidade O estado depende do estado Y e ficou pior quando tudo isso deveria estar na mesma tela ou mesmo no mesmo componente. Quaisquer bibliotecas com autorização com base em função o salvarão para esse problema.
Cansado de ler artigos diferentes com soluções de prevenção de páginas baseadas em funções, comecei a pensar e codificar sem ter em mente ainda, e de repente estava lá, encontrei uma solução simples, direta e muito flexível para resolver esse tipo de problemas e é composta por quatro componentes.

  • Um serviço para fornecer as informações atuais do usuário (o que realmente importa é uma maneira de fornecer as funções de usuário às quais o usuário atual pertence ou foi concedido para desempenhar no aplicativo)
  • Um mapa de permissões de fluxo de trabalho (arquivo JSON)
  • Um serviço de permissões para realmente fazer a verificação de autorização
  • Uma diretiva para consumir a verificação do serviço de autorização

Etapa 1: obter funções de usuário atuais

Implemente um serviço para recuperar do lado do servidor (primeira vez) ou da sessão ou dos cookies, conforme preferir para usos subsequentes, o importante aqui é fornecer a lista de habilidades (funções) do usuário.

// Exemplo
{
  nome: "John Doe",
  e-mail: '[email protected]',
  papéis: ['seller', 'seller_manager'], <- é isso que importa
  accessToken: 'muitos carteladores representam um acesso a kenhahahaaaa!'
  ... mais coisas
}

No meu caso, mais ou menos, é assim:

// importa aqui ...
@Injectable ()
classe de exportação CurrentUserService {
  private userSubject = novo ReplaySubject  (1);
  hasUser privado = false;

  construtor (usersApi privado: UserApi) {
  }

  public getUser (): Observable  {
    if (! this.hasUser) {
      this.fetchUser ();
    }
    retorne this.userSubject.asObservable ();
  }

  public fetchUser (): void {
      this.usersApi.getCurrent () // <== chamada http para buscar userInfo
        .subscribe (usuário => {
          // o usuário deve conter funções concedidas
          this.hasUser = true;
          this.userSubject.next (user);
          this.userSubject.complete ();
        }, (erro) => {
          this.hasUser = false;
          this.userSubject.error (erro);
        });
  }

}

Segunda etapa: crie seu mapa de fluxo de trabalho e permissões

Isso nada mais é do que o mapeamento do que podemos fazer e de quem podemos fazer, construindo uma árvore com as diferentes entidades e seus próprios estados. por exemplo, vamos imaginar o seguinte processo de vendas; Nosso aplicativo pode ter vários tipos de função. No nosso exemplo, vamos mapear as funções para VENDEDOR, soluções ARCHITECT e CLIENT.

Para não falar sobre o processo:

  • Primeiro, o VENDEDOR coloca uma oportunidade de vendas executando a ação Adicionar nova oportunidade para que o estado da oportunidade provavelmente seja criado
  • Nesse momento, o CLIENTE E O VENDEDOR podem adicionar os requisitos à oportunidade, para que ambos possam aplicar a ação Adicionar requisitos quando os requisitos forem colocados, e o status da oportunidade poderá mudar para enviado
  • Depois que os requisitos são colocados, o ARCHITECT pode querer adicionar uma solução, para que ele precise de uma ação: Fazer upload da solução e provavelmente o estado pode mudar para resolvido
  • Depois que a solução é fornecida, o CLIENTE pode querer aceitar, para que ele precise de uma ação para aprovar a solução e o estado mudará para solution_approved

Vamos cortar o processo aqui, caso contrário, isso aumentaria muito e não é o caso desta leitura. Portanto, com base nesse processo, o mapeamento e assumindo que a entidade da oportunidade tem um campo que rastreia o estado, nosso fluxo de trabalho ficaria assim:

{
  "oportunidade": {
    "addOpportunity": {"allowedRoles": ["SELLER"]}},
    "criado": {
      "addRequirement": {"allowedRoles": ["VENDEDOR", "CLIENTE"]}
    }
    "submetido": {
      "addSolution": {"allowedRoles": ["ARCHITECT"]}
    }
    "resolvido": {
      "approveSolution": {"allowedRoles": ["CLIENT"]}
    }
}

Etapa 3: o serviço de autorização de verificação para consumir o mapa de worflow e permissões

Agora que o processo foi mapeado em um mapa de fluxo de trabalho e permissões, precisamos criar um serviço para consumi-lo e verificar se o usuário está autorizado ou não, e pode ter a seguinte aparência:

// importa instruções aqui
// a propósito, em angular-cli, podemos colocar o arquivo JSON no diretório
// enviroment.ts
@Injectable ()
classe de exportação WorkflowEvents {
  privado somente leitura WORKFLOW_EVENTS = environment ['workflow'];
  private userRoles: Set ;
  // você se lembra da etapa 1? é usado aqui
  construtor (privado currentUserService: CurrentUserService) {
  }
  // retorna um observável booleano
  public checkAuthorization (caminho: qualquer): Observável  {
    // estamos carregando as funções apenas uma vez
   if (! this.userRoles) {
      retornar this.currentUserService.getUser ()
        .map (currentUser => currentUser.roles)
        .do (role => {
          const papéis = papéis.map (papel => papel.nome);
          this.userRoles = new Set (funções);
        })
        .map (role => this.doCheckAuthorization (path));
    }
    return Observable.of (this.doCheckAuthorization (path));
  }

  private doCheckAuthorization (caminho: string []): booleano {
    if (path.length) {
      const entrada = this.findEntry (this.WORKFLOW_EVENTS, caminho);
      if (entry && entry ['allowedRoles']
             && this.userRoles.size) {
        return entry.permittedRoles
        .alguns (allowedRole => this.userRoles.has (allowedRole));
      }
      retorna falso;
    }
    retorna falso;
  }
/ **
 * Encontre recursivamente a entrada do mapa do fluxo de trabalho com base nas cadeias de caminho
 * /
private findEntry (currentObject: any, chaves: string [], index = 0) {
    chave const = chaves [índice];
    if (currentObject [key] && index 

Basicamente, o que ele faz é procurar uma entrada válida e verificar se as funções de usuário atuais estão incluídas nos allowedRoles.

Pasta 4: A diretiva

Depois que tivermos as funções de usuário atuais, uma árvore de permissões de fluxo de trabalho e um serviço para verificar a autorização das funções de usuário atuais, agora precisamos de uma maneira de dar vida a isso, e a melhor maneira no 2/4 angular é uma diretiva. Inicialmente, a diretiva que escrevi era uma diretiva de atributo que alternava o atributo CSS de exibição, mas isso poderia levar a problemas de desempenho, porque os componentes decentes ainda estão sendo carregados no DOM, por isso é melhor usar diretivas estruturais (Obrigado ao meu colega Petyo Cholakov por essa boa captura , veja a diferença aqui), pois podemos modificar o DOM do elemento de destino e seus descendentes para evitar o carregamento de elementos não utilizados.

@Directive ({
  seletor: '[appCanAccess]'
})
classe de exportação CanAccessDirective implementa OnInit, OnDestroy {
  @Input ('appCanAccess') appCanAccess: string | corda[];
  permissão particular $: assinatura;

  construtor (private templateRef: TemplateRef ,
              private viewContainer: ViewContainerRef,
              private workflowEvents: WorkflowEvents) {
  }

  ngOnInit (): void {
    this.applyPermission ();
  }

  private applyPermission (): void {
    this.permission $ = this.workflowEvents
                        .checkAuthorization (this.appCanAccess)
      .subscribe (autorizado => {
        se (autorizado) {
          this.viewContainer.createEmbeddedView (this.templateRef);
        } outro {
          this.viewContainer.clear ();
        }
      });
  }

  ngOnDestroy (): void {
    this.permission $ .unsubscribe ();
  }

}

Finalmente, o trabalho resultante

Agora, o que temos tudo o que precisamos é hora de colocá-los em ação. Portanto, em nosso modelo HTML, a única coisa que precisamos fazer é algo como o código a seguir

Vamos supor que temos um componente de amostra que inclui o objeto de oportunidade:

@Componente({
  seletor: 'sp-pricing-panel',
  modelo: `

    
 
 
 
 `
})
classe de exportação SampleComponent implementa OnInit {

  @Input () OpportunityObject: qualquer;
  constructor () {
  }

  ngOnInit () {
  }
}

E é isso, podemos ter um componente simples apresentando comportamento, dependendo das funções do usuário e do estado da entidade.

Graças aos meus colegas Petyo e Govind pelas dificuldades e críticas à minha má codificação, pudemos encontrar esta solução que pode funcionar perfeitamente com nossas necessidades, espero que isso ajude você também.

JUN 2018, amostra pequena funcionando => https://emsedano.github.io/ng-entity-state/