Răsfoiți Sursa

feat: add server command to scan permissions

KernelDeimos 10 luni în urmă
părinte
comite
54471fada9

+ 43 - 0
src/backend/src/services/auth/PermissionService.js

@@ -243,7 +243,33 @@ class PermissionService extends BaseService {
             return res;
         });
     }
+    
+    async scan (actor, permission) {
+        const reading = [];
 
+        {
+            const old_perm = permission;
+            permission = await this._rewrite_permission(permission);
+            if ( permission !== old_perm ) {
+                reading.push({
+                    $: 'rewrite',
+                    from: old_perm,
+                    to: permission,
+                });
+            }
+        }
+        
+        
+        await require('../../structured/sequence/scan-user-permission')
+            .call(this, {
+                actor,
+                permission,
+                reading,
+            });
+            
+        return reading;
+    }
+    
     async check__ (actor, permission) {
         permission = await this._rewrite_permission(permission);
 
@@ -856,6 +882,23 @@ class PermissionService extends BaseService {
 
                     await this.grant_user_app_permission(actor, app_uid, permission, extra);
                 }
+            },
+            {
+                id: 'scan',
+                handler: async (args, ctx) => {
+                    const [ username, permission ] = args;
+
+                    // actor from username
+                    const actor = new Actor({
+                        type: new UserActorType({
+                            user: await get_user({ username }),
+                        }),
+                    })
+
+                    const reading = await this.scan(actor, permission);
+                    const util = require('node:util');
+                    ctx.log(JSON.stringify(reading, undefined, '  '));
+                }
             }
         ]);
     }

+ 56 - 0
src/backend/src/structured/sequence/scan-user-permission.js

@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Puter.
+ *
+ * Puter is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * 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/>.
+ */
+const { Sequence } = require("../../codex/Sequence");
+const { get_user } = require("../../helpers");
+const { Actor, UserActorType } = require("../../services/auth/Actor");
+const { PERMISSION_SCANNERS } = require("../../unstructured/permission-scanners");
+
+module.exports = new Sequence([
+    async function grant_if_system (a) {
+        const reading = a.get('reading');
+        const { actor } = a.values();
+        if ( actor.type.user.username === 'system' ) {
+            reading.push({
+                $: 'option',
+                source: 'implied',
+                data: {}
+            })
+            return a.stop({});
+        }
+    },
+    async function rewrite_permission (a) {
+        let { permission } = a.values();
+        permission = await a.icall('_rewrite_permission', permission);
+        a.values({ permission });
+    },
+    async function explode_permission (a) {
+        const { permission } = a.values();
+        const permission_options =
+            await a.icall('get_higher_permissions', permission);
+        a.values({ permission_options });
+    },
+    async function run_scanners (a) {
+        const scanners = PERMISSION_SCANNERS;
+        const ps = [];
+        for ( const scanner of scanners ) {
+            ps.push(scanner.scan(a));
+        }
+        await Promise.all(ps);
+    },
+]);

+ 124 - 0
src/backend/src/unstructured/permission-scanners.js

@@ -0,0 +1,124 @@
+const { get_user } = require("../helpers");
+const { Actor, UserActorType } = require("../services/auth/Actor");
+
+const PERMISSION_SCANNERS = [
+    {
+        name: 'implied',
+        async scan (a) {
+            const reading = a.get('reading');
+            const { actor, permission } = a.values();
+            
+            const _permission_implicators = a.iget('_permission_implicators');
+            for ( const implicator of _permission_implicators ) {
+                if ( ! implicator.matches(permission) ) {
+                    continue;
+                }
+                const implied = await implicator.check({
+                    actor,
+                    permission,
+                });
+                if ( implied ) {
+                    reading.push({
+                        $: 'option',
+                        source: 'implied',
+                        data: implied,
+                    });
+                }
+            }
+        }
+    },
+    {
+        name: 'user-user',
+        async scan (a) {
+            const reading = a.get('reading');
+            const db = a.iget('db');
+            const { actor, permission_options } = a.values();
+            
+            let sql_perm = permission_options.map(perm => {
+                return `\`permission\` = ?`
+            }).join(' OR ');
+            
+            if ( permission_options.length > 1 ) {
+                sql_perm = '(' + sql_perm + ')';
+            }
+
+            // SELECT permission
+            const rows = await db.read(
+                'SELECT * FROM `user_to_user_permissions` ' +
+                'WHERE `holder_user_id` = ? AND ' +
+                sql_perm,
+                [
+                    actor.type.user.id,
+                    ...permission_options,
+                ]
+            );
+
+            // Return the first matching permission where the
+            // issuer also has the permission granted
+            for ( const row of rows ) {
+                const issuer_actor = new Actor({
+                    type: new UserActorType({
+                        user: await get_user({ id: row.issuer_user_id }),
+                    }),
+                });
+
+                // const issuer_perm = await this.check(issuer_actor, row.permission);
+                const issuer_reading = await a.icall('scan', issuer_actor, row.permission);
+                reading.push({
+                    $: 'path',
+                    via: 'user',
+                    // issuer: issuer_actor,
+                    issuer_username: issuer_actor.type.user.username,
+                    reading: issuer_reading,
+                });
+            }
+        }
+    },
+    {
+        name: 'user-group-user',
+        async scan (a) {
+            const reading = a.get('reading');
+            const { actor, permission_options } = a.values();
+            const db = a.iget('db');
+
+            let sql_perm = permission_options.map((perm) =>
+                `p.permission = ?`).join(' OR ');
+
+            if ( permission_options.length > 1 ) {
+                sql_perm = '(' + sql_perm + ')';
+            }
+            const rows = await db.read(
+                'SELECT p.permission, p.user_id, p.group_id, p.extra FROM `user_to_group_permissions` p ' +
+                'JOIN `jct_user_group` ug ON p.group_id = ug.group_id ' +
+                'WHERE ug.user_id = ? AND ' + sql_perm,
+                [
+                    actor.type.user.id,
+                    ...permission_options,
+                ]
+            );
+
+            for ( const row of rows ) {
+                const issuer_actor = new Actor({
+                    type: new UserActorType({
+                        user: await get_user({ id: row.user_id }),
+                    }),
+                });
+
+                const issuer_reading = await a.icall('scan', issuer_actor, row.permission);
+
+                reading.push({
+                    $: 'path',
+                    via: 'user-group',
+                    // issuer: issuer_actor,
+                    issuer_username: issuer_actor.type.user.username,
+                    reading: issuer_reading,
+                    group_id: row.group_id,
+                });
+            }
+        }
+    }
+];
+
+module.exports = {
+    PERMISSION_SCANNERS,
+};