Skip to content
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

Merged
merged 33 commits into from
May 21, 2021
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
887c67d
ADC can load impersonation credentials
liuchaoren Mar 22, 2021
f98b61c
Add tests for new features in ImpersonationCredentials
liuchaoren Mar 23, 2021
9f805fb
Merge remote-tracking branch 'upstream/master'
liuchaoren Mar 23, 2021
ebeba8b
Add tests for GoogleCredentials
liuchaoren Mar 24, 2021
33f6db0
Fix linter errors
liuchaoren Mar 25, 2021
8b38844
Fix linter errors in ImpersonatedCredentialsTest
liuchaoren Mar 25, 2021
3a98164
Fix issues after receiving comments
liuchaoren Apr 1, 2021
ea3d14a
Merge remote-tracking branch 'upstream/master'
liuchaoren Apr 1, 2021
815e39b
Fix lint errors
liuchaoren Apr 1, 2021
0ee39aa
Handle ClassCastException in fromJson
liuchaoren Apr 2, 2021
35b347a
Fix lint errors
liuchaoren Apr 2, 2021
56ed2bb
minor refactoring
liuchaoren Apr 2, 2021
a36da60
fix doc strings
liuchaoren Apr 2, 2021
1b3bcd6
fix lint errors
liuchaoren Apr 2, 2021
685ea43
delegates can be missing from the json file
liuchaoren Apr 3, 2021
3a20de4
Mark test using @Test()
liuchaoren Apr 3, 2021
3ac3809
Merge remote-tracking branch 'upstream/master'
liuchaoren Apr 23, 2021
e3f4a66
Remove redundant methods and handle exceptions
liuchaoren Apr 23, 2021
097245e
add an empty file
liuchaoren Apr 23, 2021
9a428d7
remove an empty file
liuchaoren Apr 23, 2021
2febd2a
Fix docstring and move one variable to inner scope.
liuchaoren Apr 26, 2021
3402664
Merge branch 'master' into master
elharo May 5, 2021
eee39af
Merge branch 'master' into master
elharo May 10, 2021
b2751ae
Merge remote-tracking branch 'upstream/master'
liuchaoren May 11, 2021
8420528
Refactor ImpersonatedCredentialsTest
liuchaoren May 11, 2021
51525ab
Merge branch 'master' of github.com:liuchaoren/google-auth-library-java
liuchaoren May 11, 2021
d34dae2
Reformat the ImpersonatedCredentialsTest
liuchaoren May 11, 2021
4ea84e9
Remove redundant checks in tests
liuchaoren May 12, 2021
55a0c61
Use VisibleForTesting annotation to limit visibility
liuchaoren May 12, 2021
2884531
Merge branch 'master' into master
elharo May 18, 2021
35206dc
Merge branch 'master' into master
elharo May 20, 2021
6ea5150
Merge branch 'master' into master
elharo May 20, 2021
b5340d0
Merge branch 'master' into master
elharo May 21, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,8 @@ private ImpersonatedCredentials initializeImpersonatedCredentials() {
.build();
}

String targetPrincipal = extractTargetPrincipal(serviceAccountImpersonationUrl);
String targetPrincipal =
ImpersonatedCredentials.extractTargetPrincipal(serviceAccountImpersonationUrl);
return ImpersonatedCredentials.newBuilder()
.setSourceCredentials(sourceCredentials)
.setHttpTransportFactory(transportFactory)
Expand Down Expand Up @@ -337,19 +338,6 @@ protected AccessToken exchangeExternalCredentialForAccessToken(
return response.getAccessToken();
}

private 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.");
}
}

