go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/config_service/internal/rules/rules.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 rules
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"path"
    22  	"strings"
    23  
    24  	"google.golang.org/protobuf/encoding/prototext"
    25  
    26  	"go.chromium.org/luci/common/data/stringset"
    27  	cfgcommonpb "go.chromium.org/luci/common/proto/config"
    28  	"go.chromium.org/luci/config"
    29  	"go.chromium.org/luci/config/validation"
    30  	"go.chromium.org/luci/gae/service/info"
    31  	"go.chromium.org/luci/server/auth"
    32  
    33  	"go.chromium.org/luci/config_service/internal/common"
    34  )
    35  
    36  func init() {
    37  	addRules(&validation.Rules)
    38  }
    39  
    40  func addRules(r *validation.RuleSet) {
    41  	r.Vars.Register("appid", func(ctx context.Context) (string, error) {
    42  		if appid := info.AppID(ctx); appid != "" {
    43  			return appid, nil
    44  		}
    45  		return "", fmt.Errorf("can't resolve ${appid} from context")
    46  	})
    47  	r.Add("exact:services/${appid}", common.ACLRegistryFilePath, validateACLsCfg)
    48  	r.Add("exact:services/${appid}", common.ProjRegistryFilePath, validateProjectsCfg)
    49  	r.Add("exact:services/${appid}", common.ServiceRegistryFilePath, validateServicesCfg)
    50  	r.Add("exact:services/${appid}", common.ImportConfigFilePath, validateImportCfg)
    51  	r.Add("exact:services/${appid}", common.SchemaConfigFilePath, validateSchemaCfg)
    52  	r.Add(`regex:projects/[^/]+`, common.ProjMetadataFilePath, validateProjectMetadata)
    53  	r.Add(`regex:.+`, `regex:.+\.json`, validateJSON)
    54  }
    55  
    56  func validateACLsCfg(vctx *validation.Context, configSet, path string, content []byte) error {
    57  	vctx.SetFile(path)
    58  	cfg := &cfgcommonpb.AclCfg{}
    59  	if err := prototext.Unmarshal(content, cfg); err != nil {
    60  		vctx.Errorf("invalid AclCfg proto: %s", err)
    61  		return nil
    62  	}
    63  	if group := cfg.GetProjectAccessGroup(); group != "" && !auth.IsValidGroupName(group) {
    64  		vctx.Errorf("invalid project_access_group: %q", group)
    65  	}
    66  	if group := cfg.GetServiceAccessGroup(); group != "" && !auth.IsValidGroupName(group) {
    67  		vctx.Errorf("invalid service_access_group: %q", group)
    68  	}
    69  	if group := cfg.GetProjectValidationGroup(); group != "" && !auth.IsValidGroupName(group) {
    70  		vctx.Errorf("invalid project_validation_group: %q", group)
    71  	}
    72  	if group := cfg.GetServiceValidationGroup(); group != "" && !auth.IsValidGroupName(group) {
    73  		vctx.Errorf("invalid service_validation_group: %q", group)
    74  	}
    75  	if group := cfg.GetProjectReimportGroup(); group != "" && !auth.IsValidGroupName(group) {
    76  		vctx.Errorf("invalid project_reimport_group: %q", group)
    77  	}
    78  	if group := cfg.GetServiceReimportGroup(); group != "" && !auth.IsValidGroupName(group) {
    79  		vctx.Errorf("invalid service_reimport_group: %q", group)
    80  	}
    81  	return nil
    82  }
    83  
    84  func validateServicesCfg(vctx *validation.Context, configSet, path string, content []byte) error {
    85  	vctx.SetFile(path)
    86  	cfg := &cfgcommonpb.ServicesCfg{}
    87  	if err := prototext.Unmarshal(content, cfg); err != nil {
    88  		vctx.Errorf("invalid services proto: %s", err)
    89  		return nil
    90  	}
    91  	seenServiceIDs := stringset.New(len(cfg.GetServices()))
    92  	for i, service := range cfg.GetServices() {
    93  		vctx.Enter("services #%d", i)
    94  
    95  		vctx.Enter("id")
    96  		validateUniqueID(vctx, service.GetId(), seenServiceIDs, func(vctx *validation.Context, id string) {
    97  			if _, err := config.ServiceSet(id); err != nil {
    98  				vctx.Errorf("invalid id: %s", err)
    99  			}
   100  		})
   101  		vctx.Exit()
   102  
   103  		for i, owner := range service.GetOwners() {
   104  			vctx.Enter("owners #%d", i)
   105  			validateEmail(vctx, owner)
   106  			vctx.Exit()
   107  		}
   108  
   109  		if metadataURL := service.GetMetadataUrl(); metadataURL != "" {
   110  			vctx.Enter("metadata_url")
   111  			validateURL(vctx, metadataURL)
   112  			vctx.Exit()
   113  		}
   114  
   115  		if hostname := service.GetHostname(); hostname != "" {
   116  			vctx.Enter("hostname")
   117  			if err := validation.ValidateHostname(hostname); err != nil {
   118  				vctx.Error(err)
   119  			}
   120  			vctx.Exit()
   121  		}
   122  
   123  		for i, access := range service.GetAccess() {
   124  			vctx.Enter("access #%d", i)
   125  			validateAccess(vctx, access)
   126  			vctx.Exit()
   127  		}
   128  
   129  		vctx.Exit()
   130  	}
   131  	return nil
   132  }
   133  
   134  func validateImportCfg(vctx *validation.Context, configSet, path string, content []byte) error {
   135  	vctx.SetFile(path)
   136  	cfg := &cfgcommonpb.ImportCfg{}
   137  	if err := prototext.Unmarshal(content, cfg); err != nil {
   138  		vctx.Errorf("invalid import proto: %s", err)
   139  	}
   140  	return nil
   141  }
   142  
   143  func validateSchemaCfg(vctx *validation.Context, configSet, path string, content []byte) error {
   144  	vctx.SetFile(path)
   145  	cfg := &cfgcommonpb.SchemasCfg{}
   146  	if err := prototext.Unmarshal(content, cfg); err != nil {
   147  		vctx.Errorf("invalid schema proto: %s", err)
   148  		return nil
   149  	}
   150  	seenNames := stringset.New(len(cfg.GetSchemas()))
   151  	for i, schema := range cfg.GetSchemas() {
   152  		vctx.Enter("schemas #%d", i)
   153  
   154  		vctx.Enter("name")
   155  		validateUniqueID(vctx, schema.GetName(), seenNames, func(vctx *validation.Context, name string) {
   156  			switch {
   157  			case !strings.Contains(name, ":"):
   158  				vctx.Errorf("must contain \":\"")
   159  			default:
   160  				segs := strings.SplitN(name, ":", 2)
   161  				prefix, p := segs[0], segs[1] // guaranteed by the colon check before
   162  				if cs := config.Set(prefix); prefix != "projects" && (cs.Validate() != nil || cs.Service() == "") {
   163  					vctx.Errorf("left side of \":\" must be a service config set or \"projects\"")
   164  				}
   165  				vctx.Enter("right side of \":\" (path)")
   166  				validatePath(vctx, p)
   167  				vctx.Exit()
   168  			}
   169  		})
   170  		vctx.Exit()
   171  
   172  		vctx.Enter("url")
   173  		validateURL(vctx, schema.GetUrl())
   174  		vctx.Exit()
   175  		vctx.Exit()
   176  	}
   177  	return nil
   178  }
   179  
   180  func validateJSON(vctx *validation.Context, configSet, path string, content []byte) error {
   181  	vctx.SetFile(path)
   182  	var obj any
   183  	if err := json.Unmarshal(content, &obj); err != nil {
   184  		vctx.Errorf("invalid JSON: %s", err)
   185  	}
   186  	return nil
   187  }
   188  
   189  func validatePath(vctx *validation.Context, p string) {
   190  	switch {
   191  	case strings.TrimSpace(p) == "":
   192  		vctx.Errorf("not specified")
   193  	case path.IsAbs(p):
   194  		vctx.Errorf("must not be absolute: %q", p)
   195  	default:
   196  		pathSegs := stringset.NewFromSlice(strings.Split(p, "/")...)
   197  		if pathSegs.Has(".") || pathSegs.Has("..") {
   198  			vctx.Errorf("must not contain \".\" or \"..\" components: %q", p)
   199  		}
   200  	}
   201  }