diff --git a/extensions/servlet/src/main/java/com/stormpath/sdk/servlet/tenant/DefaultTenantResolver.java b/extensions/servlet/src/main/java/com/stormpath/sdk/servlet/tenant/DefaultTenantResolver.java new file mode 100644 index 0000000000..ce14d5fcf2 --- /dev/null +++ b/extensions/servlet/src/main/java/com/stormpath/sdk/servlet/tenant/DefaultTenantResolver.java @@ -0,0 +1,81 @@ +/* + * Copyright 2016 Stormpath, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.stormpath.sdk.servlet.tenant; + +import com.stormpath.sdk.accountStoreMapping.AccountStoreMapping; +import com.stormpath.sdk.application.Application; +import com.stormpath.sdk.application.ApplicationAccountStoreMappingList; +import com.stormpath.sdk.directory.AccountStore; +import com.stormpath.sdk.directory.AccountStoreVisitor; +import com.stormpath.sdk.directory.Directory; +import com.stormpath.sdk.group.Group; +import com.stormpath.sdk.organization.Organization; +import com.stormpath.sdk.servlet.application.ApplicationResolver; +import com.stormpath.sdk.servlet.http.Resolver; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @since 1.0.0 + */ +public class DefaultTenantResolver implements TenantResolver { + + Resolver organizationNameKeyResolver; + ApplicationResolver applicationResolver = ApplicationResolver.INSTANCE; + + public void setOrganizationNameKeyResolver(Resolver organizationNameKeyResolver) { + this.organizationNameKeyResolver = organizationNameKeyResolver; + } + + public Organization get(HttpServletRequest request, HttpServletResponse response) { + + final String domainName = organizationNameKeyResolver.get(request, response); + if (domainName == null) { + return null; + } + + Application application = applicationResolver.getApplication(request); + ApplicationAccountStoreMappingList accountStoreMappings = application.getAccountStoreMappings(); + final Organization organization[] = {null}; + for (AccountStoreMapping accountStoreMapping : accountStoreMappings) { + final AccountStore accountStore = accountStoreMapping.getAccountStore(); + + accountStore.accept(new AccountStoreVisitor() { + @Override + public void visit(Group group) { + //no-op + } + + @Override + public void visit(Directory directory) { + //no-op + } + + @Override + public void visit(Organization org) { + if (domainName.equals(org.getName())) { + organization[0] = org; + } + } + }); + + if (organization[0] != null) break; + } + return organization[0]; + } + +} diff --git a/extensions/servlet/src/main/java/com/stormpath/sdk/servlet/tenant/DefaultTenantResolverFactory.java b/extensions/servlet/src/main/java/com/stormpath/sdk/servlet/tenant/DefaultTenantResolverFactory.java new file mode 100644 index 0000000000..9f240c1613 --- /dev/null +++ b/extensions/servlet/src/main/java/com/stormpath/sdk/servlet/tenant/DefaultTenantResolverFactory.java @@ -0,0 +1,36 @@ +/* + * Copyright 2016 Stormpath, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.stormpath.sdk.servlet.tenant; + +import com.stormpath.sdk.organization.Organization; +import com.stormpath.sdk.servlet.config.ConfigSingletonFactory; +import com.stormpath.sdk.servlet.http.Resolver; +import com.stormpath.sdk.servlet.organization.DefaultOrganizationNameKeyResolver; + +import javax.servlet.ServletContext; + +/** + * @since 1.0.0 + */ +public class DefaultTenantResolverFactory extends ConfigSingletonFactory> { + + protected Resolver createInstance(ServletContext servletContext) throws Exception { + DefaultTenantResolver defaultTenantResolver = new DefaultTenantResolver(); + DefaultOrganizationNameKeyResolver organizationNameKeyResolver = new DefaultOrganizationNameKeyResolver(); + defaultTenantResolver.setOrganizationNameKeyResolver(organizationNameKeyResolver); + return defaultTenantResolver; + } +} diff --git a/extensions/servlet/src/main/java/com/stormpath/sdk/servlet/tenant/TenantResolver.java b/extensions/servlet/src/main/java/com/stormpath/sdk/servlet/tenant/TenantResolver.java new file mode 100644 index 0000000000..76ffa7fae7 --- /dev/null +++ b/extensions/servlet/src/main/java/com/stormpath/sdk/servlet/tenant/TenantResolver.java @@ -0,0 +1,30 @@ +/* + * Copyright 2016 Stormpath, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.stormpath.sdk.servlet.tenant; + +import com.stormpath.sdk.servlet.http.Resolver; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @since 1.0.0 + */ +public interface TenantResolver extends Resolver { + + T get(HttpServletRequest request, HttpServletResponse response); + +} diff --git a/extensions/servlet/src/main/resources/com/stormpath/sdk/servlet/config/web.stormpath.properties b/extensions/servlet/src/main/resources/com/stormpath/sdk/servlet/config/web.stormpath.properties index 460014f4ef..35b3a7a003 100644 --- a/extensions/servlet/src/main/resources/com/stormpath/sdk/servlet/config/web.stormpath.properties +++ b/extensions/servlet/src/main/resources/com/stormpath/sdk/servlet/config/web.stormpath.properties @@ -180,6 +180,7 @@ stormpath.web.idSite.OrganizationResolverFactory = com.stormpath.sdk.servlet.fil # Inferred based on heuristics by default. However if your application is not deployed to an apex domain, like # myapp.com, you *must* specify your application's base domain, e.g. myapp.mycompany.com stormpath.web.application.domain = +stormpath.web.application.tenant.resolver = com.stormpath.sdk.servlet.tenant.DefaultTenantResolverFactory stormpath.web.request.event.publisher = com.stormpath.sdk.servlet.event.impl.EventPublisherFactory stormpath.web.request.event.listener = com.stormpath.sdk.servlet.event.RequestEventListenerAdapter diff --git a/extensions/servlet/src/test/groovy/com/stormpath/sdk/servlet/config/SpecConfigVersusWebPropertiesTest.groovy b/extensions/servlet/src/test/groovy/com/stormpath/sdk/servlet/config/SpecConfigVersusWebPropertiesTest.groovy index 8f5bdb9376..a4983af53c 100644 --- a/extensions/servlet/src/test/groovy/com/stormpath/sdk/servlet/config/SpecConfigVersusWebPropertiesTest.groovy +++ b/extensions/servlet/src/test/groovy/com/stormpath/sdk/servlet/config/SpecConfigVersusWebPropertiesTest.groovy @@ -51,13 +51,7 @@ class SpecConfigVersusWebPropertiesTest { defaultProperties = new ResourcePropertiesSource(defaultConfig).properties } - /** - * NOTE: This test is temporarily disabled as 15 new properties have been added to the framework spec for - * multi-tenancy that are not yet implemented in the SDK. - * Per high priority ticket: https://github.com/stormpath/stormpath-sdk-java/issues/1033, - * Todo: this should be re-enabled and support for the new properties should be added asap - */ - @Test(enabled=false) + @Test void verifyPropertiesInSpecAreInDefault() { def diff = specProperties.findResults { k,v -> @@ -74,9 +68,9 @@ class SpecConfigVersusWebPropertiesTest { println "Or you could adjust the assertEquals statement in this method to allow for this missing key as a temporary solution." } - //todo: 15 new properties related to organizations were added to the spec, we do not yet suppor them. + //todo: 22 new properties related to organizations were added to the spec, we do not yet support them. //see https://github.com/stormpath/stormpath-sdk-java/issues/1052 - assertEquals 15, diff.size(), "Missing keys in default config: ${diff}" + assertEquals diff.size(), 22, "Missing keys in default config: ${diff}" } @Test @@ -85,7 +79,7 @@ class SpecConfigVersusWebPropertiesTest { specProperties.containsKey(k) ? null : k } - def expected_diff_size = 82 + def expected_diff_size = 83 if (diff.size != expected_diff_size) { println "It looks like a property was added or removed from the Framework Spec or web.stormpath.properties." @@ -95,20 +89,20 @@ class SpecConfigVersusWebPropertiesTest { assertEquals diff.size(), expected_diff_size, "Missing keys in spec config: ${diff}" // to see the keys missing in spec, uncomment the following - /*if (diff.size > 0) { + if (diff.size > 0) { println "Missing keys in spec:" diff.each { println "${it}" } - }*/ + } // to see the keys and their values for updating the wiki, uncomment the following // https://github.com/stormpath/stormpath-sdk-java/wiki/1.0-Configuration-Changes-&-Additions-Guide#not-in-specification - /* + SortedSet keys = new TreeSet(properties.keySet()); keys.each { println("|${it}|" + properties.get(it) + "|") - }*/ + } } @Test(enabled = false) diff --git a/extensions/servlet/src/test/groovy/com/stormpath/sdk/servlet/tenant/DefaultTenantResolverTest.groovy b/extensions/servlet/src/test/groovy/com/stormpath/sdk/servlet/tenant/DefaultTenantResolverTest.groovy new file mode 100644 index 0000000000..9a40a95c75 --- /dev/null +++ b/extensions/servlet/src/test/groovy/com/stormpath/sdk/servlet/tenant/DefaultTenantResolverTest.groovy @@ -0,0 +1,136 @@ +/* + * Copyright 2016 Stormpath, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.stormpath.sdk.servlet.tenant + +import com.stormpath.sdk.accountStoreMapping.AccountStoreMapping +import com.stormpath.sdk.application.Application +import com.stormpath.sdk.application.ApplicationAccountStoreMappingList +import com.stormpath.sdk.directory.AccountStoreVisitor +import com.stormpath.sdk.directory.Directory +import com.stormpath.sdk.group.Group +import com.stormpath.sdk.organization.Organization +import com.stormpath.sdk.servlet.organization.DefaultOrganizationNameKeyResolver +import com.stormpath.sdk.servlet.util.SubdomainResolver +import org.easymock.Capture +import org.easymock.IAnswer +import org.testng.annotations.Test + +import javax.servlet.http.HttpServletRequest + +import static org.easymock.EasyMock.* +import static org.testng.Assert.assertEquals + +/** + * @since 1.0.0 + */ +class DefaultTenantResolverTest { + + @Test + void testOrganizationExistInAccountStores() { + def request = createMock(HttpServletRequest) + def application = createStrictMock(Application) + def applicationAccountStoreMappingList = createStrictMock(ApplicationAccountStoreMappingList) + def iterator = createMock(Iterator) + def accountStoreMapping = createMock(AccountStoreMapping) + def dir = createMock(Directory) + def organization = createMock(Organization) + + expect(request.getAttribute(Application.getCanonicalName())).andReturn(application) + expect(request.getHeader(eq('Host'))).andStubReturn('bar.foo.com') + expect(application.getAccountStoreMappings()).andReturn(applicationAccountStoreMappingList) + expect(applicationAccountStoreMappingList.iterator()).andReturn(iterator) + expect(iterator.hasNext()).andReturn(true) + expect(iterator.next()).andReturn(accountStoreMapping) + expect(accountStoreMapping.getAccountStore()).andReturn(dir) + expect(dir.accept(anyObject())).andVoid() + expect(iterator.hasNext()).andReturn(true) + expect(iterator.next()).andReturn(accountStoreMapping) + expect(accountStoreMapping.getAccountStore()).andReturn(organization) + + Capture capturedArgument = new Capture(); + expect(organization.accept(and(capture(capturedArgument), isA(AccountStoreVisitor)))).andAnswer( + new IAnswer() { + @Override + public AccountStoreVisitor answer() throws Throwable { + AccountStoreVisitor accountStoreVisitor = (AccountStoreVisitor) capturedArgument.getValue(); + accountStoreVisitor.visit(organization); + } + } + ) + expect(organization.getName()).andReturn("bar") //we return "bar" as the organization name found in account store mappings + + replay(request, application, applicationAccountStoreMappingList, iterator, accountStoreMapping, dir, organization) + + def organizationNameKeyResolver = new DefaultOrganizationNameKeyResolver(); + organizationNameKeyResolver.setSubdomainResolver(new SubdomainResolver()) + def resolver = new DefaultTenantResolver(); + resolver.setOrganizationNameKeyResolver(organizationNameKeyResolver) + + assertEquals(resolver.get(request, null), organization) //expected Organization is 'organization' + + verify(request, application, applicationAccountStoreMappingList, iterator, accountStoreMapping, dir, organization) + } + + @Test + void testOrganizationDoesNotExistInAccountStores() { + def request = createMock(HttpServletRequest) + def application = createStrictMock(Application) + def applicationAccountStoreMappingList = createStrictMock(ApplicationAccountStoreMappingList) + def iterator = createMock(Iterator) + def accountStoreMapping = createMock(AccountStoreMapping) + def group = createMock(Group) + + expect(request.getAttribute(Application.getCanonicalName())).andReturn(application) + expect(request.getHeader(eq('Host'))).andStubReturn('bar.foo.com') + expect(application.getAccountStoreMappings()).andReturn(applicationAccountStoreMappingList) + expect(applicationAccountStoreMappingList.iterator()).andReturn(iterator) + expect(iterator.hasNext()).andReturn(true) + expect(iterator.next()).andReturn(accountStoreMapping) + expect(accountStoreMapping.getAccountStore()).andReturn(group) + expect(group.accept(anyObject())).andVoid() + expect(iterator.hasNext()).andReturn(false) + + replay(request, application, applicationAccountStoreMappingList, iterator, accountStoreMapping, group) + + def organizationNameKeyResolver = new DefaultOrganizationNameKeyResolver(); + organizationNameKeyResolver.setSubdomainResolver(new SubdomainResolver()) + def resolver = new DefaultTenantResolver(); + resolver.setOrganizationNameKeyResolver(organizationNameKeyResolver) + + assertEquals(resolver.get(request, null), null) //expected Organization is null + + verify(request, application, applicationAccountStoreMappingList, iterator, accountStoreMapping, group) + } + + @Test + void testNoOrganizationDomain() { + def request = createMock(HttpServletRequest) + + expect(request.getHeader(eq('Host'))).andStubReturn('foo.com') //no organization found in url + + replay(request) + + def organizationNameKeyResolver = new DefaultOrganizationNameKeyResolver(); + organizationNameKeyResolver.setSubdomainResolver(new SubdomainResolver()) + def resolver = new DefaultTenantResolver(); + resolver.setOrganizationNameKeyResolver(organizationNameKeyResolver) + + assertEquals(resolver.get(request, null), null) + + verify(request) + } + +}