-
Notifications
You must be signed in to change notification settings - Fork 273
fix: ImpersonatedCredentials does not use Calendar for expiration #1908
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
Changes from all commits
c8cf184
ca1c4f4
26e5e76
0061793
e30ed90
31602c7
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 |
|---|---|---|
|
|
@@ -44,6 +44,7 @@ | |
| import static org.mockito.Mockito.when; | ||
|
|
||
| import com.google.api.client.http.HttpStatusCodes; | ||
| import com.google.api.client.http.HttpTransport; | ||
| import com.google.api.client.json.GenericJson; | ||
| import com.google.api.client.json.JsonFactory; | ||
| import com.google.api.client.json.JsonGenerator; | ||
|
|
@@ -54,23 +55,22 @@ | |
| import com.google.auth.Credentials; | ||
| import com.google.auth.ServiceAccountSigner.SigningException; | ||
| import com.google.auth.TestUtils; | ||
| import com.google.auth.http.HttpTransportFactory; | ||
| import com.google.common.collect.ImmutableList; | ||
| import com.google.common.collect.ImmutableSet; | ||
| import java.io.ByteArrayOutputStream; | ||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import java.nio.charset.Charset; | ||
| import java.security.PrivateKey; | ||
| import java.text.DateFormat; | ||
| import java.text.SimpleDateFormat; | ||
| import java.time.Instant; | ||
| import java.time.temporal.ChronoUnit; | ||
| import java.util.ArrayList; | ||
| import java.util.Arrays; | ||
| import java.util.Calendar; | ||
| import java.util.Date; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.TimeZone; | ||
| import org.junit.jupiter.api.BeforeEach; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
|
|
@@ -122,8 +122,6 @@ | |
| private static final int INVALID_LIFETIME = 43210; | ||
| private static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance(); | ||
|
|
||
| private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ssX"; | ||
|
|
||
| private static final String TEST_UNIVERSE_DOMAIN = "test.xyz"; | ||
| private static final String OLD_IMPERSONATION_URL = | ||
| "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/" | ||
|
|
@@ -480,7 +478,7 @@ | |
| } | ||
|
|
||
| @Test() | ||
| void credential_with_invalid_scope() throws IOException, IllegalStateException { | ||
|
Check warning on line 481 in oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java
|
||
| assertThrows( | ||
| NullPointerException.class, | ||
| () -> | ||
|
|
@@ -653,61 +651,9 @@ | |
| assertEquals(ACCESS_TOKEN, targetCredentials.refreshAccessToken().getTokenValue()); | ||
| } | ||
|
|
||
| @Test | ||
| void refreshAccessToken_GMT_dateParsedCorrectly() throws IOException, IllegalStateException { | ||
| Calendar c = Calendar.getInstance(); | ||
| c.add(Calendar.SECOND, VALID_LIFETIME); | ||
|
|
||
| mockTransportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL); | ||
| mockTransportFactory.getTransport().setAccessToken(ACCESS_TOKEN); | ||
| mockTransportFactory.getTransport().setExpireTime(getFormattedTime(c.getTime())); | ||
| mockTransportFactory.getTransport().addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, ""); | ||
|
Comment on lines
-656
to
-664
Member
Author
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. Removed old tests that reference the obsolete api (setting custom calendar)
Contributor
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. qq: Are there still use cases where calendar may be used without serializing and deserializing the credentials?
Member
Author
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. not as far as I know. A code search through the repo shows that it's really only used for computing the expire time with the correct timezone offset. |
||
| ImpersonatedCredentials targetCredentials = | ||
| ImpersonatedCredentials.create( | ||
| sourceCredentials, | ||
| IMPERSONATED_CLIENT_EMAIL, | ||
| null, | ||
| IMMUTABLE_SCOPES_LIST, | ||
| VALID_LIFETIME, | ||
| mockTransportFactory) | ||
| .createWithCustomCalendar( | ||
| // Set system timezone to GMT | ||
| Calendar.getInstance(TimeZone.getTimeZone("GMT"))); | ||
|
|
||
| assertTrue( | ||
| c.getTime().toInstant().truncatedTo(ChronoUnit.SECONDS).toEpochMilli() | ||
| == targetCredentials.refreshAccessToken().getExpirationTimeMillis()); | ||
| } | ||
|
|
||
| @Test | ||
| void refreshAccessToken_nonGMT_dateParsedCorrectly() throws IOException, IllegalStateException { | ||
| Calendar c = Calendar.getInstance(); | ||
| c.add(Calendar.SECOND, VALID_LIFETIME); | ||
|
|
||
| mockTransportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL); | ||
| mockTransportFactory.getTransport().setAccessToken(ACCESS_TOKEN); | ||
| mockTransportFactory.getTransport().setExpireTime(getFormattedTime(c.getTime())); | ||
| mockTransportFactory.getTransport().addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, ""); | ||
| ImpersonatedCredentials targetCredentials = | ||
| ImpersonatedCredentials.create( | ||
| sourceCredentials, | ||
| IMPERSONATED_CLIENT_EMAIL, | ||
| null, | ||
| IMMUTABLE_SCOPES_LIST, | ||
| VALID_LIFETIME, | ||
| mockTransportFactory) | ||
| .createWithCustomCalendar( | ||
| // Set system timezone to one different than GMT | ||
| Calendar.getInstance(TimeZone.getTimeZone("America/Los_Angeles"))); | ||
|
|
||
| assertTrue( | ||
| c.getTime().toInstant().truncatedTo(ChronoUnit.SECONDS).toEpochMilli() | ||
| == targetCredentials.refreshAccessToken().getExpirationTimeMillis()); | ||
| } | ||
|
|
||
| @Test | ||
| void refreshAccessToken_invalidDate() throws IllegalStateException { | ||
| String expectedMessage = "Unparseable date"; | ||
| String expectedMessage = "Error parsing expireTime: "; | ||
| mockTransportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL); | ||
| mockTransportFactory.getTransport().setAccessToken("foo"); | ||
| mockTransportFactory.getTransport().setExpireTime("1973-09-29T15:01:23"); | ||
|
|
@@ -1169,7 +1115,7 @@ | |
| } | ||
|
|
||
| @Test | ||
| void universeDomain_whenExplicit_notAllowedIfNotMatchToSourceUD() throws IOException { | ||
|
Check warning on line 1118 in oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java
|
||
| GoogleCredentials sourceCredentialsNonGDU = | ||
| sourceCredentials.toBuilder().setUniverseDomain("source.domain.xyz").build(); | ||
| IllegalStateException illegalStateException = | ||
|
|
@@ -1249,7 +1195,7 @@ | |
| } | ||
|
|
||
| @Test | ||
| void hashCode_equals() throws IOException { | ||
|
Check warning on line 1198 in oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java
|
||
| mockTransportFactory.getTransport().setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL); | ||
| mockTransportFactory.getTransport().setAccessToken(ACCESS_TOKEN); | ||
| mockTransportFactory.getTransport().setExpireTime(getDefaultExpireTime()); | ||
|
|
@@ -1297,22 +1243,69 @@ | |
| assertSame(deserializedCredentials.clock, Clock.SYSTEM); | ||
| } | ||
|
|
||
| public static String getDefaultExpireTime() { | ||
| Calendar c = Calendar.getInstance(); | ||
| c.add(Calendar.SECOND, VALID_LIFETIME); | ||
| return getFormattedTime(c.getTime()); | ||
| } | ||
|
|
||
| /** | ||
| * Given a {@link Date}, it will return a string of the date formatted like | ||
| * <b>yyyy-MM-dd'T'HH:mm:ss'Z'</b> | ||
| * A stateful {@link HttpTransportFactory} that provides a shared {@link | ||
| * MockIAMCredentialsServiceTransport} instance. | ||
| * | ||
| * <p>This is necessary for serialization tests because {@link ImpersonatedCredentials} stores the | ||
| * factory's class name and re-instantiates it via reflection during deserialization. A standard | ||
| * factory would create a fresh, unconfigured transport upon re-instantiation, causing refreshed | ||
| * token requests to fail. Using a static transport ensures the mock configuration persists across | ||
| * serialization boundaries. | ||
| */ | ||
| private static String getFormattedTime(final Date date) { | ||
| // Set timezone to GMT since that's the TZ used in the response from the service impersonation | ||
| // token exchange | ||
| final DateFormat formatter = new SimpleDateFormat(RFC3339); | ||
| formatter.setTimeZone(TimeZone.getTimeZone("GMT")); | ||
| return formatter.format(date); | ||
| public static class StatefulMockIAMTransportFactory implements HttpTransportFactory { | ||
| private static final MockIAMCredentialsServiceTransport TRANSPORT = | ||
| new MockIAMCredentialsServiceTransport(GoogleCredentials.GOOGLE_DEFAULT_UNIVERSE); | ||
|
|
||
| @Override | ||
| public HttpTransport create() { | ||
| return TRANSPORT; | ||
| } | ||
|
|
||
| public static MockIAMCredentialsServiceTransport getTransport() { | ||
| return TRANSPORT; | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| void refreshAccessToken_afterSerialization_success() throws IOException, ClassNotFoundException { | ||
| // This test ensures that credentials can still refresh after being serialized. | ||
| // ImpersonatedCredentials only serializes the transport factory's class name. | ||
| // Upon deserialization, it creates a new instance of that factory via reflection. | ||
| // StatefulMockIAMTransportFactory uses a static transport instance so that the | ||
| // configuration we set here (token, expiration) is available to the new factory instance. | ||
| MockIAMCredentialsServiceTransport transport = StatefulMockIAMTransportFactory.getTransport(); | ||
| transport.setTargetPrincipal(IMPERSONATED_CLIENT_EMAIL); | ||
| transport.setAccessToken(ACCESS_TOKEN); | ||
|
|
||
| transport.setExpireTime(getDefaultExpireTime()); | ||
| transport.addStatusCodeAndMessage(HttpStatusCodes.STATUS_CODE_OK, "", true); | ||
|
|
||
| // Use a source credential that doesn't need refresh | ||
| AccessToken sourceToken = | ||
| new AccessToken("source-token", new Date(System.currentTimeMillis() + 3600000)); | ||
| GoogleCredentials sourceCredentials = GoogleCredentials.create(sourceToken); | ||
|
Check warning on line 1287 in oauth2_http/javatests/com/google/auth/oauth2/ImpersonatedCredentialsTest.java
|
||
|
|
||
| ImpersonatedCredentials targetCredentials = | ||
| ImpersonatedCredentials.create( | ||
| sourceCredentials, | ||
| IMPERSONATED_CLIENT_EMAIL, | ||
| null, | ||
| IMMUTABLE_SCOPES_LIST, | ||
| VALID_LIFETIME, | ||
| new StatefulMockIAMTransportFactory()); | ||
|
|
||
| ImpersonatedCredentials deserializedCredentials = serializeAndDeserialize(targetCredentials); | ||
|
|
||
| // This should not throw NPE. The transient 'calendar' field being null after | ||
| // deserialization is now handled by using java.time.Instant for parsing. | ||
| AccessToken token = deserializedCredentials.refreshAccessToken(); | ||
| assertNotNull(token); | ||
| assertEquals(ACCESS_TOKEN, token.getTokenValue()); | ||
| } | ||
|
|
||
| public static String getDefaultExpireTime() { | ||
| return Instant.now().plusSeconds(VALID_LIFETIME).truncatedTo(ChronoUnit.SECONDS).toString(); | ||
| } | ||
|
|
||
| private String generateErrorJson( | ||
|
|
||
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.
IIUC the only effect of the calendar parameter is to change the formatter's timezone. Does that affect the effective expiration instant in the end, or is it just format?
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.
It should not affect the effective expire time. The expireTime represents a moment in time, however, it may be a moment of time represented in a different time zone. This change is to ensure that we do not assume that it is always in Zulu time (e.g. account for offsets like
+02:00)