Skip to content

Commit

Permalink
Changed invite behaviors to return invite link instead of sending an
Browse files Browse the repository at this point in the history
email
- updated docs
- removed inviteUsers from invitationsService
- removed redundant tests

[#103822132] https://www.pivotaltracker.com/story/show/103822132

Signed-off-by: Jonathan Lo <jlo@us.ibm.com>
  • Loading branch information
Paul Warren authored and jlo committed Oct 5, 2015
1 parent 4fc803a commit fe53fe5
Show file tree
Hide file tree
Showing 14 changed files with 278 additions and 420 deletions.
14 changes: 7 additions & 7 deletions docs/UAA-APIs.rst
Expand Up @@ -39,8 +39,8 @@ Here is a summary of the different scopes that are known to the UAA.
* **scim.read** - Admin read access to all SCIM endpoints, ``/Users``, ``/Groups/``.
* **scim.create** - Reduced scope to be able to create a user using ``POST /Users`` (and verify their account using ``GET /Users/{id}/verify``) but not be able to modify, read or delete users.
* **scim.userids** - ``/ids/Users`` - Required to convert a username+origin to a user ID and vice versa.
* **scim.zones** - limited scope that only allows adding/removing a user to/from `zone management groups`_ under the path /Groups/zones
* **scim.invite** - Scope required to perform email invitations at ``/invite_users``
* **scim.zones** - Limited scope that only allows adding/removing a user to/from `zone management groups`_ under the path /Groups/zones
* **scim.invite** - Scope required by a client in order to participate in invitations using the ``/invite_users`` endpoint.
* **password.write** - ``/User*/*/password`` endpoint. Admin scope to change a user's password.
* **oauth.approval** - ``/approvals`` endpoint. Scope required to be able to approve/disapprove clients to act on a user's behalf. This is a default scope defined in uaa.yml.
* **oauth.login** - Scope used to indicate a login application, such as external login servers, to perform trusted operations, such as create users not authenticated in the UAA.
Expand Down Expand Up @@ -1703,11 +1703,11 @@ ENDPOINT DEPRECATED - Will always return score:0 and requiredScore:0
Inviting Users
--------------

The UAA supports the notion of inviting users. When a user is invited provided an email address, the system will
The UAA supports the notion of inviting users. When a user is invited by providing an email address, the system will
locate the appropriate authentication provider and create the user account.
The invitation endpoint then return the corresponding `user_id` for the email.
The invitation endpoint then returns the corresponding `user_id` and `inviteLink`.
Batch processing is allowed by specifying more than one email address.
The endpoint takes two parameters, a client_id and a redirect_uri.
The endpoint takes two parameters, a client_id (optional) and a redirect_uri.
When a user accepts the invitation, the user will be redirected to the redirect_uri.
The redirect_uri will be validated against allowed redirect_uri for the client.

Expand All @@ -1729,8 +1729,8 @@ The redirect_uri will be validated against allowed redirect_uri for the client.

{
"new_invites":[
{"email":"user1@cqv4f7.com","userId":"38de0ac4-b194-4e33-b6c2-0755a37205fb","origin":"uaa","success":true,"errorCode":null,"errorMessage":null},
{"email":"user2@cqv4f7.com","userId":"1665631f-1957-44fe-ac49-2739dd55bb3f","origin":"uaa","success":true,"errorCode":null,"errorMessage":null}
{"email":"user1@cqv4f7.com","userId":"38de0ac4-b194-4e33-b6c2-0755a37205fb","origin":"uaa","success":true,"inviteLink":"http://myuaa.cloudfoundry.com/invitations/accept?code=yuT6rd","errorCode":null,"errorMessage":null},
{"email":"user2@cqv4f7.com","userId":"1665631f-1957-44fe-ac49-2739dd55bb3f","origin":"uaa","success":true,"inviteLink":"http://myuaa.cloudfoundry.com/invitations/accept?code=yuT6rd","errorCode":null,"errorMessage":null}
],
"failed_invites":[]
}
Expand Down
@@ -0,0 +1,18 @@
package org.cloudfoundry.identity.uaa.invitations;

/*******************************************************************************
* Cloud Foundry
* Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved.
* <p>
* This product is licensed to you under the Apache License, Version 2.0 (the "License").
* You may not use this product except in compliance with the License.
* <p>
* This product includes a number of subcomponents with
* separate copyright notices and license terms. Your use of these
* subcomponents is subject to the terms and conditions of the
* subcomponent's license, as noted in the LICENSE file.
*******************************************************************************/
public interface InvitationConstants {
static final String USER_ID = "user_id";
static final String EMAIL = "email";
}
@@ -1,12 +1,14 @@
package org.cloudfoundry.identity.uaa.invitations;

import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal;
import org.cloudfoundry.identity.uaa.codestore.ExpiringCode;
import org.cloudfoundry.identity.uaa.codestore.ExpiringCodeStore;
import org.cloudfoundry.identity.uaa.error.UaaException;
import org.cloudfoundry.identity.uaa.invitations.InvitationsResponse.InvitedUser;
import org.cloudfoundry.identity.uaa.scim.ScimUser;
import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning;
import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceConflictException;
import org.cloudfoundry.identity.uaa.util.DomainFilter;
import org.cloudfoundry.identity.uaa.util.JsonUtils;
import org.cloudfoundry.identity.uaa.zone.IdentityProvider;
import org.cloudfoundry.identity.uaa.zone.IdentityProviderProvisioning;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder;
Expand All @@ -23,52 +25,55 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import java.net.MalformedURLException;
import java.net.URL;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.cloudfoundry.identity.uaa.authentication.Origin.ORIGIN;
import static org.springframework.security.oauth2.common.util.OAuth2Utils.CLIENT_ID;
import static org.springframework.security.oauth2.common.util.OAuth2Utils.REDIRECT_URI;

@Controller
public class InvitationsEndpoint {

private InvitationsService invitationsService;
public static final int INVITATION_EXPIRY_DAYS = 7;

private ScimUserProvisioning users;
private IdentityProviderProvisioning providers;
private ClientDetailsService clients;
private ExpiringCodeStore expiringCodeStore;

public InvitationsEndpoint(InvitationsService invitationsService,
ScimUserProvisioning users,
public InvitationsEndpoint(ScimUserProvisioning users,
IdentityProviderProvisioning providers,
ClientDetailsService clients) {
this.invitationsService = invitationsService;
ClientDetailsService clients,
ExpiringCodeStore expiringCodeStore) {
this.users = users;
this.providers = providers;
this.clients = clients;
this.expiringCodeStore = expiringCodeStore;
}

@RequestMapping(value="/invite_users", method= RequestMethod.POST, consumes="application/json")
public ResponseEntity<InvitationsResponse> inviteUsers(@RequestBody InvitationsRequest invitations,
@RequestParam(value="client_id") String clientId,
@RequestParam(value="client_id", required=false) String clientId,
@RequestParam(value="redirect_uri") String redirectUri) {

// todo: get clientId from token, if not supplied in clientId

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String currentUser = null;
if (authentication instanceof OAuth2Authentication) {
OAuth2Authentication oAuth2Authentication = (OAuth2Authentication)authentication;
if (!oAuth2Authentication.isClientOnly()) {
currentUser = ((UaaPrincipal) oAuth2Authentication.getPrincipal()).getName();
} else {
currentUser = oAuth2Authentication.getOAuth2Request().getClientId();
}

if (clientId==null) {
clientId = oAuth2Authentication.getOAuth2Request().getClientId();
}
}

InvitationsResponse invitationsResponse = new InvitationsResponse();
List<String> newInvitesEmails = new ArrayList<>();

DomainFilter filter = new DomainFilter();
List<IdentityProvider> activeProviders = providers.retrieveActive(IdentityZoneHolder.get().getId());
Expand All @@ -78,8 +83,25 @@ public ResponseEntity<InvitationsResponse> inviteUsers(@RequestBody InvitationsR
List<IdentityProvider> providers = filter.filter(activeProviders, client, email);
if (providers.size() == 1) {
ScimUser user = findOrCreateUser(email, providers.get(0).getOriginKey());
invitationsService.inviteUser(user, currentUser, clientId, redirectUri);
invitationsResponse.getNewInvites().add(InvitationsResponse.success(user.getPrimaryEmail(), user.getId(), user.getOrigin()));

String accountsUrl = ServletUriComponentsBuilder.fromCurrentContextPath().path("/invitations/accept").build().toUriString();

Map<String,String> data = new HashMap<>();
data.put(InvitationConstants.USER_ID, user.getId());
data.put(InvitationConstants.EMAIL, user.getPrimaryEmail());
data.put(CLIENT_ID, clientId);
data.put(REDIRECT_URI, redirectUri);
data.put(ORIGIN, user.getOrigin());
Timestamp expiry = new Timestamp(System.currentTimeMillis()+ (INVITATION_EXPIRY_DAYS * 24 * 60 * 60 * 1000));
ExpiringCode code = expiringCodeStore.generateCode(JsonUtils.writeValueAsString(data), expiry);

String invitationLink = accountsUrl + "?code=" + code.getCode();
try {
URL inviteLink = new URL(invitationLink);
invitationsResponse.getNewInvites().add(InvitationsResponse.success(user.getPrimaryEmail(), user.getId(), user.getOrigin(), inviteLink));
} catch (MalformedURLException mue){
invitationsResponse.getFailedInvites().add(InvitationsResponse.failure(email, "invitation.exception.url", String.format("Malformed url", invitationLink)));
}
} else if (providers.size() == 0) {
invitationsResponse.getFailedInvites().add(InvitationsResponse.failure(email, "provider.non-existent", "No authentication provider found."));
} else {
Expand Down
Expand Up @@ -2,61 +2,70 @@

import com.fasterxml.jackson.annotation.JsonProperty;

import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class InvitationsResponse {

@JsonProperty(value="new_invites")
private List<InvitedUser> newInvites = new ArrayList<>();
private List<Invitee> newInvites = new ArrayList<>();
@JsonProperty(value="new_invite_links")
private List<Invitee> newInviteLinks = new ArrayList<>();
@JsonProperty(value="failed_invites")
private List<InvitedUser> failedInvites = new ArrayList<>();
private List<Invitee> failedInvites = new ArrayList<>();

public InvitationsResponse() {}

public List<InvitedUser> getNewInvites() {
public List<Invitee> getNewInvites() {
return newInvites;
}

public void setNewInvites(List<InvitedUser> newInvites) {
public void setNewInvites(List<Invitee> newInvites) {
this.newInvites = newInvites;
}

public List<InvitedUser> getFailedInvites() {
public List<Invitee> getFailedInvites() {
return failedInvites;
}

public void setFailedInvites(List<InvitedUser> failedInvites) {
public void setFailedInvites(List<Invitee> failedInvites) {
this.failedInvites = failedInvites;
}

public static InvitedUser failure(String email, String errorCode, String errorMessage) {
InvitedUser user = new InvitedUser();
public static Invitee failure(String email, String errorCode, String errorMessage) {
Invitee user = new Invitee();
user.email = email;
user.errorCode = errorCode;
user.errorMessage = errorMessage;
user.success = false;
return user;
}

public static InvitedUser success(String email, String userId, String origin) {
InvitedUser user = new InvitedUser();
public static Invitee success(String email, String userId, String origin, URL inviteLink) {
Invitee user = new Invitee();
user.email = email;
user.userId = userId;
user.origin = origin;
user.success = true;
user.inviteLink = inviteLink;
return user;
}

public static class InvitedUser {
public static class Invitee {
private String email;
private String userId;
private String origin;
private boolean success;
private String errorCode;
private String errorMessage;

public InvitedUser() {
private URL inviteLink;

public Invitee() {
}

public String getEmail() {
Expand Down Expand Up @@ -106,6 +115,11 @@ public String getErrorMessage() {
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}

public URL getInviteLink() { return inviteLink; }

public void setInviteLink(URL inviteLink) { this.inviteLink = inviteLink; }

}

}
Expand Up @@ -4,8 +4,6 @@

public interface InvitationsService {

void inviteUser(ScimUser user, String currentUser, String clientId, String redirectUri);

AcceptedInvitation acceptInvitation(String code, String password);

class AcceptedInvitation {
Expand Down
Expand Up @@ -13,7 +13,6 @@
import org.cloudfoundry.identity.uaa.util.JsonUtils;
import org.cloudfoundry.identity.uaa.util.UaaUrlUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.common.util.OAuth2Utils;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.NoSuchClientException;
Expand All @@ -24,13 +23,10 @@
import org.thymeleaf.context.Context;
import org.thymeleaf.spring4.SpringTemplateEngine;

import java.io.IOException;
import java.sql.Timestamp;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import static org.cloudfoundry.identity.uaa.authentication.Origin.ORIGIN;
import static org.springframework.security.oauth2.common.util.OAuth2Utils.CLIENT_ID;
Expand Down Expand Up @@ -91,20 +87,6 @@ private String getEmailHtml(String currentUser, String code) {
return templateEngine.process("invite", ctx);
}

@Override
public void inviteUser(ScimUser user, String currentUser, String clientId, String redirectUri) {
String email = user.getPrimaryEmail();
Map<String,String> data = new HashMap<>();
data.put(USER_ID, user.getId());
data.put(EMAIL, email);
data.put(CLIENT_ID, clientId);
data.put(REDIRECT_URI, redirectUri);
data.put(ORIGIN, user.getOrigin());
Timestamp expiry = new Timestamp(System.currentTimeMillis()+ (INVITATION_EXPIRY_DAYS * 24 * 60 * 60 * 1000));
ExpiringCode code = expiringCodeStore.generateCode(JsonUtils.writeValueAsString(data), expiry);
sendInvitationEmail(email, currentUser, code.getCode());
}

@Override
public AcceptedInvitation acceptInvitation(String code, String password) {
ExpiringCode data = expiringCodeStore.retrieveCode(code);
Expand Down
2 changes: 1 addition & 1 deletion login/src/main/resources/login-ui.xml
Expand Up @@ -427,9 +427,9 @@

<bean name="invitationsEndpoint" class="org.cloudfoundry.identity.uaa.invitations.InvitationsEndpoint">
<constructor-arg name="users" ref="scimUserProvisioning"/>
<constructor-arg name="invitationsService" ref="invitationsService"/>
<constructor-arg name="providers" ref="identityProviderProvisioning"/>
<constructor-arg name="clients" ref="jdbcClientDetailsService"/>
<constructor-arg name="expiringCodeStore" ref="codeStore"/>
</bean>

<mvc:resources mapping="/resources/**" location="/resources/" />
Expand Down

0 comments on commit fe53fe5

Please sign in to comment.