Skip to content

Commit a4d89da

Browse files
committed
WIP for policies
1 parent a0c4fe0 commit a4d89da

14 files changed

Lines changed: 504 additions & 89 deletions

File tree

client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"test": "vitest"
1212
},
1313
"dependencies": {
14-
"@surfnet/sds": "^0.0.158",
14+
"@surfnet/sds": "^0.0.159",
1515
"detect-browser": "^5.3.0",
1616
"dompurify": "^3.3.1",
1717
"i18n-js": "^4.5.1",

client/src/api/index.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,22 @@ export function privacy() {
242242
return fetchJson("/api/v1/manage/privacy", {}, {}, false);
243243
}
244244

245+
export function newPolicy(policy) {
246+
return postPutJson("/api/v1/manage", policy, "POST");
247+
}
248+
249+
export function updatePolicy(policy) {
250+
return postPutJson("/api/v1/manage", policy, "PUT");
251+
}
252+
253+
export function deletePolicy(policy) {
254+
return fetchDelete(`/api/v1/manage/${policy.id}`);
255+
}
256+
257+
export function allowedAttributes() {
258+
return fetchJson("/api/v1/manage/allowed-attributes", {}, {}, false);
259+
}
260+
245261
//Connections
246262
export function newConnection(connection) {
247263
return postPutJson("/api/v1/connections", connection, "POST");

client/src/components/InfoBlock.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@
4646
border: 1px solid var(--sds--color--gray--300);
4747
}
4848

49+
&.light-grey {
50+
background-color: var(--sds--color--gray--background);
51+
border: 1px solid var(--sl-color-grey-100);
52+
53+
}
54+
4955
&.no-gap {
5056
gap: 0;
5157
}

client/src/locale/en.js

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -920,6 +920,8 @@ const en = {
920920
forms: {
921921
cancel: "Cancel",
922922
submit: "Submit",
923+
new: "New",
924+
create: "Create",
923925
save: "Save",
924926
sure: "I'm sure",
925927
edit: "Edit",
@@ -1051,7 +1053,7 @@ const en = {
10511053
cancelRequest: "Cancel the request.",
10521054
cancelRequestTitle: "Cancel the request.",
10531055
cancelRequestQuestion: "Are you sure you don't want to have access to this application? The request will be withdrawn.",
1054-
config: "⚡️ Configureer automatische toegang op basis van kenmerken",
1056+
config: "⚡️ Configure automatic access based on attributes",
10551057
accessFor: "There is access for:",
10561058
everyBody: "Everybody from {{name}}",
10571059
noAccessFor: "Geen toegang voor:",
@@ -1068,6 +1070,35 @@ const en = {
10681070
roleUsers: "<strong>{{count}}</strong> users with role",
10691071
eduIDOnly: "eduID only",
10701072
everyIdp: "any IdP",
1073+
backToAccess: "← back to access",
1074+
authorizationRules: "Authorization rules for:",
1075+
noPolicies: "No authorization rules have been configured yet",
1076+
newPolicy: "New authorization rule",
1077+
targetGroup: "Target group",
1078+
placeholderTargetGroup: "Give the rule a logical name",
1079+
allowDeny: "Allow or deny access?",
1080+
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.",
1081+
allow: "Allow",
1082+
deny: "Deny",
1083+
allAttributesMatch: "Match all or any filters?",
1084+
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.",
1085+
all: "All",
1086+
any: "Any",
1087+
filters: "Filter on attributes",
1088+
attribute: "Attribute",
1089+
attributePlaceholder: "Select an attribute",
1090+
permittedValues: "Permitted value(s)",
1091+
permittedValuesPlaceholder: "Add values(s)...",
1092+
denyEn: "English message for users without access",
1093+
denyNl: "Dutch message for users without access",
1094+
denyPlaceholder: "Type your descriptive message",
1095+
submitNew: "Save and activate",
1096+
submitExisting: "Update",
1097+
flash: {
1098+
created: "Policy {{name}} has been created and is ready for use",
1099+
updated: "Policy {{name}} has been updated",
1100+
deleted: "Policy {{name}} has been deleted",
1101+
}
10711102
}
10721103

10731104
}

client/src/pages/ApplicationDetail.jsx

Lines changed: 109 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ import {mainMenuItems} from "../utils/MenuItems.js";
4343
import {TabHeader} from "../components/TabHeader.jsx";
4444
import {InfoBlock} from "../components/InfoBlock.jsx";
4545
import DOMPurify from "dompurify";
46+
import {PolicyOverview} from "../policies/PolicyOverview.jsx";
47+
import {PolicyForm} from "../policies/PolicyForm.jsx";
48+
import {policyTemplate} from "../utils/Policy.js";
4649

4750
const confirmationModalOptions = {
4851
makeConnection: "makeConnection",
@@ -83,6 +86,9 @@ const ApplicationDetail = ({anonymous, refreshUser}) => {
8386
const [memberRequestSend, setMemberRequestSend] = useState(false);
8487
const [accessible, setAccessible] = useState(false);
8588
const [readOnly, setReadOnly] = useState(true);
89+
const [showPolicyOverview, setShowPolicyOverview] = useState(false);
90+
const [showNewPolicy, setShowNewPolicy] = useState(false);
91+
const [currentPolicy, setCurrentPolicy] = useState(null);
8692

8793
useEffect(() => {
8894
publicServiceProviderByDetail(manageType, manageId)
@@ -343,84 +349,113 @@ const ApplicationDetail = ({anonymous, refreshUser}) => {
343349
message={I18n.t("appAccess.requestedAccessNotification")}/>
344350
}
345351
<div className={`app-access ${readOnly ? "read-only" : ""}`} onClick={e => readOnly && stopEvent(e)}>
346-
<div className="app-access-central">
347-
<h2>{I18n.t("appAccess.title")}</h2>
348-
<InfoBlock className="no-gap">
349-
<div className="grouped">
350-
<div>
351-
<h3>{I18n.t("appAccess.users", {name: providerOrganizationName(I18n.locale, serviceProvider)})}</h3>
352-
<p>{I18n.t("appAccess.config")}</p>
352+
{showNewPolicy &&
353+
<PolicyForm backToAccess={() => setShowNewPolicy(false)}
354+
policy={currentPolicy}
355+
setPolicy={setCurrentPolicy}
356+
/>
357+
}
358+
{showPolicyOverview &&
359+
<PolicyOverview
360+
serviceProvider={serviceProvider}
361+
policies={policies}
362+
backToAccess={e => {
363+
stopEvent(e);
364+
setShowPolicyOverview(false);
365+
setShowNewPolicy(false);
366+
}}
367+
newPolicy={() => {
368+
setShowPolicyOverview(false);
369+
setCurrentPolicy(policyTemplate(user.identityProvider.data.entityid, serviceProvider.data.entityid))
370+
setShowNewPolicy(true);
371+
}}
372+
/>
373+
}
374+
{(!showPolicyOverview && !showNewPolicy) && <>
375+
<div className="app-access-central">
376+
<h2>{I18n.t("appAccess.title")}</h2>
377+
<InfoBlock className="no-gap">
378+
<div className="grouped">
379+
<div>
380+
<h3>{I18n.t("appAccess.users", {name: providerOrganizationName(I18n.locale, serviceProvider)})}</h3>
381+
<p>{I18n.t("appAccess.config")}</p>
382+
</div>
383+
<Button type={ButtonType.Primary}
384+
onClick={() => setShowPolicyOverview(true)}
385+
txt={I18n.t("forms.edit")}/>
353386
</div>
354-
<Button type={ButtonType.Primary}
355-
onClick={() => alert("ToDo")}
356-
txt={I18n.t("forms.edit")}/>
357-
</div>
358-
<p>{I18n.t("appAccess.accessFor")}</p>
359-
<div className="access-card large">
360-
<h4>{I18n.t("appAccess.everyBody", {name: providerOrganizationName(I18n.locale, serviceProvider)})}</h4>
361-
{renderLogo(user.identityProvider.data.metaDataFields)}
362-
</div>
363-
{!isEmpty(policies) && <>
364-
{policies.map((policy, index) =>
365-
<div key={index} className="access-card large">
366-
{policy.data.name}
367-
368-
</div>)}
369-
370-
</>}
371-
</InfoBlock>
372-
<InfoBlock className="no-gap">
373-
<div className="grouped">
374-
<div>
375-
<h3>{I18n.t("appAccess.outSideUsers")}</h3>
376-
<p>{I18n.t("appAccess.roleBasedAccess")}</p>
387+
<p>{I18n.t("appAccess.accessFor")}</p>
388+
<div className="access-card large">
389+
<h4>{I18n.t("appAccess.everyBody", {name: providerOrganizationName(I18n.locale, serviceProvider)})}</h4>
390+
{renderLogo(user.identityProvider.data.metaDataFields)}
377391
</div>
378-
<Button type={ButtonType.Primary}
379-
onClick={() => window.open(`${config.invite}/applications/${serviceProvider.id}`,
380-
"_blank").focus()}
381-
icon={<ExternalLinkIcon/>}
382-
txt={I18n.t("appAccess.roleManagement")}/>
383-
</div>
384-
{isEmpty(accessRoles) &&
385-
<div className="access-card grey">
386-
<p>{I18n.t("appAccess.noRoles")}</p>
387-
</div>}
388-
{!isEmpty(accessRoles) &&
389-
<>
390-
<p>{I18n.t("appAccess.accessFor")}</p>
391-
{accessRoles.map((role, index) =>
392-
<div key={index} className="access-card column large">
393-
<div>
394-
<p dangerouslySetInnerHTML={{
395-
__html: DOMPurify.sanitize(
396-
I18n.t("appAccess.roleUsers", {count: role.userRoleCount}))
397-
}}/>
398-
<p><strong>{role.name}</strong></p>
399-
</div>
400-
<div className={`chip ${role.eduIDOnly ? "blue" : ""}`}>
401-
{I18n.t(`appAccess.${role.eduIDOnly ? "eduIDOnly" : "everyIdp"}`)}
402-
</div>
403-
392+
{isEmpty(policies) && <>
393+
<div className="access-card grey">
394+
{I18n.t("appAccess.noOneGroups")}
395+
</div>
396+
</>}
397+
{!isEmpty(policies) && <>
398+
{policies.map((policy, index) =>
399+
<div key={index} className="access-card large">
400+
{policy.data.name}
404401

405402
</div>)}
406-
</>
407-
}
408-
<em className="role-ready" dangerouslySetInnerHTML={{
409-
__html: DOMPurify.sanitize(
410-
I18n.t(`appAccess.${isAccessRoleReady(serviceProvider) ? "roleReady" : "notRoleReady"}`))
411-
}}/>
412-
</InfoBlock>
413-
</div>
414-
<div className="app-access-decentral">
415-
<h2>{I18n.t("appAccess.decentralAccess")}</h2>
416-
<InfoBlock className="no-gap grey row">
417-
<div className="not-allowed-container">
418-
<NotAllowedIcon/>
419-
<p
420-
dangerouslySetInnerHTML={{__html: I18n.t("appAccess.noDecentralAccess")}}/>
421-
</div>
422-
</InfoBlock>
423-
</div>
403+
404+
</>}
405+
</InfoBlock>
406+
<InfoBlock className="no-gap">
407+
<div className="grouped">
408+
<div>
409+
<h3>{I18n.t("appAccess.outSideUsers")}</h3>
410+
<p>{I18n.t("appAccess.roleBasedAccess")}</p>
411+
</div>
412+
<Button type={ButtonType.Primary}
413+
onClick={() => window.open(`${config.invite}/applications/${serviceProvider.id}`,
414+
"_blank").focus()}
415+
icon={<ExternalLinkIcon/>}
416+
txt={I18n.t("appAccess.roleManagement")}/>
417+
</div>
418+
{isEmpty(accessRoles) &&
419+
<div className="access-card grey">
420+
<p>{I18n.t("appAccess.noRoles")}</p>
421+
</div>}
422+
{!isEmpty(accessRoles) &&
423+
<>
424+
<p>{I18n.t("appAccess.accessFor")}</p>
425+
{accessRoles.map((role, index) =>
426+
<div key={index} className="access-card column large">
427+
<div>
428+
<p dangerouslySetInnerHTML={{
429+
__html: DOMPurify.sanitize(
430+
I18n.t("appAccess.roleUsers", {count: role.userRoleCount}))
431+
}}/>
432+
<p><strong>{role.name}</strong></p>
433+
</div>
434+
<div className={`chip ${role.eduIDOnly ? "blue" : ""}`}>
435+
{I18n.t(`appAccess.${role.eduIDOnly ? "eduIDOnly" : "everyIdp"}`)}
436+
</div>
437+
438+
439+
</div>)}
440+
</>
441+
}
442+
<em className="role-ready" dangerouslySetInnerHTML={{
443+
__html: DOMPurify.sanitize(
444+
I18n.t(`appAccess.${isAccessRoleReady(serviceProvider) ? "roleReady" : "notRoleReady"}`))
445+
}}/>
446+
</InfoBlock>
447+
</div>
448+
<div className="app-access-decentral">
449+
<h2>{I18n.t("appAccess.decentralAccess")}</h2>
450+
<InfoBlock className="no-gap grey row">
451+
<div className="not-allowed-container">
452+
<NotAllowedIcon/>
453+
<p
454+
dangerouslySetInnerHTML={{__html: I18n.t("appAccess.noDecentralAccess")}}/>
455+
</div>
456+
</InfoBlock>
457+
</div>
458+
</>}
424459
</div>
425460
</>
426461
);

client/src/pages/ApplicationDetail.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ div.application-detail-container {
8080

8181
.sds--alert .sds--alert--textual.with-children {
8282
display: flow;
83+
8384
a {
8485
display: inline-block;
8586
margin-left: 3px;
@@ -342,6 +343,11 @@ div.application-detail-container {
342343
font-style: italic;
343344
}
344345

346+
&.border {
347+
border: 1px solid var(--sl-color-neutral-400);
348+
349+
}
350+
345351
&.large {
346352
font-size: 18px;
347353
}

0 commit comments

Comments
 (0)