Skip to content

Commit 6b3414e

Browse files
committed
test(dogfood,runtime): the org-less sharing admin holds manage_sharing globally; an org-scoped grant with no organization is refused at the capability gate
The #8158 dogfood proof's exposed persona held manage_sharing only through an organization-scoped grant, which reached adminOrgScope through the defect this branch fixes. It now holds the set globally and keeps pinning adminOrgScope; a new persona holding the grant as filed (scoped, no active organization) is refused at the capability gate. The runtime door pins gain the control and the platform admin at the disable door, and the no-grant org-less caller. Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
1 parent c9e6463 commit 6b3414e

2 files changed

Lines changed: 93 additions & 13 deletions

File tree

‎packages/qa/dogfood/test/sharing-rule-org-less-caller.dogfood.test.ts‎

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,23 @@
3232
// PRECONDITION tests below assert each link rather than assuming it.
3333
//
3434
// The user shape is ordinary, not contrived: a permission-set grant
35-
// (`sys_user_permission_set`) is independent of organization MEMBERSHIP, and
36-
// `resolveUserAuthzGrants` keeps an org-scoped grant when the caller has no
37-
// active org to compare it against (`!(org && tenantId && org !== tenantId)`).
38-
// A multi-org deployment (whose membership reconciler binds nobody — ADR-0093
35+
// (`sys_user_permission_set`) is independent of organization MEMBERSHIP. A
36+
// multi-org deployment (whose membership reconciler binds nobody — ADR-0093
3937
// D1 `no-target-org`), an `invite-only` deployment, a user removed from their
40-
// organization, or an SSO JIT user pending placement all produce it.
38+
// organization, or an SSO JIT user pending placement all produce a caller with
39+
// no active organization.
40+
//
41+
// [#20515] What such a caller HOLDS changed underneath this file. When #8158
42+
// was filed, `resolveUserAuthzGrants` kept an ORGANIZATION-scoped grant for a
43+
// caller with no active organization (`!(org && tenantId && org !== tenantId)`
44+
// read "no tenant" as "every organization"), so an org-scoped `manage_sharing`
45+
// reached `adminOrgScope`. It no longer does: with no active organization only
46+
// GLOBAL grants apply. So the two faces are pinned separately below —
47+
// - the EXPOSED persona holds `manage_sharing` through a GLOBAL grant, the one
48+
// way an org-less caller still holds it, and keeps pinning `adminOrgScope`
49+
// (#8158's defence in depth: capability held, no organization to use it in);
50+
// - the ORG-SCOPED persona holds the very grant #8158 was filed with, scoped
51+
// to tenant A, and is now refused at the ADR-0111 D6 capability gate.
4152
//
4253
// ## Anti-vacuity
4354
//
@@ -63,6 +74,7 @@ const RULE_B = 'rule_8158_tenant_b';
6374
const PASSWORD = 'Member-Pass-123';
6475
const ORG_LESS_EMAIL = 'orgless-8158@verify.test';
6576
const ORG_BOUND_EMAIL = 'orgbound-8158@verify.test';
77+
const ORG_SCOPED_ORG_LESS_EMAIL = 'orgscoped-orgless-8158@verify.test';
6678

