This is an automated email from the ASF dual-hosted git repository.

sergehuber pushed a commit to branch UNOMI-904-v2-api-compatibility
in repository https://gitbox.apache.org/repos/asf/unomi.git

commit 3efda1ae67c878bf22c170fd1f79e9cb8d934d98
Author: Serge Huber <[email protected]>
AuthorDate: Tue May 19 06:22:59 2026 +0200

    UNOMI-904: V2 API compatibility mode
    
    Let V2 client applications run on the V3 multi-tenant platform without
    rewriting authentication first: compatibility mode can be toggled at 
runtime,
    legacy third-party API keys and IP/X-Unomi-Peer checks apply for protected
    events, and public endpoints such as /context.json stay usable without 
tenant
    API keys while V3 tenant auth remains the default when the mode is off.
    
    - V2ThirdPartyConfigService wired to org.apache.unomi.thirdparty.cfg
    - REST authentication filter and config for V2 compatibility routing
    - Event collector allowlist checks in RestServiceUtilsImpl
    - Karaf packaging for org.apache.unomi.rest.authentication.cfg
    - V2CompatibilityModeIT in the integration test suite
---
 .../test/java/org/apache/unomi/itests/AllITs.java  |   1 +
 .../apache/unomi/itests/V2CompatibilityModeIT.java | 435 +++++++++++++++++++++
 kar/src/main/feature/feature.xml                   |   1 +
 .../main/resources/etc/custom.system.properties    |   6 +
 rest/pom.xml                                       |  24 ++
 .../rest/authentication/AuthenticationFilter.java  |  67 ++++
 .../authentication/RestAuthenticationConfig.java   |  19 +
 .../authentication/V2ThirdPartyConfigService.java  | 259 ++++++++++++
 .../impl/DefaultRestAuthenticationConfig.java      |  58 ++-
 .../rest/service/impl/RestServiceUtilsImpl.java    |  65 ++-
 .../org.apache.unomi.rest.authentication.cfg       |  31 ++
 11 files changed, 960 insertions(+), 6 deletions(-)

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 2be7f0641..2e835cf0a 100644
--- a/itests/src/test/java/org/apache/unomi/itests/AllITs.java
+++ b/itests/src/test/java/org/apache/unomi/itests/AllITs.java
@@ -64,6 +64,7 @@ import org.junit.runners.Suite.SuiteClasses;
         GraphQLProfileAliasesIT.class,
         SendEventActionIT.class,
         ScopeIT.class,
+        V2CompatibilityModeIT.class,
         HealthCheckIT.class,
         LegacyQueryBuilderMappingIT.class,
 })
