From c1922e8068e8c48c803ebe3252e858783db8054a Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 12 Mar 2025 11:25:46 +0100 Subject: [PATCH] bug6101: added persistence to CORS filter configuration --- .../SecurityBundleTestWrapper.java | 4 +- .../test/AbstractSecurityPersistenceTest.java | 20 +++++ ...RSFilterConfigurationsPersistenceTest.java | 37 +++++++++ .../test/SessionPersistenceTest.java | 18 +---- .../persistence/DomainObjectFactory.java | 11 +++ .../persistence/MongoObjectFactory.java | 12 ++- .../persistence/impl/CollectionNames.java | 2 +- .../impl/DomainObjectFactoryImpl.java | 17 +++++ .../security/persistence/impl/FieldNames.java | 4 +- .../impl/MongoObjectFactoryImpl.java | 26 +++++++ .../test/AbstractSecurityReplicationTest.java | 8 +- ...licationLeadingToEmailReplicationTest.java | 8 +- .../test/SecurityServiceInitialLoadTest.java | 4 +- .../jaxrs/api/SecurityResourceTest.java | 6 +- .../com/sap/sse/security/test/LoginTest.java | 4 +- .../test/PrivilegeEscalationTest.java | 4 +- ...yServiceAndHasPermissionsProviderTest.java | 8 +- .../com/sap/sse/security/SecurityService.java | 20 +++++ .../com/sap/sse/security/impl/Activator.java | 8 +- .../impl/ReplicableSecurityService.java | 3 + .../security/impl/SecurityServiceImpl.java | 76 ++++++++++++++++--- 21 files changed, 244 insertions(+), 56 deletions(-) create mode 100644 java/com.sap.sse.security.persistence.test/src/com/sap/sse/security/persistence/test/AbstractSecurityPersistenceTest.java create mode 100644 java/com.sap.sse.security.persistence.test/src/com/sap/sse/security/persistence/test/CORSFilterConfigurationsPersistenceTest.java diff --git a/java/com.sap.sailing.server.testsupport/src/com/sap/sailing/server/testsupport/SecurityBundleTestWrapper.java b/java/com.sap.sailing.server.testsupport/src/com/sap/sailing/server/testsupport/SecurityBundleTestWrapper.java index 0160a08144d..89af9b9170d 100755 --- a/java/com.sap.sailing.server.testsupport/src/com/sap/sailing/server/testsupport/SecurityBundleTestWrapper.java +++ b/java/com.sap.sailing.server.testsupport/src/com/sap/sailing/server/testsupport/SecurityBundleTestWrapper.java @@ -25,8 +25,8 @@ public class SecurityBundleTestWrapper { store.ensureServerGroupExists(); final AccessControlStoreImpl accessControlStoreImpl = new AccessControlStoreImpl(store); Activator.setTestStores(store, accessControlStoreImpl); - securityService = new SecurityServiceImpl(/* mailServiceTracker */ null, store, accessControlStoreImpl, - new MockedHasPermissionProvider(), SSESubscriptionPlan::getAllInstances); + securityService = new SecurityServiceImpl(/* mailServiceTracker */ null, /* corsFilterConfigurationTracker */ null, store, + accessControlStoreImpl, new MockedHasPermissionProvider(), SSESubscriptionPlan::getAllInstances); ((SecurityServiceImpl) securityService).clearState(); securityService.initialize(); SecurityUtils.setSecurityManager(securityService.getSecurityManager()); diff --git a/java/com.sap.sse.security.persistence.test/src/com/sap/sse/security/persistence/test/AbstractSecurityPersistenceTest.java b/java/com.sap.sse.security.persistence.test/src/com/sap/sse/security/persistence/test/AbstractSecurityPersistenceTest.java new file mode 100644 index 00000000000..8757c0aa410 --- /dev/null +++ b/java/com.sap.sse.security.persistence.test/src/com/sap/sse/security/persistence/test/AbstractSecurityPersistenceTest.java @@ -0,0 +1,20 @@ +package com.sap.sse.security.persistence.test; + +import org.junit.Before; + +import com.sap.sse.mongodb.MongoDBService; +import com.sap.sse.security.persistence.DomainObjectFactory; +import com.sap.sse.security.persistence.MongoObjectFactory; +import com.sap.sse.security.persistence.PersistenceFactory; + +public abstract class AbstractSecurityPersistenceTest { + protected DomainObjectFactory dof; + protected MongoObjectFactory mof; + + @Before + public void setUp() { + MongoDBService.INSTANCE.getDB().drop(); + dof = PersistenceFactory.INSTANCE.getDefaultDomainObjectFactory(); + mof = PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory(); + } +} diff --git a/java/com.sap.sse.security.persistence.test/src/com/sap/sse/security/persistence/test/CORSFilterConfigurationsPersistenceTest.java b/java/com.sap.sse.security.persistence.test/src/com/sap/sse/security/persistence/test/CORSFilterConfigurationsPersistenceTest.java new file mode 100644 index 00000000000..5d8f5227cf2 --- /dev/null +++ b/java/com.sap.sse.security.persistence.test/src/com/sap/sse/security/persistence/test/CORSFilterConfigurationsPersistenceTest.java @@ -0,0 +1,37 @@ +package com.sap.sse.security.persistence.test; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.junit.Test; + +import com.sap.sse.common.Util; +import com.sap.sse.common.Util.Pair; + +public class CORSFilterConfigurationsPersistenceTest extends AbstractSecurityPersistenceTest { + @Test + public void testStoringAndLoadingCORSFilterWithWildcard() { + final Map>> configs = new HashMap<>(); + final String TESTSERVER = "testserver"; + configs.put(TESTSERVER, new Pair<>(true, Collections.emptySet())); + mof.storeCORSFilterConfigurationIsWildcard(TESTSERVER); + assertEquals(configs, dof.loadCORSFilterConfigurationsForReplicaSetNames()); + } + + @Test + public void testStoringAndLoadingCORSFilterOneWithWildcardAnotherWithOriginList() { + final Map>> configs = new HashMap<>(); + final String TESTSERVER1 = "testserver1"; + final String TESTSERVER2 = "testserver2"; + configs.put(TESTSERVER1, new Pair<>(true, Collections.emptySet())); + configs.put(TESTSERVER2, new Pair<>(false, Util.asSet(Arrays.asList("https://www.example.com", "https://www.anotherexample.de")))); + mof.storeCORSFilterConfigurationIsWildcard(TESTSERVER1); + mof.storeCORSFilterConfigurationAllowedOrigins(TESTSERVER2, Util.toArray(configs.get(TESTSERVER2).getB(), new String[0])); + assertEquals(configs, dof.loadCORSFilterConfigurationsForReplicaSetNames()); + } +} diff --git a/java/com.sap.sse.security.persistence.test/src/com/sap/sse/security/persistence/test/SessionPersistenceTest.java b/java/com.sap.sse.security.persistence.test/src/com/sap/sse/security/persistence/test/SessionPersistenceTest.java index 1644556aa8a..71c3c5bd916 100755 --- a/java/com.sap.sse.security.persistence.test/src/com/sap/sse/security/persistence/test/SessionPersistenceTest.java +++ b/java/com.sap.sse.security.persistence.test/src/com/sap/sse/security/persistence/test/SessionPersistenceTest.java @@ -12,25 +12,9 @@ import org.apache.shiro.session.Session; import org.apache.shiro.session.mgt.SimpleSession; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.SimplePrincipalCollection; -import org.junit.Before; import org.junit.Test; -import com.sap.sse.mongodb.MongoDBService; -import com.sap.sse.security.persistence.DomainObjectFactory; -import com.sap.sse.security.persistence.MongoObjectFactory; -import com.sap.sse.security.persistence.PersistenceFactory; - -public class SessionPersistenceTest { - private DomainObjectFactory dof; - private MongoObjectFactory mof; - - @Before - public void setUp() { - MongoDBService.INSTANCE.getDB().drop(); - dof = PersistenceFactory.INSTANCE.getDefaultDomainObjectFactory(); - mof = PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory(); - } - +public class SessionPersistenceTest extends AbstractSecurityPersistenceTest { @Test public void testLoadAndStoreAndRemoveSimpleSession() { final String cacheName = "shiroSessionCache"; diff --git a/java/com.sap.sse.security.persistence/src/com/sap/sse/security/persistence/DomainObjectFactory.java b/java/com.sap.sse.security.persistence/src/com/sap/sse/security/persistence/DomainObjectFactory.java index 6c8f53c8171..7202c01ecc2 100755 --- a/java/com.sap.sse.security.persistence/src/com/sap/sse/security/persistence/DomainObjectFactory.java +++ b/java/com.sap.sse.security.persistence/src/com/sap/sse/security/persistence/DomainObjectFactory.java @@ -5,6 +5,17 @@ import java.util.Set; import org.apache.shiro.session.Session; +import com.sap.sse.common.Util.Pair; + public interface DomainObjectFactory { Map> loadSessionsByCacheName(); + + /** + * Loads the CORS filter configurations; as we cannot reference the {@code CORSFilterConfiguration} interface from + * here without introducing a cyclic dependency, we return the configuration data in the form of a {@link Pair} + * whose {@link Pair#getA() first} component is a boolean telling whether the filter uses the "wildcard" (*) to + * allow REST requests from all possible origins, and the {@link Pair#getB() second} component lists the allowed + * origins in case it's not a wildcard configuration. For wildcard configurations, the second component is ignored. + */ + Map>> loadCORSFilterConfigurationsForReplicaSetNames(); } diff --git a/java/com.sap.sse.security.persistence/src/com/sap/sse/security/persistence/MongoObjectFactory.java b/java/com.sap.sse.security.persistence/src/com/sap/sse/security/persistence/MongoObjectFactory.java index 80c0b25889b..a0a3ffc1375 100755 --- a/java/com.sap.sse.security.persistence/src/com/sap/sse/security/persistence/MongoObjectFactory.java +++ b/java/com.sap.sse.security.persistence/src/com/sap/sse/security/persistence/MongoObjectFactory.java @@ -9,5 +9,15 @@ public interface MongoObjectFactory { void removeSession(String cacheName, Session session); void removeAllSessions(String cacheName); - + + void storeCORSFilterConfigurationIsWildcard(String serverName); + + /** + * Makes the CORS filter configuration for the application replica set identified by {@code serverName} + * a non-wildcard configuration that accepts REST requests only from those origins listed in + * {@code allowedOrigins}. + * + * @param allowedOrigins {@code null} is allowed and handled like an empty array + */ + void storeCORSFilterConfigurationAllowedOrigins(String serverName, String... allowedOrigins); } diff --git a/java/com.sap.sse.security.persistence/src/com/sap/sse/security/persistence/impl/CollectionNames.java b/java/com.sap.sse.security.persistence/src/com/sap/sse/security/persistence/impl/CollectionNames.java index 2506e94b855..580d434bf9d 100755 --- a/java/com.sap.sse.security.persistence/src/com/sap/sse/security/persistence/impl/CollectionNames.java +++ b/java/com.sap.sse.security.persistence/src/com/sap/sse/security/persistence/impl/CollectionNames.java @@ -1,5 +1,5 @@ package com.sap.sse.security.persistence.impl; public enum CollectionNames { - CACHES, SESSIONS; + CACHES, SESSIONS, CORS_FILTER_CONFIGURATIONS; } diff --git a/java/com.sap.sse.security.persistence/src/com/sap/sse/security/persistence/impl/DomainObjectFactoryImpl.java b/java/com.sap.sse.security.persistence/src/com/sap/sse/security/persistence/impl/DomainObjectFactoryImpl.java index 6602dac0ccf..d7c438b0cfb 100755 --- a/java/com.sap.sse.security.persistence/src/com/sap/sse/security/persistence/impl/DomainObjectFactoryImpl.java +++ b/java/com.sap.sse.security.persistence/src/com/sap/sse/security/persistence/impl/DomainObjectFactoryImpl.java @@ -1,8 +1,10 @@ package com.sap.sse.security.persistence.impl; import java.io.Serializable; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Logger; @@ -15,6 +17,7 @@ import org.bson.Document; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; import com.sap.sse.common.Util; +import com.sap.sse.common.Util.Pair; import com.sap.sse.common.impl.MillisecondsDurationImpl; import com.sap.sse.common.impl.MillisecondsTimePoint; import com.sap.sse.security.persistence.DomainObjectFactory; @@ -22,9 +25,11 @@ import com.sap.sse.security.persistence.DomainObjectFactory; public class DomainObjectFactoryImpl implements DomainObjectFactory { private static final Logger logger = Logger.getLogger(DomainObjectFactoryImpl.class.getName()); private final MongoCollection sessionsCollection; + private final MongoCollection corsFilterConfigurationsCollection; public DomainObjectFactoryImpl(MongoDatabase mongoDatabase) { sessionsCollection = mongoDatabase.getCollection(CollectionNames.SESSIONS.name()); + corsFilterConfigurationsCollection = mongoDatabase.getCollection(CollectionNames.CORS_FILTER_CONFIGURATIONS.name()); } @Override @@ -84,4 +89,16 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory { } return result; } + + @Override + public Map>> loadCORSFilterConfigurationsForReplicaSetNames() { + final Map>> result = new HashMap<>(); + for (final Document d : corsFilterConfigurationsCollection.find()) { + final String serverName = d.getString(FieldNames.CORS_FILTER_CONFIGURATION_SERVER_NAME.name()); + final boolean isWildcard = d.getBoolean(FieldNames.CORS_FILTER_CONFIGURATION_IS_WILDCARD.name()); + final List allowedOrigins = isWildcard ? Collections.emptyList() : d.getList(FieldNames.CORS_FILTER_CONFIGURATION_ALLOWED_ORIGINS.name(), String.class); + result.put(serverName, new Pair<>(isWildcard, Util.asNewSet(allowedOrigins))); + } + return result; + } } diff --git a/java/com.sap.sse.security.persistence/src/com/sap/sse/security/persistence/impl/FieldNames.java b/java/com.sap.sse.security.persistence/src/com/sap/sse/security/persistence/impl/FieldNames.java index 5e20eb00cd1..dbd6a16b3c2 100755 --- a/java/com.sap.sse.security.persistence/src/com/sap/sse/security/persistence/impl/FieldNames.java +++ b/java/com.sap.sse.security.persistence/src/com/sap/sse/security/persistence/impl/FieldNames.java @@ -5,5 +5,7 @@ public enum FieldNames { SESSION_ID, SESSION_HOST, SESSION_LAST_ACCESS_TIME, SESSION_START_TIMESTAMP, SESSION_TIMEOUT, SESSION_ATTRIBUTES, SESSION_ATTRIBUTE_NAME, SESSION_ATTRIBUTE_VALUE, SESSION_PRINCIPAL_REALM_NAME, - SESSION_PRINCIPAL_REALM_VALUE; + SESSION_PRINCIPAL_REALM_VALUE, + + CORS_FILTER_CONFIGURATION_SERVER_NAME, CORS_FILTER_CONFIGURATION_IS_WILDCARD, CORS_FILTER_CONFIGURATION_ALLOWED_ORIGINS; } diff --git a/java/com.sap.sse.security.persistence/src/com/sap/sse/security/persistence/impl/MongoObjectFactoryImpl.java b/java/com.sap.sse.security.persistence/src/com/sap/sse/security/persistence/impl/MongoObjectFactoryImpl.java index 79258248ebf..2ae1ac5431c 100755 --- a/java/com.sap.sse.security.persistence/src/com/sap/sse/security/persistence/impl/MongoObjectFactoryImpl.java +++ b/java/com.sap.sse.security.persistence/src/com/sap/sse/security/persistence/impl/MongoObjectFactoryImpl.java @@ -2,6 +2,8 @@ package com.sap.sse.security.persistence.impl; import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.logging.Logger; @@ -18,12 +20,16 @@ import com.sap.sse.security.persistence.MongoObjectFactory; public class MongoObjectFactoryImpl implements MongoObjectFactory { private static final Logger logger = Logger.getLogger(MongoObjectFactoryImpl.class.getName()); private final MongoCollection sessionCollection; + private final MongoCollection corsFilterConfigurationsCollection; public MongoObjectFactoryImpl(MongoDatabase mongoDatabase) { sessionCollection = mongoDatabase.getCollection(CollectionNames.SESSIONS.name()); sessionCollection.createIndex(new Document(). append(FieldNames.CACHE_NAME.name(), 1). append(FieldNames.SESSION_ID.name(), 1), new IndexOptions().name("cachenameandsessionid").background(false)); + corsFilterConfigurationsCollection = mongoDatabase.getCollection(CollectionNames.CORS_FILTER_CONFIGURATIONS.name()); + corsFilterConfigurationsCollection.createIndex(new Document(). + append(FieldNames.CORS_FILTER_CONFIGURATION_SERVER_NAME.name(), 1), new IndexOptions().name("servername").background(false)); } private Document getKey(String cacheName, Session session) { @@ -93,4 +99,24 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory { public void removeSession(String cacheName, Session session) { sessionCollection.deleteOne(getKey(cacheName, session)); } + + @Override + public void storeCORSFilterConfigurationIsWildcard(String serverName) { + final Document filter = new Document().append(FieldNames.CORS_FILTER_CONFIGURATION_SERVER_NAME.name(), serverName); + final Document d = new Document() + .append(FieldNames.CORS_FILTER_CONFIGURATION_SERVER_NAME.name(), serverName) + .append(FieldNames.CORS_FILTER_CONFIGURATION_IS_WILDCARD.name(), true) + .append(FieldNames.CORS_FILTER_CONFIGURATION_ALLOWED_ORIGINS.name(), Collections.emptyList()); + corsFilterConfigurationsCollection.replaceOne(filter, d, new ReplaceOptions().upsert(true)); + } + + @Override + public void storeCORSFilterConfigurationAllowedOrigins(String serverName, String... allowedOrigins) { + final Document filter = new Document().append(FieldNames.CORS_FILTER_CONFIGURATION_SERVER_NAME.name(), serverName); + final Document d = new Document() + .append(FieldNames.CORS_FILTER_CONFIGURATION_SERVER_NAME.name(), serverName) + .append(FieldNames.CORS_FILTER_CONFIGURATION_IS_WILDCARD.name(), false) + .append(FieldNames.CORS_FILTER_CONFIGURATION_ALLOWED_ORIGINS.name(), allowedOrigins == null ? Collections.emptyList() : Arrays.asList(allowedOrigins)); + corsFilterConfigurationsCollection.replaceOne(filter, d, new ReplaceOptions().upsert(true)); + } } diff --git a/java/com.sap.sse.security.replication.test/src/com/sap/sse/security/replication/test/AbstractSecurityReplicationTest.java b/java/com.sap.sse.security.replication.test/src/com/sap/sse/security/replication/test/AbstractSecurityReplicationTest.java index cf900acf322..21624f430fc 100755 --- a/java/com.sap.sse.security.replication.test/src/com/sap/sse/security/replication/test/AbstractSecurityReplicationTest.java +++ b/java/com.sap.sse.security.replication.test/src/com/sap/sse/security/replication/test/AbstractSecurityReplicationTest.java @@ -39,8 +39,8 @@ public abstract class AbstractSecurityReplicationTest extends AbstractServerWith userStore.ensureDefaultRolesExist(); userStore.loadAndMigrateUsers(); final AccessControlStore accessControlStore = new AccessControlStoreImpl(userStore); - SecurityServiceImpl result = new SecurityServiceImpl(null, userStore, accessControlStore, - SecuredSecurityTypes::getAllInstances, SSESubscriptionPlan::getAllInstances); + SecurityServiceImpl result = new SecurityServiceImpl(/* mailServiceTracker */ null, /* corsFilterConfigurationTracker */ null, userStore, + accessControlStore, SecuredSecurityTypes::getAllInstances, SSESubscriptionPlan::getAllInstances); return result; } @@ -51,8 +51,8 @@ public abstract class AbstractSecurityReplicationTest extends AbstractServerWith userStore.ensureDefaultRolesExist(); userStore.loadAndMigrateUsers(); final AccessControlStore accessControlStore = new AccessControlStoreImpl(userStore); - return new SecurityServiceImpl(/* mailServiceTracker */ null, userStore, accessControlStore, - SecuredSecurityTypes::getAllInstances, SSESubscriptionPlan::getAllInstances); + return new SecurityServiceImpl(/* mailServiceTracker */ null, /* corsFilterConfigurationTracker */ null, userStore, + accessControlStore, SecuredSecurityTypes::getAllInstances, SSESubscriptionPlan::getAllInstances); } } } diff --git a/java/com.sap.sse.security.replication.test/src/com/sap/sse/security/replication/test/SecurityReplicationLeadingToEmailReplicationTest.java b/java/com.sap.sse.security.replication.test/src/com/sap/sse/security/replication/test/SecurityReplicationLeadingToEmailReplicationTest.java index 6bff455c731..db59517d2c1 100644 --- a/java/com.sap.sse.security.replication.test/src/com/sap/sse/security/replication/test/SecurityReplicationLeadingToEmailReplicationTest.java +++ b/java/com.sap.sse.security.replication.test/src/com/sap/sse/security/replication/test/SecurityReplicationLeadingToEmailReplicationTest.java @@ -63,8 +63,8 @@ public class SecurityReplicationLeadingToEmailReplicationTest extends AbstractSe userStore.ensureDefaultRolesExist(); userStore.loadAndMigrateUsers(); final AccessControlStore accessControlStore = new AccessControlStoreImpl(userStore); - SecurityServiceImpl result = new SecurityServiceImpl(trackerMock, userStore, accessControlStore, - SecuredSecurityTypes::getAllInstances, SSESubscriptionPlan::getAllInstances); + SecurityServiceImpl result = new SecurityServiceImpl(trackerMock, /* corsFilterConfigurationTracker */ null, userStore, + accessControlStore, SecuredSecurityTypes::getAllInstances, SSESubscriptionPlan::getAllInstances); result.initialize(); return result; } @@ -77,8 +77,8 @@ public class SecurityReplicationLeadingToEmailReplicationTest extends AbstractSe doReturn(replicaMailService).when(trackerMock).getService(); final UserStoreImpl userStore = new UserStoreImpl("TestDefaultTenant"); final AccessControlStore accessControlStore = new AccessControlStoreImpl(userStore); - SecurityServiceImpl result = new SecurityServiceImpl(trackerMock, userStore, accessControlStore, - SecuredSecurityTypes::getAllInstances, SSESubscriptionPlan::getAllInstances); + SecurityServiceImpl result = new SecurityServiceImpl(trackerMock, /* corsFilterConfigurationTracker */ null, userStore, + accessControlStore, SecuredSecurityTypes::getAllInstances, SSESubscriptionPlan::getAllInstances); userStore.ensureDefaultRolesExist(); userStore.ensureServerGroupExists(); result.initialize(); diff --git a/java/com.sap.sse.security.replication.test/src/com/sap/sse/security/replication/test/SecurityServiceInitialLoadTest.java b/java/com.sap.sse.security.replication.test/src/com/sap/sse/security/replication/test/SecurityServiceInitialLoadTest.java index 93445ec0e26..d4510be3946 100755 --- a/java/com.sap.sse.security.replication.test/src/com/sap/sse/security/replication/test/SecurityServiceInitialLoadTest.java +++ b/java/com.sap.sse.security.replication.test/src/com/sap/sse/security/replication/test/SecurityServiceInitialLoadTest.java @@ -41,8 +41,8 @@ public class SecurityServiceInitialLoadTest extends AbstractServerWithSingleServ final AccessControlStore accessControlStore = new AccessControlStoreImpl( PersistenceFactory.INSTANCE.getDefaultDomainObjectFactory(), PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory(), userStore); - final SecurityServiceImpl newMaster = new SecurityServiceImpl(null, userStore, accessControlStore, - SecuredSecurityTypes::getAllInstances, SSESubscriptionPlan::getAllInstances); + final SecurityServiceImpl newMaster = new SecurityServiceImpl(null, /* corsFilterConfigurationTracker */ null, userStore, + accessControlStore, SecuredSecurityTypes::getAllInstances, SSESubscriptionPlan::getAllInstances); newMaster.createSimpleUser(username, email, password, fullName, company, /* validationBaseURL */ Locale.ENGLISH, null, null); accessToken = newMaster.createAccessToken(username); diff --git a/java/com.sap.sse.security.test/src/com/sap/sse/security/jaxrs/api/SecurityResourceTest.java b/java/com.sap.sse.security.test/src/com/sap/sse/security/jaxrs/api/SecurityResourceTest.java index c7c8d50fc42..a0171da3337 100755 --- a/java/com.sap.sse.security.test/src/com/sap/sse/security/jaxrs/api/SecurityResourceTest.java +++ b/java/com.sap.sse.security.test/src/com/sap/sse/security/jaxrs/api/SecurityResourceTest.java @@ -66,9 +66,9 @@ public class SecurityResourceTest { store.ensureServerGroupExists(); accessControlStore = new AccessControlStoreImpl(store); Activator.setTestStores(store, accessControlStore); - service = new SecurityServiceImpl(/* mailServiceTracker */ null, store, accessControlStore, - /* hasPermissionsProvider */SecuredSecurityTypes::getAllInstances, - SSESubscriptionPlan::getAllInstances); + service = new SecurityServiceImpl(/* mailServiceTracker */ null, /* corsFilterConfigurationTracker */ null, store, + accessControlStore, + /* hasPermissionsProvider */SecuredSecurityTypes::getAllInstances, SSESubscriptionPlan::getAllInstances); service.initialize(); Activator.setSecurityService(service); SecurityUtils.setSecurityManager(service.getSecurityManager()); diff --git a/java/com.sap.sse.security.test/src/com/sap/sse/security/test/LoginTest.java b/java/com.sap.sse.security.test/src/com/sap/sse/security/test/LoginTest.java index 8f5508675de..0cd327cba0c 100644 --- a/java/com.sap.sse.security.test/src/com/sap/sse/security/test/LoginTest.java +++ b/java/com.sap.sse.security.test/src/com/sap/sse/security/test/LoginTest.java @@ -81,8 +81,8 @@ public class LoginTest { Activator.setTestStores(userStore, accessControlStore); // enables shiro to find classes from com.sap.sse.security Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); - securityService = new SecurityServiceImpl(/* mailServiceTracker */ null, userStore, accessControlStore, - SecuredSecurityTypes::getAllInstances, SSESubscriptionPlan::getAllInstances); + securityService = new SecurityServiceImpl(/* mailServiceTracker */ null, /* corsFilterConfigurationTracker */ null, userStore, + accessControlStore, SecuredSecurityTypes::getAllInstances, SSESubscriptionPlan::getAllInstances); Activator.setSecurityService(securityService); } diff --git a/java/com.sap.sse.security.test/src/com/sap/sse/security/test/PrivilegeEscalationTest.java b/java/com.sap.sse.security.test/src/com/sap/sse/security/test/PrivilegeEscalationTest.java index 414d9f35e74..363fb84a703 100644 --- a/java/com.sap.sse.security.test/src/com/sap/sse/security/test/PrivilegeEscalationTest.java +++ b/java/com.sap.sse.security.test/src/com/sap/sse/security/test/PrivilegeEscalationTest.java @@ -71,8 +71,8 @@ public class PrivilegeEscalationTest { userGroup.add(user2); userStore.updateUserGroup(userGroup); accessControlStore = new AccessControlStoreImpl(userStore); - securityService = new SecurityServiceImpl(null, userStore, accessControlStore, - SecuredSecurityTypes::getAllInstances, SSESubscriptionPlan::getAllInstances); + securityService = new SecurityServiceImpl(null, /* corsFilterConfigurationTracker */ null, userStore, + accessControlStore, SecuredSecurityTypes::getAllInstances, SSESubscriptionPlan::getAllInstances); securityService.initialize(); rd = new RoleDefinitionImpl(UUID.randomUUID(), "some_role", Collections.singleton(type1.getPermission(DefaultActions.READ, DefaultActions.UPDATE))); diff --git a/java/com.sap.sse.security.test/src/com/sap/sse/security/test/SecurityServiceAndHasPermissionsProviderTest.java b/java/com.sap.sse.security.test/src/com/sap/sse/security/test/SecurityServiceAndHasPermissionsProviderTest.java index d72d61e2d2d..c6c1b29278e 100644 --- a/java/com.sap.sse.security.test/src/com/sap/sse/security/test/SecurityServiceAndHasPermissionsProviderTest.java +++ b/java/com.sap.sse.security.test/src/com/sap/sse/security/test/SecurityServiceAndHasPermissionsProviderTest.java @@ -51,15 +51,15 @@ public class SecurityServiceAndHasPermissionsProviderTest { } private SecurityService createSecurityServiceWithoutHasPermissionsProvider() { - SecurityService securityService = new SecurityServiceImpl(/* mailServiceTracker */ null, userStore, - accessControlStore, /* HasPermissionsProvider */ null, SSESubscriptionPlan::getAllInstances); + SecurityService securityService = new SecurityServiceImpl(/* mailServiceTracker */ null, /* corsFilterConfigurationTracker */ null, + userStore, accessControlStore, /* HasPermissionsProvider */ null, SSESubscriptionPlan::getAllInstances); securityService.initialize(); return securityService; } private SecurityService createSecurityServiceWithHasPermissionsProvider() { - final SecurityService securityService = new SecurityServiceImpl(/* mailServiceTracker */ null, userStore, - accessControlStore, SecuredSecurityTypes::getAllInstances, SSESubscriptionPlan::getAllInstances); + final SecurityService securityService = new SecurityServiceImpl(/* mailServiceTracker */ null, /* corsFilterConfigurationTracker */ null, + userStore, accessControlStore, SecuredSecurityTypes::getAllInstances, SSESubscriptionPlan::getAllInstances); securityService.initialize(); return securityService; } diff --git a/java/com.sap.sse.security/src/com/sap/sse/security/SecurityService.java b/java/com.sap.sse.security/src/com/sap/sse/security/SecurityService.java index 951b2261d16..955e7ea7abf 100644 --- a/java/com.sap.sse.security/src/com/sap/sse/security/SecurityService.java +++ b/java/com.sap.sse.security/src/com/sap/sse/security/SecurityService.java @@ -29,6 +29,7 @@ import com.sap.sse.security.interfaces.PreferenceConverter; import com.sap.sse.security.interfaces.UserImpl; import com.sap.sse.security.interfaces.UserStore; import com.sap.sse.security.operations.SecurityOperation; +import com.sap.sse.security.persistence.PersistenceFactory; import com.sap.sse.security.shared.AccessControlListAnnotation; import com.sap.sse.security.shared.BasicUserStore; import com.sap.sse.security.shared.HasPermissions; @@ -831,4 +832,23 @@ public interface SecurityService extends ReplicableWithObjectInputStream updatedItemPrices); + Void internalSetCORSFilterConfigurationToWildcard(String serverName); + + Void internalSetCORSFilterConfigurationAllowedOrigins(String serverName, String... allowedOrigins); } diff --git a/java/com.sap.sse.security/src/com/sap/sse/security/impl/SecurityServiceImpl.java b/java/com.sap.sse.security/src/com/sap/sse/security/impl/SecurityServiceImpl.java index a32f69b059f..3d1077118f3 100755 --- a/java/com.sap.sse.security/src/com/sap/sse/security/impl/SecurityServiceImpl.java +++ b/java/com.sap.sse.security/src/com/sap/sse/security/impl/SecurityServiceImpl.java @@ -90,6 +90,7 @@ import com.sap.sse.concurrent.NamedReentrantReadWriteLock; import com.sap.sse.i18n.impl.ResourceBundleStringMessagesImpl; import com.sap.sse.mail.MailService; import com.sap.sse.replication.interfaces.impl.AbstractReplicableWithObjectInputStream; +import com.sap.sse.rest.CORSFilterConfiguration; import com.sap.sse.security.Action; import com.sap.sse.security.ClientUtils; import com.sap.sse.security.GithubApi; @@ -209,6 +210,14 @@ implements ReplicableSecurityService, ClearStateTestSupport { */ private final ReplicatingCacheManager cacheManager; + /** + * Keys are the replica set's server names. Values are {@link Pair}s whose {@link Pair#getA() first} component is a + * boolean telling whether the CORS filter for the replica set identified by the key uses the "wildcard" (*) to + * allow REST requests from all possible origins, and the {@link Pair#getB() second} component lists the allowed + * origins in case it's not a wildcard configuration. For wildcard configurations, the second component is ignored. + */ + private final ConcurrentMap>> corsFilterConfigurationsByReplicaSetName; + private final UserStore store; private final AccessControlStore accessControlStore; @@ -216,6 +225,8 @@ implements ReplicableSecurityService, ClearStateTestSupport { private boolean isNewServer; private final ServiceTracker mailServiceTracker; + + private final ServiceTracker corsFilterConfigurationTracker; private ThreadLocal temporaryDefaultTenant = new InheritableThreadLocal<>(); @@ -255,7 +266,6 @@ implements ReplicableSecurityService, ClearStateTestSupport { * Creates a security service that is not shared across subdomains, therefore leading to the use of the full * domain through which its services are requested for {@code Document.domain} and hence for the browser local * storage, session storage and the Shiro {@code JSESSIONID} cookie's domain. - * * @param setAsActivatorSecurityService * when true, the {@link Activator#setSecurityService(com.sap.sse.security.SecurityService)} * will be called with this new instance as argument so that the cache manager can already be accessed @@ -263,10 +273,12 @@ implements ReplicableSecurityService, ClearStateTestSupport { * activator's security service and passes it to the cache entries created. They need it, in turn, for * replication. */ - public SecurityServiceImpl(ServiceTracker mailServiceTracker, UserStore userStore, - AccessControlStore accessControlStore, HasPermissionsProvider hasPermissionsProvider, SubscriptionPlanProvider subscriptionPlanProvider) { - this(mailServiceTracker, userStore, accessControlStore, hasPermissionsProvider, subscriptionPlanProvider, - /* sharedAcrossSubdomainsOf */ null, /* baseUrlForCrossDomainStorage */ null); + public SecurityServiceImpl(ServiceTracker mailServiceTracker, + ServiceTracker corsFilterConfigurationTracker, + UserStore userStore, AccessControlStore accessControlStore, HasPermissionsProvider hasPermissionsProvider, + SubscriptionPlanProvider subscriptionPlanProvider) { + this(mailServiceTracker, corsFilterConfigurationTracker, userStore, accessControlStore, hasPermissionsProvider, + subscriptionPlanProvider, /* sharedAcrossSubdomainsOf */ null, /* baseUrlForCrossDomainStorage */ null); } /** @@ -275,9 +287,9 @@ implements ReplicableSecurityService, ClearStateTestSupport { * the browser local and session store shall be shared and for which sessions identified by the {@code JSESSIONID} cookie shall * be shared as well. */ - public SecurityServiceImpl(ServiceTracker mailServiceTracker, UserStore userStore, - AccessControlStore accessControlStore, HasPermissionsProvider hasPermissionsProvider, SubscriptionPlanProvider subscriptionPlanProvider, - String sharedAcrossSubdomainsOf, String baseUrlForCrossDomainStorage) { + public SecurityServiceImpl(ServiceTracker mailServiceTracker, ServiceTracker corsFilterConfigurationTracker, + UserStore userStore, AccessControlStore accessControlStore, HasPermissionsProvider hasPermissionsProvider, + SubscriptionPlanProvider subscriptionPlanProvider, String sharedAcrossSubdomainsOf, String baseUrlForCrossDomainStorage) { initialLoadClassLoaderRegistry.addClassLoader(getClass().getClassLoader()); if (hasPermissionsProvider == null) { throw new IllegalArgumentException("No HasPermissionsProvider defined"); @@ -290,8 +302,10 @@ implements ReplicableSecurityService, ClearStateTestSupport { this.store = userStore; this.accessControlStore = accessControlStore; this.mailServiceTracker = mailServiceTracker; + this.corsFilterConfigurationTracker = corsFilterConfigurationTracker; this.hasPermissionsProvider = hasPermissionsProvider; - cacheManager = loadReplicationCacheManagerContents(); + this.cacheManager = loadReplicationCacheManagerContents(); + this.corsFilterConfigurationsByReplicaSetName = loadCORSFilterConfigurations(); logger.info("Loaded shiro.ini file from: classpath:shiro.ini"); final StringBuilder logMessage = new StringBuilder("[urls] section from Shiro configuration:"); final Section urlsSection = shiroConfiguration.getSection("urls"); @@ -338,6 +352,13 @@ implements ReplicableSecurityService, ClearStateTestSupport { return result; } + private ConcurrentMap>> loadCORSFilterConfigurations() { + logger.info("Loading CORS filter configurations"); + final ConcurrentMap>> result = new ConcurrentHashMap<>(); + result.putAll(PersistenceFactory.INSTANCE.getDefaultDomainObjectFactory().loadCORSFilterConfigurationsForReplicaSetNames()); + return result; + } + @Override public Iterable getAllHasPermissions() { return hasPermissionsProvider.getAllHasPermissions(); @@ -447,6 +468,10 @@ implements ReplicableSecurityService, ClearStateTestSupport { return mailServiceTracker == null ? null : mailServiceTracker.getService(); } + private CORSFilterConfiguration getCORSFilterConfiguration() { + return corsFilterConfigurationTracker == null ? null : corsFilterConfigurationTracker.getService(); + } + @Override public void sendMail(String username, String subject, String body) throws MailException { final User user = getUserByName(username); @@ -2184,6 +2209,37 @@ implements ReplicableSecurityService, ClearStateTestSupport { } return result; } + + @Override + public void setCORSFilterConfigurationToWildcard(String serverName) { + apply(s->s.internalSetCORSFilterConfigurationToWildcard(serverName)); + } + + @Override + public Void internalSetCORSFilterConfigurationToWildcard(String serverName) { + if (Util.equalsWithNull(serverName, ServerInfo.getName())) { + getCORSFilterConfiguration().setWildcard(); + } + corsFilterConfigurationsByReplicaSetName.put(serverName, new Pair<>(true, Collections.emptySet())); + PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory().storeCORSFilterConfigurationIsWildcard(serverName); + return null; + } + + @Override + public void setCORSFilterConfigurationAllowedOrigins(String serverName, String... allowedOrigins) { + apply(s->s.internalSetCORSFilterConfigurationAllowedOrigins(serverName, allowedOrigins)); + } + + @Override + public Void internalSetCORSFilterConfigurationAllowedOrigins(String serverName, String... allowedOrigins) { + final Iterable allowedOriginsAsList = allowedOrigins == null ? Collections.emptyList() : Arrays.asList(allowedOrigins); + if (Util.equalsWithNull(serverName, ServerInfo.getName())) { + getCORSFilterConfiguration().setOrigins(allowedOriginsAsList); + } + corsFilterConfigurationsByReplicaSetName.put(serverName, new Pair<>(false, Util.asNewSet(allowedOriginsAsList))); + PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory().storeCORSFilterConfigurationAllowedOrigins(serverName, allowedOrigins); + return null; + } // ----------------- Replication ------------- @Override @@ -3022,7 +3078,7 @@ implements ReplicableSecurityService, ClearStateTestSupport { for (SubscriptionPlan subscriptionPlan : allSubscriptionPlans.values()) { for (SubscriptionPrice subscriptionPrice : subscriptionPlan.getPrices()) { final BigDecimal updatedPrice = updatedItemPrices.get(subscriptionPrice.getPriceId()); - if(updatedPrice != null) { + if (updatedPrice != null) { logger.log(Level.INFO, "Setting ItemPrice for SubscriptionPrice " + subscriptionPrice.getPriceId()); subscriptionPrice.setPrice(updatedPrice); }