Skip to content

Commit bf9410d

Browse files
committed
WIP for #294
1 parent 9edb7c4 commit bf9410d

25 files changed

Lines changed: 407 additions & 40 deletions

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

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,34 @@
11
package access.api;
22

3-
import access.exception.InvalidInputException;
43
import access.exception.NotAllowedException;
54
import access.exception.NotFoundException;
65
import access.jira.JiraClient;
76
import access.jira.JiraIssue;
8-
import access.manage.ChangeRequest;
7+
import access.mail.MailBox;
8+
import access.manage.DashBoardConnectionOption;
99
import access.manage.Manage;
10-
import access.manage.PathUpdateType;
11-
import access.manage.RequestType;
1210
import access.model.*;
1311
import access.repository.OrganizationRepository;
1412
import access.repository.UserRepository;
1513
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
16-
import lombok.SneakyThrows;
1714
import org.apache.commons.logging.Log;
1815
import org.apache.commons.logging.LogFactory;
1916
import org.springframework.http.HttpStatus;
2017
import org.springframework.http.MediaType;
2118
import org.springframework.http.ResponseEntity;
2219
import org.springframework.transaction.annotation.Transactional;
23-
import org.springframework.util.CollectionUtils;
24-
import org.springframework.util.StringUtils;
2520
import org.springframework.validation.annotation.Validated;
26-
import org.springframework.web.bind.annotation.*;
21+
import org.springframework.web.bind.annotation.PutMapping;
22+
import org.springframework.web.bind.annotation.RequestBody;
23+
import org.springframework.web.bind.annotation.RequestMapping;
24+
import org.springframework.web.bind.annotation.RestController;
2725

28-
import java.time.Instant;
29-
import java.util.Collections;
3026
import java.util.List;
3127
import java.util.Map;
3228

3329
import static access.SwaggerOpenIdConfig.API_TOKENS_SCHEME_NAME;
3430
import static access.SwaggerOpenIdConfig.OPEN_ID_SCHEME_NAME;
35-
import static access.api.Results.deleteResult;
36-
import static access.manage.ManageData.getData;
37-
import static access.manage.ManageData.getMetaDataFields;
31+
import static access.manage.ManageData.*;
3832

