1// Copyright 2021 Google LLC.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package downscope
6
7import (
8	"context"
9	"flag"
10	"fmt"
11	"io/ioutil"
12	"log"
13	"os"
14	"testing"
15	"time"
16
17	"google.golang.org/api/option"
18
19	"golang.org/x/oauth2"
20	"golang.org/x/oauth2/google"
21	"golang.org/x/oauth2/google/downscope"
22	storage "google.golang.org/api/storage/v1"
23	"google.golang.org/api/transport"
24)
25
26const (
27	rootTokenScope        = "https://www.googleapis.com/auth/cloud-platform"
28	envServiceAccountFile = "GCLOUD_TESTS_GOLANG_KEY"
29	object1               = "cab-first-c45wknuy.txt"
30	object2               = "cab-second-c45wknuy.txt"
31	bucket                = "dulcet-port-762"
32)
33
34var (
35	rootCredential *google.Credentials
36)
37
38// TestMain contains all of the setup code that needs to be run once before any of the tests are run
39func TestMain(m *testing.M) {
40	flag.Parse()
41	if testing.Short() {
42		// This line runs all of our individual tests
43		os.Exit(m.Run())
44	}
45	ctx := context.Background()
46	credentialFileName := os.Getenv(envServiceAccountFile)
47
48	var err error
49	rootCredential, err = transport.Creds(ctx, option.WithCredentialsFile(credentialFileName), option.WithScopes(rootTokenScope))
50
51	if err != nil {
52		log.Fatalf("failed to construct root credential: %v", err)
53	}
54
55	// This line runs all of our individual tests
56	os.Exit(m.Run())
57
58}
59
60// downscopeTest holds the parameters necessary for running a test of the token downscoping capabilities implemented in `oauth2/google/downscope`
61type downscopeTest struct {
62	name                 string
63	availableResource    string
64	availablePermissions []string
65	condition            downscope.AvailabilityCondition
66	objectName           string
67	rootSource           oauth2.TokenSource
68	expectError          bool
69}
70
71func TestDownscopedToken(t *testing.T) {
72	if testing.Short() {
73		t.Skip("skipping integration test")
74	}
75
76	var downscopeTests = []downscopeTest{
77		{
78			name:                 "successfulDownscopedRead",
79			availableResource:    "//storage.googleapis.com/projects/_/buckets/" + bucket,
80			availablePermissions: []string{"inRole:roles/storage.objectViewer"},
81			condition: downscope.AvailabilityCondition{
82				Expression: "resource.name.startsWith('projects/_/buckets/" + bucket + "/objects/" + object1 + "')",
83			},
84			rootSource:  rootCredential.TokenSource,
85			objectName:  object1,
86			expectError: false,
87		},
88		{
89			name:                 "readWithoutPermission",
90			availableResource:    "//storage.googleapis.com/projects/_/buckets/" + bucket,
91			availablePermissions: []string{"inRole:roles/storage.objectViewer"},
92			condition: downscope.AvailabilityCondition{
93				Expression: "resource.name.startsWith('projects/_/buckets/" + bucket + "/objects/" + object1 + "')",
94			},
95			rootSource:  rootCredential.TokenSource,
96			objectName:  object2,
97			expectError: true,
98		},
99	}
100
101	for _, tt := range downscopeTests {
102		t.Run(tt.name, func(t *testing.T) {
103			err := downscopeQuery(t, tt)
104			// If a test isn't supposed to fail, it shouldn't fail.
105			if !tt.expectError && err != nil {
106				t.Errorf("test case %v should have succeeded, but instead returned %v", tt.name, err)
107			} else if tt.expectError && err == nil { // If a test is supposed to fail, it should return a non-nil error.
108				t.Errorf(" test case %v should have returned an error, but instead returned nil", tt.name)
109			}
110		})
111	}
112}
113
114// I'm not sure what I should name this according to convention.
115func downscopeQuery(t *testing.T, tt downscopeTest) error {
116	t.Helper()
117	ctx := context.Background()
118
119	// Initializes an accessBoundary
120	var AccessBoundaryRules []downscope.AccessBoundaryRule
121	AccessBoundaryRules = append(AccessBoundaryRules, downscope.AccessBoundaryRule{AvailableResource: tt.availableResource, AvailablePermissions: tt.availablePermissions, Condition: &tt.condition})
122
123	downscopedTokenSource, err := downscope.NewTokenSource(context.Background(), downscope.DownscopingConfig{RootSource: tt.rootSource, Rules: AccessBoundaryRules})
124	if err != nil {
125		return fmt.Errorf("failed to create the initial token source: %v", err)
126	}
127	downscopedTokenSource = oauth2.ReuseTokenSource(nil, downscopedTokenSource)
128
129	ctx, cancel := context.WithTimeout(ctx, time.Second*30)
130	defer cancel()
131	storageService, err := storage.NewService(ctx, option.WithTokenSource(downscopedTokenSource))
132	if err != nil {
133		return fmt.Errorf("failed to create the storage service: %v", err)
134	}
135	resp, err := storageService.Objects.Get(bucket, tt.objectName).Download()
136	if err != nil {
137		return fmt.Errorf("failed to retrieve object from GCP project with error: %v", err)
138	}
139	defer resp.Body.Close()
140	_, err = ioutil.ReadAll(resp.Body)
141	if err != nil {
142		return fmt.Errorf("ioutil.ReadAll: %v", err)
143	}
144	return nil
145}
146