Appearance
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, andOrganizationMemberPrisma 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 behaviorcontracts/openapi/services/iam-service.yamlcontracts/openapi/services/school-service.yamlservices/iam-service/api/openapi.yamlservices/school-service/api/openapi.yamldocs/api/iam-service.mddocs/api/school-service.mddocs/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:
TestIAMHTTPRolePermissionAndOrganizationMembershipnow 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 andorg_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}/membersmay 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-Syncand behave as IAM read-model sync routes. Ordinary direct writes return409 SCHOOL_MEMBERSHIP_NOT_AUTHORITATIVE, andPOST /v1/organizationsno longer creates a localOWNERfromX-User-Idunless the same sync marker is present. iam-servicenow publishesiam.organization.member_addedandiam.organization.member_removedpayloads withuserId/accountId,organizationId,role,status,unitId, andjoinedAtwhere available.TestIAMPublishesOrganizationMembershipSyncEventscovers default register membership, default account membership, direct member add, invite accept, and remove producers.iam-servicecan now wireSCHOOL_SERVICE_URLorSCHOOL_SERVICE_BASE_URLinto a school-service sync publisher. It maps member add events toPUT /v1/organizations/{organizationId}/members/{userId}and remove events toDELETE /v1/organizations/{organizationId}/members/{userId}withX-IAM-Membership-Sync: iam-service,X-IAM-Membership-Event-Id, andX-IAM-Membership-Occurred-At;client_test.gocovers 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_outboxrows 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_stateby(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 localOWNERbecause IAM is canonical for permission-bearing membership.
Tasks:
- Audit
services/iam-service/migrations/*andservices/school-service/migrations/*for membership overlap. - Done locally: add a non-default gateway route rehearsal for
/api/organizations/:id/membersadd/remove writes throughorganization_membership_adapter. The adapter translates legacyuserIdadd-member bodies to IAMaccountId, 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, andCENTER_MANAGERare projected toORG_ADMINfor 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 ismake test-organization-membership-write-livewith 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/gatewaymake test-auth-routes test-organization-routesORGANIZATION_MEMBERSHIP_WRITE_SELF_TEST=1 make test-organization-membership-write-live