diff --git a/plugins/rbac-backend/docs/permissions.md b/plugins/rbac-backend/docs/permissions.md index 06ad76fb65..5a7cb29ade 100644 --- a/plugins/rbac-backend/docs/permissions.md +++ b/plugins/rbac-backend/docs/permissions.md @@ -66,3 +66,10 @@ When defining a permission for the RBAC Backend plugin to consume, follow these | ---------------- | ------------- | ------ | ----------------------------------------------------------------- | ------------ | | ocm.entity.read | | read | Allows the user to read from the ocm plugin | X | | ocm.cluster.read | | read | Allows the user to read the cluster information in the ocm plugin | X | + +## Topology + +| Name | Resource Type | Policy | Description | Requirements | +| ------------------ | ------------- | ------ | ----------------------------------------------------------------------------------------------------------- | ------------------- | +| topology.view.read | | read | Allows the user to view the topology plugin | X | +| kubernetes.proxy | | | Allows the user to access the proxy endpoint (ability to read pod logs and events within Showcase and RHDH) | catalog.entity.read | diff --git a/plugins/topology-common/.eslintrc.js b/plugins/topology-common/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/topology-common/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/topology-common/README.md b/plugins/topology-common/README.md new file mode 100644 index 0000000000..cef2d23d4d --- /dev/null +++ b/plugins/topology-common/README.md @@ -0,0 +1,5 @@ +# Topology plugin for Backstage + +The Topology plugin enables you to visualize the workloads such as Deployment, Job, Daemonset, Statefulset, CronJob, and Pods powering any service on the Kubernetes cluster. + +For more information about Topology plugin, see the [Topology plugin documentation](https://github.com/janus-idp/backstage-plugins/tree/main/plugins/topology) on GitHub. diff --git a/plugins/topology-common/package.json b/plugins/topology-common/package.json new file mode 100644 index 0000000000..b494ab7a13 --- /dev/null +++ b/plugins/topology-common/package.json @@ -0,0 +1,48 @@ +{ + "name": "@janus-idp/backstage-plugin-topology-common", + "description": "Common functionalities for the topology plugin", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": true, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "common-library" + }, + "sideEffects": false, + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "postpack": "backstage-cli package postpack", + "prepack": "backstage-cli package prepack", + "test": "backstage-cli package test --passWithNoTests --coverage", + "tsc": "tsc" + }, + "devDependencies": { + "@backstage/cli": "0.26.4" + }, + "files": [ + "dist" + ], + "repository": { + "type": "git", + "url": "https://github.com/janus-idp/backstage-plugins", + "directory": "plugins/topology" + }, + "keywords": [ + "backstage", + "plugin" + ], + "homepage": "https://janus-idp.io/", + "bugs": "https://github.com/janus-idp/backstage-plugins/issues", + "dependencies": { + "@backstage/plugin-permission-common": "^0.7.13" + } +} diff --git a/plugins/topology-common/src/index.ts b/plugins/topology-common/src/index.ts new file mode 100644 index 0000000000..7597e07483 --- /dev/null +++ b/plugins/topology-common/src/index.ts @@ -0,0 +1,7 @@ +/** + * Common functionalities for the topology plugin. + * + * @packageDocumentation + */ + +export * from './permissions'; diff --git a/plugins/topology-common/src/permissions.ts b/plugins/topology-common/src/permissions.ts new file mode 100644 index 0000000000..b340bc24c7 --- /dev/null +++ b/plugins/topology-common/src/permissions.ts @@ -0,0 +1,13 @@ +import { createPermission } from '@backstage/plugin-permission-common'; + +export const topologyViewPermission = createPermission({ + name: 'topology.view.read', + attributes: { + action: 'read', + }, +}); + +/** + * List of all permissions on permission polices. + */ +export const topologyPermissions = [topologyViewPermission]; diff --git a/plugins/topology-common/tsconfig.json b/plugins/topology-common/tsconfig.json new file mode 100644 index 0000000000..6f85cfdba0 --- /dev/null +++ b/plugins/topology-common/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@backstage/cli/config/tsconfig.json", + "include": ["src"], + "exclude": ["node_modules"], + "compilerOptions": { + "outDir": "../../dist-types/plugins/topology-common", + "rootDir": "." + } +} diff --git a/plugins/topology-common/turbo.json b/plugins/topology-common/turbo.json new file mode 100644 index 0000000000..37ef0169ca --- /dev/null +++ b/plugins/topology-common/turbo.json @@ -0,0 +1,9 @@ +{ + "extends": ["//"], + "pipeline": { + "tsc": { + "outputs": ["../../dist-types/plugins/topology-common/**"], + "dependsOn": ["^tsc"] + } + } +} diff --git a/plugins/topology/README.md b/plugins/topology/README.md index f1bfb31d3d..108e8a5e33 100644 --- a/plugins/topology/README.md +++ b/plugins/topology/README.md @@ -337,6 +337,16 @@ Topology is a front-end plugin that enables you to view the workloads as nodes t - Your Backstage application is installed and running. - You have installed the Topology plugin. For the installation process, see [Installation](#installation). +- If RBAC permission framework is enabled, ensure to add the following permission policies in an external permission policies configuration file named `rbac-policy.csv` to allow the rbac admins or your desired user(s)/group(s) to access the topology plugin: + +```csv rbac-policy.csv +g, user:default/, role:default/topology-viewer +p, role:default/topology-viewer, topology.view.read, read, allow +p, role:default/topology-viewer, kubernetes.proxy, use, allow +p, role:default/topology-viewer, catalog-entity, read, allow +``` + +`p, role:default/topology-viewer, topology.view.read, read, allow` grants the user the ability to see the Topology panel. `p, role:default/topology-viewer, kubernetes.proxy, use, allow` grants the user the ability to view the pod logs. `p, role:default/topology-viewer, catalog-entity, read, allow` grants the user the ability to see the catalog item. #### Procedure diff --git a/plugins/topology/package.json b/plugins/topology/package.json index 674979e616..a1cc268f7b 100644 --- a/plugins/topology/package.json +++ b/plugins/topology/package.json @@ -28,11 +28,14 @@ "@backstage/catalog-model": "^1.4.5", "@backstage/core-components": "^0.14.6", "@backstage/core-plugin-api": "^1.9.2", + "@backstage/plugin-catalog-common": "^1.0.23", "@backstage/plugin-catalog-react": "^1.11.3", "@backstage/plugin-kubernetes": "^0.11.9", "@backstage/plugin-kubernetes-common": "^0.7.5", + "@backstage/plugin-permission-react": "^0.4.22", "@backstage/theme": "^0.5.3", "@janus-idp/shared-react": "2.6.2", + "@janus-idp/backstage-plugin-topology-common": "0.1.0", "@kubernetes/client-node": "^0.20.0", "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.11.3", diff --git a/plugins/topology/src/components/Topology/TopologyEmptyState.tsx b/plugins/topology/src/components/Topology/TopologyEmptyState.tsx index 52fd9e76d7..1d6b8aa52d 100644 --- a/plugins/topology/src/components/Topology/TopologyEmptyState.tsx +++ b/plugins/topology/src/components/Topology/TopologyEmptyState.tsx @@ -2,13 +2,22 @@ import React from 'react'; import { EmptyState, + EmptyStateBody, EmptyStateHeader, EmptyStateIcon, EmptyStateVariant, } from '@patternfly/react-core'; import { TopologyIcon } from '@patternfly/react-icons'; -export const TopologyEmptyState = () => { +type TopologyEmptyStateProps = { + title?: string; + description?: string; +}; + +export const TopologyEmptyState = ({ + title, + description, +}: TopologyEmptyStateProps) => { return ( { className="pf-topology-visualization-surface" > } headingLevel="h3" - /> + > + {description} + ); }; diff --git a/plugins/topology/src/components/Topology/TopologyViewWorkloadComponent.test.tsx b/plugins/topology/src/components/Topology/TopologyViewWorkloadComponent.test.tsx index a294c13c18..a46d980038 100644 --- a/plugins/topology/src/components/Topology/TopologyViewWorkloadComponent.test.tsx +++ b/plugins/topology/src/components/Topology/TopologyViewWorkloadComponent.test.tsx @@ -1,5 +1,7 @@ import React from 'react'; +import { usePermission } from '@backstage/plugin-permission-react'; + import { render } from '@testing-library/react'; import { K8sResourcesContext } from '../../hooks/K8sResourcesContext'; @@ -12,6 +14,14 @@ jest.mock('../../hooks/useWorkloadWatcher', () => ({ const mockUseWorkloadsWatcher = useWorkloadsWatcher as jest.Mock; +jest.mock('@backstage/plugin-permission-react', () => ({ + usePermission: jest.fn(), +})); + +const mockUsePermission = usePermission as jest.MockedFunction< + typeof usePermission +>; + jest.mock('@patternfly/react-topology', () => ({ useVisualizationController: () => ({ getGraph: () => ({ @@ -35,6 +45,10 @@ jest.mock('@patternfly/react-topology', () => ({ })); describe('TopologyViewWorkloadComponent', () => { + beforeEach(() => { + mockUsePermission.mockReturnValue({ loading: false, allowed: true }); + }); + it('should show loading state when loading is true', () => { mockUseWorkloadsWatcher.mockReturnValue({ loaded: false, @@ -71,4 +85,28 @@ describe('TopologyViewWorkloadComponent', () => { ); expect(getByText(/topologyview/i)).not.toBeNull(); }); + + it('should not render TopologyView when data is available, loading is false, but user is not allowed', () => { + mockUsePermission.mockReturnValue({ loading: false, allowed: false }); + + mockUseWorkloadsWatcher.mockReturnValue({ + loaded: true, + dataModel: { nodes: [{}] }, + }); + const { getByText } = render( + {} }} + > + + , + ); + expect(getByText(/topologyview/i)).not.toBeNull(); + + const { getByRole } = render(); + expect( + getByRole('heading', { + name: /no resources found/i, + }), + ).not.toBeNull(); + }); }); diff --git a/plugins/topology/src/components/Topology/TopologyViewWorkloadComponent.tsx b/plugins/topology/src/components/Topology/TopologyViewWorkloadComponent.tsx index d782bef181..906cea2fcd 100644 --- a/plugins/topology/src/components/Topology/TopologyViewWorkloadComponent.tsx +++ b/plugins/topology/src/components/Topology/TopologyViewWorkloadComponent.tsx @@ -25,6 +25,11 @@ import TopologyToolbar from './TopologyToolbar'; import './TopologyToolbar.css'; +import { catalogEntityReadPermission } from '@backstage/plugin-catalog-common/alpha'; +import { usePermission } from '@backstage/plugin-permission-react'; + +import { topologyViewPermission } from '@janus-idp/backstage-plugin-topology-common'; + type TopologyViewWorkloadComponentProps = { useToolbar?: boolean; }; @@ -49,6 +54,15 @@ const TopologyViewWorkloadComponent = ({ removeSelectedIdParam, ] = useSideBar(); + const topologyViewPermissionResult = usePermission({ + permission: topologyViewPermission, + }); + + const catalogEntityPermissionResult = usePermission({ + permission: catalogEntityReadPermission, + resourceRef: catalogEntityReadPermission.resourceType, + }); + const allErrors: ClusterErrors = [ ...(responseError ? [{ message: responseError }] : []), ...(selectedClusterErrors ?? []), @@ -126,8 +140,15 @@ const TopologyViewWorkloadComponent = ({ sideBarOpen={sideBarOpen} minSideBarSize="400px" > - {loaded && dataModel?.nodes?.length === 0 ? ( - + {isDataModelEmpty || + !( + topologyViewPermissionResult.allowed && + catalogEntityPermissionResult.allowed + ) ? ( + ) : ( )} diff --git a/yarn.lock b/yarn.lock index 43d86eba3c..a2430bf50b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3226,6 +3226,16 @@ ajv "^8.10.0" lodash "^4.17.21" +"@backstage/catalog-model@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@backstage/catalog-model/-/catalog-model-1.5.0.tgz#7f5c4a80a3341555db5209fbc6fc2d25f6500707" + integrity sha512-CfLO5/DMGahneuLU4KTQEs1tgNhBciUtyGUDZB4Ii9i1Uha1poWcqp4HKg61lj1hmXNDUHmlbFqY9W7kmzRC0A== + dependencies: + "@backstage/errors" "^1.2.4" + "@backstage/types" "^1.1.1" + ajv "^8.10.0" + lodash "^4.17.21" + "@backstage/cli-common@^0.1.13": version "0.1.13" resolved "https://registry.yarnpkg.com/@backstage/cli-common/-/cli-common-0.1.13.tgz#cbeda6a359ca4437fc782f0ac51bb957e8d49e73" @@ -4077,6 +4087,15 @@ "@backstage/plugin-permission-common" "^0.7.13" "@backstage/plugin-search-common" "^1.2.11" +"@backstage/plugin-catalog-common@^1.0.23": + version "1.0.23" + resolved "https://registry.yarnpkg.com/@backstage/plugin-catalog-common/-/plugin-catalog-common-1.0.23.tgz#2ba1fe13450f6283e049acc83aa4fcebda6153e8" + integrity sha512-u04VUq/2wNjF9ikpGxdt1kXSQf5VlPDWTwzYyJYKD80qGa6l/klUXJ3IBs8P4XyQObkPNyS/Tho/H8XDFNeqEw== + dependencies: + "@backstage/catalog-model" "^1.5.0" + "@backstage/plugin-permission-common" "^0.7.13" + "@backstage/plugin-search-common" "^1.2.11" + "@backstage/plugin-catalog-graph@^0.4.4": version "0.4.4" resolved "https://registry.yarnpkg.com/@backstage/plugin-catalog-graph/-/plugin-catalog-graph-0.4.4.tgz#ff26dcec8ae437d07bbdc98093aa52503fe2a1b1"