Cuando desarrollamos una aplicación web o cualquier producto digital debemos tener en cuenta que sus futuros usuarios, por lo general, tendrán roles y competencias distintas y podrán acceder a unas funcionalidades o a otras dependiendo de los permisos asociados a cada uno de estos perfiles.

Podemos pensar, por ejemplo, en una aplicación para una tienda digital. Será necesario determinar si un usuario puede comprar un artículo o venderlo o, incluso, si tiene permisos para editar su descripción o retirarlo de catálogo.

Por eso, hoy vamos a ver en qué consisten los permisos y cómo gestionarlos fácilmente en React.

La arquitectura de un permiso

En pocas palabras: un permiso se encarga de decidir si un usuario puede realizar una acción o de si se debe mostrar un elemento dependiendo de su rol. Resulta tentador incluir un campo “isAdmin” en el modelo de usuario y tomar la decisión a partir de su presencia o no, pero es fácil ver lo poco escalable que es esta solución ya que pueden surgir más perfiles, además del de único administrador, que tengan permisos sobre diferentes tipos de entidades y en función de diferentes condiciones.

Una solución mucho más mantenible y extensible es la basada en perfiles o, alternativamente, permisos individuales, que se adapte a las características dadas al usuario logueado y que sean aplicables a cada vista de la aplicación.

Puesto en forma gráfica, algo así:

Entra en juego CASL

A pesar de ser un problema ubicuo a tantas aplicaciones, no parece haber una solución para Javascript estandarizada en formato ni en forma de librería. En los últimos años, sin embargo, se empieza a imponer CASL de Sergii Stotskyi por su versatilidad y su integración con frameworks.

Como veremos con ejemplos, CASL nos proporciona acceso a esta arquitectura de tres pasos y, además, con total libertad a la hora de definir permisos: definición, customización y consulta.

Definición

La recomendación es crear un archivo ability.js aunque, en algunas ocasiones, puede ser mejor crear una carpeta indexada con diferentes archivos relativos a los tipos de entidades que maneja la aplicación.
Por simplificar en un solo archivo para el ejemplo en ability.js:

import { AbilityBuilder } from '@casl/ability';

const subjectName = subject => (
  !subject || typeof subject === 'string' ? subject : subject.$type                           );

export default profile => AbilityBuilder.define(
  { subjectName },
  (can) => {
    can(‘create’, ‘article’);
    can(‘edit’, ‘article’, { ownerId: profile.id });
    if (profile.type === ‘admin’) {
      can(‘delete’, ‘article’);
    }
  },
);

Vemos aquí que usamos la API AbilityBuilder de CASL que nos permite definir los permisos en base a un parámetro profile que nosotros le pasamos.
Con can, definimos los permisos que estarán disponibles para el usuario (algunos condicionalmente). El usuario puede crear un archivo, editar los que le pertenecen (con condiciones) y eliminar artículos solo si tiene perfil de administrador (con control lógico de flujo).

En el caso de una habilidad condicional, podemos utilizar notación de Mongo para hacer una condición más compleja:

can(‘edit’, ‘article’, { status: { $in: [‘draft’, ‘revision’] } });

En este ejemplo, el usuario podría editar un artículo si éste se encuentra en estado “draft” o “revisión”. Muchas más condiciones de Mongo están disponibles como $eq, $ne, $in, $all, $gt, $lt, $gte, $lte, $exists, $regex, $elemMatch o cualificación de campos con puntos.

La flexibilidad en esta parte es infinita. El objeto profile que recibimos puede tener cualquier forma y nosotros darle el proceso que queramos dentro de este bloque define de AbilityBuilder.

Con esto estaríamos definiendo una carta de permisos generales que más adelante personalizaremos con el usuario que esté logueado.

Customización

Con los permisos generales definidos, queda adaptarlos al usuario logueado. La implementación concreta depende de nuestra aplicación en particular: puede que obtengamos el perfil del usuario al tiempo que se loguea o con una llamada aparte.

Para el propósito de nuestro ejemplo, imaginemos que estamos en una aplicación Redux y que ha quedado almacenado el perfil en el índice profile de nuestro store.

