php - usuarios - ¿Cómo puedo implementar una Lista de control de acceso en mi aplicación Web MVC?
tutorial asp.net mvc 5 español (3)
Primera parte / respuesta (implementación de ACL)
En mi humilde opinión, la mejor manera de abordar esto sería usar un patrón decorador . Básicamente, esto significa que tomas tu objeto y lo colocas dentro de otro objeto, que actuará como una capa protectora. 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 esta sería la forma en que usas 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 habrás notado, esta solución tiene varias ventajas:
- la contención se puede usar en cualquier objeto, no solo en las instancias de
Controller
- verificar si 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
- puede inyectar esta instancia segura en cualquier otro objeto, retendrá la protección
- envuélvelo y olvídalo ... puedes pretender que es el objeto original, reaccionará de la misma manera
Pero también hay un problema importante con este método: no se puede verificar de forma nativa si los implementos y la interfaz de objetos seguros (que también se aplica para buscar métodos existentes) o si es parte de alguna cadena de herencia.
Segunda parte / respuesta (RBAC para objetos)
En este caso, la diferencia principal que debe reconocer es que los Objetos del Dominio (en el ejemplo: Profile
) en sí contienen detalles sobre el propietario. Esto significa que, para que pueda verificar, si (y a qué nivel) el usuario tiene acceso, se le pedirá que cambie esta línea:
$this->acl->isAllowed( get_class($this->target), $method )
Esencialmente tienes dos opciones:
Proporcione la ACL con el objeto en cuestión. Pero debes tener cuidado de no violar la Ley de Demeter :
$this->acl->isAllowed( get_class($this->target), $method )
Solicite todos los detalles relevantes y proporcione a la ACL solo lo que necesita, lo que también lo hará un poco más amigable para las pruebas de unidades:
$command = array( get_class($this->target), $method ); /* -- snip -- */ $this->acl->isAllowed( $this->target->getPermissions(), $command )
Junte videos que puedan ayudarlo a crear su propia implementación:
Notas laterales
Parece que tienes una comprensión bastante común (y completamente errónea) de lo que es Model in MVC. El modelo no es una clase . Si tienes una clase llamada FooBarModel
o algo que hereda AbstractModel
entonces lo estás haciendo mal.
En 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 del dominio
Las instancias de este grupo de clases se ocupan del cálculo de valores, verifican las diferentes condiciones, implementan reglas de ventas y hacen todo lo demás, lo que usted llamaría "lógica de negocios". No tienen idea de cómo se almacenan los datos, dónde se almacenan o incluso si el almacenamiento existe en primer lugar.
El objeto comercial 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 de negocios no cambia.
- Acceso a datos y almacenamiento
Las instancias creadas a partir de este grupo de clases a veces se denominan objetos de acceso a datos. Por lo general, las estructuras que implementan el patrón Data Mapper (no confunda 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 servicio, que puede ser proporcionado por usted 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 envío de correo.
Otra fuente de services es la abstracción en el dominio y las capas de acceso a datos. Se crean para simplificar el código utilizado por los controladores. Por ejemplo: la creación de 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 debes recordar al hacer los servicios, es que se supone que toda la capa es delgada . No hay lógica de negocios en los servicios. Solo están ahí para manipular el objeto de dominio, los componentes y los 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 en tal medida, que pueden ser (y salir a menudo, son) utilizados fuera de la propia estructura de MVC. Además, estas estructuras autosostenidas hacen que la migración a un marco / arquitectura diferente sea mucho más fácil, debido al acoplamiento extremadamente bajo entre el servicio y el resto de la aplicación.
Primera pregunta
Por favor, ¿podría explicarme cómo simplificar ACL podría implementarse en MVC.
Aquí está el primer acercamiento de usar Acl en el 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!");
...
}
}
?>
Es un enfoque muy malo, y lo negativo es que tenemos que agregar una pieza de código Acl en el método de cada controlador, ¡pero no necesitamos ninguna dependencia adicional!
El siguiente enfoque es hacer que todos los métodos del controlador sean private
y agregar código ACL al método __call
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 los menos importantes 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 debemos mantener privados todos los métodos del controlador hijo.
¿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 acerca de obtener un rol usando Acl. Imaginemos que tenemos invitados, usuarios y amigos del usuario. El usuario tiene acceso restringido para ver su perfil que solo los 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 es el perfil
- tenemos que detectar al propietario de este perfil
- tenemos que detectar si el espectador es dueño 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 detectar el propietario del perfil. Podemos detectar quién es el propietario del perfil ejecutando únicamente el método $ model-> getOwner (), pero Acl no tiene acceso al modelo. ¿Cómo podemos implementar esto?
Espero que mis pensamientos sean claros. Lo siento por mi ingles.
Gracias.
ACL y controladores
Primero que nada: estas son cosas / capas diferentes con mayor frecuencia. Al criticar el código de controlador ejemplar, pone los dos juntos, obviamente demasiado apretado.
tereško ya delineó una forma de cómo podría desacoplar esto más con el patrón de decorador.
Daría un paso atrás primero para buscar el problema original que enfrentas y luego lo discutiré un poco.
Por un lado, quiere tener controladores que simplemente hagan el trabajo que se les ordena (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, para 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 a estos dos. En función del contexto en el que se ejecuta un comando, ACL entra en acción y las decisiones deben tomarse independientemente de si un comando específico puede ser ejecutado por un sujeto específico (por ejemplo, el usuario).
Vamos a resumir hasta este punto lo que tenemos:
- Mando
- ACL
- Usuario
El componente de ACL es central aquí: necesita saber al menos algo sobre el comando (para identificar el comando para ser preciso) y necesita poder identificar al usuario. Los usuarios normalmente se identifican fácilmente con una identificación única. Pero a menudo en las aplicaciones we hay usuarios que no están identificados en absoluto, a menudo llamados invitados, anónimos, todo el mundo, etc. Para este ejemplo suponemos 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, hay incluso argumentos (parámetros) para un comando. Entonces, ¿es válido preguntar qué identifica exactamente un comando? El nombre de 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. Por ejemplo, vamos a mantenerlo simple y especificar que un comando se identifica por el nombre de clase y el nombre del método.
Por lo tanto, el contexto de cómo estas tres partes (ACL, Comando y Usuario) se están uniendo entre sí ahora es más claro.
Podríamos decir que con un componente imaginario de ACL ya podemos hacer lo siguiente:
$acl->commandAllowedForUser($command, $user);
Solo vea lo que está sucediendo aquí: al hacer tanto el comando como el usuario identificables, la ACL puede hacer su trabajo. El trabajo de la ACL no está relacionado con el trabajo del objeto usuario y el 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 necesita el control de acceso. Echemos un vistazo a lo que sucede en una aplicación de Internet 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 tenemos que buscar en los siguientes lugares (potenciales):
User -> Browser -> Request (HTTP)
-> Request (Command)
En algún momento de su aplicación, usted sabe que un usuario específico ha solicitado ejecutar un comando concreto. Ya hace ACL aquí: si un usuario solicita un comando que no existe, no permite que ese comando se ejecute. Entonces, donde sea que suceda eso en su aplicación puede ser un buen lugar para agregar las verificaciones de ACL "reales":
El comando se ha localizado y podemos crear una identificación para que la LCA pueda manejarlo. En caso de que el comando no esté permitido para un usuario, el comando no se ejecutará (acción). Tal vez una CommandNotAllowedResponse
lugar de CommandNotFoundResponse
para el caso en que una solicitud no pudo resolverse en un comando concreto.
El lugar donde el mapeo de una HTTPRequest concreta se asigna a un comando a menudo se denomina Enrutamiento . Como el enrutamiento ya tiene el trabajo de ubicar un comando, ¿por qué no extenderlo para verificar si el comando está realmente permitido por ACL? Por ejemplo, al extender el Router
a un enrutador compatible con ACL: RouterACL
. Si su enrutador aún no conoce al User
, entonces el Router
no es el lugar correcto, porque para que ACL funcione, debe identificarse no solo el comando sino también el usuario. Por lo tanto, este lugar puede variar, pero estoy seguro de que puede ubicar fácilmente el lugar que necesita extender, porque es el lugar que satisface 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 verificaciones de ACL dentro de la implementación concreta de cada comando, lo coloca delante de él. No necesitas patrones pesados, 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 las funciones se aplican o no, si está protegido en alguna parte o no.
Así que mantén las cosas separadas que no se pertenecen. Use una nueva redacción del Principio de Responsabilidad Individual (SRP) : debe haber una sola razón para cambiar un comando, porque el comando ha cambiado. No porque ahora introduzcas ACL en tu 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í.
Una posibilidad es ajustar todos los controladores en otra clase que extienda el controlador y hacer que delegue todas las llamadas de función a la instancia envuelta después de verificar la autorización.
También podría hacerlo más arriba, en el despachador (si su aplicación sí 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 era que podría implementar una autorización basada en URLs en lugar de métodos de controlador. Esto es más sólido porque, por lo general, no cambiará sus URL (tipo de área pública de URL), pero también podría cambiar las implementaciones de sus controladores.
Normalmente, tiene uno o varios archivos de configuración donde 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 cancela el envío si no lo está.