Files
danny-avila--librechat/api/server/routes/accessPermissions.sharePolicy.test.js
wehub-resource-sync e115934061
Publish `@librechat/data-schemas` to NPM / pack (push) Failing after 8s
Publish `@librechat/client` to NPM / pack (push) Failing after 0s
GitNexus Index / index (push) Failing after 1s
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been skipped
Sync Helm Chart Tags / Sync chart tags (push) Failing after 2s
Publish `librechat-data-provider` to NPM / pack (push) Failing after 1s
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Failing after 1s
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Failing after 0s
Sync Helm Chart Tags / Ignore non-main push (push) Has been skipped
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Failing after 1s
Publish `@librechat/client` to NPM / publish-npm (push) Has been cancelled
Publish `librechat-data-provider` to NPM / publish-npm (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / publish-npm (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:08:12 +08:00

321 lines
10 KiB
JavaScript

jest.mock('~/models', () => ({
getRoleByName: jest.fn(),
findMCPServerByObjectId: jest.fn(),
getSkillById: jest.fn(),
}));
jest.mock('~/server/middleware', () => ({
requireJwtAuth: (req, _res, next) => next(),
checkBan: (_req, _res, next) => next(),
uaParser: (_req, _res, next) => next(),
canAccessResource: jest.fn(() => (_req, _res, next) => next()),
}));
jest.mock('~/server/middleware/checkPeoplePickerAccess', () => ({
checkPeoplePickerAccess: jest.fn((_req, _res, next) => next()),
}));
jest.mock('~/server/middleware/roles/capabilities', () => ({
hasCapability: jest.fn(),
}));
jest.mock('~/server/controllers/PermissionsController', () => ({
getUserEffectivePermissions: jest.fn((_req, res) => res.json({ permissions: [] })),
getAllEffectivePermissions: jest.fn((_req, res) => res.json({ permissions: [] })),
updateResourcePermissions: jest.fn((_req, res) => res.json({ success: true })),
getResourcePermissions: jest.fn((_req, res) => res.json({ permissions: [] })),
getResourceRoles: jest.fn((_req, res) => res.json({ roles: [] })),
searchPrincipals: jest.fn((_req, res) => res.json({ principals: [] })),
}));
const express = require('express');
const request = require('supertest');
const mongoose = require('mongoose');
const {
SystemRoles,
ResourceType,
AccessRoleIds,
PrincipalType,
PermissionBits,
PermissionTypes,
Permissions,
} = require('librechat-data-provider');
const { updateResourcePermissions } = require('~/server/controllers/PermissionsController');
const { hasCapability } = require('~/server/middleware/roles/capabilities');
const { canAccessResource } = require('~/server/middleware');
const accessPermissionsRouter = require('./accessPermissions');
const { getRoleByName } = require('~/models');
describe('Access permissions share policy', () => {
let app;
const mockSharedLinkFindById = jest.fn();
const originalSharedLinkModel = mongoose.models.SharedLink;
const resourceId = '507f1f77bcf86cd799439011';
const sharePolicyCases = [
{
label: 'agent',
resourceType: ResourceType.AGENT,
permissionType: PermissionTypes.AGENTS,
accessRoleId: AccessRoleIds.AGENT_VIEWER,
middlewareOptions: {
resourceType: ResourceType.AGENT,
requiredPermission: PermissionBits.SHARE,
resourceIdParam: 'resourceId',
},
},
{
label: 'prompt group',
resourceType: ResourceType.PROMPTGROUP,
permissionType: PermissionTypes.PROMPTS,
accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER,
middlewareOptions: {
resourceType: ResourceType.PROMPTGROUP,
requiredPermission: PermissionBits.SHARE,
resourceIdParam: 'resourceId',
},
},
{
label: 'MCP server',
resourceType: ResourceType.MCPSERVER,
permissionType: PermissionTypes.MCP_SERVERS,
accessRoleId: AccessRoleIds.MCPSERVER_VIEWER,
middlewareOptions: {
resourceType: ResourceType.MCPSERVER,
requiredPermission: PermissionBits.SHARE,
resourceIdParam: 'resourceId',
idResolver: expect.any(Function),
},
},
{
label: 'remote agent',
resourceType: ResourceType.REMOTE_AGENT,
permissionType: PermissionTypes.REMOTE_AGENTS,
accessRoleId: AccessRoleIds.REMOTE_AGENT_VIEWER,
middlewareOptions: {
resourceType: ResourceType.REMOTE_AGENT,
requiredPermission: PermissionBits.SHARE,
resourceIdParam: 'resourceId',
},
},
{
label: 'skill',
resourceType: ResourceType.SKILL,
permissionType: PermissionTypes.SKILLS,
accessRoleId: AccessRoleIds.SKILL_VIEWER,
middlewareOptions: {
resourceType: ResourceType.SKILL,
requiredPermission: PermissionBits.SHARE,
resourceIdParam: 'resourceId',
idResolver: expect.any(Function),
},
},
];
const createUpdatedPrincipal = (accessRoleId) => ({
type: PrincipalType.USER,
id: 'target-user',
accessRoleId,
});
const allowSharedLinkSharing = () => {
getRoleByName.mockResolvedValue({
permissions: {
[PermissionTypes.SHARED_LINKS]: {
[Permissions.SHARE]: true,
[Permissions.SHARE_PUBLIC]: true,
},
},
});
};
const mockSharedLinkOwner = (ownerId = 'owner-user') => {
mockSharedLinkFindById.mockReturnValue({
lean: jest.fn().mockResolvedValue({ user: ownerId }),
});
};
beforeEach(() => {
jest.clearAllMocks();
if (mongoose.models.SharedLink) {
mongoose.models.SharedLink.findById = mockSharedLinkFindById;
} else {
mongoose.models.SharedLink = { findById: mockSharedLinkFindById };
}
hasCapability.mockResolvedValue(false);
app = express();
app.use(express.json());
app.use((req, _res, next) => {
req.user = { id: 'skill-owner', role: SystemRoles.USER };
next();
});
app.use('/api/permissions', accessPermissionsRouter);
});
afterAll(() => {
if (originalSharedLinkModel) {
mongoose.models.SharedLink = originalSharedLinkModel;
} else {
delete mongoose.models.SharedLink;
}
});
it.each(sharePolicyCases)(
'blocks non-public $label sharing when ACL SHARE passes but role SHARE is disabled',
async ({ resourceType, permissionType, accessRoleId, middlewareOptions }) => {
getRoleByName.mockResolvedValue({
permissions: {
[permissionType]: {
[Permissions.SHARE]: false,
[Permissions.SHARE_PUBLIC]: false,
},
},
});
const response = await request(app)
.put(`/api/permissions/${resourceType}/${resourceId}`)
.send({ updated: [createUpdatedPrincipal(accessRoleId)], public: false });
expect(response.status).toBe(403);
expect(response.body).toEqual({
error: 'Forbidden',
message: `You do not have permission to share ${resourceType} resources`,
});
expect(canAccessResource).toHaveBeenCalledWith(middlewareOptions);
expect(updateResourcePermissions).not.toHaveBeenCalled();
},
);
it('allows non-public skill sharing when both ACL SHARE and role SKILLS.SHARE pass', async () => {
getRoleByName.mockResolvedValue({
permissions: {
[PermissionTypes.SKILLS]: {
[Permissions.SHARE]: true,
[Permissions.SHARE_PUBLIC]: false,
},
},
});
const response = await request(app)
.put(`/api/permissions/${ResourceType.SKILL}/${resourceId}`)
.send({ updated: [createUpdatedPrincipal(AccessRoleIds.SKILL_VIEWER)], public: false });
expect(response.status).toBe(200);
expect(response.body).toEqual({ success: true });
expect(updateResourcePermissions).toHaveBeenCalledTimes(1);
});
it('preserves resource management capability bypass for non-public skill sharing', async () => {
hasCapability.mockResolvedValue(true);
const response = await request(app)
.put(`/api/permissions/${ResourceType.SKILL}/${resourceId}`)
.send({ updated: [createUpdatedPrincipal(AccessRoleIds.SKILL_VIEWER)], public: false });
expect(response.status).toBe(200);
expect(response.body).toEqual({ success: true });
expect(getRoleByName).not.toHaveBeenCalled();
expect(updateResourcePermissions).toHaveBeenCalledTimes(1);
});
it('still requires SHARE_PUBLIC when enabling public skill sharing', async () => {
getRoleByName.mockResolvedValue({
permissions: {
[PermissionTypes.SKILLS]: {
[Permissions.SHARE]: true,
[Permissions.SHARE_PUBLIC]: false,
},
},
});
const response = await request(app)
.put(`/api/permissions/${ResourceType.SKILL}/${resourceId}`)
.send({ public: true, publicAccessRoleId: AccessRoleIds.SKILL_VIEWER });
expect(response.status).toBe(403);
expect(response.body).toEqual({
error: 'Forbidden',
message: `You do not have permission to share ${ResourceType.SKILL} resources publicly`,
});
expect(updateResourcePermissions).not.toHaveBeenCalled();
});
it('blocks granting shared-link owner through generic permission updates', async () => {
allowSharedLinkSharing();
mockSharedLinkOwner();
const response = await request(app)
.put(`/api/permissions/${ResourceType.SHARED_LINK}/${resourceId}`)
.send({
updated: [
{
type: PrincipalType.USER,
id: 'target-user',
accessRoleId: AccessRoleIds.SHARED_LINK_OWNER,
},
],
public: false,
});
expect(response.status).toBe(400);
expect(response.body.message).toBe('Shared link owner permissions cannot be changed');
expect(updateResourcePermissions).not.toHaveBeenCalled();
});
it('blocks granting shared-link owner to the public principal', async () => {
allowSharedLinkSharing();
const response = await request(app)
.put(`/api/permissions/${ResourceType.SHARED_LINK}/${resourceId}`)
.send({
public: true,
publicAccessRoleId: AccessRoleIds.SHARED_LINK_OWNER,
});
expect(response.status).toBe(400);
expect(response.body.message).toBe('Shared link owner permissions cannot be changed');
expect(updateResourcePermissions).not.toHaveBeenCalled();
expect(mockSharedLinkFindById).not.toHaveBeenCalled();
});
it('blocks removing the canonical shared-link owner', async () => {
allowSharedLinkSharing();
mockSharedLinkOwner('owner-user');
const response = await request(app)
.put(`/api/permissions/${ResourceType.SHARED_LINK}/${resourceId}`)
.send({
updated: [],
removed: [{ type: PrincipalType.USER, id: 'owner-user' }],
public: false,
});
expect(response.status).toBe(400);
expect(response.body.message).toBe('Shared link owner permissions cannot be changed');
expect(updateResourcePermissions).not.toHaveBeenCalled();
});
it('allows viewer grants for non-owner shared-link users', async () => {
allowSharedLinkSharing();
mockSharedLinkOwner('owner-user');
const response = await request(app)
.put(`/api/permissions/${ResourceType.SHARED_LINK}/${resourceId}`)
.send({
updated: [
{
type: PrincipalType.USER,
id: 'target-user',
accessRoleId: AccessRoleIds.SHARED_LINK_VIEWER,
},
],
public: false,
});
expect(response.status).toBe(200);
expect(updateResourcePermissions).toHaveBeenCalledTimes(1);
});
});