From b26f8a4f5362042fdb2d65839d877ff8a01c1c31 Mon Sep 17 00:00:00 2001 From: Leo <39062083+lsirac@users.noreply.github.com> Date: Thu, 22 Jul 2021 15:51:49 -0700 Subject: [PATCH] test: adds integration tests for downscoping with credential access boundaries (#698) * feat: Adding functional tests for Service Account (#685) ServiceAccountCredentials tests for 4110 * feat: allow scopes for self signed jwt (#689) * feat: self signed jwt support * update * address comments * allow to use uri as audience * address comments * chore: release 0.27.0 (#678) :robot: I have created a release \*beep\* \*boop\* --- ## [0.27.0](https://www.github.com/googleapis/google-auth-library-java/compare/v0.26.0...v0.27.0) (2021-07-14) ### Features * add Id token support for UserCredentials ([#650](https://www.github.com/googleapis/google-auth-library-java/issues/650)) ([5a8f467](https://www.github.com/googleapis/google-auth-library-java/commit/5a8f4676630854c53aa708a9c8b960770067f858)) * add impersonation credentials to ADC ([#613](https://www.github.com/googleapis/google-auth-library-java/issues/613)) ([b9823f7](https://www.github.com/googleapis/google-auth-library-java/commit/b9823f70d7f3f7461b7de40bee06f5e7ba0e797c)) * Adding functional tests for Service Account ([#685](https://www.github.com/googleapis/google-auth-library-java/issues/685)) ([dfe118c](https://www.github.com/googleapis/google-auth-library-java/commit/dfe118c261aadf137a3cf47a7acb9892c7a6db4d)) * allow scopes for self signed jwt ([#689](https://www.github.com/googleapis/google-auth-library-java/issues/689)) ([f4980c7](https://www.github.com/googleapis/google-auth-library-java/commit/f4980c77566bbd5ef4c532acb199d7d484dbcd01)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). * test: adds integration tests for downscoping with credential access boundaries Co-authored-by: Timur Sadykov Co-authored-by: arithmetic1728 <58957152+arithmetic1728@users.noreply.github.com> Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 10 + README.md | 6 +- appengine/pom.xml | 2 +- bom/pom.xml | 2 +- credentials/pom.xml | 2 +- .../com/google/auth/oauth2/JwtClaims.java | 4 +- .../oauth2/ServiceAccountCredentials.java | 143 ++++++++++--- .../ServiceAccountJwtAccessCredentials.java | 21 +- .../google/auth/oauth2/ITDownscopingTest.java | 150 ++++++++++++++ .../com/google/auth/oauth2/JwtClaimsTest.java | 12 ++ .../oauth2/ServiceAccountCredentialsTest.java | 114 +++++++++-- ...erviceAccountJwtAccessCredentialsTest.java | 30 +-- .../FTServiceAccountCredentialsTest.java | 189 ++++++++++++++++++ oauth2_http/pom.xml | 4 +- pom.xml | 2 +- scripts/downscoping-with-cab-setup.sh | 96 +++++++++ versions.txt | 12 +- 17 files changed, 691 insertions(+), 108 deletions(-) create mode 100644 oauth2_http/javatests/com/google/auth/oauth2/ITDownscopingTest.java create mode 100644 oauth2_http/javatests/com/google/auth/oauth2/functional/FTServiceAccountCredentialsTest.java create mode 100755 scripts/downscoping-with-cab-setup.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index bd0e1df7b..1ca79bcea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [0.27.0](https://www.github.com/googleapis/google-auth-library-java/compare/v0.26.0...v0.27.0) (2021-07-14) + + +### Features + +* add Id token support for UserCredentials ([#650](https://www.github.com/googleapis/google-auth-library-java/issues/650)) ([5a8f467](https://www.github.com/googleapis/google-auth-library-java/commit/5a8f4676630854c53aa708a9c8b960770067f858)) +* add impersonation credentials to ADC ([#613](https://www.github.com/googleapis/google-auth-library-java/issues/613)) ([b9823f7](https://www.github.com/googleapis/google-auth-library-java/commit/b9823f70d7f3f7461b7de40bee06f5e7ba0e797c)) +* Adding functional tests for Service Account ([#685](https://www.github.com/googleapis/google-auth-library-java/issues/685)) ([dfe118c](https://www.github.com/googleapis/google-auth-library-java/commit/dfe118c261aadf137a3cf47a7acb9892c7a6db4d)) +* allow scopes for self signed jwt ([#689](https://www.github.com/googleapis/google-auth-library-java/issues/689)) ([f4980c7](https://www.github.com/googleapis/google-auth-library-java/commit/f4980c77566bbd5ef4c532acb199d7d484dbcd01)) + ## [0.26.0](https://www.github.com/googleapis/google-auth-library-java/compare/v0.25.5...v0.26.0) (2021-05-20) diff --git a/README.md b/README.md index 2e21bbd85..b390b1278 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ If you are using Maven, add this to your pom.xml file (notice that you can repla com.google.auth google-auth-library-oauth2-http - 0.26.0 + 0.27.0 ``` [//]: # ({x-version-update-end}) @@ -41,7 +41,7 @@ If you are using Gradle, add this to your dependencies [//]: # ({x-version-update-start:google-auth-library-oauth2-http:released}) ```Groovy -compile 'com.google.auth:google-auth-library-oauth2-http:0.26.0' +compile 'com.google.auth:google-auth-library-oauth2-http:0.27.0' ``` [//]: # ({x-version-update-end}) @@ -49,7 +49,7 @@ If you are using SBT, add this to your dependencies [//]: # ({x-version-update-start:google-auth-library-oauth2-http:released}) ```Scala -libraryDependencies += "com.google.auth" % "google-auth-library-oauth2-http" % "0.26.0" +libraryDependencies += "com.google.auth" % "google-auth-library-oauth2-http" % "0.27.0" ``` [//]: # ({x-version-update-end}) diff --git a/appengine/pom.xml b/appengine/pom.xml index a5386bb7d..5ad4250f0 100644 --- a/appengine/pom.xml +++ b/appengine/pom.xml @@ -5,7 +5,7 @@ com.google.auth google-auth-library-parent - 0.26.1-SNAPSHOT + 0.27.0 ../pom.xml diff --git a/bom/pom.xml b/bom/pom.xml index a643fe760..e04d3eb42 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.google.auth google-auth-library-bom - 0.26.1-SNAPSHOT + 0.27.0 pom Google Auth Library for Java BOM diff --git a/credentials/pom.xml b/credentials/pom.xml index 4a4fb273c..01600d71f 100644 --- a/credentials/pom.xml +++ b/credentials/pom.xml @@ -4,7 +4,7 @@ com.google.auth google-auth-library-parent - 0.26.1-SNAPSHOT + 0.27.0 ../pom.xml diff --git a/oauth2_http/java/com/google/auth/oauth2/JwtClaims.java b/oauth2_http/java/com/google/auth/oauth2/JwtClaims.java index 83ee0a0a0..5e36ebde1 100644 --- a/oauth2_http/java/com/google/auth/oauth2/JwtClaims.java +++ b/oauth2_http/java/com/google/auth/oauth2/JwtClaims.java @@ -106,7 +106,9 @@ public JwtClaims merge(JwtClaims other) { * @return true if all required fields have been set; false otherwise */ public boolean isComplete() { - return getAudience() != null && getIssuer() != null && getSubject() != null; + boolean hasScopes = + getAdditionalClaims().containsKey("scope") && !getAdditionalClaims().get("scope").isEmpty(); + return (getAudience() != null || hasScopes) && getIssuer() != null && getSubject() != null; } @AutoValue.Builder diff --git a/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java index 377b4dbf1..27d67f809 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java @@ -77,6 +77,7 @@ import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Collection; +import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; @@ -109,9 +110,9 @@ public class ServiceAccountCredentials extends GoogleCredentials private final Collection defaultScopes; private final String quotaProjectId; private final int lifetime; + private final boolean useJwtAccessWithScope; private transient HttpTransportFactory transportFactory; - private transient ServiceAccountJwtAccessCredentials jwtCredentials = null; /** * Constructor with minimum identifying information and custom HTTP transport. @@ -133,6 +134,7 @@ public class ServiceAccountCredentials extends GoogleCredentials * most 43200 (12 hours). If the token is used for calling a Google API, then the value should * be at most 3600 (1 hour). If the given value is 0, then the default value 3600 will be used * when creating the credentials. + * @param useJwtAccessWithScope whether self signed JWT with scopes should be always used. */ ServiceAccountCredentials( String clientId, @@ -146,7 +148,8 @@ public class ServiceAccountCredentials extends GoogleCredentials String serviceAccountUser, String projectId, String quotaProjectId, - int lifetime) { + int lifetime, + boolean useJwtAccessWithScope) { this.clientId = clientId; this.clientEmail = Preconditions.checkNotNull(clientEmail); this.privateKey = Preconditions.checkNotNull(privateKey); @@ -167,18 +170,7 @@ public class ServiceAccountCredentials extends GoogleCredentials throw new IllegalStateException("lifetime must be less than or equal to 43200"); } this.lifetime = lifetime; - - // Use self signed JWT if scopes is not set, see https://google.aip.dev/auth/4111. - if (this.scopes.isEmpty()) { - jwtCredentials = - new ServiceAccountJwtAccessCredentials.Builder() - .setClientEmail(clientEmail) - .setClientId(clientId) - .setPrivateKey(privateKey) - .setPrivateKeyId(privateKeyId) - .setQuotaProjectId(quotaProjectId) - .build(); - } + this.useJwtAccessWithScope = useJwtAccessWithScope; } /** @@ -492,7 +484,8 @@ static ServiceAccountCredentials fromPkcs8( serviceAccountUser, projectId, quotaProject, - DEFAULT_LIFETIME_IN_SECONDS); + DEFAULT_LIFETIME_IN_SECONDS, + false); } /** Helper to convert from a PKCS#8 String to an RSA private key */ @@ -698,7 +691,8 @@ public GoogleCredentials createScoped( serviceAccountUser, projectId, quotaProjectId, - lifetime); + lifetime, + useJwtAccessWithScope); } /** @@ -714,6 +708,16 @@ public ServiceAccountCredentials createWithCustomLifetime(int lifetime) { return this.toBuilder().setLifetime(lifetime).build(); } + /** + * Clones the service account with a new useJwtAccessWithScope value. + * + * @param useJwtAccessWithScope whether self signed JWT with scopes should be used + * @return the cloned service account credentials with the given useJwtAccessWithScope + */ + public ServiceAccountCredentials createWithUseJwtAccessWithScope(boolean useJwtAccessWithScope) { + return this.toBuilder().setUseJwtAccessWithScope(useJwtAccessWithScope).build(); + } + @Override public GoogleCredentials createDelegated(String user) { return new ServiceAccountCredentials( @@ -728,7 +732,8 @@ public GoogleCredentials createDelegated(String user) { user, projectId, quotaProjectId, - lifetime); + lifetime, + useJwtAccessWithScope); } public final String getClientId() { @@ -776,6 +781,10 @@ int getLifetime() { return lifetime; } + public boolean getUseJwtAccessWithScope() { + return useJwtAccessWithScope; + } + @Override public String getAccount() { return getClientEmail(); @@ -833,7 +842,8 @@ public int hashCode() { scopes, defaultScopes, quotaProjectId, - lifetime); + lifetime, + useJwtAccessWithScope); } @Override @@ -849,6 +859,7 @@ public String toString() { .add("serviceAccountUser", serviceAccountUser) .add("quotaProjectId", quotaProjectId) .add("lifetime", lifetime) + .add("useJwtAccessWithScope", useJwtAccessWithScope) .toString(); } @@ -867,7 +878,8 @@ public boolean equals(Object obj) { && Objects.equals(this.scopes, other.scopes) && Objects.equals(this.defaultScopes, other.defaultScopes) && Objects.equals(this.quotaProjectId, other.quotaProjectId) - && Objects.equals(this.lifetime, other.lifetime); + && Objects.equals(this.lifetime, other.lifetime) + && Objects.equals(this.useJwtAccessWithScope, other.useJwtAccessWithScope); } String createAssertion(JsonFactory jsonFactory, long currentTime, String audience) @@ -937,11 +949,58 @@ String createAssertionForIdToken( } } + /** + * Self signed JWT uses uri as audience, which should have the "https://{host}/" format. For + * instance, if the uri is "https://compute.googleapis.com/compute/v1/projects/", then this + * function returns "https://compute.googleapis.com/". + */ + @VisibleForTesting + static URI getUriForSelfSignedJWT(URI uri) { + if (uri == null || uri.getScheme() == null || uri.getHost() == null) { + return uri; + } + try { + return new URI(uri.getScheme(), uri.getHost(), "/", null); + } catch (URISyntaxException unused) { + return uri; + } + } + + @VisibleForTesting + JwtCredentials createSelfSignedJwtCredentials(final URI uri) { + // Create a JwtCredentials for self signed JWT. See https://google.aip.dev/auth/4111. + JwtClaims.Builder claimsBuilder = + JwtClaims.newBuilder().setIssuer(clientEmail).setSubject(clientEmail); + + if (uri == null) { + // If uri is null, use scopes. + String scopeClaim = ""; + if (!scopes.isEmpty()) { + scopeClaim = Joiner.on(' ').join(scopes); + } else { + scopeClaim = Joiner.on(' ').join(defaultScopes); + } + claimsBuilder.setAdditionalClaims(Collections.singletonMap("scope", scopeClaim)); + } else { + // otherwise, use audience with the uri. + claimsBuilder.setAudience(getUriForSelfSignedJWT(uri).toString()); + } + return JwtCredentials.newBuilder() + .setPrivateKey(privateKey) + .setPrivateKeyId(privateKeyId) + .setJwtClaims(claimsBuilder.build()) + .setClock(clock) + .build(); + } + @Override public void getRequestMetadata( final URI uri, Executor executor, final RequestMetadataCallback callback) { - if (jwtCredentials != null && uri != null) { - jwtCredentials.getRequestMetadata(uri, executor, callback); + if (useJwtAccessWithScope) { + // This will call getRequestMetadata(URI uri), which handles self signed JWT logic. + // Self signed JWT doesn't use network, so here we do a blocking call to improve + // efficiency. executor will be ignored since it is intended for async operation. + blockingGetToCallback(uri, callback); } else { super.getRequestMetadata(uri, executor, callback); } @@ -950,17 +1009,31 @@ public void getRequestMetadata( /** Provide the request metadata by putting an access JWT directly in the metadata. */ @Override public Map> getRequestMetadata(URI uri) throws IOException { - if (scopes.isEmpty() && defaultScopes.isEmpty() && uri == null) { + if (createScopedRequired() && uri == null) { throw new IOException( - "Scopes and uri are not configured for service account. Either pass uri" - + " to getRequestMetadata to use self signed JWT, or specify the scopes" - + " by calling createScoped or passing scopes to constructor."); + "Scopes and uri are not configured for service account. Specify the scopes" + + " by calling createScoped or passing scopes to constructor or" + + " providing uri to getRequestMetadata."); } - if (jwtCredentials != null && uri != null) { - return jwtCredentials.getRequestMetadata(uri); - } else { + + // If scopes are provided but we cannot use self signed JWT, then use scopes to get access + // token. + if (!createScopedRequired() && !useJwtAccessWithScope) { return super.getRequestMetadata(uri); } + + // If scopes are provided and self signed JWT can be used, use self signed JWT with scopes. + // Otherwise, use self signed JWT with uri as the audience. + JwtCredentials jwtCredentials; + if (!createScopedRequired() && useJwtAccessWithScope) { + // Create JWT credentials with the scopes. + jwtCredentials = createSelfSignedJwtCredentials(null); + } else { + // Create JWT credentials with the uri as audience. + jwtCredentials = createSelfSignedJwtCredentials(uri); + } + Map> requestMetadata = jwtCredentials.getRequestMetadata(null); + return addQuotaProjectIdToRequestMetadata(quotaProjectId, requestMetadata); } @SuppressWarnings("unused") @@ -997,6 +1070,7 @@ public static class Builder extends GoogleCredentials.Builder { private HttpTransportFactory transportFactory; private String quotaProjectId; private int lifetime = DEFAULT_LIFETIME_IN_SECONDS; + private boolean useJwtAccessWithScope = false; protected Builder() {} @@ -1013,6 +1087,7 @@ protected Builder(ServiceAccountCredentials credentials) { this.projectId = credentials.projectId; this.quotaProjectId = credentials.quotaProjectId; this.lifetime = credentials.lifetime; + this.useJwtAccessWithScope = credentials.useJwtAccessWithScope; } public Builder setClientId(String clientId) { @@ -1077,6 +1152,11 @@ public Builder setLifetime(int lifetime) { return this; } + public Builder setUseJwtAccessWithScope(boolean useJwtAccessWithScope) { + this.useJwtAccessWithScope = useJwtAccessWithScope; + return this; + } + public String getClientId() { return clientId; } @@ -1125,6 +1205,10 @@ public int getLifetime() { return lifetime; } + public boolean getUseJwtAccessWithScope() { + return useJwtAccessWithScope; + } + public ServiceAccountCredentials build() { return new ServiceAccountCredentials( clientId, @@ -1138,7 +1222,8 @@ public ServiceAccountCredentials build() { serviceAccountUser, projectId, quotaProjectId, - lifetime); + lifetime, + useJwtAccessWithScope); } } } diff --git a/oauth2_http/java/com/google/auth/oauth2/ServiceAccountJwtAccessCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ServiceAccountJwtAccessCredentials.java index 2380b1cf6..707b657a7 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ServiceAccountJwtAccessCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ServiceAccountJwtAccessCredentials.java @@ -54,7 +54,6 @@ import java.io.InputStream; import java.io.ObjectInputStream; import java.net.URI; -import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; @@ -332,35 +331,17 @@ public boolean hasRequestMetadataOnly() { return true; } - /** - * Self signed JWT uses uri as audience, which should have the "https://{host}/" format. For - * instance, if the uri is "https://compute.googleapis.com/compute/v1/projects/", then this - * function returns "https://compute.googleapis.com/". - */ - @VisibleForTesting - static URI getUriForSelfSignedJWT(URI uri) { - if (uri == null || uri.getScheme() == null || uri.getHost() == null) { - return uri; - } - try { - return new URI(uri.getScheme(), uri.getHost(), "/", null); - } catch (URISyntaxException unused) { - return uri; - } - } - @Override public void getRequestMetadata( final URI uri, Executor executor, final RequestMetadataCallback callback) { // It doesn't use network. Only some CPU work on par with TLS handshake. So it's preferrable // to do it in the current thread, which is likely to be the network thread. - blockingGetToCallback(getUriForSelfSignedJWT(uri), callback); + blockingGetToCallback(uri, callback); } /** Provide the request metadata by putting an access JWT directly in the metadata. */ @Override public Map> getRequestMetadata(URI uri) throws IOException { - uri = getUriForSelfSignedJWT(uri); if (uri == null) { if (defaultAudience != null) { uri = defaultAudience; diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ITDownscopingTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ITDownscopingTest.java new file mode 100644 index 000000000..bd00f42c4 --- /dev/null +++ b/oauth2_http/javatests/com/google/auth/oauth2/ITDownscopingTest.java @@ -0,0 +1,150 @@ +/* + * Copyright 2021 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.oauth2; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +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 java.io.IOException; +import org.junit.Test; + +/** + * Integration tests for Downscoping with Credential Access Boundaries via {@link + * DownscopedCredentials}. + * + *

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 ITDownscopingTest { + + // 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 from a service account credential 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 downscoping_serviceAccountSourceWithRefresh() throws IOException { + OAuth2CredentialsWithRefresh.OAuth2RefreshHandler refreshHandler = + new OAuth2CredentialsWithRefresh.OAuth2RefreshHandler() { + @Override + public AccessToken refreshAccessToken() throws IOException { + + ServiceAccountCredentials credentials = + (ServiceAccountCredentials) + GoogleCredentials.getApplicationDefault() + .createScoped("https://www.googleapis.com/auth/cloud-platform"); + + DownscopedCredentials downscopedCredentials = + DownscopedCredentials.newBuilder() + .setSourceCredential(credentials) + .setCredentialAccessBoundary(CREDENTIAL_ACCESS_BOUNDARY) + .build(); + + return downscopedCredentials.refreshAccessToken(); + } + }; + + 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. + try { + retrieveObjectFromGcs(credentials, GCS_OBJECT_NAME_WITHOUT_PERMISSION); + fail("Call to GCS should have failed."); + } catch (HttpResponseException e) { + assertEquals(403, e.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/oauth2_http/javatests/com/google/auth/oauth2/JwtClaimsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/JwtClaimsTest.java index b67f110de..a54f87e65 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/JwtClaimsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/JwtClaimsTest.java @@ -136,4 +136,16 @@ public void testMergeAdditionalClaims() { assertEquals("bar", mergedAdditionalClaims.get("foo")); assertEquals("qwer", mergedAdditionalClaims.get("asdf")); } + + @Test + public void testIsComplete() { + // Test JwtClaim is complete if audience is not set but scope is provided. + JwtClaims claims = + JwtClaims.newBuilder() + .setIssuer("issuer-1") + .setSubject("subject-1") + .setAdditionalClaims(Collections.singletonMap("scope", "foo")) + .build(); + assertTrue(claims.isComplete()); + } } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountCredentialsTest.java index 3fc903d73..803d4fc8f 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountCredentialsTest.java @@ -420,20 +420,15 @@ public void createdScoped_enablesAccessTokens() throws IOException { try { credentials.getRequestMetadata(null); - fail("Should not be able to get token without scopes, defaultScopes and uri"); + fail("Should not be able to get token without scopes"); } catch (IOException e) { assertTrue( "expected to fail with exception", e.getMessage().contains("Scopes and uri are not configured for service account")); } - // Since scopes are not provided, self signed JWT will be used. - Map> metadata = credentials.getRequestMetadata(CALL_URI); - verifyJwtAccess(metadata); - - // Since scopes are provided, self signed JWT will not be used. GoogleCredentials scopedCredentials = credentials.createScoped(SCOPES); - metadata = scopedCredentials.getRequestMetadata(CALL_URI); + Map> metadata = scopedCredentials.getRequestMetadata(CALL_URI); TestUtils.assertContainsBearerToken(metadata, ACCESS_TOKEN); } @@ -1074,7 +1069,7 @@ public void toString_containsFields() throws IOException { String.format( "ServiceAccountCredentials{clientId=%s, clientEmail=%s, privateKeyId=%s, " + "transportFactoryClassName=%s, tokenServerUri=%s, scopes=%s, defaultScopes=%s, serviceAccountUser=%s, " - + "quotaProjectId=%s, lifetime=3600}", + + "quotaProjectId=%s, lifetime=3600, useJwtAccessWithScope=false}", CLIENT_ID, CLIENT_EMAIL, PRIVATE_KEY_ID, @@ -1236,6 +1231,29 @@ public void fromStream_noPrivateKeyId_throws() throws IOException { testFromStreamException(serviceAccountStream, "private_key_id"); } + @Test + public void getUriForSelfSignedJWT() { + assertNull(ServiceAccountCredentials.getUriForSelfSignedJWT(null)); + + URI uri = URI.create("https://compute.googleapis.com/compute/v1/projects/"); + URI expected = URI.create("https://compute.googleapis.com/"); + assertEquals(expected, ServiceAccountCredentials.getUriForSelfSignedJWT(uri)); + } + + @Test + public void getUriForSelfSignedJWT_noHost() { + URI uri = URI.create("file:foo"); + URI expected = URI.create("file:foo"); + assertEquals(expected, ServiceAccountCredentials.getUriForSelfSignedJWT(uri)); + } + + @Test + public void getUriForSelfSignedJWT_forStaticAudience_returnsURI() { + URI uri = URI.create("compute.googleapis.com"); + URI expected = URI.create("compute.googleapis.com"); + assertEquals(expected, ServiceAccountCredentials.getUriForSelfSignedJWT(uri)); + } + @Test public void getRequestMetadataSetsQuotaProjectId() throws IOException { MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory(); @@ -1328,11 +1346,65 @@ public void onFailure(Throwable exception) { } @Test - public void getRequestMetadataWithCallback_selfSignedJWT() throws IOException { - MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory(); - transportFactory.transport.addClient(CLIENT_ID, "unused-client-secret"); - transportFactory.transport.addServiceAccount(CLIENT_EMAIL, ACCESS_TOKEN); + public void getRequestMetadata_selfSignedJWT_withScopes() throws IOException { + PrivateKey privateKey = ServiceAccountCredentials.privateKeyFromPkcs8(PRIVATE_KEY_PKCS8); + GoogleCredentials credentials = + ServiceAccountCredentials.newBuilder() + .setClientId(CLIENT_ID) + .setClientEmail(CLIENT_EMAIL) + .setPrivateKey(privateKey) + .setPrivateKeyId(PRIVATE_KEY_ID) + .setScopes(SCOPES) + .setServiceAccountUser(USER) + .setProjectId(PROJECT_ID) + .setHttpTransportFactory(new MockTokenServerTransportFactory()) + .setUseJwtAccessWithScope(true) + .build(); + + Map> metadata = credentials.getRequestMetadata(CALL_URI); + verifyJwtAccess(metadata, "dummy.scope"); + } + @Test + public void getRequestMetadata_selfSignedJWT_withAudience() throws IOException { + PrivateKey privateKey = ServiceAccountCredentials.privateKeyFromPkcs8(PRIVATE_KEY_PKCS8); + GoogleCredentials credentials = + ServiceAccountCredentials.newBuilder() + .setClientId(CLIENT_ID) + .setClientEmail(CLIENT_EMAIL) + .setPrivateKey(privateKey) + .setPrivateKeyId(PRIVATE_KEY_ID) + .setServiceAccountUser(USER) + .setProjectId(PROJECT_ID) + .setHttpTransportFactory(new MockTokenServerTransportFactory()) + .build(); + + Map> metadata = credentials.getRequestMetadata(CALL_URI); + verifyJwtAccess(metadata, null); + } + + @Test + public void getRequestMetadata_selfSignedJWT_withDefaultScopes() throws IOException { + PrivateKey privateKey = ServiceAccountCredentials.privateKeyFromPkcs8(PRIVATE_KEY_PKCS8); + GoogleCredentials credentials = + ServiceAccountCredentials.newBuilder() + .setClientId(CLIENT_ID) + .setClientEmail(CLIENT_EMAIL) + .setPrivateKey(privateKey) + .setPrivateKeyId(PRIVATE_KEY_ID) + .setScopes(null, SCOPES) + .setServiceAccountUser(USER) + .setProjectId(PROJECT_ID) + .setHttpTransportFactory(new MockTokenServerTransportFactory()) + .setUseJwtAccessWithScope(true) + .build(); + + Map> metadata = credentials.getRequestMetadata(null); + verifyJwtAccess(metadata, "dummy.scope"); + } + + @Test + public void getRequestMetadataWithCallback_selfSignedJWT() throws IOException { PrivateKey privateKey = ServiceAccountCredentials.privateKeyFromPkcs8(PRIVATE_KEY_PKCS8); GoogleCredentials credentials = ServiceAccountCredentials.newBuilder() @@ -1340,11 +1412,12 @@ public void getRequestMetadataWithCallback_selfSignedJWT() throws IOException { .setClientEmail(CLIENT_EMAIL) .setPrivateKey(privateKey) .setPrivateKeyId(PRIVATE_KEY_ID) - .setScopes(null, DEFAULT_SCOPES) .setServiceAccountUser(USER) .setProjectId(PROJECT_ID) .setQuotaProjectId("my-quota-project-id") - .setHttpTransportFactory(transportFactory) + .setHttpTransportFactory(new MockTokenServerTransportFactory()) + .setUseJwtAccessWithScope(true) + .setScopes(SCOPES) .build(); final AtomicBoolean success = new AtomicBoolean(false); @@ -1355,7 +1428,7 @@ public void getRequestMetadataWithCallback_selfSignedJWT() throws IOException { @Override public void onSuccess(Map> metadata) { try { - verifyJwtAccess(metadata); + verifyJwtAccess(metadata, "dummy.scope"); } catch (IOException e) { fail("Should not throw a failure"); } @@ -1371,7 +1444,8 @@ public void onFailure(Throwable exception) { assertTrue("Should have run onSuccess() callback", success.get()); } - private void verifyJwtAccess(Map> metadata) throws IOException { + private void verifyJwtAccess(Map> metadata, String expectedScopeClaim) + throws IOException { assertNotNull(metadata); List authorizations = metadata.get(AuthHttpConstants.AUTHORIZATION); assertNotNull("Authorization headers not found", authorizations); @@ -1387,7 +1461,13 @@ private void verifyJwtAccess(Map> metadata) throws IOExcept JsonWebSignature.parse(GsonFactory.getDefaultInstance(), assertion); assertEquals(CLIENT_EMAIL, signature.getPayload().getIssuer()); assertEquals(CLIENT_EMAIL, signature.getPayload().getSubject()); - assertEquals(JWT_AUDIENCE, signature.getPayload().getAudience()); + if (expectedScopeClaim != null) { + assertEquals(expectedScopeClaim, signature.getPayload().get("scope")); + assertFalse(signature.getPayload().containsKey("aud")); + } else { + assertEquals(JWT_AUDIENCE, signature.getPayload().getAudience()); + assertFalse(signature.getPayload().containsKey("scope")); + } assertEquals(PRIVATE_KEY_ID, signature.getHeader().getKeyId()); } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountJwtAccessCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountJwtAccessCredentialsTest.java index 81b5f7de3..5020317f2 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountJwtAccessCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ServiceAccountJwtAccessCredentialsTest.java @@ -92,7 +92,6 @@ public class ServiceAccountJwtAccessCredentialsTest extends BaseSerializationTes private static final String JWT_ACCESS_PREFIX = ServiceAccountJwtAccessCredentials.JWT_ACCESS_PREFIX; private static final URI CALL_URI = URI.create("http://googleapis.com/testapi/v1/foo"); - private static final URI CALL_URI_AUDIENCE = URI.create("http://googleapis.com/"); private static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance(); private static final String QUOTA_PROJECT = "sample-quota-project-id"; @@ -172,29 +171,6 @@ public void getAuthenticationType_returnsJwtAccess() throws IOException { assertEquals(credentials.getAuthenticationType(), "JWTAccess"); } - @Test - public void getUriForSelfSignedJWT() { - assertNull(ServiceAccountJwtAccessCredentials.getUriForSelfSignedJWT(null)); - - URI uri = URI.create("https://compute.googleapis.com/compute/v1/projects/"); - URI expected = URI.create("https://compute.googleapis.com/"); - assertEquals(expected, ServiceAccountJwtAccessCredentials.getUriForSelfSignedJWT(uri)); - } - - @Test - public void getUriForSelfSignedJWT_noHost() { - URI uri = URI.create("file:foo"); - URI expected = URI.create("file:foo"); - assertEquals(expected, ServiceAccountJwtAccessCredentials.getUriForSelfSignedJWT(uri)); - } - - @Test - public void getUriForSelfSignedJWT_forStaticAudience_returnsURI() { - URI uri = URI.create("compute.googleapis.com"); - URI expected = URI.create("compute.googleapis.com"); - assertEquals(expected, ServiceAccountJwtAccessCredentials.getUriForSelfSignedJWT(uri)); - } - @Test public void hasRequestMetadata_returnsTrue() throws IOException { Credentials credentials = @@ -224,7 +200,7 @@ public void getRequestMetadata_blocking_hasJwtAccess() throws IOException { Map> metadata = credentials.getRequestMetadata(CALL_URI); - verifyJwtAccess(metadata, SA_CLIENT_EMAIL, CALL_URI_AUDIENCE, SA_PRIVATE_KEY_ID); + verifyJwtAccess(metadata, SA_CLIENT_EMAIL, CALL_URI, SA_PRIVATE_KEY_ID); } @Test @@ -329,7 +305,7 @@ public void getRequestMetadata_async_hasJwtAccess() throws IOException { credentials.getRequestMetadata(CALL_URI, executor, callback); assertEquals(0, executor.numTasks()); assertNotNull(callback.metadata); - verifyJwtAccess(callback.metadata, SA_CLIENT_EMAIL, CALL_URI_AUDIENCE, SA_PRIVATE_KEY_ID); + verifyJwtAccess(callback.metadata, SA_CLIENT_EMAIL, CALL_URI, SA_PRIVATE_KEY_ID); } @Test @@ -678,7 +654,7 @@ public void fromStream_hasJwtAccess() throws IOException { assertNotNull(credentials); Map> metadata = credentials.getRequestMetadata(CALL_URI); - verifyJwtAccess(metadata, SA_CLIENT_EMAIL, CALL_URI_AUDIENCE, SA_PRIVATE_KEY_ID); + verifyJwtAccess(metadata, SA_CLIENT_EMAIL, CALL_URI, SA_PRIVATE_KEY_ID); } @Test diff --git a/oauth2_http/javatests/com/google/auth/oauth2/functional/FTServiceAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/functional/FTServiceAccountCredentialsTest.java new file mode 100644 index 000000000..e70db86a2 --- /dev/null +++ b/oauth2_http/javatests/com/google/auth/oauth2/functional/FTServiceAccountCredentialsTest.java @@ -0,0 +1,189 @@ +/* + * Copyright 2021 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.oauth2.functional; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.gson.GsonFactory; +import com.google.api.client.json.webtoken.JsonWebSignature; +import com.google.auth.http.HttpCredentialsAdapter; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.auth.oauth2.IdToken; +import com.google.auth.oauth2.IdTokenCredentials; +import com.google.auth.oauth2.IdTokenProvider; +import java.io.FileNotFoundException; +import java.io.IOException; +import org.junit.Test; + +public final class FTServiceAccountCredentialsTest { + private final String cloudPlatformScope = "https://www.googleapis.com/auth/cloud-platform"; + + private final String cloudTasksUrl = + "https://cloudtasks.googleapis.com/v2/projects/gcloud-devel/locations"; + private final String storageUrl = + "https://storage.googleapis.com/storage/v1/b?project=gcloud-devel"; + private final String bigQueryUrl = + "https://bigquery.googleapis.com/bigquery/v2/projects/gcloud-devel/datasets"; + private final String computeUrl = + "https://compute.googleapis.com/compute/v1/projects/gcloud-devel/zones/us-central1-a/instances"; + + @Test + public void NoScopeNoAudienceComputeTest() throws Exception { + HttpResponse response = executeRequestWithCredentialsWithoutScope(computeUrl); + assertEquals(200, response.getStatusCode()); + } + + @Test + public void NoScopeNoAudienceBigQueryTest() throws Exception { + HttpResponse response = executeRequestWithCredentialsWithoutScope(bigQueryUrl); + assertEquals(200, response.getStatusCode()); + } + + @Test + public void NoScopeNoAudienceOnePlatformTest() throws Exception { + HttpResponse response = executeRequestWithCredentialsWithoutScope(cloudTasksUrl); + assertEquals(200, response.getStatusCode()); + } + + // TODO: add Storage case + + @Test + public void AudienceSetNoScopeTest() throws Exception { + final GoogleCredentials credentials = GoogleCredentials.getApplicationDefault(); + + IdTokenCredentials tokenCredential = + IdTokenCredentials.newBuilder() + .setIdTokenProvider((IdTokenProvider) credentials) + .setTargetAudience(cloudTasksUrl) + .build(); + + assertNull(tokenCredential.getIdToken()); + tokenCredential.refresh(); + IdToken idToken = tokenCredential.getIdToken(); + assertNotNull(idToken); + assertTrue(idToken.getExpirationTime().getTime() > System.currentTimeMillis()); + JsonWebSignature jws = + JsonWebSignature.parse(GsonFactory.getDefaultInstance(), idToken.getTokenValue()); + assertEquals(cloudTasksUrl, jws.getPayload().get("aud")); + assertEquals("https://accounts.google.com", jws.getPayload().get("iss")); + } + + @Test + public void ScopeSetNoAudienceStorageTest() throws Exception { + HttpResponse response = executeRequestWithCredentialsWithScope(storageUrl, cloudPlatformScope); + assertEquals(200, response.getStatusCode()); + } + + @Test + public void ScopeSetNoAudienceComputeTest() throws Exception { + + HttpResponse response = executeRequestWithCredentialsWithScope(computeUrl, cloudPlatformScope); + assertEquals(200, response.getStatusCode()); + } + + @Test + public void ScopeSetNoAudienceBigQueryTest() throws Exception { + HttpResponse response = executeRequestWithCredentialsWithScope(bigQueryUrl, cloudPlatformScope); + assertEquals(200, response.getStatusCode()); + } + + @Test + public void ScopeSetNoAudienceOnePlatformTest() throws Exception { + HttpResponse response = + executeRequestWithCredentialsWithScope(cloudTasksUrl, cloudPlatformScope); + assertEquals(200, response.getStatusCode()); + } + + @Test + public void WrongScopeComputeTest() throws Exception { + executeRequestWrongScope(computeUrl); + } + + @Test + public void WrongScopeStorageTest() throws Exception { + executeRequestWrongScope(storageUrl); + } + + @Test + public void WrongScopeBigQueryTest() throws Exception { + executeRequestWrongScope(bigQueryUrl); + } + + @Test + public void WrongScopeOnePlatformTest() throws Exception { + executeRequestWrongScope(cloudTasksUrl); + } + + private void executeRequestWrongScope(String serviceUri) + throws FileNotFoundException, IOException { + String expectedMessage = "403 Forbidden"; + + try { + executeRequestWithCredentialsWithScope( + serviceUri, "https://www.googleapis.com/auth/adexchange.buyer"); + fail("Should throw exception: " + expectedMessage); + } catch (IOException expected) { + assertTrue(expected.getMessage().contains(expectedMessage)); + } + } + + private HttpResponse executeRequestWithCredentialsWithoutScope(String serviceUrl) + throws IOException { + final GoogleCredentials credentials = GoogleCredentials.getApplicationDefault(); + GenericUrl genericUrl = new GenericUrl(serviceUrl); + HttpCredentialsAdapter adapter = new HttpCredentialsAdapter(credentials); + HttpTransport transport = new NetHttpTransport(); + HttpRequest request = transport.createRequestFactory(adapter).buildGetRequest(genericUrl); + return request.execute(); + } + + private HttpResponse executeRequestWithCredentialsWithScope(String serviceUrl, String scope) + throws IOException { + + final GoogleCredentials credentials = + GoogleCredentials.getApplicationDefault().createScoped(scope); + GenericUrl genericUrl = new GenericUrl(serviceUrl); + HttpCredentialsAdapter adapter = new HttpCredentialsAdapter(credentials); + HttpTransport transport = new NetHttpTransport(); + HttpRequest request = transport.createRequestFactory(adapter).buildGetRequest(genericUrl); + return request.execute(); + } +} diff --git a/oauth2_http/pom.xml b/oauth2_http/pom.xml index 4e42bbcc0..935685d64 100644 --- a/oauth2_http/pom.xml +++ b/oauth2_http/pom.xml @@ -5,7 +5,7 @@ com.google.auth google-auth-library-parent - 0.26.1-SNAPSHOT + 0.27.0 ../pom.xml @@ -67,6 +67,7 @@ sponge_log **/IT*.java + **/functional/*.java @@ -85,6 +86,7 @@ **/IT*.java + **/functional/*.java sponge_log diff --git a/pom.xml b/pom.xml index 0dbdff717..4f6b4962d 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.google.auth google-auth-library-parent - 0.26.1-SNAPSHOT + 0.27.0 pom Google Auth Library for Java Client libraries providing authentication and diff --git a/scripts/downscoping-with-cab-setup.sh b/scripts/downscoping-with-cab-setup.sh new file mode 100755 index 000000000..e2f847d94 --- /dev/null +++ b/scripts/downscoping-with-cab-setup.sh @@ -0,0 +1,96 @@ +#!/bin/bash + +# Copyright 2021 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. + +# This script is used to generate the project configurations needed to +# end-to-end test Downscoping with Credential Access Boundaries in the Auth +# library. +# +# In order to run this script, you need to fill in the project_id and +# service_account_email variables. +# +# This script needs to be run once. It will do the following: +# 1. Sets the current project to the one specified. +# 2. Creates a GCS bucket in the specified project. +# 3. Gives the specified service account the objectAdmin role for this bucket. +# 4. Creates two text files to be uploaded to the created bucket. +# 5. Uploads both text files. +# 6. Prints out the identifiers (bucket ID, first object ID, second object ID) +# to be used in the accompanying tests. +# 7. Deletes the created text files in the current directory. +# +# The same service account used for this setup script should be used for +# the integration tests. +# +# It is safe to run the setup script again. A new bucket is created along with +# new objects. If run multiple times, it is advisable to delete +# unused buckets. + +suffix="" + +function generate_random_string () { + local valid_chars=abcdefghijklmnopqrstuvwxyz0123456789 + for i in {1..8} ; do + suffix+="${valid_chars:RANDOM%${#valid_chars}:1}" + done +} + +generate_random_string + +bucket_id="cab-int-bucket-"${suffix} +first_object="cab-first-"${suffix}.txt +second_object="cab-second-"${suffix}.txt + +# Fill in. +project_id="" +service_account_email="" + +gcloud config set project ${project_id} + +# Create the GCS bucket. +gsutil mb -b on -l us-east1 gs://${bucket_id} + +# Give the specified service account the objectAdmin role for this bucket. +gsutil iam ch serviceAccount:${service_account_email}:objectAdmin gs://${bucket_id} + +# Create both objects. +echo "first" >> ${first_object} +echo "second" >> ${second_object} + +# Upload the created objects to the bucket. +gsutil cp ${first_object} gs://${bucket_id} +gsutil cp ${second_object} gs://${bucket_id} + +echo "Bucket ID: "${bucket_id} +echo "First object ID: "${first_object} +echo "Second object ID: "${second_object} + +# Cleanup. +rm ${first_object} +rm ${second_object} diff --git a/versions.txt b/versions.txt index c886ee625..7aef575ee 100644 --- a/versions.txt +++ b/versions.txt @@ -1,9 +1,9 @@ # Format: # module:released-version:current-version -google-auth-library:0.26.0:0.26.1-SNAPSHOT -google-auth-library-bom:0.26.0:0.26.1-SNAPSHOT -google-auth-library-parent:0.26.0:0.26.1-SNAPSHOT -google-auth-library-appengine:0.26.0:0.26.1-SNAPSHOT -google-auth-library-credentials:0.26.0:0.26.1-SNAPSHOT -google-auth-library-oauth2-http:0.26.0:0.26.1-SNAPSHOT +google-auth-library:0.27.0:0.27.0 +google-auth-library-bom:0.27.0:0.27.0 +google-auth-library-parent:0.27.0:0.27.0 +google-auth-library-appengine:0.27.0:0.27.0 +google-auth-library-credentials:0.27.0:0.27.0 +google-auth-library-oauth2-http:0.27.0:0.27.0