Skip to content

IAM Organization Membership Canonical Task

Agent workflow: follow README.md for Audit -> Investigate -> Code -> Test -> Fix; keep this pack's writable scope and verification commands authoritative.

Dispatch type: orchestrator

Owner: iam-service with orchestrator-owned gateway route updates.

Problem:

iam-service and school-service both currently have organization membership tables and APIs. This can create split-brain tenant validation, token claims, and permission checks.

Goal:

Make IAM the canonical owner for access-control organization membership while leaving organization metadata, units, campuses, centers, and school structure in organization-service / current school-service.

Legacy source evidence:

  • /Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/auth/**
  • /Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/organizations/**
  • /Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/src/modules/tenant/**
  • User, Organization, and OrganizationMember Prisma models in /Users/velikho/Desktop/WORKING/HOCTAPAZ/node-platform/apps/api/prisma/schema.prisma

Writable files:

  • services/iam-service/**
  • services/school-service/** only for compatibility/read-model behavior
  • contracts/openapi/services/iam-service.yaml
  • contracts/openapi/services/school-service.yaml
  • services/iam-service/api/openapi.yaml
  • services/school-service/api/openapi.yaml
  • docs/api/iam-service.md
  • docs/api/school-service.md
  • docs/agents/service-tasks/iam-org-membership-canonical.md

Orchestrator-owned files:

  • Gateway route tables, default /api/organizations* cutover, shared Compose, Helm, K8s, migrator, and service-index docs.

Completed foundation:

  • TestIAMHTTPRolePermissionAndOrganizationMembership now covers IAM role and permission assignment plus organization membership refresh. It creates a membership after the initial login, refreshes the existing session, and asserts the new access token/session include the assigned org role and org_ids; it also checks IAM security events for role assignment and membership addition.
  • Gateway rehearsal is already read-only for organization member routes: GET /api/organizations/{organizationId}/members may route to school-service, while public member writes remain legacy-proxied until an IAM adapter exists.
  • School-service member write routes now require X-IAM-Membership-Sync and behave as IAM read-model sync routes. Ordinary direct writes return 409 SCHOOL_MEMBERSHIP_NOT_AUTHORITATIVE, and POST /v1/organizations no longer creates a local OWNER from X-User-Id unless the same sync marker is present.
  • iam-service now publishes iam.organization.member_added and iam.organization.member_removed payloads with userId/accountId, organizationId, role, status, unitId, and joinedAt where available. TestIAMPublishesOrganizationMembershipSyncEvents covers default register membership, default account membership, direct member add, invite accept, and remove producers.
  • iam-service can now wire SCHOOL_SERVICE_URL or SCHOOL_SERVICE_BASE_URL into a school-service sync publisher. It maps member add events to PUT /v1/organizations/{organizationId}/members/{userId} and remove events to DELETE /v1/organizations/{organizationId}/members/{userId} with X-IAM-Membership-Sync: iam-service, X-IAM-Membership-Event-Id, and X-IAM-Membership-Occurred-At; client_test.go covers method, path, headers, payload, role mapping into the school-service read-model enum, transient retry, and non-retryable failure behavior.
  • IAM membership mutations now persist iam_membership_outbox rows without calling the school-service publisher inline. The background dispatcher replays due rows through the existing school-service sync publisher and marks delivered, retry, or dead state; the focused tests prove writes leave events pending until dispatcher delivery and that failures schedule a retry.
  • School-service now records iam_membership_sync_state by (organizationId,userId) and applies IAM sync routes through ordered read-model methods. Newer deletes leave tombstones that reject stale upsert replays, duplicate deletes stay idempotent, and IAM-sync deletes can remove the last local OWNER because IAM is canonical for permission-bearing membership.

Tasks:

  • Audit services/iam-service/migrations/* and services/school-service/migrations/* for membership overlap.
  • Done locally: add a non-default gateway route rehearsal for /api/organizations/:id/members add/remove writes through organization_membership_adapter. The adapter translates legacy userId add-member bodies to IAM accountId, routes deletes to IAM {accountId} paths, and keeps PATCH member updates plus default writes legacy until browser and rollback evidence exist.
  • Extend IAM tests as needed for invite-accept-driven membership refresh.
  • Keep the school read-model role contract explicit: IAM-only roles such as ADMIN, STAFF, and CENTER_MANAGER are projected to ORG_ADMIN for school-service tenant checks.
  • Keep OpenAPI/docs in sync when the runtime sync transport changes.

Acceptance:

  • There is exactly one write owner for permission-bearing organization membership.
  • Tenant validation consumes IAM membership or an explicitly synchronized read model.
  • School-service member writes reject ordinary callers unless the request is an explicit IAM read-model sync.
  • IAM membership outbox rows are durable and covered for pending, retry, and dead/delivered transitions; sync publisher failures do not silently drop membership events.
  • School-service IAM sync routes are idempotent, apply event ordering, preserve tombstone deletes against stale upsert replays, and project IAM-only roles into the school-service tenant role contract.
  • Default gateway routes remain legacy until parity and rollback evidence exists.
  • The non-default membership-write route table is covered by make test-organization-routes; the opt-in add/remove live smoke is make test-organization-membership-write-live with an IAM admin bearer token and disposable member account id. Default route promotion still requires live browser proof and a rollback rehearsal.

Verification:

  • GOTOOLCHAIN=go1.25.11 go test ./services/iam-service/... ./services/school-service/... ./services/api-gateway/internal/gateway
  • make test-auth-routes test-organization-routes
  • ORGANIZATION_MEMBERSHIP_WRITE_SELF_TEST=1 make test-organization-membership-write-live

Go-platform documentation is generated from repository Markdown.