Skip to content

Commit 6c75e4c

Browse files
committed
Wip for policies & invite roles
1 parent 386b6be commit 6c75e4c

File tree

10 files changed

+244
-76
lines changed

10 files changed

+244
-76
lines changed

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.157",
14+
"@surfnet/sds": "^0.0.158",
1515
"detect-browser": "^5.3.0",
1616
"dompurify": "^3.3.1",
1717
"i18n-js": "^4.5.1",

client/src/api/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,10 @@ export function getIdentityProviders(environment) {
225225
return fetchJson(`/api/v1/manage/identity-providers/${environment}`);
226226
}
227227

228+
export function getPolicyByServiceProviderEntityId(entityId) {
229+
return fetchJson(`/api/v1/manage/policies?entityId=${encodeURIComponent(entityId)}`);
230+
}
231+
228232
export function uniqueEntityID(environment, entityID) {
229233
return postPutJson(`/api/v1/manage/unique-entity-id/${environment}`, {entityID: entityID}, "POST");
230234
}

client/src/components/Navigation.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const Navigation = ({mobile, path}) => {
3434
<div className="links">
3535
{path !== "/login-info" &&
3636
<Button onClick={() => navigate("/login-info")}
37-
txt={I18n.t("landing.header.login")}/>}
37+
txt={I18n.t("landing.header.login")}/>}
3838
</div>
3939
</div>
4040
);

