¿Cómo puedo implementar una lista de control de acceso en mi aplicación Web MVC?


96

Primera pregunta

Por favor, ¿podría explicarme cómo se podría implementar la ACL más simple en MVC?

Aquí está el primer enfoque para usar Acl en 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!");
    ...    
  }

}
?>

Es un enfoque muy malo, y el inconveniente es que tenemos que agregar un fragmento de código Acl en el método de cada controlador, ¡pero no necesitamos ninguna dependencia adicional!

El siguiente enfoque es crear todos los métodos del controlador privatey agregar el código ACL al __callmétodo del 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!");
    ...   
  }

}
?>

Es mejor que el código anterior, pero las principales desventajas son ...

  • Todos los métodos del controlador deben ser privados
  • Tenemos que agregar el código ACL en el método __call de cada controlador.

El siguiente enfoque es poner el código Acl en el controlador principal, pero aún necesitamos mantener privados todos los métodos del controlador secundario.

¿Cuál es la solución? ¿Y cuál es la mejor práctica? ¿Dónde debo llamar a las funciones de Acl para decidir permitir o no permitir que se ejecute el método?

Segunda pregunta

La segunda pregunta es sobre cómo obtener un rol usando Acl. Imaginemos que tenemos invitados, usuarios y amigos de usuarios. El usuario tiene acceso restringido para ver su perfil que solo sus amigos pueden ver. Todos los invitados no pueden ver el perfil de este usuario. Entonces, aquí está la lógica ...

  • tenemos que asegurarnos de que el método que se llama sea perfil
  • tenemos que detectar al dueño de este perfil
  • tenemos que detectar si el espectador es propietario de este perfil o no
  • tenemos que leer las reglas de restricción sobre este perfil
  • tenemos que decidir ejecutar o no ejecutar el método de perfil

La pregunta principal es sobre la detección del propietario del perfil. Podemos detectar quién es el propietario del perfil solo ejecutando el método $ model-> getOwner () del modelo, pero Acl no tiene acceso al modelo. ¿Cómo podemos implementar esto?

Espero que mis pensamientos estén claros. Lo siento por mi ingles.

Gracias.


1
Ni siquiera entiendo por qué necesitaría "Listas de control de acceso" para las interacciones de los usuarios. ¿No dirías algo como if($user->hasFriend($other_user) || $other_user->profileIsPublic()) $other_user->renderProfile()(de lo contrario, mostrar "No tienes acceso al perfil de este usuario" o algo así? No lo entiendo.
Buttle Butkus

2
Probablemente, porque Kirzilla quiere administrar todas las condiciones de acceso en un solo lugar, principalmente en la configuración. Por lo tanto, cualquier cambio en los permisos se puede realizar en Admin en lugar de cambiar el código.
Mariyo

Respuestas:


185

Primera parte / respuesta (implementación de ACL)

En mi humilde opinión, la mejor manera de abordar esto sería utilizar patrón de decorador . Básicamente, esto significa que tomas tu objeto y lo colocas dentro de otro objeto, que actuará como un caparazón protector. Esto NO requeriría que extienda la clase original. Aquí hay un ejemplo:

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

}

Y así sería como se usa este tipo de estructura:

// 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 puede notar, esta solución tiene varias ventajas:

  1. La contención se puede usar en cualquier objeto, no solo en instancias de Controller
  2. la verificación de la autorización ocurre fuera del objeto de destino, lo que significa que:
    • El objeto original no es responsable del control de acceso, se adhiere a SRP
    • cuando obtiene "permiso denegado", no está bloqueado dentro de un controlador, más opciones
  3. puedes inyectar esto instancia segura en cualquier otro objeto, conservará la protección
  4. envuélvelo y olvídalo ... puedes fingir que es el objeto original, reaccionará de la misma manera

Pero también hay un problema importante con este método: no puede verificar de forma nativa si el objeto protegido se implementa y la interfaz (que también se aplica para buscar métodos existentes) o es parte de alguna cadena de herencia.

Segunda parte / respuesta (RBAC para objetos)

En este caso, la principal diferencia que debe reconocer es que los objetos de dominio (en el ejemplo Profile:) contienen detalles sobre el propietario. Esto significa que, para que verifique, si (y en qué nivel) el usuario tiene acceso a él, será necesario que cambie esta línea:

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

