Skip to content

Commit 94842ba

Browse files
feat(core,plugin-security)!: 18.x step 7 part 7a-2 — a grant applies only in its own organization; the organization-less grant readers retire (#24872)
Part of #24583 — step 7, part 7a-2: the organization-less readers of the authorization resolver and plugin-security retire. The card stays open for the other step-7 parts (7a-1, 7a-3, 7b-1, 7b-2, 7b-3, 7c). Clause-②: yes (narrowing) — measured: a stored grant row with no `organization_id` no longer applies in any resolution, and no longer anchors platform standing. ## What changes **`@objectstack/core` — `resolve-authz-context.ts`** - `grantAppliesInTenant(org, tenantId)` is now `org !== null && org === tenantId`. A `sys_user_position` (§4) or `sys_user_permission_set` (§6) row applies only in its own organization; with no active organization, no stored grant applies. §4 now reads the row through `grantOrganizationOf`, so a blank value is normalized to null like everywhere else. - `grantOrganizationOf` stays as the normalizer; `null` (NULL, absent or blank) confers nothing. - `grantAnchorsPlatformStanding` loses its organization-less arm: under `single`, only the Default Organization's `admin_full_access` grant anchors `platform_admin`. That is the grant the first-user promotion already writes. It still applies in every organization, or with none active. Walled postures are unchanged (no grant anchors there). - `mayApply` / `applyingGrants` keep their shape and now read the narrowed rule; their docblocks are corrected. **`@objectstack/plugin-security`** - `delegated-admin-gate.ts`: `holdingTakesEffectIn` counts a holding only when it belongs to the caller's active organization. That is exactly what the resolver grants, so an organization-less holding, or a caller with no active organization, can no longer self-delegate. The org-admin removal of an organization-less row no longer claims the row "applies in every organization". - `bootstrap-platform-admin.ts`: `isAnchorHumanHolder` matches the Default Organization's grant only. Leg A asks once, for the Default Organization's id as `organization_id`, never for `null`. When no Default Organization can be named (a wall, or none to name), the holder scan reads no grant at all. The walled "no owner email" error loses its sentence about a legacy unscoped grant row, which can no longer be found. - `organization-scope.ts`: `resolveOwnOrganizationRow` returns `{ own }`; the residue half is gone. - `grant-permission-set-name.ts`: the dead export `grantOrganizationOf` is deleted. It was not re-exported from the package and had no importer. **Two text corrections this change made false** (a declared extension of the claim's file surface, text only): - `core/src/security/admin-standing-surface.ts`: the module docblock and the `sys_user_permission_set` reason no longer name the organization-less row as an anchor. - `runtime/src/domains/automation.ts`: under `single`, the run-lifecycle refusal named "an unscoped `admin_full_access` grant" as the remedy. It now names the Default Organization's grant. **Pins and fixtures.** Every pin that asserted the retired reading is rewritten to the new rule, each with a positive control in the same test. Every fixture that relied on an organization-less grant to exercise something else is re-stamped into an organization (with the resolution run in it) or into the Default Organization. No assertion is loosened, no test is skipped, and no test is deleted. These live in 16 packages, because the readers' pins reach that far: core, plugin-security, plugin-auth, runtime, rest, cloud-connection, service-datasource, service-settings, service-automation, plugin-hono-server, plugin-approvals and plugin-sharing (test files only), and dogfood. Five suites gained a new negative pin, "an organization-less grant confers / anchors nothing". ## Premise check — the retired readers are dead after certification | tree | measurement | result | |:--|:--|:--| | fresh `single` install (`pnpm dev -- --fresh`, merged head) | `sys_user_permission_set` / `sys_user_position` rows with NULL or blank `organization_id` | 0 of 2 / 0 of 5; the one `admin_full_access` grant is owned by the Default Organization | | walled (`isolated`) `os serve`, real Organizations runtime | same tables | 0 rows at all; no grant row is written under a wall | | ceremonied 18.0 fixture (R2 harness) | `expectCertified`: zero NULL `organization_id` rows; the fixture's admin then signs in and reads | green | | composition with no organization object (`no-organization-object`) | does any shipped composition reach it? | no — `os serve` registers SecurityPlugin only inside the AuthPlugin block (which ships `sys_organization`); no example, template or `os init` output composes plugin-security without it. In such an embedding, grants stay organization-less and now confer nothing (the changeset says so). | ## Verification (head `21b172681`: `origin/main` at `81120caac` merged in, carrying #24681 `12d14765c`, 7a-1 `7c2eab929`, 7c `bd6dab009`, 7a-3 `7cacad534` and 7b-3 `df8bb40a0`) The full set below ran on `a62067ad4` (main with #24681 and 7a-1). The two later merges (7c, 7a-3, 7b-3 and the commits around them) were re-checked on the packages they touch: plugin-sharing, plugin-approvals, plugin-auth, runtime, service-settings, plugin-hono-server and CLI unit, the merged sharing and settings dogfood files, spec `check:generated`, R1, R4 live, and the harness. All green on `21b172681`. | reading | result | |:--|:--| | R1 — fresh `single` boot, random port | health 200, seeded-admin sign-in 200, session carries `platform_admin`, 0 error lines | | R2 + R3 — `upgrade-18x-harness` + `organization-ownership-boot-refusal-order` dogfood | 2 files / 12 passed (on every merged head) | | R4 — `isolated` `os serve`, throwaway app declaring the real `@objectstack/organizations` | banner reached, `Organizations` mounted, discovery 200, 0 error lines; a non-owner admin resolves `everyone` only | | R4 — step 4's 7 walled e2e cases (`OS_TEST_TIERS=nightly`) | 2 files / 7 passed | | package suites, whole | core 2680 · plugin-security 4056 · plugin-auth 3264 · runtime 7594 · rest 6424 · plugin-approvals 1170 · plugin-sharing 1201 · service-automation 2604 · plugin-hono-server 422 · cloud-connection 614 · service-datasource 852 · service-settings 899 · organizations 166 · trigger-api 55 · trigger-record-change 114 · trigger-schedule 174 · CLI `--project unit` 4635 — 0 failed | | dogfood, the 71 files that touch grant tables | 71 files / 696 passed, 1 skipped (one earlier full run went red once on `showcase-crud-persona-matrix`: its probe picked a paid invoice. That is order-dependent and unrelated, it passed alone, and main fixed it in `08d201a80`) | | `dispatch-gates --commands` | 79 derived / 79 run, each with its exit code; 79 exit 0 (`check-plugin-teardown-shape --self-test` needed its pinned commit fetched into this shallow clone, then exit 0) | | typecheck of the touched packages | exit 0 for all 13 touched packages (core, plugin-security, plugin-auth, runtime, rest, cloud-connection, service-datasource, service-settings, service-automation, plugin-hono-server, plugin-approvals, plugin-sharing, dogfood). `runtime/test-typecheck-debt.json` was re-recorded: one pinned TS7053 signature was renamed by a fixture's added table; the count is unchanged | | `check-platform-checklist --changed-from origin/main` | OK — 12 anchors judged, 0 lost | **Reverse verification (ablation), core resolver, source path (core's suite imports its own `src`):** - leg 1: `grantAppliesInTenant` back to `organizationId === null || …` → 8 failed / 145 passed across the three resolver suites. Exactly the organization-less and no-tenant pins went red. Restore proven by blob hash. - leg 2: `grantAnchorsPlatformStanding` given back its `null` arm → 5 failed / 170 passed. Exactly the anchor pins went red. Restore proven by blob hash. ## Acceptance notes - **Size.** 100+ files and more than 3,000 changed lines (`check-governed-merges --branch`: 3,341). Almost all of it is test fixtures across the 16 packages that pin these readers. No governed path is touched, but the size rule applies: this lands by an authorized approval or the maintainer's merge. - **Out-of-surface organization-less readers (not edited, for the seat):** - `plugin-auth/src/last-admin-guard.ts` `anchorOwnerTest` (about line 1028) still counts an organization-less `admin_full_access` grant as a platform-admin holder; - `plugin-auth/src/ensure-default-organization.ts` (about lines 505–509) still asks `organization_id: null` when it picks the Default Organization's owner; - `plugin-security/src/explain-engine.ts` `collectGrantProvenance` reads holdings with no organization filter, so it can annotate a row the resolver no longer applies. On a certified or fresh database none of them is reachable, because no such row exists. - **plugin-sharing** (7a-3, not touched): `assertResolvableAdminScope` (`sharing-rule-service.ts`) is no longer reachable over HTTP by any grant. Its comment still describes the old reading. - **Posture source split (pre-existing):** the resolver reads the walled posture from `OS_TENANCY_POSTURE`, while cloud-connection's install door reads it from the `tenancy` service. The organization-less grant used to mask a disagreement between them. - **Two fixture-design changes worth a look:** `service-automation/src/sys-user-default-row-scope.integration.test.ts` binds its `'*'` reader to the `everyone` anchor, because its "no organisation" legs cannot be reached by a stored grant any more. `service-datasource/src/__tests__/admin-routes-tenancy-posture-admission.test.ts` §4–§7 now pin that an organization-less API key passes the posture but holds nothing (403 rather than the posture's 401). ## 维护者速读(草稿) - **改了什么:** 授权解析器与 plugin-security 不再把「没有组织的授权行」当成全局授权:一行授权只在它自己的组织里生效;平台管理员身份只认 Default Organization 名下的 `admin_full_access` 授权。 - **为什么改:** 18.0 升级仪式已把所有无组织的行归属到组织并设为 NOT NULL,全新安装也不会再写这种行,旧的读法已是死代码(本 PR 在全新安装、隔离墙启动、仪式后数据库上实测为零行)。 - **风险与代价(含回滚):** 只影响「不注册组织对象却挂 plugin-security」的嵌入式组合——这类组合的授权将不再生效;官方组合(`os serve`)不受影响。改动大(3,341 行,几乎全是测试夹具改戳),回滚即 revert 本 PR。 - **席位意见:** - **你要做的:** 审阅后批准(超 3,000 行,需授权批准或由你合并);#24583 的其余部分落地后再发 18.0.0。 --- _Generated by [Claude Code](https://claude.ai/code/session_01QzqbxJk3oD6sqe47Q84ET8)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 19484af commit 94842ba

102 files changed

Lines changed: 2240 additions & 1133 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/core': minor
3+
'@objectstack/plugin-security': minor
4+
'@objectstack/runtime': patch
5+
---
6+
7+
feat(core,plugin-security)!: a grant applies only in its own organization — an organization-less grant row confers nothing, and the Default Organization's `admin_full_access` grant is the only platform-admin anchor
8+
9+
Clause-②: yes (narrowing)
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) No key of any metadata schema is removed, renamed or re-shaped, so there is nothing for `objectstack migrate meta` to rewrite and no tombstone. The data change this relies on is the operator's ownership ceremony, `os migrate organization-ownership`, which an upgraded database must have passed before it boots. -->
12+
13+
**BREAKING** for a stored grant row that carries no `organization_id` (`NULL`, absent or blank), shipped as `minor` under the repo's launch-window convention for breaking changes (ADR-0131 D10). After the ownership ceremony no such row exists: it attributes every one to an organization and makes the column NOT NULL, a server refuses a database it did not create or certify, and a fresh install that registers the organization object never writes one. So on a certified or fresh database nothing resolves differently.
14+
15+
- **`resolveUserAuthzGrants` / `resolveAuthzContext` (`@objectstack/core`).** A `sys_user_position` or `sys_user_permission_set` row applies only while its organization is the active one. FROM: a row with no organization was "global" and applied in every resolution, including one with no active organization. TO: it applies nowhere, and a resolution with no active organization gets no stored grant at all.
16+
- **Platform-admin standing.** FROM: under `single`, an organization-less `admin_full_access` grant anchored `platform_admin`, beside the Default Organization's. TO: only the Default Organization's grant anchors it (still in every organization, and with none active). Walled postures are unchanged: standing there comes from `OS_PLATFORM_OWNER_EMAIL` alone.
17+
- **`bootstrapPlatformAdmin` (`@objectstack/plugin-security`).** The existing-admin check reads only the Default Organization's grant. When no Default Organization can be named (a walled posture, or none to name), it reads no grant at all. The walled "no owner email declared" error drops its sentence about a legacy unscoped grant row.
18+
- **The delegated-administration gate.** A held position counts toward self-delegation only when its row belongs to the caller's active organization. An organization-less holding, or a caller with no active organization, holds nothing there, which is what the resolver grants. An organization administrator's removal of an organization-less row is refused as "not one of the organization you administer".
19+
- **The run-lifecycle refusal (`@objectstack/runtime`).** Under `single` it now names the Default Organization's `admin_full_access` grant as the remedy, where it named an unscoped one.
20+
- **What a lean composition sees.** A composition that registers no organization object (`sys_organization`, which plugin-auth ships) still stores grants without an organization, and those grants now confer nothing. No shipped composition does this: `os serve` registers the security plugin only together with plugin-auth.
21+
22+
**What an operator does.** Nothing beyond the upgrade itself: run `os migrate organization-ownership` before serving a database that 17.x or an 18.0 pre-release created. The server refuses such a database until you do. If an embedding composes `@objectstack/plugin-security` without plugin-auth, register the organization object so that grants carry an organization.

