-
Notifications
You must be signed in to change notification settings - Fork 151
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(rbac): display administration to authorized users (#895)
- Loading branch information
Showing
14 changed files
with
282 additions
and
66 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { RbacPage } from './RbacPage'; | ||
export { Administration } from './Administration'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
export { rbacPlugin, RbacPage } from './plugin'; | ||
export { rbacPlugin, RbacPage, Administration } from './plugin'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
}, | ||
}), | ||
); |
Oops, something went wrong.