From 81b294fe1c5db355d34bd23d05900ac0d416391e Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Thu, 22 Jul 2021 10:48:11 -0700 Subject: [PATCH] test(downscope): add integration tests for token downscoping with Credential Access Boundaries (#1124) Testing the code present in `oauth2/google/downscope`. I also caught a typo in the comments of a previous push- given that I only needed to add a single space, I bundled that change into this PR. --- integration-tests/downscope/downscope_test.go | 145 ++++++++++++++++++ integration-tests/downscope/setup.sh | 81 ++++++++++ .../impersonate/impersonate_test.go | 3 +- 3 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 integration-tests/downscope/downscope_test.go create mode 100644 integration-tests/downscope/setup.sh diff --git a/integration-tests/downscope/downscope_test.go b/integration-tests/downscope/downscope_test.go new file mode 100644 index 00000000000..25553c9c2d7 --- /dev/null +++ b/integration-tests/downscope/downscope_test.go @@ -0,0 +1,145 @@ +// Copyright 2021 Google LLC. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package downscope + +import ( + "context" + "flag" + "fmt" + "io/ioutil" + "log" + "os" + "testing" + "time" + + "google.golang.org/api/option" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "golang.org/x/oauth2/google/downscope" + storage "google.golang.org/api/storage/v1" + "google.golang.org/api/transport" +) + +const ( + rootTokenScope = "https://www.googleapis.com/auth/cloud-platform" + envServiceAccountFile = "GCLOUD_TESTS_GOLANG_KEY" + object1 = "cab-first-c45wknuy.txt" + object2 = "cab-second-c45wknuy.txt" + bucket = "dulcet-port-762" +) + +var ( + rootCredential *google.Credentials +) + +// TestMain contains all of the setup code that needs to be run once before any of the tests are run +func TestMain(m *testing.M) { + flag.Parse() + if testing.Short() { + // This line runs all of our individual tests + os.Exit(m.Run()) + } + ctx := context.Background() + credentialFileName := os.Getenv(envServiceAccountFile) + + var err error + rootCredential, err = transport.Creds(ctx, option.WithCredentialsFile(credentialFileName), option.WithScopes(rootTokenScope)) + + if err != nil { + log.Fatalf("failed to construct root credential: %v", err) + } + + // This line runs all of our individual tests + os.Exit(m.Run()) + +} + +// downscopeTest holds the parameters necessary for running a test of the token downscoping capabilities implemented in `oauth2/google/downscope` +type downscopeTest struct { + name string + availableResource string + availablePermissions []string + condition downscope.AvailabilityCondition + objectName string + rootSource oauth2.TokenSource + expectError bool +} + +func TestDownscopedToken(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + var downscopeTests = []downscopeTest{ + { + name: "successfulDownscopedRead", + availableResource: "//storage.googleapis.com/projects/_/buckets/" + bucket, + availablePermissions: []string{"inRole:roles/storage.objectViewer"}, + condition: downscope.AvailabilityCondition{ + Expression: "resource.name.startsWith('projects/_/buckets/" + bucket + "/objects/" + object1 + "')", + }, + rootSource: rootCredential.TokenSource, + objectName: object1, + expectError: false, + }, + { + name: "readWithoutPermission", + availableResource: "//storage.googleapis.com/projects/_/buckets/" + bucket, + availablePermissions: []string{"inRole:roles/storage.objectViewer"}, + condition: downscope.AvailabilityCondition{ + Expression: "resource.name.startsWith('projects/_/buckets/" + bucket + "/objects/" + object1 + "')", + }, + rootSource: rootCredential.TokenSource, + objectName: object2, + expectError: true, + }, + } + + for _, tt := range downscopeTests { + t.Run(tt.name, func(t *testing.T) { + err := downscopeQuery(t, tt) + // If a test isn't supposed to fail, it shouldn't fail. + if !tt.expectError && err != nil { + t.Errorf("test case %v should have succeeded, but instead returned %v", tt.name, err) + } else if tt.expectError && err == nil { // If a test is supposed to fail, it should return a non-nil error. + t.Errorf(" test case %v should have returned an error, but instead returned nil", tt.name) + } + }) + } +} + +// I'm not sure what I should name this according to convention. +func downscopeQuery(t *testing.T, tt downscopeTest) error { + t.Helper() + ctx := context.Background() + + // Initializes an accessBoundary + var AccessBoundaryRules []downscope.AccessBoundaryRule + AccessBoundaryRules = append(AccessBoundaryRules, downscope.AccessBoundaryRule{AvailableResource: tt.availableResource, AvailablePermissions: tt.availablePermissions, Condition: &tt.condition}) + + downscopedTokenSource, err := downscope.NewTokenSource(context.Background(), downscope.DownscopingConfig{RootSource: tt.rootSource, Rules: AccessBoundaryRules}) + if err != nil { + return fmt.Errorf("failed to create the initial token source: %v", err) + } + downscopedTokenSource = oauth2.ReuseTokenSource(nil, downscopedTokenSource) + + ctx, cancel := context.WithTimeout(ctx, time.Second*30) + defer cancel() + storageService, err := storage.NewService(ctx, option.WithTokenSource(downscopedTokenSource)) + if err != nil { + return fmt.Errorf("failed to create the storage service: %v", err) + } + resp, err := storageService.Objects.Get(bucket, tt.objectName).Download() + if err != nil { + return fmt.Errorf("failed to retrieve object from GCP project with error: %v", err) + } + defer resp.Body.Close() + _, err = ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("ioutil.ReadAll: %v", err) + } + return nil +} diff --git a/integration-tests/downscope/setup.sh b/integration-tests/downscope/setup.sh new file mode 100644 index 00000000000..ba0a20cc705 --- /dev/null +++ b/integration-tests/downscope/setup.sh @@ -0,0 +1,81 @@ +#!/bin/bash + +# Copyright 2021 Google LLC. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + + +# This script is used to generate the project configurations needed to +# end-to-end test Downscoping with Credential Access Boundaries in the Auth +# library. This script only needs to be run once. +# +# In order to run this script, you need to fill in the project_id and +# service_account_email variables. +# +# If an argument is provided, the script will use the provided argument +# as the bucket name. Otherwise, it will create a new bucket. +# +# This script needs to be run once. It will do the following: +# 1. Sets the current project to the one specified. +# 2. If no bucket name was provided, 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 + +first_object="cab-first-"${suffix}.txt +second_object="cab-second-"${suffix}.txt + +# Fill in. +project_id="dulcet-port-762" +service_account_email="kokoro@dulcet-port-762.iam.gserviceaccount.com" + +gcloud config set project ${project_id} + +if (( $# != 1 )) +then + # Create the GCS bucket. + bucket_id="cab-int-bucket-"${suffix} + gsutil mb -b on -l us-east1 gs://${bucket_id} +else + bucket_id="$1" +fi + +# 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/integration-tests/impersonate/impersonate_test.go b/integration-tests/impersonate/impersonate_test.go index efbf920cf31..5a30edb26ee 100644 --- a/integration-tests/impersonate/impersonate_test.go +++ b/integration-tests/impersonate/impersonate_test.go @@ -15,12 +15,11 @@ import ( "time" "google.golang.org/api/option" - "google.golang.org/api/storage/v1" ) var ( - // envReaderCredentialFile points to a service accountthat is a "Service + // envReaderCredentialFile points to a service account that is a "Service // Account Token Creator" on envReaderSA. envBaseSACredentialFile = "API_GO_CLIENT_IMPERSONATE_BASE" // envUserCredentialFile points to a user credential that is a "Service