Skip to content

Commit 48cbad7

Browse files
authored
Merge pull request #504 from IABTechLab/ian-UID2-1722-add-keypair-delete-ability
Add the ability to delete client-side keypairs
2 parents d358ecb + 22361f5 commit 48cbad7

File tree

4 files changed

+173
-11
lines changed

4 files changed

+173
-11
lines changed

src/main/java/com/uid2/admin/vertx/Endpoints.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public enum Endpoints {
2222

2323
API_CLIENT_SIDE_KEYPAIRS_ADD("/api/client_side_keypairs/add"),
2424
API_CLIENT_SIDE_KEYPAIRS_UPDATE("/api/client_side_keypairs/update"),
25+
API_CLIENT_SIDE_KEYPAIRS_DELETE("/api/client_side_keypairs/delete"),
2526
API_CLIENT_SIDE_KEYPAIRS_LIST("/api/client_side_keypairs/list"),
2627
API_CLIENT_SIDE_KEYPAIRS_SUBSCRIPTIONID("/api/client_side_keypairs/:subscriptionId"),
2728
API_CLIENT_SIDE_KEYPAIRS_BY_SITE("/api/v2/sites/:siteId/client-side-keypairs"),

src/main/java/com/uid2/admin/vertx/service/ClientSideKeypairService.java

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ public void setupRoutes(Router router) {
7676
this.handleUpdateKeypair(ctx);
7777
}
7878
}, new AuditParams(Collections.emptyList(), List.of("subscription_id", "name", "contact", "disabled")), Role.MAINTAINER, Role.SHARING_PORTAL));
79+
router.post(API_CLIENT_SIDE_KEYPAIRS_DELETE.toString()).blockingHandler(auth.handle((ctx) -> {
80+
synchronized (writeLock) {
81+
this.handleDeleteKeypair(ctx);
82+
}
83+
}, new AuditParams(Collections.emptyList(), List.of("subscription_id")), Role.PRIVILEGED, Role.SHARING_PORTAL));
7984
router.get(API_CLIENT_SIDE_KEYPAIRS_LIST.toString()).handler(
8085
auth.handle(this::handleListAllKeypairs, Role.MAINTAINER, Role.METRICS_EXPORT));
8186
router.get(API_CLIENT_SIDE_KEYPAIRS_SUBSCRIPTIONID.toString()).handler(
@@ -119,22 +124,16 @@ private void handleAddKeypair(RoutingContext rc) {
119124
}
120125

121126
private void handleUpdateKeypair(RoutingContext rc) {
122-
final JsonObject body = rc.body().asJsonObject();
123-
final String subscriptionId = body.getString("subscription_id");
127+
JsonObject body = getRequestBody(rc);
128+
if (body == null) return;
129+
124130
String contact = body.getString("contact");
125131
Boolean disabled = body.getBoolean("disabled");
126132
String name = body.getString("name");
127133

128-
if (subscriptionId == null) {
129-
ResponseUtil.error(rc, 400, "Required parameters: subscription_id");
130-
return;
131-
}
134+
ClientSideKeypair keypair = validateAndGetKeypair(rc, body);
135+
if (keypair == null) return;
132136

133-
ClientSideKeypair keypair = this.keypairStore.getSnapshot().getKeypair(subscriptionId);
134-
if (keypair == null) {
135-
ResponseUtil.error(rc, 404, "Failed to find a keypair for subscription id: " + subscriptionId);
136-
return;
137-
}
138137

139138
if (contact == null && disabled == null && name == null) {
140139
ResponseUtil.error(rc, 400, "Updatable parameters: contact, disabled, name");
@@ -179,6 +178,30 @@ private void handleUpdateKeypair(RoutingContext rc) {
179178
.end(json.encode());
180179
}
181180

181+
private void handleDeleteKeypair(RoutingContext rc) {
182+
JsonObject body = getRequestBody(rc);
183+
if (body == null) return;
184+
185+
ClientSideKeypair keypair = validateAndGetKeypair(rc, body);
186+
if (keypair == null) return;
187+
188+
Set<ClientSideKeypair> allKeypairs = new HashSet<>(this.keypairStore.getAll());
189+
allKeypairs.remove(keypair);
190+
191+
try {
192+
storeWriter.upload(allKeypairs, null);
193+
} catch (Exception e) {
194+
ResponseUtil.errorInternal(rc, "failed to upload keypairs", e);
195+
return;
196+
}
197+
198+
JsonObject responseJson = new JsonObject()
199+
.put("success", true)
200+
.put("deleted_keypair", createKeypairJsonObject(toJsonWithoutPrivateKey(keypair)));
201+
rc.response().putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
202+
.end(responseJson.encode());
203+
}
204+
182205
public Iterable<ClientSideKeypair> getKeypairsBySite(int siteId) {
183206
return this.keypairStore.getSnapshot().getSiteKeypairs(siteId);
184207
}
@@ -243,4 +266,27 @@ public ClientSideKeypair createAndSaveSiteKeypair(int siteId, String contact, bo
243266
return newKeypair;
244267

245268
}
269+
270+
private JsonObject getRequestBody(RoutingContext rc) {
271+
JsonObject body = rc.body() != null ? rc.body().asJsonObject() : null;
272+
if (body == null) {
273+
ResponseUtil.error(rc, 400, "json payload required but not provided");
274+
}
275+
return body;
276+
}
277+
278+
private ClientSideKeypair validateAndGetKeypair(RoutingContext rc, JsonObject body) {
279+
String subscriptionId = body.getString("subscription_id");
280+
if (subscriptionId == null) {
281+
ResponseUtil.error(rc, 400, "Required parameters: subscription_id");
282+
return null;
283+
}
284+
285+
ClientSideKeypair keypair = this.keypairStore.getSnapshot().getKeypair(subscriptionId);
286+
if (keypair == null) {
287+
ResponseUtil.error(rc, 404, "Failed to find a keypair for subscription id: " + subscriptionId);
288+
return null;
289+
}
290+
return keypair;
291+
}
246292
}

src/test/java/com/uid2/admin/vertx/ClientSideKeypairServiceTest.java

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@
1414
import io.vertx.junit5.VertxTestContext;
1515
import org.junit.jupiter.api.BeforeEach;
1616
import org.junit.jupiter.api.Test;
17+
import org.junit.jupiter.params.ParameterizedTest;
18+
import org.junit.jupiter.params.provider.Arguments;
19+
import org.junit.jupiter.params.provider.MethodSource;
1720
import org.mockito.ArgumentCaptor;
1821

1922
import java.time.Instant;
2023
import java.util.*;
24+
import java.util.stream.Stream;
2125

2226
import static org.junit.jupiter.api.Assertions.*;
2327
import static org.mockito.Mockito.*;
@@ -556,6 +560,102 @@ void updateKeypairDisabledAndName(Vertx vertx, VertxTestContext testContext) thr
556560
testContext.completeNow();
557561
});
558562
}
563+
564+
@Test
565+
void deleteKeypairNoSubscriptionId(Vertx vertx, VertxTestContext testContext) throws Exception {
566+
fakeAuth(Role.PRIVILEGED);
567+
568+
setKeypairs(new ArrayList<>());
569+
570+
JsonObject jo = new JsonObject();
571+
572+
post(vertx, testContext, "api/client_side_keypairs/delete", jo.encode(), response -> {
573+
assertEquals(400, response.statusCode());
574+
assertEquals("Required parameters: subscription_id", response.bodyAsJsonObject().getString("message"));
575+
verify(keypairStoreWriter, times(0)).upload(any(), isNull());
576+
testContext.completeNow();
577+
});
578+
}
579+
580+
@Test
581+
void deleteKeypairBadSubscriptionId(Vertx vertx, VertxTestContext testContext) throws Exception {
582+
fakeAuth(Role.PRIVILEGED);
583+
584+
Map<String, ClientSideKeypair> keypairs = new HashMap<>() {{
585+
put("89aZ234567", new ClientSideKeypair("89aZ234567", pub1, priv1, 124, "[email protected]", Instant.now(), false, name1));
586+
put("9aZ2345678", new ClientSideKeypair("9aZ2345678", pub2, priv2, 125, "[email protected]", Instant.now(), false, name2));
587+
}};
588+
setKeypairs(new ArrayList<>(keypairs.values()));
589+
590+
JsonObject jo = new JsonObject();
591+
jo.put("subscription_id", "bad-id");
592+
593+
post(vertx, testContext, "api/client_side_keypairs/delete", jo.encode(), response -> {
594+
assertEquals(404, response.statusCode());
595+
assertEquals("Failed to find a keypair for subscription id: bad-id", response.bodyAsJsonObject().getString("message"));
596+
verify(keypairStoreWriter, times(0)).upload(any(), isNull());
597+
testContext.completeNow();
598+
});
599+
}
600+
601+
@Test
602+
void deleteKeypair(Vertx vertx, VertxTestContext testContext) throws Exception {
603+
fakeAuth(Role.PRIVILEGED);
604+
605+
Instant time = Instant.now();
606+
ClientSideKeypair keypairToDelete = new ClientSideKeypair("89aZ234567", pub1, priv1, 124, "[email protected]", time, false, name1);
607+
ClientSideKeypair remainingKeypair = new ClientSideKeypair("9aZ2345678", pub2, priv2, 124, "[email protected]", time, false, name2);
608+
609+
setKeypairs(List.of(keypairToDelete, remainingKeypair));
610+
setSites(new Site(124, "test", true));
611+
612+
JsonObject jo = new JsonObject();
613+
jo.put("subscription_id", "89aZ234567");
614+
615+
post(vertx, testContext, "api/client_side_keypairs/delete", jo.encode(), response -> {
616+
assertEquals(200, response.statusCode());
617+
assertEquals(true, response.bodyAsJsonObject().getBoolean("success"));
618+
validateKeypair(keypairToDelete, "test", response.bodyAsJsonObject().getJsonObject("deleted_keypair"));
619+
verify(keypairStoreWriter, times(1)).upload(collectionOfSize(1), isNull());
620+
testContext.completeNow();
621+
});
622+
}
623+
624+
private static Stream<Arguments> deleteRoles() {
625+
return Stream.of(
626+
Arguments.of(Role.MAINTAINER, 401, false),
627+
Arguments.of(Role.PRIVILEGED, 200, true),
628+
Arguments.of(Role.SHARING_PORTAL, 200, true)
629+
);
630+
}
631+
632+
@ParameterizedTest
633+
@MethodSource("deleteRoles")
634+
void deleteKeypairAuthorization(Role role, int expectedStatus, boolean shouldSucceed, Vertx vertx, VertxTestContext testContext) throws Exception {
635+
fakeAuth(role);
636+
637+
Instant time = Instant.now();
638+
ClientSideKeypair keypairToDelete = new ClientSideKeypair("CC12345678", pub1, priv1, 222, "[email protected]", time, false, name1);
639+
ClientSideKeypair remainingKeypair = new ClientSideKeypair("DD12345678", pub2, priv2, 222, "[email protected]", time, false, name2);
640+
641+
setKeypairs(List.of(keypairToDelete, remainingKeypair));
642+
setSites(new Site(222, "test", true));
643+
644+
JsonObject jo = new JsonObject().put("subscription_id", "CC12345678");
645+
646+
post(vertx, testContext, "api/client_side_keypairs/delete", jo.encode(), response -> {
647+
assertEquals(expectedStatus, response.statusCode());
648+
649+
if (shouldSucceed) {
650+
assertTrue(response.bodyAsJsonObject().getBoolean("success"));
651+
validateKeypair(keypairToDelete, "test", response.bodyAsJsonObject().getJsonObject("deleted_keypair"));
652+
verify(keypairStoreWriter, times(1)).upload(collectionOfSize(1), isNull());
653+
} else {
654+
verify(keypairStoreWriter, times(0)).upload(any(), isNull());
655+
}
656+
testContext.completeNow();
657+
});
658+
}
559659
}
560660

