diff --git a/README.md b/README.md index 8ed8fba5..a89c341a 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,7 @@ and re-initialize your localstack by running `docker-compose restart`. ### Authentication and Authorization -When running locally, Okta OAuth is disabled and users are logged in as *test.user@unifiedid.com* via the -`is_auth_disabled` flag. The user has all the rights available. +When running locally, set the `is_auth_disabled` flag to true. It disables Okta OAuth and users are logged in as *test.user@unifiedid.com*. The user has all the rights available. If you want to test with Okta OAuth, set the `is_auth_disabled` flag to `false`, and fill in the `okta_client_secret` with the value under "Okta localhost deployment" in 1Password. diff --git a/pom.xml b/pom.xml index 23ae8dc1..a26b7973 100644 --- a/pom.xml +++ b/pom.xml @@ -16,7 +16,7 @@ 1.1.0 5.7.0 - 7.7.6-1e644a0ded + 7.9.0 0.5.8 ${project.version} diff --git a/src/main/java/com/uid2/admin/vertx/service/SiteService.java b/src/main/java/com/uid2/admin/vertx/service/SiteService.java index d49e2e84..0907b97c 100644 --- a/src/main/java/com/uid2/admin/vertx/service/SiteService.java +++ b/src/main/java/com/uid2/admin/vertx/service/SiteService.java @@ -1,5 +1,6 @@ package com.uid2.admin.vertx.service; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectWriter; import com.uid2.admin.auth.AdminAuthMiddleware; import com.uid2.admin.legacy.ILegacyClientKeyProvider; @@ -83,6 +84,11 @@ public void setupRoutes(Router router) { this.handleSiteDomains(ctx); } }, Role.MAINTAINER, Role.SHARING_PORTAL)); + router.post("/api/site/app_names").blockingHandler(auth.handle((ctx) -> { + synchronized (writeLock) { + this.handleSiteAppNames(ctx); + } + }, Role.MAINTAINER, Role.SHARING_PORTAL)); router.post("/api/site/update").blockingHandler(auth.handle((ctx) -> { synchronized (writeLock) { this.handleSiteUpdate(ctx); @@ -127,12 +133,16 @@ private static JsonObject createSiteJsonObject(Site site, Map normalizedAppNames = new HashSet<>(); + if (body != null) { + JsonArray appNamesJa = body.getJsonArray("app_names"); + if (appNamesJa != null) { + normalizedAppNames = getNormalizedAppNames(rc, appNamesJa); + if (normalizedAppNames == null) return; + } + } + boolean enabled = false; List enabledFlags = rc.queryParam("enabled"); if (!enabledFlags.isEmpty()) { @@ -220,7 +239,7 @@ private void handleSiteAdd(RoutingContext rc) { .collect(Collectors.toList()); final int siteId = 1 + sites.stream().mapToInt(Site::getId).max().orElse(Const.Data.AdvertisingTokenSiteId); - final Site newSite = new Site(siteId, name, description, enabled, types, new HashSet<>(normalizedDomainNames), true); + final Site newSite = new Site(siteId, name, description, enabled, types, new HashSet<>(normalizedDomainNames), normalizedAppNames, true); // add site to the array sites.add(newSite); @@ -247,15 +266,9 @@ private void handleSiteTypesSet(RoutingContext rc) { return; } - final List sites = this.siteProvider.getAllSites() - .stream().sorted(Comparator.comparingInt(Site::getId)) - .collect(Collectors.toList()); - existingSite.setClientTypes(types); - storeWriter.upload(sites, null); - - rc.response().end(jsonWriter.writeValueAsString(existingSite)); + uploadSiteToStoreWriterAndWriteExistingSiteToResponse(existingSite, rc); } catch (Exception e) { rc.fail(500, e); } @@ -318,15 +331,36 @@ private void handleSiteDomains(RoutingContext rc) { existingSite.setDomainNames(new HashSet<>(normalizedDomainNames)); - final List sites = this.siteProvider.getAllSites() - .stream().sorted(Comparator.comparingInt(Site::getId)) - .collect(Collectors.toList()); + uploadSiteToStoreWriterAndWriteExistingSiteToResponse(existingSite, rc); + } catch (Exception e) { + ResponseUtil.errorInternal(rc, "set site domain_names failed", e); + } + } - storeWriter.upload(sites, null); + private void handleSiteAppNames(RoutingContext rc) { + try { + // refresh manually + siteProvider.loadContent(); - rc.response().end(jsonWriter.writeValueAsString(existingSite)); + final Site existingSite = RequestUtil.getSiteFromParam(rc, "id", siteProvider); + if (existingSite == null) { + return; + } + + JsonObject body = rc.body().asJsonObject(); + JsonArray appNamesJa = body.getJsonArray("app_names"); + if (appNamesJa == null) { + ResponseUtil.error(rc, 400, "required parameters: app_names"); + return; + } + Set normalizedAppNames = getNormalizedAppNames(rc, appNamesJa); + if (normalizedAppNames == null) return; + + existingSite.setAppNames(normalizedAppNames); + + uploadSiteToStoreWriterAndWriteExistingSiteToResponse(existingSite, rc); } catch (Exception e) { - ResponseUtil.errorInternal(rc, "set site domain_names failed", e); + ResponseUtil.errorInternal(rc, "set site app_names failed", e); } } @@ -355,13 +389,7 @@ private void handleSiteUpdate(RoutingContext rc) { } } - final List sites = this.siteProvider.getAllSites() - .stream().sorted(Comparator.comparingInt(Site::getId)) - .collect(Collectors.toList()); - - storeWriter.upload(sites, null); - - rc.response().end(jsonWriter.writeValueAsString(existingSite)); + uploadSiteToStoreWriterAndWriteExistingSiteToResponse(existingSite, rc); } catch (Exception e) { rc.fail(500, e); } @@ -389,6 +417,17 @@ private static List getNormalizedDomainNames(RoutingContext rc, JsonArra return normalizedDomainNames; } + private static Set getNormalizedAppNames(RoutingContext rc, JsonArray appNamesJa) { + List appNames = appNamesJa.stream().map(String::valueOf).collect(Collectors.toList()); + + boolean containsDuplicates = appNames.stream().distinct().count() < appNames.size(); + if (containsDuplicates) { + ResponseUtil.error(rc, 400, "duplicate app_names not permitted"); + return null; + } + return new HashSet<>(appNames); + } + public static String getTopLevelDomainName(String origin) throws MalformedURLException { String host; try { @@ -409,4 +448,13 @@ public static String getTopLevelDomainName(String origin) throws MalformedURLExc } throw new MalformedURLException(); } + + private void uploadSiteToStoreWriterAndWriteExistingSiteToResponse(Site existingSite, RoutingContext rc) throws Exception { + final List sites = this.siteProvider.getAllSites() + .stream().sorted(Comparator.comparingInt(Site::getId)) + .collect(Collectors.toList()); + + storeWriter.upload(sites, null); + rc.response().end(jsonWriter.writeValueAsString(existingSite)); + } } diff --git a/src/main/resources/localstack/s3/core/sites/sites.json b/src/main/resources/localstack/s3/core/sites/sites.json index beee5d13..35a786ab 100644 --- a/src/main/resources/localstack/s3/core/sites/sites.json +++ b/src/main/resources/localstack/s3/core/sites/sites.json @@ -20,7 +20,8 @@ "enabled": true, "clientTypes": ["PUBLISHER"], "visible": true, - "domain_names": ["example.com"] + "domain_names": ["example.com"], + "app_names": ["com.123.Game.App.android", "123456789", "com.123.Game.App.ios"] }, { "id": 125, diff --git a/src/test/java/com/uid2/admin/vertx/SiteServiceTest.java b/src/test/java/com/uid2/admin/vertx/SiteServiceTest.java index 8a13cda1..6d5447df 100644 --- a/src/test/java/com/uid2/admin/vertx/SiteServiceTest.java +++ b/src/test/java/com/uid2/admin/vertx/SiteServiceTest.java @@ -53,6 +53,7 @@ private void checkSiteResponse(Site expectedSite, JsonObject actualSite){ assertEquals(expectedSite.getName(), actualSite.getString("name")); assertEquals(expectedSite.isEnabled(), actualSite.getBoolean("enabled")); assertEquals(expectedSite.getDomainNames(), actualSite.getJsonArray("domain_names").stream().collect(Collectors.toSet())); + assertEquals(expectedSite.getAppNames(), actualSite.getJsonArray("app_names").stream().collect(Collectors.toSet())); assertEquals(expectedSite.getCreated(), actualSite.getLong("created")); } @@ -77,6 +78,7 @@ private void checkSiteResponseWithoutCreatedAt(Site expectedSite, JsonObject act assertEquals(expectedSite.getName(), actualSite.getString("name")); assertEquals(expectedSite.isEnabled(), actualSite.getBoolean("enabled")); assertEquals(expectedSite.getDomainNames(), actualSite.getJsonArray("domain_names").stream().collect(Collectors.toSet())); + assertEquals(expectedSite.getAppNames(), actualSite.getJsonArray("app_names").stream().collect(Collectors.toSet())); } @Test @@ -101,6 +103,7 @@ void listSitesHaveSites(Vertx vertx, VertxTestContext testContext) { new Site(11, "site1", false), new Site(12, "site2", true), new Site(13, "site3", false, Set.of("test1.com", "test2.net")), + new Site(14, "site4", false, null, Set.of("test1.com", "test2.net"), Set.of("com.123.game.app.android", "12345678")), }; setSites(sites); @@ -123,6 +126,7 @@ void listSitesWithKeys(Vertx vertx, VertxTestContext testContext) { new Site(12, "site2", true), new Site(13, "site3", false, Set.of("test1.com", "test2.net")), new Site(14, "site3", false), + new Site(15, "site4", false, null, Set.of("test1.com", "test2.net"), Set.of("com.123.game.app.android", "12345678")), }; setSites(sites); @@ -131,6 +135,7 @@ void listSitesWithKeys(Vertx vertx, VertxTestContext testContext) { new LegacyClientKey("UID2-C-L-12-ck222222", "ckh2", "cks2", "cs2", "c2", Instant.MIN, Set.of(Role.MAPPER), 12, "UID2-C-L-12-ck222"), new LegacyClientKey("UID2-C-L-11-ck333333", "ckh3", "cks3", "cs3", "c3", Instant.MIN, Set.of(Role.GENERATOR, Role.MAPPER), 11, "UID2-C-L-11-ck333"), new LegacyClientKey("UID2-C-L-13-ck444444", "ckh4", "cks4", "cs4", "c4", Instant.MIN, Set.of(Role.SHARER), 13, "UID2-C-L-13-ck444"), + new LegacyClientKey("UID2-C-L-13-ck444444", "ckh5", "cks5", "cs5", "c5", Instant.MIN, Set.of(Role.SHARER), 15, "UID2-C-L-13-ck555"), }; setClientKeys(clientKeys); @@ -157,6 +162,7 @@ void getSiteWithStringId(Vertx vertx, VertxTestContext testContext){ new Site(12, "site2", true), new Site(13, "site3", false, Set.of("test1.com", "test2.net")), new Site(14, "site3", false), + new Site(15, "site4", false, null, Set.of("test1.com", "test2.net"), Set.of("com.123.game.app.android", "12345678")), }; setSites(sites); @@ -176,6 +182,7 @@ void getSiteWithInvalidId(Vertx vertx, VertxTestContext testContext){ new Site(12, "site2", true), new Site(13, "site3", false, Set.of("test1.com", "test2.net")), new Site(14, "site3", false), + new Site(15, "site4", false, null, Set.of("test1.com", "test2.net"), Set.of("com.123.game.app.android", "12345678")), }; setSites(sites); @@ -196,6 +203,7 @@ void getSiteWithUnusedId(Vertx vertx, VertxTestContext testContext){ new Site(12, "site2", true), new Site(13, "site3", false, Set.of("test1.com", "test2.net")), new Site(14, "site3", false), + new Site(15, "site4", false, null, Set.of("test1.com", "test2.net"), Set.of("com.123.game.app.android", "12345678")), }; setSites(sites); @@ -229,6 +237,7 @@ void getSiteWithValidId(Vertx vertx, VertxTestContext testContext) { new Site(12, "site2", true), new Site(13, "site3", false, Set.of("test1.com", "test2.net")), new Site(14, "site3", false), + new Site(15, "site4", false, null, Set.of("test1.com", "test2.net"), Set.of("com.123.game.app.android", "12345678")), }; setSites(sites); @@ -237,6 +246,7 @@ void getSiteWithValidId(Vertx vertx, VertxTestContext testContext) { new LegacyClientKey("UID2-C-L-12-ck222222", "ckh2", "cks2", "cs2", "c2", Instant.MIN, Set.of(Role.MAPPER), 12, "UID2-C-L-12-ck222"), new LegacyClientKey("UID2-C-L-11-ck333333", "ckh3", "cks3", "cs3", "c3", Instant.MIN, Set.of(Role.GENERATOR, Role.MAPPER), 11, "UID2-C-L-11-ck333"), new LegacyClientKey("UID2-C-L-13-ck444444", "ckh4", "cks4", "cs4", "c4", Instant.MIN, Set.of(Role.SHARER), 13, "UID2-C-L-13-ck444"), + new LegacyClientKey("UID2-C-L-13-ck555555", "ckh5", "cks5", "cs5", "c5", Instant.MIN, Set.of(Role.SHARER), 15, "UID2-C-L-13-ck555"), }; setClientKeys(clientKeys); @@ -669,7 +679,6 @@ void addSiteWithBadDomainNames(Vertx vertx, VertxTestContext testContext) { post(vertx, testContext, "api/site/add?name=test_name&enabled=true", reqBody.encode(), response -> { assertEquals(400, response.statusCode()); assertEquals("invalid domain name: bad", response.bodyAsJsonObject().getString("message")); - assertEquals("invalid domain name: bad", response.bodyAsJsonObject().getString("message")); testContext.completeNow(); }); } @@ -694,5 +703,177 @@ void addSiteWithDuplicateDomainNames(Vertx vertx, VertxTestContext testContext) }); } + @Test + void appNameNoSiteId(Vertx vertx, VertxTestContext testContext) { + fakeAuth(Role.MAINTAINER); + setSites(); + post(vertx, testContext, "api/site/app_names", "", response -> { + assertEquals(400, response.statusCode()); + assertEquals("must specify site id", response.bodyAsJsonObject().getString("message")); + testContext.completeNow(); + }); + } + + @Test + void appNameRoleUnauthorized(Vertx vertx, VertxTestContext testContext) { + fakeAuth(Role.MAPPER); + Site s = new Site(123, "name", true); + setSites(s); + + JsonObject reqBody = new JsonObject(); + JsonArray names = new JsonArray(); + names.add("abc1"); + reqBody.put("app_names", names); + + post(vertx, testContext, "api/site/app_names?id=123", "", response -> { + assertEquals(401, response.statusCode()); + testContext.completeNow(); + }); + } + + @Test + void appNameMissingSiteId(Vertx vertx, VertxTestContext testContext) { + fakeAuth(Role.MAINTAINER); + setSites(); + post(vertx, testContext, "api/site/app_names?id=123", "", response -> { + assertEquals(404, response.statusCode()); + assertEquals("site not found", response.bodyAsJsonObject().getString("message")); + testContext.completeNow(); + }); + } + + @Test + void appNameInvalidSiteId(Vertx vertx, VertxTestContext testContext) { + fakeAuth(Role.MAINTAINER); + setSites(); + post(vertx, testContext, "api/site/app_names?id=2", "", response -> { + assertEquals(400, response.statusCode()); + assertEquals("must specify a valid site id", response.bodyAsJsonObject().getString("message")); + testContext.completeNow(); + }); + } + + @Test + void appNameBadSiteId(Vertx vertx, VertxTestContext testContext) { + fakeAuth(Role.MAINTAINER); + setSites(); + post(vertx, testContext, "api/site/app_names?id=asdf", "", response -> { + assertEquals(400, response.statusCode()); + assertEquals("unable to parse site id For input string: \"asdf\"", response.bodyAsJsonObject().getString("message")); + testContext.completeNow(); + }); + } + @Test + void appNameNoDomainNames(Vertx vertx, VertxTestContext testContext) { + fakeAuth(Role.MAINTAINER); + setSites(new Site(123, "name", true)); + JsonObject reqBody = new JsonObject(); + post(vertx, testContext, "api/site/app_names?id=123", reqBody.encode(), response -> { + assertEquals(400, response.statusCode()); + assertEquals("required parameters: app_names", response.bodyAsJsonObject().getString("message")); + testContext.completeNow(); + }); + } + + @Test + void appNameEmptyNames(Vertx vertx, VertxTestContext testContext) { + fakeAuth(Role.MAINTAINER); + Site s = new Site(123, "name", true); + setSites(s); + JsonObject reqBody = new JsonObject(); + reqBody.put("app_names", new JsonArray()); + post(vertx, testContext, "api/site/app_names?id=123", reqBody.encode(), response -> { + assertEquals(200, response.statusCode()); + checkSiteResponse(s, response.bodyAsJsonObject()); + testContext.completeNow(); + }); + } + + @Test + void appNameDuplicateAppName(Vertx vertx, VertxTestContext testContext) { + fakeAuth(Role.MAINTAINER); + Site s = new Site(123, "name", true); + setSites(s); + + JsonObject reqBody = new JsonObject(); + JsonArray names = new JsonArray(); + names.add("abc"); + names.add("abc"); + reqBody.put("app_names", names); + + post(vertx, testContext, "api/site/app_names?id=123", reqBody.encode(), response -> { + assertEquals(400, response.statusCode()); + assertEquals("duplicate app_names not permitted", response.bodyAsJsonObject().getString("message")); + testContext.completeNow(); + }); + } + + @Test + void appNameMultipleAppName(Vertx vertx, VertxTestContext testContext) { + fakeAuth(Role.MAINTAINER); + Site s = new Site(123, "name", true); + setSites(s); + + JsonObject reqBody = new JsonObject(); + JsonArray names = new JsonArray(); + names.add("com.123.game.app.android"); + names.add("com.234.game.app.android"); + names.add("com.345.game.app.android"); + names.add("com.456.game.app.android"); + names.add("com.567.game.app.android"); + names.add("com.567.Game.app.android"); + names.add("com.567.Game.App.android"); + reqBody.put("app_names", names); + + post(vertx, testContext, "api/site/app_names?id=123", reqBody.encode(), response -> { + assertEquals(200, response.statusCode()); + s.setAppNames(Set.of("com.123.game.app.android", "com.234.game.app.android", "com.345.game.app.android", "com.456.game.app.android", "com.567.game.app.android", "com.567.Game.app.android", "com.567.Game.App.android")); + checkSiteResponse(s, response.bodyAsJsonObject()); + testContext.completeNow(); + }); + } + + @Test + void addSiteWithAppNames(Vertx vertx, VertxTestContext testContext) { + fakeAuth(Role.MAINTAINER); + + setSites(new Site(123, "name", true, Set.of("qwerty.com"))); + + JsonObject reqBody = new JsonObject(); + JsonArray names = new JsonArray(); + names.add("com.123.game.app.android"); + names.add("com.234.game.app.android"); + names.add("com.345.game.app.android"); + names.add("com.456.game.app.android"); + names.add("com.567.game.app.android"); + names.add("com.567.Game.app.android"); + names.add("com.567.Game.App.android"); + reqBody.put("app_names", names); + + post(vertx, testContext, "api/site/add?name=test_name&enabled=true", reqBody.encode(), response -> { + assertEquals(200, response.statusCode()); + Site expected = new Site(124, "test_name", true, null, null, Set.of("com.123.game.app.android", "com.234.game.app.android", "com.345.game.app.android", "com.456.game.app.android", "com.567.game.app.android", "com.567.Game.app.android", "com.567.Game.App.android")); + checkSiteResponse(expected, response.bodyAsJsonObject()); + testContext.completeNow(); + }); + } + + @Test + void addSiteWithDuplicateAppNames(Vertx vertx, VertxTestContext testContext) { + fakeAuth(Role.MAINTAINER); + + JsonObject reqBody = new JsonObject(); + JsonArray names = new JsonArray(); + names.add("com.123.game.app.android"); + names.add("com.234.game.app.android"); + names.add("com.234.game.app.android"); + reqBody.put("app_names", names); + + post(vertx, testContext, "api/site/add?name=test_name&enabled=true", reqBody.encode(), response -> { + assertEquals(400, response.statusCode()); + assertEquals("duplicate app_names not permitted", response.bodyAsJsonObject().getString("message")); + testContext.completeNow(); + }); + } } \ No newline at end of file diff --git a/webroot/adm/site.html b/webroot/adm/site.html index 6b3ed97f..76c6d07a 100644 --- a/webroot/adm/site.html +++ b/webroot/adm/site.html @@ -38,6 +38,10 @@

