diff --git a/src/backend/src/CoreModule.js b/src/backend/src/CoreModule.js index 30d5c97c4..c320cd157 100644 --- a/src/backend/src/CoreModule.js +++ b/src/backend/src/CoreModule.js @@ -305,6 +305,12 @@ const install = async ({ services, app, useapi }) => { const { HelloWorldService } = require('./services/HelloWorldService'); services.registerService('hello-world', HelloWorldService); + + const { SystemDataService } = require('./services/SystemDataService'); + services.registerService('system-data', SystemDataService); + + const { SUService } = require('./services/SUService'); + services.registerService('su', SUService); } const install_legacy = async ({ services }) => { diff --git a/src/backend/src/data/hardcoded-permissions.js b/src/backend/src/data/hardcoded-permissions.js index 939ae1cb4..26ba332fb 100644 --- a/src/backend/src/data/hardcoded-permissions.js +++ b/src/backend/src/data/hardcoded-permissions.js @@ -62,53 +62,29 @@ const implicit_user_app_permissions = [ }, ]; +const policy_perm = selector => ({ + policy: { + $: 'json-address', + path: '/admin/.policy/drivers.json', + selector, + } +}); + const hardcoded_user_group_permissions = { system: { 'b7220104-7905-4985-b996-649fdcdb3c8f': { - 'service:helloworld:ii:helloworld': {}, - 'driver:puter-kvstore': { - $: 'json-address', - path: '/admin/.policy/drivers.json', - selector: 'temp.kv' - }, - 'driver:puter-notifications': { - $: 'json-address', - path: '/admin/.policy/drivers.json', - selector: 'temp.es' - }, - 'driver:puter-apps': { - $: 'json-address', - path: '/admin/.policy/drivers.json', - selector: 'temp.es' - }, - 'driver:puter-subdomains': { - $: 'json-address', - path: '/admin/.policy/drivers.json', - selector: 'temp.es' - }, + 'service:hello-world:ii:hello-world': policy_perm('temp.es'), + 'driver:puter-kvstore': policy_perm('temp.kv'), + 'driver:puter-notifications': policy_perm('temp.es'), + 'driver:puter-apps': policy_perm('temp.es'), + 'driver:puter-subdomains': policy_perm('temp.es'), }, '78b1b1dd-c959-44d2-b02c-8735671f9997': { - 'service:helloworld:ii:helloworld': {}, - 'driver:puter-kvstore': { - $: 'json-address', - path: '/admin/.policy/drivers.json', - selector: 'user.kv' - }, - 'driver:puter-notifications': { - $: 'json-address', - path: '/admin/.policy/drivers.json', - selector: 'user.es' - }, - 'driver:puter-apps': { - $: 'json-address', - path: '/admin/.policy/drivers.json', - selector: 'user.es' - }, - 'driver:puter-subdomains': { - $: 'json-address', - path: '/admin/.policy/drivers.json', - selector: 'user.es' - }, + 'service:hello-world:ii:hello-world': policy_perm('user.es'), + 'driver:puter-kvstore': policy_perm('user.kv'), + 'driver:puter-notifications': policy_perm('user.es'), + 'driver:puter-apps': policy_perm('user.es'), + 'driver:puter-subdomains': policy_perm('user.es'), }, }, }; diff --git a/src/backend/src/services/SUService.js b/src/backend/src/services/SUService.js new file mode 100644 index 000000000..94124532e --- /dev/null +++ b/src/backend/src/services/SUService.js @@ -0,0 +1,35 @@ +const { get_user } = require("../helpers"); +const { Context } = require("../util/context"); +const { TeePromise } = require("../util/promise"); +const { Actor, UserActorType } = require("./auth/Actor"); +const BaseService = require("./BaseService"); + +class SUService extends BaseService { + _construct () { + this.sys_user_ = new TeePromise(); + this.sys_actor_ = new TeePromise(); + } + async ['__on_boot.consolidation'] () { + const sys_user = await get_user({ username: 'system' }); + this.sys_user_.resolve(sys_user); + const sys_actor = new Actor({ + type: new UserActorType({ + user: sys_user, + }), + }); + this.sys_actor_.resolve(sys_actor); + } + async get_system_actor () { + return this.sys_actor_; + } + async sudo (callback) { + return await Context.get().sub({ + user: await this.sys_user_, + actor: await this.sys_actor_, + }).arun(callback); + } +} + +module.exports = { + SUService, +}; diff --git a/src/backend/src/services/SystemDataService.js b/src/backend/src/services/SystemDataService.js new file mode 100644 index 000000000..dd6e35a34 --- /dev/null +++ b/src/backend/src/services/SystemDataService.js @@ -0,0 +1,58 @@ +const { LLRead } = require("../filesystem/ll_operations/ll_read"); +const { Context } = require("../util/context"); +const { whatis } = require("../util/langutil"); +const { stream_to_buffer } = require("../util/streamutil"); +const BaseService = require("./BaseService"); + +class SystemDataService extends BaseService { + async _init () {} + + async interpret (data) { + if ( whatis(data) === 'object' && data.$ ) { + return await this.dereference_(data); + } + if ( whatis(data) === 'object' ) { + const new_o = {}; + for ( const k in data ) { + new_o[k] = await this.interpret(data[k]); + } + return new_o; + } + if ( whatis(data) === 'array' ) { + const new_a = []; + for ( const v of data ) { + new_a.push(await this.interpret(v)); + } + return new_a; + } + return data; + } + + async dereference_ (data) { + const svc_fs = this.services.get('filesystem'); + if ( data.$ === 'json-address' ) { + const node = await svc_fs.node(data.path); + const ll_read = new LLRead(); + const stream = await ll_read.run({ + actor: Context.get('actor'), + fsNode: node, + }); + const buffer = await stream_to_buffer(stream); + const json = buffer.toString('utf8'); + let result = JSON.parse(json); + result = await this.interpret(result); + if ( data.selector ) { + const parts = data.selector.split('.'); + for ( const part of parts ) { + result = result[part]; + } + } + return result; + } + throw new Error(`unrecognized data type: ${data.$}`); + } +} + +module.exports = { + SystemDataService, +}; diff --git a/src/backend/src/services/auth/PermissionService.js b/src/backend/src/services/auth/PermissionService.js index 3e5fa9c8a..492799557 100644 --- a/src/backend/src/services/auth/PermissionService.js +++ b/src/backend/src/services/auth/PermissionService.js @@ -155,13 +155,39 @@ class PermissionUtil { ; } - static reading_to_options (reading, options = []) { + static reading_to_options ( + // actual arguments + reading, parameters = {}, + // recursion state + options = [], extras = [], path = [], + ) { + const to_path_item = finding => ({ + key: finding.key, + holder: finding.holder_username, + data: finding.data, + }); for ( let finding of reading ) { if ( finding.$ === 'option' ) { - options.push(finding); + path = [to_path_item(finding), ...path]; + options.push({ + ...finding, + data: [ + ...(finding.data ? [finding.data] : []), + ...extras, + ], + path, + }); } if ( finding.$ === 'path' ) { - this.reading_to_options(finding.reading, options); + const new_extras = ( finding.data ) ? [ + finding.data, + ...extras, + ] : []; + const new_path = [to_path_item(finding), ...path]; + this.reading_to_options( + finding.reading, parameters, + options, new_extras, new_path, + ); } } return options; @@ -672,7 +698,7 @@ class PermissionService extends BaseService { }) let reading = await this.scan(actor, permission); - // reading = PermissionUtil.reading_to_options(reading); + reading = PermissionUtil.reading_to_options(reading); ctx.log(JSON.stringify(reading, undefined, ' ')); } }, diff --git a/src/backend/src/services/drivers/DriverService.js b/src/backend/src/services/drivers/DriverService.js index 03a99b9d7..8ce8dd4e8 100644 --- a/src/backend/src/services/drivers/DriverService.js +++ b/src/backend/src/services/drivers/DriverService.js @@ -23,6 +23,7 @@ const { TypedValue } = require("./meta/Runtime"); const BaseService = require("../BaseService"); const { Driver } = require("../../definitions/Driver"); const { PermissionUtil } = require("../auth/PermissionService"); +const { PolicyEnforcer } = require("./PolicyEnforcer"); /** * DriverService provides the functionality of Puter drivers. @@ -129,17 +130,87 @@ class DriverService extends BaseService { const service = this.services.get(driver); const reading = await svc_permission.scan( actor, - PermissionUtil.join('driver', driver, 'ii', iface), + PermissionUtil.join('service', driver, 'ii', iface), ); + console.log({ + perm: PermissionUtil.join('service', driver, 'ii', iface), + reading, + }); const options = PermissionUtil.reading_to_options(reading); if ( options.length > 0 ) { - return await this.call_new_({ - service_name: driver, - service, - method, - args: processed_args, - iface, + const option = await this.select_best_option_(options); + const policies = await this.get_policies_for_option_(option); + console.log('SLA', JSON.stringify(policies, undefined, ' ')); + + // NOT FINAL: For now we apply monthly usage logic + // to the first holder of the permission. Later this + // will be changed so monthly usage can cascade across + // multiple actors. I decided not to implement this + // immediately because it's a hefty time sink and it's + // going to be some time before we can offer this feature + // to the end-user either way. + + let effective_policy = null; + for ( const policy of policies ) { + if ( policy.holder ) { + effective_policy = policy; + break; + } + } + + if ( ! effective_policy ) { + throw new Error( + 'policies with no effective user are not yet ' + + 'supported' + ); + } + + // NOT FINAL: this will be handled by 'get_policies_for_option_' + // when cascading monthly usage is implemented. + const svc_systemData = this.services.get('system-data'); + const svc_su = this.services.get('su'); + effective_policy = await svc_su.sudo(async () => { + return await svc_systemData.interpret(effective_policy.data); }); + + effective_policy = effective_policy.policy; + + console.log('EFFECTIVE', + JSON.stringify(effective_policy, undefined, ' ')); + + const policy_enforcer = new PolicyEnforcer({ + services: this.services, + actor, + policy: effective_policy, + driver, method, + }); + + try { + await policy_enforcer.check(); + const result = await this.call_new_({ + service_name: driver, + service, + method, + args: processed_args, + iface, + }); + await policy_enforcer.on_success(); + return result; + } catch (e) { + policy_enforcer.on_fail(); + console.error(e); + let for_user = (e instanceof APIError) || (e instanceof DriverError); + if ( ! for_user ) this.errors.report(`driver:${iface}:${method}`, { + source: e, + trace: true, + // TODO: alarm will not be suitable for all errors. + alarm: true, + extra: { + args, + } + }); + return this._driver_response_from_error(e, meta); + } } } @@ -196,6 +267,32 @@ class DriverService extends BaseService { } } + async get_policies_for_option_ (option) { + // NOT FINAL: before implementing cascading monthly usage, + // this return will be removed and the code below it will + // be uncommented + return option.path; + /* + const svc_systemData = this.services.get('system-data'); + const svc_su = this.services.get('su'); + + const policies = await Promise.all(option.path.map(async path_node => { + const policy = await svc_su.sudo(async () => { + return await svc_systemData.interpret(option.data); + }); + return { + ...path_node, + policy, + }; + })); + return policies; + */ + } + + async select_best_option_ (options) { + return options[0]; + } + async call_new_ ({ service_name, service, method, args, diff --git a/src/backend/src/services/drivers/PolicyEnforcer.js b/src/backend/src/services/drivers/PolicyEnforcer.js new file mode 100644 index 000000000..5b2347c89 --- /dev/null +++ b/src/backend/src/services/drivers/PolicyEnforcer.js @@ -0,0 +1,11 @@ +class PolicyEnforcer { + constructor (context) { + this.context = context; + } + + async check () {} + async on_success () {} + async on_fail () {} +} + +module.exports = { PolicyEnforcer }; diff --git a/src/backend/src/structured/sequence/scan-permission.js b/src/backend/src/structured/sequence/scan-permission.js index 19f333d56..b610b970e 100644 --- a/src/backend/src/structured/sequence/scan-permission.js +++ b/src/backend/src/structured/sequence/scan-permission.js @@ -25,14 +25,15 @@ const { PERMISSION_SCANNERS } = require("../../unstructured/permission-scanners" module.exports = new Sequence([ async function grant_if_system (a) { const reading = a.get('reading'); - const { actor } = a.values(); + const { actor, permission_options } = a.values(); if ( !(actor.type instanceof UserActorType) ) { return; } if ( actor.type.user.username === 'system' ) { reading.push({ $: 'option', - permission: '*', + key: `sys`, + permission: permission_options[0], source: 'implied', by: 'system', data: {} diff --git a/src/backend/src/unstructured/permission-scanners.js b/src/backend/src/unstructured/permission-scanners.js index 7f8b1d58c..f9c3dd4f2 100644 --- a/src/backend/src/unstructured/permission-scanners.js +++ b/src/backend/src/unstructured/permission-scanners.js @@ -74,6 +74,11 @@ const PERMISSION_SCANNERS = [ // Return the first matching permission where the // issuer also has the permission granted for ( const row of rows ) { + row.extra = db.case({ + mysql: () => row.extra, + otherwise: () => JSON.parse(row.extra ?? '{}') + })(); + const issuer_actor = new Actor({ type: new UserActorType({ user: await get_user({ id: row.issuer_user_id }), @@ -86,7 +91,8 @@ const PERMISSION_SCANNERS = [ $: 'path', via: 'user', permission: row.permission, - // issuer: issuer_actor, + data: row.extra, + holder_username: actor.type.user.username, issuer_username: issuer_actor.type.user.username, reading: issuer_reading, }); @@ -132,6 +138,8 @@ const PERMISSION_SCANNERS = [ $: 'path', via: 'hc-user-group', permission, + data: issuer_group[permission], + holder_username: actor.type.user.username, issuer_username, reading: issuer_reading, group_id: group_uids[group_uid].id, @@ -167,6 +175,11 @@ const PERMISSION_SCANNERS = [ ); for ( const row of rows ) { + row.extra = db.case({ + mysql: () => row.extra, + otherwise: () => JSON.parse(row.extra ?? '{}') + })(); + const issuer_actor = new Actor({ type: new UserActorType({ user: await get_user({ id: row.user_id }), @@ -180,6 +193,8 @@ const PERMISSION_SCANNERS = [ via: 'user-group', // issuer: issuer_actor, permission: row.permission, + data: row.extra, + holder_username: actor.type.user.username, issuer_username: issuer_actor.type.user.username, reading: issuer_reading, group_id: row.group_id, @@ -246,12 +261,17 @@ const PERMISSION_SCANNERS = [ if ( rows[0] ) { const row = rows[0]; + row.extra = db.case({ + mysql: () => row.extra, + otherwise: () => JSON.parse(row.extra ?? '{}') + })(); const issuer_actor = actor.get_related_actor(UserActorType); const issuer_reading = await a.icall('scan', issuer_actor, row.permission); reading.push({ $: 'path', via: 'user-app', permission: row.permission, + data: row.extra, issuer_username: actor.type.user.username, reading: issuer_reading, });