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

96

Première question

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

Voici la première approche d'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!

L'approche suivante consiste à créer toutes les méthodes du contrôleur privateet à ajouter du code ACL dans la __callméthode du contrôleur .

<?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.

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

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 du rôle en utilisant Acl. Imaginons que nous ayons des invités, des utilisateurs et des amis des utilisateurs. L'utilisateur a un accès restreint à la visualisation de son profil que seuls les amis peuvent 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 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 sur ce profil
  • nous devons décider d'exécuter ou de ne pas exécuter la méthode de profil

La question principale est de détecter le propriétaire du profil. Nous pouvons détecter qui est propriétaire du profil uniquement en exécutant 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.

Je vous remercie.

Kirzilla
la source
1
Je ne comprends même pas pourquoi vous auriez besoin de "listes de contrôle d'accès" pour les interactions des utilisateurs. Ne diriez-vous pas simplement quelque chose comme if($user->hasFriend($other_user) || $other_user->profileIsPublic()) $other_user->renderProfile()(sinon, affichez "Vous n'avez pas accès au profil de cet utilisateur" ou quelque chose comme ça? Je ne comprends pas.
Buttle Butkus
2
Probablement, parce que Kirzilla veut gérer toutes les conditions d'accès sur un seul endroit - principalement en configuration. Ainsi, toute modification des autorisations peut être effectuée dans Admin au lieu de modifier le code.
Mariyo

Réponses:

185

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

À mon humble avis, la meilleure façon d'aborder cela serait d'utiliser un motif décorateur.En gros, cela signifie que vous prenez votre objet et le placez à l' intérieur d' 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 des instances de Controller
  2. vérifier si 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 au SRP
    • lorsque vous obtenez une "permission refusée", vous n'êtes pas enfermé dans un contrôleur, plus d'options
  3. vous pouvez injecter cette instance sécurisée dans n'importe quel autre objet, elle conservera la protection
  4. enveloppez-le et oubliez-le .. vous pouvez prétendre que c'est l'objet original, il réagira de la même manière

Mais , il y a aussi un problème majeur avec cette méthode - vous ne pouvez pas vérifier nativement si l'objet sécurisé implémente et l'interface (qui s'applique également à la recherche de 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 vos objets de domaine (dans l'exemple Profile:) contiennent eux-mêmes 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 changer cette ligne:

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

Essentiellement, vous avez deux options:

  • Fournissez l'ACL avec l'objet en question. Mais vous devez faire attention à ne pas violer la 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 à créer votre propre implémentation:

Notes d'accompagnement

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 FooBarModelou quelque chose qui hérite, AbstractModelvous le faites mal.

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

- Logique métier de 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 des 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 manière dont les données sont stockées, où elles sont stockées ou même si le stockage existe en premier lieu.

Les objets métier de domaine ne dépendent pas de la base de données. Lorsque vous créez une facture, la provenance des données n'a pas d'importance. Cela peut provenir de SQL ou d'une API REST distante, 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. Structures généralement qui implémentent Data Mapper modèle (ne pas confondre avec les ORM du même nom .. pas de relation). C'est là que se trouveraient vos instructions SQL (ou peut-être votre DomDocument, car vous le stockez en XML).

À côté des deux parties principales, il y a un autre groupe d'instances / classes, qui devrait être mentionné:

- les services

C'est là que vos composants tiers et vos composants tiers entrent en jeu. Par exemple, vous pouvez considérer "l'authentification" comme un service, qui peut être fourni par vous-même ou par un code externe. Un "expéditeur de courrier" serait également un service, qui pourrait relier un objet de domaine à un PHPMailer ou SwiftMailer, ou à votre propre composant d'envoi de courrier.

Une autre source de services est l'abstraction sur les couches d'accès aux domaines 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 dont vous devez vous souvenir lors de la création de 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.

L'une des choses qu'ils ont tous en commun est que les services n'affectent pas la couche View de manière directe et sont tellement autonomes qu'ils peuvent être (et quittent souvent - sont) utilisés en dehors de la structure MVC elle-même. De plus, de telles structures auto-entretenues facilitent beaucoup la migration vers un autre cadre / architecture, en raison du couplage extrêmement faible entre le service et le reste de l'application.

tereško
la source
34
Je viens d'en apprendre plus en 5 minutes en relisant ceci, qu'en mois. Êtes-vous d'accord avec: les contrôleurs légers sont envoyés aux services qui collectent les données de vue? Aussi, si jamais vous acceptez des questions directement, envoyez-moi un message.
Stephane
2
Je suis partiellement d'accord. La collecte des données de la vue se produit en dehors du trièdre MVC, lorsque vous initialisez l' Requestinstance (ou un analogue de celle-ci). Le contrôleur extrait uniquement les données de l' Requestinstance et les transmet en grande partie aux services appropriés (une partie est également affichée). Les services exécutent les opérations que vous leur avez commandées. Ensuite, lorsque view génère la réponse, il demande des données aux services et, sur la base de ces informations, génère la réponse. Ladite réponse peut être soit du HTML créé à partir de plusieurs modèles, soit simplement un en-tête d'emplacement HTTP. Dépend de l'état défini par le contrôleur.
tereško
4
Pour utiliser une explication simplifiée: le contrôleur «écrit» dans le modèle et la vue, affiche «lit» à partir du modèle. La couche de modèle est la structure passive de tous les modèles liés au Web qui ont été inspirés par MVC.
tereško
@Stephane, quant à poser une question directement, vous pouvez toujours m'envoyer un message sur Twitter. Ou étiez-vous question un peu "forme longue", qui ne peut pas être entassée en 140 caractères?
tereško
Lecture du modèle: cela signifie-t-il un rôle actif pour le modèle? Je n'ai jamais entendu ça avant. Je peux toujours vous envoyer un lien via Twitter si tel est votre préférence. Comme vous pouvez le voir, ces réponses se transforment rapidement en conversations et j'essayais d'être respectueux de ce site et de vos abonnés Twitter.
Stephane
16

ACL et contrôleurs

Tout d'abord: il s'agit le plus souvent de choses / couches différentes. Lorsque vous critiquez le code de contrôleur exemplaire, il met les deux ensemble - de toute évidence trop serré.

tereško a déjà décrit un moyen de dissocier cela davantage avec le motif de décoration .

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

D'une part, vous voulez avoir des contrôleurs qui ne font que le travail auquel ils sont commandés (commande ou action, appelons-le 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 de quelque chose d'autre qui rapproche les deux. Sur la base du contexte dans lequel une commande est exécutée, l'ACL entre en action et des décisions doivent être prises pour savoir si une commande spécifique peut être exécutée ou non par un sujet spécifique (par exemple l'utilisateur).

