diff --git a/cab-token-generator/java/com/google/auth/credentialaccessboundary/ClientSideCredentialAccessBoundaryFactory.java b/cab-token-generator/java/com/google/auth/credentialaccessboundary/ClientSideCredentialAccessBoundaryFactory.java new file mode 100644 index 000000000..3b5745acc --- /dev/null +++ b/cab-token-generator/java/com/google/auth/credentialaccessboundary/ClientSideCredentialAccessBoundaryFactory.java @@ -0,0 +1,767 @@ +/* + * Copyright 2025, Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.auth.credentialaccessboundary; + +import static com.google.auth.oauth2.OAuth2Credentials.getFromServiceLoader; +import static com.google.auth.oauth2.OAuth2Utils.TOKEN_EXCHANGE_URL_FORMAT; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.util.Clock; +import com.google.auth.Credentials; +import com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto.ClientSideAccessBoundary; +import com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto.ClientSideAccessBoundaryRule; +import com.google.auth.http.HttpTransportFactory; +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.CredentialAccessBoundary; +import com.google.auth.oauth2.CredentialAccessBoundary.AccessBoundaryRule; +import com.google.auth.oauth2.DownscopedCredentials; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.auth.oauth2.OAuth2CredentialsWithRefresh; +import com.google.auth.oauth2.OAuth2Utils; +import com.google.auth.oauth2.StsRequestHandler; +import com.google.auth.oauth2.StsTokenExchangeRequest; +import com.google.auth.oauth2.StsTokenExchangeResponse; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.common.util.concurrent.AbstractFuture; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListenableFutureTask; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.crypto.tink.Aead; +import com.google.crypto.tink.InsecureSecretKeyAccess; +import com.google.crypto.tink.KeysetHandle; +import com.google.crypto.tink.RegistryConfiguration; +import com.google.crypto.tink.TinkProtoKeysetFormat; +import com.google.crypto.tink.aead.AeadConfig; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelOptions; +import dev.cel.common.CelProtoAbstractSyntaxTree; +import dev.cel.common.CelValidationException; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.expr.Expr; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.time.Duration; +import java.util.Base64; +import java.util.Date; +import java.util.List; +import java.util.concurrent.ExecutionException; +import javax.annotation.Nullable; + +/** + * A factory for generating downscoped access tokens using a client-side approach. + * + *

Downscoped tokens enable the ability to downscope, or restrict, the Identity and Access + * Management (IAM) permissions that a short-lived credential can use for accessing Google Cloud + * Storage. This factory allows clients to efficiently generate multiple downscoped tokens locally, + * minimizing calls to the Security Token Service (STS). This client-side approach is particularly + * beneficial when Credential Access Boundary rules change frequently or when many unique downscoped + * tokens are required. For scenarios where rules change infrequently or a single downscoped + * credential is reused many times, the server-side approach using {@link DownscopedCredentials} is + * more appropriate. + * + *

To downscope permissions you must define a {@link CredentialAccessBoundary} which specifies + * the upper bound of permissions that the credential can access. You must also provide a source + * credential which will be used to acquire the downscoped credential. + * + *

The factory can be configured with options such as the {@code refreshMargin} and {@code + * minimumTokenLifetime}. The {@code refreshMargin} controls how far in advance of the underlying + * credentials' expiry a refresh is attempted. The {@code minimumTokenLifetime} ensures that + * generated tokens have a minimum usable lifespan. See the {@link Builder} class for more details + * on these options. + * + *

Usage: + * + *


+ * GoogleCredentials sourceCredentials = GoogleCredentials.getApplicationDefault()
+ *     .createScoped("https://www.googleapis.com/auth/cloud-platform");
+ *
+ * ClientSideCredentialAccessBoundaryFactory factory =
+ *     ClientSideCredentialAccessBoundaryFactory.newBuilder()
+ *         .setSourceCredential(sourceCredentials)
+ *         .build();
+ *
+ * CredentialAccessBoundary.AccessBoundaryRule rule =
+ *     CredentialAccessBoundary.AccessBoundaryRule.newBuilder()
+ *         .setAvailableResource(
+ *             "//storage.googleapis.com/projects/_/buckets/bucket")
+ *         .addAvailablePermission("inRole:roles/storage.objectViewer")
+ *         .build();
+ *
+ * CredentialAccessBoundary credentialAccessBoundary =
+ *     CredentialAccessBoundary.newBuilder().addRule(rule).build();
+ *
+ * AccessToken downscopedAccessToken = factory.generateToken(credentialAccessBoundary);
+ *
+ * OAuth2Credentials credentials = OAuth2Credentials.create(downscopedAccessToken);
+ *
+ * Storage storage = StorageOptions.newBuilder().setCredentials(credentials).build().getService();
+ *
+ * Blob blob = storage.get(BlobId.of("bucket", "object"));
+ * System.out.printf("Blob %s retrieved.", blob.getBlobId());
+ * 
+ * + * Note that {@link OAuth2CredentialsWithRefresh} can instead be used to consume the downscoped + * token, allowing for automatic token refreshes by providing a {@link + * OAuth2CredentialsWithRefresh.OAuth2RefreshHandler}. + */ +public class ClientSideCredentialAccessBoundaryFactory { + static final Duration DEFAULT_REFRESH_MARGIN = Duration.ofMinutes(45); + static final Duration DEFAULT_MINIMUM_TOKEN_LIFETIME = Duration.ofMinutes(30); + private final GoogleCredentials sourceCredential; + private final transient HttpTransportFactory transportFactory; + private final String tokenExchangeEndpoint; + private final Duration minimumTokenLifetime; + private final Duration refreshMargin; + private RefreshTask refreshTask; + private final Object refreshLock = new byte[0]; + private IntermediateCredentials intermediateCredentials = null; + private final Clock clock; + private final CelCompiler celCompiler; + + enum RefreshType { + NONE, + ASYNC, + BLOCKING + } + + private ClientSideCredentialAccessBoundaryFactory(Builder builder) { + this.transportFactory = builder.transportFactory; + this.sourceCredential = builder.sourceCredential; + this.tokenExchangeEndpoint = builder.tokenExchangeEndpoint; + this.refreshMargin = builder.refreshMargin; + this.minimumTokenLifetime = builder.minimumTokenLifetime; + this.clock = builder.clock; + + // Initializes the Tink AEAD registry for encrypting the client-side restrictions. + try { + AeadConfig.register(); + } catch (GeneralSecurityException e) { + throw new IllegalStateException("Error occurred when registering Tink", e); + } + + CelOptions options = CelOptions.current().build(); + this.celCompiler = CelCompilerFactory.standardCelCompilerBuilder().setOptions(options).build(); + } + + /** + * Generates a downscoped access token given the {@link CredentialAccessBoundary}. + * + * @param accessBoundary The credential access boundary that defines the restrictions for the + * generated CAB token. + * @return The downscoped access token in an {@link AccessToken} object + * @throws IOException If an I/O error occurs while refreshing the source credentials + * @throws CelValidationException If the availability condition is an invalid CEL expression + * @throws GeneralSecurityException If an error occurs during encryption + */ + public AccessToken generateToken(CredentialAccessBoundary accessBoundary) + throws IOException, CelValidationException, GeneralSecurityException { + this.refreshCredentialsIfRequired(); + + String intermediateToken; + String sessionKey; + Date intermediateTokenExpirationTime; + + synchronized (refreshLock) { + intermediateToken = this.intermediateCredentials.intermediateAccessToken.getTokenValue(); + intermediateTokenExpirationTime = + this.intermediateCredentials.intermediateAccessToken.getExpirationTime(); + sessionKey = this.intermediateCredentials.accessBoundarySessionKey; + } + + byte[] rawRestrictions = this.serializeCredentialAccessBoundary(accessBoundary); + + byte[] encryptedRestrictions = this.encryptRestrictions(rawRestrictions, sessionKey); + + String tokenValue = + intermediateToken + "." + Base64.getUrlEncoder().encodeToString(encryptedRestrictions); + + return new AccessToken(tokenValue, intermediateTokenExpirationTime); + } + + /** + * Refreshes the intermediate access token and access boundary session key if required. + * + *

This method checks the expiration time of the current intermediate access token and + * initiates a refresh if necessary. The refresh process also refreshes the underlying source + * credentials. + * + * @throws IOException If an error occurs during the refresh process, such as network issues, + * invalid credentials, or problems with the token exchange endpoint. + */ + @VisibleForTesting + void refreshCredentialsIfRequired() throws IOException { + RefreshType refreshType = determineRefreshType(); + + if (refreshType == RefreshType.NONE) { + // No refresh needed, token is still valid. + return; + } + + // If a refresh is required, create or retrieve the refresh task. + RefreshTask currentRefreshTask = getOrCreateRefreshTask(); + + // Handle the refresh based on the determined refresh type. + switch (refreshType) { + case BLOCKING: + if (currentRefreshTask.isNew) { + // Start a new refresh task only if the task is new. + MoreExecutors.directExecutor().execute(currentRefreshTask.task); + } + try { + // Wait for the refresh task to complete. + currentRefreshTask.task.get(); + } catch (InterruptedException e) { + // Restore the interrupted status and throw an exception. + Thread.currentThread().interrupt(); + throw new IOException( + "Interrupted while asynchronously refreshing the intermediate credentials", e); + } catch (ExecutionException e) { + // Unwrap the underlying cause of the execution exception. + Throwable cause = e.getCause(); + if (cause instanceof IOException) { + throw (IOException) cause; + } else if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else { + // Wrap other exceptions in an IOException. + throw new IOException("Unexpected error refreshing intermediate credentials", cause); + } + } + break; + case ASYNC: + if (currentRefreshTask.isNew) { + // Starts a new background thread for the refresh task if it's a new task. + // We create a new thread because the Auth Library doesn't currently include a background + // executor. Introducing an executor would add complexity in managing its lifecycle and + // could potentially lead to memory leaks. + // We limit the number of concurrent refresh threads to 1, so the overhead of creating new + // threads for asynchronous calls should be acceptable. + new Thread(currentRefreshTask.task).start(); + } // (No else needed - if not new, another thread is handling the refresh) + break; + default: + // This should not happen unless RefreshType enum is extended and this method is not + // updated. + throw new IllegalStateException("Unexpected refresh type: " + refreshType); + } + } + + private RefreshType determineRefreshType() { + AccessToken intermediateAccessToken; + synchronized (refreshLock) { + if (intermediateCredentials == null + || intermediateCredentials.intermediateAccessToken == null) { + // A blocking refresh is needed if the intermediate access token doesn't exist. + return RefreshType.BLOCKING; + } + intermediateAccessToken = intermediateCredentials.intermediateAccessToken; + } + + Date expirationTime = intermediateAccessToken.getExpirationTime(); + if (expirationTime == null) { + // Token does not expire, no refresh needed. + return RefreshType.NONE; + } + + Duration remaining = Duration.ofMillis(expirationTime.getTime() - clock.currentTimeMillis()); + + if (remaining.compareTo(minimumTokenLifetime) <= 0) { + // Intermediate token has expired or remaining lifetime is less than the minimum required + // for CAB token generation. A blocking refresh is necessary. + return RefreshType.BLOCKING; + } else if (remaining.compareTo(refreshMargin) <= 0) { + // The token is nearing expiration, an async refresh is needed. + return RefreshType.ASYNC; + } + // Token is still fresh, no refresh needed. + return RefreshType.NONE; + } + + /** + * Atomically creates a single flight refresh task. + * + *

Only a single refresh task can be scheduled at a time. If there is an existing task, it will + * be returned for subsequent invocations. However, if a new task is created, it is the + * responsibility of the caller to execute it. The task will clear the single flight slot upon + * completion. + */ + private RefreshTask getOrCreateRefreshTask() { + synchronized (refreshLock) { + if (refreshTask != null) { + // An existing refresh task is already in progress. Return a NEW RefreshTask instance with + // the existing task, but set isNew to false. This indicates to the caller that a new + // refresh task was NOT created. + return new RefreshTask(refreshTask.task, false); + } + + final ListenableFutureTask task = + ListenableFutureTask.create(this::fetchIntermediateCredentials); + + // Store the new refresh task in the refreshTask field before returning. This ensures that + // subsequent calls to this method will return the existing task while it's still in progress. + refreshTask = new RefreshTask(task, true); + return refreshTask; + } + } + + /** + * Fetches the credentials by refreshing the source credential and exchanging it for an + * intermediate access token using the STS endpoint. + * + *

The source credential is refreshed, and a token exchange request is made to the STS endpoint + * to obtain an intermediate access token and an associated access boundary session key. This + * ensures the intermediate access token meets this factory's refresh margin and minimum lifetime + * requirements. + * + * @return The fetched {@link IntermediateCredentials} containing the intermediate access token + * and access boundary session key. + * @throws IOException If an error occurs during credential refresh or token exchange. + */ + @VisibleForTesting + IntermediateCredentials fetchIntermediateCredentials() throws IOException { + try { + // Force a refresh on the source credentials. The intermediate token's lifetime is tied to the + // source credential's expiration. The factory's refreshMargin might be different from the + // refreshMargin on source credentials. This ensures the intermediate access token + // meets this factory's refresh margin and minimum lifetime requirements. + sourceCredential.refresh(); + } catch (IOException e) { + throw new IOException("Unable to refresh the provided source credential.", e); + } + + AccessToken sourceAccessToken = sourceCredential.getAccessToken(); + if (sourceAccessToken == null || Strings.isNullOrEmpty(sourceAccessToken.getTokenValue())) { + throw new IllegalStateException("The source credential does not have an access token."); + } + + StsTokenExchangeRequest request = + StsTokenExchangeRequest.newBuilder( + sourceAccessToken.getTokenValue(), OAuth2Utils.TOKEN_TYPE_ACCESS_TOKEN) + .setRequestTokenType(OAuth2Utils.TOKEN_TYPE_ACCESS_BOUNDARY_INTERMEDIARY_TOKEN) + .build(); + + StsRequestHandler handler = + StsRequestHandler.newBuilder( + tokenExchangeEndpoint, request, transportFactory.create().createRequestFactory()) + .build(); + + StsTokenExchangeResponse response = handler.exchangeToken(); + return new IntermediateCredentials( + getTokenFromResponse(response, sourceAccessToken), response.getAccessBoundarySessionKey()); + } + + /** + * Extracts the access token from the STS exchange response and sets the appropriate expiration + * time. + * + * @param response The STS token exchange response. + * @param sourceAccessToken The original access token used for the exchange. + * @return The intermediate access token. + */ + private static AccessToken getTokenFromResponse( + StsTokenExchangeResponse response, AccessToken sourceAccessToken) { + AccessToken intermediateToken = response.getAccessToken(); + + // The STS endpoint will only return the expiration time for the intermediate token + // if the original access token represents a service account. + // The intermediate token's expiration time will always match the source credential expiration. + // When no expires_in is returned, we can copy the source credential's expiration time. + if (intermediateToken.getExpirationTime() == null + && sourceAccessToken.getExpirationTime() != null) { + return new AccessToken( + intermediateToken.getTokenValue(), sourceAccessToken.getExpirationTime()); + } + + // Return original if no modification needed. + return intermediateToken; + } + + /** + * Completes the refresh task by storing the results and clearing the single flight slot. + * + *

This method is called when a refresh task finishes. It stores the refreshed credentials if + * successful. The single-flight "slot" is cleared, allowing subsequent refresh attempts. Any + * exceptions during the refresh are caught and suppressed to prevent indefinite blocking of + * subsequent refresh attempts. + */ + private void finishRefreshTask(ListenableFuture finishedTask) + throws ExecutionException { + synchronized (refreshLock) { + try { + this.intermediateCredentials = Futures.getDone(finishedTask); + } finally { + if (this.refreshTask != null && this.refreshTask.task == finishedTask) { + this.refreshTask = null; + } + } + } + } + + @VisibleForTesting + String getTokenExchangeEndpoint() { + return tokenExchangeEndpoint; + } + + @VisibleForTesting + HttpTransportFactory getTransportFactory() { + return transportFactory; + } + + @VisibleForTesting + Duration getRefreshMargin() { + return refreshMargin; + } + + @VisibleForTesting + Duration getMinimumTokenLifetime() { + return minimumTokenLifetime; + } + + /** + * Holds intermediate credentials obtained from the STS token exchange endpoint. + * + *

These credentials include an intermediate access token and an access boundary session key. + */ + @VisibleForTesting + static class IntermediateCredentials { + private final AccessToken intermediateAccessToken; + private final String accessBoundarySessionKey; + + IntermediateCredentials(AccessToken accessToken, String accessBoundarySessionKey) { + this.intermediateAccessToken = accessToken; + this.accessBoundarySessionKey = accessBoundarySessionKey; + } + + String getAccessBoundarySessionKey() { + return accessBoundarySessionKey; + } + + AccessToken getIntermediateAccessToken() { + return intermediateAccessToken; + } + } + + /** + * Represents a task for refreshing intermediate credentials, ensuring that only one refresh + * operation is in progress at a time. + * + *

The {@code isNew} flag indicates whether this is a newly initiated refresh operation or an + * existing one already in progress. This distinction is used to prevent redundant refreshes. + */ + class RefreshTask extends AbstractFuture implements Runnable { + private final ListenableFutureTask task; + final boolean isNew; + + RefreshTask(ListenableFutureTask task, boolean isNew) { + this.task = task; + this.isNew = isNew; + + // Add listener to update factory's credentials when the task completes. + task.addListener( + () -> { + try { + finishRefreshTask(task); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + RefreshTask.this.setException(cause); + } + }, + MoreExecutors.directExecutor()); + + // Add callback to set the result or exception based on the outcome. + Futures.addCallback( + task, + new FutureCallback() { + @Override + public void onSuccess(IntermediateCredentials result) { + RefreshTask.this.set(result); + } + + @Override + public void onFailure(@Nullable Throwable t) { + RefreshTask.this.setException( + t != null ? t : new IOException("Refresh failed with null Throwable.")); + } + }, + MoreExecutors.directExecutor()); + } + + @Override + public void run() { + task.run(); + } + } + + /** Serializes a {@link CredentialAccessBoundary} object into Protobuf wire format. */ + @VisibleForTesting + byte[] serializeCredentialAccessBoundary(CredentialAccessBoundary credentialAccessBoundary) + throws CelValidationException { + List rules = credentialAccessBoundary.getAccessBoundaryRules(); + ClientSideAccessBoundary.Builder accessBoundaryBuilder = ClientSideAccessBoundary.newBuilder(); + + for (AccessBoundaryRule rule : rules) { + ClientSideAccessBoundaryRule.Builder ruleBuilder = + accessBoundaryBuilder + .addAccessBoundaryRulesBuilder() + .addAllAvailablePermissions(rule.getAvailablePermissions()) + .setAvailableResource(rule.getAvailableResource()); + + // Availability condition is an optional field from the CredentialAccessBoundary + // CEL compilation is only performed if there is a non-empty availability condition. + if (rule.getAvailabilityCondition() != null) { + String availabilityCondition = rule.getAvailabilityCondition().getExpression(); + + Expr availabilityConditionExpr = this.compileCel(availabilityCondition); + ruleBuilder.setCompiledAvailabilityCondition(availabilityConditionExpr); + } + } + + return accessBoundaryBuilder.build().toByteArray(); + } + + /** Compiles CEL expression from String to an {@link Expr} proto object. */ + private Expr compileCel(String expr) throws CelValidationException { + CelAbstractSyntaxTree ast = celCompiler.parse(expr).getAst(); + + CelProtoAbstractSyntaxTree astProto = CelProtoAbstractSyntaxTree.fromCelAst(ast); + + return astProto.getExpr(); + } + + /** Encrypts the given bytes using a sessionKey using Tink Aead. */ + private byte[] encryptRestrictions(byte[] restriction, String sessionKey) + throws GeneralSecurityException { + byte[] rawKey; + + try { + rawKey = Base64.getDecoder().decode(sessionKey); + } catch (IllegalArgumentException e) { + // Session key from the server is expected to be Base64 encoded. + throw new IllegalStateException("Session key is not Base64 encoded", e); + } + + KeysetHandle keysetHandle = + TinkProtoKeysetFormat.parseKeyset(rawKey, InsecureSecretKeyAccess.get()); + + Aead aead = keysetHandle.getPrimitive(RegistryConfiguration.get(), Aead.class); + + // For downscoped access token encryption, empty associated data is expected. + // Tink requires a byte[0] to be passed for this case. + return aead.encrypt(restriction, /* associatedData= */ new byte[0]); + } + + public static Builder newBuilder() { + return new Builder(); + } + + /** + * Builder for {@link ClientSideCredentialAccessBoundaryFactory}. + * + *

Use this builder to create instances of {@code ClientSideCredentialAccessBoundaryFactory} + * with the desired configuration options. + */ + public static class Builder { + private GoogleCredentials sourceCredential; + private HttpTransportFactory transportFactory; + private String universeDomain; + private String tokenExchangeEndpoint; + private Duration minimumTokenLifetime; + private Duration refreshMargin; + private Clock clock = Clock.SYSTEM; // Default to system clock; + + private Builder() {} + + /** + * Sets the required source credential. + * + * @param sourceCredential the {@code GoogleCredentials} to set. This is a + * required parameter. + * @return this {@code Builder} object for chaining. + * @throws NullPointerException if {@code sourceCredential} is {@code null}. + */ + @CanIgnoreReturnValue + public Builder setSourceCredential(GoogleCredentials sourceCredential) { + checkNotNull(sourceCredential, "Source credential must not be null."); + this.sourceCredential = sourceCredential; + return this; + } + + /** + * Sets the minimum acceptable lifetime for a generated downscoped access token. + * + *

This parameter ensures that any generated downscoped access token has a minimum validity + * period. If the time remaining before the underlying credentials expire is less than this + * value, the factory will perform a blocking refresh, meaning that it will wait until the + * credentials are refreshed before generating a new downscoped token. This guarantees that the + * generated token will be valid for at least {@code minimumTokenLifetime}. A reasonable value + * should be chosen based on the expected duration of operations using the downscoped token. If + * not set, the default value is defined by {@link #DEFAULT_MINIMUM_TOKEN_LIFETIME}. + * + * @param minimumTokenLifetime The minimum acceptable lifetime for a generated downscoped access + * token. Must be greater than zero. + * @return This {@code Builder} object. + * @throws IllegalArgumentException if {@code minimumTokenLifetime} is negative or zero. + */ + @CanIgnoreReturnValue + public Builder setMinimumTokenLifetime(Duration minimumTokenLifetime) { + checkNotNull(minimumTokenLifetime, "Minimum token lifetime must not be null."); + if (minimumTokenLifetime.isNegative() || minimumTokenLifetime.isZero()) { + throw new IllegalArgumentException("Minimum token lifetime must be greater than zero."); + } + this.minimumTokenLifetime = minimumTokenLifetime; + return this; + } + + /** + * Sets the refresh margin for the underlying credentials. + * + *

This duration specifies how far in advance of the credentials' expiration time an + * asynchronous refresh should be initiated. This refresh happens in the background, without + * blocking the main thread. If not provided, it will default to the value defined by {@link + * #DEFAULT_REFRESH_MARGIN}. + * + *

Note: The {@code refreshMargin} must be at least one minute longer than the {@code + * minimumTokenLifetime}. + * + * @param refreshMargin The refresh margin. Must be greater than zero. + * @return This {@code Builder} object. + * @throws IllegalArgumentException if {@code refreshMargin} is negative or zero. + */ + @CanIgnoreReturnValue + public Builder setRefreshMargin(Duration refreshMargin) { + checkNotNull(refreshMargin, "Refresh margin must not be null."); + if (refreshMargin.isNegative() || refreshMargin.isZero()) { + throw new IllegalArgumentException("Refresh margin must be greater than zero."); + } + this.refreshMargin = refreshMargin; + return this; + } + + /** + * Sets the HTTP transport factory. + * + * @param transportFactory the {@code HttpTransportFactory} to set + * @return this {@code Builder} object + */ + @CanIgnoreReturnValue + public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) { + this.transportFactory = transportFactory; + return this; + } + + /** + * Sets the optional universe domain. + * + * @param universeDomain the universe domain to set + * @return this {@code Builder} object + */ + @CanIgnoreReturnValue + public Builder setUniverseDomain(String universeDomain) { + this.universeDomain = universeDomain; + return this; + } + + /** + * Set the clock for checking token expiry. Used for testing. + * + * @param clock the clock to use. Defaults to the system clock + * @return the builder + */ + public Builder setClock(Clock clock) { + this.clock = clock; + return this; + } + + /** + * Creates a new {@code ClientSideCredentialAccessBoundaryFactory} instance based on the current + * builder configuration. + * + * @return A new {@code ClientSideCredentialAccessBoundaryFactory} instance. + * @throws IllegalStateException if the builder is not properly configured (e.g., if the source + * credential is not set). + * @throws IllegalArgumentException if the refresh margin is not at least one minute longer than + * the minimum token lifetime. + */ + public ClientSideCredentialAccessBoundaryFactory build() { + checkNotNull(sourceCredential, "Source credential must not be null."); + + // Use the default HTTP transport factory if none was provided. + if (transportFactory == null) { + this.transportFactory = + getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY); + } + + // Default to GDU when not supplied. + if (Strings.isNullOrEmpty(universeDomain)) { + this.universeDomain = Credentials.GOOGLE_DEFAULT_UNIVERSE; + } + + // Ensure source credential's universe domain matches. + try { + if (!universeDomain.equals(sourceCredential.getUniverseDomain())) { + throw new IllegalArgumentException( + "The client side access boundary credential's universe domain must be the same as the source " + + "credential."); + } + } catch (IOException e) { + // Throwing an IOException would be a breaking change, so wrap it here. + throw new IllegalStateException( + "Error occurred when attempting to retrieve source credential universe domain.", e); + } + + // Use default values for refreshMargin if not provided. + if (refreshMargin == null) { + this.refreshMargin = DEFAULT_REFRESH_MARGIN; + } + + // Use default values for minimumTokenLifetime if not provided. + if (minimumTokenLifetime == null) { + this.minimumTokenLifetime = DEFAULT_MINIMUM_TOKEN_LIFETIME; + } + + // Check if refreshMargin is at least one minute longer than minimumTokenLifetime. + Duration minRefreshMargin = minimumTokenLifetime.plusMinutes(1); + if (refreshMargin.compareTo(minRefreshMargin) < 0) { + throw new IllegalArgumentException( + "Refresh margin must be at least one minute longer than the minimum token lifetime."); + } + + this.tokenExchangeEndpoint = String.format(TOKEN_EXCHANGE_URL_FORMAT, universeDomain); + return new ClientSideCredentialAccessBoundaryFactory(this); + } + } +} diff --git a/cab-token-generator/java/com/google/auth/credentialaccessboundary/protobuf/ClientSideAccessBoundaryProto.java b/cab-token-generator/java/com/google/auth/credentialaccessboundary/protobuf/ClientSideAccessBoundaryProto.java new file mode 100644 index 000000000..7a26c73c8 --- /dev/null +++ b/cab-token-generator/java/com/google/auth/credentialaccessboundary/protobuf/ClientSideAccessBoundaryProto.java @@ -0,0 +1,2174 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: client_side_access_boundary.proto + +// Protobuf Java Version: 3.25.5 +package com.google.auth.credentialaccessboundary.protobuf; + +public final class ClientSideAccessBoundaryProto { + private ClientSideAccessBoundaryProto() {} + + public static void registerAllExtensions(com.google.protobuf.ExtensionRegistryLite registry) {} + + public static void registerAllExtensions(com.google.protobuf.ExtensionRegistry registry) { + registerAllExtensions((com.google.protobuf.ExtensionRegistryLite) registry); + } + + public interface ClientSideAccessBoundaryRuleOrBuilder + extends + // @@protoc_insertion_point(interface_extends:com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule) + com.google.protobuf.MessageOrBuilder { + + /** + * string available_resource = 1; + * + * @return The availableResource. + */ + java.lang.String getAvailableResource(); + /** + * string available_resource = 1; + * + * @return The bytes for availableResource. + */ + com.google.protobuf.ByteString getAvailableResourceBytes(); + + /** + * repeated string available_permissions = 2; + * + * @return A list containing the availablePermissions. + */ + java.util.List getAvailablePermissionsList(); + /** + * repeated string available_permissions = 2; + * + * @return The count of availablePermissions. + */ + int getAvailablePermissionsCount(); + /** + * repeated string available_permissions = 2; + * + * @param index The index of the element to return. + * @return The availablePermissions at the given index. + */ + java.lang.String getAvailablePermissions(int index); + /** + * repeated string available_permissions = 2; + * + * @param index The index of the value to return. + * @return The bytes of the availablePermissions at the given index. + */ + com.google.protobuf.ByteString getAvailablePermissionsBytes(int index); + + /** + * .cel.expr.Expr compiled_availability_condition = 4; + * + * @return Whether the compiledAvailabilityCondition field is set. + */ + boolean hasCompiledAvailabilityCondition(); + /** + * .cel.expr.Expr compiled_availability_condition = 4; + * + * @return The compiledAvailabilityCondition. + */ + dev.cel.expr.Expr getCompiledAvailabilityCondition(); + /** .cel.expr.Expr compiled_availability_condition = 4; */ + dev.cel.expr.ExprOrBuilder getCompiledAvailabilityConditionOrBuilder(); + } + /** + * Protobuf type {@code + * com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule} + */ + public static final class ClientSideAccessBoundaryRule + extends com.google.protobuf.GeneratedMessageV3 + implements + // @@protoc_insertion_point(message_implements:com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule) + ClientSideAccessBoundaryRuleOrBuilder { + private static final long serialVersionUID = 0L; + // Use ClientSideAccessBoundaryRule.newBuilder() to construct. + private ClientSideAccessBoundaryRule( + com.google.protobuf.GeneratedMessageV3.Builder builder) { + super(builder); + } + + private ClientSideAccessBoundaryRule() { + availableResource_ = ""; + availablePermissions_ = com.google.protobuf.LazyStringArrayList.emptyList(); + } + + @java.lang.Override + @SuppressWarnings({"unused"}) + protected java.lang.Object newInstance(UnusedPrivateParameter unused) { + return new ClientSideAccessBoundaryRule(); + } + + public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() { + return com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .internal_static_com_google_auth_credentialaccessboundary_proto_ClientSideAccessBoundaryRule_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internalGetFieldAccessorTable() { + return com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .internal_static_com_google_auth_credentialaccessboundary_proto_ClientSideAccessBoundaryRule_fieldAccessorTable + .ensureFieldAccessorsInitialized( + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule.class, + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule.Builder.class); + } + + private int bitField0_; + public static final int AVAILABLE_RESOURCE_FIELD_NUMBER = 1; + + @SuppressWarnings("serial") + private volatile java.lang.Object availableResource_ = ""; + /** + * string available_resource = 1; + * + * @return The availableResource. + */ + @java.lang.Override + public java.lang.String getAvailableResource() { + java.lang.Object ref = availableResource_; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + availableResource_ = s; + return s; + } + } + /** + * string available_resource = 1; + * + * @return The bytes for availableResource. + */ + @java.lang.Override + public com.google.protobuf.ByteString getAvailableResourceBytes() { + java.lang.Object ref = availableResource_; + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8((java.lang.String) ref); + availableResource_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + public static final int AVAILABLE_PERMISSIONS_FIELD_NUMBER = 2; + + @SuppressWarnings("serial") + private com.google.protobuf.LazyStringArrayList availablePermissions_ = + com.google.protobuf.LazyStringArrayList.emptyList(); + /** + * repeated string available_permissions = 2; + * + * @return A list containing the availablePermissions. + */ + public com.google.protobuf.ProtocolStringList getAvailablePermissionsList() { + return availablePermissions_; + } + /** + * repeated string available_permissions = 2; + * + * @return The count of availablePermissions. + */ + public int getAvailablePermissionsCount() { + return availablePermissions_.size(); + } + /** + * repeated string available_permissions = 2; + * + * @param index The index of the element to return. + * @return The availablePermissions at the given index. + */ + public java.lang.String getAvailablePermissions(int index) { + return availablePermissions_.get(index); + } + /** + * repeated string available_permissions = 2; + * + * @param index The index of the value to return. + * @return The bytes of the availablePermissions at the given index. + */ + public com.google.protobuf.ByteString getAvailablePermissionsBytes(int index) { + return availablePermissions_.getByteString(index); + } + + public static final int COMPILED_AVAILABILITY_CONDITION_FIELD_NUMBER = 4; + private dev.cel.expr.Expr compiledAvailabilityCondition_; + /** + * .cel.expr.Expr compiled_availability_condition = 4; + * + * @return Whether the compiledAvailabilityCondition field is set. + */ + @java.lang.Override + public boolean hasCompiledAvailabilityCondition() { + return ((bitField0_ & 0x00000001) != 0); + } + /** + * .cel.expr.Expr compiled_availability_condition = 4; + * + * @return The compiledAvailabilityCondition. + */ + @java.lang.Override + public dev.cel.expr.Expr getCompiledAvailabilityCondition() { + return compiledAvailabilityCondition_ == null + ? dev.cel.expr.Expr.getDefaultInstance() + : compiledAvailabilityCondition_; + } + /** .cel.expr.Expr compiled_availability_condition = 4; */ + @java.lang.Override + public dev.cel.expr.ExprOrBuilder getCompiledAvailabilityConditionOrBuilder() { + return compiledAvailabilityCondition_ == null + ? dev.cel.expr.Expr.getDefaultInstance() + : compiledAvailabilityCondition_; + } + + private byte memoizedIsInitialized = -1; + + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) throws java.io.IOException { + if (!com.google.protobuf.GeneratedMessageV3.isStringEmpty(availableResource_)) { + com.google.protobuf.GeneratedMessageV3.writeString(output, 1, availableResource_); + } + for (int i = 0; i < availablePermissions_.size(); i++) { + com.google.protobuf.GeneratedMessageV3.writeString( + output, 2, availablePermissions_.getRaw(i)); + } + if (((bitField0_ & 0x00000001) != 0)) { + output.writeMessage(4, getCompiledAvailabilityCondition()); + } + getUnknownFields().writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + if (!com.google.protobuf.GeneratedMessageV3.isStringEmpty(availableResource_)) { + size += com.google.protobuf.GeneratedMessageV3.computeStringSize(1, availableResource_); + } + { + int dataSize = 0; + for (int i = 0; i < availablePermissions_.size(); i++) { + dataSize += computeStringSizeNoTag(availablePermissions_.getRaw(i)); + } + size += dataSize; + size += 1 * getAvailablePermissionsList().size(); + } + if (((bitField0_ & 0x00000001) != 0)) { + size += + com.google.protobuf.CodedOutputStream.computeMessageSize( + 4, getCompiledAvailabilityCondition()); + } + size += getUnknownFields().getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj + instanceof + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule)) { + return super.equals(obj); + } + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + other = + (com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule) + obj; + + if (!getAvailableResource().equals(other.getAvailableResource())) return false; + if (!getAvailablePermissionsList().equals(other.getAvailablePermissionsList())) return false; + if (hasCompiledAvailabilityCondition() != other.hasCompiledAvailabilityCondition()) + return false; + if (hasCompiledAvailabilityCondition()) { + if (!getCompiledAvailabilityCondition().equals(other.getCompiledAvailabilityCondition())) + return false; + } + if (!getUnknownFields().equals(other.getUnknownFields())) return false; + return true; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + hash = (37 * hash) + AVAILABLE_RESOURCE_FIELD_NUMBER; + hash = (53 * hash) + getAvailableResource().hashCode(); + if (getAvailablePermissionsCount() > 0) { + hash = (37 * hash) + AVAILABLE_PERMISSIONS_FIELD_NUMBER; + hash = (53 * hash) + getAvailablePermissionsList().hashCode(); + } + if (hasCompiledAvailabilityCondition()) { + hash = (37 * hash) + COMPILED_AVAILABILITY_CONDITION_FIELD_NUMBER; + hash = (53 * hash) + getCompiledAvailabilityCondition().hashCode(); + } + hash = (29 * hash) + getUnknownFields().hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + parseFrom(java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + + public static com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + parseFrom( + java.nio.ByteBuffer data, com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + + public static com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + parseFrom(com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + + public static com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + + public static com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + parseFrom(byte[] data) throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + + public static com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + parseFrom(byte[] data, com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + + public static com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + parseFrom(java.io.InputStream input) throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3.parseWithIOException(PARSER, input); + } + + public static com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + parseFrom( + java.io.InputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3.parseWithIOException( + PARSER, input, extensionRegistry); + } + + public static com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + parseDelimitedFrom(java.io.InputStream input) throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3.parseDelimitedWithIOException(PARSER, input); + } + + public static com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + parseDelimitedFrom( + java.io.InputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3.parseDelimitedWithIOException( + PARSER, input, extensionRegistry); + } + + public static com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + parseFrom(com.google.protobuf.CodedInputStream input) throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3.parseWithIOException(PARSER, input); + } + + public static com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3.parseWithIOException( + PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { + return newBuilder(); + } + + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + + public static Builder newBuilder( + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + * Protobuf type {@code + * com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule} + */ + public static final class Builder + extends com.google.protobuf.GeneratedMessageV3.Builder + implements + // @@protoc_insertion_point(builder_implements:com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule) + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRuleOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() { + return com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .internal_static_com_google_auth_credentialaccessboundary_proto_ClientSideAccessBoundaryRule_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internalGetFieldAccessorTable() { + return com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .internal_static_com_google_auth_credentialaccessboundary_proto_ClientSideAccessBoundaryRule_fieldAccessorTable + .ensureFieldAccessorsInitialized( + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule.class, + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule.Builder.class); + } + + // Construct using + // com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto.ClientSideAccessBoundaryRule.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder(com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessageV3.alwaysUseFieldBuilders) { + getCompiledAvailabilityConditionFieldBuilder(); + } + } + + @java.lang.Override + public Builder clear() { + super.clear(); + bitField0_ = 0; + availableResource_ = ""; + availablePermissions_ = com.google.protobuf.LazyStringArrayList.emptyList(); + compiledAvailabilityCondition_ = null; + if (compiledAvailabilityConditionBuilder_ != null) { + compiledAvailabilityConditionBuilder_.dispose(); + compiledAvailabilityConditionBuilder_ = null; + } + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor getDescriptorForType() { + return com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .internal_static_com_google_auth_credentialaccessboundary_proto_ClientSideAccessBoundaryRule_descriptor; + } + + @java.lang.Override + public com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + getDefaultInstanceForType() { + return com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule.getDefaultInstance(); + } + + @java.lang.Override + public com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + build() { + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + buildPartial() { + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + result = + new com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule(this); + if (bitField0_ != 0) { + buildPartial0(result); + } + onBuilt(); + return result; + } + + private void buildPartial0( + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + result) { + int from_bitField0_ = bitField0_; + if (((from_bitField0_ & 0x00000001) != 0)) { + result.availableResource_ = availableResource_; + } + if (((from_bitField0_ & 0x00000002) != 0)) { + availablePermissions_.makeImmutable(); + result.availablePermissions_ = availablePermissions_; + } + int to_bitField0_ = 0; + if (((from_bitField0_ & 0x00000004) != 0)) { + result.compiledAvailabilityCondition_ = + compiledAvailabilityConditionBuilder_ == null + ? compiledAvailabilityCondition_ + : compiledAvailabilityConditionBuilder_.build(); + to_bitField0_ |= 0x00000001; + } + result.bitField0_ |= to_bitField0_; + } + + @java.lang.Override + public Builder clone() { + return super.clone(); + } + + @java.lang.Override + public Builder setField( + com.google.protobuf.Descriptors.FieldDescriptor field, java.lang.Object value) { + return super.setField(field, value); + } + + @java.lang.Override + public Builder clearField(com.google.protobuf.Descriptors.FieldDescriptor field) { + return super.clearField(field); + } + + @java.lang.Override + public Builder clearOneof(com.google.protobuf.Descriptors.OneofDescriptor oneof) { + return super.clearOneof(oneof); + } + + @java.lang.Override + public Builder setRepeatedField( + com.google.protobuf.Descriptors.FieldDescriptor field, + int index, + java.lang.Object value) { + return super.setRepeatedField(field, index, value); + } + + @java.lang.Override + public Builder addRepeatedField( + com.google.protobuf.Descriptors.FieldDescriptor field, java.lang.Object value) { + return super.addRepeatedField(field, value); + } + + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other + instanceof + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule) { + return mergeFrom( + (com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule) + other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom( + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + other) { + if (other + == com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule.getDefaultInstance()) return this; + if (!other.getAvailableResource().isEmpty()) { + availableResource_ = other.availableResource_; + bitField0_ |= 0x00000001; + onChanged(); + } + if (!other.availablePermissions_.isEmpty()) { + if (availablePermissions_.isEmpty()) { + availablePermissions_ = other.availablePermissions_; + bitField0_ |= 0x00000002; + } else { + ensureAvailablePermissionsIsMutable(); + availablePermissions_.addAll(other.availablePermissions_); + } + onChanged(); + } + if (other.hasCompiledAvailabilityCondition()) { + mergeCompiledAvailabilityCondition(other.getCompiledAvailabilityCondition()); + } + this.mergeUnknownFields(other.getUnknownFields()); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + case 10: + { + availableResource_ = input.readStringRequireUtf8(); + bitField0_ |= 0x00000001; + break; + } // case 10 + case 18: + { + java.lang.String s = input.readStringRequireUtf8(); + ensureAvailablePermissionsIsMutable(); + availablePermissions_.add(s); + break; + } // case 18 + case 34: + { + input.readMessage( + getCompiledAvailabilityConditionFieldBuilder().getBuilder(), + extensionRegistry); + bitField0_ |= 0x00000004; + break; + } // case 34 + default: + { + if (!super.parseUnknownField(input, extensionRegistry, tag)) { + done = true; // was an endgroup tag + } + break; + } // default: + } // switch (tag) + } // while (!done) + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.unwrapIOException(); + } finally { + onChanged(); + } // finally + return this; + } + + private int bitField0_; + + private java.lang.Object availableResource_ = ""; + /** + * string available_resource = 1; + * + * @return The availableResource. + */ + public java.lang.String getAvailableResource() { + java.lang.Object ref = availableResource_; + if (!(ref instanceof java.lang.String)) { + com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + availableResource_ = s; + return s; + } else { + return (java.lang.String) ref; + } + } + /** + * string available_resource = 1; + * + * @return The bytes for availableResource. + */ + public com.google.protobuf.ByteString getAvailableResourceBytes() { + java.lang.Object ref = availableResource_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8((java.lang.String) ref); + availableResource_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + * string available_resource = 1; + * + * @param value The availableResource to set. + * @return This builder for chaining. + */ + public Builder setAvailableResource(java.lang.String value) { + if (value == null) { + throw new NullPointerException(); + } + availableResource_ = value; + bitField0_ |= 0x00000001; + onChanged(); + return this; + } + /** + * string available_resource = 1; + * + * @return This builder for chaining. + */ + public Builder clearAvailableResource() { + availableResource_ = getDefaultInstance().getAvailableResource(); + bitField0_ = (bitField0_ & ~0x00000001); + onChanged(); + return this; + } + /** + * string available_resource = 1; + * + * @param value The bytes for availableResource to set. + * @return This builder for chaining. + */ + public Builder setAvailableResourceBytes(com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + checkByteStringIsUtf8(value); + availableResource_ = value; + bitField0_ |= 0x00000001; + onChanged(); + return this; + } + + private com.google.protobuf.LazyStringArrayList availablePermissions_ = + com.google.protobuf.LazyStringArrayList.emptyList(); + + private void ensureAvailablePermissionsIsMutable() { + if (!availablePermissions_.isModifiable()) { + availablePermissions_ = + new com.google.protobuf.LazyStringArrayList(availablePermissions_); + } + bitField0_ |= 0x00000002; + } + /** + * repeated string available_permissions = 2; + * + * @return A list containing the availablePermissions. + */ + public com.google.protobuf.ProtocolStringList getAvailablePermissionsList() { + availablePermissions_.makeImmutable(); + return availablePermissions_; + } + /** + * repeated string available_permissions = 2; + * + * @return The count of availablePermissions. + */ + public int getAvailablePermissionsCount() { + return availablePermissions_.size(); + } + /** + * repeated string available_permissions = 2; + * + * @param index The index of the element to return. + * @return The availablePermissions at the given index. + */ + public java.lang.String getAvailablePermissions(int index) { + return availablePermissions_.get(index); + } + /** + * repeated string available_permissions = 2; + * + * @param index The index of the value to return. + * @return The bytes of the availablePermissions at the given index. + */ + public com.google.protobuf.ByteString getAvailablePermissionsBytes(int index) { + return availablePermissions_.getByteString(index); + } + /** + * repeated string available_permissions = 2; + * + * @param index The index to set the value at. + * @param value The availablePermissions to set. + * @return This builder for chaining. + */ + public Builder setAvailablePermissions(int index, java.lang.String value) { + if (value == null) { + throw new NullPointerException(); + } + ensureAvailablePermissionsIsMutable(); + availablePermissions_.set(index, value); + bitField0_ |= 0x00000002; + onChanged(); + return this; + } + /** + * repeated string available_permissions = 2; + * + * @param value The availablePermissions to add. + * @return This builder for chaining. + */ + public Builder addAvailablePermissions(java.lang.String value) { + if (value == null) { + throw new NullPointerException(); + } + ensureAvailablePermissionsIsMutable(); + availablePermissions_.add(value); + bitField0_ |= 0x00000002; + onChanged(); + return this; + } + /** + * repeated string available_permissions = 2; + * + * @param values The availablePermissions to add. + * @return This builder for chaining. + */ + public Builder addAllAvailablePermissions(java.lang.Iterable values) { + ensureAvailablePermissionsIsMutable(); + com.google.protobuf.AbstractMessageLite.Builder.addAll(values, availablePermissions_); + bitField0_ |= 0x00000002; + onChanged(); + return this; + } + /** + * repeated string available_permissions = 2; + * + * @return This builder for chaining. + */ + public Builder clearAvailablePermissions() { + availablePermissions_ = com.google.protobuf.LazyStringArrayList.emptyList(); + bitField0_ = (bitField0_ & ~0x00000002); + ; + onChanged(); + return this; + } + /** + * repeated string available_permissions = 2; + * + * @param value The bytes of the availablePermissions to add. + * @return This builder for chaining. + */ + public Builder addAvailablePermissionsBytes(com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + checkByteStringIsUtf8(value); + ensureAvailablePermissionsIsMutable(); + availablePermissions_.add(value); + bitField0_ |= 0x00000002; + onChanged(); + return this; + } + + private dev.cel.expr.Expr compiledAvailabilityCondition_; + private com.google.protobuf.SingleFieldBuilderV3< + dev.cel.expr.Expr, dev.cel.expr.Expr.Builder, dev.cel.expr.ExprOrBuilder> + compiledAvailabilityConditionBuilder_; + /** + * .cel.expr.Expr compiled_availability_condition = 4; + * + * @return Whether the compiledAvailabilityCondition field is set. + */ + public boolean hasCompiledAvailabilityCondition() { + return ((bitField0_ & 0x00000004) != 0); + } + /** + * .cel.expr.Expr compiled_availability_condition = 4; + * + * @return The compiledAvailabilityCondition. + */ + public dev.cel.expr.Expr getCompiledAvailabilityCondition() { + if (compiledAvailabilityConditionBuilder_ == null) { + return compiledAvailabilityCondition_ == null + ? dev.cel.expr.Expr.getDefaultInstance() + : compiledAvailabilityCondition_; + } else { + return compiledAvailabilityConditionBuilder_.getMessage(); + } + } + /** .cel.expr.Expr compiled_availability_condition = 4; */ + public Builder setCompiledAvailabilityCondition(dev.cel.expr.Expr value) { + if (compiledAvailabilityConditionBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + compiledAvailabilityCondition_ = value; + } else { + compiledAvailabilityConditionBuilder_.setMessage(value); + } + bitField0_ |= 0x00000004; + onChanged(); + return this; + } + /** .cel.expr.Expr compiled_availability_condition = 4; */ + public Builder setCompiledAvailabilityCondition(dev.cel.expr.Expr.Builder builderForValue) { + if (compiledAvailabilityConditionBuilder_ == null) { + compiledAvailabilityCondition_ = builderForValue.build(); + } else { + compiledAvailabilityConditionBuilder_.setMessage(builderForValue.build()); + } + bitField0_ |= 0x00000004; + onChanged(); + return this; + } + /** .cel.expr.Expr compiled_availability_condition = 4; */ + public Builder mergeCompiledAvailabilityCondition(dev.cel.expr.Expr value) { + if (compiledAvailabilityConditionBuilder_ == null) { + if (((bitField0_ & 0x00000004) != 0) + && compiledAvailabilityCondition_ != null + && compiledAvailabilityCondition_ != dev.cel.expr.Expr.getDefaultInstance()) { + getCompiledAvailabilityConditionBuilder().mergeFrom(value); + } else { + compiledAvailabilityCondition_ = value; + } + } else { + compiledAvailabilityConditionBuilder_.mergeFrom(value); + } + if (compiledAvailabilityCondition_ != null) { + bitField0_ |= 0x00000004; + onChanged(); + } + return this; + } + /** .cel.expr.Expr compiled_availability_condition = 4; */ + public Builder clearCompiledAvailabilityCondition() { + bitField0_ = (bitField0_ & ~0x00000004); + compiledAvailabilityCondition_ = null; + if (compiledAvailabilityConditionBuilder_ != null) { + compiledAvailabilityConditionBuilder_.dispose(); + compiledAvailabilityConditionBuilder_ = null; + } + onChanged(); + return this; + } + /** .cel.expr.Expr compiled_availability_condition = 4; */ + public dev.cel.expr.Expr.Builder getCompiledAvailabilityConditionBuilder() { + bitField0_ |= 0x00000004; + onChanged(); + return getCompiledAvailabilityConditionFieldBuilder().getBuilder(); + } + /** .cel.expr.Expr compiled_availability_condition = 4; */ + public dev.cel.expr.ExprOrBuilder getCompiledAvailabilityConditionOrBuilder() { + if (compiledAvailabilityConditionBuilder_ != null) { + return compiledAvailabilityConditionBuilder_.getMessageOrBuilder(); + } else { + return compiledAvailabilityCondition_ == null + ? dev.cel.expr.Expr.getDefaultInstance() + : compiledAvailabilityCondition_; + } + } + /** .cel.expr.Expr compiled_availability_condition = 4; */ + private com.google.protobuf.SingleFieldBuilderV3< + dev.cel.expr.Expr, dev.cel.expr.Expr.Builder, dev.cel.expr.ExprOrBuilder> + getCompiledAvailabilityConditionFieldBuilder() { + if (compiledAvailabilityConditionBuilder_ == null) { + compiledAvailabilityConditionBuilder_ = + new com.google.protobuf.SingleFieldBuilderV3< + dev.cel.expr.Expr, dev.cel.expr.Expr.Builder, dev.cel.expr.ExprOrBuilder>( + getCompiledAvailabilityCondition(), getParentForChildren(), isClean()); + compiledAvailabilityCondition_ = null; + } + return compiledAvailabilityConditionBuilder_; + } + + @java.lang.Override + public final Builder setUnknownFields( + final com.google.protobuf.UnknownFieldSet unknownFields) { + return super.setUnknownFields(unknownFields); + } + + @java.lang.Override + public final Builder mergeUnknownFields( + final com.google.protobuf.UnknownFieldSet unknownFields) { + return super.mergeUnknownFields(unknownFields); + } + + // @@protoc_insertion_point(builder_scope:com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule) + } + + // @@protoc_insertion_point(class_scope:com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule) + private static final com.google.auth.credentialaccessboundary.protobuf + .ClientSideAccessBoundaryProto.ClientSideAccessBoundaryRule + DEFAULT_INSTANCE; + + static { + DEFAULT_INSTANCE = + new com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule(); + } + + public static com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser PARSER = + new com.google.protobuf.AbstractParser() { + @java.lang.Override + public ClientSideAccessBoundaryRule parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + Builder builder = newBuilder(); + try { + builder.mergeFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(builder.buildPartial()); + } catch (com.google.protobuf.UninitializedMessageException e) { + throw e.asInvalidProtocolBufferException() + .setUnfinishedMessage(builder.buildPartial()); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException(e) + .setUnfinishedMessage(builder.buildPartial()); + } + return builder.buildPartial(); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + } + + public interface ClientSideAccessBoundaryOrBuilder + extends + // @@protoc_insertion_point(interface_extends:com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundary) + com.google.protobuf.MessageOrBuilder { + + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + java.util.List< + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule> + getAccessBoundaryRulesList(); + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + getAccessBoundaryRules(int index); + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + int getAccessBoundaryRulesCount(); + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + java.util.List< + ? extends + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRuleOrBuilder> + getAccessBoundaryRulesOrBuilderList(); + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRuleOrBuilder + getAccessBoundaryRulesOrBuilder(int index); + } + /** + * Protobuf type {@code com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundary} + */ + public static final class ClientSideAccessBoundary extends com.google.protobuf.GeneratedMessageV3 + implements + // @@protoc_insertion_point(message_implements:com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundary) + ClientSideAccessBoundaryOrBuilder { + private static final long serialVersionUID = 0L; + // Use ClientSideAccessBoundary.newBuilder() to construct. + private ClientSideAccessBoundary(com.google.protobuf.GeneratedMessageV3.Builder builder) { + super(builder); + } + + private ClientSideAccessBoundary() { + accessBoundaryRules_ = java.util.Collections.emptyList(); + } + + @java.lang.Override + @SuppressWarnings({"unused"}) + protected java.lang.Object newInstance(UnusedPrivateParameter unused) { + return new ClientSideAccessBoundary(); + } + + public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() { + return com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .internal_static_com_google_auth_credentialaccessboundary_proto_ClientSideAccessBoundary_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internalGetFieldAccessorTable() { + return com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .internal_static_com_google_auth_credentialaccessboundary_proto_ClientSideAccessBoundary_fieldAccessorTable + .ensureFieldAccessorsInitialized( + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary.class, + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary.Builder.class); + } + + public static final int ACCESS_BOUNDARY_RULES_FIELD_NUMBER = 1; + + @SuppressWarnings("serial") + private java.util.List< + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule> + accessBoundaryRules_; + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + @java.lang.Override + public java.util.List< + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule> + getAccessBoundaryRulesList() { + return accessBoundaryRules_; + } + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + @java.lang.Override + public java.util.List< + ? extends + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRuleOrBuilder> + getAccessBoundaryRulesOrBuilderList() { + return accessBoundaryRules_; + } + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + @java.lang.Override + public int getAccessBoundaryRulesCount() { + return accessBoundaryRules_.size(); + } + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + @java.lang.Override + public com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + getAccessBoundaryRules(int index) { + return accessBoundaryRules_.get(index); + } + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + @java.lang.Override + public com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRuleOrBuilder + getAccessBoundaryRulesOrBuilder(int index) { + return accessBoundaryRules_.get(index); + } + + private byte memoizedIsInitialized = -1; + + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) throws java.io.IOException { + for (int i = 0; i < accessBoundaryRules_.size(); i++) { + output.writeMessage(1, accessBoundaryRules_.get(i)); + } + getUnknownFields().writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + for (int i = 0; i < accessBoundaryRules_.size(); i++) { + size += + com.google.protobuf.CodedOutputStream.computeMessageSize( + 1, accessBoundaryRules_.get(i)); + } + size += getUnknownFields().getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj + instanceof + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary)) { + return super.equals(obj); + } + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary + other = + (com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary) + obj; + + if (!getAccessBoundaryRulesList().equals(other.getAccessBoundaryRulesList())) return false; + if (!getUnknownFields().equals(other.getUnknownFields())) return false; + return true; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + if (getAccessBoundaryRulesCount() > 0) { + hash = (37 * hash) + ACCESS_BOUNDARY_RULES_FIELD_NUMBER; + hash = (53 * hash) + getAccessBoundaryRulesList().hashCode(); + } + hash = (29 * hash) + getUnknownFields().hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary + parseFrom(java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + + public static com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary + parseFrom( + java.nio.ByteBuffer data, com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + + public static com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary + parseFrom(com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + + public static com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary + parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + + public static com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary + parseFrom(byte[] data) throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + + public static com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary + parseFrom(byte[] data, com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + + public static com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary + parseFrom(java.io.InputStream input) throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3.parseWithIOException(PARSER, input); + } + + public static com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary + parseFrom( + java.io.InputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3.parseWithIOException( + PARSER, input, extensionRegistry); + } + + public static com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary + parseDelimitedFrom(java.io.InputStream input) throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3.parseDelimitedWithIOException(PARSER, input); + } + + public static com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary + parseDelimitedFrom( + java.io.InputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3.parseDelimitedWithIOException( + PARSER, input, extensionRegistry); + } + + public static com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary + parseFrom(com.google.protobuf.CodedInputStream input) throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3.parseWithIOException(PARSER, input); + } + + public static com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary + parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3.parseWithIOException( + PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { + return newBuilder(); + } + + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + + public static Builder newBuilder( + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary + prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + * Protobuf type {@code com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundary} + */ + public static final class Builder + extends com.google.protobuf.GeneratedMessageV3.Builder + implements + // @@protoc_insertion_point(builder_implements:com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundary) + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() { + return com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .internal_static_com_google_auth_credentialaccessboundary_proto_ClientSideAccessBoundary_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internalGetFieldAccessorTable() { + return com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .internal_static_com_google_auth_credentialaccessboundary_proto_ClientSideAccessBoundary_fieldAccessorTable + .ensureFieldAccessorsInitialized( + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary.class, + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary.Builder.class); + } + + // Construct using + // com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto.ClientSideAccessBoundary.newBuilder() + private Builder() {} + + private Builder(com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { + super(parent); + } + + @java.lang.Override + public Builder clear() { + super.clear(); + bitField0_ = 0; + if (accessBoundaryRulesBuilder_ == null) { + accessBoundaryRules_ = java.util.Collections.emptyList(); + } else { + accessBoundaryRules_ = null; + accessBoundaryRulesBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000001); + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor getDescriptorForType() { + return com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .internal_static_com_google_auth_credentialaccessboundary_proto_ClientSideAccessBoundary_descriptor; + } + + @java.lang.Override + public com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary + getDefaultInstanceForType() { + return com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary.getDefaultInstance(); + } + + @java.lang.Override + public com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary + build() { + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary + result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary + buildPartial() { + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary + result = + new com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary(this); + buildPartialRepeatedFields(result); + if (bitField0_ != 0) { + buildPartial0(result); + } + onBuilt(); + return result; + } + + private void buildPartialRepeatedFields( + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary + result) { + if (accessBoundaryRulesBuilder_ == null) { + if (((bitField0_ & 0x00000001) != 0)) { + accessBoundaryRules_ = java.util.Collections.unmodifiableList(accessBoundaryRules_); + bitField0_ = (bitField0_ & ~0x00000001); + } + result.accessBoundaryRules_ = accessBoundaryRules_; + } else { + result.accessBoundaryRules_ = accessBoundaryRulesBuilder_.build(); + } + } + + private void buildPartial0( + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary + result) { + int from_bitField0_ = bitField0_; + } + + @java.lang.Override + public Builder clone() { + return super.clone(); + } + + @java.lang.Override + public Builder setField( + com.google.protobuf.Descriptors.FieldDescriptor field, java.lang.Object value) { + return super.setField(field, value); + } + + @java.lang.Override + public Builder clearField(com.google.protobuf.Descriptors.FieldDescriptor field) { + return super.clearField(field); + } + + @java.lang.Override + public Builder clearOneof(com.google.protobuf.Descriptors.OneofDescriptor oneof) { + return super.clearOneof(oneof); + } + + @java.lang.Override + public Builder setRepeatedField( + com.google.protobuf.Descriptors.FieldDescriptor field, + int index, + java.lang.Object value) { + return super.setRepeatedField(field, index, value); + } + + @java.lang.Override + public Builder addRepeatedField( + com.google.protobuf.Descriptors.FieldDescriptor field, java.lang.Object value) { + return super.addRepeatedField(field, value); + } + + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other + instanceof + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary) { + return mergeFrom( + (com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary) + other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom( + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary + other) { + if (other + == com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary.getDefaultInstance()) return this; + if (accessBoundaryRulesBuilder_ == null) { + if (!other.accessBoundaryRules_.isEmpty()) { + if (accessBoundaryRules_.isEmpty()) { + accessBoundaryRules_ = other.accessBoundaryRules_; + bitField0_ = (bitField0_ & ~0x00000001); + } else { + ensureAccessBoundaryRulesIsMutable(); + accessBoundaryRules_.addAll(other.accessBoundaryRules_); + } + onChanged(); + } + } else { + if (!other.accessBoundaryRules_.isEmpty()) { + if (accessBoundaryRulesBuilder_.isEmpty()) { + accessBoundaryRulesBuilder_.dispose(); + accessBoundaryRulesBuilder_ = null; + accessBoundaryRules_ = other.accessBoundaryRules_; + bitField0_ = (bitField0_ & ~0x00000001); + accessBoundaryRulesBuilder_ = + com.google.protobuf.GeneratedMessageV3.alwaysUseFieldBuilders + ? getAccessBoundaryRulesFieldBuilder() + : null; + } else { + accessBoundaryRulesBuilder_.addAllMessages(other.accessBoundaryRules_); + } + } + } + this.mergeUnknownFields(other.getUnknownFields()); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + case 10: + { + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + m = + input.readMessage( + com.google.auth.credentialaccessboundary.protobuf + .ClientSideAccessBoundaryProto.ClientSideAccessBoundaryRule + .parser(), + extensionRegistry); + if (accessBoundaryRulesBuilder_ == null) { + ensureAccessBoundaryRulesIsMutable(); + accessBoundaryRules_.add(m); + } else { + accessBoundaryRulesBuilder_.addMessage(m); + } + break; + } // case 10 + default: + { + if (!super.parseUnknownField(input, extensionRegistry, tag)) { + done = true; // was an endgroup tag + } + break; + } // default: + } // switch (tag) + } // while (!done) + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.unwrapIOException(); + } finally { + onChanged(); + } // finally + return this; + } + + private int bitField0_; + + private java.util.List< + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule> + accessBoundaryRules_ = java.util.Collections.emptyList(); + + private void ensureAccessBoundaryRulesIsMutable() { + if (!((bitField0_ & 0x00000001) != 0)) { + accessBoundaryRules_ = + new java.util.ArrayList< + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule>(accessBoundaryRules_); + bitField0_ |= 0x00000001; + } + } + + private com.google.protobuf.RepeatedFieldBuilderV3< + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule, + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule.Builder, + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRuleOrBuilder> + accessBoundaryRulesBuilder_; + + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + public java.util.List< + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule> + getAccessBoundaryRulesList() { + if (accessBoundaryRulesBuilder_ == null) { + return java.util.Collections.unmodifiableList(accessBoundaryRules_); + } else { + return accessBoundaryRulesBuilder_.getMessageList(); + } + } + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + public int getAccessBoundaryRulesCount() { + if (accessBoundaryRulesBuilder_ == null) { + return accessBoundaryRules_.size(); + } else { + return accessBoundaryRulesBuilder_.getCount(); + } + } + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + public com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + getAccessBoundaryRules(int index) { + if (accessBoundaryRulesBuilder_ == null) { + return accessBoundaryRules_.get(index); + } else { + return accessBoundaryRulesBuilder_.getMessage(index); + } + } + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + public Builder setAccessBoundaryRules( + int index, + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + value) { + if (accessBoundaryRulesBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureAccessBoundaryRulesIsMutable(); + accessBoundaryRules_.set(index, value); + onChanged(); + } else { + accessBoundaryRulesBuilder_.setMessage(index, value); + } + return this; + } + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + public Builder setAccessBoundaryRules( + int index, + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule.Builder + builderForValue) { + if (accessBoundaryRulesBuilder_ == null) { + ensureAccessBoundaryRulesIsMutable(); + accessBoundaryRules_.set(index, builderForValue.build()); + onChanged(); + } else { + accessBoundaryRulesBuilder_.setMessage(index, builderForValue.build()); + } + return this; + } + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + public Builder addAccessBoundaryRules( + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + value) { + if (accessBoundaryRulesBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureAccessBoundaryRulesIsMutable(); + accessBoundaryRules_.add(value); + onChanged(); + } else { + accessBoundaryRulesBuilder_.addMessage(value); + } + return this; + } + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + public Builder addAccessBoundaryRules( + int index, + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule + value) { + if (accessBoundaryRulesBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureAccessBoundaryRulesIsMutable(); + accessBoundaryRules_.add(index, value); + onChanged(); + } else { + accessBoundaryRulesBuilder_.addMessage(index, value); + } + return this; + } + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + public Builder addAccessBoundaryRules( + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule.Builder + builderForValue) { + if (accessBoundaryRulesBuilder_ == null) { + ensureAccessBoundaryRulesIsMutable(); + accessBoundaryRules_.add(builderForValue.build()); + onChanged(); + } else { + accessBoundaryRulesBuilder_.addMessage(builderForValue.build()); + } + return this; + } + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + public Builder addAccessBoundaryRules( + int index, + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule.Builder + builderForValue) { + if (accessBoundaryRulesBuilder_ == null) { + ensureAccessBoundaryRulesIsMutable(); + accessBoundaryRules_.add(index, builderForValue.build()); + onChanged(); + } else { + accessBoundaryRulesBuilder_.addMessage(index, builderForValue.build()); + } + return this; + } + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + public Builder addAllAccessBoundaryRules( + java.lang.Iterable< + ? extends + com.google.auth.credentialaccessboundary.protobuf + .ClientSideAccessBoundaryProto.ClientSideAccessBoundaryRule> + values) { + if (accessBoundaryRulesBuilder_ == null) { + ensureAccessBoundaryRulesIsMutable(); + com.google.protobuf.AbstractMessageLite.Builder.addAll(values, accessBoundaryRules_); + onChanged(); + } else { + accessBoundaryRulesBuilder_.addAllMessages(values); + } + return this; + } + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + public Builder clearAccessBoundaryRules() { + if (accessBoundaryRulesBuilder_ == null) { + accessBoundaryRules_ = java.util.Collections.emptyList(); + bitField0_ = (bitField0_ & ~0x00000001); + onChanged(); + } else { + accessBoundaryRulesBuilder_.clear(); + } + return this; + } + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + public Builder removeAccessBoundaryRules(int index) { + if (accessBoundaryRulesBuilder_ == null) { + ensureAccessBoundaryRulesIsMutable(); + accessBoundaryRules_.remove(index); + onChanged(); + } else { + accessBoundaryRulesBuilder_.remove(index); + } + return this; + } + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + public com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule.Builder + getAccessBoundaryRulesBuilder(int index) { + return getAccessBoundaryRulesFieldBuilder().getBuilder(index); + } + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + public com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRuleOrBuilder + getAccessBoundaryRulesOrBuilder(int index) { + if (accessBoundaryRulesBuilder_ == null) { + return accessBoundaryRules_.get(index); + } else { + return accessBoundaryRulesBuilder_.getMessageOrBuilder(index); + } + } + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + public java.util.List< + ? extends + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRuleOrBuilder> + getAccessBoundaryRulesOrBuilderList() { + if (accessBoundaryRulesBuilder_ != null) { + return accessBoundaryRulesBuilder_.getMessageOrBuilderList(); + } else { + return java.util.Collections.unmodifiableList(accessBoundaryRules_); + } + } + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + public com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule.Builder + addAccessBoundaryRulesBuilder() { + return getAccessBoundaryRulesFieldBuilder() + .addBuilder( + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule.getDefaultInstance()); + } + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + public com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule.Builder + addAccessBoundaryRulesBuilder(int index) { + return getAccessBoundaryRulesFieldBuilder() + .addBuilder( + index, + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule.getDefaultInstance()); + } + /** + * + * repeated .com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundaryRule access_boundary_rules = 1; + * + */ + public java.util.List< + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule.Builder> + getAccessBoundaryRulesBuilderList() { + return getAccessBoundaryRulesFieldBuilder().getBuilderList(); + } + + private com.google.protobuf.RepeatedFieldBuilderV3< + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule, + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule.Builder, + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRuleOrBuilder> + getAccessBoundaryRulesFieldBuilder() { + if (accessBoundaryRulesBuilder_ == null) { + accessBoundaryRulesBuilder_ = + new com.google.protobuf.RepeatedFieldBuilderV3< + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule, + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRule.Builder, + com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundaryRuleOrBuilder>( + accessBoundaryRules_, + ((bitField0_ & 0x00000001) != 0), + getParentForChildren(), + isClean()); + accessBoundaryRules_ = null; + } + return accessBoundaryRulesBuilder_; + } + + @java.lang.Override + public final Builder setUnknownFields( + final com.google.protobuf.UnknownFieldSet unknownFields) { + return super.setUnknownFields(unknownFields); + } + + @java.lang.Override + public final Builder mergeUnknownFields( + final com.google.protobuf.UnknownFieldSet unknownFields) { + return super.mergeUnknownFields(unknownFields); + } + + // @@protoc_insertion_point(builder_scope:com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundary) + } + + // @@protoc_insertion_point(class_scope:com.google.auth.credentialaccessboundary.proto.ClientSideAccessBoundary) + private static final com.google.auth.credentialaccessboundary.protobuf + .ClientSideAccessBoundaryProto.ClientSideAccessBoundary + DEFAULT_INSTANCE; + + static { + DEFAULT_INSTANCE = + new com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary(); + } + + public static com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary + getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser PARSER = + new com.google.protobuf.AbstractParser() { + @java.lang.Override + public ClientSideAccessBoundary parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + Builder builder = newBuilder(); + try { + builder.mergeFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(builder.buildPartial()); + } catch (com.google.protobuf.UninitializedMessageException e) { + throw e.asInvalidProtocolBufferException() + .setUnfinishedMessage(builder.buildPartial()); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException(e) + .setUnfinishedMessage(builder.buildPartial()); + } + return builder.buildPartial(); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto + .ClientSideAccessBoundary + getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + } + + private static final com.google.protobuf.Descriptors.Descriptor + internal_static_com_google_auth_credentialaccessboundary_proto_ClientSideAccessBoundaryRule_descriptor; + private static final com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internal_static_com_google_auth_credentialaccessboundary_proto_ClientSideAccessBoundaryRule_fieldAccessorTable; + private static final com.google.protobuf.Descriptors.Descriptor + internal_static_com_google_auth_credentialaccessboundary_proto_ClientSideAccessBoundary_descriptor; + private static final com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internal_static_com_google_auth_credentialaccessboundary_proto_ClientSideAccessBoundary_fieldAccessorTable; + + public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { + return descriptor; + } + + private static com.google.protobuf.Descriptors.FileDescriptor descriptor; + + static { + java.lang.String[] descriptorData = { + "\n!client_side_access_boundary.proto\022.com" + + ".google.auth.credentialaccessboundary.pr" + + "oto\032\025cel/expr/syntax.proto\"\222\001\n\034ClientSid" + + "eAccessBoundaryRule\022\032\n\022available_resourc" + + "e\030\001 \001(\t\022\035\n\025available_permissions\030\002 \003(\t\0227" + + "\n\037compiled_availability_condition\030\004 \001(\0132" + + "\016.cel.expr.Expr\"\207\001\n\030ClientSideAccessBoun" + + "dary\022k\n\025access_boundary_rules\030\001 \003(\0132L.co" + + "m.google.auth.credentialaccessboundary.p" + + "roto.ClientSideAccessBoundaryRuleBT\n1com" + + ".google.auth.credentialaccessboundary.pr" + + "otobufB\035ClientSideAccessBoundaryProtoP\000b" + + "\006proto3" + }; + descriptor = + com.google.protobuf.Descriptors.FileDescriptor.internalBuildGeneratedFileFrom( + descriptorData, + new com.google.protobuf.Descriptors.FileDescriptor[] { + dev.cel.expr.SyntaxProto.getDescriptor(), + }); + internal_static_com_google_auth_credentialaccessboundary_proto_ClientSideAccessBoundaryRule_descriptor = + getDescriptor().getMessageTypes().get(0); + internal_static_com_google_auth_credentialaccessboundary_proto_ClientSideAccessBoundaryRule_fieldAccessorTable = + new com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( + internal_static_com_google_auth_credentialaccessboundary_proto_ClientSideAccessBoundaryRule_descriptor, + new java.lang.String[] { + "AvailableResource", "AvailablePermissions", "CompiledAvailabilityCondition", + }); + internal_static_com_google_auth_credentialaccessboundary_proto_ClientSideAccessBoundary_descriptor = + getDescriptor().getMessageTypes().get(1); + internal_static_com_google_auth_credentialaccessboundary_proto_ClientSideAccessBoundary_fieldAccessorTable = + new com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( + internal_static_com_google_auth_credentialaccessboundary_proto_ClientSideAccessBoundary_descriptor, + new java.lang.String[] { + "AccessBoundaryRules", + }); + dev.cel.expr.SyntaxProto.getDescriptor(); + } + + // @@protoc_insertion_point(outer_class_scope) +} diff --git a/cab-token-generator/javatests/com/google/auth/credentialaccessboundary/ClientSideCredentialAccessBoundaryFactoryTest.java b/cab-token-generator/javatests/com/google/auth/credentialaccessboundary/ClientSideCredentialAccessBoundaryFactoryTest.java new file mode 100644 index 000000000..85600041f --- /dev/null +++ b/cab-token-generator/javatests/com/google/auth/credentialaccessboundary/ClientSideCredentialAccessBoundaryFactoryTest.java @@ -0,0 +1,989 @@ +/* + * Copyright 2025, Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.auth.credentialaccessboundary; + +import static com.google.auth.oauth2.OAuth2Utils.TOKEN_EXCHANGE_URL_FORMAT; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.api.client.http.HttpTransport; +import com.google.api.client.util.Clock; +import com.google.auth.Credentials; +import com.google.auth.TestUtils; +import com.google.auth.credentialaccessboundary.ClientSideCredentialAccessBoundaryFactory.IntermediateCredentials; +import com.google.auth.credentialaccessboundary.ClientSideCredentialAccessBoundaryFactory.RefreshType; +import com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto.ClientSideAccessBoundary; +import com.google.auth.credentialaccessboundary.protobuf.ClientSideAccessBoundaryProto.ClientSideAccessBoundaryRule; +import com.google.auth.http.HttpTransportFactory; +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.CredentialAccessBoundary; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.auth.oauth2.MockStsTransport; +import com.google.auth.oauth2.MockTokenServerTransportFactory; +import com.google.auth.oauth2.OAuth2Utils; +import com.google.auth.oauth2.ServiceAccountCredentials; +import com.google.common.collect.ImmutableList; +import com.google.crypto.tink.Aead; +import com.google.crypto.tink.InsecureSecretKeyAccess; +import com.google.crypto.tink.KeysetHandle; +import com.google.crypto.tink.RegistryConfiguration; +import com.google.crypto.tink.TinkProtoKeysetFormat; +import dev.cel.common.CelValidationException; +import dev.cel.expr.Expr; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.time.Duration; +import java.util.Base64; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Tests for {@link + * com.google.auth.credentialaccessboundary.ClientSideCredentialAccessBoundaryFactory}. + */ +@RunWith(JUnit4.class) +public class ClientSideCredentialAccessBoundaryFactoryTest { + private static final String SA_PRIVATE_KEY_PKCS8 = + "-----BEGIN PRIVATE KEY-----\n" + + "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALX0PQoe1igW12i" + + "kv1bN/r9lN749y2ijmbc/mFHPyS3hNTyOCjDvBbXYbDhQJzWVUikh4mvGBA07qTj79Xc3yBDfKP2IeyYQIFe0t0" + + "zkd7R9Zdn98Y2rIQC47aAbDfubtkU1U72t4zL11kHvoa0/RuFZjncvlr42X7be7lYh4p3NAgMBAAECgYASk5wDw" + + "4Az2ZkmeuN6Fk/y9H+Lcb2pskJIXjrL533vrDWGOC48LrsThMQPv8cxBky8HFSEklPpkfTF95tpD43iVwJRB/Gr" + + "CtGTw65IfJ4/tI09h6zGc4yqvIo1cHX/LQ+SxKLGyir/dQM925rGt/VojxY5ryJR7GLbCzxPnJm/oQJBANwOCO6" + + "D2hy1LQYJhXh7O+RLtA/tSnT1xyMQsGT+uUCMiKS2bSKx2wxo9k7h3OegNJIu1q6nZ6AbxDK8H3+d0dUCQQDTrP" + + "SXagBxzp8PecbaCHjzNRSQE2in81qYnrAFNB4o3DpHyMMY6s5ALLeHKscEWnqP8Ur6X4PvzZecCWU9BKAZAkAut" + + "LPknAuxSCsUOvUfS1i87ex77Ot+w6POp34pEX+UWb+u5iFn2cQacDTHLV1LtE80L8jVLSbrbrlH43H0DjU5AkEA" + + "gidhycxS86dxpEljnOMCw8CKoUBd5I880IUahEiUltk7OLJYS/Ts1wbn3kPOVX3wyJs8WBDtBkFrDHW2ezth2QJ" + + "ADj3e1YhMVdjJW5jqwlD/VNddGjgzyunmiZg0uOXsHXbytYmsA545S8KRQFaJKFXYYFo2kOjqOiC1T2cAzMDjCQ" + + "==\n-----END PRIVATE KEY-----\n"; + + private MockStsTransportFactory mockStsTransportFactory; + private static MockTokenServerTransportFactory mockTokenServerTransportFactory; + + static class MockStsTransportFactory implements HttpTransportFactory { + MockStsTransport transport = new MockStsTransport(); + + @Override + public HttpTransport create() { + return transport; + } + } + + @Before + public void setUp() { + mockStsTransportFactory = new MockStsTransportFactory(); + mockStsTransportFactory.transport.setReturnAccessBoundarySessionKey(true); + + mockTokenServerTransportFactory = new MockTokenServerTransportFactory(); + mockTokenServerTransportFactory.transport.addServiceAccount( + "service-account@google.com", "accessToken"); + } + + @Test + public void fetchIntermediateCredentials() throws Exception { + GoogleCredentials sourceCredentials = + getServiceAccountSourceCredentials(mockTokenServerTransportFactory); + + ClientSideCredentialAccessBoundaryFactory factory = + ClientSideCredentialAccessBoundaryFactory.newBuilder() + .setSourceCredential(sourceCredentials) + .setHttpTransportFactory(mockStsTransportFactory) + .build(); + + IntermediateCredentials intermediateCredentials = factory.fetchIntermediateCredentials(); + + // Verify requested token type. + Map query = + TestUtils.parseQuery(mockStsTransportFactory.transport.getRequest().getContentAsString()); + assertEquals( + OAuth2Utils.TOKEN_TYPE_ACCESS_BOUNDARY_INTERMEDIARY_TOKEN, + query.get("requested_token_type")); + + // Verify intermediate token and session key. + assertEquals( + mockStsTransportFactory.transport.getAccessBoundarySessionKey(), + intermediateCredentials.getAccessBoundarySessionKey()); + assertEquals( + mockStsTransportFactory.transport.getAccessToken(), + intermediateCredentials.getIntermediateAccessToken().getTokenValue()); + } + + @Test + public void fetchIntermediateCredentials_withCustomUniverseDomain() throws IOException { + String universeDomain = "foobar"; + GoogleCredentials sourceCredentials = + getServiceAccountSourceCredentials(mockTokenServerTransportFactory) + .toBuilder() + .setUniverseDomain(universeDomain) + .build(); + + ClientSideCredentialAccessBoundaryFactory factory = + ClientSideCredentialAccessBoundaryFactory.newBuilder() + .setUniverseDomain(universeDomain) + .setSourceCredential(sourceCredentials) + .setHttpTransportFactory(mockStsTransportFactory) + .build(); + + factory.fetchIntermediateCredentials(); + + // Verify domain. + String url = mockStsTransportFactory.transport.getRequest().getUrl(); + assertEquals(url, String.format(TOKEN_EXCHANGE_URL_FORMAT, universeDomain)); + } + + @Test + public void fetchIntermediateCredentials_sourceCredentialCannotRefresh_throwsIOException() + throws Exception { + // Simulate error when refreshing the source credential. + mockTokenServerTransportFactory.transport.setError(new IOException()); + + GoogleCredentials sourceCredentials = + getServiceAccountSourceCredentials(mockTokenServerTransportFactory); + + ClientSideCredentialAccessBoundaryFactory factory = + ClientSideCredentialAccessBoundaryFactory.newBuilder() + .setSourceCredential(sourceCredentials) + .setHttpTransportFactory(mockStsTransportFactory) + .build(); + + IOException thrown = assertThrows(IOException.class, factory::fetchIntermediateCredentials); + assertEquals("Unable to refresh the provided source credential.", thrown.getMessage()); + } + + @Test + public void fetchIntermediateCredentials_noExpiresInReturned_copiesSourceExpiration() + throws Exception { + // Simulate STS not returning expires_in. + mockStsTransportFactory.transport.setReturnExpiresIn(false); + + GoogleCredentials sourceCredentials = + getServiceAccountSourceCredentials(mockTokenServerTransportFactory); + + ClientSideCredentialAccessBoundaryFactory factory = + ClientSideCredentialAccessBoundaryFactory.newBuilder() + .setSourceCredential(sourceCredentials) + .setHttpTransportFactory(mockStsTransportFactory) + .build(); + + IntermediateCredentials intermediateCredentials = factory.fetchIntermediateCredentials(); + AccessToken intermediateAccessToken = intermediateCredentials.getIntermediateAccessToken(); + + assertEquals( + mockStsTransportFactory.transport.getAccessToken(), + intermediateAccessToken.getTokenValue()); + + // Validate that the expires_in has been copied from the source credential. + AccessToken sourceAccessToken = sourceCredentials.getAccessToken(); + assertNotNull(sourceAccessToken); + assertEquals( + sourceAccessToken.getExpirationTime(), intermediateAccessToken.getExpirationTime()); + } + + @Test + public void refreshCredentialsIfRequired_firstCallWillFetchIntermediateCredentials() + throws IOException { + GoogleCredentials sourceCredentials = + getServiceAccountSourceCredentials(mockTokenServerTransportFactory); + + ClientSideCredentialAccessBoundaryFactory factory = + ClientSideCredentialAccessBoundaryFactory.newBuilder() + .setSourceCredential(sourceCredentials) + .setHttpTransportFactory(mockStsTransportFactory) + .build(); + + // Verify that the first call to refreshCredentialsIfRequired() triggers a fetch of intermediate + // credentials, resulting in one request to the STS endpoint. This happens because the + // intermediate credentials are initially null. + assertEquals(0, mockStsTransportFactory.transport.getRequestCount()); + factory.refreshCredentialsIfRequired(); + assertEquals(1, mockStsTransportFactory.transport.getRequestCount()); + } + + @Test + public void refreshCredentialsIfRequired_noRefreshNeeded() throws IOException { + final ClientSideCredentialAccessBoundaryFactory factory = + getClientSideCredentialAccessBoundaryFactory(RefreshType.NONE); + + // Call refreshCredentialsIfRequired() once to initialize the intermediate credentials. This + // should make one request to the STS endpoint. + factory.refreshCredentialsIfRequired(); + + // Verify that a subsequent call to refreshCredentialsIfRequired() does NOT trigger another + // refresh, as the token is still valid. The request count should remain the same. + assertEquals(1, mockStsTransportFactory.transport.getRequestCount()); + factory.refreshCredentialsIfRequired(); + assertEquals(1, mockStsTransportFactory.transport.getRequestCount()); + } + + @Test + public void refreshCredentialsIfRequired_blockingSingleThread() throws IOException { + final ClientSideCredentialAccessBoundaryFactory factory = + getClientSideCredentialAccessBoundaryFactory(RefreshType.BLOCKING); + + // Call refreshCredentialsIfRequired() once to initialize the intermediate credentials. This + // should make one request to the STS endpoint. + factory.refreshCredentialsIfRequired(); + assertEquals(1, mockStsTransportFactory.transport.getRequestCount()); + + // Simulate multiple calls to refreshCredentialsIfRequired. In blocking mode, each call should + // trigger a new request to the STS endpoint. + int numRefresh = 3; + for (int i = 0; i < numRefresh; i++) { + factory.refreshCredentialsIfRequired(); + } + + // Verify that the total number of requests to the STS endpoint is the initial request plus the + // number of subsequent refresh calls. + assertEquals(1 + numRefresh, mockStsTransportFactory.transport.getRequestCount()); + } + + @Test + public void refreshCredentialsIfRequired_asyncSingleThread() throws IOException { + final ClientSideCredentialAccessBoundaryFactory factory = + getClientSideCredentialAccessBoundaryFactory(RefreshType.ASYNC); + + // Call refreshCredentialsIfRequired() once to initialize the intermediate credentials. This + // should make one request to the STS endpoint. + factory.refreshCredentialsIfRequired(); + assertEquals(1, mockStsTransportFactory.transport.getRequestCount()); + + // Subsequent calls to refreshCredentialsIfRequired() in an async mode should NOT + // immediately call the STS endpoint. They should schedule an asynchronous refresh. + int numRefresh = 3; + for (int i = 0; i < numRefresh; i++) { + factory.refreshCredentialsIfRequired(); + } + + // Verify that only the initial call resulted in an immediate STS request. The async refresh + // is still pending. + assertEquals(1, mockStsTransportFactory.transport.getRequestCount()); + + // Introduce a small delay to allow the asynchronous refresh task to complete. This is + // necessary because the async task runs on a separate thread. + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new IOException(e); + } + + // After the delay, the request count should be 2 (initial fetch + one async refresh). + // Subsequent calls to refreshCredentialsIfRequired() in ASYNC mode re-use the in-progress + // refresh task, so they don't trigger additional STS requests. + assertEquals(2, mockStsTransportFactory.transport.getRequestCount()); + } + + @Test + public void refreshCredentialsIfRequired_blockingMultiThread() + throws IOException, InterruptedException { + final ClientSideCredentialAccessBoundaryFactory factory = + getClientSideCredentialAccessBoundaryFactory(RefreshType.BLOCKING); + + // Call refreshCredentialsIfRequired() once to initialize the intermediate credentials. This + // should make one request to the STS endpoint. + factory.refreshCredentialsIfRequired(); + assertEquals(1, mockStsTransportFactory.transport.getRequestCount()); + + // Simulate multiple threads concurrently calling refreshCredentialsIfRequired(). In blocking + // mode, only one of these calls should trigger a new request to the STS endpoint. The others + // should block until the first refresh completes and then use the newly acquired credentials. + triggerConcurrentRefresh(factory, 3); + + // After all threads complete, the request count should be 2 (the initial fetch plus one + // blocking refresh). + assertEquals(2, mockStsTransportFactory.transport.getRequestCount()); + } + + @Test + public void refreshCredentialsIfRequired_asyncMultiThread() + throws IOException, InterruptedException { + final ClientSideCredentialAccessBoundaryFactory factory = + getClientSideCredentialAccessBoundaryFactory(RefreshType.ASYNC); + + // Call refreshCredentialsIfRequired() once to initialize the intermediate credentials. This + // should make one request to the STS endpoint. + factory.refreshCredentialsIfRequired(); + assertEquals(1, mockStsTransportFactory.transport.getRequestCount()); + + // Simulate multiple threads concurrently calling refreshCredentialsIfRequired(). In async + // mode, the first call should trigger a background refresh. Subsequent calls should NOT + // trigger additional refreshes while the background refresh is still pending. + triggerConcurrentRefresh(factory, 5); + + // Introduce a small delay to allow the asynchronous refresh task to complete. + try { + Thread.sleep(100); + } catch (InterruptedException e) { + throw new IOException(e); + } + + // After the delay, the request count should be 2, indicating that the initial fetch and a + // single async refresh occurred (not one per thread). + assertEquals(2, mockStsTransportFactory.transport.getRequestCount()); + } + + @Test + public void refreshCredentialsIfRequired_sourceCredentialCannotRefresh_throwsIOException() + throws Exception { + // Simulate error when refreshing the source credential. + mockTokenServerTransportFactory.transport.setError(new IOException()); + + GoogleCredentials sourceCredentials = + getServiceAccountSourceCredentials(mockTokenServerTransportFactory); + + ClientSideCredentialAccessBoundaryFactory factory = + ClientSideCredentialAccessBoundaryFactory.newBuilder() + .setSourceCredential(sourceCredentials) + .setHttpTransportFactory(mockStsTransportFactory) + .build(); + + IOException exception = assertThrows(IOException.class, factory::refreshCredentialsIfRequired); + assertEquals("Unable to refresh the provided source credential.", exception.getMessage()); + } + + // Tests related to the builder methods. + @Test + public void builder_noSourceCredential_throws() { + NullPointerException exception = + assertThrows( + NullPointerException.class, + () -> + ClientSideCredentialAccessBoundaryFactory.newBuilder() + .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) + .build()); + assertEquals("Source credential must not be null.", exception.getMessage()); + } + + @Test + public void builder_minimumTokenLifetime_negative_throws() throws IOException { + GoogleCredentials sourceCredentials = + getServiceAccountSourceCredentials(mockTokenServerTransportFactory); + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + ClientSideCredentialAccessBoundaryFactory.newBuilder() + .setSourceCredential(sourceCredentials) + .setMinimumTokenLifetime(Duration.ofMinutes(-1))); + + assertEquals("Minimum token lifetime must be greater than zero.", exception.getMessage()); + } + + @Test + public void builder_minimumTokenLifetime_zero_throws() throws IOException { + GoogleCredentials sourceCredentials = + getServiceAccountSourceCredentials(mockTokenServerTransportFactory); + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + ClientSideCredentialAccessBoundaryFactory.newBuilder() + .setSourceCredential(sourceCredentials) + .setMinimumTokenLifetime(Duration.ZERO)); + + assertEquals("Minimum token lifetime must be greater than zero.", exception.getMessage()); + } + + @Test + public void builder_refreshMargin_negative_throws() throws IOException { + GoogleCredentials sourceCredentials = + getServiceAccountSourceCredentials(mockTokenServerTransportFactory); + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + ClientSideCredentialAccessBoundaryFactory.newBuilder() + .setSourceCredential(sourceCredentials) + .setRefreshMargin(Duration.ofMinutes(-1))); + + assertEquals("Refresh margin must be greater than zero.", exception.getMessage()); + } + + @Test + public void builder_refreshMargin_zero_throws() throws IOException { + GoogleCredentials sourceCredentials = + getServiceAccountSourceCredentials(mockTokenServerTransportFactory); + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + ClientSideCredentialAccessBoundaryFactory.newBuilder() + .setSourceCredential(sourceCredentials) + .setRefreshMargin(Duration.ZERO)); + + assertEquals("Refresh margin must be greater than zero.", exception.getMessage()); + } + + @Test + public void builder_setsCorrectDefaultValues() throws IOException { + GoogleCredentials sourceCredentials = + getServiceAccountSourceCredentials(mockTokenServerTransportFactory); + ClientSideCredentialAccessBoundaryFactory factory = + ClientSideCredentialAccessBoundaryFactory.newBuilder() + .setSourceCredential(sourceCredentials) + .build(); + + assertEquals(OAuth2Utils.HTTP_TRANSPORT_FACTORY, factory.getTransportFactory()); + assertEquals( + String.format(OAuth2Utils.TOKEN_EXCHANGE_URL_FORMAT, Credentials.GOOGLE_DEFAULT_UNIVERSE), + factory.getTokenExchangeEndpoint()); + } + + @Test + public void builder_universeDomainMismatch_throws() throws IOException { + GoogleCredentials sourceCredentials = + getServiceAccountSourceCredentials(mockTokenServerTransportFactory); + + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + ClientSideCredentialAccessBoundaryFactory.newBuilder() + .setSourceCredential(sourceCredentials) + .setUniverseDomain("differentUniverseDomain") + .build()); + assertEquals( + "The client side access boundary credential's universe domain must be the same as the source credential.", + exception.getMessage()); + } + + @Test + public void builder_invalidRefreshMarginAndMinimumTokenLifetime_throws() throws IOException { + GoogleCredentials sourceCredentials = + getServiceAccountSourceCredentials(mockTokenServerTransportFactory); + + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + ClientSideCredentialAccessBoundaryFactory.newBuilder() + .setSourceCredential(sourceCredentials) + .setRefreshMargin(Duration.ofMinutes(50)) + .setMinimumTokenLifetime(Duration.ofMinutes(50)) + .build()); + + assertEquals( + "Refresh margin must be at least one minute longer than the minimum token lifetime.", + exception.getMessage()); + } + + @Test + public void builder_invalidRefreshMargin_throws() throws IOException { + GoogleCredentials sourceCredentials = + getServiceAccountSourceCredentials(mockTokenServerTransportFactory); + + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + ClientSideCredentialAccessBoundaryFactory.newBuilder() + .setSourceCredential(sourceCredentials) + .setRefreshMargin(Duration.ofMinutes(25)) + .build()); + + assertEquals( + "Refresh margin must be at least one minute longer than the minimum token lifetime.", + exception.getMessage()); + } + + @Test + public void builder_invalidMinimumTokenLifetime_throws() throws IOException { + GoogleCredentials sourceCredentials = + getServiceAccountSourceCredentials(mockTokenServerTransportFactory); + + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + ClientSideCredentialAccessBoundaryFactory.newBuilder() + .setSourceCredential(sourceCredentials) + .setMinimumTokenLifetime(Duration.ofMinutes(50)) + .build()); + + assertEquals( + "Refresh margin must be at least one minute longer than the minimum token lifetime.", + exception.getMessage()); + } + + @Test + public void builder_minimumTokenLifetimeNotSet_usesDefault() throws IOException { + GoogleCredentials sourceCredentials = + getServiceAccountSourceCredentials(mockTokenServerTransportFactory); + + ClientSideCredentialAccessBoundaryFactory factory = + ClientSideCredentialAccessBoundaryFactory.newBuilder() + .setSourceCredential(sourceCredentials) + .setRefreshMargin(Duration.ofMinutes(50)) + .build(); + + assertEquals( + ClientSideCredentialAccessBoundaryFactory.DEFAULT_MINIMUM_TOKEN_LIFETIME, + factory.getMinimumTokenLifetime()); + } + + @Test + public void builder_refreshMarginNotSet_usesDefault() throws IOException { + GoogleCredentials sourceCredentials = + getServiceAccountSourceCredentials(mockTokenServerTransportFactory); + + ClientSideCredentialAccessBoundaryFactory factory = + ClientSideCredentialAccessBoundaryFactory.newBuilder() + .setSourceCredential(sourceCredentials) + .setMinimumTokenLifetime(Duration.ofMinutes(20)) + .build(); + + assertEquals( + ClientSideCredentialAccessBoundaryFactory.DEFAULT_REFRESH_MARGIN, + factory.getRefreshMargin()); + } + + private static GoogleCredentials getServiceAccountSourceCredentials( + MockTokenServerTransportFactory transportFactory) throws IOException { + String email = "service-account@google.com"; + + ServiceAccountCredentials sourceCredentials = + ServiceAccountCredentials.newBuilder() + .setClientEmail(email) + .setPrivateKey(OAuth2Utils.privateKeyFromPkcs8(SA_PRIVATE_KEY_PKCS8)) + .setPrivateKeyId("privateKeyId") + .setProjectId("projectId") + .setHttpTransportFactory(transportFactory) + .build(); + + return sourceCredentials.createScoped("https://www.googleapis.com/auth/cloud-platform"); + } + + private ClientSideCredentialAccessBoundaryFactory getClientSideCredentialAccessBoundaryFactory( + RefreshType refreshType) throws IOException { + GoogleCredentials sourceCredentials = + getServiceAccountSourceCredentials(mockTokenServerTransportFactory); + + return ClientSideCredentialAccessBoundaryFactory.newBuilder() + .setSourceCredential(sourceCredentials) + .setHttpTransportFactory(mockStsTransportFactory) + .setClock(createMockClock(refreshType, sourceCredentials)) + .build(); + } + + private Clock createMockClock(RefreshType refreshType, GoogleCredentials sourceCredentials) { + Clock mockClock = mock(Clock.class); + long currentTimeInMillis = Clock.SYSTEM.currentTimeMillis(); + long mockedTimeInMillis; + final long refreshMarginInMillis = + ClientSideCredentialAccessBoundaryFactory.DEFAULT_REFRESH_MARGIN.toMillis(); + final long minimumTokenLifetimeMillis = + ClientSideCredentialAccessBoundaryFactory.DEFAULT_MINIMUM_TOKEN_LIFETIME.toMillis(); + + // If the source credential doesn't have an access token, set the expiration time to 1 hour from + // the current time. + long expirationTimeInMillis = + sourceCredentials.getAccessToken() != null + ? sourceCredentials.getAccessToken().getExpirationTime().getTime() + : currentTimeInMillis + 3600000; + + switch (refreshType) { + case NONE: + // Set mocked time so that the token is fresh and no refresh is needed (before the refresh + // margin). + mockedTimeInMillis = expirationTimeInMillis - refreshMarginInMillis - 60000; + break; + case ASYNC: + // Set mocked time so that the token is nearing expiry and an async refresh is triggered + // (within the refresh margin). + mockedTimeInMillis = expirationTimeInMillis - refreshMarginInMillis + 60000; + break; + case BLOCKING: + // Set mocked time so that the token requires immediate refresh (just after the minimum + // token lifetime). + mockedTimeInMillis = expirationTimeInMillis - minimumTokenLifetimeMillis + 60000; + break; + default: + throw new IllegalArgumentException("Unexpected RefreshType: " + refreshType); + } + + when(mockClock.currentTimeMillis()).thenReturn(mockedTimeInMillis); + return mockClock; + } + + private static void triggerConcurrentRefresh( + ClientSideCredentialAccessBoundaryFactory factory, int numThreads) + throws InterruptedException { + Thread[] threads = new Thread[numThreads]; + CountDownLatch latch = new CountDownLatch(numThreads); + long timeoutMillis = 5000; // 5 seconds + + // Create and start threads to concurrently call refreshCredentialsIfRequired(). + for (int i = 0; i < numThreads; i++) { + threads[i] = + new Thread( + () -> { + try { + latch.countDown(); + latch.await(); + factory.refreshCredentialsIfRequired(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + threads[i].start(); + } + + // Wait for each thread to complete, with a timeout. + for (Thread thread : threads) { + thread.join(timeoutMillis); + if (thread.isAlive()) { + thread.interrupt(); + throw new AssertionError( + "Thread running refreshCredentialsIfRequired timed out after " + + timeoutMillis + + " milliseconds."); + } + } + } + + private static class CabToken { + String intermediateToken; + String encryptedRestriction; + + CabToken(String intermediateToken, String encryptedRestriction) { + this.intermediateToken = intermediateToken; + this.encryptedRestriction = encryptedRestriction; + } + } + + private static CabToken parseCabToken(AccessToken token) { + String[] parts = token.getTokenValue().split("\\."); + assertEquals(2, parts.length); + + return new CabToken(parts[0], parts[1]); + } + + private static ClientSideAccessBoundary decryptRestriction(String restriction, String sessionKey) + throws Exception { + byte[] rawKey = Base64.getDecoder().decode(sessionKey); + + KeysetHandle keysetHandle = + TinkProtoKeysetFormat.parseKeyset(rawKey, InsecureSecretKeyAccess.get()); + + Aead aead = keysetHandle.getPrimitive(RegistryConfiguration.get(), Aead.class); + byte[] rawRestrictions = + aead.decrypt(Base64.getUrlDecoder().decode(restriction), /* associatedData= */ new byte[0]); + + return ClientSideAccessBoundary.parseFrom(rawRestrictions); + } + + @Test + public void generateToken_withAvailablityCondition_success() throws Exception { + MockStsTransportFactory transportFactory = new MockStsTransportFactory(); + transportFactory.transport.setReturnAccessBoundarySessionKey(true); + + ClientSideCredentialAccessBoundaryFactory.Builder builder = + ClientSideCredentialAccessBoundaryFactory.newBuilder(); + + ClientSideCredentialAccessBoundaryFactory factory = + builder + .setSourceCredential( + getServiceAccountSourceCredentials(mockTokenServerTransportFactory)) + .setHttpTransportFactory(transportFactory) + .build(); + + CredentialAccessBoundary.Builder cabBuilder = CredentialAccessBoundary.newBuilder(); + CredentialAccessBoundary accessBoundary = + cabBuilder + .addRule( + CredentialAccessBoundary.AccessBoundaryRule.newBuilder() + .setAvailableResource("resource") + .setAvailablePermissions(ImmutableList.of("role1", "role2")) + .setAvailabilityCondition( + CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition + .newBuilder() + .setExpression("a == b") + .build()) + .build()) + .build(); + + AccessToken token = factory.generateToken(accessBoundary); + + CabToken cabToken = parseCabToken(token); + assertEquals("accessToken", cabToken.intermediateToken); + + // Checks the encrypted restriction is the correct proto format of the CredentialAccessBoundary. + ClientSideAccessBoundary clientSideAccessBoundary = + decryptRestriction( + cabToken.encryptedRestriction, + transportFactory.transport.getAccessBoundarySessionKey()); + assertEquals(1, clientSideAccessBoundary.getAccessBoundaryRulesCount()); + + ClientSideAccessBoundaryRule rule = clientSideAccessBoundary.getAccessBoundaryRules(0); + + // Available resource and available permission should be the exact same as in original format. + assertEquals("resource", rule.getAvailableResource()); + assertEquals(ImmutableList.of("role1", "role2"), rule.getAvailablePermissionsList()); + + // Availability condition should be in the correct compiled proto format. + Expr expr = rule.getCompiledAvailabilityCondition(); + assertEquals("_==_", expr.getCallExpr().getFunction()); + assertEquals("a", expr.getCallExpr().getArgs(0).getIdentExpr().getName()); + assertEquals("b", expr.getCallExpr().getArgs(1).getIdentExpr().getName()); + } + + @Test + public void generateToken_withoutAvailabilityCondition_success() throws Exception { + MockStsTransportFactory transportFactory = new MockStsTransportFactory(); + transportFactory.transport.setReturnAccessBoundarySessionKey(true); + + ClientSideCredentialAccessBoundaryFactory.Builder builder = + ClientSideCredentialAccessBoundaryFactory.newBuilder(); + + ClientSideCredentialAccessBoundaryFactory factory = + builder + .setSourceCredential( + getServiceAccountSourceCredentials(mockTokenServerTransportFactory)) + .setHttpTransportFactory(transportFactory) + .build(); + + CredentialAccessBoundary.Builder cabBuilder = CredentialAccessBoundary.newBuilder(); + CredentialAccessBoundary accessBoundary = + cabBuilder + .addRule( + CredentialAccessBoundary.AccessBoundaryRule.newBuilder() + .setAvailableResource("resource") + .setAvailablePermissions(ImmutableList.of("role")) + .build()) + .build(); + + AccessToken token = factory.generateToken(accessBoundary); + + CabToken cabToken = parseCabToken(token); + assertEquals("accessToken", cabToken.intermediateToken); + + // Checks the encrypted restriction is the correct proto format of the CredentialAccessBoundary. + ClientSideAccessBoundary clientSideAccessBoundary = + decryptRestriction( + cabToken.encryptedRestriction, + transportFactory.transport.getAccessBoundarySessionKey()); + assertEquals(1, clientSideAccessBoundary.getAccessBoundaryRulesCount()); + + ClientSideAccessBoundaryRule rule = clientSideAccessBoundary.getAccessBoundaryRules(0); + + // Available resource and available permission should be the exact same as in original format. + assertEquals("resource", rule.getAvailableResource()); + assertEquals(ImmutableList.of("role"), rule.getAvailablePermissionsList()); + + // Availability condition should be empty since it's not provided. + assertFalse(rule.hasCompiledAvailabilityCondition()); + } + + @Test + public void generateToken_withMultipleRules_success() throws Exception { + MockStsTransportFactory transportFactory = new MockStsTransportFactory(); + transportFactory.transport.setReturnAccessBoundarySessionKey(true); + + ClientSideCredentialAccessBoundaryFactory.Builder builder = + ClientSideCredentialAccessBoundaryFactory.newBuilder(); + + ClientSideCredentialAccessBoundaryFactory factory = + builder + .setSourceCredential( + getServiceAccountSourceCredentials(mockTokenServerTransportFactory)) + .setHttpTransportFactory(transportFactory) + .build(); + + CredentialAccessBoundary.Builder cabBuilder = CredentialAccessBoundary.newBuilder(); + CredentialAccessBoundary accessBoundary = + cabBuilder + .addRule( + CredentialAccessBoundary.AccessBoundaryRule.newBuilder() + .setAvailableResource("resource1") + .setAvailablePermissions(ImmutableList.of("role1-1", "role1-2")) + .setAvailabilityCondition( + CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition + .newBuilder() + .setExpression("a == b") + .build()) + .build()) + .addRule( + CredentialAccessBoundary.AccessBoundaryRule.newBuilder() + .setAvailableResource("resource") + .setAvailablePermissions(ImmutableList.of("role2")) + .build()) + .build(); + + AccessToken token = factory.generateToken(accessBoundary); + + CabToken cabToken = parseCabToken(token); + assertEquals("accessToken", cabToken.intermediateToken); + + // Checks the encrypted restriction is the correct proto format of the CredentialAccessBoundary. + ClientSideAccessBoundary clientSideAccessBoundary = + decryptRestriction( + cabToken.encryptedRestriction, + transportFactory.transport.getAccessBoundarySessionKey()); + assertEquals(2, clientSideAccessBoundary.getAccessBoundaryRulesCount()); + + // Checks the first rule. + ClientSideAccessBoundaryRule rule1 = clientSideAccessBoundary.getAccessBoundaryRules(0); + assertEquals("resource1", rule1.getAvailableResource()); + assertEquals(ImmutableList.of("role1-1", "role1-2"), rule1.getAvailablePermissionsList()); + + Expr expr = rule1.getCompiledAvailabilityCondition(); + assertEquals("_==_", expr.getCallExpr().getFunction()); + assertEquals("a", expr.getCallExpr().getArgs(0).getIdentExpr().getName()); + assertEquals("b", expr.getCallExpr().getArgs(1).getIdentExpr().getName()); + + // Checks the second rule. + ClientSideAccessBoundaryRule rule2 = clientSideAccessBoundary.getAccessBoundaryRules(1); + assertEquals("resource", rule2.getAvailableResource()); + assertEquals(ImmutableList.of("role2"), rule2.getAvailablePermissionsList()); + assertFalse(rule2.hasCompiledAvailabilityCondition()); + } + + @Test + public void generateToken_withInvalidAvailabilityCondition_failure() throws Exception { + MockStsTransportFactory transportFactory = new MockStsTransportFactory(); + transportFactory.transport.setReturnAccessBoundarySessionKey(true); + + ClientSideCredentialAccessBoundaryFactory.Builder builder = + ClientSideCredentialAccessBoundaryFactory.newBuilder(); + + ClientSideCredentialAccessBoundaryFactory factory = + builder + .setSourceCredential( + getServiceAccountSourceCredentials(mockTokenServerTransportFactory)) + .setHttpTransportFactory(transportFactory) + .build(); + + CredentialAccessBoundary.Builder cabBuilder = CredentialAccessBoundary.newBuilder(); + CredentialAccessBoundary accessBoundary = + cabBuilder + .addRule( + CredentialAccessBoundary.AccessBoundaryRule.newBuilder() + .setAvailableResource( + "//storage.googleapis.com/projects/" + "_/buckets/example-bucket") + .setAvailablePermissions(ImmutableList.of("inRole:roles/storage.objectViewer")) + .setAvailabilityCondition( + CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition + .newBuilder() + .setExpression( + "resource.name.startsWith('projects/_/" + + "buckets/example-bucket/objects/" + + "customer-a'") // No closing bracket + .build()) + .build()) + .build(); + + assertThrows(CelValidationException.class, () -> factory.generateToken(accessBoundary)); + } + + @Test + public void generateToken_withSessionKeyNotBase64Encoded_failure() throws Exception { + MockStsTransportFactory transportFactory = new MockStsTransportFactory(); + transportFactory.transport.setReturnAccessBoundarySessionKey(true); + transportFactory.transport.setAccessBoundarySessionKey("invalid_key"); + + ClientSideCredentialAccessBoundaryFactory.Builder builder = + ClientSideCredentialAccessBoundaryFactory.newBuilder(); + + ClientSideCredentialAccessBoundaryFactory factory = + builder + .setSourceCredential( + getServiceAccountSourceCredentials(mockTokenServerTransportFactory)) + .setHttpTransportFactory(transportFactory) + .build(); + + CredentialAccessBoundary.Builder cabBuilder = CredentialAccessBoundary.newBuilder(); + CredentialAccessBoundary accessBoundary = + cabBuilder + .addRule( + CredentialAccessBoundary.AccessBoundaryRule.newBuilder() + .setAvailableResource( + "//storage.googleapis.com/projects/" + "_/buckets/example-bucket") + .setAvailablePermissions(ImmutableList.of("inRole:roles/storage.objectViewer")) + .setAvailabilityCondition( + CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition + .newBuilder() + .setExpression( + "resource.name.startsWith('projects/_/" + + "buckets/example-bucket/objects/customer-a')") + .build()) + .build()) + .build(); + + assertThrows(IllegalStateException.class, () -> factory.generateToken(accessBoundary)); + } + + @Test + public void generateToken_withMalformSessionKey_failure() throws Exception { + MockStsTransportFactory transportFactory = new MockStsTransportFactory(); + transportFactory.transport.setReturnAccessBoundarySessionKey(true); + transportFactory.transport.setAccessBoundarySessionKey("aW52YWxpZF9rZXk="); + + ClientSideCredentialAccessBoundaryFactory.Builder builder = + ClientSideCredentialAccessBoundaryFactory.newBuilder(); + + ClientSideCredentialAccessBoundaryFactory factory = + builder + .setSourceCredential( + getServiceAccountSourceCredentials(mockTokenServerTransportFactory)) + .setHttpTransportFactory(transportFactory) + .build(); + + CredentialAccessBoundary.Builder cabBuilder = CredentialAccessBoundary.newBuilder(); + CredentialAccessBoundary accessBoundary = + cabBuilder + .addRule( + CredentialAccessBoundary.AccessBoundaryRule.newBuilder() + .setAvailableResource( + "//storage.googleapis.com/projects/" + "_/buckets/example-bucket") + .setAvailablePermissions(ImmutableList.of("inRole:roles/storage.objectViewer")) + .setAvailabilityCondition( + CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition + .newBuilder() + .setExpression( + "resource.name.startsWith('projects/_/" + + "buckets/example-bucket/objects/customer-a')") + .build()) + .build()) + .build(); + + assertThrows(GeneralSecurityException.class, () -> factory.generateToken(accessBoundary)); + } +} diff --git a/cab-token-generator/javatests/com/google/auth/credentialaccessboundary/ITClientSideCredentialAccessBoundaryTest.java b/cab-token-generator/javatests/com/google/auth/credentialaccessboundary/ITClientSideCredentialAccessBoundaryTest.java new file mode 100644 index 000000000..5ef5bfb68 --- /dev/null +++ b/cab-token-generator/javatests/com/google/auth/credentialaccessboundary/ITClientSideCredentialAccessBoundaryTest.java @@ -0,0 +1,153 @@ +/* + * Copyright 2025, Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.auth.credentialaccessboundary; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.JsonObjectParser; +import com.google.api.client.json.gson.GsonFactory; +import com.google.auth.Credentials; +import com.google.auth.http.HttpCredentialsAdapter; +import com.google.auth.oauth2.CredentialAccessBoundary; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.auth.oauth2.OAuth2CredentialsWithRefresh; +import com.google.auth.oauth2.ServiceAccountCredentials; +import dev.cel.common.CelValidationException; +import java.io.IOException; +import java.security.GeneralSecurityException; +import org.junit.Test; + +/** + * Integration tests for {@link ClientSideCredentialAccessBoundaryFactory}. * + * + *

The only requirements for this test suite to run is to set the environment variable + * GOOGLE_APPLICATION_CREDENTIALS to point to the same service account configured in the setup + * script (downscoping-with-cab-setup.sh). + */ +public final class ITClientSideCredentialAccessBoundaryTest { + + // Output copied from the setup script (downscoping-with-cab-setup.sh). + private static final String GCS_BUCKET_NAME = "cab-int-bucket-cbi3qrv5"; + private static final String GCS_OBJECT_NAME_WITH_PERMISSION = "cab-first-cbi3qrv5.txt"; + private static final String GCS_OBJECT_NAME_WITHOUT_PERMISSION = "cab-second-cbi3qrv5.txt"; + + // This Credential Access Boundary enables the objectViewer permission to the specified object in + // the specified bucket. + private static final CredentialAccessBoundary CREDENTIAL_ACCESS_BOUNDARY = + CredentialAccessBoundary.newBuilder() + .addRule( + CredentialAccessBoundary.AccessBoundaryRule.newBuilder() + .setAvailableResource( + String.format( + "//storage.googleapis.com/projects/_/buckets/%s", GCS_BUCKET_NAME)) + .addAvailablePermission("inRole:roles/storage.objectViewer") + .setAvailabilityCondition( + CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition.newBuilder() + .setExpression( + String.format( + "resource.name.startsWith('projects/_/buckets/%s/objects/%s')", + GCS_BUCKET_NAME, GCS_OBJECT_NAME_WITH_PERMISSION)) + .build()) + .build()) + .build(); + + /** + * A downscoped credential is obtained using ClientSideCredentialAccessBoundaryFactory with + * permissions to access an object in the GCS bucket configured. We should only have access to + * retrieve this object. + * + *

We confirm this by: 1. Validating that we can successfully retrieve this object with the + * downscoped token. 2. Validating that we do not have permission to retrieve a different object + * in the same bucket. + */ + @Test + public void clientSideCredentialAccessBoundary_serviceAccountSource() throws IOException { + OAuth2CredentialsWithRefresh.OAuth2RefreshHandler refreshHandler = + () -> { + ServiceAccountCredentials sourceCredentials = + (ServiceAccountCredentials) + GoogleCredentials.getApplicationDefault() + .createScoped("https://www.googleapis.com/auth/cloud-platform"); + + ClientSideCredentialAccessBoundaryFactory factory = + ClientSideCredentialAccessBoundaryFactory.newBuilder() + .setSourceCredential(sourceCredentials) + .build(); + + try { + return factory.generateToken(CREDENTIAL_ACCESS_BOUNDARY); + } catch (CelValidationException | GeneralSecurityException e) { + throw new RuntimeException(e); + } + }; + + OAuth2CredentialsWithRefresh credentials = + OAuth2CredentialsWithRefresh.newBuilder().setRefreshHandler(refreshHandler).build(); + + // Attempt to retrieve the object that the downscoped token has access to. + retrieveObjectFromGcs(credentials, GCS_OBJECT_NAME_WITH_PERMISSION); + + // Attempt to retrieve the object that the downscoped token does not have access to. This should + // fail. + HttpResponseException exception = + assertThrows( + HttpResponseException.class, + () -> retrieveObjectFromGcs(credentials, GCS_OBJECT_NAME_WITHOUT_PERMISSION)); + assertEquals(403, exception.getStatusCode()); + } + + private void retrieveObjectFromGcs(Credentials credentials, String objectName) + throws IOException { + String url = + String.format( + "https://storage.googleapis.com/storage/v1/b/%s/o/%s", GCS_BUCKET_NAME, objectName); + + HttpCredentialsAdapter credentialsAdapter = new HttpCredentialsAdapter(credentials); + HttpRequestFactory requestFactory = + new NetHttpTransport().createRequestFactory(credentialsAdapter); + HttpRequest request = requestFactory.buildGetRequest(new GenericUrl(url)); + + JsonObjectParser parser = new JsonObjectParser(GsonFactory.getDefaultInstance()); + request.setParser(parser); + + HttpResponse response = request.execute(); + assertTrue(response.isSuccessStatusCode()); + } +} diff --git a/cab-token-generator/pom.xml b/cab-token-generator/pom.xml new file mode 100644 index 000000000..4a3c3b0ff --- /dev/null +++ b/cab-token-generator/pom.xml @@ -0,0 +1,86 @@ + + + 4.0.0 + + com.google.auth + google-auth-library-parent + 1.31.1-SNAPSHOT + + + + google-auth-library-cab-token-generator + Google Auth Library for Java - Cab Token Generator + + + java + javatests + + + + + + com.google.auth + google-auth-library-oauth2-http + + + com.google.auth + google-auth-library-credentials + + + com.google.http-client + google-http-client + + + com.google.errorprone + error_prone_annotations + + + com.google.guava + guava + + + com.google.protobuf + protobuf-java + + + dev.cel + cel + + + com.google.code.findbugs + jsr305 + + + com.google.crypto.tink + tink + + + + + junit + junit + test + + + com.google.auth + google-auth-library-oauth2-http + test + test-jar + testlib + + + org.mockito + mockito-core + 4.11.0 + test + + + com.google.http-client + google-http-client-gson + test + + + + \ No newline at end of file diff --git a/oauth2_http/java/com/google/auth/oauth2/DownscopedCredentials.java b/oauth2_http/java/com/google/auth/oauth2/DownscopedCredentials.java index 7232a234d..cafe8d5c8 100644 --- a/oauth2_http/java/com/google/auth/oauth2/DownscopedCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/DownscopedCredentials.java @@ -31,6 +31,7 @@ package com.google.auth.oauth2; +import static com.google.auth.oauth2.OAuth2Utils.TOKEN_EXCHANGE_URL_FORMAT; import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.base.Preconditions.checkNotNull; @@ -44,6 +45,13 @@ * DownscopedCredentials enables the ability to downscope, or restrict, the Identity and Access * Management (IAM) permissions that a short-lived credential can use for Cloud Storage. * + *

This class provides a server-side approach for generating downscoped tokens, suitable for + * situations where Credential Access Boundary rules change infrequently or a single downscoped + * credential is reused many times. For scenarios where rules change frequently, or you need to + * generate many unique downscoped tokens, the client-side approach using {@code + * com.google.auth.credentialaccessboundary.ClientSideCredentialAccessBoundaryFactory} is more + * efficient. + * *

To downscope permissions you must define a {@link CredentialAccessBoundary} which specifies * the upper bound of permissions that the credential can access. You must also provide a source * credential which will be used to acquire the downscoped credential. @@ -88,7 +96,6 @@ */ public final class DownscopedCredentials extends OAuth2Credentials { - private final String TOKEN_EXCHANGE_URL_FORMAT = "https://sts.{universe_domain}/v1/token"; private final GoogleCredentials sourceCredential; private final CredentialAccessBoundary credentialAccessBoundary; private final String universeDomain; @@ -125,8 +132,7 @@ private DownscopedCredentials(Builder builder) { throw new IllegalStateException( "Error occurred when attempting to retrieve source credential universe domain.", e); } - this.tokenExchangeEndpoint = - TOKEN_EXCHANGE_URL_FORMAT.replace("{universe_domain}", universeDomain); + this.tokenExchangeEndpoint = String.format(TOKEN_EXCHANGE_URL_FORMAT, universeDomain); } @Override diff --git a/oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java b/oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java index 20e1c92e5..3d569b02f 100644 --- a/oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java @@ -484,7 +484,16 @@ protected static T newInstance(String className) throws IOException, ClassNo } } - protected static T getFromServiceLoader(Class clazz, T defaultInstance) { + /** + * Returns the first service provider from the given service loader. + * + * @param clazz The class of the service provider to load. + * @param defaultInstance The default instance to return if no service providers are found. + * @param The type of the service provider. + * @return The first service provider from the service loader, or the {@code defaultInstance} if + * no service providers are found. + */ + public static T getFromServiceLoader(Class clazz, T defaultInstance) { return Iterables.getFirst(ServiceLoader.load(clazz), defaultInstance); } diff --git a/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java b/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java index 36937ff89..0359a34e5 100644 --- a/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java +++ b/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java @@ -69,14 +69,27 @@ import java.util.Map; import java.util.Set; -/** Internal utilities for the com.google.auth.oauth2 namespace. */ -class OAuth2Utils { +/** + * Internal utilities for the com.google.auth.oauth2 namespace. + * + *

These classes are marked public but should be treated effectively as internal classes only. + * They are not subject to any backwards compatibility guarantees and might change or be removed at + * any time. They are provided only as a convenience for other libraries within the {@code + * com.google.auth} family. Application developers should avoid using these classes directly; they + * are not part of the public API. + */ +public class OAuth2Utils { + static final String SIGNATURE_ALGORITHM = "SHA256withRSA"; - static final String TOKEN_TYPE_ACCESS_TOKEN = "urn:ietf:params:oauth:token-type:access_token"; + public static final String TOKEN_TYPE_ACCESS_TOKEN = + "urn:ietf:params:oauth:token-type:access_token"; static final String TOKEN_TYPE_TOKEN_EXCHANGE = "urn:ietf:params:oauth:token-type:token-exchange"; + public static final String TOKEN_TYPE_ACCESS_BOUNDARY_INTERMEDIARY_TOKEN = + "urn:ietf:params:oauth:token-type:access_boundary_intermediary_token"; static final String GRANT_TYPE_JWT_BEARER = "urn:ietf:params:oauth:grant-type:jwt-bearer"; + public static final String TOKEN_EXCHANGE_URL_FORMAT = "https://sts.%s/v1/token"; static final URI TOKEN_SERVER_URI = URI.create("https://oauth2.googleapis.com/token"); static final URI TOKEN_REVOKE_URI = URI.create("https://oauth2.googleapis.com/revoke"); @@ -84,7 +97,8 @@ class OAuth2Utils { static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport(); - static final HttpTransportFactory HTTP_TRANSPORT_FACTORY = new DefaultHttpTransportFactory(); + public static final HttpTransportFactory HTTP_TRANSPORT_FACTORY = + new DefaultHttpTransportFactory(); static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance(); @@ -241,8 +255,15 @@ static Map validateMap(Map map, String key, Stri return (Map) value; } - /** Helper to convert from a PKCS#8 String to an RSA private key */ - static PrivateKey privateKeyFromPkcs8(String privateKeyPkcs8) throws IOException { + /** + * Converts a PKCS#8 string to an RSA private key. + * + * @param privateKeyPkcs8 the PKCS#8 string. + * @return the RSA private key. + * @throws IOException if the PKCS#8 data is invalid or if an unexpected exception occurs during + * key creation. + */ + public static PrivateKey privateKeyFromPkcs8(String privateKeyPkcs8) throws IOException { Reader reader = new StringReader(privateKeyPkcs8); Section section = PemReader.readFirstSectionAndClose(reader, "PRIVATE KEY"); if (section == null) { diff --git a/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java b/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java index ba7d27b81..bd7a2d21f 100644 --- a/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java +++ b/oauth2_http/java/com/google/auth/oauth2/StsRequestHandler.java @@ -50,8 +50,18 @@ import java.util.List; import javax.annotation.Nullable; -/** Implements the OAuth 2.0 token exchange based on https://tools.ietf.org/html/rfc8693. */ -final class StsRequestHandler { +/** + * Implements the OAuth 2.0 token exchange based on RFC 8693. + * + *

This class handles the process of exchanging one type of token for another using the Security + * Token Service (STS). It constructs and sends the token exchange request to the STS endpoint and + * parses the response to create an {@link StsTokenExchangeResponse} object. + * + *

Use the {@link #newBuilder(String, StsTokenExchangeRequest, HttpRequestFactory)} method to + * create a new builder for constructing an instance of this class. + */ +public final class StsRequestHandler { private static final String TOKEN_EXCHANGE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"; private static final String PARSE_ERROR_PREFIX = "Error parsing token response."; @@ -85,6 +95,14 @@ private StsRequestHandler( this.internalOptions = internalOptions; } + /** + * Returns a new builder for creating an instance of {@link StsRequestHandler}. + * + * @param tokenExchangeEndpoint The STS token exchange endpoint. + * @param stsTokenExchangeRequest The STS token exchange request. + * @param httpRequestFactory The HTTP request factory to use for sending the request. + * @return A new builder instance. + */ public static Builder newBuilder( String tokenExchangeEndpoint, StsTokenExchangeRequest stsTokenExchangeRequest, @@ -175,6 +193,11 @@ private StsTokenExchangeResponse buildResponse(GenericData responseData) throws String scope = OAuth2Utils.validateString(responseData, "scope", PARSE_ERROR_PREFIX); builder.setScopes(Arrays.asList(scope.trim().split("\\s+"))); } + if (responseData.containsKey("access_boundary_session_key")) { + builder.setAccessBoundarySessionKey( + OAuth2Utils.validateString( + responseData, "access_boundary_session_key", PARSE_ERROR_PREFIX)); + } return builder.build(); } diff --git a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java index a231fe383..f0ce390ed 100644 --- a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java +++ b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeRequest.java @@ -38,10 +38,18 @@ import javax.annotation.Nullable; /** - * Defines an OAuth 2.0 token exchange request. Based on - * https://tools.ietf.org/html/rfc8693#section-2.1. + * Represents an OAuth 2.0 token exchange request, as defined in RFC 8693, Section 2.1. + * + *

This class encapsulates the parameters necessary for making a token exchange request to Google + * Security Token Service (STS). It includes the subject token, subject token type, optional + * parameters like acting party, scopes, resource, audience, requested token type, and internal + * options. + * + *

Instances of this class are immutable. Use the {@link #newBuilder(String, String)} method to + * create a new builder. */ -final class StsTokenExchangeRequest { +public final class StsTokenExchangeRequest { private static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"; private final String subjectToken; @@ -73,6 +81,15 @@ private StsTokenExchangeRequest( this.internalOptions = internalOptions; } + /** + * Returns a new {@link StsTokenExchangeRequest.Builder} instance. + * + * @param subjectToken The token being exchanged. This represents the credentials being used to + * authorize the token exchange request. + * @param subjectTokenType The type of the {@code subjectToken}. For example, {@link + * OAuth2Utils#TOKEN_TYPE_ACCESS_TOKEN}. + * @return A new builder for creating {@code StsTokenExchangeRequest} instances. + */ public static Builder newBuilder(String subjectToken, String subjectTokenType) { return new Builder(subjectToken, subjectTokenType); } diff --git a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java index 90a94e16d..62275778a 100644 --- a/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java +++ b/oauth2_http/java/com/google/auth/oauth2/StsTokenExchangeResponse.java @@ -40,10 +40,18 @@ import javax.annotation.Nullable; /** - * Defines an OAuth 2.0 token exchange successful response. Based on - * https://tools.ietf.org/html/rfc8693#section-2.2.1. + * Represents a successful OAuth 2.0 token exchange response from the Google Security Token Service + * (STS), as defined in RFC 8693, + * Section 2.2.1. + * + *

This class provides access to the exchanged access token, issued token type, token type, + * expiration time, refresh token (optional), scopes (optional), and the access boundary session key + * (optional). + * + *

Instances are immutable. Use {@link #newBuilder(String, String, String)} to create an + * instance. */ -final class StsTokenExchangeResponse { +public final class StsTokenExchangeResponse { private final AccessToken accessToken; private final String issuedTokenType; private final String tokenType; @@ -51,6 +59,7 @@ final class StsTokenExchangeResponse { @Nullable private final Long expiresInSeconds; @Nullable private final String refreshToken; @Nullable private final List scopes; + @Nullable private final String accessBoundarySessionKey; private StsTokenExchangeResponse( String accessToken, @@ -58,7 +67,8 @@ private StsTokenExchangeResponse( String tokenType, @Nullable Long expiresInSeconds, @Nullable String refreshToken, - @Nullable List scopes) { + @Nullable List scopes, + @Nullable String accessBoundarySessionKey) { checkNotNull(accessToken); this.expiresInSeconds = expiresInSeconds; @@ -71,8 +81,18 @@ private StsTokenExchangeResponse( this.tokenType = checkNotNull(tokenType); this.refreshToken = refreshToken; this.scopes = scopes; + this.accessBoundarySessionKey = accessBoundarySessionKey; } + /** + * Returns a new {@link StsTokenExchangeResponse.Builder} instance. + * + * @param accessToken The exchanged access token. + * @param issuedTokenType The issued token type. For example, {@link + * OAuth2Utils#TOKEN_TYPE_ACCESS_TOKEN}. + * @param tokenType The token type (e.g., "Bearer"). + * @return A new builder for creating {@link StsTokenExchangeResponse} instances. + */ public static Builder newBuilder(String accessToken, String issuedTokenType, String tokenType) { return new Builder(accessToken, issuedTokenType, tokenType); } @@ -107,6 +127,16 @@ public List getScopes() { return new ArrayList<>(scopes); } + /** + * Returns the access boundary session key if present. + * + * @return the access boundary session key or {@code null} if not present + */ + @Nullable + public String getAccessBoundarySessionKey() { + return accessBoundarySessionKey; + } + public static class Builder { private final String accessToken; private final String issuedTokenType; @@ -115,6 +145,7 @@ public static class Builder { @Nullable private Long expiresInSeconds; @Nullable private String refreshToken; @Nullable private List scopes; + @Nullable private String accessBoundarySessionKey; private Builder(String accessToken, String issuedTokenType, String tokenType) { this.accessToken = accessToken; @@ -142,9 +173,28 @@ public StsTokenExchangeResponse.Builder setScopes(List scopes) { return this; } + /** + * Sets the access boundary session key. + * + * @param accessBoundarySessionKey the access boundary session key to set + * @return this {@code Builder} object + */ + @CanIgnoreReturnValue + public StsTokenExchangeResponse.Builder setAccessBoundarySessionKey( + String accessBoundarySessionKey) { + this.accessBoundarySessionKey = accessBoundarySessionKey; + return this; + } + public StsTokenExchangeResponse build() { return new StsTokenExchangeResponse( - accessToken, issuedTokenType, tokenType, expiresInSeconds, refreshToken, scopes); + accessToken, + issuedTokenType, + tokenType, + expiresInSeconds, + refreshToken, + scopes, + accessBoundarySessionKey); } } } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/DownscopedCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/DownscopedCredentialsTest.java index 06fb13ebe..9dc5d4eea 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/DownscopedCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/DownscopedCredentialsTest.java @@ -32,6 +32,7 @@ package com.google.auth.oauth2; import static com.google.auth.Credentials.GOOGLE_DEFAULT_UNIVERSE; +import static com.google.auth.oauth2.OAuth2Utils.TOKEN_EXCHANGE_URL_FORMAT; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; @@ -50,7 +51,6 @@ @RunWith(JUnit4.class) public class DownscopedCredentialsTest { - private final String TOKEN_EXCHANGE_URL_FORMAT = "https://sts.%s/v1/token"; private static final String SA_PRIVATE_KEY_PKCS8 = "-----BEGIN PRIVATE KEY-----\n" + "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALX0PQoe1igW12i" diff --git a/oauth2_http/javatests/com/google/auth/oauth2/MockStsTransport.java b/oauth2_http/javatests/com/google/auth/oauth2/MockStsTransport.java index 14ff35800..5b1b3fded 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/MockStsTransport.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/MockStsTransport.java @@ -65,13 +65,20 @@ public final class MockStsTransport extends MockHttpTransport { private static final String ACCESS_TOKEN = "accessToken"; private static final String TOKEN_TYPE = "Bearer"; private static final Long EXPIRES_IN = 3600L; + private static final String ACCESS_BOUNDARY_SESSION_KEY_VALUE = + "CPaEhYsKEmQKWAowdHlwZS5nb29nbGVhcGlzLmNvbS9nb29nbGUuY3J5cHRvLnRpbmsuQW" + + "VzR2NtS2V5EiIaIMx8syvGIGGu5yvrdq/" + + "I0Q9ZWIR1oqJXFnDFxHuwX4SEGAEQARj2hIWLCiAB"; private final Queue responseErrorSequence = new ArrayDeque<>(); private final Queue> scopeSequence = new ArrayDeque<>(); private final Queue refreshTokenSequence = new ArrayDeque<>(); private boolean returnExpiresIn = true; + private boolean returnAccessBoundarySessionKey = false; + private String accessBoundarySessionKey = ACCESS_BOUNDARY_SESSION_KEY_VALUE; private MockLowLevelHttpRequest request; + private int requestCount = 0; public void addResponseErrorSequence(IOException... errors) { Collections.addAll(responseErrorSequence, errors); @@ -87,6 +94,7 @@ public void addScopeSequence(List scopes) { @Override public LowLevelHttpRequest buildRequest(final String method, final String url) { + requestCount++; this.request = new MockLowLevelHttpRequest(url) { @Override @@ -133,6 +141,11 @@ public LowLevelHttpResponse execute() throws IOException { response.put("refresh_token", refreshTokenSequence.poll()); } } + + if (returnAccessBoundarySessionKey) { + response.put("access_boundary_session_key", accessBoundarySessionKey); + } + return new MockLowLevelHttpResponse() .setContentType(Json.MEDIA_TYPE) .setContent(response.toPrettyString()); @@ -169,7 +182,23 @@ public Long getExpiresIn() { return EXPIRES_IN; } + public String getAccessBoundarySessionKey() { + return ACCESS_BOUNDARY_SESSION_KEY_VALUE; + } + public void setReturnExpiresIn(boolean returnExpiresIn) { this.returnExpiresIn = returnExpiresIn; } + + public void setAccessBoundarySessionKey(String accessBoundarySessionKey) { + this.accessBoundarySessionKey = accessBoundarySessionKey; + } + + public void setReturnAccessBoundarySessionKey(boolean returnAccessBoundarySessionKey) { + this.returnAccessBoundarySessionKey = returnAccessBoundarySessionKey; + } + + public int getRequestCount() { + return requestCount; + } } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java b/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java index dd0cc7ce9..eb9294ac1 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/StsRequestHandlerTest.java @@ -275,4 +275,22 @@ public void exchangeToken_noExpiresInReturned() throws IOException { assertEquals(transport.getIssuedTokenType(), response.getIssuedTokenType()); assertNull(response.getExpiresInSeconds()); } + + @Test + public void exchangeToken_withAccessBoundarySessionKey() throws IOException { + transport.setReturnAccessBoundarySessionKey(/* returnAccessBoundarySessionKey= */ true); + + StsTokenExchangeRequest stsTokenExchangeRequest = + StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build(); + + StsRequestHandler requestHandler = + StsRequestHandler.newBuilder( + TOKEN_URL, stsTokenExchangeRequest, transport.createRequestFactory()) + .build(); + + StsTokenExchangeResponse response = requestHandler.exchangeToken(); + + // Validate response. + assertEquals(transport.getAccessBoundarySessionKey(), response.getAccessBoundarySessionKey()); + } } diff --git a/pom.xml b/pom.xml index cfdd7bed5..35b3e1991 100644 --- a/pom.xml +++ b/pom.xml @@ -1,7 +1,7 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 com.google.auth google-auth-library-parent @@ -27,7 +27,8 @@ googleapis.dev Google self-hosted documentation site - https://googleapis.dev/java/google-auth-library/${project.version} + https://googleapis.dev/java/google-auth-library/${project.version} + @@ -58,11 +59,15 @@ oauth2_http appengine bom + cab-token-generator - scm:git:https://github.com/googleapis/google-auth-library-java.git - scm:git:https://github.com/googleapis/google-auth-library-java.git + + scm:git:https://github.com/googleapis/google-auth-library-java.git + + + scm:git:https://github.com/googleapis/google-auth-library-java.git https://github.com/googleapis/google-auth-library-java @@ -76,6 +81,9 @@ 3.0.2 false 2.36.0 + 3.25.5 + 0.9.0-proto3 + 1.15.0 @@ -111,12 +119,12 @@ com.google.guava guava ${project.guava.version} - - - com.google.j2objc - j2objc-annotations - - + + + com.google.j2objc + j2objc-annotations + + com.google.code.findbugs @@ -143,10 +151,47 @@ testlib - com.google.errorprone - error_prone_annotations - ${project.error-prone.version} - compile + com.google.errorprone + error_prone_annotations + ${project.error-prone.version} + compile + + + com.google.protobuf + protobuf-java + ${project.protobuf.version} + + + dev.cel + cel + ${project.cel.version} + + + + + com.google.guava + guava + + + com.google.code.findbugs + annotations + + + com.google.errorprone + error_prone_annotations + + + + + com.google.crypto.tink + tink + ${project.tink.version} + + + com.google.code.gson + gson + + @@ -272,7 +317,8 @@ src/test - com.google.auth.oauth2.ExternalAccountCredentials + com.google.auth.oauth2.ExternalAccountCredentials + @@ -308,7 +354,8 @@ org.codehaus.mojo clirr-maven-plugin - clirr-ignored-differences.xml + clirr-ignored-differences.xml + true