Skip to content
This repository was archived by the owner on Dec 12, 2018. It is now read-only.

Issue 1257 multi tenancy support for client api #1301

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,22 @@ public interface CreateAccountRequest {
*/
AccountOptions getAccountOptions() throws IllegalStateException;

/**
* Returns {@code true} if the request has specified an organizationNameKey
*
* @return {@code true} if the request has specified an organizationNameKey
*
* @since 1.6.0
*/
boolean isOrganizationNameKeySpecified();

/**
* Returns the organizationNameKey to be used in the CreateAccountRequest
*
* @return organizationNameKey to be used in the CreateAccountRequest
*
* @since 1.6.0
*/
String getOrganizationNameKey();

}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ public interface CreateAccountRequestBuilder {
*/
CreateAccountRequestBuilder withResponseOptions(AccountOptions options) throws IllegalArgumentException;

/**
* Ensures that the account will be created in the organization specified (given that it is mapped to the Application)
*
* @return the builder instance for method chaining.
*/
CreateAccountRequestBuilder withOrganizationNameKey(String organizationNameKey) throws IllegalArgumentException;

/**
* Creates a new {@code CreateAccountRequest} instance based on the current builder state.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public interface PasswordResetToken extends Resource {

String getEmail();

String getOrganizationNameKey();

Account getAccount();

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ public interface VerificationEmailRequest {
*/
String getLogin();

/**
* Returns the organizationNameKey.
*
* @return the organizationNameKey.
*/
String getOrganizationNameKey();

/**
* Returns the {@link AccountStore} set.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ public interface VerificationEmailRequestBuilder {
*/
VerificationEmailRequestBuilder setLogin(String usernameOrEmail);

/**
* Setter for the account's organizationNameKey information.
*
* @param organizationNameKey the organizationNameKey where the account lives.
* @return this builder instance for method chaining.
*/
VerificationEmailRequestBuilder setOrganizationNameKey(String organizationNameKey);

/**
* Setter for the {@link com.stormpath.sdk.directory.AccountStore} where the specified account must be searched. Although
* this is an optional property it should to be provided when the account's AccountStore is already known since it will
Expand Down
26 changes: 26 additions & 0 deletions api/src/main/java/com/stormpath/sdk/application/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,32 @@ public interface Application extends AccountStoreHolder<Application>, Resource,
*/
PasswordResetToken sendPasswordResetEmail(String email, AccountStore accountStore) throws ResourceException;

/**
* Sends a password reset email to an account in the specified {@code Organization} matching the specified
* {@code email} address. If the email does not match an account in the specified Organization, a
* ResourceException will be thrown. If you are unsure of which of the application's mapped account stores might
* contain the account, use the more general
* {@link #sendPasswordResetEmail(String) sendPasswordResetEmail(String email)} method instead.
*
* <p>This method is useful if you want to target a specific organization context for multi-tenancy purposes.</p>
*
* <p>Like the {@link #sendPasswordResetEmail(String)} method, this email merely sends the email that contains
* a link that, when clicked, will take the user to a view (web page) that allows them to specify a new password.
* When the new password is submitted, the {@link #verifyPasswordResetToken(String)} method is expected to be
* called at that time.</p>
*
* @param email an email address of an Account that may login to the application.
* @param organizationNameKey the organizationNameKey expected to contain an account with the specified email address
* @return the {@code PasswordResetToken} created for the password reset email sent to the specified {@code email}
* @see #sendPasswordResetEmail(String)
* @see #verifyPasswordResetToken(String)
* @see #resetPassword(String, String)
* @throws ResourceException if the specified AccountStore is not mapped to this application or if the email address
* is not in the specified Account store
* @since 1.6.0
*/
PasswordResetToken sendPasswordResetEmail(String email, String organizationNameKey) throws ResourceException;

/**
* Verifies a password reset token in a user-clicked link within an email.
* <p/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,11 @@ public interface OAuthPasswordGrantRequestAuthentication extends OAuthGrantReque
*/
AccountStore getAccountStore();

/**
* Returns the specific organizationNameKey where the provided credentials will be sought in order to authenticate a request.
*
* @return the specific organizationNameKey where the provided credentials will be sought in order to authenticate a request.
*/
String getOrganizationNameKey();

}
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,12 @@ public interface OAuthPasswordGrantRequestAuthenticationBuilder extends OAuthReq
* @return this instance for method chaining.
*/
OAuthPasswordGrantRequestAuthenticationBuilder setAccountStore(AccountStore accountStore);

