Início › Desafios › Desafio 10
Leia e atualize contatos sem atravessar as permissões
Rodando como administrador, a classe passa em todos os testes. Reescreva os dois métodos Apex para que um atendente com perfil restrito não leia nem altere, por essa tela, um contato ou campo que a página de registro padrão esconderia dele.
O cenário
A Vetra Atendimento, empresa fictícia, tem um LWC que lista os contatos de uma conta e deixa o atendente corrigir telefone e e-mail. O componente chama dois métodos Apex: um para listar e outro para salvar a lista de alterações. A versão original consulta e grava tudo o que recebe, e o único teste rodava como administrador.
Com um usuário restrito, isso abre dois buracos. A listagem devolve contatos de contas que ele não enxerga. O método de salvar grava qualquer campo que vier no payload, inclusive os que o perfil dele não pode editar. Esse payload sai do navegador e pode trazer qualquer Id ou campo que alguém acrescente na requisição. Trate a lista como entrada não confiável.
O que você deve construir
Implemente a classe ContatoSeguroService com a assinatura abaixo. O LWC já consome esses métodos, então o nome da classe, os nomes dos métodos, os parâmetros e os tipos de retorno não podem mudar.
public with sharing class ContatoSeguroService {
public static List<Contact> listarPorConta(Id accountId)
public static Integer atualizarContatos(List<Contact> entradas)
}
O contrato de cada método:
listarPorConta(accountId)retorna os contatos comAccountIdigual ao recebido que o usuário corrente pode ver, com exatamente estes campos:Id,FirstName,LastName,Email,PhoneeAccountId. Não acrescente outros campos à consulta.- Para
listarPorConta, "vazio" significa umaList<Contact>sem elementos, nuncanull. Retorne vazio quandoaccountIdfor nulo, quando o usuário não puder ler o objeto Contact, quando a conta não estiver compartilhada com ele e quando a consulta for barrada por falta de permissão de leitura em algum dos seis campos. atualizarContatos(entradas)recebe contatos comIde os campos alterados, grava somente os registros que o usuário enxerga e somente os campos que ele pode editar, e retorna quantos registros foram gravados com sucesso.- Para
atualizarContatos, "vazio" significa retornar0. Isso vale para lista nula, lista vazia, lista sem nenhumIdpreenchido, lista em que nenhumIdé visível ao usuário e usuário sem permissão de edição em Contact. Elementos nulos dentro da lista são ignorados.
A verificação é feita pela classe de teste WL_Desafio10Verifier, publicada no fim desta página. Ela roda como um usuário restrito, e cada asserção indica qual das cinco regras abaixo está conferindo.
Regras de aceite
- Retorne em
listarPorContasomente contatos visíveis ao usuário e somente os seis campos do contrato. Uma conta não compartilhada devolve lista vazia. Um campo fora do contrato e sem permissão de leitura, comoTitle, não pode voltar preenchido. - Remova do payload, antes do DML, todo campo que o usuário não pode editar. Os campos permitidos do mesmo contato continuam sendo gravados.
- Descarte da atualização todo Id de contato que o usuário não enxerga. Esse contato precisa permanecer intacto depois da chamada.
- Grave com processamento parcial e retorne apenas a quantidade de sucessos. Um contato que falha numa validação não pode reverter os demais nem entrar na contagem.
- Borda: retorne
0para lista nula ou vazia e lista vazia paraaccountIdnulo. Garanta o mesmo resultado para usuário sem acesso ao objeto Contact, sem lançar exceção em nenhum desses casos.
O que este desafio avalia
- Se você separa acesso a registro de acesso a objeto e campo.
- Se você entende quando usar operações em user mode e quando sanitizar registros.
- Se o payload é tratado como entrada não confiável.
- Se a estratégia de erro preserva sucessos válidos sem esconder o resultado.
Travei. Me dá uma direção sem entregar a resposta
Responda primeiro a estas perguntas. Uma classe with sharing já confere FLS numa consulta? Se o payload traz um campo proibido junto com um permitido, você quer rejeitar o contato inteiro ou gravar só a parte permitida? E como uma falha de validação num contato fica isolada dos outros?
Termos de busca: "Apex user mode vs system mode", "Security.stripInaccessible", "Database DML allOrNone SaveResult" e "Apex permission set tests System.runAs".
Uma pista de desenho: saber quais registros o usuário enxerga e saber quais campos ele edita são perguntas diferentes. Responda cada uma numa etapa própria.
Como testar
Teste sempre com um usuário que tenha menos acesso que você. Como administrador, o código original passa em todas as regras, então um teste rodado só com o seu usuário não prova nada sobre permissão.
A preparação mínima é esta: dentro de System.runAs, use um usuário de perfil sem acesso a Contact que receba, por um permission set criado no próprio teste, leitura e edição no objeto Contact, edição em Phone e apenas leitura em Email. Crie duas contas, compartilhe só uma delas com esse usuário por AccountShare e deixe a outra fora. Com isso montado, cubra estes cenários no seu teste:
- Listar a conta compartilhada devolve os contatos dela, sem nenhum campo fora do contrato.
- Listar a conta não compartilhada devolve lista vazia.
- Enviar
PhoneeEmailnovos para o mesmo contato grava só oPhone. - Enviar o Id de um contato da conta não compartilhada deixa esse contato como estava.
- Um contato que quebra uma validação, como
LastNamevazio, não desfaz os outros, e a contagem considera só os gravados. - Lista nula e lista vazia devolvem
0;accountIdnulo devolve lista vazia.
Você não precisa escrever essa preparação do zero. O WL_Desafio10Verifier da seção seguinte é autocontido: cria o usuário, o permission set e o compartilhamento dentro do próprio teste, cobre os seis cenários acima e não depende de nenhuma classe auxiliar. Use-o como sua classe de teste e acrescente casos se quiser, por exemplo um usuário sem nenhum acesso a Contact.
Validação opcional na sua org
Deixe um teste verificável, sem entregar seu código ao site
Depois de criar a ContatoSeguroService, adicione a classe de teste abaixo com o nome exato
WL_Desafio10Verifier e execute esse teste na Developer Org ou no Trailhead Playground.
O conector procura somente o nome da classe e o resultado da execução mais recente pela Tooling API.
O corpo da sua solução não é consultado nem armazenado.
Este teste é autocontido e não depende de nenhuma classe auxiliar. Ele cria um usuário temporário, um permission set com
acesso mínimo e o compartilhamento de uma única conta, e tudo é desfeito ao fim da execução. Antes de rodar,
confira duas preparações da org: o padrão de compartilhamento de Conta precisa ser Privado, com Contato
controlado pelo pai, e a org precisa ter um perfil Salesforce sem acesso a Contact, como o perfil de acesso
mínimo. Se uma delas faltar, o teste falha com uma mensagem que começa por Preparacao, e o
problema está na org, não na sua solução.
@IsTest
private class WL_Desafio10Verifier {
@IsTest
static void validarDesafio() {
Organization org = [SELECT DefaultAccountAccess, DefaultContactAccess FROM Organization LIMIT 1];
Boolean contatoPrivado = org.DefaultContactAccess == 'ControlledByParent'
&& org.DefaultAccountAccess == 'None';
System.assert(contatoPrivado,
'Preparacao: deixe o OWD de Conta como Privado e Contato como Controlado pelo pai');
User restrito = criarUsuarioRestrito();
Account contaVisivel = new Account(Name = 'Conta compartilhada');
Account contaOculta = new Account(Name = 'Conta nao compartilhada');
insert new List<Account>{ contaVisivel, contaOculta };
Contact visivel = new Contact(LastName = 'Visivel', AccountId = contaVisivel.Id,
Email = 'original@exemplo.com', Title = 'Diretora');
Contact invalido = new Contact(LastName = 'Vai falhar', AccountId = contaVisivel.Id);
Contact oculto = new Contact(LastName = 'Oculto', AccountId = contaOculta.Id,
Phone = '1100000000');
insert new List<Contact>{ visivel, invalido, oculto };
// compartilha somente a primeira conta; com Contato controlado pelo pai,
// o contato herda o nivel da conta e ContactAccessLevel nao e gravavel
insert new AccountShare(AccountId = contaVisivel.Id, UserOrGroupId = restrito.Id,
AccountAccessLevel = 'Edit',
OpportunityAccessLevel = 'None', CaseAccessLevel = 'None');
List<Contact> listados;
List<Contact> deOutraConta;
List<Contact> contaNula;
Integer atualizados;
Integer paraNulo;
Integer paraVazio;
Test.startTest();
System.runAs(restrito) {
listados = ContatoSeguroService.listarPorConta(contaVisivel.Id);
deOutraConta = ContatoSeguroService.listarPorConta(contaOculta.Id);
atualizados = ContatoSeguroService.atualizarContatos(new List<Contact>{
new Contact(Id = visivel.Id, Phone = '5511999990000', Email = 'nao-pode@exemplo.com'),
new Contact(Id = invalido.Id, LastName = null),
new Contact(Id = oculto.Id, Phone = '0')
});
paraNulo = ContatoSeguroService.atualizarContatos(null);
paraVazio = ContatoSeguroService.atualizarContatos(new List<Contact>());
contaNula = ContatoSeguroService.listarPorConta(null);
}
Test.stopTest();
Contact visivelDepois = [SELECT Phone, Email FROM Contact WHERE Id = :visivel.Id];
Contact ocultoDepois = [SELECT Phone FROM Contact WHERE Id = :oculto.Id];
Boolean campoVazou = false;
for (Contact contato : listados) {
if (contato.isSet('Title')) {
campoVazou = true;
}
}
System.assertEquals(2, listados.size(), 'Regra 1: le somente os contatos compartilhados');
System.assert(!campoVazou, 'Regra 1: campo sem permissao de leitura nao pode voltar');
System.assertEquals(0, deOutraConta.size(), 'Regra 1: conta nao compartilhada devolve vazio');
System.assertEquals('5511999990000', visivelDepois.Phone, 'Regra 2: campo permitido e gravado');
System.assertEquals('original@exemplo.com', visivelDepois.Email,
'Regra 2: campo sem permissao de edicao nao chega ao DML');
System.assertEquals('1100000000', ocultoDepois.Phone, 'Regra 3: contato invisivel continua intacto');
System.assertEquals(1, atualizados, 'Regra 4: conta somente a atualizacao bem-sucedida');
System.assertEquals(0, paraNulo, 'Regra 5: lista nula devolve zero');
System.assertEquals(0, paraVazio, 'Regra 5: lista vazia devolve zero');
System.assertEquals(0, contaNula.size(), 'Regra 5: conta nula devolve lista vazia');
}
private static User criarUsuarioRestrito() {
// perfil sem acesso a Contact; o acesso vem so do permission set criado aqui
Set<Id> perfisComAcesso = new Set<Id>();
for (ObjectPermissions permissao : [
SELECT Parent.ProfileId FROM ObjectPermissions
WHERE Parent.IsOwnedByProfile = true
AND ((SobjectType = 'Contact' AND PermissionsRead = true)
OR (SobjectType = 'Account' AND PermissionsViewAllRecords = true))
]) {
perfisComAcesso.add(permissao.Parent.ProfileId);
}
for (FieldPermissions permissao : [
SELECT Parent.ProfileId FROM FieldPermissions
WHERE Parent.IsOwnedByProfile = true
AND ((Field = 'Contact.Title' AND PermissionsRead = true)
OR (Field = 'Contact.Email' AND PermissionsEdit = true))
]) {
perfisComAcesso.add(permissao.Parent.ProfileId);
}
List<Profile> perfis = [
SELECT Id FROM Profile
WHERE UserType = 'Standard' AND UserLicense.Name = 'Salesforce'
AND PermissionsViewAllData = false AND PermissionsModifyAllData = false
AND Id NOT IN :perfisComAcesso
LIMIT 1
];
System.assert(!perfis.isEmpty(),
'Preparacao: a org precisa de um perfil Salesforce sem acesso a Contact, como o de acesso minimo');
String sufixo = String.valueOf(Crypto.getRandomInteger()).replace('-', '');
User restrito = new User(
LastName = 'Restrito Desafio 10',
Alias = 'wld10',
Email = 'restrito.d10@exemplo.com',
Username = 'restrito.d10.' + sufixo + '@exemplo.com',
ProfileId = perfis[0].Id,
EmailEncodingKey = 'UTF-8',
LanguageLocaleKey = 'pt_BR',
LocaleSidKey = 'pt_BR',
TimeZoneSidKey = 'America/Sao_Paulo'
);
PermissionSet acesso = new PermissionSet(
Name = 'WL_Desafio10_Restrito',
Label = 'WL Desafio 10 Restrito'
);
System.runAs(new User(Id = UserInfo.getUserId())) {
insert restrito;
insert acesso;
insert new List<ObjectPermissions>{
new ObjectPermissions(ParentId = acesso.Id, SobjectType = 'Account',
PermissionsRead = true, PermissionsEdit = true),
new ObjectPermissions(ParentId = acesso.Id, SobjectType = 'Contact',
PermissionsRead = true, PermissionsEdit = true)
};
insert new List<FieldPermissions>{
campo(acesso.Id, 'Contact.Phone', true),
campo(acesso.Id, 'Contact.Email', false),
campo(acesso.Id, 'Contact.AccountId', false)
};
insert new PermissionSetAssignment(PermissionSetId = acesso.Id, AssigneeId = restrito.Id);
}
return restrito;
}
private static FieldPermissions campo(Id permissionSetId, String nomeCampo, Boolean edita) {
return new FieldPermissions(ParentId = permissionSetId, SobjectType = 'Contact',
Field = nomeCampo, PermissionsRead = true, PermissionsEdit = edita);
}
}
- Crie a classe com o nome indicado e rode WL_Desafio10Verifier em Apex Test Execution.
- Volte para Meu progresso e conecte a mesma org.
- Use o botão Validar desafio 10. A consulta é somente leitura e não executa nada na org.
Ver o tutorial guiado para conectar e validar →
Conexão segura
O ponto que fica: with sharing decide quais contatos aparecem; não decide se o Email pode ser gravado. Cada garantia precisa estar escrita no código e coberta por um teste em System.runAs com um usuário que tenha menos acesso que você.