Skip to content

Commit ce6a2ba

Browse files
committed
WIP for Idp_SP connection
1 parent 884f650 commit ce6a2ba

11 files changed

Lines changed: 146 additions & 54 deletions

File tree

client/src/api/index.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,24 +94,29 @@ export function searchUsers(pagination = {}) {
9494
export function feedback(message) {
9595
return postPutJson("/api/v1/users/feedback", {message: message}, "POST");
9696
}
97+
9798
//Organizations
9899

99100
//attributePaths = {"organizationMemberships.user", "invitations.invitee", "joinRequests.user"}
100101
export function organizationUserManagementById(id) {
101102
return fetchJson(`/api/v1/organizations/details/${id}`);
102103
}
104+
103105
//attributePaths = {"applications.connections"}
104106
export function organizationApplicationsById(id) {
105107
return fetchJson(`/api/v1/organizations/applications/${id}`);
106108
}
109+
107110
//no extra attributes, but with the metadata from manage (if internal organization)
108111
export function organizationMineById(id) {
109112
return fetchJson(`/api/v1/organizations/mine/${id}`);
110113
}
114+
111115
//attributePaths = {"organizationMemberships.user"}
112116
export function organizationUsersById(id) {
113117
return fetchJson(`/api/v1/organizations/users/${id}`);
114118
}
119+
115120
//no extra relations fetched, just plain attributes
116121
export function organizationLightById(id) {
117122
return fetchJson(`/api/v1/organizations/light/${id}`);
@@ -297,6 +302,16 @@ export function deleteAllInvitations(organization) {
297302
return fetchDelete(`/api/v1/invitations/delete/all/${organization.id}`)
298303
}
299304

305+
//IdentityProvider
306+
export function connectServiceProviderToIdentityProvider(applicationManageIdentifier, entityType, idpManageIdentifier) {
307+
const body = {
308+
applicationManageIdentifier: applicationManageIdentifier,
309+
entityType: entityType,
310+
idpManageIdentifier: idpManageIdentifier
311+
};
312+
return postPutJson("/api/v1/idp/connect", body, "PUT")
313+
}
314+
300315
//Public
301316
export function publicIdentityProviders() {
302317
return fetchJson("/api/v1/public/identity-providers");

client/src/locale/en.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1021,7 +1021,12 @@ const en = {
10211021
applicationConnect: {
10221022
connect: "Connect",
10231023
request: "Request connection",
1024-
back: "← Back to other apps"
1024+
back: "← Back to other apps",
1025+
defaultAccess: "Hoe wil je default toegang instellen?",
1026+
access: {
1027+
all:"Iedereen van de {{orgName}} heeft direct automatisch toegang",
1028+
some: "Pas toegangsregels toe"
1029+
}
10251030
}
10261031
}
10271032

client/src/pages/ApplicationDetail.jsx

Lines changed: 65 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import "./ApplicationDetail.scss";
22
import React, {useEffect, useState} from "react";
3-
import {publicServiceProviderByDetail} from "../api/index.js";
3+
import {connectServiceProviderToIdentityProvider, publicServiceProviderByDetail} from "../api/index.js";
44
import I18n from "../locale/I18n.js";
55
import {useNavigate, useParams} from "react-router-dom";
6-
import {Button, ButtonIconPlacement, ButtonType, Loader} from "@surfnet/sds";
6+
import {Button, ButtonIconPlacement, ButtonType, Loader, RadioOptions, RadioOptionsOrientation} from "@surfnet/sds";
77
import StudentPng from "../icons/student2.png";
88
import PlaceHolderImage from "@surfnet/sds/icons/placeholder-image.svg";
99
import ArrowLeftIcon from "@surfnet/sds/icons/functional-icons/arrow-left-2.svg";
@@ -13,6 +13,13 @@ import {useAppStore} from "../stores/AppStore.js";
1313
import {useShallow} from "zustand/react/shallow";
1414
import ConfirmationDialog from "../components/ConfirmationDialog.jsx";
1515

16+
const confirmationModalOptions = {
17+
makeConnection: "makeConnection",
18+
requestConnection: "requestConnection",
19+
disconnectConnection: "disconnectConnection",
20+
requestDisconnectConnection: "requestDisconnectConnection",
21+
}
22+
1623
const ApplicationDetail = ({anonymous}) => {
1724

1825
const {arp, privacy, user} = useAppStore(useShallow(state => ({
@@ -28,13 +35,24 @@ const ApplicationDetail = ({anonymous}) => {
2835
const [serviceProvider, setServiceProvider] = useState([]);
2936
const [showAttributes, setShowAttributes] = useState(false);
3037
const [showPrivacy, setShowPrivacy] = useState(false);
38+
const [metaData, setMetaData] = useState({});
39+
const [connectWithOutInteraction, setConnectWithOutInteraction] = useState(false);
3140
const [confirmation, setConfirmation] = useState({});
41+
const [accessChoice, setAccessChoice] = useState("ALL");
42+
const [confirmationModalOption, setConfirmationModalOption] = useState(null);
43+
3244

3345
useEffect(() => {
3446
publicServiceProviderByDetail(manageType, manageId)
3547
.then(res => {
3648
setServiceProvider(res);
3749
setLoading(false);
50+
const newMetaData = res.data.metaDataFields;
51+
setMetaData(newMetaData);
52+
const connectOption = newMetaData["coin:dashboard_connect_option"] || "connect_with_interaction";
53+
const sameInstitution = !isEmpty(newMetaData["coin:institution_guid"]) &&
54+
newMetaData["coin:institution_guid"] === user.identityProvider.data.metaDataFields["coin:institution_guid"]
55+
setConnectWithOutInteraction(connectOption !== "connect_with_interaction" || sameInstitution);
3856
})
3957
.catch(() => {
4058
navigate("/404");
@@ -63,8 +81,6 @@ const ApplicationDetail = ({anonymous}) => {
6381
);
6482
}
6583

66-
const metaData = serviceProvider.data.metaDataFields;
67-
6884
const toggleShowAttributes = e => {
6985
stopEvent(e);
7086
setShowAttributes(true);
@@ -79,57 +95,65 @@ const ApplicationDetail = ({anonymous}) => {
7995
return arp.attributes.find(attr => attr.urn === urn);
8096
}
8197

82-
const mayConnectWithoutInteraction = () => {
83-
const connectOption = metaData["coin:dashboard_connect_option"] || "connect_with_interaction";
84-
const sameInstitution = !isEmpty(metaData["coin:institution_guid"]) &&
85-
metaData["coin:institution_guid"] === user.identityProvider.data.metaDataFields["coin:institution_guid"]
86-
return connectOption !== "connect_with_interaction" || sameInstitution;
87-
}
88-
89-
const openConnectDialog = () => {
90-
setConfirmation({
91-
open: true,
92-
cancel: () => setConfirmation({open: false}),
93-
action: () => doDelete(invitation, false),
94-
question: I18n.t("invitationsManagement.deleteConfirmation", {email: invitation.inviter.name}),
95-
okButton: I18n.t("invitationsManagement.revoke")
96-
});
97-
}
98-
99-
const doRequestConnection = () => {
100-
98+
const confirmationModalChildren = () => {
99+
if (confirmationModalOption === confirmationModalOptions.makeConnection ) {
100+
return (
101+
<div className="connect-options-container">
102+
<RadioOptions name={"access"}
103+
label={I18n.t("applicationConnect.defaultAccess")}
104+
value={accessChoice}
105+
onChange={e => {
106+
const newValue = e.target.id.replace("access_", "").toUpperCase();
107+
setAccessChoice(newValue);
108+
}}
109+
isMultiple={true}
110+
labels={["ALL", "SOME"]}
111+
labelResolver={label => I18n.t(`applicationConnect.access.${label.toLowerCase()}`, {
112+
orgName: providerName(I18n.locale, user.identityProvider)
113+
})}
114+
orientation={RadioOptionsOrientation.column}/>
115+
</div>
116+
);
117+
}
118+
return "TODO"
101119
}
102120

103-
const openRequestConnectionDialog = () => {
104-
setConfirmation({
105-
open: true,
106-
cancel: () => setConfirmation({open: false}),
107-
action: () => doDelete(invitation, false),
108-
question: I18n.t("invitationsManagement.deleteConfirmation", {email: invitation.inviter.name}),
109-
okButton: I18n.t("invitationsManagement.revoke")
110-
});
121+
const doRequestConnection = withConfirmation => {
122+
if (withConfirmation) {
123+
setConfirmation({
124+
open: true,
125+
cancel: () => setConfirmation({open: false}),
126+
action: () => doRequestConnection(false),
127+
title: null,
128+
question: null,
129+
okButton: I18n.t("forms.proceed")
130+
});
131+
setConfirmationModalOption(confirmationModalOptions.makeConnection)
132+
} else {
133+
setConfirmation({});
134+
connectServiceProviderToIdentityProvider(serviceProvider.id,serviceProvider.type, user.identityProvider.id)
135+
.then(() => {
136+
//Where to go to next?
137+
alert("done")
138+
})
139+
}
111140
}
112141

113142
const goBack = e => {
114143
stopEvent(e);
115144
navigate(-1);
116145
}
117146

118-
const connectButtonText = () => {
119-
//Is the app already connected, may the app be connected without interaction, or is there already an outstanding change request?
120-
return I18n.t(`applicationConnect.${mayConnectWithoutInteraction() ? "connect" : "request"}`)
121-
}
122-
123-
const {open, cancel, action, question, okButton, children} = confirmation;
147+
const {open, cancel, action, question, title, okButton} = confirmation;
124148

125149
return (
126150
<div className="application-detail-container">
127151
{open && <ConfirmationDialog confirm={action}
128152
cancel={cancel}
129-
confirmationHeader={I18n.t("forms.submit")}
130153
confirmationTxt={okButton}
154+
confirmationHeader={title}
131155
question={question}>
132-
{children}
156+
{confirmationModalChildren()}
133157
</ConfirmationDialog>
134158
}
135159
{anonymous && <div className="application-detail-header-container">
@@ -164,8 +188,8 @@ const ApplicationDetail = ({anonymous}) => {
164188
iconPlacement={ButtonIconPlacement.Left}
165189
onClick={goBack}
166190
txt={I18n.t("applicationDetail.back")}/>}
167-
{!anonymous && <Button onClick={() => alert("Todo")}
168-
txt={connectButtonText()}/>}
191+
{!anonymous && <Button onClick={() => doRequestConnection(true)}
192+
txt={I18n.t(`applicationConnect.${connectWithOutInteraction ? "connect" : "request"}`)}/>}
169193
</div>
170194
<div className="details">
171195
<div className="left">

client/src/pages/ApplicationDetail.scss

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@ div.application-detail-container {
66

77
$width: 920px;
88

9+
.sds--modal--container {
10+
max-width: 620px;
11+
}
12+
13+
.connect-options-container {
14+
.sds--text-field-container > [class^="option-"] {
15+
padding: 14px 20px 14px 10px;
16+
background-color: var(--sds--color--gray--background);
17+
border-radius: 6px;
18+
}
19+
}
20+
921
.application-detail-header-container {
1022
width: 100%;
1123
background-color: var(--sds--color--gray--background);
@@ -41,6 +53,11 @@ div.application-detail-container {
4153
.application-detail-top {
4254
background-color: white;
4355
padding: 0 25px;
56+
57+
a {
58+
color: var(--sds--color--gray--500);
59+
text-decoration: none;
60+
}
4461
}
4562

4663
.application-detail {

client/src/pages/Profile.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {SESSION_STORAGE_LOCATION} from "../utils/Login.js";
1212
import {useNavigate} from "react-router";
1313
import {mainMenuItems} from "../utils/MenuItems.js";
1414
import InputField from "../components/InputField.jsx";
15+
import {providerName} from "../utils/Manage.js";
1516

1617
const Profile = ({setIsAuthenticated}) => {
1718

@@ -91,7 +92,7 @@ const Profile = ({setIsAuthenticated}) => {
9192
/>
9293
}
9394
{user.institutionAdmin &&
94-
<InputField name={I18n.t("profile.institutionAdmin")}
95+
<InputField name={I18n.t("profile.institutionAdmin", {orgName: providerName(I18n.locale, user.identityProvider)})}
9596
noInput={true}
9697
/>
9798
}

server/src/main/java/access/api/IdentityProviderController.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,18 @@ public ResponseEntity<Map<String, Object>> connect(User user, @RequestBody @Vali
8686
// TODO send email
8787
return Results.createResult();
8888
}
89+
//Now check if the connection can be made automatically
90+
Map<String, Object> spMetaDataFields = getMetaDataFields(getData(serviceProvider));
91+
String connectOption = (String) spMetaDataFields.getOrDefault("coin:dashboard_connect_option", "connect_with_interaction");
92+
String idpInstitutionGUID = (String) getMetaDataFields(getData(identityProvider)).get("coin:institution_guid");
93+
94+
boolean idpAndSpShareInstitution = spMetaDataFields.getOrDefault("coin:institution_guid", "nope")
95+
.equals(idpInstitutionGUID);
96+
boolean connectWithoutInteraction = idpAndSpShareInstitution || !connectOption.equals("connect_with_interaction");
97+
if (connectWithoutInteraction) {
98+
manage.connectWithoutInteraction(identityProvider, serviceProvider, userFromDB);
99+
return Results.createResult();
100+
}
89101

90102
String changeRequestURL = manage.changeRequestURLConnectionRequest(EntityType.saml20_idp, idpManageIdentifier);
91103

server/src/main/java/access/manage/LocalManage.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
package access.manage;
22

33
import access.exception.NotFoundException;
4-
import access.model.Connection;
5-
import access.model.EntityType;
6-
import access.model.Environment;
7-
import access.model.Organization;
4+
import access.model.*;
85
import com.fasterxml.jackson.core.type.TypeReference;
96
import com.fasterxml.jackson.databind.ObjectMapper;
107
import lombok.SneakyThrows;
@@ -247,4 +244,9 @@ public List<Map<String, Object>> identityProvidersByAllowedConnections(List<Conn
247244
})
248245
.toList();
249246
}
247+
248+
@Override
249+
public void connectWithoutInteraction(Map<String, Object> identityProvider, Map<String, Object> serviceProvider, User currentUser) {
250+
//nope
251+
}
250252
}

server/src/main/java/access/manage/Manage.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
package access.manage;
22

3-
import access.model.Connection;
4-
import access.model.EntityType;
5-
import access.model.Environment;
6-
import access.model.Organization;
3+
import access.model.*;
74

85
import java.util.*;
96

@@ -49,6 +46,8 @@ public interface Manage {
4946

5047
List<Map<String, Object>> identityProvidersByAllowedConnections(List<Connection> connections);
5148

49+
void connectWithoutInteraction(Map<String, Object> identityProvider, Map<String, Object> serviceProvider, User currentUser);
50+
5251
default Map<String, Object> sanitizeProvider(Map<String, Object> provider) {
5352
//Different Manage API calls return 'id' or '_id'
5453
if (provider.containsKey("id")) {

server/src/main/java/access/manage/ManageData.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ public static Map<String, Object> getMetaDataFields(Map<String, Object> data) {
1515
return (Map<String, Object>) data.get("metaDataFields");
1616
}
1717

18-
public static Map<String, Object> getData(Map<String, Object> data) {
19-
return (Map<String, Object>) data.get("data");
18+
public static Map<String, Object> getData(Map<String, Object> provider) {
19+
return (Map<String, Object>) provider.get("data");
2020
}
2121

2222

server/src/main/java/access/manage/RemoteManage.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ public Map<String, Object> identityProviderByEntityID(String entityID) {
230230
if (identityProviders.isEmpty()) {
231231
throw new NotFoundException("No identityProviders found for entityID: " + entityID);
232232
}
233-
return identityProviders.getFirst();
233+
return sanitizeProvider(identityProviders.getFirst());
234234
}
235235

236236
@Override
@@ -339,6 +339,21 @@ public List<Map<String, Object>> identityProvidersByAllowedConnections(List<Conn
339339
return restTemplate.postForEntity(URI.create(url), body, List.class).getBody();
340340
}
341341

342+
@Override
343+
public void connectWithoutInteraction(Map<String, Object> identityProvider, Map<String, Object> serviceProvider, User user) {
344+
RestTemplate restTemplate = environmentRestTemplate(Environment.PROD);
345+
String url = String.format("%s/manage/api/internal/connectWithoutInteraction",
346+
environmentUrl(Environment.PROD));
347+
Map<String, String> bodyMap = new HashMap<>();
348+
bodyMap.put("idpId", (String) getData(identityProvider).get("entityid"));
349+
bodyMap.put("spId", (String) getData(serviceProvider).get("entityid"));
350+
bodyMap.put("spType", (String) serviceProvider.get("type"));
351+
bodyMap.put("user", user.getName());
352+
bodyMap.put("userUrn", user.getSub());
353+
//Fire and forget. An exception will be thrown by the restTemplate if the return is not 20X
354+
restTemplate.put(url, bodyMap);
355+
}
356+
342357
private List<Map<String, Object>> getRemoteMetaData(Environment environment, String type, boolean allAttributes) {
343358
Map<String, Object> baseQuery = getBaseQuery(allAttributes);
344359
String url = String.format("%s/manage/api/internal/search/%s", environmentUrl(environment), type);

0 commit comments

Comments
 (0)