Skip to content

Commit

Permalink
feat(rbac): add support for creation of role (janus-idp#974)
Browse files Browse the repository at this point in the history
  • Loading branch information
divyanshiGupta authored Dec 12, 2023
1 parent 781cae3 commit 7cb9cbd
Show file tree
Hide file tree
Showing 25 changed files with 943 additions and 13 deletions.
6 changes: 6 additions & 0 deletions plugins/rbac/dev/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ class MockRBACApi implements RBACAPI {
constructor(fixtureData: Role[]) {
this.resources = fixtureData;
}

async getRoles(): Promise<Role[]> {
return this.resources;
}

async getPolicies(): Promise<RoleBasedPolicy[]> {
return mockPolicies;
}
Expand Down Expand Up @@ -76,6 +78,10 @@ class MockRBACApi implements RBACAPI {
): Promise<number> {
return 204;
}

async createRole(_role: Role): Promise<any> {
return { status: 200 };
}
}

const mockPermissionApi = new MockPermissionApi({ result: 'ALLOW' });
Expand Down
7 changes: 6 additions & 1 deletion plugins/rbac/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@backstage/core-components": "^0.13.6",
"@backstage/core-plugin-api": "^1.7.0",
"@backstage/plugin-catalog": "^1.15.1",
"@backstage/plugin-catalog-common": "^1.0.18",
"@backstage/plugin-permission-react": "^0.4.16",
"@backstage/theme": "^0.4.3",
"@janus-idp/backstage-plugin-rbac-common": "1.2.0",
Expand All @@ -37,8 +38,11 @@
"@material-ui/lab": "^4.0.0-alpha.45",
"@mui/icons-material": "5.14.11",
"@mui/material": "^5.14.18",
"autosuggest-highlight": "^3.3.4",
"formik": "^2.4.5",
"lodash": "^4.17.21",
"react-use": "^17.4.0",
"lodash": "^4.17.21"
"yup": "^1.3.2"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0",
Expand All @@ -54,6 +58,7 @@
"@testing-library/react": "12.1.5",
"@testing-library/react-hooks": "8.0.1",
"@testing-library/user-event": "14.5.1",
"@types/autosuggest-highlight": "3.2.3",
"@types/node": "18.18.5",
"msw": "1.3.2"
},
Expand Down
15 changes: 15 additions & 0 deletions plugins/rbac/src/api/RBACBackendClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type RBACAPI = {
getRole: (role: string) => Promise<Role[]>;
getMembers: () => Promise<MemberEntity[]>;
listPermissions: () => Promise<PermissionPolicy[]>;
createRole: (role: Role) => Promise<Response>;
};

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

async createRole(role: Role) {
const { token: idToken } = await this.identityApi.getCredentials();
const backendUrl = this.configApi.getString('backend.baseUrl');
return fetch(`${backendUrl}/api/permission/roles`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
...(idToken && { Authorization: `Bearer ${idToken}` }),
},
body: JSON.stringify(role),
});
}
}
116 changes: 116 additions & 0 deletions plugins/rbac/src/components/CreateRole/AddMembersForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import React from 'react';
import { useAsync } from 'react-use';

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

import { LinearProgress, TextField } from '@material-ui/core';
import Autocomplete from '@material-ui/lab/Autocomplete';
import FormHelperText from '@mui/material/FormHelperText';
import { FormikErrors } from 'formik';

import { rbacApiRef } from '../../api/RBACBackendClient';
import { MemberEntity } from '../../types';
import {
getChildGroupsCount,
getMembersCount,
getParentGroupsCount,
} from '../../utils/create-role-utils';
import { MembersDropdownOption } from './MembersDropdownOption';
import { CreateRoleFormValues, SelectedMember } from './types';

type AddMembersFormProps = {
selectedMembers: SelectedMember[];
selectedMembersError?: string;
setFieldValue: (
field: string,
value: any,
shouldValidate?: boolean,
) => Promise<FormikErrors<CreateRoleFormValues>> | Promise<void>;
};

