Skip to content

Commit

Permalink
storage: IAM for buckets
Browse files Browse the repository at this point in the history
Support IAM GetPolicy/SetPolicy for GCS buckets.

No support for TestPermissions (was not requested).

GCS does not have a gRPC API. It provides its own version of the IAM
API. To keep the same surface as our general cloud.google.com/go/iam
package, generalize the internals of that package.

Also, fix iam.Policy to remove empty bindings.

Change-Id: Ia51930974f46b429c63f256fa48f35853fafd988
Reviewed-on: https://code-review.googlesource.com/12130
Reviewed-by: Michael Darakananda <[email protected]>
  • Loading branch information
jba committed Apr 10, 2017
1 parent 687342e commit 0320f90
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 28 deletions.
109 changes: 83 additions & 26 deletions iam/iam.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,47 @@ import (
"google.golang.org/grpc"
)

// client abstracts the IAMPolicy API to allow multiple implementations.
type client interface {
Get(ctx context.Context, resource string) (*pb.Policy, error)
Set(ctx context.Context, resource string, p *pb.Policy) error
Test(ctx context.Context, resource string, perms []string) ([]string, error)
}

// grpcClient implements client for the standard gRPC-based IAMPolicy service.
type grpcClient struct {
c pb.IAMPolicyClient
}

func (g *grpcClient) Get(ctx context.Context, resource string) (*pb.Policy, error) {
proto, err := g.c.GetIamPolicy(ctx, &pb.GetIamPolicyRequest{Resource: resource})
if err != nil {
return nil, err
}
return proto, nil
}
func (g *grpcClient) Set(ctx context.Context, resource string, p *pb.Policy) error {
_, err := g.c.SetIamPolicy(ctx, &pb.SetIamPolicyRequest{
Resource: resource,
Policy: p,
})
return err
}

func (g *grpcClient) Test(ctx context.Context, resource string, perms []string) ([]string, error) {
res, err := g.c.TestIamPermissions(ctx, &pb.TestIamPermissionsRequest{
Resource: resource,
Permissions: perms,
})
if err != nil {
return nil, err
}
return res.Permissions, nil
}

// A Handle provides IAM operations for a resource.
type Handle struct {
c pb.IAMPolicyClient
c client
resource string
}

