Trabajando con Node access en Drupal 7

Permisos, roles, acceso a contenidos... son unos temas que pueden atragantarsele incluso al más aguerrido desarrollador de Drupal, hace un par de semanas que me he estado peleando con el sistema de acceso a contenidos en Drupal 7 y voy a resumir mis conclusiones en este post.

Drupal 7 proporciona una serie de herramientas bastante completas para controlar el acceso a los nodos, y existen módulos que tratan el caso básico de acceso a contenido, siendo el más completo y recomendado Content Access que permite controlar el acceso por roles al contenido, incluso pudiendo restringir por cada nodo, también es interesante el sandbox Simple content access que permite restringir además por tipo de contenido. Pero... ¿qué pasa si queremos algo personalizado?

Mi caso de uso ha sido la funcionalidad First Click Free de Google para un sistema de publicación. First Click Free implica que el usuario que visita un sitio puede ver la primera página libremente pero en cuanto haga click para ver más contenido, se le pide que se registre, con un modelo que suele ser de pago, pero podría ser simplemente restringir acceso a no registrados.

Si bloquearamos el sitio para usuarios no registrados, los buscadores no podrían indexar la página, esta es la razón por la que Google promociona este tipo de acceso para sitios que estarían normalmente cerrados, de forma que sus arañas puedan rastrear y evaluar mejor el contenido.

Para realizar esta funcionalidad he publicado el sandbox First click free node access, una versión del módulo First Click Free pero utilizando una aproximación basada en el acceso a nodos nativo de Drupal.

Por defecto, sin acceso

La implementación de acceso a nodos que Drupal 7 implementa deniega el acceso por defecto. Cuando instalamos Drupal, el módulo node inserta un registro en la tabla node_access que permite el acceso a todos los contenidos publicados para todos los usuarios, incluyendo anónimos.

En cuanto instalamos cualquier módulo que restrinja el acceso a los nodos, como pueda ser Content Access o First click free node access, nos pedirá reconstruir los permisos y rellenará la tabla con todos los registros configurados, denegando el acceso en caso de que ninguno de los registros cubra el acceso de un usuario a un nodo.

Usando hook_node_access

Drupal 7 introduce el hook hook_node_access que nos permite controlar el acceso a un contenido determinado para un usuario determinado cuando se ejecuta una operación (view, edit, create, delete). Si bien el uso de este hook nos permite un control directo sobre un nodo, es bastante peligroso, ya que cualquier módulo podría realizar una implementación que devuelva acceso denegado (NODE_ACCESS_DENY), que provocará que para ese caso, retorne acceso denegado sin tener en cuenta el resto de controles de acceso.

Esta estrategia tiene otros problemas, por ejemplo, Views no lo utiliza así que este código será ignorado si mostramos nuestro contenido a través de vistas.

Ejemplo:

function mimodulo_node_access($node, $op, $account) {
  if ($account->name == 'pcambra' && $op == 'view') {
    return NODE_ACCESS_DENY;
  }
}

Si utilizaramos un módulo que implementara algo parecido al código de arriba, el acceso seguiría siendo denegado aunque existieran otras reglas que garantizaran el acceso, excepto si mostraramos el contenido con Views o cualquier otro módulo que no invoque hook_node_access. El coste de mantenimiento crecería muchísimo.

La alternativa: hook_node_grants y hook_node_access_records

Por suerte, Drupal 7 nos ofrece una alternativa para la anterior problemática, un sistema que podríamos llamar de llaveros, llaves y cerraduras. Como hemos comentado más arriba, Drupal devolverá acceso denegado por defecto para todo aquello que no conozca, es decir que si nos encontramos ante una puerta (nodo), esta tendrá una cerradura (record) y para abrir esta cerradura solamente necesitamos una de las múltiples llaves posibles (grants) siempre y cuando llevemos el llavero correcto en el bolsillo (realm).

Las cerraduras (records) están almacenadas en la tabla node_access, allí habrá por cada nodo, una fila en la tabla que marque el llavero (realm) y la llave (grant) que dan acceso para la operación concreta. Los grants son flags boolean, por lo que 1 implica acceso concedido y 0 denegado, pero solamente necesitamos que una de las llaves que llevamos en nuestro llavero abra, por lo que con que haya solamente un 1 para el realm será suficiente.

La tabla node_access tiene este aspecto cuando instalamos Drupal por primera vez y se quedará así hasta que instalemos un módulo que modifique las reglas de acceso (records y grants)

mysql> select * from node_access;
+-----+-----+-------+------------+--------------+--------------+
| nid | gid | realm | grant_view | grant_update | grant_delete |
+-----+-----+-------+------------+--------------+--------------+
|   0 |   0 | all   |          1 |            0 |            0 |
+-----+-----+-------+------------+--------------+--------------+
1 row in set (0.00 sec)

El contenido de esta tabla quiere decir que todo el mundo, con cualquier llavero (realm = all) puede ver todos los nodos de nuestro sitio. Aquí entran en juego los permisos normales de Drupal, como ver contenido publicado, o editar nodos, que funcionan a un nivel diferente.

¿Cómo declaramos llaveros?

El hook hook_node_access_records nos permite declarar tantos llaveros como nos haga falta, si retomamos el ejemplo de First Click Free, necesitaremos un llavero por cada rol que acceda y por cada tipo de contenido para poder diferenciar cuando el usuario intente visualizar un nodo, dependiendo de su rol y el tipo de contenido al que acceda, puede que tenga permisos o no. En el caso de First Click Free, esta separación está hecha para poder seleccionar en una interfaz de administración los roles y tipos de contenido afectados por el acceso (puede que a ciertos tipo de contenido y roles no se les apliquen estas reglas de acceso)