‎packages/cloud-connection/src/cloud-connection-auth-gate.pin.test.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,17 +267,22 @@ const APP = {
267267
};
268268
const LEDGER_FILE = 'com.acme.gated.json';
269269

270-
/** Every test user holds the authoring capability, so a refusal below is the gate's alone. */
270+
/**
271+
* Every test user holds the authoring capability, so a refusal below is the gate's alone:
272+
* the `admin_full_access` grant owned by the Default Organization, which applies with no
273+
* active organization.
274+
*/
271275
function grantRows(): Record<string, any[]> {
272276
return {
277+
sys_organization: [{ id: 'org_default', slug: 'default' }],
273278
sys_user: Object.keys(POSTURES).map((id) => ({ id, email: `${id}@acme.test` })),
274279
sys_member: [],
275280
sys_user_position: [],
276281
sys_position: [],
277282
sys_metadata_activation: [],
278283
sys_position_permission_set: [],
279284
sys_user_permission_set: Object.keys(POSTURES).map((id, i) => ({
280-
id: `ups${i}`, user_id: id, permission_set_id: 'ps1', permission_set: 'admin_full_access', organization_id: null,
285+
id: `ups${i}`, user_id: id, permission_set_id: 'ps1', permission_set: 'admin_full_access', organization_id: 'org_default',
281286
})),
282287
sys_permission_set: [{ id: 'ps1', name: 'admin_full_access', system_permissions: ['manage_metadata', 'studio.access', 'setup.access'] }],
283288
};

‎packages/cloud-connection/src/cloud-connection-plugin.test.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,19 +56,24 @@ function makeC(url: string, body?: any) {
5656
};
5757
}
5858

