diff --git a/messages/src/client_protos b/messages/src/client_protos index 9811ecde..e96147fb 160000 --- a/messages/src/client_protos +++ b/messages/src/client_protos @@ -1 +1 @@ -Subproject commit 9811ecde6ad721d65d79bdc3c658fa8bc58f3994 +Subproject commit e96147fb1863a84055d5fd64f44c0cf8ffe2e643 diff --git a/momento-sdk/src/intTest/java/momento/sdk/SimpleCacheControlPlaneTest.java b/momento-sdk/src/intTest/java/momento/sdk/SimpleCacheControlPlaneTest.java index 1a49b4f8..4fa3bb37 100644 --- a/momento-sdk/src/intTest/java/momento/sdk/SimpleCacheControlPlaneTest.java +++ b/momento-sdk/src/intTest/java/momento/sdk/SimpleCacheControlPlaneTest.java @@ -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; @@ -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"); diff --git a/momento-sdk/src/main/java/momento/sdk/ScsControlClient.java b/momento-sdk/src/main/java/momento/sdk/ScsControlClient.java index 16c6b854..0898e5c3 100644 --- a/momento-sdk/src/main/java/momento/sdk/ScsControlClient.java +++ b/momento-sdk/src/main/java/momento/sdk/ScsControlClient.java @@ -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. */ @@ -49,7 +63,43 @@ DeleteCacheResponse deleteCache(String cacheName) { ListCachesResponse listCaches(Optional 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 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); } @@ -63,9 +113,12 @@ private static _DeleteCacheRequest buildDeleteCacheRequest(String cacheName) { return _DeleteCacheRequest.newBuilder().setCacheName(cacheName).build(); } - private static _ListCachesRequest convert(Optional 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) { @@ -80,10 +133,40 @@ private static ListCachesResponse convert(_ListCachesResponse response) { return new ListCachesResponse(caches, nextPageToken); } + private static String nextToken(Optional 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 signingKeys = new ArrayList<>(); + for (_SigningKey signingKey : response.getSigningKeyList()) { + signingKeys.add(convert(signingKey, endpoint)); + } + Optional 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(); diff --git a/momento-sdk/src/main/java/momento/sdk/ScsDataClient.java b/momento-sdk/src/main/java/momento/sdk/ScsDataClient.java index 65c38e69..0dda2249 100644 --- a/momento-sdk/src/main/java/momento/sdk/ScsDataClient.java +++ b/momento-sdk/src/main/java/momento/sdk/ScsDataClient.java @@ -45,6 +45,7 @@ final class ScsDataClient implements Closeable { private final Optional tracer; private long itemDefaultTtlSeconds; private ScsDataGrpcStubsManager scsDataGrpcStubsManager; + private final String endpoint; ScsDataClient( String authToken, @@ -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) { diff --git a/momento-sdk/src/main/java/momento/sdk/SimpleCacheClient.java b/momento-sdk/src/main/java/momento/sdk/SimpleCacheClient.java index 2f964eda..4b326e80 100644 --- a/momento-sdk/src/main/java/momento/sdk/SimpleCacheClient.java +++ b/momento-sdk/src/main/java/momento/sdk/SimpleCacheClient.java @@ -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 { @@ -78,7 +81,7 @@ public DeleteCacheResponse deleteCache(String cacheName) { *
{@code
    * Optional nextPageToken = Optional.empty();
    * do {
-   *     ListCachesResponse response = simpleCacheClient.listCaches(nextToken);
+   *     ListCachesResponse response = simpleCacheClient.listCaches(nextPageToken);
    *
    *     // Your code here to use the response
    *
@@ -90,6 +93,58 @@ public ListCachesResponse listCaches(Optional 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.
+   *
+   * 
{@code
+   * Optional nextToken = Optional.empty();
+   * do {
+   *    ListSigningKeysResponse response = simpleCacheClient.listSigningKeys(nextToken);
+   *
+   *    // Your code here to use the response
+   *
+   *    nextToken = response.nextToken();
+   * } while (nextToken.isPresent());
+   * }
+ * + * @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 nextToken) { + return scsControlClient.listSigningKeys(nextToken, scsDataClient.getEndpoint()); + } + /** * Get the cache value stored for the given key. * diff --git a/momento-sdk/src/main/java/momento/sdk/ValidationUtils.java b/momento-sdk/src/main/java/momento/sdk/ValidationUtils.java index c57910ba..18f92b0f 100644 --- a/momento-sdk/src/main/java/momento/sdk/ValidationUtils.java +++ b/momento-sdk/src/main/java/momento/sdk/ValidationUtils.java @@ -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() {} @@ -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); + } + } } diff --git a/momento-sdk/src/main/java/momento/sdk/messages/CreateSigningKeyResponse.java b/momento-sdk/src/main/java/momento/sdk/messages/CreateSigningKeyResponse.java new file mode 100644 index 00000000..2799ca6c --- /dev/null +++ b/momento-sdk/src/main/java/momento/sdk/messages/CreateSigningKeyResponse.java @@ -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; +} diff --git a/momento-sdk/src/main/java/momento/sdk/messages/ListSigningKeysResponse.java b/momento-sdk/src/main/java/momento/sdk/messages/ListSigningKeysResponse.java new file mode 100644 index 00000000..c57484fa --- /dev/null +++ b/momento-sdk/src/main/java/momento/sdk/messages/ListSigningKeysResponse.java @@ -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 signingKeys; + private final Optional nextPageToken; + + public ListSigningKeysResponse(List signingKeys, Optional nextPageToken) { + this.signingKeys = signingKeys; + this.nextPageToken = nextPageToken; + } + + public List signingKeys() { + return signingKeys; + } + + /** + * Next Page Token returned by Simple Cache Service along with the list of signing keys. + * + *

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)} + * + *

When not present, there are no more signingKeys to return. + */ + public Optional nextPageToken() { + return nextPageToken; + } +} diff --git a/momento-sdk/src/main/java/momento/sdk/messages/RevokeSigningKeyResponse.java b/momento-sdk/src/main/java/momento/sdk/messages/RevokeSigningKeyResponse.java new file mode 100644 index 00000000..02b90e7c --- /dev/null +++ b/momento-sdk/src/main/java/momento/sdk/messages/RevokeSigningKeyResponse.java @@ -0,0 +1,3 @@ +package momento.sdk.messages; + +public final class RevokeSigningKeyResponse {} diff --git a/momento-sdk/src/main/java/momento/sdk/messages/SigningKey.java b/momento-sdk/src/main/java/momento/sdk/messages/SigningKey.java new file mode 100644 index 00000000..44715b9b --- /dev/null +++ b/momento-sdk/src/main/java/momento/sdk/messages/SigningKey.java @@ -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; + } +}