Skip to content

Commit

Permalink
feat(rbac): display administration to authorized users (#895)
Browse files Browse the repository at this point in the history
  • Loading branch information
debsmita1 authored Nov 15, 2023
1 parent 0d27284 commit 70ae509
Show file tree
Hide file tree
Showing 14 changed files with 282 additions and 66 deletions.
47 changes: 39 additions & 8 deletions plugins/rbac/README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,44 @@
# rbac
# RBAC frontend plugin for Backstage

Welcome to the rbac plugin!
The RBAC UI plugin offers a streamlined user interface for effectively managing permissions in your Backstage instance. It allows you to assign permissions to users and groups, empowering them to view, create, modify and delete Roles, provided they have the necessary permissions.

_This plugin was created through the Backstage CLI_
## For administrators

## Getting started
### Installation

Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/rbac](http://localhost:3000/rbac).
#### Prerequisites

You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory.
Follow the RBAC backend plugin [README](https://github.com/janus-idp/backstage-plugins/tree/main/plugins/rbac-backend) to integrate rbac in your Backstage instance

#### Procedure

1. Install the RBAC UI plugin using the following command:

```console
yarn workspace app add @janus-idp/backstage-plugin-rbac
```

2. Add Route in `packages/app/src/App.tsx`:

```tsx title="packages/app/src/App.tsx"
/* highlight-add-next-line */
import { RbacPage } from '@janus-idp/backstage-plugin-rbac';

<Route path="/rbac" element={<RbacPage />} />;
```

3. Add **Administration** Sidebar Item in `packages/app/src/components/Root/Root.tsx`:

```tsx title="packages/app/src/components/Root/Root.tsx"
/* highlight-add-next-line */
import { Administration } from '@janus-idp/backstage-plugin-rbac';

export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarPage>
<Sidebar>
...
<Administration />
...
</SidebarPage>
);
```
24 changes: 23 additions & 1 deletion plugins/rbac/dev/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,35 @@
import React from 'react';

import { createDevApp } from '@backstage/dev-utils';
import {
PermissionApi,
permissionApiRef,
} from '@backstage/plugin-permission-react';
import { TestApiProvider } from '@backstage/test-utils';

import { RbacPage, rbacPlugin } from '../src/plugin';

class MockPermissionApi implements PermissionApi {
readonly result;

constructor(fixtureData: any) {
this.result = fixtureData;
}

async authorize(_request: any): Promise<any> {
return this.result;
}
}

const mockApi = new MockPermissionApi({ result: 'ALLOW' });
createDevApp()
.registerPlugin(rbacPlugin)
.addPage({
element: <RbacPage />,
element: (
<TestApiProvider apis={[[permissionApiRef, mockApi]]}>
<RbacPage />
</TestApiProvider>
),
title: 'Administration',
path: '/rbac',
})
Expand Down
5 changes: 4 additions & 1 deletion plugins/rbac/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.11.3",
"@material-ui/lab": "^4.0.0-alpha.45",
"react-use": "^17.4.0"
"react-use": "^17.4.0",
"@backstage/plugin-permission-react": "^0.4.16",
"@janus-idp/backstage-plugin-rbac-common": "1.1.0",
"@mui/icons-material": "5.14.11"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0"
Expand Down
54 changes: 54 additions & 0 deletions plugins/rbac/src/api/RBACBackendClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {
ConfigApi,
createApiRef,
IdentityApi,
} from '@backstage/core-plugin-api';

// @public
export type RBACAPI = {
getUserAuthorization: () => Promise<{ status: string }>;
getRoles: () => Promise<any>;
};

export type Options = {
configApi: ConfigApi;
identityApi: IdentityApi;
};

// @public
export const rbacApiRef = createApiRef<RBACAPI>({
id: 'plugin.rbac.service',
});

export class RBACBackendClient implements RBACAPI {
// @ts-ignore
private readonly configApi: ConfigApi;
private readonly identityApi: IdentityApi;

constructor(options: Options) {
this.configApi = options.configApi;
this.identityApi = options.identityApi;
}

async getUserAuthorization() {
const { token: idToken } = await this.identityApi.getCredentials();
const backendUrl = this.configApi.getString('backend.baseUrl');
const jsonResponse = await fetch(`${backendUrl}/api/permission/`, {
headers: {
...(idToken && { Authorization: `Bearer ${idToken}` }),
},
});
return jsonResponse.json();
}

async getRoles() {
const { token: idToken } = await this.identityApi.getCredentials();
const backendUrl = this.configApi.getString('backend.baseUrl');
const jsonResponse = await fetch(`${backendUrl}/api/permission/roles`, {
headers: {
...(idToken && { Authorization: `Bearer ${idToken}` }),
},
});
return jsonResponse.json();
}
}
28 changes: 28 additions & 0 deletions plugins/rbac/src/components/Administration.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';
import { useAsync } from 'react-use';

import { SidebarItem } from '@backstage/core-components';
import { IconComponent, useApi } from '@backstage/core-plugin-api';

import AdminPanelSettingsOutlinedIcon from '@mui/icons-material/AdminPanelSettingsOutlined';

import { rbacApiRef } from '../api/RBACBackendClient';

export const Administration = () => {
const rbacApi = useApi(rbacApiRef);
const { loading: isUserLoading, value: result } = useAsync(
async () => await rbacApi.getUserAuthorization(),
[],
);

if (!isUserLoading) {
return result?.status === 'Authorized' ? (
<SidebarItem
text="Administration"
to="rbac"
icon={AdminPanelSettingsOutlinedIcon as IconComponent}
/>
) : null;
}
return null;
};
67 changes: 67 additions & 0 deletions plugins/rbac/src/components/RbacPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React from 'react';

import {
RequirePermission,
usePermission,
} from '@backstage/plugin-permission-react';
import {
renderInTestApp,
setupRequestMockHandlers,
} from '@backstage/test-utils';

import { screen } from '@testing-library/react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';

import { RbacPage } from './RbacPage';

jest.mock('@backstage/plugin-permission-react', () => ({
usePermission: jest.fn(),
RequirePermission: jest.fn(),
}));
const mockUsePermission = usePermission as jest.MockedFunction<
typeof usePermission
>;

const RequirePermissionMock = RequirePermission as jest.MockedFunction<
typeof RequirePermission
>;

describe('RbacPage', () => {
const server = setupServer();
// Enable sane handlers for network requests
setupRequestMockHandlers(server);
// setup mock response
beforeEach(() => {
server.use(
rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))),
);
});

it('should render if authorized', async () => {
RequirePermissionMock.mockImplementation(props => <>{props.children}</>);
mockUsePermission.mockReturnValue({ loading: false, allowed: true });
await renderInTestApp(<RbacPage />);
expect(screen.getByText('Administration')).toBeInTheDocument();
expect(
screen.getByText('All content should be wrapped in a card like this.'),
).toBeTruthy();
});

it('should not render if not authorized', async () => {
RequirePermissionMock.mockImplementation(_props => <>Not Found</>);
mockUsePermission.mockReturnValue({ loading: false, allowed: false });

await renderInTestApp(<RbacPage />);
expect(screen.getByText('Not Found')).toBeInTheDocument();
});

it('should not render if loading', async () => {
RequirePermissionMock.mockImplementation(_props => null);
mockUsePermission.mockReturnValue({ loading: false, allowed: false });

const { queryByText } = await renderInTestApp(<RbacPage />);
expect(queryByText('Not Found')).not.toBeInTheDocument();
expect(queryByText('Administration')).not.toBeInTheDocument();
});
});
30 changes: 30 additions & 0 deletions plugins/rbac/src/components/RbacPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';

