Skip to content

Commit 561489b

Browse files
feat(rbac): support for adding conditional permissions
1 parent 4d4875e commit 561489b

29 files changed

+1349
-210
lines changed

packages/backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"@backstage/plugin-search-backend-module-pg": "^0.5.25",
3939
"@backstage/plugin-search-backend-node": "^1.2.20",
4040
"@backstage/plugin-techdocs-backend": "^1.10.3",
41-
"@janus-idp/backstage-plugin-rbac-backend": "^2.4.1",
41+
"@janus-idp/backstage-plugin-rbac-backend": "^2.7.0",
4242
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "^0.1.14",
4343
"@backstage/plugin-search-backend-module-catalog": "^0.1.21",
4444
"@backstage/plugin-search-backend-module-techdocs": "^0.1.21",

plugins/rbac/dev/index.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { mockPermissionPolicies } from '../src/__fixtures__/mockPermissionPolici
1919
import { mockPolicies } from '../src/__fixtures__/mockPolicies';
2020
import { RBACAPI, rbacApiRef } from '../src/api/RBACBackendClient';
2121
import { RbacPage, rbacPlugin } from '../src/plugin';
22-
import { MemberEntity } from '../src/types';
22+
import { MemberEntity, RoleBasedConditions } from '../src/types';
2323