/**
* Retrieves the external subject token to be exchanged for a GCP access token.
*
Expand Down
12 changes: 10 additions & 2 deletions oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public class GoogleCredentials extends OAuth2Credentials {
static final String QUOTA_PROJECT_ID_HEADER_KEY = "x-goog-user-project";
static final String USER_FILE_TYPE = "authorized_user";
static final String SERVICE_ACCOUNT_FILE_TYPE = "service_account";
static final String IMPERSONATED_SERVICE_ACCOUNT = "impersonated_service_account";
silvolu marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

@elharo elharo Apr 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This constant does not improve on the literal string. I suggest just using the string.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am debating on these two options. I think even though the constant is not more readable than the string itself. I like the way we define all credential types in one place. So people can see all supported types there. I am not very familiar with Java convention, I trust your judgement on this. Please let me know if you still want me to make this change.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's something that belongs in documentation. Don't rely on the code for that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


private static final DefaultCredentialsProvider defaultCredentialsProvider =
new DefaultCredentialsProvider();
Expand All @@ -78,8 +79,12 @@ public static GoogleCredentials create(AccessToken accessToken) {
* <ol>
* <li>Credentials file pointed to by the {@code GOOGLE_APPLICATION_CREDENTIALS} environment
* variable
* <li>Credentials provided by the Google Cloud SDK {@code gcloud auth application-default
* login} command
* <li>Credentials provided by the Google Cloud SDK.
* <ol>
* <li>{@code gcloud auth application-default login} for user account credentials.
* <li>{@code gcloud auth application-default login --impersonate-service-account} for
* impersonated service account credentials.
* </ol>
* <li>Google App Engine built-in credentials
* <li>Google Cloud Shell built-in credentials
* <li>Google Compute Engine built-in credentials
Expand Down Expand Up @@ -169,6 +174,9 @@ public static GoogleCredentials fromStream(
if (ExternalAccountCredentials.EXTERNAL_ACCOUNT_FILE_TYPE.equals(fileType)) {
return ExternalAccountCredentials.fromJson(fileContents, transportFactory);
}
if (IMPERSONATED_SERVICE_ACCOUNT.equals(fileType)) {
return ImpersonatedCredentials.fromJson(fileContents, transportFactory);
}
throw new IOException(
String.format(
"Error reading credentials from stream, 'type' value '%s' not recognized."
Expand Down
187 changes: 182 additions & 5 deletions oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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'";
Expand All @@ -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 as to acquire the impersonated credentials.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete "as",
delete final period since it's not a complete sentence

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

* 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
Expand Down Expand Up @@ -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 as to acquire the impersonated credentials.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete "as",
delete final period since it's not a complete sentence

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

* 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 as 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
Expand Down Expand Up @@ -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.
*
Expand All @@ -189,10 +251,31 @@ public String getAccount() {
return this.targetPrincipal;
}

@Override
public String getQuotaProjectId() {
return this.quotaProjectId;
}

public List<String> getDelegates() {
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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.)

Copy link
Contributor Author

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.

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
*
Expand All @@ -213,6 +296,86 @@ 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);

String serviceAccountImpersonationUrl;
List<String> delegates;
Map<String, Object> sourceCredentialsJson;
String sourceCredentialsType;
String quotaProjectId;
try {
serviceAccountImpersonationUrl = (String) json.get("service_account_impersonation_url");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be declared here instead of line 315

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

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");
} catch (ClassCastException | NullPointerException e) {
throw new CredentialFormatException("An invalid input stream was provided.", e);
}
String targetPrincipal = extractTargetPrincipal(serviceAccountImpersonationUrl);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to handle the IllegalArgumentException here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, it is handled.


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() {
Copy link
Contributor

Choose a reason for hiding this comment

The 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 is method. Is this new in this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

* Indicates whether the credentials require scopes to be specified via a call to {@link

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();
Expand All @@ -223,6 +386,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>();
Expand Down Expand Up @@ -318,7 +482,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
Expand All @@ -330,6 +495,7 @@ public String toString() {
.add("scopes", scopes)
.add("lifetime", lifetime)
.add("transportFactoryClassName", transportFactoryClassName)
.add("quotaProjectId", quotaProjectId)
.toString();
}

Expand All @@ -344,7 +510,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() {
Expand All @@ -363,6 +530,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() {}

Expand Down Expand Up @@ -425,6 +593,15 @@ public HttpTransportFactory getHttpTransportFactory() {
return transportFactory;
}

public Builder setQuotaProjectId(String quotaProjectId) {
this.quotaProjectId = quotaProjectId;
return this;
}

public String getQuotaProjectId() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is not tested. Is it needed at all? It can probably be removed at no loss.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing it out! It is not used and I removed it.

return quotaProjectId;
}

public ImpersonatedCredentials build() {
return new ImpersonatedCredentials(this);
}
Expand Down
Loading