1// Copyright 2016 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// Package iam supports the resource-specific operations of Google Cloud
16// IAM (Identity and Access Management) for the Google Cloud Libraries.
17// See https://cloud.google.com/iam for more about IAM.
18//
19// Users of the Google Cloud Libraries will typically not use this package
20// directly. Instead they will begin with some resource that supports IAM, like
21// a pubsub topic, and call its IAM method to get a Handle for that resource.
22package iam
23
24import (
25	"context"
26	"fmt"
27	"time"
28
29	gax "github.com/googleapis/gax-go/v2"
30	pb "google.golang.org/genproto/googleapis/iam/v1"
31	"google.golang.org/grpc"
32	"google.golang.org/grpc/codes"
33	"google.golang.org/grpc/metadata"
34)
35
36// client abstracts the IAMPolicy API to allow multiple implementations.
37type client interface {
38	Get(ctx context.Context, resource string) (*pb.Policy, error)
39	Set(ctx context.Context, resource string, p *pb.Policy) error
40	Test(ctx context.Context, resource string, perms []string) ([]string, error)
41}
42
43// grpcClient implements client for the standard gRPC-based IAMPolicy service.
44type grpcClient struct {
45	c pb.IAMPolicyClient
46}
47
48var withRetry = gax.WithRetry(func() gax.Retryer {
49	return gax.OnCodes([]codes.Code{
50		codes.DeadlineExceeded,
51		codes.Unavailable,
52	}, gax.Backoff{
53		Initial:    100 * time.Millisecond,
54		Max:        60 * time.Second,
55		Multiplier: 1.3,
56	})
57})
58
59func (g *grpcClient) Get(ctx context.Context, resource string) (*pb.Policy, error) {
60	var proto *pb.Policy
61	md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "resource", resource))
62	ctx = insertMetadata(ctx, md)
63
64	err := gax.Invoke(ctx, func(ctx context.Context, _ gax.CallSettings) error {
65		var err error
66		proto, err = g.c.GetIamPolicy(ctx, &pb.GetIamPolicyRequest{Resource: resource})
67		return err
68	}, withRetry)
69	if err != nil {
70		return nil, err
71	}
72	return proto, nil
73}
74
75func (g *grpcClient) Set(ctx context.Context, resource string, p *pb.Policy) error {
76	md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "resource", resource))
77	ctx = insertMetadata(ctx, md)
78
79	return gax.Invoke(ctx, func(ctx context.Context, _ gax.CallSettings) error {
80		_, err := g.c.SetIamPolicy(ctx, &pb.SetIamPolicyRequest{
81			Resource: resource,
82			Policy:   p,
83		})
84		return err
85	}, withRetry)
86}
87
88func (g *grpcClient) Test(ctx context.Context, resource string, perms []string) ([]string, error) {
89	var res *pb.TestIamPermissionsResponse
90	md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "resource", resource))
91	ctx = insertMetadata(ctx, md)
92
93	err := gax.Invoke(ctx, func(ctx context.Context, _ gax.CallSettings) error {
94		var err error
95		res, err = g.c.TestIamPermissions(ctx, &pb.TestIamPermissionsRequest{
96			Resource:    resource,
97			Permissions: perms,
98		})
99		return err
100	}, withRetry)
101	if err != nil {
102		return nil, err
103	}
104	return res.Permissions, nil
105}
106
107// A Handle provides IAM operations for a resource.
108type Handle struct {
109	c        client
110	resource string
111}
112
113// InternalNewHandle is for use by the Google Cloud Libraries only.
114//
115// InternalNewHandle returns a Handle for resource.
116// The conn parameter refers to a server that must support the IAMPolicy service.
117func InternalNewHandle(conn *grpc.ClientConn, resource string) *Handle {
118	return InternalNewHandleGRPCClient(pb.NewIAMPolicyClient(conn), resource)
119}
120
121// InternalNewHandleGRPCClient is for use by the Google Cloud Libraries only.
122//
123// InternalNewHandleClient returns a Handle for resource using the given
124// grpc service that implements IAM as a mixin
125func InternalNewHandleGRPCClient(c pb.IAMPolicyClient, resource string) *Handle {
126	return InternalNewHandleClient(&grpcClient{c: c}, resource)
127}
128
129// InternalNewHandleClient is for use by the Google Cloud Libraries only.
130//
131// InternalNewHandleClient returns a Handle for resource using the given
132// client implementation.
133func InternalNewHandleClient(c client, resource string) *Handle {
134	return &Handle{
135		c:        c,
136		resource: resource,
137	}
138}
139
140// Policy retrieves the IAM policy for the resource.
141func (h *Handle) Policy(ctx context.Context) (*Policy, error) {
142	proto, err := h.c.Get(ctx, h.resource)
143	if err != nil {
144		return nil, err
145	}
146	return &Policy{InternalProto: proto}, nil
147}
148
149// SetPolicy replaces the resource's current policy with the supplied Policy.
150//
151// If policy was created from a prior call to Get, then the modification will
152// only succeed if the policy has not changed since the Get.
153func (h *Handle) SetPolicy(ctx context.Context, policy *Policy) error {
154	return h.c.Set(ctx, h.resource, policy.InternalProto)
155}
156
157// TestPermissions returns the subset of permissions that the caller has on the resource.
158func (h *Handle) TestPermissions(ctx context.Context, permissions []string) ([]string, error) {
159	return h.c.Test(ctx, h.resource, permissions)
160}
161
162// A RoleName is a name representing a collection of permissions.
163type RoleName string
164
165// Common role names.
166const (
167	Owner  RoleName = "roles/owner"
168	Editor RoleName = "roles/editor"
169	Viewer RoleName = "roles/viewer"
170)
171
172const (
173	// AllUsers is a special member that denotes all users, even unauthenticated ones.
174	AllUsers = "allUsers"
175
176	// AllAuthenticatedUsers is a special member that denotes all authenticated users.
177	AllAuthenticatedUsers = "allAuthenticatedUsers"
178)
179
180// A Policy is a list of Bindings representing roles
181// granted to members.
182//
183// The zero Policy is a valid policy with no bindings.
184type Policy struct {
185	// TODO(jba): when type aliases are available, put Policy into an internal package
186	// and provide an exported alias here.
187
188	// This field is exported for use by the Google Cloud Libraries only.
189	// It may become unexported in a future release.
190	InternalProto *pb.Policy
191}
192
193// Members returns the list of members with the supplied role.
194// The return value should not be modified. Use Add and Remove
195// to modify the members of a role.
196func (p *Policy) Members(r RoleName) []string {
197	b := p.binding(r)
198	if b == nil {
199		return nil
200	}
201	return b.Members
202}
203
204// HasRole reports whether member has role r.
205func (p *Policy) HasRole(member string, r RoleName) bool {
206	return memberIndex(member, p.binding(r)) >= 0
207}
208
209// Add adds member member to role r if it is not already present.
210// A new binding is created if there is no binding for the role.
211func (p *Policy) Add(member string, r RoleName) {
212	b := p.binding(r)
213	if b == nil {
214		if p.InternalProto == nil {
215			p.InternalProto = &pb.Policy{}
216		}
217		p.InternalProto.Bindings = append(p.InternalProto.Bindings, &pb.Binding{
218			Role:    string(r),
219			Members: []string{member},
220		})
221		return
222	}
223	if memberIndex(member, b) < 0 {
224		b.Members = append(b.Members, member)
225		return
226	}
227}
228
229// Remove removes member from role r if it is present.
230func (p *Policy) Remove(member string, r RoleName) {
231	bi := p.bindingIndex(r)
232	if bi < 0 {
233		return
234	}
235	bindings := p.InternalProto.Bindings
236	b := bindings[bi]
237	mi := memberIndex(member, b)
238	if mi < 0 {
239		return
240	}
241	// Order doesn't matter for bindings or members, so to remove, move the last item
242	// into the removed spot and shrink the slice.
243	if len(b.Members) == 1 {
244		// Remove binding.
245		last := len(bindings) - 1
246		bindings[bi] = bindings[last]
247		bindings[last] = nil
248		p.InternalProto.Bindings = bindings[:last]
249		return
250	}
251	// Remove member.
252	// TODO(jba): worry about multiple copies of m?
253	last := len(b.Members) - 1
254	b.Members[mi] = b.Members[last]
255	b.Members[last] = ""
256	b.Members = b.Members[:last]
257}
258
259// Roles returns the names of all the roles that appear in the Policy.
260func (p *Policy) Roles() []RoleName {
261	if p.InternalProto == nil {
262		return nil
263	}
264	var rns []RoleName
265	for _, b := range p.InternalProto.Bindings {
266		rns = append(rns, RoleName(b.Role))
267	}
268	return rns
269}
270
271// binding returns the Binding for the suppied role, or nil if there isn't one.
272func (p *Policy) binding(r RoleName) *pb.Binding {
273	i := p.bindingIndex(r)
274	if i < 0 {
275		return nil
276	}
277	return p.InternalProto.Bindings[i]
278}
279
280func (p *Policy) bindingIndex(r RoleName) int {
281	if p.InternalProto == nil {
282		return -1
283	}
284	for i, b := range p.InternalProto.Bindings {
285		if b.Role == string(r) {
286			return i
287		}
288	}
289	return -1
290}
291
292// memberIndex returns the index of m in b's Members, or -1 if not found.
293func memberIndex(m string, b *pb.Binding) int {
294	if b == nil {
295		return -1
296	}
297	for i, mm := range b.Members {
298		if mm == m {
299			return i
300		}
301	}
302	return -1
303}
304
305// insertMetadata inserts metadata into the given context
306func insertMetadata(ctx context.Context, mds ...metadata.MD) context.Context {
307	out, _ := metadata.FromOutgoingContext(ctx)
308	out = out.Copy()
309	for _, md := range mds {
310		for k, v := range md {
311			out[k] = append(out[k], v...)
312		}
313	}
314	return metadata.NewOutgoingContext(ctx, out)
315}
316