InícioDesafios › Desafio 07

Desafio 07 Nível: Avançado

Descobrir por que o usuário não vê o registro

O vendedor abre o link da oportunidade que o gerente mandou e recebe "Insufficient Privileges". O registro existe, você acabou de vê-lo, mas ele jura que sumiu. Responder "é o sharing" não resolve nada: a pergunta certa é qual regra concede ou nega o acesso, e é isso que separa quem sabe a teoria de quem consegue depurar uma org de verdade sob pressão. Neste desafio você constrói a ferramenta que responde essa pergunta em vez de abrir cinco telas de setup no chute.

UserRecordAccess Sharing e OWD RowCause SOQL dinâmico getSObjectType Diagnóstico de acesso

O cenário

O suporte abre um chamado por semana com a mesma frase: "o usuário não está enxergando o registro". Às vezes é OWD privado sem regra de compartilhamento. Às vezes o dono trocou e a hierarquia de papéis não alcança. Às vezes existe um compartilhamento manual que alguém apagou. Cada investigação vira uma peregrinação por Sharing Settings, papéis, regras e o botão Sharing do registro, que nem sempre está visível.

Você decide encerrar o chute e construir um diagnóstico programático. Dado um usuário e um registro, ele precisa dizer duas coisas de forma objetiva: o usuário vê ou não, e por quê. Não interessa devolver só true ou false: o valor está no motivo, porque é o motivo que aponta onde mexer.

A dificuldade é que a origem do acesso não está num campo único. O veredito mora num objeto especial de só leitura, e a explicação mora nas tabelas de compartilhamento, cujo nome muda conforme o objeto.

O que você deve construir

Crie uma classe Apex chamada AcessoDiagnostico com uma classe interna de resultado e um método público e estático, exatamente com esta assinatura:

public class Diagnostico {
    public Boolean temAcesso;
    public String nivelMaximo;
    public List<String> motivos;
}

public static Diagnostico diagnosticar(Id userId, Id recordId)

O método deve:

  • Receber o Id de um User e o Id de um registro de qualquer objeto.
  • Devolver um Diagnostico dizendo se há acesso de leitura, o nível máximo de acesso e a lista de motivos que explicam esse acesso (ou a falta dele).
  • Funcionar tanto para objeto padrão (Account, Opportunity) quanto para objeto custom, sem hardcode do nome da tabela de compartilhamento.

Regras de aceite

São elas que definem se o desafio foi resolvido. As três últimas são o coração do exercício, porque é onde quase todo mundo simplifica demais:

  • O veredito de acesso vem de UserRecordAccess, não de uma suposição sua sobre o OWD. É esse objeto que diz, com autoridade, se o usuário tem leitura e até que nível (HasReadAccess, MaxAccessLevel).
  • Devolva o motivo, não só o booleano. Quando há acesso, os motivos precisam traduzir a origem: dono, compartilhamento manual, regra, equipe, hierarquia. Um resultado com temAcesso = true e a lista de motivos vazia é uma solução incompleta.
  • Acesso que não deixa rastro direto ainda precisa de um motivo. Se o usuário vê o registro mas não existe nenhuma linha de compartilhamento apontando para ele (caso clássico de acesso por hierarquia de papéis ou OWD público), o diagnóstico não pode devolver motivo vazio: tem que dizer que o acesso é indireto.
  • Registro inexistente não lança exceção. Um Id sintaticamente válido que não existe (ou que o usuário não alcança) devolve temAcesso = false com um motivo, nunca null e nunca um erro não tratado.
  • O dono precisa ser reportado como dono mesmo quando não há compartilhamento manual para ele. O acesso do proprietário nem sempre aparece como linha de share.
  • userId ou recordId nulo devolve um Diagnostico sem acesso, sem consultar nada e sem erro.

O que este desafio avalia

  • Se você sabe onde o Salesforce guarda a verdade sobre acesso a registro, em vez de reimplementar a lógica de sharing na mão.
  • Se você conhece a diferença entre ter acesso e saber de onde ele vem.
  • Se você monta SOQL dinâmico sobre a tabela de share certa a partir do tipo do registro.
  • Se você pensa no contrato do método diante de registro inexistente, dono sem share e acesso indireto, não só no caminho feliz.
