web-dev-qa-db-fra.com

Comment puis-je implémenter une liste de contrôle d'accès dans mon application Web MVC?

Première question

S'il vous plaît, pourriez-vous m'expliquer comment l'ACL le plus simple pourrait être implémenté dans MVC.

Voici la première approche de l'utilisation d'Acl dans Controller ...

<?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!");
    ...    
  }

}
?>

C'est une très mauvaise approche, et c'est moins que nous devons ajouter un morceau de code Acl dans la méthode de chaque contrôleur, mais nous n'avons pas besoin de dépendances supplémentaires!

La prochaine approche consiste à rendre toutes les méthodes du contrôleur private et à ajouter du code ACL dans le contrôleur __call méthode.

<?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!");
    ...   
  }

}
?>

C'est mieux que le code précédent, mais les principaux inconvénients sont ...

  • Toutes les méthodes du contrôleur doivent être privées
  • Nous devons ajouter du code ACL dans la méthode __call de chaque contrôleur.

La prochaine approche consiste à mettre du code Acl dans le contrôleur parent, mais nous devons toujours garder toutes les méthodes du contrôleur enfant privées.

Quelle est la solution? Et quelle est la meilleure pratique? Où dois-je appeler les fonctions Acl pour décider d'autoriser ou d'interdire l'exécution de la méthode.

Deuxième question

La deuxième question concerne l'obtention d'un rôle en utilisant Acl. Imaginons que nous ayons des invités, des utilisateurs et des amis d'utilisateurs. L'utilisateur a un accès limité à la visualisation de son profil que seuls les amis peuvent le voir. Tous les invités ne peuvent pas voir le profil de cet utilisateur. Alors, voici la logique ..

  • nous devons nous assurer que la méthode appelée est le profil
  • nous devons détecter le propriétaire de ce profil
  • nous devons détecter si le spectateur est propriétaire de ce profil ou non
  • nous devons lire les règles de restriction concernant ce profil
  • nous devons décider d'exécuter ou de ne pas exécuter la méthode de profil

La principale question concerne la détection du propriétaire du profil. Nous pouvons détecter qui est le propriétaire du profil exécutant uniquement la méthode du modèle $ model-> getOwner (), mais Acl n'a pas accès au modèle. Comment pouvons-nous mettre cela en œuvre?

J'espère que mes pensées sont claires. Désolé pour mon anglais.

Merci.

94
Kirzilla

Première partie/réponse (implémentation ACL)

À mon humble avis, la meilleure façon d'aborder cela serait d'utiliser motif décorateur , Fondamentalement, cela signifie que vous prenez votre objet et le placez à l'intérieur un autre objet , qui agira comme une coque protectrice. Cela ne vous obligerait PAS à étendre la classe d'origine. Voici un exemple:

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

}

Et voici comment vous utilisez ce type de structure:

// 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();

Comme vous le remarquerez peut-être, cette solution présente plusieurs avantages:

  1. le confinement peut être utilisé sur n'importe quel objet, pas seulement sur les instances de Controller
  2. la vérification de l'autorisation se produit en dehors de l'objet cible, ce qui signifie que:
    • l'objet d'origine n'est pas responsable du contrôle d'accès, adhère à SRP
    • lorsque vous obtenez "autorisation refusée", vous n'êtes pas enfermé dans un contrôleur, plus d'options
  3. vous pouvez injecter ceci instance sécurisée dans tout autre objet, il conservera la protection
  4. envelopper et oublier .. vous pouvez faire semblant que c'est l'objet d'origine, il réagira de la même façon

Mais , il y a aussi un problème majeur avec cette méthode - vous ne pouvez pas vérifier nativement si les implémentations et l'interface des objets sécurisés (qui s'appliquent également pour rechercher les méthodes existantes) ou fait partie d'une chaîne d'héritage.

Deuxième partie/réponse (RBAC pour les objets)

