From 43c19064e9477a5449ff5d56b00efe27cf640c27 Mon Sep 17 00:00:00 2001 From: Debsmita Santra Date: Tue, 5 Dec 2023 15:39:08 +0530 Subject: [PATCH] feat(rbac): role overview (#972) --- plugins/rbac-common/src/types.ts | 5 + plugins/rbac/dev/index.tsx | 107 ++----- plugins/rbac/package.json | 8 +- plugins/rbac/src/__fixtures__/mockMembers.ts | 259 ++++++++++++++++ .../__fixtures__/mockPermissionPolicies.ts | 75 +++++ plugins/rbac/src/__fixtures__/mockPolicies.ts | 76 +++++ plugins/rbac/src/api/RBACBackendClient.ts | 75 ++++- plugins/rbac/src/components/AboutCard.tsx | 69 +++++ .../rbac/src/components/DeleteRoleDialog.tsx | 4 +- .../rbac/src/components/MembersCard.test.tsx | 107 +++++++ plugins/rbac/src/components/MembersCard.tsx | 82 +++++ .../src/components/MembersListColumns.tsx | 48 +++ .../src/components/PermissionCard.test.tsx | 85 +++++ .../rbac/src/components/PermissionsCard.tsx | 87 ++++++ .../src/components/PermissionsListColumns.tsx | 33 ++ .../rbac/src/components/RoleOverviewPage.tsx | 35 +++ .../rbac/src/components/RolesList.test.tsx | 10 +- plugins/rbac/src/components/RolesList.tsx | 21 +- .../rbac/src/components/RolesListColumns.tsx | 5 +- plugins/rbac/src/components/Router.tsx | 17 + plugins/rbac/src/components/index.ts | 1 + plugins/rbac/src/hooks/useMembers.test.ts | 230 ++++++++++++++ plugins/rbac/src/hooks/useMembers.ts | 99 ++++++ .../src/hooks/usePermissionPolicies.test.ts | 139 +++++++++ .../rbac/src/hooks/usePermissionPolicies.ts | 44 +++ plugins/rbac/src/hooks/useRoles.test.ts | 128 ++++++++ plugins/rbac/src/hooks/useRoles.ts | 41 +-- plugins/rbac/src/plugin.ts | 5 +- plugins/rbac/src/routes.ts | 8 +- plugins/rbac/src/types.ts | 22 ++ plugins/rbac/src/utils/rbac-utils.test.ts | 152 ++++++++- plugins/rbac/src/utils/rbac-utils.ts | 136 ++++++-- yarn.lock | 292 +++++++++++++++++- 33 files changed, 2348 insertions(+), 157 deletions(-) create mode 100644 plugins/rbac/src/__fixtures__/mockMembers.ts create mode 100644 plugins/rbac/src/__fixtures__/mockPermissionPolicies.ts create mode 100644 plugins/rbac/src/__fixtures__/mockPolicies.ts create mode 100644 plugins/rbac/src/components/AboutCard.tsx create mode 100644 plugins/rbac/src/components/MembersCard.test.tsx create mode 100644 plugins/rbac/src/components/MembersCard.tsx create mode 100644 plugins/rbac/src/components/MembersListColumns.tsx create mode 100644 plugins/rbac/src/components/PermissionCard.test.tsx create mode 100644 plugins/rbac/src/components/PermissionsCard.tsx create mode 100644 plugins/rbac/src/components/PermissionsListColumns.tsx create mode 100644 plugins/rbac/src/components/RoleOverviewPage.tsx create mode 100644 plugins/rbac/src/components/Router.tsx create mode 100644 plugins/rbac/src/hooks/useMembers.test.ts create mode 100644 plugins/rbac/src/hooks/useMembers.ts create mode 100644 plugins/rbac/src/hooks/usePermissionPolicies.test.ts create mode 100644 plugins/rbac/src/hooks/usePermissionPolicies.ts create mode 100644 plugins/rbac/src/hooks/useRoles.test.ts diff --git a/plugins/rbac-common/src/types.ts b/plugins/rbac-common/src/types.ts index b406715920..b4fbba8272 100644 --- a/plugins/rbac-common/src/types.ts +++ b/plugins/rbac-common/src/types.ts @@ -17,3 +17,8 @@ export type UpdatePolicy = { oldPolicy: Policy; newPolicy: Policy; }; + +export type PermissionPolicy = { + pluginId?: string; + policies?: Policy[]; +}; diff --git a/plugins/rbac/dev/index.tsx b/plugins/rbac/dev/index.tsx index b8988a4fd9..397161caf3 100644 --- a/plugins/rbac/dev/index.tsx +++ b/plugins/rbac/dev/index.tsx @@ -7,10 +7,19 @@ import { } from '@backstage/plugin-permission-react'; import { TestApiProvider } from '@backstage/test-utils'; -import { Role, RoleBasedPolicy } from '@janus-idp/backstage-plugin-rbac-common'; +import { + PermissionPolicy, + Policy, + Role, + RoleBasedPolicy, +} from '@janus-idp/backstage-plugin-rbac-common'; +import { mockMembers } from '../src/__fixtures__/mockMembers'; +import { mockPermissionPolicies } from '../src/__fixtures__/mockPermissionPolicies'; +import { mockPolicies } from '../src/__fixtures__/mockPolicies'; import { RBACAPI, rbacApiRef } from '../src/api/RBACBackendClient'; import { RbacPage, rbacPlugin } from '../src/plugin'; +import { MemberEntity } from '../src/types'; class MockPermissionApi implements PermissionApi { readonly result; @@ -34,80 +43,7 @@ class MockRBACApi implements RBACAPI { return this.resources; } async getPolicies(): Promise { - return [ - { - entityReference: 'role:default/guests', - permission: 'catalog-entity', - policy: 'read', - effect: 'deny', - }, - { - entityReference: 'role:default/guests', - permission: 'catalog.entity.create', - policy: 'use', - effect: 'deny', - }, - { - entityReference: 'role:default/guests', - permission: 'catalog-entity', - policy: 'read', - effect: 'allow', - }, - { - entityReference: 'role:default/guests', - permission: 'catalog.entity.create', - policy: 'use', - effect: 'allow', - }, - { - entityReference: 'role:default/guests', - permission: 'policy-entity', - policy: 'create', - effect: 'allow', - }, - { - entityReference: 'role:default/guests', - permission: 'policy-entity', - policy: 'read', - effect: 'allow', - }, - { - entityReference: 'role:default/guests', - permission: 'policy.entity.read', - policy: 'use', - effect: 'allow', - }, - { - entityReference: 'role:default/guests', - permission: 'policy-entity', - policy: 'delete', - effect: 'allow', - }, - { - entityReference: 'role:default/rbac_admin', - permission: 'policy-entity', - policy: 'read', - effect: 'allow', - }, - { - entityReference: 'role:default/rbac_admin', - permission: 'policy-entity', - policy: 'create', - effect: 'allow', - }, - { - entityReference: 'role:default/rbac_admin', - permission: 'policy-entity', - policy: 'delete', - effect: 'allow', - }, - { - entityReference: 'role:default/rbac_admin', - permission: 'policy-entity', - policy: 'update', - effect: 'allow', - }, - ]; + return mockPolicies; } async getUserAuthorization(): Promise<{ status: string }> { @@ -116,9 +52,30 @@ class MockRBACApi implements RBACAPI { }; } + async getRole(role: string): Promise { + const roleresource = this.resources.find(res => res.name === role); + return roleresource ? [roleresource] : []; + } + async deleteRole(_roleName: string): Promise { return { status: 204 }; } + + async getMembers(): Promise { + return mockMembers; + } + + async listPermissions(): Promise { + return mockPermissionPolicies; + } + + async deletePolicy( + _entityRef: string, + _permission: string, + _policies: Policy[], + ): Promise { + return 204; + } } const mockPermissionApi = new MockPermissionApi({ result: 'ALLOW' }); diff --git a/plugins/rbac/package.json b/plugins/rbac/package.json index b6f267e92e..cd89c5aadc 100644 --- a/plugins/rbac/package.json +++ b/plugins/rbac/package.json @@ -28,6 +28,7 @@ "@backstage/catalog-model": "^1.4.3", "@backstage/core-components": "^0.13.6", "@backstage/core-plugin-api": "^1.7.0", + "@backstage/plugin-catalog": "^1.15.1", "@backstage/plugin-permission-react": "^0.4.16", "@backstage/theme": "^0.4.3", "@janus-idp/backstage-plugin-rbac-common": "1.1.0", @@ -36,10 +37,12 @@ "@material-ui/lab": "^4.0.0-alpha.45", "@mui/icons-material": "5.14.11", "@mui/material": "^5.14.18", - "react-use": "^17.4.0" + "react-use": "^17.4.0", + "lodash": "^4.17.21" }, "peerDependencies": { - "react": "^16.13.1 || ^17.0.0" + "react": "^16.13.1 || ^17.0.0", + "react-router-dom": "^6.20.0" }, "devDependencies": { "@backstage/cli": "0.23.0", @@ -49,6 +52,7 @@ "@janus-idp/cli": "1.4.6", "@testing-library/jest-dom": "5.17.0", "@testing-library/react": "12.1.5", + "@testing-library/react-hooks": "8.0.1", "@testing-library/user-event": "14.5.1", "@types/node": "18.18.5", "msw": "1.3.2" diff --git a/plugins/rbac/src/__fixtures__/mockMembers.ts b/plugins/rbac/src/__fixtures__/mockMembers.ts new file mode 100644 index 0000000000..148c07f052 --- /dev/null +++ b/plugins/rbac/src/__fixtures__/mockMembers.ts @@ -0,0 +1,259 @@ +import { MemberEntity } from '../types'; + +export const mockMembers: MemberEntity[] = [ + { + metadata: { + namespace: 'default', + annotations: {}, + name: 'team-d', + description: 'Team D', + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + spec: { + type: 'team', + profile: { + displayName: 'Team D', + }, + parent: 'boxoffice', + children: [], + }, + relations: [ + { + type: 'childOf', + targetRef: 'group:default/boxoffice', + }, + { + type: 'hasMember', + targetRef: 'user:default/eva.macdowell', + }, + { + type: 'hasMember', + targetRef: 'user:default/lucy.sheehan', + }, + ], + }, + { + metadata: { + namespace: 'default', + annotations: {}, + name: 'infrastructure', + description: 'The infra department', + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + spec: { + type: 'department', + parent: 'acme-corp', + children: ['backstage', 'boxoffice'], + }, + relations: [], + }, + { + metadata: { + namespace: 'default', + annotations: {}, + name: 'guest', + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + spec: { + profile: { + displayName: 'Guest User', + }, + memberOf: ['team-a'], + }, + relations: [ + { + type: 'memberOf', + targetRef: 'group:default/team-a', + }, + ], + }, + { + metadata: { + namespace: 'default', + annotations: {}, + name: 'janus-authors', + title: 'Janus-IDP Authors', + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + spec: { + type: 'team', + children: [], + }, + relations: [], + }, + { + metadata: { + namespace: 'default', + annotations: {}, + name: 'team-a', + description: 'Team A', + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + spec: { + type: 'team', + profile: {}, + parent: 'backstage', + children: [], + }, + relations: [ + { + type: 'childOf', + targetRef: 'group:default/backstage', + }, + { + type: 'hasMember', + targetRef: 'user:default/breanna.davison', + }, + { + type: 'hasMember', + targetRef: 'user:default/guest', + }, + { + type: 'hasMember', + targetRef: 'user:default/janelle.dawe', + }, + { + type: 'hasMember', + targetRef: 'user:default/nigel.manning', + }, + ], + }, + { + metadata: { + namespace: 'default', + annotations: {}, + name: 'backstage', + description: 'The backstage sub-department', + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + spec: { + type: 'sub-department', + profile: { + displayName: 'Backstage', + }, + parent: 'infrastructure', + children: ['team-a', 'team-b'], + }, + relations: [], + }, + { + metadata: { + namespace: 'default', + annotations: {}, + name: 'team-b', + description: 'Team B', + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + spec: { + type: 'team', + profile: { + displayName: 'Team B', + }, + parent: 'backstage', + children: [], + }, + relations: [ + { + type: 'hasMember', + targetRef: 'user:default/amelia.park', + }, + { + type: 'hasMember', + targetRef: 'user:default/colette.brock', + }, + { + type: 'hasMember', + targetRef: 'user:default/jenny.doe', + }, + { + type: 'hasMember', + targetRef: 'user:default/jonathon.page', + }, + { + type: 'hasMember', + targetRef: 'user:default/justine.barrow', + }, + ], + }, + { + metadata: { + namespace: 'default', + annotations: {}, + name: 'lucy.sheehan', + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + spec: { + profile: { + displayName: 'Lucy Sheehan', + }, + memberOf: ['team-d'], + }, + relations: [ + { + type: 'memberOf', + targetRef: 'group:default/team-d', + }, + ], + }, + { + metadata: { + namespace: 'default', + annotations: {}, + name: 'boxoffice', + description: 'The boxoffice sub-department', + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + spec: { + type: 'sub-department', + profile: { + displayName: 'Box Office', + }, + parent: 'infrastructure', + children: ['team-c', 'team-d'], + }, + relations: [ + { + type: 'childOf', + targetRef: 'group:default/infrastructure', + }, + { + type: 'parentOf', + targetRef: 'group:default/team-c', + }, + { + type: 'parentOf', + targetRef: 'group:default/team-d', + }, + ], + }, + { + metadata: { + namespace: 'default', + annotations: {}, + name: 'amelia.park', + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + spec: { + profile: { + displayName: 'Amelia Park', + }, + memberOf: ['team-b'], + }, + relations: [ + { + type: 'memberOf', + targetRef: 'group:default/team-b', + }, + ], + }, +]; diff --git a/plugins/rbac/src/__fixtures__/mockPermissionPolicies.ts b/plugins/rbac/src/__fixtures__/mockPermissionPolicies.ts new file mode 100644 index 0000000000..0165631213 --- /dev/null +++ b/plugins/rbac/src/__fixtures__/mockPermissionPolicies.ts @@ -0,0 +1,75 @@ +import { PermissionPolicy } from '@janus-idp/backstage-plugin-rbac-common'; + +export const mockPermissionPolicies: PermissionPolicy[] = [ + { + pluginId: 'catalog', + policies: [ + { + permission: 'catalog-entity', + policy: 'read', + }, + { + permission: 'catalog.entity.create', + policy: 'create', + }, + { + permission: 'catalog-entity', + policy: 'delete', + }, + { + permission: 'catalog-entity', + policy: 'update', + }, + { + permission: 'catalog.location.read', + policy: 'read', + }, + { + permission: 'catalog.location.create', + policy: 'create', + }, + { + permission: 'catalog.location.delete', + policy: 'delete', + }, + ], + }, + { + pluginId: 'scaffolder', + policies: [ + { + permission: 'scaffolder-template', + policy: 'read', + }, + { + permission: 'scaffolder-template', + policy: 'read', + }, + { + permission: 'scaffolder-action', + policy: 'use', + }, + ], + }, + { + pluginId: 'permission', + policies: [ + { + permission: 'policy-entity', + policy: 'read', + }, + { + permission: 'policy-entity', + policy: 'create', + }, + { + permission: 'policy-entity', + policy: 'delete', + }, + { + permission: 'policy-entity', + policy: 'update', + }, + ], + }, +]; diff --git a/plugins/rbac/src/__fixtures__/mockPolicies.ts b/plugins/rbac/src/__fixtures__/mockPolicies.ts new file mode 100644 index 0000000000..a1ce8896b1 --- /dev/null +++ b/plugins/rbac/src/__fixtures__/mockPolicies.ts @@ -0,0 +1,76 @@ +import { RoleBasedPolicy } from '@janus-idp/backstage-plugin-rbac-common'; + +export const mockPolicies: RoleBasedPolicy[] = [ + { + entityReference: 'role:default/guests', + permission: 'catalog-entity', + policy: 'read', + effect: 'deny', + }, + { + entityReference: 'role:default/guests', + permission: 'catalog.entity.create', + policy: 'use', + effect: 'deny', + }, + { + entityReference: 'role:default/guests', + permission: 'catalog-entity', + policy: 'read', + effect: 'allow', + }, + { + entityReference: 'role:default/guests', + permission: 'catalog.entity.create', + policy: 'use', + effect: 'allow', + }, + { + entityReference: 'role:default/guests', + permission: 'policy-entity', + policy: 'create', + effect: 'allow', + }, + { + entityReference: 'role:default/guests', + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + { + entityReference: 'role:default/guests', + permission: 'policy.entity.read', + policy: 'use', + effect: 'allow', + }, + { + entityReference: 'role:default/guests', + permission: 'policy-entity', + policy: 'delete', + effect: 'allow', + }, + { + entityReference: 'role:default/rbac_admin', + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + { + entityReference: 'role:default/rbac_admin', + permission: 'policy-entity', + policy: 'create', + effect: 'allow', + }, + { + entityReference: 'role:default/rbac_admin', + permission: 'policy-entity', + policy: 'delete', + effect: 'allow', + }, + { + entityReference: 'role:default/rbac_admin', + permission: 'policy-entity', + policy: 'update', + effect: 'allow', + }, +]; diff --git a/plugins/rbac/src/api/RBACBackendClient.ts b/plugins/rbac/src/api/RBACBackendClient.ts index 508e8b1119..be5ca7d045 100644 --- a/plugins/rbac/src/api/RBACBackendClient.ts +++ b/plugins/rbac/src/api/RBACBackendClient.ts @@ -4,14 +4,24 @@ import { IdentityApi, } from '@backstage/core-plugin-api'; -import { Role, RoleBasedPolicy } from '@janus-idp/backstage-plugin-rbac-common'; +import { + PermissionPolicy, + Role, + RoleBasedPolicy, +} from '@janus-idp/backstage-plugin-rbac-common'; + +import { MemberEntity } from '../types'; +import { getKindNamespaceName } from '../utils/rbac-utils'; // @public export type RBACAPI = { getUserAuthorization: () => Promise<{ status: string }>; getRoles: () => Promise; - getPolicies: () => Promise; + getPolicies: () => Promise; deleteRole: (role: string) => Promise; + getRole: (role: string) => Promise; + getMembers: () => Promise; + listPermissions: () => Promise; }; export type Options = { @@ -64,27 +74,78 @@ export class RBACBackendClient implements RBACAPI { ...(idToken && { Authorization: `Bearer ${idToken}` }), }, }); + if (jsonResponse.status !== 200 && jsonResponse.status !== 204) { + return jsonResponse; + } return jsonResponse.json(); } async deleteRole(role: string) { const { token: idToken } = await this.identityApi.getCredentials(); const backendUrl = this.configApi.getString('backend.baseUrl'); - const str = role.split(':'); - const kind = str[0]; - const namespace = str[1].split('/')[0]; - const name = str[1].split('/')[1]; + const { kind, namespace, name } = getKindNamespaceName(role); const jsonResponse = await fetch( `${backendUrl}/api/permission/roles/${kind}/${namespace}/${name}`, { headers: { ...(idToken && { Authorization: `Bearer ${idToken}` }), 'Content-Type': 'application/json', - Accept: 'application/json', }, method: 'DELETE', }, ); return jsonResponse; } + + async getRole(role: string) { + const { token: idToken } = await this.identityApi.getCredentials(); + const backendUrl = this.configApi.getString('backend.baseUrl'); + const { kind, namespace, name } = getKindNamespaceName(role); + const jsonResponse = await fetch( + `${backendUrl}/api/permission/roles/${kind}/${namespace}/${name}`, + { + headers: { + ...(idToken && { Authorization: `Bearer ${idToken}` }), + 'Content-Type': 'application/json', + }, + }, + ); + if (jsonResponse.status !== 200 && jsonResponse.status !== 204) { + return jsonResponse; + } + return jsonResponse.json(); + } + + async getMembers() { + const { token: idToken } = await this.identityApi.getCredentials(); + const backendUrl = this.configApi.getString('backend.baseUrl'); + const jsonResponse = await fetch( + `${backendUrl}/api/catalog/entities?filter=kind=user&filter=kind=group`, + { + headers: { + ...(idToken && { Authorization: `Bearer ${idToken}` }), + 'Content-Type': 'application/json', + }, + }, + ); + if (jsonResponse.status !== 200 && jsonResponse.status !== 204) { + return jsonResponse; + } + return jsonResponse.json(); + } + + async listPermissions() { + const { token: idToken } = await this.identityApi.getCredentials(); + const backendUrl = this.configApi.getString('backend.baseUrl'); + const jsonResponse = await fetch( + `${backendUrl}/api/permission/plugins/policies`, + { + headers: { + ...(idToken && { Authorization: `Bearer ${idToken}` }), + 'Content-Type': 'application/json', + }, + }, + ); + return jsonResponse.json(); + } } diff --git a/plugins/rbac/src/components/AboutCard.tsx b/plugins/rbac/src/components/AboutCard.tsx new file mode 100644 index 0000000000..ab7997c20f --- /dev/null +++ b/plugins/rbac/src/components/AboutCard.tsx @@ -0,0 +1,69 @@ +import React from 'react'; + +import { MarkdownContent } from '@backstage/core-components'; +import { AboutField } from '@backstage/plugin-catalog'; + +import { + Card, + CardContent, + CardHeader, + Grid, + makeStyles, +} from '@material-ui/core'; + +const useStyles = makeStyles({ + gridItemCard: { + display: 'flex', + flexDirection: 'column', + height: 'calc(100% - 10px)', // for pages without content header + marginBottom: '10px', + }, + fullHeightCard: { + display: 'flex', + flexDirection: 'column', + height: '100%', + }, + gridItemCardContent: { + flex: 1, + }, + fullHeightCardContent: { + flex: 1, + }, + text: { + wordBreak: 'break-word', + }, +}); + +export const AboutCard = () => { + const classes = useStyles(); + const cardClass = classes.gridItemCard; + const cardContentClass = classes.gridItemCardContent; + + return ( + + + + + + + + + + + + + + + + + ); +}; diff --git a/plugins/rbac/src/components/DeleteRoleDialog.tsx b/plugins/rbac/src/components/DeleteRoleDialog.tsx index 99f20e75ad..f046a54548 100644 --- a/plugins/rbac/src/components/DeleteRoleDialog.tsx +++ b/plugins/rbac/src/components/DeleteRoleDialog.tsx @@ -133,9 +133,8 @@ const DeleteRoleDialog = ({ >{`${propOptions.permissions} permission policies`}{' '} specified in this role.
-
-