Travei. Me dá uma direção (sem entregar a resposta)

Quatro perguntas que destravam o desafio:

  • Existe um objeto que responde diretamente se um usuário tem acesso a um registro, sem você calcular OWD e hierarquia na mão? Pesquise por "Salesforce UserRecordAccess SOQL" e veja quais campos ele expõe.
  • O nome da tabela de compartilhamento depende do objeto: AccountShare para padrão, MeuObjeto__Share para custom, com nomes de campo diferentes. Como descobrir o objeto a partir do Id em tempo de execução? Procure "Apex Id getSObjectType getDescribe getName".
  • Cada linha de share carrega o motivo do compartilhamento num campo. Pesquise por "Salesforce RowCause share object values" e veja os valores possíveis (Owner, Manual, Rule, Team).
  • Se o usuário tem acesso mas nenhuma linha de share aponta para ele, de onde vem o acesso? Pense no que a hierarquia de papéis e o OWD concedem sem gerar linha para o usuário. O motivo, nesse caso, é dedução por eliminação.

Sobre montar a consulta na tabela certa: procure "Apex Database.query dynamic SOQL" e lembre que nem todo objeto tem tabela de share (OWD público, por exemplo), então a consulta precisa estar protegida.

Como testar

Abra o Execute Anonymous na sua org de desenvolvimento e rode o código abaixo. Ele usa o próprio usuário logado como cobaia, criando um registro que ele passa a possuir:

// O usuario logado vira a cobaia: cria uma conta que ele mesmo passa a possuir.
Account a = new Account(Name = 'Diagnostico Acesso ' + DateTime.now().getTime());
insert a;

Id eu = UserInfo.getUserId();

// 1. O dono ve o proprio registro, e o motivo tem que dizer isso.
AcessoDiagnostico.Diagnostico dono = AcessoDiagnostico.diagnosticar(eu, a.Id);
System.assertEquals(true, dono.temAcesso, 'O dono deve ver o proprio registro');
System.assert(!dono.motivos.isEmpty(), 'Com acesso, sempre ha pelo menos um motivo');
System.debug('Nivel maximo: ' + dono.nivelMaximo);
System.debug('Motivos (dono): ' + dono.motivos);

// 2. Registro inexistente nao lanca excecao e volta sem acesso, com motivo.
Id fantasma = '001000000000000AAA';
AcessoDiagnostico.Diagnostico ausente = AcessoDiagnostico.diagnosticar(eu, fantasma);
System.assertEquals(false, ausente.temAcesso, 'Registro inexistente nao concede acesso');
System.assert(ausente != null && !ausente.motivos.isEmpty(), 'Sempre volta diagnostico com motivo');

// 3. Entrada nula nao consulta e nao lanca.
AcessoDiagnostico.Diagnostico nulo = AcessoDiagnostico.diagnosticar(null, a.Id);
System.assertEquals(false, nulo.temAcesso, 'Entrada nula nao concede acesso');

System.debug('Consultas usadas: ' + Limits.getQueries() + ' de ' + Limits.getLimitQueries());

Se você roda como administrador com "View All", vai reparar que o motivo do primeiro teste é "dono", mesmo que a permissão administrativa também te desse acesso. É de propósito: o diagnóstico reporta a origem concreta que se aplica àquele usuário e registro, e o número de consultas fica fixo, sem crescer com nada.

Gabarito comentado

A solucao completa, destrinchada parte por parte: o porque de cada decisao, os erros mais comuns e como levar o diagnostico para o nivel de producao. Tente resolver antes de abrir.

Ver o gabarito

Resolveu? O ganho aqui não é a classe: é entender que o Salesforce já sabe a resposta sobre acesso, e que o seu trabalho é perguntar no lugar certo em vez de reconstruir a lógica de sharing no braço. Esse reflexo economiza horas em cada chamado de visibilidade.