Skip to content

Commit 8e87403

Browse files
feat(plugin-security): organization administrators hold manage_connector_connections by default (ADR-0143 D5, ruling B) (#28441)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6Jao6NziWpbTDqMqVqQDfDXvBa)_ Part of #28427. This PR carries the code, pins, docs and changeset. The card's item 4 (one line in ADR-0143 D5 recording ruling B) is split out so that this PR is not held as Tier H; the line is handed to the seat as a patch, and the card stays open until it lands. Clause-②: yes (widening) Metadata-protocol: none ## 维护者速读 - 改了什么:组织管理员(`organization_admin`,以及由它派生的 `organization_admin_no_bypass`)默认拥有 `manage_connector_connections`,组织的 owner/admin 一开始就能为本组织连接 OAuth2 连接器(DocuSign、Salesforce、Google 等),不再需要有人手工绑定权限集。 - 为什么改:Tesla 在决策清单第 6 项答「同意」,即选 B:「B:开一张跟进卡,在 S4 Console Connect 之前默认授予 organization_admin 与 organization_admin_no_bypass」(裁决记录 #28327 评论 6545223261)。不改的话,S4 的 Connect 按钮上线后每个组织管理员都会被拒。 - 风险与回滚:这是一次放宽:组织管理员现在能读、发起、完成、撤销本组织的连接;只限本组织(授予按组织生效,门只作用于当前组织),看不到任何令牌。成员、只读等其它权限集不变,其它能力也不变。代表组织管理员、带 `actions:execute` 授权的 AI agent 会一起获得(沿用现有派生,平台管理员的 agent 今天已是如此),见 Acceptance notes。回滚即 revert 本 PR。 - 先看:`packages/plugins/plugin-security/src/objects/default-permission-sets.ts` 的 `organization_admin.systemPermissions` 一行;`objects/default-permission-sets.test.ts` 的两条持有者 pin;`automation-connector-connection-door.test.ts` 的"本组织放行、他组织拒绝"用例。 - 两分钟验证:`pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2 src/objects/default-permission-sets.test.ts`,预期全绿,持有者恰为 `admin_full_access`、`organization_admin`、`organization_admin_no_bypass` 三个。 - 需要你做的:无,席位落地。 ## What changes **The grant.** `organization_admin`'s `systemPermissions` gains `manage_connector_connections` (`default-permission-sets.ts`). The set is declared once; `deriveWallLessOrgAdmin` spreads the parent's declaration and drops only the wildcard superuser bits, so `organization_admin_no_bypass` carries the capability from that one line. It is never written a second time. `auto-org-admin-grant.ts` already grants one of the two sets, per organization and by posture, to every member whose role contains `owner` or `admin`, so those members hold the capability in that organization. **Scope.** The capability is `org`-scoped. Core's resolver applies an organization-scoped grant only in the organization the grant row names (`grantAppliesInTenant` in `resolve-authz-context.ts`), and the connection door hands the service the caller's own `tenantId`. So an organization admin of `org_a` connects `org_a`'s instances only. In an organization where they are a plain member they hold the member baseline, which does not carry the capability, and the door refuses them. **Unchanged.** `member_default`, `viewer_readonly` and every other shipped set hold the capability only by explicit grant. No other capability of any set moves. Nothing under `packages/spec` changes. **The agent golden.** `builtin-positions.boot.test.ts` and `grant-permission-set-name.equivalence.test.ts` build the agent row from the organization admin's resolved envelope through `assembleExecutionContext` (`packages/core/src/security/assemble-execution-context.ts`). For an OAuth agent with `actions:execute` (`delegatesActions`), that function passes the human's `systemPermissions` through unchanged. The agent's golden gains the capability through that path only. No line names an agent. **Docs.** `content/docs/automation/connectors.mdx`: the connect paragraph now says platform administrators and organization administrators hold the capability by default, and anyone else receives it through a permission set. ## Tests - `objects/default-permission-sets.test.ts`: the holders of `manage_connector_connections` across the shipped sets are exactly `admin_full_access`, `organization_admin`, `organization_admin_no_bypass`. `member_default` and `viewer_readonly` exist and do not hold it. A second pin asserts the no-bypass variant's `systemPermissions` equal its parent's, so the capability rides the derivation and is not a second list. - `automation-connector-connection-door.test.ts` (runtime), for each of GET / POST / DELETE / callback: an organization admin on the shipped grant is admitted with 200, and the service member is called once with `tenantId: 'org_a'`. The same person active in `org_b`, holding the member baseline there, is refused with 403 `PERMISSION_DENIED`, the message names the capability, and no service member runs. The transcribed `ORG_ADMIN_GRANT` now carries the capability. - Grant goldens: `resolve-authz-grant-set-by-name.golden.test.ts`, `grant-readers-by-name.golden.test.ts`, `grant-permission-set-name.equivalence.test.ts` and `builtin-positions.boot.test.ts`. Each gains the capability on the organization admin's envelope inside the organization, and on the agent acting for them. The resolver golden's `noOrganization` envelope stays `systemPermissions: []`. - `packages-capability-gate.test.ts`: the commit-trail refusal case named "the organization admin's full grant" now lists the full grant. It is still refused, because that door asks `studio.access`. Whole packages on `29c35857`: plugin-security `Test Files 238 passed (238)`, `Tests 5126 passed | 45 skipped (5171)`. runtime `Test Files 496 passed (496)`, `Tests 8972 passed | 19 skipped (8991)`. <details><summary>Ablations</summary> Both mutations went through `scripts/ablation-replace.mjs`. The anchor landed on disk (count 1 → 0), and the file was restored with the blob equal to HEAD and `git diff HEAD` empty. Both suites import their subjects from `src`, so no dist was involved. | mutation | run | red | |:--|:--|:--| | drop `'manage_connector_connections'` from `organization_admin.systemPermissions` (`default-permission-sets.ts`) | the five plugin-security files above | 17 / 93: the holders pin, all 8 boot cases, 2 equivalence, 3 grant-readers, 3 resolver goldens | | the connection gate never refuses (`refuseUngrantedConnectorConnectionAccess` call replaced by `undefined`, `automation.ts`) | `automation-connector-connection-door.test.ts -t 'refused in another'` | 4 / 4: `expected 200 to be 403` on each verb | The derivation-equality pin stays green under the first mutation, as expected, since both variants lose the capability together. The holders pin is the one that reds there. </details> ## Acceptance notes - **ADR-0143 D5 line, split out.** CODEOWNERS makes `docs/adr/**` Tier H. The line is a record of ruling B and changes no code path, so it ships separately rather than holding this PR. Patch for the seat: `server-seat-1/r2026-10-31/out/28427-adr-0143-d5.patch`. The landing of that PR closes the card. - **Three `packages/spec` comments ruling B makes false, left untouched by the seat's ruling (spec lane's line, handed over after this merges):** `packages/spec/src/security/capabilities.ts:103-110`, `packages/spec/src/identity/eval-user.zod.ts:166-169`, `packages/spec/src/identity/platform-admin-capabilities.test.ts:52-54`. Each says the organization administrator holds the capability only by explicit grant. - **Agents acting for an organization admin reach the door.** The connection door reads only `systemPermissions`, and an OAuth agent with `actions:execute` carries its human's list. So an agent acting for an organization admin can now read, begin and revoke that organization's connection, as an agent acting for a platform admin already can on `main`. Ruling B did not address agents. Whether the door should refuse an agent principal is a separate question for the seat. - `content/docs/permissions/permission-sets.mdx`'s summary row for `organization_admin` names a subset of its capabilities (it already leaves out `manage_mailboxes`), so it is not edited here. - Branch `claude/project-thread-jtzyzv` is the card thread's platform-assigned branch; claim 6545275282 names it. ## Verification Local, on `29c35857`: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 92 families. Each was run with stdin from `/dev/null`, and its exit was captured by redirect before any pipe. All 92 exit 0. Three of them (`check:skill-examples`, `check:dual-build-cjs-loads`, `check:i18n`) first exited 3, PREREQUISITE NOT MET, and were re-run after a full `pnpm build`. `--ran`: `92 derived famil(ies) accounted for — 92 run, 0 NOT-MEASURED`, exit 0. Scoped lint, the root `lint` script's eslint over `packages/plugins/plugin-security packages/runtime` with `--no-inline-config`: exit 0. `pnpm --filter @objectstack/plugin-security --filter @objectstack/runtime run typecheck`: exit 0. --- _Generated by [Claude Code](https://claude.ai/code/session_01L9qibxkKcPULsq3fo4JFAX)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 16e7a80 commit 8e87403

10 files changed

Lines changed: 92 additions & 33 deletions
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
feat(plugin-security): organization administrators hold `manage_connector_connections` by default, so an organization's owners and admins can connect its OAuth2 connectors without a hand-bound permission set (ADR-0143 D5, ruling B)
6+
7+
Clause-②: yes (widening: the shipped `organization_admin` set, and `organization_admin_no_bypass` through its derivation, now admit their holders at the connector connection door; no other set changes)
8+
Metadata-protocol: none
9+
10+
The connector connection door (`GET` / `POST` / `DELETE /api/v1/automation/connectors/:name/connection` and the OAuth callback) asks the organization-scoped `manage_connector_connections` capability. Until now only platform administrators held it by default, so an organization owner or admin was refused until someone granted it to them through a permission set.
11+
12+
- **Who gains it.** Every member whose organization role contains `owner` or `admin`, through the `organization_admin` set the platform already grants them per organization: `organization_admin` under a walled posture, `organization_admin_no_bypass` on a wall-less one. The no-bypass variant inherits the capability from `organization_admin`'s declaration; it is not a second list.
13+
- **What it reaches.** Their own organization's connection only. The grant is scoped to the organization it was made in, and the door acts on the caller's active organization, so an organization admin who is a plain member elsewhere is still refused there. No token is ever shown to the holder.
14+
- **Unchanged.** `member_default`, `viewer_readonly` and every other shipped set still hold the capability only by an explicit grant. No other capability of any set changes. An agent acting on behalf of an organization administrator resolves the administrator's system permissions as before, so it gains this one with them.

‎content/docs/automation/connectors.mdx‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -532,9 +532,10 @@ a rotating refresh token is never spent twice.
532532
- **`authorization_code`** needs an administrator to **connect** it, once per
533533
organization. Until then the instance is **degraded** "not connected" (never
534534
a failed boot). The connect is three calls, each needing the
535-
organization-scoped `manage_connector_connections` capability (platform
536-
administrators hold it by default; grant it to anyone else, an organization
537-
administrator included, through a permission set):
535+
organization-scoped `manage_connector_connections` capability. Platform
536+
administrators and organization administrators hold it by default, so an
537+
organization's owners and admins can connect its vendors from the start;
538+
grant it to anyone else through a permission set:
538539

539540
```http
540541
POST /api/v1/automation/connectors/:name/connection → { authorizationUrl, expiresAt }

‎packages/plugins/plugin-security/src/builtin-positions.boot.test.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -659,7 +659,7 @@ const GRANT_GOLDEN: Record<'single' | 'isolated', unknown> = {
659659
permissions: ['mcp_agent_data_read'],
660660
positions: [],
661661
principalKind: 'agent',
662-
systemPermissions: ['manage_mailboxes', 'manage_org_users', 'setup.access', 'setup.write'],
662+
systemPermissions: ['manage_connector_connections', 'manage_mailboxes', 'manage_org_users', 'setup.access', 'setup.write'],
663663
},
664664
member: {
665665
accessible_org_ids: ['org_eq'],
@@ -677,7 +677,7 @@ const GRANT_GOLDEN: Record<'single' | 'isolated', unknown> = {
677677
permissions: ['field_default', 'member_default', 'organization_admin_no_bypass'],
678678
positions: ['everyone', 'org_owner'],
679679
posture: 'TENANT_ADMIN',
680-
systemPermissions: ['manage_mailboxes', 'manage_org_users', 'setup.access', 'setup.write'],
680+
systemPermissions: ['manage_connector_connections', 'manage_mailboxes', 'manage_org_users', 'setup.access', 'setup.write'],
681681
},
682682
platformAdmin: {
683683
accessible_org_ids: [],
@@ -709,7 +709,7 @@ const GRANT_GOLDEN: Record<'single' | 'isolated', unknown> = {
709709
permissions: ['mcp_agent_data_read'],
710710
positions: [],
711711
principalKind: 'agent',
712-
systemPermissions: ['manage_mailboxes', 'manage_org_users', 'setup.access', 'setup.write'],
712+
systemPermissions: ['manage_connector_connections', 'manage_mailboxes', 'manage_org_users', 'setup.access', 'setup.write'],
713713
},
714714
member: {
715715
accessible_org_ids: ['org_eq'],
@@ -727,7 +727,7 @@ const GRANT_GOLDEN: Record<'single' | 'isolated', unknown> = {
727727
permissions: ['field_default', 'member_default', 'organization_admin'],
728728
positions: ['everyone', 'org_owner'],
729729
posture: 'TENANT_ADMIN',
730-
systemPermissions: ['manage_mailboxes', 'manage_org_users', 'setup.access', 'setup.write'],
730+
systemPermissions: ['manage_connector_connections', 'manage_mailboxes', 'manage_org_users', 'setup.access', 'setup.write'],
731731
},
732732
platformAdmin: {
733733
accessible_org_ids: [],

‎packages/plugins/plugin-security/src/grant-permission-set-name.equivalence.test.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ const GOLDEN: Record<'single' | 'isolated', unknown> = {
257257
permissions: ['mcp_agent_data_read'],
258258
positions: [],
259259
principalKind: 'agent',
260-
systemPermissions: ['manage_mailboxes', 'manage_org_users', 'setup.access', 'setup.write'],
260+
systemPermissions: ['manage_connector_connections', 'manage_mailboxes', 'manage_org_users', 'setup.access', 'setup.write'],
261261
},
262262
member: {
263263
accessible_org_ids: ['org_eq'],
@@ -275,7 +275,7 @@ const GOLDEN: Record<'single' | 'isolated', unknown> = {
275275
permissions: ['member_default', 'organization_admin_no_bypass'],
276276
positions: ['everyone', 'org_owner'],
277277
posture: 'TENANT_ADMIN',
278-
systemPermissions: ['manage_mailboxes', 'manage_org_users', 'setup.access', 'setup.write'],
278+
systemPermissions: ['manage_connector_connections', 'manage_mailboxes', 'manage_org_users', 'setup.access', 'setup.write'],
279279
},
280280
platformAdmin: {
281281
accessible_org_ids: [],
@@ -296,7 +296,7 @@ const GOLDEN: Record<'single' | 'isolated', unknown> = {
296296
permissions: ['mcp_agent_data_read'],
297297
positions: [],
298298
principalKind: 'agent',
299-
systemPermissions: ['manage_mailboxes', 'manage_org_users', 'setup.access', 'setup.write'],
299+
systemPermissions: ['manage_connector_connections', 'manage_mailboxes', 'manage_org_users', 'setup.access', 'setup.write'],
300300
},
301301
member: {
302302
accessible_org_ids: ['org_eq'],
@@ -314,7 +314,7 @@ const GOLDEN: Record<'single' | 'isolated', unknown> = {
314314
permissions: ['member_default', 'organization_admin'],
315315
positions: ['everyone', 'org_owner'],
316316
posture: 'TENANT_ADMIN',
317-
systemPermissions: ['manage_mailboxes', 'manage_org_users', 'setup.access', 'setup.write'],
317+
systemPermissions: ['manage_connector_connections', 'manage_mailboxes', 'manage_org_users', 'setup.access', 'setup.write'],
318318
},
319319
platformAdmin: {
320320
accessible_org_ids: [],

‎packages/plugins/plugin-security/src/grant-readers-by-name.golden.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ const SINGLE: unknown = {
369369
permissions: ['member_default', 'organization_admin_no_bypass'],
370370
positions: ['everyone', 'org_owner'],
371371
posture: 'TENANT_ADMIN',
372-
systemPermissions: ['manage_mailboxes', 'manage_org_users', 'setup.access', 'setup.write'],
372+
systemPermissions: ['manage_connector_connections', 'manage_mailboxes', 'manage_org_users', 'setup.access', 'setup.write'],
373373
tabPermissions: null,
374374
},
375375
reconcile: { action: 'noop' },
@@ -462,7 +462,7 @@ const WALLED: unknown = {
462462
permissions: ['member_default', 'organization_admin'],
463463
positions: ['everyone', 'org_owner'],
464464
posture: 'TENANT_ADMIN',
465-
systemPermissions: ['manage_mailboxes', 'manage_org_users', 'setup.access', 'setup.write'],
465+
systemPermissions: ['manage_connector_connections', 'manage_mailboxes', 'manage_org_users', 'setup.access', 'setup.write'],
466466
tabPermissions: null,
467467
},
468468
reconcile: { action: 'noop' },

‎packages/plugins/plugin-security/src/objects/default-permission-sets.test.ts‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,8 @@ describe('sys_comment_reaction delete is ownership-shaped (#22566)', () => {
392392
* platform and organization administrators hold the mailbox administrator
393393
* capability by default). And `manage_connector_connections` (ADR-0143 D5 —
394394
* platform administrators hold the connector connection capability by default;
395-
* the organization administrator's sets withhold it, pinned below).
395+
* so do both organization-administrator sets since ruling B, record
396+
* 6545223261, pinned below).
396397
*
397398
* The set's `description` is admin-facing copy, not capability, so it is left
398399
* out of the comparison (its shape is pinned by
@@ -448,11 +449,28 @@ describe('admin_full_access imports the kernel capability declaration unchanged
448449
expect(holders).toEqual(['admin_full_access']);
449450
});
450451

451-
it('[ADR-0143 D5] no other shipped set carries the connector connection capability: the organization administrator holds it only by explicit grant', () => {
452+
it('[ADR-0143 D5] the connector connection capability ships with the platform administrator and both organization-administrator sets, and no other', () => {
453+
// Ruling B (record 6545223261): the organization administrator connects
454+
// their organization's OAuth2 vendors from day one. Every other shipped
455+
// set, the member and read-only baselines included, holds it only by an
456+
// explicit grant.
452457
const holders = (defaultPermissionSets as any[])
453458
.filter((s) => (s.systemPermissions ?? []).includes('manage_connector_connections'))
454459
.map((s) => s.name);
455-
expect(holders).toEqual(['admin_full_access']);
460+
expect(holders).toEqual(['admin_full_access', 'organization_admin', 'organization_admin_no_bypass']);
461+
for (const withheld of ['member_default', 'viewer_readonly']) {
462+
expect(setByName(withheld)).toBeDefined();
463+
expect(setByName(withheld).systemPermissions ?? []).not.toContain('manage_connector_connections');
464+
}
465+
});
466+
467+
it('[ADR-0143 D5] the wall-less organization administrator inherits the capability through the derivation, not a second literal', () => {
468+
// `deriveWallLessOrgAdmin` copies the parent's system permissions
469+
// verbatim; the two lists are one list, so the grant reaches both
470+
// variants from the parent's declaration alone.
471+
expect(setByName('organization_admin_no_bypass').systemPermissions).toEqual(
472+
setByName('organization_admin').systemPermissions,
473+
);
456474
});
457475

458476
it('the imported spec constant is the declaration content — no local fork', () => {

‎packages/plugins/plugin-security/src/objects/default-permission-sets.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,15 @@ const baseDefaultPermissionSets: PermissionSet[] = [
437437
// `manage_mailboxes` (ruling 6116489638, Q4 → A): an organization
438438
// administrator reads its organization's mailboxes and their unassociated
439439
// pool; the organization wall bounds it to its own organization's rows.
440-
systemPermissions: ['manage_org_users', 'setup.access', 'setup.write', 'manage_mailboxes'],
440+
//
441+
// `manage_connector_connections` ([ADR-0143] D5, ruling B on the
442+
// capability's card, record 6545223261): the organization administrator is
443+
// who connects their organization's OAuth2 vendors, so the shipped set holds
444+
// it from day one instead of waiting on a hand-bound permission set. The
445+
// capability is `org`-scoped and this set is granted per organization, so
446+
// the holder connects their own organization's instances only. The no-bypass
447+
// variant inherits it through `deriveWallLessOrgAdmin`: never copy it there.
448+
systemPermissions: ['manage_org_users', 'setup.access', 'setup.write', 'manage_mailboxes', 'manage_connector_connections'],
441449
// [#21237] Keeps the identity object's `Admin` group for an org admin, who
442450
// resolves `member_default` too; the derived no-bypass variant carries it
443451
// over. See `identityAdminFieldSecurity`.

‎packages/plugins/plugin-security/src/resolve-authz-grant-set-by-name.golden.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,7 @@ const SINGLE: unknown = {
371371
],
372372
posture: 'TENANT_ADMIN',
373373
systemPermissions: [
374+
'manage_connector_connections',
374375
'manage_mailboxes',
375376
'manage_org_users',
376377
'setup.access',
@@ -561,6 +562,7 @@ const WALLED: unknown = {
561562
],
562563
posture: 'TENANT_ADMIN',
563564
systemPermissions: [
565+
'manage_connector_connections',
564566
'manage_mailboxes',
565567
'manage_org_users',
566568
'setup.access',

‎packages/runtime/src/domains/automation-connector-connection-door.test.ts‎

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,10 @@ function boot(opts: { posture?: 'single' | 'group' | 'isolated'; service?: Recor
5555
return { dispatcher: new HttpDispatcher(kernel as never), get, begin, complete, revoke };
5656
}
5757

58-
/** A caller holding exactly `systemPermissions`, in organization `org_a`. */
59-
const HOLDING = (systemPermissions: string[], userId = 'u_admin'): HttpProtocolContext => ({
58+
/** A caller holding exactly `systemPermissions`, in organization `tenantId` (default `org_a`). */
59+
const HOLDING = (systemPermissions: string[], userId = 'u_admin', tenantId = 'org_a'): HttpProtocolContext => ({
6060
request: {},
61-
executionContext: { userId, tenantId: 'org_a', positions: [], permissions: [], systemPermissions },
61+
executionContext: { userId, tenantId, positions: [], permissions: [], systemPermissions },
6262
} as HttpProtocolContext);
6363

6464
/** The connection administrator: the capability, and nothing else. */
@@ -68,11 +68,22 @@ const MEMBER = (): HttpProtocolContext => HOLDING([], 'u_member');
6868

6969
/**
7070
* The organization administrator's shipped grant (`organization_admin` in
71-
* plugin-security's default sets), transcribed: it does not carry the
72-
* connection capability, which an organization administrator holds only by
73-
* explicit grant.
71+
* plugin-security's default sets, which `organization_admin_no_bypass`
72+
* inherits), transcribed: since ruling B (record 6545223261) it carries the
73+
* connection capability. The source is pinned in plugin-security's
74+
* `default-permission-sets.test.ts`; this transcription pins what the door does
75+
* with it.
7476
*/
75-
const ORG_ADMIN_GRANT = ['manage_org_users', 'setup.access', 'setup.write', 'manage_mailboxes'];
77+
const ORG_ADMIN_GRANT = ['manage_org_users', 'setup.access', 'setup.write', 'manage_mailboxes', 'manage_connector_connections'];
78+
79+
/**
80+
* The same organization administrator, active in ANOTHER organization where they
81+
* are a plain member. Their `organization_admin` grant is organization-scoped,
82+
* and core's resolver applies such a grant only in the organization it names
83+
* (`grantAppliesInTenant`), so what reaches the door there is the member
84+
* baseline: no system permission at all.
85+
*/
86+
const ORG_ADMIN_IN_ANOTHER_ORGANIZATION = (): HttpProtocolContext => HOLDING([], 'u_orgadmin', 'org_b');
7687

7788
const ANONYMOUS = (): HttpProtocolContext => ({ request: {}, executionContext: {} } as HttpProtocolContext);
7889

@@ -131,18 +142,23 @@ describe('ADR-0143 D5 — connector connection door: who may reach it', () => {
131142
}
132143
});
133144

134-
it.each(EVERY_MEMBER)('%s: an organization administrator on the shipped grant is refused; granted the capability, admitted', async (_verb, call, member) => {
145+
it.each(EVERY_MEMBER)('%s: an organization administrator on the shipped grant is admitted for their own organization, and refused in another', async (_verb, call, member) => {
146+
const admitted = boot();
147+
const allowed = await call(admitted, HOLDING(ORG_ADMIN_GRANT, 'u_orgadmin'));
148+
expect(statusOf(allowed.response)).toBe(200);
149+
expect(member(admitted)).toHaveBeenCalledTimes(1);
150+
// The door acts on the caller's own organization: the service is handed
151+
// `org_a` and nothing that could name another.
152+
expect(member(admitted)).toHaveBeenCalledWith(
153+
expect.objectContaining({ context: expect.objectContaining({ userId: 'u_orgadmin', tenantId: 'org_a' }) }),
154+
);
155+
135156
const refused = boot();
136-
const denied = await call(refused, HOLDING(ORG_ADMIN_GRANT));
157+
const denied = await call(refused, ORG_ADMIN_IN_ANOTHER_ORGANIZATION());
137158
expect(statusOf(denied.response)).toBe(403);
138159
expect(codeOf(denied.response)).toBe('PERMISSION_DENIED');
160+
expect(messageOf(denied.response)).toContain('`manage_connector_connections`');
139161
for (const m of [refused.get, refused.begin, refused.complete, refused.revoke]) expect(m).not.toHaveBeenCalled();
140-
141-
const admitted = boot();
142-
const allowed = await call(admitted, HOLDING([...ORG_ADMIN_GRANT, 'manage_connector_connections']));
143-
expect(statusOf(allowed.response)).toBe(200);
144-
expect(member(admitted)).toHaveBeenCalledTimes(1);
145-
expect(member(admitted)).toHaveBeenCalledWith(expect.objectContaining({ context: expect.objectContaining({ tenantId: 'org_a' }) }));
146162
});
147163

148164
it('the system context passes without the capability; an absent capability list is refused (fail-closed)', async () => {

‎packages/runtime/src/domains/packages-capability-gate.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ describe('[#23546] /packages/:id/commits — the commit trail demands studio.acc
341341
for (const [label, caps] of [
342342
['zero-capability', []],
343343
['setup.access-only (the organization admin\'s capability)', ['setup.access']],
344-
['the organization admin\'s full grant (setup.access, setup.write, manage_org_users, manage_mailboxes)', ['setup.access', 'setup.write', 'manage_org_users', 'manage_mailboxes']],
344+
['the organization admin\'s full grant (setup.access, setup.write, manage_org_users, manage_mailboxes, manage_connector_connections)', ['setup.access', 'setup.write', 'manage_org_users', 'manage_mailboxes', 'manage_connector_connections']],
345345
['WRITE-only (manage_metadata), which this door never admitted', ['manage_metadata']],
346346
['setup.access + manage_metadata', ['setup.access', 'manage_metadata']],
347347
] as const) {

0 commit comments

Comments
 (0)