Skip to content

Commit 9ac6d28

Browse files
feat(rbac): support for adding conditional permissions
1 parent e3442a1 commit 9ac6d28

21 files changed

+1021
-178
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/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/api/RBACBackendClient.ts

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

13-
import { MemberEntity, RoleError } from '../types';
13+
import { MemberEntity, RoleBasedConditions, RoleError } from '../types';
1414
import { getKindNamespaceName } from '../utils/rbac-utils';
1515

1616
// @public
@@ -38,6 +38,9 @@ export type RBACAPI = {
3838
polices: RoleBasedPolicy[],
3939
) => Promise<RoleError | Response>;
4040
getPluginsConditionRules: () => Promise<any | Response>;
41+
createConditionalPermission: (
42+
conditionalPermission: RoleBasedConditions,
43+
) => Promise<RoleError | Response>;
4144
};
4245

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

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

Lines changed: 37 additions & 50 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 } from './types';
12+
1113
const useDrawerStyles = makeStyles(() => ({
1214
paper: {
13-
width: '40%',
15+
width: '50%',
1416
gap: '3%',
1517
},
1618
}));
@@ -27,39 +29,32 @@ 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-
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),
35+
headerSubtitle: {
36+
fontWeight: 400,
4637
},
4738
}));
4839

4940
type ConditionalAccessSidebarProps = {
5041
open: boolean;
5142
onClose: () => void;
52-
selPlugin: string;
53-
rules?: any;
54-
error?: Error;
43+
onRemoveAll: () => void;
44+
onSave: (conditions: ConditionsData) => void;
45+
selPluginResourceType: string;
46+
conditionRulesData?: any;
47+
conditionsFormVal?: ConditionsData;
5548
};
5649

5750
export const ConditionalAccessSidebar = ({
5851
open,
5952
onClose,
60-
selPlugin,
61-
rules,
62-
error,
53+
onSave,
54+
onRemoveAll,
55+
selPluginResourceType,
56+
conditionRulesData,
57+
conditionsFormVal,
6358
}: ConditionalAccessSidebarProps) => {
6459
const classes = useDrawerStyles();
6560
const contentClasses = useDrawerContentStyles();
@@ -75,10 +70,18 @@ export const ConditionalAccessSidebar = ({
7570
<Box className={contentClasses.sidebar}>
7671
<Box className={contentClasses.header}>
7772
<Typography variant="h5">
78-
<span style={{ fontWeight: '500' }}>
79-
Conditional access for the
80-
</span>{' '}
81-
{selPlugin} plugin
73+
<span style={{ fontWeight: '500' }}>Configure access for the</span>{' '}
74+
{selPluginResourceType}
75+
<Typography
76+
variant="subtitle2"
77+
className={contentClasses.headerSubtitle}
78+
align="left"
79+
>
80+
By default, the selected resource type will be visible to the
81+
chosen users in step two. If you want to restrict or grant
82+
permission to specific plugin rules, select them and add the
83+
parameters.
84+
</Typography>
8285
</Typography>
8386
<IconButton
8487
key="dismiss"
@@ -89,30 +92,14 @@ export const ConditionalAccessSidebar = ({
8992
<CloseIcon fontSize="small" />
9093
</IconButton>
9194
</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>
95+
<ConditionsForm
96+
conditionRulesData={conditionRulesData}
97+
selPluginResourceType={selPluginResourceType}
98+
conditionsFormVal={conditionsFormVal}
99+
onClose={onClose}
100+
onSave={onSave}
101+
onRemoveAll={onRemoveAll}
102+
/>
116103
</Box>
117104
</Drawer>
118105
);
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import React from 'react';
2+
3+
import { makeStyles } from '@material-ui/core';
4+
import Box from '@mui/material/Box';
5+
import Button from '@mui/material/Button';
6+
7+
import { ConditionsFormRow } from './ConditionsFormRow';
8+
import { ConditionsData } from './types';
9+
10+
const useStyles = makeStyles(theme => ({
11+
form: {
12+
padding: theme.spacing(2.5),
13+
paddingTop: theme.spacing(1),
14+
paddingBottom: theme.spacing(1),
15+
flexGrow: 1,
16+
},
17+
addConditionButton: {
18+
color: theme.palette.primary.light,
19+
},
20+
footer: {
21+
display: 'flex',
22+
flexDirection: 'row',
23+
gap: '15px',
24+
alignItems: 'baseline',
25+
borderTop: `2px solid ${theme.palette.border}`,
26+
padding: theme.spacing(2.5),
27+
},
28+
}));
29+
30+
export const ConditionsForm = ({
31+
conditionRulesData,
32+
selPluginResourceType,
33+
conditionsFormVal,
34+
onClose,
35+
onSave,
36+
onRemoveAll,
37+
}: {
38+
conditionRulesData?: any;
39+
conditionsFormVal?: ConditionsData;
40+
selPluginResourceType: string;
41+
onClose: () => void;
42+
onSave: (conditions: ConditionsData) => void;
43+
onRemoveAll: () => void;
44+
}) => {
45+
const classes = useStyles();
46+
const [conditions, setConditions] = React.useState<ConditionsData>(
47+
conditionsFormVal ?? {
48+
condition: {
49+
rule: '',
50+
resourceType: selPluginResourceType,
51+
params: {},
52+
},
53+
},
54+
);
55+
return (
56+
<>
57+
<Box className={classes.form}>
58+
<ConditionsFormRow
59+
conditionRulesData={conditionRulesData}
60+
conditionRow={conditions}
61+
selPluginResourceType={selPluginResourceType}
62+
onRuleChange={newCondition => setConditions(newCondition)}
63+
/>
64+
</Box>
65+
<Box className={classes.footer}>
66+
<Button
67+
variant="contained"
68+
onClick={() => {
69+
onSave(conditions);
70+
}}
71+
>
72+
Save
73+
</Button>
74+
<Button variant="outlined" onClick={onClose}>
75+
Cancel
76+
</Button>
77+
<Button
78+
variant="text"
79+
onClick={() => {
80+
onRemoveAll();
81+
setConditions({
82+
condition: {
83+
rule: '',
84+
resourceType: selPluginResourceType,
85+
params: {},
86+
},
87+
});
88+
}}
89+
>
90+
Remove all
91+
</Button>
92+
</Box>
93+
</>
94+
);
95+
};

0 commit comments

Comments
 (0)