artigo

ACL e roles: a porta e a fechadura não são a mesma coisa

Por que role sem ACL e ACL sem role são dois jeitos diferentes de vazar dado — e como desenhar as duas camadas juntas.

6 min de leitura avançado servicenow segurança arquitetura

#a conversa que todo projeto tem, mais cedo ou mais tarde

"Precisamos que o usuário X veja o registro Y, mas não possa editar o campo Z." Se você já ouviu essa frase num projeto ServiceNow, sabe o que vem depois: alguém abre o módulo de segurança, descobre que role e ACL foram tratadas como a mesma coisa desde o início, e qualquer regra nova ameaça quebrar uma regra velha que ninguém documentou.

#role sem ACL, ACL sem role: dois jeitos de vazar dado

O erro mais comum é achar que resolver uma camada resolve o problema inteiro. Não resolve — e resolve de dois jeitos diferentes, que valem a pena ver lado a lado:

o que faltao que acontece na prática
role sem ACLo usuário tem a role de "analista", mas como nenhuma ACL filtra por atributo do registro, ele vê contratos de todos os departamentos, não só do dele
ACL sem rolea ACL até verifica alguma condição, mas qualquer um com acesso ao módulo consegue editar qualquer campo, porque não existe hierarquia de permissão por trás

Nenhuma das duas é "mais segura" — são dois buracos diferentes na mesma parede. Role controla capacidade (quem pode aprovar mudança, quem pode fechar incidente); ACL controla escopo (quais registros e campos especificamente essa pessoa enxerga). Um sistema seguro de verdade usa as duas, e a ordem importa: desenhar a hierarquia de roles primeiro evita que a ACL vire um emaranhado de condições tentando compensar uma estrutura de permissão que não existe.

#hierarquia de role não é lista, é escada

A armadilha clássica é criar roles soltas — "user", "manager", "admin_contratos" — sem relação de herança entre elas. Resultado: você acaba atribuindo três roles manualmente pra cada usuário, e um dia esquece uma, e o aprovador não consegue nem visualizar o que está aprovando.

O jeito que funciona é hierárquico: cada nível inclui automaticamente tudo que o nível anterior tem.

javascript
// Hierarquia de roles — Scoped App x_acme_contracts
//
// x_acme_contracts.viewer
//   pode: visualizar contrato, exportar relatório padrão
//
// x_acme_contracts.analyst   (inclui viewer)
//   pode: criar rascunho, editar contrato em draft, solicitar aprovação
//
// x_acme_contracts.approver  (inclui analyst)
//   pode: aprovar contrato dentro da alçada configurada
//
// x_acme_contracts.admin     (inclui approver)
//   pode: cancelar, reverter, configurar o módulo inteiro

Duas regras de nomenclatura que evitam dor de cabeça mais tarde: prefixo do scope sempre (x_acme_contracts. na frente, nunca role solta), e nomes descritivos em vez de "manager" ou "admin" puro — esses colidem com role de sistema mais cedo ou mais tarde, e você só descobre quando alguém ganha acesso administrativo por engano.

#quando a ACL precisa de script, ela também precisa de cuidado

ACL declarativa (só role, sem script) é praticamente de graça em performance — avaliada com cache de sessão. Mas tem regra de acesso que não dá pra declarar: "usuário só vê contrato do próprio departamento, a menos que tenha role de leitura global". Isso exige script, e script em ACL tem uma pegadinha que não é óbvia na primeira vez que você esbarra nela.

A saída é cachear o resultado dentro da própria sessão, pra não repetir a mesma verificação registro após registro — usando gs.getSession().putClientData()/getClientData(), a API documentada para guardar dado na sessão do usuário sem nova consulta:

javascript
(function checkAccess() {
    // atalhos primeiro — sem custo de query nenhum
    if (gs.hasRole('x_acme_contracts.admin') || gs.hasRole('admin')) return true;
    if (gs.hasRole('x_acme_contracts.global_viewer')) return true;
    if (current.getValue('sys_created_by') === gs.getUserName()) return true;

    // para o resto: verificar departamento, mas só uma vez por sessão
    var userId = gs.getUserID();
    var dept = current.getValue('u_department');
    var cacheKey = 'contract_read_' + userId + '_' + dept;

    var session = gs.getSession();
    var cached = session.getClientData(cacheKey);
    if (cached !== null) {
        return cached === 'true'; // sem nova query
    }

    var grUser = new GlideRecord('sys_user');
    grUser.get(userId);
    var mesmoDept = (grUser.getValue('department') === dept);

    session.putClientData(cacheKey, String(mesmoDept));
    return mesmoDept;
})();

Repare na ordem: os casos que dispensam qualquer query (admin, viewer global, dono do registro) saem primeiro, antes de qualquer coisa tocar o banco. Só o caso realmente ambíguo — "esse usuário é do mesmo departamento?" — passa pelo cache. É a mesma lógica de saída rápida que já vale para Business Rule, aplicada aqui porque o custo de uma ACL mal otimizada se multiplica por linha de lista, não só por save.

#os erros que aparecem em praticamente todo scan de segurança

Fora do desenho de role/ACL, tem um punhado de descuidos de script que se repetem projeto após projeto — não por falta de conhecimento, mas porque parecem inofensivos até alguém explorar exatamente aquele ponto.

vulnerabilidadecomo aparececomo evitar
injeção via addEncodedQuery()parâmetro vindo de fora entra direto na query, sem passar por validaçãotrocar por addQuery('campo', operador, valor) — nunca concatenar string externa numa encoded query
PII em loggs.info()/gs.error() imprimindo e-mail, CPF ou dado sensível inteirologar o identificador (ID= + userId), nunca o dado em si
credencial no scriptchave de API ou senha escrita direto no código ou numa System Property em texto planoConnection & Credential Alias — a credencial fica criptografada e o script nunca a vê diretamente
GlideRecord sem ACL em contexto de usuárioscript client-callable usando new GlideRecord() puro para ler dado que o usuário chamador talvez não devesse vernew GlideRecordSecure() aplica a ACL da tabela automaticamente; GlideRecord puro fica só para script administrativo explícito

#e agora?

  • Na sua instância de dev, escolha uma tabela com ACL de leitura e confirme: existe alguma condição com script que faz query dentro do loop de renderização de lista? Se sim, veja se gs.getSession().putClientData()/getClientData() resolve antes de sair otimizando de outro jeito.
  • Rode uma busca rápida por addEncodedQuery( nos Script Includes e Business Rules da sua instância — é um dos achados mais comuns (e mais fáceis de corrigir) em qualquer revisão de segurança.
  • Este artigo é a continuação natural de precedência de campos: lá vimos quem manda no campo entre Data Policy, UI Policy e Client Script; aqui vimos quem manda no registro inteiro. Junte os dois e você tem o quadro completo de quem pode ver, editar e gravar o quê.
LinkedIn X/Twitter WhatsApp ☕ me paga um café