mirror of
https://github.com/HeyPuter/puter.git
synced 2025-12-30 09:40:00 -06:00
feat: manage permission for user to user and dev to app file sharing (#1567)
* perf: move user-user perm checks to flat kv entries * feat: manage permission for user to user and dev to app file sharing * fix typings cuz macos sucks * fix: mac os typecase * fix: chore macOs typecase * fix: bad join of permissions * feat: add check-permissions endpoint for checking an users perms * Add 'manage' to permission lists in the Sharing dialog * fix: manage being allowed through our ACL --------- Co-authored-by: Nariman Jelveh <nj@puter.com>
This commit is contained in:
@@ -13,7 +13,7 @@ export default defineConfig([
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['src/backend/**/*.{js,mjs,cjs}'],
|
||||
files: ['src/backend/**/*.{js,mjs,cjs,ts}'],
|
||||
languageOptions: { globals: globals.node },
|
||||
rules: {
|
||||
'no-unused-vars': ['error', {
|
||||
@@ -63,7 +63,7 @@ export default defineConfig([
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['extensions/**/*.{js,mjs,cjs}'],
|
||||
files: ['extensions/**/*.{js,mjs,cjs,ts}'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
extension: 'readonly',
|
||||
|
||||
@@ -36,80 +36,76 @@ const {
|
||||
} = require('../../../src/backend/src/filesystem/node/selectors.js');
|
||||
const { Context } = require('../../../src/backend/src/util/context.js');
|
||||
|
||||
|
||||
class ShareTestService extends use.Service {
|
||||
static MODULES = {
|
||||
uuidv4: require('uuid').v4,
|
||||
}
|
||||
};
|
||||
|
||||
async _init () {
|
||||
async _init() {
|
||||
const svc_commands = this.services.get('commands');
|
||||
this._register_commands(svc_commands);
|
||||
|
||||
|
||||
this.scenarios = require('./data/sharetest_scenarios');
|
||||
|
||||
const svc_db = this.services.get('database');
|
||||
this.db = svc_db.get(svc_db.DB_WRITE, 'share-test');
|
||||
}
|
||||
|
||||
_register_commands (commands) {
|
||||
|
||||
_register_commands(commands) {
|
||||
commands.registerCommands('share-test', [
|
||||
{
|
||||
id: 'start',
|
||||
description: '',
|
||||
handler: async (_, log) => {
|
||||
const results = await this.runit();
|
||||
|
||||
|
||||
for ( const result of results ) {
|
||||
log.log(`=== ${result.title} ===`);
|
||||
if ( ! result.report ) {
|
||||
log.log(`\x1B[32;1mSUCCESS\x1B[0m`);
|
||||
log.log('\x1B[32;1mSUCCESS\x1B[0m');
|
||||
continue;
|
||||
}
|
||||
log.log(
|
||||
`\x1B[31;1mSTOPPED\x1B[0m at ` +
|
||||
`${result.report.step}: ` +
|
||||
result.report.report.message,
|
||||
);
|
||||
log.log('\x1B[31;1mSTOPPED\x1B[0m at ' +
|
||||
`${result.report.step}: ${
|
||||
result.report.report.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
async runit () {
|
||||
|
||||
async runit() {
|
||||
await this.teardown_();
|
||||
await this.setup_();
|
||||
|
||||
|
||||
const results = [];
|
||||
|
||||
|
||||
for ( const scenario of this.scenarios ) {
|
||||
if ( ! scenario.title ) {
|
||||
scenario.title = scenario.sequence.map(
|
||||
step => step.title).join('; ')
|
||||
scenario.title = scenario.sequence.map(step => step.title).join('; ');
|
||||
}
|
||||
results.push({
|
||||
title: scenario.title,
|
||||
report: await this.run_scenario_(scenario)
|
||||
report: await this.run_scenario_(scenario),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
await this.teardown_();
|
||||
return results;
|
||||
}
|
||||
|
||||
async setup_ () {
|
||||
|
||||
async setup_() {
|
||||
await this.create_test_user_('testuser_eric');
|
||||
await this.create_test_user_('testuser_stan');
|
||||
await this.create_test_user_('testuser_kyle');
|
||||
await this.create_test_user_('testuser_kenny');
|
||||
}
|
||||
async run_scenario_ (scenario) {
|
||||
async run_scenario_(scenario) {
|
||||
let error;
|
||||
// Run sequence
|
||||
for ( const step of scenario.sequence ) {
|
||||
const method = this[`__scenario:${step.call}`];
|
||||
const user = await get_user({ username: step.as })
|
||||
const user = await get_user({ username: step.as });
|
||||
const actor = await Actor.create(UserActorType, { user });
|
||||
const generated = { user, actor };
|
||||
const report = await Context.get().sub({ user, actor })
|
||||
@@ -123,51 +119,49 @@ class ShareTestService extends use.Service {
|
||||
}
|
||||
return error;
|
||||
}
|
||||
async teardown_ () {
|
||||
async teardown_() {
|
||||
await this.delete_test_user_('testuser_eric');
|
||||
await this.delete_test_user_('testuser_stan');
|
||||
await this.delete_test_user_('testuser_kyle');
|
||||
await this.delete_test_user_('testuser_kenny');
|
||||
}
|
||||
|
||||
async create_test_user_ (username) {
|
||||
await this.db.write(
|
||||
`
|
||||
async create_test_user_(username) {
|
||||
await this.db.write(`
|
||||
INSERT INTO user (uuid, username, email, free_storage, password)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`,
|
||||
[
|
||||
this.modules.uuidv4(),
|
||||
username,
|
||||
username + '@example.com',
|
||||
1024 * 1024 * 500, // 500 MiB
|
||||
this.modules.uuidv4(),
|
||||
],
|
||||
);
|
||||
[
|
||||
this.modules.uuidv4(),
|
||||
username,
|
||||
`${username}@example.com`,
|
||||
1024 * 1024 * 500, // 500 MiB
|
||||
this.modules.uuidv4(),
|
||||
]);
|
||||
const user = await get_user({ username });
|
||||
const svc_user = this.services.get('user');
|
||||
await svc_user.generate_default_fsentries({ user });
|
||||
invalidate_cached_user(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
async delete_test_user_ (username) {
|
||||
|
||||
async delete_test_user_(username) {
|
||||
const user = await get_user({ username });
|
||||
if ( ! user ) return;
|
||||
await deleteUser(user.id);
|
||||
}
|
||||
|
||||
|
||||
// API for scenarios
|
||||
async ['__scenario:create-example-file'] (
|
||||
async ['__scenario:create-example-file'](
|
||||
{ actor, user },
|
||||
{ name, contents },
|
||||
) {
|
||||
const svc_fs = this.services.get('filesystem');
|
||||
const parent = await svc_fs.node(new NodePathSelector(
|
||||
`/${user.username}/Desktop`
|
||||
));
|
||||
const parent = await svc_fs.node(new NodePathSelector(`/${user.username}/Desktop`));
|
||||
console.log('test -> create-example-file',
|
||||
user, name, contents);
|
||||
user,
|
||||
name,
|
||||
contents);
|
||||
const buffer = Buffer.from(contents);
|
||||
const file = {
|
||||
size: buffer.length,
|
||||
@@ -184,7 +178,7 @@ class ShareTestService extends use.Service {
|
||||
file,
|
||||
});
|
||||
}
|
||||
async ['__scenario:assert-no-access'] (
|
||||
async ['__scenario:assert-no-access'](
|
||||
{ actor, user },
|
||||
{ path },
|
||||
) {
|
||||
@@ -195,26 +189,24 @@ class ShareTestService extends use.Service {
|
||||
const stream = await ll_read.run({
|
||||
fsNode: node,
|
||||
actor,
|
||||
})
|
||||
} catch (e) {
|
||||
});
|
||||
} catch(e) {
|
||||
expected_e = e;
|
||||
}
|
||||
if ( ! expected_e ) {
|
||||
return { message: 'expected error, got none' };
|
||||
}
|
||||
}
|
||||
async ['__scenario:grant'] (
|
||||
async ['__scenario:grant'](
|
||||
{ actor, user },
|
||||
{ to, permission },
|
||||
) {
|
||||
const svc_permission = this.services.get('permission');
|
||||
await svc_permission.grant_user_user_permission(
|
||||
actor, to, permission, {}, {},
|
||||
);
|
||||
await svc_permission.grant_user_user_permission(actor, to, permission, {}, {});
|
||||
}
|
||||
async ['__scenario:assert-access'] (
|
||||
async ['__scenario:assert-access'](
|
||||
{ actor, user },
|
||||
{ path, level }
|
||||
{ path, level },
|
||||
) {
|
||||
const svc_fs = this.services.get('filesystem');
|
||||
const svc_acl = this.services.get('acl');
|
||||
@@ -224,23 +216,28 @@ class ShareTestService extends use.Service {
|
||||
|
||||
if ( level !== 'write' && level !== 'read' ) {
|
||||
return {
|
||||
message: 'unexpected value for "level" parameter'
|
||||
message: 'unexpected value for "level" parameter',
|
||||
};
|
||||
}
|
||||
|
||||
if ( level === 'read' && has_write ) {
|
||||
return {
|
||||
message: 'expected read-only but actor can write'
|
||||
message: 'expected read-only but actor can write',
|
||||
};
|
||||
}
|
||||
if ( level === 'read' && !has_read ) {
|
||||
return {
|
||||
message: 'expected read access but no read access'
|
||||
message: 'expected read access but no read access',
|
||||
};
|
||||
}
|
||||
if ( level === 'write' && (!has_write || !has_read) ) {
|
||||
return {
|
||||
message: 'expected write access but no write access'
|
||||
message: 'expected write access but no write access',
|
||||
};
|
||||
}
|
||||
if ( level === 'manage' && (!has_write || !has_read) ) {
|
||||
return {
|
||||
message: 'expected write access but no write access',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ const install = async ({ context, services, app, useapi, modapi }) => {
|
||||
def('Library', Library);
|
||||
|
||||
def('core.util.helpers', require('./helpers'));
|
||||
def('core.util.permission', require('./services/auth/PermissionUtils.mjs').PermissionUtil);
|
||||
def('core.util.permission', require('./services/auth/permissionUtils.mjs').PermissionUtil);
|
||||
def('puter.middlewares.auth', require('./middleware/auth2'));
|
||||
def('puter.middlewares.configurable_auth', require('./middleware/configurable_auth'));
|
||||
def('puter.middlewares.anticsrf', require('./middleware/anticsrf'));
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* 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 { get_user, get_dir_size, id2path, id2uuid, is_empty, is_shared_with_anyone, suggest_app_for_fsentry, get_app } = require("../helpers");
|
||||
const { get_user, id2path, id2uuid, is_empty, suggest_app_for_fsentry, get_app } = require("../helpers");
|
||||
|
||||
const putility = require('@heyputer/putility');
|
||||
const config = require("../config");
|
||||
@@ -26,8 +26,9 @@ const { Context } = require("../util/context");
|
||||
const { NodeRawEntrySelector } = require("./node/selectors");
|
||||
const { DB_READ } = require("../services/database/consts");
|
||||
const { UserActorType, AppUnderUserActorType, Actor } = require("../services/auth/Actor");
|
||||
const { PermissionUtil } = require("../services/auth/PermissionUtils.mjs");
|
||||
const { PermissionUtil } = require("../services/auth/permissionUtils.mjs");
|
||||
const { ECMAP } = require("./ECMAP");
|
||||
const { MANAGE_PERM_PREFIX } = require("../services/auth/permissionConts.mjs");
|
||||
|
||||
/**
|
||||
* Container for information collected about a node
|
||||
@@ -72,25 +73,25 @@ module.exports = class FSNodeContext {
|
||||
* @param {*} opt_identifier.id please pass mysql_id instead
|
||||
* @param {*} opt_identifier.mysql_id a MySQL ID of the filesystem entry
|
||||
*/
|
||||
constructor ({
|
||||
constructor({
|
||||
services,
|
||||
selector,
|
||||
provider,
|
||||
fs
|
||||
fs,
|
||||
}) {
|
||||
const ecmap = Context.get(ECMAP.SYMBOL);
|
||||
|
||||
|
||||
if ( ecmap ) {
|
||||
// We might return an existing FSNodeContext
|
||||
const maybe_node = ecmap?.
|
||||
get_fsNodeContext_from_selector?.(selector);
|
||||
const maybe_node = ecmap
|
||||
?.get_fsNodeContext_from_selector?.(selector);
|
||||
if ( maybe_node ) return maybe_node;
|
||||
} else {
|
||||
if ( process.env.LOG_ECMAP ) {
|
||||
console.log('\x1B[31;1m !!! NO ECMAP !!! \x1B[0m');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// This will be used to avoid concurrent fetches. Whenever an entry is being fetched,
|
||||
// a subsequent call to fetchEntry must await this promise. Usually this means the
|
||||
// subsequent call will not perform any expensive operations.
|
||||
@@ -142,16 +143,16 @@ module.exports = class FSNodeContext {
|
||||
span.end();
|
||||
});
|
||||
return result;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
set selector (new_selector) {
|
||||
set selector(new_selector) {
|
||||
// Only add the selector if we don't already have it
|
||||
for ( const selector of this.selectors_ ) {
|
||||
if ( selector instanceof new_selector.constructor ) return;
|
||||
}
|
||||
|
||||
|
||||
const ecmap = Context.get(ECMAP.SYMBOL);
|
||||
if ( ecmap ) {
|
||||
ecmap.store_fsNodeContext_to_selector(new_selector, this);
|
||||
@@ -161,11 +162,11 @@ module.exports = class FSNodeContext {
|
||||
this.selector_ = new_selector;
|
||||
}
|
||||
|
||||
get selector () {
|
||||
get selector() {
|
||||
return this.get_optimal_selector();
|
||||
}
|
||||
|
||||
get_selector_of_type (cls) {
|
||||
get_selector_of_type(cls) {
|
||||
// Reverse iterate over selectors
|
||||
for ( let i = this.selectors_.length - 1; i >= 0; i-- ) {
|
||||
const selector = this.selectors_[i];
|
||||
@@ -181,7 +182,7 @@ module.exports = class FSNodeContext {
|
||||
return null;
|
||||
}
|
||||
|
||||
get_optimal_selector () {
|
||||
get_optimal_selector() {
|
||||
for ( const cls of FSNodeContext.SELECTOR_PRIORITY_ORDER ) {
|
||||
const selector = this.get_selector_of_type(cls);
|
||||
if ( selector ) return selector;
|
||||
@@ -190,11 +191,11 @@ module.exports = class FSNodeContext {
|
||||
return this.selector_;
|
||||
}
|
||||
|
||||
get isRoot () {
|
||||
get isRoot() {
|
||||
return this.path === '/';
|
||||
}
|
||||
|
||||
async isUserDirectory () {
|
||||
async isUserDirectory() {
|
||||
if ( this.isRoot ) return false;
|
||||
if ( this.found === undefined ) {
|
||||
await this.fetchEntry();
|
||||
@@ -204,7 +205,7 @@ module.exports = class FSNodeContext {
|
||||
return ! this.entry.parent_uid;
|
||||
}
|
||||
|
||||
async isAppDataDirectory () {
|
||||
async isAppDataDirectory() {
|
||||
if ( this.isRoot ) return false;
|
||||
if ( this.found === undefined ) {
|
||||
await this.fetchEntry();
|
||||
@@ -215,16 +216,16 @@ module.exports = class FSNodeContext {
|
||||
if ( components.length < 2 ) return false;
|
||||
return components[1] === 'AppData';
|
||||
}
|
||||
|
||||
async isPublic () {
|
||||
|
||||
async isPublic() {
|
||||
if ( this.isRoot ) return false;
|
||||
const components = await this.getPathComponents();
|
||||
if ( await this.isUserDirectory() ) return false;
|
||||
if ( components[1] === 'Public' ) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
async getPathComponents () {
|
||||
|
||||
async getPathComponents() {
|
||||
if ( this.isRoot ) return [];
|
||||
|
||||
// We can get path components for non-existing nodes if they
|
||||
@@ -243,33 +244,31 @@ module.exports = class FSNodeContext {
|
||||
if ( path.startsWith('/') ) path = path.slice(1);
|
||||
return path.split('/');
|
||||
}
|
||||
|
||||
async getUserPart () {
|
||||
|
||||
async getUserPart() {
|
||||
if ( this.isRoot ) return;
|
||||
const components = await this.getPathComponents();
|
||||
return components[0];
|
||||
}
|
||||
|
||||
async getPathSize () {
|
||||
async getPathSize() {
|
||||
if ( this.isRoot ) return;
|
||||
const components = await this.getPathComponents();
|
||||
return components.length;
|
||||
}
|
||||
|
||||
async exists ({ fetch_options } = {}) {
|
||||
|
||||
async exists({ fetch_options } = {}) {
|
||||
await this.fetchEntry(fetch_options);
|
||||
if ( ! this.found ) {
|
||||
this.log.debug(
|
||||
'here\'s why it doesn\'t exist: ' +
|
||||
this.log.debug('here\'s why it doesn\'t exist: ' +
|
||||
this.selector.describe() + ' -> ' +
|
||||
this.uid + ' ' +
|
||||
JSON.stringify(this.entry, null, ' ')
|
||||
);
|
||||
JSON.stringify(this.entry, null, ' '));
|
||||
}
|
||||
return this.found;
|
||||
}
|
||||
|
||||
async fetchPath () {
|
||||
async fetchPath() {
|
||||
if ( this.path ) return;
|
||||
|
||||
this.path = await this.services.get('information')
|
||||
@@ -288,7 +287,7 @@ module.exports = class FSNodeContext {
|
||||
* @param {*} fsEntryFetcher fetches the filesystem entry
|
||||
* @void
|
||||
*/
|
||||
async fetchEntry (fetch_entry_options = {}) {
|
||||
async fetchEntry(fetch_entry_options = {}) {
|
||||
if ( this.fetching !== null ) {
|
||||
await Context.get('services').get('traceService').spanify('fetching', async () => {
|
||||
// ???: does this need to be double-checked? I'm not actually sure...
|
||||
@@ -365,7 +364,7 @@ module.exports = class FSNodeContext {
|
||||
*
|
||||
* This just calls ResourceService under the hood.
|
||||
*/
|
||||
async awaitStableEntry () {
|
||||
async awaitStableEntry() {
|
||||
const resourceService = Context.get('services').get('resourceService');
|
||||
await resourceService.waitForResource(this.selector);
|
||||
}
|
||||
@@ -378,24 +377,22 @@ module.exports = class FSNodeContext {
|
||||
*
|
||||
* @param fs:decouple-subdomains
|
||||
*/
|
||||
async fetchSubdomains (user, force) {
|
||||
async fetchSubdomains(user, _force) {
|
||||
if ( ! this.entry.is_dir ) return;
|
||||
|
||||
const db = this.services.get('database').get(DB_READ, 'filesystem');
|
||||
|
||||
this.entry.subdomains = []
|
||||
let subdomains = await db.read(
|
||||
`SELECT * FROM subdomains WHERE root_dir_id = ? AND user_id = ?`,
|
||||
[this.entry.id, user.id]
|
||||
);
|
||||
if(subdomains.length > 0){
|
||||
subdomains.forEach((sd)=>{
|
||||
this.entry.subdomains = [];
|
||||
let subdomains = await db.read(`SELECT * FROM subdomains WHERE root_dir_id = ? AND user_id = ?`,
|
||||
[this.entry.id, user.id]);
|
||||
if ( subdomains.length > 0 ){
|
||||
subdomains.forEach((sd) => {
|
||||
this.entry.subdomains.push({
|
||||
subdomain: sd.subdomain,
|
||||
address: config.protocol + '://' + sd.subdomain + "." + 'puter.site',
|
||||
uuid: sd.uuid,
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
this.entry.has_website = true;
|
||||
}
|
||||
}
|
||||
@@ -405,7 +402,7 @@ module.exports = class FSNodeContext {
|
||||
* `owner` property of the fsentry.
|
||||
* @param {bool} force fetch owner if it was already fetched
|
||||
*/
|
||||
async fetchOwner (force) {
|
||||
async fetchOwner(_force) {
|
||||
if ( this.isRoot ) return;
|
||||
const owner = await get_user({ id: this.entry.user_id });
|
||||
this.entry.owner = {
|
||||
@@ -420,52 +417,79 @@ module.exports = class FSNodeContext {
|
||||
* of the fsentry.
|
||||
* @param {bool} force fetch shares if they were already fetched
|
||||
*/
|
||||
async fetchShares (force) {
|
||||
if (this.entry.shares && ! force ) return;
|
||||
|
||||
async fetchShares(force) {
|
||||
if ( this.entry.shares && ! force ) return;
|
||||
|
||||
const actor = Context.get('actor');
|
||||
if ( ! actor ) {
|
||||
this.entry.shares = { users: [], apps: [] };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if ( ! (actor.type instanceof UserActorType) ) {
|
||||
this.entry.shares = { users: [], apps: [] };
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const svc_permission = this.services.get('permission');
|
||||
|
||||
const permissions =
|
||||
await svc_permission.query_issuer_permissions_by_prefix(
|
||||
actor.type.user, `fs:${await this.get('uid')}:`);
|
||||
|
||||
|
||||
const fsPermPrefix = `fs:${await this.get('uid')}`;
|
||||
const [readWritePerms, managePerms] = await Promise.all([
|
||||
svc_permission.query_issuer_permissions_by_prefix(actor.type.user, `${fsPermPrefix}:`),
|
||||
svc_permission.query_issuer_permissions_by_prefix(actor.type.user, `${MANAGE_PERM_PREFIX}:${fsPermPrefix}`),
|
||||
]);
|
||||
|
||||
this.entry.shares = { users: [], apps: [] };
|
||||
|
||||
for ( const user_perm of permissions.users ) {
|
||||
for ( const readWriteUserPerms of readWritePerms.users ) {
|
||||
const access =
|
||||
PermissionUtil.split(user_perm.permission).slice(-1)[0];
|
||||
PermissionUtil.split(readWriteUserPerms.permission).slice(-1)[0];
|
||||
this.entry.shares.users.push({
|
||||
user: {
|
||||
uid: user_perm.user.uuid,
|
||||
username: user_perm.user.username,
|
||||
uid: readWriteUserPerms.user.uuid,
|
||||
username: readWriteUserPerms.user.username,
|
||||
},
|
||||
access,
|
||||
permission: user_perm.permission,
|
||||
permission: readWriteUserPerms.permission,
|
||||
});
|
||||
}
|
||||
for ( const manageUserPerms of managePerms.users ) {
|
||||
const access = MANAGE_PERM_PREFIX;
|
||||
this.entry.shares.users.push({
|
||||
user: {
|
||||
uid: manageUserPerms.user.uuid,
|
||||
username: manageUserPerms.user.username,
|
||||
},
|
||||
access,
|
||||
permission: manageUserPerms.permission,
|
||||
});
|
||||
}
|
||||
|
||||
for ( const app_perm of permissions.apps ) {
|
||||
for ( const readWriteAppPerms of readWritePerms.apps ) {
|
||||
const access =
|
||||
PermissionUtil.split(app_perm.permission).slice(-1)[0];
|
||||
PermissionUtil.split(readWriteAppPerms.permission).slice(-1)[0];
|
||||
this.entry.shares.apps.push({
|
||||
app: {
|
||||
icon: app_perm.app.icon,
|
||||
uid: app_perm.app.uid,
|
||||
name: app_perm.app.name,
|
||||
icon: readWriteAppPerms.app.icon,
|
||||
uid: readWriteAppPerms.app.uid,
|
||||
name: readWriteAppPerms.app.name,
|
||||
},
|
||||
access,
|
||||
permission: app_perm.permission,
|
||||
permission: readWriteAppPerms.permission,
|
||||
});
|
||||
}
|
||||
|
||||
for ( const manageAppPerms of readWritePerms.apps ) {
|
||||
const access =
|
||||
MANAGE_PERM_PREFIX;
|
||||
this.entry.shares.apps.push({
|
||||
app: {
|
||||
icon: manageAppPerms.app.icon,
|
||||
uid: manageAppPerms.app.uid,
|
||||
name: manageAppPerms.app.name,
|
||||
},
|
||||
access,
|
||||
permission: manageAppPerms.permission,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -478,26 +502,24 @@ module.exports = class FSNodeContext {
|
||||
*
|
||||
* @todo fs:decouple-versions
|
||||
*/
|
||||
async fetchVersions (force) {
|
||||
async fetchVersions(force) {
|
||||
if ( this.entry.versions && ! force ) return;
|
||||
|
||||
const db = this.services.get('database').get(DB_READ, 'filesystem');
|
||||
|
||||
let versions = await db.read(
|
||||
`SELECT * FROM fsentry_versions WHERE fsentry_id = ?`,
|
||||
[this.entry.id]
|
||||
);
|
||||
let versions = await db.read(`SELECT * FROM fsentry_versions WHERE fsentry_id = ?`,
|
||||
[this.entry.id]);
|
||||
const versions_tidy = [];
|
||||
for ( const version of versions ) {
|
||||
let username = version.user_id ? (await get_user({id: version.user_id})).username : null;
|
||||
let username = version.user_id ? (await get_user({ id: version.user_id })).username : null;
|
||||
versions_tidy.push({
|
||||
id: version.version_id,
|
||||
message: version.message,
|
||||
timestamp: version.ts_epoch,
|
||||
user: {
|
||||
username: username,
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.entry.versions = versions_tidy;
|
||||
@@ -507,7 +529,7 @@ module.exports = class FSNodeContext {
|
||||
* Fetches the size of a file or directory if it was not
|
||||
* already fetched.
|
||||
*/
|
||||
async fetchSize () {
|
||||
async fetchSize() {
|
||||
const { fsEntryService } = Context.get('services').values;
|
||||
|
||||
// we already have the size for files
|
||||
@@ -516,14 +538,12 @@ module.exports = class FSNodeContext {
|
||||
return this.entry.size;
|
||||
}
|
||||
|
||||
this.entry.size = await fsEntryService.get_recursive_size(
|
||||
this.entry.uuid,
|
||||
);
|
||||
this.entry.size = await fsEntryService.get_recursive_size(this.entry.uuid);
|
||||
|
||||
return this.entry.size;
|
||||
}
|
||||
|
||||
async fetchSuggestedApps (user, force) {
|
||||
async fetchSuggestedApps(user, force) {
|
||||
if ( this.entry.suggested_apps && ! force ) return;
|
||||
|
||||
await this.fetchEntry();
|
||||
@@ -533,7 +553,7 @@ module.exports = class FSNodeContext {
|
||||
await suggest_app_for_fsentry(this.entry, { user });
|
||||
}
|
||||
|
||||
async fetchIsEmpty () {
|
||||
async fetchIsEmpty() {
|
||||
if ( ! this.uid && ! this.path ) return;
|
||||
this.entry.is_empty = await is_empty({
|
||||
uid: this.uid,
|
||||
@@ -541,7 +561,7 @@ module.exports = class FSNodeContext {
|
||||
});
|
||||
}
|
||||
|
||||
async fetchAll(fsEntryFetcher, user, force) {
|
||||
async fetchAll(_fsEntryFetcher, user, _force) {
|
||||
await this.fetchEntry({ thumbnail: true });
|
||||
await this.fetchSubdomains(user);
|
||||
await this.fetchOwner();
|
||||
@@ -552,7 +572,7 @@ module.exports = class FSNodeContext {
|
||||
await this.fetchIsEmpty();
|
||||
}
|
||||
|
||||
async get (key) {
|
||||
async get(key) {
|
||||
/*
|
||||
This isn't supposed to stay like this!
|
||||
|
||||
@@ -566,19 +586,15 @@ module.exports = class FSNodeContext {
|
||||
*/
|
||||
|
||||
if ( this.found === false ) {
|
||||
throw new Error(
|
||||
`Tried to get ${key} of non-existent fsentry: ` +
|
||||
this.selector.describe(true)
|
||||
);
|
||||
throw new Error(`Tried to get ${key} of non-existent fsentry: ` +
|
||||
this.selector.describe(true));
|
||||
}
|
||||
|
||||
if ( key === 'entry' ) {
|
||||
await this.fetchEntry();
|
||||
if ( this.found === false ) {
|
||||
throw new Error(
|
||||
`Tried to get entry of non-existent fsentry: ` +
|
||||
this.selector.describe(true)
|
||||
);
|
||||
throw new Error(`Tried to get entry of non-existent fsentry: ` +
|
||||
this.selector.describe(true));
|
||||
}
|
||||
return this.entry;
|
||||
}
|
||||
@@ -586,10 +602,8 @@ module.exports = class FSNodeContext {
|
||||
if ( key === 'path' ) {
|
||||
if ( ! this.path ) await this.fetchEntry();
|
||||
if ( this.found === false ) {
|
||||
throw new Error(
|
||||
`Tried to get path of non-existent fsentry: ` +
|
||||
this.selector.describe(true)
|
||||
);
|
||||
throw new Error(`Tried to get path of non-existent fsentry: ` +
|
||||
this.selector.describe(true));
|
||||
}
|
||||
if ( ! this.path ) {
|
||||
await this.fetchPath();
|
||||
@@ -609,7 +623,7 @@ module.exports = class FSNodeContext {
|
||||
await this.fetchEntry();
|
||||
return this.mysql_id;
|
||||
}
|
||||
|
||||
|
||||
if ( key === 'owner' ) {
|
||||
const user_id = await this.get('user_id');
|
||||
const actor = new Actor({
|
||||
@@ -625,10 +639,8 @@ module.exports = class FSNodeContext {
|
||||
if ( key === k ) {
|
||||
await this.fetchEntry();
|
||||
if ( this.found === false ) {
|
||||
throw new Error(
|
||||
`Tried to get ${key} of non-existent fsentry: ` +
|
||||
this.selector.describe(true)
|
||||
);
|
||||
throw new Error(`Tried to get ${key} of non-existent fsentry: ` +
|
||||
this.selector.describe(true));
|
||||
}
|
||||
return this.entry[k];
|
||||
}
|
||||
@@ -674,7 +686,7 @@ module.exports = class FSNodeContext {
|
||||
await this.fetchEntry();
|
||||
return this.isRoot;
|
||||
}
|
||||
|
||||
|
||||
if ( key === 'writable' ) {
|
||||
const actor = Context.get('actor');
|
||||
if ( !actor || !actor.type.user ) return undefined;
|
||||
@@ -685,7 +697,7 @@ module.exports = class FSNodeContext {
|
||||
throw new Error(`unrecognize key for FSNodeContext.get: ${key}`);
|
||||
}
|
||||
|
||||
async getParent () {
|
||||
async getParent() {
|
||||
if ( this.isRoot ) {
|
||||
throw new Error('tried to get parent of root');
|
||||
}
|
||||
@@ -693,7 +705,7 @@ module.exports = class FSNodeContext {
|
||||
if ( this.path ) {
|
||||
const parent_fsNode = await this.fs.node({
|
||||
path: _path.dirname(this.path),
|
||||
})
|
||||
});
|
||||
return parent_fsNode;
|
||||
}
|
||||
|
||||
@@ -714,21 +726,20 @@ module.exports = class FSNodeContext {
|
||||
return this.fs.node(new NodeUIDSelector(parent_uid));
|
||||
}
|
||||
|
||||
async getChild (name) {
|
||||
async getChild(name) {
|
||||
// If we have a path, we can get an FSNodeContext for the child
|
||||
// without fetching anything.
|
||||
if ( this.path ) {
|
||||
const child_fsNode = await this.fs.node({
|
||||
path: _path.join(this.path, name),
|
||||
})
|
||||
});
|
||||
return child_fsNode;
|
||||
}
|
||||
|
||||
return await this.fs.node(new NodeChildSelector(
|
||||
this.selector, name));
|
||||
return await this.fs.node(new NodeChildSelector(this.selector, name));
|
||||
}
|
||||
|
||||
async getTarget () {
|
||||
async getTarget() {
|
||||
await this.fetchEntry();
|
||||
const type = await this.get('type');
|
||||
|
||||
@@ -745,7 +756,7 @@ module.exports = class FSNodeContext {
|
||||
return this;
|
||||
}
|
||||
|
||||
async is_above (child_fsNode) {
|
||||
async is_above(child_fsNode) {
|
||||
if ( this.isRoot ) return true;
|
||||
|
||||
const path_this = await this.get('path');
|
||||
@@ -754,7 +765,7 @@ module.exports = class FSNodeContext {
|
||||
return path_child.startsWith(path_this + '/');
|
||||
}
|
||||
|
||||
async is (fsNode) {
|
||||
async is(fsNode) {
|
||||
if ( this.mysql_id && fsNode.mysql_id ) {
|
||||
return this.mysql_id === fsNode.mysql_id;
|
||||
}
|
||||
@@ -768,12 +779,10 @@ module.exports = class FSNodeContext {
|
||||
return this.uid === fsNode.uid;
|
||||
}
|
||||
|
||||
async getSafeEntry (fetch_options = {}) {
|
||||
async getSafeEntry(fetch_options = {}) {
|
||||
if ( this.found === false ) {
|
||||
throw new Error(
|
||||
`Tried to get entry of non-existent fsentry: ` +
|
||||
this.selector.describe(true)
|
||||
);
|
||||
throw new Error(`Tried to get entry of non-existent fsentry: ` +
|
||||
this.selector.describe(true));
|
||||
}
|
||||
await this.fetchEntry(fetch_options);
|
||||
|
||||
@@ -794,7 +803,9 @@ module.exports = class FSNodeContext {
|
||||
|
||||
let actor; try {
|
||||
actor = Context.get('actor');
|
||||
} catch (e) {}
|
||||
} catch ( _e ) {
|
||||
// fail silently
|
||||
}
|
||||
if ( ! actor?.type?.user || actor.type.user.id !== res.user_id ) {
|
||||
if ( ! fsentry.owner ) await this.fetchOwner();
|
||||
fsentry.owner = {
|
||||
@@ -808,11 +819,9 @@ module.exports = class FSNodeContext {
|
||||
const info = this.services.get('information');
|
||||
|
||||
if ( ! this.uid && ! this.entry.uuid ) {
|
||||
this.log.noticeme(
|
||||
'whats even happening!?!? ' +
|
||||
this.selector.describe() + ' ' +
|
||||
JSON.stringify(this.entry, null, ' ')
|
||||
);
|
||||
this.log.noticeme('whats even happening!?!? ' +
|
||||
this.selector.describe() + ' ' +
|
||||
JSON.stringify(this.entry, null, ' '));
|
||||
}
|
||||
|
||||
// If fsentry was found by a path but the entry doesn't
|
||||
@@ -821,7 +830,7 @@ module.exports = class FSNodeContext {
|
||||
.with('fs.fsentry:uuid')
|
||||
.obtain('fs.fsentry:path')
|
||||
.exec(this.uid ?? this.entry.uuid);
|
||||
|
||||
|
||||
if ( fsentry.path && fsentry.path.startsWith('/-void/') ) {
|
||||
fsentry.broken = true;
|
||||
}
|
||||
@@ -857,13 +866,13 @@ module.exports = class FSNodeContext {
|
||||
try {
|
||||
fsentry.shortcut_to_path = (res.shortcut_to
|
||||
? await id2path(res.shortcut_to) : undefined);
|
||||
} catch (e) {
|
||||
} catch ( _e ) {
|
||||
fsentry.shortcut_invalid = true;
|
||||
fsentry.shortcut_uid = res.shortcut_to;
|
||||
}
|
||||
|
||||
// Add file_request_url
|
||||
if(res.file_request_token && res.file_request_token !== ''){
|
||||
if ( res.file_request_token && res.file_request_token !== '' ){
|
||||
fsentry.file_request_url = config.origin +
|
||||
'/upload?token=' + res.file_request_token;
|
||||
}
|
||||
@@ -872,7 +881,7 @@ module.exports = class FSNodeContext {
|
||||
const app = await get_app({ id: fsentry.associated_app_id });
|
||||
fsentry.associated_app = app;
|
||||
}
|
||||
|
||||
|
||||
// If this file is in an appdata directory, add `appdata_app`
|
||||
const components = await this.getPathComponents();
|
||||
if ( components[1] === 'AppData' ) {
|
||||
@@ -889,7 +898,7 @@ module.exports = class FSNodeContext {
|
||||
return fsentry;
|
||||
}
|
||||
|
||||
static sanitize_pending_entry_info (res) {
|
||||
static sanitize_pending_entry_info(res) {
|
||||
const fsentry = {};
|
||||
|
||||
// This property will not be serialized, but it can be checked
|
||||
@@ -924,4 +933,4 @@ module.exports = class FSNodeContext {
|
||||
|
||||
return fsentry;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -19,31 +19,25 @@
|
||||
// TODO: database access can be a service
|
||||
const { RESOURCE_STATUS_PENDING_CREATE } = require('../modules/puterfs/ResourceService.js');
|
||||
const { TraceService } = require('../services/TraceService.js');
|
||||
const PerformanceMonitor = require('../monitor/PerformanceMonitor.js');
|
||||
const { NodePathSelector, NodeUIDSelector, NodeInternalIDSelector } = require('./node/selectors.js');
|
||||
const FSNodeContext = require('./FSNodeContext.js');
|
||||
const { AdvancedBase } = require('@heyputer/putility');
|
||||
const { Context } = require('../util/context.js');
|
||||
const { simple_retry } = require('../util/retryutil.js');
|
||||
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, PermissionImplicator, PermissionExploder } = require('../services/auth/PermissionUtils.mjs');
|
||||
const { DB_WRITE } = require("../services/database/consts");
|
||||
const { PermissionUtil, PermissionRewriter, PermissionImplicator, PermissionExploder } = require('../services/auth/permissionUtils.mjs');
|
||||
const { DB_WRITE } = require('../services/database/consts');
|
||||
const { UserActorType } = require('../services/auth/Actor');
|
||||
const { get_user } = require('../helpers');
|
||||
const BaseService = require('../services/BaseService');
|
||||
const { PuterFSProvider } = require('../modules/puterfs/lib/PuterFSProvider.js');
|
||||
const { MANAGE_PERM_PREFIX } = require('../services/auth/permissionConts.mjs');
|
||||
|
||||
class FilesystemService extends BaseService {
|
||||
static MODULES = {
|
||||
_path: require('path'),
|
||||
uuidv4: require('uuid').v4,
|
||||
config: require('../config.js'),
|
||||
}
|
||||
};
|
||||
|
||||
old_constructor (args) {
|
||||
old_constructor(args) {
|
||||
const { services } = args;
|
||||
|
||||
services.registerService('traceService', TraceService);
|
||||
@@ -65,19 +59,19 @@ class FilesystemService extends BaseService {
|
||||
});
|
||||
}
|
||||
|
||||
async _init () {
|
||||
async _init() {
|
||||
this.old_constructor({ services: this.services });
|
||||
const svc_permission = this.services.get('permission');
|
||||
svc_permission.register_rewriter(PermissionRewriter.create({
|
||||
matcher: permission => {
|
||||
if ( ! permission.startsWith('fs:') ) return false;
|
||||
const [_, specifier] = PermissionUtil.split(permission);
|
||||
if ( !permission.startsWith('fs:') && !permission.startsWith('manage:fs:') ) return false;
|
||||
const [_, specifier] = permission.split('fs:');
|
||||
if ( ! specifier.startsWith('/') ) return false;
|
||||
return true;
|
||||
},
|
||||
rewriter: async permission => {
|
||||
const [_, path, ...rest] = PermissionUtil.split(permission);
|
||||
console.log('checking path: ', path);
|
||||
const [manageOpt, pathPerm] = permission.split('fs:');
|
||||
const [path, ...rest] = PermissionUtil.split(pathPerm);
|
||||
const node = await this.node(new NodePathSelector(path));
|
||||
if ( ! await node.exists() ) {
|
||||
// TOOD: we need a general-purpose error that can have
|
||||
@@ -89,21 +83,24 @@ class FilesystemService extends BaseService {
|
||||
if ( uid === undefined || uid === 'undefined' ) {
|
||||
throw new Error(`uid is undefined for path ${path}`);
|
||||
}
|
||||
return `fs:${uid}:${rest.join(':')}`;
|
||||
return [manageOpt.replace(':', ''), 'fs', uid, ...rest].filter(Boolean).join(':');
|
||||
},
|
||||
}));
|
||||
svc_permission.register_implicator(PermissionImplicator.create({
|
||||
id: 'is-owner',
|
||||
shortcut: true,
|
||||
matcher: permission => {
|
||||
return permission.startsWith('fs:');
|
||||
// TODO DS: for now users will only have manage access on files, that might change, and then this has to change too
|
||||
return permission.startsWith('fs:')
|
||||
|| permission.startsWith(`${MANAGE_PERM_PREFIX}:fs:`)
|
||||
|| permission.startsWith(`${MANAGE_PERM_PREFIX}:${MANAGE_PERM_PREFIX}:fs:`); // owner has implicit rule to give others manage access;
|
||||
},
|
||||
checker: async ({ actor, permission }) => {
|
||||
if ( !(actor.type instanceof UserActorType) ) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [_, uid] = PermissionUtil.split(permission);
|
||||
const [_, uid] = PermissionUtil.split(permission.replaceAll(`${MANAGE_PERM_PREFIX}:`, ''));
|
||||
const node = await this.node(new NodeUIDSelector(uid));
|
||||
|
||||
if ( ! await node.exists() ) {
|
||||
@@ -111,12 +108,10 @@ class FilesystemService extends BaseService {
|
||||
}
|
||||
|
||||
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'
|
||||
);
|
||||
throw new Error('something unexpected happened');
|
||||
}
|
||||
|
||||
if ( owner_id === actor.type.user.id ) {
|
||||
@@ -134,32 +129,26 @@ class FilesystemService extends BaseService {
|
||||
},
|
||||
exploder: async ({ permission }) => {
|
||||
const permissions = [permission];
|
||||
const parts = PermissionUtil.split(permission);
|
||||
const [fsPrefix, fileId, specifiedMode, ...rest] = PermissionUtil.split(permission);
|
||||
|
||||
const specified_mode = parts[2];
|
||||
|
||||
const rules = {
|
||||
see: ['list', 'read', 'write'],
|
||||
list: ['read', 'write'],
|
||||
read: ['write'],
|
||||
};
|
||||
|
||||
if ( rules.hasOwnProperty(specified_mode) ) {
|
||||
permissions.push(...rules[specified_mode].map(
|
||||
mode => PermissionUtil.join(
|
||||
parts[0], parts[1],
|
||||
mode,
|
||||
...parts.slice(3),
|
||||
)
|
||||
));
|
||||
|
||||
if ( rules[specifiedMode] ) {
|
||||
permissions.push(...rules[specifiedMode].map(mode => PermissionUtil.join(fsPrefix, fileId, mode, ...rest.slice(1))));
|
||||
// push manage permission as well
|
||||
permissions.push(PermissionUtil.join(MANAGE_PERM_PREFIX, fsPrefix, fileId));
|
||||
}
|
||||
|
||||
|
||||
return permissions;
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
async mkshortcut ({ parent, name, user, target }) {
|
||||
async mkshortcut({ parent, name, user, target }) {
|
||||
|
||||
// Access Control
|
||||
{
|
||||
@@ -192,7 +181,7 @@ class FilesystemService extends BaseService {
|
||||
status: RESOURCE_STATUS_PENDING_CREATE,
|
||||
});
|
||||
|
||||
console.log('registered entry')
|
||||
console.log('registered entry');
|
||||
|
||||
const raw_fsentry = {
|
||||
is_shortcut: 1,
|
||||
@@ -210,7 +199,7 @@ class FilesystemService extends BaseService {
|
||||
immutable: false,
|
||||
};
|
||||
|
||||
this.log.debug('creating fsentry', { fsentry: raw_fsentry })
|
||||
this.log.debug('creating fsentry', { fsentry: raw_fsentry });
|
||||
|
||||
const entryOp = await svc_fsEntry.insert(raw_fsentry);
|
||||
|
||||
@@ -218,7 +207,7 @@ class FilesystemService extends BaseService {
|
||||
|
||||
(async () => {
|
||||
await entryOp.awaitDone();
|
||||
this.log.debug('finished creating fsentry', { uid })
|
||||
this.log.debug('finished creating fsentry', { uid });
|
||||
resourceService.free(uid);
|
||||
})();
|
||||
|
||||
@@ -233,7 +222,7 @@ class FilesystemService extends BaseService {
|
||||
return node;
|
||||
}
|
||||
|
||||
async mklink ({ parent, name, user, target }) {
|
||||
async mklink({ parent, name, user, target }) {
|
||||
|
||||
// Access Control
|
||||
{
|
||||
@@ -274,13 +263,13 @@ class FilesystemService extends BaseService {
|
||||
immutable: false,
|
||||
};
|
||||
|
||||
this.log.debug('creating symlink', { fsentry: raw_fsentry })
|
||||
this.log.debug('creating symlink', { fsentry: raw_fsentry });
|
||||
|
||||
const entryOp = await svc_fsEntry.insert(raw_fsentry);
|
||||
|
||||
(async () => {
|
||||
await entryOp.awaitDone();
|
||||
this.log.debug('finished creating symlink', { uid })
|
||||
this.log.debug('finished creating symlink', { uid });
|
||||
resourceService.free(uid);
|
||||
})();
|
||||
|
||||
@@ -295,17 +284,15 @@ class FilesystemService extends BaseService {
|
||||
return node;
|
||||
}
|
||||
|
||||
async update_child_paths (old_path, new_path, user_id) {
|
||||
async update_child_paths(old_path, new_path, user_id) {
|
||||
const svc_performanceMonitor = this.services.get('performance-monitor');
|
||||
const monitor = svc_performanceMonitor.createContext('update_child_paths');
|
||||
|
||||
if ( ! old_path.endsWith('/') ) old_path += '/';
|
||||
if ( ! new_path.endsWith('/') ) new_path += '/';
|
||||
// TODO: fs:decouple-tree-storage
|
||||
await this.db.write(
|
||||
`UPDATE fsentries SET path = CONCAT(?, SUBSTRING(path, ?)) WHERE path LIKE ? AND user_id = ?`,
|
||||
[new_path, old_path.length + 1, old_path + '%', user_id]
|
||||
);
|
||||
await this.db.write('UPDATE fsentries SET path = CONCAT(?, SUBSTRING(path, ?)) WHERE path LIKE ? AND user_id = ?',
|
||||
[new_path, old_path.length + 1, `${old_path}%`, user_id]);
|
||||
|
||||
const log = this.services.get('log-service').create('update_child_paths');
|
||||
log.info(`updated ${old_path} -> ${new_path}`);
|
||||
@@ -322,7 +309,7 @@ class FilesystemService extends BaseService {
|
||||
* @param {*} location - path, uid, or id associated with a filesystem node
|
||||
* @returns
|
||||
*/
|
||||
async node (selector) {
|
||||
async node(selector) {
|
||||
if ( typeof selector === 'string' ) {
|
||||
if ( selector.startsWith('/') ) {
|
||||
selector = new NodePathSelector(selector);
|
||||
@@ -339,20 +326,19 @@ class FilesystemService extends BaseService {
|
||||
} else if ( selector.uid ) {
|
||||
selector = new NodeUIDSelector(selector.uid);
|
||||
} else {
|
||||
selector = new NodeInternalIDSelector(
|
||||
'mysql', selector.mysql_id);
|
||||
selector = new NodeInternalIDSelector('mysql', selector.mysql_id);
|
||||
}
|
||||
}
|
||||
|
||||
system_dir_check: {
|
||||
if ( ! (selector instanceof NodePathSelector) ) break system_dir_check;
|
||||
if ( ! selector.value.startsWith('/')) break system_dir_check;
|
||||
if ( ! selector.value.startsWith('/') ) break system_dir_check;
|
||||
|
||||
// OPTIMIZATION: Check if the path matches a system directory pattern.
|
||||
const systemDirRegex = /^\/([a-zA-Z0-9_]+)\/(Trash|AppData|Desktop|Documents|Pictures|Videos|Public)$/;
|
||||
const match = selector.value.match(systemDirRegex);
|
||||
if ( ! match ) break system_dir_check;
|
||||
|
||||
|
||||
const username = match[1];
|
||||
const dirName = match[2];
|
||||
|
||||
@@ -360,7 +346,7 @@ class FilesystemService extends BaseService {
|
||||
const user = await get_user({ username });
|
||||
if ( ! user ) break system_dir_check;
|
||||
|
||||
let uuidKey = ( selector.value === '/' + user.username )
|
||||
let uuidKey = ( selector.value === `/${user.username}` )
|
||||
? 'home_uuid'
|
||||
: `${dirName.toLowerCase()}_uuid`; // e.g., 'desktop_uuid'
|
||||
|
||||
@@ -378,9 +364,9 @@ class FilesystemService extends BaseService {
|
||||
provider,
|
||||
services: this.services,
|
||||
selector,
|
||||
fs: this
|
||||
fs: this,
|
||||
});
|
||||
|
||||
|
||||
return fsNode;
|
||||
}
|
||||
|
||||
@@ -401,7 +387,7 @@ class FilesystemService extends BaseService {
|
||||
* @param {*} param0.id please use mysql_id instead
|
||||
* @param {*} param0.mysql_id
|
||||
*/
|
||||
async get_entry ({ path, uid, id, mysql_id, ...options }) {
|
||||
async get_entry({ path, uid, id, mysql_id, ...options }) {
|
||||
let fsNode = await this.node({ path, uid, id, mysql_id });
|
||||
await fsNode.fetchEntry(options);
|
||||
return fsNode.entry;
|
||||
@@ -409,5 +395,5 @@ class FilesystemService extends BaseService {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
FilesystemService
|
||||
FilesystemService,
|
||||
};
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const { get_user } = require("../../helpers");
|
||||
const { PermissionUtil } = require("../../services/auth/PermissionUtils.mjs");
|
||||
const { MANAGE_PERM_PREFIX } = require("../../services/auth/permissionConts.mjs");
|
||||
const { PermissionUtil } = require("../../services/auth/permissionUtils.mjs");
|
||||
const { DB_WRITE } = require("../../services/database/consts");
|
||||
const { NodeUIDSelector } = require("../node/selectors");
|
||||
const { LLFilesystemOperation } = require("./definitions");
|
||||
@@ -32,8 +33,8 @@ class LLReadShares extends LLFilesystemOperation {
|
||||
found with "see" permission is found, children of that node
|
||||
will not be traversed.
|
||||
`;
|
||||
|
||||
async _run () {
|
||||
|
||||
async _run() {
|
||||
const { subject, user, actor } = this.values;
|
||||
|
||||
const svc = this.context.get('services');
|
||||
@@ -44,16 +45,14 @@ class LLReadShares extends LLFilesystemOperation {
|
||||
|
||||
const issuer_username = await subject.getUserPart();
|
||||
const issuer_user = await get_user({ username: issuer_username });
|
||||
const rows = await db.read(
|
||||
'SELECT DISTINCT permission FROM `user_to_user_permissions` ' +
|
||||
const rows = await db.read('SELECT DISTINCT permission FROM `user_to_user_permissions` ' +
|
||||
'WHERE `holder_user_id` = ? AND `issuer_user_id` = ? ' +
|
||||
'AND `permission` LIKE ?',
|
||||
[user.id, issuer_user.id, 'fs:%']
|
||||
);
|
||||
'AND (`permission` LIKE ? OR `permission` LIKE ?)',
|
||||
[user.id, issuer_user.id, 'fs:%', 'manage:fs:%']);
|
||||
|
||||
const fsentry_uuids = [];
|
||||
for ( const row of rows ) {
|
||||
const parts = PermissionUtil.split(row.permission);
|
||||
const parts = PermissionUtil.split(row.permission.replace(`${MANAGE_PERM_PREFIX}:`, ''));
|
||||
fsentry_uuids.push(parts[1]);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
const { get_app } = require("../../helpers");
|
||||
const { UserActorType } = require("../../services/auth/Actor");
|
||||
const { PermissionImplicator, PermissionUtil, PermissionRewriter } =
|
||||
require("../../services/auth/PermissionUtils.mjs");
|
||||
require("../../services/auth/permissionUtils.mjs");
|
||||
const BaseService = require("../../services/BaseService");
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
const { PassThrough } = require("stream");
|
||||
const APIError = require("../../api/APIError");
|
||||
const config = require("../../config");
|
||||
const { PermissionUtil } = require("../../services/auth/PermissionUtils.mjs");
|
||||
const { PermissionUtil } = require("../../services/auth/permissionUtils.mjs");
|
||||
const BaseService = require("../../services/BaseService");
|
||||
const { DB_WRITE } = require("../../services/database/consts");
|
||||
const { TypeSpec } = require("../../services/drivers/meta/Construct");
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const { AppUnderUserActorType, UserActorType } = require("../../services/auth/Actor");
|
||||
const { PermissionUtil } = require("../../services/auth/PermissionUtils.mjs");
|
||||
const { PermissionUtil } = require("../../services/auth/permissionUtils.mjs");
|
||||
const { Context } = require("../../util/context");
|
||||
const { BaseES } = require("./BaseES");
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ const APIError = require("../../api/APIError");
|
||||
const eggspress = require("../../api/eggspress");
|
||||
const { get_app } = require("../../helpers");
|
||||
const { UserActorType, Actor, AppUnderUserActorType } = require("../../services/auth/Actor");
|
||||
const { PermissionUtil } = require("../../services/auth/PermissionUtils.mjs");
|
||||
const { PermissionUtil } = require("../../services/auth/permissionUtils.mjs");
|
||||
const { Context } = require("../../util/context");
|
||||
|
||||
module.exports = eggspress('/auth/check-app', {
|
||||
|
||||
53
src/backend/src/routers/auth/check-permissions.js
Normal file
53
src/backend/src/routers/auth/check-permissions.js
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) 2024-present Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* 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 eggspress = require('../../api/eggspress');
|
||||
const { UserActorType } = require('../../services/auth/Actor');
|
||||
const { Context } = require('../../util/context');
|
||||
const APIError = require('../../api/APIError');
|
||||
|
||||
module.exports = eggspress('/auth/check-permissions', {
|
||||
subdomain: 'api',
|
||||
auth2: true,
|
||||
allowedMethods: ['POST'],
|
||||
}, async (req, res, _next) => {
|
||||
const context = Context.get();
|
||||
/** @type {import('../../services/auth/PermissionService').PermissionService} */
|
||||
const permissionService = context.get('services').get('permission');
|
||||
|
||||
const permsToCheck = req.body.permissions;
|
||||
|
||||
const actor = context.get('actor');
|
||||
|
||||
// Apps cannot (currently) check permissions on behalf of users
|
||||
if ( ! ( actor.type instanceof UserActorType ) ) {
|
||||
throw APIError.create('forbidden');
|
||||
}
|
||||
|
||||
const permEntryPromises = [...new Set(permsToCheck)].map(async (perm) => {
|
||||
try {
|
||||
return [perm, permissionService.check(actor, perm)];
|
||||
} catch {
|
||||
return [perm, false];
|
||||
}
|
||||
});
|
||||
|
||||
const permEntries = Promise.all(permEntryPromises);
|
||||
|
||||
res.json({ permissions: Object.fromEntries(await permEntries) });
|
||||
});
|
||||
@@ -26,7 +26,7 @@ const { TYPE_DIRECTORY } = require("../../filesystem/FSNodeContext");
|
||||
const { LLRead } = require("../../filesystem/ll_operations/ll_read");
|
||||
const { Actor, UserActorType, SiteActorType } = require("../../services/auth/Actor");
|
||||
const APIError = require("../../api/APIError");
|
||||
const { PermissionUtil } = require("../../services/auth/PermissionUtils.mjs");
|
||||
const { PermissionUtil } = require("../../services/auth/permissionUtils.mjs");
|
||||
const { default: dedent } = require("dedent");
|
||||
|
||||
const AT_DIRECTORY_NAMESPACE = '4aa6dc52-34c1-4b8a-b63c-a62b27f727cf';
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
// METADATA // {"ai-commented":{"service":"claude"}}
|
||||
const { Context } = require("../util/context");
|
||||
const { whatis } = require("../util/langutil");
|
||||
const { PermissionUtil } = require("./auth/PermissionUtils.mjs");
|
||||
const { PermissionUtil } = require("./auth/permissionUtils.mjs");
|
||||
const BaseService = require("./BaseService");
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,7 +23,6 @@ const { Endpoint } = require("../util/expressutil");
|
||||
const { whatis } = require("../util/langutil");
|
||||
const BaseService = require("./BaseService");
|
||||
|
||||
|
||||
/**
|
||||
* @class PermissionAPIService
|
||||
* @extends BaseService
|
||||
@@ -37,7 +36,6 @@ class PermissionAPIService extends BaseService {
|
||||
express: require('express'),
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Installs routes for authentication and permission management into the Express app
|
||||
* @param {Object} _ Unused parameter
|
||||
@@ -45,24 +43,23 @@ class PermissionAPIService extends BaseService {
|
||||
* @param {Express} options.app Express application instance to install routes on
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async ['__on_install.routes'] (_, { app }) {
|
||||
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-dev-app'))
|
||||
app.use(require('../routers/auth/revoke-dev-app'))
|
||||
async ['__on_install.routes'](_, { app }) {
|
||||
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-dev-app'));
|
||||
app.use(require('../routers/auth/revoke-dev-app'));
|
||||
app.use(require('../routers/auth/grant-user-user'));
|
||||
app.use(require('../routers/auth/revoke-user-user'));
|
||||
app.use(require('../routers/auth/grant-user-group'));
|
||||
app.use(require('../routers/auth/revoke-user-group'));
|
||||
app.use(require('../routers/auth/list-permissions'))
|
||||
app.use(require('../routers/auth/list-permissions'));
|
||||
app.use(require('../routers/auth/check-permissions.js'));
|
||||
|
||||
Endpoint(
|
||||
require('../routers/auth/check-app-acl.endpoint.js'),
|
||||
).but({
|
||||
Endpoint(require('../routers/auth/check-app-acl.endpoint.js')).but({
|
||||
route: '/auth/check-app-acl',
|
||||
}).attach(app);
|
||||
|
||||
|
||||
// track: scoping iife
|
||||
/**
|
||||
* Creates a scoped router for group-related endpoints using an IIFE pattern
|
||||
@@ -72,21 +69,21 @@ class PermissionAPIService extends BaseService {
|
||||
const r_group = (() => {
|
||||
const require = this.require;
|
||||
const express = require('express');
|
||||
return express.Router()
|
||||
return express.Router();
|
||||
})();
|
||||
|
||||
this.install_group_endpoints_({ router: r_group });
|
||||
app.use('/group', r_group);
|
||||
}
|
||||
|
||||
install_group_endpoints_ ({ router }) {
|
||||
|
||||
install_group_endpoints_({ router }) {
|
||||
Endpoint({
|
||||
route: '/create',
|
||||
methods: ['POST'],
|
||||
mw: [configurable_auth()],
|
||||
handler: async (req, res) => {
|
||||
const owner_user_id = req.user.id;
|
||||
|
||||
|
||||
const extra = req.body.extra ?? {};
|
||||
const metadata = req.body.metadata ?? {};
|
||||
if ( whatis(extra) !== 'object' ) {
|
||||
@@ -94,14 +91,14 @@ class PermissionAPIService extends BaseService {
|
||||
key: 'extra',
|
||||
expected: 'object',
|
||||
got: whatis(extra),
|
||||
})
|
||||
});
|
||||
}
|
||||
if ( whatis(metadata) !== 'object' ) {
|
||||
throw APIError.create('field_invalid', null, {
|
||||
key: 'metadata',
|
||||
expected: 'object',
|
||||
got: whatis(metadata),
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
const svc_group = this.services.get('group');
|
||||
@@ -112,33 +109,32 @@ class PermissionAPIService extends BaseService {
|
||||
// Metadata can be specified in request
|
||||
metadata: metadata ?? {},
|
||||
});
|
||||
|
||||
|
||||
res.json({ uid });
|
||||
}
|
||||
},
|
||||
}).attach(router);
|
||||
|
||||
|
||||
Endpoint({
|
||||
route: '/add-users',
|
||||
methods: ['POST'],
|
||||
mw: [configurable_auth()],
|
||||
handler: async (req, res) => {
|
||||
const svc_group = this.services.get('group')
|
||||
|
||||
const svc_group = this.services.get('group');
|
||||
|
||||
// TODO: validate string and uuid for request
|
||||
|
||||
const group = await svc_group.get(
|
||||
{ uid: req.body.uid });
|
||||
|
||||
const group = await svc_group.get({ uid: req.body.uid });
|
||||
|
||||
if ( ! group ) {
|
||||
throw APIError.create('entity_not_found', null, {
|
||||
identifier: req.body.uid,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if ( group.owner_user_id !== req.user.id ) {
|
||||
throw APIError.create('forbidden');
|
||||
}
|
||||
|
||||
|
||||
if ( whatis(req.body.users) !== 'array' ) {
|
||||
throw APIError.create('field_invalid', null, {
|
||||
key: 'users',
|
||||
@@ -146,8 +142,8 @@ class PermissionAPIService extends BaseService {
|
||||
got: whatis(req.body.users),
|
||||
});
|
||||
}
|
||||
|
||||
for ( let i=0 ; i < req.body.users.length ; i++ ) {
|
||||
|
||||
for ( let i = 0 ; i < req.body.users.length ; i++ ) {
|
||||
const value = req.body.users[i];
|
||||
if ( whatis(value) === 'string' ) continue;
|
||||
throw APIError.create('field_invalid', null, {
|
||||
@@ -156,14 +152,14 @@ class PermissionAPIService extends BaseService {
|
||||
got: whatis(value),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
await svc_group.add_users({
|
||||
uid: req.body.uid,
|
||||
users: req.body.users,
|
||||
});
|
||||
|
||||
|
||||
res.json({});
|
||||
}
|
||||
},
|
||||
}).attach(router);
|
||||
|
||||
// TODO: DRY: add-users is very similar
|
||||
@@ -172,23 +168,22 @@ class PermissionAPIService extends BaseService {
|
||||
methods: ['POST'],
|
||||
mw: [configurable_auth()],
|
||||
handler: async (req, res) => {
|
||||
const svc_group = this.services.get('group')
|
||||
|
||||
const svc_group = this.services.get('group');
|
||||
|
||||
// TODO: validate string and uuid for request
|
||||
|
||||
const group = await svc_group.get(
|
||||
{ uid: req.body.uid });
|
||||
|
||||
const group = await svc_group.get({ uid: req.body.uid });
|
||||
|
||||
if ( ! group ) {
|
||||
throw APIError.create('entity_not_found', null, {
|
||||
identifier: req.body.uid,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if ( group.owner_user_id !== req.user.id ) {
|
||||
throw APIError.create('forbidden');
|
||||
}
|
||||
|
||||
|
||||
if ( whatis(req.body.users) !== 'array' ) {
|
||||
throw APIError.create('field_invalid', null, {
|
||||
key: 'users',
|
||||
@@ -196,8 +191,8 @@ class PermissionAPIService extends BaseService {
|
||||
got: whatis(req.body.users),
|
||||
});
|
||||
}
|
||||
|
||||
for ( let i=0 ; i < req.body.users.length ; i++ ) {
|
||||
|
||||
for ( let i = 0 ; i < req.body.users.length ; i++ ) {
|
||||
const value = req.body.users[i];
|
||||
if ( whatis(value) === 'string' ) continue;
|
||||
throw APIError.create('field_invalid', null, {
|
||||
@@ -206,14 +201,14 @@ class PermissionAPIService extends BaseService {
|
||||
got: whatis(value),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
await svc_group.remove_users({
|
||||
uid: req.body.uid,
|
||||
users: req.body.users,
|
||||
});
|
||||
|
||||
|
||||
res.json({});
|
||||
}
|
||||
},
|
||||
}).attach(router);
|
||||
|
||||
Endpoint({
|
||||
@@ -222,26 +217,21 @@ class PermissionAPIService extends BaseService {
|
||||
mw: [configurable_auth()],
|
||||
handler: async (req, res) => {
|
||||
const svc_group = this.services.get('group');
|
||||
|
||||
|
||||
// TODO: validate string and uuid for request
|
||||
|
||||
const owned_groups = await svc_group.list_groups_with_owner(
|
||||
{ owner_user_id: req.user.id });
|
||||
const owned_groups = await svc_group.list_groups_with_owner({ owner_user_id: req.user.id });
|
||||
|
||||
const in_groups = await svc_group.list_groups_with_member(
|
||||
{ user_id: req.user.id });
|
||||
const in_groups = await svc_group.list_groups_with_member({ user_id: req.user.id });
|
||||
|
||||
const public_groups = await svc_group.list_public_groups();
|
||||
|
||||
res.json({
|
||||
owned_groups: await Promise.all(owned_groups.map(
|
||||
g => g.get_client_value({ members: true }))),
|
||||
in_groups: await Promise.all(in_groups.map(
|
||||
g => g.get_client_value({ members: true }))),
|
||||
public_groups: await Promise.all(public_groups.map(
|
||||
g => g.get_client_value())),
|
||||
owned_groups: await Promise.all(owned_groups.map(g => g.get_client_value({ members: true }))),
|
||||
in_groups: await Promise.all(in_groups.map(g => g.get_client_value({ members: true }))),
|
||||
public_groups: await Promise.all(public_groups.map(g => g.get_client_value())),
|
||||
});
|
||||
}
|
||||
},
|
||||
}).attach(router);
|
||||
|
||||
Endpoint({
|
||||
@@ -253,7 +243,7 @@ class PermissionAPIService extends BaseService {
|
||||
user: this.global_config.default_user_group,
|
||||
temp: this.global_config.default_temp_group,
|
||||
});
|
||||
}
|
||||
},
|
||||
}).attach(router);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
*/
|
||||
const { NodeInternalIDSelector, NodeUIDSelector } = require("../filesystem/node/selectors");
|
||||
const { SiteActorType } = require("./auth/Actor");
|
||||
const { PermissionUtil, PermissionRewriter, PermissionImplicator } = require("./auth/PermissionUtils.mjs");
|
||||
const { PermissionUtil, PermissionRewriter, PermissionImplicator } = require("./auth/permissionUtils.mjs");
|
||||
const BaseService = require("./BaseService");
|
||||
const { DB_WRITE } = require("./database/consts");
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { PermissionImplicator, PermissionUtil } = require("./auth/PermissionUtils.mjs");
|
||||
const { PermissionImplicator, PermissionUtil } = require("./auth/permissionUtils.mjs");
|
||||
const BaseService = require("./BaseService")
|
||||
|
||||
const APIError = require("../api/APIError");
|
||||
|
||||
@@ -26,7 +26,8 @@ const { Context } = require('../../util/context');
|
||||
const { Endpoint } = require('../../util/expressutil');
|
||||
const BaseService = require('../BaseService');
|
||||
const { AppUnderUserActorType, UserActorType, Actor, SystemActorType, AccessTokenActorType } = require('./Actor');
|
||||
const { PermissionUtil } = require('./PermissionUtils.mjs');
|
||||
const { MANAGE_PERM_PREFIX } = require('./permissionConts.mjs');
|
||||
const { PermissionUtil } = require('./permissionUtils.mjs');
|
||||
|
||||
/**
|
||||
* ACLService class handles Access Control List functionality for the Puter filesystem.
|
||||
@@ -72,7 +73,7 @@ class ACLService extends BaseService {
|
||||
*
|
||||
* @param {Actor} actor - The actor requesting permission
|
||||
* @param {FSNode} resource - The filesystem resource to check permissions for
|
||||
* @param {('see'| 'list'| 'read'| 'write')} mode - The permission mode to check ('see', 'list', 'read', 'write')
|
||||
* @param {('see'| 'list'| 'read'| 'write' | 'manage')} mode - The permission mode to check ('see', 'list', 'read', 'write', 'manage')
|
||||
* @returns {Promise<boolean>} True if actor has permission, false otherwise
|
||||
*/
|
||||
return await Context.get().sub({ logdent: ld }).arun(async () => {
|
||||
@@ -227,11 +228,13 @@ class ACLService extends BaseService {
|
||||
});
|
||||
}
|
||||
|
||||
let uid, _;
|
||||
let uid;
|
||||
|
||||
if ( typeof resource === 'string' && mode === undefined ) {
|
||||
const perm_parts = PermissionUtil.split(resource);
|
||||
([_, uid, mode] = perm_parts);
|
||||
const isManage = PermissionUtil.isManage(resource);
|
||||
uid = perm_parts.at(isManage ? -1 : -2); // always will end with fs:uid:mode
|
||||
mode = isManage ? MANAGE_PERM_PREFIX : perm_parts.at(-1);
|
||||
resource = await svc_fs.node(new NodePathSelector(uid));
|
||||
if ( ! resource ) {
|
||||
throw APIError.create('subject_does_not_exist');
|
||||
@@ -249,7 +252,7 @@ class ACLService extends BaseService {
|
||||
|
||||
const perms_on_this = stat[await resource.get('path')] ?? [];
|
||||
|
||||
const mode_parts = perms_on_this.map(perm => PermissionUtil.split(perm)[2]);
|
||||
const mode_parts = perms_on_this.map(perm => PermissionUtil.isManage(perm) ? MANAGE_PERM_PREFIX : PermissionUtil.split(perm).at(-1));
|
||||
|
||||
// If mode already present, do nothing
|
||||
if ( mode_parts.includes(mode) ) {
|
||||
@@ -259,7 +262,7 @@ class ACLService extends BaseService {
|
||||
// If higher mode already present, do nothing
|
||||
if ( options.only_if_higher ) {
|
||||
const higher_modes = this._higher_modes(mode);
|
||||
if ( mode_parts.some(m => higher_modes.includes(m)) ) {
|
||||
if ( mode_parts.some(m => m === MANAGE_PERM_PREFIX || higher_modes.includes(m)) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -267,12 +270,12 @@ class ACLService extends BaseService {
|
||||
uid = uid ?? await resource.get('uid');
|
||||
|
||||
// If mode not present, add it
|
||||
await svc_perm.grant_user_user_permission(issuer, holder.type.user.username, PermissionUtil.join('fs', uid, mode));
|
||||
await svc_perm.grant_user_user_permission(issuer, holder.type.user.username, mode === MANAGE_PERM_PREFIX ? PermissionUtil.join(MANAGE_PERM_PREFIX, 'fs', uid) : PermissionUtil.join('fs', uid, mode));
|
||||
|
||||
// Remove other modes
|
||||
for ( const perm of perms_on_this ) {
|
||||
const perm_parts = PermissionUtil.split(perm);
|
||||
if ( perm_parts[2] === mode ) continue;
|
||||
const existingPermMode = PermissionUtil.isManage(perm) ? MANAGE_PERM_PREFIX : PermissionUtil.split(perm).at(-1);
|
||||
if ( existingPermMode === mode ) continue;
|
||||
|
||||
await svc_perm.revoke_user_user_permission(issuer, holder.type.user.username, perm);
|
||||
}
|
||||
@@ -323,7 +326,7 @@ class ACLService extends BaseService {
|
||||
*
|
||||
* @param {Actor} actor - The actor requesting access (User, System, AccessToken, or AppUnderUser)
|
||||
* @param {FSNode} fsNode - The filesystem node to check permissions for
|
||||
* @param {'see'| 'list' | 'read' | 'write'} mode - The permission mode to check ('see', 'list', 'read', 'write')
|
||||
* @param {'see'| 'list' | 'read' | 'write' | 'manage'} mode - The permission mode to check ('see', 'list', 'read', 'write', 'manage)
|
||||
* @returns {Promise<boolean>} True if actor has permission, false otherwise
|
||||
*
|
||||
* @description
|
||||
@@ -483,11 +486,10 @@ class ACLService extends BaseService {
|
||||
*/
|
||||
const svc_permission = await context.get('services').get('permission');
|
||||
|
||||
const modes = [mode];
|
||||
let perm_fsNode = fsNode;
|
||||
while ( !await perm_fsNode.get('is-root') ) {
|
||||
const uid = await perm_fsNode.get('uid');
|
||||
const permissionsToCheck = modes.map(mode => PermissionUtil.join('fs', uid, mode));
|
||||
const permissionsToCheck = [mode === MANAGE_PERM_PREFIX ? PermissionUtil.join(MANAGE_PERM_PREFIX, 'fs', uid) : PermissionUtil.join('fs', uid, mode)];
|
||||
const reading = await svc_permission.scan(actor, permissionsToCheck);
|
||||
const options = PermissionUtil.reading_to_options(reading);
|
||||
if ( options.length > 0 ) {
|
||||
|
||||
@@ -25,8 +25,8 @@ const { reading_has_terminal } = require('../../unstructured/permission-scan-lib
|
||||
const BaseService = require('../BaseService');
|
||||
const { DB_WRITE } = require('../database/consts');
|
||||
const { UserActorType, Actor, AppUnderUserActorType } = require('./Actor');
|
||||
const { PermissionUtil, PermissionExploder, PermissionImplicator, PermissionRewriter } = require('./PermissionUtils.mjs');
|
||||
const PERM_KEY_PREFIX = 'perm';
|
||||
const { PERM_KEY_PREFIX, MANAGE_PERM_PREFIX } = require('./permissionConts.mjs');
|
||||
const { PermissionUtil, PermissionExploder, PermissionImplicator, PermissionRewriter } = require('./permissionUtils.mjs');
|
||||
|
||||
/**
|
||||
* @class PermissionService
|
||||
@@ -125,16 +125,11 @@ class PermissionService extends BaseService {
|
||||
* Checks if the actor has any of the specified permissions.
|
||||
*
|
||||
* @param {Actor} actor - The actor to check permissions for.
|
||||
* @param {Array|string} permission_options - The permissions to check against.
|
||||
* @param {string[]|string} permission_options - The permissions to check against.
|
||||
* Can be a single permission string or an array of permission strings.
|
||||
* @returns {Promise<boolean>} - True if the actor has at least one of the permissions, false otherwise.
|
||||
*
|
||||
* @note This method currently delegates to `scan()`, but a TODO suggests
|
||||
* an optimized implementation is planned.
|
||||
*/
|
||||
async check(actor, permission_options) {
|
||||
// TODO: optimized implementation for check instead of
|
||||
// delegating to the scan() method
|
||||
const svc_trace = this.services.get('traceService');
|
||||
return await svc_trace.spanify('permission:check', async () => {
|
||||
const reading = await this.scan(actor, permission_options);
|
||||
@@ -142,6 +137,22 @@ class PermissionService extends BaseService {
|
||||
return options.length > 0;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Checks if the actor has grant access to any of the specified permissions.
|
||||
*
|
||||
* @param {Actor} actor - The actor to check if they can manage a permission.
|
||||
* @param {string} permission - The permission to check against.
|
||||
* @returns {Promise<boolean>} - True if the actor has at least one of the permissions, false otherwise.
|
||||
*/
|
||||
async canManagePermission(actor, permission) {
|
||||
const svc_trace = this.services.get('traceService');
|
||||
return await svc_trace.spanify('permission:check', async () => {
|
||||
const managePermission = PermissionUtil.join(MANAGE_PERM_PREFIX, ...PermissionUtil.split(permission));
|
||||
const reading = await this.scan(actor, managePermission);
|
||||
const options = PermissionUtil.reading_to_options(reading);
|
||||
return options.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the permissions for an actor against specified permission options.
|
||||
@@ -366,11 +377,10 @@ class PermissionService extends BaseService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants a user permission to interact with another user.
|
||||
* Grants a user permission to an app the user is working with if the user has permission.
|
||||
*
|
||||
* @param {Actor} actor - The actor granting the permission (must be a user).
|
||||
* @param {string} app_uid - The unique identifier or name of the app.
|
||||
* @param {string} username - The username of the user receiving the permission.
|
||||
* @param {string} permission - The permission string to grant.
|
||||
* @param {Object} [extra={}] - Additional metadata or conditions for the permission.
|
||||
* @param {Object} [meta] - Metadata for logging or auditing purposes.
|
||||
@@ -420,18 +430,17 @@ class PermissionService extends BaseService {
|
||||
const sql_cols = Object.keys(audit_values).map((key) => `\`${key}\``).join(', ');
|
||||
const sql_vals = Object.keys(audit_values).map(() => '?').join(', ');
|
||||
|
||||
await this.db.write(`INSERT INTO \`audit_user_to_app_permissions\` (${sql_cols}) ` +
|
||||
this.db.write(`INSERT INTO \`audit_user_to_app_permissions\` (${sql_cols}) ` +
|
||||
`VALUES (${sql_vals})`,
|
||||
Object.values(audit_values));
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants an app a permission for any user, as long as the user granting the
|
||||
* permission also has the permission.
|
||||
* permission can manage permission.
|
||||
*
|
||||
* @param {Actor} actor - The actor granting the permission (must be a user).
|
||||
* @param {string} app_uid - The unique identifier or name of the app.
|
||||
* @param {string} username - The username of the user receiving the permission.
|
||||
* @param {string} permission - The permission string to grant.
|
||||
* @param {Object} [extra={}] - Additional metadata or conditions for the permission.
|
||||
* @param {Object} [meta] - Metadata for logging or auditing purposes.
|
||||
@@ -452,6 +461,13 @@ class PermissionService extends BaseService {
|
||||
|
||||
const app_id = app.id;
|
||||
|
||||
const canManagePerms = await this.canManagePermission(actor, permission);
|
||||
if ( !canManagePerms ){
|
||||
throw APIError.create('permission_denied', null, {
|
||||
permission,
|
||||
});
|
||||
}
|
||||
|
||||
// UPSERT permission
|
||||
await this.db.write('INSERT INTO `dev_to_app_permissions` (`user_id`, `app_id`, `permission`, `extra`) ' +
|
||||
`VALUES (?, ?, ?, ?) ${
|
||||
@@ -481,7 +497,7 @@ class PermissionService extends BaseService {
|
||||
const sql_cols = Object.keys(audit_values).map((key) => `\`${key}\``).join(', ');
|
||||
const sql_vals = Object.keys(audit_values).map(() => '?').join(', ');
|
||||
|
||||
await this.db.write(`INSERT INTO \`audit_dev_to_app_permissions\` (${sql_cols}) ` +
|
||||
this.db.write(`INSERT INTO \`audit_dev_to_app_permissions\` (${sql_cols}) ` +
|
||||
`VALUES (${sql_vals})`,
|
||||
Object.values(audit_values));
|
||||
}
|
||||
@@ -525,7 +541,7 @@ class PermissionService extends BaseService {
|
||||
const sql_cols = Object.keys(audit_values).map((key) => `\`${key}\``).join(', ');
|
||||
const sql_vals = Object.keys(audit_values).map(() => '?').join(', ');
|
||||
|
||||
await this.db.write(`INSERT INTO \`audit_dev_to_app_permissions\` (${sql_cols}) ` +
|
||||
this.db.write(`INSERT INTO \`audit_dev_to_app_permissions\` (${sql_cols}) ` +
|
||||
`VALUES (${sql_vals})`,
|
||||
Object.values(audit_values));
|
||||
}
|
||||
@@ -561,7 +577,7 @@ class PermissionService extends BaseService {
|
||||
const sql_cols = Object.keys(audit_values).map((key) => `\`${key}\``).join(', ');
|
||||
const sql_vals = Object.keys(audit_values).map(() => '?').join(', ');
|
||||
|
||||
await this.db.write(`INSERT INTO \`audit_dev_to_app_permissions\` (${sql_cols}) ` +
|
||||
this.db.write(`INSERT INTO \`audit_dev_to_app_permissions\` (${sql_cols}) ` +
|
||||
`VALUES (${sql_vals})`,
|
||||
Object.values(audit_values));
|
||||
}
|
||||
@@ -619,7 +635,7 @@ class PermissionService extends BaseService {
|
||||
const sql_cols = Object.keys(audit_values).map((key) => `\`${key}\``).join(', ');
|
||||
const sql_vals = Object.keys(audit_values).map(() => '?').join(', ');
|
||||
|
||||
await this.db.write(`INSERT INTO \`audit_user_to_app_permissions\` (${sql_cols}) ` +
|
||||
this.db.write(`INSERT INTO \`audit_user_to_app_permissions\` (${sql_cols}) ` +
|
||||
`VALUES (${sql_vals})`,
|
||||
Object.values(audit_values));
|
||||
}
|
||||
@@ -664,20 +680,11 @@ class PermissionService extends BaseService {
|
||||
const sql_cols = Object.keys(audit_values).map((key) => `\`${key}\``).join(', ');
|
||||
const sql_vals = Object.keys(audit_values).map(() => '?').join(', ');
|
||||
|
||||
await this.db.write(`INSERT INTO \`audit_user_to_app_permissions\` (${sql_cols}) ` +
|
||||
this.db.write(`INSERT INTO \`audit_user_to_app_permissions\` (${sql_cols}) ` +
|
||||
`VALUES (${sql_vals})`,
|
||||
Object.values(audit_values));
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} GrantUserUserPermissionParams
|
||||
* @property {Actor} actor - The actor granting the permission (must be a user).
|
||||
* @property {string} username - The username of the user receiving the permission.
|
||||
* @property {string} permission - The permission string to be granted.
|
||||
* @property {Object} [extra={}] - Additional metadata or conditions for the permission.
|
||||
* @property {Object} [meta] - Metadata for auditing purposes, including a reason for the action.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Grants a permission from one user to another.
|
||||
*
|
||||
@@ -685,75 +692,76 @@ class PermissionService extends BaseService {
|
||||
* ensuring that the permission is correctly formatted, the users exist,
|
||||
* and that self-granting is not allowed.
|
||||
*
|
||||
* @param {GrantUserUserPermissionParams} params - Parameters for granting permission.
|
||||
* @param {Actor} actor
|
||||
* @param {string} username
|
||||
* @param {string} permission
|
||||
* @param {object} extra
|
||||
* @param {object} meta
|
||||
* @throws {Error} Throws if the user is not found or if attempting to grant permissions to oneself.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async grant_user_user_permission(actor, username, permission, extra = {}, meta) {
|
||||
const flatRes = this.#flat_grant_user_user_permission(actor, username, permission, extra, meta);
|
||||
permission = await this._rewrite_permission(permission);
|
||||
const user = await get_user({ username });
|
||||
if ( ! user ) {
|
||||
throw APIError.create('user_does_not_exist', null, {
|
||||
username,
|
||||
});
|
||||
}
|
||||
|
||||
// Don't allow granting permissions to yourself
|
||||
if ( user.id === actor.type.user.id ) {
|
||||
throw new Error('cannot grant permissions to yourself');
|
||||
}
|
||||
|
||||
const canManagePerms = await this.canManagePermission(actor, permission);
|
||||
if ( !canManagePerms ){
|
||||
throw APIError.create('permission_denied', null, {
|
||||
permission,
|
||||
});
|
||||
}
|
||||
|
||||
const flatRes = this.#flat_grant_user_user_permission(actor, user, permission, extra);
|
||||
// shoot this async
|
||||
this.#linked_grant_user_user_permission(actor, username, permission, extra, meta);
|
||||
this.#linked_grant_user_user_permission(actor, user, permission, extra, meta);
|
||||
return flatRes;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {GrantUserUserPermissionParams} params - Parameters for granting permission.
|
||||
* @param {Actor} actor
|
||||
* @param {User} user
|
||||
* @param {string} permission
|
||||
* @param {object} extra
|
||||
* @throws {Error} Throws if the user is not found or if attempting to grant permissions to oneself.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async #flat_grant_user_user_permission(actor, username, permission, extra = {}, _meta) {
|
||||
permission = await this._rewrite_permission(permission);
|
||||
const user = await get_user({ username });
|
||||
if ( ! user ) {
|
||||
throw APIError.create('user_does_not_exist', null, {
|
||||
username,
|
||||
});
|
||||
}
|
||||
|
||||
// Don't allow granting permissions to yourself
|
||||
if ( user.id === actor.type.user.id ) {
|
||||
throw new Error('cannot grant permissions to yourself');
|
||||
}
|
||||
|
||||
// TODO DS: for now I'm just gonna check that the actor has the perm they wanna give
|
||||
const canManagePerms = await this.check(actor, permission);
|
||||
|
||||
async #flat_grant_user_user_permission(actor, user, permission, extra = {}) {
|
||||
// UPSERT permission
|
||||
if ( canManagePerms )
|
||||
{
|
||||
await this.services.get('su').sudo(() =>
|
||||
this.kvService.set({
|
||||
key: PermissionUtil.join(PERM_KEY_PREFIX, user.id, permission),
|
||||
value: {
|
||||
...extra,
|
||||
issuer_user_id: actor.type.user.id,
|
||||
permission,
|
||||
deleted: false,
|
||||
},
|
||||
}));
|
||||
}
|
||||
await this.services
|
||||
.get('su')
|
||||
.sudo(() => this.kvService.set({
|
||||
key: PermissionUtil.join(PERM_KEY_PREFIX, user.id, permission),
|
||||
value: {
|
||||
...extra,
|
||||
issuer_user_id: actor.type.user.id,
|
||||
permission,
|
||||
deleted: false,
|
||||
},
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {GrantUserUserPermissionParams} params - Parameters for granting permission.
|
||||
* @param {Actor} actor
|
||||
* @param {User} user
|
||||
* @param {string} permission
|
||||
* @param {object} extra
|
||||
* @param {object} meta
|
||||
* @throws {Error} Throws if the user is not found or if attempting to grant permissions to oneself.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async #linked_grant_user_user_permission(actor, username, permission, extra = {}, meta) {
|
||||
permission = await this._rewrite_permission(permission);
|
||||
const user = await get_user({ username });
|
||||
if ( ! user ) {
|
||||
throw APIError.create('user_does_not_exist', null, {
|
||||
username,
|
||||
});
|
||||
}
|
||||
|
||||
// Don't allow granting permissions to yourself
|
||||
if ( user.id === actor.type.user.id ) {
|
||||
throw new Error('cannot grant permissions to yourself');
|
||||
}
|
||||
|
||||
async #linked_grant_user_user_permission(actor, user, permission, extra = {}, meta) {
|
||||
// UPSERT permission
|
||||
await this.db.write('INSERT INTO `user_to_user_permissions` (`holder_user_id`, `issuer_user_id`, `permission`, `extra`) ' +
|
||||
`VALUES (?, ?, ?, ?) ${
|
||||
@@ -770,7 +778,7 @@ class PermissionService extends BaseService {
|
||||
]);
|
||||
|
||||
// INSERT audit table
|
||||
await this.db.write('INSERT INTO `audit_user_to_user_permissions` (' +
|
||||
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 (?, ?, ?, ?, ?, ?, ?)',
|
||||
@@ -809,6 +817,13 @@ class PermissionService extends BaseService {
|
||||
});
|
||||
}
|
||||
|
||||
const canManagePerms = await this.canManagePermission(actor, permission);
|
||||
if ( !canManagePerms ){
|
||||
throw APIError.create('permission_denied', null, {
|
||||
permission,
|
||||
});
|
||||
}
|
||||
|
||||
await this.db.write('INSERT INTO `user_to_group_permissions` (`user_id`, `group_id`, `permission`, `extra`) ' +
|
||||
`VALUES (?, ?, ?, ?) ${
|
||||
this.db.case({
|
||||
@@ -824,7 +839,7 @@ class PermissionService extends BaseService {
|
||||
]);
|
||||
|
||||
// INSERT audit table
|
||||
await this.db.write('INSERT INTO `audit_user_to_group_permissions` (' +
|
||||
this.db.write('INSERT INTO `audit_user_to_group_permissions` (' +
|
||||
'`user_id`, `user_id_keep`, `group_id`, `group_id_keep`, ' +
|
||||
'`permission`, `action`, `reason`) ' +
|
||||
'VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
@@ -870,24 +885,28 @@ class PermissionService extends BaseService {
|
||||
permission = await this._rewrite_permission(permission);
|
||||
|
||||
const user = await get_user({ username });
|
||||
if ( ! user ) {
|
||||
if ( ! user ) {
|
||||
if ( !user ) {
|
||||
if ( !user ) {
|
||||
throw APIError.create('user_does_not_exist', null, {
|
||||
username,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO DS: for now I'm just gonna check that the actor has the perm they wanna take away
|
||||
const canManagePerms = await this.check(actor, permission);
|
||||
const canManagePerms = await this.canManagePermission(actor, permission);
|
||||
|
||||
if ( canManagePerms ) {
|
||||
// DELETE permission
|
||||
await this.services.get('su').sudo(() =>
|
||||
this.kvService.set(PermissionUtil.join(PERM_KEY_PREFIX, user.id, permission), {
|
||||
deleted: true,
|
||||
}));
|
||||
if ( !canManagePerms ){
|
||||
throw APIError.create('permission_denied', null, {
|
||||
permission,
|
||||
});
|
||||
}
|
||||
|
||||
// DELETE permission
|
||||
await this.services.get('su').sudo(() =>
|
||||
this.kvService.set(PermissionUtil.join(PERM_KEY_PREFIX, user.id, permission), {
|
||||
deleted: true,
|
||||
}));
|
||||
|
||||
}
|
||||
/**
|
||||
* @param {RevokeUserUserPermissionParams} params - Parameters for revoking permission
|
||||
@@ -915,7 +934,7 @@ class PermissionService extends BaseService {
|
||||
]);
|
||||
|
||||
// INSERT audit table
|
||||
await this.db.write('INSERT INTO `audit_user_to_user_permissions` (' +
|
||||
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 (?, ?, ?, ?, ?, ?, ?)',
|
||||
@@ -962,7 +981,7 @@ class PermissionService extends BaseService {
|
||||
]);
|
||||
|
||||
// INSERT audit table
|
||||
await this.db.write('INSERT INTO `audit_user_to_group_permissions` (' +
|
||||
this.db.write('INSERT INTO `audit_user_to_group_permissions` (' +
|
||||
'`user_id`, `user_id_keep`, `group_id`, `group_id_keep`, ' +
|
||||
'`permission`, `action`, `reason`) ' +
|
||||
'VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const BaseService = require('../BaseService');
|
||||
const { PermissionImplicator } = require('./PermissionUtils.mjs');
|
||||
const { PermissionImplicator } = require('./permissionUtils.mjs');
|
||||
|
||||
class PermissionShortcutService extends BaseService {
|
||||
_init() {
|
||||
|
||||
2
src/backend/src/services/auth/permissionConts.mjs
Normal file
2
src/backend/src/services/auth/permissionConts.mjs
Normal file
@@ -0,0 +1,2 @@
|
||||
export const MANAGE_PERM_PREFIX = 'manage';
|
||||
export const PERM_KEY_PREFIX = 'perm';
|
||||
@@ -1,16 +1,18 @@
|
||||
import { MANAGE_PERM_PREFIX } from "./permissionConts.mjs";
|
||||
|
||||
/**
|
||||
* The PermissionUtil class provides utility methods for handling
|
||||
* permission strings and operations, including splitting, joining,
|
||||
* escaping, and unescaping permission components. It also includes
|
||||
* functionality to convert permission reading structures into options.
|
||||
*/
|
||||
export class PermissionUtil {
|
||||
export const PermissionUtil = {
|
||||
/**
|
||||
* Unescapes a permission component string, converting escape sequences to their literal characters.
|
||||
* @param {string} component - The escaped permission component string.
|
||||
* @returns {string} The unescaped permission component.
|
||||
*/
|
||||
static unescape_permission_component(component) {
|
||||
unescape_permission_component(component) {
|
||||
let unescaped_str = '';
|
||||
// Constant for unescaped permission component string
|
||||
const STATE_NORMAL = {};
|
||||
@@ -33,14 +35,14 @@ export class PermissionUtil {
|
||||
}
|
||||
}
|
||||
return unescaped_str;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Escapes special characters in a permission component string for safe joining.
|
||||
* @param {string} component - The permission component string to escape.
|
||||
* @returns {string} The escaped permission component.
|
||||
*/
|
||||
static escape_permission_component(component) {
|
||||
escape_permission_component(component) {
|
||||
let escaped_str = '';
|
||||
for ( let i = 0 ; i < component.length ; i++ ) {
|
||||
const c = component[i];
|
||||
@@ -51,31 +53,31 @@ export class PermissionUtil {
|
||||
escaped_str += c;
|
||||
}
|
||||
return escaped_str;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Splits a permission string into its component parts, unescaping each component.
|
||||
* @param {string} permission - The permission string to split.
|
||||
* @returns {string[]} Array of unescaped permission components.
|
||||
*/
|
||||
static split(permission) {
|
||||
split(permission) {
|
||||
return permission
|
||||
.split(':')
|
||||
.map(PermissionUtil.unescape_permission_component)
|
||||
;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Joins permission components into a single permission string, escaping as needed.
|
||||
* @param {...string} components - The permission components to join.
|
||||
* @returns {string} The escaped, joined permission string.
|
||||
*/
|
||||
static join(...components) {
|
||||
join(...components) {
|
||||
return components
|
||||
.map(PermissionUtil.escape_permission_component)
|
||||
.join(':')
|
||||
;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts a permission reading structure into an array of option objects.
|
||||
@@ -87,7 +89,7 @@ export class PermissionUtil {
|
||||
* @param {Array<Object>} [path=[]] - Current path in the reading tree (used internally for recursion).
|
||||
* @returns {Array<Object>} Array of option objects with path and data.
|
||||
*/
|
||||
static reading_to_options(
|
||||
reading_to_options(
|
||||
// actual arguments
|
||||
reading, parameters = {},
|
||||
// recursion state
|
||||
@@ -121,8 +123,12 @@ export class PermissionUtil {
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
}
|
||||
},
|
||||
/** @type {(permission:string)=>boolean} */
|
||||
isManage(permission ){
|
||||
return permission.startsWith(MANAGE_PERM_PREFIX + ':');
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Permission rewriters are used to map one set of permission strings to another.
|
||||
@@ -22,7 +22,7 @@ const APIError = require("../../api/APIError");
|
||||
const { DriverError } = require("./DriverError");
|
||||
const { TypedValue } = require("./meta/Runtime");
|
||||
const BaseService = require("../BaseService");
|
||||
const { PermissionUtil } = require("../auth/PermissionUtils.mjs");
|
||||
const { PermissionUtil } = require("../auth/permissionUtils.mjs");
|
||||
const { Invoker } = require("../../../../putility/src/libs/invoker");
|
||||
const { get_user } = require("../../helpers");
|
||||
const { whatis } = require('../../util/langutil');
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
|
||||
// METADATA // {"ai-commented":{"service":"openai-completion","model":"gpt-4o"}}
|
||||
const { PermissionUtil } = require("../auth/PermissionUtils.mjs");
|
||||
const { PermissionUtil } = require("../auth/permissionUtils.mjs");
|
||||
const BaseService = require("../BaseService");
|
||||
|
||||
// DO WE HAVE enough information to get the policy for the newer drivers?
|
||||
|
||||
@@ -28,7 +28,7 @@ const { quot } = require('@heyputer/putility').libs.string;
|
||||
This code is optimized for editors supporting folding.
|
||||
Fold at Level 2 to conveniently browse sequence steps.
|
||||
Fold at Level 3 after opening an inner-sequence.
|
||||
|
||||
|
||||
If you're using VSCode {
|
||||
typically "Ctrl+K, Ctrl+2" or "⌘K, ⌘2";
|
||||
to revert "Ctrl+K, Ctrl+J" or "⌘K, ⌘J";
|
||||
@@ -36,10 +36,9 @@ const { quot } = require('@heyputer/putility').libs.string;
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
module.exports = new Sequence([
|
||||
require('./share/validate.js'),
|
||||
function initialize_result_object (a) {
|
||||
function initialize_result_object(a) {
|
||||
a.set('result', {
|
||||
$: 'api:share',
|
||||
$version: 'v0.0.0',
|
||||
@@ -48,16 +47,16 @@ module.exports = new Sequence([
|
||||
Array(a.get('req_recipients').length).fill(null),
|
||||
shares:
|
||||
Array(a.get('req_shares').length).fill(null),
|
||||
serialize () {
|
||||
serialize() {
|
||||
const result = this;
|
||||
for ( let i=0 ; i < result.recipients.length ; i++ ) {
|
||||
for ( let i = 0 ; i < result.recipients.length ; i++ ) {
|
||||
if ( ! result.recipients[i] ) continue;
|
||||
if ( result.recipients[i] instanceof APIError ) {
|
||||
result.status = 'mixed';
|
||||
result.recipients[i] = result.recipients[i].serialize();
|
||||
}
|
||||
}
|
||||
for ( let i=0 ; i < result.shares.length ; i++ ) {
|
||||
for ( let i = 0 ; i < result.shares.length ; i++ ) {
|
||||
if ( ! result.shares[i] ) continue;
|
||||
if ( result.shares[i] instanceof APIError ) {
|
||||
result.status = 'mixed';
|
||||
@@ -66,38 +65,38 @@ module.exports = new Sequence([
|
||||
}
|
||||
delete result.serialize;
|
||||
return result;
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
function initialize_worklists (a) {
|
||||
function initialize_worklists(a) {
|
||||
const recipients_work = new WorkList();
|
||||
const shares_work = new WorkList();
|
||||
|
||||
|
||||
const { req_recipients, req_shares } = a.values();
|
||||
|
||||
|
||||
// track: common operations on multiple items
|
||||
|
||||
for ( let i=0 ; i < req_recipients.length ; i++ ) {
|
||||
|
||||
for ( let i = 0 ; i < req_recipients.length ; i++ ) {
|
||||
const value = req_recipients[i];
|
||||
recipients_work.push({ i, value });
|
||||
}
|
||||
|
||||
for ( let i=0 ; i < req_shares.length ; i++ ) {
|
||||
|
||||
for ( let i = 0 ; i < req_shares.length ; i++ ) {
|
||||
const value = req_shares[i];
|
||||
shares_work.push({ i, value });
|
||||
}
|
||||
|
||||
|
||||
recipients_work.lockin();
|
||||
shares_work.lockin();
|
||||
|
||||
|
||||
a.values({ recipients_work, shares_work });
|
||||
},
|
||||
require('./share/process_recipients.js'),
|
||||
require('./share/process_shares.js'),
|
||||
function abort_on_error_if_mode_is_strict (a) {
|
||||
function abort_on_error_if_mode_is_strict(a) {
|
||||
const strict_mode = a.get('strict_mode');
|
||||
if ( ! strict_mode ) return;
|
||||
|
||||
|
||||
const result = a.get('result');
|
||||
if (
|
||||
result.recipients.some(v => v !== null) ||
|
||||
@@ -110,70 +109,64 @@ module.exports = new Sequence([
|
||||
a.stop();
|
||||
}
|
||||
},
|
||||
function early_return_on_dry_run (a) {
|
||||
function early_return_on_dry_run(a) {
|
||||
if ( ! a.get('req').body.dry_run ) return;
|
||||
|
||||
|
||||
const { res, result, recipients_work } = a.values();
|
||||
for ( const item of recipients_work.list() ) {
|
||||
result.recipients[item.i] =
|
||||
{ $: 'api:status-report', status: 'success' };
|
||||
}
|
||||
|
||||
|
||||
result.serialize();
|
||||
result.status = 'success';
|
||||
result.dry_run = true;
|
||||
res.send(result);
|
||||
a.stop();
|
||||
},
|
||||
async function grant_permissions_to_existing_users (a) {
|
||||
async function grant_permissions_to_existing_users(a) {
|
||||
const {
|
||||
req, result, recipients_work, shares_work
|
||||
req, result, recipients_work, shares_work,
|
||||
} = a.values();
|
||||
|
||||
|
||||
const svc_permission = a.iget('services').get('permission');
|
||||
const svc_acl = a.iget('services').get('acl');
|
||||
const svc_notification = a.iget('services').get('notification');
|
||||
const svc_email = a.iget('services').get('email');
|
||||
|
||||
|
||||
const actor = a.get('actor');
|
||||
|
||||
for ( const recipient_item of recipients_work.list() ) {
|
||||
if ( recipient_item.type !== 'username' ) continue;
|
||||
|
||||
|
||||
const username = recipient_item.user.username;
|
||||
|
||||
for ( const share_item of shares_work.list() ) {
|
||||
const permissions = share_item.share_intent.permissions;
|
||||
for ( const perm of permissions ) {
|
||||
if ( perm.startsWith('fs:') ) {
|
||||
await svc_acl.set_user_user(
|
||||
actor,
|
||||
username,
|
||||
perm,
|
||||
undefined,
|
||||
{ only_if_higher: true },
|
||||
);
|
||||
if ( perm.startsWith('fs:') || perm.startsWith('manage:fs:') ) {
|
||||
await svc_acl.set_user_user(actor,
|
||||
username,
|
||||
perm,
|
||||
undefined,
|
||||
{ only_if_higher: true });
|
||||
} else {
|
||||
await svc_permission.grant_user_user_permission(
|
||||
actor,
|
||||
username,
|
||||
perm,
|
||||
);
|
||||
await svc_permission.grant_user_user_permission(actor,
|
||||
username,
|
||||
perm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const files = []; {
|
||||
for ( const item of shares_work.list() ) {
|
||||
if ( item.thing.$ !== 'fs-share' ) continue;
|
||||
files.push(
|
||||
await item.node.getSafeEntry(),
|
||||
);
|
||||
files.push(await item.node.getSafeEntry());
|
||||
}
|
||||
}
|
||||
|
||||
const metadata = a.get('req').body.metadata || {};
|
||||
|
||||
|
||||
svc_notification.notify(UsernameNotifSelector(username), {
|
||||
source: 'sharing',
|
||||
icon: 'shared.svg',
|
||||
@@ -190,7 +183,6 @@ module.exports = new Sequence([
|
||||
'with you.',
|
||||
});
|
||||
|
||||
|
||||
// Working on notifications
|
||||
// Email should have a link to a shared file, right?
|
||||
// .. how do I make those URLs? (gui feature)
|
||||
@@ -204,23 +196,23 @@ module.exports = new Sequence([
|
||||
message: metadata.message,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
result.recipients[recipient_item.i] =
|
||||
{ $: 'api:status-report', status: 'success' };
|
||||
}
|
||||
},
|
||||
async function email_the_email_recipients (a) {
|
||||
async function email_the_email_recipients(a) {
|
||||
const { actor, recipients_work, shares_work } = a.values();
|
||||
|
||||
|
||||
const svc_share = a.iget('services').get('share');
|
||||
const svc_token = a.iget('services').get('token');
|
||||
const svc_email = a.iget('services').get('email');
|
||||
|
||||
|
||||
for ( const recipient_item of recipients_work.list() ) {
|
||||
if ( recipient_item.type !== 'email' ) continue;
|
||||
|
||||
|
||||
const email = recipient_item.value;
|
||||
|
||||
|
||||
// data that gets stored in the `data` column of the share
|
||||
const metadata = a.get('req').body.metadata || {};
|
||||
const data = {
|
||||
@@ -229,12 +221,12 @@ module.exports = new Sequence([
|
||||
permissions: [],
|
||||
metadata,
|
||||
};
|
||||
|
||||
|
||||
for ( const share_item of shares_work.list() ) {
|
||||
const permissions = share_item.share_intent.permissions;
|
||||
data.permissions.push(...permissions);
|
||||
}
|
||||
|
||||
|
||||
// track: scoping iife
|
||||
const share_token = await (async () => {
|
||||
const share_uid = await svc_share.create_share({
|
||||
@@ -247,13 +239,13 @@ module.exports = new Sequence([
|
||||
$v: '0.0.0',
|
||||
uid: share_uid,
|
||||
}, {
|
||||
expiresIn: '14d'
|
||||
expiresIn: '14d',
|
||||
});
|
||||
})();
|
||||
|
||||
|
||||
const email_link =
|
||||
`${config.origin}?share_token=${share_token}`;
|
||||
|
||||
|
||||
await svc_email.send_email({ email }, 'share_by_email', {
|
||||
link: email_link,
|
||||
sender_name: actor.type.user.username,
|
||||
@@ -261,9 +253,9 @@ module.exports = new Sequence([
|
||||
});
|
||||
}
|
||||
},
|
||||
function send_result (a) {
|
||||
function send_result(a) {
|
||||
const { res, result } = a.values();
|
||||
result.serialize();
|
||||
res.send(result);
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -21,15 +21,16 @@ const APIError = require("../../../api/APIError");
|
||||
const { Sequence } = require("../../../codex/Sequence");
|
||||
const config = require("../../../config");
|
||||
const { get_user, get_app } = require("../../../helpers");
|
||||
const { PermissionUtil } = require("../../../services/auth/PermissionUtils.mjs");
|
||||
const { PermissionUtil } = require("../../../services/auth/permissionUtils.mjs");
|
||||
const FSNodeParam = require("../../../api/filesystem/FSNodeParam");
|
||||
const { TYPE_DIRECTORY } = require("../../../filesystem/FSNodeContext");
|
||||
const { MANAGE_PERM_PREFIX } = require("../../../services/auth/permissionConts.mjs");
|
||||
|
||||
/*
|
||||
This code is optimized for editors supporting folding.
|
||||
Fold at Level 2 to conveniently browse sequence steps.
|
||||
Fold at Level 3 after opening an inner-sequence.
|
||||
|
||||
|
||||
If you're using VSCode {
|
||||
typically "Ctrl+K, Ctrl+2" or "⌘K, ⌘2";
|
||||
to revert "Ctrl+K, Ctrl+J" or "⌘K, ⌘J";
|
||||
@@ -39,30 +40,30 @@ const { TYPE_DIRECTORY } = require("../../../filesystem/FSNodeContext");
|
||||
|
||||
module.exports = new Sequence({
|
||||
name: 'process shares',
|
||||
beforeEach (a) {
|
||||
beforeEach(a) {
|
||||
const { shares_work } = a.values();
|
||||
shares_work.clear_invalid();
|
||||
}
|
||||
},
|
||||
}, [
|
||||
function validate_share_types (a) {
|
||||
function validate_share_types(a) {
|
||||
const { result, shares_work } = a.values();
|
||||
|
||||
|
||||
const lib_typeTagged = a.iget('services').get('lib-type-tagged');
|
||||
|
||||
|
||||
for ( const item of shares_work.list() ) {
|
||||
const { i } = item;
|
||||
let { value } = item;
|
||||
|
||||
|
||||
const thing = lib_typeTagged.process(value);
|
||||
if ( thing.$ === 'error' ) {
|
||||
item.invalid = true;
|
||||
result.shares[i] =
|
||||
APIError.create('format_error', null, {
|
||||
message: thing.message
|
||||
message: thing.message,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
const allowed_things = ['fs-share', 'app-share'];
|
||||
if ( ! allowed_things.includes(thing.$) ) {
|
||||
item.invalid = true;
|
||||
@@ -73,11 +74,11 @@ module.exports = new Sequence({
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
item.thing = thing;
|
||||
}
|
||||
},
|
||||
function create_file_share_intents (a) {
|
||||
function create_file_share_intents(a) {
|
||||
const { result, shares_work } = a.values();
|
||||
for ( const item of shares_work.list() ) {
|
||||
const { thing } = item;
|
||||
@@ -90,7 +91,7 @@ module.exports = new Sequence({
|
||||
}
|
||||
let access = thing.access;
|
||||
if ( access ) {
|
||||
if ( ! ['read','write'].includes(access) ) {
|
||||
if ( ! ['read', 'write', MANAGE_PERM_PREFIX].includes(access) ) {
|
||||
errors.push('`access` should be `read` or `write`');
|
||||
}
|
||||
} else access = 'read';
|
||||
@@ -100,19 +101,19 @@ module.exports = new Sequence({
|
||||
result.shares[item.i] =
|
||||
APIError.create('field_errors', null, {
|
||||
key: `shares[${item.i}]`,
|
||||
errors
|
||||
errors,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
item.path = thing.path;
|
||||
item.share_intent = {
|
||||
$: 'share-intent:file',
|
||||
permissions: [PermissionUtil.join('fs', thing.path, access)],
|
||||
permissions: access === MANAGE_PERM_PREFIX ? [PermissionUtil.join(access, 'fs', thing.path)] : [PermissionUtil.join('fs', thing.path, access)],
|
||||
};
|
||||
}
|
||||
},
|
||||
function create_app_share_intents (a) {
|
||||
function create_app_share_intents(a) {
|
||||
const { result, shares_work } = a.values();
|
||||
for ( const item of shares_work.list() ) {
|
||||
const { thing } = item;
|
||||
@@ -129,46 +130,46 @@ module.exports = new Sequence({
|
||||
result.shares[item.i] =
|
||||
APIError.create('field_errors', null, {
|
||||
key: `shares[${item.i}]`,
|
||||
errors
|
||||
errors,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
const app_selector = thing.uid
|
||||
? `uid#${thing.uid}` : thing.name;
|
||||
|
||||
|
||||
item.share_intent = {
|
||||
$: 'share-intent:app',
|
||||
permissions: [
|
||||
PermissionUtil.join('app', app_selector, 'access')
|
||||
]
|
||||
}
|
||||
PermissionUtil.join('app', app_selector, 'access'),
|
||||
],
|
||||
};
|
||||
continue;
|
||||
}
|
||||
},
|
||||
async function fetch_nodes_for_file_shares (a) {
|
||||
async function fetch_nodes_for_file_shares(a) {
|
||||
const { req, result, shares_work } = a.values();
|
||||
for ( const item of shares_work.list() ) {
|
||||
if ( item.type !== 'fs' ) continue;
|
||||
const node = await (new FSNodeParam('path')).consolidate({
|
||||
req, getParam: () => item.path
|
||||
req, getParam: () => item.path,
|
||||
});
|
||||
|
||||
|
||||
if ( ! await node.exists() ) {
|
||||
item.invalid = true;
|
||||
result.shares[item.i] = APIError.create('subject_does_not_exist', {
|
||||
path: item.path,
|
||||
})
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
item.node = node;
|
||||
let email_path = item.path;
|
||||
let is_dir = true;
|
||||
if ( await node.get('type') !== TYPE_DIRECTORY ) {
|
||||
is_dir = false;
|
||||
// remove last component
|
||||
email_path = email_path.slice(0, item.path.lastIndexOf('/')+1);
|
||||
email_path = email_path.slice(0, item.path.lastIndexOf('/') + 1);
|
||||
}
|
||||
|
||||
if ( email_path.startsWith('/') ) email_path = email_path.slice(1);
|
||||
@@ -177,10 +178,10 @@ module.exports = new Sequence({
|
||||
item.email_link = email_link;
|
||||
}
|
||||
},
|
||||
async function fetch_apps_for_app_shares (a) {
|
||||
async function fetch_apps_for_app_shares(a) {
|
||||
const { result, shares_work } = a.values();
|
||||
const db = a.iget('db');
|
||||
|
||||
|
||||
for ( const item of shares_work.list() ) {
|
||||
if ( item.type !== 'app' ) continue;
|
||||
const { thing } = item;
|
||||
@@ -196,49 +197,45 @@ module.exports = new Sequence({
|
||||
APIError.create('entity_not_found', null, {
|
||||
identifier: thing.uid
|
||||
? { uid: thing.uid }
|
||||
: { id: { name: thing.name } }
|
||||
: { id: { name: thing.name } },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
app.metadata = db.case({
|
||||
mysql: () => app.metadata,
|
||||
otherwise: () => JSON.parse(app.metadata ?? '{}')
|
||||
otherwise: () => JSON.parse(app.metadata ?? '{}'),
|
||||
})();
|
||||
|
||||
|
||||
item.app = app;
|
||||
}
|
||||
},
|
||||
async function add_subdomain_permissions (a) {
|
||||
async function add_subdomain_permissions(a) {
|
||||
const { shares_work } = a.values();
|
||||
const actor = a.get('actor');
|
||||
const db = a.iget('db');
|
||||
|
||||
for ( const item of shares_work.list() ) {
|
||||
if ( item.type !== 'app' ) continue;
|
||||
const [subdomain] = await db.read(
|
||||
`SELECT * FROM subdomains WHERE associated_app_id = ? ` +
|
||||
const [subdomain] = await db.read(`SELECT * FROM subdomains WHERE associated_app_id = ? ` +
|
||||
`AND user_id = ? LIMIT 1`,
|
||||
[item.app.id, actor.type.user.id]
|
||||
);
|
||||
[item.app.id, actor.type.user.id]);
|
||||
if ( ! subdomain ) continue;
|
||||
|
||||
|
||||
// The subdomain is also owned by this user, so we'll
|
||||
// add a permission for that as well
|
||||
|
||||
|
||||
const site_selector = `uid#${subdomain.uuid}`;
|
||||
item.share_intent.permissions.push(
|
||||
PermissionUtil.join('site', site_selector, 'access')
|
||||
)
|
||||
item.share_intent.permissions.push(PermissionUtil.join('site', site_selector, 'access'));
|
||||
}
|
||||
},
|
||||
async function add_appdata_permissions (a) {
|
||||
const { result, shares_work } = a.values();
|
||||
async function add_appdata_permissions(a) {
|
||||
const { shares_work } = a.values();
|
||||
for ( const item of shares_work.list() ) {
|
||||
if ( item.type !== 'app' ) continue;
|
||||
if ( ! item.app.metadata?.shared_appdata ) continue;
|
||||
|
||||
|
||||
const app_owner = await get_user({ id: item.app.owner_user_id });
|
||||
|
||||
|
||||
const appdatadir =
|
||||
`/${app_owner.username}/AppData/${item.app.uid}`;
|
||||
const appdatadir_perm =
|
||||
@@ -247,7 +244,7 @@ module.exports = new Sequence({
|
||||
item.share_intent.permissions.push(appdatadir_perm);
|
||||
}
|
||||
},
|
||||
function apply_success_status_to_shares (a) {
|
||||
function apply_success_status_to_shares(a) {
|
||||
const { result, shares_work } = a.values();
|
||||
for ( const item of shares_work.list() ) {
|
||||
result.shares[item.i] =
|
||||
@@ -256,9 +253,11 @@ module.exports = new Sequence({
|
||||
status: 'success',
|
||||
fields: {
|
||||
permission: item.permission,
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
function return_state (a) { return a; }
|
||||
function return_state(a) {
|
||||
return a;
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -25,7 +25,7 @@ const { whatis } = require("../../../util/langutil");
|
||||
This code is optimized for editors supporting folding.
|
||||
Fold at Level 2 to conveniently browse sequence steps.
|
||||
Fold at Level 3 after opening an inner-sequence.
|
||||
|
||||
|
||||
If you're using VSCode {
|
||||
typically "Ctrl+K, Ctrl+2" or "⌘K, ⌘2";
|
||||
to revert "Ctrl+K, Ctrl+J" or "⌘K, ⌘J";
|
||||
@@ -36,14 +36,12 @@ const { whatis } = require("../../../util/langutil");
|
||||
module.exports = new Sequence({
|
||||
name: 'validate request',
|
||||
}, [
|
||||
function validate_metadata (a) {
|
||||
console.log('thinngggggg', a.get('thing'));
|
||||
a.set('asdf', 'zxcv');
|
||||
function validate_metadata(a) {
|
||||
const req = a.get('req');
|
||||
const metadata = req.body.metadata;
|
||||
|
||||
if ( ! metadata ) return;
|
||||
|
||||
|
||||
if ( typeof metadata !== 'object' ) {
|
||||
throw APIError.create('field_invalid', null, {
|
||||
key: 'metadata',
|
||||
@@ -54,7 +52,7 @@ module.exports = new Sequence({
|
||||
|
||||
const MAX_KEYS = 20;
|
||||
const MAX_STRING = 255;
|
||||
const MAX_MESSAGE_STRING = 10*1024;
|
||||
const MAX_MESSAGE_STRING = 10 * 1024;
|
||||
|
||||
if ( Object.keys(metadata).length > MAX_KEYS ) {
|
||||
throw APIError.create('field_invalid', null, {
|
||||
@@ -99,10 +97,10 @@ module.exports = new Sequence({
|
||||
}
|
||||
}
|
||||
},
|
||||
function validate_mode (a) {
|
||||
function validate_mode(a) {
|
||||
const req = a.get('req');
|
||||
const mode = req.body.mode;
|
||||
|
||||
|
||||
if ( mode === 'strict' ) {
|
||||
a.set('strict_mode', true);
|
||||
return;
|
||||
@@ -116,7 +114,7 @@ module.exports = new Sequence({
|
||||
expected: '`strict`, `best-effort`, or undefined',
|
||||
});
|
||||
},
|
||||
function validate_recipients (a) {
|
||||
function validate_recipients(a) {
|
||||
const req = a.get('req');
|
||||
let recipients = req.body.recipients;
|
||||
|
||||
@@ -130,7 +128,7 @@ module.exports = new Sequence({
|
||||
key: 'recipients',
|
||||
expected: 'array or string',
|
||||
got: typeof recipients,
|
||||
})
|
||||
});
|
||||
}
|
||||
// At least one recipient
|
||||
if ( recipients.length < 1 ) {
|
||||
@@ -142,14 +140,14 @@ module.exports = new Sequence({
|
||||
}
|
||||
a.set('req_recipients', recipients);
|
||||
},
|
||||
function validate_shares (a) {
|
||||
function validate_shares(a) {
|
||||
const req = a.get('req');
|
||||
let shares = req.body.shares;
|
||||
|
||||
if ( ! Array.isArray(shares) ) {
|
||||
shares = [shares];
|
||||
}
|
||||
|
||||
|
||||
// At least one share
|
||||
if ( shares.length < 1 ) {
|
||||
throw APIError.create('field_invalid', null, {
|
||||
@@ -158,8 +156,10 @@ module.exports = new Sequence({
|
||||
got: 'none',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
a.set('req_shares', shares);
|
||||
},
|
||||
function return_state (a) { return a; }
|
||||
function return_state(a) {
|
||||
return a;
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* 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/>.
|
||||
*/
|
||||
import UIWindow from './UIWindow.js'
|
||||
import UIWindow from './UIWindow.js';
|
||||
|
||||
async function UIWindowShare(items, recipient){
|
||||
return new Promise(async (resolve) => {
|
||||
@@ -84,6 +84,7 @@ async function UIWindowShare(items, recipient){
|
||||
h += `<select class="access-type" style="width: 170px; margin-bottom: 0; margin-right: 5px;">`;
|
||||
h += `<option value="Viewer">${i18n('Viewer')}</option>`;
|
||||
h += `<option value="Editor">${i18n('Editor')}</option>`;
|
||||
h += `<option value="Manager">${i18n('Manager')}</option>`;
|
||||
h += `</select>`;
|
||||
|
||||
// Share
|
||||
@@ -189,6 +190,8 @@ async function UIWindowShare(items, recipient){
|
||||
perm_list += `<span class="permission-viewer-badge">${i18n('Viewer')}</span>`;
|
||||
else if(perm.access === 'write')
|
||||
perm_list += `<span class="permission-editor-badge">${i18n('Editor')}</span>`;
|
||||
else if(perm.access === 'manager')
|
||||
perm_list += `<span class="permission-manager-badge">${i18n('Manager')}</span>`;
|
||||
perm_list += `</div>`;
|
||||
// username
|
||||
perm_list += `${perm.user.email ?? perm.user.username}`;
|
||||
@@ -264,6 +267,8 @@ async function UIWindowShare(items, recipient){
|
||||
|
||||
if($(el_window).find('.access-type').val() === 'Viewer')
|
||||
access_level = 'read';
|
||||
else if($(el_window).find('.access-type').val() === 'Manager')
|
||||
access_level = 'manage';
|
||||
|
||||
$.ajax({
|
||||
url: puter.APIOrigin + "/share",
|
||||
@@ -296,7 +301,14 @@ async function UIWindowShare(items, recipient){
|
||||
} else {
|
||||
// show success message
|
||||
$(el_window).find('.access-recipient-print').html(recipient_id);
|
||||
let perm_id = `fs:${items[0].uid}:${access_level}`;
|
||||
let perm_id;
|
||||
|
||||
if(access_level === 'manage'){
|
||||
perm_id = `manage:fs:${items[0].uid}`;
|
||||
}
|
||||
else{
|
||||
perm_id = `fs:${items[0].uid}:${access_level}`;
|
||||
}
|
||||
|
||||
// append recipient to list
|
||||
let perm_list = '';
|
||||
@@ -306,7 +318,9 @@ async function UIWindowShare(items, recipient){
|
||||
if(access_level === 'read')
|
||||
perm_list += `<span class="permission-viewer-badge">${i18n('Viewer')}</span>`;
|
||||
else if(access_level === 'write')
|
||||
perm_list += `<span class="permission-editor-badge">i18n('Viewer')</span>`;
|
||||
perm_list += `<span class="permission-editor-badge">${i18n('Editor')}</span>`;
|
||||
else if(access_level === 'manage')
|
||||
perm_list += `<span class="permission-manager-badge">${i18n('Manager')}</span>`;
|
||||
perm_list += `</div>`;
|
||||
// recipient username
|
||||
perm_list += `${recipient_username}`;
|
||||
|
||||
Reference in New Issue
Block a user