Ahora uniremos en un componente Can personalizado la carta general de permisos con el perfil particular del usuario logueado.

En components/Can.js:

import { connect } from 'react-redux';
import { Can } from '@casl/react';
import ability from '../ability';

const CustomCan = ({ children, profile, ...props }) => (
  <can {...props}="" ability="{ability(profile)}">
    {children}
  </can>
);

const mapStateToProps = state =&gt; ({
  profile: state.profile,
});

export default connect(mapStateToProps)(CustomCan);

De esta forma tenemos un componente Can que está permanentemente actualizado con el perfil que guardamos en el store y ahora veremos para qué nos va a resultar útil eso.

En el store debemos hacer que todas las entidades incluyan un valor $type que describa el tipo de entidad para que se puedan hacer más adelante las comprobaciones. Esto se puede hacer fácilmente con un cambio en la saga que recibe la llamada o en el reductor si nuestra aplicación es de Redux. El resultado en el store debe ser el siguiente:

{
  entities: {
    articles: [
      { $type: ‘article’, title:’título 1’, content: ‘...’ },
      { $type: ‘article’, title:’título 2’, content: ‘...’ },
      { $type: ‘article’, title:’título 3’, content: ‘...’ },
    ],
  },
}<br>

Esta parte va unida a la función subjectName en ability.js y en ella podríamos cambiar la key en la que se comprueba el tipo si quisiéramos, por ejemplo.

Consulta

La mayor parte de las veces, el control permisos en el frontal de la aplicación implica la diferencia entre mostrar un botón (o no) en un componente de vista. Esto lo podemos hacer de forma declarativa con el componente Can que acabamos de crear de la siguiente manera:

import { Can } from ‘../components’;

const  View = ({ article }) =&gt; (
   ...
  <can i="”delete”" this="{article}">
    <button onclick="{delete(article)}">Delete</button>
  </can>
  ...
  <can i="”create”" an="”article”">
    <button onclick="{history.push(‘/articles/create’)}">Create</button>
  </can>
  ...
  <can i="”edit”" this="{article}" passthrough="">
  {
     can =&gt; <button onclick="{edit(article)}" disabled="{!can}">Edit</button>
  }
  </can>
  ...
);

En este pequeño ejemplo podemos ver la versatilidad (y literalidad) de un componente Can de CASL. En el primer caso comprobamos si el usuario puede hacer “delete” contra el objeto “artículo” que tenemos en la vista. También, comprobamos si el usuario puede crear un artículo (uno cualquiera, pasando solo el nombre de la entidad article) y por último usamos la interfaz passthrough para mostrar el botón independientemente del caso pero con el atributo disabled activado o desactivado dependiendo de si el usuario tiene permiso para editar.

En algunos casos debemos hacer una comprobación de permisos más complicada, para lo cual podemos utilizar la interfaz orientada a objetos de CASL.

import { connect } from ‘react/redux’;
import ability from ‘../ability’;

const View = ({ article, permissions }) => (
  ...
  {
    permissions.can(‘edit’, article)
    && permissions.can(‘delete’, article)
    && User has complete permissions over this article!
  }
  ...
);

const mapStateToProps = state = ({
  permissions: ability(state.profile),
});

export default connect(mapStateToProps)(View);

En este ejemplo hacemos una fusión manual del perfil con la carta de habilidades en el mapStateToProps, que podremos consultar desde el componente a modo de objeto, por ejemplo, si el usuario puede editar y borrar este artículo en concreto mostraremos un mensaje diciendo que tiene permisos totales sobre él.

Conclusión

CASL es una librería con una API extensa que, aunque está lejos de estar bien documentada, nos permite control total sobre cualquier modelo de permisos que se nos ocurra. Su API orientada a objetos es suficiente para adaptarla a cualquier aplicación de Javascript, tanto de front como de back (con Node), ofrece beneficios adicionales al usarla con una base de datos Mongo como serializado y rehidratación de permisos y, además, su autor mantiene librerías de integración con infinidad de frameworks (React, Angular y Vue para empezar pero también muchos otros).

Todas estas razones, pero particularmente la de la flexibilidad, hacen de CASL una librería muy recomendable para gestionar el complejo problema de los permisos.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete