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  }