diff --git a/backend/src/main/java/ch/puzzle/okr/multitenancy/FlywayMultitenantMigrationInitializer.java b/backend/src/main/java/ch/puzzle/okr/multitenancy/FlywayMultitenantMigrationInitializer.java
index d58c195c77..9e7fe04603 100644
--- a/backend/src/main/java/ch/puzzle/okr/multitenancy/FlywayMultitenantMigrationInitializer.java
+++ b/backend/src/main/java/ch/puzzle/okr/multitenancy/FlywayMultitenantMigrationInitializer.java
@@ -2,6 +2,8 @@
import jakarta.persistence.EntityNotFoundException;
import org.flywaydb.core.Flyway;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@@ -10,6 +12,8 @@ public class FlywayMultitenantMigrationInitializer {
private final TenantConfigProviderInterface tenantConfigProvider;
private final String[] scriptLocations;
+ private static final Logger logger = LoggerFactory.getLogger(FlywayMultitenantMigrationInitializer.class);
+
public FlywayMultitenantMigrationInitializer(TenantConfigProviderInterface tenantConfigProvider,
final @Value("${spring.flyway.locations}") String[] scriptLocations) {
this.tenantConfigProvider = tenantConfigProvider;
@@ -20,9 +24,11 @@ public void migrateFlyway() {
this.tenantConfigProvider.getTenantConfigs().forEach((tenantConfig) -> {
TenantConfigProvider.DataSourceConfig dataSourceConfig = this.tenantConfigProvider
.getTenantConfigById(tenantConfig.tenantId())
- .map(TenantConfigProvider.TenantConfig::dataSourceConfig).orElseThrow(
+ .map(TenantConfigProvider.TenantConfig::dataSourceConfigFlyway).orElseThrow(
() -> new EntityNotFoundException("Cannot find tenant for configuring flyway migration"));
+ logUsedHibernateConfig(dataSourceConfig);
+
Flyway tenantSchemaFlyway = Flyway.configure() //
.dataSource(dataSourceConfig.url(), dataSourceConfig.name(), dataSourceConfig.password()) //
.locations(scriptLocations) //
@@ -32,6 +38,9 @@ public void migrateFlyway() {
tenantSchemaFlyway.migrate();
});
+ }
+ private void logUsedHibernateConfig(TenantConfigProvider.DataSourceConfig dataSourceConfig) {
+ logger.error("use DbConfig: user={}", dataSourceConfig.name());
}
}
\ No newline at end of file
diff --git a/backend/src/main/java/ch/puzzle/okr/multitenancy/HibernateContext.java b/backend/src/main/java/ch/puzzle/okr/multitenancy/HibernateContext.java
index 42e068ece0..86b8f0e8d9 100644
--- a/backend/src/main/java/ch/puzzle/okr/multitenancy/HibernateContext.java
+++ b/backend/src/main/java/ch/puzzle/okr/multitenancy/HibernateContext.java
@@ -1,9 +1,25 @@
package ch.puzzle.okr.multitenancy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.core.env.ConfigurableEnvironment;
import java.util.Properties;
+/**
+ * Reads the (not tenant specific) hibernate configuration form the "hibernate.x" properties in the
+ * applicationX.properties file. It then caches the configuration as DbConfig object. The data from the DbConfig object
+ * is used by the SchemaMultiTenantConnectionProvider via getHibernateConfig() and getHibernateConfig(tenantId).
+ *
+ *
+ * getHibernateConfig() returns the cached DbConfig as properties.
+ *
+ *
+ *
+ * getHibernateConfig(tenantId) patches the DbConfig data with tenant specific data (from
+ * TenantConfigProvider) and returns the patched data as properties
+ *
+ */
public class HibernateContext {
public static final String HIBERNATE_CONNECTION_URL = "hibernate.connection.url";
public static final String HIBERNATE_CONNECTION_USERNAME = "hibernate.connection.username";
@@ -14,6 +30,8 @@ public class HibernateContext {
public static String SPRING_DATASOURCE_USERNAME = "spring.datasource.username";
public static String SPRING_DATASOURCE_PASSWORD = "spring.datasource.password";
+ private static final Logger logger = LoggerFactory.getLogger(HibernateContext.class);
+
public record DbConfig(String url, String username, String password, String multiTenancy) {
public boolean isValid() {
@@ -29,8 +47,15 @@ private boolean hasEmptyValues() {
}
}
+ // general (not tenant specific) hibernate config
private static DbConfig cachedHibernateConfig;
+ public static void extractAndSetHibernateConfig(ConfigurableEnvironment environment) {
+ DbConfig dbConfig = extractHibernateConfig(environment);
+ setHibernateConfig(dbConfig);
+ logUsedHibernateConfig(dbConfig);
+ }
+
public static void setHibernateConfig(DbConfig dbConfig) {
if (dbConfig == null || !dbConfig.isValid()) {
throw new RuntimeException("Invalid hibernate configuration " + dbConfig);
@@ -38,11 +63,6 @@ public static void setHibernateConfig(DbConfig dbConfig) {
cachedHibernateConfig = dbConfig;
}
- public static void extractAndSetHibernateConfig(ConfigurableEnvironment environment) {
- DbConfig dbConfig = extractHibernateConfig(environment);
- HibernateContext.setHibernateConfig(dbConfig);
- }
-
private static DbConfig extractHibernateConfig(ConfigurableEnvironment environment) {
String url = environment.getProperty(HibernateContext.HIBERNATE_CONNECTION_URL);
String username = environment.getProperty(HibernateContext.HIBERNATE_CONNECTION_USERNAME);
@@ -60,7 +80,9 @@ public static Properties getHibernateConfig() {
if (cachedHibernateConfig == null) {
throw new RuntimeException("No cached hibernate configuration found");
}
- return getConfigAsProperties(cachedHibernateConfig);
+ var config = getConfigAsProperties(cachedHibernateConfig);
+ logUsedHibernateConfig(config);
+ return config;
}
private static Properties getConfigAsProperties(DbConfig dbConfig) {
@@ -74,4 +96,48 @@ private static Properties getConfigAsProperties(DbConfig dbConfig) {
properties.put(HibernateContext.SPRING_DATASOURCE_PASSWORD, dbConfig.password());
return properties;
}
+
+ public static Properties getHibernateConfig(String tenantIdentifier) {
+ if (cachedHibernateConfig == null) {
+ throw new RuntimeException("No cached hibernate configuration found (for tenant " + tenantIdentifier + ")");
+ }
+ var config = getConfigAsPropertiesAndPatch(cachedHibernateConfig, tenantIdentifier);
+ logUsedHibernateConfig(tenantIdentifier, config);
+ return config;
+ }
+
+ private static Properties getConfigAsPropertiesAndPatch(DbConfig dbConfig, String tenantIdentifier) {
+ Properties properties = getConfigAsProperties(dbConfig);
+ return patchConfigAppForTenant(properties, tenantIdentifier);
+ }
+
+ private static Properties patchConfigAppForTenant(Properties properties, String tenantIdentifier) {
+ TenantConfigProvider.TenantConfig cachedTenantConfig = TenantConfigProvider
+ .getCachedTenantConfig(tenantIdentifier);
+ if (cachedTenantConfig == null) {
+ throw new RuntimeException("No cached tenant configuration found (for tenant " + tenantIdentifier + ")");
+ }
+
+ TenantConfigProvider.DataSourceConfig dataSourceConfigApp = cachedTenantConfig.dataSourceConfigApp();
+ properties.put(HibernateContext.HIBERNATE_CONNECTION_USERNAME, dataSourceConfigApp.name());
+ properties.put(HibernateContext.HIBERNATE_CONNECTION_PASSWORD, dataSourceConfigApp.password());
+ properties.put(HibernateContext.SPRING_DATASOURCE_USERNAME, dataSourceConfigApp.name());
+ properties.put(HibernateContext.SPRING_DATASOURCE_PASSWORD, dataSourceConfigApp.password());
+ return properties;
+ }
+
+ private static void logUsedHibernateConfig(DbConfig hibernateConfig) {
+ logger.error("set DbConfig: user={}", hibernateConfig.username());
+ }
+
+ private static void logUsedHibernateConfig(Properties hibernateConfig) {
+ logger.error("use DbConfig: user={}",
+ hibernateConfig.getProperty(HibernateContext.HIBERNATE_CONNECTION_USERNAME)); //
+ }
+
+ private static void logUsedHibernateConfig(String tenantId, Properties hibernateConfig) {
+ logger.error("use DbConfig: tenant={} user={}", tenantId,
+ hibernateConfig.getProperty(HibernateContext.HIBERNATE_CONNECTION_USERNAME));
+ }
+
}
diff --git a/backend/src/main/java/ch/puzzle/okr/multitenancy/SchemaMultiTenantConnectionProvider.java b/backend/src/main/java/ch/puzzle/okr/multitenancy/SchemaMultiTenantConnectionProvider.java
index 05ff90ce53..5c11016066 100644
--- a/backend/src/main/java/ch/puzzle/okr/multitenancy/SchemaMultiTenantConnectionProvider.java
+++ b/backend/src/main/java/ch/puzzle/okr/multitenancy/SchemaMultiTenantConnectionProvider.java
@@ -15,6 +15,35 @@
import static ch.puzzle.okr.multitenancy.TenantContext.DEFAULT_TENANT_ID;
+/**
+ * The central piece of code of multitenancy.
+ *
+ *
+ * getConnection(tenantId) sets in each tenant request the specific db schema for the
+ * tenant. This guarantees that each tenant always works in its own DB schema.
+ *
+ * getConnection(tenantId) -> Connection calls in the abstract super class the
+ * getConnection(tenantId) -> Connection which calls the abstract
+ * selectConnectionProvider(tenantIdentifier) -> ConnectionProvider which is implemented
+ * in SchemaMultiTenantConnectionProvider.
+ *
+ *
+ *
+ * Some coding details:
+ *
+ * selectConnectionProvider(tenantId) -> ConnectionProvider returns for a tenant a
+ * ConnectionProvider. It first checks if the ConnectionProvider for the tenant is already
+ * cached (in connectionProviderMap). If the ConnectionProvider is cached, it returns it.
+ * Otherwise it creates a ConnectionProvider for the tenant, cache it and return it.
+ *
+ * To create a ConnectionProvider for the tenant, it tries to load the configuration from
+ * the hibernate properties. For this it uses 2 methods of HibernateContext:
+ * getHibernateConfig() if the tenant is the DEFAULT_TENANT_ID (public) and
+ * getHibernateConfig(tenantId) for all other tenants. With this information its then
+ * possible to create and cache a ConnectionProvider for the tenant. If no matching
+ * hibernate properties are found, then an exception is thrown.
+ *
+ */
public class SchemaMultiTenantConnectionProvider extends AbstractMultiTenantConnectionProvider {
private static final Logger logger = LoggerFactory.getLogger(SchemaMultiTenantConnectionProvider.class);
@@ -31,7 +60,7 @@ public Connection getConnection(String tenantIdentifier) throws SQLException {
return getConnection(tenantIdentifier, connection);
}
- protected Connection getConnection(String tenantIdentifier, Connection connection) throws SQLException {
+ Connection getConnection(String tenantIdentifier, Connection connection) throws SQLException {
String schema = convertTenantIdToSchemaName(tenantIdentifier);
logger.debug("Setting schema to {}", schema);
@@ -39,7 +68,7 @@ protected Connection getConnection(String tenantIdentifier, Connection connectio
return connection;
}
- private String convertTenantIdToSchemaName(String tenantIdentifier) {
+ String convertTenantIdToSchemaName(String tenantIdentifier) {
return Objects.equals(tenantIdentifier, DEFAULT_TENANT_ID) ? tenantIdentifier
: MessageFormat.format("okr_{0}", tenantIdentifier);
}
@@ -54,13 +83,13 @@ protected ConnectionProvider selectConnectionProvider(String tenantIdentifier) {
return getConnectionProvider(tenantIdentifier);
}
- protected ConnectionProvider getConnectionProvider(String tenantIdentifier) {
+ ConnectionProvider getConnectionProvider(String tenantIdentifier) {
return Optional.ofNullable(tenantIdentifier) //
.map(connectionProviderMap::get) //
- .orElseGet(() -> createNewConnectionProvider(tenantIdentifier));
+ .orElseGet(() -> createAndCacheNewConnectionProvider(tenantIdentifier));
}
- private ConnectionProvider createNewConnectionProvider(String tenantIdentifier) {
+ private ConnectionProvider createAndCacheNewConnectionProvider(String tenantIdentifier) {
return Optional.ofNullable(tenantIdentifier) //
.map(this::createConnectionProvider) //
.map(connectionProvider -> {
@@ -78,10 +107,10 @@ private ConnectionProvider createConnectionProvider(String tenantIdentifier) {
.orElse(null);
}
- protected Properties getHibernatePropertiesForTenantIdentifier(String tenantIdentifier) {
- Properties properties = getHibernateProperties();
- if (properties == null || properties.isEmpty()) {
- throw new RuntimeException("Cannot load hibernate properties from application.properties)");
+ Properties getHibernatePropertiesForTenantIdentifier(String tenantIdentifier) {
+ Properties properties = getHibernateProperties(tenantIdentifier);
+ if (properties.isEmpty()) {
+ throw new RuntimeException("Cannot load hibernate properties from application.properties");
}
if (!Objects.equals(tenantIdentifier, DEFAULT_TENANT_ID)) {
properties.put(AvailableSettings.DEFAULT_SCHEMA, MessageFormat.format("okr_{0}", tenantIdentifier));
@@ -89,18 +118,14 @@ protected Properties getHibernatePropertiesForTenantIdentifier(String tenantIden
return properties;
}
- private ConnectionProvider initConnectionProvider(Properties hibernateProperties) {
+ ConnectionProvider initConnectionProvider(Properties hibernateProperties) {
Map configProperties = convertPropertiesToMap(hibernateProperties);
- DriverManagerConnectionProviderImpl connectionProvider = getDriverManagerConnectionProviderImpl();
+ DriverManagerConnectionProviderImpl connectionProvider = new DriverManagerConnectionProviderImpl();
connectionProvider.configure(configProperties);
return connectionProvider;
}
- protected DriverManagerConnectionProviderImpl getDriverManagerConnectionProviderImpl() {
- return new DriverManagerConnectionProviderImpl();
- }
-
- private Map convertPropertiesToMap(Properties properties) {
+ Map convertPropertiesToMap(Properties properties) {
Map configProperties = new HashMap<>();
for (String key : properties.stringPropertyNames()) {
String value = properties.getProperty(key);
@@ -109,7 +134,10 @@ private Map convertPropertiesToMap(Properties properties) {
return configProperties;
}
- protected Properties getHibernateProperties() {
- return HibernateContext.getHibernateConfig();
+ private Properties getHibernateProperties(String tenantIdentifier) {
+ if (tenantIdentifier.equals(DEFAULT_TENANT_ID)) {
+ return HibernateContext.getHibernateConfig();
+ }
+ return HibernateContext.getHibernateConfig(tenantIdentifier);
}
}
\ No newline at end of file
diff --git a/backend/src/main/java/ch/puzzle/okr/multitenancy/TenantConfigProvider.java b/backend/src/main/java/ch/puzzle/okr/multitenancy/TenantConfigProvider.java
index 97521d9d7a..d1dbe99d6c 100644
--- a/backend/src/main/java/ch/puzzle/okr/multitenancy/TenantConfigProvider.java
+++ b/backend/src/main/java/ch/puzzle/okr/multitenancy/TenantConfigProvider.java
@@ -1,5 +1,7 @@
package ch.puzzle.okr.multitenancy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
@@ -7,21 +9,54 @@
import java.text.MessageFormat;
import java.util.*;
+/**
+ * Reads the configuration of the tenants (as TenantConfig objects) from the applicationX.properties and caches each
+ * TenantConfig in the TenantConfigs class.
+ */
@Component
public class TenantConfigProvider implements TenantConfigProviderInterface {
private static final String EMAIL_DELIMITER = ",";
private final Map tenantConfigs = new HashMap<>();
private final Environment env;
+ private enum DbType {
+ bootstrap, app, fly
+ }
+
+ private static final Logger logger = LoggerFactory.getLogger(TenantConfigProvider.class);
+
public TenantConfigProvider(final @Value("${okr.tenant-ids}") String[] tenantIds, Environment env) {
this.env = env;
for (String tenantId : tenantIds) {
OauthConfig c = readOauthConfig(tenantId);
- tenantConfigs.put(tenantId,
- createTenantConfig(c.jwkSetUri(), c.frontendClientIssuerUrl(), c.frontendClientId(), tenantId));
+ TenantConfig tenantConfig = createTenantConfig(c.jwkSetUri(), c.frontendClientIssuerUrl(),
+ c.frontendClientId(), tenantId);
+
+ tenantConfigs.put(tenantId, tenantConfig);
+ cacheTenantConfig(tenantId, tenantConfig); // cache tenantConfig for Hibernate connections
}
}
+ private void cacheTenantConfig(String tenantId, TenantConfig tenantConfig) {
+ TenantConfigs.add(tenantId, tenantConfig);
+ logCachingTenantConfig(tenantId, tenantConfig);
+ }
+
+ private void logCachingTenantConfig(String tenantId, TenantConfig tenantConfig) {
+ logger.error("cache TenantConfig: tenantId={}, users={}", //
+ tenantId, //
+ tenantConfig.dataSourceConfigFlyway().name() + " | " + tenantConfig.dataSourceConfigApp().name());
+ }
+
+ public static TenantConfigProvider.TenantConfig getCachedTenantConfig(String tenantId) {
+ return TenantConfigs.get(tenantId);
+ }
+
+ // for tests
+ public static void clearTenantConfigsCache() {
+ TenantConfigs.clear();
+ }
+
private OauthConfig readOauthConfig(String tenantId) {
return new OauthConfig(
env.getProperty(MessageFormat.format("okr.tenants.{0}.security.oauth2.resourceserver.jwt.jwk-set-uri",
@@ -32,8 +67,11 @@ private OauthConfig readOauthConfig(String tenantId) {
private TenantConfig createTenantConfig(String jwkSetUriTemplate, String frontendClientIssuerUrl,
String frontendClientId, String tenantId) {
- return new TenantConfig(tenantId, getOkrChampionEmailsFromTenant(tenantId), jwkSetUriTemplate,
- frontendClientIssuerUrl, frontendClientId, this.readDataSourceConfig(tenantId));
+
+ return new TenantConfig(tenantId, getOkrChampionEmailsFromTenant(tenantId), jwkSetUriTemplate, //
+ frontendClientIssuerUrl, frontendClientId, //
+ this.readDataSourceConfigFlyway(tenantId), //
+ this.readDataSourceConfigApp(tenantId));
}
private String[] getOkrChampionEmailsFromTenant(String tenantId) {
@@ -45,11 +83,19 @@ public List getTenantConfigs() {
return this.tenantConfigs.values().stream().toList();
}
- private DataSourceConfig readDataSourceConfig(String tenantId) {
+ private DataSourceConfig readDataSourceConfigFlyway(String tenantId) {
+ return readDataSourceConfig(tenantId, DbType.fly);
+ }
+
+ private DataSourceConfig readDataSourceConfigApp(String tenantId) {
+ return readDataSourceConfig(tenantId, DbType.app);
+ }
+
+ private DataSourceConfig readDataSourceConfig(String tenantId, DbType dbType) {
return new DataSourceConfig(env.getProperty("okr.datasource.driver-class-name"),
env.getProperty(MessageFormat.format("okr.tenants.{0}.datasource.url", tenantId)),
- env.getProperty(MessageFormat.format("okr.tenants.{0}.datasource.username", tenantId)),
- env.getProperty(MessageFormat.format("okr.tenants.{0}.datasource.password", tenantId)),
+ env.getProperty(MessageFormat.format("okr.tenants.{0}.datasource.username." + dbType, tenantId)),
+ env.getProperty(MessageFormat.format("okr.tenants.{0}.datasource.password." + dbType, tenantId)),
env.getProperty(MessageFormat.format("okr.tenants.{0}.datasource.schema", tenantId)));
}
@@ -62,7 +108,7 @@ public Optional getJwkSetUri(String tenantId) {
}
public record TenantConfig(String tenantId, String[] okrChampionEmails, String jwkSetUri, String issuerUrl,
- String clientId, DataSourceConfig dataSourceConfig) {
+ String clientId, DataSourceConfig dataSourceConfigFlyway, DataSourceConfig dataSourceConfigApp) {
}
public record DataSourceConfig(String driverClassName, String url, String name, String password, String schema) {
diff --git a/backend/src/main/java/ch/puzzle/okr/multitenancy/TenantConfigs.java b/backend/src/main/java/ch/puzzle/okr/multitenancy/TenantConfigs.java
new file mode 100644
index 0000000000..0a6c22f699
--- /dev/null
+++ b/backend/src/main/java/ch/puzzle/okr/multitenancy/TenantConfigs.java
@@ -0,0 +1,21 @@
+package ch.puzzle.okr.multitenancy;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class TenantConfigs {
+
+ private static final Map tenantConfigs = new ConcurrentHashMap<>();
+
+ public static void add(String tenantId, TenantConfigProvider.TenantConfig tenantConfigProvider) {
+ tenantConfigs.putIfAbsent(tenantId, tenantConfigProvider);
+ }
+
+ public static TenantConfigProvider.TenantConfig get(String tenantId) {
+ return tenantConfigs.get(tenantId);
+ }
+
+ public static void clear() {
+ tenantConfigs.clear();
+ }
+}
diff --git a/backend/src/main/resources/application-dev.properties b/backend/src/main/resources/application-dev.properties
index b995d8a397..5e4f58bcc8 100644
--- a/backend/src/main/resources/application-dev.properties
+++ b/backend/src/main/resources/application-dev.properties
@@ -16,16 +16,18 @@ okr.datasource.driver-class-name=org.postgresql.Driver
# security
connect.src=http://localhost:8544 http://localhost:8545
-# hibernate
+# hibernate (for full db access use: user/pwd)
hibernate.connection.url=jdbc:postgresql://localhost:5432/okr
-hibernate.connection.username=user
+hibernate.connection.username=bootstrap_app
hibernate.connection.password=pwd
hibernate.multiTenancy=SCHEMA
# pitc
okr.tenants.pitc.datasource.url=jdbc:postgresql://localhost:5432/okr
-okr.tenants.pitc.datasource.username=pitc
-okr.tenants.pitc.datasource.password=pwd
+okr.tenants.pitc.datasource.username.fly=pitc_fly
+okr.tenants.pitc.datasource.password.fly=pwd
+okr.tenants.pitc.datasource.username.app=pitc_app
+okr.tenants.pitc.datasource.password.app=pwd
okr.tenants.pitc.datasource.schema=okr_pitc
okr.tenants.pitc.user.champion.emails=gl@gl.com
okr.tenants.pitc.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8544/realms/pitc/protocol/openid-connect/certs
@@ -34,8 +36,10 @@ okr.tenants.pitc.security.oauth2.frontend.client-id=pitc_okr_staging
# acme
okr.tenants.acme.datasource.url=jdbc:postgresql://localhost:5432/okr
-okr.tenants.acme.datasource.username=acme
-okr.tenants.acme.datasource.password=pwd
+okr.tenants.acme.datasource.username.fly=acme_fly
+okr.tenants.acme.datasource.password.fly=pwd
+okr.tenants.acme.datasource.username.app=acme_app
+okr.tenants.acme.datasource.password.app=pwd
okr.tenants.acme.datasource.schema=okr_acme
okr.tenants.acme.user.champion.emails=gl@acme.com
okr.tenants.acme.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8545/realms/acme/protocol/openid-connect/certs
diff --git a/backend/src/main/resources/application-integration-test.properties b/backend/src/main/resources/application-integration-test.properties
index 44769ffd8a..64843cd91c 100644
--- a/backend/src/main/resources/application-integration-test.properties
+++ b/backend/src/main/resources/application-integration-test.properties
@@ -27,8 +27,10 @@ hibernate.multiTenancy=SCHEMA
# pitc
okr.tenants.pitc.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1;INIT=CREATE SCHEMA IF NOT EXISTS okr_pitc
-okr.tenants.pitc.datasource.username=user
-okr.tenants.pitc.datasource.password=sa
+okr.tenants.pitc.datasource.username.fly=user
+okr.tenants.pitc.datasource.password.fly=sa
+okr.tenants.pitc.datasource.username.app=user
+okr.tenants.pitc.datasource.password.app=sa
okr.tenants.pitc.datasource.schema=okr_pitc
okr.tenants.pitc.user.champion.emails=peggimann@puzzle.ch,wunderland@puzzle.ch
okr.tenants.pitc.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8544/realms/pitc/protocol/openid-connect/certs
@@ -37,8 +39,10 @@ okr.tenants.pitc.security.oauth2.frontend.client-id=pitc_okr_staging
# acme
okr.tenants.acme.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1;INIT=CREATE SCHEMA IF NOT EXISTS okr_acme
-okr.tenants.acme.datasource.username=user
-okr.tenants.acme.datasource.password=sa
+okr.tenants.acme.datasource.username.fly=user
+okr.tenants.acme.datasource.password.fly=sa
+okr.tenants.acme.datasource.username.app=user
+okr.tenants.acme.datasource.password.app=sa
okr.tenants.acme.datasource.schema=okr_acme
okr.tenants.acme.user.champion.emails=peggimann@puzzle.ch,wunderland@puzzle.ch
okr.tenants.acme.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8544/realms/pitc/protocol/openid-connect/certs
diff --git a/backend/src/test/java/ch/puzzle/okr/multitenancy/FlywayMultitenantMigrationInitializerTest.java b/backend/src/test/java/ch/puzzle/okr/multitenancy/FlywayMultitenantMigrationInitializerTest.java
index 3ab1cb4eb4..53b7ffa724 100644
--- a/backend/src/test/java/ch/puzzle/okr/multitenancy/FlywayMultitenantMigrationInitializerTest.java
+++ b/backend/src/test/java/ch/puzzle/okr/multitenancy/FlywayMultitenantMigrationInitializerTest.java
@@ -1,7 +1,9 @@
package ch.puzzle.okr.multitenancy;
+import jakarta.persistence.EntityNotFoundException;
import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.configuration.FluentConfiguration;
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
@@ -66,40 +68,45 @@ public String getLog() {
}
}
- private final TenantConfigProviderInterface providerInterfaceMock = new TenantConfigProviderInterface() {
-
- private final TenantConfigProvider.DataSourceConfig dataSourceConfig = new TenantConfigProvider.DataSourceConfig(
- NOT_USED, URL, NAME, PASSWORD, SCHEMA);
-
- private final TenantConfigProvider.TenantConfig tenantConfig = new TenantConfigProvider.TenantConfig(NOT_USED,
- new String[] { NOT_USED }, NOT_USED, NOT_USED, NOT_USED, dataSourceConfig);
-
- @Override
- public List getTenantConfigs() {
- return List.of(tenantConfig);
- }
-
- @Override
- public Optional getTenantConfigById(String tenantId) {
- return Optional.of(tenantConfig);
- }
-
- @Override
- public Optional getJwkSetUri(String tenantId) {
- return Optional.empty();
- }
- };
+ private final TenantConfigProvider.TenantConfig tenantConfig = new TenantConfigProvider.TenantConfig(NOT_USED,
+ new String[] { NOT_USED }, NOT_USED, NOT_USED, NOT_USED,
+ new TenantConfigProvider.DataSourceConfig(NOT_USED, URL, NAME, PASSWORD, SCHEMA), // flyway user config
+ null // app user config; not used in this test
+ );
+
+ private TenantConfigProviderInterface mockTenantConfigProviderInterface(
+ List tenantConfigs, TenantConfigProvider.TenantConfig tenantConfigById) {
+
+ return new TenantConfigProviderInterface() {
+ @Override
+ public List getTenantConfigs() {
+ return tenantConfigs;
+ }
+
+ @Override
+ public Optional getTenantConfigById(String tenantId) {
+ return Optional.ofNullable(tenantConfigById);
+ }
+
+ @Override
+ public Optional getJwkSetUri(String tenantId) {
+ return Optional.empty();
+ }
+ };
+ }
- @DisplayName("Flyway.configure() should return FluentConfiguration which we can assert")
+ @DisplayName("flywayConfigure() should return FluentConfiguration which we can assert")
@Test
void flywayConfigureShouldReturnFluentConfigurationWhichWeCanAssert() {
try (MockedStatic mockedStatic = Mockito.mockStatic(Flyway.class)) {
// arrange
- FluentConfigurationSpy fluentConfiguration = new FluentConfigurationSpy();
+ var fluentConfiguration = new FluentConfigurationSpy();
mockedStatic.when(Flyway::configure).thenReturn(fluentConfiguration);
- FlywayMultitenantMigrationInitializer migrationInitializer = new FlywayMultitenantMigrationInitializer(
- providerInterfaceMock, new String[] { SCRIPT_LOCATION });
+ // returns for getTenantConfigs() and getTenantConfigById() tenantConfig
+ var migrationInitializer = new FlywayMultitenantMigrationInitializer(
+ mockTenantConfigProviderInterface(List.of(tenantConfig), tenantConfig),
+ new String[] { SCRIPT_LOCATION });
// act
migrationInitializer.migrateFlyway();
@@ -111,4 +118,25 @@ void flywayConfigureShouldReturnFluentConfigurationWhichWeCanAssert() {
}
}
+ @DisplayName("flywayConfigure() should throw exception when TenantConfigById is not found")
+ @Test
+ void flywayConfigureShouldThrowExceptionWhenTenantConfigByIdIsNotFound() {
+ try (MockedStatic mockedStatic = Mockito.mockStatic(Flyway.class)) {
+ // arrange
+ var fluentConfiguration = new FluentConfigurationSpy();
+ mockedStatic.when(Flyway::configure).thenReturn(fluentConfiguration);
+
+ // returns for getTenantConfigs() tenantConfig and for getTenantConfigById() empty Optional
+ var migrationInitializer = new FlywayMultitenantMigrationInitializer(
+ mockTenantConfigProviderInterface(List.of(tenantConfig), null), //
+ new String[] { SCRIPT_LOCATION });
+
+ // act + assert
+ var entityNotFoundException = Assertions.assertThrows(EntityNotFoundException.class, //
+ migrationInitializer::migrateFlyway);
+
+ assertEquals("Cannot find tenant for configuring flyway migration", entityNotFoundException.getMessage());
+ }
+ }
+
}
\ No newline at end of file
diff --git a/backend/src/test/java/ch/puzzle/okr/multitenancy/HibernateContextTest.java b/backend/src/test/java/ch/puzzle/okr/multitenancy/HibernateContextTest.java
index 7dbc3a5b16..9e64499915 100644
--- a/backend/src/test/java/ch/puzzle/okr/multitenancy/HibernateContextTest.java
+++ b/backend/src/test/java/ch/puzzle/okr/multitenancy/HibernateContextTest.java
@@ -1,5 +1,6 @@
package ch.puzzle.okr.multitenancy;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -18,11 +19,18 @@
public class HibernateContextTest {
+ private static final String NOT_USED = "not_used";
+
@BeforeEach
void setUp() {
resetHibernateConfig();
}
+ @AfterEach
+ void tearDown() {
+ resetHibernateConfig();
+ }
+
@DisplayName("setHibernateConfig() should throw exception if db config is null")
@Test
void setHibernateConfigShouldThrowExceptionIfDbConfigIsNull() {
@@ -30,7 +38,7 @@ void setHibernateConfigShouldThrowExceptionIfDbConfigIsNull() {
DbConfig dbConfig = null;
// act + assert
- RuntimeException exception = assertThrows(RuntimeException.class, () -> setHibernateConfig(dbConfig));
+ var exception = assertThrows(RuntimeException.class, () -> setHibernateConfig(dbConfig));
assertEquals("Invalid hibernate configuration null", exception.getMessage());
}
@@ -41,10 +49,10 @@ void setHibernateConfigShouldThrowExceptionIfDbConfigHasNullOrEmptyValues(String
String password, String tenant) {
// arrange
- DbConfig dbConfig = new DbConfig(url, username, password, tenant);
+ var dbConfig = new DbConfig(url, username, password, tenant);
// act + assert
- RuntimeException exception = assertThrows(RuntimeException.class, () -> setHibernateConfig(dbConfig));
+ var exception = assertThrows(RuntimeException.class, () -> setHibernateConfig(dbConfig));
assertTrue(exception.getMessage().startsWith("Invalid hibernate configuration"));
}
@@ -60,13 +68,13 @@ private static Stream invalidDbConfig() {
Arguments.of("url", "username", "password", ""));
}
- @DisplayName("extractAndSetHibernateConfig() should extract hibernate properties from environment and set it")
+ @DisplayName("extractAndSetHibernateConfig() should extract hibernate properties from environment and cache it")
@Test
- void extractAndSetHibernateConfigShouldExtractHibernatePropertiesFromEnvironmentAndSetIt() {
+ void extractAndSetHibernateConfigShouldExtractHibernatePropertiesFromEnvironmentAndCacheIt() {
// arrange
String url = "url", username = "username", password = "password", multiTenancy = "multiTenancy";
- ConfigurableEnvironment environment = mock(ConfigurableEnvironment.class);
+ var environment = mock(ConfigurableEnvironment.class);
when(environment.getProperty(HIBERNATE_CONNECTION_URL)).thenReturn(url);
when(environment.getProperty(HIBERNATE_CONNECTION_USERNAME)).thenReturn(username);
when(environment.getProperty(HIBERNATE_CONNECTION_PASSWORD)).thenReturn(password);
@@ -74,7 +82,7 @@ void extractAndSetHibernateConfigShouldExtractHibernatePropertiesFromEnvironment
// act
extractAndSetHibernateConfig(environment);
- Properties hibernateProperties = getHibernateConfig();
+ var hibernateProperties = getHibernateConfig();
// assert
assertNotNull(hibernateProperties);
@@ -84,10 +92,10 @@ void extractAndSetHibernateConfigShouldExtractHibernatePropertiesFromEnvironment
@DisplayName("getHibernateConfig() should throw exception if setHibernateConfig() is not called before with valid configuration")
@Test
void getHibernateConfigShouldThrowExceptionIfSetHibernateConfigIsNotCalledBeforeWithValidConfiguration() {
- // arrange
+ // arrange: no DbConfig is set
// act + assert
- RuntimeException exception = assertThrows(RuntimeException.class, HibernateContext::getHibernateConfig);
+ var exception = assertThrows(RuntimeException.class, HibernateContext::getHibernateConfig);
assertEquals("No cached hibernate configuration found", exception.getMessage());
}
@@ -96,17 +104,67 @@ void getHibernateConfigShouldThrowExceptionIfSetHibernateConfigIsNotCalledBefore
void getHibernateConfigShouldReturnHibernateConfigAsPropertiesIfDbConfigIsValid() {
// arrange
String url = "url", username = "username", password = "password", multiTenancy = "multiTenancy";
- DbConfig dbConfig = new DbConfig(url, username, password, multiTenancy);
+ var dbConfig = new DbConfig(url, username, password, multiTenancy);
setHibernateConfig(dbConfig);
// act
- Properties hibernateProperties = getHibernateConfig();
+ var hibernateProperties = getHibernateConfig();
// assert
assertNotNull(hibernateProperties);
assertProperties(url, username, password, multiTenancy, hibernateProperties);
}
+ @DisplayName("getHibernateConfigForTenantId() should throw exception if setHibernateConfig() is not called before with valid configuration")
+ @Test
+ void getHibernateConfigForTenantIdShouldThrowExceptionIfSetHibernateConfigIsNotCalledBeforeWithValidConfiguration() {
+ // arrange: no DbConfig is set
+
+ // act + assert
+ var exception = assertThrows(RuntimeException.class, () -> getHibernateConfig("tenantId"));
+ assertEquals("No cached hibernate configuration found (for tenant tenantId)", exception.getMessage());
+ }
+
+ @DisplayName("setHibernateConfigForTenantId() should throw exception when no tenant config is cached")
+ @Test
+ void setHibernateConfigForTenantIdShouldThrowExceptionWhenNoTenantConfigIsCached() {
+ // arrange
+ String url = "url", username = "username", password = "password", multiTenancy = "multiTenancy";
+ var dbConfig = new DbConfig(url, username, password, multiTenancy);
+ setHibernateConfig(dbConfig);
+
+ var tenantId = "tenantId"; // but no tenant config is cached
+
+ // act + assert
+ var exception = assertThrows(RuntimeException.class, () -> {
+ getHibernateConfig(tenantId);
+ });
+ assertEquals("No cached tenant configuration found (for tenant tenantId)", exception.getMessage());
+ }
+
+ @DisplayName("getHibernateConfigForTenantId() should return hibernate config with patched tenant username and password as properties if db config is valid")
+ @Test
+ void getHibernateConfigForTenantIdShouldReturnHibernateConfigWithPatchedTenantUserNameAndPasswordAsPropertiesIfDbConfigIsValid() {
+ // arrange
+ String url = "url", username = "username", password = "password", multiTenancy = "multiTenancy";
+ var dbConfig = new DbConfig(url, username, password, multiTenancy);
+ setHibernateConfig(dbConfig);
+
+ String tenantId = "tenantId", tenantName = "tenantName", tenantPassword = "tenantPassword";
+ var tenantConfig = new TenantConfigProvider.TenantConfig(tenantId, new String[] {}, NOT_USED, NOT_USED,
+ NOT_USED, null, // fly user config not used in test
+ new TenantConfigProvider.DataSourceConfig(NOT_USED, NOT_USED, tenantName, tenantPassword, NOT_USED));
+
+ TenantConfigs.add(tenantId, tenantConfig); // cache tenant db config
+
+ // act
+ var hibernateProperties = getHibernateConfig(tenantId);
+
+ // assert
+ assertNotNull(hibernateProperties);
+ assertProperties(url, tenantName, tenantPassword, multiTenancy, hibernateProperties);
+ }
+
private void assertProperties(String url, String username, String password, String multiTenancy,
Properties properties) {
diff --git a/backend/src/test/java/ch/puzzle/okr/multitenancy/SchemaMultiTenantConnectionProviderInternalsTest.java b/backend/src/test/java/ch/puzzle/okr/multitenancy/SchemaMultiTenantConnectionProviderInternalsTest.java
index e8bc0dc7b0..5099c2eb17 100644
--- a/backend/src/test/java/ch/puzzle/okr/multitenancy/SchemaMultiTenantConnectionProviderInternalsTest.java
+++ b/backend/src/test/java/ch/puzzle/okr/multitenancy/SchemaMultiTenantConnectionProviderInternalsTest.java
@@ -2,87 +2,296 @@
import ch.puzzle.okr.exception.ConnectionProviderException;
import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider;
-import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.MockedStatic;
import java.util.Properties;
-import static org.mockito.Mockito.mock;
+import static ch.puzzle.okr.multitenancy.HibernateContext.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
public class SchemaMultiTenantConnectionProviderInternalsTest {
-
private static final String TENANT_ID = "pitc";
+ private static final String TENANT_ID_ACME = "acme";
+ private static final String DEFAULT_TENANT_ID = TenantContext.DEFAULT_TENANT_ID;
+ private static final String NOT_USED = "";
+
+ private static MockedStatic mockedStatic;
+
+ /**
+ * A mock for the SchemaMultiTenantConnectionProvider, which is configurable. It can be configured via static
+ * factory methods: use cachedConnectionProvider(), for a cached ConnectionProvider.Or use
+ * inPropertiesDefinedConnectionProvider(), for no cached ConnectionProvider, but the data for creating a
+ * ConnectionProvider is defined in the (hibernate) properties.
+ */
+ private static class ConnectionProviderMock extends SchemaMultiTenantConnectionProvider {
+
+ public static ConnectionProviderMock cachedConnectionProvider( //
+ String tenantId, ConnectionProvider connectionProvider) {
+ return new ConnectionProviderMock(tenantId, connectionProvider);
+ }
- private static class ConfigurableConnectionProviderMock extends SchemaMultiTenantConnectionProvider {
+ private ConnectionProviderMock(String tenantId, ConnectionProvider connectionProvider) {
+ connectionProviderMap.put(tenantId, connectionProvider);
+ }
+
+ public static ConnectionProviderMock inPropertiesDefinedConnectionProvider( //
+ String tenantId, Properties hibernateProperties) {
+ return new ConnectionProviderMock(tenantId, hibernateProperties);
+ }
+
+ private ConnectionProviderMock(String tenantId, Properties hibernateProperties) {
+ // mock dependencies to HibernateContext used in getHibernateProperties()
+ mockedStatic.when(() -> getHibernateConfig(tenantId)).thenReturn(hibernateProperties);
+ mockedStatic.when(() -> getHibernateConfig()).thenReturn(hibernateProperties);
+ }
@Override
- protected Properties getHibernateProperties() {
- return new Properties();
+ protected ConnectionProvider initConnectionProvider(Properties hibernateProperties) {
+ return mock(ConnectionProvider.class);
}
- public void registerProvider(String tenantIdentifier, ConnectionProvider connectionProvider) {
- connectionProviderMap.put(tenantIdentifier, connectionProvider);
+ public boolean isCached(String tenantIdentifier) {
+ return connectionProviderMap.containsKey(tenantIdentifier);
}
}
- @DisplayName("getConnectionProvider() return ConnectionProvider if TenantId is registered")
+ private Properties getConfigAsProperties(String tenantId) {
+ var properties = new Properties();
+ properties.put(HIBERNATE_CONNECTION_URL, concat("url", tenantId));
+ properties.put(HIBERNATE_CONNECTION_USERNAME, concat("username", tenantId));
+ properties.put(HIBERNATE_CONNECTION_PASSWORD, concat("password", tenantId));
+ properties.put(HIBERNATE_MULTITENANCY, concat("multitenancy", tenantId));
+ properties.put(SPRING_DATASOURCE_URL, concat("url", tenantId));
+ properties.put(SPRING_DATASOURCE_USERNAME, concat("username", tenantId));
+ properties.put(SPRING_DATASOURCE_PASSWORD, concat("password", tenantId));
+ return properties;
+ }
+
+ private String concat(String data, String tenantId) {
+ return data + "_" + tenantId;
+ }
+
+ @BeforeEach
+ public void setUp() {
+ mockedStatic = mockStatic(HibernateContext.class);
+ }
+
+ @AfterEach
+ public void tearDown() {
+ mockedStatic.close();
+ }
+
+ @DisplayName("getConnectionProvider() should throw exception when tenantId is null")
@Test
- void getConnectionProviderReturnConnectionProviderIfTenantIdIsRegistered() {
+ void getConnectionProviderShouldThrowExceptionWhenTenantIdIsNull() {
+ // arrange
+ String tenantId = null;
+ var mockProvider = ConnectionProviderMock.inPropertiesDefinedConnectionProvider(NOT_USED, new Properties());
+
+ // act + assert
+ var exception = assertThrows(ConnectionProviderException.class,
+ () -> mockProvider.getConnectionProvider(tenantId));
+
+ // assert
+ assertEquals("Cannot create new connection provider for tenant: null", exception.getMessage());
+ assertGetHibernateConfigNotCalled();
+ }
+
+ @DisplayName("getConnectionProvider() should return ConnectionProvider from cache")
+ @ParameterizedTest
+ @ValueSource(strings = { TENANT_ID, TENANT_ID_ACME })
+ void getConnectionProviderShouldReturnConnectionProviderFromCache(String tenantId) {
// arrange
- ConfigurableConnectionProviderMock mockProvider = new ConfigurableConnectionProviderMock();
- mockProvider.registerProvider(TENANT_ID, mock(ConnectionProvider.class));
+ var mockProvider = ConnectionProviderMock.cachedConnectionProvider(tenantId, mock(ConnectionProvider.class));
+
+ // pre-conditions
+ assertCached(mockProvider, tenantId);
// act
- ConnectionProvider foundConnectionProvider = mockProvider.getConnectionProvider(TENANT_ID);
+ var foundConnectionProvider = mockProvider.getConnectionProvider(tenantId);
// assert
- Assertions.assertNotNull(foundConnectionProvider);
+ assertNotNull(foundConnectionProvider);
+ assertGetHibernateConfigNotCalled(tenantId);
+
+ // post-condition
+ assertCached(mockProvider, tenantId);
}
- @DisplayName("getConnectionProvider() throws Exception when lookup TenantId is null")
- @Test
- void getConnectionProviderThrowsExceptionWhenLookupTenantIdIsNull() {
+ @DisplayName("getConnectionProvider() should throw exception when ConnectionProvider is not cached and tenant is not configured in hibernate properties")
+ @ParameterizedTest
+ @ValueSource(strings = { TENANT_ID, TENANT_ID_ACME })
+ void getConnectionProviderShouldThrowExceptionWhenConnectionProviderIsNotCachedAndTenantIsNotConfiguredInHibernateProperties(
+ String tenantId) {
// arrange
- ConfigurableConnectionProviderMock mockProvider = new ConfigurableConnectionProviderMock();
+ var mockProvider = ConnectionProviderMock.inPropertiesDefinedConnectionProvider(tenantId, new Properties());
+
+ // pre-conditions
+ assertNotCached(mockProvider, tenantId);
// act + assert
- Assertions.assertThrows(ConnectionProviderException.class, () -> mockProvider.getConnectionProvider(null));
+ var exception = assertThrows(RuntimeException.class, () -> mockProvider.getConnectionProvider(tenantId));
+
+ // assert
+ assertEquals("Cannot load hibernate properties from application.properties", exception.getMessage());
+ assertGetHibernateConfigCalledWithTenantId(tenantId);
+
+ // post-conditions
+ assertNotCached(mockProvider, tenantId);
}
- @DisplayName("selectConnectionProvider() return ConnectionProvider if TenantId is registered")
+ @DisplayName("getConnectionProvider() should cache ConnectionProvider for DEFAULT_TENANT_ID ('public') if ConnectionProvider is not cached but in hibernate properties")
@Test
- void selectConnectionProviderReturnConnectionProviderIfTenantIdIsRegistered() {
+ void getConnectionProviderShouldCacheConnectionProviderForDefaultTenantIdIfConnectionProviderIsNotCachedButInHibernateProperties() {
// arrange
- ConfigurableConnectionProviderMock mockProvider = new ConfigurableConnectionProviderMock();
- mockProvider.registerProvider(TENANT_ID, mock(ConnectionProvider.class));
+ Properties properties = getConfigAsProperties(DEFAULT_TENANT_ID);
+ var mockProvider = ConnectionProviderMock.inPropertiesDefinedConnectionProvider(DEFAULT_TENANT_ID, properties);
+
+ // pre-condition
+ assertNotCached(mockProvider, DEFAULT_TENANT_ID);
// act
- ConnectionProvider foundConnectionProvider = mockProvider.selectConnectionProvider(TENANT_ID);
+ var connectionProvider = mockProvider.getConnectionProvider(DEFAULT_TENANT_ID);
// assert
- Assertions.assertNotNull(foundConnectionProvider);
+ assertNotNull(connectionProvider);
+ assertGetHibernateConfigCalled(DEFAULT_TENANT_ID);
+
+ // post-condition
+ assertCached(mockProvider, DEFAULT_TENANT_ID);
}
- @DisplayName("getAnyConnectionProvider() return ConnectionProvider for TenantId public")
+ @DisplayName("selectConnectionProvider() should cache ConnectionProvider for DEFAULT_TENANT_ID ('public') if ConnectionProvider is not cached but in hibernate properties")
@Test
- void getAnyConnectionProviderReturnConnectionProviderForTenantIdPublic() {
+ void selectConnectionProviderShouldCacheConnectionProviderForDefaultTenantIdIfConnectionProviderIsNotCachedButInHibernateProperties() {
// arrange
- ConfigurableConnectionProviderMock mockProvider = new ConfigurableConnectionProviderMock();
- mockProvider.registerProvider("public", mock(ConnectionProvider.class));
+ Properties properties = getConfigAsProperties(DEFAULT_TENANT_ID);
+ var mockProvider = ConnectionProviderMock.inPropertiesDefinedConnectionProvider(DEFAULT_TENANT_ID, properties);
+
+ // pre-condition
+ assertNotCached(mockProvider, DEFAULT_TENANT_ID);
// act
- ConnectionProvider foundConnectionProvider = mockProvider.getAnyConnectionProvider();
+ var connectionProvider = mockProvider.selectConnectionProvider(DEFAULT_TENANT_ID);
// assert
- Assertions.assertNotNull(foundConnectionProvider);
+ assertNotNull(connectionProvider);
+ assertGetHibernateConfigCalled(DEFAULT_TENANT_ID);
+
+ // post-condition
+ assertCached(mockProvider, DEFAULT_TENANT_ID);
}
- @DisplayName("getConnectionProviderShouldThrowRuntimeExceptionWhenNoPropertiesAreFound")
+ @DisplayName("getAnyConnectionProvider() should cache ConnectionProvider for DEFAULT_TENANT_ID ('public') if ConnectionProvider is not cached but in hibernate properties")
@Test
- void getConnectionProviderShouldThrowRuntimeExceptionWhenNoPropertiesAreFound() {
- ConfigurableConnectionProviderMock mockProvider = new ConfigurableConnectionProviderMock();
+ void getAnyConnectionProviderShouldCacheConnectionProviderForDefaultTenantIdIfConnectionProviderIsNotCachedButInHibernateProperties() {
+ // arrange
+ Properties properties = getConfigAsProperties(DEFAULT_TENANT_ID);
+ var mockProvider = ConnectionProviderMock.inPropertiesDefinedConnectionProvider(DEFAULT_TENANT_ID, properties);
+
+ // pre-condition
+ assertNotCached(mockProvider, DEFAULT_TENANT_ID);
+
+ // act
+ var connectionProvider = mockProvider.getAnyConnectionProvider();
- Assertions.assertThrows(RuntimeException.class, () -> mockProvider.getConnectionProvider(TENANT_ID));
+ // assert
+ assertNotNull(connectionProvider);
+ assertGetHibernateConfigCalled(DEFAULT_TENANT_ID);
+
+ // post-condition
+ assertCached(mockProvider, DEFAULT_TENANT_ID);
+ }
+
+ @DisplayName("getConnectionProvider() should cache ConnectionProvider for tenantId if ConnectionProvider is not cached but in hibernate properties")
+ @ParameterizedTest
+ @ValueSource(strings = { TENANT_ID, TENANT_ID_ACME })
+ void getConnectionProviderShouldCacheConnectionProviderForTenantIdIfConnectionProviderIsNotCachedButInHibernateProperties(
+ String tenantId) {
+ // arrange
+ Properties properties = getConfigAsProperties(tenantId);
+ var mockProvider = ConnectionProviderMock.inPropertiesDefinedConnectionProvider(tenantId, properties);
+
+ // pre-condition
+ assertNotCached(mockProvider, tenantId);
+
+ // act
+ var connectionProvider = mockProvider.getConnectionProvider(tenantId);
+
+ // assert
+ assertNotNull(connectionProvider);
+ assertGetHibernateConfigCalledWithTenantId(tenantId);
+
+ // post-condition
+ assertCached(mockProvider, tenantId);
+ }
+
+ @DisplayName("convertPropertiesToMap() should convert properties to map")
+ @Test
+ void convertPropertiesToMapShouldConvertPropertiesToMap() {
+ // arrange
+ var connectionProvider = new SchemaMultiTenantConnectionProvider();
+ var properties = getConfigAsProperties(TENANT_ID);
+
+ // act
+ var map = connectionProvider.convertPropertiesToMap(properties);
+
+ // assert
+ assertEquals(properties.size(), map.size());
+
+ assertEquals("url_pitc", map.get(HIBERNATE_CONNECTION_URL));
+ assertEquals("username_pitc", map.get(HIBERNATE_CONNECTION_USERNAME));
+ assertEquals("password_pitc", map.get(HIBERNATE_CONNECTION_PASSWORD));
+ assertEquals("multitenancy_pitc", map.get(HIBERNATE_MULTITENANCY));
+ assertEquals("url_pitc", map.get(SPRING_DATASOURCE_URL));
+ assertEquals("username_pitc", map.get(SPRING_DATASOURCE_USERNAME));
+ assertEquals("password_pitc", map.get(SPRING_DATASOURCE_PASSWORD));
}
+
+ @DisplayName("connectionProvider() should return the schema name for tenantId")
+ @Test
+ void connectionProviderShouldReturnTheSchemaNameForTenantId() {
+ // arrange
+ var connectionProvider = new SchemaMultiTenantConnectionProvider();
+ // act
+ var schema = connectionProvider.convertTenantIdToSchemaName(TENANT_ID);
+ // assert
+ assertEquals("okr_pitc", schema);
+ }
+
+ private void assertCached(ConnectionProviderMock mockProvider, String tenantId) {
+ assertTrue(mockProvider.isCached(tenantId));
+ }
+
+ private void assertNotCached(ConnectionProviderMock mockProvider, String tenantId) {
+ assertFalse(mockProvider.isCached(tenantId));
+ }
+
+ private void assertGetHibernateConfigCalledWithTenantId(String tenantId) {
+ mockedStatic.verify(() -> getHibernateConfig(tenantId));
+ mockedStatic.verify(() -> getHibernateConfig(), never());
+ }
+
+ private void assertGetHibernateConfigCalled(String tenantId) {
+ mockedStatic.verify(() -> getHibernateConfig());
+ mockedStatic.verify(() -> getHibernateConfig(tenantId), never());
+ }
+
+ private void assertGetHibernateConfigNotCalled() {
+ mockedStatic.verify(() -> getHibernateConfig(), never());
+ mockedStatic.verify(() -> getHibernateConfig(anyString()), never());
+ }
+
+ private void assertGetHibernateConfigNotCalled(String tenantId) {
+ mockedStatic.verify(() -> getHibernateConfig(), never());
+ mockedStatic.verify(() -> getHibernateConfig(tenantId), never());
+ }
+
}
diff --git a/backend/src/test/java/ch/puzzle/okr/multitenancy/TenantConfigProviderTestIT.java b/backend/src/test/java/ch/puzzle/okr/multitenancy/TenantConfigProviderTestIT.java
index 940473d020..4aaf694f7f 100644
--- a/backend/src/test/java/ch/puzzle/okr/multitenancy/TenantConfigProviderTestIT.java
+++ b/backend/src/test/java/ch/puzzle/okr/multitenancy/TenantConfigProviderTestIT.java
@@ -1,6 +1,7 @@
package ch.puzzle.okr.multitenancy;
import ch.puzzle.okr.test.SpringIntegrationTest;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -24,8 +25,10 @@ public class TenantConfigProviderTestIT {
private static final String FRONTEND_CLIENT_ISSUER_URL = "frontendClientIssuerUrl";
private static final String FRONTEND_CLIENT_ID = "frontendClientId";
private static final String DATASOURCE_URL = "datasourceUrl";
- private static final String DATASOURCE_NAME = "datasourceName";
- private static final String DATASOURCE_PASSWORD = "datasourcePassword";
+ private static final String DATASOURCE_NAME_FLY = "datasourceNameFly";
+ private static final String DATASOURCE_PASSWORD_FLY = "datasourcePasswordFly";
+ private static final String DATASOURCE_NAME_APP = "datasourceNameApp";
+ private static final String DATASOURCE_PASSWORD_APP = "datasourcePasswordApp";
private static final String DATASOURCE_SCHEMA = "datasourceSchema";
private static final String DRIVER_CLASS_NAME = "driverClassName";
private static final String CHAMPION_EMAILS_1 = "a@pitc.ch";
@@ -37,11 +40,19 @@ public class TenantConfigProviderTestIT {
@Mock
private Environment env;
+ private TenantConfigProvider configProvider;
+
@BeforeEach
void setUp() {
for (String tenantId : tenantIds) {
setupPropertiesForTenantWithId(tenantId);
}
+ configProvider = new TenantConfigProvider(tenantIds, env);
+ }
+
+ @AfterEach
+ void tearDown() {
+ TenantConfigProvider.clearTenantConfigsCache();
}
private void setupPropertiesForTenantWithId(String id) {
@@ -49,8 +60,10 @@ private void setupPropertiesForTenantWithId(String id) {
mockProperty("okr.tenants.{0}.security.oauth2.frontend.issuer-url", FRONTEND_CLIENT_ISSUER_URL, id);
mockProperty("okr.tenants.{0}.security.oauth2.frontend.client-id", FRONTEND_CLIENT_ID, id);
mockProperty("okr.tenants.{0}.datasource.url", DATASOURCE_URL, id);
- mockProperty("okr.tenants.{0}.datasource.username", DATASOURCE_NAME, id);
- mockProperty("okr.tenants.{0}.datasource.password", DATASOURCE_PASSWORD, id);
+ mockProperty("okr.tenants.{0}.datasource.username.fly", DATASOURCE_NAME_FLY, id);
+ mockProperty("okr.tenants.{0}.datasource.password.fly", DATASOURCE_PASSWORD_FLY, id);
+ mockProperty("okr.tenants.{0}.datasource.username.app", DATASOURCE_NAME_APP, id);
+ mockProperty("okr.tenants.{0}.datasource.password.app", DATASOURCE_PASSWORD_APP, id);
mockProperty("okr.tenants.{0}.datasource.schema", DATASOURCE_SCHEMA, id);
mockProperty("okr.datasource.driver-class-name", DRIVER_CLASS_NAME);
@@ -78,10 +91,10 @@ private String prefix(String tenantId) {
@DisplayName("getTenantConfigs returns all TenantConfigs as List")
@Test
public void testGetTenantConfigs() {
- TenantConfigProvider configProvider = new TenantConfigProvider(tenantIds, env);
List tenantConfigs = configProvider.getTenantConfigs();
for (TenantConfigProvider.TenantConfig config : tenantConfigs) {
assertTenantConfigProvider(config);
+ assertTenantConfigIsCached(config.tenantId());
}
}
@@ -89,7 +102,6 @@ public void testGetTenantConfigs() {
@ParameterizedTest
@CsvSource({ "pitc, acme" })
void testGetTenantConfigByIdForExistingTenantId(String tenantId) {
- TenantConfigProvider configProvider = new TenantConfigProvider(tenantIds, env);
Optional config = configProvider.getTenantConfigById(tenantId);
assertTrue(config.isPresent());
assertTenantConfigProvider(config.get());
@@ -99,7 +111,6 @@ void testGetTenantConfigByIdForExistingTenantId(String tenantId) {
@ParameterizedTest
@CsvSource({ "PITC-London" })
void testGetTenantConfigByIdForNonExistingTenantId(String nonExistingTenantId) {
- TenantConfigProvider configProvider = new TenantConfigProvider(tenantIds, env);
Optional config = configProvider.getTenantConfigById(nonExistingTenantId);
assertTrue(config.isEmpty());
}
@@ -108,9 +119,6 @@ void testGetTenantConfigByIdForNonExistingTenantId(String nonExistingTenantId) {
@ParameterizedTest
@CsvSource({ "pitc", "acme" })
void testGetJwkSetUriForExistingTenantId(String tenantId) {
- // arrange
- TenantConfigProvider configProvider = new TenantConfigProvider(tenantIds, env);
-
// act
Optional jwkSetUri = configProvider.getJwkSetUri(tenantId);
@@ -123,7 +131,6 @@ void testGetJwkSetUriForExistingTenantId(String tenantId) {
@ParameterizedTest
@CsvSource({ "PITC-London" })
void testGetJwkSetUriForNonExistingTenantId(String nonExistingTenantId) {
- TenantConfigProvider configProvider = new TenantConfigProvider(tenantIds, env);
Optional jwkSetUri = configProvider.getJwkSetUri(nonExistingTenantId);
assertTrue(jwkSetUri.isEmpty());
}
@@ -134,13 +141,20 @@ private void assertTenantConfigProvider(TenantConfigProvider.TenantConfig tenant
assertEquals(prefix(tenantId) + JWK_SET_URI, tenantConfig.jwkSetUri());
assertEquals(prefix(tenantId) + FRONTEND_CLIENT_ISSUER_URL, tenantConfig.issuerUrl());
assertEquals(prefix(tenantId) + FRONTEND_CLIENT_ID, tenantConfig.clientId());
- assertEquals(prefix(tenantId) + DATASOURCE_URL, tenantConfig.dataSourceConfig().url());
- assertEquals(prefix(tenantId) + DATASOURCE_NAME, tenantConfig.dataSourceConfig().name());
- assertEquals(prefix(tenantId) + DATASOURCE_PASSWORD, tenantConfig.dataSourceConfig().password());
- assertEquals(prefix(tenantId) + DATASOURCE_SCHEMA, tenantConfig.dataSourceConfig().schema());
+ assertEquals(prefix(tenantId) + DATASOURCE_URL, tenantConfig.dataSourceConfigFlyway().url());
+ assertEquals(prefix(tenantId) + DATASOURCE_NAME_FLY, tenantConfig.dataSourceConfigFlyway().name());
+ assertEquals(prefix(tenantId) + DATASOURCE_PASSWORD_FLY, tenantConfig.dataSourceConfigFlyway().password());
+ assertEquals(prefix(tenantId) + DATASOURCE_NAME_APP, tenantConfig.dataSourceConfigApp().name());
+ assertEquals(prefix(tenantId) + DATASOURCE_PASSWORD_APP, tenantConfig.dataSourceConfigApp().password());
+ assertEquals(prefix(tenantId) + DATASOURCE_SCHEMA, tenantConfig.dataSourceConfigApp().schema());
assertArrayEquals(new String[] { CHAMPION_EMAILS_1, CHAMPION_EMAILS_2 }, tenantConfig.okrChampionEmails());
- assertEquals(DRIVER_CLASS_NAME, tenantConfig.dataSourceConfig().driverClassName());
+ assertEquals(DRIVER_CLASS_NAME, tenantConfig.dataSourceConfigFlyway().driverClassName());
}
+ private void assertTenantConfigIsCached(String tenantId) {
+ var cachedConfig = TenantConfigProvider.getCachedTenantConfig(tenantId);
+ assertNotNull(cachedConfig);
+ assertTenantConfigProvider(cachedConfig);
+ }
}
diff --git a/backend/src/test/java/ch/puzzle/okr/multitenancy/TenantConfigsTest.java b/backend/src/test/java/ch/puzzle/okr/multitenancy/TenantConfigsTest.java
new file mode 100644
index 0000000000..8eb3119c0e
--- /dev/null
+++ b/backend/src/test/java/ch/puzzle/okr/multitenancy/TenantConfigsTest.java
@@ -0,0 +1,70 @@
+package ch.puzzle.okr.multitenancy;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class TenantConfigsTest {
+
+ private static final String PITC = "pitc";
+ private static final String ACME = "acme";
+
+ @BeforeEach
+ void setUp() {
+ TenantConfigs.clear();
+ }
+
+ @AfterEach
+ void tearDown() {
+ TenantConfigs.clear();
+ }
+
+ private TenantConfigProvider.TenantConfig createTenantConfig(String tenantId) {
+ return new TenantConfigProvider.TenantConfig( //
+ tenantId, null, "jwkSetUri", "issuerUrl", "clientId", //
+ new TenantConfigProvider.DataSourceConfig("driverClassName", "url", //
+ "flyway_" + tenantId, "password", "schema_" + tenantId),
+ new TenantConfigProvider.DataSourceConfig("driverClassName", "url", //
+ "app_" + tenantId, "password", "schema_" + tenantId));
+ }
+
+ @DisplayName("add() should add TenantConfig for tenantId in the map")
+ @ParameterizedTest
+ @ValueSource(strings = { PITC, ACME })
+ void addShouldAddTenantConfigForTenantIdToMap(String tenantId) {
+ // arrange + act
+ TenantConfigs.add(tenantId, createTenantConfig(tenantId));
+
+ // act
+ var currentTenantConfig = TenantConfigs.get(tenantId);
+
+ // assert
+ assertTenantConfig(tenantId, currentTenantConfig);
+ }
+
+ @DisplayName("add() should not override TenantConfig for existing tenantId in the map")
+ @Test
+ void addShouldNotOverrideTenantConfigForExistingTenantIdInMap() {
+ // arrange + act
+ TenantConfigs.add(PITC, createTenantConfig(PITC));
+ TenantConfigs.add(PITC, createTenantConfig("dummy"));
+
+ // act
+ var currentTenantConfig = TenantConfigs.get(PITC);
+
+ // assert
+ assertTenantConfig(PITC, currentTenantConfig);
+ }
+
+ private void assertTenantConfig(String tenantId, TenantConfigProvider.TenantConfig current) {
+ assertEquals(tenantId, current.tenantId());
+ assertEquals("flyway_" + tenantId, current.dataSourceConfigFlyway().name());
+ assertEquals("app_" + tenantId, current.dataSourceConfigApp().name());
+ }
+
+}
diff --git a/backend/src/test/java/ch/puzzle/okr/security/JwtHelperTest.java b/backend/src/test/java/ch/puzzle/okr/security/JwtHelperTest.java
index 35f0367478..e454e38aaf 100644
--- a/backend/src/test/java/ch/puzzle/okr/security/JwtHelperTest.java
+++ b/backend/src/test/java/ch/puzzle/okr/security/JwtHelperTest.java
@@ -84,7 +84,7 @@ void getTenantFromTokenReturnsTenantIfTenantFoundInTenantConfigProvider() {
when(tenantConfigProviderMock.getTenantConfigById(PITC)).thenReturn(Optional.of( //
new TenantConfigProvider.TenantConfig(PITC, //
new String[] {}, "jwkSetUri", "issuerUrl", //
- "clientId", null) //
+ "clientId", null, null) //
));
JwtHelper jwtHelper = new JwtHelper(tenantConfigProviderMock, null, null, null);
@@ -122,7 +122,7 @@ void getTenantFromJWTClaimsSetReturnsTenantIfTenantFoundInTenantConfigProvider()
when(tenantConfigProviderWithDataMock.getTenantConfigById(PITC)).thenReturn(Optional.of( //
new TenantConfigProvider.TenantConfig(PITC, //
new String[] {}, "jwkSetUri", "issuerUrl", //
- "clientId", null) //
+ "clientId", null, null) //
));
JwtHelper jwtHelper = new JwtHelper(tenantConfigProviderWithDataMock, null, null, null);
diff --git a/backend/src/test/java/ch/puzzle/okr/security/TenantJwtIssuerValidatorTest.java b/backend/src/test/java/ch/puzzle/okr/security/TenantJwtIssuerValidatorTest.java
index 7c675c4204..cb94be43c2 100644
--- a/backend/src/test/java/ch/puzzle/okr/security/TenantJwtIssuerValidatorTest.java
+++ b/backend/src/test/java/ch/puzzle/okr/security/TenantJwtIssuerValidatorTest.java
@@ -51,7 +51,7 @@ void validateReturnOAuth2TokenValidatorResultIfTenantConfigWithIssuerUrlIsFound(
when(tenantConfigProviderWithPitcConfig.getTenantConfigById(PITC)).thenReturn(Optional.of( //
new TenantConfigProvider.TenantConfig( //
PITC, new String[] {}, "jwkSetUri", //
- ISSUER_URL, "clientId", null)));
+ ISSUER_URL, "clientId", null, null)));
TenantJwtIssuerValidator tenantJwtIssuerValidator = new TenantJwtIssuerValidator(
tenantConfigProviderWithPitcConfig, jwtHelper) {
diff --git a/backend/src/test/java/ch/puzzle/okr/service/authorization/UserUpdateHelperTest.java b/backend/src/test/java/ch/puzzle/okr/service/authorization/UserUpdateHelperTest.java
index 958eaac311..32838f4882 100644
--- a/backend/src/test/java/ch/puzzle/okr/service/authorization/UserUpdateHelperTest.java
+++ b/backend/src/test/java/ch/puzzle/okr/service/authorization/UserUpdateHelperTest.java
@@ -47,7 +47,7 @@ void updateUserAsNoChampion() {
// arrange
User noChampionUser = User.Builder.builder().withEmail("no@champions.ch").build();
TenantConfigProvider.TenantConfig tenantConfig = new TenantConfigProvider.TenantConfig(null, //
- new String[] { "yes@champions.ch" }, null, null, null, null);
+ new String[] { "yes@champions.ch" }, null, null, null, null, null);
// act
User updatedUser = helper.setOkrChampionFromProperties(noChampionUser, tenantConfig);
@@ -61,7 +61,7 @@ void updateUserAsChampion() {
// arrange
User championUser = User.Builder.builder().withEmail("yes@champions.ch").build();
TenantConfigProvider.TenantConfig tenantConfig = new TenantConfigProvider.TenantConfig(null, //
- new String[] { "yes@champions.ch" }, null, null, null, null);
+ new String[] { "yes@champions.ch" }, null, null, null, null, null);
// act
User updatedUser = helper.setOkrChampionFromProperties(championUser, tenantConfig);
diff --git a/backend/src/test/java/ch/puzzle/okr/service/clientconfig/ClientConfigServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/clientconfig/ClientConfigServiceTest.java
index aced05adae..3e5b180fe5 100644
--- a/backend/src/test/java/ch/puzzle/okr/service/clientconfig/ClientConfigServiceTest.java
+++ b/backend/src/test/java/ch/puzzle/okr/service/clientconfig/ClientConfigServiceTest.java
@@ -102,7 +102,7 @@ private TenantConfigProvider.TenantConfig getTenantConfig(String tenantId) {
prefix(tenantId) + "jwkSetUri", //
prefix(tenantId) + "issuerUrl", //
prefix(tenantId) + "clientId", //
- null);
+ null, null);
}
private TenantClientCustomization getTenantClientCustomization(String tenantId) {
diff --git a/docker/dataset/init.sql b/docker/dataset/init.sql
index 9824907b93..cba97a314e 100644
--- a/docker/dataset/init.sql
+++ b/docker/dataset/init.sql
@@ -1,15 +1,56 @@
--- pitc
-create user pitc with encrypted password 'pwd';
+-- flyway user for pitc
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+create user pitc_fly with encrypted password 'pwd';
create schema if not exists okr_pitc;
-grant create on schema okr_pitc to pitc;
-grant all privileges on all tables in schema okr_pitc to pitc;
-grant all privileges on all sequences in schema okr_pitc to pitc;
-grant usage on schema okr_pitc to pitc;
+grant create on schema okr_pitc to pitc_fly; -- "create" permissions on schema okr_pitc
+grant usage on schema okr_pitc to pitc_fly; -- "usage" permissions on schema okr_pitc
--- acme
-create user acme with encrypted password 'pwd';
+-- grants the highest possible level of access to all objects (tables, sequences) within the okr_pitc schema to the user account pitc_fly.
+-- pitc_fly will have complete access to all objects (tables, sequences) within the okr_pitc schema and can perform any operation on those objects,
+-- including:
+-- Creating new tables
+-- Modifying existing tables (e.g., adding or dropping columns)
+-- Dropping tables
+-- Executing queries against tables
+grant all privileges on all tables in schema okr_pitc to pitc_fly;
+grant all privileges on all sequences in schema okr_pitc to pitc_fly;
+
+
+-- flyway user for acme (has access (read, write, truncate etc.) to schema okr_acme)
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+create user acme_fly with encrypted password 'pwd';
create schema if not exists okr_acme;
-grant create on schema okr_acme to acme;
-grant all privileges on all tables in schema okr_acme to acme;
-grant all privileges on all sequences in schema okr_acme to acme;
-grant usage on schema okr_acme to acme;
\ No newline at end of file
+grant create on schema okr_acme to acme_fly;
+grant usage on schema okr_acme to acme_fly;
+grant all privileges on all tables in schema okr_acme to acme_fly;
+grant all privileges on all sequences in schema okr_acme to acme_fly;
+
+
+-- app user for no specific tenant (has no access to okr_pitc/okr_acme schemas)
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+create user bootstrap_app with encrypted password 'pwd';
+grant usage on schema public to bootstrap_app;
+
+
+-- pitc application user
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+create user pitc_app with encrypted password 'pwd';
+grant usage on schema okr_pitc to pitc_app;
+
+-- grant select/insert/update/delete to user pitc_app in schema okr_pitc
+alter default privileges for user pitc_fly in schema okr_pitc
+ grant select, insert, update, delete on tables to pitc_app;
+
+alter default privileges for user pitc_fly in schema okr_pitc
+ grant usage, select on sequences to pitc_app;
+
+
+-- acme application user
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+create user acme_app with encrypted password 'pwd';
+grant usage on schema okr_acme to acme_app;
+alter default privileges for user acme_fly in schema okr_acme
+ grant select, insert, update, delete on tables to acme_app;
+
+alter default privileges for user acme_fly in schema okr_acme
+ grant usage, select on sequences to acme_app;
diff --git a/frontend/src/assets/i18n/de.json b/frontend/src/assets/i18n/de.json
index be9c8d61a4..fb5e054302 100644
--- a/frontend/src/assets/i18n/de.json
+++ b/frontend/src/assets/i18n/de.json
@@ -65,6 +65,38 @@
}
}
},
+ "CONFIRMATION": {
+ "DRAFT_CREATE": {
+ "TITLE": "Check-in im Draft-Status",
+ "TEXT": "Dein Objective befindet sich noch im DRAFT Status. Möchtest du das Check-in trotzdem erfassen?"
+ },
+ "RELEASE": {
+ "TITLE": "Objective veröffentlichen",
+ "TEXT": "Soll dieses Objective veröffentlicht werden?"
+ },
+ "TO_DRAFT": {
+ "TITLE": "Objective als Draft speichern",
+ "TEXT": "Soll dieses Objective als Draft gespeichert werden?"
+ },
+ "DELETE": {
+ "ACTION":{
+ "TITLE": "Löschen bestätigen",
+ "TEXT": "Möchtest du die Action wirklich löschen?"
+ },
+ "TEAM":{
+ "TITLE": "Löschen bestätigen",
+ "TEXT": "Möchtest du dieses Team wirklich löschen? Zugehörige Objectives werden dadurch in allen Quartalen ebenfalls gelöscht!"
+ },
+ "OBJECTIVE":{
+ "TITLE": "Objective löschen",
+ "TEXT": "Möchtest du dieses Objective wirklich löschen? Zugehörige Key Results werden dadurch ebenfalls gelöscht!"
+ },
+ "KEYRESULT":{
+ "TITLE": "Key Result löschen",
+ "TEXT": "Möchtest du dieses Key Result wirklich löschen? Zugehörige Check-ins werden dadurch ebenfalls gelöscht!"
+ }
+ }
+ },
"ERROR": {
"UNAUTHORIZED": "Du bist nicht autorisiert, um das Objekt mit der Id {1} zu öffnen.",
"NOT_FOUND": "Das Objekt '{0}' mit der Id {1} konnte nicht gefunden werden.",