Skip to content

Commit 1d1e751

Browse files
committed
Bugfix for connection request for guest user from eduID
1 parent cb2959d commit 1d1e751

File tree

5 files changed

+112
-32
lines changed

5 files changed

+112
-32
lines changed

client/src/locale/en.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,10 +1040,10 @@ const en = {
10401040
all: "Iedereen van de {{orgName}} heeft direct automatisch toegang",
10411041
some: "Pas toegangsregels toe"
10421042
},
1043-
memberRequestInfo: [
1044-
"Om een applicatie gekoppelde te krijgen met het SURF Access platform, moet akkoord gegeven worden worden door de SURF Access Verantwoordelijke van de {{orgName}}.",
1045-
"Geef hieronder aan waarom je deze applicatie geactiveerd wil hebben. Wij versturen het bericht naar hem of haar, je ontvangt zelf ook een kopie."
1046-
],
1043+
memberRequestInfo: {
1044+
info:"Om een applicatie gekoppelde te krijgen met het SURF Access platform, moet akkoord gegeven worden worden door de SURF Access Verantwoordelijke van de {{orgName}} organisatie.",
1045+
subInfo:"Geef hieronder aan waarom je deze applicatie geactiveerd wil hebben. Wij versturen het bericht naar hem of haar, je ontvangt zelf ook een kopie."
1046+
},
10471047
messagePlaceholder: "Your message",
10481048
sendMessage: "Send message",
10491049
flash: {

client/src/pages/ApplicationDetail.jsx

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,13 @@ const confirmationModalOptions = {
5656

5757
const ApplicationDetail = ({anonymous, refreshUser}) => {
5858

59-
const {arp, privacy, user, config, setFlash} = useAppStore(useShallow(state => ({
59+
const {arp, privacy, user, config, setFlash, currentOrganization} = useAppStore(useShallow(state => ({
6060
arp: state.arp,
6161
privacy: state.privacy,
6262
user: state.user,
6363
config: state.config,
64-
setFlash: state.setFlash
64+
setFlash: state.setFlash,
65+
currentOrganization: state.currentOrganization
6566
})));
6667

6768
const navigate = useNavigate();
@@ -118,7 +119,7 @@ const ApplicationDetail = ({anonymous, refreshUser}) => {
118119
return;
119120
}
120121
//See if this application is already connected
121-
const { isAccessible, isReadOnly } = deriveAccess(user, res.data.entityid);
122+
const {isAccessible, isReadOnly} = deriveAccess(user, res.data.entityid);
122123
const adminUser = isAdmin(user, authorities);
123124
setAccessible(isAccessible);
124125
setIsAdminUser(adminUser);
@@ -245,10 +246,11 @@ const ApplicationDetail = ({anonymous, refreshUser}) => {
245246
return (
246247
<div className="connect-options-container">
247248
<h3>{I18n.t("applicationConnect.requestMember")}</h3>
248-
{I18n.translations[I18n.locale].applicationConnect.memberRequestInfo
249-
.map((info, index) =>
250-
<p key={index} dangerouslySetInnerHTML={{__html: info}}/>
251-
)}
249+
<p dangerouslySetInnerHTML={{
250+
__html: I18n.t("applicationConnect.memberRequestInfo.info",
251+
{orgName: currentOrganization.name})
252+
}}/>
253+
<p dangerouslySetInnerHTML={{__html: I18n.t("applicationConnect.memberRequestInfo.subInfo")}}/>
252254
<InputField multiline={true}
253255
displayLabel={false}
254256
value={message}
@@ -278,6 +280,7 @@ const ApplicationDetail = ({anonymous, refreshUser}) => {
278280

279281
const cancelConnectionRequest = (withConfirmation, e) => {
280282
stopEvent(e);
283+
setConfirmationModalOption(null);
281284
if (withConfirmation) {
282285
setConfirmation({
283286
open: true,
@@ -315,10 +318,11 @@ const ApplicationDetail = ({anonymous, refreshUser}) => {
315318
} else {
316319
cancelConfirmation();
317320
setLoading(true);
321+
const manageIdentifierOrg = user.identityProvider?.id || currentOrganization.manageIdentifier;
318322
connectServiceProviderToIdentityProvider(
319323
serviceProvider.id,
320324
serviceProvider.type,
321-
user.identityProvider.id,
325+
manageIdentifierOrg,
322326
message)
323327
.then(() => {
324328
if (confirmationModalOption === confirmationModalOptions.requestConnectionByMember) {

client/src/pages/MyOrganization.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const MyOrganization = ({refreshUser}) => {
4242
const [section, setSection] = useState(externalUser ? sections.general : sections.contactPersons);
4343
const [focusedId, setFocusedId] = useState(null);
4444
const [initial, setInitial] = useState(true);
45+
const [originalOrganizationName, setOriginalOrganizationName] = useState("");
4546
const [affectedIdentityProviders, setAffectedIdentityProviders] = useState([]);
4647
const [dirty, setDirty] = useState(new Date());
4748

@@ -63,6 +64,7 @@ const MyOrganization = ({refreshUser}) => {
6364
.then(res => {
6465
const convertedOrganization = convertServerApplicationToClient(res);
6566
setOrganization(convertedOrganization);
67+
setOriginalOrganizationName(res.name);
6668
setLoading(false);
6769
}).catch(() => {
6870
navigate("/home")
@@ -261,7 +263,7 @@ const MyOrganization = ({refreshUser}) => {
261263
<p dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(I18n.t("myOrganization.info"))}}/>
262264
</div>
263265
<div className="my-organization">
264-
<h1>{I18n.t("myOrganization.maintenance", {name: organization.name})}</h1>
266+
<h1>{I18n.t("myOrganization.maintenance", {name: originalOrganizationName})}</h1>
265267
<div className="menu-container">
266268
<div className="left-menu">
267269
{availableSections

client/src/utils/Permissions.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,14 @@ export const hasApplicationDeleteAccess = (user, application) => {
7979
}
8080

8181
export const deriveAccess = (user, spEntityId) => {
82+
let isAccessible = false, isReadOnly = false;
83+
if (isEmpty(user.identityProvider)) {
84+
//External user
85+
return {isAccessible, isReadOnly};
86+
}
8287
const allowedEntities = user.identityProvider.data.allowedEntities.map(e => e.name);
83-
let isAccessible = allowedEntities.includes(spEntityId);
84-
let isReadOnly = !isAccessible;
88+
isAccessible = allowedEntities.includes(spEntityId);
89+
isReadOnly = !isAccessible;
8590

8691
if (!isAccessible) {
8792
isAccessible = user.changeRequests.some(cr =>
@@ -91,10 +96,14 @@ export const deriveAccess = (user, spEntityId) => {
9196
);
9297
}
9398

94-
return { isAccessible, isReadOnly };
99+
return {isAccessible, isReadOnly};
95100
}
96101

97102
export const isAdmin = (user, authorities) => {
103+
if (isEmpty(user.identityProvider)) {
104+
//External user
105+
return false;
106+
}
98107
const idpId = user.identityProvider.id;
99108
const orgMembership = user.organizationMemberships.find(
100109
m => m.organization.manageIdentifier === idpId

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

Lines changed: 81 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,18 @@
55
import access.jira.JiraClient;
66
import access.jira.JiraIssue;
77
import access.mail.MailBox;
8-
import access.manage.*;
9-
import access.model.*;
8+
import access.manage.ChangeRequest;
9+
import access.manage.DashBoardConnectionOption;
10+
import access.manage.Manage;
11+
import access.manage.PathUpdateType;
12+
import access.manage.RequestType;
13+
import access.model.Authority;
14+
import access.model.ConnectionRequest;
15+
import access.model.EntityType;
16+
import access.model.Environment;
17+
import access.model.Organization;
18+
import access.model.OrganizationMembership;
19+
import access.model.User;
1020
import access.repository.OrganizationRepository;
1121
import access.repository.UserRepository;
1222
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
@@ -17,7 +27,6 @@
1727
import org.springframework.http.ResponseEntity;
1828
import org.springframework.transaction.annotation.Transactional;
1929
import org.springframework.util.CollectionUtils;
20-
import org.springframework.util.StringUtils;
2130
import org.springframework.validation.annotation.Validated;
2231
import org.springframework.web.bind.annotation.PutMapping;
2332
import org.springframework.web.bind.annotation.RequestBody;
@@ -61,7 +70,10 @@ public IdentityProviderController(UserRepository userRepository,
6170

6271
@PutMapping({"/connect"})
6372
public ResponseEntity<Map<String, Object>> connect(User user, @RequestBody @Validated ConnectionRequest connectionRequest) {
64-
LOG.debug("/connect SP to IdP connection for " + user.getEmail());
73+
String email = user.getEmail();
74+
LOG.debug("/connect SP to IdP connection for " + email);
75+
76+
user = reinitializeUser(user, userRepository);
6577

6678
String idpManageIdentifier = connectionRequest.getIdpManageIdentifier();
6779
Organization organization = organizationRepository.findByManageIdentifier(idpManageIdentifier)
@@ -70,15 +82,11 @@ public ResponseEntity<Map<String, Object>> connect(User user, @RequestBody @Vali
7082
Map<String, Object> serviceProvider = manage.providerById(connectionRequest.getEntityType(),
7183
connectionRequest.getApplicationManageIdentifier(), Environment.PROD);
7284

73-
Map<String, Object> identityProvider = manage.providerById(EntityType.saml20_idp, idpManageIdentifier, Environment.PROD);
74-
75-
User userFromDB = reinitializeUser(user, userRepository);
76-
//See https://github.com/OpenConext/OpenConext-access/wiki/Service-Connect-Flow
77-
boolean memberRequest = !userFromDB.isSuperUser();
85+
boolean memberRequest = !user.isSuperUser();
7886
if (memberRequest) {
79-
OrganizationMembership organizationMembership = getOrganizationMembership(userFromDB, organization, Authority.GUEST)
87+
OrganizationMembership organizationMembership = getOrganizationMembership(user, organization, Authority.GUEST)
8088
.orElseThrow(() -> new NotAllowedException(
81-
String.format("User %s is not a member of organization %s", userFromDB.getEmail(), organization.getName())));
89+
String.format("User %s is not a member of organization %s", email, organization.getName())));
8290
memberRequest = !organizationMembership.getAuthority().equals(Authority.ADMIN);
8391
}
8492
if (memberRequest) {
@@ -97,10 +105,14 @@ public ResponseEntity<Map<String, Object>> connect(User user, @RequestBody @Vali
97105
//Avoid UnsupportedException for immutable collections
98106
admins = new ArrayList<>(admins);
99107
admins.add(user);
100-
mailBox.sendConnectionRequest(userFromDB, admins, organization, getProviderName(serviceProvider),
108+
mailBox.sendConnectionRequest(user, admins, organization, getProviderName(serviceProvider),
101109
connectionRequest.getMessage(), deeplink);
102110
return Results.createResult();
103111
}
112+
113+
Map<String, Object> identityProvider = manage.providerById(EntityType.saml20_idp, idpManageIdentifier, Environment.PROD);
114+
115+
//See https://github.com/OpenConext/OpenConext-access/wiki/Service-Connect-Flow
104116
//Now check if the connection can be made automatically
105117
Map<String, Object> spMetaDataFields = getMetaDataFields(getData(serviceProvider));
106118
DashBoardConnectionOption connectOption = DashBoardConnectionOption
@@ -111,12 +123,12 @@ public ResponseEntity<Map<String, Object>> connect(User user, @RequestBody @Vali
111123
.equals(idpInstitutionGUID);
112124
boolean connectWithoutInteraction = idpAndSpShareInstitution || !connectOption.equals(DashBoardConnectionOption.connectWithInteraction);
113125
if (connectWithoutInteraction) {
114-
manage.connectWithoutInteraction(identityProvider, serviceProvider, userFromDB);
126+
manage.connectWithoutInteraction(identityProvider, serviceProvider, user);
115127
if (connectOption.equals(DashBoardConnectionOption.connectWithoutInteractionWithEmail)) {
116128
List<String> recipients = contactPersons(serviceProvider);
117129
if (!CollectionUtils.isEmpty(recipients)) {
118130
mailBox.sendNewConnectionCreated(
119-
userFromDB,
131+
user,
120132
recipients,
121133
getProviderName(identityProvider),
122134
getProviderName(serviceProvider),
@@ -143,13 +155,13 @@ public ResponseEntity<Map<String, Object>> connect(User user, @RequestBody @Vali
143155
changeRequestURL),
144156
summary,
145157
EntityType.valueOf((String) serviceProvider.get("type")),
146-
user.getEmail()
158+
email
147159
));
148160
ChangeRequest changeRequest = new ChangeRequest(
149161
idpManageIdentifier,
150162
EntityType.saml20_idp,
151163
Map.of("allowedEntities", Map.of("name", serviceProviderEntityID)),
152-
Map.of("user", user.getEmail(),
164+
Map.of("user", email,
153165
"notes", String.format("Connection request requested by %s from %s for %s. See Jira %s",
154166
user.getName(),
155167
identityProviderEntityID,
@@ -164,5 +176,58 @@ public ResponseEntity<Map<String, Object>> connect(User user, @RequestBody @Vali
164176
Map.of("status", HttpStatus.CREATED.value(), "jiraKey", jiraKey));
165177
}
166178

179+
@PutMapping({"/disconnect"})
180+
public ResponseEntity<Map<String, Object>> disconnect(User user, @RequestBody @Validated ConnectionRequest connectionRequest) {
181+
LOG.debug("/disconnect SP to IdP request by " + user.getEmail());
182+
183+
user = reinitializeUser(user, userRepository);
184+
185+
String idpManageIdentifier = connectionRequest.getIdpManageIdentifier();
186+
Organization organization = organizationRepository.findByManageIdentifier(idpManageIdentifier)
187+
.orElseThrow(() -> new NotFoundException("Organization with manageIdentifier not found: " + idpManageIdentifier));
188+
189+
Map<String, Object> serviceProvider = manage.providerById(connectionRequest.getEntityType(),
190+
connectionRequest.getApplicationManageIdentifier(), Environment.PROD);
191+
192+
confirmOrganizationMembership(user, organization, Authority.ADMIN);
193+
Map<String, Object> identityProvider = manage.providerById(EntityType.saml20_idp, idpManageIdentifier, Environment.PROD);
194+
195+
String changeRequestURL = manage.changeRequestURLConnectionRequest(EntityType.saml20_idp, idpManageIdentifier);
196+
197+
String identityProviderEntityID = getEntityID(identityProvider);
198+
String serviceProviderEntityID = getEntityID(serviceProvider);
199+
String lineSeparator = System.lineSeparator();
200+
String summary = String.format("Disconnection request requested by %s for %s.",
201+
user.getName(), getProviderName(identityProvider));
202+
String jiraKey = jiraClient.create(new JiraIssue(
203+
serviceProviderEntityID,
204+
identityProviderEntityID,
205+
String.format("%s%sA change request in manage has been created to merge this user request. See:%s%s",
206+
summary,
207+
lineSeparator,
208+
lineSeparator,
209+
changeRequestURL),
210+
summary,
211+
EntityType.valueOf((String) serviceProvider.get("type")),
212+
user.getEmail()
213+
));
214+
ChangeRequest changeRequest = new ChangeRequest(
215+
idpManageIdentifier,
216+
EntityType.saml20_idp,
217+
Map.of("allowedEntities", Map.of("name", serviceProviderEntityID)),
218+
Map.of("user", user.getEmail(),
219+
"notes", String.format("Disconnection request requested by %s from %s for %s. See Jira %s",
220+
user.getName(),
221+
identityProviderEntityID,
222+
serviceProviderEntityID,
223+
jiraKey)),
224+
true,
225+
PathUpdateType.REMOVAL,
226+
RequestType.UnlinkRequest);
227+
manage.createChangeRequest(Environment.PROD, changeRequest);
228+
229+
return ResponseEntity.status(HttpStatus.CREATED).body(
230+
Map.of("status", HttpStatus.CREATED.value(), "jiraKey", jiraKey));
231+
}
167232

168233
}

0 commit comments

Comments
 (0)