Skip to content

Commit a1ef624

Browse files
committed
feat: Support user defined or json defined scopes for impersonated token
1 parent b910dad commit a1ef624

File tree

3 files changed

+101
-50
lines changed

3 files changed

+101
-50
lines changed

oauth2_http/java/com/google/auth/oauth2/ImpersonatedCredentials.java

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -50,21 +50,16 @@
5050
import com.google.auth.oauth2.MetricsUtils.RequestType;
5151
import com.google.common.annotations.VisibleForTesting;
5252
import com.google.common.base.MoreObjects;
53+
import com.google.common.base.Preconditions;
54+
import com.google.common.collect.ImmutableList;
5355
import com.google.common.collect.ImmutableMap;
5456
import com.google.errorprone.annotations.CanIgnoreReturnValue;
5557
import java.io.IOException;
5658
import java.io.ObjectInputStream;
5759
import java.text.DateFormat;
5860
import java.text.ParseException;
5961
import java.text.SimpleDateFormat;
60-
import java.util.ArrayList;
61-
import java.util.Arrays;
62-
import java.util.Calendar;
63-
import java.util.Collection;
64-
import java.util.Date;
65-
import java.util.List;
66-
import java.util.Map;
67-
import java.util.Objects;
62+
import java.util.*;
6863

6964
/**
7065
* ImpersonatedCredentials allowing credentials issued to a user or service account to impersonate
@@ -104,7 +99,7 @@ public class ImpersonatedCredentials extends GoogleCredentials
10499
private GoogleCredentials sourceCredentials;
105100
private String targetPrincipal;
106101
private List<String> delegates;
107-
private List<String> scopes;
102+
private final List<String> scopes;
108103
private int lifetime;
109104
private String iamEndpointOverride;
110105
private final String transportFactoryClassName;
@@ -390,6 +385,10 @@ static ImpersonatedCredentials fromJson(
390385
String quotaProjectId;
391386
String targetPrincipal;
392387
String serviceAccountImpersonationUrl;
388+
// This applies to the scopes applied for the impersonated token and not the
389+
// underlying source credential. Default to empty list to keep the existing
390+
// behavior (when json file did not populate a scopes field).
391+
List<String> scopes = new ArrayList<>();
393392
try {
394393
serviceAccountImpersonationUrl = (String) json.get("service_account_impersonation_url");
395394
if (json.containsKey("delegates")) {
@@ -399,18 +398,21 @@ static ImpersonatedCredentials fromJson(
399398
sourceCredentialsType = (String) sourceCredentialsJson.get("type");
400399
quotaProjectId = (String) json.get("quota_project_id");
401400
targetPrincipal = extractTargetPrincipal(serviceAccountImpersonationUrl);
401+
if (json.containsKey("scopes")) {
402+
scopes = (List<String>) json.get("scopes");
403+
}
402404
} catch (ClassCastException | NullPointerException | IllegalArgumentException e) {
403405
throw new CredentialFormatException("An invalid input stream was provided.", e);
404406
}
405407

406408
GoogleCredentials sourceCredentials;
407-
if (GoogleCredentialsInfo.USER_CREDENTIALS.getFileType().equals(sourceCredentialsType)) {
409+
if (sourceCredentialsType.equals(GoogleCredentialsInfo.USER_CREDENTIALS.getFileType())) {
408410
sourceCredentials = UserCredentials.fromJson(sourceCredentialsJson, transportFactory);
409-
} else if (GoogleCredentialsInfo.SERVICE_ACCOUNT_CREDENTIALS
410-
.getFileType()
411-
.equals(sourceCredentialsType)) {
411+
} else if (sourceCredentialsType.equals(
412+
GoogleCredentialsInfo.SERVICE_ACCOUNT_CREDENTIALS.getFileType())) {
412413
sourceCredentials =
413414
ServiceAccountCredentials.fromJson(sourceCredentialsJson, transportFactory);
415+
414416
} else {
415417
throw new IOException(
416418
String.format(
@@ -421,7 +423,7 @@ static ImpersonatedCredentials fromJson(
421423
.setSourceCredentials(sourceCredentials)
422424
.setTargetPrincipal(targetPrincipal)
423425
.setDelegates(delegates)
424-
.setScopes(new ArrayList<>())
426+
.setScopes(scopes)
425427
.setLifetime(DEFAULT_LIFETIME_IN_SECONDS)
426428
.setHttpTransportFactory(transportFactory)
427429
.setQuotaProjectId(quotaProjectId)
@@ -468,7 +470,9 @@ private ImpersonatedCredentials(Builder builder) throws IOException {
468470
this.sourceCredentials = builder.getSourceCredentials();
469471
this.targetPrincipal = builder.getTargetPrincipal();
470472
this.delegates = builder.getDelegates();
471-
this.scopes = builder.getScopes();
473+
474+
// Precedence for scopes: 1. User configured scopes 2. Scopes set in the JSON
475+
this.scopes = ImmutableList.copyOf(builder.getScopes());
472476
this.lifetime = builder.getLifetime();
473477
this.transportFactory =
474478
firstNonNull(
@@ -480,9 +484,6 @@ private ImpersonatedCredentials(Builder builder) throws IOException {
480484
if (this.delegates == null) {
481485
this.delegates = new ArrayList<>();
482486
}
483-
if (this.scopes == null) {
484-
throw new IllegalStateException("Scopes cannot be null");
485-
}
486487
if (this.lifetime > TWELVE_HOURS_IN_SECONDS) {
487488
throw new IllegalStateException("lifetime must be less than or equal to 43200");
488489
}
@@ -517,7 +518,7 @@ public String getUniverseDomain() throws IOException {
517518
public AccessToken refreshAccessToken() throws IOException {
518519
if (this.sourceCredentials.getAccessToken() == null) {
519520
this.sourceCredentials =
520-
this.sourceCredentials.createScoped(Arrays.asList(CLOUD_PLATFORM_SCOPE));
521+
this.sourceCredentials.createScoped(Collections.singletonList(CLOUD_PLATFORM_SCOPE));
521522
}
522523

523524
// skip for SA with SSJ flow because it uses self-signed JWT
@@ -741,12 +742,22 @@ public List<String> getDelegates() {
741742
return this.delegates;
742743
}
743744

745+
/**
746+
* Set the scopes to be applied on the impersonated token and not on the source credential. This
747+
* user configuration has precedence over the scopes listed in the source credential json file.
748+
*
749+
* @param scopes List of scopes to apply to the impersonated token
750+
*/
744751
@CanIgnoreReturnValue
745752
public Builder setScopes(List<String> scopes) {
753+
Preconditions.checkNotNull(scopes, "Scopes cannot be null");
746754
this.scopes = scopes;
747755
return this;
748756
}
749757

758+
/**
759+
* @return List of scopes to be applied to the impersonated token.
760+
*/
750761
public List<String> getScopes() {
751762
return this.scopes;
752763
}

oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,8 @@ public void fromStream_Impersonation_providesToken_WithQuotaProject() throws IOE
619619
ImpersonatedCredentialsTest.writeImpersonationCredentialsStream(
620620
ImpersonatedCredentialsTest.IMPERSONATION_OVERRIDE_URL,
621621
ImpersonatedCredentialsTest.DELEGATES,
622-
ImpersonatedCredentialsTest.QUOTA_PROJECT_ID);
622+
ImpersonatedCredentialsTest.QUOTA_PROJECT_ID,
623+
ImpersonatedCredentialsTest.IMMUTABLE_SCOPES_LIST);
623624

624625
ImpersonatedCredentials credentials =
625626
(ImpersonatedCredentials)
@@ -649,7 +650,8 @@ public void fromStream_Impersonation_defaultUniverse() throws IOException {
649650
ImpersonatedCredentialsTest.writeImpersonationCredentialsStream(
650651
ImpersonatedCredentialsTest.IMPERSONATION_OVERRIDE_URL,
651652
ImpersonatedCredentialsTest.DELEGATES,
652-
ImpersonatedCredentialsTest.QUOTA_PROJECT_ID);
653+
ImpersonatedCredentialsTest.QUOTA_PROJECT_ID,
654+
ImpersonatedCredentialsTest.IMMUTABLE_SCOPES_LIST);
653655

654656
ImpersonatedCredentials credentials =
655657
(ImpersonatedCredentials)
@@ -684,7 +686,8 @@ public void fromStream_Impersonation_providesToken_WithoutQuotaProject() throws
684686
ImpersonatedCredentialsTest.writeImpersonationCredentialsStream(
685687
ImpersonatedCredentialsTest.IMPERSONATION_OVERRIDE_URL,
686688
ImpersonatedCredentialsTest.DELEGATES,
687-
null);
689+
null,
690+
ImpersonatedCredentialsTest.IMMUTABLE_SCOPES_LIST);
688691

