dev(perms): [+] apps-of-user:<user-uuid>: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.)
This commit is contained in:
KernelDeimos
2025-12-01 17:14:41 -05:00
committed by Eric Dubé
parent a71fa826b3
commit b42a106676
@@ -16,6 +16,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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 = {