mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-21 17:44:11 +00:00
🪪 fix: Resolve Group-Scoped Config Overrides (#13176)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* fix: resolve group-scoped config overrides * test: fix endpoint config request mock typing * fix: keep remote agent preauth config tenant-scoped * test: align config scoping expectations * test: reproduce group endpoint override resolution
This commit is contained in:
parent
ac163c2212
commit
c342e2345b
19 changed files with 311 additions and 40 deletions
|
|
@ -6,7 +6,13 @@ const { getAppConfig } = require('~/server/services/Config');
|
|||
|
||||
const getAvailablePluginsController = async (req, res) => {
|
||||
try {
|
||||
const appConfig = await getAppConfig({ role: req.user?.role, tenantId: req.user?.tenantId });
|
||||
const appConfig =
|
||||
req.config ??
|
||||
(await getAppConfig({
|
||||
role: req.user?.role,
|
||||
userId: req.user?.id,
|
||||
tenantId: req.user?.tenantId,
|
||||
}));
|
||||
const { filteredTools = [], includedTools = [] } = appConfig;
|
||||
|
||||
const uniquePlugins = filterUniquePlugins(availableTools);
|
||||
|
|
@ -41,7 +47,12 @@ const getAvailableTools = async (req, res) => {
|
|||
}
|
||||
|
||||
const appConfig =
|
||||
req.config ?? (await getAppConfig({ role: req.user?.role, tenantId: req.user?.tenantId }));
|
||||
req.config ??
|
||||
(await getAppConfig({
|
||||
role: req.user?.role,
|
||||
userId: req.user?.id,
|
||||
tenantId: req.user?.tenantId,
|
||||
}));
|
||||
|
||||
let toolDefinitions = await getCachedTools();
|
||||
|
||||
|
|
|
|||
|
|
@ -98,10 +98,10 @@ describe('PluginController', () => {
|
|||
|
||||
require('~/app/clients/tools').availableTools.push(...mockPlugins);
|
||||
|
||||
getAppConfig.mockResolvedValueOnce({
|
||||
mockReq.config = {
|
||||
filteredTools: [],
|
||||
includedTools: ['key1'],
|
||||
});
|
||||
};
|
||||
|
||||
await getAvailablePluginsController(mockReq, mockRes);
|
||||
|
||||
|
|
@ -118,10 +118,10 @@ describe('PluginController', () => {
|
|||
|
||||
require('~/app/clients/tools').availableTools.push(...mockPlugins);
|
||||
|
||||
getAppConfig.mockResolvedValueOnce({
|
||||
mockReq.config = {
|
||||
filteredTools: ['key2'],
|
||||
includedTools: [],
|
||||
});
|
||||
};
|
||||
|
||||
await getAvailablePluginsController(mockReq, mockRes);
|
||||
|
||||
|
|
@ -139,10 +139,10 @@ describe('PluginController', () => {
|
|||
|
||||
require('~/app/clients/tools').availableTools.push(...mockPlugins);
|
||||
|
||||
getAppConfig.mockResolvedValueOnce({
|
||||
mockReq.config = {
|
||||
includedTools: ['key1', 'key2'],
|
||||
filteredTools: ['key2'],
|
||||
});
|
||||
};
|
||||
|
||||
await getAvailablePluginsController(mockReq, mockRes);
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,13 @@ const { getLogStores } = require('~/cache');
|
|||
const db = require('~/models');
|
||||
|
||||
const getUserController = async (req, res) => {
|
||||
const appConfig = await getAppConfig({ role: req.user?.role, tenantId: req.user?.tenantId });
|
||||
const appConfig =
|
||||
req.config ??
|
||||
(await getAppConfig({
|
||||
role: req.user?.role,
|
||||
userId: req.user?.id,
|
||||
tenantId: req.user?.tenantId,
|
||||
}));
|
||||
/** @type {IUser} */
|
||||
const userData = req.user.toObject != null ? req.user.toObject() : { ...req.user };
|
||||
/**
|
||||
|
|
@ -165,7 +171,13 @@ const deleteUserMcpServers = async (userId) => {
|
|||
};
|
||||
|
||||
const updateUserPluginsController = async (req, res) => {
|
||||
const appConfig = await getAppConfig({ role: req.user?.role, tenantId: req.user?.tenantId });
|
||||
const appConfig =
|
||||
req.config ??
|
||||
(await getAppConfig({
|
||||
role: req.user?.role,
|
||||
userId: req.user?.id,
|
||||
tenantId: req.user?.tenantId,
|
||||
}));
|
||||
const { user } = req;
|
||||
const { pluginKey, action, auth, isEntityTool } = req.body;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ const checkDomainAllowed = async (req, res, next) => {
|
|||
const email = req?.user?.email;
|
||||
const appConfig = await getAppConfig({
|
||||
role: req?.user?.role,
|
||||
userId: req?.user?.id,
|
||||
tenantId: req?.user?.tenantId,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -93,6 +93,41 @@ describe('loadConfigModels', () => {
|
|||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('passes userId when resolving scoped model config', async () => {
|
||||
getAppConfig.mockResolvedValue({});
|
||||
|
||||
await loadConfigModels({
|
||||
user: { id: 'testUserId', role: 'USER', tenantId: 'tenant-a' },
|
||||
});
|
||||
|
||||
expect(getAppConfig).toHaveBeenCalledWith({
|
||||
role: 'USER',
|
||||
userId: 'testUserId',
|
||||
tenantId: 'tenant-a',
|
||||
});
|
||||
});
|
||||
|
||||
it('uses req.config when available instead of calling getAppConfig', async () => {
|
||||
const result = await loadConfigModels({
|
||||
user: { id: 'testUserId' },
|
||||
config: {
|
||||
endpoints: {
|
||||
custom: [
|
||||
{
|
||||
name: 'LocalOnly',
|
||||
apiKey: 'local-key',
|
||||
baseURL: 'https://example.com/v1',
|
||||
models: { default: ['local-model'], fetch: false },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getAppConfig).not.toHaveBeenCalled();
|
||||
expect(result.LocalOnly).toEqual(['local-model']);
|
||||
});
|
||||
|
||||
it('handles azure models and endpoint correctly', async () => {
|
||||
getAppConfig.mockResolvedValue({
|
||||
endpoints: {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,12 @@ const { getAppConfig } = require('./app');
|
|||
async function loadDefaultModels(req) {
|
||||
try {
|
||||
const appConfig =
|
||||
req.config ?? (await getAppConfig({ role: req.user?.role, tenantId: req.user?.tenantId }));
|
||||
req.config ??
|
||||
(await getAppConfig({
|
||||
role: req.user?.role,
|
||||
userId: req.user?.id,
|
||||
tenantId: req.user?.tenantId,
|
||||
}));
|
||||
const vertexConfig = appConfig?.endpoints?.[EModelEndpoint.anthropic]?.vertexConfig;
|
||||
|
||||
const [openAI, anthropic, azureOpenAI, assistants, azureAssistants, google, bedrock] =
|
||||
|
|
|
|||
|
|
@ -142,6 +142,7 @@ class STTService {
|
|||
req.config ??
|
||||
(await getAppConfig({
|
||||
role: req?.user?.role,
|
||||
userId: req?.user?.id,
|
||||
tenantId: req?.user?.tenantId,
|
||||
}));
|
||||
const sttSchema = appConfig?.speech?.stt;
|
||||
|
|
|
|||
|
|
@ -297,6 +297,7 @@ class TTSService {
|
|||
req.config ??
|
||||
(await getAppConfig({
|
||||
role: req.user?.role,
|
||||
userId: req.user?.id,
|
||||
tenantId: req.user?.tenantId,
|
||||
}));
|
||||
try {
|
||||
|
|
@ -366,6 +367,7 @@ class TTSService {
|
|||
req.config ??
|
||||
(await getAppConfig({
|
||||
role: req.user?.role,
|
||||
userId: req.user?.id,
|
||||
tenantId: req.user?.tenantId,
|
||||
}));
|
||||
const provider = this.getProvider(appConfig);
|
||||
|
|
|
|||
|
|
@ -15,10 +15,13 @@ const { getAppConfig } = require('~/server/services/Config');
|
|||
*/
|
||||
async function getCustomConfigSpeech(req, res) {
|
||||
try {
|
||||
const appConfig = await getAppConfig({
|
||||
role: req.user?.role,
|
||||
tenantId: req.user?.tenantId,
|
||||
});
|
||||
const appConfig =
|
||||
req.config ??
|
||||
(await getAppConfig({
|
||||
role: req.user?.role,
|
||||
userId: req.user?.id,
|
||||
tenantId: req.user?.tenantId,
|
||||
}));
|
||||
|
||||
if (!appConfig) {
|
||||
return res.status(200).send({
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ async function getVoices(req, res) {
|
|||
req.config ??
|
||||
(await getAppConfig({
|
||||
role: req.user?.role,
|
||||
userId: req.user?.id,
|
||||
tenantId: req.user?.tenantId,
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -415,7 +415,11 @@ async function createMCPTools({
|
|||
const serverConfig =
|
||||
config ?? (await getMCPServersRegistry().getServerConfig(serverName, user?.id, configServers));
|
||||
if (serverConfig?.url) {
|
||||
const appConfig = await getAppConfig({ role: user?.role, tenantId: user?.tenantId });
|
||||
const appConfig = await getAppConfig({
|
||||
role: user?.role,
|
||||
tenantId: user?.tenantId,
|
||||
userId: user?.id,
|
||||
});
|
||||
const allowedDomains = appConfig?.mcpSettings?.allowedDomains;
|
||||
const allowedAddresses = appConfig?.mcpSettings?.allowedAddresses;
|
||||
const isDomainAllowed = await isMCPDomainAllowed(
|
||||
|
|
@ -503,7 +507,11 @@ async function createMCPTool({
|
|||
const serverConfig =
|
||||
config ?? (await getMCPServersRegistry().getServerConfig(serverName, user?.id, configServers));
|
||||
if (serverConfig?.url) {
|
||||
const appConfig = await getAppConfig({ role: user?.role, tenantId: user?.tenantId });
|
||||
const appConfig = await getAppConfig({
|
||||
role: user?.role,
|
||||
tenantId: user?.tenantId,
|
||||
userId: user?.id,
|
||||
});
|
||||
const allowedDomains = appConfig?.mcpSettings?.allowedDomains;
|
||||
const allowedAddresses = appConfig?.mcpSettings?.allowedAddresses;
|
||||
const isDomainAllowed = await isMCPDomainAllowed(
|
||||
|
|
|
|||
|
|
@ -922,8 +922,12 @@ describe('User parameter passing tests', () => {
|
|||
// Should not call reinitMCPServer since domain check failed
|
||||
expect(mockReinitMCPServer).not.toHaveBeenCalled();
|
||||
|
||||
// Verify getAppConfig was called with user role
|
||||
expect(mockGetAppConfig).toHaveBeenCalledWith({ role: 'user' });
|
||||
// Verify getAppConfig was called with the user scope
|
||||
expect(mockGetAppConfig).toHaveBeenCalledWith({
|
||||
role: 'user',
|
||||
tenantId: undefined,
|
||||
userId: 'domain-test-user',
|
||||
});
|
||||
|
||||
// Verify domain validation was called with correct parameters
|
||||
expect(mockIsMCPDomainAllowed).toHaveBeenCalledWith(
|
||||
|
|
@ -971,8 +975,12 @@ describe('User parameter passing tests', () => {
|
|||
// Should create tool successfully
|
||||
expect(result).toBeDefined();
|
||||
|
||||
// Verify getAppConfig was called with user role
|
||||
expect(mockGetAppConfig).toHaveBeenCalledWith({ role: 'admin' });
|
||||
// Verify getAppConfig was called with the user scope
|
||||
expect(mockGetAppConfig).toHaveBeenCalledWith({
|
||||
role: 'admin',
|
||||
tenantId: undefined,
|
||||
userId: 'domain-test-user',
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip domain validation for stdio transports (no URL)', async () => {
|
||||
|
|
@ -1047,8 +1055,12 @@ describe('User parameter passing tests', () => {
|
|||
// Should not call reinitMCPServer since domain check failed early
|
||||
expect(mockReinitMCPServer).not.toHaveBeenCalled();
|
||||
|
||||
// Verify getAppConfig was called with user role
|
||||
expect(mockGetAppConfig).toHaveBeenCalledWith({ role: 'user' });
|
||||
// Verify getAppConfig was called with the user scope
|
||||
expect(mockGetAppConfig).toHaveBeenCalledWith({
|
||||
role: 'user',
|
||||
tenantId: undefined,
|
||||
userId: 'domain-test-user',
|
||||
});
|
||||
});
|
||||
|
||||
it('should use user role when fetching domain restrictions', async () => {
|
||||
|
|
@ -1100,9 +1112,17 @@ describe('User parameter passing tests', () => {
|
|||
availableTools,
|
||||
});
|
||||
|
||||
// Verify getAppConfig was called with correct roles
|
||||
expect(mockGetAppConfig).toHaveBeenNthCalledWith(1, { role: 'admin' });
|
||||
expect(mockGetAppConfig).toHaveBeenNthCalledWith(2, { role: 'user' });
|
||||
// Verify getAppConfig was called with the correct user scopes
|
||||
expect(mockGetAppConfig).toHaveBeenNthCalledWith(1, {
|
||||
role: 'admin',
|
||||
tenantId: undefined,
|
||||
userId: 'admin-user',
|
||||
});
|
||||
expect(mockGetAppConfig).toHaveBeenNthCalledWith(2, {
|
||||
role: 'user',
|
||||
tenantId: undefined,
|
||||
userId: 'regular-user',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,46 @@
|
|||
import { Types } from 'mongoose';
|
||||
import {
|
||||
AgentCapabilities,
|
||||
EModelEndpoint,
|
||||
PrincipalType,
|
||||
defaultAgentCapabilities,
|
||||
} from 'librechat-data-provider';
|
||||
import { createEndpointsConfigService } from './endpoints';
|
||||
import type { AppConfig } from '@librechat/data-schemas';
|
||||
|
||||
import type { AppConfig, IConfig } from '@librechat/data-schemas';
|
||||
import type { AppConfigServiceDeps } from '~/app/service';
|
||||
import type { EndpointsConfigDeps } from './endpoints';
|
||||
import type { ServerRequest } from '~/types';
|
||||
|
||||
import { createEndpointsConfigService } from './endpoints';
|
||||
import { createAppConfigService } from '~/app/service';
|
||||
|
||||
function appConfig(partial: Record<string, unknown>): AppConfig {
|
||||
return partial as unknown as AppConfig;
|
||||
}
|
||||
|
||||
function configDoc(partial: Record<string, unknown>): IConfig {
|
||||
return partial as unknown as IConfig;
|
||||
}
|
||||
|
||||
function createAppConfigCache() {
|
||||
const store = new Map<string, unknown>();
|
||||
return {
|
||||
get: jest.fn((key: string) => Promise.resolve(store.get(key))),
|
||||
set: jest.fn((key: string, value: unknown) => {
|
||||
store.set(key, value);
|
||||
return Promise.resolve(undefined);
|
||||
}),
|
||||
delete: jest.fn((key: string) => {
|
||||
store.delete(key);
|
||||
return Promise.resolve(true);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
type ConfigPrincipals = NonNullable<
|
||||
Parameters<AppConfigServiceDeps['getApplicableConfigs']>[0]
|
||||
>;
|
||||
|
||||
function createMockDeps(overrides: Partial<EndpointsConfigDeps> = {}): EndpointsConfigDeps {
|
||||
return {
|
||||
getAppConfig: jest.fn().mockResolvedValue(appConfig({ endpoints: {} })),
|
||||
|
|
@ -23,7 +52,13 @@ function createMockDeps(overrides: Partial<EndpointsConfigDeps> = {}): Endpoints
|
|||
};
|
||||
}
|
||||
|
||||
function fakeReq(overrides: Partial<ServerRequest> = {}): ServerRequest {
|
||||
type TestRequestOverrides = {
|
||||
body?: Partial<ServerRequest['body']>;
|
||||
config?: AppConfig;
|
||||
user?: { id?: string; role?: string; tenantId?: string };
|
||||
};
|
||||
|
||||
function fakeReq(overrides: TestRequestOverrides = {}): ServerRequest {
|
||||
return { user: { id: 'u1', role: 'USER' }, ...overrides } as ServerRequest;
|
||||
}
|
||||
|
||||
|
|
@ -172,6 +207,83 @@ describe('createEndpointsConfigService', () => {
|
|||
|
||||
expect(mockGetAppConfig).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes userId when resolving scoped endpoint config', async () => {
|
||||
const mockGetAppConfig = jest.fn().mockResolvedValue(appConfig({ endpoints: {} }));
|
||||
const deps = createMockDeps({ getAppConfig: mockGetAppConfig });
|
||||
const { getEndpointsConfig } = createEndpointsConfigService(deps);
|
||||
|
||||
await getEndpointsConfig(
|
||||
fakeReq({ user: { id: 'u1', role: 'USER', tenantId: 'tenant-a' } }),
|
||||
);
|
||||
|
||||
expect(mockGetAppConfig).toHaveBeenCalledWith({
|
||||
role: 'USER',
|
||||
userId: 'u1',
|
||||
tenantId: 'tenant-a',
|
||||
});
|
||||
});
|
||||
|
||||
it('exposes custom endpoints from group-scoped overrides for grouped users', async () => {
|
||||
const groupId = new Types.ObjectId('6a0aea2172e2d59d4658c9d2');
|
||||
const getUserPrincipals = jest.fn().mockResolvedValue([
|
||||
{ principalType: PrincipalType.ROLE, principalId: 'USER' },
|
||||
{ principalType: PrincipalType.USER, principalId: 'u1' },
|
||||
{ principalType: PrincipalType.GROUP, principalId: groupId },
|
||||
]);
|
||||
const getApplicableConfigs = jest.fn((principals: ConfigPrincipals = []) =>
|
||||
Promise.resolve(
|
||||
principals.some(
|
||||
(principal) =>
|
||||
principal.principalType === PrincipalType.GROUP &&
|
||||
String(principal.principalId) === groupId.toString(),
|
||||
)
|
||||
? [
|
||||
configDoc({
|
||||
principalType: PrincipalType.GROUP,
|
||||
principalId: groupId,
|
||||
priority: 20,
|
||||
isActive: true,
|
||||
overrides: {
|
||||
endpoints: {
|
||||
custom: [
|
||||
{
|
||||
name: 'FOO',
|
||||
apiKey: '${FOO_KEY}',
|
||||
baseURL: '${FOO_URL}',
|
||||
models: { fetch: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
]
|
||||
: [],
|
||||
),
|
||||
);
|
||||
const { getAppConfig } = createAppConfigService({
|
||||
loadBaseConfig: jest.fn().mockResolvedValue(appConfig({ endpoints: {} })),
|
||||
setCachedTools: jest.fn().mockResolvedValue(undefined),
|
||||
getCache: jest.fn().mockReturnValue(createAppConfigCache()),
|
||||
cacheKeys: { APP_CONFIG: 'app_config' },
|
||||
getApplicableConfigs,
|
||||
getUserPrincipals,
|
||||
});
|
||||
const { getEndpointsConfig } = createEndpointsConfigService({
|
||||
getAppConfig,
|
||||
loadDefaultEndpointsConfig: jest.fn().mockResolvedValue({}),
|
||||
});
|
||||
|
||||
const result = await getEndpointsConfig(fakeReq({ user: { id: 'u1', role: 'USER' } }));
|
||||
|
||||
expect(getUserPrincipals).toHaveBeenCalledWith({ userId: 'u1', role: 'USER' });
|
||||
expect(getApplicableConfigs).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ principalType: PrincipalType.GROUP, principalId: groupId }),
|
||||
]),
|
||||
);
|
||||
expect(result?.FOO).toEqual(expect.objectContaining({ type: EModelEndpoint.custom }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkCapability', () => {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,11 @@ type DefaultEndpointsResult = Record<string, PartialEndpointEntry | false | null
|
|||
type MutableEndpointsConfig = Record<string, PartialEndpointEntry | false | null | undefined>;
|
||||
|
||||
export interface EndpointsConfigDeps {
|
||||
getAppConfig: (params: { role?: string | null; tenantId?: string }) => Promise<AppConfig>;
|
||||
getAppConfig: (params: {
|
||||
role?: string;
|
||||
userId?: string;
|
||||
tenantId?: string;
|
||||
}) => Promise<AppConfig>;
|
||||
loadDefaultEndpointsConfig: (appConfig: AppConfig) => Promise<DefaultEndpointsResult>;
|
||||
loadCustomEndpointsConfig?: (custom: unknown) => TCustomEndpointsConfig | undefined;
|
||||
}
|
||||
|
|
@ -28,7 +32,12 @@ export function createEndpointsConfigService(deps: EndpointsConfigDeps) {
|
|||
|
||||
async function getEndpointsConfig(req: ServerRequest): Promise<TEndpointsConfig> {
|
||||
const appConfig =
|
||||
req.config ?? (await getAppConfig({ role: req.user?.role, tenantId: req.user?.tenantId }));
|
||||
req.config ??
|
||||
(await getAppConfig({
|
||||
role: req.user?.role,
|
||||
userId: req.user?.id,
|
||||
tenantId: req.user?.tenantId,
|
||||
}));
|
||||
const defaultEndpointsConfig = await loadDefaultEndpointsConfig(appConfig);
|
||||
const customEndpointsConfig = loadCustomEndpointsConfig(appConfig?.endpoints?.custom);
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,11 @@ interface ResolvedEndpoint {
|
|||
}
|
||||
|
||||
export interface LoadConfigModelsDeps {
|
||||
getAppConfig: (params: { role?: string | null; tenantId?: string }) => Promise<AppConfig>;
|
||||
getAppConfig: (params: {
|
||||
role?: string;
|
||||
userId?: string;
|
||||
tenantId?: string;
|
||||
}) => Promise<AppConfig>;
|
||||
getUserKeyValues: GetUserKeyValuesFunction;
|
||||
fetchModels?: (params: FetchModelsParams) => Promise<string[]>;
|
||||
}
|
||||
|
|
@ -31,10 +35,13 @@ export function createLoadConfigModels(deps: LoadConfigModelsDeps) {
|
|||
const { getAppConfig, getUserKeyValues, fetchModels = defaultFetchModels } = deps;
|
||||
|
||||
return async function loadConfigModels(req: ServerRequest): Promise<TModelsConfig> {
|
||||
const appConfig = await getAppConfig({
|
||||
role: req.user?.role,
|
||||
tenantId: req.user?.tenantId,
|
||||
});
|
||||
const appConfig =
|
||||
req.config ??
|
||||
(await getAppConfig({
|
||||
role: req.user?.role,
|
||||
userId: req.user?.id,
|
||||
tenantId: req.user?.tenantId,
|
||||
}));
|
||||
if (!appConfig) {
|
||||
return {};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -381,6 +381,34 @@ describe('createSetBalanceConfig', () => {
|
|||
// lastRefill should have default value from schema
|
||||
expect(balanceRecord?.lastRefill).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
test('should pass user identity when resolving balance config', async () => {
|
||||
const userId = new mongoose.Types.ObjectId();
|
||||
const getAppConfig = jest.fn().mockResolvedValue({
|
||||
balance: { enabled: false },
|
||||
});
|
||||
|
||||
const middleware = createSetBalanceConfig({
|
||||
getAppConfig,
|
||||
findBalanceByUser,
|
||||
upsertBalanceFields,
|
||||
});
|
||||
|
||||
const req = createMockRequest(userId);
|
||||
req.user = {
|
||||
...req.user,
|
||||
role: 'USER',
|
||||
tenantId: 'tenant-a',
|
||||
} as typeof req.user;
|
||||
|
||||
await middleware(req as ServerRequest, createMockResponse() as ServerResponse, mockNext);
|
||||
|
||||
expect(getAppConfig).toHaveBeenCalledWith({
|
||||
role: 'USER',
|
||||
userId: userId.toString(),
|
||||
tenantId: 'tenant-a',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Update Scenarios', () => {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { getBalanceConfig } from '~/app/config';
|
|||
export interface BalanceMiddlewareOptions {
|
||||
getAppConfig: (options?: {
|
||||
role?: string;
|
||||
userId?: string;
|
||||
tenantId?: string;
|
||||
refresh?: boolean;
|
||||
}) => Promise<AppConfig>;
|
||||
|
|
@ -119,6 +120,7 @@ export function createSetBalanceConfig({
|
|||
const user = req.user as IUser & { _id: string | ObjectId };
|
||||
const appConfig = await getAppConfig({
|
||||
role: user?.role,
|
||||
userId: user?.id,
|
||||
tenantId: user?.tenantId,
|
||||
});
|
||||
const balanceConfig = getBalanceConfig(appConfig);
|
||||
|
|
|
|||
|
|
@ -429,7 +429,11 @@ describe('createRemoteAgentAuth', () => {
|
|||
await createRemoteAgentAuth(deps)(req as Request, makeRes().res, mockNext);
|
||||
|
||||
expect(deps.getAppConfig).toHaveBeenNthCalledWith(1, { baseOnly: true });
|
||||
expect(deps.getAppConfig).toHaveBeenNthCalledWith(2, { tenantId: 'tenant-strict' });
|
||||
expect(deps.getAppConfig).toHaveBeenNthCalledWith(2, {
|
||||
role: 'user',
|
||||
userId: 'uid123',
|
||||
tenantId: 'tenant-strict',
|
||||
});
|
||||
expect(jwt.verify).toHaveBeenCalledTimes(2);
|
||||
expect((jwt.verify as jest.Mock).mock.calls[1][2]).toEqual(
|
||||
expect.objectContaining({ audience: 'tenant-audience' }),
|
||||
|
|
@ -461,7 +465,11 @@ describe('createRemoteAgentAuth', () => {
|
|||
await createRemoteAgentAuth(deps)(req as Request, res, mockNext);
|
||||
|
||||
expect(deps.getAppConfig).toHaveBeenNthCalledWith(1, { baseOnly: true });
|
||||
expect(deps.getAppConfig).toHaveBeenNthCalledWith(2, { tenantId: 'tenant-strict' });
|
||||
expect(deps.getAppConfig).toHaveBeenNthCalledWith(2, {
|
||||
role: 'user',
|
||||
userId: 'uid123',
|
||||
tenantId: 'tenant-strict',
|
||||
});
|
||||
expect(jwt.verify).toHaveBeenCalledTimes(2);
|
||||
expect(status).toHaveBeenCalledWith(401);
|
||||
expect(json).toHaveBeenCalledWith({ error: 'Unauthorized' });
|
||||
|
|
@ -493,7 +501,11 @@ describe('createRemoteAgentAuth', () => {
|
|||
await createRemoteAgentAuth(deps)(req as Request, res, mockNext);
|
||||
|
||||
expect(deps.getAppConfig).toHaveBeenNthCalledWith(1, { baseOnly: true });
|
||||
expect(deps.getAppConfig).toHaveBeenNthCalledWith(2, { tenantId: 'tenant-api-key-only' });
|
||||
expect(deps.getAppConfig).toHaveBeenNthCalledWith(2, {
|
||||
role: 'user',
|
||||
userId: 'uid123',
|
||||
tenantId: 'tenant-api-key-only',
|
||||
});
|
||||
expect(jwt.verify).toHaveBeenCalledTimes(1);
|
||||
expect(status).toHaveBeenCalledWith(401);
|
||||
expect(json).toHaveBeenCalledWith({ error: 'Unauthorized' });
|
||||
|
|
|
|||
|
|
@ -247,7 +247,7 @@ function getConfigOptions(req: Request): GetAppConfigOptions {
|
|||
}
|
||||
|
||||
function getUserConfigOptions(user: IUser): GetAppConfigOptions {
|
||||
if (user.tenantId) return { tenantId: user.tenantId };
|
||||
if (user.tenantId) return { role: user.role, userId: user.id, tenantId: user.tenantId };
|
||||
return { baseOnly: true };
|
||||
}
|
||||
|
||||
|
|
@ -255,6 +255,8 @@ function isResolvedUserConfigScope(initialOptions: GetAppConfigOptions, user: IU
|
|||
const userOptions = getUserConfigOptions(user);
|
||||
return (
|
||||
initialOptions.tenantId === userOptions.tenantId &&
|
||||
initialOptions.userId === userOptions.userId &&
|
||||
initialOptions.role === userOptions.role &&
|
||||
initialOptions.baseOnly === userOptions.baseOnly
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue