go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/auth_service/internal/gs/client.go (about)

     1  // Copyright 2024 The LUCI Authors.
     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 gs
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"net/http"
    21  	"strings"
    22  	"time"
    23  
    24  	"cloud.google.com/go/storage"
    25  	"google.golang.org/api/option"
    26  
    27  	"go.chromium.org/luci/common/data/stringset"
    28  	"go.chromium.org/luci/common/errors"
    29  	"go.chromium.org/luci/common/logging"
    30  	"go.chromium.org/luci/server/auth"
    31  )
    32  
    33  const (
    34  	// The chunk size to use when uploading to GS.
    35  	//
    36  	// Must be under 10 MB to avoid hitting GAE URL Fetch request size
    37  	// limits and must be larger than 262144 bytes to satisfy GCS
    38  	// requirements. Recommended by GCS to be a multiple of 262144.
    39  	maxChunkSize = 262144 * 34 // ~= 9 MB
    40  )
    41  
    42  // Client abstracts functionality to connect with and use Google
    43  // Storage.
    44  //
    45  // Non-production implementations are used primarily for testing.
    46  type Client interface {
    47  	// Close closes the connection to Google Storage.
    48  	Close() error
    49  
    50  	// UpdateReadACL updates the object ACLs to grant read access to the
    51  	// given readers; readers must be user emails.
    52  	UpdateReadACL(ctx context.Context, objectPath string, readers stringset.Set) error
    53  
    54  	// WriteFile writes the given data to the GS path with the object ACLs
    55  	// provided.
    56  	WriteFile(ctx context.Context, objectPath, contentType string, data []byte, acls []storage.ACLRule) error
    57  }
    58  
    59  type gsClient struct {
    60  	baseClient *storage.Client
    61  }
    62  
    63  // NewGSClient creates a new production Google Storage client; i.e. this
    64  // client is actually Google Storage, not a mock.
    65  func NewGSClient(ctx context.Context) (*gsClient, error) {
    66  	logging.Debugf(ctx, "Creating new Google Storage client")
    67  	tr, err := auth.GetRPCTransport(ctx, auth.AsSelf, auth.WithScopes(auth.CloudOAuthScopes...))
    68  	if err != nil {
    69  		return nil, errors.Annotate(err, "aborting - failed setting up authenticated requests to Google Storage").Err()
    70  	}
    71  
    72  	var opts []option.ClientOption
    73  	if tr != nil {
    74  		opts = []option.ClientOption{
    75  			option.WithHTTPClient(&http.Client{Transport: tr}),
    76  		}
    77  	}
    78  
    79  	client, err := storage.NewClient(ctx, opts...)
    80  	if err != nil {
    81  		return nil, errors.Annotate(err, "failed to create Google Storage client").Err()
    82  	}
    83  
    84  	return &gsClient{
    85  		baseClient: client,
    86  	}, nil
    87  }
    88  
    89  func (c *gsClient) Close() error {
    90  	if c.baseClient != nil {
    91  		err := c.baseClient.Close()
    92  		if err != nil {
    93  			return err
    94  		}
    95  		c.baseClient = nil
    96  	}
    97  	return nil
    98  }
    99  
   100  func (c *gsClient) WriteFile(ctx context.Context, objectPath, contentType string, data []byte, acls []storage.ACLRule) (retErr error) {
   101  	if c.baseClient == nil {
   102  		return fmt.Errorf("aborting - no Google Storage client")
   103  	}
   104  
   105  	bucket, name, found := strings.Cut(objectPath, "/")
   106  	if !found {
   107  		return fmt.Errorf("aborting - invalid object path %s", objectPath)
   108  	}
   109  
   110  	writer := c.baseClient.Bucket(bucket).Object(name).NewWriter(ctx)
   111  	defer func() {
   112  		err := writer.Close()
   113  		if retErr == nil && err != nil {
   114  			retErr = errors.Annotate(err, "error uploading %s", objectPath).Err()
   115  			return
   116  		}
   117  
   118  		logging.Debugf(ctx, "GS write successful.\nAttributes for %s: %+v",
   119  			objectPath, writer.Attrs())
   120  	}()
   121  
   122  	writer.ContentType = contentType
   123  	writer.ACL = acls
   124  	writer.ChunkSize = maxChunkSize
   125  	writer.ChunkRetryDeadline = 30 * time.Second
   126  
   127  	if _, err := writer.Write(data); err != nil {
   128  		return errors.Annotate(err, "error uploading %s", objectPath).Err()
   129  	}
   130  
   131  	return nil
   132  }
   133  
   134  func (c *gsClient) UpdateReadACL(ctx context.Context, objectPath string, readers stringset.Set) error {
   135  	if c.baseClient == nil {
   136  		return fmt.Errorf("aborting - no Google Storage client")
   137  	}
   138  
   139  	bucket, name, found := strings.Cut(objectPath, "/")
   140  	if !found {
   141  		return fmt.Errorf("aborting - invalid object path %s", objectPath)
   142  	}
   143  
   144  	acl := c.baseClient.Bucket(bucket).Object(name).ACL()
   145  
   146  	oldACL, err := acl.List(ctx)
   147  	if err != nil {
   148  		return errors.Annotate(err, "error listing ACLs").Err()
   149  	}
   150  	oldAccessors := stringset.New(len(oldACL))
   151  	oldReaders := stringset.New(len(oldACL))
   152  	for _, rule := range oldACL {
   153  		oldAccessors.Add(string(rule.Email))
   154  		if rule.Role == storage.RoleReader {
   155  			oldReaders.Add(string(rule.Email))
   156  		}
   157  	}
   158  
   159  	errs := errors.MultiError{}
   160  	// Only set read access for users that don't have any access to
   161  	// prevent overwriting existing roles to the reader role.
   162  	toAdd := readers.Difference(oldAccessors)
   163  	for reader := range toAdd {
   164  		user := storage.ACLEntity(fmt.Sprintf("user-%s", reader))
   165  		if err := acl.Set(ctx, user, storage.RoleReader); err != nil {
   166  			logging.Errorf(ctx, "error granting read access to %s for user %s", objectPath, user)
   167  			errs = append(errs, err)
   168  		}
   169  	}
   170  	// Revoke read access for users that currently have read access but
   171  	// aren't in readers.
   172  	toDelete := oldReaders.Difference(readers)
   173  	for reader := range toDelete {
   174  		user := storage.ACLEntity(fmt.Sprintf("user-%s", reader))
   175  		if err := acl.Delete(ctx, user); err != nil {
   176  			logging.Errorf(ctx, "error revoking read access to %s for user %s", objectPath, user)
   177  			errs = append(errs, err)
   178  		}
   179  	}
   180  
   181  	return errs.AsError()
   182  }