Skip to content

Commit

Permalink
feat(rbac): support for adding permission policies to roles (janus-id…
Browse files Browse the repository at this point in the history
  • Loading branch information
divyanshiGupta authored Dec 21, 2023
1 parent fdb0dd9 commit dd11c3a
Show file tree
Hide file tree
Showing 40 changed files with 1,069 additions and 67 deletions.
14 changes: 14 additions & 0 deletions plugins/rbac/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ The RBAC UI plugin offers a streamlined user interface for effectively managing

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

---

**NOTE**

To enable create role button on Administration -> RBAC roles list page, the role associacted with your user should have the following permission policies associated with it. Add the following in your permission policies configuration file:

```CSV
p, role:default/team_a, catalog-entity, read, allow
p, role:default/team_a, policy-entity, create, allow
g, user:default/user, role:default/team_a
```

---

#### Procedure

1. Install the RBAC UI plugin using the following command:
Expand Down
4 changes: 4 additions & 0 deletions plugins/rbac/dev/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ class MockRBACApi implements RBACAPI {
async createRole(_role: Role): Promise<Response> {
return { status: 200 } as Response;
}

async createPolicy(_data: any): Promise<Response> {
return { status: 200 } as Response;
}
}

const mockPermissionApi = new MockPermissionApi({ result: 'ALLOW' });
Expand Down
19 changes: 19 additions & 0 deletions plugins/rbac/src/api/RBACBackendClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export type RBACAPI = {
oldPolicy: RoleBasedPolicy,
newPolicy: RoleBasedPolicy,
) => Promise<Response>;
createPolicy: (data: any) => Promise<Response>;
};

export type Options = {
Expand Down Expand Up @@ -268,4 +269,22 @@ export class RBACBackendClient implements RBACAPI {
);
return jsonResponse;
}

