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 }