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 }