Skip to content

Commit dd9064a

Browse files
committed
[Prod] Implement InvitationsController zone path
1 parent b518220 commit dd9064a

File tree

4 files changed

+42
-19
lines changed

4 files changed

+42
-19
lines changed

server/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsController.java

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.cloudfoundry.identity.uaa.util.JsonUtils;
3030
import org.cloudfoundry.identity.uaa.util.ObjectUtils;
3131
import org.cloudfoundry.identity.uaa.util.UaaHttpRequestUtils;
32+
import org.cloudfoundry.identity.uaa.util.UaaUrlUtils;
3233
import org.cloudfoundry.identity.uaa.zone.BrandingInformation;
3334
import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManager;
3435
import org.springframework.beans.factory.annotation.Qualifier;
@@ -74,7 +75,7 @@
7475

7576
@Slf4j
7677
@Controller
77-
@RequestMapping("/invitations")
78+
@RequestMapping(value = {"/invitations", "/z/{subdomain}/invitations"})
7879
public class InvitationsController {
7980

8081
private static final String EMAIL = "email";
@@ -119,6 +120,7 @@ public String acceptInvitePage(@RequestParam String code, Model model, HttpServl
119120

120121
ExpiringCode expiringCode = expiringCodeStore.peekCode(code, identityZoneManager.getCurrentIdentityZoneId());
121122
if ((null == expiringCode) || (null != expiringCode.getIntent() && !INVITATION.name().equals(expiringCode.getIntent()))) {
123+
model.addAttribute("pathPrefix", UaaUrlUtils.getZonePathPrefix(request));
122124
return handleUnprocessableEntity(model, response, "error_message_code", "code_expired", "invitations/accept_invite");
123125
}
124126

@@ -161,6 +163,7 @@ public String acceptInvitePage(@RequestParam String code, Model model, HttpServl
161163
AnonymousAuthenticationToken token = new AnonymousAuthenticationToken("scim.invite", uaaPrincipal,
162164
Collections.singletonList(UaaAuthority.UAA_INVITED));
163165
SecurityContextHolder.getContext().setAuthentication(token);
166+
model.addAttribute("pathPrefix", UaaUrlUtils.getZonePathPrefix(request));
164167
model.addAttribute("provider", provider.getType());
165168
model.addAttribute("code", code);
166169
model.addAttribute(EMAIL, codeData.get(EMAIL));
@@ -170,6 +173,7 @@ public String acceptInvitePage(@RequestParam String code, Model model, HttpServl
170173
return "invitations/accept_invite";
171174
} catch (EmptyResultDataAccessException noProviderFound) {
172175
log.debug("No available invitation providers for email:%s, id:%s".formatted(codeData.get(EMAIL), codeData.get("user_id")));
176+
model.addAttribute("pathPrefix", UaaUrlUtils.getZonePathPrefix(request));
173177
return handleUnprocessableEntity(model, response, "error_message_code", "no_suitable_idp", "invitations/accept_invite");
174178
}
175179
}
@@ -244,8 +248,11 @@ public String acceptInvitation(@RequestParam("password") String password,
244248
@RequestParam("code") String code,
245249
@RequestParam(value = "does_user_consent", required = false) boolean doesUserConsent,
246250
Model model,
251+
HttpServletRequest request,
247252
HttpServletResponse response) {
248253

254+
String pathPrefix = UaaUrlUtils.getZonePathPrefix(request);
255+
249256
PasswordConfirmationValidation validation = new PasswordConfirmationValidation(password, passwordConfirmation);
250257

251258
UaaPrincipal principal = (UaaPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
@@ -255,43 +262,46 @@ public String acceptInvitation(@RequestParam("password") String password,
255262
if (expiringCode == null || expiringCode.getData() == null) {
256263
log.debug("Failing invitation. Code not found.");
257264
SecurityContextHolder.clearContext();
265+
model.addAttribute("pathPrefix", pathPrefix);
258266
return handleUnprocessableEntity(model, response, "error_message_code", "code_expired", "invitations/accept_invite");
259267
}
260268
Map<String, String> data = JsonUtils.readValue(expiringCode.getData(), new TypeReference<>() {
261269
});
262270
if (principal == null || data.get("user_id") == null || !data.get("user_id").equals(principal.getId())) {
263271
log.debug("Failing invitation. Code and user ID mismatch.");
264272
SecurityContextHolder.clearContext();
273+
model.addAttribute("pathPrefix", pathPrefix);
265274
return handleUnprocessableEntity(model, response, "error_message_code", "code_expired", "invitations/accept_invite");
266275
}
267276

268277
final String newCode = expiringCodeStore.generateCode(expiringCode.getData(), new Timestamp(System.currentTimeMillis() + (10 * 60 * 1000)), expiringCode.getIntent(), identityZoneManager.getCurrentIdentityZoneId()).getCode();
269278
BrandingInformation zoneBranding = identityZoneManager.getCurrentIdentityZone().getConfig().getBranding();
270279
if (zoneBranding != null && zoneBranding.getConsent() != null && !doesUserConsent) {
271-
return processErrorReload(newCode, model, response, "error_message_code", "missing_consent");
280+
return processErrorReload(newCode, model, response, "error_message_code", "missing_consent", pathPrefix);
272281
}
273282
if (!validation.valid()) {
274-
return processErrorReload(newCode, model, response, "error_message_code", validation.getMessageCode());
283+
return processErrorReload(newCode, model, response, "error_message_code", validation.getMessageCode(), pathPrefix);
275284
}
276285
try {
277286
passwordValidator.validate(password);
278287
} catch (InvalidPasswordException e) {
279-
return processErrorReload(newCode, model, response, "error_message", e.getMessagesAsOneString());
288+
return processErrorReload(newCode, model, response, "error_message", e.getMessagesAsOneString(), pathPrefix);
280289
}
281290
AcceptedInvitation invitation;
282291
try {
283292
invitation = invitationsService.acceptInvitation(newCode, password);
284293
} catch (HttpClientErrorException e) {
294+
model.addAttribute("pathPrefix", pathPrefix);
285295
return handleUnprocessableEntity(model, response, "error_message_code", "code_expired", "invitations/accept_invite");
286296
}
287-
String res = "redirect:/login?success=invite_accepted";
297+
String res = pathPrefix + "/login?success=invite_accepted";
288298
if (!invitation.getRedirectUri().equals("/home")) {
289299
res += "&" + FORM_REDIRECT_PARAMETER + "=" + invitation.getRedirectUri();
290300
}
291-
return res;
301+
return "redirect:" + res;
292302
}
293303

294-
private String processErrorReload(String code, Model model, HttpServletResponse response, String errorCode, String error) {
304+
private String processErrorReload(String code, Model model, HttpServletResponse response, String errorCode, String error, String pathPrefix) {
295305
ExpiringCode expiringCode = expiringCodeStore.retrieveCode(code, identityZoneManager.getCurrentIdentityZoneId());
296306
Map<String, String> codeData = JsonUtils.readValue(expiringCode.getData(), new TypeReference<>() {
297307
});
@@ -300,9 +310,11 @@ private String processErrorReload(String code, Model model, HttpServletResponse
300310

301311
model.addAttribute(errorCode, error);
302312
model.addAttribute("code", newCode);
303-
return "redirect:accept";
313+
String redirectTarget = (pathPrefix != null && !pathPrefix.isEmpty()) ? pathPrefix + "/invitations/accept" : "accept";
314+
return "redirect:" + redirectTarget;
304315
} catch (EmptyResultDataAccessException noProviderFound) {
305316
log.debug("No available invitation providers for email:%s, id:%s".formatted(codeData.get(EMAIL), codeData.get("user_id")));
317+
model.addAttribute("pathPrefix", pathPrefix);
306318
return handleUnprocessableEntity(model, response, "error_message_code", "no_suitable_idp", "invitations/accept_invite");
307319
}
308320
}
@@ -312,7 +324,8 @@ public String acceptLdapInvitation(@RequestParam("enterprise_username") String u
312324
@RequestParam("enterprise_password") String password,
313325
@RequestParam("enterprise_email") String email,
314326
@RequestParam String code,
315-
Model model, HttpServletResponse response) {
327+
Model model, HttpServletRequest request, HttpServletResponse response) {
328+
String pathPrefix = UaaUrlUtils.getZonePathPrefix(request);
316329

317330
ExpiringCode expiringCode = expiringCodeStore.retrieveCode(code, identityZoneManager.getCurrentIdentityZoneId());
318331
if (expiringCode == null) {
@@ -330,9 +343,11 @@ public String acceptLdapInvitation(@RequestParam("enterprise_username") String u
330343
authenticationManager = zoneAwareAuthenticationManager.getLdapAuthenticationManager(identityZoneManager.getCurrentIdentityZone(), ldapProvider).getLdapManagerActual();
331344
} catch (EmptyResultDataAccessException e) {
332345
//ldap provider was not available
346+
model.addAttribute("pathPrefix", pathPrefix);
333347
return handleUnprocessableEntity(model, response, "error_message_code", "no_suitable_idp", "invitations/accept_invite");
334348
} catch (Exception x) {
335349
log.error("Unable to retrieve LDAP config.", x);
350+
model.addAttribute("pathPrefix", pathPrefix);
336351
return handleUnprocessableEntity(model, response, "error_message_code", "no_suitable_idp", "invitations/accept_invite");
337352
}
338353
Authentication authentication;
@@ -345,6 +360,7 @@ public String acceptLdapInvitation(@RequestParam("enterprise_username") String u
345360
model.addAttribute(EMAIL, data.get(EMAIL));
346361
model.addAttribute("provider", OriginKeys.LDAP);
347362
model.addAttribute("code", expiringCodeStore.generateCode(expiringCode.getData(), new Timestamp(System.currentTimeMillis() + (10 * 60 * 1000)), null, identityZoneManager.getCurrentIdentityZoneId()).getCode());
363+
model.addAttribute("pathPrefix", pathPrefix);
348364
return handleUnprocessableEntity(model, response, "error_message", "invite.email_mismatch", "invitations/accept_invite");
349365
}
350366

@@ -354,16 +370,19 @@ public String acceptLdapInvitation(@RequestParam("enterprise_username") String u
354370
userProvisioning.update(user.getId(), user, identityZoneManager.getCurrentIdentityZoneId());
355371
zoneAwareAuthenticationManager.getLdapAuthenticationManager(identityZoneManager.getCurrentIdentityZone(), ldapProvider).authenticate(token);
356372
AcceptedInvitation accept = invitationsService.acceptInvitation(newCode, "");
357-
return "redirect:" + "/login?success=invite_accepted&form_redirect_uri=" + URLEncoder.encode(accept.getRedirectUri(), StandardCharsets.UTF_8);
373+
return "redirect:" + pathPrefix + "/login?success=invite_accepted&form_redirect_uri=" + URLEncoder.encode(accept.getRedirectUri(), StandardCharsets.UTF_8);
358374
} else {
375+
model.addAttribute("pathPrefix", pathPrefix);
359376
return handleUnprocessableEntity(model, response, "error_message", "not authenticated", "invitations/accept_invite");
360377
}
361378
} catch (AuthenticationException x) {
379+
model.addAttribute("pathPrefix", pathPrefix);
362380
return handleUnprocessableEntity(model, response, "error_message", x.getMessage(), "invitations/accept_invite");
363381
} catch (Exception x) {
364382
log.error("Unable to authenticate against LDAP", x);
365383
model.addAttribute("ldap", true);
366384
model.addAttribute(EMAIL, email);
385+
model.addAttribute("pathPrefix", pathPrefix);
367386
return handleUnprocessableEntity(model, response, "error_message", "bad_credentials", "invitations/accept_invite");
368387
}
369388
}

server/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsEndpoint.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public InvitationsEndpoint(final ScimUserProvisioning scimUserProvisioning,
6767
this.expiringCodeStore = expiringCodeStore;
6868
}
6969

70-
@PostMapping(value = {"/invite_users", "/invite_users/"}, consumes = "application/json")
70+
@PostMapping(value = {"/invite_users", "/invite_users/", "/z/{subdomain}/invite_users", "/z/{subdomain}/invite_users/"}, consumes = "application/json")
7171
public ResponseEntity<InvitationsResponse> inviteUsers(@RequestBody InvitationsRequest invitations,
7272
@RequestParam(value = "client_id", required = false) String clientId,
7373
@RequestParam(value = "redirect_uri") String redirectUri) {
@@ -85,6 +85,7 @@ public ResponseEntity<InvitationsResponse> inviteUsers(@RequestBody InvitationsR
8585
List<IdentityProvider> activeProviders = identityProviderProvisioning.retrieveActive(IdentityZoneHolder.get().getId());
8686

8787
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
88+
String pathPrefix = UaaUrlUtils.getZonePathPrefix(request);
8889
String subdomainHeader = request.getHeader(SUBDOMAIN_HEADER);
8990
String zoneIdHeader = request.getHeader(HEADER);
9091

@@ -94,13 +95,16 @@ public ResponseEntity<InvitationsResponse> inviteUsers(@RequestBody InvitationsR
9495
client = multitenantClientServices.loadClientByClientId(clientId, IdentityZoneHolder.get().getId());
9596
}
9697

98+
String acceptPath = (pathPrefix != null && !pathPrefix.isEmpty()) ? pathPrefix + "/invitations/accept" : "/invitations/accept";
99+
boolean useSubdomainHost = !IdentityZoneHolder.isUaa() && (pathPrefix == null || pathPrefix.isEmpty());
100+
97101
for (String email : invitations.getEmails()) {
98102
try {
99103
if (email != null && validateEmail(email)) {
100104
List<IdentityProvider> providers = filter(activeProviders, client, email);
101105
if (providers.size() == 1) {
102106
ScimUser user = findOrCreateUser(email, providers.getFirst().getOriginKey());
103-
String accountsUrl = UaaUrlUtils.getUaaUrl("/invitations/accept", !IdentityZoneHolder.isUaa(), IdentityZoneHolder.get());
107+
String accountsUrl = UaaUrlUtils.getUaaUrl(acceptPath, useSubdomainHost, IdentityZoneHolder.get());
104108

105109
Map<String, String> data = new HashMap<>();
106110
data.put(USER_ID, user.getId());

server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginSecurityConfiguration.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -403,12 +403,12 @@ UaaFilterChain invitation(
403403
) throws Exception {
404404
var originalChain = http
405405
.securityMatcher(
406-
"/invitations/**"
406+
"/invitations/**", "/z/*/invitations/**"
407407
)
408408
.authorizeHttpRequests(auth -> {
409-
auth.requestMatchers(HttpMethod.GET, "/invitations/accept").access(anyOf().anonymous().fullyAuthenticated());
410-
auth.requestMatchers(HttpMethod.POST, "/invitations/accept.do").hasAuthority("uaa.invited");
411-
auth.requestMatchers(HttpMethod.POST, "/invitations/accept_enterprise.do").hasAuthority("uaa.invited");
409+
auth.requestMatchers(HttpMethod.GET, "/invitations/accept", "/z/*/invitations/accept").access(anyOf().anonymous().fullyAuthenticated());
410+
auth.requestMatchers(HttpMethod.POST, "/invitations/accept.do", "/z/*/invitations/accept.do").hasAuthority("uaa.invited");
411+
auth.requestMatchers(HttpMethod.POST, "/invitations/accept_enterprise.do", "/z/*/invitations/accept_enterprise.do").hasAuthority("uaa.invited");
412412
auth.anyRequest().denyAll();
413413

414414
})
@@ -436,7 +436,7 @@ UaaFilterChain inviteUser(
436436
@Qualifier("resourceAgnosticAuthenticationFilter") FilterRegistrationBean<OAuth2AuthenticationProcessingFilter> oauth2ResourceFilter
437437
) throws Exception {
438438
var originalChain = http
439-
.securityMatcher("/invite_users/**")
439+
.securityMatcher("/invite_users/**", "/z/*/invite_users/**")
440440
.authorizeHttpRequests(auth -> {
441441
auth.requestMatchers(HttpMethod.POST, "/**").access(
442442
anyOf().isUaaAdmin()

server/src/main/resources/templates/web/invitations/accept_invite.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ <h1><th:block th:unless="${isLdap}">Create</th:block><th:block th:if="${isLdap}"
1616
</div>
1717
<th:block th:unless="${error_message_code == 'code_expired'}" >
1818
<th:block th:if="${provider == 'uaa'}">
19-
<form th:action="@{/invitations/accept.do}" method="post" novalidate="novalidate">
19+
<form th:action="${pathPrefix != null && !#strings.isEmpty(pathPrefix) ? pathPrefix + '/invitations/accept.do' : '/invitations/accept.do'}" method="post" novalidate="novalidate">
2020
<input name="code" type="hidden" value="code" th:value="${code}"/>
2121
<input name="password" type="password" placeholder="Password" aria-label="Password" autocomplete="off" class="form-control"/>
2222
<input name="password_confirmation" type="password" placeholder="Confirm" aria-label="Confirm Password" autocomplete="off" class="form-control"/>
@@ -33,7 +33,7 @@ <h1><th:block th:unless="${isLdap}">Create</th:block><th:block th:if="${isLdap}"
3333
</th:block>
3434
<th:block th:if="${isLdap}">
3535
<p>Sign in with enterprise credentials: </p>
36-
<form th:action="@{/invitations/accept_enterprise.do}" method="post" novalidate="novalidate">
36+
<form th:action="${pathPrefix != null && !#strings.isEmpty(pathPrefix) ? pathPrefix + '/invitations/accept_enterprise.do' : '/invitations/accept_enterprise.do'}" method="post" novalidate="novalidate">
3737
<input name="enterprise_email" type="hidden" th:value="${email}"/>
3838
<input name="code" type="hidden" value="code" th:value="${code}"/>
3939
<input name="enterprise_username" type="text" placeholder="Username" aria-label="Username" autocomplete="off" class="form-control"/>

0 commit comments

Comments
 (0)