Skip to content

Commit 8440a8f

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

22 files changed

+1103
-180
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: 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: 35 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,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+
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+
selPluginResourceType: string;
47+
conditionRulesData?: RulesData;
48+
conditionsFormVal?: ConditionsData;
5549
};
5650

5751
export const ConditionalAccessSidebar = ({
5852
open,
5953
onClose,
60-
selPlugin,
61-
rules,
62-
error,
54+
onSave,
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="body2"
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 resource type rule, select it and
83+
add the required parameters.
84+
</Typography>
8285
</Typography>
8386
<IconButton
8487
key="dismiss"
@@ -89,30 +92,13 @@ 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+
/>
116102
</Box>
117103
</Drawer>
118104
);
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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 { criterias } from './const';
9+
import { ConditionsData, RulesData } from './types';
10+
11+
const useStyles = makeStyles(theme => ({
12+
form: {
13+
padding: theme.spacing(2.5),
14+
paddingTop: theme.spacing(1),
15+
paddingBottom: theme.spacing(1),
16+
flexGrow: 1,
17+
},
18+
addConditionButton: {
19+
color: theme.palette.primary.light,
20+
},
21+
footer: {
22+
display: 'flex',
23+
flexDirection: 'row',
24+
gap: '15px',
25+
alignItems: 'baseline',
26+
borderTop: `2px solid ${theme.palette.border}`,
27+
padding: theme.spacing(2.5),
28+
},
29+
}));
30+
31+
type ConditionFormProps = {
32+
conditionRulesData?: RulesData;
33+
conditionsFormVal?: ConditionsData;
34+
selPluginResourceType: string;
35+
onClose: () => void;
36+
onSave: (conditions: ConditionsData) => void;
37+
};
38+
39+
export const ConditionsForm = ({
40+
conditionRulesData,
41+
selPluginResourceType,
42+
conditionsFormVal,
43+
onClose,
44+
onSave,
45+
}: ConditionFormProps) => {
46+
const classes = useStyles();
47+
const [conditions, setConditions] = React.useState<ConditionsData>(
48+
conditionsFormVal ?? {
49+
condition: {
50+
rule: '',
51+
resourceType: selPluginResourceType,
52+
params: {},
53+
},
54+
},
55+
);
56+
const [criteria, setCriteria] = React.useState<string>(
57+
Object.keys(conditions)[0] ?? criterias.condition,
58+
);
59+
return (
60+
<>
61+
<Box className={classes.form}>
62+
<ConditionsFormRow
63+
conditionRulesData={conditionRulesData}
64+
conditionRow={conditions}
65+
criteria={criteria}
66+
selPluginResourceType={selPluginResourceType}
67+
onRuleChange={newCondition => setConditions(newCondition)}
68+
setCriteria={setCriteria}
69+
/>
70+
</Box>
71+
<Box className={classes.footer}>
72+
<Button
73+
variant="contained"
74+
onClick={() => {
75+
onSave(conditions);
76+
}}
77+
>
78+
Save
79+
</Button>
80+
<Button variant="outlined" onClick={onClose}>
81+
Cancel
82+
</Button>
83+
<Button
84+
variant="text"
85+
onClick={() => {
86+
setCriteria(criterias.condition);
87+
setConditions({
88+
condition: {
89+
rule: '',
90+
resourceType: selPluginResourceType,
91+
params: {},
92+
},
93+
});
94+
}}
95+
>
96+
Remove all
97+
</Button>
98+
</Box>
99+
</>
100+
);
101+
};

0 commit comments

Comments
 (0)