mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-24 15:07:17 +00:00
feat(permissions): let manage answer a write check
A manage grant let its holder re-share a folder but not work in it: the ACL mode family stops at write, and the fs exploder had no rule for the narrowest mode, so `manage:fs:<uid>` never satisfied `fs:<uid>:write`. Fold manage into the candidate list for every non-manage mode, in both the access-token branch and the scan branch, and give `write` an (empty) exploder rule so the manage arm is emitted for it too.
This commit is contained in:
@@ -522,6 +522,39 @@ describe('ACLService.check — stronger modes imply weaker ones', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it.each(['see', 'list', 'read', 'write'] as const)(
|
||||
'answers %s from a manage grant on the same node',
|
||||
async (mode) => {
|
||||
const { service, services } = makeService();
|
||||
services.permission.scan.mockImplementation(
|
||||
async (_actor: unknown, permissions: string[]) =>
|
||||
permissions.includes('manage:fs:uid\\C/other/f')
|
||||
? [{ $: 'option', key: 'k' }]
|
||||
: [],
|
||||
);
|
||||
expect(
|
||||
await service.check(issuerActor, resource('/other/f'), mode),
|
||||
).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it('answers write from a manage grant on an ancestor directory', async () => {
|
||||
const { service, services } = makeService();
|
||||
services.permission.scan.mockImplementation(
|
||||
async (_actor: unknown, permissions: string[]) =>
|
||||
permissions.includes('manage:fs:uid\\C/other')
|
||||
? [{ $: 'option', key: 'k' }]
|
||||
: [],
|
||||
);
|
||||
expect(
|
||||
await service.check(
|
||||
issuerActor,
|
||||
resource('/other/deep/file.txt'),
|
||||
'write',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('inherits access granted on an ancestor directory', async () => {
|
||||
const { service, services } = makeService();
|
||||
services.permission.scan.mockImplementation(
|
||||
@@ -569,6 +602,21 @@ describe('ACLService.check — scoped tokens and manage', () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts a manage grant recorded against the token for a write', async () => {
|
||||
const { service, stores } = makeService();
|
||||
stores.permission.hasAccessTokenPerm.mockImplementation(
|
||||
async (_uid: string, permission: string) =>
|
||||
permission === 'manage:fs:uid\\C/issuer/projects',
|
||||
);
|
||||
expect(
|
||||
await service.check(
|
||||
scopedTokenActor(),
|
||||
resource('/issuer/projects'),
|
||||
'write',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts an ancestor grant recorded against the token', async () => {
|
||||
const { service, stores } = makeService();
|
||||
stores.permission.hasAccessTokenPerm.mockImplementation(
|
||||
|
||||
@@ -180,19 +180,10 @@ export class ACLService extends PuterService {
|
||||
if (actor.accessToken.fullAccess) return true;
|
||||
|
||||
for (const ancestor of ancestors) {
|
||||
const permissions =
|
||||
mode === MANAGE_PERM_PREFIX
|
||||
? [
|
||||
PermissionUtil.join(
|
||||
MANAGE_PERM_PREFIX,
|
||||
'fs',
|
||||
ancestor.uid,
|
||||
),
|
||||
]
|
||||
: MODES_ABOVE[mode].map((m) =>
|
||||
PermissionUtil.join('fs', ancestor.uid, m),
|
||||
);
|
||||
for (const permission of permissions) {
|
||||
for (const permission of this.#permissionsFor(
|
||||
ancestor.uid,
|
||||
mode,
|
||||
)) {
|
||||
if (
|
||||
await this.stores.permission.hasAccessTokenPerm(
|
||||
actor.accessToken.uid,
|
||||
@@ -227,21 +218,9 @@ export class ACLService extends PuterService {
|
||||
// Widen the scan to all "higher" modes (`write` covers `read`/`list`/
|
||||
// `see`, etc.) so granting a stronger mode implies the weaker ones.
|
||||
for (const ancestor of ancestors) {
|
||||
const permissions =
|
||||
mode === MANAGE_PERM_PREFIX
|
||||
? [
|
||||
PermissionUtil.join(
|
||||
MANAGE_PERM_PREFIX,
|
||||
'fs',
|
||||
ancestor.uid,
|
||||
),
|
||||
]
|
||||
: MODES_ABOVE[mode].map((m) =>
|
||||
PermissionUtil.join('fs', ancestor.uid, m),
|
||||
);
|
||||
const reading = await this.services.permission.scan(
|
||||
actor,
|
||||
permissions,
|
||||
this.#permissionsFor(ancestor.uid, mode),
|
||||
);
|
||||
const options = PermissionUtil.readingToOptions(reading);
|
||||
if (options.length > 0) return true;
|
||||
@@ -250,6 +229,19 @@ export class ACLService extends PuterService {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Permissions on `uid` that satisfy `mode`. `manage` sits above the whole
|
||||
* family — it answers any mode, but nothing answers it.
|
||||
*/
|
||||
#permissionsFor(uid: string, mode: AclMode): string[] {
|
||||
const manage = PermissionUtil.join(MANAGE_PERM_PREFIX, 'fs', uid);
|
||||
if (mode === MANAGE_PERM_PREFIX) return [manage];
|
||||
return [
|
||||
...MODES_ABOVE[mode].map((m) => PermissionUtil.join('fs', uid, m)),
|
||||
manage,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* When a check fails, return a user-safe error: 404 if the actor can't even
|
||||
* `see` the resource (don't leak existence), 403 otherwise.
|
||||
|
||||
@@ -2948,6 +2948,13 @@ describe('FSService permission rules', () => {
|
||||
);
|
||||
expect(higher).not.toContain(`fs:${file.uuid}:read`);
|
||||
});
|
||||
|
||||
it('lets a manage grant answer a write on the narrowest mode', async () => {
|
||||
const higher = await server.services.permission.getHigherPermissions(
|
||||
`fs:${file.uuid}:write`,
|
||||
);
|
||||
expect(higher).toContain(`manage:fs:${file.uuid}`);
|
||||
});
|
||||
});
|
||||
|
||||
// -- Cross-app AppData (app-data:<uid>:fs:<class>) ----------------------
|
||||
|
||||
@@ -359,6 +359,8 @@ export class FSService extends PuterService {
|
||||
see: ['list', 'read', 'write'],
|
||||
list: ['read', 'write'],
|
||||
read: ['write'],
|
||||
// Widens to nothing, but still emits the manage arm.
|
||||
write: [],
|
||||
};
|
||||
permissions.registerExploder({
|
||||
id: 'fs-access-levels',
|
||||
|
||||
Reference in New Issue
Block a user