diff --git a/api/models/Role.spec.js b/api/models/Role.spec.js index c344f719dd..deac4e5c35 100644 --- a/api/models/Role.spec.js +++ b/api/models/Role.spec.js @@ -46,7 +46,7 @@ describe('updateAccessPermissions', () => { [PermissionTypes.PROMPTS]: { CREATE: true, USE: true, - SHARED_GLOBAL: false, + SHARE: false, }, }, }).save(); @@ -55,7 +55,7 @@ describe('updateAccessPermissions', () => { [PermissionTypes.PROMPTS]: { CREATE: true, USE: true, - SHARED_GLOBAL: true, + SHARE: true, }, }); @@ -63,7 +63,7 @@ describe('updateAccessPermissions', () => { expect(updatedRole.permissions[PermissionTypes.PROMPTS]).toEqual({ CREATE: true, USE: true, - SHARED_GLOBAL: true, + SHARE: true, }); }); @@ -74,7 +74,7 @@ describe('updateAccessPermissions', () => { [PermissionTypes.PROMPTS]: { CREATE: true, USE: true, - SHARED_GLOBAL: false, + SHARE: false, }, }, }).save(); @@ -83,7 +83,7 @@ describe('updateAccessPermissions', () => { [PermissionTypes.PROMPTS]: { CREATE: true, USE: true, - SHARED_GLOBAL: false, + SHARE: false, }, }); @@ -91,7 +91,7 @@ describe('updateAccessPermissions', () => { expect(updatedRole.permissions[PermissionTypes.PROMPTS]).toEqual({ CREATE: true, USE: true, - SHARED_GLOBAL: false, + SHARE: false, }); }); @@ -110,20 +110,20 @@ describe('updateAccessPermissions', () => { [PermissionTypes.PROMPTS]: { CREATE: true, USE: true, - SHARED_GLOBAL: false, + SHARE: false, }, }, }).save(); await updateAccessPermissions(SystemRoles.USER, { - [PermissionTypes.PROMPTS]: { SHARED_GLOBAL: true }, + [PermissionTypes.PROMPTS]: { SHARE: true }, }); const updatedRole = await getRoleByName(SystemRoles.USER); expect(updatedRole.permissions[PermissionTypes.PROMPTS]).toEqual({ CREATE: true, USE: true, - SHARED_GLOBAL: true, + SHARE: true, }); }); @@ -134,7 +134,7 @@ describe('updateAccessPermissions', () => { [PermissionTypes.PROMPTS]: { CREATE: true, USE: true, - SHARED_GLOBAL: false, + SHARE: false, }, }, }).save(); @@ -147,7 +147,7 @@ describe('updateAccessPermissions', () => { expect(updatedRole.permissions[PermissionTypes.PROMPTS]).toEqual({ CREATE: true, USE: false, - SHARED_GLOBAL: false, + SHARE: false, }); }); @@ -155,13 +155,13 @@ describe('updateAccessPermissions', () => { await new Role({ name: SystemRoles.USER, permissions: { - [PermissionTypes.PROMPTS]: { CREATE: true, USE: true, SHARED_GLOBAL: false }, + [PermissionTypes.PROMPTS]: { CREATE: true, USE: true, SHARE: false }, [PermissionTypes.BOOKMARKS]: { USE: true }, }, }).save(); await updateAccessPermissions(SystemRoles.USER, { - [PermissionTypes.PROMPTS]: { USE: false, SHARED_GLOBAL: true }, + [PermissionTypes.PROMPTS]: { USE: false, SHARE: true }, [PermissionTypes.BOOKMARKS]: { USE: false }, }); @@ -169,7 +169,7 @@ describe('updateAccessPermissions', () => { expect(updatedRole.permissions[PermissionTypes.PROMPTS]).toEqual({ CREATE: true, USE: false, - SHARED_GLOBAL: true, + SHARE: true, }); expect(updatedRole.permissions[PermissionTypes.BOOKMARKS]).toEqual({ USE: false }); }); @@ -178,19 +178,19 @@ describe('updateAccessPermissions', () => { await new Role({ name: SystemRoles.USER, permissions: { - [PermissionTypes.PROMPTS]: { CREATE: true, USE: true, SHARED_GLOBAL: false }, + [PermissionTypes.PROMPTS]: { CREATE: true, USE: true, SHARE: false }, }, }).save(); await updateAccessPermissions(SystemRoles.USER, { - [PermissionTypes.PROMPTS]: { USE: false, SHARED_GLOBAL: true }, + [PermissionTypes.PROMPTS]: { USE: false, SHARE: true }, }); const updatedRole = await getRoleByName(SystemRoles.USER); expect(updatedRole.permissions[PermissionTypes.PROMPTS]).toEqual({ CREATE: true, USE: false, - SHARED_GLOBAL: true, + SHARE: true, }); }); @@ -214,13 +214,13 @@ describe('updateAccessPermissions', () => { await new Role({ name: SystemRoles.USER, permissions: { - [PermissionTypes.PROMPTS]: { CREATE: true, USE: true, SHARED_GLOBAL: false }, + [PermissionTypes.PROMPTS]: { CREATE: true, USE: true, SHARE: false }, [PermissionTypes.MULTI_CONVO]: { USE: false }, }, }).save(); await updateAccessPermissions(SystemRoles.USER, { - [PermissionTypes.PROMPTS]: { SHARED_GLOBAL: true }, + [PermissionTypes.PROMPTS]: { SHARE: true }, [PermissionTypes.MULTI_CONVO]: { USE: true }, }); @@ -228,7 +228,7 @@ describe('updateAccessPermissions', () => { expect(updatedRole.permissions[PermissionTypes.PROMPTS]).toEqual({ CREATE: true, USE: true, - SHARED_GLOBAL: true, + SHARE: true, }); expect(updatedRole.permissions[PermissionTypes.MULTI_CONVO]).toEqual({ USE: true }); }); @@ -271,7 +271,7 @@ describe('initializeRoles', () => { }); // Example: Check default values for ADMIN role - expect(adminRole.permissions[PermissionTypes.PROMPTS].SHARED_GLOBAL).toBe(true); + expect(adminRole.permissions[PermissionTypes.PROMPTS].SHARE).toBe(true); expect(adminRole.permissions[PermissionTypes.BOOKMARKS].USE).toBe(true); expect(adminRole.permissions[PermissionTypes.AGENTS].CREATE).toBe(true); }); @@ -283,7 +283,7 @@ describe('initializeRoles', () => { [PermissionTypes.PROMPTS]: { [Permissions.USE]: false, [Permissions.CREATE]: true, - [Permissions.SHARED_GLOBAL]: true, + [Permissions.SHARE]: true, }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false }, }, @@ -320,7 +320,7 @@ describe('initializeRoles', () => { expect(userRole.permissions[PermissionTypes.AGENTS]).toBeDefined(); expect(userRole.permissions[PermissionTypes.AGENTS].CREATE).toBeDefined(); expect(userRole.permissions[PermissionTypes.AGENTS].USE).toBeDefined(); - expect(userRole.permissions[PermissionTypes.AGENTS].SHARED_GLOBAL).toBeDefined(); + expect(userRole.permissions[PermissionTypes.AGENTS].SHARE).toBeDefined(); }); it('should handle multiple runs without duplicating or modifying data', async () => { @@ -348,7 +348,7 @@ describe('initializeRoles', () => { [PermissionTypes.PROMPTS]: { [Permissions.USE]: false, [Permissions.CREATE]: false, - [Permissions.SHARED_GLOBAL]: false, + [Permissions.SHARE]: false, }, [PermissionTypes.BOOKMARKS]: roleDefaults[SystemRoles.ADMIN].permissions[PermissionTypes.BOOKMARKS], @@ -365,7 +365,7 @@ describe('initializeRoles', () => { expect(adminRole.permissions[PermissionTypes.AGENTS]).toBeDefined(); expect(adminRole.permissions[PermissionTypes.AGENTS].CREATE).toBeDefined(); expect(adminRole.permissions[PermissionTypes.AGENTS].USE).toBeDefined(); - expect(adminRole.permissions[PermissionTypes.AGENTS].SHARED_GLOBAL).toBeDefined(); + expect(adminRole.permissions[PermissionTypes.AGENTS].SHARE).toBeDefined(); }); it('should include MULTI_CONVO permissions when creating default roles', async () => { diff --git a/api/server/middleware/accessResources/canAccessAgentResource.spec.js b/api/server/middleware/accessResources/canAccessAgentResource.spec.js index e3dca73bd2..1106390e72 100644 --- a/api/server/middleware/accessResources/canAccessAgentResource.spec.js +++ b/api/server/middleware/accessResources/canAccessAgentResource.spec.js @@ -29,7 +29,7 @@ describe('canAccessAgentResource middleware', () => { AGENTS: { USE: true, CREATE: true, - SHARED_GLOBAL: false, + SHARE: true, }, }, }); diff --git a/api/server/middleware/accessResources/canAccessMCPServerResource.spec.js b/api/server/middleware/accessResources/canAccessMCPServerResource.spec.js index 5eef1438ff..075cddb000 100644 --- a/api/server/middleware/accessResources/canAccessMCPServerResource.spec.js +++ b/api/server/middleware/accessResources/canAccessMCPServerResource.spec.js @@ -26,10 +26,10 @@ describe('canAccessMCPServerResource middleware', () => { await Role.create({ name: 'test-role', permissions: { - MCPSERVERS: { + MCP_SERVERS: { USE: true, CREATE: true, - SHARED_GLOBAL: false, + SHARE: true, }, }, }); diff --git a/api/server/middleware/accessResources/fileAccess.spec.js b/api/server/middleware/accessResources/fileAccess.spec.js index de7c7d50f6..cc0d57ac48 100644 --- a/api/server/middleware/accessResources/fileAccess.spec.js +++ b/api/server/middleware/accessResources/fileAccess.spec.js @@ -32,7 +32,7 @@ describe('fileAccess middleware', () => { AGENTS: { USE: true, CREATE: true, - SHARED_GLOBAL: false, + SHARE: true, }, }, }); diff --git a/api/server/middleware/checkSharePublicAccess.js b/api/server/middleware/checkSharePublicAccess.js new file mode 100644 index 0000000000..c094d54acb --- /dev/null +++ b/api/server/middleware/checkSharePublicAccess.js @@ -0,0 +1,84 @@ +const { logger } = require('@librechat/data-schemas'); +const { ResourceType, PermissionTypes, Permissions } = require('librechat-data-provider'); +const { getRoleByName } = require('~/models/Role'); + +/** + * Maps resource types to their corresponding permission types + */ +const resourceToPermissionType = { + [ResourceType.AGENT]: PermissionTypes.AGENTS, + [ResourceType.PROMPTGROUP]: PermissionTypes.PROMPTS, + [ResourceType.MCPSERVER]: PermissionTypes.MCP_SERVERS, +}; + +/** + * Middleware to check if user has SHARE_PUBLIC permission for a resource type + * Only enforced when request body contains `public: true` + * @param {import('express').Request} req - Express request + * @param {import('express').Response} res - Express response + * @param {import('express').NextFunction} next - Express next function + */ +const checkSharePublicAccess = async (req, res, next) => { + try { + const { public: isPublic } = req.body; + + // Only check if trying to enable public sharing + if (!isPublic) { + return next(); + } + + const user = req.user; + if (!user || !user.role) { + return res.status(401).json({ + error: 'Unauthorized', + message: 'Authentication required', + }); + } + + const { resourceType } = req.params; + const permissionType = resourceToPermissionType[resourceType]; + + if (!permissionType) { + return res.status(400).json({ + error: 'Bad Request', + message: `Unsupported resource type for public sharing: ${resourceType}`, + }); + } + + const role = await getRoleByName(user.role); + if (!role || !role.permissions) { + return res.status(403).json({ + error: 'Forbidden', + message: 'No permissions configured for user role', + }); + } + + const resourcePerms = role.permissions[permissionType] || {}; + const canSharePublic = resourcePerms[Permissions.SHARE_PUBLIC] === true; + + if (!canSharePublic) { + logger.warn( + `[checkSharePublicAccess][${user.id}] User denied SHARE_PUBLIC for ${resourceType}`, + ); + return res.status(403).json({ + error: 'Forbidden', + message: `You do not have permission to share ${resourceType} resources publicly`, + }); + } + + next(); + } catch (error) { + logger.error( + `[checkSharePublicAccess][${req.user?.id}] Error checking SHARE_PUBLIC permission`, + error, + ); + return res.status(500).json({ + error: 'Internal Server Error', + message: 'Failed to check public sharing permissions', + }); + } +}; + +module.exports = { + checkSharePublicAccess, +}; diff --git a/api/server/middleware/checkSharePublicAccess.spec.js b/api/server/middleware/checkSharePublicAccess.spec.js new file mode 100644 index 0000000000..c73e71693b --- /dev/null +++ b/api/server/middleware/checkSharePublicAccess.spec.js @@ -0,0 +1,164 @@ +const { ResourceType, PermissionTypes, Permissions } = require('librechat-data-provider'); +const { checkSharePublicAccess } = require('./checkSharePublicAccess'); +const { getRoleByName } = require('~/models/Role'); + +jest.mock('~/models/Role'); + +describe('checkSharePublicAccess middleware', () => { + let mockReq; + let mockRes; + let mockNext; + + beforeEach(() => { + jest.clearAllMocks(); + mockReq = { + user: { id: 'user123', role: 'USER' }, + params: { resourceType: ResourceType.AGENT }, + body: {}, + }; + mockRes = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + mockNext = jest.fn(); + }); + + it('should call next() when public is not true', async () => { + mockReq.body = { public: false }; + + await checkSharePublicAccess(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalled(); + expect(mockRes.status).not.toHaveBeenCalled(); + }); + + it('should call next() when public is undefined', async () => { + mockReq.body = { updated: [] }; + + await checkSharePublicAccess(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalled(); + expect(mockRes.status).not.toHaveBeenCalled(); + }); + + it('should return 401 when user is not authenticated', async () => { + mockReq.body = { public: true }; + mockReq.user = null; + + await checkSharePublicAccess(mockReq, mockRes, mockNext); + + expect(mockRes.status).toHaveBeenCalledWith(401); + expect(mockRes.json).toHaveBeenCalledWith({ + error: 'Unauthorized', + message: 'Authentication required', + }); + expect(mockNext).not.toHaveBeenCalled(); + }); + + it('should return 403 when user role has no SHARE_PUBLIC permission for agents', async () => { + mockReq.body = { public: true }; + mockReq.params = { resourceType: ResourceType.AGENT }; + getRoleByName.mockResolvedValue({ + permissions: { + [PermissionTypes.AGENTS]: { + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: false, + }, + }, + }); + + await checkSharePublicAccess(mockReq, mockRes, mockNext); + + expect(mockRes.status).toHaveBeenCalledWith(403); + expect(mockRes.json).toHaveBeenCalledWith({ + error: 'Forbidden', + message: `You do not have permission to share ${ResourceType.AGENT} resources publicly`, + }); + expect(mockNext).not.toHaveBeenCalled(); + }); + + it('should call next() when user has SHARE_PUBLIC permission for agents', async () => { + mockReq.body = { public: true }; + mockReq.params = { resourceType: ResourceType.AGENT }; + getRoleByName.mockResolvedValue({ + permissions: { + [PermissionTypes.AGENTS]: { + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, + }, + }, + }); + + await checkSharePublicAccess(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalled(); + expect(mockRes.status).not.toHaveBeenCalled(); + }); + + it('should check prompts permission for promptgroup resource type', async () => { + mockReq.body = { public: true }; + mockReq.params = { resourceType: ResourceType.PROMPTGROUP }; + getRoleByName.mockResolvedValue({ + permissions: { + [PermissionTypes.PROMPTS]: { + [Permissions.SHARE_PUBLIC]: true, + }, + }, + }); + + await checkSharePublicAccess(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalled(); + }); + + it('should check mcp_servers permission for mcpserver resource type', async () => { + mockReq.body = { public: true }; + mockReq.params = { resourceType: ResourceType.MCPSERVER }; + getRoleByName.mockResolvedValue({ + permissions: { + [PermissionTypes.MCP_SERVERS]: { + [Permissions.SHARE_PUBLIC]: true, + }, + }, + }); + + await checkSharePublicAccess(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalled(); + }); + + it('should return 400 for unsupported resource type', async () => { + mockReq.body = { public: true }; + mockReq.params = { resourceType: 'unsupported' }; + + await checkSharePublicAccess(mockReq, mockRes, mockNext); + + expect(mockRes.status).toHaveBeenCalledWith(400); + expect(mockRes.json).toHaveBeenCalledWith({ + error: 'Bad Request', + message: 'Unsupported resource type for public sharing: unsupported', + }); + }); + + it('should return 403 when role has no permissions object', async () => { + mockReq.body = { public: true }; + getRoleByName.mockResolvedValue({ permissions: null }); + + await checkSharePublicAccess(mockReq, mockRes, mockNext); + + expect(mockRes.status).toHaveBeenCalledWith(403); + }); + + it('should return 500 on error', async () => { + mockReq.body = { public: true }; + getRoleByName.mockRejectedValue(new Error('Database error')); + + await checkSharePublicAccess(mockReq, mockRes, mockNext); + + expect(mockRes.status).toHaveBeenCalledWith(500); + expect(mockRes.json).toHaveBeenCalledWith({ + error: 'Internal Server Error', + message: 'Failed to check public sharing permissions', + }); + }); +}); diff --git a/api/server/middleware/roles/access.spec.js b/api/server/middleware/roles/access.spec.js index fe8d77a4f5..9de840819d 100644 --- a/api/server/middleware/roles/access.spec.js +++ b/api/server/middleware/roles/access.spec.js @@ -51,9 +51,9 @@ describe('Access Middleware', () => { permissions: { [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true }, [PermissionTypes.PROMPTS]: { - [Permissions.SHARED_GLOBAL]: false, [Permissions.USE]: true, [Permissions.CREATE]: true, + [Permissions.SHARE]: true, }, [PermissionTypes.MEMORIES]: { [Permissions.USE]: true, @@ -65,7 +65,7 @@ describe('Access Middleware', () => { [PermissionTypes.AGENTS]: { [Permissions.USE]: true, [Permissions.CREATE]: false, - [Permissions.SHARED_GLOBAL]: false, + [Permissions.SHARE]: false, }, [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, @@ -79,9 +79,9 @@ describe('Access Middleware', () => { permissions: { [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true }, [PermissionTypes.PROMPTS]: { - [Permissions.SHARED_GLOBAL]: true, [Permissions.USE]: true, [Permissions.CREATE]: true, + [Permissions.SHARE]: true, }, [PermissionTypes.MEMORIES]: { [Permissions.USE]: true, @@ -93,7 +93,7 @@ describe('Access Middleware', () => { [PermissionTypes.AGENTS]: { [Permissions.USE]: true, [Permissions.CREATE]: true, - [Permissions.SHARED_GLOBAL]: true, + [Permissions.SHARE]: true, }, [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, @@ -110,7 +110,7 @@ describe('Access Middleware', () => { [PermissionTypes.AGENTS]: { [Permissions.USE]: false, [Permissions.CREATE]: false, - [Permissions.SHARED_GLOBAL]: false, + [Permissions.SHARE]: false, }, // Has permissions for other types [PermissionTypes.PROMPTS]: { @@ -241,7 +241,7 @@ describe('Access Middleware', () => { req: {}, user: { id: 'admin123', role: 'admin' }, permissionType: PermissionTypes.AGENTS, - permissions: [Permissions.SHARED_GLOBAL], + permissions: [Permissions.SHARE], getRoleByName, }); expect(shareResult).toBe(true); @@ -318,7 +318,7 @@ describe('Access Middleware', () => { const middleware = generateCheckAccess({ permissionType: PermissionTypes.AGENTS, - permissions: [Permissions.USE, Permissions.CREATE, Permissions.SHARED_GLOBAL], + permissions: [Permissions.USE, Permissions.CREATE, Permissions.SHARE], getRoleByName, }); await middleware(req, res, next); @@ -349,7 +349,7 @@ describe('Access Middleware', () => { [PermissionTypes.AGENTS]: { [Permissions.USE]: false, [Permissions.CREATE]: false, - [Permissions.SHARED_GLOBAL]: false, + [Permissions.SHARE]: false, }, }, }); diff --git a/api/server/routes/accessPermissions.js b/api/server/routes/accessPermissions.js index 3e70f2610f..2cfd19289d 100644 --- a/api/server/routes/accessPermissions.js +++ b/api/server/routes/accessPermissions.js @@ -10,6 +10,7 @@ const { } = require('~/server/controllers/PermissionsController'); const { requireJwtAuth, checkBan, uaParser, canAccessResource } = require('~/server/middleware'); const { checkPeoplePickerAccess } = require('~/server/middleware/checkPeoplePickerAccess'); +const { checkSharePublicAccess } = require('~/server/middleware/checkSharePublicAccess'); const { findMCPServerById } = require('~/models'); const router = express.Router(); @@ -91,10 +92,12 @@ router.get( * PUT /api/permissions/{resourceType}/{resourceId} * Bulk update permissions for a specific resource * SECURITY: Requires SHARE permission to modify resource permissions + * SECURITY: Requires SHARE_PUBLIC permission to enable public sharing */ router.put( '/:resourceType/:resourceId', checkResourcePermissionAccess(PermissionBits.SHARE), + checkSharePublicAccess, updateResourcePermissions, ); diff --git a/api/server/routes/agents/v1.js b/api/server/routes/agents/v1.js index 1e4f1c0118..682a9c795f 100644 --- a/api/server/routes/agents/v1.js +++ b/api/server/routes/agents/v1.js @@ -25,7 +25,7 @@ const checkGlobalAgentShare = generateCheckAccess({ permissionType: PermissionTypes.AGENTS, permissions: [Permissions.USE, Permissions.CREATE], bodyProps: { - [Permissions.SHARED_GLOBAL]: ['projectIds', 'removeProjectIds'], + [Permissions.SHARE]: ['projectIds', 'removeProjectIds'], }, getRoleByName, }); diff --git a/api/server/routes/prompts.js b/api/server/routes/prompts.js index c833719075..037bf04813 100644 --- a/api/server/routes/prompts.js +++ b/api/server/routes/prompts.js @@ -60,7 +60,7 @@ const checkGlobalPromptShare = generateCheckAccess({ permissionType: PermissionTypes.PROMPTS, permissions: [Permissions.USE, Permissions.CREATE], bodyProps: { - [Permissions.SHARED_GLOBAL]: ['projectIds', 'removeProjectIds'], + [Permissions.SHARE]: ['projectIds', 'removeProjectIds'], }, getRoleByName, }); diff --git a/api/server/routes/prompts.test.js b/api/server/routes/prompts.test.js index 1aeca1c93c..caeb90ddfb 100644 --- a/api/server/routes/prompts.test.js +++ b/api/server/routes/prompts.test.js @@ -159,7 +159,7 @@ async function setupTestData() { case SystemRoles.USER: return { permissions: { PROMPTS: { USE: true, CREATE: true } } }; case SystemRoles.ADMIN: - return { permissions: { PROMPTS: { USE: true, CREATE: true, SHARED_GLOBAL: true } } }; + return { permissions: { PROMPTS: { USE: true, CREATE: true, SHARE: true } } }; default: return null; } diff --git a/client/src/components/Prompts/AdminSettings.tsx b/client/src/components/Prompts/AdminSettings.tsx index 6a5d6a7152..6d382fbb91 100644 --- a/client/src/components/Prompts/AdminSettings.tsx +++ b/client/src/components/Prompts/AdminSettings.tsx @@ -8,9 +8,10 @@ import { useLocalize } from '~/hooks'; import type { PermissionConfig } from '~/components/ui'; const permissions: PermissionConfig[] = [ - { permission: Permissions.SHARED_GLOBAL, labelKey: 'com_ui_prompts_allow_share' }, - { permission: Permissions.CREATE, labelKey: 'com_ui_prompts_allow_create' }, { permission: Permissions.USE, labelKey: 'com_ui_prompts_allow_use' }, + { permission: Permissions.CREATE, labelKey: 'com_ui_prompts_allow_create' }, + { permission: Permissions.SHARE, labelKey: 'com_ui_prompts_allow_share' }, + { permission: Permissions.SHARE_PUBLIC, labelKey: 'com_ui_prompts_allow_share_public' }, ]; const AdminSettings = () => { diff --git a/client/src/components/Prompts/PromptForm.tsx b/client/src/components/Prompts/PromptForm.tsx index cde24dbc2a..6f575f8577 100644 --- a/client/src/components/Prompts/PromptForm.tsx +++ b/client/src/components/Prompts/PromptForm.tsx @@ -65,7 +65,7 @@ const RightPanel = React.memo( const editorMode = useRecoilValue(store.promptsEditorMode); const hasShareAccess = useHasAccess({ permissionType: PermissionTypes.PROMPTS, - permission: Permissions.SHARED_GLOBAL, + permission: Permissions.SHARE, }); const updateGroupMutation = useUpdatePromptGroup({ diff --git a/client/src/components/Prompts/SharePrompt.tsx b/client/src/components/Prompts/SharePrompt.tsx index aeb9543140..65e8f20f24 100644 --- a/client/src/components/Prompts/SharePrompt.tsx +++ b/client/src/components/Prompts/SharePrompt.tsx @@ -16,10 +16,10 @@ const SharePrompt = React.memo( ({ group, disabled }: { group?: TPromptGroup; disabled: boolean }) => { const { user } = useAuthContext(); - // Check if user has permission to share prompts globally + // Check if user has permission to share prompts const hasAccessToSharePrompts = useHasAccess({ permissionType: PermissionTypes.PROMPTS, - permission: Permissions.SHARED_GLOBAL, + permission: Permissions.SHARE, }); // Check user's permissions on this specific promptGroup diff --git a/client/src/components/Sharing/GenericGrantAccessDialog.tsx b/client/src/components/Sharing/GenericGrantAccessDialog.tsx index d3c5547313..d9270ef2d3 100644 --- a/client/src/components/Sharing/GenericGrantAccessDialog.tsx +++ b/client/src/components/Sharing/GenericGrantAccessDialog.tsx @@ -18,6 +18,7 @@ import { usePeoplePickerPermissions, useResourcePermissionState, useCopyToClipboard, + useCanSharePublic, useLocalize, } from '~/hooks'; import UnifiedPeopleSearch from './PeoplePicker/UnifiedPeopleSearch'; @@ -33,6 +34,7 @@ export default function GenericGrantAccessDialog({ resourceType, onGrantAccess, disabled = false, + buttonClassName, children, }: { resourceDbId?: string | null; @@ -41,15 +43,19 @@ export default function GenericGrantAccessDialog({ resourceType: ResourceType; onGrantAccess?: (shares: TPrincipal[], isPublic: boolean, publicRole?: AccessRoleIds) => void; disabled?: boolean; + buttonClassName?: string; children?: React.ReactNode; }) { const localize = useLocalize(); const { showToast } = useToastContext(); - const [isModalOpen, setIsModalOpen] = useState(false); const [isCopying, setIsCopying] = useState(false); - - // Use shared hooks + const [isModalOpen, setIsModalOpen] = useState(false); + const canSharePublic = useCanSharePublic(resourceType); const { hasPeoplePickerAccess, peoplePickerTypeFilter } = usePeoplePickerPermissions(); + + /** User can use the share dialog if they have people picker access OR can share publicly */ + const canUseShareDialog = hasPeoplePickerAccess || canSharePublic; + const { config, permissionsData, @@ -65,7 +71,7 @@ export default function GenericGrantAccessDialog({ setPublicRole, } = useResourcePermissionState(resourceType, resourceDbId, isModalOpen); - // State for unified list of all shares (existing + newly added) + /** State for unified list of all shares (existing + newly added) */ const [allShares, setAllShares] = useState([]); const [hasChanges, setHasChanges] = useState(false); const [defaultPermissionId, setDefaultPermissionId] = useState( @@ -88,6 +94,11 @@ export default function GenericGrantAccessDialog({ return null; } + // Don't render if user has no useful sharing permissions + if (!canUseShareDialog) { + return null; + } + if (!config) { console.error(`Unsupported resource type: ${resourceType}`); return null; @@ -238,11 +249,11 @@ export default function GenericGrantAccessDialog({ })} type="button" disabled={disabled} - className="h-full" + className={cn('h-9', buttonClassName)} >
- {totalCurrentShares > 0 && (
-
+ {canSharePublic && ( + <> +
- {/* Public Access Section */} - + {/* Public Access Section */} + + + )} {/* Footer Actions */}
diff --git a/client/src/components/Sharing/PeoplePickerAdminSettings.tsx b/client/src/components/Sharing/PeoplePickerAdminSettings.tsx index 7f6530d1f7..38d44ad311 100644 --- a/client/src/components/Sharing/PeoplePickerAdminSettings.tsx +++ b/client/src/components/Sharing/PeoplePickerAdminSettings.tsx @@ -57,9 +57,9 @@ const LabelController: React.FC = ({ render={({ field }) => ( )} diff --git a/client/src/components/SidePanel/Agents/AdminSettings.tsx b/client/src/components/SidePanel/Agents/AdminSettings.tsx index 85ceb1875d..d0872f32aa 100644 --- a/client/src/components/SidePanel/Agents/AdminSettings.tsx +++ b/client/src/components/SidePanel/Agents/AdminSettings.tsx @@ -6,9 +6,10 @@ import { useLocalize } from '~/hooks'; import type { PermissionConfig } from '~/components/ui'; const permissions: PermissionConfig[] = [ - { permission: Permissions.SHARED_GLOBAL, labelKey: 'com_ui_agents_allow_share' }, - { permission: Permissions.CREATE, labelKey: 'com_ui_agents_allow_create' }, { permission: Permissions.USE, labelKey: 'com_ui_agents_allow_use' }, + { permission: Permissions.CREATE, labelKey: 'com_ui_agents_allow_create' }, + { permission: Permissions.SHARE, labelKey: 'com_ui_agents_allow_share' }, + { permission: Permissions.SHARE_PUBLIC, labelKey: 'com_ui_agents_allow_share_public' }, ]; const AdminSettings = () => { diff --git a/client/src/components/SidePanel/Agents/AgentFooter.tsx b/client/src/components/SidePanel/Agents/AgentFooter.tsx index 7fe90a7cc5..80a449bb2d 100644 --- a/client/src/components/SidePanel/Agents/AgentFooter.tsx +++ b/client/src/components/SidePanel/Agents/AgentFooter.tsx @@ -42,7 +42,7 @@ export default function AgentFooter({ const agent_id = useWatch({ control, name: 'id' }); const hasAccessToShareAgents = useHasAccess({ permissionType: PermissionTypes.AGENTS, - permission: Permissions.SHARED_GLOBAL, + permission: Permissions.SHARE, }); const { hasPermission, isLoading: permissionsLoading } = useResourcePermissions( ResourceType.AGENT, diff --git a/client/src/components/SidePanel/MCPBuilder/MCPAdminSettings.tsx b/client/src/components/SidePanel/MCPBuilder/MCPAdminSettings.tsx index dcba33640e..af677999bc 100644 --- a/client/src/components/SidePanel/MCPBuilder/MCPAdminSettings.tsx +++ b/client/src/components/SidePanel/MCPBuilder/MCPAdminSettings.tsx @@ -9,6 +9,7 @@ const permissions: PermissionConfig[] = [ { permission: Permissions.USE, labelKey: 'com_ui_mcp_servers_allow_use' }, { permission: Permissions.CREATE, labelKey: 'com_ui_mcp_servers_allow_create' }, { permission: Permissions.SHARE, labelKey: 'com_ui_mcp_servers_allow_share' }, + { permission: Permissions.SHARE_PUBLIC, labelKey: 'com_ui_mcp_servers_allow_share_public' }, ]; const MCPAdminSettings = () => { diff --git a/client/src/hooks/Sharing/index.ts b/client/src/hooks/Sharing/index.ts index 164cb2c05b..dec520ec3a 100644 --- a/client/src/hooks/Sharing/index.ts +++ b/client/src/hooks/Sharing/index.ts @@ -1,2 +1,3 @@ export { usePeoplePickerPermissions } from './usePeoplePickerPermissions'; export { useResourcePermissionState } from './useResourcePermissionState'; +export { useCanSharePublic } from './useCanSharePublic'; diff --git a/client/src/hooks/Sharing/useCanSharePublic.ts b/client/src/hooks/Sharing/useCanSharePublic.ts new file mode 100644 index 0000000000..699ccc9e73 --- /dev/null +++ b/client/src/hooks/Sharing/useCanSharePublic.ts @@ -0,0 +1,22 @@ +import { ResourceType, PermissionTypes, Permissions } from 'librechat-data-provider'; +import { useHasAccess } from '~/hooks'; + +const resourceToPermissionMap: Record = { + [ResourceType.AGENT]: PermissionTypes.AGENTS, + [ResourceType.PROMPTGROUP]: PermissionTypes.PROMPTS, + [ResourceType.MCPSERVER]: PermissionTypes.MCP_SERVERS, +}; + +/** + * Hook to check if a user can share a specific resource type publicly (with everyone) + * @param resourceType The type of resource to check public sharing permission for + * @returns boolean indicating if the user can share the resource publicly + */ +export const useCanSharePublic = (resourceType: ResourceType): boolean => { + const permissionType = resourceToPermissionMap[resourceType]; + const hasAccess = useHasAccess({ + permissionType, + permission: Permissions.SHARE_PUBLIC, + }); + return hasAccess; +}; diff --git a/client/src/hooks/Sharing/usePeoplePickerPermissions.ts b/client/src/hooks/Sharing/usePeoplePickerPermissions.ts index d8579d37a7..be1b6d5bff 100644 --- a/client/src/hooks/Sharing/usePeoplePickerPermissions.ts +++ b/client/src/hooks/Sharing/usePeoplePickerPermissions.ts @@ -4,6 +4,7 @@ import { useHasAccess } from '~/hooks'; /** * Hook to check people picker permissions and return the appropriate type filter + * Note: SHARE_PUBLIC is now per-resource type (AGENTS, PROMPTS, MCP_SERVERS), not on PEOPLE_PICKER * @returns Object with permission states and type filter */ export const usePeoplePickerPermissions = () => { diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index c144e8bda5..aba6ea9fb0 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -699,6 +699,7 @@ "com_ui_agents": "Agents", "com_ui_agents_allow_create": "Allow creating Agents", "com_ui_agents_allow_share": "Allow sharing Agents", + "com_ui_agents_allow_share_public": "Allow sharing Agents publicly", "com_ui_agents_allow_use": "Allow using Agents", "com_ui_all": "all", "com_ui_all_proper": "All", @@ -1088,6 +1089,7 @@ "com_ui_mcp_servers": "MCP Servers", "com_ui_mcp_servers_allow_create": "Allow users to create MCP servers", "com_ui_mcp_servers_allow_share": "Allow users to share MCP servers", + "com_ui_mcp_servers_allow_share_public": "Allow users to share MCP servers publicly", "com_ui_mcp_servers_allow_use": "Allow users to use MCP servers", "com_ui_mcp_title_invalid": "Title can only contain letters, numbers, and spaces", "com_ui_mcp_transport": "Transport", @@ -1207,6 +1209,7 @@ "com_ui_prompts": "Prompts", "com_ui_prompts_allow_create": "Allow creating Prompts", "com_ui_prompts_allow_share": "Allow sharing Prompts", + "com_ui_prompts_allow_share_public": "Allow sharing Prompts publicly", "com_ui_prompts_allow_use": "Allow using Prompts", "com_ui_provider": "Provider", "com_ui_quality": "Quality", diff --git a/librechat.example.yaml b/librechat.example.yaml index 7b7ad9d521..c90ab6592a 100644 --- a/librechat.example.yaml +++ b/librechat.example.yaml @@ -85,10 +85,16 @@ interface: parameters: true sidePanel: true presets: true - prompts: true + prompts: + use: true + share: false + public: false bookmarks: true multiConvo: true - agents: true + agents: + use: true + share: false + public: false peoplePicker: users: true groups: true @@ -102,9 +108,11 @@ interface: # - use: Allow users to use configured MCP servers # - create: Allow users to create and manage new MCP servers # - share: Allow users to share MCP servers with other users + # - public: Allow users to share MCP servers publicly (with everyone) use: false - create: false share: false + create: false + public: false # Creation / edit MCP server config Dialog config example # trustCheckbox: # label: diff --git a/packages/api/src/app/permissions.spec.ts b/packages/api/src/app/permissions.spec.ts index 9890ad5299..b84ad63498 100644 --- a/packages/api/src/app/permissions.spec.ts +++ b/packages/api/src/app/permissions.spec.ts @@ -17,11 +17,19 @@ describe('updateInterfacePermissions - permissions', () => { it('should call updateAccessPermissions with the correct parameters when permission types are true', async () => { const config = { interface: { - prompts: true, + prompts: { + use: true, + share: false, + public: false, + }, bookmarks: true, memories: true, multiConvo: true, - agents: true, + agents: { + use: true, + share: false, + public: false, + }, temporaryChat: true, runCode: true, webSearch: true, @@ -35,6 +43,12 @@ describe('updateInterfacePermissions - permissions', () => { marketplace: { use: true, }, + mcpServers: { + use: true, + create: true, + share: false, + public: false, + }, }, }; const configDefaults = { interface: {} } as TConfigDefaults; @@ -50,6 +64,9 @@ describe('updateInterfacePermissions - permissions', () => { const expectedPermissionsForUser = { [PermissionTypes.PROMPTS]: { [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true }, [PermissionTypes.MEMORIES]: { @@ -62,6 +79,9 @@ describe('updateInterfacePermissions - permissions', () => { [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true }, [PermissionTypes.AGENTS]: { [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: true }, @@ -78,12 +98,16 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.USE]: true, [Permissions.CREATE]: true, [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, }, }; const expectedPermissionsForAdmin = { [PermissionTypes.PROMPTS]: { [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true }, [PermissionTypes.MEMORIES]: { @@ -96,6 +120,9 @@ describe('updateInterfacePermissions - permissions', () => { [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true }, [PermissionTypes.AGENTS]: { [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: true }, @@ -111,7 +138,8 @@ describe('updateInterfacePermissions - permissions', () => { [PermissionTypes.MCP_SERVERS]: { [Permissions.USE]: true, [Permissions.CREATE]: true, - [Permissions.SHARE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, }, }; @@ -135,11 +163,19 @@ describe('updateInterfacePermissions - permissions', () => { it('should call updateAccessPermissions with false when permission types are false', async () => { const config = { interface: { - prompts: false, + prompts: { + use: false, + share: false, + public: false, + }, bookmarks: false, memories: false, multiConvo: false, - agents: false, + agents: { + use: false, + share: false, + public: false, + }, temporaryChat: false, runCode: false, webSearch: false, @@ -153,6 +189,12 @@ describe('updateInterfacePermissions - permissions', () => { marketplace: { use: false, }, + mcpServers: { + use: true, + create: true, + share: false, + public: false, + }, }, }; const configDefaults = { interface: {} } as TConfigDefaults; @@ -168,6 +210,9 @@ describe('updateInterfacePermissions - permissions', () => { const expectedPermissionsForUser = { [PermissionTypes.PROMPTS]: { [Permissions.USE]: false, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false }, [PermissionTypes.MEMORIES]: { @@ -180,6 +225,9 @@ describe('updateInterfacePermissions - permissions', () => { [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: false }, [PermissionTypes.AGENTS]: { [Permissions.USE]: false, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: false }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: false }, @@ -196,12 +244,16 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.USE]: true, [Permissions.CREATE]: true, [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, }, }; const expectedPermissionsForAdmin = { [PermissionTypes.PROMPTS]: { [Permissions.USE]: false, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false }, [PermissionTypes.MEMORIES]: { @@ -214,6 +266,9 @@ describe('updateInterfacePermissions - permissions', () => { [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: false }, [PermissionTypes.AGENTS]: { [Permissions.USE]: false, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: false }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: false }, @@ -229,7 +284,8 @@ describe('updateInterfacePermissions - permissions', () => { [PermissionTypes.MCP_SERVERS]: { [Permissions.USE]: true, [Permissions.CREATE]: true, - [Permissions.SHARE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, }, }; @@ -286,6 +342,9 @@ describe('updateInterfacePermissions - permissions', () => { const expectedPermissionsForUser = { [PermissionTypes.PROMPTS]: { [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true }, [PermissionTypes.MEMORIES]: { @@ -298,6 +357,9 @@ describe('updateInterfacePermissions - permissions', () => { [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true }, [PermissionTypes.AGENTS]: { [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: true }, @@ -314,12 +376,16 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.USE]: true, [Permissions.CREATE]: true, [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, }, }; const expectedPermissionsForAdmin = { [PermissionTypes.PROMPTS]: { [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true }, [PermissionTypes.MEMORIES]: { @@ -332,6 +398,9 @@ describe('updateInterfacePermissions - permissions', () => { [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true }, [PermissionTypes.AGENTS]: { [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: true }, @@ -348,6 +417,7 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.USE]: true, [Permissions.CREATE]: true, [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, }, }; @@ -417,6 +487,9 @@ describe('updateInterfacePermissions - permissions', () => { const expectedPermissionsForUser = { [PermissionTypes.PROMPTS]: { [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false }, [PermissionTypes.MEMORIES]: { @@ -429,6 +502,9 @@ describe('updateInterfacePermissions - permissions', () => { [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true }, [PermissionTypes.AGENTS]: { [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: false }, @@ -445,12 +521,16 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.USE]: true, [Permissions.CREATE]: true, [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, }, }; const expectedPermissionsForAdmin = { [PermissionTypes.PROMPTS]: { [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false }, [PermissionTypes.MEMORIES]: { @@ -463,6 +543,9 @@ describe('updateInterfacePermissions - permissions', () => { [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true }, [PermissionTypes.AGENTS]: { [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: false }, @@ -479,6 +562,7 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.USE]: true, [Permissions.CREATE]: true, [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, }, }; @@ -535,6 +619,9 @@ describe('updateInterfacePermissions - permissions', () => { const expectedPermissionsForUser = { [PermissionTypes.PROMPTS]: { [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true }, [PermissionTypes.MEMORIES]: { @@ -547,6 +634,9 @@ describe('updateInterfacePermissions - permissions', () => { [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true }, [PermissionTypes.AGENTS]: { [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: true }, @@ -563,12 +653,16 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.USE]: true, [Permissions.CREATE]: true, [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, }, }; const expectedPermissionsForAdmin = { [PermissionTypes.PROMPTS]: { [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true }, [PermissionTypes.MEMORIES]: { @@ -581,6 +675,9 @@ describe('updateInterfacePermissions - permissions', () => { [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true }, [PermissionTypes.AGENTS]: { [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, }, [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, [PermissionTypes.RUN_CODE]: { [Permissions.USE]: true }, @@ -597,6 +694,7 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.USE]: true, [Permissions.CREATE]: true, [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, }, }; @@ -684,6 +782,7 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.USE]: true, [Permissions.CREATE]: true, [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, }, }; @@ -712,6 +811,7 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.USE]: true, [Permissions.CREATE]: true, [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, }, }; @@ -790,6 +890,9 @@ describe('updateInterfacePermissions - permissions', () => { const expectedPermissionsForUser = { [PermissionTypes.PROMPTS]: { [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, }, // Explicitly configured // All other permissions that don't exist in the database [PermissionTypes.MEMORIES]: { @@ -815,12 +918,16 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.USE]: true, [Permissions.CREATE]: true, [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, }, }; const expectedPermissionsForAdmin = { [PermissionTypes.PROMPTS]: { [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, }, // Explicitly configured // All other permissions that don't exist in the database [PermissionTypes.MEMORIES]: { @@ -846,6 +953,7 @@ describe('updateInterfacePermissions - permissions', () => { [Permissions.USE]: true, [Permissions.CREATE]: true, [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, }, }; @@ -1016,19 +1124,31 @@ describe('updateInterfacePermissions - permissions', () => { // Check PROMPTS permissions use role defaults expect(userCall[1][PermissionTypes.PROMPTS]).toEqual({ [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, }); expect(adminCall[1][PermissionTypes.PROMPTS]).toEqual({ [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, }); // Check AGENTS permissions use role defaults expect(userCall[1][PermissionTypes.AGENTS]).toEqual({ [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, }); expect(adminCall[1][PermissionTypes.AGENTS]).toEqual({ [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, }); // Check MEMORIES permissions use role defaults @@ -1258,6 +1378,9 @@ describe('updateInterfacePermissions - permissions', () => { // Explicitly configured permissions should be updated expect(userCall[1][PermissionTypes.PROMPTS]).toEqual({ [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, }); expect(userCall[1][PermissionTypes.BOOKMARKS]).toEqual({ [Permissions.USE]: true }); expect(userCall[1][PermissionTypes.MARKETPLACE]).toEqual({ [Permissions.USE]: true }); @@ -1579,7 +1702,12 @@ describe('updateInterfacePermissions - permissions', () => { // Memory permissions should be updated even though they already exist expect(userCall[1][PermissionTypes.MEMORIES]).toEqual(expectedMemoryPermissions); // Prompts should be updated (explicitly configured) - expect(userCall[1][PermissionTypes.PROMPTS]).toEqual({ [Permissions.USE]: true }); + expect(userCall[1][PermissionTypes.PROMPTS]).toEqual({ + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: false, + [Permissions.SHARE_PUBLIC]: false, + }); // Bookmarks should be updated (explicitly configured) expect(userCall[1][PermissionTypes.BOOKMARKS]).toEqual({ [Permissions.USE]: true }); @@ -1589,7 +1717,12 @@ describe('updateInterfacePermissions - permissions', () => { ); // Memory permissions should be updated even though they already exist expect(adminCall[1][PermissionTypes.MEMORIES]).toEqual(expectedMemoryPermissions); - expect(adminCall[1][PermissionTypes.PROMPTS]).toEqual({ [Permissions.USE]: true }); + expect(adminCall[1][PermissionTypes.PROMPTS]).toEqual({ + [Permissions.USE]: true, + [Permissions.CREATE]: true, + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, + }); expect(adminCall[1][PermissionTypes.BOOKMARKS]).toEqual({ [Permissions.USE]: true }); // Verify the existing role data was passed to updateAccessPermissions diff --git a/packages/api/src/app/permissions.ts b/packages/api/src/app/permissions.ts index eb93cd4e7d..bfa49e6bbd 100644 --- a/packages/api/src/app/permissions.ts +++ b/packages/api/src/app/permissions.ts @@ -141,12 +141,52 @@ export async function updateInterfacePermissions({ } }; + // Helper to extract value from boolean or object config + const getConfigUse = ( + config: boolean | { use?: boolean; share?: boolean; public?: boolean } | undefined, + ) => (typeof config === 'boolean' ? config : config?.use); + const getConfigShare = ( + config: boolean | { use?: boolean; share?: boolean; public?: boolean } | undefined, + ) => (typeof config === 'boolean' ? undefined : config?.share); + const getConfigPublic = ( + config: boolean | { use?: boolean; share?: boolean; public?: boolean } | undefined, + ) => (typeof config === 'boolean' ? undefined : config?.public); + + // Get default use values (for backward compat when config is boolean) + const promptsDefaultUse = + typeof defaults.prompts === 'boolean' ? defaults.prompts : defaults.prompts?.use; + const agentsDefaultUse = + typeof defaults.agents === 'boolean' ? defaults.agents : defaults.agents?.use; + const promptsDefaultShare = + typeof defaults.prompts === 'object' ? defaults.prompts?.share : undefined; + const agentsDefaultShare = + typeof defaults.agents === 'object' ? defaults.agents?.share : undefined; + const promptsDefaultPublic = + typeof defaults.prompts === 'object' ? defaults.prompts?.public : undefined; + const agentsDefaultPublic = + typeof defaults.agents === 'object' ? defaults.agents?.public : undefined; + const allPermissions: Partial>> = { [PermissionTypes.PROMPTS]: { [Permissions.USE]: getPermissionValue( - loadedInterface.prompts, + getConfigUse(loadedInterface.prompts), defaultPerms[PermissionTypes.PROMPTS]?.[Permissions.USE], - defaults.prompts, + promptsDefaultUse, + ), + [Permissions.CREATE]: getPermissionValue( + undefined, + defaultPerms[PermissionTypes.PROMPTS]?.[Permissions.CREATE], + true, + ), + [Permissions.SHARE]: getPermissionValue( + getConfigShare(loadedInterface.prompts), + defaultPerms[PermissionTypes.PROMPTS]?.[Permissions.SHARE], + promptsDefaultShare, + ), + [Permissions.SHARE_PUBLIC]: getPermissionValue( + getConfigPublic(loadedInterface.prompts), + defaultPerms[PermissionTypes.PROMPTS]?.[Permissions.SHARE_PUBLIC], + promptsDefaultPublic, ), }, [PermissionTypes.BOOKMARKS]: { @@ -194,9 +234,24 @@ export async function updateInterfacePermissions({ }, [PermissionTypes.AGENTS]: { [Permissions.USE]: getPermissionValue( - loadedInterface.agents, + getConfigUse(loadedInterface.agents), defaultPerms[PermissionTypes.AGENTS]?.[Permissions.USE], - defaults.agents, + agentsDefaultUse, + ), + [Permissions.CREATE]: getPermissionValue( + undefined, + defaultPerms[PermissionTypes.AGENTS]?.[Permissions.CREATE], + true, + ), + [Permissions.SHARE]: getPermissionValue( + getConfigShare(loadedInterface.agents), + defaultPerms[PermissionTypes.AGENTS]?.[Permissions.SHARE], + agentsDefaultShare, + ), + [Permissions.SHARE_PUBLIC]: getPermissionValue( + getConfigPublic(loadedInterface.agents), + defaultPerms[PermissionTypes.AGENTS]?.[Permissions.SHARE_PUBLIC], + agentsDefaultPublic, ), }, [PermissionTypes.TEMPORARY_CHAT]: { @@ -274,6 +329,11 @@ export async function updateInterfacePermissions({ defaultPerms[PermissionTypes.MCP_SERVERS]?.[Permissions.SHARE], defaults.mcpServers?.share, ), + [Permissions.SHARE_PUBLIC]: getPermissionValue( + loadedInterface.mcpServers?.public, + defaultPerms[PermissionTypes.MCP_SERVERS]?.[Permissions.SHARE_PUBLIC], + defaults.mcpServers?.public, + ), }, }; diff --git a/packages/api/src/middleware/access.spec.ts b/packages/api/src/middleware/access.spec.ts index 7731957259..d7ca690c48 100644 --- a/packages/api/src/middleware/access.spec.ts +++ b/packages/api/src/middleware/access.spec.ts @@ -209,7 +209,7 @@ describe('access middleware', () => { permissions: { [PermissionTypes.AGENTS]: { [Permissions.USE]: true, - [Permissions.SHARED_GLOBAL]: false, + [Permissions.SHARE]: false, }, }, } as unknown as IRole; @@ -223,9 +223,9 @@ describe('access middleware', () => { const result = await checkAccess({ ...defaultParams, - permissions: [Permissions.USE, Permissions.SHARED_GLOBAL], + permissions: [Permissions.USE, Permissions.SHARE], bodyProps: { - [Permissions.SHARED_GLOBAL]: ['projectIds', 'removeProjectIds'], + [Permissions.SHARE]: ['projectIds', 'removeProjectIds'], } as Record, checkObject, }); @@ -237,7 +237,7 @@ describe('access middleware', () => { name: 'user', permissions: { [PermissionTypes.AGENTS]: { - [Permissions.SHARED_GLOBAL]: false, + [Permissions.SHARE]: false, }, }, } as unknown as IRole; @@ -251,9 +251,9 @@ describe('access middleware', () => { const result = await checkAccess({ ...defaultParams, - permissions: [Permissions.SHARED_GLOBAL], + permissions: [Permissions.SHARE], bodyProps: { - [Permissions.SHARED_GLOBAL]: ['projectIds', 'removeProjectIds'], + [Permissions.SHARE]: ['projectIds', 'removeProjectIds'], } as Record, checkObject, }); @@ -337,7 +337,7 @@ describe('access middleware', () => { [PermissionTypes.AGENTS]: { [Permissions.USE]: true, [Permissions.CREATE]: true, - [Permissions.SHARED_GLOBAL]: false, + [Permissions.SHARE]: false, }, }, } as unknown as IRole; @@ -350,9 +350,9 @@ describe('access middleware', () => { const middleware = generateCheckAccess({ permissionType: PermissionTypes.AGENTS, - permissions: [Permissions.USE, Permissions.CREATE, Permissions.SHARED_GLOBAL], + permissions: [Permissions.USE, Permissions.CREATE, Permissions.SHARE], bodyProps: { - [Permissions.SHARED_GLOBAL]: ['projectIds', 'removeProjectIds'], + [Permissions.SHARE]: ['projectIds', 'removeProjectIds'], } as Record, getRoleByName: mockGetRoleByName, }); @@ -490,7 +490,7 @@ describe('access middleware', () => { [PermissionTypes.PROMPTS]: { [Permissions.USE]: true, [Permissions.CREATE]: true, - [Permissions.SHARED_GLOBAL]: false, + [Permissions.SHARE]: false, }, }, } as unknown as IRole; diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index d7b44d3c80..ebfcfa93f1 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -587,6 +587,7 @@ const mcpServersSchema = z use: z.boolean().optional(), create: z.boolean().optional(), share: z.boolean().optional(), + public: z.boolean().optional(), trustCheckbox: z .object({ label: localizedStringSchema.optional(), @@ -617,8 +618,26 @@ export const interfaceSchema = z bookmarks: z.boolean().optional(), memories: z.boolean().optional(), presets: z.boolean().optional(), - prompts: z.boolean().optional(), - agents: z.boolean().optional(), + prompts: z + .union([ + z.boolean(), + z.object({ + use: z.boolean().optional(), + share: z.boolean().optional(), + public: z.boolean().optional(), + }), + ]) + .optional(), + agents: z + .union([ + z.boolean(), + z.object({ + use: z.boolean().optional(), + share: z.boolean().optional(), + public: z.boolean().optional(), + }), + ]) + .optional(), temporaryChat: z.boolean().optional(), temporaryChatRetention: z.number().min(1).max(8760).optional(), runCode: z.boolean().optional(), @@ -647,8 +666,16 @@ export const interfaceSchema = z multiConvo: true, bookmarks: true, memories: true, - prompts: true, - agents: true, + prompts: { + use: true, + share: false, + public: false, + }, + agents: { + use: true, + share: false, + public: false, + }, temporaryChat: true, runCode: true, webSearch: true, @@ -664,6 +691,7 @@ export const interfaceSchema = z use: true, create: true, share: false, + public: false, }, fileSearch: true, fileCitations: true, diff --git a/packages/data-provider/src/permissions.ts b/packages/data-provider/src/permissions.ts index b5c90aadeb..0d53dbe29b 100644 --- a/packages/data-provider/src/permissions.ts +++ b/packages/data-provider/src/permissions.ts @@ -62,7 +62,6 @@ export enum PermissionTypes { * Enum for Role-Based Access Control Constants */ export enum Permissions { - SHARED_GLOBAL = 'SHARED_GLOBAL', USE = 'USE', CREATE = 'CREATE', UPDATE = 'UPDATE', @@ -74,13 +73,15 @@ export enum Permissions { VIEW_USERS = 'VIEW_USERS', VIEW_GROUPS = 'VIEW_GROUPS', VIEW_ROLES = 'VIEW_ROLES', + /** Can share resources publicly (with everyone) */ + SHARE_PUBLIC = 'SHARE_PUBLIC', } export const promptPermissionsSchema = z.object({ - [Permissions.SHARED_GLOBAL]: z.boolean().default(false), [Permissions.USE]: z.boolean().default(true), [Permissions.CREATE]: z.boolean().default(true), - // [Permissions.SHARE]: z.boolean().default(false), + [Permissions.SHARE]: z.boolean().default(false), + [Permissions.SHARE_PUBLIC]: z.boolean().default(false), }); export type TPromptPermissions = z.infer; @@ -99,10 +100,10 @@ export const memoryPermissionsSchema = z.object({ export type TMemoryPermissions = z.infer; export const agentPermissionsSchema = z.object({ - [Permissions.SHARED_GLOBAL]: z.boolean().default(false), [Permissions.USE]: z.boolean().default(true), [Permissions.CREATE]: z.boolean().default(true), - // [Permissions.SHARE]: z.boolean().default(false), + [Permissions.SHARE]: z.boolean().default(false), + [Permissions.SHARE_PUBLIC]: z.boolean().default(false), }); export type TAgentPermissions = z.infer; @@ -152,6 +153,7 @@ export const mcpServersPermissionsSchema = z.object({ [Permissions.USE]: z.boolean().default(true), [Permissions.CREATE]: z.boolean().default(true), [Permissions.SHARE]: z.boolean().default(false), + [Permissions.SHARE_PUBLIC]: z.boolean().default(false), }); export type TMcpServersPermissions = z.infer; diff --git a/packages/data-provider/src/roles.ts b/packages/data-provider/src/roles.ts index 02fbeaf6db..b3d44a49d9 100644 --- a/packages/data-provider/src/roles.ts +++ b/packages/data-provider/src/roles.ts @@ -43,10 +43,10 @@ const defaultRolesSchema = z.object({ name: z.literal(SystemRoles.ADMIN), permissions: permissionsSchema.extend({ [PermissionTypes.PROMPTS]: promptPermissionsSchema.extend({ - [Permissions.SHARED_GLOBAL]: z.boolean().default(true), [Permissions.USE]: z.boolean().default(true), [Permissions.CREATE]: z.boolean().default(true), - // [Permissions.SHARE]: z.boolean().default(true), + [Permissions.SHARE]: z.boolean().default(true), + [Permissions.SHARE_PUBLIC]: z.boolean().default(true), }), [PermissionTypes.BOOKMARKS]: bookmarkPermissionsSchema.extend({ [Permissions.USE]: z.boolean().default(true), @@ -59,10 +59,10 @@ const defaultRolesSchema = z.object({ [Permissions.OPT_OUT]: z.boolean().default(true), }), [PermissionTypes.AGENTS]: agentPermissionsSchema.extend({ - [Permissions.SHARED_GLOBAL]: z.boolean().default(true), [Permissions.USE]: z.boolean().default(true), [Permissions.CREATE]: z.boolean().default(true), - // [Permissions.SHARE]: z.boolean().default(true), + [Permissions.SHARE]: z.boolean().default(true), + [Permissions.SHARE_PUBLIC]: z.boolean().default(true), }), [PermissionTypes.MULTI_CONVO]: multiConvoPermissionsSchema.extend({ [Permissions.USE]: z.boolean().default(true), @@ -94,6 +94,7 @@ const defaultRolesSchema = z.object({ [Permissions.USE]: z.boolean().default(true), [Permissions.CREATE]: z.boolean().default(true), [Permissions.SHARE]: z.boolean().default(true), + [Permissions.SHARE_PUBLIC]: z.boolean().default(true), }), }), }), @@ -108,9 +109,10 @@ export const roleDefaults = defaultRolesSchema.parse({ name: SystemRoles.ADMIN, permissions: { [PermissionTypes.PROMPTS]: { - [Permissions.SHARED_GLOBAL]: true, [Permissions.USE]: true, [Permissions.CREATE]: true, + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true, @@ -123,9 +125,10 @@ export const roleDefaults = defaultRolesSchema.parse({ [Permissions.OPT_OUT]: true, }, [PermissionTypes.AGENTS]: { - [Permissions.SHARED_GLOBAL]: true, [Permissions.USE]: true, [Permissions.CREATE]: true, + [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, }, [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true, @@ -157,6 +160,7 @@ export const roleDefaults = defaultRolesSchema.parse({ [Permissions.USE]: true, [Permissions.CREATE]: true, [Permissions.SHARE]: true, + [Permissions.SHARE_PUBLIC]: true, }, }, }, diff --git a/packages/data-schemas/src/schema/role.ts b/packages/data-schemas/src/schema/role.ts index e8da248c8d..3ac0a7f8a2 100644 --- a/packages/data-schemas/src/schema/role.ts +++ b/packages/data-schemas/src/schema/role.ts @@ -11,9 +11,10 @@ const rolePermissionsSchema = new Schema( [Permissions.USE]: { type: Boolean }, }, [PermissionTypes.PROMPTS]: { - [Permissions.SHARED_GLOBAL]: { type: Boolean }, [Permissions.USE]: { type: Boolean }, [Permissions.CREATE]: { type: Boolean }, + [Permissions.SHARE]: { type: Boolean }, + [Permissions.SHARE_PUBLIC]: { type: Boolean }, }, [PermissionTypes.MEMORIES]: { [Permissions.USE]: { type: Boolean }, @@ -23,9 +24,10 @@ const rolePermissionsSchema = new Schema( [Permissions.OPT_OUT]: { type: Boolean }, }, [PermissionTypes.AGENTS]: { - [Permissions.SHARED_GLOBAL]: { type: Boolean }, [Permissions.USE]: { type: Boolean }, [Permissions.CREATE]: { type: Boolean }, + [Permissions.SHARE]: { type: Boolean }, + [Permissions.SHARE_PUBLIC]: { type: Boolean }, }, [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: { type: Boolean }, @@ -57,6 +59,7 @@ const rolePermissionsSchema = new Schema( [Permissions.USE]: { type: Boolean }, [Permissions.CREATE]: { type: Boolean }, [Permissions.SHARE]: { type: Boolean }, + [Permissions.SHARE_PUBLIC]: { type: Boolean }, }, }, { _id: false }, diff --git a/packages/data-schemas/src/types/role.ts b/packages/data-schemas/src/types/role.ts index 679e80010f..fc6340da2c 100644 --- a/packages/data-schemas/src/types/role.ts +++ b/packages/data-schemas/src/types/role.ts @@ -10,9 +10,10 @@ export interface IRole extends Document { [Permissions.USE]?: boolean; }; [PermissionTypes.PROMPTS]?: { - [Permissions.SHARED_GLOBAL]?: boolean; [Permissions.USE]?: boolean; [Permissions.CREATE]?: boolean; + [Permissions.SHARE]?: boolean; + [Permissions.SHARE_PUBLIC]?: boolean; }; [PermissionTypes.MEMORIES]?: { [Permissions.USE]?: boolean; @@ -21,9 +22,10 @@ export interface IRole extends Document { [Permissions.READ]?: boolean; }; [PermissionTypes.AGENTS]?: { - [Permissions.SHARED_GLOBAL]?: boolean; [Permissions.USE]?: boolean; [Permissions.CREATE]?: boolean; + [Permissions.SHARE]?: boolean; + [Permissions.SHARE_PUBLIC]?: boolean; }; [PermissionTypes.MULTI_CONVO]?: { [Permissions.USE]?: boolean; @@ -55,6 +57,7 @@ export interface IRole extends Document { [Permissions.USE]?: boolean; [Permissions.CREATE]?: boolean; [Permissions.SHARE]?: boolean; + [Permissions.SHARE_PUBLIC]?: boolean; }; }; }