import { Content, Header, InfoCard, Page } from '@backstage/core-components';
import { RequirePermission } from '@backstage/plugin-permission-react';

import { Grid, Typography } from '@material-ui/core';

import { policyEntityReadPermission } from '@janus-idp/backstage-plugin-rbac-common';

export const RbacPage = () => (
<RequirePermission
permission={policyEntityReadPermission}
resourceRef={policyEntityReadPermission.resourceType}
>
<Page themeId="tool">
<Header title="Administration" />
<Content>
<Grid container spacing={3} direction="column">
<Grid item>
<InfoCard title="Information card">
<Typography variant="body1">
All content should be wrapped in a card like this.
</Typography>
</InfoCard>
</Grid>
</Grid>
</Content>
</Page>
</RequirePermission>
);
30 changes: 0 additions & 30 deletions plugins/rbac/src/components/RbacPage/RbacPage.test.tsx

This file was deleted.

22 changes: 0 additions & 22 deletions plugins/rbac/src/components/RbacPage/RbacPage.tsx

This file was deleted.

1 change: 0 additions & 1 deletion plugins/rbac/src/components/RbacPage/index.ts

This file was deleted.

2 changes: 2 additions & 0 deletions plugins/rbac/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { RbacPage } from './RbacPage';
export { Administration } from './Administration';
2 changes: 1 addition & 1 deletion plugins/rbac/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { rbacPlugin, RbacPage } from './plugin';
export { rbacPlugin, RbacPage, Administration } from './plugin';
27 changes: 26 additions & 1 deletion plugins/rbac/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,46 @@
import {
configApiRef,
createApiFactory,
createComponentExtension,
createPlugin,
createRoutableExtension,
identityApiRef,
} from '@backstage/core-plugin-api';

import { rbacApiRef, RBACBackendClient } from './api/RBACBackendClient';
import { rootRouteRef } from './routes';

export const rbacPlugin = createPlugin({
id: 'rbac',
routes: {
root: rootRouteRef,
},
apis: [
createApiFactory({
api: rbacApiRef,
deps: {
configApi: configApiRef,
identityApi: identityApiRef,
},
factory: ({ configApi, identityApi }) =>
new RBACBackendClient({ configApi, identityApi }),
}),
],
});

export const RbacPage = rbacPlugin.provide(
createRoutableExtension({
name: 'RbacPage',
component: () => import('./components/RbacPage').then(m => m.RbacPage),
component: () => import('./components').then(m => m.RbacPage),
mountPoint: rootRouteRef,
}),
);

export const Administration = rbacPlugin.provide(
createComponentExtension({
name: 'Administration',
component: {
lazy: () => import('./components').then(m => m.Administration),
},
}),
);
Loading

0 comments on commit 70ae509

Please sign in to comment.