ソースを参照

dev: prepare to implement policy enforcer

KernelDeimos 10 ヶ月 前
コミット
9e38e048c1

+ 6 - 0
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 }) => {

+ 18 - 42
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'),
         },
     },
 };

+ 35 - 0
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,
+};

+ 58 - 0
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,
+};

+ 30 - 4
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, '  '));
                 }
             },

+ 104 - 7
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,

+ 11 - 0
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 };

+ 3 - 2
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: {}

+ 21 - 1
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,
                 });