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  }