🔧 refactor: Permission handling for Resource Sharing (#11283)

* 🔧 refactor: permission handling for public sharing

- Updated permission keys from SHARED_GLOBAL to SHARE across various files for consistency.
- Added public access configuration in librechat.example.yaml.
- Adjusted related tests and components to reflect the new permission structure.

* chore: Update default SHARE permission to false

* fix: Update SHARE permissions in tests and implementation

- Added SHARE permission handling for user and admin roles in permissions.spec.ts and permissions.ts.
- Updated expected permissions in tests to reflect new SHARE permission values for various permission types.

* fix: Handle undefined values in PeoplePickerAdminSettings component

- Updated the checked and value props of the Switch component to handle undefined values gracefully by defaulting to false. This ensures consistent behavior when the field value is not set.

* feat: Add CREATE permission handling for prompts and agents

- Introduced CREATE permission for user and admin roles in permissions.spec.ts and permissions.ts.
- Updated expected permissions in tests to include CREATE permission for various permission types.

* 🔧 refactor: Enhance permission handling for sharing dialog usability

* refactor: public sharing permissions for resources

- Added middleware to check SHARE_PUBLIC permissions for agents, prompts, and MCP servers.
- Updated interface configuration in librechat.example.yaml to include public sharing options.
- Enhanced components and hooks to support public sharing functionality.
- Adjusted tests to validate new permission handling for public sharing across various resource types.

* refactor: update Share2Icon styling in GenericGrantAccessDialog

* refactor: update Share2Icon size in GenericGrantAccessDialog for consistency

* refactor: improve layout and styling of Share2Icon in GenericGrantAccessDialog

* refactor: update Share2Icon size in GenericGrantAccessDialog for improved consistency

* chore: remove redundant public sharing option from People Picker

* refactor: add SHARE_PUBLIC permission handling in updateInterfacePermissions tests
This commit is contained in:
Danny Avila 2026-01-10 14:02:56 -05:00 committed by GitHub
parent 083251508e
commit 76e17ba701
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 646 additions and 109 deletions

View file

@ -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 () => {

View file

@ -29,7 +29,7 @@ describe('canAccessAgentResource middleware', () => {
AGENTS: {
USE: true,
CREATE: true,
SHARED_GLOBAL: false,
SHARE: true,
},
},
});

View file

@ -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,
},
},
});

View file

@ -32,7 +32,7 @@ describe('fileAccess middleware', () => {
AGENTS: {
USE: true,
CREATE: true,
SHARED_GLOBAL: false,
SHARE: true,
},
},
});

View file

@ -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,
};

View file

@ -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',
});
});
});

View file

@ -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,
},
},
});

View file

@ -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,
);

View file

@ -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,
});

View file

@ -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,
});

View file

@ -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;
}

View file

@ -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 = () => {

View file

@ -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({

View file

@ -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

View file

@ -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<TPrincipal[]>([]);
const [hasChanges, setHasChanges] = useState(false);
const [defaultPermissionId, setDefaultPermissionId] = useState<AccessRoleIds | undefined>(
@ -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)}
>
<div className="flex min-w-[32px] items-center justify-center gap-2 text-blue-500">
<span className="flex h-6 w-6 items-center justify-center">
<Share2Icon className="icon-md h-4 w-4" aria-hidden="true" />
<Share2Icon className="icon-md h-4 w-4" />
</span>
{totalCurrentShares > 0 && (
<Label className="cursor-pointer text-sm font-medium text-text-secondary">
@ -332,16 +343,20 @@ export default function GenericGrantAccessDialog({
)}
</div>
<div className="flex border-t border-border-light" />
{canSharePublic && (
<>
<div className="flex border-t border-border-light" />
{/* Public Access Section */}
<PublicSharingToggle
isPublic={isPublic}
publicRole={publicRole}
onPublicToggle={handlePublicToggle}
onPublicRoleChange={handlePublicRoleChange}
resourceType={resourceType}
/>
{/* Public Access Section */}
<PublicSharingToggle
isPublic={isPublic}
publicRole={publicRole}
onPublicToggle={handlePublicToggle}
onPublicRoleChange={handlePublicRoleChange}
resourceType={resourceType}
/>
</>
)}
{/* Footer Actions */}
<div className="flex justify-between pt-4">

View file

@ -57,9 +57,9 @@ const LabelController: React.FC<LabelControllerProps> = ({
render={({ field }) => (
<Switch
{...field}
checked={field.value}
checked={field.value ?? false}
onCheckedChange={field.onChange}
value={field.value.toString()}
value={(field.value ?? false).toString()}
aria-label={label}
/>
)}

View file

@ -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 = () => {

View file

@ -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,

View file

@ -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 = () => {

View file

@ -1,2 +1,3 @@
export { usePeoplePickerPermissions } from './usePeoplePickerPermissions';
export { useResourcePermissionState } from './useResourcePermissionState';
export { useCanSharePublic } from './useCanSharePublic';

View file

@ -0,0 +1,22 @@
import { ResourceType, PermissionTypes, Permissions } from 'librechat-data-provider';
import { useHasAccess } from '~/hooks';
const resourceToPermissionMap: Record<ResourceType, PermissionTypes> = {
[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;
};

View file

@ -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 = () => {

View file

@ -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",

View file

@ -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:

View file

@ -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

View file

@ -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<Record<PermissionTypes, Record<string, boolean | undefined>>> = {
[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,
),
},
};

View file

@ -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<Permissions, string[]>,
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<Permissions, string[]>,
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<Permissions, string[]>,
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;

View file

@ -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,

View file

@ -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<typeof promptPermissionsSchema>;
@ -99,10 +100,10 @@ export const memoryPermissionsSchema = z.object({
export type TMemoryPermissions = z.infer<typeof memoryPermissionsSchema>;
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<typeof agentPermissionsSchema>;
@ -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<typeof mcpServersPermissionsSchema>;

View file

@ -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,
},
},
},

View file

@ -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 },

View file

@ -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;
};
};
}