go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/auth_service/internal/configs/validation/service.go (about) 1 // Copyright 2022 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 validation 16 17 import ( 18 "fmt" 19 "net" 20 "regexp" 21 "sort" 22 "strings" 23 24 "google.golang.org/protobuf/encoding/prototext" 25 26 "go.chromium.org/luci/auth/identity" 27 "go.chromium.org/luci/common/data/stringset" 28 "go.chromium.org/luci/common/errors" 29 "go.chromium.org/luci/common/lhttp" 30 "go.chromium.org/luci/config/validation" 31 "go.chromium.org/luci/server/auth/service/protocol" 32 33 "go.chromium.org/luci/auth_service/api/configspb" 34 ) 35 36 // ipAllowlistNameRE is the regular expression for IP Allowlist Names. 37 var ipAllowlistNameRE = regexp.MustCompile(`^[0-9A-Za-z_\-\+\.\ ]{2,200}$`) 38 39 const ( 40 PrefixBuiltinRole = "role/" 41 PrefixCustomRole = "customRole/" 42 PrefixRoleInternal = "role/luci.internal." 43 ) 44 45 // validateAllowlist validates an ip_allowlist.cfg file. 46 func validateAllowlist(ctx *validation.Context, configSet, path string, content []byte) error { 47 cfg := configspb.IPAllowlistConfig{} 48 49 if err := prototext.Unmarshal(content, &cfg); err != nil { 50 ctx.Error(err) 51 return nil 52 } 53 54 ctx.SetFile(path) 55 if err := validateAllowlistCfg(&cfg); err != nil { 56 ctx.Error(err) 57 } 58 59 if _, err := GetSubnets(cfg.GetIpAllowlists()); err != nil { 60 ctx.Error(err) 61 } 62 return nil 63 } 64 65 func validateAllowlistCfg(cfg *configspb.IPAllowlistConfig) error { 66 // Allowlist validation. 67 allowlists := stringset.New(len(cfg.GetIpAllowlists())) 68 for _, a := range cfg.GetIpAllowlists() { 69 switch name := a.GetName(); { 70 case !ipAllowlistNameRE.MatchString(name): 71 return errors.New(fmt.Sprintf("invalid ip allowlist name %s", name)) 72 case allowlists.Has(name): 73 return errors.New(fmt.Sprintf("ip allowlist is defined twice %s", name)) 74 default: 75 allowlists.Add(name) 76 } 77 78 // Validate subnets, check that the format is valid. 79 // Either in CIDR format or just a textual representation 80 // of an IP. 81 // e.g. "192.0.0.1", "127.0.0.1/23" 82 for _, subnet := range a.Subnets { 83 if strings.Contains(subnet, "/") { 84 if _, _, err := net.ParseCIDR(subnet); err != nil { 85 return err 86 } 87 } else { 88 if ip := net.ParseIP(subnet); ip == nil { 89 return errors.New(fmt.Sprintf("unable to parse ip for subnet: %s", subnet)) 90 } 91 } 92 } 93 } 94 95 // Assignment validation 96 idents := stringset.New(len(cfg.GetAssignments())) 97 for _, a := range cfg.GetAssignments() { 98 ident := a.GetIdentity() 99 alName := a.GetIpAllowlistName() 100 101 // Checks if valid Identity. 102 if _, err := identity.MakeIdentity(ident); err != nil { 103 return err 104 } 105 if !allowlists.Has(alName) { 106 return errors.New(fmt.Sprintf("unknown allowlist %s", alName)) 107 } 108 if idents.Has(ident) { 109 return errors.New(fmt.Sprintf("identity %s defined twice", ident)) 110 } 111 idents.Add(ident) 112 } 113 114 return nil 115 } 116 117 func validateOAuth(ctx *validation.Context, configSet, path string, content []byte) error { 118 cfg := configspb.OAuthConfig{} 119 120 if err := prototext.Unmarshal(content, &cfg); err != nil { 121 ctx.Error(err) 122 return nil 123 } 124 125 ctx.SetFile(path) 126 if cfg.GetTokenServerUrl() != "" { 127 if _, err := lhttp.ParseHostURL(cfg.GetTokenServerUrl()); err != nil { 128 ctx.Error(err) 129 } 130 } 131 132 return nil 133 } 134 135 func validateSecurityCfg(ctx *validation.Context, configSet, path string, content []byte) error { 136 ctx.SetFile(path) 137 cfg := protocol.SecurityConfig{} 138 139 if err := prototext.Unmarshal(content, &cfg); err != nil { 140 ctx.Error(err) 141 return nil 142 } 143 144 ctx.Enter("internal_service_regexp") 145 for i, re := range cfg.GetInternalServiceRegexp() { 146 ctx.Enter(fmt.Sprintf("# %d", i)) 147 if _, err := regexp.Compile(fmt.Sprintf("^%s$", re)); err != nil { 148 ctx.Error(err) 149 } 150 ctx.Exit() 151 } 152 ctx.Exit() 153 154 return nil 155 } 156 157 func validateImportsCfg(ctx *validation.Context, configSet, path string, content []byte) error { 158 ctx.SetFile(path) 159 cfg := configspb.GroupImporterConfig{} 160 urlErr := errors.New("url field required") 161 ctx.Enter("validating imports.cfg") 162 defer ctx.Exit() 163 164 if err := prototext.Unmarshal(content, &cfg); err != nil { 165 ctx.Error(err) 166 } 167 168 ctx.Enter("checking tarball URLs...") 169 for _, tb := range cfg.GetTarball() { 170 if tb.Url == "" { 171 ctx.Error(urlErr) 172 } 173 } 174 ctx.Exit() 175 176 ctx.Enter("checking plainlist URLs...") 177 for _, pl := range cfg.GetPlainlist() { 178 if pl.Url == "" { 179 ctx.Error(urlErr) 180 } 181 } 182 ctx.Exit() 183 184 ctx.Enter("validating tarball_upload names...") 185 tarballUploadNames := make(map[string]bool) 186 for _, entry := range cfg.GetTarballUpload() { 187 entryName := entry.GetName() 188 if entryName == "" { 189 ctx.Error(errors.New("Some tarball_upload entry doesn't have a name")) 190 } 191 192 if tarballUploadNames[entryName] { 193 ctx.Errorf("tarball_upload entry %s is specified twice", entryName) 194 } 195 tarballUploadNames[entryName] = true 196 197 authorizedUploader := entry.GetAuthorizedUploader() 198 if authorizedUploader == nil { 199 ctx.Errorf("authorized_uploader is required in tarball_upload entry %s", entryName) 200 } 201 202 for _, email := range authorizedUploader { 203 _, err := identity.MakeIdentity(fmt.Sprintf("user:%s", email)) 204 if err != nil { 205 ctx.Error(err) 206 } 207 } 208 } 209 ctx.Exit() 210 211 ctx.Enter("validating systems") 212 seenSystems := make(map[string]bool) 213 seenSystems["external"] = true 214 for _, entry := range cfg.GetTarball() { 215 title := fmt.Sprintf(`"tarball" entry with URL %q`, entry.GetUrl()) 216 if err := validateSystems(entry.GetSystems(), seenSystems, title); err != nil { 217 ctx.Error(err) 218 } 219 } 220 for _, entry := range cfg.GetTarballUpload() { 221 title := fmt.Sprintf(`"tarball_upload" entry with name %q`, entry.GetName()) 222 if err := validateSystems(entry.GetSystems(), seenSystems, title); err != nil { 223 ctx.Error(err) 224 } 225 } 226 ctx.Exit() 227 228 ctx.Enter("validating plainlist groups") 229 seenGroups := make(map[string]bool) 230 for _, entry := range cfg.GetPlainlist() { 231 group := entry.GetGroup() 232 if group == "" { 233 ctx.Errorf(`"plainlist" entry %q needs a "group" field`, entry.GetUrl()) 234 } 235 if seenGroups[group] { 236 ctx.Errorf(`the group %q is imported twice`, group) 237 } 238 seenGroups[group] = true 239 } 240 ctx.Exit() 241 242 return nil 243 } 244 245 // validatePermissionsCfg does basic validation that the permissions.cfg file has the proper format. 246 func validatePermissionsCfg(ctx *validation.Context, configSet, path string, content []byte) error { 247 ctx.SetFile(path) 248 cfg := configspb.PermissionsConfig{} 249 250 // Helper Functions 251 testPrefixes := func(s string, prefixes ...string) bool { 252 for _, p := range prefixes { 253 if strings.HasPrefix(s, p) { 254 return true 255 } 256 } 257 return false 258 } 259 260 // Start validation 261 ctx.Enter("validating permissions.cfg") 262 defer ctx.Exit() 263 264 if err := prototext.Unmarshal(content, &cfg); err != nil { 265 ctx.Error(err) 266 } 267 268 roleMap := make(map[string]*configspb.PermissionsConfig_Role, len(cfg.GetRole())) 269 roleSet := stringset.Set{} 270 271 ctx.Enter("checking role names and building map") 272 for _, role := range cfg.GetRole() { 273 if role.GetName() == "" { 274 ctx.Errorf("name is required") 275 } 276 277 if !testPrefixes(role.GetName(), PrefixBuiltinRole, PrefixCustomRole, PrefixRoleInternal) { 278 ctx.Errorf(`invalid prefix, possible prefixes: ("%s", "%s", "%s")`, PrefixBuiltinRole, PrefixCustomRole, PrefixRoleInternal) 279 } 280 281 if _, ok := roleMap[role.GetName()]; ok { 282 ctx.Errorf("%s is already defined", role.GetName()) 283 } 284 roleMap[role.GetName()] = role 285 roleSet.Add(role.GetName()) 286 } 287 ctx.Exit() 288 289 ctx.Enter("checking permissions and includes") 290 for _, roleObj := range roleMap { 291 for _, perm := range roleObj.GetPermissions() { 292 if strings.Count(perm.GetName(), ".") != 2 { 293 ctx.Errorf("invalid format: Permissions must have the form <service>.<subject>.<verb>") 294 } 295 if perm.GetInternal() && !strings.HasPrefix(roleObj.GetName(), PrefixRoleInternal) { 296 ctx.Errorf("invalid format: can only define internal permissions for internal roles") 297 } 298 } 299 300 for _, inc := range roleObj.GetIncludes() { 301 if _, ok := roleMap[inc]; !ok { 302 ctx.Errorf("%s not defined", inc) 303 } 304 } 305 } 306 ctx.Exit() 307 308 ctx.Enter("checking for cycles") 309 seen := stringset.New(len(roleSet)) 310 for _, roleName := range roleSet.ToSortedSlice() { 311 if !seen.Has(roleName) { 312 cycle, visited, err := findRoleDependencyCycle(roleName, roleMap) 313 if err != nil { 314 ctx.Error(err) 315 } 316 if cycle != nil { 317 cycleStr := strings.Join(cycle, " -> ") 318 ctx.Errorf(fmt.Sprintf("cycle found: %s", cycleStr)) 319 } 320 seen.AddAll(visited) 321 } 322 } 323 324 ctx.Exit() 325 326 return nil 327 } 328 329 // findRoleDependencyCycle performs a DFS over our roleMap to see if there is a cycle present. 330 func findRoleDependencyCycle(startPoint string, roleMap map[string]*configspb.PermissionsConfig_Role) ([]string, []string, error) { 331 visited := stringset.Set{} 332 333 stack := []*configspb.PermissionsConfig_Role{} 334 335 indexOf := func(roles []*configspb.PermissionsConfig_Role, name string) int { 336 for i, r := range roles { 337 if r.GetName() == name { 338 return i 339 } 340 } 341 return -1 342 } 343 344 var visit func(roleName string) (bool, error) 345 visit = func(roleName string) (bool, error) { 346 // Push the current role mapping onto the stack 347 stack = append(stack, roleMap[roleName]) 348 349 // Examine children. 350 for _, included := range roleMap[roleName].GetIncludes() { 351 if visited.Has(included) { 352 // cross edge is okay. 353 continue 354 } 355 356 if i := indexOf(stack, included); i > -1 { 357 stack = append(stack, stack[i]) 358 return true, nil 359 } 360 361 cycle, err := visit(included) 362 if err != nil { 363 return false, err 364 } 365 if cycle { 366 return true, nil 367 } 368 } 369 stack = stack[:len(stack)-1] 370 visited.Add(roleName) 371 372 return false, nil 373 } 374 375 cycle, err := visit(startPoint) 376 if err != nil { 377 return nil, nil, err 378 } 379 if cycle { 380 if len(stack) == 0 { 381 return nil, nil, errors.New("cycle found with empty stack") 382 } 383 names := make([]string, len(stack)) 384 for i, g := range stack { 385 names[i] = g.GetName() 386 } 387 return names, nil, nil 388 } 389 return nil, visited.ToSlice(), nil 390 } 391 392 func validateSystems(systems []string, seenSystems map[string]bool, title string) error { 393 if systems == nil { 394 return errors.New(fmt.Sprintf(`%s needs a "systems" field`, title)) 395 } 396 twice := []string{} 397 for _, system := range systems { 398 if seenSystems[system] { 399 twice = append(twice, system) 400 } else { 401 seenSystems[system] = true 402 } 403 } 404 if len(twice) > 0 { 405 sort.Strings(twice) 406 return errors.New(fmt.Sprintf("%s is specifying a duplicated system(s): %v", title, twice)) 407 } 408 return nil 409 } 410 411 // GetSubnets validates the includes of all allowlists and generates a map {allowlistName: []subnets}. 412 func GetSubnets(allowlists []*configspb.IPAllowlistConfig_IPAllowlist) (map[string][]string, error) { 413 allowlistsByName := make(map[string]*configspb.IPAllowlistConfig_IPAllowlist, len(allowlists)) 414 for _, al := range allowlists { 415 allowlistsByName[al.GetName()] = al 416 } 417 418 subnetMap := make(map[string][]string, len(allowlists)) 419 for _, al := range allowlists { 420 subnets, err := getSubnetsRecursive(al, make([]string, 0, len(allowlists)), allowlistsByName, subnetMap) 421 if err != nil { 422 return nil, err 423 } 424 subnetMap[al.GetName()] = subnets 425 } 426 return subnetMap, nil 427 } 428 429 // getSubnetsRecursive does a depth first search traversal to find all transitively included subnets for a given allowlist. 430 func getSubnetsRecursive(al *configspb.IPAllowlistConfig_IPAllowlist, visiting []string, allowlistsByName map[string]*configspb.IPAllowlistConfig_IPAllowlist, subnetMap map[string][]string) ([]string, error) { 431 alName := al.GetName() 432 433 // If we've already seen this allowlist before. 434 if val, ok := subnetMap[alName]; ok { 435 return val, nil 436 } 437 438 // Cycle check. 439 if contains(visiting, alName) { 440 errorCycle := fmt.Sprintf("%s -> %s", strings.Join(visiting, " -> "), alName) 441 return nil, errors.New(fmt.Sprintf("IP allowlist is part of an included cycle %s", errorCycle)) 442 } 443 444 visiting = append(visiting, alName) 445 subnets := stringset.NewFromSlice(al.GetSubnets()...) 446 for _, inc := range al.GetIncludes() { 447 val, ok := allowlistsByName[inc] 448 if !ok { 449 return nil, errors.New(fmt.Sprintf("IP Allowlist contains unknown allowlist %s", inc)) 450 } 451 452 resolved, err := getSubnetsRecursive(val, visiting, allowlistsByName, subnetMap) 453 if err != nil { 454 return nil, err 455 } 456 subnets.AddAll(resolved) 457 } 458 return subnets.ToSortedSlice(), nil 459 } 460 461 func contains(s []string, val string) bool { 462 for _, v := range s { 463 if v == val { 464 return true 465 } 466 } 467 return false 468 }