Add support for GCP IAM credentials API.

We now support the IAM Credentials API in addition to the deprecated IAM API for signing JWT.

Closes gh-600.
Original pull request: gh-619.
This commit is contained in:
Andreas Gebauer
2021-01-21 16:59:11 +01:00
committed by Mark Paluch
parent 701f931669
commit 6bfd192dd8
10 changed files with 861 additions and 0 deletions

View File

@@ -194,6 +194,25 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-iamcredentials</artifactId>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</exclusion>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
</exclusions>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.auth</groupId>

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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<GoogleCredentials> {
/**
* 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;
}

View File

@@ -64,7 +64,9 @@ import org.springframework.web.client.RestOperations;
* @see <a href=
* "https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts/signJwt">GCP:
* projects.serviceAccounts.signJwt</a>
* @deprecated Use {@link GcpIamCredentialsAuthentication} instead.
*/
@Deprecated
public class GcpIamAuthentication extends GcpJwtAuthenticationSupport implements ClientAuthentication {
private static final JsonFactory JSON_FACTORY = new JacksonFactory();

View File

@@ -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.
* <p/>
* 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).
* <p/>
* {@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 <a href="https://www.vaultproject.io/docs/auth/gcp.html">Auth Backend: gcp
* (IAM)</a>
* @see <a href=
* "https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/signJwt">GCP:
* projects.serviceAccounts.signJwt</a>
*/
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<String, Object> 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<String, Object> getJwtPayload(GcpIamCredentialsAuthenticationOptions options,
String serviceAccount) {
Instant validUntil = options.getClock().instant().plus(options.getJwtValidity());
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("sub", serviceAccount);
payload.put("aud", "vault/" + options.getRole());
payload.put("exp", validUntil.getEpochSecond());
return payload;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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<SignJwtRequest, SignJwtResponse> 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);
}
}

View File

@@ -65,6 +65,7 @@
<jackson-databind.version>2.12.0</jackson-databind.version>
<aws-java-sdk.version>1.11.924</aws-java-sdk.version>
<google-api-services-iam.version>v1-rev20201112-1.31.0</google-api-services-iam.version>
<google-cloud-iamcredentials.version>1.1.9</google-cloud-iamcredentials.version>
<google-auth-library-oauth2-http.version>0.22.2</google-auth-library-oauth2-http.version>
<bcpkix-jdk15on.version>1.67</bcpkix-jdk15on.version>
</properties>
@@ -144,6 +145,13 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-iamcredentials</artifactId>
<version>${google-cloud-iamcredentials.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>