mirror of
https://github.com/HeyPuter/puter.git
synced 2026-09-26 23:26:04 +00:00
fix(data-access): add guard for app actors
This commit is contained in:
@@ -494,6 +494,26 @@ export default class AppService extends BaseService {
|
||||
return result.insertId;
|
||||
}
|
||||
|
||||
async #check_app_owner_permission (old_app, actor) {
|
||||
// Check if app has write permission to all user's apps
|
||||
const svc_permission = this.services.get('permission');
|
||||
const user = actor.type.user;
|
||||
const perm = `es:app:${user.uuid}:write`;
|
||||
const can_write_any = await svc_permission.check(actor, perm);
|
||||
if ( can_write_any ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise verify the app owns this entity
|
||||
const app = actor.type.app;
|
||||
const app_owner = old_app.app_owner;
|
||||
const app_owner_uid = app_owner?.uid;
|
||||
|
||||
if ( ! app_owner_uid || app_owner_uid !== app.uid ) {
|
||||
throw APIError.create('forbidden');
|
||||
}
|
||||
}
|
||||
|
||||
async #update ({ object, id, options }) {
|
||||
const old_app = await this.#read({
|
||||
uid: object.uid,
|
||||
@@ -515,6 +535,11 @@ export default class AppService extends BaseService {
|
||||
// Check owner permission (WriteByOwnerOnlyES behavior)
|
||||
await this.#check_owner_permission(old_app);
|
||||
|
||||
// If actor is AppUnderUserActorType, check app_owner (AppLimitedES behavior)
|
||||
if ( actor.type instanceof AppUnderUserActorType ) {
|
||||
await this.#check_app_owner_permission(old_app, actor);
|
||||
}
|
||||
|
||||
// Remove protected/read_only fields from the update (ValidationES behavior)
|
||||
{
|
||||
object = { ...object };
|
||||
|
||||
@@ -1195,6 +1195,62 @@ describe('AppService', () => {
|
||||
expect.arrayContaining(['https://mysite.puter.site'])
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw forbidden when app actor does not own the entity (AppLimitedES behavior)', async () => {
|
||||
// App actor trying to update an app it didn't create
|
||||
setupContextForWrite(createMockAppUnderUserActor(1, 999));
|
||||
mockDb.read.mockResolvedValue([createMockAppRow({
|
||||
owner_user_id: 1,
|
||||
app_owner_uid: 'different-app-uid',
|
||||
})]);
|
||||
|
||||
const crudQ = AppService.IMPLEMENTS['crud-q'];
|
||||
|
||||
await expect(crudQ.update.call(appService, {
|
||||
object: { uid: 'app-uid-123', title: 'Hacked Title' },
|
||||
})).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should allow app actor to update entity it owns (AppLimitedES behavior)', async () => {
|
||||
// App actor updating an app it created
|
||||
const actor = createMockAppUnderUserActor(1, 100);
|
||||
actor.type.app.uid = 'creator-app-uid';
|
||||
setupContextForWrite(actor);
|
||||
mockDb.read.mockResolvedValue([createMockAppRow({
|
||||
owner_user_id: 1,
|
||||
app_owner_uid: 'creator-app-uid',
|
||||
})]);
|
||||
|
||||
const crudQ = AppService.IMPLEMENTS['crud-q'];
|
||||
await crudQ.update.call(appService, {
|
||||
object: { uid: 'app-uid-123', title: 'Updated by App' },
|
||||
});
|
||||
|
||||
expect(mockDbWrite.write).toHaveBeenCalledWith(
|
||||
expect.stringContaining('UPDATE apps SET'),
|
||||
expect.arrayContaining(['Updated by App'])
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow app actor with write permission to update any entity (AppLimitedES behavior)', async () => {
|
||||
setupContextForWrite(createMockAppUnderUserActor(1, 999));
|
||||
mockDb.read.mockResolvedValue([createMockAppRow({
|
||||
owner_user_id: 1,
|
||||
app_owner_uid: 'different-app-uid',
|
||||
})]);
|
||||
// Grant write permission
|
||||
mockPermissionService.check.mockResolvedValue(true);
|
||||
|
||||
const crudQ = AppService.IMPLEMENTS['crud-q'];
|
||||
await crudQ.update.call(appService, {
|
||||
object: { uid: 'app-uid-123', title: 'Admin Update' },
|
||||
});
|
||||
|
||||
expect(mockDbWrite.write).toHaveBeenCalledWith(
|
||||
expect.stringContaining('UPDATE apps SET'),
|
||||
expect.arrayContaining(['Admin Update'])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#upsert', () => {
|
||||
|
||||
Reference in New Issue
Block a user