Skip to content

Commit edc1034

Browse files
committed
🥳🍾 First Manage PdP Policy created from access
Disclaimer: from localhost
1 parent de262d2 commit edc1034

18 files changed

Lines changed: 608 additions & 253 deletions

File tree

client/src/api/index.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -243,15 +243,19 @@ export function privacy() {
243243
}
244244

245245
export function newPolicy(policy) {
246-
return postPutJson("/api/v1/manage", policy, "POST");
246+
return postPutJson("/api/v1/manage/policies", policy, "POST");
247247
}
248248

249249
export function updatePolicy(policy) {
250-
return postPutJson("/api/v1/manage", policy, "PUT");
250+
return postPutJson("/api/v1/manage/policies", policy, "PUT");
251+
}
252+
253+
export function uniquePolicyName(name) {
254+
return postPutJson("/api/v1/manage/unique-policy-name", {name:name}, "POST");
251255
}
252256

253257
export function deletePolicy(policy) {
254-
return fetchDelete(`/api/v1/manage/${policy.id}`);
258+
return fetchDelete(`/api/v1/manage/policies/${policy.id}`);
255259
}
256260

257261
export function allowedAttributes() {

client/src/locale/en.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,11 +1003,13 @@ const en = {
10031003
},
10041004
profile: {
10051005
title: "Profile",
1006-
info: "Your account was created on {{createdAt}}",
1006+
info: "Your account was created on {{createdAt}}, and your last activity was on {{lastActivity}}",
10071007
name: "Name",
10081008
email: "Mail",
10091009
eduPersonPrincipalName: "EPPN",
10101010
schacHomeOrganization: "Institution identifier",
1011+
authenticatingAuthority: "Identity provider",
1012+
loaLevel: "Level of Assurance",
10111013
superUser: "Congrats🥳, your are a super user",
10121014
institutionAdmin: "Congrats🥳, your are an institution admin of {{orgName}}",
10131015
delete: "Delete",
@@ -1048,7 +1050,7 @@ const en = {
10481050
},
10491051
appAccess: {
10501052
title: "Central Access",
1051-
users: "User from {{name}}",
1053+
users: "Users from {{name}}",
10521054
requestedAccessNotification: "You have requested access to this application. If the supplier (SP) approves this, you can manage access to the app below. Still don't want to connect?",
10531055
cancelRequest: "Cancel the request.",
10541056
cancelRequestTitle: "Cancel the request.",
@@ -1075,6 +1077,9 @@ const en = {
10751077
noPolicies: "No authorization rules have been configured yet",
10761078
newPolicy: "New authorization rule",
10771079
targetGroup: "Target group",
1080+
duplicateName: "Name {{name}} is already taken, please pick a different name.",
1081+
description: "Description",
1082+
placeholderDescription: "Describe the implications of this policy rule",
10781083
placeholderTargetGroup: "Give the rule a logical name",
10791084
allowDeny: "Allow or deny access?",
10801085
denyRuleTooltip: "Permit policies enforce that only a successful match of the attributes defined will result in a Permit. No match will result in a Deny<br/><br/>Deny policies are less common to use. If the attributes in the policy match those of the person trying to log in then this will result in a Deny. No match will result in a Permit.",

client/src/pages/ApplicationDetail.jsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,17 @@ const ApplicationDetail = ({anonymous, refreshUser}) => {
168168
return <Loader/>
169169
}
170170

171+
const refreshPolicies = () => {
172+
setLoading(true);
173+
getPolicyByServiceProviderEntityId(serviceProvider.data.entityid)
174+
.then(res => {
175+
setPolicies(res);
176+
setShowPolicyOverview(true);
177+
setShowNewPolicy(false);
178+
setLoading(false);
179+
})
180+
}
181+
171182
const externalLink = (link, metaData, index) => {
172183
const attribute = link.languageProperty ?
173184
(I18n.locale === "en" ? metaData[`${link.metaData}:en`] : metaData[`${link.metaData}:nl`] || metaData[`${link.metaData}:en`]) :
@@ -353,6 +364,9 @@ const ApplicationDetail = ({anonymous, refreshUser}) => {
353364
<PolicyForm backToAccess={() => setShowNewPolicy(false)}
354365
policy={currentPolicy}
355366
setPolicy={setCurrentPolicy}
367+
isExistingPolicy={false}
368+
originalName={null}
369+
refreshPolicies={refreshPolicies}
356370
/>
357371
}
358372
{showPolicyOverview &&

client/src/pages/Profile.jsx

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,10 @@ const Profile = ({setIsAuthenticated}) => {
8282
</div>
8383
<p dangerouslySetInnerHTML={{
8484
__html: DOMPurify.sanitize(I18n.t("profile.info",
85-
{createdAt: dateFromEpoch(user.createdAt)}))
85+
{
86+
createdAt: dateFromEpoch(user.createdAt),
87+
lastActivity: dateFromEpoch(user.lastActivity)
88+
}))
8689
}}/>
8790
</div>
8891
<div className="profile">
@@ -92,13 +95,14 @@ const Profile = ({setIsAuthenticated}) => {
9295
/>
9396
}
9497
{user.institutionAdmin &&
95-
<InputField name={I18n.t("profile.institutionAdmin", {orgName: providerName(I18n.locale, user.identityProvider)})}
96-
noInput={true}
98+
<InputField
99+
name={I18n.t("profile.institutionAdmin", {orgName: providerName(I18n.locale, user.identityProvider)})}
100+
noInput={true}
97101
/>
98102
}
99103
<div className="user-container">
100104
<h5>{I18n.t("profile.attributes")}</h5>
101-
{["name", "eduPersonPrincipalName", "email", "schacHomeOrganization"]
105+
{["name", "eduPersonPrincipalName", "email", "schacHomeOrganization", "authenticatingAuthority"]
102106
.map((attr, index) =>
103107
<Fragment key={index}>
104108
<InputField name={I18n.t(`profile.${attr}`)}
@@ -107,6 +111,24 @@ const Profile = ({setIsAuthenticated}) => {
107111
/>
108112
</Fragment>
109113
)}
114+
{externalUser &&
115+
<div className="multi-list">
116+
<div className="external-user-container">
117+
<p>{I18n.t("profile.externalUser")}</p>
118+
<Tooltip tip={I18n.t("profile.externalUserTooltip")}/>
119+
</div>
120+
<Checkbox value={externalUser}
121+
readOnly={true}/>
122+
</div>}
123+
{!externalUser &&
124+
<div className="multi-list">
125+
<div className="external-user-container">
126+
<p>{I18n.t("profile.internalUser")}</p>
127+
<Tooltip tip={I18n.t("profile.internalUserTooltip")}/>
128+
</div>
129+
<Checkbox value={!externalUser}
130+
readOnly={true}/>
131+
</div>}
110132

111133
{!isEmpty(user.organizationMemberships) &&
112134
<div className="multi-list">
@@ -121,6 +143,7 @@ const Profile = ({setIsAuthenticated}) => {
121143
)}
122144
</ul>
123145
</div>}
146+
124147
{!isEmpty(applicationMemberships) &&
125148
<div className="multi-list">
126149
<p>{I18n.t(`profile.application${applicationMemberships.length > 1 ? "Multiple" : ""}`)}</p>
@@ -134,24 +157,6 @@ const Profile = ({setIsAuthenticated}) => {
134157
)}
135158
</ul>
136159
</div>}
137-
{externalUser &&
138-
<div className="multi-list">
139-
<div className="external-user-container">
140-
<p>{I18n.t("profile.externalUser")}</p>
141-
<Tooltip tip={I18n.t("profile.externalUserTooltip")}/>
142-
</div>
143-
<Checkbox value={externalUser}
144-
readOnly={true}/>
145-
</div>}
146-
{!externalUser &&
147-
<div className="multi-list">
148-
<div className="external-user-container">
149-
<p>{I18n.t("profile.internalUser")}</p>
150-
<Tooltip tip={I18n.t("profile.internalUserTooltip")}/>
151-
</div>
152-
<Checkbox value={!externalUser}
153-
readOnly={true}/>
154-
</div>}
155160
</div>
156161
<div className="delete-container">
157162
<Button onClick={e => doDelete(e, true)}

client/src/policies/PolicyForm.jsx

Lines changed: 100 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,18 @@ import I18n from "../locale/I18n.js";
55
import InputField from "../components/InputField.jsx";
66
import {useAppStore} from "../stores/AppStore.js";
77
import {useShallow} from "zustand/react/shallow";
8-
import {allowedAttributes, newPolicy, updatePolicy} from "../api/index.js";
8+
import {allowedAttributes, newPolicy, uniquePolicyName, updatePolicy} from "../api/index.js";
99
import {isEmpty} from "../utils/Utils.js";
1010
import ErrorIndicator from "../components/ErrorIndicator.jsx";
1111
import SelectField from "../components/SelectField.jsx";
12+
import {defaultAttributes, flatMapByValues} from "../utils/Policy.js";
1213

1314

14-
export const PolicyForm = ({backToAccess, policy, setPolicy}) => {
15+
export const PolicyForm = ({backToAccess, policy, setPolicy, isExistingPolicy, originalName, refreshPolicies}) => {
1516

16-
const [allAllowedAttributes, setAllAllowedAttributes] = useState(null);
17+
const [allAllowedAttributes, setAllAllowedAttributes] = useState([]);
1718
const [initial, setInitial] = useState(true);
18-
19+
const [duplicatePolicyName, setDuplicatePolicyName] = useState(false);
1920
const required = ["name", "denyAdvice", "denyAdviceNl"];
2021

2122
const {setFlash} = useAppStore(useShallow(state => ({
@@ -36,32 +37,98 @@ export const PolicyForm = ({backToAccess, policy, setPolicy}) => {
3637
const submit = () => {
3738
setInitial(false);
3839
if (isValid()) {
39-
const promise = policy.id ? updatePolicy : newPolicy;
40-
promise(policy).then(res => {
41-
setPolicy(res);
42-
setFlash(I18n.t(`appAccess.flash.${policy.id ? "updated" : "created"}`, {name: res.name}));
40+
const promise = isExistingPolicy ? updatePolicy : newPolicy;
41+
//We need to destructure the attributes with multiple values, to single attribute / value pairs
42+
policy.attributes = flatMapByValues(policy.attributes);
43+
if (!isExistingPolicy) {
44+
policy.entityid = crypto.randomUUID();
45+
}
46+
promise(policy)
47+
.then(res => {
48+
setFlash(I18n.t(`appAccess.flash.${isExistingPolicy ? "updated" : "created"}`, {name: res.name}));
49+
refreshPolicies();
50+
});
51+
}
52+
}
53+
54+
const attributeSelected = (option, index) => {
55+
const newAttributes = [...policy.attributes];
56+
newAttributes[index] = {name: option.value, value: []};
57+
setPolicy({...policy, attributes: newAttributes})
58+
}
59+
60+
const attributeDeleted = index => {
61+
const newAttributes = policy.attributes.filter((item, i) => i !== index);
62+
setPolicy({...policy, attributes: defaultAttributes(newAttributes)})
63+
}
64+
65+
const attributeValueChanged = (value, index) => {
66+
const newAttributes = [...policy.attributes];
67+
const attribute = newAttributes[index];
68+
attribute.value = value;
69+
setPolicy({...policy, attributes: newAttributes});
70+
}
71+
72+
const denyRuleToggle = val => {
73+
const denyRule = val === "deny";
74+
const newAttributes = denyRule ? policy.attributes
75+
.filter(attr => allAllowedAttributes.some(option => option.allowedInDenyRule && option.value === attr.name))
76+
: [...policy.attributes];
77+
setPolicy({...policy, denyRule: denyRule, attributes: defaultAttributes(newAttributes)});
78+
}
79+
80+
const policyNameChanged = e => {
81+
const name = e.target.value.trim();
82+
setPolicy({...policy, name: name});
83+
setDuplicatePolicyName(false);
84+
}
85+
86+
const validatePolicyName = e => {
87+
const name = e.target.value.trim();
88+
//empty is handled by required
89+
if (!isEmpty(name) && name !== originalName) {
90+
uniquePolicyName(name).then(res => {
91+
setDuplicatePolicyName(res.length > 0);
4392
})
4493
}
4594
}
4695

96+
4797
return (
4898
<div className="policy-form-container">
4999
<div className="policy-form">
50100
<InputField name={I18n.t("appAccess.targetGroup")}
51101
value={policy.name}
52102
required={true}
53-
error={!initial && isEmpty(policy.name)}
103+
onBlur={validatePolicyName}
104+
error={!initial && isEmpty(policy.name) || duplicatePolicyName}
54105
placeholder={I18n.t("appAccess.placeholderTargetGroup")}
55-
onChange={e => setPolicy({...policy, name: e.target.value})}
106+
onChange={policyNameChanged}
56107
/>
57108
{(!initial && isEmpty(policy.name)) &&
58-
<ErrorIndicator msg={I18n.t("forms.required", {name: I18n.t("appAccess.targetGroup")})}/>}
109+
<ErrorIndicator adjustMargin={true}
110+
msg={I18n.t("forms.required", {name: I18n.t("appAccess.targetGroup")})}/>}
111+
{duplicatePolicyName &&
112+
<ErrorIndicator adjustMargin={true} msg={I18n.t("appAccess.duplicateName", {name: policy.name})}/>}
113+
114+
<InputField name={I18n.t("appAccess.description")}
115+
value={policy.description}
116+
required={true}
117+
multiline={true}
118+
error={!initial && isEmpty(policy.description)}
119+
placeholder={I18n.t("appAccess.placeholderDescription")}
120+
onChange={e => setPolicy({...policy, description: e.target.value})}
121+
/>
122+
{(!initial && isEmpty(policy.description)) &&
123+
<ErrorIndicator adjustMargin={true}
124+
msg={I18n.t("forms.required", {name: I18n.t("appAccess.description")})}/>}
125+
59126
<div className="row">
60127
<div className="row-item">
61128
<span className="label standalone">{I18n.t("appAccess.allowDeny")}
62129
<Tooltip tip={I18n.t("appAccess.denyRuleTooltip")}/>
63130
</span>
64-
<SegmentedControl onClick={val => setPolicy({...policy, denyRule: val === "deny"})}
131+
<SegmentedControl onClick={val => denyRuleToggle(val)}
65132
option={policy.denyRule ? "deny" : "allow"}
66133
options={["deny", "allow"]}
67134
optionLabelResolver={option => I18n.t(`appAccess.${option}`)}/>
@@ -78,23 +145,30 @@ export const PolicyForm = ({backToAccess, policy, setPolicy}) => {
78145
</div>
79146
<span className="label standalone">{I18n.t("appAccess.filters")}</span>
80147
<div className="filters">
148+
<p>{JSON.stringify(policy.attributes)}</p>
81149
{policy.attributes.map((attribute, index) =>
82150
<div key={index} className="attribute">
83-
<SelectField name={I18n.t("appAccess.attribute")}
84-
placeholder={I18n.t("appAccess.attributePlaceholder")}
85-
value={attribute.name}
86-
required={true}
87-
onChange={val => {
88-
console.log("attribute " + JSON.stringify(val))
89-
}}
90-
options={allAllowedAttributes}/>
151+
<div className="attribute-name-wrapper">
152+
<SelectField name={I18n.t("appAccess.attribute")}
153+
placeholder={I18n.t("appAccess.attributePlaceholder")}
154+
value={allAllowedAttributes.find(attr => attr.value === attribute.name)}
155+
required={true}
156+
onChange={option => attributeSelected(option, index)}
157+
options={policy.denyRule ? allAllowedAttributes
158+
.filter(option => option.allowedInDenyRule) : allAllowedAttributes}/>
159+
{(!isEmpty(attribute.name) && !isEmpty(attribute.value)) &&
160+
<Button type={ButtonType.Delete}
161+
onClick={() => attributeDeleted(index)}
162+
/>
163+
}
164+
</div>
91165
<SelectField name={I18n.t("appAccess.permittedValues")}
92166
value={attribute.value}
93167
creatable={true}
94168
required={true}
95169
error={!initial && isEmpty(attribute.value)}
96170
placeholder={I18n.t("appAccess.permittedValuesPlaceholder")}
97-
onChange={e => console.log("value" + JSON.stringify(e))}
171+
onChange={values => attributeValueChanged(values, index)}
98172
/>
99173
</div>)}
100174

@@ -107,7 +181,8 @@ export const PolicyForm = ({backToAccess, policy, setPolicy}) => {
107181
onChange={e => setPolicy({...policy, denyAdvice: e.target.value})}
108182
/>
109183
{(!initial && isEmpty(policy.denyAdvice)) &&
110-
<ErrorIndicator msg={I18n.t("forms.required", {name: I18n.t("appAccess.denyEn")})}/>}
184+
<ErrorIndicator adjustMargin={true}
185+
msg={I18n.t("forms.required", {name: I18n.t("appAccess.denyEn")})}/>}
111186

112187
<InputField name={I18n.t("appAccess.denyNl")}
113188
value={policy.denyAdviceNl}
@@ -117,7 +192,8 @@ export const PolicyForm = ({backToAccess, policy, setPolicy}) => {
117192
onChange={e => setPolicy({...policy, denyAdviceNl: e.target.value})}
118193
/>
119194
{(!initial && isEmpty(policy.denyAdviceNl)) &&
120-
<ErrorIndicator msg={I18n.t("forms.required", {name: I18n.t("appAccess.denyNl")})}/>}
195+
<ErrorIndicator adjustMargin={true}
196+
msg={I18n.t("forms.required", {name: I18n.t("appAccess.denyNl")})}/>}
121197

122198
</div>
123199
<div className="actions">
@@ -127,7 +203,7 @@ export const PolicyForm = ({backToAccess, policy, setPolicy}) => {
127203
<Button type={ButtonType.Primary}
128204
onClick={() => submit()}
129205
disabled={!initial && !isValid()}
130-
txt={I18n.t(`appAccess.${policy.id ? "submitExisting" : "submitNew"}`)}/>
206+
txt={I18n.t(`appAccess.${isExistingPolicy ? "submitExisting" : "submitNew"}`)}/>
131207
</div>
132208
</div>
133209
);

0 commit comments

Comments
 (0)