diff --git a/src/main/java/com/uid2/admin/vertx/service/ServiceLinkService.java b/src/main/java/com/uid2/admin/vertx/service/ServiceLinkService.java index 6b376358..109f7b57 100644 --- a/src/main/java/com/uid2/admin/vertx/service/ServiceLinkService.java +++ b/src/main/java/com/uid2/admin/vertx/service/ServiceLinkService.java @@ -19,6 +19,7 @@ import org.slf4j.LoggerFactory; import java.util.*; +import java.util.regex.Pattern; import java.util.stream.Collectors; import static com.uid2.admin.vertx.Endpoints.*; @@ -118,6 +119,12 @@ private void handleServiceLinkAdd(RoutingContext rc) { return; } + String linkIdRegex = serviceProvider.getService(serviceId).getLinkIdRegex(); + if (!isValidLinkId(linkId, linkIdRegex)) { + ResponseUtil.error(rc, 400, "link_id " + linkId + " does not match service_id " + serviceId + " link_id_regex: " + linkIdRegex); + return; + } + Set serviceRoles = serviceProvider.getService(serviceId).getRoles(); final Set roles; try { @@ -270,4 +277,11 @@ private Set validateRoles(JsonArray rolesToValidate, Set serviceRole } return roles; } + + private boolean isValidLinkId(String linkId, String serviceRegex) { + if (serviceRegex == null) { + return true; + } + return Pattern.matches(serviceRegex, linkId); + } } diff --git a/src/main/java/com/uid2/admin/vertx/service/ServiceService.java b/src/main/java/com/uid2/admin/vertx/service/ServiceService.java index 9d19cd1c..0f4f7239 100644 --- a/src/main/java/com/uid2/admin/vertx/service/ServiceService.java +++ b/src/main/java/com/uid2/admin/vertx/service/ServiceService.java @@ -58,12 +58,12 @@ public void setupRoutes(Router router) { synchronized (writeLock) { this.handleServiceAdd(ctx); } - }, new AuditParams(Collections.emptyList(), List.of("site_id", "name", "roles")), Role.PRIVILEGED)); + }, new AuditParams(Collections.emptyList(), List.of("site_id", "name", "roles", "link_id_regex")), Role.PRIVILEGED)); router.post(API_SERVICE_UPDATE.toString()).blockingHandler(auth.handle((ctx) -> { synchronized (writeLock) { this.handleUpdate(ctx); } - }, new AuditParams(Collections.emptyList(), List.of("service_id", "site_id", "name", "roles")), Role.PRIVILEGED)); + }, new AuditParams(Collections.emptyList(), List.of("service_id", "site_id", "name", "roles", "link_id_regex")), Role.PRIVILEGED)); router.post(API_SERVICE_DELETE.toString()).blockingHandler(auth.handle((ctx) -> { synchronized (writeLock) { this.handleDelete(ctx); diff --git a/src/test/java/com/uid2/admin/vertx/ServiceLinkServiceTest.java b/src/test/java/com/uid2/admin/vertx/ServiceLinkServiceTest.java index 4b92fa16..5d8ae1ad 100644 --- a/src/test/java/com/uid2/admin/vertx/ServiceLinkServiceTest.java +++ b/src/test/java/com/uid2/admin/vertx/ServiceLinkServiceTest.java @@ -13,6 +13,7 @@ import io.vertx.junit5.VertxTestContext; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import java.util.ArrayList; @@ -584,4 +585,57 @@ void deleteServiceLink_invalidServiceId_returnsError(Vertx vertx, VertxTestConte testContext.completeNow(); }); } + + @ParameterizedTest + @MethodSource("linkIdRegexCases") + void addServiceLink_linkIdRegex_validation(String linkIdRegex, String linkId, boolean expectSuccess, + Vertx vertx, VertxTestContext testContext) { + fakeAuth(Role.MAINTAINER); + + setSites(new Site(123, "name1", false)); + setServices(new Service(1, 123, "name1", Set.of(Role.MAINTAINER), linkIdRegex)); + + JsonObject jo = new JsonObject(); + jo.put("link_id", linkId); + jo.put("service_id", 1); + jo.put("site_id", 123); + jo.put("name", "name1"); + jo.put("roles", JsonArray.of(Role.MAINTAINER)); + + post(vertx, testContext, "api/service_link/add", jo.encode(), response -> { + if (expectSuccess) { + assertEquals(200, response.statusCode()); + ServiceLink expected = new ServiceLink(linkId, 1, 123, "name1", Set.of(Role.MAINTAINER)); + checkServiceLinkJson(expected, response.bodyAsJsonObject()); + verify(serviceLinkStoreWriter, times(1)).upload(List.of(expected), null); + } else { + assertEquals(400, response.statusCode()); + String expectedMsg = "link_id " + linkId + " does not match service_id 1 link_id_regex: " + linkIdRegex; + assertEquals(expectedMsg, response.bodyAsJsonObject().getString("message")); + verify(serviceLinkStoreWriter, never()).upload(any(), any()); + } + + verify(serviceStoreWriter, never()).upload(null, null); + testContext.completeNow(); + }); + } + + private static java.util.stream.Stream linkIdRegexCases() { + return java.util.stream.Stream.of( + org.junit.jupiter.params.provider.Arguments.of("link[0-9]+", "invalidLink", false), + org.junit.jupiter.params.provider.Arguments.of("link[0-9]+", "link42", true), + org.junit.jupiter.params.provider.Arguments.of("^[A-Z0-9_]{1,256}$", "XY12345", true), // snowflake valid + org.junit.jupiter.params.provider.Arguments.of("^[A-Z0-9_]{1,256}$", "UID2_ENVIRONMENT", true), // snowflake valid + org.junit.jupiter.params.provider.Arguments.of("^[A-Z0-9_]{1,256}$", "xy12345", false), // snowflake invalid, lowercase + org.junit.jupiter.params.provider.Arguments.of("^[A-Z0-9_]{1,256}$", "X", true), // snowflake valid, minimum length + org.junit.jupiter.params.provider.Arguments.of("^[A-Z0-9_]{1,256}$", "X".repeat(256), true), // snowflake valid, maximum length + org.junit.jupiter.params.provider.Arguments.of("^[A-Z0-9_]{1,256}$", "X".repeat(257), false), // snowflake invalid, exceeds maximum length + org.junit.jupiter.params.provider.Arguments.of("^[A-Z0-9_]{1,256}$", " XY12345", false), // snowflake invalid, leading whitespace + org.junit.jupiter.params.provider.Arguments.of("^[A-Z0-9_]{1,256}$", "XY12345 ", false), // snowflake invalid, trailing whitespace + org.junit.jupiter.params.provider.Arguments.of("^[A-Z0-9_]{1,256}$", "XY 12345", false), // snowflake invalid, whitespace in the middle + org.junit.jupiter.params.provider.Arguments.of("^[A-Z0-9_]{1,256}$", "", false), // snowflake invalid, empty + org.junit.jupiter.params.provider.Arguments.of("^[A-Z0-9_]{1,256}$", " ", false), // snowflake invalid, just whitespace + org.junit.jupiter.params.provider.Arguments.of("^[A-Z0-9_]{1,256}$", "XY_12345", true) // snowflake valid, used underscore + ); + } }