mirror of
https://github.com/HeyPuter/puter
synced 2024-11-14 22:06:00 +00:00
Merge pull request #231 from HeyPuter/eric/user-to-user-permissions
User-to-User Permission Granting
This commit is contained in:
commit
c6fb75c65f
@ -109,7 +109,7 @@ class Sequence {
|
|||||||
async run (values) {
|
async run (values) {
|
||||||
// Initialize scope
|
// Initialize scope
|
||||||
values = values || this.thisArg?.values || {};
|
values = values || this.thisArg?.values || {};
|
||||||
Object.assign(this.scope_, values);
|
Object.assign(this.scope_, values); // TODO: can this be __proto__?
|
||||||
|
|
||||||
// Run sequence
|
// Run sequence
|
||||||
for ( ; this.i < this.steps.length ; this.i++ ) {
|
for ( ; this.i < this.steps.length ; this.i++ ) {
|
||||||
|
@ -34,8 +34,10 @@ const APIError = require('../api/APIError.js');
|
|||||||
const { LLMkdir } = require('./ll_operations/ll_mkdir.js');
|
const { LLMkdir } = require('./ll_operations/ll_mkdir.js');
|
||||||
const { LLCWrite, LLOWrite } = require('./ll_operations/ll_write.js');
|
const { LLCWrite, LLOWrite } = require('./ll_operations/ll_write.js');
|
||||||
const { LLCopy } = require('./ll_operations/ll_copy.js');
|
const { LLCopy } = require('./ll_operations/ll_copy.js');
|
||||||
const { PermissionUtil, PermissionRewriter } = require('../services/auth/PermissionService.js');
|
const { PermissionUtil, PermissionRewriter, PermissionImplicator } = require('../services/auth/PermissionService.js');
|
||||||
const { DB_WRITE } = require("../services/database/consts");
|
const { DB_WRITE } = require("../services/database/consts");
|
||||||
|
const { UserActorType } = require('../services/auth/Actor');
|
||||||
|
const { get_user } = require('../helpers');
|
||||||
|
|
||||||
class FilesystemService extends AdvancedBase {
|
class FilesystemService extends AdvancedBase {
|
||||||
static MODULES = {
|
static MODULES = {
|
||||||
@ -131,6 +133,39 @@ class FilesystemService extends AdvancedBase {
|
|||||||
return `fs:${uid}:${rest.join(':')}`;
|
return `fs:${uid}:${rest.join(':')}`;
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
svc_permission.register_implicator(PermissionImplicator.create({
|
||||||
|
matcher: permission => {
|
||||||
|
return permission.startsWith('fs:');
|
||||||
|
},
|
||||||
|
checker: async (actor, permission) => {
|
||||||
|
debugger;
|
||||||
|
if ( !(actor.type instanceof UserActorType) ) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [_, uid] = PermissionUtil.split(permission);
|
||||||
|
const node = await this.node(new NodeUIDSelector(uid));
|
||||||
|
|
||||||
|
if ( ! await node.exists() ) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const owner_id = await node.get('user_id');
|
||||||
|
|
||||||
|
// These conditions should never happen
|
||||||
|
if ( ! owner_id || ! actor.type.user.id ) {
|
||||||
|
throw new Error(
|
||||||
|
'something unexpected happened'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( owner_id === actor.type.user.id ) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -45,7 +45,7 @@ class HLStat extends HLFilesystemOperation {
|
|||||||
const svc_acl = context.get('services').get('acl');
|
const svc_acl = context.get('services').get('acl');
|
||||||
const actor = context.get('actor');
|
const actor = context.get('actor');
|
||||||
if ( ! await svc_acl.check(actor, subject, 'read') ) {
|
if ( ! await svc_acl.check(actor, subject, 'read') ) {
|
||||||
throw await svc_acl.get_safe_acl_error(actor, subject.entry, 'read');
|
throw await svc_acl.get_safe_acl_error(actor, subject, 'read');
|
||||||
}
|
}
|
||||||
|
|
||||||
// check permission
|
// check permission
|
||||||
|
30
packages/backend/src/routers/auth/grant-user-user.js
Normal file
30
packages/backend/src/routers/auth/grant-user-user.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
const APIError = require("../../api/APIError");
|
||||||
|
const eggspress = require("../../api/eggspress");
|
||||||
|
const { UserActorType } = require("../../services/auth/Actor");
|
||||||
|
const { Context } = require("../../util/context");
|
||||||
|
|
||||||
|
module.exports = eggspress('/auth/grant-user-user', {
|
||||||
|
subdomain: 'api',
|
||||||
|
auth2: true,
|
||||||
|
allowedMethods: ['POST'],
|
||||||
|
}, async (req, res, next) => {
|
||||||
|
const x = Context.get();
|
||||||
|
const svc_permission = x.get('services').get('permission');
|
||||||
|
|
||||||
|
// Only users can grant user-user permissions
|
||||||
|
const actor = Context.get('actor');
|
||||||
|
if ( ! (actor.type instanceof UserActorType) ) {
|
||||||
|
throw APIError.create('forbidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! req.body.target_username ) {
|
||||||
|
throw APIError.create('field_missing', null, { key: 'target_username' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await svc_permission.grant_user_user_permission(
|
||||||
|
actor, req.body.target_username, req.body.permission,
|
||||||
|
req.body.extra || {}, req.body.meta || {}
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({});
|
||||||
|
});
|
31
packages/backend/src/routers/auth/revoke-user-user.js
Normal file
31
packages/backend/src/routers/auth/revoke-user-user.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
const APIError = require("../../api/APIError");
|
||||||
|
const eggspress = require("../../api/eggspress");
|
||||||
|
const { UserActorType } = require("../../services/auth/Actor");
|
||||||
|
const { Context } = require("../../util/context");
|
||||||
|
|
||||||
|
module.exports = eggspress('/auth/revoke-user-user', {
|
||||||
|
subdomain: 'api',
|
||||||
|
auth2: true,
|
||||||
|
allowedMethods: ['POST'],
|
||||||
|
}, async (req, res, next) => {
|
||||||
|
const x = Context.get();
|
||||||
|
const svc_permission = x.get('services').get('permission');
|
||||||
|
|
||||||
|
// Only users can grant user-user permissions
|
||||||
|
const actor = Context.get('actor');
|
||||||
|
if ( ! (actor.type instanceof UserActorType) ) {
|
||||||
|
throw APIError.create('forbidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! req.body.target_username ) {
|
||||||
|
throw APIError.create('field_missing', null, { key: 'target_username' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await svc_permission.revoke_user_user_permission(
|
||||||
|
actor, req.body.target_username, req.body.permission,
|
||||||
|
req.body.meta || {}
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({});
|
||||||
|
});
|
||||||
|
|
@ -30,6 +30,8 @@ class PuterAPIService extends BaseService {
|
|||||||
app.use(require('../routers/auth/get-user-app-token'))
|
app.use(require('../routers/auth/get-user-app-token'))
|
||||||
app.use(require('../routers/auth/grant-user-app'))
|
app.use(require('../routers/auth/grant-user-app'))
|
||||||
app.use(require('../routers/auth/revoke-user-app'))
|
app.use(require('../routers/auth/revoke-user-app'))
|
||||||
|
app.use(require('../routers/auth/grant-user-user'));
|
||||||
|
app.use(require('../routers/auth/revoke-user-user'));
|
||||||
app.use(require('../routers/auth/list-permissions'))
|
app.use(require('../routers/auth/list-permissions'))
|
||||||
app.use(require('../routers/auth/check-app'))
|
app.use(require('../routers/auth/check-app'))
|
||||||
app.use(require('../routers/auth/app-uid-from-origin'))
|
app.use(require('../routers/auth/app-uid-from-origin'))
|
||||||
|
@ -76,35 +76,6 @@ class ACLService extends BaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hard rule: if actor is owner, allow
|
|
||||||
if ( actor.type instanceof UserActorType ) {
|
|
||||||
const owner = await fsNode.get('user_id');
|
|
||||||
if ( this.verbose ) {
|
|
||||||
const user = await get_user({ id: owner });
|
|
||||||
this.log.info(
|
|
||||||
`user ${user.username} is ` +
|
|
||||||
(owner == actor.type.user.id ? '' : 'not ') +
|
|
||||||
'owner of ' + await fsNode.get('path'), {
|
|
||||||
actor_user_id: actor.type.user.id,
|
|
||||||
fsnode_user_id: owner,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if ( owner == actor.type.user.id ) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// For these actors, deny if the user component is not the owner
|
|
||||||
// -> user
|
|
||||||
// -> app-under-user
|
|
||||||
if ( ! (actor.type instanceof AccessTokenActorType) ) {
|
|
||||||
const owner = await fsNode.get('user_id');
|
|
||||||
if ( owner != actor.type.user.id ) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// app-under-user only works if the user also has permission
|
// app-under-user only works if the user also has permission
|
||||||
if ( actor.type instanceof AppUnderUserActorType ) {
|
if ( actor.type instanceof AppUnderUserActorType ) {
|
||||||
const user_actor = new Actor({
|
const user_actor = new Actor({
|
||||||
@ -146,6 +117,7 @@ class ACLService extends BaseService {
|
|||||||
return APIError.create('forbidden');
|
return APIError.create('forbidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: DRY: Also in FilesystemService
|
||||||
_higher_modes (mode) {
|
_higher_modes (mode) {
|
||||||
// If you want to X, you can do so with any of [...Y]
|
// If you want to X, you can do so with any of [...Y]
|
||||||
if ( mode === 'see' ) return ['see', 'list', 'read', 'write'];
|
if ( mode === 'see' ) return ['see', 'list', 'read', 'write'];
|
||||||
|
@ -84,6 +84,10 @@ const implicit_user_app_permissions = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const implicit_user_permissions = {
|
||||||
|
'driver': {},
|
||||||
|
};
|
||||||
|
|
||||||
class PermissionRewriter {
|
class PermissionRewriter {
|
||||||
static create ({ id, matcher, rewriter }) {
|
static create ({ id, matcher, rewriter }) {
|
||||||
return new PermissionRewriter({ id, matcher, rewriter });
|
return new PermissionRewriter({ id, matcher, rewriter });
|
||||||
@ -104,6 +108,32 @@ class PermissionRewriter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PermissionImplicator {
|
||||||
|
static create ({ id, matcher, checker }) {
|
||||||
|
return new PermissionImplicator({ id, matcher, checker });
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor ({ id, matcher, checker }) {
|
||||||
|
this.id = id;
|
||||||
|
this.matcher = matcher;
|
||||||
|
this.checker = checker;
|
||||||
|
}
|
||||||
|
|
||||||
|
matches (permission) {
|
||||||
|
return this.matcher(permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the permission is implied by this implicator
|
||||||
|
* @param {Actor} actor
|
||||||
|
* @param {string} permission
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async check (actor, permission) {
|
||||||
|
return await this.checker(actor, permission);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class PermissionUtil {
|
class PermissionUtil {
|
||||||
static unescape_permission_component (component) {
|
static unescape_permission_component (component) {
|
||||||
let unescaped_str = '';
|
let unescaped_str = '';
|
||||||
@ -142,6 +172,7 @@ class PermissionService extends BaseService {
|
|||||||
this._register_commands(this.services.get('commands'));
|
this._register_commands(this.services.get('commands'));
|
||||||
|
|
||||||
this._permission_rewriters = [];
|
this._permission_rewriters = [];
|
||||||
|
this._permission_implicators = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async _rewrite_permission (permission) {
|
async _rewrite_permission (permission) {
|
||||||
@ -162,30 +193,86 @@ class PermissionService extends BaseService {
|
|||||||
});
|
});
|
||||||
// For now we're only checking driver permissions, and users have all of them
|
// For now we're only checking driver permissions, and users have all of them
|
||||||
if ( actor.type instanceof UserActorType ) {
|
if ( actor.type instanceof UserActorType ) {
|
||||||
return {};
|
return await this.check_user_permission(actor, permission);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( actor.type instanceof AccessTokenActorType ) {
|
if ( actor.type instanceof AccessTokenActorType ) {
|
||||||
|
// Authorizer must have permission
|
||||||
|
const authorizer_permission = await this.check(authorizer, permission);
|
||||||
|
if ( ! authorizer_permission ) return false;
|
||||||
|
|
||||||
return await this.check_access_token_permission(
|
return await this.check_access_token_permission(
|
||||||
actor.type.authorizer, actor.type.token, permission
|
actor.type.authorizer, actor.type.token, permission
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent undefined behaviour
|
// Prevent undefined behaviour
|
||||||
if ( ! (actor.type instanceof AppUnderUserActorType) ) {
|
if ( actor.type instanceof AppUnderUserActorType ) {
|
||||||
throw new Error('actor must be an app under a user');
|
// NEXT:
|
||||||
}
|
|
||||||
|
|
||||||
// Now it's an app under a user
|
|
||||||
const app_uid = actor.type.app.uid;
|
const app_uid = actor.type.app.uid;
|
||||||
|
const user_has_permission = await this.check_user_permission(actor, permission);
|
||||||
|
if ( ! user_has_permission ) return undefined;
|
||||||
|
|
||||||
return await this.check_user_app_permission(actor, app_uid, permission);
|
return await this.check_user_app_permission(actor, app_uid, permission);
|
||||||
}
|
}
|
||||||
|
|
||||||
async check_access_token_permission (authorizer, token, permission) {
|
throw new Error('unrecognized actor type');
|
||||||
// Authorizer must have permission
|
}
|
||||||
const authorizer_permission = await this.check(authorizer, permission);
|
|
||||||
if ( ! authorizer_permission ) return false;
|
|
||||||
|
|
||||||
|
// TODO: context meta for cycle detection
|
||||||
|
async check_user_permission (actor, permission) {
|
||||||
|
permission = await this._rewrite_permission(permission);
|
||||||
|
const parent_perms = this.get_parent_permissions(permission);
|
||||||
|
|
||||||
|
// Check implicit permissions
|
||||||
|
for ( const parent_perm of parent_perms ) {
|
||||||
|
if ( implicit_user_permissions[parent_perm] ) {
|
||||||
|
return implicit_user_permissions[parent_perm];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for ( const implicator of this._permission_implicators ) {
|
||||||
|
if ( ! implicator.matches(permission) ) continue;
|
||||||
|
const implied = await implicator.check(actor, permission);
|
||||||
|
if ( implied ) return implied;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permissions granted by other users
|
||||||
|
let sql_perm = parent_perms.map((perm) =>
|
||||||
|
`\`permission\` = ?`).join(' OR ');
|
||||||
|
if ( parent_perms.length > 1 ) sql_perm = '(' + sql_perm + ')';
|
||||||
|
|
||||||
|
// SELECT permission
|
||||||
|
const rows = await this.db.read(
|
||||||
|
'SELECT * FROM `user_to_user_permissions` ' +
|
||||||
|
'WHERE `holder_user_id` = ? AND ' +
|
||||||
|
sql_perm,
|
||||||
|
[
|
||||||
|
actor.type.user.id,
|
||||||
|
...parent_perms,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Return the first matching permission where the
|
||||||
|
// issuer also has the permission granted
|
||||||
|
for ( const row of rows ) {
|
||||||
|
const issuer_actor = new Actor({
|
||||||
|
type: new UserActorType({
|
||||||
|
user: await get_user({ id: row.issuer_user_id }),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const issuer_perm = await this.check(issuer_actor, row.permission);
|
||||||
|
|
||||||
|
if ( ! issuer_perm ) continue;
|
||||||
|
|
||||||
|
return row.extra;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async check_access_token_permission (authorizer, token, permission) {
|
||||||
const rows = await this.db.read(
|
const rows = await this.db.read(
|
||||||
'SELECT * FROM `access_token_permissions` ' +
|
'SELECT * FROM `access_token_permissions` ' +
|
||||||
'WHERE `token_uid` = ? AND `permission` = ?',
|
'WHERE `token_uid` = ? AND `permission` = ?',
|
||||||
@ -208,18 +295,7 @@ class PermissionService extends BaseService {
|
|||||||
if ( ! app ) app = await get_app({ name: app_uid });
|
if ( ! app ) app = await get_app({ name: app_uid });
|
||||||
const app_id = app.id;
|
const app_id = app.id;
|
||||||
|
|
||||||
const parent_perms = [];
|
const parent_perms = this.get_parent_permissions(permission);
|
||||||
{
|
|
||||||
// We don't use PermissionUtil.split here because it unescapes
|
|
||||||
// components; we want to keep the components escaped for matching.
|
|
||||||
const parts = permission.split(':');
|
|
||||||
|
|
||||||
// Add sub-permissions
|
|
||||||
for ( let i = 1 ; i < parts.length ; i++ ) {
|
|
||||||
parent_perms.push(parts.slice(0, i + 1).join(':'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parent_perms.reverse();
|
|
||||||
|
|
||||||
for ( const permission of parent_perms ) {
|
for ( const permission of parent_perms ) {
|
||||||
// Check hardcoded permissions
|
// Check hardcoded permissions
|
||||||
@ -394,6 +470,107 @@ class PermissionService extends BaseService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async grant_user_user_permission (actor, username, permission, extra = {}, meta) {
|
||||||
|
permission = await this._rewrite_permission(permission);
|
||||||
|
const user = await get_user({ username });
|
||||||
|
if ( ! user ) {
|
||||||
|
throw new Error('user not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow granting permissions to yourself
|
||||||
|
if ( user.id === actor.type.user.id ) {
|
||||||
|
throw new Error('cannot grant permissions to yourself');
|
||||||
|
}
|
||||||
|
|
||||||
|
// UPSERT permission
|
||||||
|
await this.db.write(
|
||||||
|
'INSERT INTO `user_to_user_permissions` (`holder_user_id`, `issuer_user_id`, `permission`, `extra`) ' +
|
||||||
|
'VALUES (?, ?, ?, ?) ' +
|
||||||
|
this.db.case({
|
||||||
|
mysql: 'ON DUPLICATE KEY UPDATE `extra` = ?',
|
||||||
|
otherwise: 'ON CONFLICT(`holder_user_id`, `issuer_user_id`, `permission`) DO UPDATE SET `extra` = ?',
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
user.id,
|
||||||
|
actor.type.user.id,
|
||||||
|
permission,
|
||||||
|
JSON.stringify(extra),
|
||||||
|
JSON.stringify(extra),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// INSERT audit table
|
||||||
|
await this.db.write(
|
||||||
|
'INSERT INTO `audit_user_to_user_permissions` (' +
|
||||||
|
'`holder_user_id`, `holder_user_id_keep`, `issuer_user_id`, `issuer_user_id_keep`, ' +
|
||||||
|
'`permission`, `action`, `reason`) ' +
|
||||||
|
'VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||||
|
[
|
||||||
|
user.id,
|
||||||
|
user.id,
|
||||||
|
actor.type.user.id,
|
||||||
|
actor.type.user.id,
|
||||||
|
permission,
|
||||||
|
'grant',
|
||||||
|
meta?.reason || 'granted via PermissionService',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async revoke_user_user_permission (actor, username, permission, meta) {
|
||||||
|
permission = await this._rewrite_permission(permission);
|
||||||
|
|
||||||
|
const user = await get_user({ username });
|
||||||
|
if ( ! user ) {
|
||||||
|
throw new Error('user not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE permission
|
||||||
|
await this.db.write(
|
||||||
|
'DELETE FROM `user_to_user_permissions` ' +
|
||||||
|
'WHERE `holder_user_id` = ? AND `issuer_user_id` = ? AND `permission` = ?',
|
||||||
|
[
|
||||||
|
user.id,
|
||||||
|
actor.type.user.id,
|
||||||
|
permission,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// INSERT audit table
|
||||||
|
await this.db.write(
|
||||||
|
'INSERT INTO `audit_user_to_user_permissions` (' +
|
||||||
|
'`holder_user_id`, `holder_user_id_keep`, `issuer_user_id`, `issuer_user_id_keep`, ' +
|
||||||
|
'`permission`, `action`, `reason`) ' +
|
||||||
|
'VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||||
|
[
|
||||||
|
user.id,
|
||||||
|
user.id,
|
||||||
|
actor.type.user.id,
|
||||||
|
actor.type.user.id,
|
||||||
|
permission,
|
||||||
|
'revoke',
|
||||||
|
meta?.reason || 'revoked via PermissionService',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get_parent_permissions (permission) {
|
||||||
|
const parent_perms = [];
|
||||||
|
{
|
||||||
|
// We don't use PermissionUtil.split here because it unescapes
|
||||||
|
// components; we want to keep the components escaped for matching.
|
||||||
|
const parts = permission.split(':');
|
||||||
|
|
||||||
|
// Add sub-permissions
|
||||||
|
for ( let i = 0 ; i < parts.length ; i++ ) {
|
||||||
|
parent_perms.push(parts.slice(0, i + 1).join(':'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parent_perms.reverse();
|
||||||
|
return parent_perms;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
register_rewriter (translator) {
|
register_rewriter (translator) {
|
||||||
if ( ! (translator instanceof PermissionRewriter) ) {
|
if ( ! (translator instanceof PermissionRewriter) ) {
|
||||||
throw new Error('translator must be a PermissionRewriter');
|
throw new Error('translator must be a PermissionRewriter');
|
||||||
@ -402,6 +579,14 @@ class PermissionService extends BaseService {
|
|||||||
this._permission_rewriters.push(translator);
|
this._permission_rewriters.push(translator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
register_implicator (implicator) {
|
||||||
|
if ( ! (implicator instanceof PermissionImplicator) ) {
|
||||||
|
throw new Error('implicator must be a PermissionImplicator');
|
||||||
|
}
|
||||||
|
|
||||||
|
this._permission_implicators.push(implicator);
|
||||||
|
}
|
||||||
|
|
||||||
_register_commands (commands) {
|
_register_commands (commands) {
|
||||||
commands.registerCommands('perms', [
|
commands.registerCommands('perms', [
|
||||||
{
|
{
|
||||||
@ -425,6 +610,7 @@ class PermissionService extends BaseService {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
PermissionRewriter,
|
PermissionRewriter,
|
||||||
|
PermissionImplicator,
|
||||||
PermissionUtil,
|
PermissionUtil,
|
||||||
PermissionService,
|
PermissionService,
|
||||||
};
|
};
|
||||||
|
@ -16,6 +16,8 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
const { es_import_promise } = require("../../fun/dev-console-ui-utils");
|
||||||
|
const { surrounding_box } = require("../../fun/dev-console-ui-utils");
|
||||||
const { BaseDatabaseAccessService } = require("./BaseDatabaseAccessService");
|
const { BaseDatabaseAccessService } = require("./BaseDatabaseAccessService");
|
||||||
|
|
||||||
class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||||
@ -40,23 +42,68 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
|||||||
this.db = new Database(this.config.path);
|
this.db = new Database(this.config.path);
|
||||||
|
|
||||||
if ( do_setup ) {
|
if ( do_setup ) {
|
||||||
|
this.log.noticeme(`SETUP: creating database at ${this.config.path}`);
|
||||||
const sql_files = [
|
const sql_files = [
|
||||||
'0001_create-tables.sql',
|
'0001_create-tables.sql',
|
||||||
'0002_add-default-apps.sql',
|
'0002_add-default-apps.sql',
|
||||||
|
'0003_user-permissions.sql',
|
||||||
].map(p => path_.join(__dirname, 'sqlite_setup', p));
|
].map(p => path_.join(__dirname, 'sqlite_setup', p));
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
for ( const filename of sql_files ) {
|
for ( const filename of sql_files ) {
|
||||||
|
const basename = path_.basename(filename);
|
||||||
|
this.log.noticeme(`applying ${basename}`);
|
||||||
const contents = fs.readFileSync(filename, 'utf8');
|
const contents = fs.readFileSync(filename, 'utf8');
|
||||||
this.db.exec(contents);
|
this.db.exec(contents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the tables if they don't exist.
|
// Database upgrade logic
|
||||||
const check =
|
const TARGET_VERSION = 1;
|
||||||
`SELECT name FROM sqlite_master WHERE type='table' AND name='fsentries'`;
|
|
||||||
const rows = await this.db.prepare(check).all();
|
const [{ user_version }] = await this._read('PRAGMA user_version');
|
||||||
if ( rows.length === 0 ) {
|
this.log.info('database version: ' + user_version);
|
||||||
throw new Error('it works');
|
|
||||||
|
const upgrade_files = [];
|
||||||
|
|
||||||
|
if ( user_version <= 0 ) {
|
||||||
|
upgrade_files.push('0003_user-permissions.sql');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( upgrade_files.length > 0 ) {
|
||||||
|
this.log.noticeme(`Database out of date: ${this.config.path}`);
|
||||||
|
this.log.noticeme(`UPGRADING DATABASE: ${user_version} -> ${TARGET_VERSION}`);
|
||||||
|
this.log.noticeme(`${upgrade_files.length} .sql files to apply`);
|
||||||
|
|
||||||
|
const sql_files = upgrade_files.map(
|
||||||
|
p => path_.join(__dirname, 'sqlite_setup', p)
|
||||||
|
);
|
||||||
|
const fs = require('fs');
|
||||||
|
for ( const filename of sql_files ) {
|
||||||
|
const basename = path_.basename(filename);
|
||||||
|
this.log.noticeme(`applying ${basename}`);
|
||||||
|
const contents = fs.readFileSync(filename, 'utf8');
|
||||||
|
this.db.exec(contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update version number
|
||||||
|
await this.db.exec(`PRAGMA user_version = ${TARGET_VERSION};`);
|
||||||
|
|
||||||
|
// Add sticky notification
|
||||||
|
this.database_update_notice = () => {
|
||||||
|
const lines = [
|
||||||
|
`Database has been updated!`,
|
||||||
|
`Current version: ${TARGET_VERSION}`,
|
||||||
|
`Type sqlite:dismiss to dismiss this message`,
|
||||||
|
];
|
||||||
|
surrounding_box('33;1', lines);
|
||||||
|
return lines;
|
||||||
|
};
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
await es_import_promise;
|
||||||
|
const svc_devConsole = this.services.get('dev-console');
|
||||||
|
svc_devConsole.add_widget(this.database_update_notice);
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,6 +195,19 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'dismiss',
|
||||||
|
description: 'dismiss the database update notice',
|
||||||
|
handler: async (_, log) => {
|
||||||
|
const svc_devConsole = this.services.get('dev-console');
|
||||||
|
if ( ! svc_devConsole ) return;
|
||||||
|
if ( ! this.database_update_notice ) return;
|
||||||
|
svc_devConsole.remove_widget(this.database_update_notice);
|
||||||
|
const lines = this.database_update_notice();
|
||||||
|
for ( const line of lines ) log.log(line);
|
||||||
|
this.database_update_notice = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
CREATE TABLE `user_to_user_permissions` (
|
||||||
|
"issuer_user_id" INTEGER NOT NULL,
|
||||||
|
"holder_user_id" INTEGER NOT NULL,
|
||||||
|
"permission" TEXT NOT NULL,
|
||||||
|
"extra" JSON DEFAULT NULL,
|
||||||
|
|
||||||
|
FOREIGN KEY("issuer_user_id") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
FOREIGN KEY("holder_user_id") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
PRIMARY KEY ("issuer_user_id", "holder_user_id", "permission")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "audit_user_to_user_permissions" (
|
||||||
|
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
|
||||||
|
"issuer_user_id" INTEGER NOT NULL,
|
||||||
|
"issuer_user_id_keep" INTEGER DEFAULT NULL,
|
||||||
|
|
||||||
|
"holder_user_id" INTEGER NOT NULL,
|
||||||
|
"holder_user_id_keep" INTEGER DEFAULT NULL,
|
||||||
|
|
||||||
|
"permission" TEXT NOT NULL,
|
||||||
|
"extra" JSON DEFAULT NULL,
|
||||||
|
|
||||||
|
"action" TEXT DEFAULT NULL,
|
||||||
|
"reason" TEXT DEFAULT NULL,
|
||||||
|
|
||||||
|
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
FOREIGN KEY("issuer_user_id") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
|
FOREIGN KEY("holder_user_id") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
Loading…
Reference in New Issue
Block a user