mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-25 07:27:04 +00:00
refactor(permissions): drop hardcoded group permission map for a flat default
This commit is contained in:
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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:<perm>', async () => {
|
||||
const { user, actor } = await makeUserActor();
|
||||
const perm = `service:manage-test-${uuidv4()}:ii:read`;
|
||||
const perm = `zztest:manage-test-${uuidv4()}:ii:read`;
|
||||
// Grant manage:<perm> 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:<permission>', 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:<perm> 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<Actor> => {
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -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<string, unknown>
|
||||
> = {};
|
||||
|
||||
// -- 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<string> {
|
||||
@@ -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<string, unknown>)[
|
||||
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<void> {
|
||||
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<string, { id: number; uid: string }> = {};
|
||||
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<string, Record<string, unknown>>
|
||||
>;
|
||||
const hasRuntimeGrants =
|
||||
Object.keys(this.systemGrantsByGroupUid).length > 0;
|
||||
const byIssuer: Record<
|
||||
string,
|
||||
Record<string, Record<string, unknown>>
|
||||
> = 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[],
|
||||
|
||||
@@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user