Merge branch 'main' of https://github.com/HeyPuter/puter into main

This commit is contained in:
Nariman Jelveh
2024-06-19 21:07:48 -07:00
8 changed files with 275 additions and 2 deletions
+28
View File
@@ -0,0 +1,28 @@
# Comment Prefixes
Comments have prefixes using
[Conventional: Comments](https://conventionalcomments.org/)
as a **loose** guideline, and using this markdown file as a
the actual guideline.
This document will be updated on an _as-needed_ basis.
## The rules
- A comment line always looks like this:
- A whitespace character
- Optional prefix matching `/[a-z-]+\([a-z-]a+\):/`
- A whitespace character
- The comment
- Formalized prefixes must follow the rules below
- Any other prefix can be used. After some uses it
might be good to formalize it, but that's not a hard rule.
## Formalized prefixes
- `todo:` is interchangable with the famous `TODO:`, **except:**
when lowercase (`todo:`) it can include a scope: `todo(security):`.
- `track:` is used to track common patterns.
- Anything written after `track:` must be registered in
[track-comments.md](../devmeta/track-comments.md)
+24
View File
@@ -0,0 +1,24 @@
# Track Comments
Comments beginning with `// track:`. See
[comment_prefixes.md](../contributors/comment_prefixes.md)
## Track Comment Registry
- `track: type check`:
A condition that's used to check the type of an imput.
- `track: bounds check`:
A condition that's used to check the bounds of an array
or other list-like entity.
- `track: ruleset`
A series of conditions that early-return or `continue`
- `track: object description in comment`
A comment above the creation of some object which
could potentially have a `description` property.
This is especially relevant if the object is stored
in some kind of registry where multiple objects
could be listed in the console.
- `track: slice a prefix`
A common pattern where a prefix string is "sliced off"
of another string to obtain a significant value, such
as an indentifier.
+11
View File
@@ -18,6 +18,7 @@
*/
const { AdvancedBase } = require("@heyputer/puter-js-common");
const { NotificationES } = require("./om/entitystorage/NotificationES");
const { ProtectedAppES } = require("./om/entitystorage/ProtectedAppES");
const { Context } = require('./util/context');
@@ -51,6 +52,12 @@ const install = async ({ services, app, useapi }) => {
def('puter.middlewares.auth', require('./middleware/auth2'));
});
// === LIBRARIES ===
const ArrayUtil = require('./libraries/ArrayUtil');
services.registerService('util-array', ArrayUtil);
// === SERVICES ===
// /!\ IMPORTANT /!\
// For new services, put the import immediate above the
@@ -153,6 +160,7 @@ const install = async ({ services, app, useapi }) => {
WriteByOwnerOnlyES,
ValidationES,
SetOwnerES,
ProtectedAppES,
MaxLimitES, { max: 5000 },
]),
});
@@ -269,6 +277,9 @@ const install = async ({ services, app, useapi }) => {
const { NotificationService } = require('./services/NotificationService');
services.registerService('notification', NotificationService);
const { ProtectedAppService } = require('./services/ProtectedAppService');
services.registerService('__protected-app', ProtectedAppService);
}
const install_legacy = async ({ services }) => {
@@ -0,0 +1,7 @@
const BaseService = require("../services/BaseService");
class Library extends BaseService {
//
}
module.exports = Library;
@@ -33,7 +33,7 @@ const { Context } = require('../util/context');
module.exports = class FSOperationContext {
// TODO: rename this.fs to this.access
constructor (op_name, context, options) {
// TRACK: fs:create-service
// migration: fs:create-service
// TODO: rename this.fs to this.access
// NOTE: the 2nd parameter of this constructor
// was called `fs` and was expected to be FSAccessContext.
@@ -75,7 +75,7 @@ module.exports = class FSOperationContext {
this.doneReject = reject;
});
// TRACK: arch:trace-service:move-outta-fs
// migration: arch:trace-service:move-outta-fs
if ( this.fs.traceService ) {
// Set 'span_' to current active span
const { context, trace } = require('@opentelemetry/api');
@@ -0,0 +1,77 @@
const Library = require("../definitions/Library");
class ArrayUtil extends Library {
/**
*
* @param {*} marked_map
* @param {*} subject
*/
remove_marked_items (marked_map, subject) {
for ( let i=0 ; i < marked_map.length ; i++ ) {
let ii = marked_map[i];
// track: type check
if ( ! Number.isInteger(ii) ) {
throw new Error(
'marked_map can only contain integers'
);
}
// track: bounds check
if ( ii < 0 && ii >= subject.length ) {
throw new Error(
'each item in `marked_map` must be within that bounds ' +
'of `subject`'
);
}
}
marked_map.sort((a, b) => b - a);
for ( let i=0 ; i < marked_map.length ; i++ ) {
let ii = marked_map[i];
subject.splice(ii, 1);
}
return subject;
}
_test ({ assert }) {
// inner indices
{
const subject = [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
// 0 1 2 3 4 5 6 7
const marked_map = [2, 5];
this.remove_marked_items(marked_map, subject);
assert(() => subject.join('') === 'abdegh');
}
// left edge
{
const subject = [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
// 0 1 2 3 4 5 6 7
const marked_map = [0]
this.remove_marked_items(marked_map, subject);
assert(() => subject.join('') === 'bcdefgh');
}
// right edge
{
const subject = [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
// 0 1 2 3 4 5 6 7
const marked_map = [7]
this.remove_marked_items(marked_map, subject);
assert(() => subject.join('') === 'abcdefg');
}
// both edges
{
const subject = [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
// 0 1 2 3 4 5 6 7
const marked_map = [0, 7]
this.remove_marked_items(marked_map, subject);
assert(() => subject.join('') === 'bcdefg');
}
}
}
module.exports = ArrayUtil;
@@ -0,0 +1,82 @@
const { AppUnderUserActorType, UserActorType } = require("../../services/auth/Actor");
const { Context } = require("../../util/context");
const { BaseES } = require("./BaseES");
class ProtectedAppES extends BaseES {
async select (options){
const results = await this.upstream.select(options);
const actor = Context.get('actor');
const services = Context.get('services');
const to_delete = [];
for ( let i=0 ; i < results.length ; i++ ) {
const entity = results[i];
if ( ! await this.check_({ actor, services }, entity) ) {
continue;
}
to_delete.push(i);
}
const svc_utilArray = services.get('util-array');
svc_utilArray.remove_marked_items(to_delete, results);
return results;
}
async read (uid){
const entity = await this.upstream.read(uid);
if ( ! entity ) return null;
const actor = Context.get('actor');
const services = Context.get('services');
if ( await this.check_({ actor, services }, entity) ) {
return null;
}
return entity;
}
/**
* returns true if the entity should not be sent downstream
*/
async check_ ({ actor, services }, entity) {
// track: ruleset
{
// if it's not a protected app, no worries
if ( ! await entity.get('protected') ) return;
// if actor is this app, no worries
if (
actor.type instanceof AppUnderUserActorType &&
await entity.get('uid') === actor.type.app.uid
) return;
// if actor is owner of this app, no worries
if (
actor.type instanceof UserActorType &&
(await entity.get('owner')).id === actor.type.user.id
) return;
}
// now we need to check for permission
const app_uid = await entity.get('uid');
const svc_permission = services.get('permission');
const permission_to_check = `app:uid#${app_uid}:access`;
const perm = await svc_permission.check(
actor, permission_to_check,
);
if ( perm ) return;
// `true` here means "do not send downstream"
return true;
}
};
module.exports = {
ProtectedAppES,
};
@@ -0,0 +1,44 @@
const { get_app } = require("../helpers");
const { UserActorType } = require("./auth/Actor");
const { PermissionImplicator, PermissionUtil } = require("./auth/PermissionService");
const BaseService = require("./BaseService");
class ProtectedAppService extends BaseService {
async _init () {
const svc_permission = this.services.get('permission');
// track: object description in comment
// Owner of procted app has implicit permission to access it
svc_permission.register_implicator(PermissionImplicator.create({
matcher: permission => {
return permission.startsWith('app:');
},
checker: async ({ actor, permission }) => {
if ( !(actor.type instanceof UserActorType) ) {
return undefined;
}
const parts = PermissionUtil.split(permission);
if ( parts.length !== 3 ) return undefined;
const [_, uid_part, lvl] = parts;
if ( lvl !== 'access' ) return undefined;
// track: slice a prefix
const uid = uid_part.slice('uid#'.length);
const app = await get_app({ uid });
if ( app.owner_user_id !== actor.type.user.id ) {
return undefined;
}
return {};
},
}));
}
}
module.exports = {
ProtectedAppService,
};