Repository navigation
Commit 8e87403
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
File tree
- .changeset
- content/docs/automation
- packages
- plugins/plugin-security/src
- objects
- runtime/src/domains
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
532 | 532 | | |
533 | 533 | | |
534 | 534 | | |
535 | | - | |
536 | | - | |
537 | | - | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
538 | 539 | | |
539 | 540 | | |
540 | 541 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
659 | 659 | | |
660 | 660 | | |
661 | 661 | | |
662 | | - | |
| 662 | + | |
663 | 663 | | |
664 | 664 | | |
665 | 665 | | |
| |||
677 | 677 | | |
678 | 678 | | |
679 | 679 | | |
680 | | - | |
| 680 | + | |
681 | 681 | | |
682 | 682 | | |
683 | 683 | | |
| |||
709 | 709 | | |
710 | 710 | | |
711 | 711 | | |
712 | | - | |
| 712 | + | |
713 | 713 | | |
714 | 714 | | |
715 | 715 | | |
| |||
727 | 727 | | |
728 | 728 | | |
729 | 729 | | |
730 | | - | |
| 730 | + | |
731 | 731 | | |
732 | 732 | | |
733 | 733 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
257 | 257 | | |
258 | 258 | | |
259 | 259 | | |
260 | | - | |
| 260 | + | |
261 | 261 | | |
262 | 262 | | |
263 | 263 | | |
| |||
275 | 275 | | |
276 | 276 | | |
277 | 277 | | |
278 | | - | |
| 278 | + | |
279 | 279 | | |
280 | 280 | | |
281 | 281 | | |
| |||
296 | 296 | | |
297 | 297 | | |
298 | 298 | | |
299 | | - | |
| 299 | + | |
300 | 300 | | |
301 | 301 | | |
302 | 302 | | |
| |||
314 | 314 | | |
315 | 315 | | |
316 | 316 | | |
317 | | - | |
| 317 | + | |
318 | 318 | | |
319 | 319 | | |
320 | 320 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
369 | 369 | | |
370 | 370 | | |
371 | 371 | | |
372 | | - | |
| 372 | + | |
373 | 373 | | |
374 | 374 | | |
375 | 375 | | |
| |||
462 | 462 | | |
463 | 463 | | |
464 | 464 | | |
465 | | - | |
| 465 | + | |
466 | 466 | | |
467 | 467 | | |
468 | 468 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
392 | 392 | | |
393 | 393 | | |
394 | 394 | | |
395 | | - | |
| 395 | + | |
| 396 | + | |
396 | 397 | | |
397 | 398 | | |
398 | 399 | | |
| |||
448 | 449 | | |
449 | 450 | | |
450 | 451 | | |
451 | | - | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
452 | 457 | | |
453 | 458 | | |
454 | 459 | | |
455 | | - | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
456 | 474 | | |
457 | 475 | | |
458 | 476 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
437 | 437 | | |
438 | 438 | | |
439 | 439 | | |
440 | | - | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
441 | 449 | | |
442 | 450 | | |
443 | 451 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
371 | 371 | | |
372 | 372 | | |
373 | 373 | | |
| 374 | + | |
374 | 375 | | |
375 | 376 | | |
376 | 377 | | |
| |||
561 | 562 | | |
562 | 563 | | |
563 | 564 | | |
| 565 | + | |
564 | 566 | | |
565 | 567 | | |
566 | 568 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
55 | 55 | | |
56 | 56 | | |
57 | 57 | | |
58 | | - | |
59 | | - | |
| 58 | + | |
| 59 | + | |
60 | 60 | | |
61 | | - | |
| 61 | + | |
62 | 62 | | |
63 | 63 | | |
64 | 64 | | |
| |||
68 | 68 | | |
69 | 69 | | |
70 | 70 | | |
71 | | - | |
72 | | - | |
73 | | - | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
74 | 76 | | |
75 | | - | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
76 | 87 | | |
77 | 88 | | |
78 | 89 | | |
| |||
131 | 142 | | |
132 | 143 | | |
133 | 144 | | |
134 | | - | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
135 | 156 | | |
136 | | - | |
| 157 | + | |
137 | 158 | | |
138 | 159 | | |
| 160 | + | |
139 | 161 | | |
140 | | - | |
141 | | - | |
142 | | - | |
143 | | - | |
144 | | - | |
145 | | - | |
146 | 162 | | |
147 | 163 | | |
148 | 164 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
341 | 341 | | |
342 | 342 | | |
343 | 343 | | |
344 | | - | |
| 344 | + | |
345 | 345 | | |
346 | 346 | | |
347 | 347 | | |
| |||
0 commit comments