Como posso implementar uma Lista de Controle de Acesso em meu aplicativo Web MVC?


96

Primeira pergunta

Por favor, você poderia me explicar como o ACL mais simples pode ser implementado no MVC.

Aqui está a primeira abordagem de uso de Acl no controlador ...

<?php
class MyController extends Controller {

  public function myMethod() {        
    //It is just abstract code
    $acl = new Acl();
    $acl->setController('MyController');
    $acl->setMethod('myMethod');
    $acl->getRole();
    if (!$acl->allowed()) die("You're not allowed to do it!");
    ...    
  }

}
?>

É uma abordagem muito ruim, e o menos é que temos que adicionar parte do código Acl em cada método do controlador, mas não precisamos de nenhuma dependência adicional!

A próxima abordagem é criar todos os métodos do controlador privatee adicionar o código ACL ao __callmétodo do controlador .

<?php
class MyController extends Controller {

  private function myMethod() {
    ...
  }

  public function __call($name, $params) {
    //It is just abstract code
    $acl = new Acl();
    $acl->setController(__CLASS__);
    $acl->setMethod($name);
    $acl->getRole();
    if (!$acl->allowed()) die("You're not allowed to do it!");
    ...   
  }

}
?>

É melhor do que o código anterior, mas os principais pontos negativos são ...

  • Todos os métodos do controlador devem ser privados
  • Temos que adicionar o código ACL no método __call de cada controlador.

A próxima abordagem é colocar o código Acl no controlador pai, mas ainda precisamos manter todos os métodos do controlador filho privados.

Qual é a solução? E qual é a melhor prática? Onde devo chamar funções Acl para decidir permitir ou proibir o método a ser executado.

Segunda questão

A segunda pergunta é sobre como obter o papel usando Acl. Vamos imaginar que temos convidados, usuários e amigos de usuários. O usuário restringiu o acesso à visualização de seu perfil, de forma que apenas amigos podem visualizá-lo. Todos os convidados não podem ver o perfil deste usuário. Então, aqui está a lógica ..

  • temos que garantir que o método que está sendo chamado é o perfil
  • temos que detectar o dono deste perfil
  • temos que detectar se o visualizador é o proprietário deste perfil ou não
  • temos que ler as regras de restrição sobre este perfil
  • temos que decidir executar ou não executar o método de perfil

A principal questão é sobre como detectar o proprietário do perfil. Podemos detectar quem é o dono do perfil apenas executando o método $ model-> getOwner () do modelo, mas Acl não tem acesso ao modelo. Como podemos implementar isso?

Espero que meus pensamentos estejam claros. Desculpe pelo meu Inglês.

Obrigado.


