Skip to content

Commit

Permalink
feat(tekton): add permissions support for tekton plugin (janus-idp#1854)
Browse files Browse the repository at this point in the history
  • Loading branch information
karthikjeeyar committed Jul 26, 2024
1 parent e04d231 commit f744896
Show file tree
Hide file tree
Showing 18 changed files with 241 additions and 113 deletions.
7 changes: 7 additions & 0 deletions plugins/rbac-backend/docs/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ Resource type permissions on the other hand are basic named permissions with a r
| 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 |

## Tekton

| Name | Resource Type | Policy | Description | Requirements |
| ---------------- | ------------- | ------ | ------------------------------------------------------------------------------------------------------------------ | ------------------- |
| tekton.view.read | | read | Allows the user to view the tekton plugin | catalog.entity.read |
| kubernetes.proxy | | | Allows the user to access the proxy endpoint (ability to read tekton pod logs and events within Showcase and RHDH) | catalog.entity.read |

## Topology

| Name | Resource Type | Policy | Description | Requirements |
Expand Down
1 change: 1 addition & 0 deletions plugins/tekton-common/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
11 changes: 11 additions & 0 deletions plugins/tekton-common/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Tekton Common plugin

Welcome to the tekton-common plugin!

This plugin contains common utilities for the tekton plugin.

# Tekton plugin for Backstage

The Tekton plugin enables you to visualize the `PipelineRun` resources available on the Kubernetes cluster.

For more information about Tekton plugin, see the [Tekton plugin documentation](https://github.com/janus-idp/backstage-plugins/tree/main/plugins/tekton) on GitHub.
59 changes: 59 additions & 0 deletions plugins/tekton-common/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"name": "@janus-idp/backstage-plugin-tekton-common",
"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",
"supported-versions": "1.28.4",
"pluginId": "tekton",
"pluginPackages": [
"@janus-idp/backstage-plugin-tekton",
"@janus-idp/backstage-plugin-tekton-common"
]
},
"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"
},
"dependencies": {
"@backstage/plugin-permission-common": "^0.8.0"
},
"devDependencies": {
"@backstage/cli": "0.26.11"
},
"files": [
"dist"
],
"repository": {
"type": "git",
"url": "https://github.com/janus-idp/backstage-plugins",
"directory": "plugins/tekton-common"
},
"keywords": [
"support:production",
"lifecycle:active",
"backstage",
"plugin"
],
"author": "Red Hat",
"homepage": "https://red.ht/rhdh",
"bugs": "https://github.com/janus-idp/backstage-plugins/issues",
"maintainers": [
"janus-idp/rhtap"
]
}
7 changes: 7 additions & 0 deletions plugins/tekton-common/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Common functionalities for the tekton plugin.
*
* @packageDocumentation
*/

export * from './permissions';
13 changes: 13 additions & 0 deletions plugins/tekton-common/src/permissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createPermission } from '@backstage/plugin-permission-common';

export const tektonViewPermission = createPermission({
name: 'tekton.view.read',
attributes: {
action: 'read',
},
});

/**
* List of all permissions on permission polices.
*/
export const tektonPermissions = [tektonViewPermission];
9 changes: 9 additions & 0 deletions plugins/tekton-common/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "@backstage/cli/config/tsconfig.json",
"include": ["src", "dev"],
"exclude": ["node_modules"],
"compilerOptions": {
"outDir": "../../dist-types/plugins/tekton-common",
"rootDir": "."
}
}
9 changes: 9 additions & 0 deletions plugins/tekton-common/turbo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": ["//"],
"pipeline": {
"tsc": {
"outputs": ["../../dist-types/plugins/tekton-common/**"],
"dependsOn": ["^tsc"]
}
}
}
5 changes: 4 additions & 1 deletion plugins/tekton/dev/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
KubernetesProxyApi,
kubernetesProxyApiRef,
} from '@backstage/plugin-kubernetes';
import { TestApiProvider } from '@backstage/test-utils';
import { permissionApiRef } from '@backstage/plugin-permission-react';
import { MockPermissionApi, TestApiProvider } from '@backstage/test-utils';

import { createDevAppThemes } from '@redhat-developer/red-hat-developer-hub-theme';

Expand Down Expand Up @@ -41,6 +42,7 @@ const mockEntity: Entity = {
},
};