2424
class MockPermissionApi implements PermissionApi {
2525
readonly result;
@@ -113,6 +113,12 @@ class MockRBACApi implements RBACAPI {
113113
async getPluginsConditionRules(): Promise<any | Response> {
114114
return mockConditionRules;
115115
}
116+
117+
async createConditionalPermission(
118+
_conditionalPermission: RoleBasedConditions,
119+
): Promise<Response> {
120+
return { status: 200 } as Response;
121+
}
116122
}
117123

118124
const mockPermissionApi = new MockPermissionApi({ result: 'ALLOW' });

plugins/rbac/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"@backstage/core-plugin-api": "^1.9.1",
3232
"@backstage/plugin-catalog": "^1.18.2",
3333
"@backstage/plugin-catalog-common": "^1.0.22",
34+
"@backstage/plugin-permission-common": "^0.7.13",
3435
"@backstage/plugin-permission-react": "^0.4.21",
3536
"@backstage/theme": "^0.5.2",
3637
"@janus-idp/backstage-plugin-rbac-common": "1.4.1",
@@ -40,6 +41,10 @@
4041
"@material-ui/lab": "^4.0.0-alpha.45",
4142
"@mui/icons-material": "5.14.11",
4243
"@mui/material": "^5.14.18",
44+
"@rjsf/core": "^5.18.2",
45+
"@rjsf/mui": "^5.18.2",
46+
"@rjsf/utils": "^5.18.2",
47+
"@rjsf/validator-ajv8": "^5.18.2",
4348
"autosuggest-highlight": "^3.3.4",
4449
"formik": "^2.4.5",
4550
"react-use": "^17.4.0",

plugins/rbac/src/__fixtures__/mockPermissionPolicies.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const mockPermissionPolicies: PermissionPolicy[] = [
77
{
88
permission: 'catalog-entity',
99
policy: 'read',
10+
isResourced: true,
1011
},
1112
{
1213
permission: 'catalog.entity.create',
@@ -15,10 +16,12 @@ export const mockPermissionPolicies: PermissionPolicy[] = [
1516
{
1617
permission: 'catalog-entity',
1718
policy: 'delete',
19+
isResourced: true,
1820
},
1921
{
2022
permission: 'catalog-entity',
2123
policy: 'update',
24+
isResourced: true,
2225
},
2326
{
2427
permission: 'catalog.location.read',
@@ -40,14 +43,17 @@ export const mockPermissionPolicies: PermissionPolicy[] = [
4043
{
4144
permission: 'scaffolder-template',
4245
policy: 'read',
46+
isResourced: true,
4347
},
4448
{
4549
permission: 'scaffolder-template',
4650
policy: 'read',
51+
isResourced: true,
4752
},
4853
{
4954
permission: 'scaffolder-action',
5055
policy: 'use',
56+
isResourced: true,
5157
},
5258
],
5359
},
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { ConditionRulesData } from '../components/ConditionalAccess/types';
2+
import { mockConditionRules } from './mockConditionRules';
3+
4+
export const mockTransformedConditionRules: ConditionRulesData = {
5+
catalog: {
6+
'catalog-entity': {
7+
HAS_ANNOTATION: {
8+
description: mockConditionRules[0].rules[0].description,
9+
schema: mockConditionRules[0].rules[0].paramsSchema,
10+
},
11+
HAS_LABEL: {
12+
description: mockConditionRules[0].rules[1].description,
13+
schema: mockConditionRules[0].rules[1].paramsSchema,
14+
},
15+
rules: [
16+
mockConditionRules[0].rules[0].name,
17+
mockConditionRules[0].rules[1].name,
18+
],
19+
},
20+
},
21+
scaffolder: {
22+
'scaffolder-template': {
23+
HAS_TAG: {
24+
description: mockConditionRules[1].rules[0].description,
25+
schema: mockConditionRules[1].rules[0].paramsSchema,
26+
},
27+
rules: [mockConditionRules[1].rules[0].name],
28+
},
29+
},
30+
};

plugins/rbac/src/api/RBACBackendClient.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ import {
1010
RoleBasedPolicy,
1111
} from '@janus-idp/backstage-plugin-rbac-common';
1212

13-
import { MemberEntity, RoleError } from '../types';
13+
import {
14+
MemberEntity,
15+
PluginConditionRules,
16+
RoleBasedConditions,
17+
RoleError,
18+
} from '../types';
1419
import { getKindNamespaceName } from '../utils/rbac-utils';
1520

1621
// @public
@@ -37,7 +42,10 @@ export type RBACAPI = {
3742
entityReference: string,
3843
polices: RoleBasedPolicy[],
3944
) => Promise<RoleError | Response>;
40-
getPluginsConditionRules: () => Promise<any | Response>;
45+
getPluginsConditionRules: () => Promise<PluginConditionRules[] | Response>;
46+
createConditionalPermission: (
47+
conditionalPermission: RoleBasedConditions,
48+
) => Promise<RoleError | Response>;
4149
};
4250

4351
export type Options = {
@@ -331,4 +339,27 @@ export class RBACBackendClient implements RBACAPI {
331339
}
332340
return jsonResponse.json();
333341
}
342+
343+
async createConditionalPermission(
344+
conditionalPermission: RoleBasedConditions,
345+
) {
346+
const { token: idToken } = await this.identityApi.getCredentials();
347+
const backendUrl = this.configApi.getString('backend.baseUrl');
348+
const jsonResponse = await fetch(
349+
`${backendUrl}/api/permission/roles/conditions`,
350+
{
351+
method: 'POST',
352+
headers: {
353+
'Content-Type': 'application/json',
354+
Accept: 'application/json',
355+
...(idToken && { Authorization: `Bearer ${idToken}` }),
356+
},
357+
body: JSON.stringify(conditionalPermission),
358+
},
359+
);
360+
if (jsonResponse.status !== 200 && jsonResponse.status !== 201) {
361+
return jsonResponse.json();
362+
}
363+
return jsonResponse;
364+
}
334365
}

plugins/rbac/src/components/ConditionalAccess/ConditionalAccessSidebar.tsx

Lines changed: 38 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@ import React from 'react';
33
import { makeStyles } from '@material-ui/core';
44
import CloseIcon from '@mui/icons-material/Close';
55
import Box from '@mui/material/Box';
6-
import Button from '@mui/material/Button';
76
import Drawer from '@mui/material/Drawer';
87
import IconButton from '@mui/material/IconButton';
98
import Typography from '@mui/material/Typography';
109

10+
import { ConditionsForm } from './ConditionsForm';
11+
import { ConditionsData, RulesData } from './types';
12+
1113
const useDrawerStyles = makeStyles(() => ({
1214
paper: {
13-
width: '40%',
15+
width: '50%',
1416
gap: '3%',
1517
},
1618
}));
@@ -27,39 +29,34 @@ const useDrawerContentStyles = makeStyles(theme => ({
2729
flexDirection: 'row',
2830
justifyContent: 'space-between',
2931
alignItems: 'baseline',
30-
borderBottom: `2px solid ${theme.palette.border}`,
3132
padding: theme.spacing(2.5),
33+
fontFamily: theme.typography.fontFamily,
3234
},
33-
body: {
34-
padding: theme.spacing(2.5),
35+
headerSubtitle: {
36+
fontWeight: 400,
37+
fontFamily: theme.typography.fontFamily,
3538
paddingTop: theme.spacing(1),
36-
paddingBottom: theme.spacing(1),
37-
flexGrow: 1,
38-
},
39-
footer: {
40-
display: 'flex',
41-
flexDirection: 'row',
42-
gap: '15px',
43-
alignItems: 'baseline',
44-
borderTop: `2px solid ${theme.palette.border}`,
45-
padding: theme.spacing(2.5),
4639
},
4740
}));
4841

4942
type ConditionalAccessSidebarProps = {
5043
open: boolean;
5144
onClose: () => void;
52-
selPlugin: string;
53-
rules?: any;
54-
error?: Error;
45+
onSave: (conditions: ConditionsData) => void;
46+
onRemoveAll: () => void;
47+
selPluginResourceType: string;
48+
conditionRulesData?: RulesData;
49+
conditionsFormVal?: ConditionsData;
5550
};
5651

5752
export const ConditionalAccessSidebar = ({
5853
open,
5954
onClose,
60-
selPlugin,
61-
rules,
62-
error,
55+
onSave,
56+
onRemoveAll,
57+
selPluginResourceType,
58+
conditionRulesData,
59+
conditionsFormVal,
6360
}: ConditionalAccessSidebarProps) => {
6461
const classes = useDrawerStyles();
6562
const contentClasses = useDrawerContentStyles();
@@ -75,10 +72,18 @@ export const ConditionalAccessSidebar = ({
7572
<Box className={contentClasses.sidebar}>
7673
<Box className={contentClasses.header}>
7774
<Typography variant="h5">
78-
<span style={{ fontWeight: '500' }}>
79-
Conditional access for the
80-
</span>{' '}
81-
{selPlugin} plugin
75+
<span style={{ fontWeight: '500' }}>Configure access for the</span>{' '}
76+
{selPluginResourceType}
77+
<Typography
78+
variant="body2"
79+
className={contentClasses.headerSubtitle}
80+
align="left"
81+
>
82+
By default, the selected resource type will be visible to the
83+
chosen users in step two. If you want to restrict or grant
84+
permission to specific plugin resource type rule, select it and
85+
add the required parameters.
86+
</Typography>
8287
</Typography>
8388
<IconButton
8489
key="dismiss"
@@ -89,30 +94,14 @@ export const ConditionalAccessSidebar = ({
8994
<CloseIcon fontSize="small" />
9095
</IconButton>
9196
</Box>
92-
<Box className={contentClasses.body}>
93-
{rules ? (
94-
<>
95-
<Typography variant="body2">Rules</Typography>
96-
<ul>
97-
{rules.map((rule: any) => (
98-
<li key={rule.name}>{rule.name}</li>
99-
))}
100-
</ul>
101-
</>
102-
) : (
103-
<Typography variant="body2">
104-
{error ? error.message : 'No rules found'}
105-
</Typography>
106-
)}
107-
</Box>
108-
<Box className={contentClasses.footer}>
109-
<Button variant="contained" disabled>
110-
Save
111-
</Button>{' '}
112-
<Button variant="outlined" onClick={onClose}>
113-
Cancel
114-
</Button>
115-
</Box>
97+
<ConditionsForm
98+
conditionRulesData={conditionRulesData}
99+
selPluginResourceType={selPluginResourceType}
100+
conditionsFormVal={conditionsFormVal}
101+
onClose={onClose}
102+
onSave={onSave}
103+
onRemoveAll={onRemoveAll}
104+
/>
116105
</Box>
117106
</Drawer>
118107
);

0 commit comments

Comments
 (0)