Skip to content

Commit

Permalink
feat: add protected apps
Browse files Browse the repository at this point in the history
  • Loading branch information
KernelDeimos committed Jun 20, 2024
1 parent 7006dcc commit f2f3d6f
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 0 deletions.
11 changes: 11 additions & 0 deletions packages/backend/src/CoreModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -153,6 +160,7 @@ const install = async ({ services, app, useapi }) => {
WriteByOwnerOnlyES,
ValidationES,
SetOwnerES,
ProtectedAppES,
MaxLimitES, { max: 5000 },
]),
});
Expand Down Expand Up @@ -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 }) => {
Expand Down
7 changes: 7 additions & 0 deletions packages/backend/src/definitions/Library.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const BaseService = require("../services/BaseService");

class Library extends BaseService {
//
}

module.exports = Library;
77 changes: 77 additions & 0 deletions packages/backend/src/libraries/ArrayUtil.js
Original file line number Diff line number Diff line change
@@ -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;
82 changes: 82 additions & 0 deletions packages/backend/src/om/entitystorage/ProtectedAppES.js
Original file line number Diff line number Diff line change
@@ -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,
};
44 changes: 44 additions & 0 deletions packages/backend/src/services/ProtectedAppService.js
Original file line number Diff line number Diff line change
@@ -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,
};

0 comments on commit f2f3d6f

Please sign in to comment.