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
198 lines
6.0 KiB
JavaScript
198 lines
6.0 KiB
JavaScript
const mongoose = require('mongoose');
|
|
const express = require('express');
|
|
const {
|
|
AccessRoleIds,
|
|
PrincipalType,
|
|
ResourceType,
|
|
PermissionBits,
|
|
} = require('librechat-data-provider');
|
|
const {
|
|
getUserEffectivePermissions,
|
|
getAllEffectivePermissions,
|
|
updateResourcePermissions,
|
|
getResourcePermissions,
|
|
getResourceRoles,
|
|
searchPrincipals,
|
|
} = require('~/server/controllers/PermissionsController');
|
|
const {
|
|
checkShareAccess,
|
|
checkSharePublicAccess,
|
|
} = require('~/server/middleware/checkSharePublicAccess');
|
|
const { requireJwtAuth, checkBan, uaParser, canAccessResource } = require('~/server/middleware');
|
|
const { checkPeoplePickerAccess } = require('~/server/middleware/checkPeoplePickerAccess');
|
|
const { findMCPServerByObjectId, getSkillById } = require('~/models');
|
|
|
|
const router = express.Router();
|
|
|
|
// Apply common middleware
|
|
router.use(requireJwtAuth);
|
|
router.use(checkBan);
|
|
router.use(uaParser);
|
|
|
|
/**
|
|
* Generic routes for resource permissions
|
|
* Pattern: /api/permissions/{resourceType}/{resourceId}
|
|
*/
|
|
|
|
/**
|
|
* GET /api/permissions/search-principals
|
|
* Search for users and groups to grant permissions
|
|
*/
|
|
router.get('/search-principals', checkPeoplePickerAccess, searchPrincipals);
|
|
|
|
/**
|
|
* GET /api/permissions/{resourceType}/roles
|
|
* Get available roles for a resource type
|
|
*/
|
|
router.get('/:resourceType/roles', getResourceRoles);
|
|
|
|
/**
|
|
* Middleware factory to check resource access for permission-related operations.
|
|
* SECURITY: Users must have SHARE permission to view or modify resource permissions.
|
|
* @param {string} requiredPermission - The permission bit required (e.g., SHARE)
|
|
* @returns Express middleware function
|
|
*/
|
|
const checkResourcePermissionAccess = (requiredPermission) => (req, res, next) => {
|
|
const { resourceType } = req.params;
|
|
let middleware;
|
|
|
|
if (resourceType === ResourceType.AGENT) {
|
|
middleware = canAccessResource({
|
|
resourceType: ResourceType.AGENT,
|
|
requiredPermission,
|
|
resourceIdParam: 'resourceId',
|
|
});
|
|
} else if (resourceType === ResourceType.REMOTE_AGENT) {
|
|
middleware = canAccessResource({
|
|
resourceType: ResourceType.REMOTE_AGENT,
|
|
requiredPermission,
|
|
resourceIdParam: 'resourceId',
|
|
});
|
|
} else if (resourceType === ResourceType.PROMPTGROUP) {
|
|
middleware = canAccessResource({
|
|
resourceType: ResourceType.PROMPTGROUP,
|
|
requiredPermission,
|
|
resourceIdParam: 'resourceId',
|
|
});
|
|
} else if (resourceType === ResourceType.MCPSERVER) {
|
|
middleware = canAccessResource({
|
|
resourceType: ResourceType.MCPSERVER,
|
|
requiredPermission,
|
|
resourceIdParam: 'resourceId',
|
|
idResolver: findMCPServerByObjectId,
|
|
});
|
|
} else if (resourceType === ResourceType.SKILL) {
|
|
middleware = canAccessResource({
|
|
resourceType: ResourceType.SKILL,
|
|
requiredPermission,
|
|
resourceIdParam: 'resourceId',
|
|
idResolver: getSkillById,
|
|
});
|
|
} else if (resourceType === ResourceType.SHARED_LINK) {
|
|
middleware = canAccessResource({
|
|
resourceType: ResourceType.SHARED_LINK,
|
|
requiredPermission,
|
|
resourceIdParam: 'resourceId',
|
|
});
|
|
} else {
|
|
return res.status(400).json({
|
|
error: 'Bad Request',
|
|
message: `Unsupported resource type: ${resourceType}`,
|
|
});
|
|
}
|
|
|
|
// Execute the middleware
|
|
middleware(req, res, next);
|
|
};
|
|
|
|
const rejectSharedLinkOwnerPermissionChanges = async (req, res, next) => {
|
|
if (req.params.resourceType !== ResourceType.SHARED_LINK) {
|
|
return next();
|
|
}
|
|
|
|
const updated = Array.isArray(req.body?.updated) ? req.body.updated : [];
|
|
const removed = Array.isArray(req.body?.removed) ? req.body.removed : [];
|
|
const grantsOwner = updated.some(
|
|
(principal) => principal?.accessRoleId === AccessRoleIds.SHARED_LINK_OWNER,
|
|
);
|
|
const grantsPublicOwner = req.body?.publicAccessRoleId === AccessRoleIds.SHARED_LINK_OWNER;
|
|
|
|
if (grantsOwner || grantsPublicOwner) {
|
|
return res.status(400).json({
|
|
error: 'Bad Request',
|
|
message: 'Shared link owner permissions cannot be changed',
|
|
});
|
|
}
|
|
|
|
const userMutations = [...updated, ...removed].filter(
|
|
(principal) => principal?.type === PrincipalType.USER && principal?.id,
|
|
);
|
|
|
|
if (userMutations.length === 0) {
|
|
return next();
|
|
}
|
|
|
|
try {
|
|
const SharedLink = mongoose.models.SharedLink;
|
|
const link = await SharedLink.findById(req.params.resourceId, 'user').lean();
|
|
const ownerId = link?.user?.toString();
|
|
const touchesOwner = ownerId
|
|
? userMutations.some((principal) => principal.id?.toString() === ownerId)
|
|
: false;
|
|
|
|
if (touchesOwner) {
|
|
return res.status(400).json({
|
|
error: 'Bad Request',
|
|
message: 'Shared link owner permissions cannot be changed',
|
|
});
|
|
}
|
|
} catch (_error) {
|
|
return res.status(500).json({
|
|
error: 'Internal Server Error',
|
|
message: 'Failed to validate shared link owner permissions',
|
|
});
|
|
}
|
|
|
|
return next();
|
|
};
|
|
|
|
/**
|
|
* GET /api/permissions/{resourceType}/{resourceId}
|
|
* Get all permissions for a specific resource
|
|
* SECURITY: Requires SHARE permission to view resource permissions
|
|
*/
|
|
router.get(
|
|
'/:resourceType/:resourceId',
|
|
checkResourcePermissionAccess(PermissionBits.SHARE),
|
|
getResourcePermissions,
|
|
);
|
|
|
|
/**
|
|
* PUT /api/permissions/{resourceType}/{resourceId}
|
|
* Bulk update permissions for a specific resource
|
|
* SECURITY: Requires resource ACL SHARE and role SHARE to modify resource permissions
|
|
* SECURITY: Requires SHARE_PUBLIC permission to enable public sharing
|
|
*/
|
|
router.put(
|
|
'/:resourceType/:resourceId',
|
|
checkResourcePermissionAccess(PermissionBits.SHARE),
|
|
checkShareAccess,
|
|
checkSharePublicAccess,
|
|
rejectSharedLinkOwnerPermissionChanges,
|
|
updateResourcePermissions,
|
|
);
|
|
|
|
/**
|
|
* GET /api/permissions/{resourceType}/effective/all
|
|
* Get user's effective permissions for all accessible resources of a type
|
|
*/
|
|
router.get('/:resourceType/effective/all', getAllEffectivePermissions);
|
|
|
|
/**
|
|
* GET /api/permissions/{resourceType}/{resourceId}/effective
|
|
* Get user's effective permissions for a specific resource
|
|
*/
|
|
router.get('/:resourceType/:resourceId/effective', getUserEffectivePermissions);
|
|
|
|
module.exports = router;
|