Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,6 @@
* </ul>
* </p>
*
* <p>Dependency-injection setters on this class are intended for OSGi/Blueprint wiring and are not part of the
* {@link IRouterCamelContext} API surface.</p>
*
* @since 1.0
*/
public class RouterCamelContext implements IRouterCamelContext {
Expand Down Expand Up @@ -144,11 +141,6 @@ public void init() throws Exception {
LOGGER.info("Camel Context initialized successfully.");
}

/**
* Stops the configuration refresh scheduler and shuts down the Camel context (all routes and components).
*
* @throws Exception if Camel shutdown fails
*/
public void destroy() throws Exception {
if (scheduledTask != null) {
schedulerService.cancelTask(scheduledTask.getItemId());
Expand Down
1 change: 1 addition & 0 deletions itests/src/test/java/org/apache/unomi/itests/AllITs.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
GraphQLProfileAliasesIT.class,
SendEventActionIT.class,
ScopeIT.class,
V2CompatibilityModeIT.class,
HealthCheckIT.class,
LegacyQueryBuilderMappingIT.class,
})
Expand Down
5 changes: 4 additions & 1 deletion itests/src/test/java/org/apache/unomi/itests/PatchIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.apache.unomi.api.actions.ActionType;
import org.apache.unomi.api.conditions.ConditionType;
import org.apache.unomi.persistence.spi.CustomObjectMapper;
import java.util.Objects;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
Expand Down Expand Up @@ -147,7 +148,9 @@ public void testPatchOnConditionType() throws IOException, InterruptedException

@Test
public void testPatchOnActionType() throws IOException, InterruptedException {
ActionType mailAction = definitionsService.getActionType("sendMailAction");
ActionType mailAction = keepTrying("sendMailAction should exist",
() -> definitionsService.getActionType("sendMailAction"),
Objects::nonNull, DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
Assert.assertNotNull("sendMailAction should exist", mailAction);
Assert.assertNotNull("ActionType metadata should not be null", mailAction.getMetadata());
Assert.assertNotNull("ActionType systemTags should not be null", mailAction.getMetadata().getSystemTags());
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public void testCRUD() throws Exception {
}

refreshPersistence(UserList.class);
refreshPersistence(Profile.class);

final ResponseContext findListsContext = keepTrying("Failed waiting for profile in list query",
() -> {
Expand Down
37 changes: 37 additions & 0 deletions itests/src/test/resources/events/viewEvent.json
Original file line number Diff line number Diff line change
@@ -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": {}
}
]
}
1 change: 1 addition & 0 deletions kar/src/main/feature/feature.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
6 changes: 6 additions & 0 deletions package/src/main/resources/etc/custom.system.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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}
24 changes: 24 additions & 0 deletions rest/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.apache.karaf.jaas.boot.principal.UserPrincipal;
import org.apache.unomi.api.ExecutionContext;
import org.apache.unomi.api.security.SecurityService;
import org.apache.unomi.api.security.TenantPrincipal;
import org.apache.unomi.api.security.UnomiRoles;
import org.apache.unomi.api.services.ExecutionContextManager;
import org.apache.unomi.api.tenants.ApiKey;
Expand All @@ -41,6 +42,8 @@
import javax.ws.rs.container.PreMatching;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import org.apache.commons.lang3.StringUtils;

import java.io.IOException;
import java.util.Base64;
import java.util.Collections;
Expand Down Expand Up @@ -107,6 +110,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);
Expand Down Expand Up @@ -239,6 +248,87 @@ 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 (StringUtils.isNotBlank(defaultTenantId)) {
Tenant defaultTenant = tenantService.getTenant(defaultTenantId);
if (defaultTenant == null) {
logger.error("V2 compatibility mode: configured default tenant '{}' does not exist", defaultTenantId);
unauthorized(requestContext);
return;
}
// Create a guest subject for public endpoints
Subject subject = securityService.createSubject(defaultTenantId, false);

Comment thread
sergehuber marked this conversation as resolved.
// Set CXF security context
JAXRSUtils.getCurrentMessage().put(SecurityContext.class,
new RolePrefixSecurityContextImpl(subject, ROLE_CLASSIFIER, ROLE_CLASSIFIER_TYPE));

// Set the security service subject
securityService.setCurrentSubject(subject);

// Set the execution context for the default tenant
executionContextManager.setCurrentContext(executionContextManager.createContext(defaultTenantId));
return;
} else {
logger.warn("V2 compatibility mode: public path request denied because v2CompatibilityDefaultTenantId is not configured");
unauthorized(requestContext);
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);
// JAASAuthenticationFilter handles credential failures internally (calls abortWith + returns normally).
// A null security context here means auth was rejected or an unexpected state occurred — deny either way.
SecurityContext securityContext = JAXRSUtils.getCurrentMessage().get(SecurityContext.class);
if (securityContext == null) {
logger.debug("V2 compatibility mode: no security context after JAAS filter, denying access");
unauthorized(requestContext);
return;
}
Subject jaasSubject = ((RolePrefixSecurityContextImpl) securityContext).getSubject();

// Build a merged subject that combines the JAAS principals with a TenantPrincipal
// for the default tenant, so that resolveTenantId() can find it downstream.
String defaultTenantId = restAuthenticationConfig.getV2CompatibilityDefaultTenantId();
Subject mergedSubject = new Subject();
mergedSubject.getPrincipals().addAll(jaasSubject.getPrincipals());
if (StringUtils.isNotBlank(defaultTenantId)) {
mergedSubject.getPrincipals().add(new TenantPrincipal(defaultTenantId));
executionContextManager.setCurrentContext(executionContextManager.createContext(defaultTenantId));
} else {
executionContextManager.setCurrentContext(ExecutionContext.systemContext());
}
JAXRSUtils.getCurrentMessage().put(SecurityContext.class,
new RolePrefixSecurityContextImpl(mergedSubject, ROLE_CLASSIFIER, ROLE_CLASSIFIER_TYPE));
securityService.setCurrentSubject(mergedSubject);
return;
} catch (Exception e) {
// Only fires for unexpected exceptions — credential failures are handled inside JAASAuthenticationFilter.
logger.debug("V2 compatibility mode: unexpected exception during JAAS processing", e);
}
} 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Loading
Loading