Skip to content

Commit

Permalink
feat(rbac): list roles
Browse files Browse the repository at this point in the history
  • Loading branch information
debsmita1 committed Nov 20, 2023
1 parent dacd46d commit 56a8e90
Show file tree
Hide file tree
Showing 15 changed files with 731 additions and 39 deletions.
2 changes: 1 addition & 1 deletion packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"@backstage/plugin-search-backend-module-pg": "^0.5.15",
"@backstage/plugin-search-backend-node": "^1.2.10",
"@backstage/plugin-techdocs-backend": "^1.8.0",
"@janus-idp/backstage-plugin-rbac-backend": "^1.0.0",
"@janus-idp/backstage-plugin-rbac-backend": "^1.6.3",
"better-sqlite3": "^9.0.0",
"dockerode": "^4.0.0",
"express": "^4.18.2",
Expand Down
120 changes: 118 additions & 2 deletions plugins/rbac/dev/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import {
} from '@backstage/plugin-permission-react';
import { TestApiProvider } from '@backstage/test-utils';

import { Role, RoleBasedPolicy } from '@janus-idp/backstage-plugin-rbac-common';

import { RBACAPI, rbacApiRef } from '../src/api/RBACBackendClient';
import { RbacPage, rbacPlugin } from '../src/plugin';

class MockPermissionApi implements PermissionApi {
Expand All @@ -21,12 +24,125 @@ class MockPermissionApi implements PermissionApi {
}
}

const mockApi = new MockPermissionApi({ result: 'ALLOW' });
class MockRBACApi implements RBACAPI {
readonly resources;

constructor(fixtureData: Role[]) {
this.resources = fixtureData;
}
async getRoles(): Promise<Role[]> {
return this.resources;
}
async getPolicies(): Promise<RoleBasedPolicy[]> {
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',
},
];
}

async getUserAuthorization(): Promise<{ status: string }> {
return {
status: 'Authorized',
};
}

async deleteRole(_roleName: string): Promise<number> {
return 204;
}
}

const mockPermissionApi = new MockPermissionApi({ result: 'ALLOW' });
const mockRBACApi = new MockRBACApi([
{
memberReferences: ['user:default/guest'],
name: 'role:default/guests',
},
{
memberReferences: ['user:default/xyz', 'group:default/admins'],
name: 'role:default/rbac_admin',
},
]);

createDevApp()
.registerPlugin(rbacPlugin)
.addPage({
element: (
<TestApiProvider apis={[[permissionApiRef, mockApi]]}>
<TestApiProvider
apis={[
[permissionApiRef, mockPermissionApi],
[rbacApiRef, mockRBACApi],
]}
>
<RbacPage />
</TestApiProvider>
),
Expand Down
9 changes: 5 additions & 4 deletions plugins/rbac/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,17 @@
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/catalog-model": "^1.4.3",
"@backstage/core-components": "^0.13.6",
"@backstage/core-plugin-api": "^1.7.0",
"@backstage/plugin-permission-react": "^0.4.16",
"@backstage/theme": "^0.4.3",
"@janus-idp/backstage-plugin-rbac-common": "1.1.0",
"@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",
"@backstage/plugin-permission-react": "^0.4.16",
"@janus-idp/backstage-plugin-rbac-common": "1.1.0",
"@mui/icons-material": "5.14.11"
"@mui/icons-material": "5.14.11",
"react-use": "^17.4.0"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0"
Expand Down
38 changes: 37 additions & 1 deletion plugins/rbac/src/api/RBACBackendClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import {
IdentityApi,
} from '@backstage/core-plugin-api';

import { Role, RoleBasedPolicy } from '@janus-idp/backstage-plugin-rbac-common';

// @public
export type RBACAPI = {
getUserAuthorization: () => Promise<{ status: string }>;
getRoles: () => Promise<any>;
getRoles: () => Promise<Role[]>;
getPolicies: () => Promise<RoleBasedPolicy[]>;
deleteRole: (role: string) => Promise<number>;
};

export type Options = {
Expand Down Expand Up @@ -51,4 +55,36 @@ export class RBACBackendClient implements RBACAPI {
});
return jsonResponse.json();
}

async getPolicies() {
const { token: idToken } = await this.identityApi.getCredentials();
const backendUrl = this.configApi.getString('backend.baseUrl');
const jsonResponse = await fetch(`${backendUrl}/api/permission/policies`, {
headers: {
...(idToken && { Authorization: `Bearer ${idToken}` }),
},
});
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 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.status;
}
}
24 changes: 24 additions & 0 deletions plugins/rbac/src/components/DeleteRole.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';

import { IconButton } from '@material-ui/core';
import Delete from '@mui/icons-material/Delete';

type DeleteRoleProps = {
openDialog: (name: string) => void;
roleName: string;
};

const DeleteRole = ({ openDialog, roleName }: DeleteRoleProps) => (
<span data-testid="delete-role">
<IconButton
onClick={() => openDialog(roleName)}
aria-label="Delete"
disabled={false}
title="Delete Role"
>
<Delete />
</IconButton>
</span>
);

export default DeleteRole;
101 changes: 101 additions & 0 deletions plugins/rbac/src/components/DeleteRoleDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React from 'react';

import { DismissableBanner } from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';

import {
Box,
Button,
createStyles,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
IconButton,
makeStyles,
Theme,
} from '@material-ui/core';
import CloseIcon from '@material-ui/icons/Close';

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

const useStyles = makeStyles((theme: Theme) =>
createStyles({
titleContainer: {
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
},
closeButton: {
position: 'absolute',
right: theme.spacing(1),
top: theme.spacing(1),
color: theme.palette.grey[500],
},
}),
);

type DeleteRoleDialogProps = {
open: boolean;
closeDialog: () => void;
roleName: string;
};

const DeleteRoleDialog = ({
open,
closeDialog,
roleName,
}: DeleteRoleDialogProps) => {
const classes = useStyles();
const [error, setError] = React.useState<string>();

const rbacApi = useApi(rbacApiRef);

const deleteRole = async () => {
try {
await rbacApi.deleteRole(roleName);
} catch (err: any) {
setError(err instanceof Error ? err.message : `${err}`);
}
closeDialog();
};

return (
<Dialog maxWidth="xl" fullWidth open={open} onClose={closeDialog}>
<DialogTitle id="delete-role" title="Delete Role">
<Box className={classes.titleContainer}>
Delete Role
<IconButton
aria-label="close"
className={classes.closeButton}
onClick={closeDialog}
>
<CloseIcon />
</IconButton>
</Box>
</DialogTitle>
<DialogContent>
Do you want to delete the role{' '}
<span style={{ fontWeight: 'bold' }}>{roleName}</span> ?
</DialogContent>
{error && (
<DismissableBanner
message={error}
variant="error"
fixed={false}
id="delete-role-error"
/>
)}
<DialogActions>
<Button color="primary" onClick={deleteRole}>
OK
</Button>
<Button color="primary" onClick={closeDialog}>
Cancel
</Button>
</DialogActions>
</Dialog>
);
};

export default DeleteRoleDialog;
Loading

0 comments on commit 56a8e90

Please sign in to comment.