go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/config_service/rpc/get_config.go (about) 1 // Copyright 2023 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 rpc 16 17 import ( 18 "context" 19 "net/http" 20 21 "google.golang.org/grpc/codes" 22 "google.golang.org/grpc/status" 23 24 "go.chromium.org/luci/auth/identity" 25 "go.chromium.org/luci/common/errors" 26 "go.chromium.org/luci/common/gcloud/gs" 27 "go.chromium.org/luci/common/logging" 28 "go.chromium.org/luci/common/proto/mask" 29 "go.chromium.org/luci/config" 30 "go.chromium.org/luci/server/auth" 31 32 "go.chromium.org/luci/config_service/internal/acl" 33 "go.chromium.org/luci/config_service/internal/clients" 34 "go.chromium.org/luci/config_service/internal/common" 35 "go.chromium.org/luci/config_service/internal/model" 36 pb "go.chromium.org/luci/config_service/proto" 37 ) 38 39 // validateGetConfig validates the given request. 40 func validateGetConfig(req *pb.GetConfigRequest) error { 41 switch { 42 case req.GetConfigSet() == "": 43 return errors.Reason("config_set is not specified").Err() 44 case req.GetContentSha256() != "" && req.GetPath() != "": 45 return errors.Reason("content_sha256 and path are mutually exclusive").Err() 46 case req.GetContentSha256() == "" && req.GetPath() == "": 47 return errors.Reason("content_sha256 or path is required").Err() 48 case req.GetPath() != "": 49 if err := validatePath(req.Path); err != nil { 50 return errors.Annotate(err, "path %q", req.Path).Err() 51 } 52 } 53 return errors.Annotate(config.Set(req.ConfigSet).Validate(), "config_set %q", req.ConfigSet).Err() 54 } 55 56 // GetConfig handles a request to retrieve a config. Implements pb.ConfigsServer. 57 func (c Configs) GetConfig(ctx context.Context, req *pb.GetConfigRequest) (*pb.Config, error) { 58 if err := validateGetConfig(req); err != nil { 59 return nil, status.Errorf(codes.InvalidArgument, "%s", err) 60 } 61 m, err := toConfigMask(req.Fields) 62 if err != nil { 63 return nil, status.Errorf(codes.InvalidArgument, "invalid fields mask: %s", err) 64 } 65 66 var f *model.File 67 var noSuchCfgErr *model.NoSuchConfigError 68 cs := req.ConfigSet 69 70 // Check read access for the config set. 71 switch hasPerm, err := acl.CanReadConfigSet(ctx, config.Set(cs)); { 72 case err != nil: 73 logging.Errorf(ctx, "cannot check permission for %q: %s", auth.CurrentIdentity(ctx), err) 74 return nil, status.Errorf(codes.Internal, "error while checking permission for %q", auth.CurrentIdentity(ctx)) 75 case !hasPerm: 76 logging.Infof(ctx, "%q does not have access to %s", auth.CurrentIdentity(ctx), cs) 77 return nil, notFoundErr(auth.CurrentIdentity(ctx)) 78 } 79 80 // Fetch by ContentSha256 81 if req.ContentSha256 != "" { 82 switch f, err = model.GetConfigFileByHash(ctx, config.Set(cs), req.ContentSha256); { 83 case errors.As(err, &noSuchCfgErr): 84 logging.Infof(ctx, "no such config: %s", noSuchCfgErr.Error()) 85 return nil, notFoundErr(auth.CurrentIdentity(ctx)) 86 case err != nil: 87 logging.Errorf(ctx, "cannot fetch config (hash %s): %s", req.ContentSha256, err) 88 return nil, status.Errorf(codes.Internal, "error while fetching the config") 89 } 90 } else { 91 // Fetch by config set + path. 92 switch f, err = model.GetLatestConfigFile(ctx, config.Set(cs), req.Path); { 93 case errors.As(err, &noSuchCfgErr): 94 logging.Infof(ctx, "no such config: %s", noSuchCfgErr.Error()) 95 return nil, notFoundErr(auth.CurrentIdentity(ctx)) 96 case err != nil: 97 logging.Errorf(ctx, "cannot fetch config (configset:%s, path:%s): %s", cs, req.Path, err) 98 return nil, status.Errorf(codes.Internal, "error while fetching the config") 99 } 100 } 101 102 // Convert to the response proto by the Fields mask. 103 configPb := toConfigPb(cs, f) 104 if len(f.Content) != 0 && f.Size < int64(maxRawContentSize) { 105 rawContent, err := f.GetRawContent(ctx) 106 if err != nil { 107 logging.Errorf(ctx, "failed to get the raw content of the config for %s-%s: %s", cs, f.Path, err) 108 return nil, status.Errorf(codes.Internal, "error while getting raw content") 109 } 110 configPb.Content = &pb.Config_RawContent{RawContent: rawContent} 111 } else if m.MustIncludes("signed_url") != mask.Exclude { 112 urls, err := common.CreateSignedURLs(ctx, clients.GetGsClient(ctx), []gs.Path{f.GcsURI}, http.MethodGet, nil) 113 if err != nil { 114 logging.Errorf(ctx, "failed to generate signed url for %s-%s: %s", cs, f.Path, err) 115 return nil, status.Errorf(codes.Internal, "error while generating the config signed url") 116 } 117 configPb.Content = &pb.Config_SignedUrl{SignedUrl: urls[0]} 118 } 119 if err := m.Trim(configPb); err != nil { 120 logging.Errorf(ctx, "cannot trim the config proto: %s", err) 121 return nil, status.Errorf(codes.Internal, "error while constructing response") 122 } 123 return configPb, nil 124 } 125 126 // notFoundErr returns an gRPC NotFound error with a generic message, which 127 // obfuscates "not found" and "permission denied" intentionally to avoid 128 // revealing accurate information to unauthorized users. 129 func notFoundErr(id identity.Identity) error { 130 return status.Errorf(codes.NotFound, "requested resource not found or %q does not have permission to access it", id) 131 }