diff --git a/README.md b/README.md index 30be365..e4df35a 100644 --- a/README.md +++ b/README.md @@ -290,6 +290,7 @@ Disable Property: `org.springframework.cloud.bindings.boot.oauth2.enable` | `spring.security.oauth2.client.registration.{name}.provider` | `{provider}` | `spring.security.oauth2.client.registration.{name}.client-name` | `{client-name}` | `spring.security.oauth2.client.registration.{name}.client-authentication-method` | `{client-authentication-method}` +| `spring.security.oauth2.client.registration.{name}.authorization-grant-type` | `{authorization-grant-type}` or if not set then `{authorization-grant-types}` if it contains only one value (comma-separated) | `spring.security.oauth2.client.registration.{name}.redirect-uri` | `{redirect-uri}` | `spring.security.oauth2.client.registration.{name}.scope` | `{scope}` | `spring.security.oauth2.client.provider.{provider}.issuer-uri` | `{issuer-uri}` diff --git a/src/main/java/org/springframework/cloud/bindings/boot/MapMapper.java b/src/main/java/org/springframework/cloud/bindings/boot/MapMapper.java index d65fb77..4309490 100644 --- a/src/main/java/org/springframework/cloud/bindings/boot/MapMapper.java +++ b/src/main/java/org/springframework/cloud/bindings/boot/MapMapper.java @@ -19,6 +19,7 @@ package org.springframework.cloud.bindings.boot; import java.util.Arrays; import java.util.Map; import java.util.function.Function; +import java.util.function.Predicate; final class MapMapper { @@ -32,26 +33,49 @@ final class MapMapper { } Source from(String... keys) { - return new Source(keys); + return new SourceImpl(keys); } interface TriFunction { R apply(T t, U u, V v); } - final class Source { + interface Source { + void to(String key); + + void toIfAbsent(String key); + + void to(String key, Function function); + + void to(String key, TriFunction function); + + Source when(Predicate predicate); + + } + + final class SourceImpl implements Source { private final String[] keys; - private Source(String[] keys) { + private SourceImpl(String[] keys) { this.keys = keys; } - void to(String key) { + @Override + public void to(String key) { to(key, v -> v); } - void to(String key, Function function) { + @Override + public void toIfAbsent(String key) { + if (destination.containsKey(key)) { + return; + } + to(key, v -> v); + } + + @Override + public void to(String key, Function function) { if (keys.length != 1) { throw new IllegalStateException( String.format("source size %d cannot be transformed as one argument", keys.length)); @@ -64,7 +88,8 @@ final class MapMapper { destination.put(key, function.apply(source.get(keys[0]))); } - void to(String key, TriFunction function) { + @Override + public void to(String key, TriFunction function) { if (keys.length != 3) { throw new IllegalStateException( String.format("source size %d cannot be consumed as three arguments", keys.length)); @@ -77,6 +102,47 @@ final class MapMapper { destination.put(key, function.apply(source.get(keys[0]), source.get(keys[1]), source.get(keys[2]))); } + @Override + public Source when(Predicate predicate) { + if (keys.length != 1) { + throw new IllegalStateException( + String.format("source size %d cannot be transformed as one argument", keys.length)); + } + + if (predicate.test(source.get(keys[0]))) { + return this; + } else { + return new NoopSource(); + } + } + } + + final static class NoopSource implements Source { + + @Override + public void to(String key) { + + } + + @Override + public void toIfAbsent(String key) { + + } + + @Override + public void to(String key, Function function) { + + } + + @Override + public void to(String key, TriFunction function) { + + } + + @Override + public Source when(Predicate predicate) { + return this; + } } } diff --git a/src/main/java/org/springframework/cloud/bindings/boot/SpringSecurityOAuth2BindingsPropertiesProcessor.java b/src/main/java/org/springframework/cloud/bindings/boot/SpringSecurityOAuth2BindingsPropertiesProcessor.java index a804d74..28fca47 100644 --- a/src/main/java/org/springframework/cloud/bindings/boot/SpringSecurityOAuth2BindingsPropertiesProcessor.java +++ b/src/main/java/org/springframework/cloud/bindings/boot/SpringSecurityOAuth2BindingsPropertiesProcessor.java @@ -25,6 +25,8 @@ import org.springframework.core.env.Environment; import java.util.*; +import javax.annotation.Nullable; + import static org.springframework.cloud.bindings.boot.Guards.isTypeEnabled; /** @@ -58,6 +60,9 @@ public final class SpringSecurityOAuth2BindingsPropertiesProcessor implements Bi map.from("client-secret").to(String.format("spring.security.oauth2.client.registration.%s.client-secret", clientName)); map.from("client-authentication-method").to(String.format("spring.security.oauth2.client.registration.%s.client-authentication-method", clientName)); map.from("authorization-grant-type").to(String.format("spring.security.oauth2.client.registration.%s.authorization-grant-type", clientName)); + map.from("authorization-grant-types") + .when(SpringSecurityOAuth2BindingsPropertiesProcessor::hasSingleValue) + .toIfAbsent(String.format("spring.security.oauth2.client.registration.%s.authorization-grant-type", clientName)); map.from("redirect-uri").to(String.format("spring.security.oauth2.client.registration.%s.redirect-uri", clientName)); map.from("scope").to(String.format("spring.security.oauth2.client.registration.%s.scope", clientName)); map.from("client-name").to(String.format("spring.security.oauth2.client.registration.%s.client-name", clientName)); @@ -71,6 +76,15 @@ public final class SpringSecurityOAuth2BindingsPropertiesProcessor implements Bi }); } + private static boolean hasSingleValue(@Nullable Object value) { + return Optional.ofNullable(value) + .filter(String.class::isInstance) + .map(String.class::cast) + .map(s -> s.split(",")) + .filter(r -> r.length == 1) + .isPresent(); + } + @Override public void onApplicationEvent(ApplicationPreparedEvent event) { LOG.replayTo(getClass()); diff --git a/src/test/java/org/springframework/cloud/bindings/boot/MapMapperTest.java b/src/test/java/org/springframework/cloud/bindings/boot/MapMapperTest.java index b18ac18..3d18cf4 100644 --- a/src/test/java/org/springframework/cloud/bindings/boot/MapMapperTest.java +++ b/src/test/java/org/springframework/cloud/bindings/boot/MapMapperTest.java @@ -17,12 +17,14 @@ package org.springframework.cloud.bindings.boot; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import java.util.HashMap; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; @DisplayName("Map Mapper test") final class MapMapperTest { @@ -94,4 +96,99 @@ final class MapMapperTest { assertThat(destination).doesNotContainKey("test-destination-key"); } + @Nested + class ToIfAbsentTests { + @Test + @DisplayName("puts if absent in destination") + void absent() { + source.put("test-source-key-1", "test-source-value-1"); + + map.from("test-source-key-1").toIfAbsent("test-destination-key"); + + assertThat(destination).containsEntry("test-destination-key", "test-source-value-1"); + } + + @Test + @DisplayName("does not put if present in destination") + void present() { + source.put("test-source-key-1", "test-source-value-1"); + source.put("test-source-key-2", "test-source-value-2"); + + map.from("test-source-key-1").to("test-destination-key"); + map.from("test-source-key-2").toIfAbsent("test-destination-key"); + + assertThat(destination).containsEntry("test-destination-key", "test-source-value-1"); + } + + } + + @Nested + class WhenTests { + @Test + @DisplayName("puts when predicate is true") + void truePredicate() { + source.put("test-source-key-1", "test-source-value-1"); + + map.from("test-source-key-1").when(value -> true).to("test-destination-key"); + + assertThat(destination).containsEntry("test-destination-key", "test-source-value-1"); + } + + @Test + @DisplayName("does not put when predicate is false") + void falsePredicate() { + source.put("test-source-key-1", "test-source-value-1"); + + map.from("test-source-key-1").when(value -> false).to("test-destination-key"); + + assertThat(destination).doesNotContainKey("test-destination-key").doesNotContainValue("test-source-value-1"); + } + + @Test + @DisplayName("does not put when the key is missing") + void missingKey() { + source.put("test-source-key-1", "test-source-value-1"); + + map.from("missing-key").when(value -> true).to("test-destination-key"); + + assertThat(destination).doesNotContainKey("test-destination-key"); + } + + @Test + @DisplayName("only supports one key") + void onlySupportsOneKey() { + assertThatThrownBy(() -> map.from("one", "two", "three").when(value -> true)) + .isInstanceOf(IllegalStateException.class); + } + + @Test + @DisplayName("is complete noop when predicate is false") + void falsePredicateIsNoop() { + source.put("test-source-key-1", "test-source-value-1"); + source.put("test-source-key-2", "test-source-value-2"); + source.put("test-source-key-3", "test-source-value-3"); + + MapMapper.Source noopSource = map.from("test-source-key-1").when(value -> false); + noopSource.to("one"); + noopSource.toIfAbsent("two"); + noopSource.to("three", str -> "content"); + noopSource.to("four", (a, b, c) -> "content"); + + assertThat(destination).isEmpty(); + } + + @Test + @DisplayName("noopSource.when with false predicate is idempotent") + void isIdemPotentWhenPredicateIsFalse() { + source.put("test-source-key-1", "test-source-value-1"); + + MapMapper.Source noopSource = map.from("test-source-key-1").when(value -> false); + + assertThat(noopSource) + .isSameAs(noopSource.when(value -> true)) + .isSameAs(noopSource.when(value -> false)); + } + + } + } diff --git a/src/test/java/org/springframework/cloud/bindings/boot/SpringSecurityOAuth2BindingsPropertiesProcessorTest.java b/src/test/java/org/springframework/cloud/bindings/boot/SpringSecurityOAuth2BindingsPropertiesProcessorTest.java index 67c9ebb..176895b 100644 --- a/src/test/java/org/springframework/cloud/bindings/boot/SpringSecurityOAuth2BindingsPropertiesProcessorTest.java +++ b/src/test/java/org/springframework/cloud/bindings/boot/SpringSecurityOAuth2BindingsPropertiesProcessorTest.java @@ -123,6 +123,49 @@ final class SpringSecurityOAuth2BindingsPropertiesProcessorTest { ; } + @Test + @DisplayName("contributes a authorization-grant-type is there is only one in authorization-grant-types") + void testAuthorizationGrantTypesOneEntry() { + Bindings bindings = new Bindings(new Binding("binding-name", Paths.get("test-path"), + new FluentMap() + .withEntry(Binding.TYPE, TYPE) + .withEntry("provider", "some-provider") + .withEntry("authorization-grant-types", "authorization_code") + )); + new SpringSecurityOAuth2BindingsPropertiesProcessor().process(new MockEnvironment(), bindings, properties); + assertThat(properties) + .containsEntry("spring.security.oauth2.client.registration.binding-name.authorization-grant-type", "authorization_code"); + } + + @Test + @DisplayName("does not contributes a authorization-grant-type is there are multiple entries in authorization-grant-types") + void testAuthorizationGrantTypesMultipleEntries() { + Bindings bindings = new Bindings(new Binding("binding-name", Paths.get("test-path"), + new FluentMap() + .withEntry(Binding.TYPE, TYPE) + .withEntry("provider", "some-provider") + .withEntry("authorization-grant-types", "authorization_code,client_credentials") + )); + new SpringSecurityOAuth2BindingsPropertiesProcessor().process(new MockEnvironment(), bindings, properties); + assertThat(properties) + .doesNotContainKey("spring.security.oauth2.client.registration.binding-name.authorization-grant-type"); + } + + @Test + @DisplayName("uses the value from authorization-grant-type if authorization-grant-types is also present") + void testAuthorizationGrantTypeAndAuthorizationGrantTypes() { + Bindings bindings = new Bindings(new Binding("binding-name", Paths.get("test-path"), + new FluentMap() + .withEntry(Binding.TYPE, TYPE) + .withEntry("provider", "some-provider") + .withEntry("authorization-grant-types", "authorization_code,refresh_token") + .withEntry("authorization-grant-type", "client_credentials") + )); + new SpringSecurityOAuth2BindingsPropertiesProcessor().process(new MockEnvironment(), bindings, properties); + assertThat(properties) + .containsEntry("spring.security.oauth2.client.registration.binding-name.authorization-grant-type", "client_credentials"); + } + @Test @DisplayName("can be disabled") void disabled() {