const mockPermissionApi = new MockPermissionApi();
class MockKubernetesProxyApi implements KubernetesProxyApi {
async getPodLogs(_request: any): Promise<any> {
const delayedResponse = (data: string, ms: number) =>
Expand Down Expand Up @@ -177,6 +179,7 @@ createDevApp()
new MockKubernetesClient(mockKubernetesPlrResponse),
],
[kubernetesProxyApiRef, new MockKubernetesProxyApi()],
[permissionApiRef, mockPermissionApi],
]}
>
<EntityProvider entity={mockEntity}>
Expand Down
5 changes: 4 additions & 1 deletion plugins/tekton/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"pluginId": "tekton",
"pluginPackage": "@janus-idp/backstage-plugin-tekton",
"pluginPackages": [
"@janus-idp/backstage-plugin-tekton"
"@janus-idp/backstage-plugin-tekton",
"@janus-idp/backstage-plugin-tekton-common"
]
},
"sideEffects": false,
Expand All @@ -42,6 +43,8 @@
"@backstage/plugin-kubernetes-common": "^0.8.1",
"@backstage/theme": "^0.5.6",
"@janus-idp/shared-react": "2.9.0",
"@backstage/plugin-permission-react": "^0.4.24",
"@janus-idp/backstage-plugin-tekton-common": "0.1.0",
"@kubernetes/client-node": "^0.20.0",
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.11.3",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from 'react';
import { BrowserRouter } from 'react-router-dom';

import { usePermission } from '@backstage/plugin-permission-react';

import { render } from '@testing-library/react';

import { ComputedStatus } from '@janus-idp/shared-react';
Expand All @@ -19,7 +21,19 @@ jest.mock('@backstage/plugin-catalog-react', () => ({
}),
}));

jest.mock('@backstage/plugin-permission-react', () => ({
usePermission: jest.fn(),
}));

const mockUsePermission = usePermission as jest.MockedFunction<
typeof usePermission
>;

