Come posso implementare un elenco di controllo degli accessi nella mia applicazione Web MVC?


96

Prima domanda

Per favore, potresti spiegarmi come potrebbe essere implementato un ACL più semplice in MVC.

Ecco il primo approccio per utilizzare Acl in 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!");
    ...    
  }

}
?>

È un approccio pessimo, ed è meno che dobbiamo aggiungere un pezzo di codice Acl nel metodo di ciascun controller, ma non abbiamo bisogno di dipendenze aggiuntive!

Il prossimo approccio consiste nel creare tutti i metodi del controller privatee aggiungere il codice ACL nel __callmetodo del controller .

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

}
?>

È migliore del codice precedente, ma i principali svantaggi sono ...

  • Tutti i metodi del controller dovrebbero essere privati
  • Dobbiamo aggiungere il codice ACL nel metodo __call di ogni controller.

Il prossimo approccio consiste nell'inserire il codice Acl nel controller principale, ma è comunque necessario mantenere privati ​​tutti i metodi del controller figlio.

Qual'è la soluzione? E qual è la migliore pratica? Dove devo chiamare le funzioni Acl per decidere di consentire o meno l'esecuzione del metodo.

Seconda domanda

La seconda domanda riguarda l'acquisizione del ruolo utilizzando Acl. Immaginiamo di avere ospiti, utenti e amici dell'utente. L'utente ha accesso limitato alla visualizzazione del suo profilo che solo gli amici possono visualizzare. Tutti gli ospiti non possono visualizzare il profilo di questo utente. Quindi, ecco la logica ..

  • dobbiamo assicurarci che il metodo che viene chiamato sia il profilo
  • dobbiamo rilevare il proprietario di questo profilo
  • dobbiamo rilevare se lo spettatore è il proprietario di questo profilo o no
  • dobbiamo leggere le regole di restrizione su questo profilo
  • dobbiamo decidere di eseguire o non eseguire il metodo del profilo

La domanda principale riguarda il rilevamento del proprietario del profilo. Possiamo rilevare chi è il proprietario del profilo solo eseguendo il metodo del modello $ model-> getOwner (), ma Acl non ha accesso al modello. Come possiamo implementarlo?

Spero che i miei pensieri siano chiari. Mi scusi per il mio inglese.

Grazie.


