diff --git a/spring-vault-core/pom.xml b/spring-vault-core/pom.xml index a3721355..f5d52e53 100644 --- a/spring-vault-core/pom.xml +++ b/spring-vault-core/pom.xml @@ -194,6 +194,25 @@ + + com.google.cloud + google-cloud-iamcredentials + + + com.fasterxml.jackson.core + jackson-core + + + org.apache.httpcomponents + httpclient + + + commons-logging + commons-logging + + + true + com.google.auth diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/DefaultGcpCredentialsAccessors.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/DefaultGcpCredentialsAccessors.java new file mode 100644 index 00000000..83af9794 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/DefaultGcpCredentialsAccessors.java @@ -0,0 +1,50 @@ +/* + * Copyright 2021 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.vault.authentication; + +import org.springframework.util.Assert; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.auth.oauth2.ServiceAccountCredentials; + +/** + * Default implementation of {@link GcpCredentialsAccountIdAccessor}. Used by + * {@link GcpIamCredentialsAuthentication}. + * + * @author Andreas Gebauer + * @since 2.4 + * @see GcpIamCredentialsAuthentication + */ +enum DefaultGcpCredentialsAccessors implements GcpCredentialsAccountIdAccessor { + + INSTANCE; + + /** + * Get a the service account id (email) to be placed in the signed JWT. + * @param credentials credentials object to obtain the service account id from. + * @return the service account id to use. + */ + @Override + public String getServiceAccountId(GoogleCredentials credentials) { + + Assert.notNull(credentials, "GoogleCredentials must not be null"); + Assert.isInstanceOf(ServiceAccountCredentials.class, credentials, + "The configured GoogleCredentials does not represent a service account. Configure the service account id with GcpIamCredentialsAuthenticationOptionsBuilder#serviceAccountId(String)."); + + return ((ServiceAccountCredentials) credentials).getAccount(); + } + +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpCredentialsAccountIdAccessor.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpCredentialsAccountIdAccessor.java new file mode 100644 index 00000000..6fa974a9 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpCredentialsAccountIdAccessor.java @@ -0,0 +1,38 @@ +/* + * Copyright 2021 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.vault.authentication; + +import com.google.auth.oauth2.GoogleCredentials; + +/** + * Interface to obtain a service account id for GCP IAM credentials authentication. + * Implementations are used by {@link GcpIamCredentialsAuthentication}. + * + * @author Andreas Gebauer + * @since 2.4 + * @see GcpIamCredentialsAuthentication + */ +@FunctionalInterface +public interface GcpCredentialsAccountIdAccessor { + + /** + * Get a the service account id (email) to be placed in the signed JWT. + * @param credentials credential object to obtain the service account id from. + * @return the service account id to use. + */ + String getServiceAccountId(GoogleCredentials credentials); + +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpCredentialsSupplier.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpCredentialsSupplier.java new file mode 100644 index 00000000..bca78113 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpCredentialsSupplier.java @@ -0,0 +1,59 @@ +/* + * Copyright 2021 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.vault.authentication; + +import java.io.IOException; +import java.util.function.Supplier; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.auth.oauth2.ServiceAccountCredentials; + +/** + * Interface to obtain a {@link ServiceAccountCredentials} for GCP IAM credentials + * authentication. Implementations are used by {@link GcpIamCredentialsAuthentication}. + * + * @author Andreas Gebauer + * @since 2.4 + * @see GcpIamCredentialsAuthentication + */ +@FunctionalInterface +public interface GcpCredentialsSupplier extends Supplier { + + /** + * Exception-safe helper to get {@link ServiceAccountCredentials} from + * {@link #getCredentials}. + * @return the ServiceAccountCredentials for JWT signing. + */ + @Override + default GoogleCredentials get() { + + try { + return getCredentials(); + } + catch (IOException e) { + throw new IllegalStateException("Cannot obtain GoogleCredential", e); + } + } + + /** + * Get a {@link GoogleCredentials} for GCP IAM credentials authentication via JWT + * signing. + * @return the {@link GoogleCredentials}. + * @throws IOException if the credentials lookup fails. + */ + GoogleCredentials getCredentials() throws IOException; + +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpIamAuthentication.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpIamAuthentication.java index fe9dece4..307718a1 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpIamAuthentication.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpIamAuthentication.java @@ -64,7 +64,9 @@ import org.springframework.web.client.RestOperations; * @see GCP: * projects.serviceAccounts.signJwt + * @deprecated Use {@link GcpIamCredentialsAuthentication} instead. */ +@Deprecated public class GcpIamAuthentication extends GcpJwtAuthenticationSupport implements ClientAuthentication { private static final JsonFactory JSON_FACTORY = new JacksonFactory(); diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpIamCredentialsAuthentication.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpIamCredentialsAuthentication.java new file mode 100644 index 00000000..5dd19635 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpIamCredentialsAuthentication.java @@ -0,0 +1,159 @@ +/* + * Copyright 2021 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.vault.authentication; + +import java.io.IOException; +import java.time.Instant; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.util.Assert; +import org.springframework.vault.VaultException; +import org.springframework.vault.support.VaultToken; +import org.springframework.web.client.RestOperations; + +import com.google.api.client.http.HttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider; +import com.google.api.gax.rpc.TransportChannelProvider; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.cloud.iam.credentials.v1.IamCredentialsClient; +import com.google.cloud.iam.credentials.v1.IamCredentialsSettings; +import com.google.cloud.iam.credentials.v1.ServiceAccountName; +import com.google.cloud.iam.credentials.v1.SignJwtResponse; +import com.google.cloud.iam.credentials.v1.stub.IamCredentialsStubSettings; + +/** + * GCP IAM credentials login implementation using GCP IAM service accounts to legitimate + * its authenticity via JSON Web Token. + *

+ * This authentication method uses Googles IAM Credentials API to obtain a signed token + * for a specific {@link com.google.api.client.auth.oauth2.Credential}. Service account + * details are obtained from a {@link GoogleCredentials} that can be retrieved either from + * a JSON file or the runtime environment (GAE, GCE). + *

+ * {@link GcpIamCredentialsAuthentication} uses Google Java API that uses synchronous API. + * + * @author Andreas Gebauer + * @since 2.4 + * @see GcpIamCredentialsAuthenticationOptions + * @see HttpTransport + * @see GoogleCredentials + * @see GoogleCredentials#getApplicationDefault() + * @see RestOperations + * @see Auth Backend: gcp + * (IAM) + * @see GCP: + * projects.serviceAccounts.signJwt + */ +public class GcpIamCredentialsAuthentication extends GcpJwtAuthenticationSupport implements ClientAuthentication { + + private static final JsonFactory JSON_FACTORY = new JacksonFactory(); + + private final GcpIamCredentialsAuthenticationOptions options; + + private final TransportChannelProvider transportChannelProvider; + + private final GoogleCredentials credentials; + + /** + * Create a new instance of {@link GcpIamCredentialsAuthentication} given + * {@link GcpIamCredentialsAuthenticationOptions} and {@link RestOperations}. This + * constructor initializes {@link InstantiatingGrpcChannelProvider} for Google API + * usage. + * @param options must not be {@literal null}. + * @param restOperations HTTP client for for Vault login, must not be {@literal null}. + */ + public GcpIamCredentialsAuthentication(GcpIamCredentialsAuthenticationOptions options, + RestOperations restOperations) { + this(options, restOperations, IamCredentialsStubSettings.defaultGrpcTransportProviderBuilder().build()); + } + + /** + * Create a new instance of {@link GcpIamCredentialsAuthentication} given + * {@link GcpIamCredentialsAuthenticationOptions}, {@link RestOperations} and + * {@link TransportChannelProvider}. + * @param options must not be {@literal null}. + * @param restOperations HTTP client for for Vault login, must not be {@literal null}. + * @param transportChannelProvider Provider for transport channel Google API use, must + * not be {@literal null}. + */ + public GcpIamCredentialsAuthentication(GcpIamCredentialsAuthenticationOptions options, + RestOperations restOperations, TransportChannelProvider transportChannelProvider) { + + super(restOperations); + + Assert.notNull(options, "GcpAuthenticationOptions must not be null"); + Assert.notNull(restOperations, "RestOperations must not be null"); + Assert.notNull(transportChannelProvider, "TransportChannelProvider must not be null"); + + this.options = options; + this.transportChannelProvider = transportChannelProvider; + this.credentials = options.getCredentialSupplier().get(); + } + + @Override + public VaultToken login() throws VaultException { + + String signedJwt = signJwt(); + + return doLogin("GCP-IAM", signedJwt, this.options.getPath(), this.options.getRole()); + } + + protected String signJwt() { + + String serviceAccount = getServiceAccountId(); + Map jwtPayload = getJwtPayload(this.options, serviceAccount); + + try { + IamCredentialsSettings credentialsSettings = IamCredentialsSettings.newBuilder() + .setCredentialsProvider(() -> this.credentials) + .setTransportChannelProvider(this.transportChannelProvider).build(); + try (IamCredentialsClient iamCredentialsClient = IamCredentialsClient.create(credentialsSettings)) { + String payload = JSON_FACTORY.toString(jwtPayload); + ServiceAccountName serviceAccountName = ServiceAccountName.of("-", serviceAccount); + SignJwtResponse response = iamCredentialsClient.signJwt(serviceAccountName, Collections.emptyList(), + payload); + return response.getSignedJwt(); + } + } + catch (IOException e) { + throw new VaultLoginException("Cannot sign JWT", e); + } + } + + private String getServiceAccountId() { + return this.options.getServiceAccountIdAccessor().getServiceAccountId(this.credentials); + } + + private static Map getJwtPayload(GcpIamCredentialsAuthenticationOptions options, + String serviceAccount) { + + Instant validUntil = options.getClock().instant().plus(options.getJwtValidity()); + + Map payload = new LinkedHashMap<>(); + + payload.put("sub", serviceAccount); + payload.put("aud", "vault/" + options.getRole()); + payload.put("exp", validUntil.getEpochSecond()); + + return payload; + } + +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpIamCredentialsAuthenticationOptions.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpIamCredentialsAuthenticationOptions.java new file mode 100644 index 00000000..01bd3948 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpIamCredentialsAuthenticationOptions.java @@ -0,0 +1,290 @@ +/* + * Copyright 2021 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.vault.authentication; + +import java.time.Clock; +import java.time.Duration; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +import com.google.api.client.auth.oauth2.Credential; +import com.google.api.core.ApiClock; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.auth.oauth2.ServiceAccountCredentials; + +public class GcpIamCredentialsAuthenticationOptions { + + public static final String DEFAULT_GCP_AUTHENTICATION_PATH = "gcp"; + + /** + * Path of the gcp authentication backend mount. + */ + private final String path; + + private final GcpCredentialsSupplier credentialSupplier; + + /** + * Name of the role against which the login is being attempted. If role is not + * specified, the friendly name (i.e., role name or username) of the IAM principal + * authenticated. If a matching role is not found, login fails. + */ + private final String role; + + /** + * JWT validity/expiration. + */ + private final Duration jwtValidity; + + /** + * {@link ApiClock} to calculate JWT expiration. + */ + private final Clock clock; + + /** + * Provide the service account id to use as sub/iss claims. + */ + private final GcpCredentialsAccountIdAccessor serviceAccountIdAccessor; + + private GcpIamCredentialsAuthenticationOptions(String path, GcpCredentialsSupplier credentialSupplier, String role, + Duration jwtValidity, Clock clock, GcpCredentialsAccountIdAccessor serviceAccountIdSupplier) { + + this.path = path; + this.credentialSupplier = credentialSupplier; + this.role = role; + this.jwtValidity = jwtValidity; + this.clock = clock; + this.serviceAccountIdAccessor = serviceAccountIdSupplier; + } + + /** + * @return a new + * {@link GcpIamCredentialsAuthenticationOptions.GcpIamCredentialsAuthenticationOptionsBuilder}. + */ + public static GcpIamCredentialsAuthenticationOptions.GcpIamCredentialsAuthenticationOptionsBuilder builder() { + return new GcpIamCredentialsAuthenticationOptions.GcpIamCredentialsAuthenticationOptionsBuilder(); + } + + /** + * @return the path of the gcp authentication backend mount. + */ + public String getPath() { + return this.path; + } + + /** + * @return the gcp {@link Credential} supplier. + */ + public GcpCredentialsSupplier getCredentialSupplier() { + return this.credentialSupplier; + } + + /** + * @return name of the role against which the login is being attempted. + */ + public String getRole() { + return this.role; + } + + /** + * @return {@link Duration} of the JWT to generate. + */ + public Duration getJwtValidity() { + return this.jwtValidity; + } + + /** + * @return {@link Clock} used to calculate epoch seconds until the JWT expires. + */ + public Clock getClock() { + return this.clock; + } + + /** + * @return the service account id to use as sub/iss claims. + * @since 2.1 + */ + public GcpCredentialsAccountIdAccessor getServiceAccountIdAccessor() { + return this.serviceAccountIdAccessor; + } + + /** + * Builder for {@link GcpIamCredentialsAuthenticationOptions}. + */ + public static class GcpIamCredentialsAuthenticationOptionsBuilder { + + private String path = DEFAULT_GCP_AUTHENTICATION_PATH; + + @Nullable + private String role; + + @Nullable + private GcpCredentialsSupplier credentialsSupplier; + + private Duration jwtValidity = Duration.ofMinutes(15); + + private Clock clock = Clock.systemDefaultZone(); + + private GcpCredentialsAccountIdAccessor serviceAccountIdAccessor = DefaultGcpCredentialsAccessors.INSTANCE; + + GcpIamCredentialsAuthenticationOptionsBuilder() { + } + + /** + * Configure the mount path, defaults to {@literal aws}. + * @param path must not be empty or {@literal null}. + * @return {@code this} + * {@link GcpIamCredentialsAuthenticationOptions.GcpIamCredentialsAuthenticationOptionsBuilder}. + */ + public GcpIamCredentialsAuthenticationOptions.GcpIamCredentialsAuthenticationOptionsBuilder path(String path) { + + Assert.hasText(path, "Path must not be empty"); + + this.path = path; + return this; + } + + /** + * Configure static Google credentials, required to create a signed JWT. Either + * use static credentials or provide a + * {@link #credentialsSupplier(GcpCredentialsSupplier) credentials provider}. + * @param credentials must not be {@literal null}. + * @return {@code this} + * {@link GcpIamCredentialsAuthenticationOptions.GcpIamCredentialsAuthenticationOptionsBuilder}. + * @see #credentialsSupplier(GcpCredentialsSupplier) + */ + public GcpIamCredentialsAuthenticationOptions.GcpIamCredentialsAuthenticationOptionsBuilder credentials( + GoogleCredentials credentials) { + + Assert.notNull(credentials, "ServiceAccountCredentials must not be null"); + + return credentialsSupplier(() -> credentials); + } + + /** + * Configure a {@link GcpCredentialsSupplier}, required to create a signed JWT. + * Alternatively, configure static {@link #credentials(GoogleCredentials) + * credentials}. + * @param credentialsSupplier must not be {@literal null}. + * @return {@code this} + * {@link GcpIamCredentialsAuthenticationOptions.GcpIamCredentialsAuthenticationOptionsBuilder}. + * @see #credentials(GoogleCredentials) + */ + public GcpIamCredentialsAuthenticationOptions.GcpIamCredentialsAuthenticationOptionsBuilder credentialsSupplier( + GcpCredentialsSupplier credentialsSupplier) { + + Assert.notNull(credentialsSupplier, "GcpServiceAccountCredentialsSupplier must not be null"); + + this.credentialsSupplier = credentialsSupplier; + return this; + } + + /** + * Configure an explicit service account id to use in GCP IAM calls. If none is + * configured, falls back to using {@link ServiceAccountCredentials#getAccount()}. + * @param serviceAccountId the service account id (email) to use + * @return {@code this} + * {@link GcpIamCredentialsAuthenticationOptions.GcpIamCredentialsAuthenticationOptionsBuilder}. + * @since 2.1 + */ + public GcpIamCredentialsAuthenticationOptions.GcpIamCredentialsAuthenticationOptionsBuilder serviceAccountId( + String serviceAccountId) { + + Assert.notNull(serviceAccountId, "Service account id may not be null"); + + return serviceAccountIdAccessor((GoogleCredentials credentials) -> serviceAccountId); + } + + /** + * Configure an {@link GcpCredentialsAccountIdAccessor} to obtain the service + * account id used in GCP IAM calls. If none is configured, falls back to using + * {@link ServiceAccountCredentials#getAccount()}. + * @param serviceAccountIdAccessor the service account id provider to use + * @return {@code this} + * {@link GcpIamCredentialsAuthenticationOptions.GcpIamCredentialsAuthenticationOptionsBuilder}. + * @see GcpCredentialsAccountIdAccessor + * @since 2.1 + */ + GcpIamCredentialsAuthenticationOptions.GcpIamCredentialsAuthenticationOptionsBuilder serviceAccountIdAccessor( + GcpCredentialsAccountIdAccessor serviceAccountIdAccessor) { + + Assert.notNull(serviceAccountIdAccessor, "GcpServiceAccountIdAccessor must not be null"); + + this.serviceAccountIdAccessor = serviceAccountIdAccessor; + return this; + } + + /** + * Configure the name of the role against which the login is being attempted. + * @param role must not be empty or {@literal null}. + * @return {@code this} + * {@link GcpIamCredentialsAuthenticationOptions.GcpIamCredentialsAuthenticationOptionsBuilder}. + */ + public GcpIamCredentialsAuthenticationOptions.GcpIamCredentialsAuthenticationOptionsBuilder role(String role) { + + Assert.hasText(role, "Role must not be null or empty"); + + this.role = role; + return this; + } + + /** + * Configure the {@link Duration} for the JWT expiration. This defaults to 15 + * minutes and cannot be more than a hour. + * @param jwtValidity must not be {@literal null}. + * @return {@code this} + * {@link GcpIamCredentialsAuthenticationOptions.GcpIamCredentialsAuthenticationOptionsBuilder}. + */ + public GcpIamCredentialsAuthenticationOptions.GcpIamCredentialsAuthenticationOptionsBuilder jwtValidity( + Duration jwtValidity) { + + Assert.hasText(this.role, "JWT validity duration must not be null"); + + this.jwtValidity = jwtValidity; + return this; + } + + /** + * Configure the {@link Clock} used to calculate epoch seconds until the JWT + * expiration. + * @param clock must not be {@literal null}. + * @return {@code this} + * {@link GcpIamCredentialsAuthenticationOptions.GcpIamCredentialsAuthenticationOptionsBuilder}. + */ + public GcpIamCredentialsAuthenticationOptions.GcpIamCredentialsAuthenticationOptionsBuilder clock(Clock clock) { + + Assert.hasText(this.role, "Clock must not be null"); + + this.clock = clock; + return this; + } + + /** + * Build a new {@link GcpIamCredentialsAuthenticationOptions} instance. + * @return a new {@link GcpIamCredentialsAuthenticationOptions}. + */ + public GcpIamCredentialsAuthenticationOptions build() { + + Assert.notNull(this.credentialsSupplier, "GcpServiceAccountCredentialsSupplier must not be null"); + Assert.notNull(this.role, "Role must not be null"); + + return new GcpIamCredentialsAuthenticationOptions(this.path, this.credentialsSupplier, this.role, + this.jwtValidity, this.clock, this.serviceAccountIdAccessor); + } + + } + +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/GcpIamCredentialsAuthenticationOptionsBuilderUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/GcpIamCredentialsAuthenticationOptionsBuilderUnitTests.java new file mode 100644 index 00000000..d618b1e4 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/GcpIamCredentialsAuthenticationOptionsBuilderUnitTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2021 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.vault.authentication; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import java.security.PrivateKey; +import java.util.Date; + +import org.junit.jupiter.api.Test; + +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.auth.oauth2.ServiceAccountCredentials; + +/** + * Unit tests for {@link GcpIamCredentialsAuthenticationOptions}. + * + * @author Andreas Gebauer + */ +class GcpIamCredentialsAuthenticationOptionsBuilderUnitTests { + + @Test + void shouldDefaultToCredentialServiceAccountId() { + + ServiceAccountCredentials credentials = createServiceAccountCredentials(); + + GcpIamCredentialsAuthenticationOptions options = GcpIamCredentialsAuthenticationOptions.builder() + .credentials(credentials).role("foo").build(); + + assertThat(options.getServiceAccountIdAccessor().getServiceAccountId(credentials)).isEqualTo("hello@world"); + } + + @Test + void shouldAllowServiceAccountIdOverride() { + + ServiceAccountCredentials credential = createServiceAccountCredentials(); + + GcpIamCredentialsAuthenticationOptions options = GcpIamCredentialsAuthenticationOptions.builder() + .credentials(credential).serviceAccountId("override@foo.com").role("foo").build(); + + assertThat(options.getServiceAccountIdAccessor().getServiceAccountId(credential)).isEqualTo("override@foo.com"); + } + + @Test + void shouldAllowServiceAccountIdProviderOverride() { + + ServiceAccountCredentials credential = createServiceAccountCredentials(); + + GcpIamCredentialsAuthenticationOptions options = GcpIamCredentialsAuthenticationOptions.builder() + .credentials(credential) + .serviceAccountIdAccessor((GoogleCredentials googleCredential) -> "override@foo.com").role("foo") + .build(); + + assertThat(options.getServiceAccountIdAccessor().getServiceAccountId(credential)).isEqualTo("override@foo.com"); + } + + private static ServiceAccountCredentials createServiceAccountCredentials() { + return (ServiceAccountCredentials) ServiceAccountCredentials.newBuilder().setClientEmail("hello@world") + .setProjectId("project-id").setPrivateKey(mock(PrivateKey.class)).setPrivateKeyId("key-id") + .setAccessToken(new AccessToken("foobar", new Date())).build(); + } + +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/GcpIamCredentialsAuthenticationUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/GcpIamCredentialsAuthenticationUnitTests.java new file mode 100644 index 00000000..e81b3e34 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/GcpIamCredentialsAuthenticationUnitTests.java @@ -0,0 +1,158 @@ +/* + * Copyright 2021 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.vault.authentication; + +import static io.grpc.stub.ServerCalls.asyncUnaryCall; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.jsonPath; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +import java.io.IOException; +import java.security.PrivateKey; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.vault.client.VaultClients.PrefixAwareUriTemplateHandler; +import org.springframework.vault.support.VaultToken; +import org.springframework.web.client.RestTemplate; + +import com.google.api.gax.grpc.GrpcTransportChannel; +import com.google.api.gax.rpc.FixedTransportChannelProvider; +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.ServiceAccountCredentials; +import com.google.cloud.iam.credentials.v1.SignJwtRequest; +import com.google.cloud.iam.credentials.v1.SignJwtResponse; + +import io.grpc.ManagedChannel; +import io.grpc.MethodDescriptor; +import io.grpc.Server; +import io.grpc.ServerServiceDefinition; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.protobuf.lite.ProtoLiteUtils; +import io.grpc.stub.ServerCalls; + +/** + * Unit tests for {@link GcpIamCredentialsAuthentication}. + * + * @author Andreas Gebauer + */ +class GcpIamCredentialsAuthenticationUnitTests { + + RestTemplate restTemplate; + + MockRestServiceServer mockRest; + + private Server server; + + private ManagedChannel managedChannel; + + private ServerCalls.UnaryMethod serverCall; + + @BeforeEach + void before() throws IOException { + + RestTemplate restTemplate = new RestTemplate(); + restTemplate.setUriTemplateHandler(new PrefixAwareUriTemplateHandler()); + + this.mockRest = MockRestServiceServer.createServer(restTemplate); + this.restTemplate = restTemplate; + + String serverName = InProcessServerBuilder.generateName(); + this.server = InProcessServerBuilder.forName(serverName).directExecutor() + .addService(ServerServiceDefinition.builder("google.iam.credentials.v1.IAMCredentials") + .addMethod( + MethodDescriptor + .newBuilder(ProtoLiteUtils.marshaller(SignJwtRequest.getDefaultInstance()), + ProtoLiteUtils.marshaller(SignJwtResponse.getDefaultInstance())) + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName("google.iam.credentials.v1.IAMCredentials/SignJwt").build(), + asyncUnaryCall((request, responseObserver) -> { + this.serverCall.invoke(request, responseObserver); + })) + .build()) + .build().start(); + this.managedChannel = InProcessChannelBuilder.forName(serverName).directExecutor().build(); + } + + @AfterEach + void after() { + this.server.shutdown(); + } + + @Test + void shouldLogin() { + this.serverCall = ((request, responseObserver) -> { + SignJwtResponse signJwtResponse = SignJwtResponse.newBuilder().setSignedJwt("my-jwt").setKeyId("key-id") + .build(); + responseObserver.onNext(signJwtResponse); + responseObserver.onCompleted(); + }); + + this.mockRest.expect(requestTo("/auth/gcp/login")).andExpect(method(HttpMethod.POST)) + .andExpect(jsonPath("$.role").value("dev-role")).andExpect(jsonPath("$.jwt").value("my-jwt")) + .andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON).body( + "{" + "\"auth\":{\"client_token\":\"my-token\", \"renewable\": true, \"lease_duration\": 10}" + + "}")); + + PrivateKey privateKeyMock = mock(PrivateKey.class); + ServiceAccountCredentials credential = (ServiceAccountCredentials) ServiceAccountCredentials.newBuilder() + .setClientEmail("hello@world").setProjectId("foobar").setPrivateKey(privateKeyMock) + .setPrivateKeyId("key-id") + .setAccessToken(new AccessToken("foobar", Date.from(Instant.now().plus(1, ChronoUnit.DAYS)))).build(); + + GcpIamCredentialsAuthenticationOptions options = GcpIamCredentialsAuthenticationOptions.builder() + .role("dev-role").credentials(credential).build(); + GcpIamCredentialsAuthentication authentication = new GcpIamCredentialsAuthentication(options, this.restTemplate, + FixedTransportChannelProvider.create(GrpcTransportChannel.create(managedChannel))); + + VaultToken login = authentication.login(); + + assertThat(login).isInstanceOf(LoginToken.class); + assertThat(login.getToken()).isEqualTo("my-token"); + + LoginToken loginToken = (LoginToken) login; + assertThat(loginToken.isRenewable()).isTrue(); + assertThat(loginToken.getLeaseDuration()).isEqualTo(Duration.ofSeconds(10)); + } + + @Test + void shouldCreateNewGcpIamObjectInstance() { + + PrivateKey privateKeyMock = mock(PrivateKey.class); + ServiceAccountCredentials credential = (ServiceAccountCredentials) ServiceAccountCredentials.newBuilder() + .setClientEmail("hello@world").setProjectId("foobar").setPrivateKey(privateKeyMock) + .setPrivateKeyId("key-id") + .setAccessToken(new AccessToken("foobar", Date.from(Instant.now().plus(1, ChronoUnit.DAYS)))).build(); + + GcpIamCredentialsAuthenticationOptions options = GcpIamCredentialsAuthenticationOptions.builder() + .role("dev-role").credentials(credential).build(); + + new GcpIamCredentialsAuthentication(options, this.restTemplate); + } + +} diff --git a/spring-vault-dependencies/pom.xml b/spring-vault-dependencies/pom.xml index 583e7e4c..d0e3200f 100644 --- a/spring-vault-dependencies/pom.xml +++ b/spring-vault-dependencies/pom.xml @@ -65,6 +65,7 @@ 2.12.0 1.11.924 v1-rev20201112-1.31.0 + 1.1.9 0.22.2 1.67 @@ -144,6 +145,13 @@ true + + com.google.cloud + google-cloud-iamcredentials + ${google-cloud-iamcredentials.version} + true + + com.google.auth google-auth-library-oauth2-http