From 41645f1034ee299cb28e688aa83fe77fb9d024e6 Mon Sep 17 00:00:00 2001 From: Magnus Jungsbluth Date: Thu, 23 Aug 2018 10:25:32 +0200 Subject: [PATCH] Allow configuration of service account id and project id values GCP IAM Authentication. Original pull request: gh-287. Closes gh-261. --- .../DefaultGcpProjectIdProvider.java | 40 ++++++++ .../DefaultGcpServiceAccountIdProvider.java | 39 ++++++++ .../authentication/GcpIamAuthentication.java | 4 +- .../GcpIamAuthenticationOptions.java | 92 +++++++++++++++++- .../authentication/GcpProjectIdProvider.java | 37 +++++++ .../GcpServiceAccountIdProvider.java | 37 +++++++ ...AuthenticationOptionsBuilderUnitTests.java | 96 +++++++++++++++++++ 7 files changed, 339 insertions(+), 6 deletions(-) create mode 100644 spring-vault-core/src/main/java/org/springframework/vault/authentication/DefaultGcpProjectIdProvider.java create mode 100644 spring-vault-core/src/main/java/org/springframework/vault/authentication/DefaultGcpServiceAccountIdProvider.java create mode 100644 spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpProjectIdProvider.java create mode 100644 spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpServiceAccountIdProvider.java create mode 100644 spring-vault-core/src/test/java/org/springframework/vault/authentication/GcpIamAuthenticationOptionsBuilderUnitTests.java diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/DefaultGcpProjectIdProvider.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/DefaultGcpProjectIdProvider.java new file mode 100644 index 00000000..15067a5c --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/DefaultGcpProjectIdProvider.java @@ -0,0 +1,40 @@ +/* + * Copyright 2018 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 + * + * http://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.StringUtils; + +import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; + +/** + * Default implementation to obtain a GCP project id for GCP IAM authentication. + * Used by {@link GcpIamAuthentication}. + * + * @author Magnus Jungsbluth + * @since 2.1 + * @see GcpIamAuthentication + */ +public class DefaultGcpProjectIdProvider implements GcpProjectIdProvider { + + @Override + public String getProjectId(GoogleCredential credential) { + if (StringUtils.isEmpty(credential.getServiceAccountProjectId())) { + return "-"; + } else { + return credential.getServiceAccountProjectId(); + } + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/DefaultGcpServiceAccountIdProvider.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/DefaultGcpServiceAccountIdProvider.java new file mode 100644 index 00000000..78f1e2f4 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/DefaultGcpServiceAccountIdProvider.java @@ -0,0 +1,39 @@ +/* + * Copyright 2018 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 + * + * http://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.api.client.googleapis.auth.oauth2.GoogleCredential; + +/** + * Default implementation to obtain a service account id for GCP IAM authentication. + * Used by {@link GcpIamAuthentication}. + * + * @author Magnus Jungsbluth + * @since 2.1 + * @see GcpIamAuthentication + */ +public class DefaultGcpServiceAccountIdProvider implements GcpServiceAccountIdProvider { + + @Override + public String getServiceAccountId(GoogleCredential credential) { + Assert.notNull(credential.getServiceAccountId(), "The configured GoogleCredential does not represent a service account. Configure the service account id manually"); + + return credential.getServiceAccountId(); + } + +} 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 da782652..80944a16 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 @@ -125,8 +125,8 @@ public class GcpIamAuthentication extends GcpJwtAuthenticationSupport implements protected String signJwt() { - String projectId = credential.getServiceAccountProjectId(); - String serviceAccount = credential.getServiceAccountId(); + String projectId = options.getProjectIdProvider().getProjectId(credential); + String serviceAccount = options.getServiceAccountIdProvider().getServiceAccountId(credential); Map jwtPayload = getJwtPayload(options, serviceAccount); Iam iam = new Builder(httpTransport, JSON_FACTORY, credential) diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpIamAuthenticationOptions.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpIamAuthenticationOptions.java index b9518430..28c52942 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpIamAuthenticationOptions.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpIamAuthenticationOptions.java @@ -18,7 +18,6 @@ package org.springframework.vault.authentication; import java.time.Clock; import java.time.Duration; -import com.amazonaws.auth.AWSCredentialsProvider; import com.google.api.client.auth.oauth2.Credential; import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; @@ -65,15 +64,27 @@ public class GcpIamAuthenticationOptions { */ private final Clock clock; + /** + * Provide the service account id to use as sub/iss claims + */ + private final GcpServiceAccountIdProvider serviceAccountIdSupplier; + + /** + * The GCP project id to use in GCP IAM API calls + */ + private final GcpProjectIdProvider projectIdSupplier; + private GcpIamAuthenticationOptions(String path, GcpCredentialSupplier credentialSupplier, String role, Duration jwtValidity, - Clock clock) { + Clock clock, GcpServiceAccountIdProvider serviceAccountIdSupplier, GcpProjectIdProvider projectIdSupplier) { this.path = path; this.credentialSupplier = credentialSupplier; this.role = role; this.jwtValidity = jwtValidity; this.clock = clock; + this.serviceAccountIdSupplier = serviceAccountIdSupplier; + this.projectIdSupplier = projectIdSupplier; } /** @@ -118,6 +129,20 @@ public class GcpIamAuthenticationOptions { return clock; } + /** + * Provide the service account id to use as sub/iss claims + */ + public GcpServiceAccountIdProvider getServiceAccountIdProvider() { + return serviceAccountIdSupplier; + } + + /** + * The GCP project id to use in GCP IAM API calls + */ + public GcpProjectIdProvider getProjectIdProvider() { + return projectIdSupplier; + } + /** * Builder for {@link GcpIamAuthenticationOptions}. */ @@ -135,6 +160,10 @@ public class GcpIamAuthenticationOptions { private Clock clock = Clock.systemDefaultZone(); + private GcpServiceAccountIdProvider serviceAccountIdProvider = new DefaultGcpServiceAccountIdProvider(); + + private GcpProjectIdProvider projectIdProvider = new DefaultGcpProjectIdProvider(); + GcpIamAuthenticationOptionsBuilder() { } @@ -169,7 +198,7 @@ public class GcpIamAuthenticationOptions { } /** - * Configure an {@link AWSCredentialsProvider}, required to create a signed JWT. + * Configure an {@link GcpCredentialSupplier}, required to create a signed JWT. * Alternatively, configure static {@link #credential(GoogleCredential) * credentials}. * @@ -186,6 +215,61 @@ public class GcpIamAuthenticationOptions { return this; } + /** + * Configure an explicit service account id to use in GCP IAM calls. If none is configured, falls back to using + * {@link GoogleCredential#getServiceAccountId()}. + * + * @param serviceAccountId the service account id (email) to use + * @return {@code this} {@link GcpIamAuthenticationOptionsBuilder}. + */ + public GcpIamAuthenticationOptionsBuilder serviceAccountId(String serviceAccountId) { + Assert.notNull(serviceAccountId, "Service account id may not be null"); + + return serviceAccountIdProvider((GoogleCredential credential) -> serviceAccountId); + } + + /** + * Configure an {@link GcpServiceAccountIdProvider} to obtain the service account id used in GCP IAM calls. + * If none is configured, falls back to using {@link GoogleCredential#getServiceAccountId()}. + * + * @param serviceAccountIdProvider the service account id provider to use + * @return {@code this} {@link GcpIamAuthenticationOptionsBuilder}. + * @see GcpServiceAccountIdProvider + */ + public GcpIamAuthenticationOptionsBuilder serviceAccountIdProvider(GcpServiceAccountIdProvider serviceAccountIdProvider) { + Assert.notNull(serviceAccountIdProvider, "GcpServiceAccountIdProvider must not be null"); + + this.serviceAccountIdProvider = serviceAccountIdProvider; + return this; + } + + /** + * Configure an explicit GCP project id to use in GCP IAM API calls. If none is configured, falls back using + * {@link GoogleCredential#getServiceAccountProjectId()}. + * + * @param projectId the GCP project id to use in GCP IAM API calls + * @return {@code this} {@link GcpIamAuthenticationOptionsBuilder}. + */ + public GcpIamAuthenticationOptionsBuilder projectId(String projectId) { + Assert.notNull(projectId, "GCP project id must not be null"); + + return projectIdProvider((GoogleCredential credential) -> projectId); + } + + /** + * Configure an {@link GcpProjectIdProvider} to use in GCP IAM API calls. If none is configured, falls back using + * {@link GoogleCredential#getServiceAccountProjectId()}. + * + * @param projectIdProvider the GCP project id supplier to use in GCP IAM API calls + * @return {@code this} {@link GcpIamAuthenticationOptionsBuilder}. + */ + public GcpIamAuthenticationOptionsBuilder projectIdProvider(GcpProjectIdProvider projectIdProvider) { + Assert.notNull(projectIdProvider, "GcpProjectIdProvider must not be null"); + + this.projectIdProvider = projectIdProvider; + return this; + } + /** * Configure the name of the role against which the login is being attempted. * @@ -241,7 +325,7 @@ public class GcpIamAuthenticationOptions { Assert.notNull(role, "Role must not be null"); return new GcpIamAuthenticationOptions(path, credentialSupplier, role, - jwtValidity, clock); + jwtValidity, clock, serviceAccountIdProvider, projectIdProvider); } } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpProjectIdProvider.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpProjectIdProvider.java new file mode 100644 index 00000000..765038c4 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpProjectIdProvider.java @@ -0,0 +1,37 @@ +/* + * Copyright 2018 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 + * + * http://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.api.client.googleapis.auth.oauth2.GoogleCredential; + +/** + * Interface to obtain a GCP project id for GCP IAM authentication. + * Implementations are used by {@link GcpIamAuthentication}. + * + * @author Magnus Jungsbluth + * @since 2.1 + * @see GcpIamAuthentication + */ +@FunctionalInterface +public interface GcpProjectIdProvider { + + /** + * Get a the GCP project id to used in Google Cloud IAM API calls. + * + * @return the service account id to use. + */ + String getProjectId(GoogleCredential credential); +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpServiceAccountIdProvider.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpServiceAccountIdProvider.java new file mode 100644 index 00000000..369228ce --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/GcpServiceAccountIdProvider.java @@ -0,0 +1,37 @@ +/* + * Copyright 2018 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 + * + * http://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.api.client.googleapis.auth.oauth2.GoogleCredential; + +/** + * Interface to obtain a service account id for GCP IAM authentication. + * Implementations are used by {@link GcpIamAuthentication}. + * + * @author Magnus Jungsbluth + * @since 2.1 + * @see GcpIamAuthentication + */ +@FunctionalInterface +public interface GcpServiceAccountIdProvider { + + /** + * Get a the service account id (email) to be placed in the signed JWT. + * + * @return the service account id to use. + */ + String getServiceAccountId(GoogleCredential credential); +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/GcpIamAuthenticationOptionsBuilderUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/GcpIamAuthenticationOptionsBuilderUnitTests.java new file mode 100644 index 00000000..855b1635 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/GcpIamAuthenticationOptionsBuilderUnitTests.java @@ -0,0 +1,96 @@ +package org.springframework.vault.authentication; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import java.security.PrivateKey; + +import org.junit.Test; + +import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; + +/** + * Unit tests for {@link GcpIamAuthenticationOptions.GcpIamAuthenticationOptionsBuilder} + */ +public class GcpIamAuthenticationOptionsBuilderUnitTests { + + private GoogleCredential createGoogleCredential() { + GoogleCredential credential = new GoogleCredential.Builder().setServiceAccountId("hello@world") + .setServiceAccountProjectId("foobar") + .setServiceAccountPrivateKey(mock(PrivateKey.class)) + .setServiceAccountPrivateKeyId("key-id").build(); + credential.setAccessToken("foobar"); + return credential; + } + + @Test + public void shouldDefaultToCredentialServiceAccountId() { + GoogleCredential credential = createGoogleCredential(); + GcpIamAuthenticationOptions options = GcpIamAuthenticationOptions.builder() + .credential(credential) + .role("foo") + .build(); + + assertThat(options.getServiceAccountIdProvider().getServiceAccountId(credential)).isEqualTo("hello@world"); + } + + @Test + public void shouldAllowServiceAccountIdOverride() { + GoogleCredential credential = createGoogleCredential(); + GcpIamAuthenticationOptions options = GcpIamAuthenticationOptions.builder() + .credential(credential) + .serviceAccountId("override@foo.com") + .role("foo") + .build(); + + assertThat(options.getServiceAccountIdProvider().getServiceAccountId(credential)).isEqualTo("override@foo.com"); + } + + @Test + public void shouldAllowServiceAccountIdProviderOverride() { + GoogleCredential credential = createGoogleCredential(); + GcpIamAuthenticationOptions options = GcpIamAuthenticationOptions.builder() + .credential(credential) + .serviceAccountIdProvider((GoogleCredential googleCredential) -> "override@foo.com") + .role("foo") + .build(); + + assertThat(options.getServiceAccountIdProvider().getServiceAccountId(credential)).isEqualTo("override@foo.com"); + } + + @Test + public void shouldDefaultToCredentialProjectId() { + GoogleCredential credential = createGoogleCredential(); + GcpIamAuthenticationOptions options = GcpIamAuthenticationOptions.builder() + .credential(credential) + .role("foo") + .build(); + + assertThat(options.getProjectIdProvider().getProjectId(credential)).isEqualTo("foobar"); + } + + @Test + public void shouldAllowProjectIdOverride() { + GoogleCredential credential = createGoogleCredential(); + GcpIamAuthenticationOptions options = GcpIamAuthenticationOptions.builder() + .credential(credential) + .projectId("my-project") + .role("foo") + .build(); + + assertThat(options.getProjectIdProvider().getProjectId(credential)).isEqualTo("my-project"); + } + + @Test + public void shouldAllowProjectIdProviderOverride() { + GoogleCredential credential = createGoogleCredential(); + GcpIamAuthenticationOptions options = GcpIamAuthenticationOptions.builder() + .credential(credential) + .projectIdProvider((GoogleCredential googleCredential) -> "my-project") + .role("foo") + .build(); + + assertThat(options.getProjectIdProvider().getProjectId(credential)).isEqualTo("my-project"); + } + +}