diff --git a/packages/openchoreo-client-node/openapi/openchoreo-api.yaml b/packages/openchoreo-client-node/openapi/openchoreo-api.yaml index 9254c41f..7fb37f27 100644 --- a/packages/openchoreo-client-node/openapi/openchoreo-api.yaml +++ b/packages/openchoreo-client-node/openapi/openchoreo-api.yaml @@ -1441,7 +1441,7 @@ components: RoleEntitlementMapping: type: object required: - - role_name + - role - entitlement - hierarchy - effect @@ -1449,8 +1449,8 @@ components: id: type: integer description: Unique identifier for the mapping - role_name: - type: string + role: + $ref: '#/components/schemas/RoleRef' entitlement: $ref: '#/components/schemas/Entitlement' hierarchy: @@ -1460,6 +1460,18 @@ components: context: type: object + RoleRef: + type: object + required: + - name + properties: + name: + type: string + description: Name of the role + namespace: + type: string + description: Optional namespace for the role + UpdateRoleRequest: type: object required: diff --git a/packages/openchoreo-client-node/src/generated/openchoreo/types.ts b/packages/openchoreo-client-node/src/generated/openchoreo/types.ts index 6cc0d1dd..8be04133 100644 --- a/packages/openchoreo-client-node/src/generated/openchoreo/types.ts +++ b/packages/openchoreo-client-node/src/generated/openchoreo/types.ts @@ -1559,12 +1559,18 @@ export interface components { RoleEntitlementMapping: { /** @description Unique identifier for the mapping */ id?: number; - role_name: string; + role: components['schemas']['RoleRef']; entitlement: components['schemas']['Entitlement']; hierarchy: components['schemas']['AuthzResourceHierarchy']; effect: components['schemas']['PolicyEffectType']; context?: Record; }; + RoleRef: { + /** @description Name of the role */ + name: string; + /** @description Optional namespace for the role */ + namespace?: string; + }; UpdateRoleRequest: { /** @description List of actions to assign to the role */ actions: string[]; diff --git a/plugins/openchoreo-backend/src/router.ts b/plugins/openchoreo-backend/src/router.ts index 1d362a18..5c85654c 100644 --- a/plugins/openchoreo-backend/src/router.ts +++ b/plugins/openchoreo-backend/src/router.ts @@ -804,9 +804,9 @@ export async function createRouter({ router.post('/authz/role-mappings', requireAuth, async (req, res) => { const mapping = req.body; - if (!mapping || !mapping.role_name || !mapping.entitlement) { + if (!mapping || !mapping.role.name || !mapping.entitlement) { throw new InputError( - 'Mapping must have role_name and entitlement fields', + 'Mapping must have role name and entitlement fields', ); } const userToken = getUserTokenFromRequest(req); @@ -822,9 +822,9 @@ export async function createRouter({ throw new InputError('Invalid mapping ID'); } const mapping = req.body; - if (!mapping || !mapping.role_name || !mapping.entitlement) { + if (!mapping || !mapping.role.name || !mapping.entitlement) { throw new InputError( - 'Mapping must have role_name and entitlement fields', + 'Mapping must have role name and entitlement fields', ); } const userToken = getUserTokenFromRequest(req); diff --git a/plugins/openchoreo-backend/src/services/AuthzService/AuthzService.ts b/plugins/openchoreo-backend/src/services/AuthzService/AuthzService.ts index 446870c8..faea4968 100644 --- a/plugins/openchoreo-backend/src/services/AuthzService/AuthzService.ts +++ b/plugins/openchoreo-backend/src/services/AuthzService/AuthzService.ts @@ -291,7 +291,7 @@ export class AuthzService { mapping: RoleEntitlementMapping, userToken?: string, ): Promise<{ data: RoleEntitlementMapping }> { - this.logger.debug(`Creating role mapping for role: ${mapping.role_name}`); + this.logger.debug(`Creating role mapping for role: ${mapping.role?.name}`); try { const client = this.createClient(userToken); @@ -313,7 +313,7 @@ export class AuthzService { const mappingResponse = data as RoleMappingResponse; this.logger.debug( - `Successfully created role mapping for role: ${mapping.role_name}`, + `Successfully created role mapping for role: ${mapping.role.name}`, ); return { data: mappingResponse.data! }; diff --git a/plugins/openchoreo/src/api/OpenChoreoClientApi.ts b/plugins/openchoreo/src/api/OpenChoreoClientApi.ts index f679700a..56bbd7fc 100644 --- a/plugins/openchoreo/src/api/OpenChoreoClientApi.ts +++ b/plugins/openchoreo/src/api/OpenChoreoClientApi.ts @@ -106,7 +106,7 @@ export interface Entitlement { } export interface ResourceHierarchy { - organization?: string; + namespace?: string; organization_units?: string[]; project?: string; component?: string; @@ -116,13 +116,18 @@ export type PolicyEffect = 'allow' | 'deny'; export interface RoleEntitlementMapping { id?: number; - role_name: string; + role: RoleEntitlementMappingRoleRef; entitlement: Entitlement; hierarchy: ResourceHierarchy; effect: PolicyEffect; context?: Record; } +export interface RoleEntitlementMappingRoleRef { + name: string; + namespace?: string; +} + /** Filters for listing role mappings */ export interface RoleMappingFilters { role?: string; diff --git a/plugins/openchoreo/src/components/AccessControl/MappingsTab/MappingDialog.tsx b/plugins/openchoreo/src/components/AccessControl/MappingsTab/MappingDialog.tsx index 69ae2219..a5bc0b56 100644 --- a/plugins/openchoreo/src/components/AccessControl/MappingsTab/MappingDialog.tsx +++ b/plugins/openchoreo/src/components/AccessControl/MappingsTab/MappingDialog.tsx @@ -95,16 +95,16 @@ export const MappingDialog = ({ ); setWizardState({ - selectedRole: editingMapping.role_name, + selectedRole: editingMapping.role.name, subjectType: matchingUserType?.type || userTypes[0]?.type || '', entitlementValue: editingMapping.entitlement.value, scopeType: - editingMapping.hierarchy.organization || + editingMapping.hierarchy.namespace || editingMapping.hierarchy.project || editingMapping.hierarchy.component ? 'specific' : 'global', - organization: editingMapping.hierarchy.organization || '', + organization: editingMapping.hierarchy.namespace || '', orgUnits: editingMapping.hierarchy.organization_units || [], project: editingMapping.hierarchy.project || '', component: editingMapping.hierarchy.component || '', @@ -165,7 +165,7 @@ export const MappingDialog = ({ setError(null); const mapping: RoleEntitlementMapping = { - role_name: wizardState.selectedRole, + role: { name: wizardState.selectedRole }, entitlement: { claim: entitlementClaim, value: wizardState.entitlementValue.trim(), @@ -174,7 +174,7 @@ export const MappingDialog = ({ wizardState.scopeType === 'global' ? {} : { - organization: wizardState.organization || undefined, + namespace: wizardState.organization || undefined, organization_units: wizardState.orgUnits.filter(u => u.trim()) || undefined, project: wizardState.project || undefined, diff --git a/plugins/openchoreo/src/components/AccessControl/MappingsTab/MappingsTab.tsx b/plugins/openchoreo/src/components/AccessControl/MappingsTab/MappingsTab.tsx index 184523eb..7d1e0b3c 100644 --- a/plugins/openchoreo/src/components/AccessControl/MappingsTab/MappingsTab.tsx +++ b/plugins/openchoreo/src/components/AccessControl/MappingsTab/MappingsTab.tsx @@ -121,7 +121,7 @@ const useStyles = makeStyles(theme => ({ const formatHierarchy = (hierarchy: ResourceHierarchy): string => { const parts: string[] = []; - if (hierarchy.organization) parts.push(`org/${hierarchy.organization}`); + if (hierarchy.namespace) parts.push(`org/${hierarchy.namespace}`); if (hierarchy.organization_units?.length) { parts.push(`units/${hierarchy.organization_units.join('/')}`); } @@ -228,7 +228,7 @@ export const MappingsTab = () => { if (searchQuery) { const query = searchQuery.toLowerCase(); const searchFields = [ - mapping.role_name, + mapping.role.name, mapping.entitlement.claim, mapping.entitlement.value, formatHierarchy(mapping.hierarchy), @@ -264,7 +264,7 @@ export const MappingsTab = () => { try { await deleteMapping(mappingToDelete.id); notification.showSuccess( - `Mapping for role "${mappingToDelete.role_name}" deleted successfully`, + `Mapping for role "${mappingToDelete.role.name}" deleted successfully`, ); setDeleteConfirmOpen(false); setMappingToDelete(null); @@ -504,9 +504,9 @@ export const MappingsTab = () => { {filteredMappings.map((mapping, index) => ( - + - {mapping.role_name} + {mapping.role.name} @@ -585,7 +585,7 @@ export const MappingsTab = () => { {mappingToDelete && ( <> Are you sure you want to delete the mapping for role   - {mappingToDelete.role_name} with entitlement + {mappingToDelete.role.name} with entitlement   {mappingToDelete.entitlement.claim}= diff --git a/plugins/permission-backend-module-openchoreo-policy/src/rules/matchesCapability.ts b/plugins/permission-backend-module-openchoreo-policy/src/rules/matchesCapability.ts index 9f436303..6124ef2e 100644 --- a/plugins/permission-backend-module-openchoreo-policy/src/rules/matchesCapability.ts +++ b/plugins/permission-backend-module-openchoreo-policy/src/rules/matchesCapability.ts @@ -28,7 +28,7 @@ export const openchoreoComponentResourceRef = createPermissionResourceRef< const paramsSchema = z.object({ /** The OpenChoreo action to check (e.g., 'component:deploy') */ action: z.string(), - /** Allowed paths from user's capabilities (e.g., ['org/*', 'org/project/*']) */ + /** Allowed paths from user's capabilities (e.g., ['ns/*', 'ns/project/*']) */ allowedPaths: z.array(z.string()), /** Denied paths from user's capabilities */ deniedPaths: z.array(z.string()), @@ -39,27 +39,28 @@ export type MatchesCapabilityParams = z.infer; /** * Parses capability path from backend format. * - * Backend format: "org/{orgName}/project/{projectName}/component/{componentName}" - * or wildcards like "org/*", "org/{orgName}/project/*", etc. + * Backend format: "ns/{namespaceName}/project/{projectName}/component/{componentName}" + * or wildcards like "ns/*", "ns/{namespaceName}/project/*", etc. * - * Returns parsed { org, project, component } values. + * Returns parsed { namespace, project, component } values. */ function parseCapabilityPath(path: string): { - org?: string; + namespace?: string; project?: string; component?: string; } { // Handle global wildcard if (path === '*') { - return { org: '*', project: '*', component: '*' }; + return { namespace: '*', project: '*', component: '*' }; } - const result: { org?: string; project?: string; component?: string } = {}; + const result: { namespace?: string; project?: string; component?: string } = + {}; - // Parse org/orgName pattern - const orgMatch = path.match(/^org\/([^/]+)/); - if (orgMatch) { - result.org = orgMatch[1]; + // Parse namespace/namespaceName pattern + const namespaceMatch = path.match(/^ns\/([^/]+)/); + if (namespaceMatch) { + result.namespace = namespaceMatch[1]; } // Parse project/projectName pattern @@ -82,13 +83,13 @@ function parseCapabilityPath(path: string): { * * Paths from backend are in format: * - "*" - matches everything - * - "org/{orgName}/*" - matches all resources in the org - * - "org/{orgName}/project/{projectName}/*" - matches all resources in the project - * - "org/{orgName}/project/{projectName}/component/{componentName}" - matches specific component + * - "ns/{namespaceName}/*" - matches all resources in the namespace + * - "ns/{namespaceName}/project/{projectName}/*" - matches all resources in the project + * - "ns/{namespaceName}/project/{projectName}/component/{componentName}" - matches specific component */ function matchesScope( path: string, - scope: { org?: string; project?: string; component?: string }, + scope: { namespace?: string; project?: string; component?: string }, ): boolean { // Wildcard matches everything if (path === '*') { @@ -97,13 +98,17 @@ function matchesScope( const parsed = parseCapabilityPath(path); - // Check organization - if (parsed.org && parsed.org !== '*' && parsed.org !== scope.org) { + // Check namespace + if ( + parsed.namespace && + parsed.namespace !== '*' && + parsed.namespace !== scope.namespace + ) { return false; } - // If org is wildcard or path only specifies org, it matches - if (parsed.org === '*' || (!parsed.project && !parsed.component)) { + // If namespace is wildcard or path only specifies namespace, it matches + if (parsed.namespace === '*' || (!parsed.project && !parsed.component)) { return true; } @@ -137,7 +142,7 @@ function matchesScope( * Permission rule that checks if a user's OpenChoreo capabilities * allow a specific action on a catalog entity. * - * The rule extracts the scope (org/project/component) from entity + * The rule extracts the scope (namespace/project/component) from entity * annotations and matches it against the user's capability patterns. */ export const matchesCapability = createPermissionRule({ @@ -150,17 +155,19 @@ export const matchesCapability = createPermissionRule({ const { allowedPaths, deniedPaths } = params; // Extract scope from entity annotations - const org = entity.metadata.annotations?.[CHOREO_ANNOTATIONS.ORGANIZATION]; + // TODO: need to handle annotation change from org to namespace + const namespace = + entity.metadata.annotations?.[CHOREO_ANNOTATIONS.ORGANIZATION]; const project = entity.metadata.annotations?.[CHOREO_ANNOTATIONS.PROJECT]; const component = entity.metadata.annotations?.[CHOREO_ANNOTATIONS.COMPONENT]; - // If no org annotation, we can't check - deny - if (!org) { + // If no namespace annotation, we can't check - deny + if (!namespace) { return false; } - const scope = { org, project, component }; + const scope = { namespace, project, component }; // Check if explicitly denied at this scope for (const deniedPath of deniedPaths) {