- Fix ObjectId comparison in getListAgentsHandler using .equals() method instead of strict equality

- Add findPubliclyAccessibleResources function to PermissionService for bulk public resource queries
  - Add hasPublicPermission function to PermissionService for individual resource public permission checks
  - Update getAgentHandler to use hasPublicPermission for accurate individual agent public status
  - Replace instanceProjectId-based global checks with isPublic property from backend in client code
  - Add isPublic property to Agent type definition
  - Add NODE_TLS_REJECT_UNAUTHORIZED debug setting to VS Code launch config
This commit is contained in:
Atef Bellaaj 2025-06-13 10:23:00 +02:00 committed by Danny Avila
parent 5979efd607
commit c9aa10d3d5
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
7 changed files with 102 additions and 12 deletions

3
.vscode/launch.json vendored
View file

@ -8,7 +8,8 @@
"skipFiles": ["<node_internals>/**"], "skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}/api/server/index.js", "program": "${workspaceFolder}/api/server/index.js",
"env": { "env": {
"NODE_ENV": "production" "NODE_ENV": "production",
"NODE_TLS_REJECT_UNAUTHORIZED": "0"
}, },
"console": "integratedTerminal", "console": "integratedTerminal",
"envFile": "${workspaceFolder}/.env" "envFile": "${workspaceFolder}/.env"

View file

