Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/backend/src/filesystem/FSNodeContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,8 @@ module.exports = class FSNodeContext {
* @param {*} fsEntryFetcher fetches the filesystem entry
* @void
*/
async fetchEntry(fetch_entry_options = {}) {
async fetchEntry (fetch_entry_options = {}) {
const svc_event = this.services.get('event');
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...
Expand All @@ -308,6 +309,9 @@ module.exports = class FSNodeContext {
) {
const promise = this.fetching;
this.fetching = null;
if (this.entry.thumbnail) {
await svc_event.emit("thumbnail.read", this.entry);
}
promise.resolve();
return;
}
Expand Down Expand Up @@ -355,6 +359,8 @@ module.exports = class FSNodeContext {

const promise = this.fetching;
this.fetching = null;

await svc_event.emit("thumbnail.read", this.entry);
promise.resolve();
}

Expand Down
7 changes: 6 additions & 1 deletion src/backend/src/filesystem/hl_operations/hl_write.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ class HLWrite extends HLFilesystemOperation {
const { _path } = this.modules;

const fs = context.get('services').get('filesystem');
const svc_event = context.get('services').get('event');

let parent = values.destination_or_parent;
let destination = null;
Expand Down Expand Up @@ -336,7 +337,11 @@ class HLWrite extends HLFilesystemOperation {
stream_to_the_void(thumb_file.stream);
return 'thumbnail error: ' + e.message;
}
thumbnail_promise.resolve(thumbnail);

const thumbnailData = {url: thumbnail}
await svc_event.emit('thumbnail.created', thumbnailData); // An extension can modify where this thumbnail is stored

thumbnail_promise.resolve(thumbnailData.url);
})();
if ( reason ) {
console.log('REASON', reason);
Expand Down
4 changes: 2 additions & 2 deletions src/backend/src/filesystem/ll_operations/ll_rmnode.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class LLRmNode extends LLFilesystemOperation {

const { context } = this;

const svc = context.get('services');
const svc_event = context.get('services').get("event");

// Access Control
{
Expand All @@ -36,7 +36,7 @@ class LLRmNode extends LLFilesystemOperation {
throw await svc_acl.get_safe_acl_error(actor, target, 'write');
}
}

await svc_event.emit('fs.remove.node', this.values);
await target.provider.unlink({ context, node: target });
}
}
Expand Down
250 changes: 250 additions & 0 deletions src/backend/src/services/auth/PermissionUtils.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
/**
* 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 {
/**
* 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) {
let unescaped_str = '';
// Constant for unescaped permission component string
const STATE_NORMAL = {};
// Constant for escaping special characters in permission strings
const STATE_ESCAPE = {};
let state = STATE_NORMAL;
const const_escapes = { C: ':' };
for ( let i = 0 ; i < component.length ; i++ ) {
const c = component[i];
if ( state === STATE_NORMAL ) {
if ( c === '\\' ) {
state = STATE_ESCAPE;
} else {
unescaped_str += c;
}
} else if ( state === STATE_ESCAPE ) {
unescaped_str += Object.prototype.hasOwnProperty.call(const_escapes, c)
? const_escapes[c] : c;
state = STATE_NORMAL;
}
}
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) {
let escaped_str = '';
for ( let i = 0 ; i < component.length ; i++ ) {
const c = component[i];
if ( c === ':' ) {
escaped_str += '\\C';
continue;
}
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) {
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) {
return components
.map(PermissionUtil.escape_permission_component)
.join(':')
;
}

/**
* Converts a permission reading structure into an array of option objects.
* Recursively traverses the reading tree to collect all options with their associated path and data.
* @param {Array<Object>} reading - The permission reading structure to convert.
* @param {Object} [parameters={}] - Optional parameters for the conversion.
* @param {Array<Object>} [options=[]] - Accumulator for options (used internally for recursion).
* @param {Array<any>} [extras=[]] - Extra data to include (used internally for recursion).
* @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(
// actual arguments
reading, parameters = {},
// recursion state
options = [], extras = [], path = [],
) {
const to_path_item = finding => ({
key: finding.key,
holder: finding.holder_username,
data: finding.data,
});
for ( let finding of reading ) {
if ( finding.$ === 'option' ) {
path = [to_path_item(finding), ...path];
options.push({
...finding,
data: [
...(finding.data ? [finding.data] : []),
...extras,
],
path,
});
}
if ( finding.$ === 'path' ) {
if ( finding.has_terminal === false ) continue;
const new_extras = ( finding.data ) ? [
finding.data,
...extras,
] : [];
const new_path = [to_path_item(finding), ...path];
this.reading_to_options(finding.reading, parameters, options, new_extras, new_path);
}
}
return options;
}
}

/**
* Permission rewriters are used to map one set of permission strings to another.
* These are invoked during permission scanning and when permissions are granted or revoked.
*
* For example, Puter's filesystem uses this to map 'fs:/some/path:mode' to
* 'fs:SOME-UUID:mode'.
*
* A rewriter is constructed using the static method PermissionRewriter.create({ matcher, rewriter }).
* The matcher is a function that takes a permission string and returns true if the rewriter should be applied.
* The rewriter is a function that takes a permission string and returns the rewritten permission string.
*/
export class PermissionRewriter {
static create({ id, matcher, rewriter }) {
return new PermissionRewriter({ id, matcher, rewriter });
}

constructor({ id, matcher, rewriter }) {
this.id = id;
this.matcher = matcher;
this.rewriter = rewriter;
}

matches(permission) {
return this.matcher(permission);
}

/**
* Determines if the given permission matches the criteria set for this rewriter.
*
* @param {string} permission - The permission string to check.
* @returns {boolean} - True if the permission matches, false otherwise.
*/
async rewrite(permission) {
return await this.rewriter(permission);
}
}

/**
* Permission implicators are used to manage implicit permissions.
* It defines a method to check if a given permission is implicitly granted to an actor.
*
* For example, Puter's filesystem uses this to grant permission to a file if the specified
* 'actor' is the owner of the file.
*
* An implicator is constructed using the static method PermissionImplicator.create({ matcher, checker }).
* `matcher is a function that takes a permission string and returns true if the implicator should be applied.
* `checker` is a function that takes an actor and a permission string and returns true if the permission is implied.
* The actor and permission are passed to checker({ actor, permission }) as an object.
*/
export class PermissionImplicator {
static create({ id, matcher, checker, ...options }) {
return new PermissionImplicator({ id, matcher, checker, options });
}

constructor({ id, matcher, checker, options }) {
this.id = id;
this.matcher = matcher;
this.checker = checker;
this.options = options;
}

matches(permission) {
return this.matcher(permission);
}

/**
* Check if the permission is implied by this implicator
* @param {Actor} actor
* @param {string} permission
* @returns
*/
/**
* Rewrites a permission string if it matches any registered rewriter.
* @param {string} permission - The permission string to potentially rewrite.
* @returns {Promise<string>} The possibly rewritten permission string.
*/
async check({ actor, permission, recurse }) {
return await this.checker({ actor, permission, recurse });
}
}

/**
* Permission exploders are used to map any permission to a list of permissions
* which are considered to imply the specified permission.
*
* It uses a matcher function to determine if a permission should be exploded
* and an exploder function to perform the expansion.
*
* The exploder is constructed using the static method PermissionExploder.create({ matcher, explode }).
* The `matcher` is a function that takes a permission string and returns true if the exploder should be applied.
* The `explode` is a function that takes an actor and a permission string and returns a list of implied permissions.
* The actor and permission are passed to explode({ actor, permission }) as an object.
*/
export class PermissionExploder {
static create({ id, matcher, exploder }) {
return new PermissionExploder({ id, matcher, exploder });
}

constructor({ id, matcher, exploder }) {
this.id = id;
this.matcher = matcher;
this.exploder = exploder;
}

matches(permission) {
return this.matcher(permission);
}

/**
* Explodes a permission into a set of implied permissions.
*
* This method takes a permission string and an actor object,
* then uses the associated exploder function to derive additional
* permissions that are implied by the given permission.
*
* @param {Object} options - The options object containing:
* @param {Actor} options.actor - The actor requesting the permission explosion.
* @param {string} options.permission - The base permission to be exploded.
* @returns {Promise<Array<string>>} A promise resolving to an array of implied permissions.
*/
async explode({ actor, permission }) {
return await this.exploder({ actor, permission });
}
}
Loading