export const AddMembersForm = ({
selectedMembers,
selectedMembersError,
setFieldValue,
}: AddMembersFormProps) => {
const rbacApi = useApi(rbacApiRef);
const [search, setSearch] = React.useState<string>('');
const {
loading: membersLoading,
value: members,
error,
} = useAsync(async () => {
return await rbacApi.getMembers();
});

const getDescription = (member: MemberEntity) => {
const memberCount = getMembersCount(member);
const parentCount = getParentGroupsCount(member);
const childCount = getChildGroupsCount(member);

return member.kind === 'Group'
? `${memberCount} members, ${parentCount} parent group, ${childCount} child groups`
: undefined;
};

const membersOptions: SelectedMember[] = members
? members.map((member: MemberEntity, index) => ({
label: member.metadata.name,
description: getDescription(member),
etag:
member.metadata.etag ??
`${member.metadata.name}-${member.kind}-${index}`,
type: member.kind,
namespace: member.metadata.namespace,
members: getMembersCount(member),
}))
: [];

return (
<>
<FormHelperText>
Search and select users and groups to be added. Selected users and
groups will appear in the members table.
</FormHelperText>
<br />
<Autocomplete
options={membersOptions}
getOptionLabel={(option: SelectedMember) => option.label}
getOptionSelected={(option: SelectedMember, value: SelectedMember) =>
option.etag === value.etag
}
loading={membersLoading}
loadingText={<LinearProgress />}
onChange={(_e, value: SelectedMember) =>
setFieldValue('selectedMembers', [...selectedMembers, value])
}
disableClearable
getOptionDisabled={(option: SelectedMember) =>
!!selectedMembers.find(
(sm: SelectedMember) => sm.etag === option.etag,
)
}
renderOption={(option: SelectedMember, state) => (
<MembersDropdownOption option={option} state={state} />
)}
renderInput={params => (
<TextField
{...params}
variant="outlined"
label="Users and groups"
placeholder="Search by user name or group name"
error={!!selectedMembersError}
helperText={selectedMembersError ?? ''}
value={search}
onChange={e => setSearch(e.target.value)}
required
/>
)}
/>
<br />
{error?.message && (
<FormHelperText error={!error}>{error.message}</FormHelperText>
)}
</>
);
};
50 changes: 50 additions & 0 deletions plugins/rbac/src/components/CreateRole/AddedMembersTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react';

import { Table } from '@backstage/core-components';

import { makeStyles } from '@material-ui/core';
import { FormikErrors } from 'formik';

import { getMembers } from '../../utils/rbac-utils';
import { selectedMembersColumns } from './AddedMembersTableColumn';
import { CreateRoleFormValues, SelectedMember } from './types';

const useStyles = makeStyles(theme => ({
empty: {
padding: theme.spacing(2),
display: 'flex',
justifyContent: 'center',
},
}));

type AddedMembersTableProps = {
selectedMembers: SelectedMember[];
setFieldValue: (
field: string,
value: any,
shouldValidate?: boolean,
) => Promise<FormikErrors<CreateRoleFormValues>> | Promise<void>;
};

export const AddedMembersTable = ({
selectedMembers,
setFieldValue,
}: AddedMembersTableProps) => {
const classes = useStyles();
return (
<Table
title={
selectedMembers.length > 0
? `Users and groups (${getMembers(selectedMembers)})`
: 'Users and groups'
}
data={selectedMembers}
columns={selectedMembersColumns(selectedMembers, setFieldValue)}
emptyContent={
<div className={classes.empty}>
No records. Selected users and groups appear here.
</div>
}
/>
);
};
62 changes: 62 additions & 0 deletions plugins/rbac/src/components/CreateRole/AddedMembersTableColumn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from 'react';

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

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

import { CreateRoleFormValues, SelectedMember } from './types';

export const selectedMembersColumns = (
selectedMembers: SelectedMember[],
setFieldValue: (
field: string,
value: any,
shouldValidate?: boolean,
) => Promise<FormikErrors<CreateRoleFormValues>> | Promise<void>,
): TableColumn<SelectedMember>[] => {
const onRemove = (etag: string) => {
const updatedMembers = selectedMembers.filter(
(mem: SelectedMember) => mem.etag !== etag,
);
setFieldValue('selectedMembers', updatedMembers);
};

return [
{
title: 'Name',
field: 'label',
type: 'string',
},
{
title: 'Type',
field: 'type',
type: 'string',
},
{
title: 'Members',
field: 'members',
type: 'numeric',
align: 'left',
emptyValue: '-',
},
{
title: 'Actions',
sorting: false,
render: (mem: SelectedMember) => {
return (
<span key={mem.etag}>
<IconButton
onClick={() => onRemove(mem.etag)}
aria-label="Remove"
title="Remove member"
>
<Delete />
</IconButton>
</span>
);
},
},
];
};
45 changes: 45 additions & 0 deletions plugins/rbac/src/components/CreateRole/CreateRoleForm.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';

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

import { CreateRoleForm } from './CreateRoleForm';

jest.mock('react-router-dom', () => ({
useNavigate: jest.fn(),
}));

jest.mock('@backstage/core-plugin-api', () => ({
...jest.requireActual('@backstage/core-plugin-api'),
useApi: jest.fn(),
}));

jest.mock('formik', () => ({
...jest.requireActual('formik'),
useFormik: jest.fn().mockReturnValue({
errors: {},
values: {},
// mocked useFormik to return formik status with submitError
status: { submitError: 'Unexpected error' },
}),
}));

describe('CreateRoleForm', () => {
it('renders create role form correctly', async () => {
render(<CreateRoleForm />);

expect(
screen.getByText(/enter name and description of role/i),
).toBeInTheDocument();
expect(screen.getByTestId(/role-name/i)).toBeInTheDocument();
expect(screen.getByTestId(/role-description/i)).toBeInTheDocument();
expect(screen.getByText(/add users and groups/i)).toBeInTheDocument();
});

it('shows error if there is any error in formik status', async () => {
render(<CreateRoleForm />);

expect(
screen.getByText(/unable to create role. unexpected error/i),
).toBeInTheDocument();
});
});
Loading

0 comments on commit 7cb9cbd

Please sign in to comment.