-
Notifications
You must be signed in to change notification settings - Fork 229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add impersonation credentials to ADC #613
Changes from 27 commits
887c67d
f98b61c
9f805fb
ebeba8b
33f6db0
8b38844
3a98164
ea3d14a
815e39b
0ee39aa
35b347a
56ed2bb
a36da60
1b3bcd6
685ea43
3a20de4
3ac3809
e3f4a66
097245e
9a428d7
2febd2a
3402664
eee39af
b2751ae
8420528
51525ab
d34dae2
4ea84e9
55a0c61
2884531
35206dc
6ea5150
b5340d0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -32,6 +32,7 @@ | |||
package com.google.auth.oauth2; | ||||
|
||||
import static com.google.common.base.MoreObjects.firstNonNull; | ||||
import static com.google.common.base.Preconditions.checkNotNull; | ||||
|
||||
import com.google.api.client.http.GenericUrl; | ||||
import com.google.api.client.http.HttpContent; | ||||
|
@@ -53,6 +54,7 @@ | |||
import java.text.SimpleDateFormat; | ||||
import java.util.ArrayList; | ||||
import java.util.Arrays; | ||||
import java.util.Collection; | ||||
import java.util.Date; | ||||
import java.util.List; | ||||
import java.util.Map; | ||||
|
@@ -85,7 +87,7 @@ | |||
* </pre> | ||||
*/ | ||||
public class ImpersonatedCredentials extends GoogleCredentials | ||||
implements ServiceAccountSigner, IdTokenProvider { | ||||
implements ServiceAccountSigner, IdTokenProvider, QuotaProjectIdProvider { | ||||
|
||||
private static final long serialVersionUID = -2133257318957488431L; | ||||
private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ss'Z'"; | ||||
|
@@ -101,12 +103,14 @@ public class ImpersonatedCredentials extends GoogleCredentials | |||
private List<String> delegates; | ||||
private List<String> scopes; | ||||
private int lifetime; | ||||
private String quotaProjectId; | ||||
private final String transportFactoryClassName; | ||||
|
||||
private transient HttpTransportFactory transportFactory; | ||||
|
||||
/** | ||||
* @param sourceCredentials the source credential used as to acquire the impersonated credentials | ||||
* @param sourceCredentials the source credential used to acquire the impersonated credentials. It | ||||
* should be either a user account credential or a service account credential. | ||||
* @param targetPrincipal the service account to impersonate | ||||
* @param delegates the chained list of delegates required to grant the final access_token. If | ||||
* set, the sequence of identities must have "Service Account Token Creator" capability | ||||
|
@@ -144,7 +148,52 @@ public static ImpersonatedCredentials create( | |||
} | ||||
|
||||
/** | ||||
* @param sourceCredentials the source credential used as to acquire the impersonated credentials | ||||
* @param sourceCredentials the source credential used to acquire the impersonated credentials. It | ||||
* should be either a user account credential or a service account credential. | ||||
* @param targetPrincipal the service account to impersonate | ||||
* @param delegates the chained list of delegates required to grant the final access_token. If | ||||
* set, the sequence of identities must have "Service Account Token Creator" capability | ||||
* granted to the preceding identity. For example, if set to [serviceAccountB, | ||||
* serviceAccountC], the sourceCredential must have the Token Creator role on serviceAccountB. | ||||
* serviceAccountB must have the Token Creator on serviceAccountC. Finally, C must have Token | ||||
* Creator on target_principal. If unset, sourceCredential must have that role on | ||||
* targetPrincipal. | ||||
* @param scopes scopes to request during the authorization grant | ||||
* @param lifetime number of seconds the delegated credential should be valid. By default this | ||||
* value should be at most 3600. However, you can follow <a | ||||
* href='https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials#sa-credentials-oauth'>these | ||||
* instructions</a> to set up the service account and extend the maximum lifetime to 43200 (12 | ||||
* hours). If the given lifetime is 0, default value 3600 will be used instead when creating | ||||
* the credentials. | ||||
* @param transportFactory HTTP transport factory that creates the transport used to get access | ||||
* tokens. | ||||
* @param quotaProjectId the project used for quota and billing purposes. Should be null unless | ||||
* the caller wants to use a project different from the one that owns the impersonated | ||||
* credential for billing/quota purposes. | ||||
* @return new credentials | ||||
*/ | ||||
public static ImpersonatedCredentials create( | ||||
GoogleCredentials sourceCredentials, | ||||
String targetPrincipal, | ||||
List<String> delegates, | ||||
List<String> scopes, | ||||
int lifetime, | ||||
HttpTransportFactory transportFactory, | ||||
String quotaProjectId) { | ||||
return ImpersonatedCredentials.newBuilder() | ||||
.setSourceCredentials(sourceCredentials) | ||||
.setTargetPrincipal(targetPrincipal) | ||||
.setDelegates(delegates) | ||||
.setScopes(scopes) | ||||
.setLifetime(lifetime) | ||||
.setHttpTransportFactory(transportFactory) | ||||
.setQuotaProjectId(quotaProjectId) | ||||
.build(); | ||||
} | ||||
|
||||
/** | ||||
* @param sourceCredentials the source credential used to acquire the impersonated credentials. It | ||||
* should be either a user account credential or a service account credential. | ||||
* @param targetPrincipal the service account to impersonate | ||||
* @param delegates the chained list of delegates required to grant the final access_token. If | ||||
* set, the sequence of identities must have "Service Account Token Creator" capability | ||||
|
@@ -179,6 +228,19 @@ public static ImpersonatedCredentials create( | |||
.build(); | ||||
} | ||||
|
||||
static String extractTargetPrincipal(String serviceAccountImpersonationUrl) { | ||||
// Extract the target principal. | ||||
int startIndex = serviceAccountImpersonationUrl.lastIndexOf('/'); | ||||
int endIndex = serviceAccountImpersonationUrl.indexOf(":generateAccessToken"); | ||||
|
||||
if (startIndex != -1 && endIndex != -1 && startIndex < endIndex) { | ||||
return serviceAccountImpersonationUrl.substring(startIndex + 1, endIndex); | ||||
} else { | ||||
throw new IllegalArgumentException( | ||||
"Unable to determine target principal from service account impersonation URL."); | ||||
} | ||||
} | ||||
|
||||
/** | ||||
* Returns the email field of the serviceAccount that is being impersonated. | ||||
* | ||||
|
@@ -189,10 +251,31 @@ public String getAccount() { | |||
return this.targetPrincipal; | ||||
} | ||||
|
||||
@Override | ||||
public String getQuotaProjectId() { | ||||
return this.quotaProjectId; | ||||
} | ||||
|
||||
public List<String> getDelegates() { | ||||
return delegates; | ||||
} | ||||
|
||||
public List<String> getScopes() { | ||||
liuchaoren marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
return scopes; | ||||
} | ||||
|
||||
public GoogleCredentials getSourceCredentials() { | ||||
return sourceCredentials; | ||||
} | ||||
|
||||
int getLifetime() { | ||||
return this.lifetime; | ||||
} | ||||
|
||||
public void setTransportFactory(HttpTransportFactory httpTransportFactory) { | ||||
this.transportFactory = httpTransportFactory; | ||||
} | ||||
|
||||
/** | ||||
* Signs the provided bytes using the private key associated with the impersonated service account | ||||
* | ||||
|
@@ -213,6 +296,89 @@ public byte[] sign(byte[] toSign) { | |||
ImmutableMap.of("delegates", this.delegates)); | ||||
} | ||||
|
||||
/** | ||||
* Returns impersonation account credentials defined by JSON using the format generated by gCloud. | ||||
* The source credentials in the JSON should be either user account credentials or service account | ||||
* credentials. | ||||
* | ||||
* @param json a map from the JSON representing the credentials | ||||
* @param transportFactory HTTP transport factory, creates the transport used to get access tokens | ||||
* @return the credentials defined by the JSON | ||||
* @throws IOException if the credential cannot be created from the JSON. | ||||
*/ | ||||
static ImpersonatedCredentials fromJson( | ||||
Map<String, Object> json, HttpTransportFactory transportFactory) throws IOException { | ||||
|
||||
checkNotNull(json); | ||||
checkNotNull(transportFactory); | ||||
|
||||
List<String> delegates = null; | ||||
Map<String, Object> sourceCredentialsJson; | ||||
String sourceCredentialsType; | ||||
String quotaProjectId; | ||||
String targetPrincipal; | ||||
try { | ||||
String serviceAccountImpersonationUrl = | ||||
(String) json.get("service_account_impersonation_url"); | ||||
if (json.containsKey("delegates")) { | ||||
delegates = (List<String>) json.get("delegates"); | ||||
} | ||||
sourceCredentialsJson = (Map<String, Object>) json.get("source_credentials"); | ||||
sourceCredentialsType = (String) sourceCredentialsJson.get("type"); | ||||
quotaProjectId = (String) json.get("quota_project_id"); | ||||
targetPrincipal = extractTargetPrincipal(serviceAccountImpersonationUrl); | ||||
} catch (ClassCastException | NullPointerException | IllegalArgumentException e) { | ||||
throw new CredentialFormatException("An invalid input stream was provided.", e); | ||||
} | ||||
|
||||
GoogleCredentials sourceCredentials; | ||||
if (GoogleCredentials.USER_FILE_TYPE.equals(sourceCredentialsType)) { | ||||
sourceCredentials = UserCredentials.fromJson(sourceCredentialsJson, transportFactory); | ||||
} else if (GoogleCredentials.SERVICE_ACCOUNT_FILE_TYPE.equals(sourceCredentialsType)) { | ||||
sourceCredentials = | ||||
ServiceAccountCredentials.fromJson(sourceCredentialsJson, transportFactory); | ||||
} else { | ||||
throw new IOException( | ||||
String.format( | ||||
"A credential of type %s is not supported as source credential for impersonation.", | ||||
sourceCredentialsType)); | ||||
} | ||||
return ImpersonatedCredentials.newBuilder() | ||||
.setSourceCredentials(sourceCredentials) | ||||
.setTargetPrincipal(targetPrincipal) | ||||
.setDelegates(delegates) | ||||
.setScopes(new ArrayList<String>()) | ||||
.setLifetime(DEFAULT_LIFETIME_IN_SECONDS) | ||||
.setHttpTransportFactory(transportFactory) | ||||
.setQuotaProjectId(quotaProjectId) | ||||
.build(); | ||||
} | ||||
|
||||
@Override | ||||
public boolean createScopedRequired() { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This name doesn't quite fit. Create tends t be a factory method. This is more of an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method is not invented in this PR. It is defined in the base class 'GoogleCredentials' which is overridden by its subclasses. I think it uses "create" in its name because of its relationship with the createScoped method. google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java Line 218 in 292e81a
|
||||
return this.scopes == null || this.scopes.isEmpty(); | ||||
} | ||||
|
||||
@Override | ||||
public GoogleCredentials createScoped(Collection<String> scopes) { | ||||
return toBuilder() | ||||
.setScopes((List<String>) scopes) | ||||
.setLifetime(this.lifetime) | ||||
.setDelegates(this.delegates) | ||||
.setHttpTransportFactory(this.transportFactory) | ||||
.setQuotaProjectId(this.quotaProjectId) | ||||
.build(); | ||||
} | ||||
|
||||
@Override | ||||
protected Map<String, List<String>> getAdditionalHeaders() { | ||||
Map<String, List<String>> headers = super.getAdditionalHeaders(); | ||||
if (quotaProjectId != null) { | ||||
return addQuotaProjectIdToRequestMetadata(quotaProjectId, headers); | ||||
} | ||||
return headers; | ||||
} | ||||
|
||||
private ImpersonatedCredentials(Builder builder) { | ||||
this.sourceCredentials = builder.getSourceCredentials(); | ||||
this.targetPrincipal = builder.getTargetPrincipal(); | ||||
|
@@ -223,6 +389,7 @@ private ImpersonatedCredentials(Builder builder) { | |||
firstNonNull( | ||||
builder.getHttpTransportFactory(), | ||||
getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY)); | ||||
this.quotaProjectId = builder.quotaProjectId; | ||||
this.transportFactoryClassName = this.transportFactory.getClass().getName(); | ||||
if (this.delegates == null) { | ||||
this.delegates = new ArrayList<String>(); | ||||
|
@@ -318,7 +485,8 @@ public IdToken idTokenWithAudience(String targetAudience, List<IdTokenProvider.O | |||
|
||||
@Override | ||||
public int hashCode() { | ||||
return Objects.hash(sourceCredentials, targetPrincipal, delegates, scopes, lifetime); | ||||
return Objects.hash( | ||||
sourceCredentials, targetPrincipal, delegates, scopes, lifetime, quotaProjectId); | ||||
} | ||||
|
||||
@Override | ||||
|
@@ -330,6 +498,7 @@ public String toString() { | |||
.add("scopes", scopes) | ||||
.add("lifetime", lifetime) | ||||
.add("transportFactoryClassName", transportFactoryClassName) | ||||
.add("quotaProjectId", quotaProjectId) | ||||
.toString(); | ||||
} | ||||
|
||||
|
@@ -344,7 +513,8 @@ public boolean equals(Object obj) { | |||
&& Objects.equals(this.delegates, other.delegates) | ||||
&& Objects.equals(this.scopes, other.scopes) | ||||
&& Objects.equals(this.lifetime, other.lifetime) | ||||
&& Objects.equals(this.transportFactoryClassName, other.transportFactoryClassName); | ||||
&& Objects.equals(this.transportFactoryClassName, other.transportFactoryClassName) | ||||
&& Objects.equals(this.quotaProjectId, other.quotaProjectId); | ||||
} | ||||
|
||||
public Builder toBuilder() { | ||||
|
@@ -363,6 +533,7 @@ public static class Builder extends GoogleCredentials.Builder { | |||
private List<String> scopes; | ||||
private int lifetime = DEFAULT_LIFETIME_IN_SECONDS; | ||||
private HttpTransportFactory transportFactory; | ||||
private String quotaProjectId; | ||||
|
||||
protected Builder() {} | ||||
|
||||
|
@@ -425,6 +596,11 @@ public HttpTransportFactory getHttpTransportFactory() { | |||
return transportFactory; | ||||
} | ||||
|
||||
public Builder setQuotaProjectId(String quotaProjectId) { | ||||
this.quotaProjectId = quotaProjectId; | ||||
return this; | ||||
} | ||||
|
||||
public ImpersonatedCredentials build() { | ||||
return new ImpersonatedCredentials(this); | ||||
} | ||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method doesn't seem used. Can it be removed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method is used in tests to verify the delegates.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case please annotate it as
@VisibleForTesting
. Also, make it non-public if possible. (It might not be but worth checking.)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great, thanks for letting me know the annotation! I added it and removed the "public" to limit the visibility.