diff --git a/src/backend/src/routers/auth/grant-user-group.js b/src/backend/src/routers/auth/grant-user-group.js index 55ace1dbbd..98f5e9d076 100644 --- a/src/backend/src/routers/auth/grant-user-group.js +++ b/src/backend/src/routers/auth/grant-user-group.js @@ -18,7 +18,8 @@ */ const APIError = require("../../api/APIError"); const eggspress = require("../../api/eggspress"); -const { UserActorType } = require("../../services/auth/Actor"); +const { UserActorType, AppUnderUserActorType } = require("../../services/auth/Actor"); +const { PermissionUtil } = require("../../services/auth/PermissionService"); const { Context } = require("../../util/context"); module.exports = eggspress('/auth/grant-user-group', { @@ -31,8 +32,21 @@ module.exports = eggspress('/auth/grant-user-group', { // Only users can grant user-group permissions const actor = Context.get('actor'); - if ( ! (actor.type instanceof UserActorType) ) { - throw APIError.create('forbidden'); + + // Check for permission to configure permissions + { + if ( ! ( + actor.type instanceof UserActorType || + actor.type instanceof AppUnderUserActorType + ) ) throw APIError.create('forbidden'); + + const perm = PermissionUtil.join( + 'permission', 'config', actor.type.user.uuid, + ...PermissionUtil.split(req.body.permission), + ); + if ( ! await svc_permission.check(actor, perm) ) { + throw APIError.create('forbidden'); + } } if ( ! req.body.group_uid ) { diff --git a/src/backend/src/routers/auth/grant-user-user.js b/src/backend/src/routers/auth/grant-user-user.js index 302f3fce22..df988041b1 100644 --- a/src/backend/src/routers/auth/grant-user-user.js +++ b/src/backend/src/routers/auth/grant-user-user.js @@ -18,7 +18,8 @@ */ const APIError = require("../../api/APIError"); const eggspress = require("../../api/eggspress"); -const { UserActorType } = require("../../services/auth/Actor"); +const { UserActorType, AppUnderUserActorType } = require("../../services/auth/Actor"); +const { PermissionUtil } = require("../../services/auth/PermissionService"); const { Context } = require("../../util/context"); module.exports = eggspress('/auth/grant-user-user', { @@ -29,10 +30,22 @@ module.exports = eggspress('/auth/grant-user-user', { const x = Context.get(); const svc_permission = x.get('services').get('permission'); - // Only users can grant user-user permissions const actor = Context.get('actor'); - if ( ! (actor.type instanceof UserActorType) ) { - throw APIError.create('forbidden'); + + // Check for permission to configure permissions + { + if ( ! ( + actor.type instanceof UserActorType || + actor.type instanceof AppUnderUserActorType + ) ) throw APIError.create('forbidden'); + + const perm = PermissionUtil.join( + 'permission', 'config', actor.type.user.uuid, + ...PermissionUtil.split(req.body.permission), + ); + if ( ! await svc_permission.check(actor, perm) ) { + throw APIError.create('forbidden'); + } } if ( ! req.body.target_username ) { diff --git a/src/backend/src/routers/auth/revoke-user-group.js b/src/backend/src/routers/auth/revoke-user-group.js index 3e1eb30fe5..584fa053e7 100644 --- a/src/backend/src/routers/auth/revoke-user-group.js +++ b/src/backend/src/routers/auth/revoke-user-group.js @@ -18,7 +18,8 @@ */ const APIError = require("../../api/APIError"); const eggspress = require("../../api/eggspress"); -const { UserActorType } = require("../../services/auth/Actor"); +const { UserActorType, AppUnderUserActorType } = require("../../services/auth/Actor"); +const { PermissionUtil } = require("../../services/auth/PermissionService"); const { Context } = require("../../util/context"); module.exports = eggspress('/auth/revoke-user-group', { @@ -29,10 +30,22 @@ module.exports = eggspress('/auth/revoke-user-group', { const x = Context.get(); const svc_permission = x.get('services').get('permission'); - // Only users can grant user-user permissions const actor = Context.get('actor'); - if ( ! (actor.type instanceof UserActorType) ) { - throw APIError.create('forbidden'); + + // Check for permission to configure permissions + { + if ( ! ( + actor.type instanceof UserActorType || + actor.type instanceof AppUnderUserActorType + ) ) throw APIError.create('forbidden'); + + const perm = PermissionUtil.join( + 'permission', 'config', actor.type.user.uuid, + ...PermissionUtil.split(req.body.permission), + ); + if ( ! await svc_permission.check(actor, perm) ) { + throw APIError.create('forbidden'); + } } if ( ! req.body.group_uid ) { diff --git a/src/backend/src/routers/auth/revoke-user-user.js b/src/backend/src/routers/auth/revoke-user-user.js index 0f48f7ba28..3c5383469c 100644 --- a/src/backend/src/routers/auth/revoke-user-user.js +++ b/src/backend/src/routers/auth/revoke-user-user.js @@ -18,7 +18,8 @@ */ const APIError = require("../../api/APIError"); const eggspress = require("../../api/eggspress"); -const { UserActorType } = require("../../services/auth/Actor"); +const { UserActorType, AppUnderUserActorType } = require("../../services/auth/Actor"); +const { PermissionUtil } = require("../../services/auth/PermissionService"); const { Context } = require("../../util/context"); module.exports = eggspress('/auth/revoke-user-user', { @@ -29,10 +30,22 @@ module.exports = eggspress('/auth/revoke-user-user', { const x = Context.get(); const svc_permission = x.get('services').get('permission'); - // Only users can grant user-user permissions const actor = Context.get('actor'); - if ( ! (actor.type instanceof UserActorType) ) { - throw APIError.create('forbidden'); + + // Check for permission to configure permissions + { + if ( ! ( + actor.type instanceof UserActorType || + actor.type instanceof AppUnderUserActorType + ) ) throw APIError.create('forbidden'); + + const perm = PermissionUtil.join( + 'permission', 'config', actor.type.user.uuid, + ...PermissionUtil.split(req.body.permission), + ); + if ( ! await svc_permission.check(actor, perm) ) { + throw APIError.create('forbidden'); + } } if ( ! req.body.target_username ) { diff --git a/src/backend/src/services/auth/ACLService.js b/src/backend/src/services/auth/ACLService.js index a805026a71..5c6dc06cf5 100644 --- a/src/backend/src/services/auth/ACLService.js +++ b/src/backend/src/services/auth/ACLService.js @@ -19,14 +19,14 @@ */ const APIError = require("../../api/APIError"); const FSNodeParam = require("../../api/filesystem/FSNodeParam"); -const { NodePathSelector } = require("../../filesystem/node/selectors"); +const { NodePathSelector, NodeUIDSelector } = require("../../filesystem/node/selectors"); const { get_user } = require("../../helpers"); const configurable_auth = require("../../middleware/configurable_auth"); const { Context } = require("../../util/context"); const { Endpoint } = require("../../util/expressutil"); const BaseService = require("../BaseService"); const { AppUnderUserActorType, UserActorType, Actor, SystemActorType, AccessTokenActorType } = require("./Actor"); -const { PermissionUtil } = require("./PermissionService"); +const { PermissionUtil, PermissionImplicator } = require("./PermissionService"); /** @@ -58,6 +58,79 @@ class ACLService extends BaseService { $: 'config-flag', value: this.global_config.enable_public_folders ?? false, }); + + const svc_fs = this.services.get('filesystem'); + + const svc_permission = this.services.get('permission'); + svc_permission.register_implicator(PermissionImplicator.create({ + id: 'apps-can-config-perms-on-appdata', + matcher: permission => { + console.log('\x1B[32;1m 1245124512451245', permission); + return permission.startsWith('permission:config:'); + }, + checker: async ({ actor, permission }) => { + if ( !(actor.type instanceof AppUnderUserActorType) ) { + return undefined; + } + + const [_0, _1, userUUID, ...innerPerm] = PermissionUtil.split(permission); + + // The current actor must be the permission issuer of the + // permission being configured, otherwise this implicator + // is not applicable. + if ( actor.type.user.uuid !== userUUID ) { + return undefined; + } + + + // For this permission implicator we're only concerned with + // `fs:`-scoped permissions that specify a node. + if ( innerPerm.length < 2 || innerPerm[0] !== 'fs' ) { + return undefined; + } + + const node = await svc_fs.node( + innerPerm[1].startsWith('/') + ? new NodePathSelector(innerPerm[1]) + : new NodeUIDSelector(innerPerm[1]) + ); + + if ( ! await node.exists() ) { + return undefined; + } + + const owner_id = await node.get('user_id'); + + // These conditions should never happen + if ( ! owner_id || ! actor.type.user.id ) { + throw new Error( + 'something unexpected happened' + ); + } + + // If this file isn't owner by the current user, we're going to + // play it safe and not involve this permission implicator. + if ( owner_id !== actor.type.user.id ) { + return undefined; + } + + // Is this file in the current app's AppData subdirectory? + // If it is, the app is allowed to configure permissions on it + const appdata_path = `/${actor.type.user.username}/AppData/${actor.type.app.uid}`; + const appdata_node = await svc_fs.node(new NodePathSelector(appdata_path)); + + if ( + await appdata_node.exists() && ( + await appdata_node.is(node) || + await appdata_node.is_above(node) + ) + ) { + return true; + } + + return undefined; + }, + })); } /** * Checks if an actor has permission to perform a specific mode of access on a resource diff --git a/src/backend/src/services/auth/PermissionService.js b/src/backend/src/services/auth/PermissionService.js index ea375fe9ed..37a89231d5 100644 --- a/src/backend/src/services/auth/PermissionService.js +++ b/src/backend/src/services/auth/PermissionService.js @@ -286,6 +286,26 @@ class PermissionService extends BaseService { async _init () { this.db = this.services.get('database').get(DB_WRITE, 'permissions'); this._register_commands(this.services.get('commands')); + + // Register permission implicator for permission to configure permissions + // (yes, this part is incredibly meta) + const svc_permission = this; + svc_permission.register_implicator(PermissionImplicator.create({ + id: 'user-can-configure-perms-from-self', + matcher: permission => { + return permission.startsWith('permission:config:'); + }, + checker: async ({ actor, permission }) => { + // This permission is only implied to user actors + if ( ! (actor.type instanceof UserActorType) ) return undefined; + + // Granted if user UUID for "from" user matches + const [_0, _1, fromUserUID] = PermissionUtil.split(permission); + if ( fromUserUID === actor.type.user.uuid ) return {}; + + return undefined; + } + })) }