Pular para o conteúdo principal

Permissão

Quando usar: toda rota tem uma. A permissão é o que decide se uma chamada passa — o serviço não decide por papel, por claim de e-mail, nem por lista de administradores.

Uma permissão é local ao produto (id curto, no iam.yaml) e vale qualificada (<produto>.<local>) no resto do sistema: no api.yaml da rota, no token expandido pelo gateway e no código.

1. Declare a permissão no iam.yaml​

nnp/iam.yaml:

apiVersion: console.nnumbers.io/v1alpha3
kind: IamDescriptor
product: sample
permissions:
- {id: context.read, description: Ver o contexto verificado pelo serviço (whoami)}
- {id: item.read, description: Ver itens e o estado das varreduras}
- {id: item.create, description: Criar item}
- {id: item.delete, description: Apagar item}
- {id: item.scan, description: Iniciar a varredura assíncrona de um item}
roles:
viewer: {description: Leitura, permissions: [context.read, item.read]}
operator: {description: "Leitura, criação e varredura", permissions: [context.read, item.read, item.create, item.scan]}
admin: {description: Tudo do produto, permissions: ["*"]}

2. Amarre a rota à permissão​

nnp/api.yaml:

routes:
- {id: whoami, method: GET, path: /whoami, permission: sample.context.read}
- {id: items.get, method: GET, path: "/items/{item_id}", permission: sample.item.read}
- {id: items.delete, method: DELETE, path: "/items/{item_id}", permission: sample.item.delete, idempotency: optional, max_body_bytes: 4096}

3. Confira dentro do handler quando o motivo for outro​

A permissão da rota é conferida pelo runtime antes do handler (o serviço refaz a conferência do lado dele: o gateway expande, o serviço não confia). Dentro do handler você chama ServiceContext só quando a operação precisa de uma permissão a mais — um caminho de escrita dentro de uma rota de leitura, um campo administrativo:

@NnsRoute("whoami")
Views.WhoamiView whoami(ServiceContext sc, Introspection who) {
// A permissão da rota (`sample.context.read`) já passou. Aqui a conferência é EXPLÍCITA: o
// `whoami` também mostra o que a chamada NÃO tem, e isso não autoriza nada.
return Views.WhoamiView.of(who, sc, service.module.iam(), service.roleText(), SampleModule.MARKER, ScanEngine.ENGINE);
}
// A permissão QUALIFICADA, como o gateway a expande e o serviço a recebe.
if (sc.can("sample.item.delete")) {
// apaga de verdade
} else {
// soft delete
}

// Ou exigindo: sem ela, é 403 `permission_denied/permission_required`, com a permissão em details.
sc.require("sample.item.delete");

O whoami responde, para cada permissão do produto, o que esta chamada tem:

Map<String, Boolean> verified = new TreeMap<>();
for (Permission p : iam.permissions()) {
String q = p.id();
if (!Descriptor.productOf(q).equals(iam.product())) {
q = iam.product() + "." + q;
}
verified.put(q, sc.can(q));
}

Como testar​

O teste que importa é o negativo: com o token sem a permissão, a resposta é 403 — e não 500, não 200 e não "só esconde no front". Ele está em java/src/test/java/.../app/HttpAuthorizationTest.java:

@Test
void routeWithoutThePermissionIs403WithTheReason() {
// O token é válido e o usuário existe: o que falta é a PERMISSÃO da rota.
String token = ctxToken(ctx("whoami", "sample.item.read"), "GET", "/v1/sample/whoami", null);
HttpResponse<String> r = call("GET", "/v1/sample/whoami", token, null);
assertThat(r.statusCode()).isEqualTo(403);
JsonNode error = body(r).get("error");
assertThat(error.get("code").asText()).isEqualTo("permission_denied");
assertThat(error.get("reason").asText()).isEqualTo("permission_required");
assertThat(error.get("details").get("permission").asText()).isEqualTo("sample.context.read");
}

@Test
void whoamiShowsTheVerifiedContextOfTheToken() {
String token = ctxToken(ctx("whoami", "sample.context.read", "sample.item.read"), "GET", "/v1/sample/whoami", null);
HttpResponse<String> r = call("GET", "/v1/sample/whoami", token, null);
assertThat(r.statusCode()).isEqualTo(200);
JsonNode b = body(r);
assertThat(b.get("verified").get("sample.context.read").asBoolean()).isTrue();
assertThat(b.get("verified").get("sample.item.scan").asBoolean()).isFalse();
}

E, do lado do descritor, um teste que confere que as permissões declaradas são exatamente as esperadas (assim, trocar o id de uma permissão quebra o build em vez de quebrar a autorização em produção):

@Test
void rolesOnlyUsePermissionsOfTheProduct() {
Iam iam = MODULE.iam();
assertThat(iam.permissions()).extracting(p -> p.id())
.containsExactly("sample.context.read", "sample.item.read", "sample.item.create",
"sample.item.delete", "sample.item.scan");
assertThat(iam.roles().get("viewer").permissions())
.containsExactly("sample.context.read", "sample.item.read");
}

Erros comuns​

  • Usar o id LOCAL no código — sc.can("item.read") nunca é verdade: o contexto traz a forma qualificada (sample.item.read).
  • Inventar permissão fora do iam.yaml — o descriptors validate recusa os dois lados: a permissão da rota que não existe no descritor e o papel que agrupa permissão inexistente.
  • Decidir por papel — sc.roles() é informativo. Quem autoriza é a permissão.
  • Conferir depois de mexer — sc.require(...) é a primeira linha da operação, antes de qualquer escrita e antes de qualquer leitura de dado sensível.
  • Duplicar a conferência da rota — o runtime já fez; repetir a mesma checagem no handler só polui. Confira o que é a mais.
  • Achar que implies dá permissão no serviço — implies expande o escopo do papel no gateway; no serviço chega a lista já expandida.
  • Esquecer que admin com "*" vale só as permissões tenant do produto — poder de plataforma não se herda por wildcard.