Skip to content

Commit d0bdee7

Browse files
authored
[Security] Access constraints are not enforced for Java endpoints (#6280)
1 parent de11e6d commit d0bdee7

5 files changed

Lines changed: 173 additions & 1 deletion

File tree

components/engine/engine-security/src/main/java/org/eclipse/dirigible/components/security/config/SecurityFilterConfig.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,14 @@ public FilterRegistrationBean<SecurityFilter> securityFilterRegistrationBean(Sec
3434
filterRegistrationBean.addUrlPatterns(//
3535
"/services/js/*", //
3636
"/services/ts/*", //
37+
"/services/java/*", //
3738
"/services/public/*", //
3839
"/services/web/*", //
3940
"/services/wiki/*", //
4041
"/services/command/*", //
4142
"/public/js/*", //
4243
"/public/ts/*", //
44+
"/public/java/*", //
4345
"/public/public/*", //
4446
"/public/web/*", //
4547
"/public/wiki/*", //

components/engine/engine-security/src/main/java/org/eclipse/dirigible/components/security/filter/SecurityFilter.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,12 @@ public SecurityFilter(AccessVerifier securityAccessVerifier) {
7878
public void init(FilterConfig filterConfig) {
7979
SECURED_PREFIXES.add("/services/js");
8080
SECURED_PREFIXES.add("/services/ts");
81+
SECURED_PREFIXES.add("/services/java");
8182
SECURED_PREFIXES.add("/services/public");
8283
SECURED_PREFIXES.add("/services/web");
8384
SECURED_PREFIXES.add("/services/wiki");
8485
SECURED_PREFIXES.add("/services/command");
86+
SECURED_PREFIXES.add("/public/java");
8587

8688
ALLOWED_PREFIXES.add("/services/web/resources");
8789
ALLOWED_PREFIXES.add("/services/js/platform");

components/engine/engine-security/src/main/java/org/eclipse/dirigible/components/security/synchronizer/AccessSynchronizer.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.eclipse.dirigible.components.security.domain.Access;
2222
import org.eclipse.dirigible.components.security.domain.Constraints;
2323
import org.eclipse.dirigible.components.security.service.AccessService;
24+
import org.eclipse.dirigible.components.security.verifier.AccessVerifier;
2425
import org.slf4j.Logger;
2526
import org.slf4j.LoggerFactory;
2627
import org.springframework.beans.factory.annotation.Autowired;
@@ -55,6 +56,11 @@ public class AccessSynchronizer extends BaseSynchronizer<Access, Long> {
5556
*/
5657
private final AccessService securityAccessService;
5758

59+
/**
60+
* The security access verifier.
61+
*/
62+
private final AccessVerifier securityAccessVerifier;
63+
5864
/**
5965
* The synchronization callback.
6066
*/
@@ -64,10 +70,12 @@ public class AccessSynchronizer extends BaseSynchronizer<Access, Long> {
6470
* Instantiates a new security access synchronizer.
6571
*
6672
* @param securityAccessService the security access service
73+
* @param securityAccessVerifier the security access verifier
6774
*/
6875
@Autowired
69-
public AccessSynchronizer(AccessService securityAccessService) {
76+
public AccessSynchronizer(AccessService securityAccessService, AccessVerifier securityAccessVerifier) {
7077
this.securityAccessService = securityAccessService;
78+
this.securityAccessVerifier = securityAccessVerifier;
7179
}
7280

7381
/**
@@ -179,6 +187,19 @@ public void cleanupImpl(Access access) {
179187
}
180188
}
181189

190+
/**
191+
* Called once at the end of every synchronization round. Reloads the {@link AccessVerifier} cache
192+
* so that constraints synchronized into the database are enforced as soon as the round completes.
193+
* The verifier's scheduled refresh reacts to registry filesystem events only, which never fire on
194+
* instances whose registry content is pre-baked into the container image - and even on
195+
* publish-driven instances the synchronization round consumes the watcher flag before the
196+
* verifier's poll can observe it.
197+
*/
198+
@Override
199+
public void finishing() {
200+
securityAccessVerifier.refreshCache(true);
201+
}
202+
182203
/**
183204
* Sets the callback.
184205
*

tests/tests-framework/src/main/java/org/eclipse/dirigible/tests/framework/restassured/RestAssuredExecutor.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,4 +156,18 @@ public void execute(CallableNoResultAndNoException callable, String user, String
156156
this.execute(callable, "localhost", user, password);
157157
}
158158

159+
public void execute(CallableNoResultAndNoException callable, String user, String password, long timeoutSeconds) {
160+
await().atMost(timeoutSeconds, TimeUnit.SECONDS)
161+
.pollInterval(500, TimeUnit.MILLISECONDS)
162+
.until(() -> {
163+
try {
164+
this.execute(callable, user, password);
165+
return true;
166+
} catch (AssertionError err) {
167+
LOGGER.warn("Assertion error. Will try again until timeout is reached.", err);
168+
return false;
169+
}
170+
});
171+
}
172+
159173
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* Copyright (c) 2010-2026 Eclipse Dirigible contributors
3+
*
4+
* All rights reserved. This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
6+
* http://www.eclipse.org/legal/epl-v20.html
7+
*
8+
* SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0
9+
*/
10+
package org.eclipse.dirigible.integration.tests.api;
11+
12+
import static io.restassured.RestAssured.given;
13+
14+
import java.nio.charset.StandardCharsets;
15+
16+
import org.eclipse.dirigible.components.base.http.roles.Roles;
17+
import org.eclipse.dirigible.components.initializers.synchronizer.SynchronizationProcessor;
18+
import org.eclipse.dirigible.repository.api.IRepository;
19+
import org.eclipse.dirigible.repository.api.IRepositoryStructure;
20+
import org.eclipse.dirigible.tests.base.IntegrationTest;
21+
import org.eclipse.dirigible.tests.framework.restassured.RestAssuredExecutor;
22+
import org.eclipse.dirigible.tests.framework.security.SecurityUtil;
23+
import org.junit.jupiter.api.Test;
24+
import org.springframework.beans.factory.annotation.Autowired;
25+
26+
/**
27+
* End-to-end test for HTTP access constraints ({@code *.access}) on client Java endpoints served
28+
* under {@code /services/java/...}: a role constraint synchronized from the registry must deny a
29+
* user without the role (403) and admit a user with it (200) as soon as the synchronization round
30+
* completes.
31+
*/
32+
class JavaAccessConstraintsIT extends IntegrationTest {
33+
34+
/** Project segment used for all sources in this IT. */
35+
private static final String PROJECT = "java-access-it";
36+
37+
/** Registry path of the client Java handler. */
38+
private static final String SOURCE_REGISTRY_PATH = IRepositoryStructure.PATH_REGISTRY_PUBLIC + "/" + PROJECT + "/demo/Hello.java";
39+
40+
/** Registry path of the access constraint artefact. */
41+
private static final String ACCESS_REGISTRY_PATH = IRepositoryStructure.PATH_REGISTRY_PUBLIC + "/" + PROJECT + "/security/demo.access";
42+
43+
/** Endpoint where the {@code demo.Hello} handler answers. */
44+
private static final String ENDPOINT = "/services/java/" + PROJECT + "/demo/Hello";
45+
46+
private static final String ADMIN_USER = "java-access-it-admin";
47+
private static final String NON_ADMIN_USER = "java-access-it-developer";
48+
private static final String PASSWORD = "java-access-it-password";
49+
50+
/**
51+
* Wait cap for the first request after the handler is synchronized - the in-process compile +
52+
* class-define cycle can finish shortly after {@code forceProcessSynchronizers()} returns.
53+
*/
54+
private static final long ASSERTION_TIMEOUT_SECONDS = 30;
55+
56+
private static final String HANDLER_SOURCE = """
57+
package demo;
58+
import jakarta.servlet.http.HttpServletRequest;
59+
import jakarta.servlet.http.HttpServletResponse;
60+
import org.eclipse.dirigible.engine.java.handler.JavaHandler;
61+
public class Hello implements JavaHandler {
62+
@Override
63+
public void handle(HttpServletRequest request, HttpServletResponse response) throws Exception {
64+
response.setContentType("application/json");
65+
response.getWriter().write("{\\"message\\": \\"hello\\"}");
66+
}
67+
}
68+
""";
69+
70+
/** The constraint path is registry-relative - the filter strips the /services/java prefix. */
71+
private static final String ACCESS_CONSTRAINT = """
72+
{
73+
"constraints": [
74+
{
75+
"path": "/%s/**",
76+
"method": "*",
77+
"scope": "HTTP",
78+
"roles": [
79+
"%s"
80+
]
81+
}
82+
]
83+
}
84+
""".formatted(PROJECT, Roles.RoleNames.ADMINISTRATOR);
85+
86+
@Autowired
87+
private IRepository repository;
88+
89+
@Autowired
90+
private SynchronizationProcessor synchronizationProcessor;
91+
92+
@Autowired
93+
private RestAssuredExecutor restAssuredExecutor;
94+
95+
@Autowired
96+
private SecurityUtil securityUtil;
97+
98+
@Test
99+
void access_constraints_are_enforced_for_java_endpoints() {
100+
securityUtil.createUserInDefaultTenant(ADMIN_USER, PASSWORD, Roles.RoleNames.ADMINISTRATOR);
101+
securityUtil.createUserInDefaultTenant(NON_ADMIN_USER, PASSWORD, Roles.RoleNames.DEVELOPER);
102+
103+
// without a constraint any authenticated user can call the handler
104+
repository.createResource(SOURCE_REGISTRY_PATH, HANDLER_SOURCE.getBytes(StandardCharsets.UTF_8), false, "text/x-java", true);
105+
synchronizationProcessor.forceProcessSynchronizers();
106+
assertEndpointStatusEventually(NON_ADMIN_USER, 200);
107+
108+
// the constraint restricts the project to ADMINISTRATOR; it must be enforced as soon as the
109+
// forced synchronization returns - no retry window for the 403
110+
repository.createResource(ACCESS_REGISTRY_PATH, ACCESS_CONSTRAINT.getBytes(StandardCharsets.UTF_8), false, "application/json",
111+
true);
112+
synchronizationProcessor.forceProcessSynchronizers();
113+
assertEndpointStatus(NON_ADMIN_USER, 403);
114+
assertEndpointStatus(ADMIN_USER, 200);
115+
}
116+
117+
private void assertEndpointStatusEventually(String user, int expectedStatus) {
118+
restAssuredExecutor.execute(() -> given().when()
119+
.get(ENDPOINT)
120+
.then()
121+
.statusCode(expectedStatus),
122+
user, PASSWORD, ASSERTION_TIMEOUT_SECONDS);
123+
}
124+
125+
private void assertEndpointStatus(String user, int expectedStatus) {
126+
restAssuredExecutor.execute(() -> given().when()
127+
.get(ENDPOINT)
128+
.then()
129+
.statusCode(expectedStatus),
130+
user, PASSWORD);
131+
}
132+
133+
}

0 commit comments

Comments
 (0)