Como o workspace regista sozinho um cliente OAuth por microserviço e um callback de login por frontend, sem listas para manter.
O workspace regista sozinho tudo o que os seus serviços precisam no Access Management: um cliente OAuth por microserviço e um callback de login por frontend. Não há passo manual, e um projeto criado hoje pelo IGRP Studio é apanhado no próximo up sem se editar nenhuma lista.
Porque tem de ser automático
Na v2 não existe token M2M estático: cada serviço autentica-se com o seu par client_credentials e identifica-se no header X-Machine-Service-ID. Sem cliente registado, um serviço arranca, autentica-se com o que tem e falha a publicação dos seus recursos — normalmente com 404 IGRP_AUTH_RESOURCE_NOT_FOUND_BY_NAME, longe da causa real.
E há um detalhe que decide a arquitectura toda:
O clientSecret só é devolvido no momento da criação
Nunca mais é possível lê-lo. Logo não se pode escrever o segredo no .env antes de o cliente existir: tem de ser o próprio registo a criá-lo e a escrevê-lo. É isso que fixa a ordem de arranque do workspace.
Os dois mecanismos
| Registo de clientes | Registo de redirects | |
|---|---|---|
| O que cria | cliente OAuth + service account por serviço | os callbacks de login no cliente seeded |
| Quem precisa | backends e frontends | só frontends |
| Tarefa | -oauth-clients | -oauth-client-uri-sync |
| Script | register-oauth-clients.sh | sync-default-client-uris.sh |
| Resultado | segredos escritos nos env-files | redirectUris do cliente seeded actualizados |
São separados porque respondem a perguntas diferentes. O cliente é como o serviço fala com o Access Management (máquina a máquina); o callback é como o utilizador faz login na app (browser) — e todas as UIs partilham o mesmo cliente OIDC, o seeded, pelo que cada app só precisa que o seu caminho esteja na lista dele.
Onde isto vive no compose
Só nos ficheiros da raiz. Os composes do Process e dos projetos não registam nada, apenas consomem o resultado.
access-management <- arranca
|- access-management-ready portão
|- ...-oauth-clients clientes
|- ...-oauth-client-uri-sync redirects
|- application-center e o resto da stackAs três últimas dependem do portão com condition: service_completed_successfully. O portão é o wait-for-catalog.sh, que espera pela saúde do serviço e contorna o defeito de primeiro arranque.
Parte 1 — Registo dos clientes
O register-oauth-clients.sh corre uma vez e morre. Para cada serviço:
1. Espera pela API através do gateway. Pede token e faz GET /api/clients até 200, 30 tentativas de 10s. Em cada falha faz POST no GATEWAY_REFRESH_URL — depois do restart do portão o Access Management tem um IP novo e a rota antiga pode ficar em cache durante minutos, fazendo tudo em /gateway-api devolver 404.
2. Decide o que fazer, comparando o Access Management com o estado local:
| Cliente existe? | Segredo local | Acção |
|---|---|---|
| sim | real | ignora |
| sim | CHANGE_ME_* | apaga e re-cria |
| não | qualquer | cria |
O caso do meio existe porque o segredo é irrecuperável: se o local ainda é placeholder, ninguém o está a usar e re-criar é seguro.
3. Apaga na ordem certa — primeiro o service account, só depois o cliente. O DELETE /api/clients/{uuid} não faz cascade, e a violação de chave estrangeira sai como 500, não 409.
4. Cria o cliente — POST /api/clients com clientId, clientName, description, active, requirePkce: false, os três TTLs, scopes: ["openid"] e grantTypes: ["client_credentials"]. Sem redirectUris: os backends não precisam e os frontends fazem login pelo cliente seeded.
5. Cria o service account — POST /api/service-accounts com oauthClientId igual ao UUID devolvido, nunca a string do clientId. Sem isto o token autentica mas não consegue usar a API.
6. Escreve o segredo em cada env-file indicado. Acabou de criar o cliente, logo o valor em mão é o único válido e ganha sempre.
7. Sai com 1 se algo falhou. Sair 0 com "0 criados" deixaria o instalador arrancar os projetos com placeholders.
A escrita preserva o inode
Os env-files são bind-mounted ficheiro a ficheiro, não a pasta. Um sed -i ou um mv substituiria o ficheiro e desligaria o mount — a alteração nunca chegaria ao host. Por isso a escrita é awk > tmp seguido de cat tmp > ficheiro.
Lista fixa mais descoberta
O que torna isto utilizável num workspace real: os projetos não estão listados em lado nenhum.
| Origem | O que cobre | Porquê |
|---|---|---|
oauth-clients.spec | serviços de plataforma | os env-files deles vivem fora de projects/ |
| descoberta em runtime | tudo em projects/ | são criados dinamicamente pelo IGRP Studio |
A spec tem prioridade: uma linha lá escrita sobrepõe-se à descoberta, pelo que fixar um projeto à mão continua possível.
O sinal da descoberta é o compose do próprio projeto, que já declara as variáveis de que precisa:
- IGRP_ACCESS_M2M_CLIENT_ID=${DEMO_NOVO_M2M_CLIENT_ID} # backend
- IGRP_M2M_CLIENT_ID=${THEQUE_M2M_CLIENT_ID} # frontendDessa única linha sai tudo:
| Derivado | De onde |
|---|---|
| variável do segredo | ..._CLIENT_ID → ..._CLIENT_SECRET |
| tipo | IGRP_ACCESS_M2M_* é backend, IGRP_M2M_* é frontend |
clientId | valor de ..._M2M_CLIENT_ID no .env; se ainda não existir, o nome do directório, escrito de volta nos dois env-files da raiz |
O último ponto fecha o ciclo para um projeto acabado de criar, e o nome do directório mantém, por construção, a igualdade obrigatória clientId == spring.application.name.
Um projeto sem variáveis M2M é ignorado — não fala com o Access Management, não precisa de cliente.
A regra anti-cópia
Um directório só é considerado projeto se tiver igrp-compose-<nome-do-directório>.yaml. Um directório cujo compose nomeia outro projeto é uma cópia, e registá-la reclamaria o cliente de outro:
[OAuth Clients] . gestao-utente has no igrp-compose-gestao-utente.yaml — skippingComo os outros stacks recebem o resultado
Não há comunicação entre composes: o canal é o ficheiro .env.
Backends Spring — o SPRING_APPLICATION_NAME tem de ser o clientId. Frontends Next.js — o IGRP_SERVICE_ID vai no header X-Machine-Service-ID.
Dois serviços ficam fora deste mecanismo, de propósito:
- o access-management é o próprio servidor: semeia o seu cliente no arranque a partir de
IGRP_OAUTH_DEFAULT_CLIENT_ID, e é com ele que as duas tarefas se autenticam; - o app-center reutiliza esse mesmo cliente seeded.
Parte 2 — Registo dos redirects
O problema
Todas as UIs fazem login pelo mesmo cliente OIDC, o seeded. O Spring Authorization Server só aceita um redirect_uri registado nesse cliente; se o callback de uma app não estiver lá, o login falha antes de o utilizador ver seja o que for. A imagem do Access Management semeia um callback numa base de dados nova — todos os outros têm de ser acrescentados.
O que a tarefa faz
O sync-default-client-uris.sh:
- obtém token com o cliente seeded e encontra o UUID dele;
- lê os
redirectUrisepostLogoutRedirectUrisactuais; - faz merge, não substitui — acrescenta o que falta e preserva o resto;
- se nada mudou, sai 0 sem escrever;
- caso contrário faz
PUT /api/clients/{uuid}, depois de remover os campos que o modelo de leitura tem e o endpoint de actualização rejeita (id,clientSecret,applicationCode,createdAt,updatedAt).
Lista fixa mais descoberta, outra vez
O caminho base vem do descritor que o IGRP Studio escreve em cada projeto — projects/theque/.igrp.theque.env → NEXT_PUBLIC_BASE_PATH="/apps/theque". Daí saem os dois URIs, na forma que o NextAuth constrói:
| URI | Construção |
|---|---|
| callback | ${BASE_URL}<basePath>/api/auth/callback/${AUTH_PROVIDER} |
| pós-logout | ${BASE_URL}<basePath>/login |
A ausência de NEXT_PUBLIC_BASE_PATH identifica um backend
Não tem callback de browser. Há um fallback: sem o descritor, o caminho é extraído do NEXTAUTH_URL do compose do projeto. Aplica-se a mesma regra anti-cópia.
Discovering project frontends under /workspace/projects
. bibliotheque declares no base path — treating it as a backend (no callback)
. gestao-utente has no igrp-compose-gestao-utente.yaml — skipping
+ theque -> /apps/theque
+ utente -> /apps/utente
Default OAuth client callback URIs reconciled.Limite conhecido: o merge é aditivo
A tarefa nunca remove. Um projeto apagado deixa o seu callback no cliente seeded indefinidamente. Não é grave — aponta para um caminho que já não responde — mas suja a configuração, e a limpeza é manual.
Acrescentar um projeto novo
Não há nada a configurar; basta seguir a convenção:
| Ficheiro | Necessário para |
|---|---|
projects/<nome>/igrp-compose-<nome>.yaml | ser reconhecido como projeto |
…com IGRP_ACCESS_M2M_CLIENT_ID (backend) ou IGRP_M2M_CLIENT_ID (frontend) | ter cliente registado |
projects/<nome>/.igrp.<nome>.env com NEXT_PUBLIC_BASE_PATH | ter callback registado (só frontends) |
Depois:
docker compose --env-file .env -f igrp-compose.yaml up -d
docker wait demoteste-access-management-oauth-clientsA armadilha da ordem de arranque
O docker wait não é decorativo
docker compose up -d arranca tarefas one-shot mas só espera por elas quando outro serviço declara service_completed_successfully. Nada nos composes separados o declara — não pode, são ficheiros diferentes.
Arrancar um projeto demasiado cedo faz com que leia o .env antes da escrita e apanhe um CHANGE_ME_*. O sintoma aparece longe da causa: um backend que autentica e falha o sync com 404. É por isso que se espera pela tarefa de registo (docker wait <slug>-access-management-oauth-clients) antes de arrancar os projetos — ver Stack principal.
Nomes: três coisas diferentes
Confusão frequente, e a causa de 404s difíceis de ler:
| Nome | É | Definido por |
|---|---|---|
clientId | o cliente OAuth e o nome do resource no Access Management | a spec ou a descoberta |
spring.application.name | o nome por que o AuthorizationSyncRunner procura o resource | SPRING_APPLICATION_NAME, igual ao clientId |
| nome no Eureka | o segmento da rota /gateway-api/<x> | eureka.instance.appname + os dois virtual-host names |
O nome no Eureka apenas por omissão coincide com o spring.application.name. É por isso que o workspace consegue ter a rota demoteste-process-management-api com o cliente igrp-process-management-api.
Verificação
docker logs demoteste-access-management-oauth-clients
docker logs demoteste-access-management-oauth-client-uri-syncOs clientes registados, pela API:
CID=$(sed -n 's/^ACCESS_MANAGEMENT_OAUTH_CLIENT_ID=//p' .env | head -1 | tr -d '\r')
CSEC=$(sed -n 's/^ACCESS_MANAGEMENT_OAUTH_CLIENT_SECRET=//p' .env | head -1 | tr -d '\r')
AM=http://localhost:2575/gateway-api/demoteste-access-management
TOK=$(curl -s -X POST "$AM/oauth2/token" -H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode grant_type=client_credentials \
--data-urlencode "client_id=$CID" --data-urlencode "client_secret=$CSEC" \
| sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')
curl -s -H "Authorization: Bearer $TOK" "$AM/api/clients" \
| sed 's/},{/}\n{/g' | sed -n 's/.*"clientId":"\([^"]*\)".*/\1/p' | sortA prova final é o que cada serviço publicou. O GET /api/applications responde 403 ao service account seeded, pelo que a via fiável é a base de dados:
docker exec demoteste-database-postgres psql -U igrp -d igrp_db -tAc \
"SELECT name FROM t_resource ORDER BY name;"Com os quatro projetos e a stack Process a correr devolve 9: os 8 serviços mais o igrp-access-management.
Equivalência com o Helm chart
No igrp-stack cada serviço traz o seu Job oauth-register com hook post-install,post-upgrade. Aqui um único one-shot faz o mesmo para todos.
| Passo | Job do chart | Tarefa do compose |
|---|---|---|
| já registado? | sai se o Secret alvo existir | sai se o cliente existir e o segredo local for real |
| espera pelo AM | GET /.well-known/openid-configuration | POST /oauth2/token + GET /api/clients até 200 |
| rota do gateway em falta | POST /actuator/gateway/refresh a cada tentativa | idem, via GATEWAY_REFRESH_URL |
| cliente sem segredo | falha, salvo autoRegister.forceRecreate=true | apaga e re-cria quando o valor local é placeholder |
| apagar o cliente | DELETE /api/clients/{uuid} | apaga primeiro o service account |
| payload | clientId, clientName, description, active, requirePkce, TTLs, scopes, grantTypes | o mesmo |
| service account | POST /api/service-accounts, oauthClientId = UUID | o mesmo |
| onde guarda o segredo | kubectl create secret | env-files, preservando o inode |
| lista de serviços | um Job por chart, no values.yaml | spec para a plataforma + descoberta em projects/ |
A diferença de fundo é o destino do segredo. A última linha é a divergência deliberada: no chart cada serviço é uma release Helm conhecida à instalação, enquanto num workspace os projetos aparecem a qualquer momento.
Última atualização:
