blob: cb817301b40a780e48b6379d022819d58b078287 [file] [log] [blame]
// Copyright 2016 The LUCI Authors.
//
// 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 common
import (
"context"
"net/http"
"go.chromium.org/luci/gae/service/datastore"
bbAccess "go.chromium.org/luci/buildbucket/access"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
accessProto "go.chromium.org/luci/common/proto/access"
"go.chromium.org/luci/grpc/grpcutil"
"go.chromium.org/luci/server/auth"
"go.chromium.org/luci/server/caching"
)
// Helper functions for ACL checking.
// IsAllowed checks to see if the user in the context is allowed to access
// the given project.
//
// Returns false for unknown projects. Returns an internal error if the check
// itself fails.
func IsAllowed(c context.Context, project string) (bool, error) {
proj := Project{ID: project}
switch err := datastore.Get(c, &proj); {
case err == datastore.ErrNoSuchEntity:
return false, nil
case err != nil:
// Do not leak internal error details to unauthorized users.
return false, errors.Reason("internal server error").
Tag(grpcutil.InternalTag).
InternalReason("datastore error when fetching project %q: %s", project, err).Err()
default:
return CheckACL(c, proj.ACL)
}
}
// CheckACL returns true if the caller is in the ACL.
//
// Returns an internal error if the check itself fails.
func CheckACL(c context.Context, acl ACL) (bool, error) {
// Try to find a direct hit first, it is cheaper.
caller := auth.CurrentIdentity(c)
for _, ident := range acl.Identities {
if caller == ident {
return true, nil
}
}
// More expensive groups check comes second. Note that admins implicitly have
// access to all projects.
// TODO(nodir): unhardcode group name to config file if there is a need
yes, err := auth.IsMember(c, append(acl.Groups, "administrators")...)
if err != nil {
// Do not leak internal error details to unauthorized users.
return false, errors.Reason("internal server error").
Tag(grpcutil.InternalTag).
InternalReason("error when checking ACL: %s", err).Err()
}
return yes, nil
}
var accessClientKey = "access client key"
// CachedAccessClient wraps an accessProto.AccessClient and caches its response.
type CachedAccessClient struct {
accessProto.AccessClient
cache caching.BlobCache
}
// NewCachedAccessClient creates a new AccessClient for talking to this milo instance's buildbucket instance.
func NewCachedAccessClient(c context.Context, buildbucketHost string, cache caching.BlobCache) (*CachedAccessClient, error) {
t, err := auth.GetRPCTransport(c, auth.AsUser)
if err != nil {
return nil, errors.Annotate(err, "getting RPC Transport").Err()
}
return &CachedAccessClient{
AccessClient: bbAccess.NewClient(buildbucketHost, &http.Client{Transport: t}),
cache: cache,
}, nil
}
// NewTestCachedAccessClient creates a cached access client for testing purpose.
func NewTestCachedAccessClient(client *bbAccess.TestClient, cache caching.BlobCache) *CachedAccessClient {
return &CachedAccessClient{
AccessClient: client,
cache: cache,
}
}
// WithCachedAccessClient attaches an AccessClient to the given context.
func WithCachedAccessClient(c context.Context, a *CachedAccessClient) context.Context {
return context.WithValue(c, &accessClientKey, a)
}
// GetCachedAccessClient retrieves an AccessClient from the given context.
func GetCachedAccessClient(c context.Context) *CachedAccessClient {
if client, ok := c.Value(&accessClientKey).(*CachedAccessClient); !ok {
panic("access client not found in context")
} else {
return client
}
}
// BucketPermissions gets permissions for the current identity for all given buckets.
//
// TODO(mknyszek): If a cache entry expires, then there could be QPS issues if all
// instances query buildbucket for an update simultaneously. Evaluate whether there's
// an issue in practice, and if so, consider expiring cache entries randomly.
func (ac *CachedAccessClient) BucketPermissions(c context.Context, buckets ...string) (bbAccess.Permissions, error) {
perms := make(bbAccess.Permissions, len(buckets))
var bucketsToCache []string
// Create cache entries for each bucket.
identityString := string(auth.CurrentIdentity(c))
for _, bucket := range buckets {
blob, err := ac.cache.Get(c, identityString+"|"+bucket)
if err != nil {
bucketsToCache = append(bucketsToCache, bucket)
continue
}
action := bbAccess.Action(0)
err = action.UnmarshalBinary(blob)
if err != nil {
return nil, err
}
perms[bucket] = action
}
// Finish early if all of the buckets were in the cache.
if len(bucketsToCache) == 0 {
return perms, nil
}
// Make an RPC to get uncached buckets from buildbucket.
newPerms, validTime, err := bbAccess.BucketPermissions(c, ac, bucketsToCache)
if err != nil {
return nil, err
}
// Update items, collect them, and put their values into perms.
for _, bucket := range bucketsToCache {
action, ok := newPerms[bucket]
if !ok {
continue
}
bytes, err := action.MarshalBinary()
if err != nil {
return nil, errors.Annotate(err, "failed to marshal Action").Err()
}
err = ac.cache.Set(c, identityString+"|"+bucket, bytes, validTime)
if err != nil {
logging.WithError(err).Warningf(c, "failed to cache permission for bucket: %s", bucket)
}
perms[bucket] = action
}
return perms, nil
}