Skip to content

Organization IAM Membership And School Sync Proof ​

Status: bounded SVC-005 proof accepted, not organization-service or rollout completion.

This page records the ownership and read-model contract for organization membership. IAM is the permission-bearing write authority; school-service is an organization/read-model owner and accepts membership changes only from the IAM sync contract.

IAM Membership Write Authority ​

IAM owns the canonical organization membership operations and emits iam.organization.member_added / iam.organization.member_removed events. The IAM membership outbox keeps delivery pending/retry/dead state in the IAM-owned database and the focused IAM usecase tests prove:

  • account/register, organization member add, invite acceptance, and removal enqueue membership events without waiting on the school-service publisher;
  • only the background dispatcher delivers the pending outbox rows, so a blocked school-service endpoint cannot delay the canonical IAM write;
  • publisher failures leave an outbox item due for retry;
  • successful dispatch marks the outbox delivered;
  • membership attributes include account/user id, organization id, role, status, unit, and event timing where available.

The IAM school-sync publisher at services/iam-service/internal/schoolsync/client.go maps those events to:

  • PUT /v1/organizations/{organizationId}/members/{userId} for add/update;
  • DELETE /v1/organizations/{organizationId}/members/{userId} for removal;
  • X-IAM-Membership-Sync: iam-service;
  • X-Internal-Service: iam-service and the shared X-Internal-Token;
  • X-IAM-Membership-Event-Id and X-IAM-Membership-Occurred-At ordering metadata;
  • bounded retry for transient failures and permanent rejection for unsupported roles.

School-service never mutates IAM roles, permissions, or canonical membership.

School-Service Read-Model Sync ​

The school-service endpoints under /v1/organizations/{organizationId}/members* require the IAM sync marker. They also require trusted X-Internal-Service: iam-service and X-Internal-Token; marker-only or spoofed requests receive 401 SCHOOL_INTERNAL_UNAUTHORIZED. Ordinary callers receive 409 SCHOOL_MEMBERSHIP_NOT_AUTHORITATIVE. Accepted sync responses carry X-HocTapAZ-Membership-Source: iam-sync-read-model.

The read model records event id/timestamp/action state and protects against:

  • stale upserts after a newer delete tombstone;
  • stale or replayed patches after a newer update;
  • replayed deletes for already absent rows;
  • local last-owner restrictions being applied to IAM canonical removal.

The focused HTTP tests cover the direct-write rejection, marker/header contract, stale replay/tombstone ordering, patch ordering, and last-owner read-model removal.

Gateway Route Proof ​

make test-organization-routes proves the route split:

  • deploy/gateway/routes.json keeps broad /api/organizations* legacy-backed;
  • non-default organization read routes target school-service only;
  • non-default member add/remove write routes target IAM through the gateway organization_membership_adapter;
  • school-service is never made the public permission-bearing member-write owner.

The member-write route rehearsal remains non-default and requires auth, organization context, and IAM authority. Student roster import and broad organization writes remain legacy-backed.

Student Roster Import Boundary ​

The versioned student.roster.import.requested.v1 contract is documented in contracts/events/README.md. It carries stable importJobId, rowId, and sourceEventId keys plus the organization/unit/classroom context and row display fields. The ownership sequence is explicit:

  1. IAM resolves or creates the account and writes canonical organization membership.
  2. Profile-service writes only student display/profile fields using the IAM account id.
  3. Classroom-service writes only local classroom membership using the IAM account id.
  4. School-service consumes IAM membership sync as a read model and does not create accounts, profiles, or classroom members.

Duplicate deliveries are replayed by row idempotency key; rejected rows are reported without replaying accepted owner mutations. No public native student-import route is promoted by this contract. The legacy route remains the active rollback target until all owner contracts have live, browser, and rollback proof.

Rollback Notes ​

Rollback is returning the gateway to deploy/gateway/routes.json, where broad organization routes remain legacy_proxy and native membership entries are absent. Disable the IAM school-sync publisher or leave its outbox pending if school-service is unavailable; IAM canonical membership remains authoritative.

bash
GATEWAY_ROUTE_TABLE=deploy/gateway/routes.json make run-gateway
make test-organization-routes

No default gateway route or shared deployment manifest was changed by this slice. Browser/live member-write proof and rollback-after-candidate review are still required before public promotion.

Required Evidence Boundary ​

  • IAM membership write authority proof: complete for the tested event/outbox contract;
  • school-service read-model sync proof: complete for marker, ordering, and tombstone behavior;
  • member-write route rollback notes: complete for the non-default rehearsal;
  • student roster import ownership contract: complete as a versioned boundary, with native route implementation and live/browser proof still open;
  • public/default route promotion: still blocked by runtime/browser and candidate rollback evidence, plus the open IAM/Profile dependency.

Verification ​

bash
GOTOOLCHAIN=go1.25.11 go test ./services/school-service/...
make test-organization-routes
GOTOOLCHAIN=go1.25.11 go test ./services/iam-service/internal/schoolsync -count=1
GOTOOLCHAIN=go1.25.11 go test ./services/iam-service/internal/usecase -run 'TestIAMPublishesOrganizationMembershipSyncEvents|TestIAMMembershipOutboxRetriesAfterPublisherFailure' -count=1
ruby -e 'require "yaml"; ARGV.each { |path| YAML.load_file(path) }' services/school-service/api/openapi.yaml contracts/openapi/services/school-service.yaml
diff -u services/school-service/api/openapi.yaml contracts/openapi/services/school-service.yaml
docker compose -f deploy/docker-compose.yml config --quiet

All commands passed on 2026-07-10.

Current No-Write Recheck (2026-07-15) ​

The current worktree rechecked the read-model boundary without a live bearer, organization, or member mutation:

  • GOTOOLCHAIN=go1.25.11 go test ./services/school-service/... -count=1 passed, including the school-service HTTP membership boundary coverage;
  • GOTOOLCHAIN=go1.25.11 go test ./services/iam-service/internal/schoolsync -count=1 passed;
  • the focused IAM publisher and outbox retry tests passed;
  • make test-organization-routes confirmed default organization routes remain legacy-backed while only non-default member-write examples target IAM; and
  • ORGANIZATION_MEMBERSHIP_WRITE_SELF_TEST=1 make test-organization-membership-write-live passed the smoke-script contract harness.

This recheck did not supply a bearer token, start a candidate Gateway, or send an IAM membership write. It refreshes contract and route-guard evidence only; the public member-write browser/runtime and rollback-after-candidate gates remain open.

Go-platform documentation is generated from repository Markdown.