From f2f3d6ff460932698fb8da7309fbce3e96132950 Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Wed, 19 Jun 2024 23:35:41 -0400 Subject: [PATCH] feat: add protected apps --- packages/backend/src/CoreModule.js | 11 +++ packages/backend/src/definitions/Library.js | 7 ++ packages/backend/src/libraries/ArrayUtil.js | 77 +++++++++++++++++ .../src/om/entitystorage/ProtectedAppES.js | 82 +++++++++++++++++++ .../src/services/ProtectedAppService.js | 44 ++++++++++ 5 files changed, 221 insertions(+) create mode 100644 packages/backend/src/definitions/Library.js create mode 100644 packages/backend/src/libraries/ArrayUtil.js create mode 100644 packages/backend/src/om/entitystorage/ProtectedAppES.js create mode 100644 packages/backend/src/services/ProtectedAppService.js diff --git a/packages/backend/src/CoreModule.js b/packages/backend/src/CoreModule.js index ce9a04ea8..9ec245baa 100644 --- a/packages/backend/src/CoreModule.js +++ b/packages/backend/src/CoreModule.js @@ -18,6 +18,7 @@ */ const { AdvancedBase } = require("@heyputer/puter-js-common"); const { NotificationES } = require("./om/entitystorage/NotificationES"); +const { ProtectedAppES } = require("./om/entitystorage/ProtectedAppES"); const { Context } = require('./util/context'); @@ -51,6 +52,12 @@ const install = async ({ services, app, useapi }) => { def('puter.middlewares.auth', require('./middleware/auth2')); }); + + // === LIBRARIES === + const ArrayUtil = require('./libraries/ArrayUtil'); + services.registerService('util-array', ArrayUtil); + + // === SERVICES === // /!\ IMPORTANT /!\ // For new services, put the import immediate above the @@ -153,6 +160,7 @@ const install = async ({ services, app, useapi }) => { WriteByOwnerOnlyES, ValidationES, SetOwnerES, + ProtectedAppES, MaxLimitES, { max: 5000 }, ]), }); @@ -269,6 +277,9 @@ const install = async ({ services, app, useapi }) => { const { NotificationService } = require('./services/NotificationService'); services.registerService('notification', NotificationService); + + const { ProtectedAppService } = require('./services/ProtectedAppService'); + services.registerService('__protected-app', ProtectedAppService); } const install_legacy = async ({ services }) => { diff --git a/packages/backend/src/definitions/Library.js b/packages/backend/src/definitions/Library.js new file mode 100644 index 000000000..10e16cf5d --- /dev/null +++ b/packages/backend/src/definitions/Library.js @@ -0,0 +1,7 @@ +const BaseService = require("../services/BaseService"); + +class Library extends BaseService { + // +} + +module.exports = Library; diff --git a/packages/backend/src/libraries/ArrayUtil.js b/packages/backend/src/libraries/ArrayUtil.js new file mode 100644 index 000000000..20da4c73c --- /dev/null +++ b/packages/backend/src/libraries/ArrayUtil.js @@ -0,0 +1,77 @@ +const Library = require("../definitions/Library"); + +class ArrayUtil extends Library { + /** + * + * @param {*} marked_map + * @param {*} subject + */ + remove_marked_items (marked_map, subject) { + for ( let i=0 ; i < marked_map.length ; i++ ) { + let ii = marked_map[i]; + // track: type check + if ( ! Number.isInteger(ii) ) { + throw new Error( + 'marked_map can only contain integers' + ); + } + // track: bounds check + if ( ii < 0 && ii >= subject.length ) { + throw new Error( + 'each item in `marked_map` must be within that bounds ' + + 'of `subject`' + ); + } + } + + marked_map.sort((a, b) => b - a); + + for ( let i=0 ; i < marked_map.length ; i++ ) { + let ii = marked_map[i]; + subject.splice(ii, 1); + } + + return subject; + } + + _test ({ assert }) { + // inner indices + { + const subject = [ + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']; + // 0 1 2 3 4 5 6 7 + const marked_map = [2, 5]; + this.remove_marked_items(marked_map, subject); + assert(() => subject.join('') === 'abdegh'); + } + // left edge + { + const subject = [ + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']; + // 0 1 2 3 4 5 6 7 + const marked_map = [0] + this.remove_marked_items(marked_map, subject); + assert(() => subject.join('') === 'bcdefgh'); + } + // right edge + { + const subject = [ + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']; + // 0 1 2 3 4 5 6 7 + const marked_map = [7] + this.remove_marked_items(marked_map, subject); + assert(() => subject.join('') === 'abcdefg'); + } + // both edges + { + const subject = [ + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']; + // 0 1 2 3 4 5 6 7 + const marked_map = [0, 7] + this.remove_marked_items(marked_map, subject); + assert(() => subject.join('') === 'bcdefg'); + } + } +} + +module.exports = ArrayUtil; diff --git a/packages/backend/src/om/entitystorage/ProtectedAppES.js b/packages/backend/src/om/entitystorage/ProtectedAppES.js new file mode 100644 index 000000000..8647cebc9 --- /dev/null +++ b/packages/backend/src/om/entitystorage/ProtectedAppES.js @@ -0,0 +1,82 @@ +const { AppUnderUserActorType, UserActorType } = require("../../services/auth/Actor"); +const { Context } = require("../../util/context"); +const { BaseES } = require("./BaseES"); + +class ProtectedAppES extends BaseES { + async select (options){ + const results = await this.upstream.select(options); + + const actor = Context.get('actor'); + const services = Context.get('services'); + + const to_delete = []; + for ( let i=0 ; i < results.length ; i++ ) { + const entity = results[i]; + + if ( ! await this.check_({ actor, services }, entity) ) { + continue; + } + + to_delete.push(i); + } + + const svc_utilArray = services.get('util-array'); + svc_utilArray.remove_marked_items(to_delete, results); + + return results; + } + + async read (uid){ + const entity = await this.upstream.read(uid); + if ( ! entity ) return null; + + const actor = Context.get('actor'); + const services = Context.get('services'); + + if ( await this.check_({ actor, services }, entity) ) { + return null; + } + + return entity; + } + + /** + * returns true if the entity should not be sent downstream + */ + async check_ ({ actor, services }, entity) { + // track: ruleset + { + // if it's not a protected app, no worries + if ( ! await entity.get('protected') ) return; + + // if actor is this app, no worries + if ( + actor.type instanceof AppUnderUserActorType && + await entity.get('uid') === actor.type.app.uid + ) return; + + // if actor is owner of this app, no worries + if ( + actor.type instanceof UserActorType && + (await entity.get('owner')).id === actor.type.user.id + ) return; + } + + // now we need to check for permission + const app_uid = await entity.get('uid'); + const svc_permission = services.get('permission'); + const permission_to_check = `app:uid#${app_uid}:access`; + const perm = await svc_permission.check( + actor, permission_to_check, + ); + + if ( perm ) return; + + // `true` here means "do not send downstream" + return true; + } +}; + +module.exports = { + ProtectedAppES, +}; diff --git a/packages/backend/src/services/ProtectedAppService.js b/packages/backend/src/services/ProtectedAppService.js new file mode 100644 index 000000000..a1a68e4ff --- /dev/null +++ b/packages/backend/src/services/ProtectedAppService.js @@ -0,0 +1,44 @@ +const { get_app } = require("../helpers"); +const { UserActorType } = require("./auth/Actor"); +const { PermissionImplicator, PermissionUtil } = require("./auth/PermissionService"); +const BaseService = require("./BaseService"); + +class ProtectedAppService extends BaseService { + async _init () { + const svc_permission = this.services.get('permission'); + + // track: object description in comment + // Owner of procted app has implicit permission to access it + svc_permission.register_implicator(PermissionImplicator.create({ + matcher: permission => { + return permission.startsWith('app:'); + }, + checker: async ({ actor, permission }) => { + if ( !(actor.type instanceof UserActorType) ) { + return undefined; + } + + const parts = PermissionUtil.split(permission); + if ( parts.length !== 3 ) return undefined; + + const [_, uid_part, lvl] = parts; + if ( lvl !== 'access' ) return undefined; + + // track: slice a prefix + const uid = uid_part.slice('uid#'.length); + + const app = await get_app({ uid }); + + if ( app.owner_user_id !== actor.type.user.id ) { + return undefined; + } + + return {}; + }, + })); + } +} + +module.exports = { + ProtectedAppService, +};