mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-09-06 22:49:29 +00:00
🧲 feat: Enable Secure Attached Environment Pairing (#15355)
* feat: add secure code environment pairing * fix: satisfy code environment type checks * fix: secure code environment administration * fix: isolate code pairing control plane * fix: validate code pairing control responses * fix: secure code pairing transport * fix: validate code pairing wire format * fix: harden pairing secret lookup
This commit is contained in:
parent
7533d138fa
commit
a9ccac8656
15 changed files with 810 additions and 0 deletions
|
|
@ -520,6 +520,7 @@ if (cluster.isMaster) {
|
|||
app.use('/api/admin/insights', routes.insights);
|
||||
app.use('/api/admin', routes.adminAuth);
|
||||
app.use('/api/admin/skills', routes.adminSkills);
|
||||
app.use('/api/admin/code-environments', routes.adminCodeEnvironments);
|
||||
app.use('/api/actions', routes.actions);
|
||||
app.use('/api/keys', routes.keys);
|
||||
app.use('/api/api-keys', routes.apiKeys);
|
||||
|
|
|
|||
|
|
@ -367,6 +367,7 @@ const startServer = async () => {
|
|||
app.use('/api/admin/insights', routes.insights);
|
||||
app.use('/api/admin', routes.adminAuth);
|
||||
app.use('/api/admin/config', routes.adminConfig);
|
||||
app.use('/api/admin/code-environments', routes.adminCodeEnvironments);
|
||||
app.use('/api/admin/langfuse', routes.adminLangfuse);
|
||||
app.use('/api/admin/grants', routes.adminGrants);
|
||||
app.use('/api/admin/groups', routes.adminGroups);
|
||||
|
|
|
|||
17
api/server/routes/admin/code.js
Normal file
17
api/server/routes/admin/code.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
const express = require('express');
|
||||
const { createAdminCodeEnvironmentHandlers } = require('@librechat/api');
|
||||
const { SystemCapabilities } = require('@librechat/data-schemas');
|
||||
const { requireCapability } = require('~/server/middleware/roles/capabilities');
|
||||
const { getAppConfig } = require('~/server/services/Config');
|
||||
const { requireJwtAuth } = require('~/server/middleware');
|
||||
|
||||
const router = express.Router();
|
||||
const requireAdminAccess = requireCapability(SystemCapabilities.ACCESS_ADMIN);
|
||||
const requireCodeEnvironmentManage = requireCapability(SystemCapabilities.MANAGE_CODE_ENVIRONMENTS);
|
||||
const handlers = createAdminCodeEnvironmentHandlers({ getAppConfig });
|
||||
|
||||
router.use(requireJwtAuth, requireAdminAccess, requireCodeEnvironmentManage);
|
||||
router.post('/:environmentId/pairings', handlers.createPairing);
|
||||
router.post('/:environmentId/revoke', handlers.revokeWorker);
|
||||
|
||||
module.exports = router;
|
||||
74
api/server/routes/admin/code.test.js
Normal file
74
api/server/routes/admin/code.test.js
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
const express = require('express');
|
||||
const request = require('supertest');
|
||||
|
||||
const middlewareCalls = [];
|
||||
const mockRequireJwtAuth = jest.fn((req, _res, next) => {
|
||||
middlewareCalls.push('jwt');
|
||||
req.user = { id: 'admin-1', role: 'ADMIN' };
|
||||
next();
|
||||
});
|
||||
const mockRequireCapability = jest.fn((capability) => (req, _res, next) => {
|
||||
middlewareCalls.push(capability);
|
||||
next();
|
||||
});
|
||||
const mockHandlers = {
|
||||
createPairing: jest.fn((req, res) =>
|
||||
res.status(200).json({ operation: 'pair', environmentId: req.params.environmentId }),
|
||||
),
|
||||
revokeWorker: jest.fn((req, res) =>
|
||||
res.status(200).json({ operation: 'revoke', environmentId: req.params.environmentId }),
|
||||
),
|
||||
};
|
||||
const mockGetAppConfig = jest.fn();
|
||||
|
||||
jest.mock('@librechat/data-schemas', () => ({
|
||||
SystemCapabilities: {
|
||||
ACCESS_ADMIN: 'access:admin',
|
||||
MANAGE_CODE_ENVIRONMENTS: 'manage:code_environments',
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@librechat/api', () => ({
|
||||
createAdminCodeEnvironmentHandlers: jest.fn(() => mockHandlers),
|
||||
}));
|
||||
|
||||
jest.mock('~/server/middleware/roles/capabilities', () => ({
|
||||
requireCapability: mockRequireCapability,
|
||||
}));
|
||||
|
||||
jest.mock('~/server/middleware', () => ({
|
||||
requireJwtAuth: mockRequireJwtAuth,
|
||||
}));
|
||||
|
||||
jest.mock('~/server/services/Config', () => ({
|
||||
getAppConfig: mockGetAppConfig,
|
||||
}));
|
||||
|
||||
function createApp() {
|
||||
delete require.cache[require.resolve('./code')];
|
||||
const router = require('./code');
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use('/api/admin/code-environments', router);
|
||||
return app;
|
||||
}
|
||||
|
||||
describe('admin code environment routes', () => {
|
||||
beforeEach(() => {
|
||||
middlewareCalls.length = 0;
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['pairings', 'createPairing', 'pair'],
|
||||
['revoke', 'revokeWorker', 'revoke'],
|
||||
])('protects and delegates the %s operation', async (path, handlerName, operation) => {
|
||||
const response = await request(createApp())
|
||||
.post(`/api/admin/code-environments/attached-vm/${path}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toEqual({ operation, environmentId: 'attached-vm' });
|
||||
expect(middlewareCalls).toEqual(['jwt', 'access:admin', 'manage:code_environments']);
|
||||
expect(mockHandlers[handlerName]).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
@ -3,6 +3,7 @@ const assistants = require('./assistants');
|
|||
const categories = require('./categories');
|
||||
const adminAuth = require('./admin/auth');
|
||||
const adminConfig = require('./admin/config');
|
||||
const adminCodeEnvironments = require('./admin/code');
|
||||
const adminLangfuse = require('./admin/langfuse');
|
||||
const adminGrants = require('./admin/grants');
|
||||
const adminGroups = require('./admin/groups');
|
||||
|
|
@ -47,6 +48,7 @@ module.exports = {
|
|||
auth,
|
||||
adminAuth,
|
||||
adminConfig,
|
||||
adminCodeEnvironments,
|
||||
adminLangfuse,
|
||||
adminGrants,
|
||||
adminGroups,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue