Receitas de autorização
Cenários concretos — SaaS multi-tenant, recursos aninhados, colaboradores externos, acesso de serviço.
Cada uma destas é uma forma que os times reconstroem sem parar. Todas são as mesmas quatro peças do modelo arranjadas de outro jeito.
SaaS multi-tenant
O formato padrão: usuários pertencem a organizações e têm um papel em cada uma.
Defina os papéis uma vez, por contexto.
const context = 'org';
await nulvless.auth.admin.roles.create({
name: 'Owner', key: 'owner', context,
scopes: ['org:delete', 'billing:write', 'members:write', 'projects:write'],
});
await nulvless.auth.admin.roles.create({
name: 'Admin', key: 'admin', context,
scopes: ['members:write', 'projects:write'],
});
await nulvless.auth.admin.roles.create({
name: 'Member', key: 'member', context,
scopes: ['projects:read'],
});Lembre que o verify casa papéis pela key, então é 'owner' e 'admin' que
aparecem nas verificações — não 'Owner'.
Semeie o criador como dono, transacionalmente, para um recurso nunca existir sem administrador:
await db.transaction(async (tx) => {
const org = await tx.organizations.create({ slug, name });
await nulvless.auth.admin.grants.set({
resource: org.slug,
granted_to_user_id: creatorId,
role_id: ownerRoleId,
});
return org;
});Liste os inquilinos a partir dos grants, não de uma tabela de membros:
const { data: grants } = await nulvless.auth.grants.list('org');
const orgs = await db.organizations.findBySlugs(grants.map((g) => g.resource));Verifique a cada requisição, nomeando o recurso:
const { error } = await nulvless.auth.verify(
{ resource: req.params.org, scopes: ['projects:write'] },
{ headers: { Authorization: `Bearer ${token}` } },
);Recursos aninhados
Uma organização que contém projetos, onde algumas pessoas devem alcançar um projeto mas não a organização inteira.
Use dois contextos e verifique aquele sobre o qual a rota realmente é:
// Papéis em 'org' para acesso à organização inteira.
// Papéis em 'project' para um projeto só.
await nulvless.auth.admin.roles.create({
name: 'Project editor', key: 'editor', context: 'project',
scopes: ['project:write'],
});Um grant com escopo de projeto nomeia o projeto como recurso:
await nulvless.auth.admin.grants.set({
resource: project.id,
granted_to_user_id: userId,
role_id: projectEditorRoleId,
});Grants não cascateiam. Um owner em acme não tem nada em acme/website a
menos que exista um grant dizendo isso. Se donos da organização devem alcançar
todos os projetos, verifique os dois explicitamente — uma chamada para o projeto
e um fallback para a organização.
async function canEditProject(token: string, org: string, project: string) {
const direct = await nulvless.auth.verify(
{ resource: project, scopes: ['project:write'] },
{ headers: { Authorization: `Bearer ${token}` } },
);
if (!direct.error) return true;
const viaOrg = await nulvless.auth.verify(
{ resource: org, scopes: ['projects:write'] },
{ headers: { Authorization: `Bearer ${token}` } },
);
return !viaOrg.error;
}Tornar essa herança explícita é proposital: permissão que chega de dois níveis acima é exatamente o tipo que surpreende as pessoas durante uma auditoria.
Colaboradores externos
Um terceirizado que deve ver um projeto e mais nada não precisa de mecanismo especial — isso é só um grant com um papel restrito sobre um recurso.
await nulvless.auth.grants.invite(
{
invites: [{ email: '[email protected]', role_id: viewerRoleId }],
resource: project.id,
message: project.name,
},
{ headers: forwardUserHeaders(req) },
);Ele nunca aparece na lista de membros da organização, porque não tem grant sobre a organização. Quando o trabalho acaba:
await nulvless.auth.grants.revoke({ id: grantId, nonce });Uma pessoa, dois chapéus
A mesma pessoa com papéis diferentes em lugares diferentes não precisa de nada especial.
// dona da própria empresa
{ resource: 'grace-consulting', role: { key: 'owner' } }
// membro da org de um cliente
{ resource: 'acme', role: { key: 'member' } }Toda verificação nomeia um recurso, então a resposta muda por recurso sem um único condicional no seu código. É esse o retorno de recusar uma flag global de admin.
Concedendo uma capacidade extra
Alguém precisa de uma coisa que o papel dele não carrega. Duas opções, e a segunda geralmente é a errada:
Some um escopo ao grant — uma exceção por pessoa:
await nulvless.auth.grants.update(grantId, {
scopes: ['members:read', 'billing:read'],
});Ou crie um papel para isso — reutilizável e auditável:
await nulvless.auth.admin.roles.create({
name: 'Finance', key: 'finance', context: 'org',
scopes: ['billing:read', 'billing:write'],
});Prefira o papel. Escopos por grant são invisíveis numa tela de papéis, então um ano depois ninguém consegue responder "quem vê o faturamento?" sem percorrer todos os grants.
Back office e ferramentas de suporte
A sua equipe interna não é membro das organizações dos seus clientes, e não deveria receber grants lá — isso a colocaria na lista de membros do cliente.
Use o auth.admin a partir do seu próprio serviço de back
office, que age como a plataforma e não como um usuário, e proteja isso com a sua
própria autenticação de equipe.
const { data } = await nulvless.auth.admin.grants.list({ resource: 'acme' });O auth.admin não guarda registro de qual pessoa da equipe pediu, porque age
como a plataforma. Registre isso do seu lado — a trilha de auditoria não tem como
recuperar essa informação para você.
Apagando um recurso
await db.transaction(async (tx) => {
await tx.organizations.delete(slug);
await nulvless.auth.admin.grants.deleteByResource(slug);
});Pular isso deixa grants apontando para o nada — inofensivo até um slug ser reaproveitado, quando os membros antigos reaparecem silenciosamente.