Bladeren bron

feat: add protected apps

KernelDeimos 11 maanden geleden
bovenliggende
commit
f2f3d6ff46

+ 11 - 0
packages/backend/src/CoreModule.js

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

+ 7 - 0
packages/backend/src/definitions/Library.js

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

+ 77 - 0
packages/backend/src/libraries/ArrayUtil.js

@@ -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 - 0
packages/backend/src/om/entitystorage/ProtectedAppES.js

@@ -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 - 0
packages/backend/src/services/ProtectedAppService.js

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