diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientBuilder.java b/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientBuilder.java index 1b0a07dde..5a3e092c5 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientBuilder.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientBuilder.java @@ -147,6 +147,21 @@ public DatabaseClientBuilder withCloudAuth(String apiKey, String basePath) { .withBasePath(basePath); } + /** + * + * @param apiKey + * @param basePath + * @param tokenDuration length in minutes until the generated access token expires + * @return + * @since 6.3.0 + */ + public DatabaseClientBuilder withCloudAuth(String apiKey, String basePath, Integer tokenDuration) { + return withAuthType(AUTH_TYPE_MARKLOGIC_CLOUD) + .withCloudApiKey(apiKey) + .withBasePath(basePath) + .withCloudTokenDuration(tokenDuration != null ? tokenDuration.toString() : null); + } + public DatabaseClientBuilder withKerberosAuth(String principal) { return withAuthType(AUTH_TYPE_KERBEROS) .withKerberosPrincipal(principal); @@ -186,6 +201,16 @@ public DatabaseClientBuilder withCloudApiKey(String cloudApiKey) { return this; } + /** + * @param tokenDuration length in minutes until the generated access token expires + * @return + * @since 6.3.0 + */ + public DatabaseClientBuilder withCloudTokenDuration(String tokenDuration) { + props.put(PREFIX + "cloud.tokenDuration", tokenDuration); + return this; + } + public DatabaseClientBuilder withCertificateFile(String file) { props.put(PREFIX + "certificate.file", file); return this; diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientFactory.java b/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientFactory.java index 36fa384b4..6209590b3 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientFactory.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientFactory.java @@ -450,45 +450,89 @@ public SecurityContext withSSLContext(SSLContext context, X509TrustManager trust * @since 6.1.0 */ public static class MarkLogicCloudAuthContext extends AuthContext { - private String tokenEndpoint; - private String grantType; - private String apiKey; + private String tokenEndpoint; + private String grantType; + private String apiKey; + private Integer tokenDuration; - public MarkLogicCloudAuthContext(String apiKey) { - this(apiKey, "/token", "apikey"); - } + /** + * @param apiKey user's API key for accessing MarkLogic Cloud + */ + public MarkLogicCloudAuthContext(String apiKey) { + this(apiKey, null); + } - public MarkLogicCloudAuthContext(String apiKey, String tokenEndpoint, String grantType) { - this.apiKey = apiKey; - this.tokenEndpoint = tokenEndpoint; - this.grantType = grantType; - } + /** + * @param apiKey user's API key for accessing MarkLogic Cloud + * @param tokenDuration length in minutes until the generated access token expires + * @since 6.3.0 + */ + public MarkLogicCloudAuthContext(String apiKey, Integer tokenDuration) { + this(apiKey, "/token", "apikey", tokenDuration); + } - public String getTokenEndpoint() { - return tokenEndpoint; - } + /** + * Only intended to be used in the scenario that the token endpoint of "/token" and the grant type of "apikey" + * are not the intended values. + * + * @param apiKey user's API key for accessing MarkLogic Cloud + * @param tokenEndpoint for overriding the default token endpoint if necessary + * @param grantType for overriding the default grant type if necessary + */ + public MarkLogicCloudAuthContext(String apiKey, String tokenEndpoint, String grantType) { + this(apiKey, tokenEndpoint, grantType, null); + } - public String getGrantType() { - return grantType; - } + /** + * Only intended to be used in the scenario that the token endpoint of "/token" and the grant type of "apikey" + * are not the intended values. + * + * @param apiKey user's API key for accessing MarkLogic Cloud + * @param tokenEndpoint for overriding the default token endpoint if necessary + * @param grantType for overriding the default grant type if necessary + * @param tokenDuration length in minutes until the generated access token expires + * @since 6.3.0 + */ + public MarkLogicCloudAuthContext(String apiKey, String tokenEndpoint, String grantType, Integer tokenDuration) { + this.apiKey = apiKey; + this.tokenEndpoint = tokenEndpoint; + this.grantType = grantType; + this.tokenDuration = tokenDuration; + } - public String getApiKey() { - return apiKey; - } + public String getTokenEndpoint() { + return tokenEndpoint; + } - @Override - public MarkLogicCloudAuthContext withSSLContext(SSLContext context, X509TrustManager trustManager) { - this.sslContext = context; - this.trustManager = trustManager; - return this; - } + public String getGrantType() { + return grantType; + } - @Override - public MarkLogicCloudAuthContext withSSLHostnameVerifier(SSLHostnameVerifier verifier) { - this.sslVerifier = verifier; - return this; - } - } + public String getApiKey() { + return apiKey; + } + + /** + * @return + * @since 6.3.0 + */ + public Integer getTokenDuration() { + return tokenDuration; + } + + @Override + public MarkLogicCloudAuthContext withSSLContext(SSLContext context, X509TrustManager trustManager) { + this.sslContext = context; + this.trustManager = trustManager; + return this; + } + + @Override + public MarkLogicCloudAuthContext withSSLHostnameVerifier(SSLHostnameVerifier verifier) { + this.sslVerifier = verifier; + return this; + } + } public static class BasicAuthContext extends AuthContext { String user; diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/DatabaseClientPropertySource.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/DatabaseClientPropertySource.java index dd00bdb6c..787e3a708 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/DatabaseClientPropertySource.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/DatabaseClientPropertySource.java @@ -206,7 +206,17 @@ private DatabaseClientFactory.SecurityContext newDigestAuthContext() { } private DatabaseClientFactory.SecurityContext newCloudAuthContext() { - return new DatabaseClientFactory.MarkLogicCloudAuthContext(getRequiredStringValue("cloud.apiKey")); + String apiKey = getRequiredStringValue("cloud.apiKey"); + String val = getNullableStringValue("cloud.tokenDuration"); + Integer duration = null; + if (val != null) { + try { + duration = Integer.parseInt(val); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Cloud token duration must be numeric"); + } + } + return new DatabaseClientFactory.MarkLogicCloudAuthContext(apiKey, duration); } private DatabaseClientFactory.SecurityContext newCertificateAuthContext(SSLInputs sslInputs) { diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/MarkLogicCloudAuthenticationConfigurer.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/MarkLogicCloudAuthenticationConfigurer.java index fd8a84fbb..db7ce3849 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/MarkLogicCloudAuthenticationConfigurer.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/okhttp/MarkLogicCloudAuthenticationConfigurer.java @@ -109,18 +109,24 @@ private Response callTokenEndpoint() { protected HttpUrl buildTokenUrl() { // For the near future, it's guaranteed that https and 443 will be required for connecting to MarkLogic Cloud, // so providing the ability to customize this would be misleading. - return new HttpUrl.Builder() - .scheme("https") - .host(host) - .port(443) - .build() - .resolve(securityContext.getTokenEndpoint()).newBuilder().build(); + HttpUrl.Builder builder = new HttpUrl.Builder() + .scheme("https") + .host(host) + .port(443) + .build() + .resolve(securityContext.getTokenEndpoint()).newBuilder(); + + Integer duration = securityContext.getTokenDuration(); + return duration != null ? + builder.addQueryParameter("duration", duration.toString()).build() : + builder.build(); } protected FormBody newFormBody() { return new FormBody.Builder() - .add("grant_type", securityContext.getGrantType()) - .add("key", securityContext.getApiKey()).build(); + .add("grant_type", securityContext.getGrantType()) + .add("key", securityContext.getApiKey()) + .build(); } private String getAccessTokenFromResponse(Response response) { @@ -191,8 +197,8 @@ private synchronized void generateNewTokenIfNecessary(String currentToken) { private Request addTokenToRequest(Chain chain) { return chain.request().newBuilder() - .header("Authorization", "Bearer " + token) - .build(); + .header("Authorization", "Bearer " + token) + .build(); } } } diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/impl/DatabaseClientPropertySourceTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/impl/DatabaseClientPropertySourceTest.java index 58bd68ec9..fc05f7f4a 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/impl/DatabaseClientPropertySourceTest.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/impl/DatabaseClientPropertySourceTest.java @@ -11,6 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -96,6 +97,17 @@ void cloudAuthWithNoSslInputs() { "trust manager should be used"); } + @Test + void cloudWithNonNumericDuration() { + props.put(PREFIX + "authType", "cloud"); + props.put(PREFIX + "cloud.apiKey", "abc123"); + props.put(PREFIX + "basePath", "/my/path"); + props.put(PREFIX + "cloud.tokenDuration", "abc"); + + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> buildBean()); + assertEquals("Cloud token duration must be numeric", ex.getMessage()); + } + private DatabaseClientFactory.Bean buildBean() { DatabaseClientPropertySource source = new DatabaseClientPropertySource(propertyName -> props.get(propertyName)); return source.newClientBean(); diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/impl/okhttp/MarkLogicCloudAuthenticationConfigurerTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/impl/okhttp/MarkLogicCloudAuthenticationConfigurerTest.java index a590f7590..62ce7166c 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/impl/okhttp/MarkLogicCloudAuthenticationConfigurerTest.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/impl/okhttp/MarkLogicCloudAuthenticationConfigurerTest.java @@ -41,6 +41,30 @@ void buildTokenUrlWithCustomTokenPath() throws Exception { assertEquals("https://otherhost/customToken", tokenUrl.toString()); } + @Test + void buildTokenUrlWithDuration() throws Exception { + Integer duration = 10; + MarkLogicCloudAuthenticationConfigurer.DefaultTokenGenerator client = new MarkLogicCloudAuthenticationConfigurer.DefaultTokenGenerator("somehost", + new DatabaseClientFactory.MarkLogicCloudAuthContext("doesnt-matter", duration) + .withSSLContext(SSLContext.getDefault(), null) + ); + + HttpUrl tokenUrl = client.buildTokenUrl(); + assertEquals("https://somehost/token?duration=10", tokenUrl.toString()); + } + + @Test + void buildTokenUrlWithDurationAndCustomPath() throws Exception { + Integer duration = 10; + MarkLogicCloudAuthenticationConfigurer.DefaultTokenGenerator client = new MarkLogicCloudAuthenticationConfigurer.DefaultTokenGenerator("somehost", + new DatabaseClientFactory.MarkLogicCloudAuthContext("doesnt-matter", "/customToken", "doesnt-matter", duration) + .withSSLContext(SSLContext.getDefault(), null) + ); + + HttpUrl tokenUrl = client.buildTokenUrl(); + assertEquals("https://somehost/customToken?duration=10", tokenUrl.toString()); + } + @Test void newFormBody() { FormBody body = new MarkLogicCloudAuthenticationConfigurer.DefaultTokenGenerator("host-doesnt-matter", diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/DatabaseClientBuilderTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/DatabaseClientBuilderTest.java index 76b247680..1053000cd 100644 --- a/marklogic-client-api/src/test/java/com/marklogic/client/test/DatabaseClientBuilderTest.java +++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/DatabaseClientBuilderTest.java @@ -123,6 +123,16 @@ void cloudWithBasePath() { "trust manager should be used as well if the user doesn't provide their own"); } + @Test + void cloudWithDuration() { + bean = Common.newClientBuilder().withCloudAuth("abc123", "/my/path", 10).buildBean(); + DatabaseClientFactory.MarkLogicCloudAuthContext context = + (DatabaseClientFactory.MarkLogicCloudAuthContext) bean.getSecurityContext(); + assertEquals("abc123", context.getApiKey()); + assertEquals("/my/path", bean.getBasePath()); + assertEquals(10, context.getTokenDuration()); + } + @Test void cloudNoApiKey() { IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> Common.newClientBuilder()