From 5dfc07fe7f6de8a955ee1492c90feaf22c3305f1 Mon Sep 17 00:00:00 2001 From: Lionell Pack Date: Fri, 22 Sep 2023 19:07:26 +1000 Subject: [PATCH 01/15] Initial pattern for new API: - Introduce Google Guice for dependency injection in a small way - if we like it we can broaden usage over time. - Admin verticle now registers v2 api. - V2 api with one endpoint - Tests (other tests are broken - TODO!) --- pom.xml | 5 ++ src/main/java/com/uid2/admin/Main.java | 19 ++++- .../admin/RequireInjectAnnotationsModule.java | 16 +++++ .../com/uid2/admin/vertx/AdminVerticle.java | 11 ++- .../vertx/api/ClientSideKeypairResponse.java | 19 +++++ .../uid2/admin/vertx/api/IRouteProvider.java | 13 ++++ .../uid2/admin/vertx/api/SiteIdRouter.java | 52 ++++++++++++++ .../com/uid2/admin/vertx/api/V2Router.java | 31 ++++++++ .../uid2/admin/vertx/api/V2RouterModule.java | 49 +++++++++++++ .../service/ClientSideKeypairService.java | 4 ++ .../admin/vertx/service/ServicesModule.java | 29 ++++++++ .../uid2/admin/GuiceMockInjectingModule.java | 33 +++++++++ .../v2Router/RouterConfigurationTests.java | 39 ++++++++++ .../SiteId_ClientSideKeypair_Tests.java | 72 +++++++++++++++++++ 14 files changed, 390 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/uid2/admin/RequireInjectAnnotationsModule.java create mode 100644 src/main/java/com/uid2/admin/vertx/api/ClientSideKeypairResponse.java create mode 100644 src/main/java/com/uid2/admin/vertx/api/IRouteProvider.java create mode 100644 src/main/java/com/uid2/admin/vertx/api/SiteIdRouter.java create mode 100644 src/main/java/com/uid2/admin/vertx/api/V2Router.java create mode 100644 src/main/java/com/uid2/admin/vertx/api/V2RouterModule.java create mode 100644 src/main/java/com/uid2/admin/vertx/service/ServicesModule.java create mode 100644 src/test/java/com/uid2/admin/GuiceMockInjectingModule.java create mode 100644 src/test/java/com/uid2/admin/v2Router/RouterConfigurationTests.java create mode 100644 src/test/java/com/uid2/admin/v2Router/SiteId_ClientSideKeypair_Tests.java diff --git a/pom.xml b/pom.xml index 08bcaec0..63a11047 100644 --- a/pom.xml +++ b/pom.xml @@ -68,6 +68,11 @@ vertx-auth-oauth2 ${vertx.version} + + com.google.inject + guice + 7.0.0 + io.micrometer micrometer-registry-jmx diff --git a/src/main/java/com/uid2/admin/Main.java b/src/main/java/com/uid2/admin/Main.java index a92826eb..855ff981 100644 --- a/src/main/java/com/uid2/admin/Main.java +++ b/src/main/java/com/uid2/admin/Main.java @@ -1,6 +1,8 @@ package com.uid2.admin; import com.fasterxml.jackson.databind.ObjectWriter; +import com.google.inject.Guice; +import com.google.inject.Injector; import com.uid2.admin.auth.AdminUserProvider; import com.uid2.admin.auth.GithubAuthFactory; import com.uid2.admin.auth.AuthFactory; @@ -19,6 +21,8 @@ import com.uid2.admin.vertx.AdminVerticle; import com.uid2.admin.vertx.JsonUtil; import com.uid2.admin.vertx.WriteLock; +import com.uid2.admin.vertx.api.V2Router; +import com.uid2.admin.vertx.api.V2RouterModule; import com.uid2.admin.vertx.service.*; import com.uid2.shared.Const; import com.uid2.shared.Utils; @@ -50,6 +54,7 @@ import io.vertx.core.http.HttpServerOptions; import io.vertx.core.http.impl.HttpUtils; import io.vertx.core.json.JsonObject; +import lombok.val; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.vertx.micrometer.Label; @@ -251,7 +256,19 @@ public void run() { "admins", 10000, adminUserProvider); vertx.deployVerticle(rotatingAdminUserStoreVerticle); - AdminVerticle adminVerticle = new AdminVerticle(config, authFactory, adminUserProvider, services); + + // Begin introducing dependency injection - for now, it just knows about all of the IService classes and creates api handlers. + // N.b. there should only ever be one injector! + Injector injector = Guice.createInjector( + new RequireInjectAnnotationsModule(), + new ServicesModule(services), + new V2RouterModule() + ); + // Grab the V2 API route provider. N.b. there should usually only be a single call to injector. + // The next step is probably to get Guice to construct the Admin verticle. + val v2Api = injector.getInstance(V2Router.class); + + AdminVerticle adminVerticle = new AdminVerticle(config, authFactory, adminUserProvider, services, v2Api); vertx.deployVerticle(adminVerticle); CloudPath keysetMetadataPath = new CloudPath(config.getString("keysets_metadata_path")); diff --git a/src/main/java/com/uid2/admin/RequireInjectAnnotationsModule.java b/src/main/java/com/uid2/admin/RequireInjectAnnotationsModule.java new file mode 100644 index 00000000..1dbfc909 --- /dev/null +++ b/src/main/java/com/uid2/admin/RequireInjectAnnotationsModule.java @@ -0,0 +1,16 @@ +package com.uid2.admin; + +import com.google.inject.AbstractModule; + +/* + * This is used as part of the gradual introduction of DI within the codebase to ensure Google Guice doesn't try + * to instantiate anything that hasn't been marked as available for automated construction. + * Eventually we probably won't want this, but this helps ensure a staged DI adoption doesn't do anything unexpected. + */ +public class RequireInjectAnnotationsModule extends AbstractModule { + @Override + protected void configure() { + // Prevent Guice from using any constructors which haven't been marked with the @Inject attribute + binder().requireAtInjectOnConstructors(); + } +} diff --git a/src/main/java/com/uid2/admin/vertx/AdminVerticle.java b/src/main/java/com/uid2/admin/vertx/AdminVerticle.java index 5febf6c2..5940f6ca 100644 --- a/src/main/java/com/uid2/admin/vertx/AdminVerticle.java +++ b/src/main/java/com/uid2/admin/vertx/AdminVerticle.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.ObjectWriter; import com.uid2.admin.auth.*; +import com.uid2.admin.vertx.api.V2Router; import com.uid2.admin.vertx.service.IService; import com.uid2.shared.Const; import com.uid2.shared.Utils; @@ -9,6 +10,7 @@ import io.vertx.core.Promise; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; +import lombok.val; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.vertx.ext.auth.authentication.TokenCredentials; @@ -29,16 +31,19 @@ public class AdminVerticle extends AbstractVerticle { private final AuthFactory authFactory; private final IAdminUserProvider adminUserProvider; private final IService[] services; + private final V2Router v2Router; private final ObjectWriter jsonWriter = JsonUtil.createJsonWriter(); public AdminVerticle(JsonObject config, AuthFactory authFactory, IAdminUserProvider adminUserProvider, - IService[] services) { + IService[] services, + V2Router v2Router) { this.config = config; this.authFactory = authFactory; this.adminUserProvider = adminUserProvider; this.services = services; + this.v2Router = v2Router; } public void start(Promise startPromise) { @@ -78,6 +83,10 @@ private Router createRoutesSetup() { service.setupRoutes(router); } + val v2routes = v2Router.createRouter(vertx); + // TODO: Make all API requests go via the auth handler + router.route("/v2api/*").subRouter(v2routes); + return router; } diff --git a/src/main/java/com/uid2/admin/vertx/api/ClientSideKeypairResponse.java b/src/main/java/com/uid2/admin/vertx/api/ClientSideKeypairResponse.java new file mode 100644 index 00000000..86e70ce6 --- /dev/null +++ b/src/main/java/com/uid2/admin/vertx/api/ClientSideKeypairResponse.java @@ -0,0 +1,19 @@ +package com.uid2.admin.vertx.api; + +import com.uid2.shared.model.ClientSideKeypair; +import lombok.AllArgsConstructor; + +import java.time.Instant; + +@AllArgsConstructor +public class ClientSideKeypairResponse { + public int siteId; + public String subscriptionId; + public String publicKey; + public Instant created; + public boolean disabled; + + static ClientSideKeypairResponse fromClientSiteKeypair(ClientSideKeypair keypair) { + return new ClientSideKeypairResponse(keypair.getSiteId(), keypair.getSubscriptionId(), keypair.encodePublicKeyToString(), keypair.getCreated(), keypair.isDisabled()); + } +} diff --git a/src/main/java/com/uid2/admin/vertx/api/IRouteProvider.java b/src/main/java/com/uid2/admin/vertx/api/IRouteProvider.java new file mode 100644 index 00000000..0cefdbb2 --- /dev/null +++ b/src/main/java/com/uid2/admin/vertx/api/IRouteProvider.java @@ -0,0 +1,13 @@ +package com.uid2.admin.vertx.api; + +import io.vertx.ext.web.Router; + +/* + * Implement this interface to automatically be picked up by V2Router and have your routes registered under /v2api/*. + * Any constructor dependencies which are registered should be auto-injected by Guice, as long as it knows about them. + */ +public interface IRouteProvider { + void setupRoutes(Router router); +} + + diff --git a/src/main/java/com/uid2/admin/vertx/api/SiteIdRouter.java b/src/main/java/com/uid2/admin/vertx/api/SiteIdRouter.java new file mode 100644 index 00000000..1332eb4a --- /dev/null +++ b/src/main/java/com/uid2/admin/vertx/api/SiteIdRouter.java @@ -0,0 +1,52 @@ +package com.uid2.admin.vertx.api; + +import com.google.common.collect.Streams; +import com.google.inject.Inject; +import com.uid2.admin.vertx.ResponseUtil; +import com.uid2.admin.vertx.service.ClientSideKeypairService; +import io.vertx.core.Handler; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import lombok.val; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SiteIdRouter implements IRouteProvider { + private static final Logger LOGGER = LoggerFactory.getLogger(SiteIdRouter.class); + + private final ClientSideKeypairService clientSideKeypairService; + @Inject + public SiteIdRouter(ClientSideKeypairService clientSideKeypairService) { + this.clientSideKeypairService = clientSideKeypairService; + } + + @FunctionalInterface + interface ISiteIdRouteHandler { + void handle(RoutingContext rc, int siteId); + } + private Handler provideSiteId(ISiteIdRouteHandler handler) { + return (RoutingContext rc) -> { + val siteId = Integer.parseInt(rc.pathParam("siteId")); + handler.handle(rc, siteId); + }; + } + + @Override + public void setupRoutes(Router router) { + router.get("/sites/:siteId/client-side-keys").handler(provideSiteId(this::handleGetClientSideKeys)); + } + + + public void handleGetClientSideKeys(RoutingContext rc, int siteId) { + val keypairs = clientSideKeypairService.getKeypairsBySite(siteId); + if (keypairs != null) { + val result = Streams.stream(keypairs) + .map(kp -> ClientSideKeypairResponse.fromClientSiteKeypair(kp)) + .toArray(ClientSideKeypairResponse[]::new); + rc.json(result); + } + else { + ResponseUtil.error(rc, 404, "No keypairs available for site ID: " + siteId); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/uid2/admin/vertx/api/V2Router.java b/src/main/java/com/uid2/admin/vertx/api/V2Router.java new file mode 100644 index 00000000..6ce2aa84 --- /dev/null +++ b/src/main/java/com/uid2/admin/vertx/api/V2Router.java @@ -0,0 +1,31 @@ +package com.uid2.admin.vertx.api; + +import com.google.inject.Inject; +import io.vertx.core.Vertx; +import io.vertx.ext.web.Router; +import lombok.val; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Set; + +public class V2Router { + private static final Logger LOGGER = LoggerFactory.getLogger(V2Router.class); + private final Set routerProviders; + + @Inject + public V2Router(Set routerProviders) { + this.routerProviders = routerProviders; + } + + public Router createRouter(Vertx verticle) { + val v2router = Router.router(verticle); + + for (IRouteProvider provider : routerProviders) { + LOGGER.info("Configuring v2 router with " + provider.getClass()); + provider.setupRoutes(v2router); + } + + return v2router; + } +} diff --git a/src/main/java/com/uid2/admin/vertx/api/V2RouterModule.java b/src/main/java/com/uid2/admin/vertx/api/V2RouterModule.java new file mode 100644 index 00000000..b364f597 --- /dev/null +++ b/src/main/java/com/uid2/admin/vertx/api/V2RouterModule.java @@ -0,0 +1,49 @@ +package com.uid2.admin.vertx.api; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.reflect.ClassPath; +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.inject.Singleton; +import com.google.inject.multibindings.Multibinder; +import lombok.val; + +import java.io.IOException; +import java.util.Arrays; +import java.util.stream.Collectors; + +public class V2RouterModule extends AbstractModule { + @Provides + @V2Mapper + @Singleton + static ObjectMapper provideMapper() { + return new ObjectMapper(); + } + + /* + * Finds all classes in com.uid2.admin.vertx.api which implement IRouterProvider and register them. + * They are registered both as IRouterProvider and as their individual class. + */ + @Override + protected void configure() { + try { + Multibinder interfaceBinder = Multibinder.newSetBinder(binder(), IRouteProvider.class); + + val cp = ClassPath.from(getClass().getClassLoader()); + val routerProviders = cp + .getTopLevelClasses() + .stream() + .filter(ci -> ci.getName().startsWith("com.uid2.admin.vertx.api")) + .map(ci -> ci.load()) + .filter(cl -> Arrays.stream(cl.getInterfaces()).anyMatch(interf -> interf == IRouteProvider.class)) + .map(cl -> (Class)cl) + .collect(Collectors.toSet()); + for (val routerProviderClass : routerProviders) { + bind(routerProviderClass); + interfaceBinder.addBinding().to(routerProviderClass); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/uid2/admin/vertx/service/ClientSideKeypairService.java b/src/main/java/com/uid2/admin/vertx/service/ClientSideKeypairService.java index 61dad470..a586894a 100644 --- a/src/main/java/com/uid2/admin/vertx/service/ClientSideKeypairService.java +++ b/src/main/java/com/uid2/admin/vertx/service/ClientSideKeypairService.java @@ -157,6 +157,10 @@ private void handleUpdateKeypair(RoutingContext rc) { .end(json.encode()); } + public Iterable getKeypairsBySite(int siteId) { + return this.keypairStore.getSnapshot().getSiteKeypairs(siteId); + } + private void handleListAllKeypairs(RoutingContext rc) { final JsonArray ja = new JsonArray(); this.keypairStore.getSnapshot().getAll().forEach(k -> ja.add(toJsonWithoutPrivateKey(k))); diff --git a/src/main/java/com/uid2/admin/vertx/service/ServicesModule.java b/src/main/java/com/uid2/admin/vertx/service/ServicesModule.java new file mode 100644 index 00000000..7ab71865 --- /dev/null +++ b/src/main/java/com/uid2/admin/vertx/service/ServicesModule.java @@ -0,0 +1,29 @@ +package com.uid2.admin.vertx.service; + +import com.google.inject.AbstractModule; +import com.google.inject.multibindings.Multibinder; +import lombok.val; + +import java.util.Arrays; + +/* + * This is a temporary module which accepts an array of pre-created singleton services and makes them available as a module. + * Over time, we would ideally move to letting the DI framework create the services as well - this temporary solution + * is being used to support a strangler-pattern introduction of DI. + */ +public class ServicesModule extends AbstractModule { + private final IService[] services; + + public ServicesModule(IService[] services) { + this.services = services; + } + + @Override + protected void configure() { + Multibinder interfaceBinder = Multibinder.newSetBinder(binder(), IService.class); + Arrays.stream(services).forEach(s -> { + bind((Class)s.getClass()).toInstance(s); + interfaceBinder.addBinding().toInstance(s); + }); + } +} diff --git a/src/test/java/com/uid2/admin/GuiceMockInjectingModule.java b/src/test/java/com/uid2/admin/GuiceMockInjectingModule.java new file mode 100644 index 00000000..e74cdf6a --- /dev/null +++ b/src/test/java/com/uid2/admin/GuiceMockInjectingModule.java @@ -0,0 +1,33 @@ +package com.uid2.admin; + +import com.google.inject.AbstractModule; +import com.google.inject.multibindings.Multibinder; +import com.uid2.admin.vertx.service.IService; +import lombok.val; + +import java.io.InvalidClassException; +import java.util.Arrays; + +import static org.mockito.Mockito.*; + +public class GuiceMockInjectingModule extends AbstractModule { + private final Object[] mocks; + + public GuiceMockInjectingModule(Object... mocks) throws InvalidClassException { + for (Object mock : mocks) { + val mockDetails = mockingDetails(mock); + if (!mockDetails.isMock()) throw new InvalidClassException( + "GuiceMockInjectingModule is for injecting mocks, but found an object which was not a mock:" + mockDetails.getClass().getName() + ); + } + this.mocks = mocks; + } + + @Override + protected void configure() { + Arrays.stream(mocks).forEach(mock -> { + System.out.println("Configuring mock for class " + mock.getClass().getName()); + bind((Class)mock.getClass()).toInstance(mock); + }); + } +} diff --git a/src/test/java/com/uid2/admin/v2Router/RouterConfigurationTests.java b/src/test/java/com/uid2/admin/v2Router/RouterConfigurationTests.java new file mode 100644 index 00000000..2b00bd2a --- /dev/null +++ b/src/test/java/com/uid2/admin/v2Router/RouterConfigurationTests.java @@ -0,0 +1,39 @@ +package com.uid2.admin.v2Router; + +import com.google.inject.CreationException; +import com.google.inject.Guice; +import com.uid2.admin.GuiceMockInjectingModule; +import com.uid2.admin.RequireInjectAnnotationsModule; +import com.uid2.admin.vertx.api.SiteIdRouter; +import com.uid2.admin.vertx.api.V2RouterModule; +import com.uid2.admin.vertx.service.ClientSideKeypairService; +import lombok.val; +import org.junit.jupiter.api.Test; + +import java.io.InvalidClassException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +public class RouterConfigurationTests { + @Test + public void IfNeededDependencyIsNotProvided_CreateThrowsAnException() { + assertThrows(CreationException.class, () -> { + val injector = Guice.createInjector(new RequireInjectAnnotationsModule(), new V2RouterModule()); + injector.getInstance(SiteIdRouter.class); + }); + } + + @Test + public void IfNeededDependenciesAreAvailable_ARouterModuleCanBeCreated() throws InvalidClassException { + val keypairServiceMock = mock(ClientSideKeypairService.class); + + val injector = Guice.createInjector( + new RequireInjectAnnotationsModule(), + new V2RouterModule(), + new GuiceMockInjectingModule(keypairServiceMock) + ); + val siteIdRouter = injector.getInstance(SiteIdRouter.class); + assertEquals("/sites/:siteId/", siteIdRouter.getBasePath()); + } +} diff --git a/src/test/java/com/uid2/admin/v2Router/SiteId_ClientSideKeypair_Tests.java b/src/test/java/com/uid2/admin/v2Router/SiteId_ClientSideKeypair_Tests.java new file mode 100644 index 00000000..95b6555f --- /dev/null +++ b/src/test/java/com/uid2/admin/v2Router/SiteId_ClientSideKeypair_Tests.java @@ -0,0 +1,72 @@ +package com.uid2.admin.v2Router; + +import com.google.inject.Guice; +import com.uid2.admin.GuiceMockInjectingModule; +import com.uid2.admin.RequireInjectAnnotationsModule; +import com.uid2.admin.vertx.api.ClientSideKeypairResponse; +import com.uid2.admin.vertx.api.SiteIdRouter; +import com.uid2.admin.vertx.api.V2RouterModule; +import com.uid2.admin.vertx.service.ClientSideKeypairService; +import com.uid2.shared.model.ClientSideKeypair; +import io.vertx.ext.web.RoutingContext; +import lombok.val; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.InvalidClassException; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +public class SiteId_ClientSideKeypair_Tests { + @Captor + private ArgumentCaptor keypairResponseCaptor; + @Mock + private ClientSideKeypairService clientSideKeypairMock; + @Mock + private RoutingContext contextMock; + ClientSideKeypair createKeypairMock(int siteId, String publicKey) { + val kpMock = mock(ClientSideKeypair.class); + when(kpMock.getSiteId()).thenReturn(siteId); + when(kpMock.encodePublicKeyToString()).thenReturn(publicKey); + return kpMock; + } + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void WhenTheSiteHasASingleKey_ItIsReturned() throws InvalidClassException { + val siteIdToTest = 5; + val fakePublicKey = "Fake public key"; + val expectedResult = new ArrayList(List.of( + createKeypairMock(siteIdToTest, fakePublicKey) + )); + + when(clientSideKeypairMock.getKeypairsBySite(5)) + .thenReturn(expectedResult); + + val injector = Guice.createInjector( + new RequireInjectAnnotationsModule(), + new V2RouterModule(), + new GuiceMockInjectingModule(clientSideKeypairMock) + ); + val service = injector.getInstance(SiteIdRouter.class); + + service.handleGetClientSideKeys(contextMock, siteIdToTest); + + verify(contextMock).json(keypairResponseCaptor.capture()); + val response = keypairResponseCaptor.getValue(); + assertEquals(1, response.length); + val item = response[0]; + assertEquals(siteIdToTest, item.siteId); + assertEquals(fakePublicKey, item.publicKey); + } +} From da5007e55579b64cd979a71ca1b422eef3f14b31 Mon Sep 17 00:00:00 2001 From: Lionell Pack Date: Mon, 25 Sep 2023 12:41:18 +1000 Subject: [PATCH 02/15] Inject auth provider using a new singletons provider. Tidy up inconsistent comment formats. Allow the admin verticle to start without a v2 API. Add auth to the new site ID router. Switch to passing the parent router in to the v2 router - that way the admin Verticle doesn't need to know the path the v2 router wants to use. --- src/main/java/com/uid2/admin/Main.java | 18 +++++++++---- .../admin/RequireInjectAnnotationsModule.java | 8 +++--- .../java/com/uid2/admin/SingletonsModule.java | 27 +++++++++++++++++++ .../com/uid2/admin/vertx/AdminVerticle.java | 8 +++--- .../uid2/admin/vertx/api/IRouteProvider.java | 6 ++--- .../uid2/admin/vertx/api/SiteIdRouter.java | 12 +++++++-- .../com/uid2/admin/vertx/api/V2Router.java | 4 +-- .../uid2/admin/vertx/api/V2RouterModule.java | 13 +++------ .../admin/vertx/service/ServicesModule.java | 10 +++---- .../v2Router/RouterConfigurationTests.java | 2 +- .../admin/vertx/test/ServiceTestBase.java | 2 +- 11 files changed, 74 insertions(+), 36 deletions(-) create mode 100644 src/main/java/com/uid2/admin/SingletonsModule.java diff --git a/src/main/java/com/uid2/admin/Main.java b/src/main/java/com/uid2/admin/Main.java index 855ff981..83baf77d 100644 --- a/src/main/java/com/uid2/admin/Main.java +++ b/src/main/java/com/uid2/admin/Main.java @@ -256,16 +256,24 @@ public void run() { "admins", 10000, adminUserProvider); vertx.deployVerticle(rotatingAdminUserStoreVerticle); - - // Begin introducing dependency injection - for now, it just knows about all of the IService classes and creates api handlers. - // N.b. there should only ever be one injector! + /* + Begin introducing dependency injection - for now, it just knows about: + - all of the IService classes + - v2 API handlers + - authHandler + N.b. there should only ever be one injector! + */ Injector injector = Guice.createInjector( new RequireInjectAnnotationsModule(), new ServicesModule(services), + new SingletonsModule(auth), new V2RouterModule() ); - // Grab the V2 API route provider. N.b. there should usually only be a single call to injector. - // The next step is probably to get Guice to construct the Admin verticle. + /* + Grab the V2 API route provider. N.b. there should usually only be a single call to injector. + The next step is probably to get Guice to construct the Admin verticle instead of the v2 router - + but we'll need to get the Admin Verticle's other dependencies managed by Guice first. + */ val v2Api = injector.getInstance(V2Router.class); AdminVerticle adminVerticle = new AdminVerticle(config, authFactory, adminUserProvider, services, v2Api); diff --git a/src/main/java/com/uid2/admin/RequireInjectAnnotationsModule.java b/src/main/java/com/uid2/admin/RequireInjectAnnotationsModule.java index 1dbfc909..55cb7853 100644 --- a/src/main/java/com/uid2/admin/RequireInjectAnnotationsModule.java +++ b/src/main/java/com/uid2/admin/RequireInjectAnnotationsModule.java @@ -3,10 +3,10 @@ import com.google.inject.AbstractModule; /* - * This is used as part of the gradual introduction of DI within the codebase to ensure Google Guice doesn't try - * to instantiate anything that hasn't been marked as available for automated construction. - * Eventually we probably won't want this, but this helps ensure a staged DI adoption doesn't do anything unexpected. - */ + This is used as part of the gradual introduction of DI within the codebase to ensure Google Guice doesn't try + to instantiate anything that hasn't been marked as available for automated construction. + Eventually we probably won't want this, but this helps ensure a staged DI adoption doesn't do anything unexpected. +*/ public class RequireInjectAnnotationsModule extends AbstractModule { @Override protected void configure() { diff --git a/src/main/java/com/uid2/admin/SingletonsModule.java b/src/main/java/com/uid2/admin/SingletonsModule.java new file mode 100644 index 00000000..85c95808 --- /dev/null +++ b/src/main/java/com/uid2/admin/SingletonsModule.java @@ -0,0 +1,27 @@ +package com.uid2.admin; + +import com.google.inject.AbstractModule; +import com.uid2.admin.vertx.service.IService; + +import java.util.Arrays; + +/* + This is a temporary module which accepts an array of pre-created singletons and makes them available as a module. + Over time, we would ideally move to letting the DI framework create the singletons as well - this temporary solution + is being used to support a strangler-pattern introduction of DI. +*/ +public class SingletonsModule extends AbstractModule { + private final Object[] singletons; + + public SingletonsModule(Object... singletons) { + this.singletons = singletons; + } + + @Override + protected void configure() { + super.configure(); + Arrays.stream(singletons).forEach(s -> { + bind((Class)s.getClass()).toInstance(s); + }); + } +} diff --git a/src/main/java/com/uid2/admin/vertx/AdminVerticle.java b/src/main/java/com/uid2/admin/vertx/AdminVerticle.java index 5940f6ca..6f3f4638 100644 --- a/src/main/java/com/uid2/admin/vertx/AdminVerticle.java +++ b/src/main/java/com/uid2/admin/vertx/AdminVerticle.java @@ -83,9 +83,11 @@ private Router createRoutesSetup() { service.setupRoutes(router); } - val v2routes = v2Router.createRouter(vertx); - // TODO: Make all API requests go via the auth handler - router.route("/v2api/*").subRouter(v2routes); + if (v2Router != null) { + v2Router.setupSubRouter(vertx, router); + // TODO: Make all API requests go via the auth handler + + } return router; } diff --git a/src/main/java/com/uid2/admin/vertx/api/IRouteProvider.java b/src/main/java/com/uid2/admin/vertx/api/IRouteProvider.java index 0cefdbb2..5b90da00 100644 --- a/src/main/java/com/uid2/admin/vertx/api/IRouteProvider.java +++ b/src/main/java/com/uid2/admin/vertx/api/IRouteProvider.java @@ -3,9 +3,9 @@ import io.vertx.ext.web.Router; /* - * Implement this interface to automatically be picked up by V2Router and have your routes registered under /v2api/*. - * Any constructor dependencies which are registered should be auto-injected by Guice, as long as it knows about them. - */ + Implement this interface to automatically be picked up by V2Router and have your routes registered under /v2api/*. + Any constructor dependencies which are registered should be auto-injected by Guice, as long as it knows about them. +*/ public interface IRouteProvider { void setupRoutes(Router router); } diff --git a/src/main/java/com/uid2/admin/vertx/api/SiteIdRouter.java b/src/main/java/com/uid2/admin/vertx/api/SiteIdRouter.java index 1332eb4a..2cf31152 100644 --- a/src/main/java/com/uid2/admin/vertx/api/SiteIdRouter.java +++ b/src/main/java/com/uid2/admin/vertx/api/SiteIdRouter.java @@ -4,6 +4,8 @@ import com.google.inject.Inject; import com.uid2.admin.vertx.ResponseUtil; import com.uid2.admin.vertx.service.ClientSideKeypairService; +import com.uid2.shared.auth.Role; +import com.uid2.shared.middleware.AuthMiddleware; import io.vertx.core.Handler; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; @@ -15,15 +17,21 @@ public class SiteIdRouter implements IRouteProvider { private static final Logger LOGGER = LoggerFactory.getLogger(SiteIdRouter.class); private final ClientSideKeypairService clientSideKeypairService; + private final AuthMiddleware auth; + @Inject - public SiteIdRouter(ClientSideKeypairService clientSideKeypairService) { + public SiteIdRouter(ClientSideKeypairService clientSideKeypairService, AuthMiddleware auth) { this.clientSideKeypairService = clientSideKeypairService; + this.auth = auth; } @FunctionalInterface interface ISiteIdRouteHandler { void handle(RoutingContext rc, int siteId); } + private Handler checkAuth(Handler handler, Role... roles) { + return auth.handle(handler, roles); + } private Handler provideSiteId(ISiteIdRouteHandler handler) { return (RoutingContext rc) -> { val siteId = Integer.parseInt(rc.pathParam("siteId")); @@ -33,7 +41,7 @@ private Handler provideSiteId(ISiteIdRouteHandler handler) { @Override public void setupRoutes(Router router) { - router.get("/sites/:siteId/client-side-keys").handler(provideSiteId(this::handleGetClientSideKeys)); + router.get("/sites/:siteId/client-side-keys").handler(checkAuth(provideSiteId(this::handleGetClientSideKeys), Role.ADMINISTRATOR)); } diff --git a/src/main/java/com/uid2/admin/vertx/api/V2Router.java b/src/main/java/com/uid2/admin/vertx/api/V2Router.java index 6ce2aa84..25072016 100644 --- a/src/main/java/com/uid2/admin/vertx/api/V2Router.java +++ b/src/main/java/com/uid2/admin/vertx/api/V2Router.java @@ -18,7 +18,7 @@ public V2Router(Set routerProviders) { this.routerProviders = routerProviders; } - public Router createRouter(Vertx verticle) { + public void setupSubRouter(Vertx verticle, Router parentRouter) { val v2router = Router.router(verticle); for (IRouteProvider provider : routerProviders) { @@ -26,6 +26,6 @@ public Router createRouter(Vertx verticle) { provider.setupRoutes(v2router); } - return v2router; + parentRouter.route("/v2api/*").subRouter(v2router); } } diff --git a/src/main/java/com/uid2/admin/vertx/api/V2RouterModule.java b/src/main/java/com/uid2/admin/vertx/api/V2RouterModule.java index b364f597..b8e52005 100644 --- a/src/main/java/com/uid2/admin/vertx/api/V2RouterModule.java +++ b/src/main/java/com/uid2/admin/vertx/api/V2RouterModule.java @@ -13,17 +13,10 @@ import java.util.stream.Collectors; public class V2RouterModule extends AbstractModule { - @Provides - @V2Mapper - @Singleton - static ObjectMapper provideMapper() { - return new ObjectMapper(); - } - /* - * Finds all classes in com.uid2.admin.vertx.api which implement IRouterProvider and register them. - * They are registered both as IRouterProvider and as their individual class. - */ + Finds all classes in com.uid2.admin.vertx.api which implement IRouterProvider and register them. + They are registered both as IRouterProvider and as their individual class. + */ @Override protected void configure() { try { diff --git a/src/main/java/com/uid2/admin/vertx/service/ServicesModule.java b/src/main/java/com/uid2/admin/vertx/service/ServicesModule.java index 7ab71865..b3a60a10 100644 --- a/src/main/java/com/uid2/admin/vertx/service/ServicesModule.java +++ b/src/main/java/com/uid2/admin/vertx/service/ServicesModule.java @@ -7,11 +7,11 @@ import java.util.Arrays; /* - * This is a temporary module which accepts an array of pre-created singleton services and makes them available as a module. - * Over time, we would ideally move to letting the DI framework create the services as well - this temporary solution - * is being used to support a strangler-pattern introduction of DI. - */ -public class ServicesModule extends AbstractModule { + This is a temporary module which accepts an array of pre-created singleton services and makes them available as a module. + Over time, we would ideally move to letting the DI framework create the services as well - this temporary solution + is being used to support a strangler-pattern introduction of DI. +*/ +public class ServicesModule extends AbstractModule { private final IService[] services; public ServicesModule(IService[] services) { diff --git a/src/test/java/com/uid2/admin/v2Router/RouterConfigurationTests.java b/src/test/java/com/uid2/admin/v2Router/RouterConfigurationTests.java index 2b00bd2a..4d16121e 100644 --- a/src/test/java/com/uid2/admin/v2Router/RouterConfigurationTests.java +++ b/src/test/java/com/uid2/admin/v2Router/RouterConfigurationTests.java @@ -34,6 +34,6 @@ public void IfNeededDependenciesAreAvailable_ARouterModuleCanBeCreated() throws new GuiceMockInjectingModule(keypairServiceMock) ); val siteIdRouter = injector.getInstance(SiteIdRouter.class); - assertEquals("/sites/:siteId/", siteIdRouter.getBasePath()); + assertNotNull(siteIdRouter); } } diff --git a/src/test/java/com/uid2/admin/vertx/test/ServiceTestBase.java b/src/test/java/com/uid2/admin/vertx/test/ServiceTestBase.java index 0381a4df..ab4c8e48 100644 --- a/src/test/java/com/uid2/admin/vertx/test/ServiceTestBase.java +++ b/src/test/java/com/uid2/admin/vertx/test/ServiceTestBase.java @@ -115,7 +115,7 @@ public void deployVerticle(Vertx vertx, VertxTestContext testContext) throws Thr auth = new AuthMiddleware(this.adminUserProvider); IService[] services = {createService()}; - AdminVerticle verticle = new AdminVerticle(config, authFactory, adminUserProvider, services); + AdminVerticle verticle = new AdminVerticle(config, authFactory, adminUserProvider, services, null); vertx.deployVerticle(verticle, testContext.succeeding(id -> testContext.completeNow())); } From c928a2a7abdb3ad04de59a01d2d1149da4bbdd96 Mon Sep 17 00:00:00 2001 From: Lionell Pack Date: Mon, 25 Sep 2023 12:47:06 +1000 Subject: [PATCH 03/15] Remove unnecessary auth wrapper. --- src/main/java/com/uid2/admin/vertx/api/SiteIdRouter.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/com/uid2/admin/vertx/api/SiteIdRouter.java b/src/main/java/com/uid2/admin/vertx/api/SiteIdRouter.java index 2cf31152..61fd9ff7 100644 --- a/src/main/java/com/uid2/admin/vertx/api/SiteIdRouter.java +++ b/src/main/java/com/uid2/admin/vertx/api/SiteIdRouter.java @@ -29,9 +29,6 @@ public SiteIdRouter(ClientSideKeypairService clientSideKeypairService, AuthMiddl interface ISiteIdRouteHandler { void handle(RoutingContext rc, int siteId); } - private Handler checkAuth(Handler handler, Role... roles) { - return auth.handle(handler, roles); - } private Handler provideSiteId(ISiteIdRouteHandler handler) { return (RoutingContext rc) -> { val siteId = Integer.parseInt(rc.pathParam("siteId")); @@ -41,7 +38,7 @@ private Handler provideSiteId(ISiteIdRouteHandler handler) { @Override public void setupRoutes(Router router) { - router.get("/sites/:siteId/client-side-keys").handler(checkAuth(provideSiteId(this::handleGetClientSideKeys), Role.ADMINISTRATOR)); + router.get("/sites/:siteId/client-side-keys").handler(auth.handle(provideSiteId(this::handleGetClientSideKeys), Role.ADMINISTRATOR)); } From 5d7490ea1537a251ffcfa61544ec91110cc60e59 Mon Sep 17 00:00:00 2001 From: Lionell Pack Date: Mon, 25 Sep 2023 15:37:37 +1000 Subject: [PATCH 04/15] Inject auth module for API tests. --- .../com/uid2/admin/v2Router/RouterConfigurationTests.java | 4 +++- .../uid2/admin/v2Router/SiteId_ClientSideKeypair_Tests.java | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/uid2/admin/v2Router/RouterConfigurationTests.java b/src/test/java/com/uid2/admin/v2Router/RouterConfigurationTests.java index 4d16121e..59a72b35 100644 --- a/src/test/java/com/uid2/admin/v2Router/RouterConfigurationTests.java +++ b/src/test/java/com/uid2/admin/v2Router/RouterConfigurationTests.java @@ -7,6 +7,7 @@ import com.uid2.admin.vertx.api.SiteIdRouter; import com.uid2.admin.vertx.api.V2RouterModule; import com.uid2.admin.vertx.service.ClientSideKeypairService; +import com.uid2.shared.middleware.AuthMiddleware; import lombok.val; import org.junit.jupiter.api.Test; @@ -27,11 +28,12 @@ public void IfNeededDependencyIsNotProvided_CreateThrowsAnException() { @Test public void IfNeededDependenciesAreAvailable_ARouterModuleCanBeCreated() throws InvalidClassException { val keypairServiceMock = mock(ClientSideKeypairService.class); + val authMock = mock(AuthMiddleware.class); val injector = Guice.createInjector( new RequireInjectAnnotationsModule(), new V2RouterModule(), - new GuiceMockInjectingModule(keypairServiceMock) + new GuiceMockInjectingModule(keypairServiceMock, authMock) ); val siteIdRouter = injector.getInstance(SiteIdRouter.class); assertNotNull(siteIdRouter); diff --git a/src/test/java/com/uid2/admin/v2Router/SiteId_ClientSideKeypair_Tests.java b/src/test/java/com/uid2/admin/v2Router/SiteId_ClientSideKeypair_Tests.java index 95b6555f..3f3c53ee 100644 --- a/src/test/java/com/uid2/admin/v2Router/SiteId_ClientSideKeypair_Tests.java +++ b/src/test/java/com/uid2/admin/v2Router/SiteId_ClientSideKeypair_Tests.java @@ -7,6 +7,7 @@ import com.uid2.admin.vertx.api.SiteIdRouter; import com.uid2.admin.vertx.api.V2RouterModule; import com.uid2.admin.vertx.service.ClientSideKeypairService; +import com.uid2.shared.middleware.AuthMiddleware; import com.uid2.shared.model.ClientSideKeypair; import io.vertx.ext.web.RoutingContext; import lombok.val; @@ -31,6 +32,8 @@ public class SiteId_ClientSideKeypair_Tests { private ClientSideKeypairService clientSideKeypairMock; @Mock private RoutingContext contextMock; + @Mock + private AuthMiddleware authMock; ClientSideKeypair createKeypairMock(int siteId, String publicKey) { val kpMock = mock(ClientSideKeypair.class); when(kpMock.getSiteId()).thenReturn(siteId); @@ -56,7 +59,7 @@ public void WhenTheSiteHasASingleKey_ItIsReturned() throws InvalidClassException val injector = Guice.createInjector( new RequireInjectAnnotationsModule(), new V2RouterModule(), - new GuiceMockInjectingModule(clientSideKeypairMock) + new GuiceMockInjectingModule(clientSideKeypairMock, authMock) ); val service = injector.getInstance(SiteIdRouter.class); From ed811eb46b4867c8391a8dcc20cdf2486e9a6a9b Mon Sep 17 00:00:00 2001 From: Lionell Pack Date: Mon, 25 Sep 2023 17:17:41 +1000 Subject: [PATCH 05/15] Update endpoint based on feedback. --- src/main/java/com/uid2/admin/vertx/api/SiteIdRouter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/uid2/admin/vertx/api/SiteIdRouter.java b/src/main/java/com/uid2/admin/vertx/api/SiteIdRouter.java index 61fd9ff7..1396f50d 100644 --- a/src/main/java/com/uid2/admin/vertx/api/SiteIdRouter.java +++ b/src/main/java/com/uid2/admin/vertx/api/SiteIdRouter.java @@ -38,7 +38,7 @@ private Handler provideSiteId(ISiteIdRouteHandler handler) { @Override public void setupRoutes(Router router) { - router.get("/sites/:siteId/client-side-keys").handler(auth.handle(provideSiteId(this::handleGetClientSideKeys), Role.ADMINISTRATOR)); + router.get("/sites/:siteId/client-side-keypairs").handler(auth.handle(provideSiteId(this::handleGetClientSideKeys), Role.ADMINISTRATOR)); } From bdd10be6c5896f7255f0226d3760c5b3853f95da Mon Sep 17 00:00:00 2001 From: Lionell Pack Date: Wed, 27 Sep 2023 10:56:59 +1000 Subject: [PATCH 06/15] Switch to using attributes for v2 paths, methods, and roles. --- README.md | 20 +++++++++- .../uid2/admin/vertx/api/IRouteProvider.java | 4 +- .../vertx/api/UrlParameterProviders.java | 18 +++++++++ .../com/uid2/admin/vertx/api/V2Router.java | 28 ++++++++++--- .../vertx/api/annotations/ApiMethod.java | 12 ++++++ .../admin/vertx/api/annotations/Method.java | 14 +++++++ .../admin/vertx/api/annotations/Path.java | 12 ++++++ .../admin/vertx/api/annotations/Roles.java | 14 +++++++ .../{ => cstg}/ClientSideKeypairResponse.java | 2 +- .../GetClientSideKeypairsBySite.java} | 39 ++++++++----------- .../v2Router/RouterConfigurationTests.java | 6 +-- .../SiteId_ClientSideKeypair_Tests.java | 6 +-- 12 files changed, 138 insertions(+), 37 deletions(-) create mode 100644 src/main/java/com/uid2/admin/vertx/api/UrlParameterProviders.java create mode 100644 src/main/java/com/uid2/admin/vertx/api/annotations/ApiMethod.java create mode 100644 src/main/java/com/uid2/admin/vertx/api/annotations/Method.java create mode 100644 src/main/java/com/uid2/admin/vertx/api/annotations/Path.java create mode 100644 src/main/java/com/uid2/admin/vertx/api/annotations/Roles.java rename src/main/java/com/uid2/admin/vertx/api/{ => cstg}/ClientSideKeypairResponse.java (93%) rename src/main/java/com/uid2/admin/vertx/api/{SiteIdRouter.java => cstg/GetClientSideKeypairsBySite.java} (54%) diff --git a/README.md b/README.md index 53a94a4a..19b96b85 100644 --- a/README.md +++ b/README.md @@ -15,4 +15,22 @@ When running locally, GitHub OAuth 2.0 is disabled and users are logged in as *t `is_auth_disabled` flag. The user has all the rights available. To change the user rights, make changes to `src/main/resources/localstack/s3/admins/admins.json` and `docker-compose restart`. -If you want to test with GitHub OAuth 2.0, you will need to create an OAuth application on GitHub with `http://localhost:8089/oauth2-callback` as the callback URL, then generate a client ID/secret. Once generated, set the `is_auth_disabled` flag to `false`, and copy the client ID/secret into `github_client_id` and `github_client_secret`. \ No newline at end of file +If you want to test with GitHub OAuth 2.0, you will need to create an OAuth application on GitHub with `http://localhost:8089/oauth2-callback` as the callback URL, then generate a client ID/secret. Once generated, set the `is_auth_disabled` flag to `false`, and copy the client ID/secret into `github_client_id` and `github_client_secret`. + +## V2 API + +The v2 API is based on individual route provider classes. Each class should provide exactly one endpoint and must implement IRouteProvider. It may accept constructor parameters, which will be auto-wired by our DI system. Currently, DI is configured to provide: +- All the IService classes which are provided to the Admin Verticle. +- The Auth middleware (but see IRouteProvider - you probably don't need it). + +### IRouteProvider + +IRouteProvider requires a `getHandler` method, which should return a valid handler function - see `GetClientSideKeypairsBySite.java`. This method *must* be annotated with the Path, Method, and Roles annotations. + +All classes which implement IRouteProvider will automatically be picked up by DI and registered as route handlers. The route handler will automatically be wrapped by the Auth middleware based on the roles specified in the Roles annotation. + +## Dependency injection - current state and plans + +We are in the process of introducing dependency injection to the code base. Currently, a number of singletons which are constructed explicitly are provided via `ServicesModule` (for `IService` classes) and the `SingletonsModule` (for other singletons - e.g. the Auth middleware). + +Over time, it would be nice to expand what is being constructed via DI and reduce our reliance on manually constructing objects. Once we have all of the dependencies for `AdminVerticle` available via DI, we can stop creating the `V2Router` via DI and instead just create the `AdminVerticle` (DI will then create the `V2Router` for us). \ No newline at end of file diff --git a/src/main/java/com/uid2/admin/vertx/api/IRouteProvider.java b/src/main/java/com/uid2/admin/vertx/api/IRouteProvider.java index 5b90da00..ee328b9b 100644 --- a/src/main/java/com/uid2/admin/vertx/api/IRouteProvider.java +++ b/src/main/java/com/uid2/admin/vertx/api/IRouteProvider.java @@ -1,13 +1,15 @@ package com.uid2.admin.vertx.api; +import io.vertx.core.Handler; import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; /* Implement this interface to automatically be picked up by V2Router and have your routes registered under /v2api/*. Any constructor dependencies which are registered should be auto-injected by Guice, as long as it knows about them. */ public interface IRouteProvider { - void setupRoutes(Router router); + Handler getHandler(); } diff --git a/src/main/java/com/uid2/admin/vertx/api/UrlParameterProviders.java b/src/main/java/com/uid2/admin/vertx/api/UrlParameterProviders.java new file mode 100644 index 00000000..44411d5a --- /dev/null +++ b/src/main/java/com/uid2/admin/vertx/api/UrlParameterProviders.java @@ -0,0 +1,18 @@ +package com.uid2.admin.vertx.api; + +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; +import lombok.val; + +public class UrlParameterProviders { + @FunctionalInterface + public interface ISiteIdRouteHandler { + void handle(RoutingContext rc, int siteId); + } + public static Handler provideSiteId(ISiteIdRouteHandler handler) { + return (RoutingContext rc) -> { + val siteId = Integer.parseInt(rc.pathParam("siteId")); + handler.handle(rc, siteId); + }; + } +} diff --git a/src/main/java/com/uid2/admin/vertx/api/V2Router.java b/src/main/java/com/uid2/admin/vertx/api/V2Router.java index 25072016..8a1eb2be 100644 --- a/src/main/java/com/uid2/admin/vertx/api/V2Router.java +++ b/src/main/java/com/uid2/admin/vertx/api/V2Router.java @@ -1,29 +1,47 @@ package com.uid2.admin.vertx.api; import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.uid2.admin.vertx.api.annotations.Method; +import com.uid2.admin.vertx.api.annotations.Path; +import com.uid2.admin.vertx.api.annotations.Roles; +import com.uid2.shared.middleware.AuthMiddleware; import io.vertx.core.Vertx; import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; import lombok.val; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Set; +@Singleton public class V2Router { private static final Logger LOGGER = LoggerFactory.getLogger(V2Router.class); - private final Set routerProviders; + private final Set routeProviders; + private final AuthMiddleware auth; @Inject - public V2Router(Set routerProviders) { - this.routerProviders = routerProviders; + public V2Router(Set routeProviders, AuthMiddleware auth) { + this.routeProviders = routeProviders; + this.auth = auth; } public void setupSubRouter(Vertx verticle, Router parentRouter) { val v2router = Router.router(verticle); - for (IRouteProvider provider : routerProviders) { + for (IRouteProvider provider : routeProviders) { LOGGER.info("Configuring v2 router with " + provider.getClass()); - provider.setupRoutes(v2router); + java.lang.reflect.Method handler = null; + try { + handler = provider.getClass().getMethod("getHandler"); + val path = handler.getAnnotation(Path.class).value(); + val method = handler.getAnnotation(Method.class).value().vertxMethod; + val roles = handler.getAnnotation(Roles.class).value(); + v2router.route(method, path).handler(auth.handle(provider.getHandler(), roles)); + } catch (NoSuchMethodException e) { + LOGGER.error("Could not find handle method for API handler: " + provider.getClass().getName()); + } } parentRouter.route("/v2api/*").subRouter(v2router); diff --git a/src/main/java/com/uid2/admin/vertx/api/annotations/ApiMethod.java b/src/main/java/com/uid2/admin/vertx/api/annotations/ApiMethod.java new file mode 100644 index 00000000..081bca0f --- /dev/null +++ b/src/main/java/com/uid2/admin/vertx/api/annotations/ApiMethod.java @@ -0,0 +1,12 @@ +package com.uid2.admin.vertx.api.annotations; + +import io.vertx.core.http.HttpMethod; + +public enum ApiMethod { + GET(HttpMethod.GET); + public final HttpMethod vertxMethod; + + ApiMethod(HttpMethod vertxMethod) { + this.vertxMethod = vertxMethod; + } +} diff --git a/src/main/java/com/uid2/admin/vertx/api/annotations/Method.java b/src/main/java/com/uid2/admin/vertx/api/annotations/Method.java new file mode 100644 index 00000000..01b2a586 --- /dev/null +++ b/src/main/java/com/uid2/admin/vertx/api/annotations/Method.java @@ -0,0 +1,14 @@ +package com.uid2.admin.vertx.api.annotations; + +import io.vertx.core.http.HttpMethod; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Method { + ApiMethod value(); +} diff --git a/src/main/java/com/uid2/admin/vertx/api/annotations/Path.java b/src/main/java/com/uid2/admin/vertx/api/annotations/Path.java new file mode 100644 index 00000000..f0a0dec2 --- /dev/null +++ b/src/main/java/com/uid2/admin/vertx/api/annotations/Path.java @@ -0,0 +1,12 @@ +package com.uid2.admin.vertx.api.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Path { + String value(); +} diff --git a/src/main/java/com/uid2/admin/vertx/api/annotations/Roles.java b/src/main/java/com/uid2/admin/vertx/api/annotations/Roles.java new file mode 100644 index 00000000..e3e269a2 --- /dev/null +++ b/src/main/java/com/uid2/admin/vertx/api/annotations/Roles.java @@ -0,0 +1,14 @@ +package com.uid2.admin.vertx.api.annotations; + +import com.uid2.shared.auth.Role; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Roles { + Role[] value(); +} diff --git a/src/main/java/com/uid2/admin/vertx/api/ClientSideKeypairResponse.java b/src/main/java/com/uid2/admin/vertx/api/cstg/ClientSideKeypairResponse.java similarity index 93% rename from src/main/java/com/uid2/admin/vertx/api/ClientSideKeypairResponse.java rename to src/main/java/com/uid2/admin/vertx/api/cstg/ClientSideKeypairResponse.java index 86e70ce6..b5f3cbe9 100644 --- a/src/main/java/com/uid2/admin/vertx/api/ClientSideKeypairResponse.java +++ b/src/main/java/com/uid2/admin/vertx/api/cstg/ClientSideKeypairResponse.java @@ -1,4 +1,4 @@ -package com.uid2.admin.vertx.api; +package com.uid2.admin.vertx.api.cstg; import com.uid2.shared.model.ClientSideKeypair; import lombok.AllArgsConstructor; diff --git a/src/main/java/com/uid2/admin/vertx/api/SiteIdRouter.java b/src/main/java/com/uid2/admin/vertx/api/cstg/GetClientSideKeypairsBySite.java similarity index 54% rename from src/main/java/com/uid2/admin/vertx/api/SiteIdRouter.java rename to src/main/java/com/uid2/admin/vertx/api/cstg/GetClientSideKeypairsBySite.java index 1396f50d..ae00c26a 100644 --- a/src/main/java/com/uid2/admin/vertx/api/SiteIdRouter.java +++ b/src/main/java/com/uid2/admin/vertx/api/cstg/GetClientSideKeypairsBySite.java @@ -1,47 +1,40 @@ -package com.uid2.admin.vertx.api; +package com.uid2.admin.vertx.api.cstg; import com.google.common.collect.Streams; import com.google.inject.Inject; import com.uid2.admin.vertx.ResponseUtil; +import com.uid2.admin.vertx.api.IRouteProvider; +import com.uid2.admin.vertx.api.UrlParameterProviders; +import com.uid2.admin.vertx.api.annotations.ApiMethod; +import com.uid2.admin.vertx.api.annotations.Method; +import com.uid2.admin.vertx.api.annotations.Path; +import com.uid2.admin.vertx.api.annotations.Roles; import com.uid2.admin.vertx.service.ClientSideKeypairService; import com.uid2.shared.auth.Role; -import com.uid2.shared.middleware.AuthMiddleware; import io.vertx.core.Handler; -import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; import lombok.val; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class SiteIdRouter implements IRouteProvider { - private static final Logger LOGGER = LoggerFactory.getLogger(SiteIdRouter.class); +public class GetClientSideKeypairsBySite implements IRouteProvider { + private static final Logger LOGGER = LoggerFactory.getLogger(GetClientSideKeypairsBySite.class); private final ClientSideKeypairService clientSideKeypairService; - private final AuthMiddleware auth; + @Inject - public SiteIdRouter(ClientSideKeypairService clientSideKeypairService, AuthMiddleware auth) { + public GetClientSideKeypairsBySite(ClientSideKeypairService clientSideKeypairService) { this.clientSideKeypairService = clientSideKeypairService; - this.auth = auth; } - @FunctionalInterface - interface ISiteIdRouteHandler { - void handle(RoutingContext rc, int siteId); - } - private Handler provideSiteId(ISiteIdRouteHandler handler) { - return (RoutingContext rc) -> { - val siteId = Integer.parseInt(rc.pathParam("siteId")); - handler.handle(rc, siteId); - }; + @Path("/sites/:siteId/client-side-keypairs") + @Method(ApiMethod.GET) + @Roles({Role.ADMINISTRATOR}) + public Handler getHandler() { + return UrlParameterProviders.provideSiteId(this::handleGetClientSideKeys); } - @Override - public void setupRoutes(Router router) { - router.get("/sites/:siteId/client-side-keypairs").handler(auth.handle(provideSiteId(this::handleGetClientSideKeys), Role.ADMINISTRATOR)); - } - - public void handleGetClientSideKeys(RoutingContext rc, int siteId) { val keypairs = clientSideKeypairService.getKeypairsBySite(siteId); if (keypairs != null) { diff --git a/src/test/java/com/uid2/admin/v2Router/RouterConfigurationTests.java b/src/test/java/com/uid2/admin/v2Router/RouterConfigurationTests.java index 59a72b35..3e358d8d 100644 --- a/src/test/java/com/uid2/admin/v2Router/RouterConfigurationTests.java +++ b/src/test/java/com/uid2/admin/v2Router/RouterConfigurationTests.java @@ -4,7 +4,7 @@ import com.google.inject.Guice; import com.uid2.admin.GuiceMockInjectingModule; import com.uid2.admin.RequireInjectAnnotationsModule; -import com.uid2.admin.vertx.api.SiteIdRouter; +import com.uid2.admin.vertx.api.cstg.GetClientSideKeypairsBySite; import com.uid2.admin.vertx.api.V2RouterModule; import com.uid2.admin.vertx.service.ClientSideKeypairService; import com.uid2.shared.middleware.AuthMiddleware; @@ -21,7 +21,7 @@ public class RouterConfigurationTests { public void IfNeededDependencyIsNotProvided_CreateThrowsAnException() { assertThrows(CreationException.class, () -> { val injector = Guice.createInjector(new RequireInjectAnnotationsModule(), new V2RouterModule()); - injector.getInstance(SiteIdRouter.class); + injector.getInstance(GetClientSideKeypairsBySite.class); }); } @@ -35,7 +35,7 @@ public void IfNeededDependenciesAreAvailable_ARouterModuleCanBeCreated() throws new V2RouterModule(), new GuiceMockInjectingModule(keypairServiceMock, authMock) ); - val siteIdRouter = injector.getInstance(SiteIdRouter.class); + val siteIdRouter = injector.getInstance(GetClientSideKeypairsBySite.class); assertNotNull(siteIdRouter); } } diff --git a/src/test/java/com/uid2/admin/v2Router/SiteId_ClientSideKeypair_Tests.java b/src/test/java/com/uid2/admin/v2Router/SiteId_ClientSideKeypair_Tests.java index 3f3c53ee..889c78d6 100644 --- a/src/test/java/com/uid2/admin/v2Router/SiteId_ClientSideKeypair_Tests.java +++ b/src/test/java/com/uid2/admin/v2Router/SiteId_ClientSideKeypair_Tests.java @@ -3,8 +3,8 @@ import com.google.inject.Guice; import com.uid2.admin.GuiceMockInjectingModule; import com.uid2.admin.RequireInjectAnnotationsModule; -import com.uid2.admin.vertx.api.ClientSideKeypairResponse; -import com.uid2.admin.vertx.api.SiteIdRouter; +import com.uid2.admin.vertx.api.cstg.ClientSideKeypairResponse; +import com.uid2.admin.vertx.api.cstg.GetClientSideKeypairsBySite; import com.uid2.admin.vertx.api.V2RouterModule; import com.uid2.admin.vertx.service.ClientSideKeypairService; import com.uid2.shared.middleware.AuthMiddleware; @@ -61,7 +61,7 @@ public void WhenTheSiteHasASingleKey_ItIsReturned() throws InvalidClassException new V2RouterModule(), new GuiceMockInjectingModule(clientSideKeypairMock, authMock) ); - val service = injector.getInstance(SiteIdRouter.class); + val service = injector.getInstance(GetClientSideKeypairsBySite.class); service.handleGetClientSideKeys(contextMock, siteIdToTest); From c8e245f1db64d2ebbabe043b836e2126b8972aa5 Mon Sep 17 00:00:00 2001 From: Lionell Pack Date: Wed, 27 Sep 2023 11:07:18 +1000 Subject: [PATCH 07/15] Make response fields final. Add portal role. --- .../vertx/api/cstg/ClientSideKeypairResponse.java | 10 +++++----- .../vertx/api/cstg/GetClientSideKeypairsBySite.java | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/uid2/admin/vertx/api/cstg/ClientSideKeypairResponse.java b/src/main/java/com/uid2/admin/vertx/api/cstg/ClientSideKeypairResponse.java index b5f3cbe9..022bdeb1 100644 --- a/src/main/java/com/uid2/admin/vertx/api/cstg/ClientSideKeypairResponse.java +++ b/src/main/java/com/uid2/admin/vertx/api/cstg/ClientSideKeypairResponse.java @@ -7,11 +7,11 @@ @AllArgsConstructor public class ClientSideKeypairResponse { - public int siteId; - public String subscriptionId; - public String publicKey; - public Instant created; - public boolean disabled; + public final int siteId; + public final String subscriptionId; + public final String publicKey; + public final Instant created; + public final boolean disabled; static ClientSideKeypairResponse fromClientSiteKeypair(ClientSideKeypair keypair) { return new ClientSideKeypairResponse(keypair.getSiteId(), keypair.getSubscriptionId(), keypair.encodePublicKeyToString(), keypair.getCreated(), keypair.isDisabled()); diff --git a/src/main/java/com/uid2/admin/vertx/api/cstg/GetClientSideKeypairsBySite.java b/src/main/java/com/uid2/admin/vertx/api/cstg/GetClientSideKeypairsBySite.java index ae00c26a..9db13328 100644 --- a/src/main/java/com/uid2/admin/vertx/api/cstg/GetClientSideKeypairsBySite.java +++ b/src/main/java/com/uid2/admin/vertx/api/cstg/GetClientSideKeypairsBySite.java @@ -30,7 +30,7 @@ public GetClientSideKeypairsBySite(ClientSideKeypairService clientSideKeypairSer @Path("/sites/:siteId/client-side-keypairs") @Method(ApiMethod.GET) - @Roles({Role.ADMINISTRATOR}) + @Roles({Role.ADMINISTRATOR, Role.SHARING_PORTAL}) public Handler getHandler() { return UrlParameterProviders.provideSiteId(this::handleGetClientSideKeys); } From 30cf8e5da349161d981d43b7bffaf2038a11918d Mon Sep 17 00:00:00 2001 From: Lionell Pack Date: Wed, 27 Sep 2023 11:13:49 +1000 Subject: [PATCH 08/15] Remove out-of-date comment. --- src/main/java/com/uid2/admin/vertx/AdminVerticle.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/uid2/admin/vertx/AdminVerticle.java b/src/main/java/com/uid2/admin/vertx/AdminVerticle.java index 6f3f4638..d8151ba8 100644 --- a/src/main/java/com/uid2/admin/vertx/AdminVerticle.java +++ b/src/main/java/com/uid2/admin/vertx/AdminVerticle.java @@ -85,8 +85,6 @@ private Router createRoutesSetup() { if (v2Router != null) { v2Router.setupSubRouter(vertx, router); - // TODO: Make all API requests go via the auth handler - } return router; From 49fa9083c5aece0f595903d41a964ae355f6c756 Mon Sep 17 00:00:00 2001 From: Lionell Pack Date: Wed, 27 Sep 2023 11:17:10 +1000 Subject: [PATCH 09/15] Add useful information on constructors and DI. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 19b96b85..b8fd3bd5 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ IRouteProvider requires a `getHandler` method, which should return a valid handl All classes which implement IRouteProvider will automatically be picked up by DI and registered as route handlers. The route handler will automatically be wrapped by the Auth middleware based on the roles specified in the Roles annotation. +Currently, we require the explicit `@Inject` annotation on all constructors which are valid for the DI framework to use. Your IRouteProvider implementation *must* have a constructor with the @Inject annotation. + ## Dependency injection - current state and plans We are in the process of introducing dependency injection to the code base. Currently, a number of singletons which are constructed explicitly are provided via `ServicesModule` (for `IService` classes) and the `SingletonsModule` (for other singletons - e.g. the Auth middleware). From 38946439999f25860ddc0bea456d1959e01164f3 Mon Sep 17 00:00:00 2001 From: Lionell Pack Date: Wed, 27 Sep 2023 11:22:41 +1000 Subject: [PATCH 10/15] Add a useful comment. --- src/main/java/com/uid2/admin/vertx/api/IRouteProvider.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/uid2/admin/vertx/api/IRouteProvider.java b/src/main/java/com/uid2/admin/vertx/api/IRouteProvider.java index ee328b9b..7bedf02f 100644 --- a/src/main/java/com/uid2/admin/vertx/api/IRouteProvider.java +++ b/src/main/java/com/uid2/admin/vertx/api/IRouteProvider.java @@ -7,7 +7,8 @@ /* Implement this interface to automatically be picked up by V2Router and have your routes registered under /v2api/*. Any constructor dependencies which are registered should be auto-injected by Guice, as long as it knows about them. -*/ + You must have a constructor marked with @Inject for DI to use it. + */ public interface IRouteProvider { Handler getHandler(); } From d75faac97c36fcb5dd70e545d4429218a72793dd Mon Sep 17 00:00:00 2001 From: Lionell Pack Date: Wed, 27 Sep 2023 13:22:38 +1000 Subject: [PATCH 11/15] Provide a way to select between blocking and non-blocking handler. --- README.md | 2 ++ .../uid2/admin/vertx/api/IBlockingRouteProvider.java | 4 ++++ src/main/java/com/uid2/admin/vertx/api/V2Router.java | 11 ++++++++++- .../java/com/uid2/admin/vertx/api/V2RouterModule.java | 7 ++++++- .../vertx/api/cstg/GetClientSideKeypairsBySite.java | 1 + 5 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/uid2/admin/vertx/api/IBlockingRouteProvider.java diff --git a/README.md b/README.md index b8fd3bd5..e0a29d63 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ The v2 API is based on individual route provider classes. Each class should prov ### IRouteProvider +**Caution:** When implementing an API endpoint, you need to decide whether you should have a blocking or a non-blocking handler. Non-blocking handlers are suitable for most read-only operations, while most write operations should be done on a blocking handler. If you are calling into a service with a `synchronized` block, you **must** use a blocking handler. You can make your handler blocking by implementing the `IBlockingRouteProvider` interface *instead of* the `IRouteProvider` interface. + IRouteProvider requires a `getHandler` method, which should return a valid handler function - see `GetClientSideKeypairsBySite.java`. This method *must* be annotated with the Path, Method, and Roles annotations. All classes which implement IRouteProvider will automatically be picked up by DI and registered as route handlers. The route handler will automatically be wrapped by the Auth middleware based on the roles specified in the Roles annotation. diff --git a/src/main/java/com/uid2/admin/vertx/api/IBlockingRouteProvider.java b/src/main/java/com/uid2/admin/vertx/api/IBlockingRouteProvider.java new file mode 100644 index 00000000..2fd86125 --- /dev/null +++ b/src/main/java/com/uid2/admin/vertx/api/IBlockingRouteProvider.java @@ -0,0 +1,4 @@ +package com.uid2.admin.vertx.api; + +public interface IBlockingRouteProvider extends IRouteProvider { +} diff --git a/src/main/java/com/uid2/admin/vertx/api/V2Router.java b/src/main/java/com/uid2/admin/vertx/api/V2Router.java index 8a1eb2be..c66e64c7 100644 --- a/src/main/java/com/uid2/admin/vertx/api/V2Router.java +++ b/src/main/java/com/uid2/admin/vertx/api/V2Router.java @@ -13,6 +13,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Arrays; import java.util.Set; @Singleton @@ -38,7 +39,15 @@ public void setupSubRouter(Vertx verticle, Router parentRouter) { val path = handler.getAnnotation(Path.class).value(); val method = handler.getAnnotation(Method.class).value().vertxMethod; val roles = handler.getAnnotation(Roles.class).value(); - v2router.route(method, path).handler(auth.handle(provider.getHandler(), roles)); + val authWrappedHandler = auth.handle(provider.getHandler(), roles); + if (Arrays.stream(provider.getClass().getInterfaces()).anyMatch(iface -> iface == IBlockingRouteProvider.class)) { + LOGGER.info("Using blocking handler for " + provider.getClass().getName()); + v2router.route(method, path).blockingHandler(authWrappedHandler); + } + else { + LOGGER.info("Using non-blocking handler for " + provider.getClass().getName()); + v2router.route(method, path).handler(authWrappedHandler); + } } catch (NoSuchMethodException e) { LOGGER.error("Could not find handle method for API handler: " + provider.getClass().getName()); } diff --git a/src/main/java/com/uid2/admin/vertx/api/V2RouterModule.java b/src/main/java/com/uid2/admin/vertx/api/V2RouterModule.java index b8e52005..e975401e 100644 --- a/src/main/java/com/uid2/admin/vertx/api/V2RouterModule.java +++ b/src/main/java/com/uid2/admin/vertx/api/V2RouterModule.java @@ -7,12 +7,16 @@ import com.google.inject.Singleton; import com.google.inject.multibindings.Multibinder; import lombok.val; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.Arrays; import java.util.stream.Collectors; public class V2RouterModule extends AbstractModule { + private static final Logger LOGGER = LoggerFactory.getLogger(V2RouterModule.class); + /* Finds all classes in com.uid2.admin.vertx.api which implement IRouterProvider and register them. They are registered both as IRouterProvider and as their individual class. @@ -28,10 +32,11 @@ protected void configure() { .stream() .filter(ci -> ci.getName().startsWith("com.uid2.admin.vertx.api")) .map(ci -> ci.load()) - .filter(cl -> Arrays.stream(cl.getInterfaces()).anyMatch(interf -> interf == IRouteProvider.class)) + .filter(cl -> !cl.isInterface() && Arrays.stream(cl.getInterfaces()).anyMatch(interf -> interf == IRouteProvider.class || interf == IBlockingRouteProvider.class)) .map(cl -> (Class)cl) .collect(Collectors.toSet()); for (val routerProviderClass : routerProviders) { + LOGGER.info("Registering v2 route provider " + routerProviderClass.getName()); bind(routerProviderClass); interfaceBinder.addBinding().to(routerProviderClass); } diff --git a/src/main/java/com/uid2/admin/vertx/api/cstg/GetClientSideKeypairsBySite.java b/src/main/java/com/uid2/admin/vertx/api/cstg/GetClientSideKeypairsBySite.java index 9db13328..7907a29d 100644 --- a/src/main/java/com/uid2/admin/vertx/api/cstg/GetClientSideKeypairsBySite.java +++ b/src/main/java/com/uid2/admin/vertx/api/cstg/GetClientSideKeypairsBySite.java @@ -3,6 +3,7 @@ import com.google.common.collect.Streams; import com.google.inject.Inject; import com.uid2.admin.vertx.ResponseUtil; +import com.uid2.admin.vertx.api.IBlockingRouteProvider; import com.uid2.admin.vertx.api.IRouteProvider; import com.uid2.admin.vertx.api.UrlParameterProviders; import com.uid2.admin.vertx.api.annotations.ApiMethod; From 8d9cb2d24cef75087c2da59270f509add2ba13b0 Mon Sep 17 00:00:00 2001 From: Lionell Pack Date: Wed, 27 Sep 2023 13:26:24 +1000 Subject: [PATCH 12/15] Provide some useful comments. --- .../com/uid2/admin/vertx/api/IBlockingRouteProvider.java | 5 +++++ src/main/java/com/uid2/admin/vertx/api/IRouteProvider.java | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/uid2/admin/vertx/api/IBlockingRouteProvider.java b/src/main/java/com/uid2/admin/vertx/api/IBlockingRouteProvider.java index 2fd86125..d7bd1c14 100644 --- a/src/main/java/com/uid2/admin/vertx/api/IBlockingRouteProvider.java +++ b/src/main/java/com/uid2/admin/vertx/api/IBlockingRouteProvider.java @@ -1,4 +1,9 @@ package com.uid2.admin.vertx.api; +/* + *Important* + If you implement this interface, your route will be registered as a blocking handler. Use IRouteProvider + instead if you want to provide a non-blocking handler. See `readme.md` for more information. +*/ public interface IBlockingRouteProvider extends IRouteProvider { } diff --git a/src/main/java/com/uid2/admin/vertx/api/IRouteProvider.java b/src/main/java/com/uid2/admin/vertx/api/IRouteProvider.java index 7bedf02f..62d74020 100644 --- a/src/main/java/com/uid2/admin/vertx/api/IRouteProvider.java +++ b/src/main/java/com/uid2/admin/vertx/api/IRouteProvider.java @@ -5,9 +5,13 @@ import io.vertx.ext.web.RoutingContext; /* - Implement this interface to automatically be picked up by V2Router and have your routes registered under /v2api/*. + Implement this interface to automatically be picked up by V2Router and have your routes registered under /v2api/. Any constructor dependencies which are registered should be auto-injected by Guice, as long as it knows about them. You must have a constructor marked with @Inject for DI to use it. + + *Important* + If you implement this interface, your route will be registered as a non-blocking handler. Use IBlockingRouteProvider + instead if you want to provide a blocking handler. See `readme.md` for more information. */ public interface IRouteProvider { Handler getHandler(); From dfa46cd047d885cac2ac66d2941ec38bedc3c4e9 Mon Sep 17 00:00:00 2001 From: Lionell Pack Date: Wed, 27 Sep 2023 16:11:14 +1000 Subject: [PATCH 13/15] Bind services to other interfaces they implement as well as IService. Make the GetClientSideKeypairsBySite handler depend on an interface instead of the full clientside keypairs class. --- .../java/com/uid2/admin/secret/IKeypairManager.java | 1 + .../vertx/api/cstg/GetClientSideKeypairsBySite.java | 10 +++++----- .../com/uid2/admin/vertx/service/ServicesModule.java | 6 ++++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/uid2/admin/secret/IKeypairManager.java b/src/main/java/com/uid2/admin/secret/IKeypairManager.java index 732a68e6..8badba31 100644 --- a/src/main/java/com/uid2/admin/secret/IKeypairManager.java +++ b/src/main/java/com/uid2/admin/secret/IKeypairManager.java @@ -4,4 +4,5 @@ public interface IKeypairManager { ClientSideKeypair createAndSaveSiteKeypair(int siteId, String contact, boolean disabled) throws Exception; + Iterable getKeypairsBySite(int siteId); } diff --git a/src/main/java/com/uid2/admin/vertx/api/cstg/GetClientSideKeypairsBySite.java b/src/main/java/com/uid2/admin/vertx/api/cstg/GetClientSideKeypairsBySite.java index 7907a29d..93a03b13 100644 --- a/src/main/java/com/uid2/admin/vertx/api/cstg/GetClientSideKeypairsBySite.java +++ b/src/main/java/com/uid2/admin/vertx/api/cstg/GetClientSideKeypairsBySite.java @@ -2,8 +2,8 @@ import com.google.common.collect.Streams; import com.google.inject.Inject; +import com.uid2.admin.secret.IKeypairManager; import com.uid2.admin.vertx.ResponseUtil; -import com.uid2.admin.vertx.api.IBlockingRouteProvider; import com.uid2.admin.vertx.api.IRouteProvider; import com.uid2.admin.vertx.api.UrlParameterProviders; import com.uid2.admin.vertx.api.annotations.ApiMethod; @@ -21,12 +21,12 @@ public class GetClientSideKeypairsBySite implements IRouteProvider { private static final Logger LOGGER = LoggerFactory.getLogger(GetClientSideKeypairsBySite.class); - private final ClientSideKeypairService clientSideKeypairService; + private final IKeypairManager keypairManager; @Inject - public GetClientSideKeypairsBySite(ClientSideKeypairService clientSideKeypairService) { - this.clientSideKeypairService = clientSideKeypairService; + public GetClientSideKeypairsBySite(IKeypairManager keypairManager) { + this.keypairManager = keypairManager; } @Path("/sites/:siteId/client-side-keypairs") @@ -37,7 +37,7 @@ public Handler getHandler() { } public void handleGetClientSideKeys(RoutingContext rc, int siteId) { - val keypairs = clientSideKeypairService.getKeypairsBySite(siteId); + val keypairs = keypairManager.getKeypairsBySite(siteId); if (keypairs != null) { val result = Streams.stream(keypairs) .map(kp -> ClientSideKeypairResponse.fromClientSiteKeypair(kp)) diff --git a/src/main/java/com/uid2/admin/vertx/service/ServicesModule.java b/src/main/java/com/uid2/admin/vertx/service/ServicesModule.java index b3a60a10..0dc0b7ae 100644 --- a/src/main/java/com/uid2/admin/vertx/service/ServicesModule.java +++ b/src/main/java/com/uid2/admin/vertx/service/ServicesModule.java @@ -20,10 +20,12 @@ public ServicesModule(IService[] services) { @Override protected void configure() { - Multibinder interfaceBinder = Multibinder.newSetBinder(binder(), IService.class); + Multibinder serviceInterfaceBinder = Multibinder.newSetBinder(binder(), IService.class); Arrays.stream(services).forEach(s -> { bind((Class)s.getClass()).toInstance(s); - interfaceBinder.addBinding().toInstance(s); + serviceInterfaceBinder.addBinding().toInstance(s); + val interfaces = Arrays.stream(s.getClass().getInterfaces()).filter(i -> i != IService.class); + interfaces.forEach(i -> bind((Class)i).toInstance(s)); }); } } From 926aeaf9a5168df93df22690ef85136b39db34cb Mon Sep 17 00:00:00 2001 From: Lionell Pack Date: Wed, 27 Sep 2023 16:24:29 +1000 Subject: [PATCH 14/15] Inject mocks as all interfaces (apart from IService) they implement as well. --- src/test/java/com/uid2/admin/GuiceMockInjectingModule.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/java/com/uid2/admin/GuiceMockInjectingModule.java b/src/test/java/com/uid2/admin/GuiceMockInjectingModule.java index e74cdf6a..f2873634 100644 --- a/src/test/java/com/uid2/admin/GuiceMockInjectingModule.java +++ b/src/test/java/com/uid2/admin/GuiceMockInjectingModule.java @@ -7,6 +7,7 @@ import java.io.InvalidClassException; import java.util.Arrays; +import java.util.stream.Collectors; import static org.mockito.Mockito.*; @@ -28,6 +29,8 @@ protected void configure() { Arrays.stream(mocks).forEach(mock -> { System.out.println("Configuring mock for class " + mock.getClass().getName()); bind((Class)mock.getClass()).toInstance(mock); + val interfaces = Arrays.stream(mock.getClass().getInterfaces()).filter(iface -> iface != IService.class); + interfaces.forEach(iface -> bind((Class)iface).toInstance(mock)); }); } } From 340f100b15f1f9fe4f06eef399754f6b85c1bdf1 Mon Sep 17 00:00:00 2001 From: Lionell Pack Date: Wed, 27 Sep 2023 16:28:14 +1000 Subject: [PATCH 15/15] Simplify the handler test - injector functionality is tested elsewhere. --- .../admin/v2Router/SiteId_ClientSideKeypair_Tests.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/test/java/com/uid2/admin/v2Router/SiteId_ClientSideKeypair_Tests.java b/src/test/java/com/uid2/admin/v2Router/SiteId_ClientSideKeypair_Tests.java index 889c78d6..833d305a 100644 --- a/src/test/java/com/uid2/admin/v2Router/SiteId_ClientSideKeypair_Tests.java +++ b/src/test/java/com/uid2/admin/v2Router/SiteId_ClientSideKeypair_Tests.java @@ -32,8 +32,6 @@ public class SiteId_ClientSideKeypair_Tests { private ClientSideKeypairService clientSideKeypairMock; @Mock private RoutingContext contextMock; - @Mock - private AuthMiddleware authMock; ClientSideKeypair createKeypairMock(int siteId, String publicKey) { val kpMock = mock(ClientSideKeypair.class); when(kpMock.getSiteId()).thenReturn(siteId); @@ -56,13 +54,7 @@ public void WhenTheSiteHasASingleKey_ItIsReturned() throws InvalidClassException when(clientSideKeypairMock.getKeypairsBySite(5)) .thenReturn(expectedResult); - val injector = Guice.createInjector( - new RequireInjectAnnotationsModule(), - new V2RouterModule(), - new GuiceMockInjectingModule(clientSideKeypairMock, authMock) - ); - val service = injector.getInstance(GetClientSideKeypairsBySite.class); - + val service = new GetClientSideKeypairsBySite(clientSideKeypairMock); service.handleGetClientSideKeys(contextMock, siteIdToTest); verify(contextMock).json(keypairResponseCaptor.capture());