Skip to content

Commit

Permalink
feat: add support for singing keys workflow (#133)
Browse files Browse the repository at this point in the history
  • Loading branch information
tylerburdsall committed Apr 26, 2022
1 parent 5e38099 commit 2d4cff1
Show file tree
Hide file tree
Showing 10 changed files with 273 additions and 6 deletions.
2 changes: 1 addition & 1 deletion messages/src/client_protos
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
import momento.sdk.exceptions.InvalidArgumentException;
import momento.sdk.exceptions.NotFoundException;
import momento.sdk.messages.CacheInfo;
import momento.sdk.messages.CreateSigningKeyResponse;
import momento.sdk.messages.ListCachesResponse;
import momento.sdk.messages.ListSigningKeysResponse;
import momento.sdk.messages.SigningKey;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
Expand All @@ -37,6 +40,22 @@ void tearDown() {
target.close();
}

@Test
public void createListRevokeSigningKeyWorks() {
CreateSigningKeyResponse createSigningKeyResponse = target.createSigningKey(30);
ListSigningKeysResponse listSigningKeysResponse = target.listSigningKeys(null);
assertTrue(
listSigningKeysResponse.signingKeys().stream()
.map(SigningKey::getKeyId)
.anyMatch(keyId -> createSigningKeyResponse.getKeyId().equals(keyId)));
target.revokeSigningKey(createSigningKeyResponse.getKeyId());
listSigningKeysResponse = target.listSigningKeys(null);
assertFalse(
listSigningKeysResponse.signingKeys().stream()
.map(SigningKey::getKeyId)
.anyMatch(keyId -> createSigningKeyResponse.getKeyId().equals(keyId)));
}

@Test
public void throwsAlreadyExistsWhenCreatingExistingCache() {
String existingCache = System.getenv("TEST_CACHE_NAME");
Expand Down
91 changes: 87 additions & 4 deletions momento-sdk/src/main/java/momento/sdk/ScsControlClient.java
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
package momento.sdk;

import static momento.sdk.ValidationUtils.checkCacheNameValid;
import static momento.sdk.ValidationUtils.ensureValidTtlMinutes;

import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import grpc.control_client._Cache;
import grpc.control_client._CreateCacheRequest;
import grpc.control_client._CreateSigningKeyRequest;
import grpc.control_client._CreateSigningKeyResponse;
import grpc.control_client._DeleteCacheRequest;
import grpc.control_client._ListCachesRequest;
import grpc.control_client._ListCachesResponse;
import grpc.control_client._ListSigningKeysRequest;
import grpc.control_client._ListSigningKeysResponse;
import grpc.control_client._RevokeSigningKeyRequest;
import grpc.control_client._SigningKey;
import java.io.Closeable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import momento.sdk.exceptions.CacheServiceExceptionMapper;
import momento.sdk.messages.CacheInfo;
import momento.sdk.messages.CreateCacheResponse;
import momento.sdk.messages.CreateSigningKeyResponse;
import momento.sdk.messages.DeleteCacheResponse;
import momento.sdk.messages.ListCachesResponse;
import momento.sdk.messages.ListSigningKeysResponse;
import momento.sdk.messages.RevokeSigningKeyResponse;
import momento.sdk.messages.SigningKey;
import org.apache.commons.lang3.StringUtils;

/** Client for interacting with Scs Control Plane. */
Expand Down Expand Up @@ -49,7 +63,43 @@ DeleteCacheResponse deleteCache(String cacheName) {

ListCachesResponse listCaches(Optional<String> nextToken) {
try {
return convert(controlGrpcStubsManager.getBlockingStub().listCaches(convert(nextToken)));
_ListCachesRequest request =
_ListCachesRequest.newBuilder().setNextToken(nextToken(nextToken)).build();
return convert(controlGrpcStubsManager.getBlockingStub().listCaches(request));
} catch (Exception e) {
throw CacheServiceExceptionMapper.convert(e);
}
}

CreateSigningKeyResponse createSigningKey(int ttlMinutes, String endpoint) {
ensureValidTtlMinutes(ttlMinutes);
try {
return convert(
controlGrpcStubsManager
.getBlockingStub()
.createSigningKey(buildCreateSigningKeyRequest(ttlMinutes)),
endpoint);
} catch (Exception e) {
throw CacheServiceExceptionMapper.convert(e);
}
}

RevokeSigningKeyResponse revokeSigningKey(String keyId) {
try {
controlGrpcStubsManager
.getBlockingStub()
.revokeSigningKey(buildRevokeSigningKeyRequest(keyId));
return new RevokeSigningKeyResponse();
} catch (Exception e) {
throw CacheServiceExceptionMapper.convert(e);
}
}

ListSigningKeysResponse listSigningKeys(Optional<String> nextToken, String endpoint) {
try {
_ListSigningKeysRequest request =
_ListSigningKeysRequest.newBuilder().setNextToken(nextToken(nextToken)).build();
return convert(controlGrpcStubsManager.getBlockingStub().listSigningKeys(request), endpoint);
} catch (Exception e) {
throw CacheServiceExceptionMapper.convert(e);
}
Expand All @@ -63,9 +113,12 @@ private static _DeleteCacheRequest buildDeleteCacheRequest(String cacheName) {
return _DeleteCacheRequest.newBuilder().setCacheName(cacheName).build();
}

private static _ListCachesRequest convert(Optional<String> nextToken) {
String grpcNextToken = nextToken == null || !nextToken.isPresent() ? "" : nextToken.get();
return _ListCachesRequest.newBuilder().setNextToken(grpcNextToken).build();
private static _CreateSigningKeyRequest buildCreateSigningKeyRequest(int ttlMinutes) {
return _CreateSigningKeyRequest.newBuilder().setTtlMinutes(ttlMinutes).build();
}

private static _RevokeSigningKeyRequest buildRevokeSigningKeyRequest(String keyId) {
return _RevokeSigningKeyRequest.newBuilder().setKeyId(keyId).build();
}

private static ListCachesResponse convert(_ListCachesResponse response) {
Expand All @@ -80,10 +133,40 @@ private static ListCachesResponse convert(_ListCachesResponse response) {
return new ListCachesResponse(caches, nextPageToken);
}

private static String nextToken(Optional<String> nextToken) {
return nextToken == null || !nextToken.isPresent() ? "" : nextToken.get();
}

private static CacheInfo convert(_Cache cache) {
return new CacheInfo(cache.getCacheName());
}

private static ListSigningKeysResponse convert(
_ListSigningKeysResponse response, String endpoint) {
List<SigningKey> signingKeys = new ArrayList<>();
for (_SigningKey signingKey : response.getSigningKeyList()) {
signingKeys.add(convert(signingKey, endpoint));
}
Optional<String> nextPageToken =
StringUtils.isEmpty(response.getNextToken())
? Optional.empty()
: Optional.of(response.getNextToken());
return new ListSigningKeysResponse(signingKeys, nextPageToken);
}

private static SigningKey convert(_SigningKey signingKey, String endpoint) {
return new SigningKey(
signingKey.getKeyId(), new Date(signingKey.getExpiresAt() * 1000), endpoint);
}

private static CreateSigningKeyResponse convert(
_CreateSigningKeyResponse response, String endpoint) {
JsonObject jsonObject = JsonParser.parseString(response.getKey()).getAsJsonObject();
String keyId = jsonObject.get("kid").getAsString();
return new CreateSigningKeyResponse(
keyId, endpoint, response.getKey(), new Date(response.getExpiresAt() * 1000));
}

@Override
public void close() {
controlGrpcStubsManager.close();
Expand Down
6 changes: 6 additions & 0 deletions momento-sdk/src/main/java/momento/sdk/ScsDataClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ final class ScsDataClient implements Closeable {
private final Optional<Tracer> tracer;
private long itemDefaultTtlSeconds;
private ScsDataGrpcStubsManager scsDataGrpcStubsManager;
private final String endpoint;

ScsDataClient(
String authToken,
Expand All @@ -56,6 +57,11 @@ final class ScsDataClient implements Closeable {
this.itemDefaultTtlSeconds = defaultTtlSeconds;
this.scsDataGrpcStubsManager =
new ScsDataGrpcStubsManager(authToken, endpoint, openTelemetry, requestTimeout);
this.endpoint = endpoint;
}

public String getEndpoint() {
return endpoint;
}

CacheGetResponse get(String cacheName, String key) {
Expand Down
57 changes: 56 additions & 1 deletion momento-sdk/src/main/java/momento/sdk/SimpleCacheClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@
import momento.sdk.messages.CacheGetResponse;
import momento.sdk.messages.CacheSetResponse;
import momento.sdk.messages.CreateCacheResponse;
import momento.sdk.messages.CreateSigningKeyResponse;
import momento.sdk.messages.DeleteCacheResponse;
import momento.sdk.messages.ListCachesResponse;
import momento.sdk.messages.ListSigningKeysResponse;
import momento.sdk.messages.RevokeSigningKeyResponse;

/** Client to perform operations against the Simple Cache Service */
public final class SimpleCacheClient implements Closeable {
Expand Down Expand Up @@ -78,7 +81,7 @@ public DeleteCacheResponse deleteCache(String cacheName) {
* <pre>{@code
* Optional<String> nextPageToken = Optional.empty();
* do {
* ListCachesResponse response = simpleCacheClient.listCaches(nextToken);
* ListCachesResponse response = simpleCacheClient.listCaches(nextPageToken);
*
* // Your code here to use the response
*
Expand All @@ -90,6 +93,58 @@ public ListCachesResponse listCaches(Optional<String> nextToken) {
return scsControlClient.listCaches(nextToken);
}

/**
* Creates a new Momento signing key
*
* @param ttlMinutes The key's time-to-live in minutes
* @return The created key and its metadata
* @throws momento.sdk.exceptions.PermissionDeniedException
* @throws NotFoundException
* @throws momento.sdk.exceptions.InternalServerException
* @throws ClientSdkException if the {@code ttlMinutes} is invalid.
*/
public CreateSigningKeyResponse createSigningKey(int ttlMinutes) {
return scsControlClient.createSigningKey(ttlMinutes, scsDataClient.getEndpoint());
}

/**
* Revokes a Momento signing key, all tokens signed by which will be invalid
*
* @param keyId The id of the key to revoke
* @return
* @throws momento.sdk.exceptions.PermissionDeniedException
* @throws NotFoundException
* @throws momento.sdk.exceptions.InternalServerException
* @throws ClientSdkException if the {@code keyId} is null.
*/
public RevokeSigningKeyResponse revokeSigningKey(String keyId) {
return scsControlClient.revokeSigningKey(keyId);
}

/**
* Lists all Momento signing keys for the provided auth token.
*
* <pre>{@code
* Optional<String> nextToken = Optional.empty();
* do {
* ListSigningKeysResponse response = simpleCacheClient.listSigningKeys(nextToken);
*
* // Your code here to use the response
*
* nextToken = response.nextToken();
* } while (nextToken.isPresent());
* }</pre>
*
* @param nextToken Optional pagination token
* @return A list of Momento signing keys along with a pagination token (if present)
* @throws momento.sdk.exceptions.PermissionDeniedException
* @throws NotFoundException
* @throws momento.sdk.exceptions.InternalServerException
*/
public ListSigningKeysResponse listSigningKeys(Optional<String> nextToken) {
return scsControlClient.listSigningKeys(nextToken, scsDataClient.getEndpoint());
}

/**
* Get the cache value stored for the given key.
*
Expand Down
7 changes: 7 additions & 0 deletions momento-sdk/src/main/java/momento/sdk/ValidationUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ final class ValidationUtils {
static final String A_NON_NULL_KEY_IS_REQUIRED = "A non-null key is required.";
static final String A_NON_NULL_VALUE_IS_REQUIRED = "A non-null value is required.";
static final String CACHE_NAME_IS_REQUIRED = "Cache name is required.";
static final String SIGNING_KEY_TTL_CANNOT_BE_NEGATIVE = "Signing key TTL cannot be negative.";

ValidationUtils() {}

Expand Down Expand Up @@ -38,4 +39,10 @@ static void ensureValidTtl(long ttlSeconds) {
throw new InvalidArgumentException(CACHE_ITEM_TTL_CANNOT_BE_NEGATIVE);
}
}

static void ensureValidTtlMinutes(int ttlMinutes) {
if (ttlMinutes < 0) {
throw new InvalidArgumentException(SIGNING_KEY_TTL_CANNOT_BE_NEGATIVE);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package momento.sdk.messages;

import java.util.Date;

public class CreateSigningKeyResponse {
public CreateSigningKeyResponse(String keyId, String endpoint, String key, Date expiresAt) {
this.keyId = keyId;
this.endpoint = endpoint;
this.key = key;
this.expiresAt = expiresAt;
}

public String getKeyId() {
return keyId;
}

public String getEndpoint() {
return endpoint;
}

public String getKey() {
return key;
}

public Date getExpiresAt() {
return expiresAt;
}

private final String keyId;
private final String endpoint;
private final String key;
private final Date expiresAt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package momento.sdk.messages;

import java.util.List;
import java.util.Optional;
import momento.sdk.SimpleCacheClient;

/** Response object for list of signing keys. */
public final class ListSigningKeysResponse {

private final List<SigningKey> signingKeys;
private final Optional<String> nextPageToken;

public ListSigningKeysResponse(List<SigningKey> signingKeys, Optional<String> nextPageToken) {
this.signingKeys = signingKeys;
this.nextPageToken = nextPageToken;
}

public List<SigningKey> signingKeys() {
return signingKeys;
}

/**
* Next Page Token returned by Simple Cache Service along with the list of signing keys.
*
* <p>If nextPageToken().isPresent(), then this token must be provided in the next call to
* continue paginating through the list. This is done by setting the value in {@link
* SimpleCacheClient#listSigningKeys(Optional)}
*
* <p>When not present, there are no more signingKeys to return.
*/
public Optional<String> nextPageToken() {
return nextPageToken;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package momento.sdk.messages;

public final class RevokeSigningKeyResponse {}
27 changes: 27 additions & 0 deletions momento-sdk/src/main/java/momento/sdk/messages/SigningKey.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package momento.sdk.messages;

import java.util.Date;

public class SigningKey {
private final String keyId;
private final Date expiresAt;
private final String endpoint;

public SigningKey(String keyId, Date expiresAt, String endpoint) {
this.keyId = keyId;
this.expiresAt = expiresAt;
this.endpoint = endpoint;
}

public String getKeyId() {
return keyId;
}

public Date getExpiresAt() {
return expiresAt;
}

public String getEndpoint() {
return endpoint;
}
}

0 comments on commit 2d4cff1

Please sign in to comment.