Básicamente tienes dos opciones:

  • Proporcione a la ACL el objeto en cuestión. Pero debes tener cuidado de no violar la Ley de Deméter :

    $this->acl->isAllowed( get_class($this->target), $method )
  • Solicite todos los detalles relevantes y proporcione al ACL solo lo que necesita, lo que también lo hará un poco más amigable para las pruebas unitarias:

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

Un par de videos que pueden ayudarlo a crear su propia implementación:

Notas al margen

Parece tener una comprensión bastante común (y completamente incorrecta) de lo que es el modelo en MVC. El modelo no es una clase . Si tiene una clase nombrada FooBarModelo algo que heredaAbstractModel entonces lo está haciendo mal.

En el MVC adecuado, el modelo es una capa que contiene muchas clases. Gran parte de las clases se pueden separar en dos grupos, según la responsabilidad:

- Lógica empresarial de dominio

( leer más : aquí y aquí ):

Las instancias de este grupo de clases se ocupan del cálculo de valores, verifican diferentes condiciones, implementan reglas de ventas y hacen todo lo demás lo que llamaría "lógica de negocios". No tienen idea de cómo se almacenan los datos, dónde se almacenan o incluso si existe almacenamiento en primer lugar.

El objeto de negocio de dominio no depende de la base de datos. Cuando crea una factura, no importa de dónde provienen los datos. Puede ser desde SQL o desde una API REST remota, o incluso una captura de pantalla de un documento MSWord. La lógica empresarial no cambia.

- Acceso y almacenamiento de datos

Las instancias creadas a partir de este grupo de clases a veces se denominan objetos de acceso a datos. Generalmente estructuras que implementan Data Mapper patrón (no confundir con ORM del mismo nombre ... sin relación). Aquí es donde estarían sus declaraciones SQL (o tal vez su DomDocument, porque lo almacena en XML).

Además de las dos partes principales, hay un grupo más de instancias / clases, que deben mencionarse:

- servicios

Aquí es donde entran en juego sus componentes y los de terceros. Por ejemplo, puede pensar en la "autenticación" como un servicio, que puede proporcionarlo usted mismo o algún código externo. También "remitente de correo" sería un servicio, que podría unir algún objeto de dominio con un PHPMailer o SwiftMailer, o su propio componente de remitente de correo.

Otra fuente de servicios es la abstracción de las capas de acceso a datos y dominio. Se crean para simplificar el código utilizado por los controladores. Por ejemplo: crear una nueva cuenta de usuario puede requerir trabajar con varios objetos de dominio y mapeadores . Pero, al usar un servicio, solo necesitará una o dos líneas en el controlador.

Lo que debe recordar al realizar servicios es que se supone que toda la capa es delgada . No hay lógica empresarial en los servicios. Solo están ahí para hacer malabarismos con objetos de dominio, componentes y mapeadores.

Una de las cosas que todos tienen en común es que los servicios no afectan la capa de Vista de ninguna manera directa, y son autónomos hasta tal punto que pueden ser (y dejar de ser a menudo) utilizados fuera de la estructura MVC. Además, estas estructuras autosostenidas facilitan mucho la migración a un marco / arquitectura diferente, debido al acoplamiento extremadamente bajo entre el servicio y el resto de la aplicación.


34
Aprendí más en 5 minutos releyendo esto, que en meses. ¿Estaría de acuerdo con: los controladores delgados se envían a servicios que recopilan datos de visualización? Además, si alguna vez acepta preguntas directamente, envíeme un mensaje.
Stephane

2
Estoy parcialmente de acuerdo. La recopilación de datos de la vista ocurre fuera de la tríada MVC, cuando inicializa la Requestinstancia (o algún análogo de ella). El controlador solo extrae datos de la Requestinstancia y pasa la mayor parte a los servicios adecuados (algunos de ellos también se muestran). Los servicios realizan operaciones que usted les ordenó que hicieran. Luego, cuando view genera la respuesta, solicita datos a los servicios y, en base a esa información, genera la respuesta. Dicha respuesta puede ser HTML hecha a partir de múltiples plantillas o simplemente un encabezado de ubicación HTTP. Depende del estado establecido por el controlador.
tereško

4
Para utilizar una explicación simplificada: el controlador "escribe" en el modelo y la vista, la vista "lee" del modelo. La capa de modelo es la estructura pasiva en todos los patrones relacionados con la Web que se han inspirado en MVC.
tereško

