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) {
|
||||
// Initialize scope
|
||||
values = values || this.thisArg?.values || {};
|
||||
Object.assign(this.scope_, values);
|
||||
Object.assign(this.scope_, values); // TODO: can this be __proto__?
|
||||
|
||||
// Run sequence
|
||||
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 { LLCWrite, LLOWrite } = require('./ll_operations/ll_write.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 { UserActorType } = require('../services/auth/Actor');
|
||||
const { get_user } = require('../helpers');
|
||||
|
||||
class FilesystemService extends AdvancedBase {
|
||||
static MODULES = {
|
||||
@ -131,6 +133,39 @@ class FilesystemService extends AdvancedBase {
|
||||
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 actor = context.get('actor');
|
||||
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
|
||||
|
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/grant-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/check-app'))
|
||||
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
|
||||
if ( actor.type instanceof AppUnderUserActorType ) {
|
||||
const user_actor = new Actor({
|
||||
@ -146,6 +117,7 @@ class ACLService extends BaseService {
|
||||
return APIError.create('forbidden');
|
||||
}
|
||||
|
||||
// TODO: DRY: Also in FilesystemService
|
||||
_higher_modes (mode) {
|
||||
// If you want to X, you can do so with any of [...Y]
|
||||
if ( mode === 'see' ) return ['see', 'list', 'read', 'write'];
|
||||
|
@ -84,6 +84,10 @@ const implicit_user_app_permissions = [
|
||||
},
|
||||
];
|
||||
|
||||
const implicit_user_permissions = {
|
||||
'driver': {},
|
||||
};
|
||||
|
||||
class PermissionRewriter {
|
||||
static create ({ 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 {
|
||||
static unescape_permission_component (component) {
|
||||
let unescaped_str = '';
|
||||
@ -142,6 +172,7 @@ class PermissionService extends BaseService {
|
||||
this._register_commands(this.services.get('commands'));
|
||||
|
||||
this._permission_rewriters = [];
|
||||
this._permission_implicators = [];
|
||||
}
|
||||
|
||||
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
|
||||
if ( actor.type instanceof UserActorType ) {
|
||||
return {};
|
||||
return await this.check_user_permission(actor, permission);
|
||||
}
|
||||
|
||||
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(
|
||||
actor.type.authorizer, actor.type.token, permission
|
||||
);
|
||||
}
|
||||
|
||||
// Prevent undefined behaviour
|
||||
if ( ! (actor.type instanceof AppUnderUserActorType) ) {
|
||||
throw new Error('actor must be an app under a user');
|
||||
}
|
||||
|
||||
// Now it's an app under a user
|
||||
if ( actor.type instanceof AppUnderUserActorType ) {
|
||||
// NEXT:
|
||||
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);
|
||||
}
|
||||
|
||||
async check_access_token_permission (authorizer, token, permission) {
|
||||
// Authorizer must have permission
|
||||
const authorizer_permission = await this.check(authorizer, permission);
|
||||
if ( ! authorizer_permission ) return false;
|
||||
throw new Error('unrecognized actor type');
|
||||
}
|
||||
|
||||
// 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(
|
||||
'SELECT * FROM `access_token_permissions` ' +
|
||||
'WHERE `token_uid` = ? AND `permission` = ?',
|
||||
@ -208,18 +295,7 @@ class PermissionService extends BaseService {
|
||||
if ( ! app ) app = await get_app({ name: app_uid });
|
||||
const app_id = app.id;
|
||||
|
||||
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 = 1 ; i < parts.length ; i++ ) {
|
||||
parent_perms.push(parts.slice(0, i + 1).join(':'));
|
||||
}
|
||||
}
|
||||
parent_perms.reverse();
|
||||
const parent_perms = this.get_parent_permissions(permission);
|
||||
|
||||
for ( const permission of parent_perms ) {
|
||||
// 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) {
|
||||
if ( ! (translator instanceof PermissionRewriter) ) {
|
||||
throw new Error('translator must be a PermissionRewriter');
|
||||
@ -402,6 +579,14 @@ class PermissionService extends BaseService {
|
||||
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) {
|
||||
commands.registerCommands('perms', [
|
||||
{
|
||||
@ -425,6 +610,7 @@ class PermissionService extends BaseService {
|
||||
|
||||
module.exports = {
|
||||
PermissionRewriter,
|
||||
PermissionImplicator,
|
||||
PermissionUtil,
|
||||
PermissionService,
|
||||
};
|
||||
|
@ -16,6 +16,8 @@
|
||||
* 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/>.
|
||||
*/
|
||||
const { es_import_promise } = require("../../fun/dev-console-ui-utils");
|
||||
const { surrounding_box } = require("../../fun/dev-console-ui-utils");
|
||||
const { BaseDatabaseAccessService } = require("./BaseDatabaseAccessService");
|
||||
|
||||
class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
@ -40,23 +42,68 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
this.db = new Database(this.config.path);
|
||||
|
||||
if ( do_setup ) {
|
||||
this.log.noticeme(`SETUP: creating database at ${this.config.path}`);
|
||||
const sql_files = [
|
||||
'0001_create-tables.sql',
|
||||
'0002_add-default-apps.sql',
|
||||
'0003_user-permissions.sql',
|
||||
].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);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the tables if they don't exist.
|
||||
const check =
|
||||
`SELECT name FROM sqlite_master WHERE type='table' AND name='fsentries'`;
|
||||
const rows = await this.db.prepare(check).all();
|
||||
if ( rows.length === 0 ) {
|
||||
throw new Error('it works');
|
||||
// Database upgrade logic
|
||||
const TARGET_VERSION = 1;
|
||||
|
||||
const [{ user_version }] = await this._read('PRAGMA user_version');
|
||||
this.log.info('database version: ' + user_version);
|
||||
|
||||
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