Skip to content

Commit 2acb11d

Browse files
committed
WIP for #533
1 parent 53ed836 commit 2acb11d

9 files changed

Lines changed: 206 additions & 80 deletions

File tree

client/src/components/SelectField.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export default function SelectField({
1111
toolTip = null, searchable = false, small = false,
1212
clearable = false, isMulti = false, creatable = false,
1313
onInputChange = null, required = false, info = null,
14-
className="", isAlert=false
14+
className = "", isAlert = false
1515
}) {
1616
return (
1717
<div className={`select-field ${className}`}>

client/src/locale/en.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,6 +1102,8 @@ const en = {
11021102
noStepUpPolicies: "No stepUp policies have been configured yet",
11031103
newPolicy: "New authorization rule",
11041104
editPolicy: "Edit authorization rule",
1105+
newStepUpPolicy: "New stepup policy",
1106+
editStepUpPolicy: "Edit stepup policy",
11051107
new: "New authorization rule",
11061108
active: "Active",
11071109
paused: "Paused",
@@ -1122,10 +1124,10 @@ const en = {
11221124
allAttributesMatchTooltip: "Policies with a logical AND rule enforce that all attributes defined must match those of the person trying to log in.<br/><br/>Policies defined with a logical OR only require one of the attributes to match the attributes of the person requesting access.",
11231125
all: "All",
11241126
any: "Any",
1125-
filters: "Filter on attributes",
1127+
filters: "Filter on attribute(s)",
11261128
attribute: "Attribute",
11271129
attributePlaceholder: "Select an attribute",
1128-
addAttributePlaceholder: "Add another attribute",
1130+
addAttributePlaceholder: "Add attribute +",
11291131
permittedValues: "Permitted value(s)",
11301132
permittedValuesPlaceholder: "Add values(s)...",
11311133
denyEn: "English message for users without access",
@@ -1175,7 +1177,21 @@ const en = {
11751177
negateApplication: "Select <strong>Negate selection</strong> to apply this policy to all service providers except the ones you chose.",
11761178
serviceProvidersPlaceholder: "Select application(s) to filter the policies",
11771179
serviceProvidersPlaceholderPolicy: "Choose application(s) for this policy",
1178-
serviceProviders: "Applications"
1180+
serviceProviders: "Applications",
1181+
policiesFound: "{{nbr}} policies found",
1182+
policiesFoundForServiceProvider: "{{nbr}} policies found for {{names}}",
1183+
policiesFoundSingle: "{{nbr}} policy found",
1184+
policiesFoundSingleForServiceProvider: "{{nbr}} policiy found for {{names}}",
1185+
attributesRequired:"At least one attribute with one of more valid values is required",
1186+
form: {
1187+
allow: "Allow access when",
1188+
deny: "Deny access when",
1189+
any: "is any of",
1190+
all: "is all of",
1191+
none: "is none of",
1192+
and: "and",
1193+
or: "or"
1194+
}
11791195
},
11801196
manage: {
11811197
name: "Name",

client/src/pages/ApplicationMigrate.jsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const ApplicationMigrate = () => {
1818
const [application, setApplication] = useState(null);
1919
const [applications, setApplications] = useState([]);
2020
const [confirmation, setConfirmation] = useState({});
21+
const [refresh, setRefresh] = useState(new Date());
2122

2223
useEffect(() => {
2324
Promise.all([allAplicationsLight(), organizationsLight()])
@@ -26,7 +27,7 @@ const ApplicationMigrate = () => {
2627
setOrganizations(res[1].map(org => ({...org, label: org.name, value: org.id})));
2728
setLoading(false);
2829
});
29-
}, []);
30+
}, [refresh]);
3031

3132
if (loading) {
3233
return <Loader/>
@@ -51,8 +52,9 @@ const ApplicationMigrate = () => {
5152
setConfirmation({});
5253
migrateApplication(application.id, organization.id)
5354
.then(() => {
55+
setApplication(null);
5456
setOrganization(null);
55-
setLoading(false);
57+
setRefresh(new Date());
5658
setFlash(I18n.t("applicationMigrate.flash.migrated", {
5759
application: application.name,
5860
organisation: organization.label

client/src/pages/Policies.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ const Policies = () => {
5555
return;
5656
}
5757
newCurrentPolicy.data.attributes = groupByValues([...newCurrentPolicy.data.attributes]);
58+
newCurrentPolicy.originalName = newCurrentPolicy.data.name;
5859
}
5960
window.scrollTo({top: 0, behavior: "smooth"});
6061
setCurrentPolicy(newCurrentPolicy);
@@ -145,6 +146,7 @@ const Policies = () => {
145146
.some(sp => selectedServiceProviders.some(sel => sp.name === sel.value)))}
146147
currentOrganization={currentOrganization}
147148
policyDetails={toPolicyDetail}
149+
selectedServiceProviders={selectedServiceProviders}
148150
refreshPolicies={refreshPolicies}
149151
serviceProviders={serviceProviders}
150152
/>

client/src/pages/Policies.scss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ div.policies-outer-container {
2727

2828
.app-policies {
2929
display: flex;
30-
width: 75%;
30+
width: 100%;
31+
padding-right: 100px;
3132
gap: 25px;
3233
@media (max-width: $medium) {
3334
width: 100%;

client/src/policies/PolicyForm.jsx

Lines changed: 83 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import "./PolicyForm.scss";
2-
import React, {useState} from "react";
3-
import {Button, ButtonType, Chip, ChipType, SegmentedControl, Tooltip} from "@surfnet/sds";
2+
import React, {Fragment, useState} from "react";
3+
import {Button, ButtonType, Chip, ChipType} from "@surfnet/sds";
44
import I18n from "../locale/I18n.js";
55
import InputField from "../components/InputField.jsx";
66
import {useAppStore} from "../stores/AppStore.js";
@@ -29,6 +29,11 @@ export const PolicyForm = ({
2929
const [confirmation, setConfirmation] = useState({});
3030
const [attributeValueErrors, setAttributeValueErrors] = useState({});
3131

32+
const accessOptions = ["allow", "deny"].map(name => ({value: name, label: I18n.t(`policies.form.${name}`)}));
33+
const conditionalOptions = ["any", "all"].map(name => ({value: name, label: I18n.t(`policies.form.${name}`)}));
34+
const negatedOptions = ["any", "none"].map(name => ({value: name, label: I18n.t(`policies.form.${name}`)}));
35+
const orOptions = ["and", "or"].map(name => ({value: name, label: I18n.t(`policies.form.${name}`)}));
36+
3237
const required = ["name", "denyAdvice", "denyAdviceNl"];
3338

3439
const {setFlash, allowedAttributes} = useAppStore(useShallow(state => ({
@@ -42,7 +47,8 @@ export const PolicyForm = ({
4247

4348
const isValid = () => {
4449
const allAttributesValuesValid = Object.values(attributeValueErrors).every(values => isEmpty(values));
45-
return required.every(attr => !isEmpty(policy.data[attr])) && !duplicatePolicyName && allAttributesValuesValid;
50+
const hasAttributes = policy.data.attributes.filter(attr => !isEmpty(attr.name) && !isEmpty(attr.value)).length > 0;
51+
return required.every(attr => !isEmpty(policy.data[attr])) && !duplicatePolicyName && allAttributesValuesValid && hasAttributes;
4652
}
4753

4854
const doDeletePolicy = (confirmationRequired, policy) => {
@@ -68,6 +74,7 @@ export const PolicyForm = ({
6874

6975
const submit = () => {
7076
setInitial(false);
77+
7178
if (isValid()) {
7279
const promise = isExistingPolicy ? updatePolicy : newPolicy;
7380
//We need to destructure the attributes with multiple values, to single attribute / value pairs
@@ -103,11 +110,13 @@ export const PolicyForm = ({
103110
}
104111

105112
const attributeDeleted = index => {
106-
const newAttributes = policy.data.attributes.filter((item, i) => i !== index);
107-
internalUpdatePolicy({attributes: defaultAttributes(newAttributes)});
108113
const deletedAttribute = policy.data.attributes[index];
109114
delete attributeValueErrors[deletedAttribute.name];
110115
setAttributeValueErrors({...attributeValueErrors});
116+
117+
const filteredAttributes = policy.data.attributes.filter((item, i) => i !== index);
118+
const newAttributes = defaultAttributes(filteredAttributes);
119+
internalUpdatePolicy({attributes: newAttributes});
111120
}
112121

113122
const attributeValueChanged = (values, index) => {
@@ -159,7 +168,7 @@ export const PolicyForm = ({
159168
/>}
160169
<div className="policy-form-header">
161170
<div className="header-top">
162-
<h2>{I18n.t(`appAccess.${isExistingPolicy ? "editPolicy" : "newPolicy"}`)}</h2>
171+
<h2>{I18n.t(`appAccess.${isExistingPolicy ? (policy.data.type === "reg" ? "editPolicy" : "editStepUpPolicy") : (policy.data.type === "reg" ? "newPolicy" : "newStepUpPolicy")}`)}</h2>
163172
{isExistingPolicy &&
164173
<div className="policy-header-actions">
165174
<Chip type={ChipType.Status_info}
@@ -184,64 +193,72 @@ export const PolicyForm = ({
184193
<ErrorIndicator adjustMargin={true}
185194
msg={I18n.t("appAccess.duplicateName", {name: policy.data.name})}/>}
186195

187-
<SelectField
188-
value={serviceProviderOptions.filter(option => policy.data.serviceProviderIds.some(sp => sp.name === option.value))}
189-
searchable={true}
190-
name={I18n.t("policies.serviceProviders")}
191-
options={serviceProviderOptions}
192-
placeholder={I18n.t("policies.serviceProvidersPlaceholderPolicy")}
193-
onChange={val => internalUpdatePolicy({
194-
serviceProviderIds: isEmpty(val) ? [] :
195-
val.map(sp => ({name: sp.value}))
196-
})}
197-
isMulti={true}/>
198-
199-
<div className="row">
200-
<div className="row-item">
201-
<span className="label standalone">{I18n.t("appAccess.allowDeny")}
202-
<Tooltip tip={I18n.t("appAccess.denyRuleTooltip")}/>
203-
</span>
204-
<SegmentedControl onClick={val => denyRuleToggle(val)}
205-
option={policy.data.denyRule ? "deny" : "allow"}
206-
options={["deny", "allow"]}
207-
optionLabelResolver={option => I18n.t(`appAccess.${option}`)}/>
208-
</div>
209-
<div className="row-item">
210-
<span className="label standalone">{I18n.t("appAccess.allAttributesMatch")}
211-
<Tooltip tip={I18n.t("appAccess.allAttributesMatchTooltip")}/>
212-
</span>
213-
<SegmentedControl onClick={val => internalUpdatePolicy({allAttributesMustMatch: val === "all"})}
214-
option={policy.data.allAttributesMustMatch ? "all" : "any"}
215-
options={["all", "any"]}
216-
optionLabelResolver={option => I18n.t(`appAccess.${option}`)}/>
217-
</div>
196+
<label className="stand-alone">{I18n.t("policies.serviceProviders")}</label>
197+
<div className="row-service-providers">
198+
<SelectField value={policy.data.serviceProvidersNegated ? negatedOptions[1] : negatedOptions[0]}
199+
required={true}
200+
onChange={() => internalUpdatePolicy({serviceProvidersNegated: !policy.data.serviceProvidersNegated})}
201+
className="any-of-service-providers"
202+
options={negatedOptions}/>
203+
<SelectField
204+
value={serviceProviderOptions.filter(option => policy.data.serviceProviderIds.some(sp => sp.name === option.value))}
205+
searchable={true}
206+
options={serviceProviderOptions}
207+
className="service-providers"
208+
placeholder={I18n.t("policies.serviceProvidersPlaceholderPolicy")}
209+
onChange={val => internalUpdatePolicy({
210+
serviceProviderIds: isEmpty(val) ? [] :
211+
val.map(sp => ({name: sp.value}))
212+
})}
213+
isMulti={true}/>
218214
</div>
215+
219216
<span className="label standalone">{I18n.t("appAccess.filters")}</span>
220217
<div className="filters">
221218
{policy.data.attributes.map((attribute, index) =>
222-
<div key={index} className="attribute">
223-
<div className="attribute-name-wrapper">
224-
<SelectField name={I18n.t("appAccess.attribute")}
225-
placeholder={I18n.t("appAccess.attributePlaceholder")}
226-
value={allowedAttributes.find(attr => attr.value === attribute.name)}
219+
<Fragment key={index}>
220+
<div className="deletable-attribute">
221+
{index === 0 &&
222+
<SelectField value={policy.data.denyRule ? accessOptions[1] : accessOptions[0]}
223+
required={true}
224+
className="select-access-rule"
225+
onChange={option => denyRuleToggle(option.value)}
226+
options={accessOptions}/>
227+
}
228+
{index !== 0 &&
229+
<SelectField
230+
value={policy.data.allAttributesMustMatch ? orOptions[0] : orOptions[1]}
231+
required={true}
232+
className="select-access-rule"
233+
onChange={() => internalUpdatePolicy({allAttributesMustMatch: !policy.data.allAttributesMustMatch})}
234+
options={orOptions}/>
235+
}
236+
<SelectField placeholder={I18n.t("appAccess.attributePlaceholder")}
237+
value={isEmpty(attribute.name) ? null : allowedAttributes.find(attr => attr.value === attribute.name)}
227238
required={true}
239+
className="attribute-name"
228240
onChange={option => attributeSelected(option, index)}
229241
options={policy.data.denyRule ? allowedAttributes
230242
.filter(option => option.allowedInDenyRule) : allowedAttributes}/>
231-
{(!isEmpty(attribute.name) && !isEmpty(attribute.value)) &&
232-
<Button type={ButtonType.Delete}
233-
onClick={() => attributeDeleted(index)}
234-
/>
235-
}
243+
244+
<SelectField value={conditionalOptions[0]}
245+
required={true}
246+
disabled={true}
247+
className="conditional-options"
248+
options={conditionalOptions}/>
249+
250+
<SelectField value={attribute.value}
251+
creatable={true}
252+
required={true}
253+
className="attribute-value"
254+
error={!initial && isEmpty(attribute.value)}
255+
placeholder={I18n.t("appAccess.permittedValuesPlaceholder")}
256+
onChange={values => attributeValueChanged(values, index)}
257+
/>
258+
<Button type={ButtonType.Delete}
259+
onClick={() => attributeDeleted(index)}
260+
/>
236261
</div>
237-
<SelectField name={I18n.t("appAccess.permittedValues")}
238-
value={attribute.value}
239-
creatable={true}
240-
required={true}
241-
error={!initial && isEmpty(attribute.value)}
242-
placeholder={I18n.t("appAccess.permittedValuesPlaceholder")}
243-
onChange={values => attributeValueChanged(values, index)}
244-
/>
245262
{!isEmpty(attributeValueErrors[attribute.name]) &&
246263
<ErrorIndicator adjustMargin={false}
247264
msg={I18n.t("appAccess.attributeValueErrors",
@@ -251,18 +268,19 @@ export const PolicyForm = ({
251268
attributeValueErrors[attribute.name].map(val => `'${val}'`),
252269
I18n.t("forms.and"))
253270
})}/>}
254-
</div>)}
255-
{policy.data.attributes.every(attribute => attribute.name && !isEmpty(attribute.value)) &&
256-
<div className="add-attribute-container">
257-
<SelectField placeholder={I18n.t("appAccess.addAttributePlaceholder")}
258-
value={null}
259-
onChange={option => attributeAdded(option)}
260-
options={policy.data.denyRule ? allowedAttributes
261-
.filter(option => option.allowedInDenyRule) : allowedAttributes}/>
262-
</div>
263-
}
271+
</Fragment>)}
272+
{(!initial && policy.data.attributes.filter(attr => !isEmpty(attr.name) && !isEmpty(attr.value)).length === 0) &&
273+
<ErrorIndicator msg={I18n.t("policies.attributesRequired")}/>}
274+
<div className="add-attribute-container">
275+
<SelectField placeholder={I18n.t("appAccess.addAttributePlaceholder")}
276+
value={null}
277+
onChange={option => attributeAdded(option)}
278+
options={policy.data.denyRule ? allowedAttributes
279+
.filter(option => option.allowedInDenyRule) : allowedAttributes}/>
280+
</div>
264281

265282
</div>
283+
266284
<InputField name={I18n.t("appAccess.denyEn")}
267285
value={policy.data.denyAdvice}
268286
required={true}

0 commit comments

Comments
 (0)