From ee194f4a6d0f2abe7a778b0f09ad5b2d588abdb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Preu=C3=9F?= Date: Fri, 9 Sep 2022 11:30:04 +0200 Subject: [PATCH] Encode auth parameters into `authParams` property - Replace `authParamsMap` with `authentication` Fixes #95 --- .../src/main/asciidoc/authentication.adoc | 91 +++++++++++++++++++ .../src/main/asciidoc/pulsar.adoc | 34 +++++++ .../autoconfigure/AuthParameterUtils.java | 72 +++++++++++++++ .../autoconfigure/PulsarProperties.java | 37 +++++--- .../WellKnownAuthParameters.java | 82 +++++++++++++++++ .../AuthParameterUtilsTests.java | 65 +++++++++++++ .../PulsarAutoConfigurationTests.java | 17 ++++ spring-pulsar/build.gradle | 4 +- 8 files changed, 388 insertions(+), 14 deletions(-) create mode 100644 spring-pulsar-docs/src/main/asciidoc/authentication.adoc create mode 100644 spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/AuthParameterUtils.java create mode 100644 spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/WellKnownAuthParameters.java create mode 100644 spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/AuthParameterUtilsTests.java diff --git a/spring-pulsar-docs/src/main/asciidoc/authentication.adoc b/spring-pulsar-docs/src/main/asciidoc/authentication.adoc new file mode 100644 index 00000000..c05ca415 --- /dev/null +++ b/spring-pulsar-docs/src/main/asciidoc/authentication.adoc @@ -0,0 +1,91 @@ +[appendix] +[[appendix.authentication]] += Authentication + +[[Athenz]] +== Athenz +==== +[source, yaml] +---- +spring.pulsar.client.auth-plugin-class-name: org.apache.pulsar.client.impl.auth.AuthenticationAthenz +spring.pulsar.client.authentication.tenant-domain: ... +spring.pulsar.client.authentication.tenant-service: ... +spring.pulsar.client.authentication.provider-domain: ... +spring.pulsar.client.authentication.private-key: ... +spring.pulsar.client.authentication.private-key-path: ... +spring.pulsar.client.authentication.key-id: ... +spring.pulsar.client.authentication.auto-prefetch-enabled: ... +spring.pulsar.client.authentication.athenz-conf-path: ... +spring.pulsar.client.authentication.principal-header: ... +spring.pulsar.client.authentication.role-header: ... +spring.pulsar.client.authentication.zts-url: ... +---- +==== + +[[Basic]] +== Basic +==== +[source, yaml] +---- +spring.pulsar.client.auth-plugin-class-name: org.apache.pulsar.client.impl.auth.AuthenticationBasic +spring.pulsar.client.authentication.user-id: ... +spring.pulsar.client.authentication.password: ... +---- +==== + +[[KeyStoreTls]] +== KeyStoreTls +==== +[source, yaml] +---- +spring.pulsar.client.auth-plugin-class-name: org.apache.pulsar.client.impl.auth.AuthenticationKeyStoreTls +spring.pulsar.client.authentication.key-store-type: ... +spring.pulsar.client.authentication.key-store-path: ... +spring.pulsar.client.authentication.key-store-password: ... +---- +==== + +[[OAuth2]] +== OAuth2 +==== +[source, yaml] +---- +spring.pulsar.client.auth-plugin-class-name: ... +spring.pulsar.client.authentication.issuer-url: ... +spring.pulsar.client.authentication.private-key: ... +spring.pulsar.client.authentication.audience: ... +spring.pulsar.client.authentication.scope: ... +---- +==== + +[[Sasl]] +== Sasl +==== +[source, yaml] +---- +spring.pulsar.client.auth-plugin-class-name: org.apache.pulsar.client.impl.auth.AuthenticationSasl +spring.pulsar.client.authentication.sasl-jaas-client-section-name: ... +spring.pulsar.client.authentication.server-type: ... +---- +==== + +[[Tls]] +== Tls +==== +[source, yaml] +---- +spring.pulsar.client.auth-plugin-class-name: org.apache.pulsar.client.impl.auth.AuthenticationTls +spring.pulsar.client.authentication.tls-cert-file: ... +spring.pulsar.client.authentication.tls-key-file: ... +---- +==== + +[[Token]] +== Token +==== +[source, yaml] +---- +spring.pulsar.client.auth-plugin-class-name: org.apache.pulsar.client.impl.auth.AuthenticationToken +spring.pulsar.client.authentication.token: ... +---- +==== \ No newline at end of file diff --git a/spring-pulsar-docs/src/main/asciidoc/pulsar.adoc b/spring-pulsar-docs/src/main/asciidoc/pulsar.adoc index 5dbfb529..0476debe 100644 --- a/spring-pulsar-docs/src/main/asciidoc/pulsar.adoc +++ b/spring-pulsar-docs/src/main/asciidoc/pulsar.adoc @@ -19,6 +19,40 @@ This is done through a factory bean called `PulsarClientFactoryBean`, which take include::application-properties/pulsar-client.adoc[lines=3..-1] ==== +To connect against a Pulsar cluster requiring authentication, you need to set the `authPluginClassName` and parameters for your authentication scheme. +To set the authentication parameters you can either pass them as a json-encoded string or as a nested map. Please see the examples below for each option: + +==== +[source, yaml] +---- +# using json-encoded string +spring: + pulsar: + client: + auth-plugin-class-name: org.apache.pulsar.client.impl.auth.oauth2.AuthenticationOAuth2 + auth-params: "{\"privateKey\":\"file:///Users/some-key.json\",\"issuerUrl\":\"https://auth.server.cloud/", \"audience\":\"urn:sn:acme:dev:my-instance"}" +---- +==== + +==== +[source, yaml] +---- +# using nested parameter map +spring: + pulsar: + client: + auth-plugin-class-name: org.apache.pulsar.client.impl.auth.oauth2.AuthenticationOAuth2 + authentication.issuer-url: https://auth.server.cloud/ + authentication.private-key: file:///Users/some-key.json + authentication.audience: urn:sn:acme:dev:my-instance +---- +==== + +.[.underline]#Click ##here## to view how to configure the different **authentication schemes**#. +[%collapsible] +==== +include::authentication.adoc[lines=3..-1] +==== [[pulsar-producer]] ==== Pulsar Producer diff --git a/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/AuthParameterUtils.java b/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/AuthParameterUtils.java new file mode 100644 index 00000000..64eb0a22 --- /dev/null +++ b/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/AuthParameterUtils.java @@ -0,0 +1,72 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.pulsar.autoconfigure; + +import java.util.Map; +import java.util.TreeMap; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.apache.pulsar.common.util.ObjectMapperFactory; + +import org.springframework.util.CollectionUtils; + +/** + * Utility methods for Pulsar authentication parameters. + * + * @author Alexander Preuß + */ +final class AuthParameterUtils { + + private static final Pattern KEBAB_CASE_PATTERN = Pattern.compile("-(.)"); + + private AuthParameterUtils() { + + } + + private static String convertKebabCaseToCamelCase(String kebabString) { + return KEBAB_CASE_PATTERN.matcher(kebabString).replaceAll(mr -> mr.group(1).toUpperCase()); + } + + private static Map convertWellKnownLowerCaseKeysToCamelCase(Map params) { + return params.entrySet().stream().collect(Collectors + .toMap(entry -> WellKnownAuthParameters.getCamelCaseKey(entry.getKey()), Map.Entry::getValue)); + } + + private static Map convertKebabCaseKeysToCamelCase(Map params) { + return params.entrySet().stream() + .collect(Collectors.toMap(entry -> convertKebabCaseToCamelCase(entry.getKey()), Map.Entry::getValue)); + } + + static String maybeConvertToEncodedParamString(Map params) { + if (CollectionUtils.isEmpty(params)) { + return null; + } + // env vars are bound like this ISSUER_ID -> issuerid, have to be camel-cased to + // work + params = convertWellKnownLowerCaseKeysToCamelCase(params); + params = convertKebabCaseKeysToCamelCase(params); + params = new TreeMap<>(params); // sort keys for testing and readability + try { + return ObjectMapperFactory.create().writeValueAsString(params); + } + catch (Exception e) { + throw new RuntimeException("Could not convert parameters to encoded string", e); + } + } + +} diff --git a/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/PulsarProperties.java b/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/PulsarProperties.java index 58304eea..4aecd286 100644 --- a/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/PulsarProperties.java +++ b/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/PulsarProperties.java @@ -39,6 +39,8 @@ import org.apache.pulsar.common.schema.SchemaType; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.pulsar.listener.AckMode; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; /** * Configuration properties for Spring for Apache Pulsar. @@ -753,7 +755,7 @@ public class PulsarProperties { /** * Authentication parameter map of the client. */ - private Map authParamsMap; + private Map authentication; /** * Client operation timeout in milliseconds. @@ -972,12 +974,12 @@ public class PulsarProperties { this.authParams = authParams; } - public Map getAuthParamsMap() { - return this.authParamsMap; + public Map getAuthentication() { + return this.authentication; } - public void setAuthParamsMap(Map authParamsMap) { - this.authParamsMap = authParamsMap; + public void setAuthentication(Map authentication) { + this.authentication = authentication; } public long getOperationTimeoutMs() { @@ -1253,6 +1255,11 @@ public class PulsarProperties { } public Map buildProperties() { + if (StringUtils.hasText(this.getAuthParams()) && !CollectionUtils.isEmpty(this.getAuthentication())) { + throw new IllegalArgumentException( + "Cannot set both spring.pulsar.client.authParams and spring.pulsar.client.authentication.*"); + } + PulsarProperties.Properties properties = new Properties(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); @@ -1261,7 +1268,8 @@ public class PulsarProperties { map.from(this::getListenerName).to(properties.in("listenerName")); map.from(this::getAuthPluginClassName).to(properties.in("authPluginClassName")); map.from(this::getAuthParams).to(properties.in("authParams")); - map.from(this::getAuthParamsMap).to(properties.in("authParamMap")); + map.from(AuthParameterUtils.maybeConvertToEncodedParamString(this.getAuthentication())) + .to(properties.in("authParams")); map.from(this::getOperationTimeoutMs).to(properties.in("operationTimeoutMs")); map.from(this::getLookupTimeoutMs).to(properties.in("lookupTimeoutMs")); map.from(this::getNumIoThreads).to(properties.in("numIoThreads")); @@ -1353,7 +1361,7 @@ public class PulsarProperties { /** * Authentication parameter map of the client. */ - private Map authParamMap; + private Map authentication; /** * Path to the trusted TLS certificate file. @@ -1434,12 +1442,12 @@ public class PulsarProperties { this.authParams = authParams; } - public Map getAuthParamMap() { - return this.authParamMap; + public Map getAuthentication() { + return this.authentication; } - public void setAuthParamMap(Map authParamMap) { - this.authParamMap = authParamMap; + public void setAuthentication(Map authentication) { + this.authentication = authentication; } public String getTlsTrustCertsFilePath() { @@ -1523,6 +1531,10 @@ public class PulsarProperties { } public Map buildProperties() { + if (!StringUtils.hasText(this.getAuthParams()) && !CollectionUtils.isEmpty(this.getAuthentication())) { + throw new IllegalArgumentException( + "Cannot set both spring.pulsar.admin.authParams and spring.pulsar.admin.authentication.*"); + } PulsarProperties.Properties properties = new Properties(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); @@ -1530,7 +1542,8 @@ public class PulsarProperties { map.from(this::getServiceUrl).to(properties.in("serviceUrl")); map.from(this::getAuthPluginClassName).to(properties.in("authPluginClassName")); map.from(this::getAuthParams).to(properties.in("authParams")); - map.from(this::getAuthParamMap).to(properties.in("authParamMap")); + map.from(AuthParameterUtils.maybeConvertToEncodedParamString(this.getAuthentication())) + .to(properties.in("authParams")); map.from(this::getTlsTrustCertsFilePath).to(properties.in("tlsTrustCertsFilePath")); map.from(this::isTlsAllowInsecureConnection).to(properties.in("tlsAllowInsecureConnection")); map.from(this::isTlsHostnameVerificationEnable).to(properties.in("tlsHostnameVerificationEnable")); diff --git a/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/WellKnownAuthParameters.java b/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/WellKnownAuthParameters.java new file mode 100644 index 00000000..118e3e89 --- /dev/null +++ b/spring-pulsar-spring-boot-autoconfigure/src/main/java/org/springframework/pulsar/autoconfigure/WellKnownAuthParameters.java @@ -0,0 +1,82 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.pulsar.autoconfigure; + +import java.util.HashMap; +import java.util.Map; + +/** + * Utility class to map Pulsar auth parameters to well-known keys. + * + * @author Alexander Preuß + */ +public final class WellKnownAuthParameters { + + private static final Map lowerCaseToCamelCase = new HashMap<>(); + + static { + // Athenz + lowerCaseToCamelCase.put("tenantdomain", "tenantDomain"); + lowerCaseToCamelCase.put("tenantservice", "tenantService"); + lowerCaseToCamelCase.put("providerdomain", "providerDomain"); + lowerCaseToCamelCase.put("privatekey", "privateKey"); + lowerCaseToCamelCase.put("privatekeypath", "privateKeyPath"); + lowerCaseToCamelCase.put("keyid", "keyId"); + lowerCaseToCamelCase.put("autoprefetchenabled", "autoPrefetchEnabled"); + lowerCaseToCamelCase.put("athenzconfpath", "athenzConfPath"); + lowerCaseToCamelCase.put("principalheader", "principalHeader"); + lowerCaseToCamelCase.put("roleheader", "roleHeader"); + lowerCaseToCamelCase.put("ztsurl", "ztsUrl"); + // Basic + lowerCaseToCamelCase.put("userid", "userId"); + lowerCaseToCamelCase.put("password", "password"); + // KeyStoreTls + lowerCaseToCamelCase.put("keystoretype", "keyStoreType"); + lowerCaseToCamelCase.put("keystorepath", "keyStorePath"); + lowerCaseToCamelCase.put("keystorepassword", "keyStorePassword"); + // OAuth2 + lowerCaseToCamelCase.put("type", "type"); + lowerCaseToCamelCase.put("issuerurl", "issuerUrl"); + lowerCaseToCamelCase.put("privatekey", "privateKey"); + lowerCaseToCamelCase.put("audience", "audience"); + lowerCaseToCamelCase.put("scope", "scope"); + // Sasl + lowerCaseToCamelCase.put("sasljaasclientsectionname", "saslJaasClientSectionName"); + lowerCaseToCamelCase.put("servertype", "serverType"); + // Tls + lowerCaseToCamelCase.put("tlscertfile", "tlsCertFile"); + lowerCaseToCamelCase.put("tlskeyfile", "tlsKeyFile"); + // Token + lowerCaseToCamelCase.put("token", "token"); + } + + private WellKnownAuthParameters() { + + } + + /** + * Returns the camel-cased version a Pulsar auth parameter or the given key in case it + * is not part of the well-known ones. + * @param lowerCaseKey the lower-cased auth parameter + * @return the camel-cased auth parameter, or the lowerCaseKey if the parameter is not + * found. + */ + public static String getCamelCaseKey(String lowerCaseKey) { + return lowerCaseToCamelCase.getOrDefault(lowerCaseKey, lowerCaseKey); + } + +} diff --git a/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/AuthParameterUtilsTests.java b/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/AuthParameterUtilsTests.java new file mode 100644 index 00000000..5bbc3dc9 --- /dev/null +++ b/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/AuthParameterUtilsTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.pulsar.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import java.util.Collections; +import java.util.Map; +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Tests for {@link AuthParameterUtils}. + * + * @author Alexander Preuß + */ +public class AuthParameterUtilsTests { + + @ParameterizedTest(name = "{0}") + @MethodSource("encodedParamStringConversionProvider") + void encodedParamStringConversion(String testName, Map authParamsMap) { + String encodedAuthParamString = AuthParameterUtils.maybeConvertToEncodedParamString(authParamsMap); + if (authParamsMap == null || authParamsMap.isEmpty()) { + assertThat(encodedAuthParamString).isNull(); + } + else { + assertThat(encodedAuthParamString).isEqualTo("{\"audience\":\"urn:sn:pulsar:abc:xyz\"," + + "\"issuerUrl\":\"https://auth.server.cloud\",\"privateKey\":\"file://Users/xyz/key.json\"}"); + } + } + + private static Stream encodedParamStringConversionProvider() { + return Stream.of(arguments("null", null), arguments("empty", Collections.emptyMap()), + arguments("camelCase", + Map.of("issuerUrl", "https://auth.server.cloud", "privateKey", "file://Users/xyz/key.json", + "audience", "urn:sn:pulsar:abc:xyz")), + arguments("kebabCase", + Map.of("issuer-url", "https://auth.server.cloud", "private-key", "file://Users/xyz/key.json", + "audience", "urn:sn:pulsar:abc:xyz")), + arguments("lowerCase", + Map.of("issuerurl", "https://auth.server.cloud", "privatekey", "file://Users/xyz/key.json", + "audience", "urn:sn:pulsar:abc:xyz")), + arguments("mixed", Map.of("issuerurl", "https://auth.server.cloud", "private-key", + "file://Users/xyz/key.json", "audience", "urn:sn:pulsar:abc:xyz"))); + } + +} diff --git a/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/PulsarAutoConfigurationTests.java b/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/PulsarAutoConfigurationTests.java index eb761d37..064b7f9f 100644 --- a/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/PulsarAutoConfigurationTests.java +++ b/spring-pulsar-spring-boot-autoconfigure/src/test/java/org/springframework/pulsar/autoconfigure/PulsarAutoConfigurationTests.java @@ -201,6 +201,23 @@ class PulsarAutoConfigurationTests { InterceptorTestConfiguration.interceptorFoo))); } + @Nested + class ClientAutoConfigurationTests { + + @Test + void authParamMapConvertedToEncodedParamString() { + contextRunner.withPropertyValues( + "spring.pulsar.client.auth-plugin-class-name=org.apache.pulsar.client.impl.auth.AuthenticationBasic", + "spring.pulsar.client.authentication.userId=username", + "spring.pulsar.client.authentication.password=topsecret") + .run((context -> assertThat(context).hasNotFailed().getBean(PulsarClientConfiguration.class) + .extracting("configs", InstanceOfAssertFactories.map(String.class, Object.class)) + .doesNotContainKey("authParamMap").doesNotContainKey("userId").doesNotContainKey("password") + .containsEntry("authParams", "{\"password\":\"topsecret\",\"userId\":\"username\"}"))); + } + + } + @Nested class ProducerFactoryAutoConfigurationTests { diff --git a/spring-pulsar/build.gradle b/spring-pulsar/build.gradle index 6175179e..c7efe923 100644 --- a/spring-pulsar/build.gradle +++ b/spring-pulsar/build.gradle @@ -16,9 +16,9 @@ dependencies { api ('org.springframework.retry:spring-retry') { exclude group: 'org.springframework' } + implementation 'com.fasterxml.jackson.core:jackson-core' + implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'com.google.code.findbugs:jsr305' - optional 'com.fasterxml.jackson.core:jackson-core' - optional 'com.fasterxml.jackson.core:jackson-databind' optional 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8' optional 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' optional 'com.fasterxml.jackson.datatype:jackson-datatype-joda'