feat: add protected apps

This commit is contained in:
KernelDeimos 2024-06-19 23:35:41 -04:00 committed by Eric Dubé
parent 7006dcc31c
commit f2f3d6ff46
5 changed files with 221 additions and 0 deletions

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 }) => {

View File

@ -0,0 +1,7 @@
const BaseService = require("../services/BaseService");
class Library extends BaseService {
//
}
module.exports = Library;

View File

@ -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;

View File

@ -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,
};

View File

@ -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,
};