/* * Copyright (C) 2024-present 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 . */ const { APIError } = require('openai'); const configurable_auth = require('../middleware/configurable_auth'); const { Endpoint } = require('../util/expressutil'); const BaseService = require('./BaseService'); /** * @class PermissionAPIService * @extends BaseService * @description Service class that handles API endpoints for permission management, including user-app permissions, * user-user permissions, and group management. Provides functionality for creating groups, managing group memberships, * granting/revoking various types of permissions, and checking access control lists (ACLs). Implements RESTful * endpoints for group operations like creation, adding/removing users, and listing groups. */ class PermissionAPIService extends BaseService { static MODULES = { express: require('express'), }; /** * Installs routes for authentication and permission management into the Express app * @param {Object} _ Unused parameter * @param {Object} options Installation options * @param {Express} options.app Express application instance to install routes on * @returns {Promise} */ async ['__on_install.routes'] (_, { app }) { app.use(require('../routers/auth/get-user-app-token')); app.use(require('../routers/auth/grant-user-app')); app.use(require('../routers/auth/revoke-user-app')); app.use(require('../routers/auth/grant-dev-app')); app.use(require('../routers/auth/revoke-dev-app')); app.use(require('../routers/auth/grant-user-user')); app.use(require('../routers/auth/revoke-user-user')); app.use(require('../routers/auth/grant-user-group')); app.use(require('../routers/auth/revoke-user-group')); app.use(require('../routers/auth/list-permissions')); app.use(require('../routers/auth/check-permissions.js')); Endpoint(require('../routers/auth/check-app-acl.endpoint.js')).but({ route: '/auth/check-app-acl', }).attach(app); // track: scoping iife /** * Creates a scoped router for group-related endpoints using an IIFE pattern * @private * @returns {express.Router} Express router instance with isolated require scope */ const r_group = (() => { const require = this.require; const express = require('express'); return express.Router(); })(); this.install_group_endpoints_({ router: r_group }); app.use('/group', r_group); } install_group_endpoints_ ({ router }) { Endpoint({ route: '/create', methods: ['POST'], mw: [configurable_auth()], handler: async (req, res) => { const owner_user_id = req.user.id; const extra = req.body.extra ?? {}; const metadata = req.body.metadata ?? {}; if ( !extra || typeof extra !== 'object' || Array.isArray(extra) ) { throw APIError.create('field_invalid', null, { key: 'extra', expected: 'object', got: extra, }); } if ( !metadata || typeof metadata !== 'object' || Array.isArray(metadata) ) { throw APIError.create('field_invalid', null, { key: 'metadata', expected: 'object', got: metadata, }); } const svc_group = this.services.get('group'); const uid = await svc_group.create({ owner_user_id, // TODO: includeslist for allowed 'extra' fields extra: {}, // Metadata can be specified in request metadata: metadata ?? {}, }); res.json({ uid }); }, }).attach(router); Endpoint({ route: '/add-users', methods: ['POST'], mw: [configurable_auth()], handler: async (req, res) => { const svc_group = this.services.get('group'); // TODO: validate string and uuid for request const group = await svc_group.get({ uid: req.body.uid }); if ( ! group ) { throw APIError.create('entity_not_found', null, { identifier: req.body.uid, }); } if ( group.owner_user_id !== req.user.id ) { throw APIError.create('forbidden'); } if ( ! Array.isArray(req.body.users) ) { throw APIError.create('field_invalid', null, { key: 'users', expected: 'array', got: req.body.users, }); } for ( let i = 0 ; i < req.body.users.length ; i++ ) { const value = req.body.users[i]; if ( typeof value === 'string' ) continue; throw APIError.create('field_invalid', null, { key: `users[${i}]`, expected: 'string', got: value, }); } await svc_group.add_users({ uid: req.body.uid, users: req.body.users, }); res.json({}); }, }).attach(router); // TODO: DRY: add-users is very similar Endpoint({ route: '/remove-users', methods: ['POST'], mw: [configurable_auth()], handler: async (req, res) => { const svc_group = this.services.get('group'); // TODO: validate string and uuid for request const group = await svc_group.get({ uid: req.body.uid }); if ( ! group ) { throw APIError.create('entity_not_found', null, { identifier: req.body.uid, }); } if ( group.owner_user_id !== req.user.id ) { throw APIError.create('forbidden'); } if ( Array.isArray(req.body.users) ) { throw APIError.create('field_invalid', null, { key: 'users', expected: 'array', got: req.body.users, }); } for ( let i = 0 ; i < req.body.users.length ; i++ ) { const value = req.body.users[i]; if ( typeof value === 'string' ) continue; throw APIError.create('field_invalid', null, { key: `users[${i}]`, expected: 'string', got: value, }); } await svc_group.remove_users({ uid: req.body.uid, users: req.body.users, }); res.json({}); }, }).attach(router); Endpoint({ route: '/list', methods: ['GET'], mw: [configurable_auth()], handler: async (req, res) => { const svc_group = this.services.get('group'); // TODO: validate string and uuid for request const owned_groups = await svc_group.list_groups_with_owner({ owner_user_id: req.user.id }); const in_groups = await svc_group.list_groups_with_member({ user_id: req.user.id }); const public_groups = await svc_group.list_public_groups(); res.json({ owned_groups: await Promise.all(owned_groups.map(g => g.get_client_value({ members: true }))), in_groups: await Promise.all(in_groups.map(g => g.get_client_value({ members: true }))), public_groups: await Promise.all(public_groups.map(g => g.get_client_value())), }); }, }).attach(router); Endpoint({ route: '/public-groups', methods: ['GET'], mw: [configurable_auth()], handler: async (req, res) => { res.json({ user: this.global_config.default_user_group, temp: this.global_config.default_temp_group, }); }, }).attach(router); } } module.exports = { PermissionAPIService, };