InfraNotes IAM · v0
Welcome
Select a document from the sidebar to read it.
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog,
and this project adheres to Semantic Versioning.
Unreleased
Added
- None
Changed
- None
Fixed
- None
v0.22.1 - 2026-06-09
Added
- None
Changed
- None
Fixed
- None
v0.22.0 - 2026-06-09
Added
- None
Changed
- None
Fixed
- None
v0.21.0 - 2026-06-09
Added
- None
Changed
- None
Fixed
- None
v0.20.0 - 2026-06-09
Added
- None
Changed
- None
Fixed
- None
v0.19.0 - 2026-06-09
Added
- None
Changed
- None
Fixed
- None
v0.18.0 - 2026-06-08
Added
- None
Changed
- None
Fixed
- None
v0.17.1 - 2026-06-08
Added
- None
Changed
- None
Fixed
- None
v0.17.0 - 2026-06-06
Added
- None
Changed
- None
Fixed
- None
v0.16.0 - 2026-06-04
Added
- None
Changed
- None
Fixed
- None
v0.15.0 - 2026-06-04
Added
- None
Changed
- None
Fixed
- None
v0.14.0 - 2026-06-04
Added
- None
Changed
- None
Fixed
- None
v0.13.1 - 2026-06-04
Added
- None
Changed
- None
Fixed
- None
v0.13.0 - 2026-06-04
Added
- None
Changed
- None
Fixed
- None
v0.12.1 - 2026-05-20
Added
- None
Changed
- None
Fixed
- None
v0.12.0 - 2026-05-17
Added
- Phase 6 —
/admin/audit-logsfilterable stream. Three new
query params extend the audit-log surface to cover platform
triage at scale:severity— whitelisted toinfo/warning/error/
critical; rejects unknown values with 400 so a typo cannot
silently return an empty result set.cursor— opaque base64 keyset pointer over(created_at, id)
using the same codec QuerySessions adopted in INFT-87 (the
helpers are now generalized asencodeKeysetCursor/
decodeKeysetCursor).limit— operator-controlled page size with a hard cap at 500
so a single call cannot scan an unbounded slice of
audit_events. The previous behavior (hard-coded 100) is
preserved as the default when the param is omitted.
- Response now always carries the
next_cursorfield. Clients walk
the full result set by replaying the prior page'snext_cursor
value on the next call. The field is set tonullon the
terminal page to signal pagination is complete and the client
should stop paging. Existing callers that ignored the field
continue to work. - Phase 6 Item #2 —
/admin/audit-logs/export. Streams the
same filter slice the screen endpoint returns, but as CSV
(default) or JSONL, capped at 50,000 rows per call. Each
response carries anX-Content-SHA256header so an operator
handing the export to a compliance reviewer can prove
integrity, plusX-Row-Countfor quick sanity checks. The
export itself emits anadmin.audit_log.exportedaudit row
recording the row count, filter set, and integrity hash so the
export is auditable end-to-end. - Phase 6 Item #3 — migration
000029_audit_capture_legacy_marker.
Stampsdetails.audit_capture_legacy = trueon every
pre-2026-05-16 audit row withactor_type = 'user',
actor_id IS NOT NULL, andsession_id IS NULL. The marker
lets future audit-integrity tooling distinguish historical
rows (when session capture had not yet been plumbed through
every handler) from new rows that bug out — the former are
expected, the latter are a regression. Idempotent both
directions.
Changed
- None
Fixed
- None
v0.11.1 - 2026-05-16
Added
- None
Changed
- None
Fixed
- None
v0.11.0 - 2026-05-16
Added
- Phase 5 — tenant switching + self-leave. Three new endpoints
give users self-service control over which workspace they operate
in without an admin in the loop:GET /auth/tenants— returns the caller's active tenant
memberships with anis_defaultflag derived from
users.default_tenant_id. Powers the FE workspace switcher.POST /auth/tenants/switch— flipsusers.default_tenant_id
to the target tenant and returns a fresh access+refresh token
pair scoped to it. Revokes the caller's prior session with
reasontenant_switchso the old pair cannot keep operating
on the prior tenant. Refuses with 404 when the caller has no
active membership in the target, and 409 when the target is
suspended or archived. Emitsauth.tenant_switchedaudit row.DELETE /tenants/{id}/members/me— self-leave. Refuses with
409 when the caller is the soletenant_admin("transfer ownership first") or the sole remaining active member of the
workspace ("archive the workspace instead"). On success,
strips role assignments + membership, revokes the caller's
sessions scoped to this tenant only (other tenants' sessions
stay live), and clearsdefault_tenant_idwhen it pointed at
the abandoned tenant. Fails closed with 503 when the sessions
repository is not wired — same posture as the Phase 4 status /
entitlements paths. Emitstenant.member.leftaudit row.
- New repository helper
sessions.Repository.RevokeAllForUserInTenant
for scoping revocation to a single(user, tenant)pair without
affecting the user's sessions in other tenants.
Changed
wouldOrphanPlatformAdminshort-circuits when the(tenant, user)
row does not currently holdplatform_admin— without this gate
the helper would refuse legitimate self-leave / removal calls on
any platform with zeroplatform_adminusers (a state that should
never exist in production but did surface in the Phase 5 self-leave
test path where the caller is a plaintenant_member).
Fixed
- None
v0.10.0 - 2026-05-15
Added
- None
Changed
- None
Fixed
- None
v0.9.0 - 2026-05-15
Added
- INFT-88 platform-admin (founder) bootstrap. Env-var-driven seed
of the firstplatform_adminuser at startup, sourced from secrets
the operator projects via External Secrets Operator (Authentik /
Keycloak / Grafana shape). Idempotent byplatform_adminrole
existence; concurrent IAM pod starts are serialized via
pg_advisory_xact_lockinside a single tx that wraps the
existence check + create/recover path. Recovery flag
(IAM_BOOTSTRAP_RESET_PASSWORD=true) refuses to elevate users who
do not already hold the role. Every action emits an
admin.platform_admin.bootstrappedaudit row with mode in
{create, recover, noop}. Seeinternal/bootstrap/. POST /auth/change-password: authenticated password rotation.
Verifies the current password, rotates the hash, clears
users.password_change_required, revokes every active session for
the user. Federation-only accounts (emptypassword_hash) surface
asErrInvalidCredentials→ 401 instead ofErrInvalidHash→ 500.password_change_requiredJWT claim. Propagated through
Login,Refresh, andExchangeAuthorizationCodeso the frontend
forced-rotation gate honours the contract regardless of which auth
flow issued the access token.- Self-lockout guard on
PUT /tenants/{id}/members/{user_id}/role
andDELETE /tenants/{id}/members/{user_id}. Refuses to demote or
remove the last user holdingplatform_adminanywhere on the
platform; counts distinct users across the post-mutation state so
a multi-tenant platform admin remains demoteable from any single
tenant. Returns 409 withRefusing to demote/remove the last platform_admin. - Migration
000028_users_password_change_requiredadds
users.password_change_required boolean NOT NULL DEFAULT FALSE.
Changed
users.Repository.UpdatePasswordHashclears
password_change_requiredas part of the same UPDATE so a manual
rotation cannot leave the gate dangling.- Bootstrap reuse / recovery branches now flip
is_email_verified=TRUEso a promoted-but-unverified founder is
not stranded behindErrEmailNotVerifiedafter the hash rewrite. - Bootstrap
validateConfignormalises emptyUsernameto the
local-part of the email and emptyFullNameto the email itself,
satisfying theNOT NULL UNIQUEcolumns when the operator supplies
onlyIAM_BOOTSTRAP_PLATFORM_ADMIN_EMAIL+_PASSWORD. ChangePasswordhandler parsesclaims.PrincipalIDup front and
returns 401CodeTokenInvalidon a malformed claim, matching the
pattern inLogoutAll/GetProfile.- OTel observability uplift: HTTP / gRPC / Kafka trace
instrumentation, OTLP protocol normalization, honour the standard
OTEL_*env vars.
Fixed
isDuplicateMembershipunwraps*pgconn.PgErrorand matches
Code=="23505"(unique_violation) before falling back to string
match, so bootstrap re-runs against a pgx version that reformats
the error message stay idempotent.- Bootstrap
MembershipCreatedevent no longer fires on duplicate
inserts; mirrors the existingcreatedUser/createdTenant
pattern via a newcreatedMembershipflag.
v0.8.0 - 2026-05-14
Added
- None
Changed
- None
Fixed
- None
v0.7.0 - 2026-05-14
Added
- None
Changed
- None
Fixed
- None
v0.6.0 - 2026-05-14
Added
- None
Changed
- None
Fixed
- None
v0.5.1 - 2026-05-06
Added
- None
Changed
- None
Fixed
- Tenant member-management mutations no longer accept any tenant member
as authorisation.PUT /tenants/{id}/members/{user_id}/roleand
DELETE /tenants/{id}/members/{user_id}are now gated on
requireTenantAdmininstead ofrequireTenantAccess, matching the
rule documented atinternal/handlers/tenant/tenant.go:52-56. The
previous code let any authenticated tenant member rewrite role
assignments or remove other members, which could be used to
self-promote to any role the caller could pass via UUID.
Additionally,UpdateMemberRolesnow refuses to grant the
platform_adminrole unless the caller already holds it — a
tenant_admin can no longer escalate themselves or another in-tenant
member to platform-wide authority. Discovered while bootstrapping a
staging platform_admin during the INFT-69 workload-identity rollout.
v0.5.0 - 2026-05-06
Added
- INFT-69: ADR-0007 "Workload identity operating model and Profile C consumer
contract". Defines the platform pattern for backend-to-backend authentication:
service-account registration, token issuance viaclient_credentials, tenancy
semantics, secret distribution and rotation, consumer-side caching and refresh,
failure modes, validator-side dispatch onprincipal_type, and the four-test
negative-test contract every Profile C consumer must satisfy. First two
consumers areinfranotes-recon(audienceinfranotes-core-ledger) and
infranotes-project-finance(audienceinfranotes); first two validators
areinfranotes-core-ledgerandInfra_Notes. No code change in this PR;
the ADR closes the operating-model gap that ADR-0003 §"Profile C" left open
and that recon ADR-0001 surfaced concretely. - INFT-69:
POST /service-accounts/now accepts an optionalclient_id
field on the request body. When supplied, the caller-provided slug is
used as the service account'sclient_idinstead of the auto-generated
sa-{12chars}value. This lets ADR-0007 §1's "slug matching the
consumer's repo name" wording (e.g.,recon,project-finance) be
honored at runtime, which is whatinfranotes-core-ledgerv0.11.0's
per-route policy table relies on (AllowedClientIDs: ["recon"]).
Caller-supplied values require platform_admin (tenant admins must
accept the auto-generated value), must be lowercase kebab-case 3-100
chars long, and must not use the reservedsa-prefix. UNIQUE
collisions surface as 409 Conflict via the new
auth.ErrClientIDInUsesentinel; previously they returned 500.
Changed
- None
Fixed
- None
v0.4.1 - 2026-05-02
Added
- None
Changed
- None
Fixed
- None
v0.4.0 - 2026-04-30
Added
- INFT-82: Eight new RBAC permissions covering the AT (Mozambique) compliance
surface in the Infra_Notes service:
invoice:create_credit_note,invoice:create_debit_note,
invoice:create_factura_recibo,invoice:cancel,
withholding:read,withholding:write,withholding:configure,
accounting:export_with_withholding(migration 000026). - INFT-82: Role-template grants mapping the eight permissions onto the
existingtenant_admin,finance_manager,finance_viewer,
external_auditor, andexternal_accountantsystem roles
(migration 000027). Configure and export-with-withholding stay
admin-only;withholding:readis granted to tenant_admin, finance_manager,
finance_viewer, external_auditor, and external_accountant;withholding:write
is granted to tenant_admin, finance_manager, and external_accountant.
Changed
- None
Fixed
- None
v0.3.0 - 2026-04-29
Added
- None
Changed
- None
Fixed
- None
v0.2.1 - 2026-04-24
Added
- None
Changed
- None
Fixed
- None
v0.2.0 - 2026-04-24
Added
- None
Changed
- None
Fixed
- None