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 @@ 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 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 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/itests/src/test/resources/events/viewEvent.json b/itests/src/test/resources/events/viewEvent.json new file mode 100644 index 000000000..424621384 --- /dev/null +++ b/itests/src/test/resources/events/viewEvent.json @@ -0,0 +1,37 @@ +{ + "sessionId": "test-session-id", + "events": [ + { + "eventType": "view", + "scope": "testScope", + "source": { + "itemType": "site", + "scope": "testScope", + "itemId": "test-site" + }, + "target": { + "itemType": "page", + "scope": "testScope", + "itemId": "test-page", + "properties": { + "pageInfo": { + "pageID": "test-page", + "nodeType": "jnt:page", + "pageName": "Test Page", + "pagePath": "/test-page", + "templateName": "test", + "destinationURL": "http://localhost:8080/test-page", + "destinationSearch": "", + "referringURL": "http://localhost:8080/", + "language": "en", + "categories": [], + "tags": [], + "sameDomainReferrer": false + }, + "consentTypes": [] + } + }, + "flattenedProperties": {} + } + ] +} 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 @@ unomi-services unomi-cxs-privacy-extension-services mvn:org.apache.unomi/unomi-json-schema-services/${project.version}/cfg/schemacfg + mvn:org.apache.unomi/unomi-rest/${project.version}/cfg/restauth mvn:org.apache.unomi/unomi-json-schema-services/${project.version} mvn:org.apache.unomi/unomi-rest/${project.version} mvn:org.apache.unomi/unomi-json-schema-rest/${project.version} 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 @@ + + org.codehaus.mojo + build-helper-maven-plugin + + + attach-artifacts + package + + attach-artifact + + + + + + src/main/resources/org.apache.unomi.rest.authentication.cfg + + cfg + restauth + + + + + + org.apache.felix maven-bundle-plugin 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 void filter(ContainerRequestContext requestContext) throws IOException { 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 void filter(ContainerRequestContext requestContext) throws IOException { } } + /** + * 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 ipAddresses; + private final Set allowedEvents; + + public ProviderConfig(String key, Set ipAddresses, Set allowedEvents) { + this.key = key; + this.ipAddresses = ipAddresses; + this.allowedEvents = allowedEvents; + } + + public String getKey() { return key; } + public Set getIpAddresses() { return ipAddresses; } + public Set getAllowedEvents() { return allowedEvents; } + } + + private volatile Map providers = new HashMap<>(); + + @Activate + public void activate(Map properties) { + modified(properties); + } + + @Modified + public void modified(Map properties) { + Map newProviders = new HashMap<>(); + + if (properties != null) { + // Parse all provider configurations dynamically + for (Map.Entry 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 configIpAddresses = existingConfig != null ? existingConfig.getIpAddresses() : new HashSet<>(); + Set 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 getAllProtectedEventTypes() { + Set 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 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 parseCommaSeparatedList(String value) { + if (StringUtils.isBlank(value)) { + return new HashSet<>(); + } + + Set 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 @@ 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 @@ /** * 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 getPublicPathPatterns() { return PUBLIC_PATH_PATTERNS; @@ -77,4 +104,33 @@ public Map getMethodRolesMap() { 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.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 EventsRequestContext performEventsRequest(List events, EventsReque 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 @@ private Profile createNewProfile(String existingProfileId, Date timestamp) { 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}