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:
committed by
Mark Paluch
parent
701f931669
commit
6bfd192dd8
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user