This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch drop-public-cors-credentials in repository https://gitbox.apache.org/repos/asf/unomi.git
commit 1068f7de23ccf6fd5d987085e1dfe33c617e88d8 Author: Serge Huber <[email protected]> AuthorDate: Tue Sep 8 20:35:40 2026 +0200 Drop credentialed CORS on the public tracker endpoints. Keep any-origin access for browser clients; do not advertise credentials on context, eventcollector, or client. --- manual/src/main/asciidoc/configuration.adoc | 2 +- .../unomi/rest/endpoints/ClientEndpoint.java | 2 +- .../unomi/rest/endpoints/ContextJsonEndpoint.java | 2 +- .../rest/endpoints/EventsCollectorEndpoint.java | 2 +- .../rest/endpoints/PublicTrackerCorsTest.java | 52 ++++++++++++++++++++++ 5 files changed, 56 insertions(+), 4 deletions(-) diff --git a/manual/src/main/asciidoc/configuration.adoc b/manual/src/main/asciidoc/configuration.adoc index 5f51a0561..75844b7e6 100644 --- a/manual/src/main/asciidoc/configuration.adoc +++ b/manual/src/main/asciidoc/configuration.adoc @@ -452,7 +452,7 @@ curl -X GET "http://localhost:8181/cxs/tenants" \ Authentication rules (Unomi 3.1): -1. Public paths (for example `/cxs/context.json`, `/cxs/eventcollector`, `/cxs/client/*`) require a valid **public** API key via `X-Unomi-Api-Key` (unless <<_v2_compatibility_mode,V2 compatibility mode>> is enabled). +1. Public paths (for example `/cxs/context.json`, `/cxs/eventcollector`, `/cxs/client/*`) require a valid **public** API key via `X-Unomi-Api-Key` (unless <<_v2_compatibility_mode,V2 compatibility mode>> is enabled). Browser clients may call them from any origin; those responses do not include credentialed CORS headers. 2. Private paths accept `tenantId:privateApiKey` Basic auth, or JAAS admin credentials (often with `X-Unomi-Tenant-Id` when a tenant context is required). 3. Tenant administration (`/cxs/tenants`) requires system administrator JAAS credentials. 4. JAAS admin does **not** replace the public API key on public endpoints. diff --git a/rest/src/main/java/org/apache/unomi/rest/endpoints/ClientEndpoint.java b/rest/src/main/java/org/apache/unomi/rest/endpoints/ClientEndpoint.java index 6fae685b0..7a8be14df 100644 --- a/rest/src/main/java/org/apache/unomi/rest/endpoints/ClientEndpoint.java +++ b/rest/src/main/java/org/apache/unomi/rest/endpoints/ClientEndpoint.java @@ -43,7 +43,7 @@ import java.util.Set; /** * A servlet filter to serve a context-specific Javascript containing the current request context object. */ -@CrossOriginResourceSharing(allowAllOrigins = true, allowCredentials = true) +@CrossOriginResourceSharing(allowAllOrigins = true, allowCredentials = false) @Path("/") @Component(service = ClientEndpoint.class, property = "osgi.jaxrs.resource=true") public class ClientEndpoint { diff --git a/rest/src/main/java/org/apache/unomi/rest/endpoints/ContextJsonEndpoint.java b/rest/src/main/java/org/apache/unomi/rest/endpoints/ContextJsonEndpoint.java index c36b663dc..f984482e2 100644 --- a/rest/src/main/java/org/apache/unomi/rest/endpoints/ContextJsonEndpoint.java +++ b/rest/src/main/java/org/apache/unomi/rest/endpoints/ContextJsonEndpoint.java @@ -57,7 +57,7 @@ import java.util.stream.Collectors; * events, evaluate personalization filters, and return consented profile data. */ @Consumes(MediaType.APPLICATION_JSON) -@CrossOriginResourceSharing(allowAllOrigins = true, allowCredentials = true) +@CrossOriginResourceSharing(allowAllOrigins = true, allowCredentials = false) @Path("/") @Component(service = ContextJsonEndpoint.class, property = "osgi.jaxrs.resource=true") public class ContextJsonEndpoint { diff --git a/rest/src/main/java/org/apache/unomi/rest/endpoints/EventsCollectorEndpoint.java b/rest/src/main/java/org/apache/unomi/rest/endpoints/EventsCollectorEndpoint.java index 48e7e5c7f..b6f646e24 100644 --- a/rest/src/main/java/org/apache/unomi/rest/endpoints/EventsCollectorEndpoint.java +++ b/rest/src/main/java/org/apache/unomi/rest/endpoints/EventsCollectorEndpoint.java @@ -49,7 +49,7 @@ import java.util.List; */ @Produces(MediaType.APPLICATION_JSON + ";charset=UTF-8") @Consumes(MediaType.APPLICATION_JSON) -@CrossOriginResourceSharing(allowAllOrigins = true, allowCredentials = true) +@CrossOriginResourceSharing(allowAllOrigins = true, allowCredentials = false) @Path("/") @Component(service = EventsCollectorEndpoint.class, property = "osgi.jaxrs.resource=true") public class EventsCollectorEndpoint { diff --git a/rest/src/test/java/org/apache/unomi/rest/endpoints/PublicTrackerCorsTest.java b/rest/src/test/java/org/apache/unomi/rest/endpoints/PublicTrackerCorsTest.java new file mode 100644 index 000000000..d9b0bc594 --- /dev/null +++ b/rest/src/test/java/org/apache/unomi/rest/endpoints/PublicTrackerCorsTest.java @@ -0,0 +1,52 @@ +/* + * 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.endpoints; + +import org.apache.cxf.rs.security.cors.CrossOriginResourceSharing; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Public tracker endpoints keep a broad origin policy and do not advertise credentialed CORS. + */ +class PublicTrackerCorsTest { + + @Test + void contextJsonAllowsAnyOriginWithoutCredentials() { + assertPublicTrackerCors(ContextJsonEndpoint.class); + } + + @Test + void eventsCollectorAllowsAnyOriginWithoutCredentials() { + assertPublicTrackerCors(EventsCollectorEndpoint.class); + } + + @Test + void clientAllowsAnyOriginWithoutCredentials() { + assertPublicTrackerCors(ClientEndpoint.class); + } + + private static void assertPublicTrackerCors(Class<?> endpoint) { + CrossOriginResourceSharing cors = endpoint.getAnnotation(CrossOriginResourceSharing.class); + assertNotNull(cors, endpoint.getSimpleName() + " must declare CORS"); + assertTrue(cors.allowAllOrigins()); + assertFalse(cors.allowCredentials()); + } +}
