mirror of
https://github.com/HeyPuter/puter.git
synced 2025-01-23 22:40:20 +08:00
dev: prepare to implement policy enforcer
This commit is contained in:
parent
e514dfcf50
commit
9e38e048c1
@ -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 }) => {
|
||||
|
@ -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
src/backend/src/services/SUService.js
Normal file
35
src/backend/src/services/SUService.js
Normal 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,
|
||||
};
|
58
src/backend/src/services/SystemDataService.js
Normal file
58
src/backend/src/services/SystemDataService.js
Normal 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,
|
||||
};
|
@ -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, ' '));
|
||||
}
|
||||
},
|
||||
|
@ -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
src/backend/src/services/drivers/PolicyEnforcer.js
Normal file
11
src/backend/src/services/drivers/PolicyEnforcer.js
Normal file
@ -0,0 +1,11 @@
|
||||
class PolicyEnforcer {
|
||||
constructor (context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
async check () {}
|
||||
async on_success () {}
|
||||
async on_fail () {}
|
||||
}
|
||||
|
||||
module.exports = { PolicyEnforcer };
|
@ -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: {}
|
||||
|
@ -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,
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user