3933
@RestController
4034
@RequestMapping(value = {"/api/v1/idp"}, produces = MediaType.APPLICATION_JSON_VALUE)
@@ -49,15 +43,18 @@ public class IdentityProviderController implements UserAccessRights {
4943
private final OrganizationRepository organizationRepository;
5044
private final Manage manage;
5145
private final JiraClient jiraClient;
46+
private final MailBox mailBox;
5247

5348
public IdentityProviderController(UserRepository userRepository,
5449
OrganizationRepository organizationRepository,
5550
Manage manage,
56-
JiraClient jiraClient) {
51+
JiraClient jiraClient,
52+
MailBox mailBox) {
5753
this.userRepository = userRepository;
5854
this.organizationRepository = organizationRepository;
5955
this.manage = manage;
6056
this.jiraClient = jiraClient;
57+
this.mailBox = mailBox;
6158
}
6259

6360
@PutMapping({"/connect"})
@@ -74,6 +71,7 @@ public ResponseEntity<Map<String, Object>> connect(User user, @RequestBody @Vali
7471
.orElseThrow(() -> new NotFoundException("Organization with manageIdentifier not found: " + idpManageIdentifier));
7572

7673
User userFromDB = reinitializeUser(user, userRepository);
74+
//See https://github.com/OpenConext/OpenConext-access/wiki/Service-Connect-Flow
7775
boolean memberRequest = !userFromDB.isSuperUser();
7876
if (memberRequest) {
7977
OrganizationMembership organizationMembership = getOrganizationMembership(userFromDB, organization, Authority.GUEST)
@@ -83,19 +81,40 @@ public ResponseEntity<Map<String, Object>> connect(User user, @RequestBody @Vali
8381
}
8482
if (memberRequest) {
8583
//The only action is to email the institution admin of the organization, with a deep link to App
86-
// TODO send email
84+
List<User> admins = organization.getOrganizationMemberships().stream()
85+
.filter(membership -> membership.getAuthority().equals(Authority.ADMIN))
86+
.map(membership -> membership.getUser())
87+
.toList();
88+
if (admins.isEmpty()) {
89+
//Edge case, send the mail to the superusers instead
90+
admins = userRepository.findBySuperUser(true);
91+
}
92+
String deeplink = String.format("/application-detail/%s/%s",
93+
serviceProvider.get("type"),
94+
serviceProvider.get("id"));
95+
mailBox.sendConnectionRequest(userFromDB, admins, organization, getProviderName(serviceProvider),
96+
connectionRequest.getMessage(), deeplink);
8797
return Results.createResult();
8898
}
8999
//Now check if the connection can be made automatically
90100
Map<String, Object> spMetaDataFields = getMetaDataFields(getData(serviceProvider));
91-
String connectOption = (String) spMetaDataFields.getOrDefault("coin:dashboard_connect_option", "connect_with_interaction");
101+
DashBoardConnectionOption connectOption = DashBoardConnectionOption
102+
.fromValue((String) spMetaDataFields.getOrDefault("coin:dashboard_connect_option", "connect_with_interaction"));
92103
String idpInstitutionGUID = (String) getMetaDataFields(getData(identityProvider)).get("coin:institution_guid");
93104

94105
boolean idpAndSpShareInstitution = spMetaDataFields.getOrDefault("coin:institution_guid", "nope")
95106
.equals(idpInstitutionGUID);
96-
boolean connectWithoutInteraction = idpAndSpShareInstitution || !connectOption.equals("connect_with_interaction");
107+
boolean connectWithoutInteraction = idpAndSpShareInstitution || !connectOption.equals(DashBoardConnectionOption.connectWithInteraction);
97108
if (connectWithoutInteraction) {
98109
manage.connectWithoutInteraction(identityProvider, serviceProvider, userFromDB);
110+
if (connectOption.equals(DashBoardConnectionOption.connectWithoutInteractionWithEmail)) {
111+
mailBox.sendNewConnectionCreated(
112+
userFromDB,
113+
contactPersons(serviceProvider),
114+
getProviderName(identityProvider),
115+
getProviderName(serviceProvider),
116+
(String) getData(serviceProvider).get("entityid"));
117+
}
99118
return Results.createResult();
100119
}
101120

server/src/main/java/access/mail/MailBox.java

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,59 @@ public void sendInviteMail(Invitation invitation) {
7575
invitation.getEmail());
7676
}
7777

78+
@SneakyThrows
79+
public void sendConnectionRequest(User requester,
80+
List<User> recipients,
81+
Organization organization,
82+
String serviceProviderName,
83+
String message,
84+
String deepLink) {
85+
Language language = Language.valueOf(preferredLanguage());
86+
String title = String.format(subjects.get(language.name()).get("connectionRequest"),
87+
serviceProviderName);
88+
Map<String, Object> variables = new HashMap<>();
89+
if (StringUtils.hasText(message)) {
90+
variables.put("message", message.replaceAll("\n", "<br/>"));
91+
}
92+
variables.put("title", title);
93+
variables.put("serviceProviderName", serviceProviderName);
94+
variables.put("organization", organization);
95+
variables.put("requester", requester);
96+
variables.put("deepLink", String.format("%s/%s", clientUrl, deepLink));
97+
if (!environment.equalsIgnoreCase("prod")) {
98+
variables.put("environment", environment);
99+
}
100+
sendMail(String.format("connection_request_%s", language.name()),
101+
title,
102+
variables,
103+
recipients.stream().map(user -> user.getEmail()).toList().toArray(new String[]{}));
104+
}
105+
106+
@SneakyThrows
107+
public void sendNewConnectionCreated(User institutionAdmin,
108+
List<String> recipients,
109+
String identityProviderName,
110+
String serviceProviderName,
111+
String serviceProviderEntityId) {
112+
Language language = Language.valueOf(preferredLanguage());
113+
String title = String.format(subjects.get(language.name()).get("newConnectionMade"),
114+
serviceProviderName,
115+
identityProviderName);
116+
Map<String, Object> variables = new HashMap<>();
117+
variables.put("title", title);
118+
variables.put("identityProviderName", identityProviderName);
119+
variables.put("serviceProviderName", serviceProviderName);
120+
variables.put("serviceProviderEntityId", serviceProviderEntityId);
121+
variables.put("institutionAdmin", institutionAdmin);
122+
if (!environment.equalsIgnoreCase("prod")) {
123+
variables.put("environment", environment);
124+
}
125+
sendMail(String.format("new_connection_made_%s", language.name()),
126+
title,
127+
variables,
128+
recipients.toArray(new String[]{}));
129+
}
130+
78131
@SneakyThrows
79132
public void sendJoinRequestMail(JoinRequest joinRequest) {
80133
Language language = joinRequest.getLanguage();
@@ -140,7 +193,7 @@ public void sendFeedbackMail(User user, String message) {
140193
Map<String, Object> variables = new HashMap<>();
141194
variables.put("user", user);
142195
variables.put("title", "SURF Access feedback form");
143-
String now = LocalDate.now().format(DateTimeFormatter.ofPattern("dd-MM-yyyy"));
196+
String now = LocalDate.now().format(DateTimeFormatter.ofPattern("dd-MM-yyyy"));
144197
variables.put("date", now);
145198
variables.put("message", message.replaceAll("\n", "<br/>"));
146199
if (!environment.equalsIgnoreCase("prod")) {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package access.manage;
2+
3+
import lombok.Getter;
4+
5+
import java.util.Arrays;
6+
7+
public enum DashBoardConnectionOption {
8+
9+
connectWithInteraction("connect_with_interaction"),
10+
connectWithoutInteractionWithEmail("connect_without_interaction_with_email"),
11+
connectWithoutInteractionWithoutEmail("connect_without_interaction_without_email");
12+
13+
@Getter
14+
private final String value;
15+
16+
DashBoardConnectionOption(String value) {
17+
this.value = value;
18+
}
19+
20+
public static DashBoardConnectionOption fromValue(String value) {
21+
return Arrays.stream(DashBoardConnectionOption.values())
22+
.filter(option -> option.value.equals(value))
23+
.findFirst()
24+
.orElseThrow(() -> new IllegalArgumentException("No coin:dashboard_connect_option enum constant with value: " + value));
25+
}
26+
}

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import java.util.List;
66
import java.util.Map;
7+
import java.util.regex.Pattern;
78

89
@SuppressWarnings({"unchecked", "rawtypes"})
910
public class ManageData {
@@ -19,6 +20,22 @@ public static Map<String, Object> getData(Map<String, Object> provider) {
1920
return (Map<String, Object>) provider.get("data");
2021
}
2122

23+
public static String getProviderName(Map<String, Object> provider) {
24+
Map<String, Object> metaDataFields = getMetaDataFields(getData(provider));
25+
return (String) metaDataFields.get("name:en");
26+
}
27+
28+
public static List<String> contactPersons(Map<String, Object> provider) {
29+
Pattern pattern = Pattern.compile("contacts:[0-9]:contactType");
30+
List contactTypes = List.of("technical", "support", "administrative");
31+
Map<String, Object> metaDataFields = getMetaDataFields(getData(provider));
32+
return metaDataFields.keySet()
33+
.stream()
34+
.filter(key -> pattern.matcher(key).matches() && contactTypes.contains(metaDataFields.get(key)))
35+
.map(key -> (String) metaDataFields.get(key.replace("contactType", "emailAddress")))
36+
.toList();
37+
38+
}
2239

2340
public static boolean isEmpty(Object object) {
2441
return switch (object) {

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@
1010
import org.apache.commons.logging.LogFactory;
1111
import org.springframework.core.ParameterizedTypeReference;
1212
import org.springframework.core.io.ClassPathResource;
13-
import org.springframework.http.HttpEntity;
14-
import org.springframework.http.HttpMethod;
15-
import org.springframework.http.ResponseEntity;
13+
import org.springframework.http.*;
1614
import org.springframework.util.StringUtils;
1715
import org.springframework.web.client.ResponseErrorHandler;
1816
import org.springframework.web.client.RestTemplate;
@@ -77,11 +75,15 @@ public Map<String, Object> providerById(Connection connection) {
7775
return providerDetails(environment, protocol, manageIdentifier);
7876
}
7977

80-
private Map providerDetails(Environment environment, EntityType protocol, String manageIdentifier) {
78+
private Map<String, Object> providerDetails(Environment environment, EntityType protocol, String manageIdentifier) {
8179
String url = environmentUrl(environment);
8280
String queryUrl = String.format("%s/manage/api/internal/metadata/%s/%s", url, protocol.name(), manageIdentifier);
8381
RestTemplate restTemplate = environmentRestTemplate(environment);
84-
return restTemplate.getForEntity(queryUrl, Map.class).getBody();
82+
ResponseEntity<Map> responseEntity = restTemplate.getForEntity(queryUrl, Map.class);
83+
if (responseEntity.getStatusCode().equals(HttpStatus.OK)) {
84+
return sanitizeProvider(responseEntity.getBody());
85+
}
86+
return responseEntity.getBody();
8587
}
8688

8789
public Map<String, Object> providerById(EntityType entityType, String manageIdentifier, Environment environment) {

server/src/main/java/access/model/ConnectionRequest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ public class ConnectionRequest implements Serializable {
1212
private String applicationManageIdentifier;
1313
private EntityType entityType;
1414
private String idpManageIdentifier;
15+
private String message;
1516

1617
}

server/src/main/java/access/repository/UserRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.springframework.data.jpa.repository.Query;
99
import org.springframework.stereotype.Repository;
1010

11+
import java.util.List;
1112
import java.util.Map;
1213
import java.util.Optional;
1314

@@ -22,6 +23,8 @@ public interface UserRepository extends JpaRepository<User, Long> {//, QueryRewr
2223

2324
Optional<User> findBySubIgnoreCase(String sub);
2425

26+
List<User> findBySuperUser(boolean isSuperUser);
27+
2528
@Query(value = """
2629
SELECT u.id, u.name, u.email, u.schac_home_organization, u.super_user, u.institution_admin,
2730
u.created_at as createdAt, u.last_activity as lastActivity

server/src/main/resources/application.yml

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,19 @@ lifecycle:
7474
password: secret
7575

7676
jira:
77-
enabled: false
78-
base-url: "http://localhost:8081"
79-
user-name: "test-beheer-rpc"
77+
# enabled: false
78+
# base-url: "http://localhost:8081"
79+
# user-name: "test-beheer-rpc"
80+
# project-key: CXT
81+
# environment: test
82+
# api-key: secret
83+
enabled: true
84+
base-url: https://sd-jira-staging.surf.nl/jira/rest/api/2
85+
user-name: test-beheer-rpc
8086
project-key: CXT
8187
environment: test
82-
api-key: secret
88+
api-key: ODU1NzA3MDk3ODg5Onogb+ahwgLPgM7CLU0h1oqVSfEb
89+
8390
# Timeout in milliseconds
8491
connection-timeout: 60_000
8592

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>{{title}}</title>
5+
</head>
6+
<body>
7+
<div class="main" style="font-family: Helvetica, Arial, sans-serif;line-height: 18px;font-size: 16px;color: #353535;">
8+
<div class="header" style="background-color: #0077C8;padding:10px 40px;">
9+
{{> logo-surf.svg}}
10+
<span style="display: inline-block;color:white;margin-left: 10px;font-size: 18px;">Access</span>
11+
</div>
12+
<div class="head" style="padding: 30px 40px 20px 40px;">
13+
{{#environment}}
14+
<p style="font-weight: bold;margin: 0; padding: 20px 5px; background-color: #f1f19b;">
15+
This mail is from the {{environment}} environment
16+
</p>
17+
{{/environment}}
18+
<p class="title" style="">Hello,</p>
19+
20+
<p><strong>{{requester.name}}</strong>
21+
has requested to connect service {{serviceProviderName} to your organization {{organization.name}}.</p>
22+
23+
{{#message}}
24+
<p style="margin: 20px 0 0 0;">Personal message from {{invitation.invitee.name}}</p>
25+
<div class="head" style="background-color: #f5f5f5;padding: 20px;margin:5px 0 15px 0;font-style: italic;">
26+
<p style="margin: 0;">
27+
{{{message}}}
28+
</p>
29+
</div>
30+
{{/message}}
31+
32+
<div style="display: flex;">
33+
<a class="button" href="{{{ deepLink }}}"
34+
style="border-radius: 4px;color: white;display: flex;width: 300px;background-color: #0077c8;text-decoration: none;margin: 15px auto;cursor:pointer;padding: 14px 26px;">
35+
<span style="color: white;margin: auto 0 auto 25px;">Click to open {{serviceProviderName}} in Access</span>
36+
</a>
37+
</div>
38+
</div>
39+
{{> footer_en.html}}
40+
41+
</div>
42+
</body>
43+
</html>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{{requester.name}}</strong>
2+
has requested to connect service {{serviceProviderName} to your organization {{organization.name}}
3+
4+
Visit service:
5+
6+
{{{ deepLink }}}
7+
8+
{{> footer_en.txt}}

0 commit comments

Comments
 (0)