59+
/** The organization the sessions act in, and the one {@link setupAccessEngine}'s grant is owned by. */
60+
const ORG = 'o1';
61+
5962
const sessionAuth = (userId?: string) => ({
60-
api: { getSession: async () => (userId ? { user: { id: userId } } : null) },
63+
api: { getSession: async () => (userId ? { user: { id: userId }, session: { activeOrganizationId: ORG } } : null) },
6164
});
6265

6366
/**
6467
* [#24148] A data engine under which `userId` holds `setup.access` — what
6568
* `/status` serves its full answer behind. Real permission-set rows, read by
66-
* the shared resolver through `find`.
69+
* the shared resolver through `find`. The grant is owned by {@link ORG}, the
70+
* organization the session acts in: a grant applies only in its own
71+
* organization.
6772
*/
6873
const setupAccessEngine = (userId: string) => {
6974
const rows: Record<string, any[]> = {
7075
sys_user: [{ id: userId, email: `${userId}@example.test` }],
71-
sys_user_permission_set: [{ id: 'ups_1', user_id: userId, permission_set_id: 'ps_1', permission_set: 'setup_admin', organization_id: null }],
76+
sys_user_permission_set: [{ id: 'ups_1', user_id: userId, permission_set_id: 'ps_1', permission_set: 'setup_admin', organization_id: ORG }],
7277
sys_permission_set: [{ id: 'ps_1', name: 'setup_admin', system_permissions: ['setup.access'] }],
7378
};
7479
return bindCatalogFromTables({ find: async (object: string) => rows[object] ?? [] }, rows);

‎packages/cloud-connection/src/cloud-connection-runtime-version.pin.test.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,11 @@ const CC = '/api/v1/cloud-connection';
3535
const ORIGIN = 'https://env.example.test';
3636
const CLOUD = 'http://cloud.test';
3737

38-
/** Holds `setup.access`, which the two bind doors demand. */
38+
/** Holds `setup.access`, which the two bind doors demand — in {@link ORG}, the organization the session acts in. */
3939
const ADMIN = 'u_admin';
40+
const ORG = 'o1';
4041
const GRANTS: Record<string, any[]> = {
41-
sys_user_permission_set: [{ id: 'ups_1', user_id: ADMIN, permission_set_id: 'ps_1', permission_set: 'setup_admin', organization_id: null }],
42+
sys_user_permission_set: [{ id: 'ups_1', user_id: ADMIN, permission_set_id: 'ps_1', permission_set: 'setup_admin', organization_id: ORG }],
4243
sys_permission_set: [{ id: 'ps_1', name: 'setup_admin', system_permissions: ['setup.access'] }],
4344
};
4445

@@ -95,7 +96,7 @@ async function mount(mode: Mode, discovery: Discovery) {
9596
post: (p: string, h: Handler) => routes.set(`POST ${p}`, h),
9697
};
9798
const hooks = new Map<string, any>();
98-
const auth = { api: { getSession: async () => ({ user: { id: ADMIN }, session: { id: 'sess_admin' } }) } };
99+
const auth = { api: { getSession: async () => ({ user: { id: ADMIN }, session: { id: 'sess_admin', activeOrganizationId: ORG } }) } };
99100
const objectql = bindCatalogFromTables({ find: async (object: string) => GRANTS[object] ?? [] }, GRANTS);
100101
const protocol = discovery === 'real'
101102
? discoveryProducer()

‎packages/cloud-connection/src/cloud-connection-setup-access.pin.test.ts‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,22 @@ import { bindCatalogFromTables } from './__tests__/security-catalog.testkit.js';
3838
const CC = '/api/v1/cloud-connection';
3939
const ORIGIN = 'https://env.example.test';
4040

41-
/** Holds `setup.access` through an unscoped grant (the platform-admin shape). */
41+
/** Holds `setup.access` through the Default Organization's `admin_full_access` grant (the platform-admin shape: it applies with no active organization). */
4242
const ADMIN = 'u_setup_admin';
4343
/** Holds `setup.access` through a grant scoped to the organization its session is active in (the organization-admin shape). */
4444
const ORG_ADMIN = 'u_org_admin';
45-
/** Signed in, holds a permission set, and the set carries no `setup.access`. */
45+
/** Signed in, holds a permission set in the organization its session is active in, and the set carries no `setup.access`. */
4646
const MEMBER = 'u_member';
47-
/** Holds `setup.access`, but the ADR-0069 policy holds the session. */
47+
/** Holds `setup.access` (the platform-admin shape, as {@link ADMIN}), but the ADR-0069 policy holds the session. */
4848
const HELD_ADMIN = 'u_held_admin';
4949

5050
const ORG = 'org_a';
51+
/** The Default Organization — the owner of the one `admin_full_access` grant that anchors platform standing. */
52+
const DEFAULT_ORG = 'org_default';
5153
const SESSIONS: Record<string, { activeOrganizationId?: string; authGate?: { code: string; message: string } }> = {
5254
[ADMIN]: {},
5355
[ORG_ADMIN]: { activeOrganizationId: ORG },
54-
[MEMBER]: {},
56+
[MEMBER]: { activeOrganizationId: ORG },
5557
[HELD_ADMIN]: { authGate: { code: 'PASSWORD_EXPIRED', message: 'You must change your password before continuing.' } },
5658
};
5759

@@ -61,18 +63,20 @@ const ADMIN_KEY = 'osk_setup_admin_key_24148';
6163
function grantRows(): Record<string, any[]> {
6264
return {
6365
sys_user: Object.keys(SESSIONS).map((id) => ({ id, email: `${id}@example.test` })),
66+
sys_organization: [{ id: DEFAULT_ORG, slug: 'default' }, { id: ORG, slug: 'org-a' }],
6467
sys_member: [{ id: 'mem_1', user_id: ORG_ADMIN, organization_id: ORG, role: 'admin' }],
6568
sys_user_position: [],
6669
sys_position: [],
6770
sys_metadata_activation: [],
6871
sys_position_permission_set: [],
6972
sys_user_permission_set: [
70-
{ id: 'ups_admin', user_id: ADMIN, permission_set_id: 'ps_setup', permission_set: 'setup_admin', organization_id: null },
73+
{ id: 'ups_admin', user_id: ADMIN, permission_set_id: 'ps_full', permission_set: 'admin_full_access', organization_id: DEFAULT_ORG },
7174
{ id: 'ups_org_admin', user_id: ORG_ADMIN, permission_set_id: 'ps_setup', permission_set: 'setup_admin', organization_id: ORG },
72-
{ id: 'ups_held', user_id: HELD_ADMIN, permission_set_id: 'ps_setup', permission_set: 'setup_admin', organization_id: null },
73-
{ id: 'ups_member', user_id: MEMBER, permission_set_id: 'ps_member', permission_set: 'member_basic', organization_id: null },
75+
{ id: 'ups_held', user_id: HELD_ADMIN, permission_set_id: 'ps_full', permission_set: 'admin_full_access', organization_id: DEFAULT_ORG },
76+
{ id: 'ups_member', user_id: MEMBER, permission_set_id: 'ps_member', permission_set: 'member_basic', organization_id: ORG },
7477
],
7578
sys_permission_set: [
79+
{ id: 'ps_full', name: 'admin_full_access', system_permissions: ['manage_metadata', 'studio.access', 'setup.access'] },
7680
{ id: 'ps_setup', name: 'setup_admin', system_permissions: ['setup.access'] },
7781
{ id: 'ps_member', name: 'member_basic', system_permissions: ['export_data'] },
7882
],

‎packages/cloud-connection/src/connection-credential-store.test.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,18 @@ function makeRawApp() {
5555
post: (p: string, h: Handler) => routes.set(`POST ${p}`, h),
5656
};
5757
}
58-
const sessionAuth = (userId?: string) => ({ api: { getSession: async () => (userId ? { user: { id: userId } } : null) } });
58+
/** The organization the sessions act in, and the one {@link ADMIN_GRANTS}' grant is owned by. */
59+
const ORG = 'o1';
60+
const sessionAuth = (userId?: string) => ({ api: { getSession: async () => (userId ? { user: { id: userId }, session: { activeOrganizationId: ORG } } : null) } });
5961
/**
6062
* [#24148] The suites below sign in as `admin`, who must hold `setup.access` to
6163
* reach the connection-changing doors (their subject is the credential, not the
6264
* permission — that is `cloud-connection-setup-access.pin.test.ts`). Real
63-
* permission-set rows, read by the shared resolver through `find`.
65+
* permission-set rows, read by the shared resolver through `find`; the grant is
66+
* owned by {@link ORG}, the organization the session acts in.
6467
*/
6568
const ADMIN_GRANTS: Record<string, any[]> = {
66-
sys_user_permission_set: [{ id: 'ups_1', user_id: 'admin', permission_set_id: 'ps_1', permission_set: 'setup_admin', organization_id: null }],
69+
sys_user_permission_set: [{ id: 'ups_1', user_id: 'admin', permission_set_id: 'ps_1', permission_set: 'setup_admin', organization_id: ORG }],
6770
sys_permission_set: [{ id: 'ps_1', name: 'setup_admin', system_permissions: ['setup.access'] }],
6871
};
6972
/**

‎packages/cloud-connection/src/error-envelope.conformance.test.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,12 +189,13 @@ async function mountCloudConnection(opts: {
189189
const hooks = new Map<string, any>();
190190
const auth = {
191191
api: {
192-
getSession: async () => (opts.userId ? { user: { id: opts.userId }, session: {} } : null),
192+
// The session acts in `o1`, the organization the grant below is owned by.
193+
getSession: async () => (opts.userId ? { user: { id: opts.userId }, session: { activeOrganizationId: 'o1' } } : null),
193194
},
194195
};
195196
const grantRows: Record<string, any[]> = opts.userId && opts.setupAccess !== false
196197
? {
197-
sys_user_permission_set: [{ id: 'ups_1', user_id: opts.userId, permission_set_id: 'ps_1', permission_set: 'setup_admin', organization_id: null }],
198+
sys_user_permission_set: [{ id: 'ups_1', user_id: opts.userId, permission_set_id: 'ps_1', permission_set: 'setup_admin', organization_id: 'o1' }],
198199
sys_permission_set: [{ id: 'ps_1', name: 'setup_admin', system_permissions: ['setup.access'] }],
199200
}
200201
: {};

0 commit comments

Comments
 (0)