Skip to main content

Claim

Quando usar: sempre que o handler precisa saber quem está chamando. O gateway verifica o token do usuário e cunha o contexto; o serviço confere de novo e entrega o resultado no ServiceContext. Nada disso vem do corpo, da query ou de um cabeçalho solto.

O que chega no handler​

ServiceContext é um record com os claims já interpretados:

CampoO que é
principalQuem age: id (user:<realm>:<sub>, svcacct:<realm>:<client_id>, service:<iss>), type, realm, subject, name.
tenantO tenant efetivo (id, uuid, slug) — o de origem, ou o atuante em admin_context.
adminStaff da operação atuando num tenant de cliente: acting e o tenant de origem.
platformO principal é de plataforma (issuer do realm de plataforma).
rolesOs papéis do produto, já expandidos. Informativo.
permissionsAs permissões qualificadas do produto, já expandidas. É o que autoriza.
routeA rota que o gateway casou (o serviço confere contra o próprio roteador).
authMethodconsole_session, bearer…
expires, correlationId, requestId, actionIdPrazo e os ids de correlação.

O tenant vem daqui — sc.requireTenantId() — e é ele que escopa toda consulta:

@NnsRoute("items.get")
Views.ItemView get(Ctx ctx, ServiceContext sc, Match m) {
String tenant = sc.requireTenantId(); // sem tenant no contexto: 403, não uma consulta ampla
return Views.ItemView.of(ItemStore.get(ctx, service.db, tenant, m.params().get("item_id"), false));
}

Expondo o contexto (útil para depurar e para o cliente saber o que pode)​

nnp/api.yaml:

routes:
- {id: whoami, method: GET, path: /whoami, permission: sample.context.read}

java/src/main/java/.../app/ItemsController.java:

@NnsRoute("whoami")
Views.WhoamiView whoami(ServiceContext sc, Introspection who) {
return Views.WhoamiView.of(who, sc, service.module.iam(), service.roleText(), SampleModule.MARKER, ScanEngine.ENGINE);
}

java/src/main/java/.../app/Views.java:

static WhoamiView of(
Introspection who, ServiceContext sc, Iam iam, String role, String marker, String engine) {
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));
}
return new WhoamiView(
who.serviceId(),
who.releaseId(),
who.version(),
who.routeId(),
sc.tenantId(),
sc.principal().id(),
sc.principal().type(),
sc.platform(),
sc.admin() != null && sc.admin().acting(),
sc.authMethod(),
sc.requestId(),
sc.correlationId(),
role,
marker,
engine,
sc.roles(),
verified);
}

Três decisões que valem a pena copiar:

  • A vista é explícita. Só os campos que o contrato publica saem na resposta; o resto do contexto fica no serviço.
  • verified é o que a chamada TEM, permissão por permissão — é o que permite ao front esconder o que não pode ser feito (e é só isso: esconder).
  • O principal.id pode ser longo e opaco (user:<realm>:<uuid>). Use-o como identidade, não como nome de exibição.

Como testar​

O teste do contexto é o teste HTTP com token de verdade:

@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("tenant_id").asText()).isEqualTo("4");
assertThat(b.get("principal_id").asText()).startsWith("user:nnumbers:");
assertThat(b.get("auth_method").asText()).isEqualTo("console_session");
assertThat(b.get("verified").get("sample.context.read").asBoolean()).isTrue();
assertThat(b.get("verified").get("sample.item.scan").asBoolean()).isFalse();
}

O tenant do teste é o do token: ctx(...) monta o contexto com new ServiceContext.Tenant("4", "", "cliente") e o token é assinado com a chave do gateway. Trocar o tenant no corpo da requisição não muda nada — e é exatamente isso que o teste prova.

Erros comuns​

  • Ler tenant_id do corpo, da query ou de um cabeçalho (X-Tenant-Id, X-User) — o gateway não repassa cabeçalho de identidade, e o serviço não deve acreditar em nenhum. O tenant é sempre sc.requireTenantId().
  • Trocar requireTenantId() por sc.tenant().id() — a segunda forma devolve null quando não há tenant e o erro aparece longe, como NullPointerException em vez de 403.
  • Confiar em sc.roles() — papel é agrupamento; quem autoriza é a permissão (veja Permissão).
  • Achar que platform é "admin" — é o issuer do realm de plataforma. Um principal de plataforma continua precisando da permissão da rota.
  • Ignorar admin_context — quando o staff da operação atua num tenant de cliente, o efetivo é o tenant do cliente e admin.acting() é true: audite os dois (o evento de audit já leva admin_context).
  • Montar o contexto à mão no handler — quem monta é o runtime, a partir do token verificado.