Dans ce cas, la principale différence que vous devez reconnaître est que vous Objets de domaine (dans l'exemple: Profile) lui-même contient des détails sur le propriétaire. Cela signifie que pour que vous puissiez vérifier si (et à quel niveau) l'utilisateur y a accès, il vous faudra modifier cette ligne:

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

Vous avez essentiellement deux options:

  • Fournissez à l'ACL l'objet en question. Mais vous devez faire attention à ne pas violer Loi de Déméter :

    $this->acl->isAllowed( get_class($this->target), $method )
    
  • Demandez tous les détails pertinents et ne fournissez à l'ACL que ce dont elle a besoin, ce qui la rendra également un peu plus conviviale pour les tests unitaires:

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

Quelques vidéos qui pourraient vous aider à trouver votre propre implémentation:

Notes annexes

Vous semblez avoir la compréhension assez courante (et complètement fausse) de ce qu'est le modèle dans MVC. Le modèle n'est pas une classe. Si vous avez une classe nommée FooBarModel ou quelque chose qui hérite de AbstractModel, vous vous trompez.

Dans un MVC approprié, le modèle est une couche qui contient de nombreuses classes. Une grande partie des cours peut être séparée en deux groupes, selon la responsabilité:

- Logique métier du domaine

( en savoir plus: ici et ici ):

Les instances de ce groupe de classes traitent du calcul des valeurs, vérifient les différentes conditions, implémentent les règles de vente et font tout le reste ce que vous appelleriez la "logique métier". Ils n'ont aucune idée de la façon dont les données sont stockées, où elles sont stockées ou même si le stockage existe en premier lieu.

L'objet métier de domaine ne dépend pas de la base de données. Lorsque vous créez une facture, peu importe d'où viennent les données. Il peut s'agir de SQL ou d'une API distante REST, ou même d'une capture d'écran d'un document MSWord. La logique métier ne change pas.

- Accès et stockage des données

Les instances créées à partir de ce groupe de classes sont parfois appelées objets d'accès aux données. Habituellement, les structures qui implémentent Data Mapper modèle (ne pas confondre avec les ORM du même nom .. aucune relation). C'est là que seraient vos instructions SQL (ou peut-être votre DomDocument, car vous le stockez en XML).

En plus des deux parties principales, il existe un autre groupe d'instances/classes, à mentionner:

- Services

C'est là que vos composants tiers et tiers entrent en jeu. Par exemple, vous pouvez considérer "l'authentification" comme un service, qui peut être fourni par le vôtre ou par un code externe. "Expéditeur de courrier" serait également un service, qui pourrait assembler un objet de domaine avec un PHPMailer ou SwiftMailer, ou votre propre composant expéditeur de courrier.

Une autre source de services est l'abstraction sur les couches d'accès au domaine et aux données. Ils sont créés pour simplifier le code utilisé par les contrôleurs. Par exemple: la création d'un nouveau compte utilisateur peut nécessiter de travailler avec plusieurs objets de domaine et mappeurs. Mais, en utilisant un service, il n'aura besoin que d'une ou deux lignes dans le contrôleur.

Ce que vous devez vous rappeler lorsque vous faites des services, c'est que toute la couche est censée être mince. Il n'y a pas de logique métier dans les services. Ils ne sont là que pour jongler avec les objets de domaine, les composants et les mappeurs.

Une des choses qu'ils ont tous en commun serait que les services n'affectent pas la couche View de manière directe et sont autonomes à un tel point qu'ils peuvent être (et quitter souvent - sont) utilisés en dehors de la structure MVC elle-même. De telles structures autonomes facilitent également la migration vers un cadre/une architecture différente, en raison du couplage extrêmement faible entre le service et le reste de l'application.

182
tereško

ACL et contrôleurs

Tout d'abord: ce sont des choses/couches différentes le plus souvent. Lorsque vous critiquez le code de contrôleur exemplaire, il rassemble les deux - le plus évidemment trop serré.

tereško déjà décrit une façon de découpler davantage avec le motif décorateur.

Je ferais d'abord un pas en arrière pour rechercher le problème d'origine auquel vous êtes confronté et en discuter un peu ensuite.

D'une part, vous voulez avoir des contrôleurs qui font juste le travail auquel ils sont commandés (commande ou action, appelons-la commande).

D'autre part, vous voulez pouvoir mettre ACL dans votre application. Le domaine de travail de ces ACL devrait être - si j'ai bien compris votre question - de contrôler l'accès à certaines commandes de vos applications.

Ce type de contrôle d'accès a donc besoin d'autre chose qui les rapproche. En fonction du contexte dans lequel une commande est exécutée, ACL entre en jeu et il faut décider si une commande spécifique peut ou non être exécutée par un sujet spécifique (par exemple l'utilisateur).

Résumons à ce point ce que nous avons:

  • Commander
  • ACL
  • Utilisateur

Le composant ACL est central ici: il doit savoir au moins quelque chose sur la commande (pour identifier la commande pour être précis) et il doit être capable d'identifier l'utilisateur. Les utilisateurs sont normalement facilement identifiés par un identifiant unique. Mais souvent, dans les applications Web, il y a des utilisateurs qui ne sont pas du tout identifiés, souvent appelés invités, anonymes, tout le monde, etc. Pour cet exemple, nous supposons que l'ACL peut consommer un objet utilisateur et encapsuler ces détails. L'objet utilisateur est lié à l'objet de demande d'application et l'ACL peut le consommer.

Qu'en est-il de l'identification d'une commande? Votre interprétation du modèle MVC suggère qu'une commande est composée d'un nom de classe et d'un nom de méthode. Si nous regardons de plus près, il existe même des arguments (paramètres) pour une commande. Il est donc valable de demander ce qui identifie exactement une commande? Le nom de classe, le nom de méthode, le nombre ou les noms d'arguments, même les données contenues dans l'un des arguments ou un mélange de tout cela?

Selon le niveau de détail dont vous avez besoin pour identifier une commande dans votre ACL, cela peut varier considérablement. Pour l'exemple, gardons-le simplement et spécifions qu'une commande est identifiée par le nom de classe et le nom de méthode.

Ainsi, le contexte de l'appartenance de ces trois parties (ACL, commande et utilisateur) est maintenant plus clair.

Nous pourrions dire qu'avec un composant ACL imaginaire, nous pouvons déjà faire ce qui suit:

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

Voyez simplement ce qui se passe ici: En rendant à la fois la commande et l'utilisateur identifiables, l'ACL peut faire son travail. Le travail de l'ACL n'est pas lié au travail de l'objet utilisateur et de la commande concrète.

Il ne manque qu'une partie, cela ne peut pas vivre dans l'air. Et ce n'est pas le cas. Vous devez donc localiser l'endroit où le contrôle d'accès doit intervenir. Voyons ce qui se passe dans une application Web standard:

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

Pour localiser cet endroit, nous savons que cela doit être avant que la commande concrète ne soit exécutée, afin que nous puissions réduire cette liste et ne devons examiner que les endroits (potentiels) suivants:

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

À un moment donné de votre application, vous savez qu'un utilisateur spécifique a demandé d'exécuter une commande concrète. Vous faites déjà une sorte d'ACL ici: Si un utilisateur demande une commande qui n'existe pas, vous ne permettez pas à cette commande de s'exécuter. Ainsi, où que cela se produise dans votre application, cela peut être un bon endroit pour ajouter les "vrais" contrôles ACL:

La commande a été localisée et nous pouvons créer son identification afin que l'ACL puisse y faire face. Dans le cas où la commande n'est pas autorisée pour un utilisateur, la commande ne sera pas exécutée (action). Peut-être un CommandNotAllowedResponse au lieu du CommandNotFoundResponse dans le cas où une requête n'a pas pu être résolue sur une commande concrète.

L'endroit où le mappage d'une requête HTTP concrète est mappé sur une commande est souvent appelé Routage . Comme le routage a déjà la tâche de localiser une commande, pourquoi ne pas l'étendre pour vérifier si la commande est réellement autorisée par ACL? Par exemple. en étendant Router à un routeur compatible ACL: RouterACL. Si votre routeur ne connaît pas encore le User, alors le Router n'est pas le bon endroit, car pour que l'ACL fonctionne, non seulement la commande mais aussi l'utilisateur doivent être identifiés. Donc, cet endroit peut varier, mais je suis sûr que vous pouvez facilement trouver l'endroit que vous devez étendre, car c'est l'endroit qui remplit les exigences de l'utilisateur et de la commande:

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

L'utilisateur est disponible depuis le début, Commandez d'abord avec Request(Command).

Ainsi, au lieu de placer vos vérifications ACL dans l'implémentation concrète de chaque commande , vous la placez devant elle. Vous n'avez pas besoin de modèles lourds, de magie ou autre, l'ACL fait son travail, l'utilisateur fait son travail et surtout la commande fait son travail: juste la commande, rien d'autre. La commande n'a aucun intérêt à savoir si les rôles s'appliquent à elle, qu'elle soit gardée quelque part ou non.

Il suffit donc de séparer les choses qui ne s’appartiennent pas. Utilisez une légère reformulation du principe de responsabilité unique (SRP) : il ne devrait y avoir qu'une seule raison de modifier une commande - car la commande a changé. Pas parce que vous introduisez maintenant ACL'ing dans votre application. Pas parce que vous changez d'objet utilisateur. Pas parce que vous migrez d'une interface HTTP/HTML vers une interface SOAP ou ligne de commande.

L'ACL dans votre cas contrôle l'accès à une commande, pas la commande elle-même.

16
hakre

Une possibilité consiste à encapsuler tous vos contrôleurs dans une autre classe qui étend Controller et à lui déléguer tous les appels de fonction à l'instance encapsulée après avoir vérifié l'autorisation.

Vous pouvez également le faire plus en amont, dans le répartiteur (si votre application en a effectivement un) et rechercher les autorisations en fonction des URL, au lieu des méthodes de contrôle.

edit : Que vous ayez besoin d'accéder à une base de données, un serveur LDAP, etc. est orthogonal à la question. Mon point était que vous pouviez implémenter une autorisation basée sur des URL au lieu de méthodes de contrôleur. Celles-ci sont plus robustes car vous ne changerez généralement pas vos URL (type d'interface publique de la zone URL), mais vous pourriez aussi bien changer les implémentations de vos contrôleurs.

En règle générale, vous disposez d'un ou plusieurs fichiers de configuration dans lesquels vous mappez des modèles d'URL spécifiques à des méthodes d'authentification et des directives d'autorisation spécifiques. Le répartiteur, avant d'envoyer la demande aux contrôleurs, détermine si l'utilisateur est autorisé et interrompt la répartition s'il ne l'est pas.

13
Artefacto