dev: prepare to implement policy enforcer

This commit is contained in:
KernelDeimos 2024-07-24 23:21:31 -04:00 committed by Eric Dubé
parent e514dfcf50
commit 9e38e048c1
9 changed files with 286 additions and 56 deletions

View File

@ -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 }) => {

View File

@ -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'),
},
},
};

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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, ' '));
}
},

View File

@ -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,

View File

@ -0,0 +1,11 @@
class PolicyEnforcer {
constructor (context) {
this.context = context;
}
async check () {}
async on_success () {}
async on_fail () {}
}
module.exports = { PolicyEnforcer };

View File

@ -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: {}

View File

@ -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,
});