Skip to content

Commit d9f01f1

Browse files
authored
feat: Adds Client-Side Credential Access Boundary Factory example. (#9994)
Adds Credential Access Boundary Factory example. Add integration test for the cab. Temporarily disable the failing test while ldetmer is working on it. See issue #10023 for details.
1 parent ddf61ce commit d9f01f1

File tree

6 files changed

+296
-2
lines changed

6 files changed

+296
-2
lines changed

auth/README.md

+15-2
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,21 @@ Once you have an API key replace it in the main function in ApiKeyAuthExample an
4747

4848
The same configuration above applies.
4949

50-
To run the samples for [Downscoping with Credential Access Boundaries](https://cloud.google.com/iam/docs/downscoping-short-lived-credentials)
51-
you must provide both a bucket name and object name under the TODO(developer): in the main method of `DownscopingExample`.
50+
This section provides examples for [Downscoping with Credential Access Boundaries](https://cloud.google.com/iam/docs/downscoping-short-lived-credentials).
51+
There are two examples demonstrating different ways to implement downscoping.
52+
53+
**`DownscopedAccessTokenGenerator` and `DownscopedAccessTokenConsumer` Examples:**
54+
55+
These examples demonstrate a common pattern for downscoping, using a token broker and consumer.
56+
The `DownscopedAccessTokenGenerator` generates the downscoped access token using a client-side approach, and the `DownscopedAccessTokenConsumer` uses it to access Cloud Storage resources.
57+
To run the `DownscopedAccessTokenConsumer`, you must provide a bucket name and object name under the `TODO(developer):` in the `main` method.
58+
You can then run `DownscopedAccessTokenConsumer` via:
59+
60+
mvn exec:java -Dexec.mainClass=com.google.cloud.auth.samples.DownscopedAccessTokenConsumer
61+
62+
**`DownscopingExample` Example:**
63+
64+
This example demonstrates downscoping using a server-side approach. To run this example you must provide both a bucket name and object name under the TODO(developer): in the main method of `DownscopingExample`.
5265

5366
You can then run `DownscopingExample` via:
5467

auth/pom.xml

+6
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ limitations under the License.
6767
<dependency>
6868
<groupId>com.google.auth</groupId>
6969
<artifactId>google-auth-library-oauth2-http</artifactId>
70+
<version>1.32.0</version>
71+
</dependency>
72+
<dependency>
73+
<groupId>com.google.auth</groupId>
74+
<artifactId>google-auth-library-cab-token-generator</artifactId>
75+
<version>1.32.0</version>
7076
</dependency>
7177
<dependency>
7278
<groupId>com.google.cloud</groupId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.auth.samples;
18+
19+
// [START auth_client_cab_consumer]
20+
import com.google.auth.oauth2.AccessToken;
21+
import com.google.auth.oauth2.OAuth2CredentialsWithRefresh;
22+
import com.google.cloud.storage.Blob;
23+
import com.google.cloud.storage.Storage;
24+
import com.google.cloud.storage.StorageOptions;
25+
import java.io.IOException;
26+
// [END auth_client_cab_consumer]
27+
28+
29+
/**
30+
* Demonstrates retrieving a Cloud Storage blob using a downscoped. This example showcases the
31+
* consumer side of the downscoping process. It retrieves a blob's content using credentials that
32+
* have limited access based on a pre-defined Credential Access Boundary.
33+
*/
34+
public class DownscopedAccessTokenConsumer {
35+
36+
public static void main(String[] args) throws IOException {
37+
// TODO(developer): Replace these variables before running the sample.
38+
// The Cloud Storage bucket name.
39+
String bucketName = "your-gcs-bucket-name";
40+
// The Cloud Storage object name that resides in the specified bucket.
41+
String objectName = "your-gcs-object-name";
42+
43+
retrieveBlobWithDownscopedToken(bucketName, objectName);
44+
}
45+
46+
/**
47+
* Simulates token consumer readonly access to the specified object.
48+
*
49+
* @param bucketName The name of the Cloud Storage bucket containing the blob.
50+
* @param objectName The name of the Cloud Storage object (blob).
51+
* @return The content of the blob as a String, or {@code null} if the blob does not exist.
52+
* @throws IOException If an error occurs during communication with Cloud Storage or token
53+
* retrieval. This can include issues with authentication, authorization, or network
54+
* connectivity.
55+
*/
56+
// [START auth_client_cab_consumer]
57+
public static String retrieveBlobWithDownscopedToken(
58+
final String bucketName, final String objectName) throws IOException {
59+
// You can pass an `OAuth2RefreshHandler` to `OAuth2CredentialsWithRefresh` which will allow the
60+
// library to seamlessly handle downscoped token refreshes on expiration.
61+
OAuth2CredentialsWithRefresh.OAuth2RefreshHandler handler =
62+
new OAuth2CredentialsWithRefresh.OAuth2RefreshHandler() {
63+
@Override
64+
public AccessToken refreshAccessToken() throws IOException {
65+
// The common pattern of usage is to have a token broker pass the downscoped short-lived
66+
// access tokens to a token consumer via some secure authenticated channel.
67+
// For illustration purposes, we are generating the downscoped token locally.
68+
// We want to test the ability to limit access to objects with a certain prefix string
69+
// in the resource bucket. objectName.substring(0, 3) is the prefix here. This field is
70+
// not required if access to all bucket resources are allowed. If access to limited
71+
// resources in the bucket is needed, this mechanism can be used.
72+
return DownscopedAccessTokenGenerator
73+
.getTokenFromBroker(bucketName, objectName);
74+
}
75+
};
76+
77+
AccessToken downscopedToken = handler.refreshAccessToken();
78+
79+
OAuth2CredentialsWithRefresh credentials =
80+
OAuth2CredentialsWithRefresh.newBuilder()
81+
.setAccessToken(downscopedToken)
82+
.setRefreshHandler(handler)
83+
.build();
84+
85+
StorageOptions options = StorageOptions.newBuilder().setCredentials(credentials).build();
86+
Storage storage = options.getService();
87+
88+
Blob blob = storage.get(bucketName, objectName);
89+
if (blob == null) {
90+
return null;
91+
}
92+
return new String(blob.getContent());
93+
}
94+
// [END auth_client_cab_consumer]
95+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.auth.samples;
18+
19+
// [START auth_client_cab_token_broker]
20+
import com.google.auth.credentialaccessboundary.ClientSideCredentialAccessBoundaryFactory;
21+
import com.google.auth.oauth2.AccessToken;
22+
import com.google.auth.oauth2.CredentialAccessBoundary;
23+
import com.google.auth.oauth2.GoogleCredentials;
24+
import dev.cel.common.CelValidationException;
25+
import java.io.IOException;
26+
import java.security.GeneralSecurityException;
27+
// [END auth_client_cab_token_broker]
28+
29+
/**
30+
* Demonstrates how to use ClientSideCredentialAccessBoundaryFactory to generate downscoped tokens.
31+
*/
32+
public class DownscopedAccessTokenGenerator {
33+
34+
/**
35+
* Simulates a token broker generating downscoped tokens for specific objects in a bucket.
36+
*
37+
* @param bucketName The name of the Cloud Storage bucket.
38+
* @param objectPrefix Prefix of the object name for downscoped token access.
39+
* @return An AccessToken representing the downscoped token.
40+
* @throws IOException If an error occurs during token generation.
41+
*/
42+
// [START auth_client_cab_token_broker]
43+
public static AccessToken getTokenFromBroker(String bucketName, String objectPrefix)
44+
throws IOException {
45+
// Retrieve the source credentials from ADC.
46+
GoogleCredentials sourceCredentials =
47+
GoogleCredentials.getApplicationDefault()
48+
.createScoped("https://www.googleapis.com/auth/cloud-platform");
49+
50+
// Initialize the Credential Access Boundary rules.
51+
String availableResource = "//storage.googleapis.com/projects/_/buckets/" + bucketName;
52+
53+
// Downscoped credentials will have readonly access to the resource.
54+
String availablePermission = "inRole:roles/storage.objectViewer";
55+
56+
// Only objects starting with the specified prefix string in the object name will be allowed
57+
// read access.
58+
String expression =
59+
"resource.name.startsWith('projects/_/buckets/"
60+
+ bucketName
61+
+ "/objects/"
62+
+ objectPrefix
63+
+ "')";
64+
65+
// Build the AvailabilityCondition.
66+
CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition availabilityCondition =
67+
CredentialAccessBoundary.AccessBoundaryRule.AvailabilityCondition.newBuilder()
68+
.setExpression(expression)
69+
.build();
70+
71+
// Define the single access boundary rule using the above properties.
72+
CredentialAccessBoundary.AccessBoundaryRule rule =
73+
CredentialAccessBoundary.AccessBoundaryRule.newBuilder()
74+
.setAvailableResource(availableResource)
75+
.addAvailablePermission(availablePermission)
76+
.setAvailabilityCondition(availabilityCondition)
77+
.build();
78+
79+
// Define the Credential Access Boundary with all the relevant rules.
80+
CredentialAccessBoundary credentialAccessBoundary =
81+
CredentialAccessBoundary.newBuilder().addRule(rule).build();
82+
83+
// Create an instance of ClientSideCredentialAccessBoundaryFactory.
84+
ClientSideCredentialAccessBoundaryFactory factory =
85+
ClientSideCredentialAccessBoundaryFactory.newBuilder()
86+
.setSourceCredential(sourceCredentials)
87+
.build();
88+
89+
// Generate the token and pass it to the Token Consumer.
90+
try {
91+
return factory.generateToken(credentialAccessBoundary);
92+
} catch (GeneralSecurityException | CelValidationException e) {
93+
throw new IOException("Error generating downscoped token", e);
94+
}
95+
}
96+
// [END auth_client_cab_token_broker]
97+
}

auth/src/test/java/com/google/cloud/auth/samples/AuthExampleIT.java

+3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.io.IOException;
2626
import java.io.PrintStream;
2727
import org.junit.Before;
28+
import org.junit.Ignore;
2829
import org.junit.Test;
2930
import org.junit.runner.RunWith;
3031
import org.junit.runners.JUnit4;
@@ -60,8 +61,10 @@ public void testAuthExplicitNoPath() throws IOException {
6061
assertTrue(output.contains("Buckets:"));
6162
}
6263

64+
@Ignore("Temporarily disabled due to failing test (Issue #10023).")
6365
@Test
6466
public void testAuthApiKey() throws IOException, IllegalStateException {
67+
//TODO: Re-enable this test after fixing issue #10023.
6568
String projectId = ServiceOptions.getDefaultProjectId();
6669
String keyDisplayName = "Test API Key";
6770
String service = "language.googleapis.com";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.auth.samples;
18+
19+
import static com.google.cloud.auth.samples.DownscopedAccessTokenConsumer.retrieveBlobWithDownscopedToken;
20+
import static org.junit.Assert.assertEquals;
21+
import static org.junit.Assert.assertNotNull;
22+
23+
import com.google.cloud.storage.Blob;
24+
import com.google.cloud.storage.BlobId;
25+
import com.google.cloud.storage.BlobInfo;
26+
import com.google.cloud.storage.Bucket;
27+
import com.google.cloud.storage.BucketInfo;
28+
import com.google.cloud.storage.Storage;
29+
import com.google.cloud.storage.StorageOptions;
30+
import java.io.IOException;
31+
import java.nio.charset.StandardCharsets;
32+
import java.util.UUID;
33+
import org.junit.After;
34+
import org.junit.Before;
35+
import org.junit.Test;
36+
import org.junit.runner.RunWith;
37+
import org.junit.runners.JUnit4;
38+
39+
@RunWith(JUnit4.class)
40+
// CHECKSTYLE OFF: AbbreviationAsWordInName
41+
public class DownscopedAccessTokenIT {
42+
// CHECKSTYLE ON: AbbreviationAsWordInName
43+
private static final String CONTENT = "CONTENT";
44+
private Bucket bucket;
45+
private Blob blob;
46+
47+
@Before
48+
public void setUp() {
49+
String credentials = System.getenv("GOOGLE_APPLICATION_CREDENTIALS");
50+
assertNotNull(credentials);
51+
52+
// Create a bucket and object that are deleted once the test completes.
53+
Storage storage = StorageOptions.newBuilder().build().getService();
54+
55+
String suffix = UUID.randomUUID().toString().substring(0, 18);
56+
String bucketName = String.format("bucket-client-side-cab-test-%s", suffix);
57+
bucket = storage.create(BucketInfo.newBuilder(bucketName).build());
58+
59+
String objectName = String.format("blob-client-side-cab-test-%s", suffix);
60+
BlobId blobId = BlobId.of(bucketName, objectName);
61+
BlobInfo blobInfo = Blob.newBuilder(blobId).build();
62+
blob = storage.create(blobInfo, CONTENT.getBytes(StandardCharsets.UTF_8));
63+
}
64+
65+
@After
66+
public void cleanup() {
67+
if (blob != null) {
68+
blob.delete();
69+
}
70+
if (bucket != null) {
71+
bucket.delete();
72+
}
73+
}
74+
75+
@Test
76+
public void testDownscopedAccessToken() throws IOException {
77+
String content = retrieveBlobWithDownscopedToken(bucket.getName(), blob.getName());
78+
assertEquals(CONTENT, content);
79+
}
80+
}

0 commit comments

Comments
 (0)