561661

webroot/adm/client-side-keypair.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ <h3>Operations</h3>
3333
<li class="ro-sem" style="display: none"><a href="#" id="doListSubscription">Reveal Keypair By Subscription Id</a></li>
3434
<li class="ro-sem" style="display: none"><a href="#" id="doCreate">Create Keypair</a></li>
3535
<li class="ro-sem" style="display: none"><a href="#" id="doUpdate">Update Keypair</a></li>
36+
<li class="ro-sem" style="display: none"><a href="#" id="doDelete">Delete Keypair</a></li>
3637
</ul>
3738

3839
<br>
@@ -109,6 +110,20 @@ <h3>Output</h3>
109110
doApiCall('POST', '/api/client_side_keypairs/update', '#standardOutput', '#errorOutput', JSON.stringify(payload));
110111
});
111112

113+
$('#doDelete').on('click', function () {
114+
const subscriptionId = $('#subscriptionId').val();
115+
if (!subscriptionId) {
116+
$('#errorOutput').text("required parameters: subscription_id");
117+
return;
118+
}
119+
120+
if (!confirm(`Are you sure you want to delete ${subscriptionId}?`)) return;
121+
122+
const payload = {"subscription_id": subscriptionId};
123+
124+
doApiCall('POST', '/api/client_side_keypairs/delete', '#standardOutput', '#errorOutput', JSON.stringify(payload));
125+
});
126+
112127
});
113128

114129
function getUpdateKeypairConfirmationMessage(disabled, subscriptionId) {

0 commit comments

Comments
 (0)