async createPolicy(data: RoleBasedPolicy[]) {
const { token: idToken } = await this.identityApi.getCredentials();
const backendUrl = this.configApi.getString('backend.baseUrl');
const jsonResponse = await fetch(`${backendUrl}/api/permission/policies`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
...(idToken && { Authorization: `Bearer ${idToken}` }),
},
body: JSON.stringify(data),
});
if (jsonResponse.status !== 200 && jsonResponse.status !== 201) {
return jsonResponse.json();
}
return jsonResponse;
}
}
16 changes: 11 additions & 5 deletions plugins/rbac/src/components/CreateRole/AddMembersForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const AddMembersForm = ({
membersData,
}: AddMembersFormProps) => {
const [search, setSearch] = React.useState<string>('');
const [selectedMember, setSelectedMember] = React.useState<SelectedMember>();

const getDescription = (member: MemberEntity) => {
const memberCount = getMembersCount(member);
Expand Down Expand Up @@ -74,10 +75,16 @@ export const AddMembersForm = ({
}
loading={membersData.loading}
loadingText={<LinearProgress />}
onChange={(_e, value: SelectedMember | null) =>
setFieldValue('selectedMembers', [...selectedMembers, value])
}
disableClearable
value={selectedMember}
onChange={(_e, value: SelectedMember) => {
setSelectedMember(value);
if (value) {
setFieldValue('selectedMembers', [...selectedMembers, value]);
}
}}
inputValue={search}
onInputChange={(_e, newSearch: string) => setSearch(newSearch)}
getOptionDisabled={(option: SelectedMember) =>
!!selectedMembers.find(
(sm: SelectedMember) => sm.etag === option.etag,
Expand All @@ -86,6 +93,7 @@ export const AddMembersForm = ({
renderOption={(option: SelectedMember, state) => (
<MembersDropdownOption option={option} state={state} />
)}
clearOnEscape
renderInput={params => (
<TextField
{...params}
Expand All @@ -94,8 +102,6 @@ export const AddMembersForm = ({
placeholder="Search by user name or group name"
error={!!selectedMembersError}
helperText={selectedMembersError ?? ''}
value={search}
onChange={e => setSearch(e.target.value)}
required
/>
)}
Expand Down
49 changes: 36 additions & 13 deletions plugins/rbac/src/components/CreateRole/AddedMembersTableColumn.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,39 @@
import React from 'react';

import { TableColumn } from '@backstage/core-components';
import { Link, TableColumn } from '@backstage/core-components';

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

import { getKindNamespaceName } from '../../utils/rbac-utils';
import { RoleFormValues, SelectedMember } from './types';

export const basicSelectedMembersColumns =
(): TableColumn<SelectedMember>[] => [
{
title: 'Type',
field: 'type',
type: 'string',
},
{
title: 'Members',
field: 'members',
type: 'numeric',
align: 'left',
emptyValue: '-',
},
];

export const reviewStepMemebersTableColumns = () => [
{
title: 'Name',
field: 'label',
type: 'string',
},
...basicSelectedMembersColumns(),
];

export const selectedMembersColumns = (
selectedMembers: SelectedMember[],
setFieldValue: (
Expand All @@ -28,19 +54,16 @@ export const selectedMembersColumns = (
title: 'Name',
field: 'label',
type: 'string',
render: props => {
const { kind, namespace, name } = getKindNamespaceName(props.ref);
return (
<Link to={`/catalog/${namespace}/${kind}/${name}`} target="blank">
{props.label}
</Link>
);
},
},
{
title: 'Type',
field: 'type',
type: 'string',
},
{
title: 'Members',
field: 'members',
type: 'numeric',
align: 'left',
emptyValue: '-',
},
...basicSelectedMembersColumns(),
{
title: 'Actions',
sorting: false,
Expand Down
4 changes: 3 additions & 1 deletion plugins/rbac/src/components/CreateRole/CreateRolePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { RequirePermission } from '@backstage/plugin-permission-react';

import { rbacApiRef } from '../../api/RBACBackendClient';
import { MemberEntity } from '../../types';
import { initialPermissionPolicyRowValue } from './const';
import { RoleForm } from './RoleForm';
import { RoleFormValues } from './types';

Expand All @@ -27,6 +28,7 @@ export const CreateRolePage = () => {
kind: 'role',
description: '',
selectedMembers: [],
permissionPoliciesRows: [initialPermissionPolicyRowValue],
};

return (
Expand All @@ -35,7 +37,7 @@ export const CreateRolePage = () => {
resourceRef={catalogEntityReadPermission.resourceType}
>
<Page themeId="tool">
<Header title="Create role" />
<Header title="Create role" type="RBAC" typeLink="/rbac" />
<Content>
<RoleForm
initialValues={initialValues}
Expand Down
12 changes: 12 additions & 0 deletions plugins/rbac/src/components/CreateRole/EditRolePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ export const EditRolePage = () => {
kind: roleKind || 'role',
description: '',
selectedMembers,
permissionPoliciesRows: [
{
plugin: '',
permission: '',
policies: [
{ label: 'Create', checked: false },
{ label: 'Read', checked: false },
{ label: 'Update', checked: false },
{ label: 'Delete', checked: false },
],
},
],
};
const renderPage = () => {
if (loading) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const MembersDropdownOption = ({
const parts = parse(label, matches);

return (
<Box>
<Box key={`${etag}`}>
{parts.map(part => (
<span
key={`${part.text}-${etag}`}
Expand Down
153 changes: 153 additions & 0 deletions plugins/rbac/src/components/CreateRole/PermissionPoliciesForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import React from 'react';
import { useAsync } from 'react-use';

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

import { makeStyles } from '@material-ui/core';
import AddIcon from '@mui/icons-material/Add';
import Button from '@mui/material/Button';
import FormHelperText from '@mui/material/FormHelperText';
import { FormikErrors } from 'formik';

import { rbacApiRef } from '../../api/RBACBackendClient';
import { getPluginsPermissionPoliciesData } from '../../utils/create-role-utils';
import { initialPermissionPolicyRowValue } from './const';
import { PermissionPoliciesFormRow } from './PermissionPoliciesFormRow';
import { PermissionPolicyRow, RoleFormValues } from './types';

const useStyles = makeStyles(theme => ({
permissionPoliciesForm: {
padding: '20px',
border: `2px solid ${theme.palette.border}`,
borderRadius: '5px',
},
addButton: {
color: theme.palette.primary.light,
},
}));

type PermissionPoliciesFormProps = {
permissionPoliciesRows: PermissionPolicyRow[];
permissionPoliciesRowsError: FormikErrors<PermissionPolicyRow>[];
setFieldValue: (
field: string,
value: any,
shouldValidate?: boolean,
) => Promise<FormikErrors<RoleFormValues>> | Promise<void>;
setFieldError: (field: string, value: string | undefined) => void;
handleBlur: React.FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
};

export const PermissionPoliciesForm = ({
permissionPoliciesRows,
permissionPoliciesRowsError,
setFieldValue,
setFieldError,
handleBlur,
}: PermissionPoliciesFormProps) => {
const classes = useStyles();
const rbacApi = useApi(rbacApiRef);

const { value: permissionPolicies, loading: permissionPoliciesLoading } =
useAsync(async () => {
return await rbacApi.listPermissions();
});

const permissionPoliciesData =
!permissionPoliciesLoading && permissionPolicies
? getPluginsPermissionPoliciesData(permissionPolicies)
: undefined;

const onChangePlugin = (plugin: string, index: number) => {
setFieldValue(`permissionPoliciesRows[${index}].plugin`, plugin, true);
setFieldValue(`permissionPoliciesRows[${index}].permission`, '', false);
setFieldValue(
`permissionPoliciesRows[${index}].policies`,
initialPermissionPolicyRowValue.policies,
false,
);
};

const onChangePermission = (
permission: string,
index: number,
policies?: string[],
) => {
setFieldValue(
`permissionPoliciesRows[${index}].permission`,
permission,
true,
);
setFieldValue(
`permissionPoliciesRows[${index}].policies`,
policies
? policies.map(p => ({ label: p, checked: true }))
: initialPermissionPolicyRowValue.policies,
false,
);
};

const onChangePolicy = (
isChecked: boolean,
policyIndex: number,
index: number,
) => {
setFieldValue(
`permissionPoliciesRows[${index}].policies[${policyIndex}].checked`,
isChecked,
true,
);
};

const onRowRemove = (index: number) => {
const finalPps = permissionPoliciesRows.filter(
(_pp, ppIndex) => index !== ppIndex,
);
setFieldError(`permissionPoliciesRows[${index}]`, undefined);
setFieldValue('permissionPoliciesRows', finalPps, false);
};

const onRowAdd = () =>
setFieldValue(
'permissionPoliciesRows',
[...permissionPoliciesRows, initialPermissionPolicyRowValue],
false,
);

return (
<div>
<FormHelperText>
Permission policies can be selected for each plugin. You can add
multiple permission policies using +Add option.
</FormHelperText>
<br />
<div className={classes.permissionPoliciesForm}>
{permissionPoliciesRows.map((pp, index) => (
<PermissionPoliciesFormRow
key={index}
permissionPoliciesRowError={
permissionPoliciesRowsError?.[index] ?? {}
}
rowName={`permissionPoliciesRows[${index}]`}
permissionPoliciesRowData={pp}
permissionPoliciesData={permissionPoliciesData}
rowCount={permissionPoliciesRows.length}
onChangePlugin={(plugin: string) => onChangePlugin(plugin, index)}
onChangePermission={(permission: string, policies?: string[]) =>
onChangePermission(permission, index, policies)
}
onChangePolicy={(isChecked: boolean, policyIndex: number) =>
onChangePolicy(isChecked, policyIndex, index)
}
onRemove={() => onRowRemove(index)}
handleBlur={handleBlur}
/>
))}
<Button className={classes.addButton} size="small" onClick={onRowAdd}>
<AddIcon />
Add
</Button>
</div>
</div>
);
};
Loading

0 comments on commit dd11c3a

Please sign in to comment.