Résumons à ce point ce que nous avons:

  • Commander
  • ACL
  • Utilisateur

Le composant ACL est ici central: il a besoin de 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 se demander ce qui identifie exactement une commande? Le nom de la classe, le nom de la méthode, le nombre ou les noms d'arguments, même les données à l'intérieur de 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 beaucoup varier. Pour l'exemple, gardons-le simplement et spécifions qu'une commande est identifiée par le nom de la classe et le nom de la 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 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, celle-ci 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 qu'il doit l'être avant que la commande concrète ne soit exécutée, nous pouvons donc réduire cette liste et n'avons besoin de regarder que dans 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é à exécuter une commande concrète. Vous faites déjà une sorte de ACL ici: Si un utilisateur demande une commande qui n'existe pas, vous ne permettez pas à cette commande de s'exécuter. Donc, partout où cela se produit 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 la traiter. 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 qu'un CommandNotAllowedResponseau lieu du CommandNotFoundResponsepour le cas une demande ne pourrait pas être résolue sur une commande concrète.

L'endroit où le mappage d'une HTTPRequest concrète est mappé sur une commande est souvent appelé Routage . Comme le routage a déjà le travail 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 le Router à un routeur en ACL: RouterACL. Si votre routeur ne connaît pas encore le User, alors le Routern'est pas le bon endroit, car pour que l'ACL fonctionne non seulement la commande mais aussi l'utilisateur doit être identifié. Donc, cet endroit peut varier, mais je suis sûr que vous pouvez facilement localiser 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).

Donc, au lieu de placer vos vérifications ACL dans l'implémentation concrète de chaque commande, vous la placez avant. Vous n'avez pas besoin de motifs 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 des rôles lui s'appliquent ou non, si elle est gardée quelque part ou non.

Alors gardez simplement les choses séparées qui n'appartiennent pas les unes aux autres. Utilisez une légère reformulation du principe de responsabilité unique (SRP) : Il ne devrait y avoir qu'une seule raison de changer une commande - parce que la commande a changé. Pas parce que vous introduisez maintenant ACL dans votre application. Pas parce que vous changez l'objet utilisateur. Pas parce que vous migrez d'une interface HTTP / HTML vers une interface SOAP ou de ligne de commande.

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

hakre
la source
Deux questions: CommandNotFoundResponse & CommandNotAllowedResponse: les transmettriez-vous de la classe ACL au routeur ou au contrôleur et attendriez-vous une réponse universelle? 2: Si vous vouliez inclure la méthode + les attributs, comment géreriez-vous cela?
Stephane
1: La réponse est réponse, ici ce n'est pas de l'ACL mais du routeur, l'ACL aide le routeur à trouver le type de réponse (non trouvé, notamment: interdit). 2: dépend. Si vous voulez dire des attributs comme des paramètres d'actions, et que vous avez besoin d'une ACL avec des paramètres, placez-les sous ACL.
hakre
13

Une possibilité est d'encapsuler tous vos contrôleurs dans une autre classe qui étend Controller et de lui faire 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 modifierez généralement pas vos URL (zone URL de type interface publique), mais vous pouvez tout aussi bien modifier 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 abandonne l'envoi s'il ne l'est pas.

Artefacto
la source
S'il vous plaît, pourriez-vous mettre à jour votre réponse et ajouter plus de détails sur Dispatcher. J'ai dispatcher - il détecte la méthode du contrôleur que je dois appeler par URL. Mais je ne peux pas comprendre comment puis-je obtenir un rôle (j'ai besoin d'accéder à DB pour le faire) dans Dispatcher. J'espère vous entendre bientôt.
Kirzilla
Aha, j'ai ton idée. Je devrais décider d'autoriser ou non l'exécution sans accéder à la méthode! Pouces vers le haut! La dernière question non résolue - comment accéder au modèle d'Acl. Des idées?
Kirzilla
@Kirzilla J'ai les mêmes problèmes avec les contrôleurs. Il semble que les dépendances doivent être là quelque part. Même si l'ACL ne l'est pas, qu'en est-il de la couche de modèle? Comment pouvez-vous éviter que cela devienne une dépendance?
Stephane