design-patterns permissions servicestack

design patterns - ¿El mejor patrón de diseño para controlar los permisos en base a "por objeto por usuario" con ServiceStack?



design-patterns permissions (1)

Utilizo permisos por objeto en mis aplicaciones ServiceStack. Efectivamente, esta es una lista de control de acceso (ACL).

He creado un ejemplo de consola de trabajo con alojamiento propio que puede bifurcar en GitHub.

Patrón de ACL:

Utilizo la estructura de base de datos que se muestra en el diagrama a continuación, donde los recursos en mi base de datos, como documentos, archivos, contactos, etc. (cualquier recurso que quiera proteger) reciben una identificación de ObjectType .

La tabla de permisos contiene reglas que se aplican a usuarios específicos, grupos específicos, objetos específicos y tipos de objetos específicos, y es flexible para aceptarlos en combinaciones, donde un valor null se tratará como un comodín.

Asegurando el servicio y las rutas:

Encuentro que la forma más fácil de manejarlos es usar un atributo de filtro de solicitud. Con mi solución, simplemente agrego un par de atributos a mi declaración de ruta de solicitud:

[RequirePermission(ObjectType.Document)] [Route("/Documents/{Id}", "GET")] public class DocumentRequest : IReturn<string> { [ObjectId] public int Id { get; set; } } [Authenticate] public class DocumentService : Service { public string Get(DocumentRequest request) { // We have permission to access this document } }

Tengo un atributo de filtro llamado RequirePermission , esto realizará la verificación para ver si el usuario actual que solicita la DTO DocumentRequest tiene acceso al objeto Document cuyo ObjectId está dado por el Id propiedad. Eso es todo lo que hay que hacer al verificar las rutas en mis rutas, por lo que es muy SECO.

El atributo de filtro de solicitud de RequirePermission :

El trabajo de prueba de permiso se realiza en el atributo de filtro, antes de alcanzar el método de acción del servicio. Tiene la prioridad más baja, lo que significa que se ejecutará antes de los filtros de validación.

Este método obtendrá la sesión activa, un tipo de sesión personalizado (detalles a continuación) , que proporciona la identificación del usuario activo y las identificaciones de grupo a las que pueden acceder. También determinará el objectId si alguno de la solicitud.

Determina la identificación del objeto al examinar las propiedades del DTO de la solicitud para encontrar el valor que tiene el atributo [ObjectId] .

Con esa información, consultará la fuente de permisos para encontrar el permiso más apropiado.

public class RequirePermissionAttribute : Attribute, IHasRequestFilter { readonly int objectType; public RequirePermissionAttribute(int objectType) { // Set the object type this.objectType = objectType; } IHasRequestFilter IHasRequestFilter.Copy() { return this; } public void RequestFilter(IRequest req, IResponse res, object requestDto) { // Get the active user''s session var session = req.GetSession() as MyServiceUserSession; if(session == null || session.UserAuthId == 0) throw HttpError.Unauthorized("You do not have a valid session"); // Determine the Id of the requested object, if applicable int? objectId = null; var property = requestDto.GetType().GetPublicProperties().FirstOrDefault(p=>Attribute.IsDefined(p, typeof(ObjectIdAttribute))); if(property != null) objectId = property.GetValue(requestDto,null) as int?; // You will want to use your database here instead to the Mock database I''m using // So resolve it from the container // var db = HostContext.TryResolve<IDbConnectionFactory>().OpenDbConnection()); // You will need to write the equivalent ''hasPermission'' query with your provider // Get the most appropriate permission // The orderby clause ensures that priority is given to object specific permissions first, belonging to the user, then to groups having the permission // descending selects int value over null var hasPermission = session.IsAdministrator || (from p in Db.Permissions where p.ObjectType == objectType && ((p.ObjectId == objectId || p.ObjectId == null) && (p.UserId == session.UserAuthId || p.UserId == null) && (session.Groups.Contains(p.GroupId) || p.GroupId == null)) orderby p.ObjectId descending, p.UserId descending, p.Permitted, p.GroupId descending select p.Permitted).FirstOrDefault(); if(!hasPermission) throw new HttpError(System.Net.HttpStatusCode.Forbidden, "Forbidden", "You do not have permission to access the requested object"); } public int Priority { get { return int.MinValue; } } }

Prioridad de permiso:

Cuando se leen los permisos de la tabla de permisos, el permiso de mayor prioridad se usa para determinar si tienen acceso. Cuanto más específica sea la entrada de permiso, mayor será la prioridad que tiene cuando se ordenan los resultados.

  • Los permisos que coinciden con el usuario actual tienen mayor prioridad que los permisos generales para todos los usuarios, es decir, donde UserId == null . De manera similar, un permiso para el objeto solicitado específicamente tiene mayor prioridad que el permiso general para ese tipo de objeto.

  • Los permisos específicos del usuario tienen prioridad sobre los permisos de grupo. Esto significa que a un usuario se le puede otorgar acceso mediante un permiso grupal, pero se le puede negar el acceso a nivel de usuario, o viceversa.

  • Cuando el usuario pertenece a un grupo que les permite acceder a un recurso y a otro grupo que les niega el acceso, el usuario tendrá acceso.

  • La regla predeterminada es denegar el acceso.

Implementación:

En mi código de ejemplo anterior, he usado esta consulta linq para determinar si el usuario tiene permiso. El ejemplo utiliza una base de datos simulada y deberá sustituirla por su propio proveedor.

session.IsAdministrator || (from p in Db.Permissions where p.ObjectType == objectType && ((p.ObjectId == objectId || p.ObjectId == null) && (p.UserId == session.UserAuthId || p.UserId == null) && (session.Groups.Contains(p.GroupId) || p.GroupId == null)) orderby p.ObjectId descending, p.UserId descending, p.Permitted, p.GroupId descending select p.Permitted).FirstOrDefault();

Sesión personalizada:

He utilizado un objeto de sesión personalizado para almacenar las membresías de grupo, estas se buscan y se agregan a la sesión cuando el usuario se autentica.

// Custom session handles adding group membership information to our session public class MyServiceUserSession : AuthUserSession { public int?[] Groups { get; set; } public bool IsAdministrator { get; set; } // The int value of our UserId is converted to a string!?! :( by ServiceStack, we want an int public new int UserAuthId { get { return base.UserAuthId == null ? 0 : int.Parse(base.UserAuthId); } set { base.UserAuthId = value.ToString(); } } // Helper method to convert the int[] to int?[] // Groups needs to allow for null in Contains method check in permissions // Never set a member of Groups to null static T?[] ConvertArray<T>(T[] array) where T : struct { T?[] nullableArray = new T?[array.Length]; for(int i = 0; i < array.Length; i++) nullableArray[i] = array[i]; return nullableArray; } public override void OnAuthenticated(IServiceBase authService, ServiceStack.Auth.IAuthSession session, ServiceStack.Auth.IAuthTokens tokens, System.Collections.Generic.Dictionary<string, string> authInfo) { // Determine UserId from the Username that is in the session var userId = Db.Users.Where(u => u.Username == session.UserName).Select(u => u.Id).First(); // Determine the Group Memberships of the User using the UserId var groups = Db.GroupMembers.Where(g => g.UserId == userId).Select(g => g.GroupId).ToArray(); IsAdministrator = groups.Contains(1); // Set IsAdministrator (where 1 is the Id of the Administrator Group) Groups = ConvertArray<int>(groups); base.OnAuthenticated(authService, this, tokens, authInfo); } }

Espero que encuentres este ejemplo útil. Déjame saber si algo no está claro.

Validación fluida:

Además, ¿se puede integrar esto con FluentValidation y devolver las respuestas HTTP apropiadas?

No debe intentar hacer esto en el controlador de validación, ya que no es una validación . Verificar si tienes permiso es un proceso de verificación. Si necesita comparar algo con un valor específico en una fuente de datos, ya no está realizando la validación. Vea esta otra respuesta mía que también cubre esto.

Sé que ServiceStack proporciona un atributo RequiredRole para controlar los permisos, sin embargo, esto no funciona completamente para mi caso de uso. Tengo un sitio web con un montón de contenido generado por el usuario. Los usuarios solo pueden editar documentos para los que tienen permisos explícitos. Los permisos son controlados por objeto o grupo del objeto. Por lo tanto, si un usuario es administrador de un grupo, puede editar todos los documentos administrados por ese grupo.

¿Cuál es el mejor patrón de diseño para controlar el acceso a una solicitud per object per user ? Quiero abordar esto con una metodología lo más DRY posible, ya que afectará al 95% de todos mis puntos finales de API.

Además, ¿se puede integrar esto con FluentValidation y devolver las respuestas HTTP apropiadas?

Muchas gracias,

Ricardo.