1
Eu nem entendo porque você precisaria de "Listas de controle de acesso" para interações do usuário. Você não diria algo como if($user->hasFriend($other_user) || $other_user->profileIsPublic()) $other_user->renderProfile()(senão, exibir "Você não tem acesso ao perfil deste usuário" ou algo parecido? Não entendi.
Buttle Butkus

2
Provavelmente, porque Kirzilla deseja gerenciar todas as condições de acesso em um só lugar - principalmente na configuração. Portanto, qualquer alteração nas permissões pode ser feita no Admin em vez de alterar o código.
Mariyo

Respostas:


185

Primeira parte / resposta (implementação ACL)

Na minha humilde opinião, a melhor maneira de abordar isso seria usar o padrão decorator . Basicamente, isso significa que você pega seu objeto e o coloca dentro de outro objeto, que funcionará como uma concha de proteção. Isso NÃO exigiria que você estendesse a aula original. Aqui está um exemplo:

class SecureContainer
{

    protected $target = null;
    protected $acl = null;

    public function __construct( $target, $acl )
    {
        $this->target = $target;
        $this->acl = $acl;
    }

    public function __call( $method, $arguments )
    {
        if ( 
             method_exists( $this->target, $method )
          && $this->acl->isAllowed( get_class($this->target), $method )
        ){
            return call_user_func_array( 
                array( $this->target, $method ),
                $arguments
            );
        }
    }

}

E é assim que você usa esse tipo de estrutura:

// assuming that you have two objects already: $currentUser and $controller
$acl = new AccessControlList( $currentUser );

$controller = new SecureContainer( $controller, $acl );
// you can execute all the methods you had in previous controller 
// only now they will be checked against ACL
$controller->actionIndex();

Como você pode notar, esta solução tem várias vantagens:

  1. a contenção pode ser usada em qualquer objeto, não apenas instâncias de Controller
  2. a verificação de autorização acontece fora do objeto de destino, o que significa que:
    • objeto original não é responsável pelo controle de acesso, segue SRP
    • quando você obtém "permissão negada", você não está trancado dentro de um controlador, mais opções
  3. você pode injetar esta instância segura em qualquer outro objeto, ela manterá a proteção
  4. embrulhe e esqueça .. você pode fingir que é o objeto original, ele reagirá da mesma maneira

Mas , há um grande problema com esse método também - você não pode verificar nativamente se o objeto seguro implementa e interface (o que também se aplica à pesquisa de métodos existentes) ou faz parte de alguma cadeia de herança.

Segunda parte / resposta (RBAC para objetos)

Nesse caso, a principal diferença que você deve reconhecer é que seus Objetos de Domínio (por exemplo Profile:) contêm detalhes sobre o proprietário. Isso significa que para você verificar se (e em que nível) o usuário tem acesso a ele, será necessário alterar esta linha:

$this->acl->isAllowed( get_class($this->target), $method )

Essencialmente, você tem duas opções:

  • Forneça à ACL o objeto em questão. Mas você deve ter cuidado para não violar a Lei de Deméter :

    $this->acl->isAllowed( get_class($this->target), $method )
  • Solicite todos os detalhes relevantes e forneça à ACL apenas o que ela precisa, o que também a tornará um pouco mais amigável para testes de unidade:

    $command = array( get_class($this->target), $method );
    /* -- snip -- */
    $this->acl->isAllowed( $this->target->getPermissions(), $command )
    

Alguns vídeos que podem ajudá-lo a criar sua própria implementação:

Notas laterais

Você parece ter um entendimento bastante comum (e completamente errado) do que é o Modelo em MVC. O modelo não é uma classe . Se você tem uma classe nomeada FooBarModelou algo que herda AbstractModel, você está fazendo isso errado.

No MVC adequado, o modelo é uma camada, que contém muitas classes. Grande parte das turmas podem ser separadas em dois grupos, com base na responsabilidade:

- Domínio Business Logic

( leia mais : aqui e aqui ):

As instâncias desse grupo de classes tratam do cálculo de valores, verificam as diferentes condições, implementam regras de vendas e fazem todo o resto o que você chamaria de "lógica de negócios". Eles não têm ideia de como os dados são armazenados, onde estão armazenados ou mesmo se existe armazenamento em primeiro lugar.

O objeto Domain Business não depende do banco de dados. Quando você está criando uma fatura, não importa de onde vêm os dados. Pode ser de SQL ou de uma API REST remota, ou até mesmo uma captura de tela de um documento MSWord. A lógica de negócios não muda.

- Acesso e armazenamento de dados

As instâncias feitas a partir desse grupo de classes às vezes são chamadas de Objetos de Acesso a Dados. Normalmente estruturas que implementam o padrão Data Mapper (não confunda com ORMs de mesmo nome .. sem relação). É aqui que estariam suas instruções SQL (ou talvez seu DomDocument, porque você o armazena em XML).

Ao lado das duas partes principais, existe mais um grupo de instâncias / classes, que deve ser mencionado:

- Serviços

É aqui que entram em jogo seus componentes e de terceiros. Por exemplo, você pode pensar em "autenticação" como um serviço, que pode ser fornecido por você ou algum código externo. Além disso, "remetente de correio" seria um serviço, que poderia unir algum objeto de domínio com um PHPMailer ou SwiftMailer, ou seu próprio componente de remetente de correio.

Outra fonte de serviços é a abstração em camadas de domínio e acesso a dados. Eles são criados para simplificar o código usado pelos controladores. Por exemplo: a criação de uma nova conta de usuário pode exigir o trabalho com vários objetos de domínio e mapeadores . Mas, ao usar um serviço, será necessário apenas uma ou duas linhas no controlador.

O que você precisa lembrar ao fazer serviços é que toda a camada deve ser fina . Não há lógica de negócios em serviços. Eles estão lá apenas para manipular objetos de domínio, componentes e mapeadores.

Uma das coisas que todos eles têm em comum é que os serviços não afetam a camada View de nenhuma maneira direta e são autônomos a tal ponto que podem ser (e encerrar com frequência - são) usados ​​fora da própria estrutura MVC. Além disso, essas estruturas autossustentáveis ​​tornam a migração para uma estrutura / arquitetura diferente muito mais fácil, devido ao acoplamento extremamente baixo entre o serviço e o restante do aplicativo.


34
Acabei de aprender mais em 5 minutos relendo isso, do que em meses. Você concorda com: thin controllers despacham para serviços que coletam dados de visualização? Além disso, se você aceitar perguntas diretamente, envie-me uma mensagem.
Stephane

2
Eu concordo parcialmente. A coleta de dados da visualização acontece fora da tríade MVC, quando você inicializa a Requestinstância (ou algum análogo dela). O controlador apenas extrai dados da Requestinstância e passa a maior parte deles para os serviços adequados (alguns deles também vão para visualização). Os serviços executam operações que você os comandou a fazer. Então, quando o view está gerando a resposta, ele solicita dados dos serviços e, com base nessas informações, gera a resposta. A referida resposta pode ser HTML feita a partir de vários modelos ou apenas um cabeçalho de localização HTTP. Depende do estado definido pelo controlador.
tereško

4
Para usar uma explicação simplificada: o controlador "grava" no modelo e visualiza, visualiza "lê" no modelo. A camada de modelo é a estrutura passiva em todos os padrões relacionados à Web que foram inspirados pelo MVC.
tereško

@Stephane, quanto a fazer perguntas diretamente, você sempre pode me enviar uma mensagem no twitter. Ou você estava questionando um tipo "longo", que não pode ser amontoado em 140 caracteres?
tereško

Leituras do modelo: isso significa alguma função ativa para o modelo? Eu nunca ouvi isso antes. Sempre posso te enviar um link via twitter, se essa for a sua preferência. Como você pode ver, essas respostas se transformam em conversas rapidamente e eu estava tentando ser respeitoso com este site e seus seguidores no Twitter.
Stephane

16

ACL e controladores

Em primeiro lugar: na maioria das vezes, trata-se de coisas / camadas diferentes. Conforme você critica o código do controlador exemplar, ele coloca os dois juntos - obviamente muito apertados.

tereško já esboçou uma maneira de desacoplar isso mais com o padrão do decorador.

Eu daria um passo para trás primeiro para procurar o problema original que você está enfrentando e discutiria um pouco depois.

Por um lado, você quer ter controladores que apenas façam o trabalho para o qual são comandados (comando ou ação, vamos chamá-lo de comando).

Por outro lado, você deseja poder colocar ACL em seu aplicativo. O campo de trabalho dessas ACLs deve ser - se bem entendi sua pergunta - controlar o acesso a determinados comandos de seus aplicativos.

Esse tipo de controle de acesso, portanto, precisa de algo mais que reúna esses dois. Com base no contexto em que um comando é executado, o ACL entra em ação e decisões precisam ser tomadas se um comando específico pode ou não ser executado por um sujeito específico (por exemplo, o usuário).

Vamos resumir até este ponto o que temos:

  • Comando
  • ACL
  • Do utilizador

O componente ACL é central aqui: ele precisa saber pelo menos algo sobre o comando (para identificar o comando para ser preciso) e precisa ser capaz de identificar o usuário. Os usuários normalmente são facilmente identificados por um ID exclusivo. Mas muitas vezes em aplicativos da web há usuários que não são identificados de forma alguma, geralmente chamados de convidados, anônimos, todos, etc. Para este exemplo, assumimos que a ACL pode consumir um objeto de usuário e encapsular esses detalhes. O objeto de usuário está vinculado ao objeto de solicitação do aplicativo e a ACL pode consumi-lo.

Que tal identificar um comando? Sua interpretação do padrão MVC sugere que um comando é composto de um nome de classe e um nome de método. Se olharmos mais de perto, existem até argumentos (parâmetros) para um comando. Portanto, é válido perguntar o que exatamente identifica um comando? O nome da classe, o nome do método, o número ou nomes dos argumentos, até mesmo os dados dentro de qualquer um dos argumentos ou uma mistura de tudo isso?

Dependendo do nível de detalhe que você precisa para identificar um comando em seu ACL, isso pode variar muito. Para o exemplo, vamos mantê-lo simples e especificar que um comando seja identificado pelo nome da classe e pelo nome do método.

Portanto, o contexto de como essas três partes (ACL, Comando e Usuário) pertencem umas às outras está agora mais claro.

Poderíamos dizer, com um componente ACL imaginário, já podemos fazer o seguinte:

$acl->commandAllowedForUser($command, $user);

Veja o que está acontecendo aqui: ao tornar o comando e o usuário identificáveis, a ACL pode fazer seu trabalho. O trabalho da ACL não está relacionado ao trabalho do objeto de usuário e do comando concreto.

Só falta uma parte, isso não pode viver no ar. E isso não acontece. Portanto, você precisa localizar o local onde o controle de acesso precisa ser acionado. Vamos dar uma olhada no que acontece em um aplicativo da web padrão:

User -> Browser -> Request (HTTP)
   -> Request (Command) -> Action (Command) -> Response (Command) 
   -> Response(HTTP) -> Browser -> User

Para localizar esse lugar, sabemos que deve ser antes que o comando concreto seja executado, portanto, podemos reduzir essa lista e só precisamos olhar para os seguintes locais (potenciais):

User -> Browser -> Request (HTTP)
   -> Request (Command)

Em algum ponto de seu aplicativo, você sabe que um usuário específico solicitou a execução de um comando concreto. Você já faz algum tipo de ACL aqui: Se um usuário solicitar um comando que não existe, você não permite que esse comando seja executado. Então, onde quer que isso aconteça em seu aplicativo, pode ser um bom lugar para adicionar as verificações de ACL "reais":

O comando foi localizado e podemos criar a identificação dele para que a ACL possa lidar com ele. Caso o comando não seja permitido para um usuário, o comando não será executado (ação). Talvez um, em CommandNotAllowedResponsevez de CommandNotFoundResponsepara o caso, uma solicitação não pudesse ser resolvida em um comando concreto.

O local onde o mapeamento de um HTTPRequest concreto é mapeado em um comando costuma ser chamado de Roteamento . Como o Routing já tem o trabalho de localizar um comando, por que não estendê-lo para verificar se o comando é realmente permitido por ACL? Por exemplo, alargando o Router a um roteador ciente ACL: RouterACL. Se o seu roteador ainda não conhece o User, então Routernão é o lugar certo, pois para o ACL funcionar não só o comando, mas também o usuário deve estar identificado. Portanto, este local pode variar, mas tenho certeza de que você pode localizar facilmente o local que precisa estender, porque é o local que atende aos requisitos de usuário e comando:

User -> Browser -> Request (HTTP)
   -> Request (Command)

O usuário está disponível desde o início, Comando primeiro com Request(Command).

Portanto, em vez de colocar suas verificações de ACL dentro da implementação concreta de cada comando, você o coloca antes dele. Você não precisa de nenhum padrão pesado, mágica ou qualquer outra coisa, o ACL faz o seu trabalho, o usuário faz o seu trabalho e especialmente o comando faz o seu trabalho: apenas o comando, nada mais. O comando não tem interesse em saber se as funções se aplicam ou não a ele, se está guardado em algum lugar ou não.

Portanto, mantenha separadas as coisas que não pertencem uma à outra. Use uma pequena reformulação do Princípio de Responsabilidade Única (SRP) : Deve haver apenas um motivo para alterar um comando - porque o comando foi alterado. Não porque agora você introduz ACL'ing em seu aplicativo. Não porque você troca o objeto Usuário. Não porque você migra de uma interface HTTP / HTML para um SOAP ou interface de linha de comando.

A ACL no seu caso controla o acesso a um comando, não o comando em si.


Duas perguntas: CommandNotFoundResponse & CommandNotAllowedResponse: você passaria isso da classe ACL para o roteador ou controlador e esperaria uma resposta universal? 2: Se você quisesse incluir métodos + atributos, como você lidaria com isso?
Stephane

1: Resposta é resposta, aqui não é do ACL, mas do roteador, o ACL ajuda o roteador a descobrir o tipo de resposta (não encontrado, especialmente: proibido). 2: Depende. Se você quer dizer atributos como parâmetros de ações e precisa de ACL com parâmetros, coloque-os sob ACL.
hakre

13

Uma possibilidade é agrupar todos os seus controladores em outra classe que estenda Controller e fazer com que ele delegue todas as chamadas de função à instância agrupada após verificar a autorização.

Você também pode fazer mais upstream, no dispatcher (se seu aplicativo realmente tiver um) e consultar as permissões com base nas URLs, em vez de métodos de controle.

editar : se você precisa acessar um banco de dados, um servidor LDAP, etc. é ortogonal à questão. Meu ponto é que você pode implementar uma autorização baseada em URLs em vez de métodos de controlador. Eles são mais robustos porque você normalmente não mudará seus URLs (URLs como área de interface pública), mas você também pode mudar as implementações de seus controladores.

Normalmente, você tem um ou vários arquivos de configuração nos quais mapeia padrões de URL específicos para métodos de autenticação e diretivas de autorização específicos. O despachante, antes de despachar a solicitação para os controladores, determina se o usuário está autorizado e aborta o despacho se não estiver.


Por favor, você poderia atualizar sua resposta e adicionar mais detalhes sobre o Dispatcher. Eu tenho o despachante - ele detecta qual método do controlador devo chamar por URL. Mas não consigo entender como posso obter a função (preciso acessar o banco de dados para fazer isso) no Dispatcher. Espero ouvir você em breve.
Kirzilla

Aha, entendi sua ideia. Devo decidir permitir a execução ou não sem acessar o método! Afirmativo! A última questão não resolvida - como acessar o modelo de Acl. Alguma ideia?
Kirzilla

@Kirzilla Tenho os mesmos problemas com controladores. Parece que as dependências devem estar em algum lugar. Mesmo que a ACL não seja, e quanto à camada do modelo? Como você pode evitar que isso seja uma dependência?
Stephane
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.