소스 검색

dev: add endpoint for GUI to query app file access

KernelDeimos 7 달 전
부모
커밋
1ea6d270f8

+ 78 - 0
src/backend/src/routers/auth/check-app-acl.endpoint.js

@@ -0,0 +1,78 @@
+const APIError = require("../../api/APIError");
+const FSNodeParam = require("../../api/filesystem/FSNodeParam");
+const StringParam = require("../../api/filesystem/StringParam");
+const { get_app } = require("../../helpers");
+const configurable_auth = require("../../middleware/configurable_auth");
+const { Eq, Or } = require("../../om/query/query");
+const { UserActorType, Actor, AppUnderUserActorType } = require("../../services/auth/Actor");
+const { Context } = require("../../util/context");
+
+module.exports = {
+    route: '/check-app-acl',
+    methods: ['POST'],
+
+    // TODO: "alias" should be part of parameters somehow
+    alias: {
+        uid: 'subject',
+        path: 'subject',
+    },
+    parameters: {
+        subject: new FSNodeParam('subject'),
+        mode: new StringParam('mode', { optional: true }),
+
+        // TODO: There should be an "AppParam", but it feels wrong to include
+        // so many concerns into `src/api/filesystem` like that. This needs to
+        // be de-coupled somehow first.
+        app: new StringParam('app'),
+    },
+    mw: [configurable_auth()],
+    handler: async (req, res) => {
+        const context = Context.get();
+        const actor = req.actor;
+
+        if ( ! (actor.type instanceof UserActorType) ) {
+            throw APIError.create('forbidden');
+        }
+
+        const subject = req.values.subject;
+
+        const svc_acl = context.get('services').get('acl');
+        if ( ! await svc_acl.check(actor, subject, 'see') ) {
+            throw APIError.create('subject_does_not_exist');
+        }
+
+        const es_app = context.get('services').get('es:app');
+        const app = await es_app.read({
+            predicate:  new Or({
+                children: [
+                    new Eq({ key: 'uid', value: req.values.app }),
+                    new Eq({ key: 'name', value: req.values.app }),
+                ]
+            }),
+        });
+        if ( ! app ) {
+            throw APIError.create('app_does_not_exist', null, {
+                identifier: req.values.app,
+            });
+        }
+
+        const app_actor = new Actor({
+            type: new AppUnderUserActorType({
+                user: actor.type.user,
+                // TODO: get legacy app object from entity instead of fetching again
+                app: await get_app({ uid: await app.get('uid') }),
+            })
+        });
+
+        console.log('app?', app);
+
+        res.json({
+            allowed: await svc_acl.check(
+                app_actor, subject,
+                // If mode is not specified, check the HIGHEST mode, because this
+                // will grant the LEAST cases
+                req.values.mode ?? svc_acl.get_highest_mode()
+            )
+        });
+    }
+};

+ 6 - 0
src/backend/src/services/PermissionAPIService.js

@@ -36,6 +36,12 @@ class PermissionAPIService extends BaseService {
         app.use(require('../routers/auth/grant-user-group'));
         app.use(require('../routers/auth/revoke-user-group'));
         app.use(require('../routers/auth/list-permissions'))
+
+        Endpoint(
+            require('../routers/auth/check-app-acl.endpoint.js'),
+        ).but({
+            route: '/auth/check-app-acl',
+        }).attach(app);
         
         // track: scoping iife
         const r_group = (() => {

+ 6 - 0
src/backend/src/services/auth/ACLService.js

@@ -167,6 +167,12 @@ class ACLService extends BaseService {
         return APIError.create('forbidden');
     }
 
+    // If any logic depends on knowledge of the highest ACL mode, it should use
+    // this method in case a higher mode is added (ex: might add 'config' mode)
+    get_highest_mode () {
+        return 'write';
+    }
+
     // TODO: DRY: Also in FilesystemService
     _higher_modes (mode) {
         // If you want to X, you can do so with any of [...Y]

+ 10 - 0
src/backend/src/util/expressutil.js

@@ -23,6 +23,9 @@ const Endpoint = function Endpoint (spec) {
         attach (route) {
             const eggspress_options = {
                 allowedMethods: spec.methods ?? ['GET'],
+                ...(spec.subdomain ? { subdomain: spec.subdomain } : {}),
+                ...(spec.parameters ? { parameters: spec.parameters } : {}),
+                ...(spec.alias ? { alias: spec.alias } : {}),
                 ...(spec.mw ? { mw: spec.mw } : {}),
             };
             const eggspress_router = eggspress(
@@ -31,6 +34,13 @@ const Endpoint = function Endpoint (spec) {
                 spec.handler,
             );
             route.use(eggspress_router);
+        },
+        but (newSpec) {
+            // TODO: add merge with '$' behaviors (like config has)
+            return Endpoint({
+                ...spec,
+                ...newSpec,
+            });
         }
     };
 }

+ 5 - 0
src/contextlink/context.js

@@ -16,6 +16,11 @@
  * You should have received a copy of the GNU Affero General Public License
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
+
+// TODO: move dependants of this Context to use the one in putility instead
+
+// DO NOT EDIT THIS FILE: update the one in putility instead and migrate.
+
 export class Context {
     constructor (values) {
         for ( const k in values ) this[k] = values[k];