diff --git a/extras/scripts/validateRoles.mjs b/extras/scripts/validateRoles.mjs new file mode 100644 index 00000000000..d547dbc090e --- /dev/null +++ b/extras/scripts/validateRoles.mjs @@ -0,0 +1,165 @@ +import { ACCESS_SCHEMA } from "../../server/utils/AccessManager/schema.js"; +import { ValidateObjectsAgainstSchemas } from "../../server/node_modules/object-deep-compare/dist/index.js"; +import fs from "fs"; +import path from "path"; + +const __dirname = path.resolve(); +const ROLES_DIR = path.join(__dirname, "../../server/utils/AccessManager/defaults"); + +// Deep-check every value in the permissions object to ensure it is true +function allPermissionsTrue(permissions = {}) { + for (const key in permissions) { + const value = permissions[key]; + + // If the value is an object, recursively check its contents + if (typeof value === 'object' && value !== null) { + if (!allPermissionsTrue(value)) return false; + } + // If the value is not a boolean true, return false + else if (value !== true) { + return false; + } + } + return true; +} + +function permissionIsTrue(rolePermissions = {}, rulePath = "") { + const permPath = rulePath.split("."); + if (permPath.length === 0) return false; + + let rolePermission = rolePermissions; + for (const subPerm of permPath) { + if (rolePermission.hasOwnProperty(subPerm)) + rolePermission = rolePermission[subPerm]; + else break; + } + + // Ending the evaluation if the permission should be a boolean, string, or number + if ( + rolePermission === undefined || + !["boolean", "string", "number"].includes(typeof rolePermission) + ) return false; + + return rolePermission === true; +} + +const defaultRoleDefinitions = []; +for (const file of fs.readdirSync(ROLES_DIR)) { + const roleDefinition = JSON.parse(fs.readFileSync(path.join(ROLES_DIR, file), "utf8")); + defaultRoleDefinitions.push(roleDefinition); +} + +const LegacyRoleMap = { + // Every permission in here means every role type should have this permission + 'all': [ + "workspaceThread.create", + "workspaceChats.playTTS", + "workspace.chat", + "users.pfp.read", + "users.pfp.update", + "users.pfp.delete", + "welcomeMessages.read", + "slashCommands.read", + "slashCommands.create", + "slashCommands.update", + "slashCommands.delete", + "systemPromptVariables.read", + "workspaceThread.read", + "workspaceThread.delete", + "workspaceChats.read", + "workspaceThread.update", + "workspaceChats.delete", + "workspaceChats.update", + "workspace.read", + "workspaceSuggestedMessages.read", + "promptHistory.read" + ], + // any endpoint that previously had [Roles.manager] should be in this array + have all items in Roles.all + 'manager': [ + "users.read", + "users.create", + "users.update", + "users.delete", + "invite.read", + "invite.create", + "invite.delete", + "workspace.read", + "workspaceUsers.read", + "workspace.create", + "workspaceUsers.update", + "workspace.delete", + "systemSettings.read", + "systemSettings.update", + "browserExtensionKey.read", + "browserExtensionKey.create", + "browserExtensionKey.delete", + "documents.createFolder", + "documents.moveFiles", + "documentSync.update", + "collector.useExtension", + "system.countVectors", + "documents.remove", + "documents.read", + "system.branding.update", + "system.branding.delete", + "welcomeMessages.update", + "workspaceChats.read", + "workspaceChats.delete", + "workspaceChats.export", + "workspace.update", + "documents.upload", + "workspace.embed", + "workspace.resetVectorDb", + "workspaceSuggestedMessages.update", + "workspace.documentPinStatus", + "workspace.readAny" + ], + // Every available permission should be true in the admin role + // 'admin': [], +} + +for (const role of defaultRoleDefinitions) { + console.log(`-> Validating schema for role: ${role.name}`); + try { + // First validate against schema + ValidateObjectsAgainstSchemas(role, {}, { + firstObjectSchema: ACCESS_SCHEMA, + throwOnValidationFailure: true, + }); + + // Then check for extra properties + function checkForExtraProps(obj, schema, currentPath = '') { + for (const key in obj) { + const newPath = currentPath ? `${currentPath}.${key}` : key; + if (!schema.hasOwnProperty(key)) throw new Error(`Extra property found: ${newPath}`); + if (typeof obj[key] === 'object') checkForExtraProps(obj[key], schema[key], newPath); + } + } + + checkForExtraProps(role, ACCESS_SCHEMA); + console.log(`✅ Standard role '${role.name}' conforms to the standard role schema!`); + + } catch (error) { + console.log(`❌ Standard role '${role.name}' does not conform to the standard role schema!`); + throw error; + } +} + +for (const role of defaultRoleDefinitions) { + console.log(`-> Validating legacy permissions for role: ${role.name}`); + if (role.name === 'admin' && !allPermissionsTrue(role.permissions)) throw new Error(`❌ Admin role has permissions that are not all true!`); + + if (role.name === 'manager') { + const allRequiredPermissions = new Set([...LegacyRoleMap.all, ...LegacyRoleMap.manager]); + for (const perm of allRequiredPermissions) { + if (!permissionIsTrue(role.permissions, perm)) throw new Error(`❌ Manager role is missing permission: ${perm}`); + } + } + + if (role.name === 'default') { + const allRequiredPermissions = new Set([...LegacyRoleMap.all]); + for (const perm of allRequiredPermissions) { + if (!permissionIsTrue(role.permissions, perm)) throw new Error(`❌ Default role is missing permission: ${perm}`); + } + } +} \ No newline at end of file diff --git a/server/endpoints/admin.js b/server/endpoints/admin.js index 211f50465b1..a36a354d289 100644 --- a/server/endpoints/admin.js +++ b/server/endpoints/admin.js @@ -3,7 +3,6 @@ const { Document } = require("../models/documents"); const { EventLogs } = require("../models/eventLogs"); const { Invite } = require("../models/invite"); const { SystemSettings } = require("../models/systemSettings"); -const { Telemetry } = require("../models/telemetry"); const { User } = require("../models/user"); const { DocumentVectors } = require("../models/vectors"); const { Workspace } = require("../models/workspace"); @@ -18,20 +17,16 @@ const { validCanModify, } = require("../utils/helpers/admin"); const { reqBody, userFromSession, safeJsonParse } = require("../utils/http"); -const { - strictMultiUserRoleValid, - flexUserRoleValid, - ROLES, -} = require("../utils/middleware/multiUserProtected"); const { validatedRequest } = require("../utils/middleware/validatedRequest"); const ImportedPlugin = require("../utils/agents/imported"); +const AccessManager = require("../utils/AccessManager"); function adminEndpoints(app) { if (!app) return; app.get( "/admin/users", - [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.strictAC(["users.read"])], async (_request, response) => { try { const users = await User.where(); @@ -45,7 +40,7 @@ function adminEndpoints(app) { app.post( "/admin/users/new", - [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.strictAC(["users.create"])], async (request, response) => { try { const currUser = await userFromSession(request, response); @@ -81,7 +76,7 @@ function adminEndpoints(app) { app.post( "/admin/user/:id", - [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.strictAC(["users.update"])], async (request, response) => { try { const currUser = await userFromSession(request, response); @@ -122,7 +117,7 @@ function adminEndpoints(app) { app.delete( "/admin/user/:id", - [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.strictAC(["users.delete"])], async (request, response) => { try { const currUser = await userFromSession(request, response); @@ -154,7 +149,7 @@ function adminEndpoints(app) { app.get( "/admin/invites", - [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.strictAC(["invite.read"])], async (_request, response) => { try { const invites = await Invite.whereWithUsers(); @@ -168,7 +163,7 @@ function adminEndpoints(app) { app.post( "/admin/invite/new", - [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.strictAC(["invite.create"])], async (request, response) => { try { const user = await userFromSession(request, response); @@ -196,7 +191,7 @@ function adminEndpoints(app) { app.delete( "/admin/invite/:id", - [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.strictAC(["invite.delete"])], async (request, response) => { try { const { id } = request.params; @@ -216,7 +211,7 @@ function adminEndpoints(app) { app.get( "/admin/workspaces", - [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.strictAC(["workspace.read"])], async (_request, response) => { try { const workspaces = await Workspace.whereWithUsers(); @@ -230,7 +225,7 @@ function adminEndpoints(app) { app.get( "/admin/workspaces/:workspaceId/users", - [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.strictAC(["workspaceUsers.read"])], async (request, response) => { try { const { workspaceId } = request.params; @@ -245,7 +240,7 @@ function adminEndpoints(app) { app.post( "/admin/workspaces/new", - [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.strictAC(["workspace.create"])], async (request, response) => { try { const user = await userFromSession(request, response); @@ -264,7 +259,7 @@ function adminEndpoints(app) { app.post( "/admin/workspaces/:workspaceId/update-users", - [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.strictAC(["workspaceUsers.update"])], async (request, response) => { try { const { workspaceId } = request.params; @@ -283,7 +278,7 @@ function adminEndpoints(app) { app.delete( "/admin/workspaces/:id", - [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.strictAC(["workspace.delete"])], async (request, response) => { try { const { id } = request.params; @@ -315,7 +310,7 @@ function adminEndpoints(app) { // System preferences but only by array of labels app.get( "/admin/system-preferences-for", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.flexibleAC(["systemSettings.read"])], async (request, response) => { try { const requestedSettings = {}; @@ -412,7 +407,7 @@ function adminEndpoints(app) { // DEPRECATED - use /admin/system-preferences-for instead with ?labels=... comma separated string of labels app.get( "/admin/system-preferences", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.flexibleAC(["systemSettings.read"])], async (_, response) => { try { const embedder = getEmbeddingEngineSelection(); @@ -473,7 +468,7 @@ function adminEndpoints(app) { app.post( "/admin/system-preferences", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.flexibleAC(["systemSettings.update"])], async (request, response) => { try { const updates = reqBody(request); @@ -488,7 +483,7 @@ function adminEndpoints(app) { app.get( "/admin/api-keys", - [validatedRequest, strictMultiUserRoleValid([ROLES.admin])], + [validatedRequest, AccessManager.strictAC(["apiKeys.read"])], async (_request, response) => { try { const apiKeys = await ApiKey.whereWithUser({}); @@ -508,7 +503,7 @@ function adminEndpoints(app) { app.post( "/admin/generate-api-key", - [validatedRequest, strictMultiUserRoleValid([ROLES.admin])], + [validatedRequest, AccessManager.strictAC(["apiKeys.create"])], async (request, response) => { try { const user = await userFromSession(request, response); @@ -531,7 +526,7 @@ function adminEndpoints(app) { app.delete( "/admin/delete-api-key/:id", - [validatedRequest, strictMultiUserRoleValid([ROLES.admin])], + [validatedRequest, AccessManager.strictAC(["apiKeys.delete"])], async (request, response) => { try { const { id } = request.params; diff --git a/server/endpoints/agentFlows.js b/server/endpoints/agentFlows.js index 75a16c774a0..f124fc3a9a2 100644 --- a/server/endpoints/agentFlows.js +++ b/server/endpoints/agentFlows.js @@ -1,10 +1,7 @@ const { AgentFlows } = require("../utils/agentFlows"); -const { - flexUserRoleValid, - ROLES, -} = require("../utils/middleware/multiUserProtected"); const { validatedRequest } = require("../utils/middleware/validatedRequest"); const { Telemetry } = require("../models/telemetry"); +const AccessManager = require("../utils/AccessManager"); function agentFlowEndpoints(app) { if (!app) return; @@ -12,7 +9,7 @@ function agentFlowEndpoints(app) { // Save a flow configuration app.post( "/agent-flows/save", - [validatedRequest, flexUserRoleValid([ROLES.admin])], + [validatedRequest, AccessManager.flexibleAC(["agentFlows.create"])], async (request, response) => { try { const { name, config, uuid } = request.body; @@ -55,7 +52,7 @@ function agentFlowEndpoints(app) { // List all available flows app.get( "/agent-flows/list", - [validatedRequest, flexUserRoleValid([ROLES.admin])], + [validatedRequest, AccessManager.flexibleAC(["agentFlows.read"])], async (_request, response) => { try { const flows = AgentFlows.listFlows(); @@ -76,7 +73,7 @@ function agentFlowEndpoints(app) { // Get a specific flow by UUID app.get( "/agent-flows/:uuid", - [validatedRequest, flexUserRoleValid([ROLES.admin])], + [validatedRequest, AccessManager.flexibleAC(["agentFlows.read"])], async (request, response) => { try { const { uuid } = request.params; @@ -102,44 +99,10 @@ function agentFlowEndpoints(app) { } ); - // Run a specific flow - // app.post( - // "/agent-flows/:uuid/run", - // [validatedRequest, flexUserRoleValid([ROLES.admin])], - // async (request, response) => { - // try { - // const { uuid } = request.params; - // const { variables = {} } = request.body; - - // // TODO: Implement flow execution - // console.log("Running flow with UUID:", uuid); - - // await Telemetry.sendTelemetry("agent_flow_executed", { - // variableCount: Object.keys(variables).length, - // }); - - // return response.status(200).json({ - // success: true, - // results: { - // success: true, - // results: "test", - // variables: variables, - // }, - // }); - // } catch (error) { - // console.error("Error running flow:", error); - // return response.status(500).json({ - // success: false, - // error: error.message, - // }); - // } - // } - // ); - // Delete a specific flow app.delete( "/agent-flows/:uuid", - [validatedRequest, flexUserRoleValid([ROLES.admin])], + [validatedRequest, AccessManager.flexibleAC(["agentFlows.delete"])], async (request, response) => { try { const { uuid } = request.params; @@ -168,7 +131,7 @@ function agentFlowEndpoints(app) { // Toggle flow active status app.post( "/agent-flows/:uuid/toggle", - [validatedRequest, flexUserRoleValid([ROLES.admin])], + [validatedRequest, AccessManager.flexibleAC(["agentFlows.update"])], async (request, response) => { try { const { uuid } = request.params; diff --git a/server/endpoints/browserExtension.js b/server/endpoints/browserExtension.js index 844da00e760..f54a5f8a5ad 100644 --- a/server/endpoints/browserExtension.js +++ b/server/endpoints/browserExtension.js @@ -7,11 +7,8 @@ const { const { CollectorApi } = require("../utils/collectorApi"); const { reqBody, multiUserMode, userFromSession } = require("../utils/http"); const { validatedRequest } = require("../utils/middleware/validatedRequest"); -const { - flexUserRoleValid, - ROLES, -} = require("../utils/middleware/multiUserProtected"); const { Telemetry } = require("../models/telemetry"); +const AccessManager = require("../utils/AccessManager"); function browserExtensionEndpoints(app) { if (!app) return; @@ -154,7 +151,7 @@ function browserExtensionEndpoints(app) { // Internal endpoints for managing API keys app.get( "/browser-extension/api-keys", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.flexibleAC(["browserExtensionKey.read"])], async (request, response) => { try { const user = await userFromSession(request, response); @@ -174,7 +171,10 @@ function browserExtensionEndpoints(app) { app.post( "/browser-extension/api-keys/new", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [ + validatedRequest, + AccessManager.flexibleAC(["browserExtensionKey.create"]), + ], async (request, response) => { try { const user = await userFromSession(request, response); @@ -194,21 +194,21 @@ function browserExtensionEndpoints(app) { app.delete( "/browser-extension/api-keys/:id", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [ + validatedRequest, + AccessManager.flexibleAC(["browserExtensionKey.delete"]), + ], async (request, response) => { try { const { id } = request.params; const user = await userFromSession(request, response); + const apiKey = await BrowserExtensionApiKey.get({ + id: parseInt(id), + user_id: user?.id, + }); - if (multiUserMode(response) && user.role !== ROLES.admin) { - const apiKey = await BrowserExtensionApiKey.get({ - id: parseInt(id), - user_id: user?.id, - }); - if (!apiKey) { - return response.status(403).json({ error: "Unauthorized" }); - } - } + if (!apiKey) + return response.status(403).json({ error: "Unauthorized" }); const { success, error } = await BrowserExtensionApiKey.delete(id); if (!success) throw new Error(error); diff --git a/server/endpoints/chat.js b/server/endpoints/chat.js index c8770857ea2..528bfb1e91d 100644 --- a/server/endpoints/chat.js +++ b/server/endpoints/chat.js @@ -3,10 +3,6 @@ const { reqBody, userFromSession, multiUserMode } = require("../utils/http"); const { validatedRequest } = require("../utils/middleware/validatedRequest"); const { Telemetry } = require("../models/telemetry"); const { streamChatWithWorkspace } = require("../utils/chats/stream"); -const { - ROLES, - flexUserRoleValid, -} = require("../utils/middleware/multiUserProtected"); const { EventLogs } = require("../models/eventLogs"); const { validWorkspaceAndThreadSlug, @@ -17,13 +13,18 @@ const { WorkspaceThread } = require("../models/workspaceThread"); const { User } = require("../models/user"); const truncate = require("truncate"); const { getModelTag } = require("./utils"); +const AccessManager = require("../utils/AccessManager"); function chatEndpoints(app) { if (!app) return; app.post( "/workspace/:slug/stream-chat", - [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug], + [ + validatedRequest, + AccessManager.flexibleAC(["workspace.chat"]), + validWorkspaceSlug, + ], async (request, response) => { try { const user = await userFromSession(request, response); @@ -107,7 +108,7 @@ function chatEndpoints(app) { "/workspace/:slug/thread/:threadSlug/stream-chat", [ validatedRequest, - flexUserRoleValid([ROLES.all]), + AccessManager.flexibleAC(["workspace.chat"]), validWorkspaceAndThreadSlug, ], async (request, response) => { diff --git a/server/endpoints/communityHub.js b/server/endpoints/communityHub.js index b8f0981ab52..7505cf26874 100644 --- a/server/endpoints/communityHub.js +++ b/server/endpoints/communityHub.js @@ -8,17 +8,14 @@ const { } = require("../utils/middleware/communityHubDownloadsEnabled"); const { EventLogs } = require("../models/eventLogs"); const { Telemetry } = require("../models/telemetry"); -const { - flexUserRoleValid, - ROLES, -} = require("../utils/middleware/multiUserProtected"); +const AccessManager = require("../utils/AccessManager"); function communityHubEndpoints(app) { if (!app) return; app.get( "/community-hub/settings", - [validatedRequest, flexUserRoleValid([ROLES.admin])], + [validatedRequest, AccessManager.flexibleAC(["systemSettings.read"])], async (_, response) => { try { const { connectionKey } = await SystemSettings.hubSettings(); @@ -32,7 +29,7 @@ function communityHubEndpoints(app) { app.post( "/community-hub/settings", - [validatedRequest, flexUserRoleValid([ROLES.admin])], + [validatedRequest, AccessManager.flexibleAC(["systemSettings.update"])], async (request, response) => { try { const data = reqBody(request); @@ -48,7 +45,7 @@ function communityHubEndpoints(app) { app.get( "/community-hub/explore", - [validatedRequest, flexUserRoleValid([ROLES.admin])], + [validatedRequest, AccessManager.flexibleAC(["communityHub.explore"])], async (_, response) => { try { const exploreItems = await CommunityHub.fetchExploreItems(); @@ -66,7 +63,11 @@ function communityHubEndpoints(app) { app.post( "/community-hub/item", - [validatedRequest, flexUserRoleValid([ROLES.admin]), communityHubItem], + [ + validatedRequest, + AccessManager.flexibleAC(["communityHub.viewItems"]), + communityHubItem, + ], async (_request, response) => { try { response.status(200).json({ @@ -90,7 +91,11 @@ function communityHubEndpoints(app) { */ app.post( "/community-hub/apply", - [validatedRequest, flexUserRoleValid([ROLES.admin]), communityHubItem], + [ + validatedRequest, + AccessManager.flexibleAC(["communityHub.importItems"]), + communityHubItem, + ], async (request, response) => { try { const { options = {} } = reqBody(request); @@ -131,7 +136,7 @@ function communityHubEndpoints(app) { "/community-hub/import", [ validatedRequest, - flexUserRoleValid([ROLES.admin]), + AccessManager.flexibleAC(["communityHub.importItems"]), communityHubItem, communityHubDownloadsEnabled, ], @@ -169,7 +174,7 @@ function communityHubEndpoints(app) { app.get( "/community-hub/items", - [validatedRequest, flexUserRoleValid([ROLES.admin])], + [validatedRequest, AccessManager.flexibleAC(["communityHub.viewItems"])], async (_, response) => { try { const { connectionKey } = await SystemSettings.hubSettings(); diff --git a/server/endpoints/document.js b/server/endpoints/document.js index e4c311aee51..a15b496a3c2 100644 --- a/server/endpoints/document.js +++ b/server/endpoints/document.js @@ -1,19 +1,16 @@ const { Document } = require("../models/documents"); const { normalizePath, documentsPath, isWithin } = require("../utils/files"); const { reqBody } = require("../utils/http"); -const { - flexUserRoleValid, - ROLES, -} = require("../utils/middleware/multiUserProtected"); const { validatedRequest } = require("../utils/middleware/validatedRequest"); const fs = require("fs"); const path = require("path"); +const AccessManager = require("../utils/AccessManager"); function documentEndpoints(app) { if (!app) return; app.post( "/document/create-folder", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.flexibleAC(["documents.createFolder"])], async (request, response) => { try { const { name } = reqBody(request); @@ -43,7 +40,7 @@ function documentEndpoints(app) { app.post( "/document/move-files", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.flexibleAC(["documents.moveFiles"])], async (request, response) => { try { const { files } = reqBody(request); diff --git a/server/endpoints/embedManagement.js b/server/endpoints/embedManagement.js index 8bee4dd75b4..8c834724ed8 100644 --- a/server/endpoints/embedManagement.js +++ b/server/endpoints/embedManagement.js @@ -3,21 +3,18 @@ const { EmbedConfig } = require("../models/embedConfig"); const { EventLogs } = require("../models/eventLogs"); const { reqBody, userFromSession } = require("../utils/http"); const { validEmbedConfigId } = require("../utils/middleware/embedMiddleware"); -const { - flexUserRoleValid, - ROLES, -} = require("../utils/middleware/multiUserProtected"); const { validatedRequest } = require("../utils/middleware/validatedRequest"); const { chatHistoryViewable, } = require("../utils/middleware/chatHistoryViewable"); +const AccessManager = require("../utils/AccessManager"); function embedManagementEndpoints(app) { if (!app) return; app.get( "/embeds", - [validatedRequest, flexUserRoleValid([ROLES.admin])], + [validatedRequest, AccessManager.flexibleAC(["chatEmbeds.read"])], async (_, response) => { try { const embeds = await EmbedConfig.whereWithWorkspace({}, null, { @@ -33,7 +30,7 @@ function embedManagementEndpoints(app) { app.post( "/embeds/new", - [validatedRequest, flexUserRoleValid([ROLES.admin])], + [validatedRequest, AccessManager.flexibleAC(["chatEmbeds.create"])], async (request, response) => { try { const user = await userFromSession(request, response); @@ -54,7 +51,11 @@ function embedManagementEndpoints(app) { app.post( "/embed/update/:embedId", - [validatedRequest, flexUserRoleValid([ROLES.admin]), validEmbedConfigId], + [ + validatedRequest, + AccessManager.flexibleAC(["chatEmbeds.update"]), + validEmbedConfigId, + ], async (request, response) => { try { const user = await userFromSession(request, response); @@ -72,7 +73,11 @@ function embedManagementEndpoints(app) { app.delete( "/embed/:embedId", - [validatedRequest, flexUserRoleValid([ROLES.admin]), validEmbedConfigId], + [ + validatedRequest, + AccessManager.flexibleAC(["chatEmbeds.delete"]), + validEmbedConfigId, + ], async (request, response) => { try { const { embedId } = request.params; @@ -92,7 +97,11 @@ function embedManagementEndpoints(app) { app.post( "/embed/chats", - [chatHistoryViewable, validatedRequest, flexUserRoleValid([ROLES.admin])], + [ + chatHistoryViewable, + validatedRequest, + AccessManager.flexibleAC(["chatEmbedChats.read"]), + ], async (request, response) => { try { const { offset = 0, limit = 20 } = reqBody(request); @@ -114,7 +123,7 @@ function embedManagementEndpoints(app) { app.delete( "/embed/chats/:chatId", - [validatedRequest, flexUserRoleValid([ROLES.admin])], + [validatedRequest, AccessManager.flexibleAC(["chatEmbedChats.delete"])], async (request, response) => { try { const { chatId } = request.params; diff --git a/server/endpoints/experimental/imported-agent-plugins.js b/server/endpoints/experimental/imported-agent-plugins.js index cabe23d89e7..05f6992d075 100644 --- a/server/endpoints/experimental/imported-agent-plugins.js +++ b/server/endpoints/experimental/imported-agent-plugins.js @@ -1,17 +1,14 @@ const ImportedPlugin = require("../../utils/agents/imported"); const { reqBody } = require("../../utils/http"); -const { - flexUserRoleValid, - ROLES, -} = require("../../utils/middleware/multiUserProtected"); const { validatedRequest } = require("../../utils/middleware/validatedRequest"); +const AccessManager = require("../../utils/AccessManager"); function importedAgentPluginEndpoints(app) { if (!app) return; app.post( "/experimental/agent-plugins/:hubId/toggle", - [validatedRequest, flexUserRoleValid([ROLES.admin])], + [validatedRequest, AccessManager.flexibleAC(["agent.imported.update"])], (request, response) => { try { const { hubId } = request.params; @@ -29,7 +26,7 @@ function importedAgentPluginEndpoints(app) { app.post( "/experimental/agent-plugins/:hubId/config", - [validatedRequest, flexUserRoleValid([ROLES.admin])], + [validatedRequest, AccessManager.flexibleAC(["agent.imported.update"])], (request, response) => { try { const { hubId } = request.params; @@ -48,7 +45,7 @@ function importedAgentPluginEndpoints(app) { app.delete( "/experimental/agent-plugins/:hubId", - [validatedRequest, flexUserRoleValid([ROLES.admin])], + [validatedRequest, AccessManager.flexibleAC(["agent.imported.delete"])], async (request, response) => { try { const { hubId } = request.params; diff --git a/server/endpoints/experimental/liveSync.js b/server/endpoints/experimental/liveSync.js index 2a22d9a963b..25e00ef7c2e 100644 --- a/server/endpoints/experimental/liveSync.js +++ b/server/endpoints/experimental/liveSync.js @@ -7,19 +7,16 @@ const { reqBody } = require("../../utils/http"); const { featureFlagEnabled, } = require("../../utils/middleware/featureFlagEnabled"); -const { - flexUserRoleValid, - ROLES, -} = require("../../utils/middleware/multiUserProtected"); const { validWorkspaceSlug } = require("../../utils/middleware/validWorkspace"); const { validatedRequest } = require("../../utils/middleware/validatedRequest"); +const AccessManager = require("../../utils/AccessManager"); function liveSyncEndpoints(app) { if (!app) return; app.post( "/experimental/toggle-live-sync", - [validatedRequest, flexUserRoleValid([ROLES.admin])], + [validatedRequest, AccessManager.flexibleAC(["systemSettings.update"])], async (request, response) => { try { const { updatedStatus = false } = reqBody(request); @@ -61,7 +58,7 @@ function liveSyncEndpoints(app) { "/experimental/live-sync/queues", [ validatedRequest, - flexUserRoleValid([ROLES.admin]), + AccessManager.flexibleAC(["documentSync.read"]), featureFlagEnabled(DocumentSyncQueue.featureKey), ], async (_, response) => { @@ -86,7 +83,7 @@ function liveSyncEndpoints(app) { "/workspace/:slug/update-watch-status", [ validatedRequest, - flexUserRoleValid([ROLES.admin, ROLES.manager]), + AccessManager.flexibleAC(["documentSync.update"]), validWorkspaceSlug, featureFlagEnabled(DocumentSyncQueue.featureKey), ], diff --git a/server/endpoints/extensions/index.js b/server/endpoints/extensions/index.js index 7bfff06724a..eba07451e25 100644 --- a/server/endpoints/extensions/index.js +++ b/server/endpoints/extensions/index.js @@ -1,13 +1,10 @@ const { Telemetry } = require("../../models/telemetry"); const { CollectorApi } = require("../../utils/collectorApi"); -const { - flexUserRoleValid, - ROLES, -} = require("../../utils/middleware/multiUserProtected"); const { validatedRequest } = require("../../utils/middleware/validatedRequest"); const { isSupportedRepoProvider, } = require("../../utils/middleware/isSupportedRepoProviders"); +const AccessManager = require("../../utils/AccessManager"); function extensionEndpoints(app) { if (!app) return; @@ -16,7 +13,7 @@ function extensionEndpoints(app) { "/ext/:repo_platform/branches", [ validatedRequest, - flexUserRoleValid([ROLES.admin, ROLES.manager]), + AccessManager.flexibleAC(["collector.useExtension"]), isSupportedRepoProvider, ], async (request, response) => { @@ -40,7 +37,7 @@ function extensionEndpoints(app) { "/ext/:repo_platform/repo", [ validatedRequest, - flexUserRoleValid([ROLES.admin, ROLES.manager]), + AccessManager.flexibleAC(["collector.useExtension"]), isSupportedRepoProvider, ], async (request, response) => { @@ -65,7 +62,7 @@ function extensionEndpoints(app) { app.post( "/ext/youtube/transcript", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.flexibleAC(["collector.useExtension"])], async (request, response) => { try { const responseFromProcessor = @@ -87,7 +84,7 @@ function extensionEndpoints(app) { app.post( "/ext/confluence", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.flexibleAC(["collector.useExtension"])], async (request, response) => { try { const responseFromProcessor = @@ -108,7 +105,7 @@ function extensionEndpoints(app) { ); app.post( "/ext/website-depth", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.flexibleAC(["collector.useExtension"])], async (request, response) => { try { const responseFromProcessor = @@ -129,7 +126,7 @@ function extensionEndpoints(app) { ); app.post( "/ext/drupalwiki", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.flexibleAC(["collector.useExtension"])], async (request, response) => { try { const responseFromProcessor = diff --git a/server/endpoints/mcpServers.js b/server/endpoints/mcpServers.js index 3cd5a865696..89f08fdac78 100644 --- a/server/endpoints/mcpServers.js +++ b/server/endpoints/mcpServers.js @@ -1,17 +1,14 @@ const { reqBody } = require("../utils/http"); const MCPCompatibilityLayer = require("../utils/MCP"); -const { - flexUserRoleValid, - ROLES, -} = require("../utils/middleware/multiUserProtected"); const { validatedRequest } = require("../utils/middleware/validatedRequest"); +const AccessManager = require("../utils/AccessManager"); function mcpServersEndpoints(app) { if (!app) return; app.get( "/mcp-servers/force-reload", - [validatedRequest, flexUserRoleValid([ROLES.admin])], + [validatedRequest, AccessManager.flexibleAC(["mcp.forceReload"])], async (_request, response) => { try { const mcp = new MCPCompatibilityLayer(); @@ -34,7 +31,7 @@ function mcpServersEndpoints(app) { app.get( "/mcp-servers/list", - [validatedRequest, flexUserRoleValid([ROLES.admin])], + [validatedRequest, AccessManager.flexibleAC(["mcp.read"])], async (_request, response) => { try { const servers = await new MCPCompatibilityLayer().servers(); @@ -54,7 +51,7 @@ function mcpServersEndpoints(app) { app.post( "/mcp-servers/toggle", - [validatedRequest, flexUserRoleValid([ROLES.admin])], + [validatedRequest, AccessManager.flexibleAC(["mcp.update"])], async (request, response) => { try { const { name } = reqBody(request); @@ -77,7 +74,7 @@ function mcpServersEndpoints(app) { app.post( "/mcp-servers/delete", - [validatedRequest, flexUserRoleValid([ROLES.admin])], + [validatedRequest, AccessManager.flexibleAC(["mcp.delete"])], async (request, response) => { try { const { name } = reqBody(request); diff --git a/server/endpoints/system.js b/server/endpoints/system.js index 4077844e733..b53de8b272a 100644 --- a/server/endpoints/system.js +++ b/server/endpoints/system.js @@ -34,11 +34,7 @@ const { WelcomeMessages } = require("../models/welcomeMessages"); const { ApiKey } = require("../models/apiKeys"); const { getCustomModels } = require("../utils/helpers/customModels"); const { WorkspaceChats } = require("../models/workspaceChats"); -const { - flexUserRoleValid, - ROLES, - isMultiUserSetup, -} = require("../utils/middleware/multiUserProtected"); +const { isMultiUserSetup } = require("../utils/middleware/multiUserProtected"); const { fetchPfp, determinePfpFilepath } = require("../utils/files/pfp"); const { exportChatsAsType } = require("../utils/helpers/chat/convertTo"); const { EventLogs } = require("../models/eventLogs"); @@ -58,6 +54,7 @@ const { simpleSSOEnabled } = require("../utils/middleware/simpleSSOEnabled"); const { TemporaryAuthToken } = require("../models/temporaryAuthToken"); const { SystemPromptVariables } = require("../models/systemPromptVariables"); const { VALID_COMMANDS } = require("../utils/chats"); +const AccessManager = require("../utils/AccessManager"); function systemEndpoints(app) { if (!app) return; @@ -350,7 +347,7 @@ function systemEndpoints(app) { app.get( "/system/system-vectors", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.flexibleAC(["system.countVectors"])], async (request, response) => { try { const query = queryParams(request); @@ -368,7 +365,7 @@ function systemEndpoints(app) { app.delete( "/system/remove-document", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.flexibleAC(["documents.remove"])], async (request, response) => { try { const { name } = reqBody(request); @@ -383,7 +380,7 @@ function systemEndpoints(app) { app.delete( "/system/remove-documents", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.flexibleAC(["documents.remove"])], async (request, response) => { try { const { names } = reqBody(request); @@ -398,7 +395,7 @@ function systemEndpoints(app) { app.delete( "/system/remove-folder", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.flexibleAC(["documents.remove"])], async (request, response) => { try { const { name } = reqBody(request); @@ -413,7 +410,7 @@ function systemEndpoints(app) { app.get( "/system/local-files", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.flexibleAC(["documents.read"])], async (_, response) => { try { const localFiles = await viewLocalFiles(); @@ -460,7 +457,7 @@ function systemEndpoints(app) { app.post( "/system/update-env", - [validatedRequest, flexUserRoleValid([ROLES.admin])], + [validatedRequest, AccessManager.flexibleAC(["system.update"])], async (request, response) => { try { const body = reqBody(request); @@ -528,7 +525,7 @@ function systemEndpoints(app) { const { user, error } = await User.create({ username, password, - role: ROLES.admin, + role: AccessManager.defaultRoles.admin, }); if (error || !user) { @@ -657,7 +654,7 @@ function systemEndpoints(app) { app.get( "/system/pfp/:id", - [validatedRequest, flexUserRoleValid([ROLES.all])], + [validatedRequest, AccessManager.flexibleAC(["users.pfp.read"])], async function (request, response) { try { const { id } = request.params; @@ -686,7 +683,11 @@ function systemEndpoints(app) { app.post( "/system/upload-pfp", - [validatedRequest, flexUserRoleValid([ROLES.all]), handlePfpUpload], + [ + validatedRequest, + AccessManager.flexibleAC(["users.pfp.update"]), + handlePfpUpload, + ], async function (request, response) { try { const user = await userFromSession(request, response); @@ -726,7 +727,7 @@ function systemEndpoints(app) { app.delete( "/system/remove-pfp", - [validatedRequest, flexUserRoleValid([ROLES.all])], + [validatedRequest, AccessManager.flexibleAC(["users.pfp.delete"])], async function (request, response) { try { const user = await userFromSession(request, response); @@ -764,7 +765,7 @@ function systemEndpoints(app) { "/system/upload-logo", [ validatedRequest, - flexUserRoleValid([ROLES.admin, ROLES.manager]), + AccessManager.flexibleAC(["system.branding.update"]), handleAssetUpload, ], async (request, response) => { @@ -813,7 +814,7 @@ function systemEndpoints(app) { app.get( "/system/remove-logo", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.flexibleAC(["system.branding.delete"])], async (_request, response) => { try { const currentLogoFilename = await SystemSettings.currentLogoFilename(); @@ -836,7 +837,7 @@ function systemEndpoints(app) { app.get( "/system/welcome-messages", - [validatedRequest, flexUserRoleValid([ROLES.all])], + [validatedRequest, AccessManager.flexibleAC(["welcomeMessages.read"])], async function (_, response) { try { const welcomeMessages = await WelcomeMessages.getMessages(); @@ -852,7 +853,7 @@ function systemEndpoints(app) { app.post( "/system/set-welcome-messages", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.flexibleAC(["welcomeMessages.update"])], async (request, response) => { try { const { messages = [] } = reqBody(request); @@ -955,7 +956,7 @@ function systemEndpoints(app) { app.post( "/system/custom-models", - [validatedRequest, flexUserRoleValid([ROLES.admin])], + [validatedRequest, AccessManager.flexibleAC(["system.models.read"])], async (request, response) => { try { const { provider, apiKey = null, basePath = null } = reqBody(request); @@ -977,7 +978,7 @@ function systemEndpoints(app) { app.post( "/system/event-logs", - [validatedRequest, flexUserRoleValid([ROLES.admin])], + [validatedRequest, AccessManager.flexibleAC(["eventLogs.read"])], async (request, response) => { try { const { offset = 0, limit = 10 } = reqBody(request); @@ -997,7 +998,7 @@ function systemEndpoints(app) { app.delete( "/system/event-logs", - [validatedRequest, flexUserRoleValid([ROLES.admin])], + [validatedRequest, AccessManager.flexibleAC(["eventLogs.delete"])], async (_, response) => { try { await EventLogs.delete(); @@ -1019,7 +1020,7 @@ function systemEndpoints(app) { [ chatHistoryViewable, validatedRequest, - flexUserRoleValid([ROLES.admin, ROLES.manager]), + AccessManager.flexibleAC(["workspaceChats.read"]), ], async (request, response) => { try { @@ -1043,7 +1044,7 @@ function systemEndpoints(app) { app.delete( "/system/workspace-chats/:id", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.flexibleAC(["workspaceChats.delete"])], async (request, response) => { try { const { id } = request.params; @@ -1063,7 +1064,7 @@ function systemEndpoints(app) { [ chatHistoryViewable, validatedRequest, - flexUserRoleValid([ROLES.manager, ROLES.admin]), + AccessManager.flexibleAC(["workspaceChats.export"]), ], async (request, response) => { try { @@ -1122,7 +1123,7 @@ function systemEndpoints(app) { app.get( "/system/slash-command-presets", - [validatedRequest, flexUserRoleValid([ROLES.all])], + [validatedRequest, AccessManager.flexibleAC(["slashCommands.read"])], async (request, response) => { try { const user = await userFromSession(request, response); @@ -1137,7 +1138,7 @@ function systemEndpoints(app) { app.post( "/system/slash-command-presets", - [validatedRequest, flexUserRoleValid([ROLES.all])], + [validatedRequest, AccessManager.flexibleAC(["slashCommands.create"])], async (request, response) => { try { const user = await userFromSession(request, response); @@ -1175,7 +1176,7 @@ function systemEndpoints(app) { app.post( "/system/slash-command-presets/:slashCommandId", - [validatedRequest, flexUserRoleValid([ROLES.all])], + [validatedRequest, AccessManager.flexibleAC(["slashCommands.update"])], async (request, response) => { try { const user = await userFromSession(request, response); @@ -1221,7 +1222,7 @@ function systemEndpoints(app) { app.delete( "/system/slash-command-presets/:slashCommandId", - [validatedRequest, flexUserRoleValid([ROLES.all])], + [validatedRequest, AccessManager.flexibleAC(["slashCommands.delete"])], async (request, response) => { try { const { slashCommandId } = request.params; @@ -1248,7 +1249,10 @@ function systemEndpoints(app) { app.get( "/system/prompt-variables", - [validatedRequest, flexUserRoleValid([ROLES.all])], + [ + validatedRequest, + AccessManager.flexibleAC(["systemPromptVariables.read"]), + ], async (request, response) => { try { const user = await userFromSession(request, response); @@ -1266,7 +1270,10 @@ function systemEndpoints(app) { app.post( "/system/prompt-variables", - [validatedRequest, flexUserRoleValid([ROLES.admin])], + [ + validatedRequest, + AccessManager.flexibleAC(["systemPromptVariables.create"]), + ], async (request, response) => { try { const user = await userFromSession(request, response); @@ -1302,7 +1309,10 @@ function systemEndpoints(app) { app.put( "/system/prompt-variables/:id", - [validatedRequest, flexUserRoleValid([ROLES.admin])], + [ + validatedRequest, + AccessManager.flexibleAC(["systemPromptVariables.update"]), + ], async (request, response) => { try { const { id } = request.params; @@ -1344,7 +1354,10 @@ function systemEndpoints(app) { app.delete( "/system/prompt-variables/:id", - [validatedRequest, flexUserRoleValid([ROLES.admin])], + [ + validatedRequest, + AccessManager.flexibleAC(["systemPromptVariables.delete"]), + ], async (request, response) => { try { const { id } = request.params; diff --git a/server/endpoints/workspaceThreads.js b/server/endpoints/workspaceThreads.js index a34616cfa7f..1cebf628c13 100644 --- a/server/endpoints/workspaceThreads.js +++ b/server/endpoints/workspaceThreads.js @@ -6,10 +6,6 @@ const { } = require("../utils/http"); const { validatedRequest } = require("../utils/middleware/validatedRequest"); const { Telemetry } = require("../models/telemetry"); -const { - flexUserRoleValid, - ROLES, -} = require("../utils/middleware/multiUserProtected"); const { EventLogs } = require("../models/eventLogs"); const { WorkspaceThread } = require("../models/workspaceThread"); const { @@ -19,13 +15,18 @@ const { const { WorkspaceChats } = require("../models/workspaceChats"); const { convertToChatHistory } = require("../utils/helpers/chat/responses"); const { getModelTag } = require("./utils"); +const AccessManager = require("../utils/AccessManager"); function workspaceThreadEndpoints(app) { if (!app) return; app.post( "/workspace/:slug/thread/new", - [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug], + [ + validatedRequest, + AccessManager.flexibleAC(["workspaceThread.create"]), + validWorkspaceSlug, + ], async (request, response) => { try { const user = await userFromSession(request, response); @@ -64,7 +65,11 @@ function workspaceThreadEndpoints(app) { app.get( "/workspace/:slug/threads", - [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug], + [ + validatedRequest, + AccessManager.flexibleAC(["workspaceThread.read"]), + validWorkspaceSlug, + ], async (request, response) => { try { const user = await userFromSession(request, response); @@ -85,7 +90,7 @@ function workspaceThreadEndpoints(app) { "/workspace/:slug/thread/:threadSlug", [ validatedRequest, - flexUserRoleValid([ROLES.all]), + AccessManager.flexibleAC(["workspaceThread.delete"]), validWorkspaceAndThreadSlug, ], async (_, response) => { @@ -102,7 +107,11 @@ function workspaceThreadEndpoints(app) { app.delete( "/workspace/:slug/thread-bulk-delete", - [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug], + [ + validatedRequest, + AccessManager.flexibleAC(["workspaceThread.delete"]), + validWorkspaceSlug, + ], async (request, response) => { try { const { slugs = [] } = reqBody(request); @@ -127,7 +136,7 @@ function workspaceThreadEndpoints(app) { "/workspace/:slug/thread/:threadSlug/chats", [ validatedRequest, - flexUserRoleValid([ROLES.all]), + AccessManager.flexibleAC(["workspaceChats.read"]), validWorkspaceAndThreadSlug, ], async (request, response) => { @@ -159,7 +168,7 @@ function workspaceThreadEndpoints(app) { "/workspace/:slug/thread/:threadSlug/update", [ validatedRequest, - flexUserRoleValid([ROLES.all]), + AccessManager.flexibleAC(["workspaceThread.update"]), validWorkspaceAndThreadSlug, ], async (request, response) => { @@ -182,7 +191,7 @@ function workspaceThreadEndpoints(app) { "/workspace/:slug/thread/:threadSlug/delete-edited-chats", [ validatedRequest, - flexUserRoleValid([ROLES.all]), + AccessManager.flexibleAC(["workspaceChats.delete"]), validWorkspaceAndThreadSlug, ], async (request, response) => { @@ -211,7 +220,7 @@ function workspaceThreadEndpoints(app) { "/workspace/:slug/thread/:threadSlug/update-chat", [ validatedRequest, - flexUserRoleValid([ROLES.all]), + AccessManager.flexibleAC(["workspaceChats.update"]), validWorkspaceAndThreadSlug, ], async (request, response) => { diff --git a/server/endpoints/workspaces.js b/server/endpoints/workspaces.js index 547d0339644..e7020f354e4 100644 --- a/server/endpoints/workspaces.js +++ b/server/endpoints/workspaces.js @@ -15,10 +15,6 @@ const { getVectorDbClass } = require("../utils/helpers"); const { handleFileUpload, handlePfpUpload } = require("../utils/files/multer"); const { validatedRequest } = require("../utils/middleware/validatedRequest"); const { Telemetry } = require("../models/telemetry"); -const { - flexUserRoleValid, - ROLES, -} = require("../utils/middleware/multiUserProtected"); const { EventLogs } = require("../models/eventLogs"); const { WorkspaceSuggestedMessages, @@ -35,6 +31,7 @@ const { WorkspaceThread } = require("../models/workspaceThread"); const truncate = require("truncate"); const { purgeDocument } = require("../utils/files/purgeDocument"); const { getModelTag } = require("./utils"); +const AccessManager = require("../utils/AccessManager"); function workspaceEndpoints(app) { if (!app) return; @@ -43,7 +40,7 @@ function workspaceEndpoints(app) { app.post( "/workspace/new", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.flexibleAC(["workspace.create"])], async (request, response) => { try { const user = await userFromSession(request, response); @@ -82,7 +79,7 @@ function workspaceEndpoints(app) { app.post( "/workspace/:slug/update", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.flexibleAC(["workspace.update"])], async (request, response) => { try { const user = await userFromSession(request, response); @@ -114,7 +111,7 @@ function workspaceEndpoints(app) { "/workspace/:slug/upload", [ validatedRequest, - flexUserRoleValid([ROLES.admin, ROLES.manager]), + AccessManager.flexibleAC(["documents.upload"]), handleFileUpload, ], async function (request, response) { @@ -162,7 +159,7 @@ function workspaceEndpoints(app) { app.post( "/workspace/:slug/upload-link", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.flexibleAC(["documents.upload"])], async (request, response) => { try { const Collector = new CollectorApi(); @@ -205,7 +202,7 @@ function workspaceEndpoints(app) { app.post( "/workspace/:slug/update-embeddings", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.flexibleAC(["workspace.embed"])], async (request, response) => { try { const user = await userFromSession(request, response); @@ -249,7 +246,7 @@ function workspaceEndpoints(app) { app.delete( "/workspace/:slug", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.flexibleAC(["workspace.delete"])], async (request, response) => { try { const { slug = "" } = request.params; @@ -292,7 +289,7 @@ function workspaceEndpoints(app) { app.delete( "/workspace/:slug/reset-vector-db", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.flexibleAC(["workspace.resetVectorDb"])], async (request, response) => { try { const { slug = "" } = request.params; @@ -333,7 +330,7 @@ function workspaceEndpoints(app) { app.get( "/workspaces", - [validatedRequest, flexUserRoleValid([ROLES.all])], + [validatedRequest, AccessManager.flexibleAC(["workspace.read"])], async (request, response) => { try { const user = await userFromSession(request, response); @@ -351,7 +348,7 @@ function workspaceEndpoints(app) { app.get( "/workspace/:slug", - [validatedRequest, flexUserRoleValid([ROLES.all])], + [validatedRequest, AccessManager.flexibleAC(["workspace.read"])], async (request, response) => { try { const { slug } = request.params; @@ -370,7 +367,7 @@ function workspaceEndpoints(app) { app.get( "/workspace/:slug/chats", - [validatedRequest, flexUserRoleValid([ROLES.all])], + [validatedRequest, AccessManager.flexibleAC(["workspaceChats.read"])], async (request, response) => { try { const { slug } = request.params; @@ -397,7 +394,11 @@ function workspaceEndpoints(app) { app.delete( "/workspace/:slug/delete-chats", - [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug], + [ + validatedRequest, + AccessManager.flexibleAC(["workspaceChats.delete"]), + validWorkspaceSlug, + ], async (request, response) => { try { const { chatIds = [] } = reqBody(request); @@ -428,7 +429,11 @@ function workspaceEndpoints(app) { app.delete( "/workspace/:slug/delete-edited-chats", - [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug], + [ + validatedRequest, + AccessManager.flexibleAC(["workspaceChats.delete"]), + validWorkspaceSlug, + ], async (request, response) => { try { const { startingId } = reqBody(request); @@ -452,7 +457,11 @@ function workspaceEndpoints(app) { app.post( "/workspace/:slug/update-chat", - [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug], + [ + validatedRequest, + AccessManager.flexibleAC(["workspaceChats.update"]), + validWorkspaceSlug, + ], async (request, response) => { try { const { chatId, newText = null } = reqBody(request); @@ -489,7 +498,11 @@ function workspaceEndpoints(app) { app.post( "/workspace/:slug/chat-feedback/:chatId", - [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug], + [ + validatedRequest, + AccessManager.flexibleAC(["workspaceChats.update"]), + validWorkspaceSlug, + ], async (request, response) => { try { const { chatId } = request.params; @@ -518,7 +531,10 @@ function workspaceEndpoints(app) { app.get( "/workspace/:slug/suggested-messages", - [validatedRequest, flexUserRoleValid([ROLES.all])], + [ + validatedRequest, + AccessManager.flexibleAC(["workspaceSuggestedMessages.read"]), + ], async function (request, response) { try { const { slug } = request.params; @@ -536,7 +552,10 @@ function workspaceEndpoints(app) { app.post( "/workspace/:slug/suggested-messages", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [ + validatedRequest, + AccessManager.flexibleAC(["workspaceSuggestedMessages.update"]), + ], async (request, response) => { try { const { messages = [] } = reqBody(request); @@ -567,7 +586,7 @@ function workspaceEndpoints(app) { "/workspace/:slug/update-pin", [ validatedRequest, - flexUserRoleValid([ROLES.admin, ROLES.manager]), + AccessManager.flexibleAC(["workspace.documentPinStatus"]), validWorkspaceSlug, ], async (request, response) => { @@ -592,7 +611,11 @@ function workspaceEndpoints(app) { app.get( "/workspace/:slug/tts/:chatId", - [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug], + [ + validatedRequest, + AccessManager.flexibleAC(["workspaceChats.playTTS"]), + validWorkspaceSlug, + ], async function (request, response) { try { const { chatId } = request.params; @@ -634,7 +657,7 @@ function workspaceEndpoints(app) { app.get( "/workspace/:slug/pfp", - [validatedRequest, flexUserRoleValid([ROLES.all])], + [validatedRequest, AccessManager.flexibleAC(["workspace.read"])], async function (request, response) { try { const { slug } = request.params; @@ -679,7 +702,7 @@ function workspaceEndpoints(app) { "/workspace/:slug/upload-pfp", [ validatedRequest, - flexUserRoleValid([ROLES.admin, ROLES.manager]), + AccessManager.flexibleAC(["workspace.update"]), handlePfpUpload, ], async function (request, response) { @@ -727,7 +750,7 @@ function workspaceEndpoints(app) { app.delete( "/workspace/:slug/remove-pfp", - [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], + [validatedRequest, AccessManager.flexibleAC(["workspace.update"])], async function (request, response) { try { const { slug } = request.params; @@ -771,7 +794,11 @@ function workspaceEndpoints(app) { app.post( "/workspace/:slug/thread/fork", - [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug], + [ + validatedRequest, + AccessManager.flexibleAC(["workspaceThread.create"]), + validWorkspaceSlug, + ], async (request, response) => { try { const user = await userFromSession(request, response); @@ -846,7 +873,7 @@ function workspaceEndpoints(app) { app.put( "/workspace/workspace-chats/:id", - [validatedRequest, flexUserRoleValid([ROLES.all])], + [validatedRequest, AccessManager.flexibleAC(["workspaceChats.update"])], async (request, response) => { try { const { id } = request.params; @@ -874,7 +901,7 @@ function workspaceEndpoints(app) { "/workspace/:slug/upload-and-embed", [ validatedRequest, - flexUserRoleValid([ROLES.admin, ROLES.manager]), + AccessManager.flexibleAC(["documents.upload", "workspace.embed"]), handleFileUpload, ], async function (request, response) { @@ -952,7 +979,7 @@ function workspaceEndpoints(app) { "/workspace/:slug/remove-and-unembed", [ validatedRequest, - flexUserRoleValid([ROLES.admin, ROLES.manager]), + AccessManager.flexibleAC(["documents.upload", "workspace.embed"]), handleFileUpload, ], async function (request, response) { @@ -979,7 +1006,11 @@ function workspaceEndpoints(app) { app.get( "/workspace/:slug/prompt-history", - [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug], + [ + validatedRequest, + AccessManager.flexibleAC(["promptHistory.read"]), + validWorkspaceSlug, + ], async (_, response) => { try { response.status(200).json({ @@ -998,7 +1029,7 @@ function workspaceEndpoints(app) { "/workspace/:slug/prompt-history", [ validatedRequest, - flexUserRoleValid([ROLES.admin, ROLES.manager]), + AccessManager.flexibleAC(["workspace.read", "promptHistory.delete"]), validWorkspaceSlug, ], async (_, response) => { @@ -1019,7 +1050,7 @@ function workspaceEndpoints(app) { "/workspace/prompt-history/:id", [ validatedRequest, - flexUserRoleValid([ROLES.admin, ROLES.manager]), + AccessManager.flexibleAC(["workspace.read", "promptHistory.delete"]), validWorkspaceSlug, ], async (request, response) => { diff --git a/server/index.js b/server/index.js index 4e79e8fdc31..4acca57e851 100644 --- a/server/index.js +++ b/server/index.js @@ -28,10 +28,12 @@ const { browserExtensionEndpoints } = require("./endpoints/browserExtension"); const { communityHubEndpoints } = require("./endpoints/communityHub"); const { agentFlowEndpoints } = require("./endpoints/agentFlows"); const { mcpServersEndpoints } = require("./endpoints/mcpServers"); +const AccessManager = require("./utils/AccessManager"); const app = express(); const apiRouter = express.Router(); const FILE_LIMIT = "3GB"; +AccessManager.loadRoles(); app.use(cors({ origin: true })); app.use(bodyParser.text({ limit: FILE_LIMIT })); app.use(bodyParser.json({ limit: FILE_LIMIT })); diff --git a/server/models/browserExtensionApiKey.js b/server/models/browserExtensionApiKey.js index 45759d98d12..342beb8d6a1 100644 --- a/server/models/browserExtensionApiKey.js +++ b/server/models/browserExtensionApiKey.js @@ -1,6 +1,6 @@ const prisma = require("../utils/prisma"); const { SystemSettings } = require("./systemSettings"); -const { ROLES } = require("../utils/middleware/multiUserProtected"); +const AccessManager = require("../utils/AccessManager"); const BrowserExtensionApiKey = { /** @@ -122,9 +122,13 @@ const BrowserExtensionApiKey = { limit = null, orderBy = null ) { - // Admin can view and use any keys - if ([ROLES.admin].includes(user.role)) - return await this.where(clause, limit, orderBy); + const acm = new AccessManager(); + const perms = acm.roles?.[user?.role]?.permissions || {}; + const canViewAll = acm.parsePermissionFromRole( + perms, + "browserExtensionKey.readAny" + ); + if (canViewAll) return await this.where(clause, limit, orderBy); try { const apiKeys = await prisma.browser_extension_api_keys.findMany({ diff --git a/server/models/user.js b/server/models/user.js index 35e8271bcfd..0811aec1717 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -1,5 +1,6 @@ const prisma = require("../utils/prisma"); const { EventLogs } = require("./eventLogs"); +const AccessManager = require("../utils/AccessManager"); /** * @typedef {Object} User @@ -37,7 +38,7 @@ const User = { } }, role: (role = "default") => { - const VALID_ROLES = ["default", "admin", "manager"]; + const VALID_ROLES = AccessManager.availableRolesAsArray(); if (!VALID_ROLES.includes(role)) { throw new Error( `Invalid role. Allowed roles are: ${VALID_ROLES.join(", ")}` @@ -309,8 +310,11 @@ const User = { * @returns {Promise} True if the user can send a chat, false otherwise. */ canSendChat: async function (user) { - const { ROLES } = require("../utils/middleware/multiUserProtected"); - if (!user || user.dailyMessageLimit === null || user.role === ROLES.admin) + if ( + !user || + user.dailyMessageLimit === null || + user.role === AccessManager.defaultRoles.admin + ) return true; const { WorkspaceChats } = require("./workspaceChats"); diff --git a/server/models/workspace.js b/server/models/workspace.js index c195b176143..89ea039a58f 100644 --- a/server/models/workspace.js +++ b/server/models/workspace.js @@ -2,10 +2,10 @@ const prisma = require("../utils/prisma"); const slugifyModule = require("slugify"); const { Document } = require("./documents"); const { WorkspaceUser } = require("./workspaceUsers"); -const { ROLES } = require("../utils/middleware/multiUserProtected"); const { v4: uuidv4 } = require("uuid"); const { User } = require("./user"); const { PromptHistory } = require("./promptHistory"); +const AccessManager = require("../utils/AccessManager"); function isNullOrNaN(value) { if (value === null) return true; @@ -259,8 +259,10 @@ const Workspace = { }, getWithUser: async function (user = null, clause = {}) { - if ([ROLES.admin, ROLES.manager].includes(user.role)) - return this.get(clause); + const acm = new AccessManager(); + const perms = acm.roles?.[user?.role]?.permissions || {}; + const canViewAll = acm.parsePermissionFromRole(perms, "workspace.readAny"); + if (canViewAll) return this.get(clause); try { const workspace = await prisma.workspaces.findFirst({ @@ -338,8 +340,10 @@ const Workspace = { limit = null, orderBy = null ) { - if ([ROLES.admin, ROLES.manager].includes(user.role)) - return await this.where(clause, limit, orderBy); + const acm = new AccessManager(); + const perms = acm.roles?.[user?.role]?.permissions || {}; + const canViewAll = acm.parsePermissionFromRole(perms, "workspace.readAny"); + if (canViewAll) return await this.where(clause, limit, orderBy); try { const workspaces = await prisma.workspaces.findMany({ diff --git a/server/package.json b/server/package.json index f226239d898..bb9d33124f2 100644 --- a/server/package.json +++ b/server/package.json @@ -97,6 +97,7 @@ "hermes-eslint": "^0.15.0", "node-html-markdown": "^1.3.0", "nodemon": "^2.0.22", + "object-deep-compare": "^2.0.0", "prettier": "^3.0.3" } -} \ No newline at end of file +} diff --git a/server/utils/AccessManager/defaults/admin.json b/server/utils/AccessManager/defaults/admin.json new file mode 100644 index 00000000000..f9ef54e6cb7 --- /dev/null +++ b/server/utils/AccessManager/defaults/admin.json @@ -0,0 +1,158 @@ +{ + "name": "admin", + "description": "Administrator with full system access", + "permissions": { + "system": { + "update": true, + "countVectors": true, + "branding": { + "update": true, + "delete": true + }, + "models": { + "read": true + } + }, + "systemSettings": { + "read": true, + "update": true + }, + "apiKeys": { + "create": true, + "read": true, + "delete": true + }, + "browserExtensionKey": { + "readAny": true, + "create": true, + "read": true, + "delete": true + }, + "workspace": { + "create": true, + "read": true, + "update": true, + "delete": true, + "chat": true, + "embed": true, + "resetVectorDb": true, + "documentPinStatus": true, + "readAny": true + }, + "workspaceThread": { + "create": true, + "read": true, + "update": true, + "delete": true + }, + "workspaceChats": { + "create": true, + "read": true, + "update": true, + "delete": true, + "export": true, + "playTTS": true + }, + "documents": { + "read": true, + "remove": true, + "createFolder": true, + "moveFiles": true, + "upload": true + }, + "documentSync": { + "read": true, + "update": true + }, + "users": { + "create": true, + "read": true, + "update": true, + "delete": true, + "pfp": { + "read": true, + "update": true, + "delete": true + } + }, + "chatEmbeds": { + "create": true, + "read": true, + "update": true, + "delete": true + }, + "chatEmbedChats": { + "read": true, + "delete": true + }, + "agentFlows": { + "create": true, + "read": true, + "update": true, + "delete": true + }, + "agent": { + "imported": { + "update": true, + "delete": true + } + }, + "roles": { + "create": true, + "read": true, + "update": true, + "delete": true + }, + "eventLogs": { + "read": true, + "delete": true + }, + "invite": { + "create": true, + "read": true, + "delete": true + }, + "promptHistory": { + "read": true, + "delete": true + }, + "slashCommands": { + "create": true, + "read": true, + "update": true, + "delete": true + }, + "systemPromptVariables": { + "create": true, + "read": true, + "update": true, + "delete": true + }, + "welcomeMessages": { + "read": true, + "update": true + }, + "workspaceSuggestedMessages": { + "read": true, + "update": true + }, + "workspaceUsers": { + "read": true, + "update": true + }, + "communityHub": { + "explore": true, + "viewItems": true, + "importItems": true + }, + "mcp": { + "forceReload": true, + "read": true, + "update": true, + "delete": true + }, + "collector": { + "useExtension": true + } + } +} diff --git a/server/utils/AccessManager/defaults/default.json b/server/utils/AccessManager/defaults/default.json new file mode 100644 index 00000000000..7339bb06f69 --- /dev/null +++ b/server/utils/AccessManager/defaults/default.json @@ -0,0 +1,158 @@ +{ + "name": "default", + "description": "Default user with lowest level of access", + "permissions": { + "system": { + "update": false, + "countVectors": false, + "branding": { + "update": false, + "delete": false + }, + "models": { + "read": false + } + }, + "systemSettings": { + "read": false, + "update": false + }, + "apiKeys": { + "create": false, + "read": false, + "delete": false + }, + "browserExtensionKey": { + "readAny": false, + "create": false, + "read": false, + "delete": false + }, + "workspace": { + "create": false, + "read": true, + "update": false, + "delete": false, + "chat": true, + "embed": false, + "resetVectorDb": false, + "documentPinStatus": false, + "readAny": false + }, + "workspaceThread": { + "create": true, + "read": true, + "update": true, + "delete": true + }, + "workspaceChats": { + "create": false, + "read": true, + "update": true, + "delete": true, + "export": false, + "playTTS": true + }, + "documents": { + "read": false, + "remove": false, + "createFolder": false, + "moveFiles": false, + "upload": false + }, + "documentSync": { + "read": false, + "update": false + }, + "users": { + "create": false, + "read": false, + "update": false, + "delete": false, + "pfp": { + "read": true, + "update": true, + "delete": true + } + }, + "chatEmbeds": { + "create": false, + "read": false, + "update": false, + "delete": false + }, + "chatEmbedChats": { + "read": false, + "delete": false + }, + "agentFlows": { + "create": false, + "read": false, + "update": false, + "delete": false + }, + "agent": { + "imported": { + "update": false, + "delete": false + } + }, + "roles": { + "create": false, + "read": false, + "update": false, + "delete": false + }, + "eventLogs": { + "read": false, + "delete": false + }, + "invite": { + "create": false, + "read": false, + "delete": false + }, + "promptHistory": { + "read": false, + "delete": false + }, + "slashCommands": { + "create": true, + "read": true, + "update": true, + "delete": true + }, + "systemPromptVariables": { + "create": false, + "read": false, + "update": false, + "delete": false + }, + "welcomeMessages": { + "read": true, + "update": false + }, + "workspaceSuggestedMessages": { + "read": true, + "update": false + }, + "workspaceUsers": { + "read": false, + "update": false + }, + "communityHub": { + "explore": false, + "viewItems": false, + "importItems": false + }, + "mcp": { + "forceReload": false, + "read": false, + "update": false, + "delete": false + }, + "collector": { + "useExtension": false + } + } +} \ No newline at end of file diff --git a/server/utils/AccessManager/defaults/manager.json b/server/utils/AccessManager/defaults/manager.json new file mode 100644 index 00000000000..b30231ef542 --- /dev/null +++ b/server/utils/AccessManager/defaults/manager.json @@ -0,0 +1,158 @@ +{ + "name": "manager", + "description": "Manager user with some basic access to the system for managing users and workspaces", + "permissions": { + "system": { + "update": false, + "countVectors": true, + "branding": { + "update": true, + "delete": true + }, + "models": { + "read": false + } + }, + "systemSettings": { + "read": true, + "update": true + }, + "apiKeys": { + "create": false, + "read": false, + "delete": false + }, + "browserExtensionKey": { + "readAny": false, + "create": true, + "read": true, + "delete": true + }, + "workspace": { + "create": true, + "read": true, + "update": true, + "delete": true, + "chat": true, + "embed": true, + "resetVectorDb": true, + "documentPinStatus": true, + "readAny": true + }, + "workspaceThread": { + "create": true, + "read": true, + "update": true, + "delete": true + }, + "workspaceChats": { + "create": true, + "read": true, + "update": true, + "delete": true, + "export": true, + "playTTS": true + }, + "documents": { + "read": true, + "remove": true, + "createFolder": true, + "moveFiles": true, + "upload": true + }, + "documentSync": { + "read": false, + "update": true + }, + "users": { + "create": true, + "read": true, + "update": true, + "delete": true, + "pfp": { + "read": true, + "update": true, + "delete": true + } + }, + "chatEmbeds": { + "create": false, + "read": false, + "update": false, + "delete": false + }, + "chatEmbedChats": { + "read": false, + "delete": false + }, + "agentFlows": { + "create": false, + "read": false, + "update": false, + "delete": false + }, + "agent": { + "imported": { + "update": false, + "delete": false + } + }, + "roles": { + "create": false, + "read": false, + "update": false, + "delete": false + }, + "eventLogs": { + "read": false, + "delete": false + }, + "invite": { + "create": true, + "read": true, + "delete": true + }, + "promptHistory": { + "read": true, + "delete": false + }, + "slashCommands": { + "create": true, + "read": true, + "update": true, + "delete": true + }, + "systemPromptVariables": { + "create": false, + "read": true, + "update": false, + "delete": false + }, + "welcomeMessages": { + "read": true, + "update": true + }, + "workspaceSuggestedMessages": { + "read": true, + "update": true + }, + "workspaceUsers": { + "read": true, + "update": true + }, + "communityHub": { + "explore": false, + "viewItems": false, + "importItems": false + }, + "mcp": { + "forceReload": false, + "read": false, + "update": false, + "delete": false + }, + "collector": { + "useExtension": true + } + } +} diff --git a/server/utils/AccessManager/index.js b/server/utils/AccessManager/index.js new file mode 100644 index 00000000000..11b9d99ac4f --- /dev/null +++ b/server/utils/AccessManager/index.js @@ -0,0 +1,259 @@ +const fs = require("fs"); +const path = require("path"); +const { getDefaultAccessSchema } = require("./schema"); + +class AccessManager { + static instance; + static defaultRoles = { + admin: "admin", + manager: "manager", + default: "default", + }; + + /** + * @typedef {Object} + * @property {admin: Object} - The admin system role + * @property {manager: Object} - The manager system role + * @property {default: Object} - The default system role + * @property {[string]: Object} - A custom role + */ + roles = {}; + + constructor() { + if (AccessManager.instance) return AccessManager.instance; + AccessManager.instance = this; + this.defaultAC = getDefaultAccessSchema(); + } + + /** + * Get the folder location of the roles definitions + * @returns {string} The path to the roles definitions folder + */ + get rolesFolder() { + const rolesDefinitionFolder = + process.env.NODE_ENV === "development" + ? path.resolve(__dirname, `../../storage/roles`) + : path.resolve( + process.env.STORAGE_DIR ?? path.resolve(__dirname, `../../storage`), + `roles` + ); + + if (!fs.existsSync(rolesDefinitionFolder)) + fs.mkdirSync(rolesDefinitionFolder, { recursive: true }); + return rolesDefinitionFolder; + } + + log(text, ...args) { + console.log(`\x1b[32m[AccessManager]\x1b[0m ${text}`, ...args); + } + + static loadRoles() { + const acm = new AccessManager(); + acm.loadRoles(); + } + + /** + * Get the available roles as an array of strings that are the names of the roles available + * @returns {string[]} The available roles + */ + static availableRolesAsArray() { + const acm = new AccessManager(); + return Object.keys(acm.roles); + } + + loadRoles() { + const { safeJsonParse } = require("../http"); + this.roles = { + admin: safeJsonParse( + fs.readFileSync(path.join(__dirname, "defaults", "admin.json"), { + encoding: "utf8", + }), + {} + ), + manager: safeJsonParse(fs.readFileSync(path.join(__dirname, "defaults", "manager.json"), { encoding: "utf8" }), {}), + default: safeJsonParse(fs.readFileSync(path.join(__dirname, "defaults", "default.json"), { encoding: "utf8" }), {}), + }; + + // Custom roles + for (const definitionFile of fs.readdirSync(this.rolesFolder)) { + const role = safeJsonParse( + fs.readFileSync(path.join(this.rolesFolder, definitionFile), "utf8"), + null + ); + if (!role) { + this.log( + `Skipping custom role ${definitionFile} because it is not a valid JSON file` + ); + continue; + } + + if (role.name in AccessManager.defaultRoles) { + this.log( + `Skipping custom role ${role.name} because it is attempting to override a default role` + ); + continue; + } + + this.roles[role.name] = role; + } + + this.log( + `Loaded ${Object.keys(this.roles).length} roles into ACM`, + Object.keys(this.roles) + ); + } + + /** + * Validates the access control rule path that is being evaluated + * + * @notice This function will crash the server if the rule path is invalid + * This is intentional since these permissions are being evaluated at runtime + * and during development you should know if you have defined an invalid permission + * path so that you can fix it before committing your changes. + * + * @param {string} rulePath - The access control rule path + * @returns {boolean} - True if the rule path is valid, false otherwise + */ + isValidRule(rulePath = "") { + // If we are not in development mode, we will not validate the rule path + // since permissions cannot be modified at runtime in production + if (process.env.NODE_ENV !== "development") return true; + + const permPath = rulePath.split("."); + if (permPath.length === 0) + return this.log(`Provided rule path dot notation is empty: ${rulePath}`); + + let rolePermission = this.defaultAC.permissions; + for (const subPerm of permPath) { + if (rolePermission.hasOwnProperty(subPerm)) + rolePermission = rolePermission[subPerm]; + else break; + } + + if ( + rolePermission === undefined || + !["boolean", "string", "number"].includes(typeof rolePermission) + ) + throw new Error(`Invalid access control rule path: ${rulePath}`); + + return true; + } + + parsePermissionFromRole(rolePermissions = {}, rulePath = "") { + !this.isValidRule(rulePath); + + const permPath = rulePath.split("."); + if (permPath.length === 0) return false; + + let rolePermission = rolePermissions; + for (const subPerm of permPath) { + if (rolePermission.hasOwnProperty(subPerm)) + rolePermission = rolePermission[subPerm]; + else break; + } + + // Ending the evaluation if the permission should be a boolean, string, or number + if ( + rolePermission === undefined || + !["boolean", "string", "number"].includes(typeof rolePermission) + ) + return false; + + return rolePermission; + } + + /** + * @description Strict access control middleware. + * @param {string[]} permissionSet - The permissions to check + * @returns {function} - The middleware function + */ + static strictAC(permissionSet = []) { + return async (request, response, next) => { + const { SystemSettings } = require("../../models/systemSettings"); + const { userFromSession } = require("../http"); + const acm = new AccessManager(); + + const multiUserMode = + response.locals?.multiUserMode ?? + (await SystemSettings.isMultiUserMode()); + if (!multiUserMode) return response.sendStatus(401).end(); + + const user = + response.locals?.user ?? (await userFromSession(request, response)); + if (!user) return response.sendStatus(401).end(); + + const userRole = user?.role; + if (!userRole) return response.sendStatus(401).end(); + + const rolePermissions = acm.roles[userRole]?.permissions; + if (!rolePermissions) return response.sendStatus(401).end(); + + const permEvals = {}; + for (const rule of permissionSet) { + const canDo = acm.parsePermissionFromRole(rolePermissions, rule); + permEvals[rule] = canDo; + } + + if (Object.values(permEvals).every((evalResult) => evalResult === true)) + next(); + else { + console.log( + `User ${user.username} ${userRole} does not have permission to do the all following: ${permissionSet.join(", ")}`, + permEvals + ); + return response.sendStatus(401).end(); + } + }; + } + + /** + * @description Flexible access control middleware. + * This middleware will only check permissions if multi-user mode is enabled. + * This helper is useful for dual-support of single-user and multi-user endpoints. + * @param {string[]} permissionSet - The permissions to check + * @returns {function} - The middleware function + */ + static flexibleAC(permissionSet = []) { + return async (request, response, next) => { + const { SystemSettings } = require("../../models/systemSettings"); + const { userFromSession } = require("../http"); + const acm = new AccessManager(); + + // If multi-user mode is not enabled, we will not check permissions + const multiUserMode = + response.locals?.multiUserMode ?? + (await SystemSettings.isMultiUserMode()); + if (!multiUserMode) return next(); + + // Otherwise, we will check the permissions for the user as if they are in + // strict mode + const user = + response.locals?.user ?? (await userFromSession(request, response)); + if (!user) return response.sendStatus(401).end(); + + const userRole = user?.role; + if (!userRole) return response.sendStatus(401).end(); + + const rolePermissions = acm.roles[userRole]?.permissions; + if (!rolePermissions) return response.sendStatus(401).end(); + + const permEvals = {}; + for (const rule of permissionSet) { + const canDo = acm.parsePermissionFromRole(rolePermissions, rule); + permEvals[rule] = canDo; + } + + if (Object.values(permEvals).every((evalResult) => evalResult === true)) + next(); + else { + console.log( + `User ${user.username} ${userRole} does not have permission to do the all following: ${permissionSet.join(", ")}`, + permEvals + ); + return response.sendStatus(401).end(); + } + }; + } +} + +module.exports = AccessManager; diff --git a/server/utils/AccessManager/schema.js b/server/utils/AccessManager/schema.js new file mode 100644 index 00000000000..176f07aa061 --- /dev/null +++ b/server/utils/AccessManager/schema.js @@ -0,0 +1,148 @@ +const CRUD_DEFAULTS = { + create: "boolean", + read: "boolean", + update: "boolean", + delete: "boolean", +}; + +const ACCESS_SCHEMA = { + name: "string", + description: "string", + permissions: { + system: { + update: "boolean", + countVectors: "boolean", + branding: { + update: "boolean", + delete: "boolean", + }, + models: { + read: "boolean", + }, + }, + systemSettings: { + read: "boolean", + update: "boolean", + }, + apiKeys: { + create: "boolean", + read: "boolean", + delete: "boolean", + }, + browserExtensionKey: { + readAny: "boolean", + read: "boolean", + create: "boolean", + delete: "boolean", + }, + workspace: { + ...CRUD_DEFAULTS, + chat: true, + embed: "boolean", + resetVectorDb: "boolean", + documentPinStatus: "boolean", + readAny: "boolean", + }, + workspaceThread: { + ...CRUD_DEFAULTS, + }, + workspaceChats: { + ...CRUD_DEFAULTS, + export: "boolean", + playTTS: "boolean", + }, + documents: { + read: "boolean", + remove: "boolean", + createFolder: "boolean", + moveFiles: "boolean", + upload: "boolean", + }, + documentSync: { + read: "boolean", + update: "boolean", + }, + users: { + ...CRUD_DEFAULTS, + pfp: { + read: "boolean", + update: "boolean", + delete: "boolean", + }, + }, + chatEmbeds: { + ...CRUD_DEFAULTS, + }, + chatEmbedChats: { + read: "boolean", + delete: "boolean", + // export? + }, + agentFlows: { + ...CRUD_DEFAULTS, + }, + agent: { + imported: { + update: "boolean", + delete: "boolean", + }, + }, + roles: { + ...CRUD_DEFAULTS, + }, + eventLogs: { + read: "boolean", + delete: "boolean", + }, + invite: { + create: "boolean", + read: "boolean", + delete: "boolean", + }, + promptHistory: { + read: "boolean", + delete: "boolean", + }, + slashCommands: { + ...CRUD_DEFAULTS, + }, + systemPromptVariables: { + ...CRUD_DEFAULTS, + }, + welcomeMessages: { + read: "boolean", + update: "boolean", + }, + workspaceSuggestedMessages: { + read: "boolean", + update: "boolean", + }, + workspaceUsers: { + read: "boolean", + update: "boolean", + }, + communityHub: { + explore: "boolean", + viewItems: "boolean", + importItems: "boolean", + }, + mcp: { + forceReload: "boolean", + read: "boolean", + update: "boolean", + delete: "boolean", + }, + collector: { + useExtension: "boolean", + }, + }, +}; + +const getDefaultAccessSchema = () => JSON.parse( + JSON.stringify(ACCESS_SCHEMA) + .replaceAll("boolean", false) + .replaceAll("string", "") + .replaceAll("number", 0) +); + +module.exports = { ACCESS_SCHEMA, getDefaultAccessSchema }; diff --git a/server/utils/helpers/admin/index.js b/server/utils/helpers/admin/index.js index c114417dc8b..b51a42368cf 100644 --- a/server/utils/helpers/admin/index.js +++ b/server/utils/helpers/admin/index.js @@ -1,5 +1,13 @@ const { User } = require("../../../models/user"); -const { ROLES } = require("../../middleware/multiUserProtected"); +const AccessManager = require("../../../utils/AccessManager"); + +/** + * TODO we need some way for ACM to be aware of the current user's role as well as + * understanding the "heirarchy" of roles for default to custom roles. + * + * For now, we will just check if the current user is an admin or manager and if so, + * we will allow them to update the role of the user being updated. + */ // When a user is updating or creating a user in multi-user, we need to check if they // are allowed to do this and that the new or existing user will be at or below their permission level. @@ -7,9 +15,14 @@ const { ROLES } = require("../../middleware/multiUserProtected"); function validRoleSelection(currentUser = {}, newUserParams = {}) { if (!newUserParams.hasOwnProperty("role")) return { valid: true, error: null }; // not updating role, so skip. - if (currentUser.role === ROLES.admin) return { valid: true, error: null }; - if (currentUser.role === ROLES.manager) { - const validRoles = [ROLES.manager, ROLES.default]; + + if (currentUser.role === AccessManager.defaultRoles.admin) + return { valid: true, error: null }; + if (currentUser.role === AccessManager.defaultRoles.manager) { + const validRoles = [ + AccessManager.defaultRoles.manager, + AccessManager.defaultRoles.default, + ]; if (!validRoles.includes(newUserParams.role)) return { valid: false, error: "Invalid role selection for user." }; return { valid: true, error: null }; @@ -25,10 +38,13 @@ async function canModifyAdmin(userToModify, updates) { // or the updates role is equal to the users current role. // skip validation. if (!updates.hasOwnProperty("role")) return { valid: true, error: null }; - if (userToModify.role !== ROLES.admin) return { valid: true, error: null }; + if (userToModify.role !== AccessManager.defaultRoles.admin) + return { valid: true, error: null }; if (updates.role === userToModify.role) return { valid: true, error: null }; - const adminCount = await User.count({ role: ROLES.admin }); + const adminCount = await User.count({ + role: AccessManager.defaultRoles.admin, + }); if (adminCount - 1 <= 0) return { valid: false, @@ -38,9 +54,13 @@ async function canModifyAdmin(userToModify, updates) { } function validCanModify(currentUser, existingUser) { - if (currentUser.role === ROLES.admin) return { valid: true, error: null }; - if (currentUser.role === ROLES.manager) { - const validRoles = [ROLES.manager, ROLES.default]; + if (currentUser.role === AccessManager.defaultRoles.admin) + return { valid: true, error: null }; + if (currentUser.role === AccessManager.defaultRoles.manager) { + const validRoles = [ + AccessManager.defaultRoles.manager, + AccessManager.defaultRoles.default, + ]; if (!validRoles.includes(existingUser.role)) return { valid: false, error: "Cannot perform that action on user." }; return { valid: true, error: null }; diff --git a/server/utils/middleware/multiUserProtected.js b/server/utils/middleware/multiUserProtected.js index cf7e58cfe1b..453d9e45966 100644 --- a/server/utils/middleware/multiUserProtected.js +++ b/server/utils/middleware/multiUserProtected.js @@ -1,75 +1,4 @@ const { SystemSettings } = require("../../models/systemSettings"); -const { userFromSession } = require("../http"); -const ROLES = { - all: "", - admin: "admin", - manager: "manager", - default: "default", -}; -const DEFAULT_ROLES = [ROLES.admin, ROLES.admin]; - -/** - * Explicitly check that multi user mode is enabled as well as that the - * requesting user has the appropriate role to modify or call the URL. - * @param {string[]} allowedRoles - The roles that are allowed to access the route - * @returns {function} - */ -function strictMultiUserRoleValid(allowedRoles = DEFAULT_ROLES) { - return async (request, response, next) => { - // If the access-control is allowable for all - skip validations and continue; - if (allowedRoles.includes(ROLES.all)) { - next(); - return; - } - - const multiUserMode = - response.locals?.multiUserMode ?? - (await SystemSettings.isMultiUserMode()); - if (!multiUserMode) return response.sendStatus(401).end(); - - const user = - response.locals?.user ?? (await userFromSession(request, response)); - if (allowedRoles.includes(user?.role)) { - next(); - return; - } - return response.sendStatus(401).end(); - }; -} - -/** - * Apply role permission checks IF the current system is in multi-user mode. - * This is relevant for routes that are shared between MUM and single-user mode. - * @param {string[]} allowedRoles - The roles that are allowed to access the route - * @returns {function} - */ -function flexUserRoleValid(allowedRoles = DEFAULT_ROLES) { - return async (request, response, next) => { - // If the access-control is allowable for all - skip validations and continue; - // It does not matter if multi-user or not. - if (allowedRoles.includes(ROLES.all)) { - next(); - return; - } - - // Bypass if not in multi-user mode - const multiUserMode = - response.locals?.multiUserMode ?? - (await SystemSettings.isMultiUserMode()); - if (!multiUserMode) { - next(); - return; - } - - const user = - response.locals?.user ?? (await userFromSession(request, response)); - if (allowedRoles.includes(user?.role)) { - next(); - return; - } - return response.sendStatus(401).end(); - }; -} // Middleware check on a public route if the instance is in a valid // multi-user set up. @@ -87,8 +16,5 @@ async function isMultiUserSetup(_request, response, next) { } module.exports = { - ROLES, - strictMultiUserRoleValid, - flexUserRoleValid, isMultiUserSetup, }; diff --git a/server/utils/vectorDbProviders/pgvector/SETUP.md b/server/utils/vectorDbProviders/pgvector/SETUP.md index 0281b8afc54..d70cd393764 100644 --- a/server/utils/vectorDbProviders/pgvector/SETUP.md +++ b/server/utils/vectorDbProviders/pgvector/SETUP.md @@ -123,5 +123,3 @@ Then, you will need to create the extension on the database. This can be done by psql CREATE EXTENSION vector; ``` - - diff --git a/server/yarn.lock b/server/yarn.lock index 34da5c41658..a9f909211ac 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -6222,6 +6222,11 @@ object-assign@^4, object-assign@^4.1.1: resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== +object-deep-compare@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/object-deep-compare/-/object-deep-compare-2.0.0.tgz#ca07d91b25674e210e106029da7f094415154fc1" + integrity sha512-bIu+4D1j36NtcclajmXyhReqNx3xicvaSHZgNYEnIVI3g+5ksPwQVqBFHmDK/9FeDL2cNNQHrM01fFKzWcgqIQ== + object-inspect@^1.13.1: version "1.13.1" resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz"