From 3eb9d64bb44e7dd836352e73d44a8cad426d67da Mon Sep 17 00:00:00 2001 From: Juan Castro Date: Tue, 11 Aug 2026 18:27:17 -0400 Subject: [PATCH] refactor(permissions): drop hardcoded group permission map for a flat default --- src/backend/data/hardcoded-permissions.js | 59 ++--- .../permission/PermissionService.test.ts | 211 +++++++++++++----- .../services/permission/PermissionService.ts | 167 +++----------- src/backend/testUtil.ts | 5 +- 4 files changed, 203 insertions(+), 239 deletions(-) diff --git a/src/backend/data/hardcoded-permissions.js b/src/backend/data/hardcoded-permissions.js index a3cd6a28f..1f0b81b4a 100644 --- a/src/backend/data/hardcoded-permissions.js +++ b/src/backend/data/hardcoded-permissions.js @@ -88,55 +88,22 @@ const implicit_user_app_permissions = [ }, ]; -// Pre-v2 each entry below carried a `policy: { 'rate-limit': { max, period } }` -// block attached by a `policyPerm('temp.kv')` / `policyPerm('user.es')` helper, -// and a `driverPolicies` table held the actual `{max, period}` values. Nothing -// ever read those policy blocks in v2 — rate limiting is now declared -// directly on each driver via `@Driver({ rateLimit })` / `readonly rateLimit` -// (see KVStoreDriver, AppDriver, SubdomainDriver, NotificationDriver), with -// per-subscription overrides via `bySubscription`. The grants themselves -// stay so the permission scan still emits a path for each group/permission; -// only the dead policy plumbing was removed. -const hardcoded_user_group_permissions = { - system: { - 'ca342a5e-b13d-4dee-9048-58b11a57cc55': { - driver: {}, - service: {}, - feature: {}, - 'local-terminal:access': {}, - }, - 'b7220104-7905-4985-b996-649fdcdb3c8f': { - driver: {}, - service: {}, - 'service:hello-world:ii:hello-world': {}, - 'service:puter-kvstore:ii:puter-kvstore': {}, - 'driver:puter-kvstore': {}, - 'service:puter-notifications:ii:crud-q': {}, - 'service:puter-apps:ii:crud-q': {}, - 'service:puter-subdomains:ii:crud-q': {}, - 'service:apps:ii:crud-q': {}, - 'service:es\\Cnotification:ii:crud-q': {}, - 'service:es\\Capp:ii:crud-q': {}, - 'service:app:ii:crud-q': {}, - 'service:es\\Csubdomain:ii:crud-q': {}, - }, - '78b1b1dd-c959-44d2-b02c-8735671f9997': { - driver: {}, - service: {}, - 'service:hello-world:ii:hello-world': {}, - 'service:puter-kvstore:ii:puter-kvstore': {}, - 'driver:puter-kvstore': {}, - 'service:es\\Cnotification:ii:crud-q': {}, - 'service:es\\Capp:ii:crud-q': {}, - 'service:app:ii:crud-q': {}, - 'service:es\\Csubdomain:ii:crud-q': {}, - 'service:apps:ii:crud-q': {}, - }, - }, +// Permissions every user actor holds, regardless of group membership. +// +// Roots only: a grant subsumes everything beneath it, so `service` already +// answers `service:puter-kvstore:ii:puter-kvstore`. Listing descendants adds +// entries no check can ever need. +// +// A floor, not a ceiling — anything that depends on *who* the user is belongs +// in the ACL as a `user_to_group_permissions` / `user_to_user_permissions` +// row. Adding a root here grants it to every user, temp accounts included. +const default_user_permissions = { + driver: {}, + service: {}, }; module.exports = { implicit_user_app_permissions, default_implicit_user_app_permissions, - hardcoded_user_group_permissions, + default_user_permissions, }; diff --git a/src/backend/services/permission/PermissionService.test.ts b/src/backend/services/permission/PermissionService.test.ts index 9b8c93929..762f71736 100644 --- a/src/backend/services/permission/PermissionService.test.ts +++ b/src/backend/services/permission/PermissionService.test.ts @@ -222,14 +222,14 @@ describe('PermissionService (integration)', () => { const { actor } = await makeUserActor(); const allowed = await permService.check( actor, - `service:nope-${uuidv4()}:ii:read`, + `zztest:nope-${uuidv4()}:ii:read`, ); expect(allowed).toBeFalsy(); }); it('canManagePermission delegates to check on manage:', async () => { const { user, actor } = await makeUserActor(); - const perm = `service:manage-test-${uuidv4()}:ii:read`; + const perm = `zztest:manage-test-${uuidv4()}:ii:read`; // Grant manage: via the flat store. await server.stores.permission.setFlatUserPerm( user.id, @@ -253,7 +253,7 @@ describe('PermissionService (integration)', () => { permService.grantUserUserPermission( actor, `does-not-exist-${uuidv4()}`, - 'service:foo:ii:read', + 'zztest:foo:ii:read', ), ).rejects.toMatchObject({ statusCode: 404 }); }); @@ -264,7 +264,7 @@ describe('PermissionService (integration)', () => { permService.grantUserUserPermission( actor, user.username, - 'service:foo:ii:read', + 'zztest:foo:ii:read', ), ).rejects.toMatchObject({ statusCode: 400 }); }); @@ -277,7 +277,7 @@ describe('PermissionService (integration)', () => { permService.grantUserUserPermission( issuer, target.username, - `service:unmanaged-${uuidv4()}:ii:read`, + `zztest:unmanaged-${uuidv4()}:ii:read`, ), ), ).rejects.toMatchObject({ statusCode: 403 }); @@ -286,7 +286,7 @@ describe('PermissionService (integration)', () => { it('grant persists when issuer holds manage:', async () => { const { user: issuer, actor: issuerActor } = await makeUserActor(); const { user: target, actor: targetActor } = await makeUserActor(); - const permission = `service:user-user-${uuidv4()}:ii:read`; + const permission = `zztest:user-user-${uuidv4()}:ii:read`; await server.stores.permission.setFlatUserPerm( issuer.id, `manage:${permission}`, @@ -316,7 +316,7 @@ describe('PermissionService (integration)', () => { permService.revokeUserUserPermission( actor, `does-not-exist-${uuidv4()}`, - 'service:foo:ii:read', + 'zztest:foo:ii:read', ), ).rejects.toMatchObject({ statusCode: 404 }); }); @@ -329,7 +329,7 @@ describe('PermissionService (integration)', () => { permService.revokeUserUserPermission( issuer, target.username, - `service:unmanaged-${uuidv4()}:ii:read`, + `zztest:unmanaged-${uuidv4()}:ii:read`, ), ), ).rejects.toMatchObject({ statusCode: 403 }); @@ -359,7 +359,7 @@ describe('PermissionService (integration)', () => { permService.grantUserAppPermission( actor, `does-not-exist-${uuidv4()}`, - 'service:foo:ii:read', + 'zztest:foo:ii:read', ), ), ).rejects.toMatchObject({ statusCode: 404 }); @@ -368,7 +368,7 @@ describe('PermissionService (integration)', () => { it('persists a user→app grant and is idempotent', async () => { const { user, actor } = await makeUserActor(); const app = await makeApp(user.id); - const permission = `service:gua-${uuidv4()}:ii:read`; + const permission = `zztest:gua-${uuidv4()}:ii:read`; await runWithContext({ actor }, () => permService.grantUserAppPermission(actor, app.uid, permission), @@ -389,7 +389,7 @@ describe('PermissionService (integration)', () => { it('revokeUserAppPermission removes the row', async () => { const { user, actor } = await makeUserActor(); const app = await makeApp(user.id); - const permission = `service:rua-${uuidv4()}:ii:read`; + const permission = `zztest:rua-${uuidv4()}:ii:read`; await runWithContext({ actor }, () => permService.grantUserAppPermission(actor, app.uid, permission), ); @@ -417,7 +417,7 @@ describe('PermissionService (integration)', () => { permService.revokeUserAppPermission( appActor, app.uid, - 'service:foo:ii:read', + 'zztest:foo:ii:read', ), ).rejects.toMatchObject({ statusCode: 403 }); }); @@ -448,8 +448,8 @@ describe('PermissionService (integration)', () => { const { user, actor } = await makeUserActor(); const app = await makeApp(user.id); for (const p of [ - `service:rua-${uuidv4()}:ii:read`, - `service:rua-${uuidv4()}:ii:write`, + `zztest:rua-${uuidv4()}:ii:read`, + `zztest:rua-${uuidv4()}:ii:write`, ]) { await runWithContext({ actor }, () => permService.grantUserAppPermission(actor, app.uid, p), @@ -488,7 +488,7 @@ describe('PermissionService (integration)', () => { permService.grantDevAppPermission( actor, `does-not-exist-${uuidv4()}`, - 'service:foo:ii:read', + 'zztest:foo:ii:read', ), ), ).rejects.toMatchObject({ statusCode: 404 }); @@ -502,7 +502,7 @@ describe('PermissionService (integration)', () => { permService.grantDevAppPermission( actor, app.uid, - `service:unmanaged-${uuidv4()}:ii:read`, + `zztest:unmanaged-${uuidv4()}:ii:read`, ), ), ).rejects.toMatchObject({ statusCode: 403 }); @@ -511,7 +511,7 @@ describe('PermissionService (integration)', () => { it('grant persists when manage: is held', async () => { const { user, actor } = await makeUserActor(); const app = await makeApp(user.id); - const permission = `service:dev-${uuidv4()}:ii:read`; + const permission = `zztest:dev-${uuidv4()}:ii:read`; await server.stores.permission.setFlatUserPerm( user.id, `manage:${permission}`, @@ -542,7 +542,7 @@ describe('PermissionService (integration)', () => { permService.revokeDevAppPermission( appActor, app.uid, - 'service:foo:ii:read', + 'zztest:foo:ii:read', ), ).rejects.toMatchObject({ statusCode: 403 }); }); @@ -566,7 +566,7 @@ describe('PermissionService (integration)', () => { permService.grantUserGroupPermission( actor, { id: 1, uid: 'grp-doesnt-matter' }, - `service:unmanaged-${uuidv4()}:ii:read`, + `zztest:unmanaged-${uuidv4()}:ii:read`, ), ), ).rejects.toMatchObject({ statusCode: 403 }); @@ -577,7 +577,7 @@ describe('PermissionService (integration)', () => { permService.revokeUserGroupPermission( { user: undefined } as unknown as Actor, { id: 1, uid: 'grp-x' }, - 'service:foo:ii:read', + 'zztest:foo:ii:read', ), ).rejects.toMatchObject({ statusCode: 403 }); }); @@ -587,7 +587,7 @@ describe('PermissionService (integration)', () => { it('listUserPermissionIssuers returns the issuer who granted the target a perm', async () => { const { user: issuer, actor: issuerActor } = await makeUserActor(); const { user: target } = await makeUserActor(); - const permission = `service:lst-${uuidv4()}:ii:read`; + const permission = `zztest:lst-${uuidv4()}:ii:read`; await server.stores.permission.setFlatUserPerm( issuer.id, `manage:${permission}`, @@ -666,7 +666,7 @@ describe('PermissionService (integration)', () => { it('revoke is visible immediately — a cached "granted" reading is not served', async () => { const { user: issuer, actor: issuerActor } = await makeUserActor(); const { user: target, actor: targetActor } = await makeUserActor(); - const permission = `service:revoke-now-${uuidv4()}:ii:read`; + const permission = `zztest:revoke-now-${uuidv4()}:ii:read`; await grantManage(issuer, permission); await runWithContext({ actor: issuerActor }, () => @@ -721,7 +721,7 @@ describe('PermissionService (integration)', () => { it('grant is visible immediately — a cached "denied" reading is not served', async () => { const { user: issuer, actor: issuerActor } = await makeUserActor(); const { user: target, actor: targetActor } = await makeUserActor(); - const permission = `service:grant-now-${uuidv4()}:ii:read`; + const permission = `zztest:grant-now-${uuidv4()}:ii:read`; await grantManage(issuer, permission); // Prime a "denied" reading into the cache. @@ -780,7 +780,7 @@ describe('PermissionService (integration)', () => { const { user: issuer, actor: issuerActor } = await makeUserActor(); const { user: target, actor: targetActor } = await makeUserActor(); const app = await makeApp(target.id); - const permission = `service:app-revoke-now-${uuidv4()}:ii:read`; + const permission = `zztest:app-revoke-now-${uuidv4()}:ii:read`; await grantManage(issuer, permission); await runWithContext({ actor: issuerActor }, () => @@ -825,7 +825,7 @@ describe('PermissionService (integration)', () => { const { user: issuer, actor: issuerActor } = await makeUserActor(); const { user: target, actor: targetActor } = await makeUserActor(); const app = await makeApp(target.id); - const permission = `service:app-grant-now-${uuidv4()}:ii:read`; + const permission = `zztest:app-grant-now-${uuidv4()}:ii:read`; await grantManage(issuer, permission); // App is allowed to act with the permission, but the user does @@ -874,7 +874,7 @@ describe('PermissionService (integration)', () => { it('revokeUserUserPermission deletes the linked SQL row before resolving', async () => { const { user: issuer, actor: issuerActor } = await makeUserActor(); const { user: target } = await makeUserActor(); - const permission = `service:rvk-sync-${uuidv4()}:ii:read`; + const permission = `zztest:rvk-sync-${uuidv4()}:ii:read`; await grantManage(issuer, permission); await runWithContext({ actor: issuerActor }, () => permService.grantUserUserPermission( @@ -906,7 +906,7 @@ describe('PermissionService (integration)', () => { it('revokeUserUserPermission surfaces a failed SQL delete instead of swallowing it', async () => { const { user: issuer, actor: issuerActor } = await makeUserActor(); const { user: target } = await makeUserActor(); - const permission = `service:rvk-fail-${uuidv4()}:ii:read`; + const permission = `zztest:rvk-fail-${uuidv4()}:ii:read`; await grantManage(issuer, permission); await runWithContext({ actor: issuerActor }, () => permService.grantUserUserPermission( @@ -946,7 +946,7 @@ describe('PermissionService (integration)', () => { it('scan-path warms of the flat view carry an expiry (grants are permanent)', async () => { const { user: issuer, actor: issuerActor } = await makeUserActor(); const { user: target, actor: targetActor } = await makeUserActor(); - const permission = `service:warm-ttl-${uuidv4()}:ii:read`; + const permission = `zztest:warm-ttl-${uuidv4()}:ii:read`; await grantManage(issuer, permission); // The linked (SQL) path is a delegation chain: it only grants // if the issuer holds the permission themselves. Give the @@ -1088,47 +1088,78 @@ describe('PermissionService — scan paths', () => { { ownerUserId }, ); - describe('group-derived grants', () => { - it('members of the default user group inherit the system driver grants', async () => { + describe('default user permissions and group grants', () => { + it('grants the default permissions to a member of the default user group', async () => { const { actor } = await makeGroupedUser(); expect(await permService.check(actor, 'driver:puter-kvstore')).toBe( true, ); + expect( + await permService.check( + actor, + 'service:puter-kvstore:ii:puter-kvstore', + ), + ).toBe(true); }); - it('a user in no group inherits nothing', async () => { + it('grants the default permissions to a user in no group at all', async () => { + // The floor does not depend on membership, which is what repairs + // the account whose best-effort group insert failed at signup — + // previously locked out of every driver call with no recovery. const { actor } = await makeLooseUser(); expect(await permService.check(actor, 'driver:puter-kvstore')).toBe( + true, + ); + expect( + await permService.check( + actor, + 'service:puter-kvstore:ii:puter-kvstore', + ), + ).toBe(true); + }); + + it('grants the default permissions to a temp user', async () => { + const { row: temp, actor: tempActor } = await makeLooseUser(); + await server.stores.group.addUsers(DEFAULT_TEMP_GROUP_UID, [ + temp.username, + ]); + expect( + await permService.check(tempActor, 'driver:puter-kvstore'), + ).toBe(true); + }); + + it('does not grant a permission outside the default set', async () => { + // Both used to be admin-only entries in the group-keyed map. + // Nothing grants them now. + const { actor } = await makeLooseUser(); + expect(await permService.check(actor, 'local-terminal:access')).toBe( false, ); + expect( + await permService.check(actor, `feature:${uuidv4()}`), + ).toBe(false); }); - it('registerSystemGrantForUsers reaches the user group but not the temp group', async () => { - const permission = `feature:users-only-${uuidv4()}`; - const { actor: member } = await makeGroupedUser(); - const { row: temp, actor: tempActor } = await makeLooseUser(); - await server.stores.group.addUsers(DEFAULT_TEMP_GROUP_UID, [ - temp.username, - ]); - - permService.registerSystemGrantForUsers(permission); - - expect(await permService.check(member, permission)).toBe(true); - expect(await permService.check(tempActor, permission)).toBe(false); - }); - - it('registerSystemGrantForEveryone reaches temp users too', async () => { - const permission = `feature:everyone-${uuidv4()}`; - const { row: temp, actor: tempActor } = await makeLooseUser(); - await server.stores.group.addUsers(DEFAULT_TEMP_GROUP_UID, [ - temp.username, - ]); - - permService.registerSystemGrantForEveryone(permission, { - note: 'test', - }); - - expect(await permService.check(tempActor, permission)).toBe(true); + it('never queries group membership to resolve a user permission', async () => { + // The membership lookup existed only to re-derive the flattened + // constant above, so no scan should reach for it now. + const { actor } = await makeGroupedUser(); + const spy = vi.spyOn(server.stores.group, 'listGroupsWithMember'); + try { + expect( + await permService.check(actor, 'driver:puter-kvstore', { + noCache: true, + }), + ).toBe(true); + expect( + await permService.check(actor, `zztest:${uuidv4()}:read`, { + noCache: true, + }), + ).toBe(false); + expect(spy).not.toHaveBeenCalled(); + } finally { + spy.mockRestore(); + } }); it('honours a group grant issued by a user, and drops it on revoke', async () => { @@ -1372,6 +1403,22 @@ describe('PermissionService — scan paths', () => { ).toBe(false); }); + it('a scoped token does not inherit the default user permissions', async () => { + // The floor applies to user actors only. A token must still carry + // its own row, or a scoped token would silently widen to every + // driver the moment its issuer held the `driver` root. + const { actor } = await makeGroupedUser(); + expect(await permService.check(actor, 'driver:puter-kvstore')).toBe( + true, + ); + expect( + await permService.check( + scopedToken(actor, `tok-${uuidv4()}`), + 'driver:puter-kvstore', + ), + ).toBe(false); + }); + it('a scoped token resolves a permission it carries and its issuer holds', async () => { const { row, actor } = await makeGroupedUser(); const permission = `zztest:tok-${uuidv4()}:ii:read`; @@ -1731,3 +1778,53 @@ describe('PermissionService — scan paths', () => { }); }); }); + +describe('PermissionService — default user permissions vs. group config', () => { + /** A user with no group membership, on an arbitrary server. */ + const makeLooseActor = async (srv: PuterServer): Promise => { + const username = `pdg${Math.random().toString(36).slice(2, 10)}`; + const u = await srv.stores.user.create({ + username, + uuid: uuidv4(), + password: null, + email: `${username}@test.local`, + requires_email_confirmation: false, + }); + return { + user: { + id: u.id, + uuid: u.uuid, + username: u.username, + email: u.email ?? null, + }, + }; + }; + + it('grants the default permissions with no default group configured', async () => { + // Self-hosted deployments may run without default groups. The floor + // is not group-derived, so it applies regardless — a deployment that + // clears both no longer has every driver call fail closed. + const bare = await setupTestServer({ + default_user_group: '', + default_temp_group: '', + } as never); + try { + const perms = bare.services + .permission as unknown as PermissionService; + const actor = await makeLooseActor(bare); + expect(await perms.check(actor, 'driver:puter-kvstore')).toBe(true); + expect( + await perms.check( + actor, + 'service:puter-kvstore:ii:puter-kvstore', + ), + ).toBe(true); + // Still only the roots the floor names. + expect(await perms.check(actor, 'local-terminal:access')).toBe( + false, + ); + } finally { + await bare.shutdown(); + } + }, 60_000); +}); diff --git a/src/backend/services/permission/PermissionService.ts b/src/backend/services/permission/PermissionService.ts index c6c0ae7ec..3a8d827b8 100644 --- a/src/backend/services/permission/PermissionService.ts +++ b/src/backend/services/permission/PermissionService.ts @@ -41,7 +41,7 @@ import { // @ts-ignore — hardcoded-permissions.js is plain JS import { default_implicit_user_app_permissions, - hardcoded_user_group_permissions, + default_user_permissions, implicit_user_app_permissions, } from '../../data/hardcoded-permissions.js'; import { UserRow } from '../../stores/user/UserStore'; @@ -81,16 +81,6 @@ export class PermissionService extends PuterService { private readonly rewriters: PermissionRewriter[] = []; private readonly implicators: PermissionImplicator[] = []; private readonly exploders: PermissionExploder[] = []; - /** - * System-issued grants registered at runtime by other services. Keyed by - * group UID, then by permission string. Merged with the imported - * `hardcoded_user_group_permissions.system` map during the hc-user-group - * scan. - */ - private readonly systemGrantsByGroupUid: Record< - string, - Record - > = {}; // -- Extension hooks ---------------------------------------------- // @@ -110,38 +100,6 @@ export class PermissionService extends PuterService { this.exploders.push(exploder); } - /** - * Grant a permission (as issued by `system`) to everyone — members of both - * the default user group and the default temp group. - * - * Call from an owning service's `onServerStart` (or later). - */ - registerSystemGrantForEveryone( - permission: string, - data: unknown = {}, - ): void { - const userGroup = this.config.default_user_group; - const tempGroup = this.config.default_temp_group; - if (userGroup) this.#addSystemGrant(userGroup, permission, data); - if (tempGroup) this.#addSystemGrant(tempGroup, permission, data); - } - - /** - * Grant a permission (as issued by `system`) to non-temp users only — - * members of the default user group, but not the default temp group. - */ - registerSystemGrantForUsers(permission: string, data: unknown = {}): void { - const userGroup = this.config.default_user_group; - if (userGroup) this.#addSystemGrant(userGroup, permission, data); - } - - #addSystemGrant(groupUid: string, permission: string, data: unknown): void { - if (!this.systemGrantsByGroupUid[groupUid]) { - this.systemGrantsByGroupUid[groupUid] = {}; - } - this.systemGrantsByGroupUid[groupUid][permission] = data; - } - // -- Rewrite / explode (pure-ish helpers) ------------------------ async rewritePermission(permission: string): Promise { @@ -373,6 +331,38 @@ export class PermissionService extends PuterService { } options = exploded.flat(); + // -- default user permissions -- + // A group-independent floor every user actor holds, resolved in + // memory (see `default_user_permissions`). Derived actors are + // excluded: an app-under-user is gated by its own implicit grant map + // and an access token by its issuer, both of which recurse into a + // scan of the user actor and so still see this floor. + if (!actor.app && !actor.accessToken && actor.user?.id) { + const granted = options.find((option) => + Object.prototype.hasOwnProperty.call( + default_user_permissions, + option, + ), + ); + if (granted !== undefined) { + reading.push({ + $: 'option', + key: 'default-user-permission', + permission: granted, + source: 'implied', + by: 'default-user-permission', + data: (default_user_permissions as Record)[ + granted + ], + holder_username: actor.user.username, + issuer_username: 'system', + }); + reading.push({ $: 'time', value: Date.now() - startTs }); + await this.#maybeCacheScan(cacheKey, reading); + return reading; + } + } + // -- shortcut implicators -- let shortCircuit = false; for (const permission of options) { @@ -409,7 +399,6 @@ export class PermissionService extends PuterService { this.#scanNonShortcutImplicators(actor, options, reading), this.#scanAccessToken(actor, options, reading), this.#scanUserUser(actor, options, reading, workingState), - this.#scanHcUserGroupUser(actor, options, reading), this.#scanUserGroup(actor, options, reading), this.#scanUserAppImplied(actor, options, reading), this.#scanUserApp(actor, options, reading), @@ -527,96 +516,6 @@ export class PermissionService extends PuterService { reading.push(...subReadings); } - /** - * Resolve permissions that a persistent group's members inherit from an - * issuer (typically `system`) via the hardcoded map in - * `hardcoded-permissions.js`, merged with any runtime grants registered - * through `registerSystemGrantForEveryone` / - * `registerSystemGrantForUsers`. - */ - async #scanHcUserGroupUser( - actor: Actor, - options: string[], - reading: ReadingNode[], - ): Promise { - if (actor.app || actor.accessToken) return; - if (!actor.user?.id) return; - - const memberGroups = await this.stores.group.listGroupsWithMember( - actor.user.id, - ); - if (memberGroups.length === 0) return; - - const groupByUid: Record = {}; - for (const g of memberGroups) { - groupByUid[g.uid] = { id: g.id, uid: g.uid }; - } - - // Compose the effective issuer → group → permission → data map by - // merging the imported hardcoded data with runtime-registered system - // grants. Runtime grants are always attributed to the `system` issuer. - const hcMap = hardcoded_user_group_permissions as Record< - string, - Record> - >; - const hasRuntimeGrants = - Object.keys(this.systemGrantsByGroupUid).length > 0; - const byIssuer: Record< - string, - Record> - > = hasRuntimeGrants - ? { ...hcMap, system: { ...(hcMap.system ?? {}) } } - : hcMap; - if (hasRuntimeGrants) { - for (const [gUid, perms] of Object.entries( - this.systemGrantsByGroupUid, - )) { - byIssuer.system[gUid] = { - ...(byIssuer.system[gUid] ?? {}), - ...perms, - }; - } - } - - for (const issuerUsername of Object.keys(byIssuer)) { - const issuerUser = - await this.stores.user.getByUsername(issuerUsername); - if (!issuerUser) continue; - const issuerActor = this.#userToActor(issuerUser); - const issuerGroups = byIssuer[issuerUsername]; - - for (const groupUid of Object.keys(issuerGroups)) { - if (!groupByUid[groupUid]) continue; - const issuerGroupPerms = issuerGroups[groupUid]; - - for (const permission of options) { - if ( - !Object.prototype.hasOwnProperty.call( - issuerGroupPerms, - permission, - ) - ) - continue; - const issuerReading = await this.scan( - issuerActor, - permission, - ); - reading.push({ - $: 'path', - via: 'hc-user-group', - has_terminal: readingHasTerminal(issuerReading), - permission, - data: issuerGroupPerms[permission], - holder_username: actor.user.username, - issuer_username: issuerUsername, - reading: issuerReading, - group_id: groupByUid[groupUid].id, - }); - } - } - } - } - async #scanUserGroup( actor: Actor, options: string[], diff --git a/src/backend/testUtil.ts b/src/backend/testUtil.ts index b86bffb30..2aaea3bf6 100644 --- a/src/backend/testUtil.ts +++ b/src/backend/testUtil.ts @@ -243,8 +243,9 @@ export const createTestUser = async ( requires_email_confirmation: false, }); - // Base driver permissions (kv, notifications, …) are system grants on - // the default user group — membership is what a verified signup gets. + // Driver permissions no longer come from here (see + // `default_user_permissions`), but group-issued ACL grants do, and a + // verified signup is a member of the default user group. const { default_user_group } = await loadDefaultConfig(); if (default_user_group) { await server.stores.group.addUsers(default_user_group, [opts.username]);