/**
* Specifies the target Organization via nameKey to be used for the authentication token creation.
*
* @param organizationNameKey the sole specific nameKey of the {@link com.stormpath.sdk.organization.Organization organization} where the provided credentials will be sought in order to authenticate this request.
* @return this instance for method chaining.
*/
OAuthPasswordGrantRequestAuthenticationBuilder setOrganizationNameKey(String organizationNameKey);
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ public OAuthPasswordGrantRequestAuthentication createAccessTokenAuthenticationRe
requestBuilder.setAccountStore(accountStore);
}

if (request.getParameter("organizationNameKey") != null) {
requestBuilder.setOrganizationNameKey(request.getParameter("organizationNameKey"));
}

return requestBuilder.build();
} catch (Exception e){
throw new OAuthException(OAuthErrorCode.INVALID_REQUEST);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ protected ViewModel onValidSubmit(HttpServletRequest request, HttpServletRespons

VerificationEmailRequest verificationEmailRequest = Applications.verificationEmailBuilder()
.setLogin(login)
.setOrganizationNameKey(getFieldValueResolver().getValue(request, "organizationNameKey"))
.setAccountStore(accountStore)
.build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,26 @@
*/
package com.stormpath.sdk.servlet.mvc.provider;

import com.stormpath.sdk.accountStoreMapping.AccountStoreMapping;
import com.stormpath.sdk.application.Application;
import com.stormpath.sdk.application.ApplicationAccountStoreMapping;
import com.stormpath.sdk.application.ApplicationAccountStoreMappingCriteria;
import com.stormpath.sdk.application.ApplicationAccountStoreMappingList;
import com.stormpath.sdk.application.ApplicationAccountStoreMappings;
import com.stormpath.sdk.application.webconfig.ApplicationWebConfig;
import com.stormpath.sdk.application.webconfig.ApplicationWebConfigStatus;
import com.stormpath.sdk.directory.AccountStore;
import com.stormpath.sdk.directory.AccountStoreVisitor;
import com.stormpath.sdk.directory.AccountStoreVisitorAdapter;
import com.stormpath.sdk.directory.Directory;
import com.stormpath.sdk.group.Group;
import com.stormpath.sdk.lang.Strings;
import com.stormpath.sdk.organization.Organization;
import com.stormpath.sdk.organization.OrganizationAccountStoreMappingList;
import com.stormpath.sdk.provider.GoogleProvider;
import com.stormpath.sdk.provider.OAuthProvider;
import com.stormpath.sdk.provider.Provider;
import com.stormpath.sdk.provider.saml.SamlProvider;
import com.stormpath.sdk.resource.CollectionResource;
import com.stormpath.sdk.servlet.application.ApplicationResolver;

import javax.servlet.http.HttpServletRequest;
Expand All @@ -49,17 +55,25 @@ public class ExternalAccountStoreModelFactory implements AccountStoreModelFactor
public List<AccountStoreModel> getAccountStores(HttpServletRequest request) {

Application app = ApplicationResolver.INSTANCE.getApplication(request);
CollectionResource<? extends AccountStoreMapping> accountStoreMappings;

int pageSize = 100; //get as much as we can in a single request
ApplicationAccountStoreMappingCriteria criteria = ApplicationAccountStoreMappings.criteria().limitTo(pageSize);
ApplicationAccountStoreMappingList mappings = app.getAccountStoreMappings(criteria);
String onk = request.getParameter("organizationNameKey");
if (Strings.hasText(onk)) {
accountStoreMappings = getOrganizationAccountStoreMappings(app, onk);
} else {
accountStoreMappings = getApplicationAccountStoreMappings(app);
}

if (accountStoreMappings == null) {
return new ArrayList<>(); //maybe error if onk isn't found???
}

final List<AccountStoreModel> accountStores = new ArrayList<>(mappings.getSize());
final List<AccountStoreModel> accountStores = new ArrayList<>(accountStoreMappings.getSize());

AccountStoreModelVisitor visitor =
new AccountStoreModelVisitor(accountStores, getAuthorizeBaseUri(request, app.getWebConfig()));

for (ApplicationAccountStoreMapping mapping : mappings) {
for (AccountStoreMapping mapping : accountStoreMappings) {

final AccountStore accountStore = mapping.getAccountStore();

Expand All @@ -69,6 +83,42 @@ public List<AccountStoreModel> getAccountStores(HttpServletRequest request) {
return visitor.getAccountStores();
}

private ApplicationAccountStoreMappingList getApplicationAccountStoreMappings(Application app) {
int pageSize = 100; //get as much as we can in a single request
ApplicationAccountStoreMappingCriteria criteria = ApplicationAccountStoreMappings.criteria().limitTo(pageSize);
return app.getAccountStoreMappings(criteria);
}

private OrganizationAccountStoreMappingList getOrganizationAccountStoreMappings(Application app, final String nameKey) {
ApplicationAccountStoreMappingList accountStoreMappings = app.getAccountStoreMappings();

final Organization[] organization = new Organization[1];

for (final AccountStoreMapping accountStoreMapping : accountStoreMappings) {
AccountStore accountStore = accountStoreMapping.getAccountStore();
accountStore.accept(new AccountStoreVisitor() {
@Override
public void visit(Group group) {}

@Override
public void visit(Directory directory) {}

@Override
public void visit(Organization org) {
if (org.getNameKey().equals(nameKey)) {
organization[0] = org;
}
}
});
}

if (organization[0] == null) {
return null;
}

return organization[0].getAccountStoreMappings();
}

@SuppressWarnings("WeakerAccess") // Want to allow overriding this method
protected String getAuthorizeBaseUri(@SuppressWarnings("UnusedParameters") HttpServletRequest request, ApplicationWebConfig webConfig) {
String authorizeBaseUri = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ stormpath.web.register.form.fields.confirmPassword.label = stormpath.web.registe
stormpath.web.register.form.fields.confirmPassword.placeholder = stormpath.web.register.form.fields.confirmPassword.placeholder
stormpath.web.register.form.fields.confirmPassword.required = true
stormpath.web.register.form.fields.confirmPassword.type = password
stormpath.web.register.form.fields.organizationNameKey.enabled = true
stormpath.web.register.form.fields.organizationNameKey.visible = false
stormpath.web.register.form.fields.organizationNameKey.label = Organization Name Key
stormpath.web.register.form.fields.organizationNameKey.placeholder = Organization Name Key
stormpath.web.register.form.fields.organizationNameKey.required = false
stormpath.web.register.form.fields.organizationNameKey.type = text
stormpath.web.register.form.fieldOrder = username,givenName,middleName,surname,email,password,confirmPassword
stormpath.web.register.view = register
# If verify is enabled, the login view will show a link to to a page where users will be able to have the account
Expand All @@ -109,6 +115,12 @@ stormpath.web.verifyEmail.form.fields.email.label = stormpath.web.verifyEmail.fo
stormpath.web.verifyEmail.form.fields.email.placeholder = stormpath.web.verifyEmail.form.fields.email.placeholder
stormpath.web.verifyEmail.form.fields.email.required = true
stormpath.web.verifyEmail.form.fields.email.type = text
stormpath.web.verifyEmail.form.fields.organizationNameKey.enabled = true
stormpath.web.verifyEmail.form.fields.organizationNameKey.visible = false
stormpath.web.verifyEmail.form.fields.organizationNameKey.label = Organization Name Key
stormpath.web.verifyEmail.form.fields.organizationNameKey.placeholder = Organization Name Key
stormpath.web.verifyEmail.form.fields.organizationNameKey.required = false
stormpath.web.verifyEmail.form.fields.organizationNameKey.type = text

## default SAML SP initiated endpoint
stormpath.web.saml.uri = /saml
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class SpecConfigVersusWebPropertiesTest {
specProperties.containsKey(k) ? null : k
}

def expected_diff_size = 87
def expected_diff_size = 93

if (diff.size != expected_diff_size) {
println "It looks like a property was added or removed from the Framework Spec or web.stormpath.properties."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,16 @@ public class DefaultCreateAccountRequest implements CreateAccountRequest {

private final AccountOptions options;

private final String organizationNameKey;

private PasswordFormat passwordFormat;

public DefaultCreateAccountRequest(Account account, Boolean registrationWorkflowEnabled, AccountOptions options) {
public DefaultCreateAccountRequest(Account account, Boolean registrationWorkflowEnabled, AccountOptions options, String organizationNameKey) {
Assert.notNull(account, "Account cannot be null.");
this.account = account;
this.registrationWorkflowEnabled = registrationWorkflowEnabled;
this.options = options;
this.organizationNameKey = organizationNameKey;
}

public Account getAccount() {
Expand Down Expand Up @@ -88,4 +91,14 @@ public AccountOptions getAccountOptions() {
}
return this.options;
}

@Override
public boolean isOrganizationNameKeySpecified() {
return this.organizationNameKey != null;
}

@Override
public String getOrganizationNameKey() {
return organizationNameKey;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public class DefaultCreateAccountRequestBuilder implements CreateAccountRequestB
private Boolean registrationWorkflowEnabled;
private PasswordFormat passwordFormat;
private AccountOptions options;
private String organizationNameKey;

public DefaultCreateAccountRequestBuilder(Account account) {
Assert.notNull(account, "Account cannot be null.");
Expand Down Expand Up @@ -56,8 +57,14 @@ public CreateAccountRequestBuilder withResponseOptions(AccountOptions options) {
return this;
}

@Override
public CreateAccountRequestBuilder withOrganizationNameKey(String organizationNameKey) throws IllegalArgumentException {
this.organizationNameKey = organizationNameKey;
return this;
}

@Override
public CreateAccountRequest build() {
return new DefaultCreateAccountRequest(this.account, this.registrationWorkflowEnabled, this.options).setPasswordFormat(this.passwordFormat);
return new DefaultCreateAccountRequest(this.account, this.registrationWorkflowEnabled, this.options, this.organizationNameKey).setPasswordFormat(this.passwordFormat);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@ public class DefaultPasswordResetToken extends AbstractResource implements Passw

// SIMPLE PROPERTIES
static final StringProperty EMAIL = new StringProperty("email");
static final StringProperty ORGANIZATION_NAME_KEY = new StringProperty("organizationNameKey");
static final StringProperty PASSWORD = new StringProperty("password");

// INSTANCE RESOURCE REFERENCES:
static final ResourceReference<Account> ACCOUNT = new ResourceReference<Account>("account", Account.class);
static final ResourceReference<AccountStore> ACCOUNT_STORE = new ResourceReference<AccountStore>("accountStore", AccountStore.class);

private static final Map<String, Property> PROPERTY_DESCRIPTORS = createPropertyDescriptorMap(EMAIL, ACCOUNT_STORE, PASSWORD, ACCOUNT);
private static final Map<String, Property> PROPERTY_DESCRIPTORS = createPropertyDescriptorMap(EMAIL, ACCOUNT_STORE, PASSWORD, ACCOUNT, ORGANIZATION_NAME_KEY);

public DefaultPasswordResetToken(InternalDataStore dataStore) {
super(dataStore);
Expand All @@ -66,6 +67,16 @@ public PasswordResetToken setEmail(String email) {
return this;
}

@Override
public String getOrganizationNameKey() {
return getString(ORGANIZATION_NAME_KEY);
}

public PasswordResetToken setOrganizationNameKey(String organizationNameKey) {
setProperty(ORGANIZATION_NAME_KEY, organizationNameKey);
return this;
}

@Override
public Account getAccount() {
return getResourceProperty(ACCOUNT);
Expand Down
Loading