This is an automated email from the ASF dual-hosted git repository.
sergehuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/unomi.git
The following commit(s) were added to refs/heads/master by this push:
new e9c6439fe UNOMI-980: create the compatibility tenant, and name the
mode for what it does (#861)
e9c6439fe is described below
commit e9c6439fed4de3dcd0644316e5e537b041a7b765
Author: Jonathan SINOVASSIN-NAIK <[email protected]>
AuthorDate: Sat Sep 26 17:23:19 2026 +0200
UNOMI-980: create the compatibility tenant, and name the mode for what it
does (#861)
Co-authored-by: Serge Huber <[email protected]>
---
.../apache/unomi/api/tenants/TenantService.java | 12 ++
.../test/java/org/apache/unomi/itests/AllITs.java | 2 +-
.../apache/unomi/itests/CorePersistenceITs.java | 2 +-
...T.java => SingleTenantCompatibilityModeIT.java} | 164 +++++++--------------
.../asciidoc/migrations/migrate-3.0-to-3.1.adoc | 17 ++-
.../asciidoc/migrations/v2-compatibility-mode.adoc | 84 +++++------
manual/src/main/asciidoc/whats-new.adoc | 2 +-
.../main/resources/etc/custom.system.properties | 3 +-
.../rest/authentication/AuthenticationFilter.java | 93 ++++++------
.../authentication/RestAuthenticationConfig.java | 13 +-
.../impl/DefaultRestAuthenticationConfig.java | 52 ++-----
.../rest/service/impl/RestServiceUtilsImpl.java | 16 +-
.../org.apache.unomi.rest.authentication.cfg | 7 +-
.../AuthenticationFilterBlankPasswordTest.java | 17 +--
.../services/impl/tenants/TenantServiceImpl.java | 19 +++
.../unomi/services/impl/TestTenantService.java | 6 +
16 files changed, 224 insertions(+), 285 deletions(-)
diff --git a/api/src/main/java/org/apache/unomi/api/tenants/TenantService.java
b/api/src/main/java/org/apache/unomi/api/tenants/TenantService.java
index a5fdb0ab5..dff0128b2 100644
--- a/api/src/main/java/org/apache/unomi/api/tenants/TenantService.java
+++ b/api/src/main/java/org/apache/unomi/api/tenants/TenantService.java
@@ -61,6 +61,18 @@ public interface TenantService {
* @param tenantId the ID of the tenant to retrieve
* @return the Tenant object if found, null otherwise
*/
+ /**
+ * Returns the tenant with the given identifier, and creates it when it
does not exist yet.
+ * <p>
+ * Two callers that race on the same identifier both get the tenant, and
only one creation
+ * happens.
+ *
+ * @param tenantId the identifier of the tenant
+ * @param properties the properties to set when the tenant has to be
created, may be {@code null}
+ * @return the tenant, never {@code null}
+ */
+ Tenant getOrCreateTenant(String tenantId, Map<String, Object> properties);
+
Tenant getTenant(String tenantId);
/**
diff --git a/itests/src/test/java/org/apache/unomi/itests/AllITs.java
b/itests/src/test/java/org/apache/unomi/itests/AllITs.java
index 19b07796b..552ad1599 100644
--- a/itests/src/test/java/org/apache/unomi/itests/AllITs.java
+++ b/itests/src/test/java/org/apache/unomi/itests/AllITs.java
@@ -77,7 +77,7 @@ import org.junit.runners.Suite.SuiteClasses;
GraphQLProfileAliasesIT.class,
SendEventActionIT.class,
ScopeIT.class,
- V2CompatibilityModeIT.class,
+ SingleTenantCompatibilityModeIT.class,
CrudCommandsIT.class,
CacheCommandsIT.class,
TailCommandsIT.class,
diff --git
a/itests/src/test/java/org/apache/unomi/itests/CorePersistenceITs.java
b/itests/src/test/java/org/apache/unomi/itests/CorePersistenceITs.java
index 705356297..3d19d4538 100644
--- a/itests/src/test/java/org/apache/unomi/itests/CorePersistenceITs.java
+++ b/itests/src/test/java/org/apache/unomi/itests/CorePersistenceITs.java
@@ -75,7 +75,7 @@ import org.junit.runners.Suite.SuiteClasses;
GraphQLProfileAliasesIT.class,
SendEventActionIT.class,
ScopeIT.class,
- V2CompatibilityModeIT.class,
+ SingleTenantCompatibilityModeIT.class,
CrudCommandsIT.class,
CacheCommandsIT.class,
TailCommandsIT.class,
diff --git
a/itests/src/test/java/org/apache/unomi/itests/V2CompatibilityModeIT.java
b/itests/src/test/java/org/apache/unomi/itests/SingleTenantCompatibilityModeIT.java
similarity index 76%
rename from
itests/src/test/java/org/apache/unomi/itests/V2CompatibilityModeIT.java
rename to
itests/src/test/java/org/apache/unomi/itests/SingleTenantCompatibilityModeIT.java
index f27089368..8291e60c8 100644
--- a/itests/src/test/java/org/apache/unomi/itests/V2CompatibilityModeIT.java
+++
b/itests/src/test/java/org/apache/unomi/itests/SingleTenantCompatibilityModeIT.java
@@ -54,25 +54,27 @@ import java.util.Objects;
import static org.junit.Assert.*;
/**
- * Integration tests for V2 compatibility mode authentication.
+ * Integration tests for single-tenant compatibility mode authentication.
* Tests the behavior when switching between V2 and V3 authentication modes
* using OSGi configuration admin without restarting bundles.
*/
@RunWith(PaxExam.class)
@ExamReactorStrategy(PerSuite.class)
-public class V2CompatibilityModeIT extends BaseIT {
+public class SingleTenantCompatibilityModeIT extends BaseIT {
- private final static Logger LOGGER =
LoggerFactory.getLogger(V2CompatibilityModeIT.class);
+ private final static Logger LOGGER =
LoggerFactory.getLogger(SingleTenantCompatibilityModeIT.class);
private final static String CONTEXT_URL = "/cxs/context.json";
private static final String TEST_SCOPE = "testScope";
private String TEST_SESSION_ID;
+ /** The tenant the single-tenant compatibility mode runs on, as
AuthenticationFilter names it. */
+ private static final String COMPATIBILITY_TENANT_ID = "default";
+
private String TEST_PROFILE_ID;
private final static String UNOMI_API_KEY_HEADER = "X-Unomi-Api-Key";
private final static String UNOMI_TENANT_ID_HEADER = "X-Unomi-Tenant-Id";
private final static String UNOMI_PEER_HEADER = "X-Unomi-Peer";
private boolean originalV2Mode;
- private String originalDefaultTenantId;
private V2ThirdPartyConfigService v2ThirdPartyConfigService;
@Before
@@ -82,26 +84,30 @@ public class V2CompatibilityModeIT extends BaseIT {
v2ThirdPartyConfigService =
getService(V2ThirdPartyConfigService.class);
TestUtils.createScope(TEST_SCOPE, "Test scope", scopeService);
+
+ // The compatibility mode runs on its own tenant, not the one BaseIT
works in, so an event it
+ // carries is validated against the scopes of that tenant. Create the
scope there too, then
+ // put the context back where BaseIT left it.
+
executionContextManager.setCurrentContext(executionContextManager.createContext(COMPATIBILITY_TENANT_ID));
+ try {
+ TestUtils.createScope(TEST_SCOPE, "Test scope", scopeService);
+ } finally {
+
executionContextManager.setCurrentContext(executionContextManager.createContext(testTenant.getItemId()));
+ }
keepTrying("Scope "+ TEST_SCOPE +" not found in the required time", ()
-> scopeService.getScope(TEST_SCOPE),
Objects::nonNull, DEFAULT_TRYING_TIMEOUT,
DEFAULT_TRYING_TRIES);
// Store original V2 mode setting and default tenant ID
- originalV2Mode =
restAuthenticationConfig.isV2CompatibilityModeEnabled();
- originalDefaultTenantId =
restAuthenticationConfig.getV2CompatibilityDefaultTenantId();
+ originalV2Mode =
restAuthenticationConfig.isSingleTenantCompatibilityModeEnabled();
- // Configure V2 compatibility mode to use the BaseIT test tenant as
default
+ // Configure single-tenant compatibility mode to use the BaseIT test
tenant as default
Map<String, Object> v2Config = new HashMap<>();
- v2Config.put("v2.compatibilitymode.enabled", false); // Start in V3
mode
- v2Config.put("v2.compatibilitymode.defaultTenantId", TEST_TENANT_ID);
// Use BaseIT tenant
+ v2Config.put("singletenantcompatibility.enabled", false); // Start in
V3 mode
updateConfiguration(null,
"org.apache.unomi.rest.authentication",
v2Config);
- // Wait for configuration to be applied
- keepTrying("V2 compatibility configuration not applied in the required
time",
- () ->
restAuthenticationConfig.getV2CompatibilityDefaultTenantId(),
- tenantId -> TEST_TENANT_ID.equals(tenantId),
DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
// Create test profile
Profile profile = new Profile(TEST_PROFILE_ID);
@@ -118,10 +124,7 @@ public class V2CompatibilityModeIT extends BaseIT {
try {
// Restore original V2 mode setting and default tenant ID
Map<String, Object> originalConfig = new HashMap<>();
- originalConfig.put("v2.compatibilitymode.enabled", originalV2Mode);
- if (originalDefaultTenantId != null) {
- originalConfig.put("v2.compatibilitymode.defaultTenantId",
originalDefaultTenantId);
- }
+ originalConfig.put("singletenantcompatibility.enabled",
originalV2Mode);
updateConfiguration(null,
"org.apache.unomi.rest.authentication",
@@ -149,45 +152,45 @@ public class V2CompatibilityModeIT extends BaseIT {
@Test
public void testV2CompatibilityModeSwitch() throws Exception {
- LOGGER.info("Starting V2 compatibility mode switch test");
+ LOGGER.info("Starting single-tenant compatibility mode switch test");
// STEP 1: Test V3 mode (default) - V2 requests should be rejected, V3
requests should work
LOGGER.info("STEP 1: Testing V3 mode (default)");
testV3ModeBehavior();
- // STEP 2: Switch to V2 compatibility mode
- LOGGER.info("STEP 2: Switching to V2 compatibility mode");
+ // STEP 2: Switch to single-tenant compatibility mode
+ LOGGER.info("STEP 2: Switching to single-tenant compatibility mode");
updateConfiguration(null,
"org.apache.unomi.rest.authentication",
- "v2.compatibilitymode.enabled",
+ "singletenantcompatibility.enabled",
true);
// Wait for configuration to take effect
- keepTrying("V2 compatibility mode not enabled in the required time",
- () -> restAuthenticationConfig.isV2CompatibilityModeEnabled(),
+ keepTrying("single-tenant compatibility mode not enabled in the
required time",
+ () ->
restAuthenticationConfig.isSingleTenantCompatibilityModeEnabled(),
enabled -> enabled, DEFAULT_TRYING_TIMEOUT,
DEFAULT_TRYING_TRIES);
// STEP 3: Test V2 mode - V2 requests should work, V3 requests should
be rejected
- LOGGER.info("STEP 3: Testing V2 compatibility mode");
+ LOGGER.info("STEP 3: Testing single-tenant compatibility mode");
testV2ModeBehavior();
// STEP 4: Switch back to V3 mode
LOGGER.info("STEP 4: Switching back to V3 mode");
updateConfiguration(null,
"org.apache.unomi.rest.authentication",
- "v2.compatibilitymode.enabled",
+ "singletenantcompatibility.enabled",
false);
// Wait for configuration to take effect
- keepTrying("V2 compatibility mode not disabled in the required time",
- () -> restAuthenticationConfig.isV2CompatibilityModeEnabled(),
+ keepTrying("single-tenant compatibility mode not disabled in the
required time",
+ () ->
restAuthenticationConfig.isSingleTenantCompatibilityModeEnabled(),
enabled -> !enabled, DEFAULT_TRYING_TIMEOUT,
DEFAULT_TRYING_TRIES);
// STEP 5: Test V3 mode again - V2 requests should be rejected, V3
requests should work
LOGGER.info("STEP 5: Testing V3 mode again");
testV3ModeBehavior();
- LOGGER.info("V2 compatibility mode switch test completed
successfully");
+ LOGGER.info("single-tenant compatibility mode switch test completed
successfully");
}
/**
@@ -242,7 +245,7 @@ public class V2CompatibilityModeIT extends BaseIT {
}
/**
- * Test behavior in V2 compatibility mode:
+ * Test behavior in single-tenant compatibility mode:
* - V2 requests (no auth for public endpoints) should work
* - V3 requests should be rejected
*/
@@ -254,21 +257,26 @@ public class V2CompatibilityModeIT extends BaseIT {
HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL));
request.setEntity(new
StringEntity(getObjectMapper().writeValueAsString(contextRequest),
ContentType.APPLICATION_JSON));
TestUtils.RequestResponse response =
executeContextJSONRequest(request, TEST_SESSION_ID);
- assertEquals("V2-style request should work in V2 compatibility mode",
200, response.getStatusCode());
+ assertEquals("V2-style request should work in single-tenant
compatibility mode", 200, response.getStatusCode());
+ // The profile this request created belongs to the tenant the
compatibility mode runs on, which
+ // is not the tenant BaseIT works in. Read that profile back below, so
the write and the read
+ // both go through the compatibility path.
+ String compatibilityProfileId =
response.getContextResponse().getProfileId();
+ assertNotNull("V2-style request should have created a profile",
compatibilityProfileId);
// Test V2-style request with X-Unomi-Peer header (V2 third-party
auth) - should work
request = new HttpPost(getFullUrl(CONTEXT_URL));
request.addHeader(UNOMI_PEER_HEADER,
"670c26d1cc413346c3b2fd9ce65dab41");
request.setEntity(new
StringEntity(getObjectMapper().writeValueAsString(contextRequest),
ContentType.APPLICATION_JSON));
response = executeContextJSONRequest(request, TEST_SESSION_ID);
- assertEquals("V2-style request with X-Unomi-Peer should work in V2
compatibility mode", 200, response.getStatusCode());
+ assertEquals("V2-style request with X-Unomi-Peer should work in
single-tenant compatibility mode", 200, response.getStatusCode());
// Test V3-style request with public API key - in V2 mode, V3 API keys
are ignored (request succeeds but no events processed)
request = new HttpPost(getFullUrl(CONTEXT_URL));
request.addHeader(UNOMI_API_KEY_HEADER, testPublicKeyValue);
request.setEntity(new
StringEntity(getObjectMapper().writeValueAsString(contextRequest),
ContentType.APPLICATION_JSON));
response = executeContextJSONRequest(request, TEST_SESSION_ID);
- assertEquals("V3-style request with public API key should return 200
in V2 compatibility mode", 200, response.getStatusCode());
+ assertEquals("V3-style request with public API key should return 200
in single-tenant compatibility mode", 200, response.getStatusCode());
assertEquals("V3-style request with public API key should have 0
processed events in V2 mode", 0,
response.getContextResponse().getProcessedEvents());
// Test V3-style request with private API key - in V2 mode, V3 API
keys are ignored (request succeeds but no events processed)
@@ -276,11 +284,11 @@ public class V2CompatibilityModeIT extends BaseIT {
addPrivateTenantAuth(request, testTenant, testPrivateKeyValue);
request.setEntity(new
StringEntity(getObjectMapper().writeValueAsString(contextRequest),
ContentType.APPLICATION_JSON));
response = executeContextJSONRequest(request, TEST_SESSION_ID);
- assertEquals("V3-style request with private API key should return 200
in V2 compatibility mode", 200, response.getStatusCode());
+ assertEquals("V3-style request with private API key should return 200
in single-tenant compatibility mode", 200, response.getStatusCode());
assertEquals("V3-style request with private API key should have 0
processed events in V2 mode", 0,
response.getContextResponse().getProcessedEvents());
// Test private endpoint with JAAS authentication - should work (like
V2)
- HttpGet getRequest = new HttpGet(getFullUrl("/cxs/profiles/" +
TEST_PROFILE_ID));
+ HttpGet getRequest = new HttpGet(getFullUrl("/cxs/profiles/" +
compatibilityProfileId));
BasicCredentialsProvider credsProvider = new
BasicCredentialsProvider();
credsProvider.setCredentials(AuthScope.ANY, new
UsernamePasswordCredentials(BASIC_AUTH_USER_NAME, BASIC_AUTH_PASSWORD));
@@ -295,26 +303,26 @@ public class V2CompatibilityModeIT extends BaseIT {
.setDefaultRequestConfig(requestConfig)
.build()) {
try (CloseableHttpResponse jaasResponse =
adminClient.execute(getRequest)) {
- assertEquals("Private endpoint with JAAS auth should work in
V2 compatibility mode", 200, jaasResponse.getStatusLine().getStatusCode());
+ assertEquals("Private endpoint with JAAS auth should work in
single-tenant compatibility mode", 200,
jaasResponse.getStatusLine().getStatusCode());
}
try (CloseableHttpResponse privacyResponse =
adminClient.execute(new HttpGet(getFullUrl("/cxs/privacy/info")))) {
- assertEquals("GET /cxs/privacy/info with Karaf auth should
work in V2 compatibility mode", 200,
privacyResponse.getStatusLine().getStatusCode());
+ assertEquals("GET /cxs/privacy/info with Karaf auth should
work in single-tenant compatibility mode", 200,
privacyResponse.getStatusLine().getStatusCode());
}
}
}
@Test
public void testV2CompatibilityModeWithProtectedEvents() throws Exception {
- LOGGER.info("Testing V2 compatibility mode with protected events");
+ LOGGER.info("Testing single-tenant compatibility mode with protected
events");
- // Switch to V2 compatibility mode
+ // Switch to single-tenant compatibility mode
updateConfiguration(null,
"org.apache.unomi.rest.authentication",
- "v2.compatibilitymode.enabled",
+ "singletenantcompatibility.enabled",
true);
- keepTrying("V2 compatibility mode not enabled in the required time",
- () -> restAuthenticationConfig.isV2CompatibilityModeEnabled(),
+ keepTrying("single-tenant compatibility mode not enabled in the
required time",
+ () ->
restAuthenticationConfig.isSingleTenantCompatibilityModeEnabled(),
enabled -> enabled, DEFAULT_TRYING_TIMEOUT,
DEFAULT_TRYING_TRIES);
// Test protected event (login) without V2 third-party authentication
- should be rejected
@@ -363,79 +371,13 @@ public class V2CompatibilityModeIT extends BaseIT {
assertEquals("Non-protected event without auth should have 1 processed
event", 1, response.getContextResponse().getProcessedEvents());
}
- @Test
- public void testV2CompatibilityModeDefaultTenant() throws Exception {
- LOGGER.info("Testing V2 compatibility mode default tenant behavior");
-
- // Verify the configuration was applied correctly in setUp()
- assertEquals("Default tenant should be set to BaseIT tenant",
TEST_TENANT_ID, restAuthenticationConfig.getV2CompatibilityDefaultTenantId());
-
- // Switch to V2 compatibility mode
- updateConfiguration(null,
- "org.apache.unomi.rest.authentication",
- "v2.compatibilitymode.enabled",
- true);
-
- keepTrying("V2 compatibility mode not enabled in the required time",
- () -> restAuthenticationConfig.isV2CompatibilityModeEnabled(),
- enabled -> enabled, DEFAULT_TRYING_TIMEOUT,
DEFAULT_TRYING_TRIES);
-
- // Verify the configuration was applied
- assertTrue("V2 compatibility mode should be enabled",
restAuthenticationConfig.isV2CompatibilityModeEnabled());
- assertEquals("Default tenant should be set to BaseIT tenant",
TEST_TENANT_ID, restAuthenticationConfig.getV2CompatibilityDefaultTenantId());
-
- // Test that requests work with the BaseIT tenant as default
- ContextRequest contextRequest = new ContextRequest();
- contextRequest.setSessionId(TEST_SESSION_ID);
-
- HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL));
- request.setEntity(new
StringEntity(getObjectMapper().writeValueAsString(contextRequest),
ContentType.APPLICATION_JSON));
- TestUtils.RequestResponse response =
executeContextJSONRequest(request, TEST_SESSION_ID);
- assertEquals("V2-style request should work with BaseIT tenant as
default", 200, response.getStatusCode());
- }
-
- @Test
- public void testV2CompatibilityModeConfigurationPersistence() throws
Exception {
- LOGGER.info("Testing V2 compatibility mode configuration persistence");
-
- // Test that configuration changes persist across service updates
- updateConfiguration(null,
- "org.apache.unomi.rest.authentication",
- "v2.compatibilitymode.enabled",
- true);
-
- keepTrying("V2 compatibility mode not enabled in the required time",
- () -> restAuthenticationConfig.isV2CompatibilityModeEnabled(),
- enabled -> enabled, DEFAULT_TRYING_TIMEOUT,
DEFAULT_TRYING_TRIES);
-
- // Verify configuration is applied
- assertTrue("V2 compatibility mode should be enabled",
restAuthenticationConfig.isV2CompatibilityModeEnabled());
- assertEquals("Default tenant should persist", TEST_TENANT_ID,
restAuthenticationConfig.getV2CompatibilityDefaultTenantId());
-
- // Update services to simulate service restart
- updateServices();
-
- // Verify configuration persists
- assertTrue("V2 compatibility mode should persist after service
update", restAuthenticationConfig.isV2CompatibilityModeEnabled());
- assertEquals("Default tenant should persist after service update",
TEST_TENANT_ID, restAuthenticationConfig.getV2CompatibilityDefaultTenantId());
-
- // Test that behavior is still correct
- ContextRequest contextRequest = new ContextRequest();
- contextRequest.setSessionId(TEST_SESSION_ID);
-
- HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL));
- request.setEntity(new
StringEntity(getObjectMapper().writeValueAsString(contextRequest),
ContentType.APPLICATION_JSON));
- TestUtils.RequestResponse response =
executeContextJSONRequest(request, TEST_SESSION_ID);
- assertEquals("V2-style request should still work after service
update", 200, response.getStatusCode());
- }
-
@Test
public void testV2CompatibilityProtectedEventNegativeCases() throws
Exception {
- LOGGER.info("Testing V2 compatibility mode - protected event negative
cases");
+ LOGGER.info("Testing single-tenant compatibility mode - protected
event negative cases");
- updateConfiguration(null, "org.apache.unomi.rest.authentication",
"v2.compatibilitymode.enabled", true);
- keepTrying("V2 compatibility mode not enabled in the required time",
- () -> restAuthenticationConfig.isV2CompatibilityModeEnabled(),
+ updateConfiguration(null, "org.apache.unomi.rest.authentication",
"singletenantcompatibility.enabled", true);
+ keepTrying("single-tenant compatibility mode not enabled in the
required time",
+ () ->
restAuthenticationConfig.isSingleTenantCompatibilityModeEnabled(),
enabled -> enabled, DEFAULT_TRYING_TIMEOUT,
DEFAULT_TRYING_TRIES);
Event loginEvent = new Event();
diff --git a/manual/src/main/asciidoc/migrations/migrate-3.0-to-3.1.adoc
b/manual/src/main/asciidoc/migrations/migrate-3.0-to-3.1.adoc
index 56edd5da1..c8e9d81ef 100644
--- a/manual/src/main/asciidoc/migrations/migrate-3.0-to-3.1.adoc
+++ b/manual/src/main/asciidoc/migrations/migrate-3.0-to-3.1.adoc
@@ -349,7 +349,7 @@ Before starting the migration, please ensure that:
- You did practice the migration in a staging environment, NEVER migrate a
production environment without prior validation
- You are currently running Apache Unomi 3.0 (or a later 3.0.x version)
- You understand the multi-tenancy impact on your data model
-- You have a plan to update client applications to tenant API keys (or
temporary <<_v2_compatibility_mode,V2 compatibility mode>> only if coming from
2.x)
+- You have a plan to update client applications to tenant API keys (or
temporary <<_v2_compatibility_mode,single-tenant compatibility mode>> only if
coming from 2.x)
- You have reviewed the <<_client_facing_hardening_3_1,client-facing
hardening>> notes (cookie-only public profile binding, HttpOnly default,
required passwords, privileged REST roles)
- You know how to obtain plaintext API keys after upgrade (regenerate via
`/cxs/tenants/{id}/apikeys`; create responses expose masked keys only)
@@ -375,7 +375,7 @@ Scripts applied for the 3.1 line include (under
`tools/shell-commands/.../META-I
* `migrate-3.1.0-05-fixSystemItemIds`
* `migrate-3.1.0-10-tenantInitialization` (creates the default tenant used for
isolation)
* `migrate-3.1.0-15-updateLegacyQueryBuilder`
-4. **Update client applications** to the 3.1 authentication model (public key
header / tenant private key), or enable <<_v2_compatibility_mode,V2
compatibility mode>> only when migrating **from Unomi 2.x**.
+4. **Update client applications** to the 3.1 authentication model (public key
header / tenant private key), or enable <<_v2_compatibility_mode,single-tenant
compatibility mode>> only when migrating **from Unomi 2.x**.
5. **Start your Apache Unomi 3.1 cluster**.
6. **Regenerate and store tenant API keys** (plaintext is returned only from
the key-creation endpoint), then **test** applications end to end.
@@ -387,20 +387,21 @@ Unomi 3.1 does **not** provide a separate "3.0
compatibility mode" system proper
Adopt tenant public API keys for public endpoints and tenant private keys or
JAAS for administrative work. See <<_multitenancy,Multi-tenancy>> and the
authentication examples in this guide.
-==== Option 2: V2 compatibility mode (2.x clients only)
+==== Option 2: single-tenant compatibility mode (2.x clients only)
-If you are migrating from **Unomi 2.x** (not 3.0), you can enable
<<_v2_compatibility_mode,V2 compatibility mode>> so legacy clients work without
API keys during a phased rollout:
+If you are migrating from **Unomi 2.x** (not 3.0), you can enable
<<_v2_compatibility_mode,single-tenant compatibility mode>> so legacy clients
work without API keys during a phased rollout:
[source,properties]
----
# etc/org.apache.unomi.rest.authentication.cfg
-v2.compatibilitymode.enabled = true
-v2.compatibilitymode.defaultTenantId = default
+singletenantcompatibility.enabled = true
----
-Environment variable equivalent:
`UNOMI_REST_AUTHENTICATION_V2COMPATIBILITYMODEENABLED=true`
+This mode always uses tenant `default`. There is no property to point it at
another tenant. If tenant `default` does not exist yet, the first public
request creates it. Migrated 2.x data has to live in that tenant to be visible
while the mode is on.
-WARNING: V2 compatibility mode is intended for migration from 2.x only. It
bypasses tenant API key requirements on public endpoints. Disable it once all
clients use 3.1 authentication.
+Environment variable equivalent:
`UNOMI_REST_AUTHENTICATION_SINGLETENANTCOMPATIBILITYMODEENABLED=true`
+
+WARNING: single-tenant compatibility mode is intended for migration from 2.x
only. It bypasses tenant API key requirements on public endpoints. Disable it
once all clients use 3.1 authentication.
=== Post Migration
diff --git a/manual/src/main/asciidoc/migrations/v2-compatibility-mode.adoc
b/manual/src/main/asciidoc/migrations/v2-compatibility-mode.adoc
index 623820939..5900d7d4e 100644
--- a/manual/src/main/asciidoc/migrations/v2-compatibility-mode.adoc
+++ b/manual/src/main/asciidoc/migrations/v2-compatibility-mode.adoc
@@ -13,39 +13,38 @@
//
[#_v2_compatibility_mode]
-=== V2 compatibility mode
+=== single-tenant compatibility mode
-This document explains how to use the V2 compatibility mode in Apache Unomi
V3, which allows V2 client applications to work with Unomi V3 without requiring
API keys.
+This document explains how to use the single-tenant compatibility mode in
Apache Unomi V3, which allows V2 client applications to work with Unomi V3
without requiring API keys.
==== Overview
-The V2 compatibility mode is designed to ease the migration from Unomi V2 to
V3 by allowing V2 clients to continue working without immediate changes to
their authentication logic. This mode provides backward compatibility while
still leveraging the multi-tenant architecture of V3.
+The single-tenant compatibility mode is designed to ease the migration from
Unomi V2 to V3 by allowing V2 clients to continue working without immediate
changes to their authentication logic. This mode provides backward
compatibility while still leveraging the multi-tenant architecture of V3.
===== How It Works
-When V2 compatibility mode is enabled:
+When single-tenant compatibility mode is enabled:
- **Public endpoints** (like `/context.json`) require no authentication (like
V2)
- **Protected events** (like `login`, `updateProperties`) require IP +
X-Unomi-Peer (like V2)
- **Private endpoints** require system administrator authentication (like V2)
-- **A default tenant** is automatically used for all operations
+- **Tenant `default`** is used for every operation. The mode cannot be pointed
at another tenant.
- **No authentication** is required for non-protected events (like V2)
This allows V2 clients to work with Unomi V3 immediately after migration,
giving you time to gradually update client applications to use the new V3
authentication model.
==== Prerequisites
-Before enabling V2 compatibility mode, ensure that:
+Before enabling single-tenant compatibility mode, ensure that:
-1. **Data Migration Completed**: Your V2 data has been migrated to a tenant
using the migration scripts
-2. **Default Tenant Exists**: A default tenant exists that will be used for
all operations
-3. **V3 Installation**: Unomi V3 is properly installed and configured
+1. **Data Migration Completed**: Your V2 data has been migrated into tenant
`default` using the migration scripts. The mode does not read data that lives
in any other tenant.
+2. **V3 Installation**: Unomi V3 is properly installed and configured. If
tenant `default` is missing, the first public request creates it.
==== Configuration
===== Enable V2 Compatibility Mode
-You can also set the environment variable
`UNOMI_REST_AUTHENTICATION_V2COMPATIBILITYMODEENABLED=true` (maps to
`org.apache.unomi.rest.authentication.v2CompatibilityModeEnabled` in
`custom.system.properties`).
+You can also set the environment variable
`UNOMI_REST_AUTHENTICATION_SINGLETENANTCOMPATIBILITYMODEENABLED=true` (maps to
`org.apache.unomi.rest.authentication.singleTenantCompatibilityModeEnabled` in
`custom.system.properties`).
1. **Edit the configuration file**:
```bash
@@ -53,13 +52,11 @@ You can also set the environment variable
`UNOMI_REST_AUTHENTICATION_V2COMPATIBI
vi etc/org.apache.unomi.rest.authentication.cfg
```
-2. **Enable V2 compatibility mode**:
+2. **Enable single-tenant compatibility mode**:
```properties
- # Enable V2 compatibility mode
- v2.compatibilitymode.enabled = true
-
- # Set the default tenant ID (should match the tenant ID used during
migration)
- v2.compatibilitymode.defaultTenantId = your-migration-tenant-id
+ # Enable single-tenant compatibility mode.
+ # The mode always uses tenant "default". There is no setting for another
tenant id.
+ singletenantcompatibility.enabled = true
```
3. **Restart the server** to apply the configuration changes:
@@ -73,7 +70,7 @@ You can also set the environment variable
`UNOMI_REST_AUTHENTICATION_V2COMPATIBI
===== Configuration Management
-V2 compatibility mode is managed through configuration files only. This
approach is safer and prevents accidental changes to authentication settings.
+single-tenant compatibility mode is managed through configuration files only.
This approach is safer and prevents accidental changes to authentication
settings.
==== Migration Workflow
@@ -86,18 +83,18 @@ First, migrate your data with Apache Unomi stopped using
the single shell comman
unomi:migrate 2.0.0
```
-The `migrate-3.1.0-10-tenantInitialization` script (run as part of that chain
when migrating through 3.1) creates a default tenant used for V2 compatibility
mode. See <<_migrate_from_3_0_to_3_1,Migrate from 3.0 to 3.1>> and
<<_shell_commands,Shell commands>>.
+The `migrate-3.1.0-10-tenantInitialization` script (run as part of that chain
when migrating through 3.1) creates tenant `default`, which is the only tenant
this mode uses. If that tenant is missing when the mode is on, the first public
request creates it. See <<_migrate_from_3_0_to_3_1,Migrate from 3.0 to 3.1>>
and <<_shell_commands,Shell commands>>.
===== Step 2: Enable V2 Compatibility Mode
-Enable V2 compatibility mode by updating the configuration file:
+Enable single-tenant compatibility mode by updating the configuration file:
```bash
# Edit the configuration file
vi etc/org.apache.unomi.rest.authentication.cfg
-# Set v2.compatibilitymode.enabled = true
-# Set v2.compatibilitymode.defaultTenantId = your-tenant-id
+# Set singletenantcompatibility.enabled = true
+# The mode always uses tenant "default".
# Restart the server to apply changes
./bin/stop
@@ -126,8 +123,8 @@ RestAssured.given()
Over time, gradually update your clients to use V3 authentication:
1. **Update client applications** to use API keys
-2. **Test with V3 authentication** while keeping V2 compatibility mode enabled
-3. **Disable V2 compatibility mode** once all clients are updated
+2. **Test with V3 authentication** while keeping single-tenant compatibility
mode enabled
+3. **Disable single-tenant compatibility mode** once all clients are updated
==== Client Migration Examples
@@ -135,7 +132,7 @@ Over time, gradually update your clients to use V3
authentication:
**V2 Client (continues to work)**:
```java
-// This continues to work in V2 compatibility mode
+// This continues to work in single-tenant compatibility mode
RestAssured.authentication = RestAssured.preemptive()
.basic("karaf", System.getenv("UNOMI_ROOT_PASSWORD"));
@@ -158,26 +155,26 @@ given()
===== Gradual Migration Strategy
-1. **Phase 1**: Enable V2 compatibility mode, V2 clients continue working
+1. **Phase 1**: Enable single-tenant compatibility mode, V2 clients continue
working
2. **Phase 2**: Develop and test V3 clients alongside V2 clients
3. **Phase 3**: Migrate clients one by one to V3 authentication
-4. **Phase 4**: Disable V2 compatibility mode once all clients are migrated
+4. **Phase 4**: Disable single-tenant compatibility mode once all clients are
migrated
==== Security Considerations
===== V2 Compatibility Mode Security
-When V2 compatibility mode is enabled:
+When single-tenant compatibility mode is enabled:
- **Public endpoints** are accessible without authentication (same as V2)
- **Protected events** require IP + X-Unomi-Peer authentication (same as V2)
- **Private endpoints** require system administrator authentication (same as
V2)
-- **All operations** use the default tenant context
+- **All operations** use tenant `default`
- **Non-protected events** require no authentication (same as V2)
===== Protected Events in V2 Compatibility Mode
-In V2 compatibility mode, protected event types are configured dynamically
using the V2 third-party configuration file. By default, the following event
types are protected:
+In single-tenant compatibility mode, protected event types are configured
dynamically using the V2 third-party configuration file. By default, the
following event types are protected:
- `login` - User authentication events
- `updateProperties` - Profile property updates
@@ -244,10 +241,10 @@ The V2 third-party configuration supports dynamic updates:
===== Recommendations
-1. **Use V2 compatibility mode temporarily** during migration
+1. **Use single-tenant compatibility mode temporarily** during migration
2. **Plan for gradual migration** to V3 authentication
3. **Monitor access patterns** during the transition
-4. **Disable V2 compatibility mode** once migration is complete
+4. **Disable single-tenant compatibility mode** once migration is complete
==== Troubleshooting
@@ -255,9 +252,8 @@ The V2 third-party configuration supports dynamic updates:
**V2 clients still not working**:
- Check configuration file: `etc/org.apache.unomi.rest.authentication.cfg`
-- Verify `v2.compatibilitymode.enabled = true`
-- Ensure `v2.compatibilitymode.defaultTenantId` matches the tenant ID used
during migration
-- Ensure the tenant exists and is accessible
+- Verify `singletenantcompatibility.enabled = true`
+- Confirm migrated data is in tenant `default`. The mode cannot be pointed at
another tenant.
**Authentication errors**:
- Verify system administrator credentials (the `karaf` user and your
configured `UNOMI_ROOT_PASSWORD`)
@@ -265,8 +261,8 @@ The V2 third-party configuration supports dynamic updates:
- Review logs for authentication errors
**Tenant context issues**:
-- Ensure the default tenant ID matches your migrated tenant
-- Verify tenant exists in the tenant index
+- Migrated data must be in tenant `default`. Other tenants are not used while
the mode is on.
+- If tenant `default` is missing, the first public request creates it. A
private request does not.
- Check tenant configuration in the migration scripts
===== Debugging
@@ -291,7 +287,7 @@ Once all clients are migrated to V3 authentication:
1. **Update configuration**:
```properties
- v2.compatibilitymode.enabled = false
+ singletenantcompatibility.enabled = false
```
2. **Restart the server**:
@@ -306,14 +302,14 @@ Once all clients are migrated to V3 authentication:
==== Testing V2 Compatibility Mode
-The existing test framework supports testing V2 compatibility mode using
system properties.
+The existing test framework supports testing single-tenant compatibility mode
using system properties.
===== Running Tests in V2 Compatibility Mode
-To run tests with V2 compatibility mode enabled:
+To run tests with single-tenant compatibility mode enabled:
```bash
-# Enable V2 compatibility mode for tests
+# Enable single-tenant compatibility mode for tests
mvn test -Dunomi.v2.compatibility.mode=true
# Or set the property in your test environment
@@ -323,17 +319,17 @@ mvn test
===== Test Framework Integration
-The test framework automatically detects V2 compatibility mode and uses the
appropriate client:
+The test framework automatically detects single-tenant compatibility mode and
uses the appropriate client:
- **V2 Compatibility Mode Enabled**: Uses `UnomiV2Client` for all tests
- **V2 Compatibility Mode Disabled**: Uses normal V2/V3 detection logic
-This allows you to test both V2 compatibility mode and normal V3 mode using
the same test suite.
+This allows you to test both single-tenant compatibility mode and normal V3
mode using the same test suite.
===== Example Test Execution
```bash
-# Test with V2 compatibility mode (server should be configured for V2
compatibility)
+# Test with single-tenant compatibility mode (server should be configured for
V2 compatibility)
mvn test -Dunomi.v2.compatibility.mode=true -Dunomi.url=http://localhost:8181
# Test with normal V3 mode
@@ -342,7 +338,7 @@ mvn test -Dunomi.url=http://localhost:8181
==== Conclusion
-The V2 compatibility mode provides a smooth migration path from Unomi V2 to
V3, allowing you to:
+The single-tenant compatibility mode provides a smooth migration path from
Unomi V2 to V3, allowing you to:
- **Maintain existing V2 clients** during migration
- **Gradually migrate** to V3 authentication
diff --git a/manual/src/main/asciidoc/whats-new.adoc
b/manual/src/main/asciidoc/whats-new.adoc
index d49febed1..2433bcef3 100644
--- a/manual/src/main/asciidoc/whats-new.adoc
+++ b/manual/src/main/asciidoc/whats-new.adoc
@@ -22,7 +22,7 @@ Complete tenant isolation for profiles, events, segments,
rules, and schemas. Pu
* Operator guide: <<_multitenancy,Multi-tenancy>>
* Migration: <<_migrate_from_3_0_to_3_1,Migrate from 3.0 to 3.1>> (includes
<<_client_facing_hardening_3_1,client-facing hardening>>)
-* Migrating from Unomi 2.x: <<_v2_compatibility_mode,V2 compatibility mode>>
(`v2.compatibilitymode.enabled` in `org.apache.unomi.rest.authentication.cfg`)
+* Migrating from Unomi 2.x: <<_v2_compatibility_mode,single-tenant
compatibility mode>> (`singletenantcompatibility.enabled` in
`org.apache.unomi.rest.authentication.cfg`)
==== Security hardening (credentials, profile binding, privileged APIs)
diff --git a/package/src/main/resources/etc/custom.system.properties
b/package/src/main/resources/etc/custom.system.properties
index 1b7625aa5..9da27617d 100644
--- a/package/src/main/resources/etc/custom.system.properties
+++ b/package/src/main/resources/etc/custom.system.properties
@@ -529,5 +529,4 @@
org.apache.unomi.campaigns.refresh.interval=${env:UNOMI_CAMPAIGNS_REFRESH_INTERV
#######################################################################################################################
## REST API Authorization Settings
##
#######################################################################################################################
-org.apache.unomi.rest.authentication.v2CompatibilityModeEnabled=${env:UNOMI_REST_AUTHENTICATION_V2COMPATIBILITYMODEENABLED:-false}
-org.apache.unomi.rest.authentication.v2CompatibilityDefaultTenantId=${env:UNOMI_REST_AUTHENTICATION_V2COMPATIBILITYDEFAULTTENANTID:-default}
+org.apache.unomi.rest.authentication.singleTenantCompatibilityModeEnabled=${env:UNOMI_REST_AUTHENTICATION_SINGLETENANTCOMPATIBILITYMODEENABLED:-false}
diff --git
a/rest/src/main/java/org/apache/unomi/rest/authentication/AuthenticationFilter.java
b/rest/src/main/java/org/apache/unomi/rest/authentication/AuthenticationFilter.java
index 0e990ffaa..6f547f935 100644
---
a/rest/src/main/java/org/apache/unomi/rest/authentication/AuthenticationFilter.java
+++
b/rest/src/main/java/org/apache/unomi/rest/authentication/AuthenticationFilter.java
@@ -41,7 +41,6 @@ import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.PreMatching;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
-import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.util.Base64;
@@ -60,6 +59,13 @@ import java.util.Set;
@Priority(Priorities.AUTHENTICATION)
public class AuthenticationFilter implements ContainerRequestFilter {
+ /**
+ * The tenant that single-tenant compatibility mode runs on. A client from
before Unomi 3.1 knows
+ * no tenant, so it never names one, and nothing outside this class reads
this value. It is a
+ * constant rather than a setting for that reason.
+ */
+ private static final String COMPATIBILITY_TENANT_ID = "default";
+
private static final String UNOMI_API_KEY_HEADER = "X-Unomi-Api-Key";
private static final String UNOMI_TENANT_ID_HEADER = "X-Unomi-Tenant-Id";
private static final Logger logger =
LoggerFactory.getLogger(AuthenticationFilter.class);
@@ -135,9 +141,9 @@ public class AuthenticationFilter implements
ContainerRequestFilter {
try {
String path = requestContext.getUriInfo().getPath();
- // Check if V2 compatibility mode is enabled
- if (restAuthenticationConfig.isV2CompatibilityModeEnabled()) {
- handleV2CompatibilityMode(requestContext, path);
+ // Check if single-tenant compatibility mode is enabled
+ if
(restAuthenticationConfig.isSingleTenantCompatibilityModeEnabled()) {
+ handleSingleTenantCompatibilityMode(requestContext, path);
return;
}
@@ -280,42 +286,39 @@ public class AuthenticationFilter implements
ContainerRequestFilter {
}
/**
- * Handle authentication in V2 compatibility mode.
+ * Handle authentication in single-tenant compatibility mode.
* In this mode:
* - Public endpoints (like /context.json) require no authentication (like
V2)
* - Protected events require IP + X-Unomi-Peer (like V2)
* - Private endpoints require system administrator authentication (like
V2)
* - A default tenant is automatically used for all operations
*/
- private void handleV2CompatibilityMode(ContainerRequestContext
requestContext, String path) throws IOException {
+ private void handleSingleTenantCompatibilityMode(ContainerRequestContext
requestContext, String path) throws IOException {
// For public paths, allow access without authentication (like V2)
if (isPublicPath(requestContext)) {
- String defaultTenantId =
restAuthenticationConfig.getV2CompatibilityDefaultTenantId();
- if (StringUtils.isNotBlank(defaultTenantId)) {
- Tenant defaultTenant =
tenantService.getTenant(defaultTenantId);
- if (defaultTenant == null) {
- logger.error("V2 compatibility mode: configured default
tenant '{}' does not exist", defaultTenantId);
- unauthorized(requestContext);
- return;
- }
- // Create a guest subject for public endpoints
- Subject subject =
securityService.createSubject(defaultTenantId, false);
-
- // Set CXF security context
- JAXRSUtils.getCurrentMessage().put(SecurityContext.class,
- new RolePrefixSecurityContextImpl(subject,
ROLE_CLASSIFIER, ROLE_CLASSIFIER_TYPE));
-
- // Set the security service subject
- securityService.setCurrentSubject(subject);
-
- // Set the execution context for the default tenant
-
executionContextManager.setCurrentContext(executionContextManager.createContext(defaultTenantId));
- return;
- } else {
- logger.warn("V2 compatibility mode: public path request denied
because v2CompatibilityDefaultTenantId is not configured");
+ String tenantId = COMPATIBILITY_TENANT_ID;
+ // V2 knew no tenant, so a V2 client cannot name one and cannot
create one. Create the
+ // tenant this mode runs on, rather than refuse every request
until an operator creates
+ // it by hand.
+ try {
+ tenantService.getOrCreateTenant(tenantId,
Collections.singletonMap("name", tenantId));
+ } catch (RuntimeException e) {
+ logger.error("single-tenant compatibility mode: could not
obtain tenant '{}'", tenantId, e);
unauthorized(requestContext);
return;
}
+ // Create a guest subject for public endpoints
+ Subject subject = securityService.createSubject(tenantId, false);
+
+ // Set CXF security context
+ JAXRSUtils.getCurrentMessage().put(SecurityContext.class,
+ new RolePrefixSecurityContextImpl(subject, ROLE_CLASSIFIER,
ROLE_CLASSIFIER_TYPE));
+
+ // Set the security service subject
+ securityService.setCurrentSubject(subject);
+
+
executionContextManager.setCurrentContext(executionContextManager.createContext(tenantId));
+ return;
}
// For private endpoints, require system administrator authentication
(like V2)
@@ -330,47 +333,43 @@ public class AuthenticationFilter implements
ContainerRequestFilter {
// A null security context here means auth was rejected or an
unexpected state occurred — deny either way.
SecurityContext securityContext =
JAXRSUtils.getCurrentMessage().get(SecurityContext.class);
if (securityContext == null) {
- logger.debug("V2 compatibility mode: no security context
after JAAS filter, denying access");
+ logger.debug("single-tenant compatibility mode: no
security context after JAAS filter, denying access");
unauthorized(requestContext);
return;
}
Subject jaasSubject = ((RolePrefixSecurityContextImpl)
securityContext).getSubject();
- // Private endpoints in V2 compatibility mode require system
administrator
+ // Private endpoints in single-tenant compatibility mode
require system administrator
// authentication (like V2) — a JAAS login alone isn't enough,
since any Karaf
// user (not just admins) can authenticate against the realm.
if
(!securityService.extractRolesFromSubject(jaasSubject).contains(UnomiRoles.ADMINISTRATOR))
{
- logger.debug("V2 compatibility mode: authenticated user
lacks administrator role, denying access to private endpoint");
+ logger.debug("single-tenant compatibility mode:
authenticated user lacks administrator role, denying access to private
endpoint");
unauthorized(requestContext);
return;
}
- // Build a merged subject that combines the JAAS principals
with tenant admin
- // principals for the default tenant, so that
resolveTenantId() can find it downstream.
- String defaultTenantId =
restAuthenticationConfig.getV2CompatibilityDefaultTenantId();
+ // Build a merged subject that combines the JAAS principals
with the tenant admin
+ // principals, so that resolveTenantId() can find the tenant
downstream.
+ String tenantId = COMPATIBILITY_TENANT_ID;
Subject mergedSubject = new Subject();
mergedSubject.getPrincipals().addAll(jaasSubject.getPrincipals());
- if (StringUtils.isNotBlank(defaultTenantId)) {
-
mergedSubject.getPrincipals().addAll(securityService.createSubject(defaultTenantId,
true).getPrincipals());
- Set<String> roles =
securityService.extractRolesFromSubject(mergedSubject);
- Set<String> permissions = new HashSet<>();
- for (String role : roles) {
-
permissions.addAll(securityService.getPermissionsForRole(role));
- }
- executionContextManager.setCurrentContext(new
ExecutionContext(defaultTenantId, roles, permissions));
- } else {
-
executionContextManager.setCurrentContext(ExecutionContext.systemContext());
+
mergedSubject.getPrincipals().addAll(securityService.createSubject(tenantId,
true).getPrincipals());
+ Set<String> roles =
securityService.extractRolesFromSubject(mergedSubject);
+ Set<String> permissions = new HashSet<>();
+ for (String role : roles) {
+
permissions.addAll(securityService.getPermissionsForRole(role));
}
+ executionContextManager.setCurrentContext(new
ExecutionContext(tenantId, roles, permissions));
JAXRSUtils.getCurrentMessage().put(SecurityContext.class,
new RolePrefixSecurityContextImpl(mergedSubject,
ROLE_CLASSIFIER, ROLE_CLASSIFIER_TYPE));
securityService.setCurrentSubject(mergedSubject);
return;
} catch (Exception e) {
// Only fires for unexpected exceptions — credential failures
are handled inside JAASAuthenticationFilter.
- logger.debug("V2 compatibility mode: unexpected exception
during JAAS processing", e);
+ logger.debug("single-tenant compatibility mode: unexpected
exception during JAAS processing", e);
}
} else {
- logger.debug("V2 compatibility mode: Missing Basic Auth header for
private endpoint");
+ logger.debug("single-tenant compatibility mode: Missing Basic Auth
header for private endpoint");
}
// If we get here, no valid authentication was provided
diff --git
a/rest/src/main/java/org/apache/unomi/rest/authentication/RestAuthenticationConfig.java
b/rest/src/main/java/org/apache/unomi/rest/authentication/RestAuthenticationConfig.java
index ed7022dc0..fe3eed098 100644
---
a/rest/src/main/java/org/apache/unomi/rest/authentication/RestAuthenticationConfig.java
+++
b/rest/src/main/java/org/apache/unomi/rest/authentication/RestAuthenticationConfig.java
@@ -61,21 +61,14 @@ public interface RestAuthenticationConfig {
String getGlobalRoles();
/**
- * Check if V2 compatibility mode is enabled.
+ * Check if single-tenant compatibility mode is enabled.
* When enabled, V2 clients can use Unomi V3 without requiring API keys:
* - Public endpoints (like /context.json) require no authentication (like
V2)
* - Private endpoints require system administrator authentication (like
V2)
* - A default tenant is automatically used for all operations
*
- * @return true if V2 compatibility mode is enabled, false otherwise
+ * @return true if single-tenant compatibility mode is enabled, false
otherwise
*/
- boolean isV2CompatibilityModeEnabled();
+ boolean isSingleTenantCompatibilityModeEnabled();
- /**
- * Get the default tenant ID to use in V2 compatibility mode.
- * This tenant will be used for all operations when V2 compatibility mode
is enabled.
- *
- * @return the default tenant ID
- */
- String getV2CompatibilityDefaultTenantId();
}
diff --git
a/rest/src/main/java/org/apache/unomi/rest/authentication/impl/DefaultRestAuthenticationConfig.java
b/rest/src/main/java/org/apache/unomi/rest/authentication/impl/DefaultRestAuthenticationConfig.java
index 2d3bf5fa1..a9529e1c7 100644
---
a/rest/src/main/java/org/apache/unomi/rest/authentication/impl/DefaultRestAuthenticationConfig.java
+++
b/rest/src/main/java/org/apache/unomi/rest/authentication/impl/DefaultRestAuthenticationConfig.java
@@ -33,7 +33,7 @@ import java.util.regex.Pattern;
/**
* Default OSGi-backed implementation of {@link RestAuthenticationConfig} for
REST endpoint
- * authentication, role mappings, and V2 compatibility mode settings.
+ * authentication, role mappings, and single-tenant compatibility mode
settings.
*/
@Component(service = { RestAuthenticationConfig.class}, configurationPid =
"org.apache.unomi.rest.authentication", immediate = true)
@Designate(ocd = DefaultRestAuthenticationConfig.Config.class)
@@ -76,8 +76,7 @@ public class DefaultRestAuthenticationConfig implements
RestAuthenticationConfig
ROLES_MAPPING = Collections.unmodifiableMap(roles);
}
- private volatile boolean v2CompatibilityModeEnabled = false;
- private volatile String v2CompatibilityDefaultTenantId = "default";
+ private volatile boolean singleTenantCompatibilityModeEnabled = false;
/**
* Updates authentication settings from OSGi configuration.
@@ -91,19 +90,9 @@ public class DefaultRestAuthenticationConfig implements
RestAuthenticationConfig
LOGGER.warn("Config is null in modified method");
return;
}
- boolean v2Mode = config.v2_compatibilitymode_enabled();
- String defaultTenant = config.v2_compatibilitymode_defaultTenantId();
- if (defaultTenant != null) {
- defaultTenant = defaultTenant.trim();
- }
- if (StringUtils.isBlank(defaultTenant)) {
- LOGGER.warn("v2CompatibilityDefaultTenantId is blank, falling back
to 'default'");
- defaultTenant = "default";
- }
- LOGGER.info("Configuration updated - v2CompatibilityModeEnabled: {},
v2CompatibilityDefaultTenantId: {}",
- v2Mode, defaultTenant);
- this.v2CompatibilityModeEnabled = v2Mode;
- this.v2CompatibilityDefaultTenantId = defaultTenant;
+ boolean singleTenantMode = config.singletenantcompatibility_enabled();
+ LOGGER.info("Configuration updated -
singleTenantCompatibilityModeEnabled: {}", singleTenantMode);
+ this.singleTenantCompatibilityModeEnabled = singleTenantMode;
}
@@ -123,44 +112,29 @@ public class DefaultRestAuthenticationConfig implements
RestAuthenticationConfig
}
@Override
- public boolean isV2CompatibilityModeEnabled() {
- return v2CompatibilityModeEnabled;
+ public boolean isSingleTenantCompatibilityModeEnabled() {
+ return singleTenantCompatibilityModeEnabled;
}
- @Override
- public String getV2CompatibilityDefaultTenantId() {
- return v2CompatibilityDefaultTenantId;
- }
/**
* OSGi configuration for REST authentication.
*/
@ObjectClassDefinition(
name = "Unomi REST Authentication Configuration",
- description = "Configuration for Unomi REST authentication including
V2 compatibility mode"
+ description = "Configuration for Unomi REST authentication including
single-tenant compatibility mode"
)
public @interface Config {
/**
- * Whether V2 compatibility mode is enabled.
- *
- * @return {@code true} when V2 compatibility mode is enabled
- */
- @AttributeDefinition(
- name = "V2 Compatibility Mode Enabled",
- description = "Enable V2 compatibility mode to allow V2 clients to
use Unomi V3 without API keys"
- )
- boolean v2_compatibilitymode_enabled() default false;
-
- /**
- * Default tenant identifier used in V2 compatibility mode.
+ * Whether single-tenant compatibility mode is enabled.
*
- * @return the default tenant identifier
+ * @return {@code true} when single-tenant compatibility mode is
enabled
*/
@AttributeDefinition(
- name = "V2 Compatibility Default Tenant ID",
- description = "Default tenant ID to use in V2 compatibility mode"
+ name = "Single-tenant compatibility mode enabled",
+ description = "Serve clients from before Unomi 3.1, which send no
tenant API key, and run them all on one tenant"
)
- String v2_compatibilitymode_defaultTenantId() default "default";
+ boolean singletenantcompatibility_enabled() default false;
}
}
diff --git
a/rest/src/main/java/org/apache/unomi/rest/service/impl/RestServiceUtilsImpl.java
b/rest/src/main/java/org/apache/unomi/rest/service/impl/RestServiceUtilsImpl.java
index 7ed3a0987..9bee93127 100644
---
a/rest/src/main/java/org/apache/unomi/rest/service/impl/RestServiceUtilsImpl.java
+++
b/rest/src/main/java/org/apache/unomi/rest/service/impl/RestServiceUtilsImpl.java
@@ -388,10 +388,10 @@ public class RestServiceUtilsImpl implements
RestServiceUtils {
Event eventToSend = new Event(event.getEventType(),
eventsRequestContext.getSession(), eventsRequestContext.getProfile(),
event.getScope(),
event.getSource(), event.getTarget(),
event.getProperties(), eventsRequestContext.getTimestamp(),
event.isPersistent());
eventToSend.setFlattenedProperties(event.getFlattenedProperties());
- // Check if V2 compatibility mode is enabled and handle
V2-style event authorization
- if
(restAuthenticationConfig.isV2CompatibilityModeEnabled()) {
+ // Check if single-tenant compatibility mode is enabled
and handle V2-style event authorization
+ if
(restAuthenticationConfig.isSingleTenantCompatibilityModeEnabled()) {
if (!isEventAllowedInV2CompatibilityMode(event,
eventsRequestContext.getRequest())) {
- LOGGER.debug("Event {} not authorized in V2
compatibility mode from IP {}", event.getEventType(),
eventsRequestContext.getRequest().getRemoteAddr());
+ LOGGER.debug("Event {} not authorized in
single-tenant compatibility mode from IP {}", event.getEventType(),
eventsRequestContext.getRequest().getRemoteAddr());
//Don't count the event that failed
eventsRequestContext.setProcessedItems(eventsRequestContext.getProcessedItems()
- 1);
continue;
@@ -540,7 +540,7 @@ public class RestServiceUtilsImpl implements
RestServiceUtils {
}
/**
- * Check if an event is allowed in V2 compatibility mode.
+ * Check if an event is allowed in single-tenant compatibility mode.
* In V2, protected events required IP + X-Unomi-Peer (third-party key)
authentication.
*
* @param event the event to check
@@ -550,7 +550,7 @@ public class RestServiceUtilsImpl implements
RestServiceUtils {
private boolean isEventAllowedInV2CompatibilityMode(Event event,
HttpServletRequest request) {
// Check if this is a protected event type using the V2 third-party
configuration
if
(!v2ThirdPartyConfigService.isProtectedEventType(event.getEventType())) {
- // Non-protected events are always allowed in V2 compatibility mode
+ // Non-protected events are always allowed in single-tenant
compatibility mode
return true;
}
@@ -559,18 +559,18 @@ public class RestServiceUtilsImpl implements
RestServiceUtils {
String thirdPartyKey = request.getHeader("X-Unomi-Peer");
if (StringUtils.isBlank(thirdPartyKey)) {
- LOGGER.debug("V2 compatibility mode: Protected event {} rejected -
missing X-Unomi-Peer header", event.getEventType());
+ LOGGER.debug("single-tenant compatibility mode: Protected event {}
rejected - missing X-Unomi-Peer header", event.getEventType());
return false;
}
// Validate the third-party provider using the V2 configuration
if (!v2ThirdPartyConfigService.validateProviderByKey(thirdPartyKey,
event.getEventType(), sourceIP)) {
- LOGGER.debug("V2 compatibility mode: Protected event {} rejected -
invalid third-party provider key: {} from IP: {}",
+ LOGGER.debug("single-tenant compatibility mode: Protected event {}
rejected - invalid third-party provider key: {} from IP: {}",
event.getEventType(),
SecurityUtils.maskSecret(thirdPartyKey), sourceIP);
return false;
}
- LOGGER.debug("V2 compatibility mode: Protected event {} allowed for
provider key: {} from IP: {}",
+ LOGGER.debug("single-tenant compatibility mode: Protected event {}
allowed for provider key: {} from IP: {}",
event.getEventType(),
SecurityUtils.maskSecret(thirdPartyKey), sourceIP);
return true;
}
diff --git a/rest/src/main/resources/org.apache.unomi.rest.authentication.cfg
b/rest/src/main/resources/org.apache.unomi.rest.authentication.cfg
index db79d2627..58a9475f4 100644
--- a/rest/src/main/resources/org.apache.unomi.rest.authentication.cfg
+++ b/rest/src/main/resources/org.apache.unomi.rest.authentication.cfg
@@ -22,10 +22,9 @@
# - Public endpoints (like /context.json) require no authentication (like V2)
# - Private endpoints require system administrator authentication (like V2)
# - A default tenant is automatically used for all operations
-v2.compatibilitymode.enabled =
${org.apache.unomi.rest.authentication.v2CompatibilityModeEnabled:-false}
+singletenantcompatibility.enabled =
${org.apache.unomi.rest.authentication.singleTenantCompatibilityModeEnabled:-false}
# V2 Compatibility Default Tenant ID
-# Default tenant ID to use in V2 compatibility mode
-# This tenant will be used for all operations when V2 compatibility mode is
enabled
+# Default tenant ID to use in single-tenant compatibility mode
+# This tenant will be used for all operations when single-tenant compatibility
mode is enabled
# Should match the tenant ID used during migration (e.g., "default" or
"system")
-v2.compatibilitymode.defaultTenantId =
${org.apache.unomi.rest.authentication.v2CompatibilityDefaultTenantId:-default}
diff --git
a/rest/src/test/java/org/apache/unomi/rest/authentication/AuthenticationFilterBlankPasswordTest.java
b/rest/src/test/java/org/apache/unomi/rest/authentication/AuthenticationFilterBlankPasswordTest.java
index 016a45756..1b4baac8e 100644
---
a/rest/src/test/java/org/apache/unomi/rest/authentication/AuthenticationFilterBlankPasswordTest.java
+++
b/rest/src/test/java/org/apache/unomi/rest/authentication/AuthenticationFilterBlankPasswordTest.java
@@ -171,15 +171,15 @@ class AuthenticationFilterBlankPasswordTest {
}
/**
- * V2 compatibility mode routes every request through {@link
AuthenticationFilter}'s own
+ * single-tenant compatibility mode routes every request through {@link
AuthenticationFilter}'s own
* private-endpoint branch, which consumes the Basic credential at a
third, separate call site.
* Without this test that call site is unreachable from the suite: the
other tests leave
- * {@code isV2CompatibilityModeEnabled()} at the unstubbed Mockito {@code
false}, so deleting
+ * {@code isSingleTenantCompatibilityModeEnabled()} at the unstubbed
Mockito {@code false}, so deleting
* the guard there would leave every test green.
*/
@Test
void filterRejectsBlankPasswordOnAPrivatePathInV2CompatibilityMode()
throws IOException {
-
when(restAuthenticationConfig.isV2CompatibilityModeEnabled()).thenReturn(true);
+
when(restAuthenticationConfig.isSingleTenantCompatibilityModeEnabled()).thenReturn(true);
when(restAuthenticationConfig.getPublicPathPatterns()).thenReturn(Collections.emptyList());
ContainerRequestContext requestContext = request("profiles",
basic("karaf:"));
@@ -191,7 +191,7 @@ class AuthenticationFilterBlankPasswordTest {
/** Control for the V2 branch: a non-blank credential must still reach
JAAS there too. */
@Test
void
filterPassesNonBlankPasswordToJaasOnAPrivatePathInV2CompatibilityMode() throws
IOException {
-
when(restAuthenticationConfig.isV2CompatibilityModeEnabled()).thenReturn(true);
+
when(restAuthenticationConfig.isSingleTenantCompatibilityModeEnabled()).thenReturn(true);
when(restAuthenticationConfig.getPublicPathPatterns()).thenReturn(Collections.emptyList());
ContainerRequestContext requestContext = request("profiles",
basic("karaf:a-strong-password"));
@@ -201,20 +201,19 @@ class AuthenticationFilterBlankPasswordTest {
}
/**
- * A public path in V2 compatibility mode authenticates by default tenant,
ignoring
+ * A public path in single-tenant compatibility mode authenticates by
default tenant, ignoring
* {@code Authorization} entirely — so a stray blank Basic header must not
turn it into a 401.
*/
@Test
- void
filterDoesNotRejectAStrayBlankBasicHeaderOnAPublicPathInV2CompatibilityMode()
throws IOException {
-
when(restAuthenticationConfig.isV2CompatibilityModeEnabled()).thenReturn(true);
+ void
filterDoesNotRejectAStrayBlankBasicHeaderOnAPublicPathInSingleTenantCompatibilityMode()
throws IOException {
+
when(restAuthenticationConfig.isSingleTenantCompatibilityModeEnabled()).thenReturn(true);
when(restAuthenticationConfig.getPublicPathPatterns())
.thenReturn(Collections.singletonList(Pattern.compile("POST
context\\.json")));
-
when(restAuthenticationConfig.getV2CompatibilityDefaultTenantId()).thenReturn("default");
ContainerRequestContext requestContext = request("context.json",
basic("someone:"));
filter.filter(requestContext);
- verify(tenantService).getTenant("default");
+ verify(tenantService).getOrCreateTenant(eq("default"), any());
}
private void assertUnauthorizedWithoutReachingJaas(ContainerRequestContext
requestContext) throws IOException {
diff --git
a/services/src/main/java/org/apache/unomi/services/impl/tenants/TenantServiceImpl.java
b/services/src/main/java/org/apache/unomi/services/impl/tenants/TenantServiceImpl.java
index 0359d9dbf..c2c6e474d 100644
---
a/services/src/main/java/org/apache/unomi/services/impl/tenants/TenantServiceImpl.java
+++
b/services/src/main/java/org/apache/unomi/services/impl/tenants/TenantServiceImpl.java
@@ -113,6 +113,25 @@ public class TenantServiceImpl implements TenantService {
}
}
+ @Override
+ public synchronized Tenant getOrCreateTenant(String tenantId, Map<String,
Object> properties) {
+ Tenant tenant = getTenant(tenantId);
+ if (tenant != null) {
+ return tenant;
+ }
+ try {
+ return createTenant(tenantId, properties);
+ } catch (IllegalArgumentException e) {
+ // Another node created the tenant between the read and the write,
which is the outcome
+ // this method is asked for. Re-read rather than fail.
+ Tenant concurrent = getTenant(tenantId);
+ if (concurrent == null) {
+ throw e;
+ }
+ return concurrent;
+ }
+ }
+
@Override
public Tenant createTenant(String requestedId, Map<String, Object>
properties) {
validateTenantId(requestedId);
diff --git
a/services/src/test/java/org/apache/unomi/services/impl/TestTenantService.java
b/services/src/test/java/org/apache/unomi/services/impl/TestTenantService.java
index 59723a95f..b33839b28 100644
---
a/services/src/test/java/org/apache/unomi/services/impl/TestTenantService.java
+++
b/services/src/test/java/org/apache/unomi/services/impl/TestTenantService.java
@@ -105,6 +105,12 @@ public class TestTenantService implements TenantService {
&& secretHashService.verify(plainTextKey, apiKey.getKeyHash());
}
+ @Override
+ public Tenant getOrCreateTenant(String tenantId, Map<String, Object>
properties) {
+ Tenant tenant = getTenant(tenantId);
+ return tenant != null ? tenant : createTenant(tenantId, properties);
+ }
+
@Override
public Tenant createTenant(String tenantId, Map<String, Object>
properties) {
Tenant tenant = new Tenant();