describe('PipelineRunList', () => {
beforeEach(() => {
mockUsePermission.mockReturnValue({ loading: false, allowed: true });
});

it('should render PipelineRunList if available', () => {
const mockContextData = {
watchResourcesData: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import * as React from 'react';

import { kubernetesProxyPermission } from '@backstage/plugin-kubernetes-common';
import { usePermission } from '@backstage/plugin-permission-react';

import { IconButton } from '@material-ui/core';
import { Flex, FlexItem } from '@patternfly/react-core';
import { Tooltip } from '@patternfly/react-core/dist/esm/components/Tooltip/Tooltip';
Expand Down Expand Up @@ -39,6 +42,10 @@ const PipelineRunRowActions: React.FC<{ pipelineRun: PipelineRunKind }> = ({
);
const activeTaskName = sbomTaskRun?.metadata?.name;

const hasKubernetesProxyAccess = usePermission({
permission: kubernetesProxyPermission,
});

const openDialog = (viewLogs?: boolean) => {
if (viewLogs) setNoActiveTask(true);
setOpen(true);
Expand Down Expand Up @@ -100,9 +107,21 @@ const PipelineRunRowActions: React.FC<{ pipelineRun: PipelineRunKind }> = ({
/>
<Flex gap={{ default: 'gapXs' }}>
<FlexItem>
<Tooltip content="View logs">
<IconButton size="small" onClick={() => openDialog(true)}>
<ViewLogsIcon />
<Tooltip
content={
hasKubernetesProxyAccess.allowed
? 'View logs'
: 'Unauthorized to view logs'
}
>
<IconButton
size="small"
data-testid="view-logs-icon"
onClick={() => openDialog(true)}
disabled={!hasKubernetesProxyAccess.allowed}
style={{ pointerEvents: 'auto', padding: 0 }}
>
<ViewLogsIcon disabled={!hasKubernetesProxyAccess.allowed} />
</IconButton>
</Tooltip>
</FlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import '@testing-library/jest-dom';
import { BrowserRouter } from 'react-router-dom';

import { LinkProps } from '@backstage/core-components';
import { usePermission } from '@backstage/plugin-permission-react';

import {
act,
Expand All @@ -22,6 +23,14 @@ import { TektonResourcesContext } from '../../../hooks/TektonResourcesContext';
import { TektonResourcesContextData } from '../../../types/types';
import PipelineRunRowActions from '../PipelineRunRowActions';

jest.mock('@backstage/plugin-permission-react', () => ({
usePermission: jest.fn(),
}));

const mockUsePermission = usePermission as jest.MockedFunction<
typeof usePermission
>;

jest.mock('@material-ui/core', () => ({
...jest.requireActual('@material-ui/core'),
makeStyles: () => () => {
Expand Down Expand Up @@ -84,6 +93,10 @@ const TestPipelineRunRowActions = ({
);

describe('PipelineRunRowActions', () => {
beforeEach(() => {
mockUsePermission.mockReturnValue({ loading: false, allowed: true });
});

it('should render the icon space holder', () => {
render(
<TestPipelineRunRowActions
Expand Down Expand Up @@ -134,6 +147,21 @@ describe('PipelineRunRowActions', () => {
expect(screen.queryByTestId('external-sbom-link')).toBeInTheDocument();
});

it('should disable the view logs action if the user does not have enough permission', () => {
mockUsePermission.mockReturnValue({ loading: false, allowed: false });

render(
<TestPipelineRunRowActions
pipelineRun={mockKubernetesPlrResponse.pipelineruns[1]}
/>,
);

expect(screen.queryByTestId('view-logs-icon')).toBeInTheDocument();
expect(
screen.queryByTestId('view-logs-icon')?.getAttribute('disabled'),
).not.toBeNull();
});

it('should not open sbom logs modal when the view external SBOM link is clicked', async () => {
render(
<TestPipelineRunRowActions
Expand Down
20 changes: 20 additions & 0 deletions plugins/tekton/src/components/Tekton/TektonCIComponent.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React from 'react';

import { usePermission } from '@backstage/plugin-permission-react';

import { render } from '@testing-library/react';

import { TektonCIComponent } from './TektonCIComponent';
Expand All @@ -23,7 +25,25 @@ jest.mock('../../hooks/useTektonObjectsResponse', () => ({
}),
}));

jest.mock('@backstage/plugin-permission-react', () => ({
usePermission: jest.fn(),
}));

const mockUsePermission = usePermission as jest.MockedFunction<
typeof usePermission
>;

describe('TektonComponent', () => {
beforeEach(() => {
mockUsePermission.mockReturnValue({ loading: false, allowed: true });
});

it('should render Permission alert if the user does not have view permission', () => {
mockUsePermission.mockReturnValue({ loading: false, allowed: false });
const { getByTestId } = render(<TektonCIComponent />);
expect(getByTestId('no-permission-alert')).toBeInTheDocument();
});

it('should render TektonComponent', () => {
const { getByText } = render(<TektonCIComponent />);
expect(getByText(/No Pipeline Runs found/i)).not.toBeNull();
Expand Down
6 changes: 6 additions & 0 deletions plugins/tekton/src/components/Tekton/TektonCIComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import React from 'react';
import { TektonResourcesContext } from '../../hooks/TektonResourcesContext';
import { useDarkTheme } from '../../hooks/useDarkTheme';
import { useTektonObjectsResponse } from '../../hooks/useTektonObjectsResponse';
import { useTektonViewPermission } from '../../hooks/useTektonViewPermission';
import { ModelsPlural } from '../../models';
import PermissionAlert from '../common/PermissionAlert';
import PipelineRunList from '../PipelineRunList/PipelineRunList';

export const TektonCIComponent = () => {
Expand All @@ -15,7 +17,11 @@ export const TektonCIComponent = () => {
ModelsPlural.pods,
];
const tektonResourcesContextData = useTektonObjectsResponse(watchedResources);
const hasViewPermission = useTektonViewPermission();

if (!hasViewPermission) {
return <PermissionAlert />;
}
return (
<TektonResourcesContext.Provider value={tektonResourcesContextData}>
<PipelineRunList />
Expand Down
14 changes: 14 additions & 0 deletions plugins/tekton/src/components/common/PermissionAlert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';

import { Alert, AlertTitle } from '@material-ui/lab';

const PermissionAlert = () => {
return (
<Alert severity="warning" data-testid="no-permission-alert">
<AlertTitle>Permission required</AlertTitle>
To view Tekton Pipeline Runs, contact your administrator to give you the
tekton.view.read permission.
</Alert>
);
};
export default PermissionAlert;
11 changes: 11 additions & 0 deletions plugins/tekton/src/hooks/useTektonViewPermission.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { usePermission } from '@backstage/plugin-permission-react';

import { tektonViewPermission } from '@janus-idp/backstage-plugin-tekton-common';

export const useTektonViewPermission = () => {
const tektonViewPermissionResult = usePermission({
permission: tektonViewPermission,
});

return tektonViewPermissionResult.allowed;
};
Loading

0 comments on commit f744896

Please sign in to comment.