client/src/locale/en.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,6 +1047,10 @@ const en = {
10471047
appAccess: {
10481048
title: "Central Access",
10491049
users: "User from {{name}}",
1050+
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?",
1051+
cancelRequest: "Cancel the request.",
1052+
cancelRequestTitle: "Cancel the request.",
1053+
cancelRequestQuestion: "Are you sure you don't want to have access to this application? The request will be withdrawn.",
10501054
config: "⚡️ Configureer automatische toegang op basis van kenmerken",
10511055
accessFor: "There is access for:",
10521056
everyBody: "Everybody from {{name}}",
@@ -1058,9 +1062,12 @@ const en = {
10581062
roleManagement: "To role management",
10591063
decentralAccess: "Decentral Access",
10601064
noDecentralAccess: "This application <span class='red'>is not used</span> by collaborative groups.",
1061-
roleReady: "This application <span class='green'>supports</span> SURF Access roles.",
1062-
notRoleReady: "This application <span class='red'>does not yet</span> support SURF Access roles.",
1063-
noRoles: "There are no outside users yet with a role"
1065+
roleReady: "This application <span class='green'>receives</span> SURF Access authorization roles.",
1066+
notRoleReady: "This application <span class='red'>does not yet</span> receive SURF Access authorization roles.",
1067+
noRoles: "There are no outside users yet with a role",
1068+
roleUsers: "<strong>{{count}}</strong> users with role",
1069+
eduIDOnly: "eduID only",
1070+
everyIdp: "any IdP",
10641071
}
10651072

10661073
}

client/src/pages/ApplicationDetail.jsx

Lines changed: 151 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import "./ApplicationDetail.scss";
22
import React, {useEffect, useState} from "react";
3-
import {connectServiceProviderToIdentityProvider, publicServiceProviderByDetail} from "../api/index.js";
3+
import {
4+
connectServiceProviderToIdentityProvider,
5+
getPolicyByServiceProviderEntityId,
6+
inviteRoles,
7+
publicServiceProviderByDetail
8+
} from "../api/index.js";
49
import I18n from "../locale/I18n.js";
510
import ExternalLinkIcon from "../icons/external-link.svg";
611
import NotAllowedIcon from "../icons/not-allowed.svg";
712
import {useNavigate, useParams} from "react-router-dom";
813
import {
14+
Alert,
15+
AlertType,
916
Button,
1017
ButtonIconPlacement,
1118
ButtonType,
@@ -21,6 +28,7 @@ import ArrowLeftIcon from "@surfnet/sds/icons/functional-icons/arrow-left-2.svg"
2128
import {
2229
APPLICATION_LINKS,
2330
CHANGE_REQUEST_TYPE,
31+
isAccessRoleReady,
2432
providerDescription,
2533
providerName,
2634
providerOrganizationName
@@ -34,6 +42,7 @@ import InputField from "../components/InputField.jsx";
3442
import {mainMenuItems} from "../utils/MenuItems.js";
3543
import {TabHeader} from "../components/TabHeader.jsx";
3644
import {InfoBlock} from "../components/InfoBlock.jsx";
45+
import DOMPurify from "dompurify";
3746

3847
const confirmationModalOptions = {
3948
makeConnection: "makeConnection",
@@ -43,8 +52,6 @@ const confirmationModalOptions = {
4352
requestDisconnectConnection: "requestDisconnectConnection",
4453
}
4554

46-
const tabNames = ["access", "information"]
47-
4855
const ApplicationDetail = ({anonymous, refreshUser}) => {
4956

5057
const {arp, privacy, user, config, setFlash} = useAppStore(useShallow(state => ({
@@ -58,9 +65,12 @@ const ApplicationDetail = ({anonymous, refreshUser}) => {
5865
const navigate = useNavigate();
5966
const {manageType, manageId, tab = "access"} = useParams();
6067

68+
const [tabNames, setTabNames] = useState(["access", "information"]);
6169
const [currentTab, setCurrentTab] = useState(tab);
6270
const [loading, setLoading] = useState(true);
63-
const [serviceProvider, setServiceProvider] = useState([]);
71+
const [serviceProvider, setServiceProvider] = useState({});
72+
const [accessRoles, setAccessRoles] = useState({});
73+
const [policies, setPolicies] = useState({});
6474
const [showAttributes, setShowAttributes] = useState(false);
6575
const [showPrivacy, setShowPrivacy] = useState(false);
6676
const [metaData, setMetaData] = useState({});
@@ -80,19 +90,23 @@ const ApplicationDetail = ({anonymous, refreshUser}) => {
8090
setServiceProvider(res);
8191
const newMetaData = res.data.metaDataFields;
8292
setMetaData(newMetaData);
93+
if (anonymous) {
94+
setLoading(false);
95+
return;
96+
}
8397
//See if this application is already connected
8498
const allowedEntities = user.identityProvider.data.allowedEntities.map(entity => entity.name);
8599
let isAccessible = allowedEntities.includes(res.data.entityid);
100+
let isReadOnly = true;
86101
if (isAccessible) {
87-
setReadOnly(false);
102+
isReadOnly = false;
88103
} else {
89104
//Check if there is an outstanding change request
90105
isAccessible = user.changeRequests
91106
.some(changeRequest => changeRequest.requestType === CHANGE_REQUEST_TYPE.LINK_REQUEST &&
92107
changeRequest.pathUpdateType === "ADDITION" &&
93108
changeRequest.pathUpdates.allowedEntities.name === res.data.entityid
94109
);
95-
setReadOnly(true);
96110
}
97111
setAccessible(isAccessible);
98112
useAppStore.setState({
@@ -112,8 +126,32 @@ const ApplicationDetail = ({anonymous, refreshUser}) => {
112126
setConnectWithoutInteraction(connectOption !== "connect_with_interaction" || sameInstitution);
113127
const idpId = user.identityProvider.id
114128
const orgMembership = user.organizationMemberships.find(orgMembership => orgMembership.organization.manageIdentifier === idpId);
115-
setIsAdminUser(user.superUser || (!isEmpty(orgMembership) && orgMembership.authority === authorities.ADMIN));
116-
setLoading(false);
129+
const adminUser = user.superUser || (!isEmpty(orgMembership) && orgMembership.authority === authorities.ADMIN);
130+
setIsAdminUser(adminUser);
131+
setReadOnly(isReadOnly);
132+
if (isAccessible) {
133+
if (adminUser) {
134+
if (isReadOnly) {
135+
setLoading(false);
136+
} else {
137+
Promise.all([
138+
inviteRoles(user.organizationGUID, res.id),
139+
getPolicyByServiceProviderEntityId(res.data.entityid)
140+
]).then(res => {
141+
setAccessRoles(res[0]);
142+
setPolicies(res[1]);
143+
setLoading(false);
144+
})
145+
}
146+
} else {
147+
setTabNames(["information"]);
148+
setCurrentTab("information");
149+
setLoading(false);
150+
}
151+
} else {
152+
setLoading(false);
153+
}
154+
117155
})
118156
.catch(() => {
119157
navigate("/404");
@@ -201,7 +239,7 @@ const ApplicationDetail = ({anonymous, refreshUser}) => {
201239
);
202240

203241
}
204-
return "TODO"
242+
return null;
205243
}
206244

207245
const cancelConfirmation = () => {
@@ -210,6 +248,23 @@ const ApplicationDetail = ({anonymous, refreshUser}) => {
210248
setAccessChoice("ALL")
211249
}
212250

251+
const cancelConnectionRequest = (withConfirmation, e) => {
252+
stopEvent(e);
253+
if (withConfirmation) {
254+
setConfirmation({
255+
open: true,
256+
cancel: () => cancelConfirmation(),
257+
action: () => cancelConnectionRequest(false),
258+
title: I18n.t("appAccess.cancelRequestTitle"),
259+
okButton: I18n.t("forms.sure"),
260+
question: I18n.t("appAccess.cancelRequestQuestion"),
261+
});
262+
} else {
263+
cancelConfirmation();
264+
alert("cancel request")
265+
}
266+
}
267+
213268
const doRequestConnection = (withConfirmation, modalOption) => {
214269
if (withConfirmation) {
215270
let newModalOption;
@@ -280,52 +335,94 @@ const ApplicationDetail = ({anonymous, refreshUser}) => {
280335

281336
const renderAccessApp = () => {
282337
return (
283-
<div className="app-access">
284-
<div className="app-access-central">
285-
<h2>{I18n.t("appAccess.title")}</h2>
286-
<InfoBlock className="no-gap">
287-
<div className="grouped">
288-
<div>
289-
<h3>{I18n.t("appAccess.users", {name: providerOrganizationName(I18n.locale, serviceProvider)})}</h3>
290-
<p>{I18n.t("appAccess.config")}</p>
338+
<>
339+
{readOnly && <Alert alertType={AlertType.Warning}
340+
asChild={true}
341+
children={<a href="/" onClick={e => cancelConnectionRequest(true, e)}>
342+
{I18n.t("appAccess.cancelRequest")}</a>}
343+
message={I18n.t("appAccess.requestedAccessNotification")}/>
344+
}
345+
<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>
353+
</div>
354+
<Button type={ButtonType.Primary}
355+
onClick={() => alert("ToDo")}
356+
txt={I18n.t("forms.edit")}/>
291357
</div>
292-
<Button type={ButtonType.Primary}
293-
onClick={() => alert("ToDo")}
294-
txt={I18n.t("forms.edit")}/>
295-
</div>
296-
<p>{I18n.t("appAccess.accessFor")}</p>
297-
<div className="access-card">
298-
<h4>{I18n.t("appAccess.everyBody", {name: providerOrganizationName(I18n.locale, serviceProvider)})}</h4>
299-
{renderLogo(user.identityProvider.data.metaDataFields)}
300-
</div>
301-
</InfoBlock>
302-
<InfoBlock className="no-gap">
303-
<div className="grouped">
304-
<div>
305-
<h3>{I18n.t("appAccess.outSideUsers")}</h3>
306-
<p>{I18n.t("appAccess.roleBasedAccess")}</p>
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)}
307362
</div>
308-
<Button type={ButtonType.Primary}
309-
onClick={() => window.open(config.invite, "_blank").focus()}
310-
icon={<ExternalLinkIcon/>}
311-
txt={I18n.t("appAccess.roleManagement")}/>
312-
</div>
313-
<div className="access-card grey">
314-
<p>{I18n.t("appAccess.noRoles")}</p>
315-
</div>
316-
</InfoBlock>
317-
</div>
318-
<div className="app-access-decentral">
319-
<h2>{I18n.t("appAccess.decentralAccess")}</h2>
320-
<InfoBlock className="no-gap grey row">
321-
<div className="not-allowed-container">
322-
<NotAllowedIcon/>
323-
<p
324-
dangerouslySetInnerHTML={{__html: I18n.t("appAccess.noDecentralAccess")}}/>
325-
</div>
326-
</InfoBlock>
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>
377+
</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+
404+
405+
</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>
327424
</div>
328-
</div>
425+
</>
329426
);
330427
}
331428

@@ -451,7 +548,7 @@ const ApplicationDetail = ({anonymous, refreshUser}) => {
451548

452549
const renderDetailsApp = () => {
453550
return (
454-
<div className="details">
551+
<div className={`details ${anonymous ? "anonymous" : ""}`}>
455552
<div className="left">
456553
<p>{providerDescription(I18n.locale, serviceProvider)}</p>
457554
{renderAppAttributes()}
@@ -550,7 +647,7 @@ const ApplicationDetail = ({anonymous, refreshUser}) => {
550647
}
551648

552649
return (
553-
<div className="application-detail-container">
650+
<div className={`application-detail-container`}>
554651
{open && <ConfirmationDialog confirm={action}
555652
cancel={cancel}
556653
confirmationTxt={okButton}

0 commit comments

Comments
 (0)