Skip to content

Commit

Permalink
downscope(examples): show credential downscoping with CAB (GoogleClou…
Browse files Browse the repository at this point in the history
…dPlatform#2181)

This push adds some examples showing how to downscope credentials using CredentialAccessBoundaries (documented [here](https://cloud.google.com/iam/docs/downscoping-short-lived-credentials)).  The added examples use the code stored [here](https://github.com/golang/oauth2/tree/master/google/downscope).
  • Loading branch information
Galadros authored Aug 30, 2021
1 parent 88b2a10 commit 8bbbe5c
Show file tree
Hide file tree
Showing 5 changed files with 382 additions and 0 deletions.
52 changes: 52 additions & 0 deletions auth/downscoping/credential_initialization.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// package downscopedoverview contains Google Cloud auth snippets showing how to
// downscope credentials with Credential Access Boundaries.
// https://cloud.google.com/iam/docs/downscoping-short-lived-credentials
package downscopedoverview

// [START auth_downscoping_initialize_downscoped_cred]

import (
"context"
"fmt"

"golang.org/x/oauth2/google"
"golang.org/x/oauth2/google/downscope"
)

// initializeCredentials will generate a downscoped token using the provided Access Boundary Rules.
func initializeCredentials(accessBoundary []downscope.AccessBoundaryRule) error {
ctx := context.Background()

// You must provide the "https://www.googleapis.com/auth/cloud-platform" scope.
// This Source can be initialized in multiple ways; the following example uses
// Application Default Credentials.
rootSource, err := google.DefaultTokenSource(ctx, "https://www.googleapis.com/auth/cloud-platform")
if err != nil {
return fmt.Errorf("failed to generate rootSource: %v", err)
}

// downscope.NewTokenSource constructs the token source with the configuration provided.
dts, err := downscope.NewTokenSource(ctx, downscope.DownscopingConfig{RootSource: rootSource, Rules: accessBoundary})
if err != nil {
return fmt.Errorf("failed to generate downscoped token source: %v", err)
}
_ = dts
// You can now use dts to access Google Storage resources.
return nil
}

// [END auth_downscoping_initialize_downscoped_cred]
95 changes: 95 additions & 0 deletions auth/downscoping/downscoped_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package downscopedoverview

import (
"bytes"
"context"
"io"
"strings"
"testing"
"time"

"cloud.google.com/go/storage"
"github.com/GoogleCloudPlatform/golang-samples/internal/testutil"
"github.com/google/uuid"
"golang.org/x/oauth2/google/downscope"
)

func TestInitializeCredentials(t *testing.T) {
accessBoundary := []downscope.AccessBoundaryRule{
{
AvailableResource: "//storage.googleapis.com/projects/_/buckets/foo",
AvailablePermissions: []string{"inRole:roles/storage.objectViewer"},
},
}
err := initializeCredentials(accessBoundary)
if err != nil {
t.Errorf("got %v; wanted nil", err)
}
}

func TestReadObjectContents(t *testing.T) {
ctx := context.Background()
tc := testutil.SystemTest(t)

ctx, cancel := context.WithTimeout(ctx, time.Second*30)
defer cancel()
client, err := storage.NewClient(ctx)
if err != nil {
t.Errorf("%v", err)
return
}
randSuffix, err := uuid.NewRandom()
if err != nil {
t.Fatalf("Failed to generate random UUID suffix: %v", err)
}
bucketName := "bucket-downscoping-test-golang-" + randSuffix.String()[:8]
objectName := "object-downscoping-test-golang-" + randSuffix.String()[:8]
content := "CONTENT"
bucket := client.Bucket(bucketName)

if err := bucket.Create(ctx, tc.ProjectID, nil); err != nil {
t.Errorf("Failed to create bucket: %v", err)
return
}
defer bucket.Delete(ctx)
obj := bucket.Object(objectName)
write := obj.NewWriter(ctx)
_, err = io.Copy(write, strings.NewReader(content))
if err != nil {
t.Errorf("failed to copy the text: %v", err)
return
}
defer obj.Delete(ctx)

err = write.Close()
if err != nil {
t.Errorf("failed to close the writer: %v", err)
return
}

buf := new(bytes.Buffer)
err = getObjectContents(buf, bucketName, objectName)
if err != nil {
t.Errorf("failed to retrieve object contents: %v", err)
return
}
out := buf.String()
if out != content {
t.Errorf("got %v but want "+content, out)
return
}
}
49 changes: 49 additions & 0 deletions auth/downscoping/initialize_CAB.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package downscopedoverview

// [START auth_downscoping_rules]

import "golang.org/x/oauth2/google/downscope"

// constructCAB shows how to initialize a Credential Access Boundary for downscoping tokens.
func constructCAB(bucketName string, prefix string) {
// bucketName := "foo"
// prefix := "profile-picture-"

// A condition can optionally be provided to further restrict access permissions.
// Note that the "profile-picture-" prefix is an arbitrary example to show how to
// construct an AvailabilityCondition; it can be changed to anything.
condition := downscope.AvailabilityCondition{
Expression: "resource.name.startsWith('projects/_/buckets/" + bucketName + "/objects/" + prefix + "'",
Title: prefix + " Only",
Description: "Restricts a token to only be able to access objects that start with `" + prefix + "`",
}
// Initializes an accessBoundary with one Rule which restricts the downscoped
// token to only be able to access the passed in bucket and only grants it the
// permission "storage.objectViewer".
accessBoundary := []downscope.AccessBoundaryRule{
{
AvailableResource: "//storage.googleapis.com/projects/_/buckets/" + bucketName,
AvailablePermissions: []string{"inRole:roles/storage.objectViewer"},
Condition: &condition, // Optional
},
}

// You can now use this accessBoundary to generate a downscoped token.
_ = accessBoundary
}

// [END auth_downscoping_rules]
78 changes: 78 additions & 0 deletions auth/downscoping/token_broker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package downscopedoverview

// [START auth_downscoping_token_broker]

import (
"context"
"fmt"

"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"golang.org/x/oauth2/google/downscope"
)

// createDownscopedToken would be run on the token broker in order to generate
// a downscoped access token that only grants access to objects whose name begins with prefix.
// The token broker would then pass the newly created token to the requesting token consumer for use.
func createDownscopedToken(bucketName string, prefix string) error {
// bucketName := "foo"
// prefix := "profile-picture-"

ctx := context.Background()
// A condition can optionally be provided to further restrict access permissions.
condition := downscope.AvailabilityCondition{
Expression: "resource.name.startsWith('projects/_/buckets/" + bucketName + "/objects/" + prefix + "'",
Title: prefix + " Only",
Description: "Restricts a token to only be able to access objects that start with `" + prefix + "`",
}
// Initializes an accessBoundary with one Rule which restricts the downscoped
// token to only be able to access the bucket "bucketName" and only grants it the
// permission "storage.objectViewer".
accessBoundary := []downscope.AccessBoundaryRule{
{
AvailableResource: "//storage.googleapis.com/projects/_/buckets/" + bucketName,
AvailablePermissions: []string{"inRole:roles/storage.objectViewer"},
Condition: &condition, // Optional
},
}

// This Source can be initialized in multiple ways; the following example uses
// Application Default Credentials.
var rootSource oauth2.TokenSource

// You must provide the "https://www.googleapis.com/auth/cloud-platform" scope.
rootSource, err := google.DefaultTokenSource(ctx, "https://www.googleapis.com/auth/cloud-platform")
if err != nil {
return fmt.Errorf("failed to generate rootSource: %v", err)
}

// downscope.NewTokenSource constructs the token source with the configuration provided.
dts, err := downscope.NewTokenSource(ctx, downscope.DownscopingConfig{RootSource: rootSource, Rules: accessBoundary})
if err != nil {
return fmt.Errorf("failed to generate downscoped token source: %v", err)
}
// Token() uses the previously declared TokenSource to generate a downscoped token.
tok, err := dts.Token()
if err != nil {
return fmt.Errorf("failed to generate token: %v", err)
}
// Pass this token back to the token consumer.
_ = tok
return nil
}

// [END auth_downscoping_token_broker]
108 changes: 108 additions & 0 deletions auth/downscoping/token_consumer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package downscopedoverview

// [START auth_downscoping_token_consumer]

import (
"context"
"fmt"
"io"
"io/ioutil"

"golang.org/x/oauth2/google"
"golang.org/x/oauth2/google/downscope"

"cloud.google.com/go/storage"
"golang.org/x/oauth2"
"google.golang.org/api/option"
)

// A token consumer should define their own tokenSource. In the Token() method,
// it should send a query to a token broker requesting a downscoped token.
// The token broker holds the root credential that is used to generate the
// downscoped token.
type localTokenSource struct {
ctx context.Context
bucketName string
brokerURL string
}

func (lts localTokenSource) Token() (*oauth2.Token, error) {
var remoteToken *oauth2.Token
// Usually you would now retrieve remoteToken, an oauth2.Token, from token broker.
// This snippet performs the same functionality locally.
accessBoundary := []downscope.AccessBoundaryRule{
{
AvailableResource: "//storage.googleapis.com/projects/_/buckets/" + lts.bucketName,
AvailablePermissions: []string{"inRole:roles/storage.objectViewer"},
},
}
rootSource, err := google.DefaultTokenSource(lts.ctx, "https://www.googleapis.com/auth/cloud-platform")
if err != nil {
return nil, fmt.Errorf("failed to generate rootSource: %v", err)
}
dts, err := downscope.NewTokenSource(lts.ctx, downscope.DownscopingConfig{RootSource: rootSource, Rules: accessBoundary})
if err != nil {
return nil, fmt.Errorf("failed to generate downscoped token source: %v", err)
}
// Token() uses the previously declared TokenSource to generate a downscoped token.
remoteToken, err = dts.Token()
if err != nil {
return nil, fmt.Errorf("failed to generate token: %v", err)
}

return remoteToken, nil
}

// getObjectContents will read the contents of an object in Google Storage
// named objectName, contained in the bucket "bucketName".
func getObjectContents(output io.Writer, bucketName string, objectName string) error {
// bucketName := "foo"
// prefix := "profile-picture-"

ctx := context.Background()

thisTokenSource := localTokenSource{
ctx: ctx,
bucketName: bucketName,
brokerURL: "yourURL.com/internal/broker",
}

// Wrap the TokenSource in an oauth2.ReuseTokenSource to enable automatic refreshing.
refreshableTS := oauth2.ReuseTokenSource(nil, thisTokenSource)
// You can now use the token source to access Google Cloud Storage resources as follows.
storageClient, err := storage.NewClient(ctx, option.WithTokenSource(refreshableTS))
if err != nil {
return fmt.Errorf("failed to create the storage client: %v", err)
}
defer storageClient.Close()
bkt := storageClient.Bucket(bucketName)
obj := bkt.Object(objectName)
rc, err := obj.NewReader(ctx)
if err != nil {
return fmt.Errorf("failed to retrieve the object: %v", err)
}
defer rc.Close()
data, err := ioutil.ReadAll(rc)
if err != nil {
return fmt.Errorf("could not read the object's contents: %v", err)
}
// Data now contains the contents of the requested object.
output.Write(data)
return nil
}

// [END auth_downscoping_token_consumer]

0 comments on commit 8bbbe5c

Please sign in to comment.