Pressione enter para ver os resultados ou esc para cancelar.

Hold the door! Controle de acesso no Drupal

Esse texto foi escrito a partir de experiências realizadas no Drupal 7. Porém, é provável que os mesmos conceitos abordados aqui sejam válidos para outras versões do Drupal, inclusive o Drupal 8.

O Drupal é reconhecido, dentre outras coisas, por seu sistema de controle de acesso robusto e flexível. Virtualmente todos os sistemas desenvolvidos em Drupal fazem uso de papéis e permissões, por exemplo, e criar uma permissão ou criar um papel de usuário no Drupal é algo razoavelmente trivial. De igual forma, é muito simples atribuir um papel à um usuário, ou relacionar uma permissão a um papel; boa parte desse processo, inclusive, pode ser realizada diretamente pela interface administrativa, garantindo uma gigantesca autonomia de controle aos administradores do site.

Ainda assim, esse sistema não é a solução para todos os problemas, e não raro precisamos de saídas mais granulares ou mais dinâmicas de controle de acesso, principalmente quando falamos em conteúdo. Para o caso de nodes, o Drupal oferece um sistema muito mais completo e programável: o sistema de acesso à nodes, ou…

Node access system

Sempre que tentamos acessar um node no Drupal, estamos sujeitos ao sistema de acesso. Esse sistema é disparado, por exemplo, ao se executar a função node_access, que recebe três argumentos: a operação (create, delete, update, view); o node; e o usuário requisitante. Essa função validará o direito de acesso na seguinte ordem:

  1. Usuário possui a permissão bypass node access;
  2. Usuário possui a permissão access content;
  3. Validação por hook_node_access;
  4. Validação por sistema de concessão de chaves.

Tão cedo uma das etapas tiver uma informação conclusiva o processo é finalizado. É o caso, por exemplo, de quando um usuário não possui a permissão access content; o resultado é negativo, e as validações conseguintes não são sequer executadas.

O terceiro e quarto passos são programáveis, e portanto mais granulares.

Como funciona o hook_node_access

Todo módulo que implementar o hook_node_access receberá três argumentos – o node, a operação (criação, remoção, atualização, ou visualização), e o usuário requisitante – e poderá responder à requisição de acesso de três formas:

  1. Explicitamente negando acesso;
  2. Explicitamente concedendo acesso;
  3. Explicita ou implicitamente ignorando.

A escolha se dá ao retornar do hook uma das constantes equivalentes: NODE_ACCESS_DENY, NODE_ACCESS_ALLOW, NODE_ACCESS_IGNORE. Não retornar valor da função consiste em implicitamente ignorar a requisição.

Caso qualquer implementação negar a requisição, o acesso ao node será negado. Caso nenhuma implementação negar a requisição, e ao menos uma conceder acesso, o acesso será permitido.

Se todas as implementações ignorarem a requisição, o sistema de acesso seguirá para o quarto e último passo do sistema de acesso: o sistema de concessão de chaves

O sistema de concessão de chaves

Embora o hook_node_access seja infinitamente flexível e portanto suficiente para resolver qualquer regra de negócio que envolva controle de acesso no Drupal, ele não só é performativamente inviável como conceitualmente errado quando falamos de listas (e, para o Drupal, listar nodes é considerado uma operação básica, quase como um uma quinta operação no tradicional CRUD). Isso ocorre porque esse tipo de operação é realizada diretamente no banco de dados, pode incluir paginação ou mesmo queries combinadas. Seria, portanto, inviável depender do hook_node_access para controlar acesso nesse nível, visto que teríamos que executar o hook para cada node possivelmente listado antes mesmo de gerar a lista em questão.

Entra o sistema de concessão de chaves. Esse sistema é composto por três conceitos fundamentais:

  1. Reino (ou, Realm);
  2. Registros de controle (ou, “cadeado”);
  3. Concessão de acesso (ou, “chave”).

Reino

