From 4216346384d90dcba429dbcb175e6f86482d19f4 Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Wed, 26 Jun 2024 16:21:32 -0400 Subject: [PATCH] feat: add group management endpoints --- packages/backend/src/CoreModule.js | 6 + .../src/services/PermissionAPIService.js | 173 ++++++++++++++++++ .../backend/src/services/PuterAPIService.js | 6 - .../backend/src/services/auth/GroupService.js | 89 +++++++++ 4 files changed, 268 insertions(+), 6 deletions(-) create mode 100644 packages/backend/src/services/PermissionAPIService.js create mode 100644 packages/backend/src/services/auth/GroupService.js diff --git a/packages/backend/src/CoreModule.js b/packages/backend/src/CoreModule.js index 2594463ccd..dc8fbb9ea4 100644 --- a/packages/backend/src/CoreModule.js +++ b/packages/backend/src/CoreModule.js @@ -286,6 +286,12 @@ const install = async ({ services, app, useapi }) => { const { ShareService } = require('./services/ShareService'); services.registerService('share', ShareService); + + const { GroupService } = require('./services/auth/GroupService'); + services.registerService('group', GroupService); + + const { PermissionAPIService } = require('./services/PermissionAPIService'); + services.registerService('__permission-api', PermissionAPIService); } const install_legacy = async ({ services }) => { diff --git a/packages/backend/src/services/PermissionAPIService.js b/packages/backend/src/services/PermissionAPIService.js new file mode 100644 index 0000000000..1aa04d11f7 --- /dev/null +++ b/packages/backend/src/services/PermissionAPIService.js @@ -0,0 +1,173 @@ +const { APIError } = require("openai"); +const configurable_auth = require("../middleware/configurable_auth"); +const { Endpoint } = require("../util/expressutil"); +const { whatis } = require("../util/langutil"); +const BaseService = require("./BaseService"); + +class PermissionAPIService extends BaseService { + static MODULES = { + express: require('express'), + }; + + async ['__on_install.routes'] () { + const { app } = this.services.get('web-server'); + + 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-user-user')); + app.use(require('../routers/auth/revoke-user-user')); + app.use(require('../routers/auth/list-permissions')) + + // track: scoping iife + 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 ( whatis(extra) !== 'object' ) { + throw APIError.create('field_invalid', null, { + key: 'extra', + expected: 'object', + got: whatis(extra), + }) + } + if ( whatis(metadata) !== 'object' ) { + throw APIError.create('field_invalid', null, { + key: 'metadata', + expected: 'object', + got: whatis(metadata), + }) + } + + const svc_group = this.services.get('group'); + const uid = await svc_group.create({ + owner_user_id, + // TODO: allow specifying these in request + extra: {}, + 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 ( whatis(req.body.users) !== 'array' ) { + throw APIError.create('field_invalid', null, { + key: 'users', + expected: 'array', + got: whatis(req.body.users), + }); + } + + for ( let i=0 ; i < req.body.users.length ; i++ ) { + const value = req.body.users[i]; + if ( whatis(value) === 'string' ) continue; + throw APIError.create('field_invalid', null, { + key: `users[${i}]`, + expected: 'string', + got: whatis(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 ( whatis(req.body.users) !== 'array' ) { + throw APIError.create('field_invalid', null, { + key: 'users', + expected: 'array', + got: whatis(req.body.users), + }); + } + + for ( let i=0 ; i < req.body.users.length ; i++ ) { + const value = req.body.users[i]; + if ( whatis(value) === 'string' ) continue; + throw APIError.create('field_invalid', null, { + key: `users[${i}]`, + expected: 'string', + got: whatis(value), + }); + } + + await svc_group.remove_users({ + uid: req.body.uid, + users: req.body.users, + }); + + res.json({}); + } + }).attach(router); + } +} + +module.exports = { + PermissionAPIService, +}; diff --git a/packages/backend/src/services/PuterAPIService.js b/packages/backend/src/services/PuterAPIService.js index f8191a3200..d8a7f53bef 100644 --- a/packages/backend/src/services/PuterAPIService.js +++ b/packages/backend/src/services/PuterAPIService.js @@ -26,12 +26,6 @@ class PuterAPIService extends BaseService { app.use(require('../routers/query/app')) app.use(require('../routers/change_username')) require('../routers/change_email')(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-user-user')); - app.use(require('../routers/auth/revoke-user-user')); - app.use(require('../routers/auth/list-permissions')) app.use(require('../routers/auth/list-sessions')) app.use(require('../routers/auth/revoke-session')) app.use(require('../routers/auth/check-app')) diff --git a/packages/backend/src/services/auth/GroupService.js b/packages/backend/src/services/auth/GroupService.js new file mode 100644 index 0000000000..7120488268 --- /dev/null +++ b/packages/backend/src/services/auth/GroupService.js @@ -0,0 +1,89 @@ +const BaseService = require("../BaseService"); +const { DB_WRITE } = require("../database/consts"); + +class GroupService extends BaseService { + static MODULES = { + uuidv4: require('uuid').v4, + }; + + _init () { + this.db = this.services.get('database').get(DB_WRITE, 'permissions'); + } + + async get({ uid }) { + const [group] = + await this.db.read('SELECT * FROM `group` WHERE uid=?', [uid]); + if ( ! group ) return; + group.extra = this.db.case({ + mysql: () => group.extra, + otherwise: () => JSON.parse(group.extra), + })(); + group.metadata = this.db.case({ + mysql: () => group.metadata, + otherwise: () => JSON.parse(group.metadata), + })(); + return group; + } + + async create ({ owner_user_id, extra, metadata }) { + extra = extra ?? {}; + metadata = metadata ?? {}; + + const uid = this.modules.uuidv4(); + + await this.db.write( + 'INSERT INTO `group` ' + + '(`uid`, `owner_user_id`, `extra`, `metadata`) ' + + 'VALUES (?, ?, ?, ?)', + [ + uid, owner_user_id, + JSON.stringify(extra), + JSON.stringify(metadata), + ] + ); + + return uid; + } + + async add_users ({ uid, users }) { + const question_marks = + '(' + Array(users.length).fill('?').join(', ') + ')'; + await this.db.write( + 'INSERT INTO `jct_user_group` ' + + '(user_id, group_id) ' + + 'SELECT u.id, g.id FROM user u '+ + 'JOIN (SELECT id FROM `group` WHERE uid=?) g ON 1=1 ' + + 'WHERE u.username IN ' + + question_marks, + [uid, ...users], + ); + } + + async remove_users ({ uid, users }) { + const question_marks = + '(' + Array(users.length).fill('?').join(', ') + ')'; + /* +DELETE FROM `jct_user_group` +WHERE group_id = 1 +AND user_id IN ( + SELECT u.id + FROM user u + WHERE u.username IN ('user_that_shares', 'user_that_gets_shared_to') +); + */ + await this.db.write( + 'DELETE FROM `jct_user_group` ' + + 'WHERE group_id = (SELECT id FROM `group` WHERE uid=?) ' + + 'AND user_id IN (' + + 'SELECT u.id FROM user u ' + + 'WHERE u.username IN ' + + question_marks + + ')', + [uid, ...users], + ); + } +} + +module.exports = { + GroupService, +};