InfraForge Docs

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-logs filterable stream. Three new
    query params extend the audit-log surface to cover platform
    triage at scale:
    • severity — whitelisted to info / 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 as encodeKeysetCursor /
      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_cursor field. Clients walk
    the full result set by replaying the prior page's next_cursor
    value on the next call. The field is set to null on 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 an X-Content-SHA256 header so an operator
    handing the export to a compliance reviewer can prove
    integrity, plus X-Row-Count for quick sanity checks. The
    export itself emits an admin.audit_log.exported audit 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.
    Stamps details.audit_capture_legacy = true on every
    pre-2026-05-16 audit row with actor_type = 'user',
    actor_id IS NOT NULL, and session_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 an is_default flag derived from
      users.default_tenant_id. Powers the FE workspace switcher.
    • POST /auth/tenants/switch — flips users.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
      reason tenant_switch so 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. Emits auth.tenant_switched audit row.
    • DELETE /tenants/{id}/members/me — self-leave. Refuses with
      409 when the caller is the sole tenant_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 clears default_tenant_id when 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. Emits tenant.member.left audit 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

  • wouldOrphanPlatformAdmin short-circuits when the (tenant, user)
    row does not currently hold platform_admin — without this gate
    the helper would refuse legitimate self-leave / removal calls on
    any platform with zero platform_admin users (a state that should
    never exist in production but did surface in the Phase 5 self-leave
    test path where the caller is a plain tenant_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 first platform_admin user at startup, sourced from secrets
    the operator projects via External Secrets Operator (Authentik /
    Keycloak / Grafana shape). Idempotent by platform_admin role
    existence; concurrent IAM pod starts are serialized via
    pg_advisory_xact_lock inside 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.bootstrapped audit row with mode in
    {create, recover, noop}. See internal/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 (empty password_hash) surface
    as ErrInvalidCredentials → 401 instead of ErrInvalidHash → 500.
  • password_change_required JWT claim. Propagated through
    Login, Refresh, and ExchangeAuthorizationCode so 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
    and DELETE /tenants/{id}/members/{user_id}. Refuses to demote or
    remove the last user holding platform_admin anywhere 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 with Refusing to demote/remove the last platform_admin.
  • Migration 000028_users_password_change_required adds
    users.password_change_required boolean NOT NULL DEFAULT FALSE.

Changed

  • users.Repository.UpdatePasswordHash clears
    password_change_required as part of the same UPDATE so a manual
    rotation cannot leave the gate dangling.
  • Bootstrap reuse / recovery branches now flip
    is_email_verified=TRUE so a promoted-but-unverified founder is
    not stranded behind ErrEmailNotVerified after the hash rewrite.
  • Bootstrap validateConfig normalises empty Username to the
    local-part of the email and empty FullName to the email itself,
    satisfying the NOT NULL UNIQUE columns when the operator supplies
    only IAM_BOOTSTRAP_PLATFORM_ADMIN_EMAIL + _PASSWORD.
  • ChangePassword handler parses claims.PrincipalID up front and
    returns 401 CodeTokenInvalid on a malformed claim, matching the
    pattern in LogoutAll / GetProfile.
  • OTel observability uplift: HTTP / gRPC / Kafka trace
    instrumentation, OTLP protocol normalization, honour the standard
    OTEL_* env vars.

Fixed

  • isDuplicateMembership unwraps *pgconn.PgError and 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 MembershipCreated event no longer fires on duplicate
    inserts; mirrors the existing createdUser / createdTenant
    pattern via a new createdMembership flag.

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}/role and
    DELETE /tenants/{id}/members/{user_id} are now gated on
    requireTenantAdmin instead of requireTenantAccess, matching the
    rule documented at internal/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, UpdateMemberRoles now refuses to grant the
    platform_admin role 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 via client_credentials, tenancy
    semantics, secret distribution and rotation, consumer-side caching and refresh,
    failure modes, validator-side dispatch on principal_type, and the four-test
    negative-test contract every Profile C consumer must satisfy. First two
    consumers are infranotes-recon (audience infranotes-core-ledger) and
    infranotes-project-finance (audience infranotes); first two validators
    are infranotes-core-ledger and Infra_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 optional client_id
    field on the request body. When supplied, the caller-provided slug is
    used as the service account's client_id instead 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 what infranotes-core-ledger v0.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 reserved sa- prefix. UNIQUE
    collisions surface as 409 Conflict via the new
    auth.ErrClientIDInUse sentinel; 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
    existing tenant_admin, finance_manager, finance_viewer,
    external_auditor, and external_accountant system roles
    (migration 000027). Configure and export-with-withholding stay
    admin-only; withholding:read is 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

v0.1.1 - 2026-04-22

v0.1.0 - 2026-04-21