diff --git 
a/itests/src/test/java/org/apache/unomi/itests/V2CompatibilityModeIT.java 
b/itests/src/test/java/org/apache/unomi/itests/V2CompatibilityModeIT.java
new file mode 100644
index 000000000..a41d959d9
--- /dev/null
+++ b/itests/src/test/java/org/apache/unomi/itests/V2CompatibilityModeIT.java
@@ -0,0 +1,435 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.unomi.itests;
+
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.config.AuthSchemes;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.unomi.api.*;
+import org.apache.unomi.api.tenants.ApiKey;
+import org.apache.unomi.api.tenants.Tenant;
+import org.apache.unomi.itests.TestUtils.RequestResponse;
+import org.apache.unomi.rest.authentication.RestAuthenticationConfig;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerSuite;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.Base64;
+import java.util.Objects;
+
+import static org.junit.Assert.*;
+
+/**
+ * Integration tests for V2 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 {
+
+    private final static Logger LOGGER = 
LoggerFactory.getLogger(V2CompatibilityModeIT.class);
+    private final static String CONTEXT_URL = "/cxs/context.json";
+    private static final String TEST_SCOPE = "testScope";
+    private final static String TEST_SESSION_ID = "v2-compat-test-session-" + 
System.currentTimeMillis();
+    private final static String TEST_PROFILE_ID = "v2-compat-test-profile-" + 
System.currentTimeMillis();
+    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;
+
+    @Before
+    public void setUp() throws InterruptedException, IOException {
+
+        TestUtils.createScope(TEST_SCOPE, "Test scope", scopeService);
+        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();
+
+        // Configure V2 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
+
+        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);
+        profileService.save(profile);
+
+        keepTrying("Profile " + TEST_PROFILE_ID + " not found in the required 
time",
+                () -> profileService.load(TEST_PROFILE_ID),
+                Objects::nonNull, DEFAULT_TRYING_TIMEOUT, 
DEFAULT_TRYING_TRIES);
+
+    }
+
+    @After
+    public void tearDown() throws InterruptedException, IOException {
+        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);
+            }
+
+            updateConfiguration(null,
+                    "org.apache.unomi.rest.authentication",
+                    originalConfig);
+        } catch (Exception e) {
+            LOGGER.warn("Failed to restore original V2 mode setting", e);
+        }
+
+        // Clean up test data
+        try {
+            TestUtils.removeAllEvents(definitionsService, persistenceService, 
true, tenantService, executionContextManager);
+            TestUtils.removeAllSessions(definitionsService, 
persistenceService, true, tenantService, executionContextManager);
+            TestUtils.removeAllProfiles(definitionsService, 
persistenceService, true, tenantService, executionContextManager);
+
+            profileService.delete(TEST_PROFILE_ID, false);
+            removeItems(Session.class);
+
+            scopeService.delete(TEST_SCOPE);
+        } catch (Exception e) {
+            LOGGER.warn("Failed to clean up test data", e);
+        }
+
+
+    }
+
+    @Test
+    public void testV2CompatibilityModeSwitch() throws Exception {
+        LOGGER.info("Starting V2 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");
+        updateConfiguration(null,
+                "org.apache.unomi.rest.authentication",
+                "v2.compatibilitymode.enabled",
+                true);
+
+        // Wait for configuration to take effect
+        keepTrying("V2 compatibility mode not enabled in the required time",
+                () -> restAuthenticationConfig.isV2CompatibilityModeEnabled(),
+                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");
+        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",
+                false);
+
+        // Wait for configuration to take effect
+        keepTrying("V2 compatibility mode not disabled in the required time",
+                () -> restAuthenticationConfig.isV2CompatibilityModeEnabled(),
+                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");
+    }
+
+    /**
+     * Test behavior in V3 mode (default):
+     * - V2 requests (no auth) should be rejected
+     * - V3 requests with proper authentication should work
+     */
+    private void testV3ModeBehavior() throws Exception {
+        // Test V2-style request (no authentication) - should be rejected
+        ContextRequest contextRequest = new ContextRequest();
+        contextRequest.setSessionId(TEST_SESSION_ID);
+
+        HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL));
+        request.setEntity(new 
StringEntity(objectMapper.writeValueAsString(contextRequest), 
ContentType.APPLICATION_JSON));
+        TestUtils.RequestResponse response = 
TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID, 401, false);
+        assertEquals("V2-style request should be rejected in V3 mode", 401, 
response.getStatusCode());
+
+        // Test V3-style request with public API key - should work
+        request = new HttpPost(getFullUrl(CONTEXT_URL));
+        request.addHeader(UNOMI_API_KEY_HEADER, testPublicKey.getKey());
+        request.setEntity(new 
StringEntity(objectMapper.writeValueAsString(contextRequest), 
ContentType.APPLICATION_JSON));
+        response = TestUtils.executeContextJSONRequest(request, 
TEST_SESSION_ID);
+        assertEquals("V3-style request with public API key should work in V3 
mode", 200, response.getStatusCode());
+
+        // Test V3-style request with private API key - should work
+        request = new HttpPost(getFullUrl(CONTEXT_URL));
+        addPrivateTenantAuth(request, testTenant, testPrivateKey);
+        request.setEntity(new 
StringEntity(objectMapper.writeValueAsString(contextRequest), 
ContentType.APPLICATION_JSON));
+        response = TestUtils.executeContextJSONRequest(request, 
TEST_SESSION_ID);
+        assertEquals("V3-style request with private API key should work in V3 
mode", 200, response.getStatusCode());
+
+        // Test V3-style request with JAAS authentication - should work
+        request = new HttpPost(getFullUrl(CONTEXT_URL));
+        request.addHeader(UNOMI_TENANT_ID_HEADER, testTenant.getItemId());
+        request.setEntity(new 
StringEntity(objectMapper.writeValueAsString(contextRequest), 
ContentType.APPLICATION_JSON));
+
+        BasicCredentialsProvider credsProvider = new 
BasicCredentialsProvider();
+        credsProvider.setCredentials(AuthScope.ANY, new 
UsernamePasswordCredentials("karaf", "karaf"));
+
+        RequestConfig requestConfig = RequestConfig.custom()
+                .setAuthenticationEnabled(true)
+                
.setTargetPreferredAuthSchemes(Arrays.asList(AuthSchemes.BASIC))
+                .build();
+
+        CloseableHttpClient adminClient = HttpClients.custom()
+                .setDefaultCredentialsProvider(credsProvider)
+                .setDefaultRequestConfig(requestConfig)
+                .build();
+
+        CloseableHttpResponse jaasResponse = adminClient.execute(request);
+        assertEquals("V3-style request with JAAS auth should work in V3 mode", 
200, jaasResponse.getStatusLine().getStatusCode());
+        adminClient.close();
+    }
+
+    /**
+     * Test behavior in V2 compatibility mode:
+     * - V2 requests (no auth for public endpoints) should work
+     * - V3 requests should be rejected
+     */
+    private void testV2ModeBehavior() throws Exception {
+        // Test V2-style request (no authentication for public endpoint) - 
should work
+        ContextRequest contextRequest = new ContextRequest();
+        contextRequest.setSessionId(TEST_SESSION_ID);
+
+        HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL));
+        request.setEntity(new 
StringEntity(objectMapper.writeValueAsString(contextRequest), 
ContentType.APPLICATION_JSON));
+        TestUtils.RequestResponse response = 
TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID);
+        assertEquals("V2-style request should work in V2 compatibility mode", 
200, response.getStatusCode());
+
+        // 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(objectMapper.writeValueAsString(contextRequest), 
ContentType.APPLICATION_JSON));
+        response = TestUtils.executeContextJSONRequest(request, 
TEST_SESSION_ID);
+        assertEquals("V2-style request with X-Unomi-Peer should work in V2 
compatibility mode", 200, response.getStatusCode());
+
+        // Test V3-style request with public API key - should be rejected in 
V2 mode
+        request = new HttpPost(getFullUrl(CONTEXT_URL));
+        request.addHeader(UNOMI_API_KEY_HEADER, testPublicKey.getKey());
+        request.setEntity(new 
StringEntity(objectMapper.writeValueAsString(contextRequest), 
ContentType.APPLICATION_JSON));
+        response = TestUtils.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 have 0 
processed events in V2 mode", 0, 
response.getContextResponse().getProcessedEvents());
+
+        // Test V3-style request with private API key - should be rejected in 
V2 mode
+        request = new HttpPost(getFullUrl(CONTEXT_URL));
+        addPrivateTenantAuth(request, testTenant, testPrivateKey);
+        request.setEntity(new 
StringEntity(objectMapper.writeValueAsString(contextRequest), 
ContentType.APPLICATION_JSON));
+        response = TestUtils.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 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));
+
+        BasicCredentialsProvider credsProvider = new 
BasicCredentialsProvider();
+        credsProvider.setCredentials(AuthScope.ANY, new 
UsernamePasswordCredentials("karaf", "karaf"));
+
+        RequestConfig requestConfig = RequestConfig.custom()
+                .setAuthenticationEnabled(true)
+                
.setTargetPreferredAuthSchemes(Arrays.asList(AuthSchemes.BASIC))
+                .build();
+
+        CloseableHttpClient adminClient = HttpClients.custom()
+                .setDefaultCredentialsProvider(credsProvider)
+                .setDefaultRequestConfig(requestConfig)
+                .build();
+
+        CloseableHttpResponse jaasResponse = adminClient.execute(getRequest);
+        assertEquals("Private endpoint with JAAS auth should work in V2 
compatibility mode", 200, jaasResponse.getStatusLine().getStatusCode());
+        adminClient.close();
+    }
+
+    @Test
+    public void testV2CompatibilityModeWithProtectedEvents() throws Exception {
+        LOGGER.info("Testing V2 compatibility mode with protected events");
+
+        // 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);
+
+        // Test protected event (login) without V2 third-party authentication 
- should be rejected
+        Event loginEvent = new Event();
+        loginEvent.setEventType("login");
+        loginEvent.setScope(TEST_SCOPE);
+
+        ContextRequest contextRequest = new ContextRequest();
+        contextRequest.setSessionId(TEST_SESSION_ID);
+        contextRequest.setEvents(Arrays.asList(loginEvent));
+
+        HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL));
+        request.setEntity(new 
StringEntity(objectMapper.writeValueAsString(contextRequest), 
ContentType.APPLICATION_JSON));
+        TestUtils.RequestResponse response = 
TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID, 200, false);
+        assertEquals("Protected event without V2 auth should return 200", 200, 
response.getStatusCode());
+        assertEquals("Protected event without V2 auth should have 0 processed 
events", 0, response.getContextResponse().getProcessedEvents());
+
+        // Test protected event with V2 third-party authentication - should 
work
+        request = new HttpPost(getFullUrl(CONTEXT_URL));
+        request.addHeader(UNOMI_PEER_HEADER, 
"670c26d1cc413346c3b2fd9ce65dab41");
+        request.setEntity(new 
StringEntity(objectMapper.writeValueAsString(contextRequest), 
ContentType.APPLICATION_JSON));
+        response = TestUtils.executeContextJSONRequest(request, 
TEST_SESSION_ID);
+        assertEquals("Protected event with V2 auth should work", 200, 
response.getStatusCode());
+        assertEquals("Protected event with V2 auth should have 1 processed 
event", 1, response.getContextResponse().getProcessedEvents());
+
+        // Test protected event with empty X-Unomi-Peer header - should be 
rejected
+        request = new HttpPost(getFullUrl(CONTEXT_URL));
+        request.addHeader(UNOMI_PEER_HEADER, "");
+        request.setEntity(new 
StringEntity(objectMapper.writeValueAsString(contextRequest), 
ContentType.APPLICATION_JSON));
+        response = TestUtils.executeContextJSONRequest(request, 
TEST_SESSION_ID);
+        assertEquals("Protected event with empty X-Unomi-Peer should return 
200", 200, response.getStatusCode());
+        assertEquals("Protected event with empty X-Unomi-Peer should have 0 
processed events", 0, response.getContextResponse().getProcessedEvents());
+
+        // Test non-protected event (view) without authentication - should work
+        // Load the view event from JSON file
+        String contextRequestJson = resourceAsString("events/viewEvent.json");
+
+        // Replace the session ID with the test session ID
+        contextRequestJson = contextRequestJson.replace("test-session-id", 
TEST_SESSION_ID);
+        contextRequestJson = contextRequestJson.replace("testScope", 
TEST_SCOPE);
+
+        request = new HttpPost(getFullUrl(CONTEXT_URL));
+        request.setEntity(new StringEntity(contextRequestJson, 
ContentType.APPLICATION_JSON));
+        response = TestUtils.executeContextJSONRequest(request, 
TEST_SESSION_ID);
+        assertEquals("Non-protected event without auth should work in V2 
mode", 200, response.getStatusCode());
+        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(objectMapper.writeValueAsString(contextRequest), 
ContentType.APPLICATION_JSON));
+        TestUtils.RequestResponse response = 
TestUtils.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(objectMapper.writeValueAsString(contextRequest), 
ContentType.APPLICATION_JSON));
+        TestUtils.RequestResponse response = 
TestUtils.executeContextJSONRequest(request, TEST_SESSION_ID);
+        assertEquals("V2-style request should still work after service 
update", 200, response.getStatusCode());
+    }
+
+    private static void addPrivateTenantAuth(HttpPost request, Tenant tenant, 
ApiKey privateKey) {
+        request.setHeader("Authorization", "Basic " + 
Base64.getEncoder().encodeToString(
+            (tenant.getItemId() + ":" + privateKey.getKey()).getBytes()));
+    }
+
+    @Override
+    public void updateServices() throws InterruptedException {
+        super.updateServices();
+        restAuthenticationConfig = getService(RestAuthenticationConfig.class);
+    }
+}
diff --git a/kar/src/main/feature/feature.xml b/kar/src/main/feature/feature.xml
index d6768233b..2ded970f2 100644
--- a/kar/src/main/feature/feature.xml
+++ b/kar/src/main/feature/feature.xml
@@ -129,6 +129,7 @@
         <feature>unomi-services</feature>
         <feature>unomi-cxs-privacy-extension-services</feature>
         <configfile 
finalname="/etc/org.apache.unomi.schema.cfg">mvn:org.apache.unomi/unomi-json-schema-services/${project.version}/cfg/schemacfg</configfile>
+        <configfile 
finalname="/etc/org.apache.unomi.rest.authentication.cfg">mvn:org.apache.unomi/unomi-rest/${project.version}/cfg/restauth</configfile>
         <bundle 
start="false">mvn:org.apache.unomi/unomi-json-schema-services/${project.version}</bundle>
         <bundle 
start="false">mvn:org.apache.unomi/unomi-rest/${project.version}</bundle>
         <bundle 
start="false">mvn:org.apache.unomi/unomi-json-schema-rest/${project.version}</bundle>
diff --git a/package/src/main/resources/etc/custom.system.properties 
b/package/src/main/resources/etc/custom.system.properties
index 0f9a70cf1..f6385658b 100644
--- a/package/src/main/resources/etc/custom.system.properties
+++ b/package/src/main/resources/etc/custom.system.properties
@@ -511,3 +511,9 @@ karaf.local.roles = 
admin,manager,viewer,systembundles,ROLE_UNOMI_ADMIN,ROLE_UNO
 
#######################################################################################################################
 
org.apache.unomi.goals.refresh.interval=${env:UNOMI_GOALS_REFRESH_INTERVAL:-5000}
 
org.apache.unomi.campaigns.refresh.interval=${env:UNOMI_CAMPAIGNS_REFRESH_INTERVAL:-5000}
+
+#######################################################################################################################
+## 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}
diff --git a/rest/pom.xml b/rest/pom.xml
index 2c973eabe..315b9bbac 100644
--- a/rest/pom.xml
+++ b/rest/pom.xml
@@ -191,6 +191,30 @@
 
     <build>
         <plugins>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>build-helper-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>attach-artifacts</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>attach-artifact</goal>
+                        </goals>
+                        <configuration>
+                            <artifacts>
+                                <artifact>
+                                    <file>
+                                        
src/main/resources/org.apache.unomi.rest.authentication.cfg
+                                    </file>
+                                    <type>cfg</type>
+                                    <classifier>restauth</classifier>
+                                </artifact>
+                            </artifacts>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
             <plugin>
                 <groupId>org.apache.felix</groupId>
                 <artifactId>maven-bundle-plugin</artifactId>
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 47b0486dc..bfc88f4b1 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
@@ -107,6 +107,12 @@ public class AuthenticationFilter implements 
ContainerRequestFilter {
         try {
             String path = requestContext.getUriInfo().getPath();
 
+            // Check if V2 compatibility mode is enabled
+            if (restAuthenticationConfig.isV2CompatibilityModeEnabled()) {
+                handleV2CompatibilityMode(requestContext, path);
+                return;
+            }
+
             // Tenant endpoints require JAAS authentication only
             if (path.startsWith("tenants")) {
                 String authHeader = 
requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
@@ -239,6 +245,67 @@ public class AuthenticationFilter implements 
ContainerRequestFilter {
         }
     }
 
+    /**
+     * Handle authentication in V2 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 {
+        // For public paths, allow access without authentication (like V2)
+        if (isPublicPath(requestContext)) {
+            String defaultTenantId = 
restAuthenticationConfig.getV2CompatibilityDefaultTenantId();
+            if (defaultTenantId != null) {
+                // 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;
+            }
+        }
+
+        // For private endpoints, require system administrator authentication 
(like V2)
+        String authHeader = 
requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
+        if (authHeader != null && authHeader.startsWith(BASIC_AUTH_PREFIX)) {
+            try {
+                jaasAuthenticationFilter.filter(requestContext);
+                // Get the subject from the security context after successful 
JAAS auth
+                SecurityContext securityContext = 
JAXRSUtils.getCurrentMessage().get(SecurityContext.class);
+                if (securityContext != null) {
+                    Subject subject = ((RolePrefixSecurityContextImpl) 
securityContext).getSubject();
+                    // Set the authenticated subject in Unomi's security 
service
+                    securityService.setCurrentSubject(subject);
+
+                    // In V2 compatibility mode, use the default tenant for 
all operations
+                    String defaultTenantId = 
restAuthenticationConfig.getV2CompatibilityDefaultTenantId();
+                    if (defaultTenantId != null) {
+                        
executionContextManager.setCurrentContext(executionContextManager.createContext(defaultTenantId));
+                    } else {
+                        
executionContextManager.setCurrentContext(ExecutionContext.systemContext());
+                    }
+                }
+                return;
+            } catch (Exception e) {
+                logger.debug("V2 compatibility mode: JAAS authentication 
failed");
+            }
+        } else {
+            logger.debug("V2 compatibility mode: Missing Basic Auth header for 
private endpoint");
+        }
+
+        // If we get here, no valid authentication was provided
+        unauthorized(requestContext);
+    }
+
     private String[] extractBasicAuthCredentials(String authHeader) {
         try {
             String base64Credentials = 
authHeader.substring(BASIC_AUTH_PREFIX.length()).trim();
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 991c580e0..ed7022dc0 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
@@ -59,4 +59,23 @@ public interface RestAuthenticationConfig {
      * @return Global roles separated with single white spaces, like: "ROLE1 
ROLE2 ROLE3"
      */
     String getGlobalRoles();
+
+    /**
+     * Check if V2 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
+     */
+    boolean isV2CompatibilityModeEnabled();
+
+    /**
+     * 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/V2ThirdPartyConfigService.java
 
b/rest/src/main/java/org/apache/unomi/rest/authentication/V2ThirdPartyConfigService.java
new file mode 100644
index 000000000..954493f72
--- /dev/null
+++ 
b/rest/src/main/java/org/apache/unomi/rest/authentication/V2ThirdPartyConfigService.java
@@ -0,0 +1,259 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.unomi.rest.authentication;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.unomi.services.common.security.IPValidationUtils;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Modified;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Service to handle V2 third-party configuration for V2 compatibility mode.
+ * This service reads the legacy V2 third-party configuration and provides
+ * methods to validate protected events and third-party providers.
+ * Uses the original V2 configuration file: org.apache.unomi.thirdparty.cfg
+ */
+@Component(service = V2ThirdPartyConfigService.class, configurationPid = 
"org.apache.unomi.thirdparty")
+@Designate(ocd = V2ThirdPartyConfigService.Config.class)
+public class V2ThirdPartyConfigService {
+    
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(V2ThirdPartyConfigService.class);
+    
+    @ObjectClassDefinition(
+        name = "Apache Unomi Third-Party Configuration",
+        description = "Configuration for third-party providers (V2 
compatibility mode). " +
+                     "Providers are configured using the pattern: 
thirdparty.{providerName}.{property}. " +
+                     "Example: thirdparty.myapp.key, 
thirdparty.myapp.ipAddresses, thirdparty.myapp.allowedEvents"
+    )
+    public @interface Config {
+        // No hardcoded attributes - all providers are configured dynamically
+        // using the pattern: thirdparty.{providerName}.{property}
+    }
+    
+    /**
+     * Provider configuration data structure
+     */
+    private static class ProviderConfig {
+        private final String key;
+        private final Set<String> ipAddresses;
+        private final Set<String> allowedEvents;
+        
+        public ProviderConfig(String key, Set<String> ipAddresses, Set<String> 
allowedEvents) {
+            this.key = key;
+            this.ipAddresses = ipAddresses;
+            this.allowedEvents = allowedEvents;
+        }
+        
+        public String getKey() { return key; }
+        public Set<String> getIpAddresses() { return ipAddresses; }
+        public Set<String> getAllowedEvents() { return allowedEvents; }
+    }
+    
+    private volatile Map<String, ProviderConfig> providers = new HashMap<>();
+    
+    @Activate
+    public void activate(Map<String, Object> properties) {
+        modified(properties);
+    }
+    
+    @Modified
+    public void modified(Map<String, Object> properties) {
+        Map<String, ProviderConfig> newProviders = new HashMap<>();
+        
+        if (properties != null) {
+            // Parse all provider configurations dynamically
+            for (Map.Entry<String, Object> entry : properties.entrySet()) {
+                String key = entry.getKey();
+                String value = entry.getValue() != null ? 
entry.getValue().toString() : "";
+                
+                // Look for provider configuration patterns: 
thirdparty.{providerName}.{property}
+                if (key.startsWith("thirdparty.") && key.contains(".")) {
+                    String[] parts = key.split("\\.");
+                    if (parts.length >= 3) {
+                        String providerName = parts[1];
+                        String property = parts[2];
+                        
+                        ProviderConfig existingConfig = 
newProviders.get(providerName);
+                        String configKey = existingConfig != null ? 
existingConfig.getKey() : "";
+                        Set<String> configIpAddresses = existingConfig != null 
? existingConfig.getIpAddresses() : new HashSet<>();
+                        Set<String> configAllowedEvents = existingConfig != 
null ? existingConfig.getAllowedEvents() : new HashSet<>();
+                        
+                        switch (property) {
+                            case "key":
+                                configKey = value;
+                                break;
+                            case "ipAddresses":
+                                configIpAddresses = 
parseCommaSeparatedList(value);
+                                break;
+                            case "allowedEvents":
+                                configAllowedEvents = 
parseCommaSeparatedList(value);
+                                break;
+                        }
+                        
+                        // Only add provider if it has a key (required for 
authentication)
+                        if (StringUtils.isNotBlank(configKey)) {
+                            newProviders.put(providerName, new 
ProviderConfig(configKey, configIpAddresses, configAllowedEvents));
+                        }
+                    }
+                }
+            }
+        }
+        
+        // Set default provider1 if no providers configured
+        if (newProviders.isEmpty()) {
+            newProviders.put("provider1", new ProviderConfig(
+                "670c26d1cc413346c3b2fd9ce65dab41",
+                new HashSet<>(Arrays.asList("127.0.0.1", "::1")),
+                new HashSet<>(Arrays.asList("login", "updateProperties"))
+            ));
+        }
+        
+        this.providers = newProviders;
+        
+        int totalEvents = newProviders.values().stream()
+            .mapToInt(config -> config.getAllowedEvents().size())
+            .sum();
+        
+        LOGGER.info("V2 Third-Party Configuration updated - {} providers with 
{} total protected events", 
+                   newProviders.size(), totalEvents);
+    }
+    
+    /**
+     * Check if an event type is protected (requires third-party 
authentication).
+     * 
+     * @param eventType the event type to check
+     * @return true if the event type is protected, false otherwise
+     */
+    public boolean isProtectedEventType(String eventType) {
+        if (StringUtils.isBlank(eventType)) {
+            return false;
+        }
+        
+        return providers.values().stream()
+            .anyMatch(config -> config.getAllowedEvents().contains(eventType));
+    }
+    
+    /**
+     * Get all protected event types from all providers.
+     * 
+     * @return set of all protected event types
+     */
+    public Set<String> getAllProtectedEventTypes() {
+        Set<String> allProtectedEvents = new HashSet<>();
+        for (ProviderConfig config : providers.values()) {
+            allProtectedEvents.addAll(config.getAllowedEvents());
+        }
+        return Collections.unmodifiableSet(allProtectedEvents);
+    }
+    
+
+    /**
+     * Validate a third-party provider by key for a given event type.
+     * This method is used when the X-Unomi-Peer header contains the provider 
key.
+     * 
+     * @param providerKey the third-party provider key (from X-Unomi-Peer 
header)
+     * @param eventType the event type to validate
+     * @param sourceIP the source IP address
+     * @return true if the provider is authorized for this event type and IP, 
false otherwise
+     */
+    public boolean validateProviderByKey(String providerKey, String eventType, 
String sourceIP) {
+        if (StringUtils.isBlank(providerKey) || StringUtils.isBlank(eventType) 
|| StringUtils.isBlank(sourceIP)) {
+            return false;
+        }
+        
+        // Find the provider that has the matching key
+        ProviderConfig config = null;
+        String foundProviderId = null;
+        for (Map.Entry<String, ProviderConfig> entry : providers.entrySet()) {
+            if (providerKey.equals(entry.getValue().getKey())) {
+                config = entry.getValue();
+                foundProviderId = entry.getKey();
+                break;
+            }
+        }
+        
+        if (config == null) {
+            LOGGER.debug("V2 compatibility mode: Unknown provider key: {}", 
providerKey);
+            return false;
+        }
+        
+        if (!config.getAllowedEvents().contains(eventType)) {
+            LOGGER.debug("V2 compatibility mode: Event type {} not allowed for 
provider {} (key: {})", eventType, foundProviderId, providerKey);
+            return false;
+        }
+        
+        boolean ipAuthorized = IPValidationUtils.isIpAuthorized(sourceIP, 
config.getIpAddresses());
+        if (!ipAuthorized) {
+            LOGGER.debug("V2 compatibility mode: IP {} not authorized for 
provider {} (key: {})", sourceIP, foundProviderId, providerKey);
+        }
+        
+        return ipAuthorized;
+    }
+    
+    /**
+     * Get the key for a third-party provider.
+     * 
+     * @param providerId the third-party provider ID
+     * @return the provider key, or null if not found
+     */
+    public String getProviderKey(String providerId) {
+        ProviderConfig config = providers.get(providerId);
+        return config != null ? config.getKey() : null;
+    }
+    
+    /**
+     * Check if a provider ID is valid.
+     * 
+     * @param providerId the third-party provider ID
+     * @return true if the provider ID is valid, false otherwise
+     */
+    public boolean isValidProvider(String providerId) {
+        return providers.containsKey(providerId);
+    }
+    
+    private Set<String> parseCommaSeparatedList(String value) {
+        if (StringUtils.isBlank(value)) {
+            return new HashSet<>();
+        }
+        
+        Set<String> result = new HashSet<>();
+        String[] parts = value.split(",");
+        for (String part : parts) {
+            String trimmed = part.trim();
+            if (StringUtils.isNotBlank(trimmed)) {
+                result.add(trimmed);
+            }
+        }
+        return result;
+    }
+    
+
+}
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 d65cdfa0e..ea6690140 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
@@ -19,6 +19,12 @@ package org.apache.unomi.rest.authentication.impl;
 import org.apache.unomi.api.security.UnomiRoles;
 import org.apache.unomi.rest.authentication.RestAuthenticationConfig;
 import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Modified;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.util.*;
 import java.util.regex.Pattern;
@@ -26,9 +32,10 @@ import java.util.regex.Pattern;
 /**
  * Default implementation for the unomi authentication on Rest endpoints
  */
-@Component(service = RestAuthenticationConfig.class)
+@Component(service = { RestAuthenticationConfig.class}, configurationPid = 
"org.apache.unomi.rest.authentication", immediate = true)
 public class DefaultRestAuthenticationConfig implements 
RestAuthenticationConfig {
 
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(DefaultRestAuthenticationConfig.class);
     private static final String GUEST_ROLES = UnomiRoles.USER;
     private static final String ADMIN_ROLES = UnomiRoles.ADMINISTRATOR;
     private static final String TENANT_ADMIN_ROLES = UnomiRoles.ADMINISTRATOR 
+ " " + UnomiRoles.TENANT_ADMINISTRATOR;
@@ -63,6 +70,26 @@ public class DefaultRestAuthenticationConfig implements 
RestAuthenticationConfig
         ROLES_MAPPING = Collections.unmodifiableMap(roles);
     }
 
+    private volatile boolean v2CompatibilityModeEnabled = false;
+    private volatile String v2CompatibilityDefaultTenantId = "default";
+
+
+    @Activate
+    @Modified
+    public void modified(Config config) {
+        if (config == null) {
+            LOGGER.warn("Config is null in modified method");
+            return;
+        }
+        boolean v2Mode = config.v2_compatibilitymode_enabled();
+        String defaultTenant = config.v2_compatibilitymode_defaultTenantId();
+        LOGGER.info("Configuration updated - v2CompatibilityModeEnabled: {}, 
v2CompatibilityDefaultTenantId: {}",
+                    v2Mode, defaultTenant);
+            this.v2CompatibilityModeEnabled = v2Mode;
+            this.v2CompatibilityDefaultTenantId = defaultTenant;
+    }
+
+
     @Override
     public List<Pattern> getPublicPathPatterns() {
         return PUBLIC_PATH_PATTERNS;
@@ -77,4 +104,33 @@ public class DefaultRestAuthenticationConfig implements 
RestAuthenticationConfig
     public String getGlobalRoles() {
         return TENANT_ADMIN_ROLES;
     }
+
+    @Override
+    public boolean isV2CompatibilityModeEnabled() {
+        return v2CompatibilityModeEnabled;
+    }
+
+    @Override
+    public String getV2CompatibilityDefaultTenantId() {
+        return v2CompatibilityDefaultTenantId;
+    }
+
+    @ObjectClassDefinition(
+        name = "Unomi REST Authentication Configuration",
+        description = "Configuration for Unomi REST authentication including 
V2 compatibility mode"
+    )
+    public @interface Config {
+
+        @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;
+
+        @AttributeDefinition(
+            name = "V2 Compatibility Default Tenant ID",
+            description = "Default tenant ID to use in V2 compatibility mode"
+        )
+        String v2_compatibilitymode_defaultTenantId() default "default";
+    }
 }
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 a8ca7d9f3..f1dfb466b 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
@@ -29,6 +29,8 @@ import org.apache.unomi.api.services.PrivacyService;
 import org.apache.unomi.api.services.ProfileService;
 import org.apache.unomi.api.tenants.Tenant;
 import org.apache.unomi.api.tenants.TenantService;
+import org.apache.unomi.rest.authentication.RestAuthenticationConfig;
+import org.apache.unomi.rest.authentication.V2ThirdPartyConfigService;
 import org.apache.unomi.rest.exception.InvalidRequestException;
 import org.apache.unomi.rest.service.RestServiceUtils;
 import org.apache.unomi.schema.api.SchemaService;
@@ -80,6 +82,12 @@ public class RestServiceUtilsImpl implements 
RestServiceUtils {
     @Reference
     private TenantService tenantService;
 
+    @Reference
+    private RestAuthenticationConfig restAuthenticationConfig;
+
+    @Reference
+    private V2ThirdPartyConfigService v2ThirdPartyConfigService;
+
     @Override
     public String getProfileIdCookieValue(HttpServletRequest 
httpServletRequest) {
         String cookieProfileId = null;
@@ -269,11 +277,22 @@ 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());
-                    if (!eventService.isEventAllowedForTenant(event, tenantId, 
eventsRequestContext.getRequest().getRemoteAddr())) {
-                        LOGGER.debug("Tenant is not authorized to send event 
{} from IP {}", event.getEventType(), 
eventsRequestContext.getRequest().getRemoteAddr());
-                        //Don't count the event that failed
-                        
eventsRequestContext.setProcessedItems(eventsRequestContext.getProcessedItems() 
- 1);
-                        continue;
+                    // Check if V2 compatibility mode is enabled and handle 
V2-style event authorization
+                    if 
(restAuthenticationConfig.isV2CompatibilityModeEnabled()) {
+                        if (!isEventAllowedInV2CompatibilityMode(event, 
eventsRequestContext.getRequest())) {
+                            LOGGER.debug("Event {} not authorized in V2 
compatibility mode from IP {}", event.getEventType(), 
eventsRequestContext.getRequest().getRemoteAddr());
+                            //Don't count the event that failed
+                            
eventsRequestContext.setProcessedItems(eventsRequestContext.getProcessedItems() 
- 1);
+                            continue;
+                        }
+                    } else {
+                        // Normal V3 event authorization
+                        if (!eventService.isEventAllowedForTenant(event, 
tenantId, eventsRequestContext.getRequest().getRemoteAddr())) {
+                            LOGGER.debug("Tenant is not authorized to send 
event {} from IP {}", event.getEventType(), 
eventsRequestContext.getRequest().getRemoteAddr());
+                            //Don't count the event that failed
+                            
eventsRequestContext.setProcessedItems(eventsRequestContext.getProcessedItems() 
- 1);
+                            continue;
+                        }
                     }
                     if 
(securityContext.isUserInRole(UnomiRoles.TENANT_ADMINISTRATOR) && 
event.getItemId() != null) {
                         eventToSend = new Event(event.getItemId(), 
event.getEventType(), eventsRequestContext.getSession(), 
eventsRequestContext.getProfile(), event.getScope(),
@@ -376,4 +395,40 @@ public class RestServiceUtilsImpl implements 
RestServiceUtils {
         profile.setProperty("firstVisit", timestamp);
         return profile;
     }
+
+    /**
+     * Check if an event is allowed in V2 compatibility mode.
+     * In V2, protected events required IP + X-Unomi-Peer (third-party key) 
authentication.
+     *
+     * @param event the event to check
+     * @param request the HTTP request
+     * @return true if the event is allowed, false otherwise
+     */
+    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
+            return true;
+        }
+
+        // For protected events, check IP + third-party key (V2-style)
+        String sourceIP = request.getRemoteAddr();
+        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());
+            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: {}",
+                        event.getEventType(), thirdPartyKey, sourceIP);
+            return false;
+        }
+
+        LOGGER.debug("V2 compatibility mode: Protected event {} allowed for 
provider key: {} from IP: {}",
+                    event.getEventType(), 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
new file mode 100644
index 000000000..db79d2627
--- /dev/null
+++ b/rest/src/main/resources/org.apache.unomi.rest.authentication.cfg
@@ -0,0 +1,31 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Unomi REST Authentication Configuration
+# This file configures authentication settings for Unomi REST endpoints
+
+# V2 Compatibility Mode
+# When enabled, allows V2 clients to 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
+v2.compatibilitymode.enabled = 
${org.apache.unomi.rest.authentication.v2CompatibilityModeEnabled:-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
+# Should match the tenant ID used during migration (e.g., "default" or 
"system")
+v2.compatibilitymode.defaultTenantId = 
${org.apache.unomi.rest.authentication.v2CompatibilityDefaultTenantId:-default}

Reply via email to