O primeiro conceito são reinos. Um reino representa uma lógica de controle de acesso. Geralmente cada módulo que faz uso desse sistema de controle de acesso define um reino próprio, mas isso não é regra: tanto um módulo pode manipular (ou reaproveitar) um reino definido por outro módulo, como pode um módulo definir mais de um reino.

Imagine um sistema com dois módulos controlando acesso aos conteúdos: o primeiro, um sistema de grupo onde nodes e usuários podem pertencer à grupos e um usuário só terá acesso à um node caso ele faça parte de ao menos um grupo em comum com o node. Esse sistema é um “reino” próprio. O “Reino dos Grupos”. O segundo módulo define um sistema que permite escolher quais papéis de usuário terão acesso à um node através de um campo, por exemplo. Um usuário somente terá acesso à um node caso este usuário possua o papel que foi selecionado no campo deste node. Esse sistema é um novo reino: o “Reino dos Papéis”.

No exemplo descrito, para ter acesso a um determinado node o usuário haverá de ter acesso ao node em pelo menos um dos “reinos” a que o node pertence. Ou seja: ou pertencer à pelo menos um grupo do qual o node faça parte; ou possuir o papel selecionado para o node.

Esse sistema é cumulativo, bastando um reino conceder ao usuário acesso ao node para o acesso ser efetuado. Entretanto, é possível e fácil criar regras exclusivas através do conceito de “prioridade”, que não será abordado nesse momento.

Registros de controle

O segundo conceito são os registros de controle. Na prática são entradas em uma tabela do banco de dados, e podem ser vistas como cadeados a serem abertos. Cada node poderá ter múltiplas entradas nesse registro: uma para cada reino. Além do ID do node e do reino, cada registro de controle também definirá o nível de acesso (visualização, edição, remoção) e definirá uma chave (grant id) para esse acesso. Esta chave será sempre um valor numérico, e este número devera significar algo para o reino em questão. Para um “Reino de Grupos”, por exemplo, essa chave pode ser o ID do próprio grupo. Já para o “Reino dos Papéis” Portanto, essa é uma informação que pode ser alcançada tanto pelo lado do node quanto pelo lado do usuário requisitante, posteriormente. Isso nos leva ao terceiro conceito deste sistema…

Concessão de acesso

Toda vez que o sistema for verificar acesso à um determinado node, ou quando o sistema for realizar uma listagem de conteúdos, serão caculadas as concessões – ou chaves – de acesso do usuário requisitante. Perceba que diferentemente do hook_node_access, onde o algoritmo tem acesso ao node, à operação, e ao usuário, durante o calculo das concessões temos acesso apenas à operação e ao usuário. No exemplo do “Reino dos Grupos”, isso é tudo que precisamos. Definiremos que, dentro do “Reino dos Grupos”, este usuário tem diversas chaves de acesso: uma para cada grupo do qual faz parte.

Momento de execução

É importante compreender quando cada parte do processo é executada:

  • Registro de controle: executado quando um node é salvo, ou quando um novo módulo é instalado. Em alguns casos, porém, é necessário executar essa operação para todos osnodes – devido à alguma modificação nas configurações de um módulo, por exemplo. Neste caso, o Drupal exibirá a tradicional mensagem “The content access permissions need to be rebuild“. Ou, em português: “As permissões de acesso ao conteúdo precisam ser reconstruídas.“.
  • Concessão de acesso: executado toda vez que o Drupal for validar acesso à um node ou quando uma listagem estiver por ocorrer.

Exemplo prático

Para praticar os conceitos teóricos vistos até aqui e facilitar a escolha entre os dois modelos de controle de acesso (hook_node_access ou o sistema de concessão de chaves), vamos construir um sistema que permite controlar acesso aos conteúdos dependendo do período do dia.

Teremos um tipo de conteúdo chamado Guarda, e nele um campo chamado Turno onde constará o período em que o node estará disponível para visualização.

Usando hook_node_access

Com o hook_node_access poderíamos alcançar esse resultado com o seguinte código:

/**
 * Implements hook_node_access().
 */
