diff --git a/pom.xml b/pom.xml index 6a9de758..9f04772a 100644 --- a/pom.xml +++ b/pom.xml @@ -16,7 +16,7 @@ 1.12.2 5.11.2 - 10.1.0 + 10.3.0 0.5.10 ${project.version} diff --git a/src/main/java/com/uid2/admin/vertx/Endpoints.java b/src/main/java/com/uid2/admin/vertx/Endpoints.java index 0bedc579..f8089ebb 100644 --- a/src/main/java/com/uid2/admin/vertx/Endpoints.java +++ b/src/main/java/com/uid2/admin/vertx/Endpoints.java @@ -82,6 +82,7 @@ public enum Endpoints { API_SERVICE_ADD("/api/service/add"), API_SERVICE_UPDATE("/api/service/update"), API_SERVICE_DELETE("/api/service/delete"), + API_SERVICE_REMOVE_LINK_ID_REGEX("/api/service/remove-link-id-regex"), API_SHARING_LISTS("/api/sharing/lists"), API_SHARING_LIST_SITEID("/api/sharing/list/:siteId"), 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 80885a02..9d19cd1c 100644 --- a/src/main/java/com/uid2/admin/vertx/service/ServiceService.java +++ b/src/main/java/com/uid2/admin/vertx/service/ServiceService.java @@ -20,6 +20,8 @@ import org.slf4j.LoggerFactory; import java.util.*; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; import java.util.stream.Collectors; import static com.uid2.admin.vertx.Endpoints.*; @@ -67,6 +69,11 @@ public void setupRoutes(Router router) { this.handleDelete(ctx); } }, new AuditParams(Collections.emptyList(), List.of("service_id")), Role.SUPER_USER)); + router.post(API_SERVICE_REMOVE_LINK_ID_REGEX.toString()).blockingHandler(auth.handle((ctx) -> { + synchronized (writeLock) { + this.handleRemoveLinkIdRegex(ctx); + } + }, new AuditParams(Collections.emptyList(), List.of("service_id")), Role.PRIVILEGED)); } private void handleServiceListAll(RoutingContext rc) { @@ -113,6 +120,7 @@ private void handleServiceAdd(RoutingContext rc) { Integer siteId = body.getInteger("site_id"); String name = body.getString("name"); JsonArray rolesSpec = body.getJsonArray("roles"); + String linkIdRegex = body.getString("link_id_regex"); if (siteId == null || name == null || rolesSpec == null || rolesSpec.isEmpty()) { ResponseUtil.error(rc, 400, "required parameters: site_id, name, roles"); return; @@ -147,11 +155,20 @@ private void handleServiceAdd(RoutingContext rc) { return; } + if (linkIdRegex != null && !linkIdRegex.isBlank()) { + if (!isValidRegex(linkIdRegex)) { + ResponseUtil.error(rc, 400, "invalid parameter: link_id_regex; not a valid regex"); + return; + } + } else { + linkIdRegex = null; + } + final List services = this.serviceProvider.getAllServices() .stream().sorted(Comparator.comparingInt(Service::getServiceId)) .collect(Collectors.toList()); final int serviceId = 1 + services.stream().mapToInt(Service::getServiceId).max().orElse(0); - Service service = new Service(serviceId, siteId, name, roles); + Service service = new Service(serviceId, siteId, name, roles, linkIdRegex); services.add(service); @@ -163,17 +180,17 @@ private void handleServiceAdd(RoutingContext rc) { } } - // Can update the site_id, name and roles + // Can update the site_id, name, roles, and link_id_regex private void handleUpdate(RoutingContext rc) { try { + serviceProvider.loadContent(); + Service service = findServiceFromRequest(rc); + if (service == null) return; // error already handled + JsonObject body = rc.body().asJsonObject(); - if (body == null) { - ResponseUtil.error(rc, 400, "json payload required but not provided"); - return; - } - Integer serviceId = body.getInteger("service_id"); Integer siteId = body.getInteger("site_id"); String name = body.getString("name"); + String linkIdRegex = body.getString("link_id_regex"); JsonArray rolesSpec = null; if (body.getString("roles") != null && !body.getString("roles").isEmpty()) { @@ -185,16 +202,7 @@ private void handleUpdate(RoutingContext rc) { } } - if (serviceId == null) { - ResponseUtil.error(rc, 400, "required parameters: service_id"); - return; - } - - final Service service = serviceProvider.getService(serviceId); - if (service == null) { - ResponseUtil.error(rc, 404, "failed to find a service for service_id: " + serviceId); - return; - } + int serviceId = service.getServiceId(); // check that this does not create a duplicate service if (siteHasService(siteId, name, serviceId)) { @@ -226,6 +234,14 @@ private void handleUpdate(RoutingContext rc) { service.setRoles(roles); } + if (linkIdRegex != null && !linkIdRegex.isBlank()) { + if (!isValidRegex(linkIdRegex)) { + ResponseUtil.error(rc, 400, "invalid parameter: link_id_regex; not a valid regex"); + return; + } + service.setLinkIdRegex(linkIdRegex); + } + if (siteId != null && siteId != 0) { service.setSiteId(siteId); } @@ -234,10 +250,7 @@ private void handleUpdate(RoutingContext rc) { service.setName(name); } - final List services = this.serviceProvider.getAllServices() - .stream().sorted(Comparator.comparingInt(Service::getServiceId)) - .collect(Collectors.toList()); - + List services = getSortedServices(); storeWriter.upload(services, null); @@ -248,40 +261,31 @@ private void handleUpdate(RoutingContext rc) { } private void handleDelete(RoutingContext rc) { - final int serviceId; - JsonObject body = rc.body() != null ? rc.body().asJsonObject() : null; - if (body == null) { - ResponseUtil.error(rc, 400, "json payload required but not provided"); - return; - } - serviceId = body.getInteger("service_id", -1); - if (serviceId == -1) { - ResponseUtil.error(rc, 400, "required parameters: service_id"); - return; - } - try { serviceProvider.loadContent(); - - Service service = serviceProvider.getService(serviceId); - if (service == null) { - ResponseUtil.error(rc, 404, "failed to find a service for service_id: " + serviceId); - return; - } - - final List services = this.serviceProvider.getAllServices() - .stream().sorted(Comparator.comparingInt(Service::getServiceId)) - .collect(Collectors.toList()); - + Service service = findServiceFromRequest(rc); + if (service == null) return; // error already handled + List services = getSortedServices(); services.remove(service); - storeWriter.upload(services, null); - rc.response().end(toJson(service).encodePrettily()); } catch (Exception e) { ResponseUtil.errorInternal(rc, "Internal Server Error", e); } + } + private void handleRemoveLinkIdRegex(RoutingContext rc) { + try { + serviceProvider.loadContent(); + Service service = findServiceFromRequest(rc); + if (service == null) return; // error already handled + service.setLinkIdRegex(null); + List services = getSortedServices(); + storeWriter.upload(services, null); + rc.response().end(toJson(service).encodePrettily()); + } catch (Exception e) { + ResponseUtil.errorInternal(rc, "Internal Server Error", e); + } } private JsonObject toJson(Service s) { @@ -290,6 +294,7 @@ private JsonObject toJson(Service s) { jsonObject.put("site_id", s.getSiteId()); jsonObject.put("name", s.getName()); jsonObject.put("roles", s.getRoles()); + jsonObject.put("link_id_regex", s.getLinkIdRegex()); return jsonObject; } @@ -302,4 +307,42 @@ private boolean siteHasService(Integer siteId, String name, int serviceId) { && serviceProvider.getAllServices().stream().anyMatch(s -> s.getServiceId() != serviceId && s.getSiteId() == siteId && s.getName().equals(name)); } + + + private Service findServiceFromRequest(RoutingContext rc) { + JsonObject body = rc.body() != null ? rc.body().asJsonObject() : null; + if (body == null) { + ResponseUtil.error(rc, 400, "json payload required but not provided"); + return null; + } + + int serviceId = body.getInteger("service_id", -1); + if (serviceId == -1) { + ResponseUtil.error(rc, 400, "required parameters: service_id"); + return null; + } + + Service service = serviceProvider.getService(serviceId); + if (service == null) { + ResponseUtil.error(rc, 404, "failed to find a service for service_id: " + serviceId); + return null; + } + return service; + } + + private List getSortedServices() { + return serviceProvider.getAllServices() + .stream() + .sorted(Comparator.comparingInt(Service::getServiceId)) + .collect(Collectors.toList()); + } + + private boolean isValidRegex(String regex) { + try { + Pattern.compile(regex); + return true; + } catch (PatternSyntaxException e) { + return false; + } + } } diff --git a/src/test/java/com/uid2/admin/vertx/ServiceServiceTest.java b/src/test/java/com/uid2/admin/vertx/ServiceServiceTest.java index fa8f975f..36f84260 100644 --- a/src/test/java/com/uid2/admin/vertx/ServiceServiceTest.java +++ b/src/test/java/com/uid2/admin/vertx/ServiceServiceTest.java @@ -50,7 +50,8 @@ private void checkServiceJson(Service expectedService, JsonObject actualService) () -> { Set actualRoles = actualService.getJsonArray("roles").stream().map(s -> Role.valueOf((String) s)).collect(Collectors.toSet()); assertEquals(expectedService.getRoles(), actualRoles); - } + }, + () -> assertEquals(expectedService.getLinkIdRegex(), actualService.getString("link_id_regex")) ); } @@ -273,6 +274,29 @@ void addServiceBadRoles(Vertx vertx, VertxTestContext testContext) { }); } + @Test + void addServiceBadLinkIdRegex(Vertx vertx, VertxTestContext testContext) { + fakeAuth(Role.PRIVILEGED); + + setSites(new Site(123, "name1", false)); + + JsonArray ja = new JsonArray(); + ja.add("GENERATOR"); + + JsonObject jo = new JsonObject(); + jo.put("site_id", 123); + jo.put("name", "name1"); + jo.put("roles", ja); + jo.put("link_id_regex", "[unclosed"); + + post(vertx, testContext, "api/service/add", jo.encode(), response -> { + assertEquals(400, response.statusCode()); + assertEquals("invalid parameter: link_id_regex; not a valid regex", response.bodyAsJsonObject().getString("message")); + verify(serviceStoreWriter, never()).upload(null, null); + testContext.completeNow(); + }); + } + @Test void addService_emptyRoles_returnsError(Vertx vertx, VertxTestContext testContext) { fakeAuth(Role.PRIVILEGED); @@ -316,6 +340,32 @@ void addServiceNonEmptyRoles(Vertx vertx, VertxTestContext testContext) { }); } + @Test + void addServiceWithRegex(Vertx vertx, VertxTestContext testContext) { + fakeAuth(Role.PRIVILEGED); + + setSites(new Site(123, "name1", false)); + + JsonArray ja = new JsonArray(); + ja.add("GENERATOR"); + ja.add("ID_READER"); + + JsonObject jo = new JsonObject(); + jo.put("site_id", 123); + jo.put("name", "name1"); + jo.put("roles", ja); + jo.put("link_id_regex", "valid"); + + post(vertx, testContext, "api/service/add", jo.encode(), response -> { + assertEquals(200, response.statusCode()); + Service expectedService = new Service(1, 123, "name1", Set.of(Role.GENERATOR, Role.ID_READER), "valid"); + checkServiceJson(expectedService, response.bodyAsJsonObject()); + verify(serviceStoreWriter, times(1)).upload(List.of(expectedService), null); + testContext.completeNow(); + }); + } + + @Test void addServiceToExistingList(Vertx vertx, VertxTestContext testContext) { fakeAuth(Role.PRIVILEGED); @@ -333,10 +383,11 @@ void addServiceToExistingList(Vertx vertx, VertxTestContext testContext) { jo.put("site_id", 123); jo.put("name", "name2"); jo.put("roles", ja); + jo.put("link_id_regex", "valid"); post(vertx, testContext, "api/service/add", jo.encode(), response -> { assertEquals(200, response.statusCode()); - Service expectedService = new Service(2, 123, "name2", Set.of(Role.GENERATOR, Role.ID_READER)); + Service expectedService = new Service(2, 123, "name2", Set.of(Role.GENERATOR, Role.ID_READER), "valid"); checkServiceJson(expectedService, response.bodyAsJsonObject()); verify(serviceStoreWriter, times(1)).upload(List.of(existingService, expectedService), null); testContext.completeNow(); @@ -344,7 +395,7 @@ void addServiceToExistingList(Vertx vertx, VertxTestContext testContext) { } @Test - void updateRolesMissingPayload(Vertx vertx, VertxTestContext testContext) { + void updateMissingPayload(Vertx vertx, VertxTestContext testContext) { fakeAuth(Role.PRIVILEGED); postWithoutBody(vertx, testContext, "api/service/update", response -> { @@ -357,12 +408,13 @@ void updateRolesMissingPayload(Vertx vertx, VertxTestContext testContext) { @ParameterizedTest @ValueSource(strings = {"service_id"}) - void updateRolesMissingParameters(String parameter, Vertx vertx, VertxTestContext testContext) { + void updateMissingParameters(String parameter, Vertx vertx, VertxTestContext testContext) { fakeAuth(Role.PRIVILEGED); JsonObject jo = new JsonObject(); jo.put("service_id", 1); jo.put("roles", new JsonArray()); + jo.put("link_id_regex", "valid"); jo.remove(parameter); @@ -375,7 +427,7 @@ void updateRolesMissingParameters(String parameter, Vertx vertx, VertxTestContex } @Test - void updateRolesBadServiceId(Vertx vertx, VertxTestContext testContext) { + void updateBadServiceId(Vertx vertx, VertxTestContext testContext) { fakeAuth(Role.PRIVILEGED); setServices(new Service(1, 123, "name1", Set.of(Role.MAINTAINER))); @@ -386,6 +438,7 @@ void updateRolesBadServiceId(Vertx vertx, VertxTestContext testContext) { JsonObject jo = new JsonObject(); jo.put("service_id", 2); jo.put("roles", ja); + jo.put("link_id_regex", "valid"); post(vertx, testContext, "api/service/update", jo.encode(), response -> { assertEquals(404, response.statusCode()); @@ -440,6 +493,26 @@ void updateRoles(Vertx vertx, VertxTestContext testContext) { }); } + @Test + void updateLinkIdRegex(Vertx vertx, VertxTestContext testContext) { + fakeAuth(Role.PRIVILEGED); + + Service existingService = new Service(1, 123, "name1", Set.of(Role.MAINTAINER)); + setServices(existingService); + + JsonObject jo = new JsonObject(); + jo.put("service_id", 1); + jo.put("link_id_regex", "valid"); + + post(vertx, testContext, "api/service/update", jo.encode(), response -> { + assertEquals(200, response.statusCode()); + existingService.setLinkIdRegex("valid"); + checkServiceJson(existingService, response.bodyAsJsonObject()); + verify(serviceStoreWriter, times(1)).upload(List.of(existingService), null); + testContext.completeNow(); + }); + } + @Test void updateService_removeRolesNotUsedByServiceLinks_succeeds(Vertx vertx, VertxTestContext testContext) { fakeAuth(Role.PRIVILEGED); @@ -538,6 +611,7 @@ void updateWithEmptyValues(Vertx vertx, VertxTestContext testContext) { jo.put("service_id", 1); jo.put("site_id", null); jo.put("name", ""); + jo.put("link_id_regex", ""); post(vertx, testContext, "api/service/update", jo.encode(), response -> { assertEquals(200, response.statusCode()); @@ -629,6 +703,58 @@ void updateServiceDuplicateName(Vertx vertx, VertxTestContext testContext) { }); } + @Test + void removeRegex(Vertx vertx, VertxTestContext testContext) { + fakeAuth(Role.PRIVILEGED); + + Service existingService = new Service(1, 123, "name1", Set.of(Role.MAINTAINER), "valid"); + setServices(existingService); + + JsonObject jo = new JsonObject(); + jo.put("service_id", 1); + + post(vertx, testContext, "api/service/remove-link-id-regex", jo.encode(), response -> { + assertEquals(200, response.statusCode()); + existingService.setLinkIdRegex(null); + checkServiceJson(existingService, response.bodyAsJsonObject()); + verify(serviceStoreWriter, times(1)).upload(List.of(existingService), null); + testContext.completeNow(); + }); + } + + @Test + void removeRegexInvalidServiceId(Vertx vertx, VertxTestContext testContext) { + fakeAuth(Role.PRIVILEGED); + + Service existingService = new Service(1, 123, "name1", Set.of(Role.MAINTAINER), "valid"); + setServices(existingService); + + JsonObject jo = new JsonObject(); + jo.put("service_id", 2); + + post(vertx, testContext, "api/service/remove-link-id-regex", jo.encode(), response -> { + assertEquals(404, response.statusCode()); + assertEquals("failed to find a service for service_id: 2", response.bodyAsJsonObject().getString("message")); + verify(serviceStoreWriter, never()).upload(null, null); + testContext.completeNow(); + }); + } + + @Test + void removeRegexNoBody(Vertx vertx, VertxTestContext testContext) { + fakeAuth(Role.PRIVILEGED); + + Service existingService = new Service(1, 123, "name1", Set.of(Role.MAINTAINER), "valid"); + setServices(existingService); + + post(vertx, testContext, "api/service/remove-link-id-regex", "", response -> { + assertEquals(400, response.statusCode()); + assertEquals("json payload required but not provided", response.bodyAsJsonObject().getString("message")); + verify(serviceStoreWriter, never()).upload(null, null); + testContext.completeNow(); + }); + } + @Test void deleteService(Vertx vertx, VertxTestContext testContext) { fakeAuth(Role.SUPER_USER); diff --git a/webroot/adm/service.html b/webroot/adm/service.html index 031b623b..26a308a2 100644 --- a/webroot/adm/service.html +++ b/webroot/adm/service.html @@ -22,6 +22,8 @@

Inputs

+ +
@@ -34,6 +36,7 @@

Operations

+
@@ -87,8 +90,9 @@

Output

} const roles = ($('#roles').val()).replace(/\s+/g, '').split(',').filter( (value, _, __) => value !== ""); + const linkIdRegex = $('#linkIdRegex').val() - doApiCall('POST', '/api/service/add', '#standardOutput', '#errorOutput', JSON.stringify({site_id: siteId, name: serviceName, roles: roles})); + doApiCall('POST', '/api/service/add', '#standardOutput', '#errorOutput', JSON.stringify({site_id: siteId, name: serviceName, roles: roles, link_id_regex: linkIdRegex})); }); $('#doUpdate').on('click', function () { @@ -98,10 +102,12 @@

Output

return } const siteId = parseInt($('#siteId').val()) - const serviceName = $('#serviceName').val() - const roles = ($('#roles').val()).replace(/\s+/g, '').split(',').filter( (value, _, __) => value !== ""); + const serviceName = $('#serviceName').val(); + let roles = ($('#roles').val()).replace(/\s+/g, '').split(',').filter( (value, _, __) => value !== ""); + roles = roles.length > 0 ? roles : null; + const linkIdRegex = $('#linkIdRegex').val() - doApiCall('POST', '/api/service/update', '#standardOutput', '#errorOutput', JSON.stringify({service_id: serviceId, site_id: siteId, name: serviceName, roles: roles})); + doApiCall('POST', '/api/service/update', '#standardOutput', '#errorOutput', JSON.stringify({service_id: serviceId, site_id: siteId, name: serviceName, roles: roles, link_id_regex: linkIdRegex})); }); $('#doDelete').on('click', function () { @@ -117,6 +123,18 @@

Output

doApiCall('POST', '/api/service/delete', '#standardOutput', '#errorOutput', JSON.stringify({service_id: serviceId})); }); + + $('#doRemoveLinkIdRegex').on('click', function () { + if (!confirm("Are you sure you want to remove the link id regex?")) { + return; + } + const serviceId = parseInt($('#serviceId').val()) + if(!serviceId) { + $('#errorOutput').text("required parameters: service_id") + return + } + doApiCall('POST', '/api/service/remove-link-id-regex', '#standardOutput', '#errorOutput', JSON.stringify({service_id: serviceId})); + }); });