@ -15,7 +15,12 @@ const {
deleteAgent, deleteAgent,
getListAgentsByAccess, getListAgentsByAccess,
} = require('~/models/Agent'); } = require('~/models/Agent');
const { grantPermission, findAccessibleResources } = require('~/server/services/PermissionService'); const {
grantPermission,
findAccessibleResources,
findPubliclyAccessibleResources,
hasPublicPermission,
} = require('~/server/services/PermissionService');
const { getStrategyFunctions } = require('~/server/services/Files/strategies'); const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { updateAgentProjects, revertAgentVersion } = require('~/models/Agent'); const { updateAgentProjects, revertAgentVersion } = require('~/models/Agent');
const { resizeAvatar } = require('~/server/services/Files/images/avatar'); const { resizeAvatar } = require('~/server/services/Files/images/avatar');
@ -134,6 +139,14 @@ const getAgentHandler = async (req, res, expandProperties = false) => {
// @deprecated - isCollaborative replaced by ACL permissions // @deprecated - isCollaborative replaced by ACL permissions
agent.isCollaborative = !!agent.isCollaborative; agent.isCollaborative = !!agent.isCollaborative;
// Check if agent is public
const isPublic = await hasPublicPermission({
resourceType: 'agent',
resourceId: agent._id,
requiredPermissions: PermissionBits.VIEW,
});
agent.isPublic = isPublic;
if (agent.author !== author) { if (agent.author !== author) {
delete agent.author; delete agent.author;
} }
@ -152,6 +165,7 @@ const getAgentHandler = async (req, res, expandProperties = false) => {
projectIds: agent.projectIds, projectIds: agent.projectIds,
// @deprecated - isCollaborative replaced by ACL permissions // @deprecated - isCollaborative replaced by ACL permissions
isCollaborative: agent.isCollaborative, isCollaborative: agent.isCollaborative,
isPublic: agent.isPublic,
version: agent.version, version: agent.version,
// Safe metadata // Safe metadata
createdAt: agent.createdAt, createdAt: agent.createdAt,
@ -392,14 +406,23 @@ const getListAgentsHandler = async (req, res) => {
resourceType: 'agent', resourceType: 'agent',
requiredPermissions: PermissionBits.VIEW, requiredPermissions: PermissionBits.VIEW,
}); });
const publiclyAccessibleIds = await findPubliclyAccessibleResources({
resourceType: 'agent',
requiredPermissions: PermissionBits.VIEW,
});
// Use the new ACL-aware function // Use the new ACL-aware function
const data = await getListAgentsByAccess({ const data = await getListAgentsByAccess({
accessibleIds, accessibleIds,
otherParams: {}, // Can add query params here if needed otherParams: {}, // Can add query params here if needed
}); });
if (data?.data?.length) {
data.data = data.data.map((agent) => {
if (publiclyAccessibleIds.some(id => id.equals(agent._id))) {
agent.isPublic = true;
}
return agent;
});
}
return res.json(data); return res.json(data);
} catch (error) { } catch (error) {
logger.error('[/Agents] Error listing Agents', error); logger.error('[/Agents] Error listing Agents', error);

View file

@ -17,6 +17,7 @@ const {
findAccessibleResources: findAccessibleResourcesACL, findAccessibleResources: findAccessibleResourcesACL,
hasPermission, hasPermission,
getEffectivePermissions: getEffectivePermissionsACL, getEffectivePermissions: getEffectivePermissionsACL,
findEntriesByPrincipalsAndResource,
} = require('~/models'); } = require('~/models');
const { AclEntry, AccessRole, Group } = require('~/db/models'); const { AclEntry, AccessRole, Group } = require('~/db/models');
@ -178,6 +179,37 @@ const findAccessibleResources = async ({ userId, resourceType, requiredPermissio
} }
}; };
/**
* Find all publicly accessible resources of a specific type
* @param {Object} params - Parameters for finding publicly accessible resources
* @param {string} params.resourceType - Type of resource (e.g., 'agent')
* @param {number} params.requiredPermissions - The minimum permission bits required (e.g., 1 for VIEW, 3 for VIEW+EDIT)
* @returns {Promise<Array>} Array of resource IDs
*/
const findPubliclyAccessibleResources = async ({ resourceType, requiredPermissions }) => {
try {
if (typeof requiredPermissions !== 'number' || requiredPermissions < 1) {
throw new Error('requiredPermissions must be a positive number');
}
// Find all public ACL entries where the public principal has at least the required permission bits
const entries = await AclEntry.find({
principalType: 'public',
resourceType,
permBits: { $bitsAllSet: requiredPermissions },
}).distinct('resourceId');
return entries;
} catch (error) {
logger.error(`[PermissionService.findPubliclyAccessibleResources] Error: ${error.message}`);
// Re-throw validation errors
if (error.message.includes('requiredPermissions must be')) {
throw error;
}
return [];
}
};
/** /**
* Get available roles for a resource type * Get available roles for a resource type
* @param {Object} params - Parameters for getting available roles * @param {Object} params - Parameters for getting available roles
@ -407,6 +439,41 @@ const syncUserEntraGroupMemberships = async (user, accessToken, session = null)
} }
}; };
/**
* Check if public has a specific permission on a resource
* @param {Object} params - Parameters for checking public permission
* @param {string} params.resourceType - Type of resource (e.g., 'agent')
* @param {string|mongoose.Types.ObjectId} params.resourceId - The ID of the resource
* @param {number} params.requiredPermissions - The permission bits required (e.g., 1 for VIEW, 3 for VIEW+EDIT)
* @returns {Promise<boolean>} Whether public has the required permission bits
*/
const hasPublicPermission = async ({ resourceType, resourceId, requiredPermissions }) => {
try {
if (typeof requiredPermissions !== 'number' || requiredPermissions < 1) {
throw new Error('requiredPermissions must be a positive number');
}
// Use public principal to check permissions
const publicPrincipal = [{ principalType: 'public' }];
const entries = await findEntriesByPrincipalsAndResource(
publicPrincipal,
resourceType,
resourceId,
);
// Check if any entry has the required permission bits
return entries.some(entry => (entry.permBits & requiredPermissions) === requiredPermissions);
} catch (error) {
logger.error(`[PermissionService.hasPublicPermission] Error: ${error.message}`);
// Re-throw validation errors
if (error.message.includes('requiredPermissions must be')) {
throw error;
}
return false;
}
};
/** /**
* Bulk update permissions for a resource (grant, update, revoke) * Bulk update permissions for a resource (grant, update, revoke)
* Efficiently handles multiple permission changes in a single transaction * Efficiently handles multiple permission changes in a single transaction
@ -615,6 +682,8 @@ module.exports = {
checkPermission, checkPermission,
getEffectivePermissions, getEffectivePermissions,
findAccessibleResources, findAccessibleResources,
findPubliclyAccessibleResources,
hasPublicPermission,
getAvailableRoles, getAvailableRoles,
bulkUpdateResourcePermissions, bulkUpdateResourcePermissions,
ensurePrincipalExists, ensurePrincipalExists,

View file

@ -43,9 +43,7 @@ export default function AgentSelect({
const resetAgentForm = useCallback( const resetAgentForm = useCallback(
(fullAgent: Agent) => { (fullAgent: Agent) => {
const { instanceProjectId } = startupConfig ?? {}; const isGlobal = fullAgent.isPublic ?? false;
const isGlobal =
(instanceProjectId != null && fullAgent.projectIds?.includes(instanceProjectId)) ?? false;
const update = { const update = {
...fullAgent, ...fullAgent,
provider: createProviderOption(fullAgent.provider), provider: createProviderOption(fullAgent.provider),

View file

@ -125,8 +125,7 @@ export const useEndpoints = ({
if (ep === EModelEndpoint.agents && agents.length > 0) { if (ep === EModelEndpoint.agents && agents.length > 0) {
result.models = agents.map((agent) => ({ result.models = agents.map((agent) => ({
name: agent.id, name: agent.id,
isGlobal: isGlobal: agent.isPublic ?? false,
(instanceProjectId != null && agent.projectIds?.includes(instanceProjectId)) ?? false,
})); }));
result.agentNames = agents.reduce((acc, agent) => { result.agentNames = agents.reduce((acc, agent) => {
acc[agent.id] = agent.name || ''; acc[agent.id] = agent.name || '';

View file

@ -63,8 +63,7 @@ export const processAgentOption = ({
fileMap?: Record<string, TFile | undefined>; fileMap?: Record<string, TFile | undefined>;
instanceProjectId?: string; instanceProjectId?: string;
}): TAgentOption => { }): TAgentOption => {
const isGlobal = const isGlobal = _agent?.isPublic ?? false;
(instanceProjectId != null && _agent?.projectIds?.includes(instanceProjectId)) ?? false;
const agent: TAgentOption = { const agent: TAgentOption = {
...(_agent ?? ({} as Agent)), ...(_agent ?? ({} as Agent)),
label: _agent?.name ?? '', label: _agent?.name ?? '',

View file

@ -226,6 +226,7 @@ export type Agent = {
hide_sequential_outputs?: boolean; hide_sequential_outputs?: boolean;
artifacts?: ArtifactModes; artifacts?: ArtifactModes;
recursion_limit?: number; recursion_limit?: number;
isPublic?: boolean;
version?: number; version?: number;
}; };