function meu_modulo_node_access($node, $op, $account) {
  // Essa função não tem nada a dizer quando a operação
  // requisitada for uma que não a visualização do node.
  if ($op !== 'view') {
    return NODE_ACCESS_IGNORE;
  }

  $turno_atual = meu_modulo_turno_atual();
  $turno_do_node = $node->field_turno[LANGUAGE_NONE][0]['value'];

  return $turno_atual === $turno_do_node ? NODE_ACCESS_ALLOW : NODE_ACCESS_DENY;
}

function meu_modulo_turno_atual() {
  // Alguma lógica que retorne o turno atual baseado na hora atual.
  // Os turnos são representados por números, da seguinte forma:
  // manha: 1
  // tarde: 2
  // noite: 3
}

Obs.: código parcialmente em português para fins de acessibilidade.

Até aqui, perfeito! Quando um usuário tentar acessar a página de um node, a requisição será negada caso o turno atual não seja o mesmo que o especificado pelo próprio node. Repare que diferentemente do exemplo de grupos ou do exemplo de papéis, a decisão aqui independe do usuário requisitante; depende de um contexto do próprio sistema.

Usando o sistema de concessão de chaves

Podemos alcançar a mesma regra de negócio através do sistema de concessão de chaves da seguinte forma:

/**
 * Implements hook_node_access_records().
 */
function meu_modulo_access_node_access_records($node) {
  $grants = array();

  $turno_do_node = $node->field_turno[LANGUAGE_NONE][0]['value'];

  $grants[] = array(
    'realm' => 'turno',
    'gid' => $turno_do_node,
    'grant_view' => 1,
    'grant_update' => 0,
    'grant_delete' => 0,
    'priority' => 0,
  );

  return $grants;
}

/**
 * Implements hook_node_grants().
 */
function meu_modulo_access_node_grants($account, $op) {
  $grants = array();

  $grants['turno'][] = meu_modulo_turno_atual();

  return $grants;
}

function meu_modulo_turno_atual() {
  // Alguma lógica que retorne o turno atual baseado na hora atual.
  // Os turnos são representados por números, da seguinte forma:
  // manha: 1
  // tarde: 2
  // noite: 3
}

Com pouco código a mais, temos agora um sistema de permissão que será respeitado por listagens tanto de módulos do core (ex.: Menu), como módulos contribuídos (ex.: Views).

Adicional

Prioridade do registro

Vocês devem ter percebido o código 'priority' => 0 dentro do hook_node_access_records. Como falei anteriormente, podemos ter diversos módulos – e diversos reinos – atuando sobre um mesmo node. Neste caso, a concessão de acesso será sempre cumulativa; mesmo que um usuário não tenha recebido concessão dentro de um reino à um node, se outro reino conceder acesso o usuário terá acesso – diferente do que ocorre com o hook_node_access. Porém, diversas vezes temos regras que precisam se sobrepor à qualquer outro. Nesses casos, podemos usar de prioridade. Quando dois registros atuarem sobre um mesmo node, caso a prioridade deles seja diferente entre si somente o registro de maior prioridade será gravado no banco.

Tag node_access para consultas no banco

Para garantir que consultas realizadas pelo seu módulo respeitem o sistema de concessão de chaves, basta adicionar a tag node_access às suas consultas, como no exemplo abaixo:

$query = db_select('node', 'n');
  ->fields('n', array('nid', 'title'))
  ->addTag('node_access');

$result = $query->execute();

Conclusão

O sistema de controle de acesso no Drupal é extremamente flexível e poderoso. Se bem utilizado, garantirá que suas regras de acesso sejam utilizadas por todo o ecossistema do Drupal sem muito esforço. Quase todos os módulos contribuídos se integram bem nesse sistema, visto que tudo o que têm a fazer é adicionar uma tag às consultas customizadas.

Espero que esse texto tenha ajudado a compreender esse sistema razoavelmente complexo de permissionamento.


 

Créditos:
Ilustação de capa por: Mario Flores