diff --git a/app/portainer/rbac/components/access-viewer/access-viewer-datatable/access-viewer-datatable.css b/app/portainer/rbac/components/access-viewer/access-viewer-datatable/access-viewer-datatable.css deleted file mode 100644 index 1d8291e6e..000000000 --- a/app/portainer/rbac/components/access-viewer/access-viewer-datatable/access-viewer-datatable.css +++ /dev/null @@ -1,15 +0,0 @@ -.datatable.access-viewer-datatable .toolBar { - font-size: inherit; -} - -.datatable.access-viewer-datatable .toolBar .small { - font-weight: normal; -} - -.datatable.access-viewer-datatable .toolBar.pl-0 { - padding-left: 0; -} - -.datatable.access-viewer-datatable .toolBar.pr-0 { - padding-right: 0; -} diff --git a/app/portainer/rbac/components/access-viewer/access-viewer-datatable/access-viewer-datatable.html b/app/portainer/rbac/components/access-viewer/access-viewer-datatable/access-viewer-datatable.html deleted file mode 100644 index 1eec17532..000000000 --- a/app/portainer/rbac/components/access-viewer/access-viewer-datatable/access-viewer-datatable.html +++ /dev/null @@ -1,89 +0,0 @@ -
-
-
-
-
Access
-
- - Effective role for each environment will be displayed for the selected user -
-
-
- -
-
- - - - - - - - - - - - - - - - - - - - - -
- - - - Access origin
{{ item.EndpointName }}{{ item.RoleName }}{{ item.TeamName ? 'Team' : 'User' }} {{ item.TeamName }} access defined on {{ item.AccessLocation }} - {{ item.GroupName }} - Manage access - Manage access -
Select a user to show associated access and role
The selected user does not have access to any environment(s)
-
- -
diff --git a/app/portainer/rbac/components/access-viewer/access-viewer-datatable/index.js b/app/portainer/rbac/components/access-viewer/access-viewer-datatable/index.js deleted file mode 100644 index cfcc9bd17..000000000 --- a/app/portainer/rbac/components/access-viewer/access-viewer-datatable/index.js +++ /dev/null @@ -1,14 +0,0 @@ -import './access-viewer-datatable.css'; - -export const accessViewerDatatable = { - templateUrl: './access-viewer-datatable.html', - controller: 'GenericDatatableController', - bindings: { - titleText: '@', - titleIcon: '@', - tableKey: '@', - orderBy: '@', - dataset: '<', - isAdmin: '<', - }, -}; diff --git a/app/portainer/rbac/components/access-viewer/access-viewer.controller.js b/app/portainer/rbac/components/access-viewer/access-viewer.controller.js index d2d33e8d0..b231d6afb 100644 --- a/app/portainer/rbac/components/access-viewer/access-viewer.controller.js +++ b/app/portainer/rbac/components/access-viewer/access-viewer.controller.js @@ -2,7 +2,7 @@ import _ from 'lodash-es'; import { isLimitedToBE } from '@/react/portainer/feature-flags/feature-flags.service'; import { getEnvironments } from '@/react/portainer/environments/environment.service'; -import AccessViewerPolicyModel from '../../models/access'; +import { AccessViewerPolicyModel } from '@/react/portainer/users/RolesView/AccessViewer/model'; export default class AccessViewerController { /* @ngInject */ diff --git a/app/portainer/rbac/components/access-viewer/access-viewer.html b/app/portainer/rbac/components/access-viewer/access-viewer.html index 1da86123f..8be5ebb0b 100644 --- a/app/portainer/rbac/components/access-viewer/access-viewer.html +++ b/app/portainer/rbac/components/access-viewer/access-viewer.html @@ -17,7 +17,8 @@ - + + diff --git a/app/portainer/rbac/index.js b/app/portainer/rbac/index.js index 0448c55d7..21563de83 100644 --- a/app/portainer/rbac/index.js +++ b/app/portainer/rbac/index.js @@ -1,7 +1,6 @@ import { AccessHeaders } from '../authorization-guard'; import { rolesView } from './views/roles'; import { accessViewer } from './components/access-viewer'; -import { accessViewerDatatable } from './components/access-viewer/access-viewer-datatable'; import { rolesDatatable } from './components/roles-datatable'; import { RoleService } from './services/role.service'; @@ -11,7 +10,6 @@ angular .module('portainer.rbac', ['ngResource']) .constant('API_ENDPOINT_ROLES', 'api/roles') .component('accessViewer', accessViewer) - .component('accessViewerDatatable', accessViewerDatatable) .component('rolesDatatable', rolesDatatable) .component('rolesView', rolesView) .factory('RoleService', RoleService) diff --git a/app/portainer/rbac/models/access.js b/app/portainer/rbac/models/access.js deleted file mode 100644 index 18b7269fe..000000000 --- a/app/portainer/rbac/models/access.js +++ /dev/null @@ -1,16 +0,0 @@ -export default function AccessViewerPolicyModel(policy, endpoint, roles, group, team) { - this.EndpointId = endpoint.Id; - this.EndpointName = endpoint.Name; - this.RoleId = policy.RoleId; - this.RoleName = roles[policy.RoleId].Name; - this.RolePriority = roles[policy.RoleId].Priority; - if (group) { - this.GroupId = group.Id; - this.GroupName = group.Name; - } - if (team) { - this.TeamId = team.Id; - this.TeamName = team.Name; - } - this.AccessLocation = group ? 'environment group' : 'environment'; -} diff --git a/app/portainer/react/components/users.ts b/app/portainer/react/components/users.ts index 74c378c62..c8d9f4117 100644 --- a/app/portainer/react/components/users.ts +++ b/app/portainer/react/components/users.ts @@ -4,10 +4,17 @@ import { r2a } from '@/react-tools/react2angular'; import { withUIRouter } from '@/react-tools/withUIRouter'; import { UsersDatatable } from '@/react/portainer/users/ListView/UsersDatatable/UsersDatatable'; import { withCurrentUser } from '@/react-tools/withCurrentUser'; +import { EffectiveAccessViewerDatatable } from '@/react/portainer/users/RolesView/AccessViewer/EffectiveAccessViewerDatatable'; export const usersModule = angular .module('portainer.app.react.components.users', []) .component( 'usersDatatable', r2a(withUIRouter(withCurrentUser(UsersDatatable)), ['dataset', 'onRemove']) + ) + .component( + 'effectiveAccessViewerDatatable', + r2a(withUIRouter(withCurrentUser(EffectiveAccessViewerDatatable)), [ + 'dataset', + ]) ).name; diff --git a/app/portainer/users/types.ts b/app/portainer/users/types.ts index 2cb44c00d..b62fb75f5 100644 --- a/app/portainer/users/types.ts +++ b/app/portainer/users/types.ts @@ -1,4 +1,5 @@ import { EnvironmentId } from '@/react/portainer/environments/types'; +import { AuthorizationMap } from '@/react/portainer/users/RolesView/types'; import { type UserId } from './types/user-id'; @@ -16,10 +17,6 @@ export const RoleNames: { [key in Role]: string } = { [Role.EdgeAdmin]: 'edge administrator', }; -interface AuthorizationMap { - [authorization: string]: boolean; -} - export type User = { Id: UserId; Username: string; diff --git a/app/react/portainer/users/RolesView/AccessViewer/EffectiveAccessViewerDatatable.tsx b/app/react/portainer/users/RolesView/AccessViewer/EffectiveAccessViewerDatatable.tsx new file mode 100644 index 000000000..530bad8ce --- /dev/null +++ b/app/react/portainer/users/RolesView/AccessViewer/EffectiveAccessViewerDatatable.tsx @@ -0,0 +1,36 @@ +import { TextTip } from '@@/Tip/TextTip'; +import { Datatable } from '@@/datatables'; +import { useTableStateWithStorage } from '@@/datatables/useTableState'; + +import { AccessViewerPolicyModel } from './model'; +import { columns } from './columns'; + +export function EffectiveAccessViewerDatatable({ + dataset, +}: { + dataset?: Array; +}) { + const tableState = useTableStateWithStorage('access-viewer', 'Environment'); + + return ( + + Effective role for each environment will be displayed for the selected + user + + } + emptyContentLabel={ + dataset + ? 'The selected user does not have access to any environment(s)' + : 'Select a user to show associated access and role' + } + disableSelect + /> + ); +} diff --git a/app/react/portainer/users/RolesView/AccessViewer/columns.tsx b/app/react/portainer/users/RolesView/AccessViewer/columns.tsx new file mode 100644 index 000000000..41cbfe2c8 --- /dev/null +++ b/app/react/portainer/users/RolesView/AccessViewer/columns.tsx @@ -0,0 +1,81 @@ +import { createColumnHelper, CellContext } from '@tanstack/react-table'; +import { Users } from 'lucide-react'; + +import { useCurrentUser } from '@/react/hooks/useUser'; + +import { Icon } from '@@/Icon'; +import { Link } from '@@/Link'; + +import { AccessViewerPolicyModel } from './model'; + +const helper = createColumnHelper(); + +export const columns = [ + helper.accessor('EndpointName', { + header: 'Environment', + id: 'Environment', + }), + helper.accessor('RoleName', { + header: 'Role', + id: 'Role', + }), + helper.display({ + header: 'Access Origin', + cell: AccessCell, + }), +]; + +function AccessCell({ + row: { original: item }, +}: CellContext) { + const { isPureAdmin } = useCurrentUser(); + + if (item.RoleId === 0) { + return ( + <> + User access all environments + + Manage access + + + ); + } + + return ( + <> + {prefix(item.TeamName)} access defined on {item.AccessLocation}{' '} + {!!item.GroupName && {item.GroupName}}{' '} + {manageAccess(item, isPureAdmin)} + + ); +} + +function prefix(teamName: string | undefined) { + if (!teamName) { + return 'User'; + } + return ( + <> + Team {teamName} + + ); +} + +function manageAccess(item: AccessViewerPolicyModel, isPureAdmin: boolean) { + if (!isPureAdmin) { + return null; + } + + return item.GroupName ? ( + + Manage access + + ) : ( + + Manage access + + ); +} diff --git a/app/react/portainer/users/RolesView/AccessViewer/model.ts b/app/react/portainer/users/RolesView/AccessViewer/model.ts new file mode 100644 index 000000000..264c01ef7 --- /dev/null +++ b/app/react/portainer/users/RolesView/AccessViewer/model.ts @@ -0,0 +1,53 @@ +import { + Environment, + EnvironmentId, +} from '@/react/portainer/environments/types'; +import { EnvironmentGroup } from '@/react/portainer/environments/environment-groups/types'; + +import { RbacRole } from '../types'; +import { Team, TeamId } from '../../teams/types'; + +export class AccessViewerPolicyModel { + EndpointId: EnvironmentId; + + EndpointName: string; + + RoleId: RbacRole['Id']; + + RoleName: RbacRole['Name']; + + RolePriority: RbacRole['Priority']; + + GroupId?: EnvironmentGroup['Id']; + + GroupName?: EnvironmentGroup['Name']; + + TeamId?: TeamId; + + TeamName?: Team['Name']; + + AccessLocation: string; + + constructor( + policy: { RoleId: RbacRole['Id'] }, + endpoint: Environment, + roles: Record, + group?: EnvironmentGroup, + team?: Team + ) { + this.EndpointId = endpoint.Id; + this.EndpointName = endpoint.Name; + this.RoleId = policy.RoleId; + this.RoleName = roles[policy.RoleId].Name; + this.RolePriority = roles[policy.RoleId].Priority; + if (group) { + this.GroupId = group.Id; + this.GroupName = group.Name; + } + if (team) { + this.TeamId = team.Id; + this.TeamName = team.Name; + } + this.AccessLocation = group ? 'environment group' : 'environment'; + } +} diff --git a/app/react/portainer/users/RolesView/types.ts b/app/react/portainer/users/RolesView/types.ts new file mode 100644 index 000000000..ca4e6d5bc --- /dev/null +++ b/app/react/portainer/users/RolesView/types.ts @@ -0,0 +1,11 @@ +export interface AuthorizationMap { + [authorization: string]: boolean; +} + +export interface RbacRole { + Id: number; + Name: string; + Description: string; + Authorizations: AuthorizationMap; + Priority: number; +}