/**
* Implements hook_node_access_records().
*/
function fcfna_node_access_records($node) {
  $grants = array();
  $roles = variable_get('fcfna_rid', array(DRUPAL_ANONYMOUS_RID => DRUPAL_ANONYMOUS_RID));

  foreach ($roles as $rid) {
    $grants[] = array(
      'realm' => 'fcfna_' . $node->type,
      'gid' => $rid,
      'grant_view' => 1,
      'grant_update' => 0,
      'grant_delete' => 0,
      'priority' => 0,
    );
  }

  return $grants;
}

En cuanto declaremos un hook en nuestro módulo que declare access records, la interfaz de drupal nos pedirá reconstruir los permisos  ya que los registros de la tabla node_access están cacheados por motivos de rendimiento y cuando se detecta un cambio, nos pedirá rellenar esta tabla de nuevo. Hay que tener precaución con esto, ya que si tenemos muchísimos nodos y llaveros declarados (realms), la operación puede ser muy costosa.

Por ejemplo, cuando activamos First click free node access y Content Access, al reconstruir los permisos, la tabla node_access puede tener este aspecto:

mysql> select * from node_access;
+-----+-----+--------------------+------------+--------------+--------------+
| nid | gid | realm              | grant_view | grant_update | grant_delete |
+-----+-----+--------------------+------------+--------------+--------------+
|  52 |   1 | fcfna_article      |          1 |            0 |            0 |
|  52 |   2 | content_access_rid |          1 |            0 |            0 |
|  53 |   1 | fcfna_article      |          1 |            0 |            0 |
|  53 |   2 | content_access_rid |          1 |            0 |            0 |

Donde nid es el id del nodo, realm es el llavero con el que accedemos y grant_* son las llaves o permisos que tenemos.

Pero ¿Cómo sabe Drupal qué llaveros y llaves lleva el usuario encima?
Para responder a esto, vamos a ver la implementación de hook_node_grants, o lo que es lo mismo, cómo relacionar al usuario con el contenido visualizado a través de estos permisos.

/**
 * Implements hook_node_grants().
 */
function fcfna_node_grants($account, $op) {
  $grants = array();
  $roles = variable_get('fcfna_rid', array(DRUPAL_ANONYMOUS_RID => DRUPAL_ANONYMOUS_RID));
  $content_types = variable_get('fcfna_content_types', '');
  $referrer = parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST);
  $host = $_SERVER['HTTP_HOST'];

  // If the referrer is empty or it's different from the host, we need to grant
  // access.
  if ((empty($referrer) || $referrer <> $host)) {
    foreach ($content_types as $type) {
      foreach($account->roles as $rid => $unused) {
        if (!empty($roles[$rid])) {
          $grants['fcfna_' . $type] = array($rid);
        }
      }
    }
  }

  return $grants;
}

Cada vez que un usuario accede a un nodo, se genera una matriz con los llaveros (realms) y llaves (grants) que el usuario tiene, en el ejemplo, por cada tipo de contenido y rol, se asigna un 1 (acceso concedido) si el referrer de la página no es la propia página (accesos externos), el resto de casos quedan vacíos y como hemos visto, es un acceso denegado por defecto. Visto esto, el usuario accederá a un nodo usando la matriz generada por el hook de arriba y esta se cotejará con la tabla node_access, si alguno de los registros para la operación actual es 1 en ambos sitios, el usuario podrá ver el contenido, en caso contrario, se mostrará una página 403 de Access Denied.

Este hook se ejecuta por cada acceso al contenido por parte de un usuario, sin caché, así que debemos ser cuidadosos ya que puede impactar negativamente en el rendimiento de nuestro sitio.

Views respeta este hook, incluso se da el caso de que si realizamos vistas complejas con Relationships, o usamos Panels, esta comprobación de acceso es efectiva para el nodo actual y el relacionado así que se ejecutará tantas veces como nodos tengamos en nuestras relaciones en vistas. Es posible desactivar este chequeo bajo nuestra propia responsabilidad en las opciones avanzadas de Views, activando la opción Disable SQL rewriting.

Más recursos e información

Crédito de la foto: Leo Reynolds. ¡Gracias por compartir con licencia Creative Commons!

Comentarios

Hola Pedro:
En primer lugar me gustaría darle las gracias por este post.

No soy programadora pero llevo un año peleándome con Drupal y más o menos he entendido todo lo que nos explicaba.

Tengo un grave problema de acceso de los usuarios registrados a mi web, y es que una vez registrados ( sin aprobación de admin), cuando quieren iniciar una sesión normal y se logean reciben un mensaje de (acceso denegado) a la primera, si cierran sesión y vuelven a entrar ya les funciona....

No sé si la solución pasará por implementar todo lo que aquí describe pero la verdad estoy bastante desesperada pues he revisado permisos, error.log, he desisnstalado modulos, base de datos ... en fin de todo... y no tengo suerte... pero lo curioso del tema es que a la primera te echa del sistema y a la segunda ya te reconoce....alguna sugerencia??

Mil gracias por el post de todos modos,
Munts

Hola amigo. Gracias por esta información tan importante.
Hace poco visito este sitio web y la verdad me ha parecido muy bueno con información muy importante para las personas que estamos trabajando con drupal. Gracias por compartir =).

Hola, gracias, este post es super.
Tengo una duda si al declarar un hook y sale Reconstruir permisos y claro está al hacerlo perderé todo los permisos, entonces que debp hacer?
GRacias.

Muchas gracias por el post, muy esclarecedor.

Añadir nuevo comentario