@@ -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 ;
@@ -45,6 +46,7 @@
import com .google .auth .ServiceAccountSigner ;
import com .google .auth .http .HttpCredentialsAdapter ;
import com .google .auth .http .HttpTransportFactory ;
import com .google .common .annotations .VisibleForTesting ;
import com .google .common .base .MoreObjects ;
import com .google .common .collect .ImmutableMap ;
import java .io .IOException ;
@@ -53,6 +55,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 +88,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 +104,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 +149,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 +229,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 +252,33 @@ public String getAccount() {
return this .targetPrincipal ;
}
@ Override
public String getQuotaProjectId () {
return this .quotaProjectId ;
}
@ VisibleForTesting
List <String > getDelegates () {
return delegates ;
}
@ VisibleForTesting
List <String > getScopes () {
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 +299,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 () {
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 +392,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 +488,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 +501,7 @@ public String toString() {
.add ("scopes" , scopes )
.add ("lifetime" , lifetime )
.add ("transportFactoryClassName" , transportFactoryClassName )
.add ("quotaProjectId" , quotaProjectId )
.toString ();
}
@@ -344,7 +516,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 +536,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 +599,11 @@ public HttpTransportFactory getHttpTransportFactory() {
return transportFactory ;
}
public Builder setQuotaProjectId (String quotaProjectId ) {
this .quotaProjectId = quotaProjectId ;
return this ;
}
public ImpersonatedCredentials build () {
return new ImpersonatedCredentials (this );
}