@Stephane, en cuanto a hacer preguntas directamente, siempre puedes enviarme un mensaje en Twitter. ¿O tu pregunta era un poco "larga", que no se puede meter en 140 caracteres?
tereško

Lee del modelo: ¿eso significa algún papel activo para el modelo? Nunca había escuchado eso antes. Siempre puedo enviarte un enlace a través de Twitter si así lo prefieres. Como puede ver, estas respuestas se convierten rápidamente en conversaciones y estaba tratando de ser respetuoso con este sitio y con sus seguidores de Twitter.
Stephane

16

ACL y controladores

En primer lugar: estas son cosas / capas diferentes con mayor frecuencia. A medida que critica el código ejemplar del controlador, une ambos, obviamente demasiado estricto.

tereško ya describió una forma de desacoplar esto más con el patrón de decorador.

Primero retrocedería un paso para buscar el problema original al que se enfrenta y luego lo discutiría un poco.

Por un lado, desea tener controladores que solo hagan el trabajo que se les ordenó (comando o acción, llamémoslo comando).

Por otro lado, desea poder poner ACL en su aplicación. El campo de trabajo de estas ACL debería ser, si entendí bien su pregunta, controlar el acceso a ciertos comandos de sus aplicaciones.

Por lo tanto, este tipo de control de acceso necesita algo más que los una. Según el contexto en el que se ejecuta un comando, ACL se activa y es necesario tomar decisiones sobre si un sujeto específico puede ejecutar o no un comando específico (por ejemplo, el usuario).

Resumamos hasta este punto lo que tenemos:

  • Mando
  • ACL
  • Usuario

El componente ACL es fundamental aquí: necesita saber al menos algo sobre el comando (para identificar el comando para ser precisos) y necesita poder identificar al usuario. Normalmente, los usuarios se identifican fácilmente mediante una identificación única. Pero a menudo en las aplicaciones web hay usuarios que no están identificados en absoluto, a menudo llamados invitados, anónimos, todos, etc. Para este ejemplo asumimos que la ACL puede consumir un objeto de usuario y encapsular estos detalles. El objeto de usuario está vinculado al objeto de solicitud de la aplicación y la ACL puede consumirlo.

¿Qué hay de identificar un comando? Su interpretación del patrón MVC sugiere que un comando está compuesto por un nombre de clase y un nombre de método. Si miramos más de cerca, incluso hay argumentos (parámetros) para un comando. Entonces, ¿es válido preguntar qué identifica exactamente un comando? ¿El nombre de la clase, el nombre del método, el número o los nombres de los argumentos, incluso los datos dentro de cualquiera de los argumentos o una mezcla de todo esto?

Dependiendo del nivel de detalle que necesite para identificar un comando en su ACL, esto puede variar mucho. Para el ejemplo, hagámoslo simple y especifiquemos que un comando se identifica por el nombre de la clase y el nombre del método.

Entonces, el contexto de cómo estas tres partes (ACL, Comando y Usuario) se pertenecen entre sí ahora es más claro.

Podríamos decir, con un componente ACL imaginario ya podemos hacer lo siguiente:

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

Solo vea lo que sucede aquí: al hacer que tanto el comando como el usuario sean identificables, la ACL puede hacer su trabajo. El trabajo de la ACL no está relacionado con el trabajo tanto del objeto de usuario como del comando concreto.

Solo falta una parte, esto no puede vivir en el aire. Y no es así. Por lo tanto, debe ubicar el lugar donde debe activarse el control de acceso. Echemos un vistazo a lo que sucede en una aplicación web estándar:

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

Para ubicar ese lugar, sabemos que debe ser antes de que se ejecute el comando concreto, por lo que podemos reducir esa lista y solo necesitamos buscar en los siguientes lugares (potenciales):

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

En algún momento de su aplicación, sabe que un usuario específico ha solicitado realizar un comando concreto. Ya realiza algún tipo de ACL aquí: si un usuario solicita un comando que no existe, no permite que ese comando se ejecute. Entonces, donde sea que suceda en su aplicación, podría ser un buen lugar para agregar las verificaciones de ACL "reales":