6779
interface RuleRow {
6880
id: string;
@@ -76,11 +88,14 @@ describe('#8158 — a manage_sharing holder with NO active organization cannot r
7688
let ql: any;
7789
/** The harness admin: platform authority, and (by harness design) org-less. */
7890
let platform: string;
79-
/** The exposed persona: org-scoped `manage_sharing`, no membership, no active org. */
91+
/** The exposed persona: GLOBAL `manage_sharing`, no membership, no active org. */
8092
let orgLess: string;
81-
/** The control persona: the SAME grant, plus a membership in tenant A. */
93+
/** [#20515] The #8158 grant as filed — scoped to tenant A — with no membership and no active org. */
94+
let orgScopedOrgLess: string;
95+
/** The control persona: the same set scoped to tenant A, plus a membership in tenant A. */
8296
let orgBound: string;
8397
let orgLessUserId = '';
98+
let orgScopedOrgLessUserId = '';
8499
let orgBoundUserId = '';
85100

86101
const ruleRow = (organizationId: string, name: string) => ({
@@ -126,13 +141,23 @@ describe('#8158 — a manage_sharing holder with NO active organization cannot r
126141
// Real sign-ups: better-auth's own path, through every database hook.
127142
await stack.signUp(ORG_LESS_EMAIL, PASSWORD);
128143
await stack.signUp(ORG_BOUND_EMAIL, PASSWORD);
144+
await stack.signUp(ORG_SCOPED_ORG_LESS_EMAIL, PASSWORD);
129145
const uid = async (email: string): Promise<string> =>
130146
(await ql.findOne('sys_user', { where: { email }, context: SYS }))?.id;
131147
orgLessUserId = await uid(ORG_LESS_EMAIL);
132148
orgBoundUserId = await uid(ORG_BOUND_EMAIL);
149+
orgScopedOrgLessUserId = await uid(ORG_SCOPED_ORG_LESS_EMAIL);
133150

134-
// The identical grant for both, scoped to tenant A.
135-
for (const userId of [orgLessUserId, orgBoundUserId]) {
151+
// [#20515] The exposed persona holds the set GLOBALLY (no organization):
152+
// with no active organization only global grants apply, so this is the one
153+
// spelling under which it still carries `manage_sharing` and still reaches
154+
// `adminOrgScope`.
155+
await ql.insert('sys_user_permission_set', {
156+
user_id: orgLessUserId, permission_set_id: psId, organization_id: null,
157+
}, { context: SYS });
158+
// The #8158 grant as filed — scoped to tenant A — for the control (which
159+
// has tenant A active) and for the org-scoped persona (which has none).
160+
for (const userId of [orgBoundUserId, orgScopedOrgLessUserId]) {
136161
await ql.insert('sys_user_permission_set', {
137162
user_id: userId, permission_set_id: psId, organization_id: ORG_A,
138163
}, { context: SYS });
@@ -148,6 +173,7 @@ describe('#8158 — a manage_sharing holder with NO active organization cannot r
148173
// with the membership state above already in place.
149174
orgLess = await stack.signIn(ORG_LESS_EMAIL, PASSWORD);
150175
orgBound = await stack.signIn(ORG_BOUND_EMAIL, PASSWORD);
176+
orgScopedOrgLess = await stack.signIn(ORG_SCOPED_ORG_LESS_EMAIL, PASSWORD);
151177
}, 180_000);
152178

153179
afterAll(async () => {
@@ -206,6 +232,8 @@ describe('#8158 — a manage_sharing holder with NO active organization cannot r
206232
// ── the measurement ──────────────────────────────────────────────────
207233

208234
it('THE REPORTED CASE: listing is refused 403, not answered with every tenant’s rules', async () => {
235+
// The exposed persona holds `manage_sharing` GLOBALLY (#20515 — see the
236+
// header), so this is `adminOrgScope` refusing, as the next case proves.
209237
const res = await stack.apiAs(orgLess, 'GET', RULES);
210238
const payload = res.status === 200
211239
? ((await res.json()) as { data: RuleRow[] }).data.map((r) => `${r.name}@${r.organization_id}`)
@@ -274,6 +302,37 @@ describe('#8158 — a manage_sharing holder with NO active organization cannot r
274302
})).toBeFalsy();
275303
});
276304

305+
// ── [#20515] the #8158 grant as filed: ORG-scoped, no active organization ───
306+
307+
it('PRECONDITION: the org-scoped persona holds no membership, and its SESSION carries no active organization', async () => {
308+
const members = await ql.find('sys_member', { where: { user_id: orgScopedOrgLessUserId }, context: SYS });
309+
expect(Array.isArray(members) ? members : members?.records ?? []).toHaveLength(0);
310+
const sessions = await ql.find('sys_session', { where: { user_id: orgScopedOrgLessUserId }, context: SYS });
311+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
312+
const rows: any[] = Array.isArray(sessions) ? sessions : sessions?.records ?? [];
313+
expect(rows.length, 'the sign-in really did mint a session row').toBeGreaterThan(0);
314+
for (const s of rows) {
315+
expect(s.active_organization_id ?? s.activeOrganizationId ?? null).toBeFalsy();
316+
}
317+
});
318+
319+
it('an ORG-scoped manage_sharing grant with no active organization is refused 403 at the CAPABILITY gate', async () => {
320+
// With no active organization only global grants apply, so the tenant-A
321+
// grant confers nothing here: the ADR-0111 D6 gate refuses before
322+
// `adminOrgScope` is ever asked — and no tenant's rule is read.
323+
const res = await stack.apiAs(orgScopedOrgLess, 'GET', RULES);
324+
const body = (await res.json()) as { code?: string; error?: string; data?: RuleRow[] };
325+
expect(res.status, JSON.stringify(body)).toBe(403);
326+
expect(body.code).toBe('PERMISSION_DENIED');
327+
expect(body.error ?? '').toMatch(/requires the manage_sharing capability/);
328+
expect(body.data).toBeUndefined();
329+
});
330+
331+
it('the org-scoped persona is refused the other tenant’s rule by name as well', async () => {
332+
const res = await stack.apiAs(orgScopedOrgLess, 'GET', `${RULES}/${RULE_B}`);
333+
expect(res.status, await res.text()).toBe(403);
334+
});
335+
277336
// ── the control: the SAME grant, with an organization, still works ───
278337

279338
it('the org-BOUND holder of the same grant reads its own tenant and NOT the other', async () => {

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

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ function matchesWhere(row: any, where: any): boolean {
7171
* `org_alpha`-scoped `manage_metadata` set. `u_member` is the control: a
7272
* current `org_alpha` member holding the same grant. `u_global` was removed the
7373
* same way but holds the set GLOBALLY. `u_admin` holds `admin_full_access`
74-
* unscoped — the row the platform-admin bootstrap mints.
74+
* unscoped — the row the platform-admin bootstrap mints. `u_orgless` signed in,
75+
* never selected an organization and holds no grant at all.
7576
*/
7677
const TABLES: Record<string, any[]> = {
7778
sys_api_key: [],
@@ -83,7 +84,7 @@ const TABLES: Record<string, any[]> = {
8384
{ user_id: 'u_posex', organization_id: BETA, role: 'member' },
8485
{ user_id: 'u_posmember', organization_id: ALPHA, role: 'member' },
8586
],
86-
sys_user: ['u_member', 'u_exmember', 'u_gone', 'u_global', 'u_admin', 'u_posex', 'u_posmember']
87+
sys_user: ['u_member', 'u_exmember', 'u_gone', 'u_global', 'u_admin', 'u_posex', 'u_posmember', 'u_orgless']
8788
.map((id) => ({ id, email: `${id}@example.com` })),
8889
sys_user_permission_set: [
8990
{ user_id: 'u_member', permission_set_id: 'ps_meta', organization_id: ALPHA },
@@ -110,9 +111,9 @@ const TABLES: Record<string, any[]> = {
110111
],
111112
};
112113

113-
type Who = 'member' | 'exmember' | 'gone' | 'global' | 'admin' | 'admin_orgless' | 'posex' | 'posmember';
114+
type Who = 'member' | 'exmember' | 'gone' | 'global' | 'admin' | 'admin_orgless' | 'posex' | 'posmember' | 'orgless';
114115

115-
/** Every session but the last names `org_alpha`. */
116+
/** Every session names `org_alpha` except `admin_orgless` and `orgless`, which name no organization. */
116117
const SESSIONS: Record<Who, { id: string; token: string; userId: string; activeOrganizationId?: string }> = {
117118
member: { id: 'ses_member', token: 'tok_member', userId: 'u_member', activeOrganizationId: ALPHA },
118119
exmember: { id: 'ses_exmember', token: 'tok_exmember', userId: 'u_exmember', activeOrganizationId: ALPHA },
@@ -122,6 +123,7 @@ const SESSIONS: Record<Who, { id: string; token: string; userId: string; activeO
122123
admin_orgless: { id: 'ses_admin2', token: 'tok_admin2', userId: 'u_admin' },
123124
posex: { id: 'ses_posex', token: 'tok_posex', userId: 'u_posex', activeOrganizationId: ALPHA },
124125
posmember: { id: 'ses_posmember', token: 'tok_posmember', userId: 'u_posmember', activeOrganizationId: ALPHA },
126+
orgless: { id: 'ses_orgless', token: 'tok_orgless', userId: 'u_orgless' },
125127
};
126128

127129
function rig() {
@@ -249,6 +251,12 @@ describe('[#20515] what the rule leaves unchanged', () => {
249251
expect(r.deleteRequests).toEqual([{ packageId: PKG, organizationId: ALPHA }]);
250252
});
251253

254+
it('CONTROL · the same current member passes PATCH /packages/:id/disable: 200, and the package is switched off', async () => {
255+
const r = rig();
256+
expect((await r.call('PATCH', 'member', `/packages/${PKG}/disable`)).status).toBe(200);
257+
expect(switchedOff(r)).toBe(true);
258+
});
259+
252260
it('CONTROL · the position-bound set: a current org_alpha member with org_alpha active passes: 200', async () => {
253261
const r = rig();
254262
const answer = await r.call('DELETE', 'posmember', `/packages/${PKG}`);
@@ -269,8 +277,21 @@ describe('[#20515] what the rule leaves unchanged', () => {
269277

270278
it('platform-admin standing (unscoped admin_full_access) passes with org_alpha active, and with no organization at all', async () => {
271279
expect((await rig().call('DELETE', 'admin', `/packages/${PKG}`)).status).toBe(200);
280+
expect((await rig().call('PATCH', 'admin', `/packages/${PKG}/disable`)).status).toBe(200);
272281
const orgless = await rig().call('DELETE', 'admin_orgless', `/packages/${PKG}`);
273282
expect({ status: orgless.status, code: orgless.code }).toEqual({ status: 400, code: 'TENANT_SCOPE_REQUIRED' });
274283
expect((await rig().call('PATCH', 'admin_orgless', `/packages/${PKG}/disable`)).status).toBe(200);
275284
});
285+
286+
it('a caller who never selected an organization and holds no grant is refused 403 on both doors, as before', async () => {
287+
const r = rig();
288+
const del = await r.call('DELETE', 'orgless', `/packages/${PKG}`);
289+
expect({ status: del.status, code: del.code, httpStatus: del.httpStatus })
290+
.toEqual({ status: 403, code: 'PERMISSION_DENIED', httpStatus: 403 });
291+
const dis = await r.call('PATCH', 'orgless', `/packages/${PKG}/disable`);
292+
expect({ status: dis.status, code: dis.code, httpStatus: dis.httpStatus })
293+
.toEqual({ status: 403, code: 'PERMISSION_DENIED', httpStatus: 403 });
294+
expect(r.deleteRequests).toEqual([]);
295+
expect(switchedOff(r)).toBe(false);
296+
});
276297
});

0 commit comments

Comments
 (0)