Documentação
GuiasAutorizaçãoReceitas de autorização

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.

Próximos

Nesta página