diff --git a/crt/aws-c-auth b/crt/aws-c-auth index e0bd58d17..cd9d6afcd 160000 --- a/crt/aws-c-auth +++ b/crt/aws-c-auth @@ -1 +1 @@ -Subproject commit e0bd58d172cdc78d62eff5728437790d06fcce50 +Subproject commit cd9d6afcd42035d49bb2d0d3bef24b9faed57773 diff --git a/src/main/java/software/amazon/awssdk/crt/auth/credentials/CognitoCredentialsProvider.java b/src/main/java/software/amazon/awssdk/crt/auth/credentials/CognitoCredentialsProvider.java index 1df76a39a..8b9b17d90 100644 --- a/src/main/java/software/amazon/awssdk/crt/auth/credentials/CognitoCredentialsProvider.java +++ b/src/main/java/software/amazon/awssdk/crt/auth/credentials/CognitoCredentialsProvider.java @@ -9,6 +9,8 @@ import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; import software.amazon.awssdk.crt.http.HttpHeader; import software.amazon.awssdk.crt.http.HttpProxyOptions; @@ -47,6 +49,7 @@ static public class CognitoCredentialsProviderBuilder { private String identity; private String customRoleArn; private ArrayList logins = new ArrayList(); + private CognitoLoginTokenSource loginTokenSource; private TlsContext tlsContext; private ClientBootstrap clientBootstrap; @@ -148,6 +151,23 @@ public CognitoCredentialsProviderBuilder withHttpProxyOptions(HttpProxyOptions h HttpProxyOptions getHttpProxyOptions() { return httpProxyOptions; } + /** + * Sets a login token source for the credentials provider. The login token source will be used to + * gather additional login tokens to submit as part of the HTTP request sent to Cognito. A login token source + * allows you to dynamically add login tokens on a per-request basis. Using a login token source requires + * you to follow certain requirements in order to avoid undesirable behavior. See the documentation for + * `CognitoLoginTokenSource` for further details. + * + * @param loginTokenSource object to source login tokens from before every HTTP request to Cognito + * @return The current builder + */ + public CognitoCredentialsProviderBuilder withLoginTokenSource(CognitoLoginTokenSource loginTokenSource) { + this.loginTokenSource = loginTokenSource; + + return this; + } + + CognitoLoginTokenSource getLoginTokenSource() { return loginTokenSource; } /** * Creates a new Cognito credentials provider, based on this builder's configuration @@ -213,14 +233,15 @@ private CognitoCredentialsProvider(CognitoCredentialsProviderBuilder builder) { proxyTlsContextHandle, proxyAuthorizationType, proxyAuthorizationUsername != null ? proxyAuthorizationUsername.getBytes(UTF8) : null, - proxyAuthorizationPassword != null ? proxyAuthorizationPassword.getBytes(UTF8) : null); + proxyAuthorizationPassword != null ? proxyAuthorizationPassword.getBytes(UTF8) : null, + builder.loginTokenSource); acquireNativeHandle(nativeHandle); addReferenceTo(clientBootstrap); addReferenceTo(tlsContext); } - private void writeLengthPrefixedBytesSafe(ByteBuffer buffer, byte[] bytes) { + private static void writeLengthPrefixedBytesSafe(ByteBuffer buffer, byte[] bytes) { if (bytes != null) { buffer.putInt(bytes.length); buffer.put(bytes); @@ -229,7 +250,7 @@ private void writeLengthPrefixedBytesSafe(ByteBuffer buffer, byte[] bytes) { } } - private byte[] marshalLoginsForJni(ArrayList logins) { + private static byte[] marshalLoginsForJni(List logins) { int size = 0; for (CognitoLoginTokenPair login : logins) { @@ -256,6 +277,16 @@ private byte[] marshalLoginsForJni(ArrayList logins) { return buffer.array(); } + private static CompletableFuture> createChainedFuture(long invocationHandle, CompletableFuture> baseFuture) { + return baseFuture.whenComplete((token_pairs, ex) -> { + if (ex == null) { + completeLoginTokenFetch(invocationHandle, marshalLoginsForJni(token_pairs), null); + } else { + completeLoginTokenFetch(invocationHandle, null, ex); + } + }); + } + /******************************************************************************* * Native methods ******************************************************************************/ @@ -273,5 +304,8 @@ private static native long cognitoCredentialsProviderNew(CognitoCredentialsProvi long proxyTlsContext, int proxyAuthorizationType, byte[] proxyAuthorizationUsername, - byte[] proxyAuthorizationPassword); + byte[] proxyAuthorizationPassword, + CognitoLoginTokenSource loginTokenSource); + + private static native void completeLoginTokenFetch(long invocationHandle, byte[] marshalledLogins, Throwable ex); } diff --git a/src/main/java/software/amazon/awssdk/crt/auth/credentials/CognitoLoginTokenSource.java b/src/main/java/software/amazon/awssdk/crt/auth/credentials/CognitoLoginTokenSource.java new file mode 100644 index 000000000..9a41b474e --- /dev/null +++ b/src/main/java/software/amazon/awssdk/crt/auth/credentials/CognitoLoginTokenSource.java @@ -0,0 +1,46 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package software.amazon.awssdk.crt.auth.credentials; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * Interface to allow for dynamic sourcing (i.e. per fetch-credentials request submitted to Cognito) of Cognito login + * token pairs. It is *critical* to follow the guidance given in the documentation for `startLoginTokenFetch` + */ +public interface CognitoLoginTokenSource { + + /** + * Method that a Cognito credentials provider will invoke before sending a fetch credentials + * request to Cognito. The CognitoLoginTokenPairs that the future gets completed with are joined + * with the (static) CognitoLoginTokenPairs that were specified in the credential provider configuration + * on construction. The merged set of CognitoLoginTokenPairs are added to the HTTP request sent + * to Cognito that sources credentials. + * + * You must follow several guidelines to properly use this feature; not following these guidelines can result + * in deadlocks, poor performance, or other undesirable behavior. + * + * 1. If you use this feature, you must complete the future or the underlying connection attempt will hang forever. + * Credentials sourcing is halted until the future gets completed. If something goes wrong during + * login token sourcing, complete the future exceptionally. + * + * 2. You must not block or wait for asynchronous operations in this function. This function is invoked from a CRT + * event loop thread, and the event loop is halted until this function is returned from. If you need to perform + * an asynchronous or non-trivial operation in order to source the necessary login token pairs, then you must + * ensure that sourcing task executes on another thread. The easiest way to do this would be to pass the future + * to a sourcing task that runs on an external executor. + * + * 3. No attempt is made to de-duplicate login keys. If the final, unioned set of login token pairs contains + * multiple pairs with the same key, then which one of the duplicates gets used is not well-defined. For correct + * behavior, you must ensure there can be no duplicates. + * + * @param tokenFuture future to complete with dynamically sourced login token pairs in order to continue the + * credentials fetching process + */ + void startLoginTokenFetch(CompletableFuture> tokenFuture); + +} diff --git a/src/main/java/software/amazon/awssdk/crt/auth/credentials/CredentialsProvider.java b/src/main/java/software/amazon/awssdk/crt/auth/credentials/CredentialsProvider.java index a9fb0b97b..4f5917cc2 100644 --- a/src/main/java/software/amazon/awssdk/crt/auth/credentials/CredentialsProvider.java +++ b/src/main/java/software/amazon/awssdk/crt/auth/credentials/CredentialsProvider.java @@ -6,6 +6,7 @@ import java.util.concurrent.CompletableFuture; import software.amazon.awssdk.crt.CrtResource; +import software.amazon.awssdk.crt.CrtRuntimeException; import software.amazon.awssdk.crt.Log; /** @@ -41,11 +42,13 @@ public CompletableFuture getCredentials() { * @param future the future that the credentials should be applied to * @param credentials the fetched credentials, if successful */ - private void onGetCredentialsComplete(CompletableFuture future, Credentials credentials) { + private void onGetCredentialsComplete(CompletableFuture future, int errorCode, Credentials credentials) { if (credentials != null) { future.complete(credentials); + } else if (errorCode != 0) { + future.completeExceptionally(new CrtRuntimeException(errorCode)); } else { - future.completeExceptionally(new RuntimeException("Failed to get a valid set of credentials")); + future.completeExceptionally(new CrtRuntimeException("Failed to get a valid set of credentials")); } } diff --git a/src/main/resources/META-INF/native-image/software.amazon.awssdk/crt/aws-crt/jni-config.json b/src/main/resources/META-INF/native-image/software.amazon.awssdk/crt/aws-crt/jni-config.json index dccd899a9..4b6afe59d 100644 --- a/src/main/resources/META-INF/native-image/software.amazon.awssdk/crt/aws-crt/jni-config.json +++ b/src/main/resources/META-INF/native-image/software.amazon.awssdk/crt/aws-crt/jni-config.json @@ -172,6 +172,10 @@ { "name": "java.util.concurrent.CompletableFuture", "methods": [ + { + "name": "", + "parameterTypes": [] + }, { "name": "complete", "parameterTypes": [ @@ -295,6 +299,29 @@ } ] }, + { + "name": "software.amazon.awssdk.crt.auth.credentials.CognitoCredentialsProvider", + "methods": [ + { + "name": "createChainedFuture", + "parameterTypes": [ + "long", + "java.util.concurrent.CompletableFuture" + ] + } + ] + }, + { + "name": "software.amazon.awssdk.crt.auth.credentials.CognitoLoginTokenSource", + "methods": [ + { + "name": "startLoginTokenFetch", + "parameterTypes": [ + "java.util.concurrent.CompletableFuture" + ] + } + ] + }, { "name": "software.amazon.awssdk.crt.auth.credentials.Credentials", "fields": [ @@ -325,6 +352,7 @@ "name": "onGetCredentialsComplete", "parameterTypes": [ "java.util.concurrent.CompletableFuture", + "int", "software.amazon.awssdk.crt.auth.credentials.Credentials" ] }, diff --git a/src/native/credentials_provider.c b/src/native/credentials_provider.c index 98c142130..6d778e0ae 100644 --- a/src/native/credentials_provider.c +++ b/src/native/credentials_provider.c @@ -37,6 +37,14 @@ struct aws_credentials_provider_callback_data { jweak java_crt_credentials_provider; jobject jni_delegate_credential_handler; + + /** + * Right now, all provider bindings share the same basic binding setup, but some providers need some + * additional state specific to that provider. Rather than going a full vtable/base/wrapped solution, + * we just let such providers attach this data here. They are expected to clean it up before the clean up + * for this structure is called, hence the fatal assert below. + */ + void *aux_data; }; static void s_callback_data_clean_up( @@ -44,6 +52,9 @@ static void s_callback_data_clean_up( struct aws_allocator *allocator, struct aws_credentials_provider_callback_data *callback_data) { + // any provider-specific auxiliary data should have been already cleaned up + AWS_FATAL_ASSERT(callback_data->aux_data == NULL); + (*env)->DeleteWeakGlobalRef(env, callback_data->java_crt_credentials_provider); if (callback_data->jni_delegate_credential_handler != NULL) { (*env)->DeleteGlobalRef(env, callback_data->jni_delegate_credential_handler); @@ -648,6 +659,258 @@ static int s_fill_in_logins(struct aws_array_list *logins, struct aws_byte_curso return AWS_OP_SUCCESS; } +/* + * Optional auxiliary provider data used by the Cognito provider binding. Keeps a reference to the + * CognitoLoginTokenSource java object that should be used to query dynamic login tokens before each + * HTTP request to Cognito. + * + * We ref count this just to be safe; every in-progress credentials query (technically there should only be at most 1, + * but to be paranoid we ignore that) keeps a reference to this object (in addition to the reference kept by the + * generic provider binding via the aux_data field). + */ +struct aws_login_token_source_data { + struct aws_allocator *allocator; + struct aws_ref_count ref_count; + JavaVM *jvm; + jobject login_token_source; +}; + +static void s_aws_login_token_source_data_on_zero_ref(void *user_data) { + struct aws_login_token_source_data *login_token_source_data = user_data; + + AWS_FATAL_ASSERT(login_token_source_data != NULL); + JavaVM *jvm = login_token_source_data->jvm; + + /********** JNI ENV ACQUIRE **********/ + JNIEnv *env = aws_jni_acquire_thread_env(jvm); + if (env != NULL) { + if (login_token_source_data->login_token_source != NULL) { + (*env)->DeleteGlobalRef(env, login_token_source_data->login_token_source); + } + } + + aws_jni_release_thread_env(jvm, env); + /********** JNI ENV RELEASE **********/ + + aws_mem_release(login_token_source_data->allocator, login_token_source_data); +} + +static struct aws_login_token_source_data *s_aws_login_token_source_data_acquire( + struct aws_login_token_source_data *login_token_source_data) { + if (login_token_source_data != NULL) { + aws_ref_count_acquire(&login_token_source_data->ref_count); + } + + return login_token_source_data; +} + +static struct aws_login_token_source_data *s_aws_login_token_source_data_release( + struct aws_login_token_source_data *login_token_source_data) { + if (login_token_source_data != NULL) { + aws_ref_count_release(&login_token_source_data->ref_count); + } + + return NULL; +} + +static struct aws_login_token_source_data *s_aws_login_token_source_data_new( + struct aws_allocator *allocator, + JNIEnv *env, + jobject login_token_source) { + if (login_token_source == NULL) { + /* + * Not an error: if no login token source is provided, returning NULL causes us to skip dynamic token + * sourcing during credentials fetch + */ + return NULL; + } + + struct aws_login_token_source_data *login_token_source_data = + aws_mem_calloc(allocator, 1, sizeof(struct aws_login_token_source_data)); + login_token_source_data->allocator = allocator; + login_token_source_data->login_token_source = (*env)->NewGlobalRef(env, login_token_source); + aws_ref_count_init( + &login_token_source_data->ref_count, login_token_source_data, s_aws_login_token_source_data_on_zero_ref); + + jint jvmresult = (*env)->GetJavaVM(env, &login_token_source_data->jvm); + AWS_FATAL_ASSERT(jvmresult == 0); + + return login_token_source_data; +} + +static void s_on_cognito_shutdown_complete(void *user_data) { + struct aws_credentials_provider_callback_data *callback_data = user_data; + struct aws_login_token_source_data *login_token_source_data = callback_data->aux_data; + + callback_data->aux_data = s_aws_login_token_source_data_release(login_token_source_data); + + s_on_shutdown_complete(user_data); +} + +/* Per-credential-fetch binding data used when there is a cognito login token source configured for the provider */ +struct aws_login_token_source_invocation { + struct aws_allocator *allocator; + struct aws_login_token_source_data *login_token_source_data; /* strong reference */ + + /* + * This hidden/internal future is what transfers the login token pairs from the future completed by the + * user to the static callback that continues the cognito credential fetch. Must be referenced or it might + * be GCed before the dynamic login tokens are fetched. + */ + jobject chained_future; + + aws_credentials_provider_cognito_get_token_pairs_completion_fn *completion_callback; + void *completion_user_data; +}; + +static struct aws_login_token_source_invocation *aws_login_token_source_invocation_new( + struct aws_allocator *allocator, + struct aws_login_token_source_data *login_token_source_data, + aws_credentials_provider_cognito_get_token_pairs_completion_fn *completion_callback, + void *completion_user_data) { + + struct aws_login_token_source_invocation *invocation = + aws_mem_calloc(allocator, 1, sizeof(struct aws_login_token_source_invocation)); + invocation->allocator = allocator; + invocation->login_token_source_data = s_aws_login_token_source_data_acquire(login_token_source_data); + invocation->completion_callback = completion_callback; + invocation->completion_user_data = completion_user_data; + + return invocation; +} + +static void s_aws_login_token_source_invocation_destroy( + struct aws_login_token_source_invocation *invocation, + JNIEnv *env) { + if (invocation == NULL) { + return; + } + + invocation->login_token_source_data = s_aws_login_token_source_data_release(invocation->login_token_source_data); + + if (invocation->chained_future != NULL) { + (*env)->DeleteGlobalRef(env, invocation->chained_future); + } + + aws_mem_release(invocation->allocator, invocation); +} + +JNIEXPORT +void JNICALL Java_software_amazon_awssdk_crt_auth_credentials_CognitoCredentialsProvider_completeLoginTokenFetch( + JNIEnv *env, + jclass jni_class, + jlong invocation_handle, + jbyteArray marshalled_logins, + jobject ex) { + (void)jni_class; + + struct aws_login_token_source_invocation *invocation = + (struct aws_login_token_source_invocation *)invocation_handle; + + struct aws_byte_cursor logins_cursor; + AWS_ZERO_STRUCT(logins_cursor); + + struct aws_array_list logins; + aws_array_list_init_dynamic( + &logins, invocation->allocator, 0, sizeof(struct aws_cognito_identity_provider_token_pair)); + + size_t login_count = 0; + struct aws_cognito_identity_provider_token_pair *login_sequence = NULL; + + int error_code = AWS_ERROR_SUCCESS; + if (ex != NULL) { + error_code = AWS_AUTH_CREDENTIALS_PROVIDER_COGNITO_SOURCE_FAILURE; + } + + if (marshalled_logins != NULL) { + logins_cursor = aws_jni_byte_cursor_from_jbyteArray_acquire(env, marshalled_logins); + if (s_fill_in_logins(&logins, logins_cursor)) { + error_code = aws_last_error(); + } else { + login_sequence = logins.data; + login_count = aws_array_list_length(&logins); + } + } + + (*invocation->completion_callback)(login_sequence, login_count, error_code, invocation->completion_user_data); + + aws_jni_byte_cursor_from_jbyteArray_release(env, marshalled_logins, logins_cursor); + aws_array_list_clean_up(&logins); + + s_aws_login_token_source_invocation_destroy(invocation, env); +} + +static int s_cognito_get_token_pairs( + void *get_token_pairs_user_data, + aws_credentials_provider_cognito_get_token_pairs_completion_fn *completion_callback, + void *completion_user_data) { + + struct aws_login_token_source_data *login_token_source_data = get_token_pairs_user_data; + JavaVM *jvm = login_token_source_data->jvm; + + /********** JNI ENV ACQUIRE **********/ + JNIEnv *env = aws_jni_acquire_thread_env(jvm); + if (env == NULL) { + return aws_raise_error(AWS_ERROR_JAVA_CRT_JVM_DESTROYED); + } + + int result = AWS_OP_ERR; + struct aws_login_token_source_invocation *invocation = aws_login_token_source_invocation_new( + login_token_source_data->allocator, login_token_source_data, completion_callback, completion_user_data); + + // create the base future that the user must complete with login token pairs + jobject java_base_future = (*env)->NewObject( + env, + completable_future_properties.completable_future_class, + completable_future_properties.constructor_method_id); + if ((*env)->ExceptionCheck(env) || java_base_future == NULL) { + aws_jni_check_and_clear_exception(env); + aws_raise_error(AWS_AUTH_CREDENTIALS_PROVIDER_COGNITO_SOURCE_FAILURE); + goto done; + } + + // create the chained future that invokes the completion callback when the base future is completed either + // normally or exceptionally + jobject java_chained_future = (*env)->CallStaticObjectMethod( + env, + cognito_credentials_provider_properties.cognito_credentials_provider_class, + cognito_credentials_provider_properties.create_chained_future_method_id, + (jlong)invocation, + java_base_future); + if ((*env)->ExceptionCheck(env) || java_chained_future == NULL) { + aws_jni_check_and_clear_exception(env); + aws_raise_error(AWS_AUTH_CREDENTIALS_PROVIDER_COGNITO_SOURCE_FAILURE); + goto done; + } + + invocation->chained_future = (*env)->NewGlobalRef(env, java_chained_future); + + // invoke the login source java API with the base future + (*env)->CallVoidMethod( + env, + login_token_source_data->login_token_source, + cognito_login_token_source_properties.start_login_token_fetch_method_id, + java_base_future); + if ((*env)->ExceptionCheck(env)) { + aws_jni_check_and_clear_exception(env); + aws_raise_error(AWS_AUTH_CREDENTIALS_PROVIDER_COGNITO_SOURCE_FAILURE); + goto done; + } + + result = AWS_OP_SUCCESS; + +done: + + if (result != AWS_OP_SUCCESS) { + s_aws_login_token_source_invocation_destroy(invocation, env); + } + + aws_jni_release_thread_env(jvm, env); + /********** JNI ENV RELEASE **********/ + + return result; +} + JNIEXPORT jlong JNICALL Java_software_amazon_awssdk_crt_auth_credentials_CognitoCredentialsProvider_cognitoCredentialsProviderNew( JNIEnv *env, @@ -665,10 +928,12 @@ jlong JNICALL Java_software_amazon_awssdk_crt_auth_credentials_CognitoCredential jlong native_proxy_tls_context, jint proxy_authorization_type, jbyteArray proxy_authorization_username, - jbyteArray proxy_authorization_password) { + jbyteArray proxy_authorization_password, + jobject login_token_source) { (void)jni_class; (void)env; + aws_cache_jni_ids(env); struct aws_allocator *allocator = aws_jni_get_allocator(); @@ -705,10 +970,13 @@ jlong JNICALL Java_software_amazon_awssdk_crt_auth_credentials_CognitoCredential AWS_FATAL_ASSERT(jvmresult == 0); callback_data->java_crt_credentials_provider = (*env)->NewWeakGlobalRef(env, crt_credentials_provider); + /* If no login token source is provided, this evaluates to NULL */ + callback_data->aux_data = s_aws_login_token_source_data_new(allocator, env, login_token_source); + struct aws_credentials_provider_cognito_options options = { .shutdown_options = { - .shutdown_callback = s_on_shutdown_complete, + .shutdown_callback = s_on_cognito_shutdown_complete, .shutdown_user_data = callback_data, }, .endpoint = endpoint_cursor, @@ -717,6 +985,11 @@ jlong JNICALL Java_software_amazon_awssdk_crt_auth_credentials_CognitoCredential .tls_ctx = (void *)native_tls_context, }; + if (callback_data->aux_data != NULL) { + options.get_token_pairs = s_cognito_get_token_pairs; + options.get_token_pairs_user_data = callback_data->aux_data; + } + if (custom_role_arn != NULL) { custom_role_arn_cursor = aws_jni_byte_cursor_from_jstring_acquire(env, custom_role_arn); options.custom_role_arn = &custom_role_arn_cursor; @@ -839,6 +1112,7 @@ static void s_on_get_credentials_callback(struct aws_credentials *credentials, i callback_data->java_crt_credentials_provider, credentials_provider_properties.on_get_credentials_complete_method_id, callback_data->java_credentials_future, + error_code, java_credentials); AWS_FATAL_ASSERT(!aws_jni_check_and_clear_exception(env)); diff --git a/src/native/java_class_ids.c b/src/native/java_class_ids.c index 47966fbc0..ffa0041f0 100644 --- a/src/native/java_class_ids.c +++ b/src/native/java_class_ids.c @@ -281,7 +281,7 @@ static void s_cache_credentials_provider(JNIEnv *env) { env, provider_class, "onGetCredentialsComplete", - "(Ljava/util/concurrent/CompletableFuture;Lsoftware/amazon/awssdk/crt/auth/credentials/Credentials;)V"); + "(Ljava/util/concurrent/CompletableFuture;ILsoftware/amazon/awssdk/crt/auth/credentials/Credentials;)V"); AWS_FATAL_ASSERT(credentials_provider_properties.on_get_credentials_complete_method_id); } @@ -732,6 +732,10 @@ struct java_completable_future_properties completable_future_properties; static void s_cache_completable_future(JNIEnv *env) { jclass cls = (*env)->FindClass(env, "java/util/concurrent/CompletableFuture"); AWS_FATAL_ASSERT(cls); + completable_future_properties.completable_future_class = (*env)->NewGlobalRef(env, cls); + + completable_future_properties.constructor_method_id = (*env)->GetMethodID(env, cls, "", "()V"); + AWS_FATAL_ASSERT(completable_future_properties.constructor_method_id); completable_future_properties.complete_method_id = (*env)->GetMethodID(env, cls, "complete", "(Ljava/lang/Object;)Z"); @@ -2471,6 +2475,33 @@ static void s_cache_consumer_properties(JNIEnv *env) { AWS_FATAL_ASSERT(consumer_properties.accept_method_id); } +struct java_cognito_login_token_source_properties cognito_login_token_source_properties; + +static void s_cache_cognito_login_token_source(JNIEnv *env) { + jclass cls = (*env)->FindClass(env, "software/amazon/awssdk/crt/auth/credentials/CognitoLoginTokenSource"); + AWS_FATAL_ASSERT(cls); + cognito_login_token_source_properties.cognito_login_token_source_class = (*env)->NewGlobalRef(env, cls); + + cognito_login_token_source_properties.start_login_token_fetch_method_id = + (*env)->GetMethodID(env, cls, "startLoginTokenFetch", "(Ljava/util/concurrent/CompletableFuture;)V"); + AWS_FATAL_ASSERT(cognito_login_token_source_properties.start_login_token_fetch_method_id != NULL); +} + +struct java_cognito_credentials_provider_properties cognito_credentials_provider_properties; + +static void s_cache_cognito_credentials_provider(JNIEnv *env) { + jclass cls = (*env)->FindClass(env, "software/amazon/awssdk/crt/auth/credentials/CognitoCredentialsProvider"); + AWS_FATAL_ASSERT(cls); + cognito_credentials_provider_properties.cognito_credentials_provider_class = (*env)->NewGlobalRef(env, cls); + + cognito_credentials_provider_properties.create_chained_future_method_id = (*env)->GetStaticMethodID( + env, + cls, + "createChainedFuture", + "(JLjava/util/concurrent/CompletableFuture;)Ljava/util/concurrent/CompletableFuture;"); + AWS_FATAL_ASSERT(cognito_credentials_provider_properties.create_chained_future_method_id != NULL); +} + static void s_cache_java_class_ids(void *user_data) { JNIEnv *env = user_data; s_cache_http_request_body_stream(env); @@ -2584,6 +2615,8 @@ static void s_cache_java_class_ids(void *user_data) { s_cache_subscription_status_event_properties(env); s_cache_streaming_operation_options_properties(env); s_cache_consumer_properties(env); + s_cache_cognito_login_token_source(env); + s_cache_cognito_credentials_provider(env); } static aws_thread_once s_cache_once_init = AWS_THREAD_ONCE_STATIC_INIT; diff --git a/src/native/java_class_ids.h b/src/native/java_class_ids.h index 2398f113a..304c86fe1 100644 --- a/src/native/java_class_ids.h +++ b/src/native/java_class_ids.h @@ -331,6 +331,8 @@ extern struct java_s3_meta_request_response_handler_native_adapter_properties /* CompletableFuture */ struct java_completable_future_properties { + jclass completable_future_class; + jmethodID constructor_method_id; jmethodID complete_method_id; jmethodID complete_exceptionally_method_id; }; @@ -1037,6 +1039,22 @@ struct java_consumer_properties { }; extern struct java_consumer_properties consumer_properties; +/* CognitoLoginTokenSource */ +struct java_cognito_login_token_source_properties { + jclass cognito_login_token_source_class; + jmethodID start_login_token_fetch_method_id; +}; + +extern struct java_cognito_login_token_source_properties cognito_login_token_source_properties; + +/* CognitoCredentialsProvider */ +struct java_cognito_credentials_provider_properties { + jclass cognito_credentials_provider_class; + jmethodID create_chained_future_method_id; +}; + +extern struct java_cognito_credentials_provider_properties cognito_credentials_provider_properties; + /** * All functions bound to JNI MUST call this before doing anything else. * This caches all JNI IDs the first time it is called. Any further calls are no-op; it is thread-safe. diff --git a/src/test/java/software/amazon/awssdk/crt/test/CredentialsProviderTest.java b/src/test/java/software/amazon/awssdk/crt/test/CredentialsProviderTest.java index 16bbee1d2..0e6e7d82b 100644 --- a/src/test/java/software/amazon/awssdk/crt/test/CredentialsProviderTest.java +++ b/src/test/java/software/amazon/awssdk/crt/test/CredentialsProviderTest.java @@ -26,6 +26,7 @@ import java.util.concurrent.*; import static org.junit.Assert.*; +import static org.junit.Assert.fail; public class CredentialsProviderTest extends CrtTestFixture { static private String ACCESS_KEY_ID = "access_key_id"; @@ -307,8 +308,8 @@ public void testCreateDestroyProfile_MissingCreds() throws ExecutionException, I Throwable innerException = e.getCause(); // Check that the right exception type caused the completion error in the future - assertEquals("Failed to get a valid set of credentials", innerException.getMessage()); - assertEquals(RuntimeException.class, innerException.getClass()); + assertTrue(innerException.getMessage().contains("Parser encountered an error")); + assertEquals(CrtRuntimeException.class, innerException.getClass()); } } finally { Files.deleteIfExists(credsPath); @@ -372,10 +373,9 @@ public void testCreateDestroyEcs_MissingCreds() { } catch (CompletionException e) { assertNotNull(e.getCause()); Throwable innerException = e.getCause(); + assertEquals(CrtRuntimeException.class, innerException.getClass()); + // The way that this fails is different per platform, so we can't dig further at the moment. - // Check that the right exception type caused the completion error in the future - assertEquals("Failed to get a valid set of credentials", innerException.getMessage()); - assertEquals(RuntimeException.class, innerException.getClass()); } } } @@ -430,8 +430,8 @@ public void testCreateDestroySts_InvalidRole() { Throwable innerException = e.getCause(); // Check that the right exception type caused the completion error in the future - assertEquals("Failed to get a valid set of credentials", innerException.getMessage()); - assertEquals(RuntimeException.class, innerException.getClass()); + assertEquals("Unsuccessful status code returned from credentials-fetching http request", innerException.getMessage()); + assertEquals(CrtRuntimeException.class, innerException.getClass()); } } @@ -461,6 +461,165 @@ public void testGetCredentialsCognito() { } } + @Test + public void testGetCredentialsCognitoWithSyncDynamicLoginTokens() { + skipIfNetworkUnavailable(); + Assume.assumeTrue(isCIEnvironmentSetUp()); + + try (TlsContextOptions tlsContextOptions = TlsContextOptions.createDefaultClient(); + TlsContext tlsContext = new TlsContext(tlsContextOptions)) { + + CognitoCredentialsProvider.CognitoCredentialsProviderBuilder builder = new CognitoCredentialsProvider.CognitoCredentialsProviderBuilder(); + builder.withEndpoint(COGNITO_ENDPOINT); + builder.withIdentity(COGNITO_IDENTITY); + builder.withTlsContext(tlsContext); + builder.withLoginTokenSource(new CognitoLoginTokenSource() { + @Override + public void startLoginTokenFetch(CompletableFuture> tokenFuture) { + ArrayList tokens = new ArrayList<>(); + tokens.add(new CognitoCredentialsProvider.CognitoLoginTokenPair("token", "value")); + + tokenFuture.complete(tokens); + } + }); + + try (CognitoCredentialsProvider provider = builder.build()) { + CompletableFuture future = provider.getCredentials(); + future.get(); + + fail("Cognito credentials request with fake login tokens should have failed"); + } catch (ExecutionException ex) { + // passing in fake tokens causes Cognito to reject the request + assertTrue(ex.getCause().getMessage().contains("Unsuccessful status code returned from credentials-fetching http request")); + } catch (Exception ex) { + fail(ex.getMessage()); + } + } + } + + @Test + public void testGetCredentialsCognitoWithAsyncDynamicLoginTokens() { + skipIfNetworkUnavailable(); + Assume.assumeTrue(isCIEnvironmentSetUp()); + + try (TlsContextOptions tlsContextOptions = TlsContextOptions.createDefaultClient(); + TlsContext tlsContext = new TlsContext(tlsContextOptions)) { + + ExecutorService executor = Executors.newFixedThreadPool(1); + + CognitoCredentialsProvider.CognitoCredentialsProviderBuilder builder = new CognitoCredentialsProvider.CognitoCredentialsProviderBuilder(); + builder.withEndpoint(COGNITO_ENDPOINT); + builder.withIdentity(COGNITO_IDENTITY); + builder.withTlsContext(tlsContext); + builder.withLoginTokenSource(new CognitoLoginTokenSource() { + @Override + public void startLoginTokenFetch(CompletableFuture> tokenFuture) { + executor.submit(new Runnable() { + @Override + public void run() { + ArrayList tokens = new ArrayList<>(); + tokens.add(new CognitoCredentialsProvider.CognitoLoginTokenPair("token", "value")); + + tokenFuture.complete(tokens); + } + }); + } + }); + + try (CognitoCredentialsProvider provider = builder.build()) { + CompletableFuture future = provider.getCredentials(); + future.get(); + + fail("Cognito credentials request with fake login tokens should have failed"); + } catch (ExecutionException ex) { + // passing in fake tokens causes Cognito to reject the request + assertTrue(ex.getCause().getMessage().contains("Unsuccessful status code returned from credentials-fetching http request")); + } catch (Exception ex) { + fail(ex.getMessage()); + } + } + } + + @Test + public void testGetCredentialsCognitoWithEmptyDynamicLoginTokens() { + skipIfNetworkUnavailable(); + Assume.assumeTrue(isCIEnvironmentSetUp()); + + try (TlsContextOptions tlsContextOptions = TlsContextOptions.createDefaultClient(); + TlsContext tlsContext = new TlsContext(tlsContextOptions)) { + + ExecutorService executor = Executors.newFixedThreadPool(1); + + CognitoCredentialsProvider.CognitoCredentialsProviderBuilder builder = new CognitoCredentialsProvider.CognitoCredentialsProviderBuilder(); + builder.withEndpoint(COGNITO_ENDPOINT); + builder.withIdentity(COGNITO_IDENTITY); + builder.withTlsContext(tlsContext); + builder.withLoginTokenSource(new CognitoLoginTokenSource() { + @Override + public void startLoginTokenFetch(CompletableFuture> tokenFuture) { + executor.submit(new Runnable() { + @Override + public void run() { + ArrayList tokens = new ArrayList<>(); + + tokenFuture.complete(tokens); + } + }); + } + }); + + try (CognitoCredentialsProvider provider = builder.build()) { + CompletableFuture future = provider.getCredentials(); + Credentials credentials = future.get(); + + assertNotNull(credentials.getAccessKeyId()); + assertNotNull(credentials.getSecretAccessKey()); + assertNotNull(credentials.getSessionToken()); + } catch (Exception ex) { + fail(ex.getMessage()); + } + } + } + + @Test + public void testGetCredentialsCognitoWithDynamicLoginTokensException() { + skipIfNetworkUnavailable(); + Assume.assumeTrue(isCIEnvironmentSetUp()); + + try (TlsContextOptions tlsContextOptions = TlsContextOptions.createDefaultClient(); + TlsContext tlsContext = new TlsContext(tlsContextOptions)) { + + ExecutorService executor = Executors.newFixedThreadPool(1); + + CognitoCredentialsProvider.CognitoCredentialsProviderBuilder builder = new CognitoCredentialsProvider.CognitoCredentialsProviderBuilder(); + builder.withEndpoint(COGNITO_ENDPOINT); + builder.withIdentity(COGNITO_IDENTITY); + builder.withTlsContext(tlsContext); + builder.withLoginTokenSource(new CognitoLoginTokenSource() { + @Override + public void startLoginTokenFetch(CompletableFuture> tokenFuture) { + executor.submit(new Runnable() { + @Override + public void run() { + tokenFuture.completeExceptionally(new RuntimeException("Oh no")); + } + }); + } + }); + + try (CognitoCredentialsProvider provider = builder.build()) { + CompletableFuture future = provider.getCredentials(); + future.get(); + + fail("Cognito credentials request with failing login token source resolution should have failed"); + } catch (ExecutionException ex) { + assertTrue(ex.getCause().getMessage().contains("Valid credentials could not be sourced by the cognito provider")); + } catch (Exception ex) { + fail(ex.getMessage()); + } + } + } + @Test public void testGetCredentialsCognitoProxy() { skipIfNetworkUnavailable(); diff --git a/src/test/java/software/amazon/awssdk/crt/test/Mqtt5ClientTest.java b/src/test/java/software/amazon/awssdk/crt/test/Mqtt5ClientTest.java index 1fc36ea73..d167d85a2 100644 --- a/src/test/java/software/amazon/awssdk/crt/test/Mqtt5ClientTest.java +++ b/src/test/java/software/amazon/awssdk/crt/test/Mqtt5ClientTest.java @@ -1826,6 +1826,9 @@ public void Negotiated_Rejoin_Always() { events.stopFuture.get(); } + /* Avoid accidentally triggering re-connect throttle */ + Thread.sleep(2000); + builder.withSessionBehavior(ClientSessionBehavior.REJOIN_ALWAYS); LifecycleEvents_Futured rejoinEvents = new LifecycleEvents_Futured(); builder.withLifecycleEvents(rejoinEvents);