θΏ™ζ˜―indexlocζδΎ›ηš„ζœεŠ‘οΌŒδΈθ¦θΎ“ε…₯任何密码
Skip to content

Custom Roles & Perms - RBAC #3799

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions extras/scripts/validateRoles.mjs
Original file line number Diff line number Diff line change
@@ -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}`);
}
}
}
43 changes: 19 additions & 24 deletions server/endpoints/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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();
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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();
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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 = {};
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand All @@ -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({});
Expand All @@ -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);
Expand All @@ -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;
Expand Down
Loading