Skip to content

Commit

Permalink
iam: initial design
Browse files Browse the repository at this point in the history
Introduces iam.Handle, which will be returned by the IAM method of
all resources that support IAM.

Currently we support only the standard IAM methods Get, Set and
TestPermissions. We can consider adding convenience methods like
AddMemberToRole, but that is quite easy to write:

    h := pubsubTopic.IAM()
    policy, err := h.Policy(ctx)
    if err != nil { ... }
    policy.Add(iam.AllUsers, iam.Viewer)
    if err := ph.SetPolicy(ctx, policy); err != nil { ... }

That code includes an ETag check.

We don't attempt to support QueryGrantableRoles here. Although it's
per-resource, it's actually part of the admin API, which has its own
service. It's ultimately cleaner to keep them separate.

Issue #340

Change-Id: I688a39c69c0ccffc46f0d506b2105f2bdbf4dfe3
Reviewed-on: https://code-review.googlesource.com/8130
Reviewed-by: Chris Broadfoot <cbro@google.com>
Reviewed-by: Ross Light <light@google.com>
  • Loading branch information
jba committed Oct 14, 2016
1 parent 44b3a7d commit fc7a628
Show file tree
Hide file tree
Showing 2 changed files with 282 additions and 0 deletions.
194 changes: 194 additions & 0 deletions iam/iam.go
@@ -0,0 +1,194 @@
// Copyright 2016 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 iam supports the resource-specific operations of Google Cloud
// IAM (Identity and Access Management) for the Google Cloud Libraries.
// See https://cloud.google.com/iam for more about IAM.
//
// Users of the Google Cloud Libraries will typically not use this package
// directly. Instead they will begin with some resource that supports IAM, like
// a pubsub topic, and call its IAM method to get a Handle for that resource.
package iam

import (
"golang.org/x/net/context"
pb "google.golang.org/genproto/googleapis/iam/v1"
"google.golang.org/grpc"
)

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

// InternalNewHandle is for use by the Google Cloud Libraries only.
//
// 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 &Handle{
c: pb.NewIAMPolicyClient(conn),
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})
if err != nil {
return nil, err
}
return &Policy{proto: proto}, nil
}

// SetPolicy replaces the resource's current policy with the supplied Policy.
//
// 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.proto,
})
return err
}

// 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
}

// A RoleName is a name representing a collection of permissions.
type RoleName string

// Common role names.
const (
Owner RoleName = "roles/owner"
Editor RoleName = "roles/editor"
Viewer RoleName = "roles/viewer"
)

const (
// AllUsers is a special member that denotes all users, even unauthenticated ones.
AllUsers = "allUsers"

// AllAuthenticatedUsers is a special member that denotes all authenticated users.
AllAuthenticatedUsers = "allAuthenticatedUsers"
)

// A Policy is a list of Bindings representing roles
// granted to members.
//
// The zero Policy is a valid policy with no bindings.
type Policy struct {
proto *pb.Policy
}

// Members returns the list of members with the supplied role.
// The return value should not be modified. Use Add and Remove
// to modify the members of a role.
func (p *Policy) Members(r RoleName) []string {
b := p.binding(r)
if b == nil {
return nil
}
return b.Members
}

// HasRole reports whether member has role r.
func (p *Policy) HasRole(member string, r RoleName) bool {
return memberIndex(member, p.binding(r)) >= 0
}

// Add adds member member to role r if it is not already present.
// A new binding is created if there is no binding for the role.
func (p *Policy) Add(member string, r RoleName) {
b := p.binding(r)
if b == nil {
if p.proto == nil {
p.proto = &pb.Policy{}
}
p.proto.Bindings = append(p.proto.Bindings, &pb.Binding{
Role: string(r),
Members: []string{member},
})
return
}
if memberIndex(member, b) < 0 {
b.Members = append(b.Members, member)
return
}
}

// 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 {
return
}
// Order doesn't matter, so move the last member into the
// removed spot and shrink the slice.
// TODO(jba): worry about multiple copies of m?
last := len(b.Members) - 1
b.Members[i] = b.Members[last]
b.Members[last] = ""
b.Members = b.Members[:last]
}

// Roles returns the names of all the roles that appear in the Policy.
func (p *Policy) Roles() []RoleName {
if p.proto == nil {
return nil
}
var rns []RoleName
for _, b := range p.proto.Bindings {
rns = append(rns, RoleName(b.Role))
}
return rns
}

// 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.proto == nil {
return nil
}
for _, b := range p.proto.Bindings {
if b.Role == string(r) {
return b
}
}
return nil
}

// memberIndex returns the index of m in b's Members, or -1 if not found.
func memberIndex(m string, b *pb.Binding) int {
if b == nil {
return -1
}
for i, mm := range b.Members {
if mm == m {
return i
}
}
return -1
}
88 changes: 88 additions & 0 deletions iam/iam_test.go
@@ -0,0 +1,88 @@
// Copyright 2016 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 iam

import (
"fmt"
"reflect"
"sort"
"testing"
)

func TestPolicy(t *testing.T) {
p := &Policy{}

add := func(member string, role RoleName) {
p.Add(member, role)
t.Logf("Add(%q, %s)", member, role)
}
remove := func(member string, role RoleName) {
p.Remove(member, role)
t.Logf("Remove(%q, %s)", member, role)
}

if msg, ok := checkMembers(p, Owner, nil); !ok {
t.Fatal(msg)
}
add("m1", Owner)
if msg, ok := checkMembers(p, Owner, []string{"m1"}); !ok {
t.Fatal(msg)
}
add("m2", Owner)
if msg, ok := checkMembers(p, Owner, []string{"m1", "m2"}); !ok {
t.Fatal(msg)
}
add("m1", Owner) // duplicate adds ignored
if msg, ok := checkMembers(p, Owner, []string{"m1", "m2"}); !ok {
t.Fatal(msg)
}
// No other roles populated yet.
if msg, ok := checkMembers(p, Viewer, nil); !ok {
t.Fatal(msg)
}
remove("m1", Owner)
if msg, ok := checkMembers(p, Owner, []string{"m2"}); !ok {
t.Fatal(msg)
}
if msg, ok := checkMembers(p, Viewer, nil); !ok {
t.Fatal(msg)
}
remove("m3", Owner) // OK to remove non-existent member.
if msg, ok := checkMembers(p, Owner, []string{"m2"}); !ok {
t.Fatal(msg)
}
remove("m2", Owner)
if msg, ok := checkMembers(p, Owner, []string{}); !ok {
t.Fatal(msg)
}
if got, want := p.Roles(), []RoleName{Owner}; !reflect.DeepEqual(got, want) {
t.Fatalf("roles: got %v, want %v", got, want)
}
}

func checkMembers(p *Policy, role RoleName, wantMembers []string) (string, bool) {
gotMembers := p.Members(role)
sort.Strings(gotMembers)
sort.Strings(wantMembers)
if !reflect.DeepEqual(gotMembers, wantMembers) {
return fmt.Sprintf("got %v, want %v", gotMembers, wantMembers), false
}
for _, m := range wantMembers {
if !p.HasRole(m, role) {
return fmt.Sprintf("member %q should have role %s but does not", m, role), false
}
}
return "", true
}

0 comments on commit fc7a628

Please sign in to comment.