diff --git a/frontend/src/NavigationPath.tsx b/frontend/src/NavigationPath.tsx index 5476738a054..a1d7b5a1972 100644 --- a/frontend/src/NavigationPath.tsx +++ b/frontend/src/NavigationPath.tsx @@ -205,6 +205,9 @@ export enum NavigationPath { identitiesServiceAccountsRoleAssignments = '/multicloud/user-management/identities/service-accounts/:id/role-assignments', identitiesServiceAccountsGroups = '/multicloud/user-management/identities/service-accounts/:id/groups', + // TODO: Move Permissions to proper location when ready + identitiesPermissions = '/multicloud/user-management/identities/permissions', + // RBAC Roles roles = '/multicloud/user-management/roles', rolesDetails = '/multicloud/user-management/roles/:id', diff --git a/frontend/src/resources/kubernetes-client.ts b/frontend/src/resources/kubernetes-client.ts index 4f136ce577f..802f66dbb25 100644 --- a/frontend/src/resources/kubernetes-client.ts +++ b/frontend/src/resources/kubernetes-client.ts @@ -1,6 +1,6 @@ /* Copyright Contributors to the Open Cluster Management project */ import { GroupKindType, ServiceAccountKindType, UserKindType } from './rbac' -export interface PolicyRule { +export interface Rule { verbs: string[] apiGroups: string[] resources: string[] diff --git a/frontend/src/resources/rbac.ts b/frontend/src/resources/rbac.ts index 5207cff4bb7..263aff9cffb 100644 --- a/frontend/src/resources/rbac.ts +++ b/frontend/src/resources/rbac.ts @@ -2,7 +2,7 @@ import { Metadata } from './metadata' import { IResourceDefinition } from './resource' import { listResources } from './utils/resource-request' -import { PolicyRule, LocalObjectReference, Subject, RoleRef } from './kubernetes-client' +import { Rule, LocalObjectReference, Subject, RoleRef } from './kubernetes-client' import { ObjectReference } from '@openshift-console/dynamic-plugin-sdk' export const UserApiVersion = 'user.openshift.io/v1' @@ -89,7 +89,7 @@ export interface ClusterRole { apiVersion: RbacApiVersionType kind: ClusterRoleKindType metadata: Metadata - rules: PolicyRule[] + rules: Rule[] } export interface ClusterRoleBinding { @@ -104,7 +104,7 @@ export interface Role { apiVersion: RbacApiVersionType kind: RoleKindType metadata: Metadata - rules: PolicyRule[] + rules: Rule[] } export interface RoleBinding { diff --git a/frontend/src/routes/UserManagement/Identities/IdentitiesManagement.tsx b/frontend/src/routes/UserManagement/Identities/IdentitiesManagement.tsx index d89681c45ee..52e2e1a2eb9 100644 --- a/frontend/src/routes/UserManagement/Identities/IdentitiesManagement.tsx +++ b/frontend/src/routes/UserManagement/Identities/IdentitiesManagement.tsx @@ -5,6 +5,7 @@ import IdentitiesPage from './IdentitiesPage' import { Users } from './Users/Users' import { Groups } from './Groups/Groups' import { ServiceAccounts } from './ServiceAccounts/ServiceAccounts' +import { Permissions } from './Permissions/Permissions' import { UserDetail } from './Users/UserDetail' import { UserYaml } from './Users/UserYaml' import { UserRoleAssignments } from './Users/UserRoleAssignments' @@ -64,6 +65,7 @@ export default function IdentitiesManagement() { } /> } /> } /> + } /> {/* Default redirect to users */} diff --git a/frontend/src/routes/UserManagement/Identities/IdentitiesPage.tsx b/frontend/src/routes/UserManagement/Identities/IdentitiesPage.tsx index 07f04b60292..a9e0170b470 100644 --- a/frontend/src/routes/UserManagement/Identities/IdentitiesPage.tsx +++ b/frontend/src/routes/UserManagement/Identities/IdentitiesPage.tsx @@ -11,6 +11,7 @@ export default function IdentitiesPage() { const isUsersActive = location.pathname.startsWith(NavigationPath.identitiesUsers) const isGroupsActive = location.pathname.startsWith(NavigationPath.identitiesGroups) const isServiceAccountsActive = location.pathname.startsWith(NavigationPath.identitiesServiceAccounts) + const isPermissionsActive = location.pathname.startsWith(NavigationPath.identitiesPermissions) return ( {t('Service Accounts')} + + {t('Permissions')} + } /> diff --git a/frontend/src/routes/UserManagement/Identities/Permissions/Permissions.tsx b/frontend/src/routes/UserManagement/Identities/Permissions/Permissions.tsx new file mode 100644 index 00000000000..44900ab6466 --- /dev/null +++ b/frontend/src/routes/UserManagement/Identities/Permissions/Permissions.tsx @@ -0,0 +1,96 @@ +/* Copyright Contributors to the Open Cluster Management project */ + +import { PageSection, Label, Flex, FlexItem } from '@patternfly/react-core' +import { cellWidth } from '@patternfly/react-table' +import { useMemo } from 'react' +import { useTranslation } from '../../../../lib/acm-i18next' +import { AcmTable, IAcmTableColumn, compareStrings } from '../../../../ui-components' +import { ClusterRole } from '../../../../resources/rbac' +import { Rule } from '../../../../resources/kubernetes-client' +import clusterRoleData from './mock-data/kubevirt.io:admin.json' + +const clusterRole = clusterRoleData as ClusterRole +const rules: Rule[] = clusterRole.rules + +const blacklist = ['ASS', 'FART'] +export const kindToAbbreviation = (kind: string) => { + const abbreviatedKind = (kind.replace(/[^A-Z]/g, '') || kind.toUpperCase()).slice(0, 4) + return blacklist.includes(abbreviatedKind) ? abbreviatedKind.slice(0, -1) : abbreviatedKind +} + +export function Permissions() { + const { t } = useTranslation() + + const columns = useMemo[]>( + () => [ + { + id: 'actions', + header: t('Actions'), + sort: (a: Rule, b: Rule) => compareStrings(a.verbs.join(', '), b.verbs.join(', ')), + search: 'verbs', + cell: (item) => { + return ( +
+ {item.verbs.map((verb, index) => ( +
+ {verb} +
+ ))} +
+ ) + }, + transforms: [cellWidth(15)], + }, + { + id: 'apiGroups', + header: t('API groups'), + sort: (a: Rule, b: Rule) => compareStrings(a.apiGroups.join(', '), b.apiGroups.join(', ')), + search: 'apiGroups', + cell: (item) => { + return item.apiGroups.length > 0 ? item.apiGroups.join(', ') : '' + }, + transforms: [cellWidth(25)], + }, + { + id: 'resources', + header: t('Resources'), + sort: (a: Rule, b: Rule) => compareStrings(a.resources.join(', '), b.resources.join(', ')), + search: 'resources', + cell: (item) => { + return ( + + {item.resources.map((resource, index) => ( + + {' '} + {resource} + + ))} + + ) + }, + transforms: [cellWidth(60)], + }, + ], + [t] + ) + + const keyFn = (rule: Rule) => `rule-${rules.indexOf(rule)}` + + return ( + + + id="permissions-table" + key="permissions-table" + columns={columns} + keyFn={keyFn} + items={rules} + emptyState={
{t('No permissions found')}
} + autoHidePagination={true} + initialPerPage={100} + fuseThreshold={0.1} + /> +
+ ) +} diff --git a/frontend/src/routes/UserManagement/Identities/Permissions/mock-data/kubevirt.io:admin.json b/frontend/src/routes/UserManagement/Identities/Permissions/mock-data/kubevirt.io:admin.json new file mode 100644 index 00000000000..83dbc81cdca --- /dev/null +++ b/frontend/src/routes/UserManagement/Identities/Permissions/mock-data/kubevirt.io:admin.json @@ -0,0 +1,224 @@ +{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "creationTimestamp": "2025-07-31T02:03:00Z", + "labels": { + "rbac.open-cluster-management.io/filter": "vm-clusterroles" + }, + "name": "kubevirt.io:admin", + "resourceVersion": "1270329", + "uid": "53e9b312-c662-475e-8531-7894596ef26e" + }, + "rules": [ + { + "apiGroups": [ + "subresources.kubevirt.io" + ], + "resources": [ + "virtualmachineinstances/console", + "virtualmachineinstances/vnc", + "virtualmachineinstances/vnc/screenshot", + "virtualmachineinstances/portforward", + "virtualmachineinstances/guestosinfo", + "virtualmachineinstances/filesystemlist", + "virtualmachineinstances/userlist", + "virtualmachineinstances/sev/fetchcertchain", + "virtualmachineinstances/sev/querylaunchmeasurement", + "virtualmachineinstances/usbredir" + ], + "verbs": [ + "get" + ] + }, + { + "apiGroups": [ + "subresources.kubevirt.io" + ], + "resources": [ + "virtualmachineinstances/pause", + "virtualmachineinstances/unpause", + "virtualmachineinstances/addvolume", + "virtualmachineinstances/removevolume", + "virtualmachineinstances/freeze", + "virtualmachineinstances/unfreeze", + "virtualmachineinstances/softreboot", + "virtualmachineinstances/sev/setupsession", + "virtualmachineinstances/sev/injectlaunchsecret" + ], + "verbs": [ + "update" + ] + }, + { + "apiGroups": [ + "subresources.kubevirt.io" + ], + "resources": [ + "virtualmachines/expand-spec", + "virtualmachines/portforward" + ], + "verbs": [ + "get" + ] + }, + { + "apiGroups": [ + "subresources.kubevirt.io" + ], + "resources": [ + "virtualmachines/start", + "virtualmachines/stop", + "virtualmachines/restart", + "virtualmachines/addvolume", + "virtualmachines/removevolume", + "virtualmachines/migrate", + "virtualmachines/memorydump" + ], + "verbs": [ + "update" + ] + }, + { + "apiGroups": [ + "subresources.kubevirt.io" + ], + "resources": [ + "expand-vm-spec" + ], + "verbs": [ + "update" + ] + }, + { + "apiGroups": [ + "kubevirt.io" + ], + "resources": [ + "virtualmachines", + "virtualmachineinstances", + "virtualmachineinstancepresets", + "virtualmachineinstancereplicasets", + "virtualmachineinstancemigrations" + ], + "verbs": [ + "get", + "delete", + "create", + "update", + "patch", + "list", + "watch", + "deletecollection" + ] + }, + { + "apiGroups": [ + "snapshot.kubevirt.io" + ], + "resources": [ + "virtualmachinesnapshots", + "virtualmachinesnapshotcontents", + "virtualmachinerestores" + ], + "verbs": [ + "get", + "delete", + "create", + "update", + "patch", + "list", + "watch", + "deletecollection" + ] + }, + { + "apiGroups": [ + "export.kubevirt.io" + ], + "resources": [ + "virtualmachineexports" + ], + "verbs": [ + "get", + "delete", + "create", + "update", + "patch", + "list", + "watch", + "deletecollection" + ] + }, + { + "apiGroups": [ + "clone.kubevirt.io" + ], + "resources": [ + "virtualmachineclones" + ], + "verbs": [ + "get", + "delete", + "create", + "update", + "patch", + "list", + "watch", + "deletecollection" + ] + }, + { + "apiGroups": [ + "instancetype.kubevirt.io" + ], + "resources": [ + "virtualmachineinstancetypes", + "virtualmachineclusterinstancetypes", + "virtualmachinepreferences", + "virtualmachineclusterpreferences" + ], + "verbs": [ + "get", + "delete", + "create", + "update", + "patch", + "list", + "watch", + "deletecollection" + ] + }, + { + "apiGroups": [ + "pool.kubevirt.io" + ], + "resources": [ + "virtualmachinepools" + ], + "verbs": [ + "get", + "delete", + "create", + "update", + "patch", + "list", + "watch", + "deletecollection" + ] + }, + { + "apiGroups": [ + "migrations.kubevirt.io" + ], + "resources": [ + "migrationpolicies" + ], + "verbs": [ + "get", + "list", + "watch" + ] + } + ] +} diff --git a/frontend/src/routes/UserManagement/Identities/Permissions/mock-data/kubevirt.io:edit.json b/frontend/src/routes/UserManagement/Identities/Permissions/mock-data/kubevirt.io:edit.json new file mode 100644 index 00000000000..c0d1d0b0456 --- /dev/null +++ b/frontend/src/routes/UserManagement/Identities/Permissions/mock-data/kubevirt.io:edit.json @@ -0,0 +1,230 @@ +{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "creationTimestamp": "2025-07-31T02:03:01Z", + "labels": { + "rbac.open-cluster-management.io/filter": "vm-clusterroles" + }, + "name": "kubevirt.io:edit", + "resourceVersion": "1270331", + "uid": "f03e4846-9bc0-49a3-b288-c40d098b35df" + }, + "rules": [ + { + "apiGroups": [ + "subresources.kubevirt.io" + ], + "resources": [ + "virtualmachineinstances/console", + "virtualmachineinstances/vnc", + "virtualmachineinstances/vnc/screenshot", + "virtualmachineinstances/portforward", + "virtualmachineinstances/guestosinfo", + "virtualmachineinstances/filesystemlist", + "virtualmachineinstances/userlist", + "virtualmachineinstances/sev/fetchcertchain", + "virtualmachineinstances/sev/querylaunchmeasurement", + "virtualmachineinstances/usbredir" + ], + "verbs": [ + "get" + ] + }, + { + "apiGroups": [ + "subresources.kubevirt.io" + ], + "resources": [ + "virtualmachineinstances/pause", + "virtualmachineinstances/unpause", + "virtualmachineinstances/addvolume", + "virtualmachineinstances/removevolume", + "virtualmachineinstances/freeze", + "virtualmachineinstances/unfreeze", + "virtualmachineinstances/softreboot", + "virtualmachineinstances/sev/setupsession", + "virtualmachineinstances/sev/injectlaunchsecret" + ], + "verbs": [ + "update" + ] + }, + { + "apiGroups": [ + "subresources.kubevirt.io" + ], + "resources": [ + "virtualmachines/expand-spec", + "virtualmachines/portforward" + ], + "verbs": [ + "get" + ] + }, + { + "apiGroups": [ + "subresources.kubevirt.io" + ], + "resources": [ + "virtualmachines/start", + "virtualmachines/stop", + "virtualmachines/restart", + "virtualmachines/addvolume", + "virtualmachines/removevolume", + "virtualmachines/migrate", + "virtualmachines/memorydump" + ], + "verbs": [ + "update" + ] + }, + { + "apiGroups": [ + "subresources.kubevirt.io" + ], + "resources": [ + "expand-vm-spec" + ], + "verbs": [ + "update" + ] + }, + { + "apiGroups": [ + "kubevirt.io" + ], + "resources": [ + "virtualmachines", + "virtualmachineinstances", + "virtualmachineinstancepresets", + "virtualmachineinstancereplicasets", + "virtualmachineinstancemigrations" + ], + "verbs": [ + "get", + "delete", + "create", + "update", + "patch", + "list", + "watch" + ] + }, + { + "apiGroups": [ + "snapshot.kubevirt.io" + ], + "resources": [ + "virtualmachinesnapshots", + "virtualmachinesnapshotcontents", + "virtualmachinerestores" + ], + "verbs": [ + "get", + "delete", + "create", + "update", + "patch", + "list", + "watch" + ] + }, + { + "apiGroups": [ + "export.kubevirt.io" + ], + "resources": [ + "virtualmachineexports" + ], + "verbs": [ + "get", + "delete", + "create", + "update", + "patch", + "list", + "watch" + ] + }, + { + "apiGroups": [ + "clone.kubevirt.io" + ], + "resources": [ + "virtualmachineclones" + ], + "verbs": [ + "get", + "delete", + "create", + "update", + "patch", + "list", + "watch" + ] + }, + { + "apiGroups": [ + "instancetype.kubevirt.io" + ], + "resources": [ + "virtualmachineinstancetypes", + "virtualmachineclusterinstancetypes", + "virtualmachinepreferences", + "virtualmachineclusterpreferences" + ], + "verbs": [ + "get", + "delete", + "create", + "update", + "patch", + "list", + "watch" + ] + }, + { + "apiGroups": [ + "pool.kubevirt.io" + ], + "resources": [ + "virtualmachinepools" + ], + "verbs": [ + "get", + "delete", + "create", + "update", + "patch", + "list", + "watch" + ] + }, + { + "apiGroups": [ + "kubevirt.io" + ], + "resources": [ + "kubevirts" + ], + "verbs": [ + "get", + "list" + ] + }, + { + "apiGroups": [ + "migrations.kubevirt.io" + ], + "resources": [ + "migrationpolicies" + ], + "verbs": [ + "get", + "list", + "watch" + ] + } + ] +} diff --git a/frontend/src/routes/UserManagement/Identities/Permissions/mock-data/kubevirt.io:view.json b/frontend/src/routes/UserManagement/Identities/Permissions/mock-data/kubevirt.io:view.json new file mode 100644 index 00000000000..6c6aaf9a5c7 --- /dev/null +++ b/frontend/src/routes/UserManagement/Identities/Permissions/mock-data/kubevirt.io:view.json @@ -0,0 +1,154 @@ +{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "creationTimestamp": "2025-07-31T02:03:01Z", + "labels": { + "rbac.open-cluster-management.io/filter": "vm-clusterroles" + }, + "name": "kubevirt.io:view", + "resourceVersion": "1270332", + "uid": "94469dde-b61f-4ee3-864a-12b72a50a7df" + }, + "rules": [ + { + "apiGroups": [ + "kubevirt.io" + ], + "resources": [ + "kubevirts" + ], + "verbs": [ + "get", + "list" + ] + }, + { + "apiGroups": [ + "subresources.kubevirt.io" + ], + "resources": [ + "virtualmachines/expand-spec", + "virtualmachineinstances/guestosinfo", + "virtualmachineinstances/filesystemlist", + "virtualmachineinstances/userlist", + "virtualmachineinstances/sev/fetchcertchain", + "virtualmachineinstances/sev/querylaunchmeasurement" + ], + "verbs": [ + "get" + ] + }, + { + "apiGroups": [ + "subresources.kubevirt.io" + ], + "resources": [ + "expand-vm-spec" + ], + "verbs": [ + "update" + ] + }, + { + "apiGroups": [ + "kubevirt.io" + ], + "resources": [ + "virtualmachines", + "virtualmachineinstances", + "virtualmachineinstancepresets", + "virtualmachineinstancereplicasets", + "virtualmachineinstancemigrations" + ], + "verbs": [ + "get", + "list", + "watch" + ] + }, + { + "apiGroups": [ + "snapshot.kubevirt.io" + ], + "resources": [ + "virtualmachinesnapshots", + "virtualmachinesnapshotcontents", + "virtualmachinerestores" + ], + "verbs": [ + "get", + "list", + "watch" + ] + }, + { + "apiGroups": [ + "export.kubevirt.io" + ], + "resources": [ + "virtualmachineexports" + ], + "verbs": [ + "get", + "list", + "watch" + ] + }, + { + "apiGroups": [ + "clone.kubevirt.io" + ], + "resources": [ + "virtualmachineclones" + ], + "verbs": [ + "get", + "list", + "watch" + ] + }, + { + "apiGroups": [ + "instancetype.kubevirt.io" + ], + "resources": [ + "virtualmachineinstancetypes", + "virtualmachineclusterinstancetypes", + "virtualmachinepreferences", + "virtualmachineclusterpreferences" + ], + "verbs": [ + "get", + "list", + "watch" + ] + }, + { + "apiGroups": [ + "pool.kubevirt.io" + ], + "resources": [ + "virtualmachinepools" + ], + "verbs": [ + "get", + "list", + "watch" + ] + }, + { + "apiGroups": [ + "migrations.kubevirt.io" + ], + "resources": [ + "migrationpolicies" + ], + "verbs": [ + "get", + "list", + "watch" + ] + } + ] +}