El comando ha sido localizado y podemos crear la identificación del mismo para que la ACL pueda manejarlo. En caso de que el comando no esté permitido para un usuario, el comando no se ejecutará (acción). Tal vez en CommandNotAllowedResponselugar de CommandNotFoundResponseen el caso de que una solicitud no se pueda resolver en un comando concreto.

El lugar donde la asignación de una HTTPRequest concreta se asigna a un comando a menudo se llama Enrutamiento . Dado que el enrutamiento ya tiene la tarea de localizar un comando, ¿por qué no extenderlo para verificar si el comando está realmente permitido por ACL? Por ejemplo, mediante la ampliación de la Router a un router consciente ACL: RouterACL. Si su enrutador aún no conoce el User, entonces Routerno es el lugar correcto, porque para que el ACL funcione no solo el comando sino también el usuario debe estar identificado. Entonces, este lugar puede variar, pero estoy seguro de que puede ubicar fácilmente el lugar que necesita para extender, porque es el lugar que cumple con los requisitos de usuario y comando:

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

El usuario está disponible desde el principio, Comando primero con Request(Command).

Entonces, en lugar de poner sus comprobaciones de ACL dentro de cada la implementación concreta de comando, lo coloca antes. No necesita ningún patrón pesado, magia o lo que sea, la ACL hace su trabajo, el usuario hace su trabajo y especialmente el comando hace su trabajo: solo el comando, nada más. El comando no tiene interés en saber si se le aplican roles o no, si está protegido en algún lugar o no.

Así que mantén las cosas separadas que no se pertenecen entre sí. Utilice una nueva redacción leve del principio de responsabilidad única (SRP) : debe haber solo una razón para cambiar un comando: porque el comando ha cambiado. No porque ahora introduzca ACL en su aplicación. No porque cambie el objeto Usuario. No porque migre de una interfaz HTTP / HTML a una interfaz SOAP o de línea de comandos.

La ACL en su caso controla el acceso a un comando, no el comando en sí.


Dos preguntas: CommandNotFoundResponse y CommandNotAllowedResponse: ¿las pasaría de la clase ACL al enrutador o controlador y esperaría una respuesta universal? 2: Si quisiera incluir método + atributos, ¿cómo lo manejaría?
Stephane

1: La respuesta es la respuesta, aquí no es de ACL sino del enrutador, ACL ayuda al enrutador a encontrar el tipo de respuesta (no encontrado, especialmente: prohibido). 2: Depende. Si se refiere a los atributos como parámetros de las acciones y necesita ACL con parámetros, colóquelos bajo ACL.
hakre

13

Una posibilidad es envolver todos sus controladores en otra clase que amplíe Controller y hacer que delegue todas las llamadas de función a la instancia envuelta después de verificar la autorización.

También puede hacerlo más arriba, en el despachador (si su aplicación realmente tiene uno) y buscar los permisos basados ​​en las URL, en lugar de los métodos de control.

editar : Si necesita acceder a una base de datos, un servidor LDAP, etc. es ortogonal a la pregunta. Mi punto fue que podría implementar una autorización basada en URL en lugar de métodos de controlador. Estos son más sólidos porque normalmente no cambiará sus URL (tipo de interfaz pública del área de URL), pero también podría cambiar las implementaciones de sus controladores.

Por lo general, tiene uno o varios archivos de configuración en los que asigna patrones de URL específicos a métodos de autenticación y directivas de autorización específicos. El despachador, antes de enviar la solicitud a los controladores, determina si el usuario está autorizado y aborta el envío si no lo está.


Por favor, ¿podría actualizar su respuesta y agregar más detalles sobre Dispatcher? Tengo despachador: detecta qué método de controlador debo llamar por URL. Pero no puedo entender cómo puedo obtener un rol (necesito acceder a DB para hacerlo) en Dispatcher. Espero tener noticias pronto.
Kirzilla

Ajá, entendiste tu idea. ¡Debo decidir permitir la ejecución o no sin acceder al método! ¡Pulgares hacia arriba! La última pregunta sin resolver: cómo acceder al modelo desde Acl. ¿Algunas ideas?
Kirzilla

@Kirzilla Tengo los mismos problemas con los controladores. Parece que las dependencias tienen que estar ahí en alguna parte. Incluso si la ACL no lo es, ¿qué pasa con la capa del modelo? ¿Cómo puedes evitar que eso sea una dependencia?
Stephane
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.