From b42a10667642241f231f21c7192524e37bb763cd Mon Sep 17 00:00:00 2001 From: KernelDeimos <7225168+KernelDeimos@users.noreply.github.com> Date: Sun, 30 Nov 2025 18:39:18 -0500 Subject: [PATCH] dev(perms): [+] `apps-of-user::write` This commit also adds a new check to verify that the app which owns the entity being edited matches. Previously this was not necessary because a read on the same entity would have always performed the same check and caused this operation to stop early. (now that an app may have permission to read entities created by other apps, this is no longer the case.) --- .../src/om/entitystorage/AppLimitedES.js | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/backend/src/om/entitystorage/AppLimitedES.js b/src/backend/src/om/entitystorage/AppLimitedES.js index c98effb7a..771e52295 100644 --- a/src/backend/src/om/entitystorage/AppLimitedES.js +++ b/src/backend/src/om/entitystorage/AppLimitedES.js @@ -16,6 +16,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +const APIError = require('../../api/APIError'); const { AppUnderUserActorType } = require('../../services/auth/Actor'); const { PermissionUtil } = require('../../services/auth/permissionUtils.mjs'); const { Context } = require('../../util/context'); @@ -25,6 +26,8 @@ const { Entity } = require('./Entity'); class AppLimitedES extends BaseES { + // #region read operations + // Limit selection to entities owned by the app of the current actor. async select (options) { const actor = Context.get('actor'); @@ -90,6 +93,57 @@ class AppLimitedES extends BaseES { return entity; } + + // #endregion + + // #region write operations + + // Limit edit to entities owned by the app of the current actor + async upsert (entity, extra) { + const actor = Context.get('actor'); + if ( actor.type instanceof AppUnderUserActorType ) { + const { old_entity } = extra; + if ( old_entity ) { + await this._check_edit_allowed({ old_entity }); + } + } + return await this.upstream.upsert(entity, extra); + } + async delete (uid, extra) { + const actor = Context.get('actor'); + if ( actor.type instanceof AppUnderUserActorType ) { + const { old_entity } = extra; + await this._check_edit_allowed({ old_entity }); + } + return await this.upstream.delete(uid, extra); + } + async _check_edit_allowed ({ old_entity }) { + const actor = Context.get('actor'); + + // Maybe the app has been granted write access to all the user's apps + // (in which case we return early) + { + const svc_permission = Context.get('services').get('permission'); + const perm = PermissionUtil.join('apps-of-user', actor.type.user.uuid, 'write'); + const can_write_any = await svc_permission.check(actor, perm); + if ( can_write_any ) return; + } + + // Otherwise, verify the app owner + // (or we throw an APIError) + { + const app = actor.type.app; + const app_owner = await old_entity.get('app_owner'); + let app_owner_id = app_owner?.id; + if ( app_owner instanceof Entity ) { + app_owner_id = app_owner.private_meta.mysql_id; + } + if ( ( !app_owner ) || app_owner_id !== app.id ) { + throw APIError.create('forbidden'); + } + } + } + // #endregion } module.exports = {