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.
#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 falta | o que acontece na prática |
|---|---|
| role sem ACL | o 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 role | a 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.
// 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 inteiroDuas 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:
(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.
| vulnerabilidade | como aparece | como evitar |
|---|---|---|
injeção via addEncodedQuery() | parâmetro vindo de fora entra direto na query, sem passar por validação | trocar por addQuery('campo', operador, valor) — nunca concatenar string externa numa encoded query |
| PII em log | gs.info()/gs.error() imprimindo e-mail, CPF ou dado sensível inteiro | logar o identificador (ID= + userId), nunca o dado em si |
| credencial no script | chave de API ou senha escrita direto no código ou numa System Property em texto plano | Connection & Credential Alias — a credencial fica criptografada e o script nunca a vê diretamente |
GlideRecord sem ACL em contexto de usuário | script client-callable usando new GlideRecord() puro para ler dado que o usuário chamador talvez não devesse ver | new 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ê.