Inputs

+
+ + +


@@ -57,6 +61,7 @@

Operations

+ @@ -95,7 +100,8 @@

Output

var url = '/api/site/add?name=' + siteName + '&types=' + types + '&description=' + description; let domainNames = ($('#domainNames').val()).replace(/\s+/g, '').split(',').filter( (value, _, __) => value !== ""); - doApiCall('POST', url, '#standardOutput', '#errorOutput', JSON.stringify({domain_names : domainNames})); + let appNames = ($('#appNames').val()).replace(/\s+/g, '').split(',').filter( (value, _, __) => value !== ""); + doApiCall('POST', url, '#standardOutput', '#errorOutput', JSON.stringify({domain_names : domainNames, app_names : appNames})); }); $('#doDisable').on('click', function () { @@ -136,6 +142,17 @@

Output

doApiCall('POST', url, '#standardOutput', '#errorOutput', JSON.stringify(payload)); }); + $('#doAppNames').on('click', function () { + var siteId = encodeURIComponent($('#siteId').val()); + var url = '/api/site/app_names?id=' + siteId; + + let appNames = ($('#appNames').val()).replace(/\s+/g, '').split(',').filter( (value, _, __) => value !== ""); + + const payload = { app_names: appNames } + + doApiCall('POST', url, '#standardOutput', '#errorOutput', JSON.stringify(payload)); + }); + $('#doSetDescription').on('click', function () { var siteId = encodeURIComponent($('#siteId').val());