mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-26 07:57:10 +00:00
fix(perms): let a write grant satisfy a read check on apps and subdomains
`apps-of-user:<uuid>:write` covers managing the user's apps, which includes
reading them, but nothing said so to the permission system. Prefix implication
only widens the other way — an `apps-of-user:<uuid>` grant covers both modes —
so a scan for `:read` missed a `:write` grant, and `puter.perms.check('apps')`
reported an app holding write as holding nothing. A batched request would then
prompt again for access already granted.
Adds the read-from-write exploder for both namespaces, mirroring
`fs-access-levels`. The widening runs one way only, and does not cross into
another user's namespace; both are covered by tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7ec8e19bf9
commit
d8d4aeee3d
@@ -215,6 +215,92 @@ describe('AppPermissionService — own-apps / own-subdomains implicator', () =>
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
// `write` covers managing them, which includes reading them. Prefix
|
||||
// implication only widens the other way, so this needs the exploder.
|
||||
it.each([
|
||||
['apps-of-user'],
|
||||
['subdomains-of-user'],
|
||||
])('%s: a granted write satisfies a read check', async (namespace) => {
|
||||
const user = await makeUser();
|
||||
const app = await makeApp(user.user.id!);
|
||||
const appActor: Actor = {
|
||||
user: user.user,
|
||||
app: { uid: app.uid, id: app.id },
|
||||
};
|
||||
const uuid = user.user.uuid;
|
||||
|
||||
expect(
|
||||
await permissions.check(appActor, `${namespace}:${uuid}:read`),
|
||||
).toBe(false);
|
||||
|
||||
await runWithContext({ actor: user }, () =>
|
||||
permissions.grantUserAppPermission(
|
||||
user,
|
||||
app.uid,
|
||||
`${namespace}:${uuid}:write`,
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
await permissions.check(appActor, `${namespace}:${uuid}:read`),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await permissions.check(appActor, `${namespace}:${uuid}:write`),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
// The widening runs one way only: a read grant is not a licence to manage.
|
||||
it('does not let a granted read satisfy a write check', async () => {
|
||||
const user = await makeUser();
|
||||
const app = await makeApp(user.user.id!);
|
||||
const appActor: Actor = {
|
||||
user: user.user,
|
||||
app: { uid: app.uid, id: app.id },
|
||||
};
|
||||
const uuid = user.user.uuid;
|
||||
|
||||
await runWithContext({ actor: user }, () =>
|
||||
permissions.grantUserAppPermission(
|
||||
user,
|
||||
app.uid,
|
||||
`apps-of-user:${uuid}:read`,
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
await permissions.check(appActor, `apps-of-user:${uuid}:read`),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await permissions.check(appActor, `apps-of-user:${uuid}:write`),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
// Another user's namespace stays out of reach however wide the grant.
|
||||
it("does not widen into another user's namespace", async () => {
|
||||
const user = await makeUser();
|
||||
const other = await makeUser();
|
||||
const app = await makeApp(user.user.id!);
|
||||
const appActor: Actor = {
|
||||
user: user.user,
|
||||
app: { uid: app.uid, id: app.id },
|
||||
};
|
||||
|
||||
await runWithContext({ actor: user }, () =>
|
||||
permissions.grantUserAppPermission(
|
||||
user,
|
||||
app.uid,
|
||||
`apps-of-user:${user.user.uuid}:write`,
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
await permissions.check(
|
||||
appActor,
|
||||
`apps-of-user:${other.user.uuid}:read`,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// -- app-root-dir:<app_uid>:<mode> → fs:<uuid>:<mode> ------------------
|
||||
|
||||
@@ -126,6 +126,29 @@ export class AppPermissionService extends PuterService {
|
||||
},
|
||||
});
|
||||
|
||||
// -- apps-of-user:<uuid>:read ← :write --------------------------
|
||||
// `write` covers managing them, which includes reading them. Prefix
|
||||
// implication only widens the other way — an `apps-of-user:<uuid>`
|
||||
// grant covers both modes — so without this a scan for `read` misses
|
||||
// a `write` grant, and `puter.perms.check('apps')` would report an
|
||||
// app that holds write as holding nothing. Mirrors `fs-access-levels`.
|
||||
permissions.registerExploder({
|
||||
id: 'apps-of-user-access-levels',
|
||||
matches: (permission: string) =>
|
||||
(permission.startsWith('apps-of-user:') ||
|
||||
permission.startsWith('subdomains-of-user:')) &&
|
||||
PermissionUtil.split(permission).length >= 3,
|
||||
explode: ({ permission }) => {
|
||||
const parts = PermissionUtil.split(permission);
|
||||
if (parts[2] !== 'read') return [permission];
|
||||
const [namespace, userUuid, , ...rest] = parts;
|
||||
return [
|
||||
permission,
|
||||
PermissionUtil.join(namespace, userUuid, 'write', ...rest),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
// -- app-root-dir:<app_uid>:<mode> → fs:<root_uid>:<mode> -------
|
||||
// Only rewrites while a user-app permission row is being written or
|
||||
// removed — `grantUserAppPermission` / `revokeUserAppPermission`, which
|
||||
|
||||
Reference in New Issue
Block a user