689692
ImpersonatedCredentials credentials =
690693
(ImpersonatedCredentials)
@@ -916,7 +919,8 @@ public void getCredentialInfo_impersonatedServiceAccount() throws IOException {
916919
ImpersonatedCredentialsTest.writeImpersonationCredentialsStream(
917920
ImpersonatedCredentialsTest.IMPERSONATION_OVERRIDE_URL,
918921
ImpersonatedCredentialsTest.DELEGATES,
919-
null);
922+
null,
923+
ImpersonatedCredentialsTest.IMMUTABLE_SCOPES_LIST);
920924

921925
ImpersonatedCredentials credentials =
922926
(ImpersonatedCredentials) GoogleCredentials.fromStream(impersonationCredentialsStream);

oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java

Lines changed: 63 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,10 @@ public class ImpersonatedCredentialsTest extends BaseSerializationTest {
123123
static final List<String> IMMUTABLE_SCOPES_LIST = ImmutableList.of("scope1", "scope2");
124124
static final int VALID_LIFETIME = 300;
125125
private static final int INVALID_LIFETIME = 43210;
126-
private static JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance();
126+
private static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance();
127127

128128
private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ssX";
129129

130-
private static final String DEFAULT_UNIVERSE_DOMAIN = "googleapis.com";
131130
private static final String TEST_UNIVERSE_DOMAIN = "test.xyz";
132131
private static final String OLD_IMPERSONATION_URL =
133132
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/"
@@ -136,7 +135,7 @@ public class ImpersonatedCredentialsTest extends BaseSerializationTest {
136135
public static final String DEFAULT_IMPERSONATION_URL =
137136
String.format(
138137
IamUtils.IAM_ACCESS_TOKEN_ENDPOINT_FORMAT,
139-
DEFAULT_UNIVERSE_DOMAIN,
138+
GoogleCredentials.GOOGLE_DEFAULT_UNIVERSE,
140139
IMPERSONATED_CLIENT_EMAIL);
141140
private static final String NONGDU_IMPERSONATION_URL =
142141
String.format(
@@ -190,14 +189,15 @@ public void fromJson_userAsSource_WithQuotaProjectId() throws IOException {
190189
QUOTA_PROJECT_ID,
191190
USER_ACCOUNT_CLIENT_ID,
192191
USER_ACCOUNT_CLIENT_SECRET,
193-
REFRESH_TOKEN);
192+
REFRESH_TOKEN,
193+
IMMUTABLE_SCOPES_LIST);
194194
ImpersonatedCredentials credentials =
195195
ImpersonatedCredentials.fromJson(json, mockTransportFactory);
196196
assertEquals(IMPERSONATED_CLIENT_EMAIL, credentials.getAccount());
197197
assertEquals(IMPERSONATION_OVERRIDE_URL, credentials.getIamEndpointOverride());
198198
assertEquals(QUOTA_PROJECT_ID, credentials.getQuotaProjectId());
199199
assertEquals(DELEGATES, credentials.getDelegates());
200-
assertEquals(new ArrayList<String>(), credentials.getScopes());
200+
assertEquals(IMMUTABLE_SCOPES_LIST, credentials.getScopes());
201201
assertEquals(3600, credentials.getLifetime());
202202
GoogleCredentials sourceCredentials = credentials.getSourceCredentials();
203203
assertTrue(sourceCredentials instanceof UserCredentials);
@@ -212,14 +212,15 @@ public void fromJson_userAsSource_WithoutQuotaProjectId() throws IOException {
212212
null,
213213
USER_ACCOUNT_CLIENT_ID,
214214
USER_ACCOUNT_CLIENT_SECRET,
215-
REFRESH_TOKEN);
215+
REFRESH_TOKEN,
216+
IMMUTABLE_SCOPES_LIST);
216217
ImpersonatedCredentials credentials =
217218
ImpersonatedCredentials.fromJson(json, mockTransportFactory);
218219
assertEquals(IMPERSONATED_CLIENT_EMAIL, credentials.getAccount());
219220
assertEquals(IMPERSONATION_OVERRIDE_URL, credentials.getIamEndpointOverride());
220221
assertNull(credentials.getQuotaProjectId());
221222
assertEquals(DELEGATES, credentials.getDelegates());
222-
assertEquals(new ArrayList<String>(), credentials.getScopes());
223+
assertEquals(IMMUTABLE_SCOPES_LIST, credentials.getScopes());
223224
assertEquals(3600, credentials.getLifetime());
224225
GoogleCredentials sourceCredentials = credentials.getSourceCredentials();
225226
assertTrue(sourceCredentials instanceof UserCredentials);
@@ -234,15 +235,16 @@ public void fromJson_userAsSource_MissingDelegatesField() throws IOException {
234235
null,
235236
USER_ACCOUNT_CLIENT_ID,
236237
USER_ACCOUNT_CLIENT_SECRET,
237-
REFRESH_TOKEN);
238+
REFRESH_TOKEN,
239+
IMMUTABLE_SCOPES_LIST);
238240
json.remove("delegates");
239241
ImpersonatedCredentials credentials =
240242
ImpersonatedCredentials.fromJson(json, mockTransportFactory);
241243
assertEquals(IMPERSONATED_CLIENT_EMAIL, credentials.getAccount());
242244
assertEquals(IMPERSONATION_OVERRIDE_URL, credentials.getIamEndpointOverride());
243245
assertNull(credentials.getQuotaProjectId());
244246
assertEquals(new ArrayList<String>(), credentials.getDelegates());
245-
assertEquals(new ArrayList<String>(), credentials.getScopes());
247+
assertEquals(IMMUTABLE_SCOPES_LIST, credentials.getScopes());
246248
assertEquals(3600, credentials.getLifetime());
247249
GoogleCredentials sourceCredentials = credentials.getSourceCredentials();
248250
assertTrue(sourceCredentials instanceof UserCredentials);
@@ -251,14 +253,15 @@ public void fromJson_userAsSource_MissingDelegatesField() throws IOException {
251253
@Test()
252254
public void fromJson_ServiceAccountAsSource() throws IOException {
253255
GenericJson json =
254-
buildImpersonationCredentialsJson(IMPERSONATION_OVERRIDE_URL, DELEGATES, QUOTA_PROJECT_ID);
256+
buildImpersonationCredentialsJson(
257+
IMPERSONATION_OVERRIDE_URL, DELEGATES, QUOTA_PROJECT_ID, IMMUTABLE_SCOPES_LIST);
255258
ImpersonatedCredentials credentials =
256259
ImpersonatedCredentials.fromJson(json, mockTransportFactory);
257260
assertEquals(IMPERSONATED_CLIENT_EMAIL, credentials.getAccount());
258261
assertEquals(IMPERSONATION_OVERRIDE_URL, credentials.getIamEndpointOverride());
259262
assertEquals(QUOTA_PROJECT_ID, credentials.getQuotaProjectId());
260263
assertEquals(DELEGATES, credentials.getDelegates());
261-
assertEquals(new ArrayList<String>(), credentials.getScopes());
264+
assertEquals(IMMUTABLE_SCOPES_LIST, credentials.getScopes());
262265
assertEquals(3600, credentials.getLifetime());
263266
GoogleCredentials sourceCredentials = credentials.getSourceCredentials();
264267
assertTrue(sourceCredentials instanceof ServiceAccountCredentials);
@@ -481,18 +484,11 @@ public void credential_with_invalid_lifetime() throws IOException, IllegalStateE
481484

482485
@Test()
483486
public void credential_with_invalid_scope() throws IOException, IllegalStateException {
484-
485-
try {
486-
ImpersonatedCredentials targetCredentials =
487-
ImpersonatedCredentials.create(
488-
sourceCredentials, IMPERSONATED_CLIENT_EMAIL, null, null, VALID_LIFETIME);
489-
targetCredentials.refreshAccessToken().getTokenValue();
490-
fail(
491-
String.format(
492-
"Should throw exception with message containing '%s'", "Scopes cannot be null"));
493-
} catch (IllegalStateException expected) {
494-
assertTrue(expected.getMessage().contains("Scopes cannot be null"));
495-
}
487+
assertThrows(
488+
NullPointerException.class,
489+
() ->
490+
ImpersonatedCredentials.create(
491+
sourceCredentials, IMPERSONATED_CLIENT_EMAIL, null, null, VALID_LIFETIME));
496492
}
497493

498494
@Test()
@@ -1221,6 +1217,42 @@ public void universeDomain_whenExplicit_AllowedIfMatchesSourceUD() throws IOExce
12211217
assertTrue(impersonatedCredentials.isExplicitUniverseDomain());
12221218
}
12231219

1220+
@Test
1221+
public void scopes_userConfigured() {
1222+
ImpersonatedCredentials impersonatedCredentials =
1223+
ImpersonatedCredentials.newBuilder().setScopes(IMMUTABLE_SCOPES_LIST).build();
1224+
assertArrayEquals(
1225+
IMMUTABLE_SCOPES_LIST.toArray(), impersonatedCredentials.getScopes().toArray());
1226+
}
1227+
1228+
@Test
1229+
public void scopes_fromJson() throws IOException {
1230+
ImpersonatedCredentials impersonatedCredentials =
1231+
ImpersonatedCredentials.fromJson(
1232+
buildImpersonationCredentialsJson(
1233+
DEFAULT_IMPERSONATION_URL, DELEGATES, null, IMMUTABLE_SCOPES_LIST),
1234+
mockTransportFactory);
1235+
assertArrayEquals(
1236+
IMMUTABLE_SCOPES_LIST.toArray(), impersonatedCredentials.getScopes().toArray());
1237+
}
1238+
1239+
// Tests that user configured scopes has precedence over the one in the json.
1240+
// From the ADC flow, the json is parsed and the credential is returned back
1241+
// to the user
1242+
@Test
1243+
public void scopes_userConfiguredAndFromJson() throws IOException {
1244+
List<String> userConfiguredScopes = ImmutableList.of("nonsense-scopes");
1245+
ImpersonatedCredentials impersonatedCredentials =
1246+
ImpersonatedCredentials.fromJson(
1247+
buildImpersonationCredentialsJson(
1248+
DEFAULT_IMPERSONATION_URL, DELEGATES, null, IMMUTABLE_SCOPES_LIST),
1249+
mockTransportFactory);
1250+
ImpersonatedCredentials newImpersonatedCredentials =
1251+
impersonatedCredentials.toBuilder().setScopes(userConfiguredScopes).build();
1252+
assertArrayEquals(
1253+
userConfiguredScopes.toArray(), newImpersonatedCredentials.getScopes().toArray());
1254+
}
1255+
12241256
@Test
12251257
public void hashCode_equals() throws IOException {
12261258
mockTransportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL);
@@ -1333,7 +1365,8 @@ static GenericJson buildImpersonationCredentialsJson(
13331365
String quotaProjectId,
13341366
String sourceClientId,
13351367
String sourceClientSecret,
1336-
String sourceRefreshToken) {
1368+
String sourceRefreshToken,
1369+
List<String> scopes) {
13371370
GenericJson sourceJson = new GenericJson();
13381371

13391372
sourceJson.put("client_id", sourceClientId);
@@ -1348,12 +1381,13 @@ static GenericJson buildImpersonationCredentialsJson(
13481381
json.put("quota_project_id", quotaProjectId);
13491382
}
13501383
json.put("source_credentials", sourceJson);
1384+
json.put("scopes", scopes);
13511385
json.put("type", "impersonated_service_account");
13521386
return json;
13531387
}
13541388

13551389
static GenericJson buildImpersonationCredentialsJson(
1356-
String impersonationUrl, List<String> delegates, String quotaProjectId) {
1390+
String impersonationUrl, List<String> delegates, String quotaProjectId, List<String> scopes) {
13571391
GenericJson sourceJson = new GenericJson();
13581392
sourceJson.put("type", "service_account");
13591393
sourceJson.put("project_id", PROJECT_ID);
@@ -1375,6 +1409,7 @@ static GenericJson buildImpersonationCredentialsJson(
13751409
if (quotaProjectId != null) {
13761410
json.put("quota_project_id", quotaProjectId);
13771411
}
1412+
json.put("scopes", scopes);
13781413
json.put("type", "impersonated_service_account");
13791414
return json;
13801415
}
@@ -1386,9 +1421,10 @@ static GenericJson buildInvalidCredentialsJson() {
13861421
}
13871422

13881423
static InputStream writeImpersonationCredentialsStream(
1389-
String impersonationUrl, List<String> delegates, String quotaProjectId) throws IOException {
1424+
String impersonationUrl, List<String> delegates, String quotaProjectId, List<String> scopes)
1425+
throws IOException {
13901426
GenericJson json =
1391-
buildImpersonationCredentialsJson(impersonationUrl, delegates, quotaProjectId);
1427+
buildImpersonationCredentialsJson(impersonationUrl, delegates, quotaProjectId, scopes);
13921428
return TestUtils.jsonToInputStream(json);
13931429
}
13941430
}

0 commit comments

Comments
 (0)