1
Non capisco nemmeno perché avresti bisogno di "elenchi di controllo degli accessi" per le interazioni degli utenti. Non diresti semplicemente qualcosa del tipo if($user->hasFriend($other_user) || $other_user->profileIsPublic()) $other_user->renderProfile()(altrimenti, visualizza "Non hai accesso al profilo di questo utente" o qualcosa del genere? Non capisco.
Buttle Butkus

2
Probabilmente, perché Kirzilla vuole gestire tutte le condizioni per l'accesso in un unico posto, principalmente nella configurazione. Pertanto, qualsiasi modifica alle autorizzazioni può essere apportata in Admin anziché modificare il codice.
Mariyo

Risposte:


185

Prima parte / risposta (implementazione ACL)

A mio modesto parere, il modo migliore per avvicinarsi a questo sarebbe usare il motivo del decoratore , in pratica , questo significa che prendi il tuo oggetto e lo posiziona all'interno di un altro oggetto, che agirà come un guscio protettivo. Questo NON richiederebbe di estendere la classe originale. Ecco un esempio:

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 questo è il modo in cui usi questo tipo di struttura:

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

Come potresti notare, questa soluzione presenta diversi vantaggi:

  1. contenimento può essere utilizzato su qualsiasi oggetto, non solo istanze di Controller
  2. il controllo dell'autorizzazione avviene all'esterno dell'oggetto di destinazione, il che significa che:
    • oggetto originale non è responsabile per il controllo degli accessi, aderisce a SRP
    • quando ottieni "permesso negato", non sei bloccato all'interno di un controller, più opzioni
  3. puoi iniettare questa istanza protetta in qualsiasi altro oggetto, manterrà la protezione
  4. avvolgilo e dimenticalo .. puoi fingere che sia l'oggetto originale, reagirà allo stesso modo

Ma c'è anche un grosso problema con questo metodo: non è possibile verificare in modo nativo se l'oggetto protetto implementa e l'interfaccia (che si applica anche per la ricerca di metodi esistenti) o fa parte di una catena di ereditarietà.

Seconda parte / risposta (RBAC per oggetti)

In questo caso, la differenza principale che dovresti riconoscere è che gli oggetti di dominio (ad esempio Profile:) contengono i dettagli sul proprietario. Ciò significa che per poter controllare se (ea quale livello) l'utente ha accesso ad esso, sarà necessario modificare questa riga:

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

In sostanza hai due opzioni:

  • Fornisci l'ACL con l'oggetto in questione. Ma devi stare attento a non violare la Legge di Demetra :

    $this->acl->isAllowed( get_class($this->target), $method )
  • Richiedi tutti i dettagli rilevanti e fornisci all'ACL solo ciò di cui ha bisogno, il che lo renderà anche un po 'più amichevole per i test di unità:

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

Un paio di video che potrebbero aiutarti a trovare la tua implementazione:

Note a margine

Sembra che tu abbia la comprensione abbastanza comune (e completamente sbagliata) di cosa sia Model in MVC. Il modello non è una classe . Se hai un nome di classe FooBarModelo qualcosa che eredita, AbstractModello stai facendo male.

In un MVC corretto, il modello è un livello, che contiene molte classi. Gran parte delle classi può essere separata in due gruppi, in base alla responsabilità:

- Logica aziendale del dominio

( leggi di più : qui e qui ):

Le istanze di questo gruppo di classi si occupano del calcolo dei valori, controllano condizioni diverse, implementano regole di vendita e fanno tutto il resto ciò che chiamereste "logica aziendale". Non hanno idea di come vengono archiviati i dati, dove vengono archiviati o anche se l'archiviazione esiste in primo luogo.

L'oggetto business del dominio non dipende dal database. Quando crei una fattura, non importa da dove provengono i dati. Può provenire da SQL o da un'API REST remota o anche screenshot di un documento MSWord. La logica aziendale non cambia.

- Accesso e archiviazione dei dati

Le istanze create da questo gruppo di classi sono talvolta chiamate oggetti di accesso ai dati. Di solito strutture che implementano Data Mapper pattern (da non confondere con ORM con lo stesso nome .. nessuna relazione). Qui è dove sarebbero le tue istruzioni SQL (o forse il tuo DomDocument, perché lo memorizzi in XML).

Oltre alle due parti principali, c'è un altro gruppo di istanze / classi, che dovrebbe essere menzionato:

- Servizi

È qui che entrano in gioco i tuoi componenti e quelli di terze parti. Ad esempio, puoi pensare all '"autenticazione" come a un servizio, che può essere fornito dal tuo o da un codice esterno. Anche "mittente di posta" sarebbe un servizio, che potrebbe unire un oggetto di dominio con un PHPMailer o SwiftMailer, o il tuo componente mittente di posta.

Un'altra fonte di servizi è l'astrazione sui livelli di accesso ai dati e al dominio. Sono creati per semplificare il codice utilizzato dai controller. Ad esempio: la creazione di un nuovo account utente potrebbe richiedere l'utilizzo di diversi oggetti di dominio e mappatori . Ma, utilizzando un servizio, saranno necessarie solo una o due linee nel controller.

Quello che devi ricordare quando crei servizi è che l'intero livello dovrebbe essere sottile . Non c'è logica di business nei servizi. Sono lì solo per destreggiarsi tra oggetti di dominio, componenti e mappatori.

Una delle cose che hanno tutti in comune è che i servizi non influenzano il livello di visualizzazione in alcun modo diretto e sono autonomi a tal punto che possono essere (e vengono chiusi spesso) utilizzati al di fuori della struttura MVC stessa. Anche tali strutture autosufficienti rendono la migrazione a un framework / architettura diverso molto più semplice, a causa dell'accoppiamento estremamente basso tra il servizio e il resto dell'applicazione.


34
Ho appena imparato di più in 5 minuti rileggendo questo, che da mesi. Saresti d'accordo con: i controller sottili inviano a servizi che raccolgono i dati di visualizzazione? Inoltre, se accetti direttamente le domande, inviami un messaggio.
Stephane

2
Sono parzialmente d'accordo. La raccolta dei dati dalla vista avviene al di fuori della triade MVC, quando si inizializza l' Requestistanza (o qualche analogo di essa). Il controller estrae solo i dati Requestdall'istanza e ne passa la maggior parte ai servizi appropriati (alcuni vengono visualizzati anche). I servizi eseguono le operazioni che hai comandato loro di eseguire. Quindi, quando la vista genera la risposta, richiede i dati dai servizi e, in base a tali informazioni, genera la risposta. Detta risposta può essere HTML composta da più modelli o solo un'intestazione di posizione HTTP. Dipende dallo stato impostato dal controller.
tereško

4
Per utilizzare una spiegazione semplificata: il controller "scrive" nel modello e visualizza, visualizza "legge" dal modello. Il livello del modello è la struttura passiva in tutti i modelli relativi al Web che sono stati ispirati da MVC.
tereško

@Stephane, per quanto riguarda la domanda diretta, puoi sempre scrivermi su Twitter. O eri una domanda un po '"lunga", che non può essere stipata in 140 caratteri?
tereško

Legge dal modello: significa un ruolo attivo per il modello? Non l'ho mai sentito prima. Posso sempre inviarti un link tramite Twitter se questa è la tua preferenza. Come puoi vedere, queste risposte si trasformano rapidamente in conversazioni e stavo cercando di essere rispettoso di questo sito e dei tuoi follower su Twitter.
Stephane

16

ACL e controller

Prima di tutto: molto spesso si tratta di cose / strati diversi. Mentre critichi il codice esemplare del controller, mette insieme entrambi, ovviamente troppo stretto.

tereško ha già delineato un modo per disaccoppiarlo maggiormente con il motivo del decoratore.

Vorrei prima fare un passo indietro per cercare il problema originale che stai affrontando e discuterne un po 'poi.

Da un lato vuoi avere controller che eseguano semplicemente il lavoro a cui sono comandati (comando o azione, chiamiamolo comando).

D'altra parte vuoi essere in grado di mettere ACL nella tua applicazione. Il campo di lavoro di questi ACL dovrebbe essere - se ho capito bene la tua domanda - controllare l'accesso a determinati comandi delle tue applicazioni.

Questo tipo di controllo dell'accesso necessita quindi di qualcos'altro che riunisca questi due elementi. In base al contesto in cui viene eseguito un comando, ACL entra in gioco e devono essere prese decisioni se un comando specifico può essere eseguito o meno da un soggetto specifico (ad esempio l'utente).

Riassumiamo a questo punto quello che abbiamo:

  • Comando
  • ACL
  • Utente

La componente ACL è centrale qui: deve sapere almeno qualcosa sul comando (per identificare il comando per essere precisi) e deve essere in grado di identificare l'utente. Gli utenti sono normalmente facilmente identificati da un ID univoco. Ma spesso nelle applicazioni web ci sono utenti che non sono affatto identificati, spesso chiamati guest, anonimi, tutti ecc. Per questo esempio si presume che l'ACL possa consumare un oggetto utente e incapsulare questi dettagli. L'oggetto utente è associato all'oggetto richiesta dell'applicazione e l'ACL può utilizzarlo.

Che dire dell'identificazione di un comando? La tua interpretazione del pattern MVC suggerisce che un comando è composto da un nome di classe e un nome di metodo. Se guardiamo più da vicino ci sono anche argomenti (parametri) per un comando. Quindi è valido chiedere cosa identifica esattamente un comando? Il nome della classe, il nome del metodo, il numero oi nomi degli argomenti, persino i dati all'interno di uno qualsiasi degli argomenti o una miscela di tutto questo?

A seconda del livello di dettaglio necessario per identificare un comando nel tuo ACL, questo può variare molto. Per l'esempio manteniamolo semplice e specifichiamo che un comando è identificato dal nome della classe e dal nome del metodo.

Quindi il contesto di come queste tre parti (ACL, comando e utente) appartengono l'una all'altra è ora più chiaro.

Potremmo dire che con un componente ACL immaginario possiamo già fare quanto segue:

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

Guarda cosa sta succedendo qui: rendendo identificabili sia il comando che l'utente, l'ACL può fare il suo lavoro. Il lavoro dell'ACL non è correlato al lavoro sia dell'oggetto utente che del comando concreto.

Manca solo una parte, questa non può vivere nell'aria. E non è così. Quindi è necessario individuare il punto in cui deve essere attivato il controllo degli accessi. Diamo un'occhiata a cosa succede in un'applicazione web standard:

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

Per individuare quel luogo, sappiamo che deve essere prima che il comando concreto venga eseguito, quindi possiamo ridurre quell'elenco e dobbiamo solo esaminare i seguenti (potenziali) luoghi:

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

Ad un certo punto nella tua applicazione sai che un utente specifico ha richiesto di eseguire un comando concreto. Fai già una sorta di ACL qui: se un utente richiede un comando che non esiste, non permetti a quel comando di essere eseguito. Quindi, ovunque ciò accada nella tua applicazione potrebbe essere un buon posto per aggiungere i controlli ACL "reali":

Il comando è stato individuato e possiamo crearne l'identificazione in modo che l'ACL possa gestirlo. Nel caso in cui il comando non sia consentito a un utente, il comando non verrà eseguito (azione). Forse al CommandNotAllowedResponseposto del CommandNotFoundResponsecaso una richiesta non può essere risolta su un comando concreto.

Il luogo in cui la mappatura di una richiesta HTTP concreta viene mappata su un comando è spesso chiamato Routing . Dato che il Routing ha già il compito di individuare un comando, perché non estenderlo per verificare se il comando è effettivamente consentito per ACL? Ad esempio estendendo la Router a un router consapevoli ACL: RouterACL. Se il tuo router non conosce ancora User, allora Routernon è il posto giusto, perché affinché ACL'ing funzioni non solo il comando ma anche l'utente deve essere identificato. Quindi questo posto può variare, ma sono sicuro che puoi facilmente individuare il luogo che devi estendere, perché è il luogo che soddisfa i requisiti di utente e comando:

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

L'utente è disponibile dall'inizio, comando prima con Request(Command).

Quindi, invece di mettere i tuoi controlli ACL all'interno dell'implementazione concreta di ogni comando, lo metti prima di esso. Non hai bisogno di schemi pesanti, magia o altro, l'ACL fa il suo lavoro, l'utente fa il suo lavoro e soprattutto il comando fa il suo lavoro: solo il comando, nient'altro. Il comando non ha interesse a sapere se i ruoli si applicano o meno ad esso, se è custodito da qualche parte o meno.

Quindi tieni separate le cose che non appartengono l'una all'altra. Utilizzare una riformulazione leggermente del principio di responsabilità unica (SRP) : dovrebbe esserci un solo motivo per modificare un comando, perché il comando è cambiato. Non perché ora introduci ACL nella tua applicazione. Non perché cambi l'oggetto Utente. Non perché si migra da un'interfaccia HTTP / HTML a un'interfaccia SOAP o a riga di comando.

L'ACL nel tuo caso controlla l'accesso a un comando, non il comando stesso.


Due domande: CommandNotFoundResponse e CommandNotAllowedResponse: passeresti queste dalla classe ACL al router o al controller e ti aspetteresti una risposta universale? 2: Se volessi includere metodo + attributi, come lo gestiresti?
Stephane

1: La risposta è risposta, qui non proviene da ACL ma dal router, ACL aiuta il router a scoprire il tipo di risposta (non trovato, soprattutto: proibito). 2: dipende. Se intendi attributi come parametri dalle azioni e hai bisogno di ACL con parametri, inseriscili sotto ACL.
hakre

13

Una possibilità è racchiudere tutti i controller in un'altra classe che estende Controller e fare in modo che deleghi tutte le chiamate di funzione all'istanza di cui è stato eseguito il wrapping dopo aver verificato l'autorizzazione.

Puoi anche farlo più a monte, nel dispatcher (se la tua applicazione ne ha effettivamente uno) e cercare i permessi in base agli URL, invece che ai metodi di controllo.

modificare : se è necessario accedere a un database, un server LDAP, ecc. è ortogonale alla domanda. Il punto era che potresti implementare un'autorizzazione basata sugli URL anziché sui metodi del controller. Questi sono più robusti perché in genere non cambierai i tuoi URL (tipo di interfaccia pubblica dell'area URL), ma potresti anche cambiare le implementazioni dei tuoi controller.

In genere, si dispone di uno o più file di configurazione in cui si associano pattern URL specifici a metodi di autenticazione e direttive di autorizzazione specifici. Il dispatcher, prima di inviare la richiesta ai controllori, determina se l'utente è autorizzato e annulla l'invio in caso contrario.


Per favore, potresti aggiornare la tua risposta e aggiungere ulteriori dettagli su Dispatcher. Ho un dispatcher: rileva quale metodo del controller dovrei chiamare tramite URL. Ma non riesco a capire come posso ottenere il ruolo (devo accedere al DB per farlo) in Dispatcher. Spero di sentirti presto.
Kirzilla

Aha, ho avuto la tua idea. Dovrei decidere di consentire l'esecuzione o meno senza accedere al metodo! Pollice su! L'ultima domanda irrisolta: come accedere al modello da Acl. Qualche idea?
Kirzilla

@ Kirzilla Ho gli stessi problemi con i controller. Sembra che le dipendenze debbano essere lì da qualche parte. Anche se l'ACL non lo è, per quanto riguarda il livello del modello? Come puoi evitare che sia una dipendenza?
Stephane
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.