Expand All @@ -38,15 +76,23 @@ type Handle struct {
// InternalNewHandle returns a Handle for resource.
// The conn parameter refers to a server that must support the IAMPolicy service.
func InternalNewHandle(conn *grpc.ClientConn, resource string) *Handle {
return InternalNewHandleClient(&grpcClient{c: pb.NewIAMPolicyClient(conn)}, resource)
}

// InternalNewHandleClient is for use by the Google Cloud Libraries only.
//
// InternalNewHandleClient returns a Handle for resource using the given
// client implementation.
func InternalNewHandleClient(c client, resource string) *Handle {
return &Handle{
c: pb.NewIAMPolicyClient(conn),
c: c,
resource: resource,
}
}

// Policy retrieves the IAM policy for the resource.
func (h *Handle) Policy(ctx context.Context) (*Policy, error) {
proto, err := h.c.GetIamPolicy(ctx, &pb.GetIamPolicyRequest{Resource: h.resource})
proto, err := h.c.Get(ctx, h.resource)
if err != nil {
return nil, err
}
Expand All @@ -58,23 +104,12 @@ func (h *Handle) Policy(ctx context.Context) (*Policy, error) {
// If policy was created from a prior call to Get, then the modification will
// only succeed if the policy has not changed since the Get.
func (h *Handle) SetPolicy(ctx context.Context, policy *Policy) error {
_, err := h.c.SetIamPolicy(ctx, &pb.SetIamPolicyRequest{
Resource: h.resource,
Policy: policy.InternalProto,
})
return err
return h.c.Set(ctx, h.resource, policy.InternalProto)
}

// TestPermissions returns the subset of permissions that the caller has on the resource.
func (h *Handle) TestPermissions(ctx context.Context, permissions []string) ([]string, error) {
res, err := h.c.TestIamPermissions(ctx, &pb.TestIamPermissionsRequest{
Resource: h.resource,
Permissions: permissions,
})
if err != nil {
return nil, err
}
return res.Permissions, nil
return h.c.Test(ctx, h.resource, permissions)
}

// A RoleName is a name representing a collection of permissions.
Expand Down Expand Up @@ -146,16 +181,30 @@ func (p *Policy) Add(member string, r RoleName) {

// Remove removes member from role r if it is present.
func (p *Policy) Remove(member string, r RoleName) {
b := p.binding(r)
i := memberIndex(member, b)
if i < 0 {
bi := p.bindingIndex(r)
if bi < 0 {
return
}
bindings := p.InternalProto.Bindings
b := bindings[bi]
mi := memberIndex(member, b)
if mi < 0 {
return
}
// Order doesn't matter for bindings or members, so to remove, move the last item
// into the removed spot and shrink the slice.
if len(b.Members) == 1 {
// Remove binding.
last := len(bindings) - 1
bindings[bi] = bindings[last]
bindings[last] = nil
p.InternalProto.Bindings = bindings[:last]
return
}
// Order doesn't matter, so move the last member into the
// removed spot and shrink the slice.
// Remove member.
// TODO(jba): worry about multiple copies of m?
last := len(b.Members) - 1
b.Members[i] = b.Members[last]
b.Members[mi] = b.Members[last]
b.Members[last] = ""
b.Members = b.Members[:last]
}
Expand All @@ -174,15 +223,23 @@ func (p *Policy) Roles() []RoleName {

// binding returns the Binding for the suppied role, or nil if there isn't one.
func (p *Policy) binding(r RoleName) *pb.Binding {
if p.InternalProto == nil {
i := p.bindingIndex(r)
if i < 0 {
return nil
}
for _, b := range p.InternalProto.Bindings {
return p.InternalProto.Bindings[i]
}

func (p *Policy) bindingIndex(r RoleName) int {
if p.InternalProto == nil {
return -1
}
for i, b := range p.InternalProto.Bindings {
if b.Role == string(r) {
return b
return i
}
}
return nil
return -1
}

// memberIndex returns the index of m in b's Members, or -1 if not found.
Expand Down
4 changes: 2 additions & 2 deletions iam/iam_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,10 @@ func TestPolicy(t *testing.T) {
t.Fatal(msg)
}
remove("m2", Owner)
if msg, ok := checkMembers(p, Owner, []string{}); !ok {
if msg, ok := checkMembers(p, Owner, nil); !ok {
t.Fatal(msg)
}
if got, want := p.Roles(), []RoleName{Owner}; !reflect.DeepEqual(got, want) {
if got, want := p.Roles(), []RoleName(nil); !reflect.DeepEqual(got, want) {
t.Fatalf("roles: got %v, want %v", got, want)
}
}
Expand Down
13 changes: 13 additions & 0 deletions internal/pretty/pretty.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"reflect"
"sort"
"strings"
"time"
)

// Indent is the string output at each level of indentation.
Expand Down Expand Up @@ -58,7 +59,15 @@ type state struct {
defaults bool
}

const maxLevel = 100

var typeOfTime = reflect.TypeOf(time.Time{})

func fprint(w io.Writer, v reflect.Value, s state) {
if s.level > maxLevel {
fmt.Fprintln(w, "pretty: max nested depth exceeded")
return
}
indent := strings.Repeat(Indent, s.level)
fmt.Fprintf(w, "%s%s", indent, s.prefix)
if isNil(v) {
Expand All @@ -68,6 +77,10 @@ func fprint(w io.Writer, v reflect.Value, s state) {
if v.Type().Kind() == reflect.Interface {
v = v.Elem()
}
if v.Type() == typeOfTime {
fmt.Fprintf(w, "%s%s", v.Interface(), s.suffix)
return
}
for v.Type().Kind() == reflect.Ptr {
fmt.Fprintf(w, "&")
v = v.Elem()
Expand Down
99 changes: 99 additions & 0 deletions storage/iam.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright 2017 Google Inc. All Rights Reserved.
//
// 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
//
// http://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 storage

import (
"errors"

"cloud.google.com/go/iam"
"golang.org/x/net/context"
raw "google.golang.org/api/storage/v1"
iampb "google.golang.org/genproto/googleapis/iam/v1"
)

// IAM provides access to IAM access control for the bucket.
func (b *BucketHandle) IAM() *iam.Handle {
return iam.InternalNewHandleClient(&iamClient{raw: b.c.raw}, b.name)
}

// iamClient implements the iam.client interface.
type iamClient struct {
raw *raw.Service
}

func (c *iamClient) Get(ctx context.Context, resource string) (*iampb.Policy, error) {
req := c.raw.Buckets.GetIamPolicy(resource)
setClientHeader(req.Header())
var rp *raw.Policy
var err error
err = runWithRetry(ctx, func() error {
rp, err = req.Context(ctx).Do()
return err
})
if err != nil {
return nil, err
}
return iamFromStoragePolicy(rp), nil
}

func (c *iamClient) Set(ctx context.Context, resource string, p *iampb.Policy) error {
rp := iamToStoragePolicy(p)
req := c.raw.Buckets.SetIamPolicy(resource, rp)
setClientHeader(req.Header())
return runWithRetry(ctx, func() error {
_, err := req.Context(ctx).Do()
return err
})
}

func (c *iamClient) Test(context.Context, string, []string) ([]string, error) {
return nil, errors.New("TestPermissions is unimplemented")
}

func iamToStoragePolicy(ip *iampb.Policy) *raw.Policy {
return &raw.Policy{
Bindings: iamToStorageBindings(ip.Bindings),
Etag: string(ip.Etag),
}
}

func iamToStorageBindings(ibs []*iampb.Binding) []*raw.PolicyBindings {
var rbs []*raw.PolicyBindings
for _, ib := range ibs {
rbs = append(rbs, &raw.PolicyBindings{
Role: ib.Role,
Members: ib.Members,
})
}
return rbs
}

func iamFromStoragePolicy(rp *raw.Policy) *iampb.Policy {
return &iampb.Policy{
Bindings: iamFromStorageBindings(rp.Bindings),
Etag: []byte(rp.Etag),
}
}

func iamFromStorageBindings(rbs []*raw.PolicyBindings) []*iampb.Binding {
var ibs []*iampb.Binding
for _, rb := range rbs {
ibs = append(ibs, &iampb.Binding{
Role: rb.Role,
Members: rb.Members,
})
}
return ibs
}
38 changes: 38 additions & 0 deletions storage/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (

"golang.org/x/net/context"

"cloud.google.com/go/iam"
"cloud.google.com/go/internal"
"cloud.google.com/go/internal/testutil"
"google.golang.org/api/googleapi"
Expand Down Expand Up @@ -1163,6 +1164,43 @@ func TestIntegration_HashesOnUpload(t *testing.T) {
}
}

func TestIntegration_BucketIAM(t *testing.T) {
ctx := context.Background()
client, bucket := testConfig(ctx, t)
defer client.Close()

bkt := client.Bucket(bucket)

// This bucket is unique to this test run. So we don't have
// to worry about other runs interfering with our IAM policy
// changes.

member := "projectViewer:" + testutil.ProjID()
role := iam.RoleName("roles/storage.objectViewer")
// Get the bucket's IAM policy.
policy, err := bkt.IAM().Policy(ctx)
if err != nil {
t.Fatalf("Getting policy: %v", err)
}
// The member should not have the role.
if policy.HasRole(member, role) {
t.Errorf("member %q has role %q", member, role)
}
// Change the policy.
policy.Add(member, role)
if err := bkt.IAM().SetPolicy(ctx, policy); err != nil {
t.Fatalf("SetPolicy: %v", err)
}
// Confirm that the binding was added.
policy, err = bkt.IAM().Policy(ctx)
if err != nil {
t.Fatalf("Getting policy: %v", err)
}
if !policy.HasRole(member, role) {
t.Errorf("member %q does not have role %q", member, role)
}
}

func writeObject(ctx context.Context, obj *ObjectHandle, contentType string, contents []byte) error {
w := obj.NewWriter(ctx)
w.ContentType = contentType
Expand Down

0 comments on commit 0320f90

Please sign in to comment.