Portal interno de autosserviço para módulos Terraform aprovados. As Fases 0–4 fornecem fundação operacional, identidade OIDC, sessão/RBAC, segurança web, catálogo versionado, criação determinística de Pull Request e pedidos auditáveis com projeção por webhooks assinados. A Fase 5 acrescenta configuração externa validada, deploy Kubernetes endurecido, métricas/alertas e governança operacional.
O portal não aprova pedidos, não executa terraform plan/apply, não faz merge automático e não reconcilia efeitos Git de forma destrutiva. Atlantis e GHE continuam fontes externas de verdade; o portal apenas acompanha eventos.
- Go 1.26 com o patch suportado mais recente.
- Terraform 1.15.9 para executar os testes reais de renderização fora do container.
- Docker com Compose para o ambiente completo.
- Helm 4 e kubectl 1.36 para validar o deploy Kubernetes/Kustomize.
makeem Linux/macOS ou execução direta dos comandos Go no Windows.
As ferramentas de desenvolvimento (sqlc, golangci-lint e govulncheck) estão fixadas no go.mod e são executadas com go tool; não precisam de instalação global.
docker compose up --buildO Compose inicia PostgreSQL, executa migrations uma vez e só então inicia o portal. Endpoints:
http://localhost:8080/login: início do login corporativo.http://localhost:8080/: dashboard autenticado.http://localhost:8080/catalog: itens autorizados do catálogo ativo.http://localhost:8080/catalog/{id}: formulário derivado do item aprovado.POST http://localhost:8080/catalog/{id}/validate: validação server-side e fragmento HTMX, com CSRF.POST http://localhost:8080/catalog/{id}/requests: gera os arquivos aprovados e cria branch, commit e PR, com CSRF e chave idempotente.GET http://localhost:8080/requests/{id}: detalhe e timeline autorizados do pedido.GET http://localhost:8080/requests/{id}/status: fragmento HTMX de status/timeline.POST http://localhost:8080/webhooks/git: recepção GitHub/GHE autenticada por HMAC SHA-256; não usa sessão nem CSRF.http://localhost:8080/health/live: processo HTTP ativo.http://localhost:8080/health/ready: PostgreSQL acessível.http://localhost:8080/metrics: métricas Prometheus.
Para encerrar, use docker compose down. O volume do PostgreSQL é preservado; removê-lo deve ser uma decisão explícita.
O provedor OIDC é descoberto apenas ao iniciar login. No Docker Desktop, o emissor local padrão é http://host.docker.internal:5556; configure as variáveis OIDC abaixo para usar o IdP real. O callback cadastrado no IdP deve ser exatamente http://localhost:8080/auth/callback no ambiente local e aceitar response_mode=form_post.
Com PostgreSQL disponível na URL padrão:
go run ./cmd/migrate
go run ./cmd/portalNo PowerShell, os comandos de validação equivalentes ao make ci são:
go tool sqlc generate
go run ./cmd/catalog-validate -file catalog/catalog.yaml -schema catalog/schema.json
go run ./cmd/rbac-validate -file config/rbac.example.yaml -catalog catalog/catalog.yaml
gofmt -l (Get-ChildItem -Recurse -Filter *.go | Select-Object -ExpandProperty FullName)
go vet ./...
go tool golangci-lint run
go test -count=1 ./...
go test -race -count=1 ./...
go build ./cmd/portal ./cmd/migrate ./cmd/catalog-validate ./cmd/rbac-validate
go tool govulncheck ./...
helm lint deploy/helm/open-platform
kubectl kustomize --enable-helm deploy/kustomize
docker compose config --quietO teste PostgreSQL real é habilitado por TEST_DATABASE_URL; sem essa variável ele é ignorado. A CI sempre o executa.
| Variável | Default local | Finalidade |
|---|---|---|
PORTAL_SERVICE_NAME |
open-platform |
Nome do serviço |
PORTAL_SERVICE_VERSION |
development |
Versão em logs e traces |
PORTAL_ENVIRONMENT |
development |
development, test ou production |
PORTAL_LOG_LEVEL |
info |
debug, info, warn ou error |
PORTAL_HTTP_ADDRESS |
:8080 |
Endereço HTTP |
PORTAL_HTTP_TIMEOUT |
15s |
Timeout por request |
PORTAL_SHUTDOWN_TIMEOUT |
10s |
Limite do shutdown gracioso |
PORTAL_DATABASE_URL |
URL PostgreSQL local | DSN; nunca é incluída nos logs |
PORTAL_DATABASE_MAX_CONNECTIONS |
10 |
Máximo do pool pgx |
PORTAL_DATABASE_MIN_CONNECTIONS |
2 |
Mínimo do pool pgx |
PORTAL_DATABASE_MAX_CONN_LIFETIME |
30m |
Vida máxima de conexão |
PORTAL_DATABASE_READY_TIMEOUT |
2s |
Timeout do readiness |
PORTAL_METRICS_ENABLED |
true |
Habilita coleta e /metrics |
PORTAL_OTEL_TRACE_ENDPOINT |
vazio | Endpoint OTLP/HTTP completo |
PORTAL_OTEL_TRACE_INSECURE |
false |
Permite OTLP sem TLS em ambiente controlado |
PORTAL_TRACE_SAMPLE_RATE |
0.1 |
Amostragem entre 0 e 1 |
PORTAL_OIDC_ISSUER_URL |
http://localhost:5556 |
Issuer usado na discovery e validação do ID token |
PORTAL_OIDC_CLIENT_ID |
open-platform |
Client ID do cliente confidencial |
PORTAL_OIDC_CLIENT_SECRET |
segredo somente local | Client secret; obrigatório e distinto em produção |
PORTAL_OIDC_REDIRECT_URL |
http://localhost:8080/auth/callback |
Redirect URI exata cadastrada no IdP |
PORTAL_OIDC_POST_LOGOUT_REDIRECT_URL |
http://localhost:8080/login |
Retorno permitido após logout no IdP |
PORTAL_OIDC_SCOPES |
openid,profile,email,groups |
Scopes separados por vírgula; deve conter openid |
PORTAL_OIDC_EMAIL_CLAIM |
email |
Claim de e-mail |
PORTAL_OIDC_NAME_CLAIM |
name |
Claim de nome de exibição |
PORTAL_OIDC_GROUPS_CLAIM |
groups |
Claim de grupos, como string ou lista de strings |
PORTAL_OIDC_FLOW_TTL |
5m |
Validade de state, nonce e PKCE |
PORTAL_OIDC_DISCOVERY_TIMEOUT |
5s |
Timeout da discovery OIDC |
PORTAL_SESSION_TTL |
8h |
Validade máxima da sessão, limitada também pelo ID token |
PORTAL_SESSION_COOKIE_SECURE |
true |
Cookie Secure; só pode ser false no HTTP local |
PORTAL_CSRF_SECRET |
segredo somente local | Chave HMAC com no mínimo 32 caracteres |
PORTAL_MAX_BODY_BYTES |
65536 |
Limite global de payload mutável, entre 1 KiB e 1 MiB |
PORTAL_BRANDING_NAME |
Open Platform |
Nome exibido no portal |
PORTAL_BRANDING_PRIMARY_COLOR |
#5b5bd6 |
Cor primária em hexadecimal |
PORTAL_BRANDING_DEFAULT_THEME |
system |
system, light ou dark |
PORTAL_BRANDING_LOGO_FILE |
vazio | PNG, JPEG, GIF, WebP ou ICO local, até 512 KiB |
PORTAL_BRANDING_FAVICON_FILE |
vazio | Favicon local nos mesmos formatos e limite |
PORTAL_CATALOG_FILE |
catalog/catalog.yaml |
Catálogo YAML versionado validado no startup |
PORTAL_CATALOG_SCHEMA_FILE |
catalog/schema.json |
JSON Schema 2020-12 compilado antes do catálogo |
PORTAL_RBAC_BINDINGS_FILE |
vazio | Arquivo YAML RBAC estrito; obrigatório em produção |
PORTAL_TERRAFORM_BINARY |
terraform |
Executável usado exclusivamente por terraform fmt |
PORTAL_TERRAFORM_WORK_ROOT |
diretório temporário dedicado | Raiz absoluta dos workspaces efêmeros de geração |
PORTAL_GIT_PROVIDER |
disabled |
disabled ou github; obrigatório habilitar em produção |
PORTAL_GIT_BASE_URL |
https://api.github.com |
API do GitHub ou GitHub Enterprise Server |
PORTAL_GIT_API_VERSION |
2026-03-10 |
Versão REST enviada ao provider |
PORTAL_GIT_TOKEN |
vazio | Token lido apenas da configuração/secret manager |
PORTAL_GIT_OWNER |
company |
Organização proprietária dos repositórios aprovados |
PORTAL_GIT_BASE_BRANCH |
main |
Branch base fixa para os Pull Requests |
PORTAL_GIT_TIMEOUT |
15s |
Timeout das chamadas ao provider |
PORTAL_GITHUB_WEBHOOK_SECRET |
segredo somente local | Segredo HMAC independente do token da API Git |
PORTAL_GITHUB_WEBHOOK_PREVIOUS_SECRET |
vazio | Segredo anterior temporário, apenas durante rotação HMAC |
PORTAL_GITHUB_WEBHOOK_MAX_BODY_BYTES |
65536 |
Limite do corpo bruto, nunca superior ao limite global |
PORTAL_ATLANTIS_ACTORS |
atlantis,atlantis[bot] |
Logins autorizados a produzir eventos Atlantis |
Credenciais locais, URLs OIDC sem HTTPS e cookies sem Secure são rejeitados quando PORTAL_ENVIRONMENT=production.
Claims OIDC identificam a pessoa e seus grupos, mas não concedem acesso diretamente. Em produção, bindings vêm exclusivamente do arquivo externo PORTAL_RBAC_BINDINGS_FILE, validado e carregado no startup sem persistir grants. Use config/rbac.example.yaml como estrutura, substituindo os grupos genéricos. Sem o arquivo, ambientes não produtivos preservam os bindings PostgreSQL das fases anteriores. O caso de uso exige simultaneamente:
- um
role_bindinglocal comcatalog.request, global ou com escopocatalog_item; - participação em pelo menos um
authorization.request_groupsdo item.
Binding global:
- subject: group:platform-users
role: catalog.requestTambém é possível associar uma identidade específica no formato subject:<issuer>#<sub>. Sem uma associação local válida, o menu não mostra o catálogo e acesso direto a /catalog retorna 403.
Binding restrito a um item:
- subject: group:database-developers
role: catalog.request
scope_type: catalog_item
scope_value: aws-rds-postgrescatalog/catalog.yaml é a única fonte para autorização e UX dos módulos. catalog/schema.json documenta a estrutura formal; o loader também aplica regras semânticas mais restritivas. Ambos são validados no startup e explicitamente na CI.
Cada item possui ID e versão, metadados, owner/risco, source/ref fixos, target controlado pelo servidor, grupos, ambientes, campos tipados, valores fixos e um conjunto versionado de templates. Tipos publicados: string, integer, boolean e select.
O formulário envia apenas environment e os nomes presentes em fields. Campos extras, valores múltiplos, enum fora da allowlist, tipo inválido e ambiente não permitido são rejeitados no caso de uso. module, source, ref, repository, path, HCL e valores fixos nunca são aceitos do navegador.
Os templates ficam em catalog/templates/<item>/<versão>/ e possuem outputs .tf declarados explicitamente no catálogo. O loader rejeita placeholders desconhecidos, arquivos fora dessa raiz e links simbólicos. HTMX 2.0.10 é carregado por CDN com SRI e hash fixado na CSP. Sem JavaScript, os mesmos formulários retornam páginas completas.
O caso de uso repete RBAC e validação do payload antes de gerar qualquer arquivo. O destino é derivado apenas de target.path_template; paths absolutos, .., barras invertidas, segmentos inesperados e escapes do workspace são rejeitados. A geração usa somente source/ref, valores fixos e campos tipados do item autorizado, executa terraform fmt e verifica que o workspace contém exatamente os outputs aprovados.
O endpoint de criação recebe apenas os campos publicados, environment, CSRF e uma chave idempotente gerada pelo servidor. A chave bruta não é persistida: o banco guarda somente seu hash, o fingerprint do payload técnico, estado e referência do PR. Reenvios concluídos devolvem o mesmo PR; colisões de payload e operações ainda em processamento falham sem criar outra mudança.
O adaptador inicial usa a API REST do GitHub/GitHub Enterprise para criar ref, tree, commit e PR, com labels padronizados. Configure um token limitado ao repositório de destino com Contents: write e Pull requests: write; ele deve vir de secret manager como PORTAL_GIT_TOKEN. Em desenvolvimento, o provider permanece disabled por padrão e nenhum Git externo é alterado.
Cada submissão reservada cria um UUID em requests e um evento request_submitted na mesma transação. O pedido persiste solicitante, catálogo/item/versões, input normalizado sanitizado, ambiente, cliente, target calculado e referências Git. A UI nunca carrega ou renderiza normalized_input.
request_events é append-only no próprio PostgreSQL: UPDATE e DELETE são bloqueados por trigger. Eventos guardam origem, tipo, ator opcional, payload derivado por allowlist, horário da origem e ID externo deduplicável. Entregas ficam em webhook_deliveries somente com headers mínimos, hash do corpo e estado; assinatura e corpo bruto não são armazenados.
Configure o webhook do repositório com content type application/json, URL /webhooks/git, segredo vindo do secret manager e somente os eventos pull_request, pull_request_review, check_run, check_suite e issue_comment. A aplicação valida X-Hub-Signature-256 sobre o corpo bruto antes do parsing, exige X-GitHub-Delivery único e aceita apenas o owner configurado. Consulte a validação oficial de deliveries e os contratos de eventos.
Checks com nome/app contendo terraform ou atlantis alimentam estados de plan. Comentários são classificados somente quando publicados por um ator em PORTAL_ATLANTIS_ACTORS; o texto original nunca é persistido. Eventos atrasados entram na timeline, mas só alteram a projeção quando seu timestamp não é anterior ao último status. Um PR fechado sem merge vira cancelled; merge por si só não significa applied.
O solicitante consulta o próprio pedido. Para leitura administrativa, associe request.read globalmente ou nos escopos catalog_item e client:
- subject: group:platform-admins
role: request.read
scope_type: client
scope_value: acme- Sessão opaca persistida apenas pelo hash; cookies
HttpOnly,SameSite=LaxeSecureem produção. statede uso único,nonce, PKCE S256 e validação de issuer, audience, assinatura e expiração no fluxo OIDC.- CSRF HMAC vinculado à sessão em todas as mutações. O callback OIDC usa
statede uso único como proteção equivalente. - CSP restritiva,
X-Content-Type-Options: nosniff,Referrer-Policy: no-referrer,X-Frame-Options: DENYeframe-ancestors 'none'. - Login, callback e logout existem apenas como
POST; não há mutação viaGET. - A validação do formulário é
POST, exige sessão/CSRF e repete autorização e validação no caso de uso. - A criação do PR também é somente
POST, exige CSRF e repete autorização, validação e idempotência no caso de uso. - O token Git nunca é aceito pelo navegador, persistido no banco ou incluído em logs/erros.
- O webhook é a única mutação sem CSRF; usa autenticação HMAC no corpo bruto, limite próprio, deduplicação e persistência anterior à projeção.
make generate # gera o pacote sqlc
make catalog-validate # compila o schema e valida a fixture versionada
make rbac-validate # valida o RBAC externo contra o catálogo
make deploy-validate # valida Helm e renderiza o overlay Kustomize
make lint # go vet e golangci-lint
make test # testes com race detector
make build # portal, migration runner e validadores
make migrate # aplica migrations pendentes
make docker-build # imagem OCI local
make ci # aceite completo das Fases 0–5O chart está em deploy/helm/open-platform, os runbooks em docs/runbooks e o roteiro corporativo em docs/e2e-corporate.md. As decisões arquiteturais permanecem em docs/adr.