Início › Desafios › Desafio 07
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.
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
Usere o Id de um registro de qualquer objeto. - Devolver um
Diagnosticodizendo 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 = truee 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 = falsecom um motivo, nuncanulle 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.
userIdourecordIdnulo devolve umDiagnosticosem 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:
AccountSharepara padrão,MeuObjeto__Sharepara 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.
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.