go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/auth_service/internal/gs/gs.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  	"encoding/json"
    20  	"fmt"
    21  	"strings"
    22  
    23  	"cloud.google.com/go/storage"
    24  	"google.golang.org/protobuf/proto"
    25  
    26  	"go.chromium.org/luci/common/data/stringset"
    27  	"go.chromium.org/luci/common/errors"
    28  	"go.chromium.org/luci/common/logging"
    29  	"go.chromium.org/luci/server/auth/service/protocol"
    30  
    31  	"go.chromium.org/luci/auth_service/internal/configs/srvcfg/settingscfg"
    32  )
    33  
    34  // mockedGSClientKey is the context key to indicate using a mocked GS
    35  // client in tests.
    36  var mockedGSClientKey = "mock Google Storage client"
    37  
    38  func constructReadACLs(readers stringset.Set) []storage.ACLRule {
    39  	// Sorting the readers just makes it easier to set expected ACLs in
    40  	// tests.
    41  	sortedReaders := readers.ToSortedSlice()
    42  
    43  	acls := make([]storage.ACLRule, len(readers))
    44  	for i, reader := range sortedReaders {
    45  		acls[i] = storage.ACLRule{
    46  			Entity: storage.ACLEntity(fmt.Sprintf("user-%s", reader)),
    47  			Role:   storage.RoleReader,
    48  		}
    49  	}
    50  	return acls
    51  }
    52  
    53  // GetPath returns the sanitized Google Storage path from settings.cfg.
    54  func GetPath(ctx context.Context) (string, error) {
    55  	cfg, err := settingscfg.Get(ctx)
    56  	if err != nil {
    57  		return "", errors.Annotate(err, "error getting settings.cfg").Err()
    58  	}
    59  
    60  	// Allow for a single trailing slash.
    61  	path := strings.TrimSuffix(cfg.GetAuthDbGsPath(), "/")
    62  
    63  	return path, nil
    64  }
    65  
    66  // IsValidPath returns whether the given path is considered a valid
    67  // Google Storage path, where:
    68  //   - the path is not empty; and
    69  //   - the path has no trailing "/", as object paths are constructed
    70  //     assuming this.
    71  func IsValidPath(path string) bool {
    72  	return path != "" && !strings.HasSuffix(path, "/")
    73  }
    74  
    75  // UploadAuthDB uploads the signed AuthDB and AuthDBRevision to Google
    76  // Storage.
    77  func UploadAuthDB(ctx context.Context, signedAuthDB *protocol.SignedAuthDB, revision *protocol.AuthDBRevision, readers stringset.Set, dryRun bool) (retErr error) {
    78  	// Skip if the GS path is invalid.
    79  	gsPath, err := GetPath(ctx)
    80  	if err != nil {
    81  		return errors.Annotate(err, "error getting GS path").Err()
    82  	}
    83  	if !IsValidPath(gsPath) {
    84  		if gsPath == "" {
    85  			// Was not configured in settings.cfg; skip upload.
    86  			return nil
    87  		}
    88  		return fmt.Errorf("invalid GS path: %s", gsPath)
    89  	}
    90  
    91  	fileBaseName := "latest"
    92  	if dryRun {
    93  		fileBaseName = "V2latest"
    94  	}
    95  
    96  	acls := constructReadACLs(readers)
    97  
    98  	client, err := newClient(ctx)
    99  	if err != nil {
   100  		return err
   101  	}
   102  	defer func() {
   103  		err := client.Close()
   104  		if retErr == nil {
   105  			retErr = err
   106  		}
   107  	}()
   108  
   109  	// Upload signed AuthDB.
   110  	authDBData, err := proto.Marshal(signedAuthDB)
   111  	if err != nil {
   112  		return fmt.Errorf("error marshalling signed AuthDB")
   113  	}
   114  	authDBPath := fmt.Sprintf("%s/%s.db", gsPath, fileBaseName)
   115  	err = client.WriteFile(ctx, authDBPath, "application/protobuf", authDBData, acls)
   116  	if err != nil {
   117  		return errors.Annotate(err, "failed to upload %s", authDBPath).Err()
   118  	}
   119  
   120  	// Upload AuthDBRevision.
   121  	authDBRevision, err := json.Marshal(revision)
   122  	if err != nil {
   123  		return fmt.Errorf("error marshalling AuthDBRevision")
   124  	}
   125  	revPath := fmt.Sprintf("%s/%s.json", gsPath, fileBaseName)
   126  	err = client.WriteFile(ctx, revPath, "application/json", authDBRevision, acls)
   127  	if err != nil {
   128  		return errors.Annotate(err, "failed to upload %s", revPath).Err()
   129  	}
   130  
   131  	return nil
   132  }
   133  
   134  // UpdateReaders updates which users have read access to the latest
   135  // signed AuthDB and AuthDBRevision in Google Storage.
   136  func UpdateReaders(ctx context.Context, readers stringset.Set, dryRun bool) (retErr error) {
   137  	// Skip if the GS path is invalid.
   138  	gsPath, err := GetPath(ctx)
   139  	if err != nil {
   140  		return errors.Annotate(err, "error getting GS path").Err()
   141  	}
   142  	if !IsValidPath(gsPath) {
   143  		if gsPath == "" {
   144  			// Was not configured in settingcs.cfg; skip ACL update.
   145  			return nil
   146  		}
   147  		return fmt.Errorf("invalid GS path: %s", gsPath)
   148  	}
   149  
   150  	fileBaseName := "latest"
   151  	if dryRun {
   152  		fileBaseName = "V2latest"
   153  	}
   154  
   155  	client, err := newClient(ctx)
   156  	if err != nil {
   157  		return err
   158  	}
   159  	defer func() {
   160  		err := client.Close()
   161  		if retErr == nil {
   162  			retErr = err
   163  		}
   164  	}()
   165  
   166  	exts := []string{"db", "json"}
   167  	errs := errors.MultiError{}
   168  	for _, ext := range exts {
   169  		objectPath := fmt.Sprintf("%s/%s.%s", gsPath, fileBaseName, ext)
   170  		err := client.UpdateReadACL(ctx, objectPath, readers)
   171  		if err != nil {
   172  			logging.Errorf(ctx, "error updating ACLs for %s: %s", objectPath, err)
   173  			errs = append(errs, err)
   174  		}
   175  	}
   176  
   177  	return errs.AsError()
   178  }
   179  
   180  func newClient(ctx context.Context) (Client, error) {
   181  	if mockClient, ok := ctx.Value(&mockedGSClientKey).(*MockClient); ok {
   182  		// return a mock of the Google storage client for tests.
   183  		return mockClient, nil
   184  	}
   185  
   186  	client, err := NewGSClient(ctx)
   187  	if err != nil {
   188  		return nil, errors.Annotate(err, "error making Google Storage client").Err()
   189  	}
   190  
   191  	return client, nil
   192  }