Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -36,6 +36,7 @@

import com.google.api.client.json.GenericJson;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import org.jspecify.annotations.NullMarked;
Expand All @@ -49,7 +50,9 @@
* information.</a>
*/
@NullMarked
public final class CredentialAccessBoundary {
public final class CredentialAccessBoundary implements Serializable {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is needed because DownscopedCredentials implements Serializable and it has a non-transient field that references the CredentialAccessBoundary class


private static final long serialVersionUID = 1L;

private static final int RULES_SIZE_LIMIT = 10;

Expand All @@ -63,7 +66,7 @@ public final class CredentialAccessBoundary {
accessBoundaryRules.size() < RULES_SIZE_LIMIT,
String.format(
"The provided list has more than %s access boundary rules.", RULES_SIZE_LIMIT));
this.accessBoundaryRules = accessBoundaryRules;
this.accessBoundaryRules = new ArrayList<>(accessBoundaryRules);
}

/**
Expand Down Expand Up @@ -161,7 +164,9 @@ public CredentialAccessBoundary build() {
* .build();
* </code></pre>
*/
public static final class AccessBoundaryRule {
public static final class AccessBoundaryRule implements Serializable {

private static final long serialVersionUID = 1L;

private final String availableResource;
private final List<String> availablePermissions;
Expand Down Expand Up @@ -293,7 +298,10 @@ public AccessBoundaryRule build() {
* .build();
* </code></pre>
*/
public static final class AvailabilityCondition {
public static final class AvailabilityCondition implements Serializable {

private static final long serialVersionUID = 1L;

private final String expression;

@Nullable private final String title;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
import java.io.ObjectInputStream;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

Expand Down Expand Up @@ -99,11 +100,14 @@
@NullMarked
public final class DownscopedCredentials extends OAuth2Credentials {

private static final long serialVersionUID = 3159528426501345486L;

private final GoogleCredentials sourceCredential;
private final CredentialAccessBoundary credentialAccessBoundary;
private final String universeDomain;
private final String transportFactoryClassName;

private final transient HttpTransportFactory transportFactory;
private transient HttpTransportFactory transportFactory;

private final String tokenExchangeEndpoint;

Expand All @@ -113,6 +117,7 @@ private DownscopedCredentials(Builder builder) {
firstNonNull(
builder.transportFactory,
getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY));
this.transportFactoryClassName = this.transportFactory.getClass().getName();
this.sourceCredential = checkNotNull(builder.sourceCredential);
this.credentialAccessBoundary = checkNotNull(builder.credentialAccessBoundary);

Expand Down Expand Up @@ -199,6 +204,11 @@ HttpTransportFactory getTransportFactory() {
return transportFactory;
}

private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
input.defaultReadObject();
transportFactory = OAuth2Credentials.newInstance(transportFactoryClassName);
}
Comment thread
lqiu96 marked this conversation as resolved.

public static Builder newBuilder() {
return new Builder();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,11 @@
import com.google.api.client.http.HttpResponse;
import com.google.api.client.json.JsonObjectParser;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
import java.io.ObjectInputStream;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

/**
* Provider for retrieving the subject tokens for {@link IdentityPoolCredentials} to exchange for
Expand All @@ -55,7 +58,8 @@ class UrlIdentityPoolSubjectTokenSupplier implements IdentityPoolSubjectTokenSup
private static final long serialVersionUID = 4964578313468011844L;

private final IdentityPoolCredentialSource credentialSource;
private final transient HttpTransportFactory transportFactory;
private final String transportFactoryClassName;
private transient HttpTransportFactory transportFactory;

/**
* Constructor for UrlIdentityPoolSubjectTokenProvider.
Expand All @@ -64,9 +68,12 @@ class UrlIdentityPoolSubjectTokenSupplier implements IdentityPoolSubjectTokenSup
* @param transportFactory the transport factory to use for calling the URL.
*/
UrlIdentityPoolSubjectTokenSupplier(
IdentityPoolCredentialSource credentialSource, HttpTransportFactory transportFactory) {
IdentityPoolCredentialSource credentialSource,
@Nullable HttpTransportFactory transportFactory) {
this.credentialSource = credentialSource;
this.transportFactory = transportFactory;
this.transportFactory =
transportFactory != null ? transportFactory : OAuth2Utils.HTTP_TRANSPORT_FACTORY;
this.transportFactoryClassName = this.transportFactory.getClass().getName();
}

@Override
Expand Down Expand Up @@ -100,4 +107,14 @@ public String getSubjectToken(ExternalAccountSupplierContext context) throws IOE
String.format("Error getting subject token from metadata server: %s", e.getMessage()), e);
}
}

private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
input.defaultReadObject();
transportFactory = OAuth2Credentials.newInstance(transportFactoryClassName);
}
Comment thread
lqiu96 marked this conversation as resolved.

@VisibleForTesting
HttpTransportFactory getTransportFactory() {
return transportFactory;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
import org.junit.jupiter.api.Test;

/** Tests for {@link CredentialAccessBoundary} and encompassing classes. */
class CredentialAccessBoundaryTest {
class CredentialAccessBoundaryTest extends BaseSerializationTest {

@Test
void credentialAccessBoundary() {
Expand Down Expand Up @@ -288,4 +288,29 @@ void availabilityCondition_emptyExpression_throws() {
assertThrows(IllegalArgumentException.class, builder::build);
assertEquals("The provided expression is empty.", exception.getMessage());
}

@Test
void serializeAndDeserialize_success() throws Exception {
AvailabilityCondition availabilityCondition =
AvailabilityCondition.newBuilder()
.setExpression("expression")
.setTitle("title")
.setDescription("description")
.build();

AccessBoundaryRule rule =
AccessBoundaryRule.newBuilder()
.setAvailableResource("resource")
.addAvailablePermission("permission")
.setAvailabilityCondition(availabilityCondition)
.build();

CredentialAccessBoundary cab = CredentialAccessBoundary.newBuilder().addRule(rule).build();

// Verify CredentialAccessBoundary and nested rules/conditions can be serialized and
// deserialized
// so downscoped credentials containing them do not throw NotSerializableException.
CredentialAccessBoundary deserialized = serializeAndDeserialize(cab);
assertEquals(cab.toJson(), deserialized.toJson());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@
import java.io.IOException;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;

/** Tests for {@link DownscopedCredentials}. */
class DownscopedCredentialsTest {
class DownscopedCredentialsTest extends BaseSerializationTest {

private static final String SA_PRIVATE_KEY_PKCS8 =
"-----BEGIN PRIVATE KEY-----\n"
Expand Down Expand Up @@ -81,6 +82,54 @@ public HttpTransport create() {
}
}

public static class StatefulMockStsTransportFactory implements HttpTransportFactory {

private static MockStsTransport transport = new MockStsTransport();

public StatefulMockStsTransportFactory() {}

@Override
public HttpTransport create() {
return transport;
}
}

@Test
void refreshAccessToken_reserialized_success() throws Exception {
StatefulMockStsTransportFactory.transport = new MockStsTransport();
StatefulMockStsTransportFactory transportFactory = new StatefulMockStsTransportFactory();

// Set token expiration to 1 hour (3_600_000 ms) in the future so that
// sourceCredential.refreshIfExpired() considers the source token valid and
// does not attempt an external network call to refresh the source credential.
long oneHourInMillis = TimeUnit.HOURS.toMillis(1);
AccessToken sourceAccessToken =
new AccessToken(
"sourceAccessToken", new Date(System.currentTimeMillis() + oneHourInMillis));
GoogleCredentials sourceCredentials =
((ServiceAccountCredentials) getServiceAccountSourceCredentials(/* canRefresh= */ true))
.toBuilder().setAccessToken(sourceAccessToken).build();

DownscopedCredentials downscopedCredentials =
DownscopedCredentials.newBuilder()
.setSourceCredential(sourceCredentials)
.setCredentialAccessBoundary(CREDENTIAL_ACCESS_BOUNDARY)
.setHttpTransportFactory(transportFactory)
.build();

// Verify deserialization succeeds and reconstructs the transient HTTP transport factory
// so that subsequent token refreshes do not throw a NullPointerException.
DownscopedCredentials deserialized = serializeAndDeserialize(downscopedCredentials);
assertNotNull(deserialized.getTransportFactory());
assertEquals(downscopedCredentials, deserialized);
assertEquals(downscopedCredentials.hashCode(), deserialized.hashCode());

// In-memory mock transport responds with a valid token exchange without network calls.
AccessToken accessToken = deserialized.refreshAccessToken();
assertEquals(
StatefulMockStsTransportFactory.transport.getAccessToken(), accessToken.getTokenValue());
}

@Test
void refreshAccessToken() throws IOException {
MockStsTransportFactory transportFactory = new MockStsTransportFactory();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1016,6 +1016,60 @@ void serialize() throws IOException, ClassNotFoundException {
assertSame(Clock.SYSTEM, deserializedCredentials.clock);
}

@Test
void serialize_urlSourced_refreshesSuccessfully() throws Exception {
StatefulMockExternalAccountCredentialsTransportFactory.transport =
new MockExternalAccountCredentialsTransport();
StatefulMockExternalAccountCredentialsTransportFactory transportFactory =
new StatefulMockExternalAccountCredentialsTransportFactory();

IdentityPoolCredentials testCredentials =
IdentityPoolCredentials.newBuilder(createBaseFileSourcedCredentials())
.setHttpTransportFactory(transportFactory)
.setCredentialSource(
buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl()))
.build();

// Verify deserialization recreates the transient HTTP transport factory inside
// UrlIdentityPoolSubjectTokenSupplier so subject token retrieval and token refresh succeed.
IdentityPoolCredentials deserializedCredentials = serializeAndDeserialize(testCredentials);
assertEquals(testCredentials, deserializedCredentials);

// In-memory mock transport serves both the metadata server subject token and the STS exchange
// response without network calls.
AccessToken accessToken = deserializedCredentials.refreshAccessToken();
assertEquals("accessToken", accessToken.getTokenValue());
}

@Test
void serialize_urlIdentityPoolSubjectTokenSupplier_success() throws Exception {
StatefulMockExternalAccountCredentialsTransportFactory.transport =
new MockExternalAccountCredentialsTransport();
StatefulMockExternalAccountCredentialsTransportFactory transportFactory =
new StatefulMockExternalAccountCredentialsTransportFactory();

IdentityPoolCredentialSource credentialSource =
buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl());
UrlIdentityPoolSubjectTokenSupplier supplier =
new UrlIdentityPoolSubjectTokenSupplier(credentialSource, transportFactory);

// Verify deserialization recreates the transient HTTP transport factory so that subject token
// retrieval succeeds without throwing a NullPointerException.
UrlIdentityPoolSubjectTokenSupplier deserializedSupplier = serializeAndDeserialize(supplier);
assertNotNull(deserializedSupplier.getTransportFactory());

// In-memory mock transport returns the subject token without network calls.
ExternalAccountSupplierContext context =
ExternalAccountSupplierContext.newBuilder()
.setAudience("audience")
.setSubjectTokenType("subjectTokenType")
.build();
String subjectToken = deserializedSupplier.getSubjectToken(context);
assertEquals(
StatefulMockExternalAccountCredentialsTransportFactory.transport.getSubjectToken(),
subjectToken);
}

@Test
void build_withCertificateSource_succeeds() throws Exception {
// Set up credential source for certificate type.
Expand Down Expand Up @@ -1276,6 +1330,20 @@ public HttpTransport create() {
}
}

public static class StatefulMockExternalAccountCredentialsTransportFactory
implements HttpTransportFactory {

private static MockExternalAccountCredentialsTransport transport =
new MockExternalAccountCredentialsTransport();

public StatefulMockExternalAccountCredentialsTransportFactory() {}

@Override
public HttpTransport create() {
return transport;
}
}

private static class TestX509Provider extends X509Provider {
private final KeyStore keyStore;
private final String certificatePath;
Expand Down
Loading