Skip to content

Commit

Permalink
feat(rbac): allow editing roles (janus-idp#1001)
Browse files Browse the repository at this point in the history
  • Loading branch information
debsmita1 authored Dec 15, 2023
1 parent a663cb9 commit 2e81062
Show file tree
Hide file tree
Showing 38 changed files with 1,130 additions and 658 deletions.
33 changes: 26 additions & 7 deletions plugins/rbac/dev/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,41 @@ class MockRBACApi implements RBACAPI {
return mockPolicies;
}

async getAssociatedPolicies(
entityReference: string,
): Promise<RoleBasedPolicy[]> {
return mockPolicies.filter(pol => pol.entityReference === entityReference);
}

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

async getRole(role: string): Promise<Role[]> {
async getRole(role: string): Promise<Role[] | Response> {
const roleresource = this.resources.find(res => res.name === role);
return roleresource ? [roleresource] : [];
return roleresource
? [roleresource]
: ({ status: 404, statusText: 'Not Found' } as Response);
}

async updateRole(_oldRole: Role, _newRole: Role): Promise<Response> {
return { status: 200 } as Response;
}

async updatePolicy(
_oldPolicy: RoleBasedPolicy,
_newPolicy: RoleBasedPolicy,
): Promise<Response> {
return { status: 204 } as Response;
}

async deleteRole(_roleName: string): Promise<any> {
return { status: 204 };
async deleteRole(_roleName: string): Promise<Response> {
return { status: 204, statusText: 'Deleted Successfully' } as Response;
}

async getMembers(): Promise<MemberEntity[]> {
async getMembers(): Promise<MemberEntity[] | Response> {
return mockMembers;
}

Expand All @@ -79,8 +98,8 @@ class MockRBACApi implements RBACAPI {
return 204;
}

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

Expand Down
1 change: 0 additions & 1 deletion plugins/rbac/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
"@mui/material": "^5.14.18",
"autosuggest-highlight": "^3.3.4",
"formik": "^2.4.5",
"lodash": "^4.17.21",
"react-use": "^17.4.0",
"yup": "^1.3.2"
},
Expand Down
58 changes: 34 additions & 24 deletions plugins/rbac/src/__fixtures__/mockPolicies.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,38 @@
import { RoleBasedPolicy } from '@janus-idp/backstage-plugin-rbac-common';

export const mockAssociatedPolicies: RoleBasedPolicy[] = [
{
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: 'catalog-entity',
policy: 'read',
effect: 'allow',
},
{
entityReference: 'role:default/rbac_admin',
permission: 'catalog.entity.create',
policy: 'use',
effect: 'allow',
},
];

export const mockPolicies: RoleBasedPolicy[] = [
{
entityReference: 'role:default/guests',
Expand Down Expand Up @@ -49,28 +82,5 @@ export const mockPolicies: RoleBasedPolicy[] = [
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',
},
...mockAssociatedPolicies,
];
94 changes: 89 additions & 5 deletions plugins/rbac/src/api/RBACBackendClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,27 @@ import {
RoleBasedPolicy,
} from '@janus-idp/backstage-plugin-rbac-common';

import { MemberEntity } from '../types';
import { CreateRoleError, MemberEntity } from '../types';
import { getKindNamespaceName } from '../utils/rbac-utils';

// @public
export type RBACAPI = {
getUserAuthorization: () => Promise<{ status: string }>;
getRoles: () => Promise<Role[]>;
getPolicies: () => Promise<RoleBasedPolicy[] | Response>;
getAssociatedPolicies: (
entityReference: string,
) => Promise<RoleBasedPolicy[] | Response>;
deleteRole: (role: string) => Promise<Response>;
getRole: (role: string) => Promise<Role[]>;
getMembers: () => Promise<MemberEntity[]>;
getRole: (role: string) => Promise<Role[] | Response>;
getMembers: () => Promise<MemberEntity[] | Response>;
listPermissions: () => Promise<PermissionPolicy[]>;
createRole: (role: Role) => Promise<Response>;
createRole: (role: Role) => Promise<CreateRoleError | Response>;
updateRole: (oldRole: Role, newRole: Role) => Promise<Response>;
updatePolicy: (
oldPolicy: RoleBasedPolicy,
newPolicy: RoleBasedPolicy,
) => Promise<Response>;
};

export type Options = {
Expand Down Expand Up @@ -81,6 +89,24 @@ export class RBACBackendClient implements RBACAPI {
return jsonResponse.json();
}

async getAssociatedPolicies(entityReference: string) {
const { kind, namespace, name } = getKindNamespaceName(entityReference);
const { token: idToken } = await this.identityApi.getCredentials();
const backendUrl = this.configApi.getString('backend.baseUrl');
const jsonResponse = await fetch(
`${backendUrl}/api/permission/policies/${kind}/${namespace}/${name}`,
{
headers: {
...(idToken && { Authorization: `Bearer ${idToken}` }),
},
},
);
if (jsonResponse.status !== 200 && jsonResponse.status !== 204) {
return jsonResponse;
}
return jsonResponse.json();
}

async deleteRole(role: string) {
const { token: idToken } = await this.identityApi.getCredentials();
const backendUrl = this.configApi.getString('backend.baseUrl');
Expand Down Expand Up @@ -153,7 +179,7 @@ export class RBACBackendClient implements RBACAPI {
async createRole(role: Role) {
const { token: idToken } = await this.identityApi.getCredentials();
const backendUrl = this.configApi.getString('backend.baseUrl');
return fetch(`${backendUrl}/api/permission/roles`, {
const jsonResponse = await fetch(`${backendUrl}/api/permission/roles`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand All @@ -162,5 +188,63 @@ export class RBACBackendClient implements RBACAPI {
},
body: JSON.stringify(role),
});
if (jsonResponse.status !== 200 && jsonResponse.status !== 201) {
return jsonResponse.json();
}
return jsonResponse;
}

async updateRole(oldRole: Role, newRole: Role) {
const { token: idToken } = await this.identityApi.getCredentials();
const backendUrl = this.configApi.getString('backend.baseUrl');
const { kind, namespace, name } = getKindNamespaceName(oldRole.name);
const body = {
oldRole,
newRole,
};
const jsonResponse = await fetch(
`${backendUrl}/api/permission/roles/${kind}/${namespace}/${name}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
...(idToken && { Authorization: `Bearer ${idToken}` }),
},
body: JSON.stringify(body),
},
);
if (jsonResponse.status !== 200 && jsonResponse.status !== 201) {
return jsonResponse.json();
}
return jsonResponse;
}

async updatePolicy(oldPolicy: RoleBasedPolicy, newPolicy: RoleBasedPolicy) {
const { token: idToken } = await this.identityApi.getCredentials();
const backendUrl = this.configApi.getString('backend.baseUrl');
const { kind, namespace, name } = getKindNamespaceName(
oldPolicy.entityReference as string,
);
const body = {
oldPolicy,
newPolicy,
};
const jsonResponse = await fetch(
`${backendUrl}/api/permission/policies/${kind}/${namespace}/${name}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
...(idToken && { Authorization: `Bearer ${idToken}` }),
},
body: JSON.stringify(body),
},
);
if (jsonResponse.status !== 200 && jsonResponse.status !== 201) {
return jsonResponse.json();
}
return jsonResponse;
}
}
37 changes: 16 additions & 21 deletions plugins/rbac/src/components/CreateRole/AddMembersForm.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,39 @@
import React from 'react';
import { useAsync } from 'react-use';

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

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';
import { RoleFormValues, SelectedMember } from './types';

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

export const AddMembersForm = ({
selectedMembers,
selectedMembersError,
setFieldValue,
membersData,
}: 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);
Expand All @@ -53,18 +45,19 @@ export const AddMembersForm = ({
: undefined;
};

const membersOptions: SelectedMember[] = members
? members.map((member: MemberEntity, index) => ({
label: member.metadata.name,
const membersOptions: SelectedMember[] = membersData.members
? membersData.members.map((member: MemberEntity, index: number) => ({
label: member.spec?.profile?.displayName ?? 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),
ref: stringifyEntityRef(member),
}))
: [];
: ([] as SelectedMember[]);

return (
<>
Expand All @@ -79,9 +72,9 @@ export const AddMembersForm = ({
getOptionSelected={(option: SelectedMember, value: SelectedMember) =>
option.etag === value.etag
}
loading={membersLoading}
loading={membersData.loading}
loadingText={<LinearProgress />}
onChange={(_e, value: SelectedMember) =>
onChange={(_e, value: SelectedMember | null) =>
setFieldValue('selectedMembers', [...selectedMembers, value])
}
disableClearable
Expand All @@ -108,8 +101,10 @@ export const AddMembersForm = ({
)}
/>
<br />
{error?.message && (
<FormHelperText error={!error}>{error.message}</FormHelperText>
{membersData.error?.message && (
<FormHelperText error={!!membersData.error}>
{`Error fetching user and groups: ${membersData.error.message}`}
</FormHelperText>
)}
</>
);
Expand Down
4 changes: 2 additions & 2 deletions plugins/rbac/src/components/CreateRole/AddedMembersTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { FormikErrors } from 'formik';

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

const useStyles = makeStyles(theme => ({
empty: {
Expand All @@ -23,7 +23,7 @@ type AddedMembersTableProps = {
field: string,
value: any,
shouldValidate?: boolean,
) => Promise<FormikErrors<CreateRoleFormValues>> | Promise<void>;
) => Promise<FormikErrors<RoleFormValues>> | Promise<void>;
};

export const AddedMembersTable = ({
Expand Down
Loading

0 comments on commit 2e81062

Please sign in to comment.