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
Set Description
Set Types
Set Site Domain Names
+ Set Site App Names
Set Visible
Set Not Visible
@@ -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());