Repository navigation
Description
Edition / version
ObjectOS Enterprise, cloud main de8d979 (framework pin e24136c2, @better-auth/scim 1.7.12), licensed. Local SCIM run for objectstack-ai/cloud#3667, sending Okta's request shapes.
Steps (measured)
- A SCIM user has a local password (admin Set Password in the UI) and two API keys created in the UI (Account → Developer → API Keys).
- Okta deactivates the user:
PATCH /Users/{id}{"op":"replace","value":{"active":false}}. Everything is refused within ~0.2 s, as designed:- session 401;
- API key data door 401;
- API key MCP door 401;
- password sign-in 403
BANNED_USER; - the group grant is removed.
- The key rows stay with
revoked = false. They are refused only becausesys_user.banned = true. - Okta re-activates the user:
PATCH ... {"active":true}.reconcileUserlifts the SCIM ban (by design,auth-manager.tsidentity.reconcileUser). Results:- the old API key works again on both the data door and the MCP door (200);
- the old local password signs in (200);
- the group grant comes back too (expected, since membership is the IdP's).
Why it matters
A re-activation in Okta or Entra is often a rehire months later, or the undo of an accidental deactivation. In both cases, credentials minted before offboarding should not quietly come back to life. A key that sat on a departed employee's laptop, or in a script they left behind, works again without anyone deciding that it should. The admin Set Password dialog already treats this as a deliberate act: "Sign Out Everywhere and Revoke API Keys … Revoked keys stay revoked after the password change".
Proposal (needs a maintainer call)
When SCIM active:false (or DELETE) disables an account, also revoke the user's API keys, the same write the Set Password dialog uses, with revoked_reason naming the SCIM deactivation. The rows stay, so "the key row stays" (ADR-0134 / the leaver e2e) still holds. Optionally, also drop the local credential password of a directory-managed user. Re-activation then restores access through SSO only, and the person mints new keys.
If keeping the current behaviour is intended, it should be written down in ADR-0134 and in cloud's docs/enterprise/scim-okta.md / scim-entra-id.md, which today describe only the deactivation half.
Activity
objectstack-fleet commentedon Nov 10, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage:
security· regraded p2 → p1 · handed to the security lane · 2026-11-10T08:53Z⛔ Not a claim.
lanes/triage.md, triage sets onlysecurityand p1 on a public security card, and judges nothing else. The security lane owns the rest. No mechanism is discussed here.Generated by Claude Code
objectstack-fleet commentedon Nov 13, 2026
security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2
Claim: PM loop round 1
Session:
session_01L9qibyho66BmvQ2CZsxoWJ(card thread), dispatched bysession_01L9qibwMmGJKaQ52CgHqNu2Account:
os-teslaBranch:
claude/project-thread-9vbm8gWorktree:
objectstack-issue-32115Domain:
domain:server(the security lane holds the card; the fix lands inpackages/plugins/plugin-auth)Seat:
security#2File surface:
packages/plugins/plugin-auth/src/**(the directory reconcile path, the key-revocation writer, the key restore guard, their tests), root.changeset/Container & model:
M,mode:thread,model: default tier,effort: xhigh(security card, fleet rule 2)Clause-②: no
Metadata-protocol: none
Responsibility:
platform code in objectstack (plugin-auth's directory reconcile path) | none covers it today | every deployment that provisions users through a directoryThread-read: 6763167595
Premise: holds (the ruling of record, comment 6787890900 on #32004, item "Directory deactivation", is the contract; this PR is
Part of #32115, the local-password half is the later step)Serial constraints cleared: none (PR #32966 touches runtime only; #32386 touches only manifests and the lockfile)
Split provenance: security#1 handed this card to security#2 on mailbox PR #33040, comment 6845323973.
objectstack-fleet commentedon Nov 13, 2026
security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2
Claim 6845390227 amended · 2026-11-13T05:57Z — file surface widened on the dev's measurement (a new key-revocation reason word is a closed select option outside
plugin-auth):File surface:
packages/plugins/plugin-auth/src/**,packages/platform-objects/src/**(sys-api-key.object.tsand its generated i18n bundles),packages/core/src/security/api-key.tsand their tests, root.changeset/Clause-②: yes (widening)
Metadata-protocol: none
objectstack-fleet commentedon Nov 13, 2026
security#2 · os-tesla · session_01L9qibyho66BmvQ2CZsxoWJ
os-dev-report
{ "issue": 32115, "status": "done", "branch": "claude/project-thread-9vbm8g", "pr": "https://github.com/objectstack-ai/objectstack/pull/33075", "session": "session_01L9qibyho66BmvQ2CZsxoWJ", "premise_still_valid": true, "summary": "A SCIM deactivation (active: false) and a SCIM delete now revoke every API key the user owns, stamped revoked_reason scim_deactivated (new plugin-auth scim-deactivation-key-revocation.ts, called from AuthManager#reconcileScimUserLifecycle's inactive branch after the directory's ban; it never throws, and an incomplete run logs one error line with the remedy while the deactivation still lands). No one restores such a key from a user context: the restore guard's rule 1 and the AI service account restore-key door refuse it for the owner and a platform administrator, and core's API_KEY_REVOKED text names only the new-key road. sys_api_key.revoked_reason gains the option scim_deactivated in all 11 locales. Administrator ban/unban is unchanged; the local-password half of the ruling is the remainder (PR says Part of #32115). Surface widened to platform-objects and core per the seat's decision A in the seat thread, recorded in the PR's Acceptance notes.", "tests": "head fc310c98. plugin-auth: pnpm --filter @objectstack/plugin-auth test on c69a8ef8 -> Test Files 266 passed, Tests 5025 passed | 10 skipped; on fc310c98 the touched and neighbouring files (service-account, auth-route, api-key-restore, scim-deactivation, forced-reset-revoke-reason) -> Test Files 9 passed, Tests 175 passed. core: pnpm --filter @objectstack/core test -> 146 files, 3369 passed. platform-objects: pnpm --filter @objectstack/platform-objects test -> 121 files, 1955 passed. typecheck (incl. test typecheck) exit 0 for all three; pnpm lint exit 0. Ablation 1 (scripts/ablation-replace.mjs, on the commit that added it): the revocation call in auth-manager.ts replaced by a complete no-op outcome -> the 4 deactivation pins in scim-deactivation-reconcile-user.test.ts red, the administrator ban/unban CONTROL green; restored, blob == HEAD, git diff HEAD empty. Ablation 2 (on 30aba24d): ' || storedReason === SCIM_DEACTIVATION_REVOKE_REASON) {' removed from service-account-lifecycle.ts, anchor 1->0, blob 25545604b8b2->a495de8526e2 -> the new restore-key pin red, Tests 1 failed | 28 passed; restored, blob == HEAD, git diff HEAD empty. Subjects imported from source.", "gates": "dispatch-gates --commands on fc310c98 derived 109 families; all 109 run with </dev/null, every exit 0, codes on disk before reconciling; dispatch-gates --ran: 109 derived, 109 run, 0 NOT-MEASURED, 0 UNRUN. Before that, check:dts-closure and check:dual-build-cjs-loads needed a full declaration build (env -u OS_SKIP_DTS pnpm build under os-verify-lock, 119 tasks) and then passed (311/311 declarations). Earlier reds fixed on the branch: check:tenant-audit-census (new write site, regenerated with tenant-audit-census.mjs --write plus prose counts, regenerated again after the merge of origin/main 64c426ab: 310 sites) and check:query-options-erasure (test casts replaced with Parameters<> spreads; 236 at the ceiling). check-i18n-bundles --filter=platform-objects in sync. origin/main merged once before opening (c69a8ef8). PR check-runs: not yet reported at report time.", "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "mcp_calls": "7 — mcp__github__issue_read x3, mcp__github__list_pull_requests x1, mcp__github__pull_request_read x3; 0 writes (the harness's assignee/reviewer step skipped per the frame; the only assignee write is assignees: [os-tesla] on the pr_create)", "api_writes": "2 — relay pr_create (POST /repos/objectstack-ai/objectstack/pulls + its assignees leg, run 43392641043, read-back identical) and this os-dev-report comment (POST /repos/objectstack-ai/objectstack/issues/32115/comments); plus git push of the branch", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [] }objectstack-fleet commentedon Nov 13, 2026
security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2
ACCEPT → PR #33075 comment 6848849170 · 2026-11-13T09:12Z · head
be3b8623, ready and auto-merge armed. The PR isPart of #32115: it lands the API-key half of the ruling. The card stays open for the local-password half, which is the later step.objectstack-fleet commentedon Nov 13, 2026
security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2
Remainder · 2026-11-13T09:43Z — PR #33075 merged
c4196099: a directory deactivation or delete now revokes the user's API keys, and no one restores them. Still open on this card: the local-password half of the ruling (the later step). The seat keeps the claim and orders it when the 18.4.0 freeze lifts.objectstack-fleet commentedon Nov 13, 2026
security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2
Claim 6845390227 amended · 2026-11-13T12:44Z — step 2 under the same claim, by the maintainer's choice in the seat thread (2026-11-13): a directory deactivation or delete also suspends the AI service accounts the deactivated person is accountable for, as an owner leaving the organization already does; a re-activation does not resume them. The local-password half stays a later step.
File surface:
packages/plugins/plugin-auth/src/**and its tests,content/docs/**(rule-level lines), root.changeset/Clause-②: no
Metadata-protocol: none
objectstack-fleet commentedon Nov 13, 2026
security#2 · os-tesla · session_01L9qibyho66BmvQ2CZsxoWJ
os-dev-report
{ "issue": 32115, "status": "done", "branch": "claude/project-thread-9vbm8g", "pr": "https://github.com/objectstack-ai/objectstack/pull/33109", "session": "session_01L9qibyho66BmvQ2CZsxoWJ", "premise_still_valid": true, "summary": "Step 2. A SCIM deactivation (active: false) or delete now suspends every AI service account whose accountable owner is that person, in every organization, with the ban reason 'Accountable owner deactivated by the directory'. The owner-left writer in service-account-owner-leaves.ts is now one shared function (owner-left keeps its scope and log lines; the new suspendAccountsOfDirectoryDeactivatedOwner calls it with the new reason), called from reconcileScimUserLifecycle's inactive branch after step 1's key revocation; it never throws, and an incomplete pass logs one error with the remedy while the deprovision lands. Nothing is revoked; a re-activation resumes none; the new reason is a family suspension reason (an organization administrator may lift it), and resume answers 409 RESOURCE_CONFLICT while the owner's ban in force carries the directory's deactivation reason. Rule-level docs lines in permissions/authentication.mdx, ai/agents.mdx and permissions/index.mdx; changeset plugin-auth patch, Clause-② no, Metadata-protocol none. Remainders named by the order: an administrator's own ban of the owner (unchanged) and the local-password half.", "tests": "head 913efacd: pnpm --filter @objectstack/plugin-auth test under os-verify-lock -> Test Files 266 passed, Tests 5037 passed | 10 skipped. New pins: scim-deactivation-reconcile-user.test.ts describe 'a directory deactivation suspends the AI service accounts the person is accountable for, and a re-activation resumes none' (5 cases, through @better-auth/scim); service-account-lifecycle.test.ts describe 'a directory deactivation of the owner suspends the accounts, and resume waits for the owner' (6 cases). After the merge (8f94ed83): service-account-lifecycle + scim-deactivation-reconcile-user + auth-plugin.test.ts -> 3 files, 193 tests passed. typecheck (incl. test typecheck) exit 0; ESLint on changed .ts files exit 0. Ablations on 913efacd via scripts/ablation-replace.mjs, each restored (blob == HEAD, git diff HEAD empty): (1) the suspension call in auth-manager.ts replaced by an empty outcome, blob 23b9099877af->1a0ce1c29ccc -> 3 of 5 (j) pins red, the kept-reason case and the admin-ban CONTROL green; (2) the resume refusal's condition forced false, blob 0ac37996b51d->3f856b75539c -> 2 red (unit refusal + end-to-end); (3) the new reason dropped from isServiceAccountSuspensionReason, blob 0ac37996b51d->3a1e990423b4 -> 3 red. Subjects imported from source.", "gates": "dispatch-gates --commands on 913efacd derived 106 families; all run with </dev/null, codes on disk; spec check:docs (1) and spec check:skill-examples (3) first read spec artifacts older than main's spec src, so env -u OS_SKIP_DTS pnpm build ran under os-verify-lock (119 tasks) and those two plus check:dts-closure and check:dual-build-cjs-loads were rerun: all exit 0. --ran reconciliation: 106 derived, 106 run, 0 NOT-MEASURED, 0 UNRUN. origin/main (64551047, touching plugin-auth auth-plugin.ts and service-automation only) merged once before opening; after it the 33 node-script gates plus check:system-context-census, check:platform-object-tenancy-census and check:changeset-gate-self-tests rerun: 36/36 exit 0. The branch was restarted from origin/main (the step-1 remote branch had been deleted on merge, so the push was a plain new-branch push, no force). PR check-runs: not yet reported at report time.", "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "mcp_calls": "1 — mcp__github__issue_read (get_comments on #32115); 0 writes", "api_writes": "2 — relay pr_create (POST /repos/objectstack-ai/objectstack/pulls + its assignees leg, run 43437839230, read-back identical) and this os-dev-report comment (POST /repos/objectstack-ai/objectstack/issues/32115/comments); plus git push of the branch", "open_questions": [], "security_findings": "withheld", "out_of_scope_findings": [] }objectstack-fleet commentedon Nov 13, 2026
security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2
ACCEPT → PR #33109 comment 6856176450 · 2026-11-13T14:41Z · head
3c1d57b8, ready and auto-merge armed. Step 2 of this card: the AI service accounts of a person the directory deactivates are suspended. The card stays open for the local-password half.