go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cipd/common/common.go (about)

     1  // Copyright 2014 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 common
    16  
    17  import (
    18  	"crypto/sha256"
    19  	"encoding/hex"
    20  	"fmt"
    21  	"mime"
    22  	"path"
    23  	"regexp"
    24  	"sort"
    25  	"strings"
    26  
    27  	"go.chromium.org/luci/auth/identity"
    28  	"go.chromium.org/luci/common/data/stringset"
    29  	"go.chromium.org/luci/common/errors"
    30  	"go.chromium.org/luci/grpc/grpcutil"
    31  
    32  	api "go.chromium.org/luci/cipd/api/cipd/v1"
    33  	"go.chromium.org/luci/cipd/common/cipderr"
    34  )
    35  
    36  var (
    37  	// packageNameRe is a regular expression for a superset of a set of allowed
    38  	// package names.
    39  	//
    40  	// Package names must be lower case and have form "<word>/<word/<word>". See
    41  	// ValidatePackageName for the full spec of how the package name can look.
    42  	//
    43  	// Note: do NOT ever add '+' as allowed character. It will break various URL
    44  	// parsers that use '/+/' to separate parameters.
    45  	packageNameRe = regexp.MustCompile(`^([a-z0-9_\-\.]+/)*[a-z0-9_\-\.]+$`)
    46  
    47  	// A regular expression for a tag key.
    48  	tagKeyReStr = `^[a-z0-9_\-]+$`
    49  	tagKeyRe    = regexp.MustCompile(tagKeyReStr)
    50  
    51  	// A regular expression for tag values.
    52  	//
    53  	// Basically printable ASCII (plus space), except symbols that have meaning in
    54  	// command line or URL contexts (!"#$%&?'^|).
    55  	//
    56  	// Additionally, spaces are allowed only inside the value, not as a prefix or
    57  	// a suffix.
    58  	tagValReStr = `^[A-Za-z0-9$()*+,\-./:;<=>@\\_{}~ ]+$`
    59  	tagValRe    = regexp.MustCompile(tagValReStr)
    60  
    61  	// packageRefRe is a regular expression for a ref.
    62  	packageRefReStr = `^[a-z0-9_./\-]{1,256}$`
    63  	packageRefRe    = regexp.MustCompile(packageRefReStr)
    64  
    65  	// Parameters for instance metadata key-value validation.
    66  	metadataKeyMaxLen = 400
    67  	metadataKeyReStr  = `^[a-z0-9_\-]+$`
    68  	metadataKeyRe     = regexp.MustCompile(metadataKeyReStr)
    69  )
    70  
    71  // MetadataMaxLen is maximum allowed length of an instance metadata entry body.
    72  const MetadataMaxLen = 512 * 1024
    73  
    74  // Pin uniquely identifies an instance of some package.
    75  type Pin struct {
    76  	PackageName string `json:"package"`
    77  	InstanceID  string `json:"instance_id"`
    78  }
    79  
    80  // String converts pin to a human readable string.
    81  func (pin Pin) String() string {
    82  	return fmt.Sprintf("%s:%s", pin.PackageName, pin.InstanceID)
    83  }
    84  
    85  // ValidatePackageName returns error if a string isn't a valid package name.
    86  func ValidatePackageName(name string) error {
    87  	return validatePathishString(name, "package name")
    88  }
    89  
    90  // ValidatePackagePrefix normalizes and validates a package prefix.
    91  //
    92  // A prefix is basically like a package name, except it is allowed to have '/'
    93  // at the end (such trailing '/' is stripped by this function), and it can be
    94  // an empty string or "/" (to indicate the root of the repository).
    95  func ValidatePackagePrefix(p string) (string, error) {
    96  	p = strings.TrimSuffix(p, "/")
    97  	if p != "" {
    98  		if err := validatePathishString(p, "package prefix"); err != nil {
    99  			return "", err
   100  		}
   101  	}
   102  	return p, nil
   103  }
   104  
   105  // validatePathishString is common implementation of ValidatePackageName and
   106  // ValidatePackagePrefix.
   107  func validatePathishString(p, title string) error {
   108  	if !packageNameRe.MatchString(p) {
   109  		return validationErr("invalid %s %q: must be a slash-separated path where each component matches \"[a-z0-9_\\-\\.]+\"", title, p)
   110  	}
   111  	for _, chunk := range strings.Split(p, "/") {
   112  		if strings.Count(chunk, ".") == len(chunk) {
   113  			return validationErr("invalid %s %q: dots-only path components are forbidden", title, p)
   114  		}
   115  	}
   116  	return nil
   117  }
   118  
   119  // ValidatePin returns error if package name or instance id are invalid.
   120  func ValidatePin(pin Pin, v HashAlgoValidation) error {
   121  	if err := ValidatePackageName(pin.PackageName); err != nil {
   122  		return err
   123  	}
   124  	return ValidateInstanceID(pin.InstanceID, v)
   125  }
   126  
   127  // ValidatePackageRef returns error if a string doesn't look like a valid ref.
   128  func ValidatePackageRef(r string) error {
   129  	if ValidateInstanceID(r, AnyHash) == nil {
   130  		return validationErr("invalid ref name %q: it looks like an instance ID causing ambiguities", r)
   131  	}
   132  	if !packageRefRe.MatchString(r) {
   133  		return validationErr("invalid ref name %q: must match %q", r, packageRefReStr)
   134  	}
   135  	return nil
   136  }
   137  
   138  // ValidateInstanceTag returns error if a string doesn't look like a valid tag.
   139  func ValidateInstanceTag(t string) error {
   140  	_, err := ParseInstanceTag(t)
   141  	return err
   142  }
   143  
   144  // ParseInstanceTag takes "k:v" string and returns its proto representation.
   145  func ParseInstanceTag(t string) (*api.Tag, error) {
   146  	switch chunks := strings.SplitN(t, ":", 2); {
   147  	case len(chunks) != 2:
   148  		return nil, validationErr("%q doesn't look like a tag (a key:value pair)", t)
   149  	case len(t) > 400:
   150  		return nil, validationErr("the tag is too long, should be <=400 chars: %q", t)
   151  	case !tagKeyRe.MatchString(chunks[0]):
   152  		return nil, validationErr("invalid tag key in %q: should match %q", t, tagKeyReStr)
   153  	case strings.HasPrefix(chunks[1], " ") || strings.HasSuffix(chunks[1], " "):
   154  		return nil, validationErr("invalid tag value in %q: should not start or end with ' '", t)
   155  	case !tagValRe.MatchString(chunks[1]):
   156  		return nil, validationErr("invalid tag value in %q: should match %q", t, tagValReStr)
   157  	default:
   158  		return &api.Tag{
   159  			Key:   chunks[0],
   160  			Value: chunks[1],
   161  		}, nil
   162  	}
   163  }
   164  
   165  // MustParseInstanceTag takes "k:v" string returns its proto representation or
   166  // panics if the tag is invalid.
   167  func MustParseInstanceTag(t string) *api.Tag {
   168  	tag, err := ParseInstanceTag(t)
   169  	if err != nil {
   170  		panic(err)
   171  	}
   172  	return tag
   173  }
   174  
   175  // JoinInstanceTag returns "k:v" representation of the tag.
   176  //
   177  // Doesn't validate it.
   178  func JoinInstanceTag(t *api.Tag) string {
   179  	return t.Key + ":" + t.Value
   180  }
   181  
   182  // ValidateInstanceVersion return error if a string can't be used as version.
   183  //
   184  // A version can be specified as:
   185  //  1. Instance ID (hash, e.g. "1234deadbeef2234...").
   186  //  2. Package ref (e.g. "latest").
   187  //  3. Instance tag (e.g. "git_revision:abcdef...").
   188  func ValidateInstanceVersion(v string) error {
   189  	if ValidateInstanceID(v, AnyHash) == nil ||
   190  		ValidatePackageRef(v) == nil ||
   191  		ValidateInstanceTag(v) == nil {
   192  		return nil
   193  	}
   194  	return validationErr("bad version %q: not an instance ID, a ref or a tag", v)
   195  }
   196  
   197  // ValidateSubdir returns an error if the string can't be used as an ensure-file
   198  // subdir.
   199  func ValidateSubdir(subdir string) error {
   200  	if subdir == "" { // empty is fine
   201  		return nil
   202  	}
   203  	if strings.Contains(subdir, "\\") {
   204  		return validationErr(`bad subdir %q: backslashes are not allowed (use "/")`, subdir)
   205  	}
   206  	if strings.Contains(subdir, ":") {
   207  		return validationErr(`bad subdir %q: colons are not allowed`, subdir)
   208  	}
   209  	if cleaned := path.Clean(subdir); cleaned != subdir {
   210  		return validationErr("bad subdir %q: should be simplified to %q", subdir, cleaned)
   211  	}
   212  	if strings.HasPrefix(subdir, "./") || strings.HasPrefix(subdir, "../") || subdir == "." {
   213  		return validationErr(`bad subdir %q: contains disallowed dot-path prefix`, subdir)
   214  	}
   215  	if strings.HasPrefix(subdir, "/") {
   216  		return validationErr("bad subdir %q: absolute paths are not allowed", subdir)
   217  	}
   218  	return nil
   219  }
   220  
   221  // ValidatePrincipalName validates strings used to identify principals in ACLs.
   222  //
   223  // The expected format is "<key>:<value>" pair, where <key> is one of "group",
   224  // "user", "anonymous", "service". See also go.chromium.org/luci/auth/identity.
   225  func ValidatePrincipalName(p string) error {
   226  	chunks := strings.Split(p, ":")
   227  	if len(chunks) != 2 || chunks[0] == "" || chunks[1] == "" {
   228  		return validationErr("%q doesn't look like a principal id (<type>:<id>)", p)
   229  	}
   230  	if chunks[0] == "group" {
   231  		return nil // any non-empty group name is OK
   232  	}
   233  	// Should be valid identity otherwise.
   234  	_, err := identity.MakeIdentity(p)
   235  	return err
   236  }
   237  
   238  // NormalizePrefixMetadata validates and normalizes the prefix metadata proto.
   239  //
   240  // Updates r.Prefix in-place by stripping trailing '/', sorts r.Acls and
   241  // principals lists inside them. Skips r.Fingerprint, r.UpdateTime and
   242  // r.UpdateUser, since they are always overridden on the server side.
   243  func NormalizePrefixMetadata(m *api.PrefixMetadata) error {
   244  	var err error
   245  	if m.Prefix, err = ValidatePackagePrefix(m.Prefix); err != nil {
   246  		return err
   247  	}
   248  
   249  	// There should be only one ACL section per role.
   250  	perRole := make(map[api.Role]*api.PrefixMetadata_ACL, len(m.Acls))
   251  	keys := make([]int, 0, len(perRole))
   252  	for i, acl := range m.Acls {
   253  		switch {
   254  		// Note: we allow roles not currently present in *.proto, maybe they came
   255  		// from a newer server. 0 is never OK though.
   256  		case acl.Role == 0:
   257  			return validationErr("ACL entry #%d doesn't have a role specified", i)
   258  		case perRole[acl.Role] != nil:
   259  			return validationErr("role %s is specified twice", acl.Role)
   260  		}
   261  
   262  		perRole[acl.Role] = acl
   263  		keys = append(keys, int(acl.Role))
   264  
   265  		sort.Strings(acl.Principals)
   266  		for _, p := range acl.Principals {
   267  			if err := ValidatePrincipalName(p); err != nil {
   268  				return validationErr("in ACL entry for role %s: %s", acl.Role, err)
   269  			}
   270  		}
   271  	}
   272  
   273  	// Sort ACLs by role.
   274  	if len(keys) != len(m.Acls) {
   275  		panic("must not happen")
   276  	}
   277  	sort.Ints(keys)
   278  	for i, role := range keys {
   279  		m.Acls[i] = perRole[api.Role(role)]
   280  	}
   281  
   282  	return nil
   283  }
   284  
   285  // PinSlice is a simple list of Pins
   286  type PinSlice []Pin
   287  
   288  // Validate ensures that this PinSlice contains no duplicate packages or invalid
   289  // pins.
   290  func (s PinSlice) Validate(v HashAlgoValidation) error {
   291  	dedup := stringset.New(len(s))
   292  	for _, p := range s {
   293  		if err := ValidatePin(p, v); err != nil {
   294  			return err
   295  		}
   296  		if !dedup.Add(p.PackageName) {
   297  			return validationErr("duplicate package %q", p.PackageName)
   298  		}
   299  	}
   300  	return nil
   301  }
   302  
   303  // ToMap converts the PinSlice to a PinMap.
   304  func (s PinSlice) ToMap() PinMap {
   305  	ret := make(PinMap, len(s))
   306  	for _, p := range s {
   307  		ret[p.PackageName] = p.InstanceID
   308  	}
   309  	return ret
   310  }
   311  
   312  // PinMap is a map of package_name to instanceID.
   313  type PinMap map[string]string
   314  
   315  // ToSlice converts the PinMap to a PinSlice.
   316  func (m PinMap) ToSlice() PinSlice {
   317  	s := make(PinSlice, 0, len(m))
   318  	pkgs := make(sort.StringSlice, 0, len(m))
   319  	for k := range m {
   320  		pkgs = append(pkgs, k)
   321  	}
   322  	pkgs.Sort()
   323  	for _, pkg := range pkgs {
   324  		s = append(s, Pin{pkg, m[pkg]})
   325  	}
   326  	return s
   327  }
   328  
   329  // PinSliceBySubdir is a simple mapping of subdir to pin slice.
   330  type PinSliceBySubdir map[string]PinSlice
   331  
   332  // Validate ensures that this doesn't contain any invalid
   333  // subdirs, duplicate packages within the same subdir, or invalid pins.
   334  func (p PinSliceBySubdir) Validate(v HashAlgoValidation) error {
   335  	for subdir, pkgs := range p {
   336  		if err := ValidateSubdir(subdir); err != nil {
   337  			return err
   338  		}
   339  		if err := pkgs.Validate(v); err != nil {
   340  			return validationErr("subdir %q: %s", subdir, err)
   341  		}
   342  	}
   343  	return nil
   344  }
   345  
   346  // ToMap converts this to a PinMapBySubdir
   347  func (p PinSliceBySubdir) ToMap() PinMapBySubdir {
   348  	ret := make(PinMapBySubdir, len(p))
   349  	for subdir, pkgs := range p {
   350  		ret[subdir] = pkgs.ToMap()
   351  	}
   352  	return ret
   353  }
   354  
   355  // PinMapBySubdir is a simple mapping of subdir -> package_name -> instanceID
   356  type PinMapBySubdir map[string]PinMap
   357  
   358  // ToSlice converts this to a PinSliceBySubdir
   359  func (p PinMapBySubdir) ToSlice() PinSliceBySubdir {
   360  	ret := make(PinSliceBySubdir, len(p))
   361  	for subdir, pkgs := range p {
   362  		ret[subdir] = pkgs.ToSlice()
   363  	}
   364  	return ret
   365  }
   366  
   367  // ValidateInstanceMetadataKey returns an error if the given key can't be used
   368  // as an instance metadata key.
   369  func ValidateInstanceMetadataKey(key string) error {
   370  	if len(key) > metadataKeyMaxLen {
   371  		return validationErr("invalid metadata key %q: too long, should be <=%d chars", key, metadataKeyMaxLen)
   372  	}
   373  	if !metadataKeyRe.MatchString(key) {
   374  		return validationErr("invalid metadata key %q: should match %q", key, metadataKeyReStr)
   375  	}
   376  	return nil
   377  }
   378  
   379  // ValidateInstanceMetadataLen returns an error if the given length of the
   380  // metadata payload is too large.
   381  func ValidateInstanceMetadataLen(l int) error {
   382  	if l > MetadataMaxLen {
   383  		return validationErr("the metadata value is too long: should be <=%d bytes, got %d", MetadataMaxLen, l)
   384  	}
   385  	return nil
   386  }
   387  
   388  // ValidateContentType returns an error if the given string can't be used as
   389  // an instance metadata content type.
   390  func ValidateContentType(ct string) error {
   391  	if ct == "" {
   392  		return nil
   393  	}
   394  	if len(ct) > 400 {
   395  		return validationErr("the content type is too long: should be <=400 bytes, got %d", len(ct))
   396  	}
   397  	_, _, err := mime.ParseMediaType(ct)
   398  	if err != nil {
   399  		return validationErr("bad content type %q: %s", ct, err)
   400  	}
   401  	return nil
   402  }
   403  
   404  // ValidateInstanceMetadataFingerprint returns an error if the given string
   405  // doesn't look like an output of InstanceMetadataFingerprint.
   406  func ValidateInstanceMetadataFingerprint(fp string) error {
   407  	if len(fp) != 32 {
   408  		return validationErr("bad metadata fingerprint %q: expecting 32 hex chars", fp)
   409  	}
   410  	if err := checkIsHex(fp); err != nil {
   411  		return validationErr("bad metadata fingerprint %q: %s", fp, err)
   412  	}
   413  	return nil
   414  }
   415  
   416  // InstanceMetadataFingerprint calculates a fingerprint of an instance metadata
   417  // entry.
   418  //
   419  // Doesn't do any validation.
   420  func InstanceMetadataFingerprint(key string, value []byte) string {
   421  	h := sha256.New()
   422  	h.Write([]byte(key))
   423  	h.Write([]byte{':'})
   424  	h.Write(value)
   425  	sum := h.Sum(nil)
   426  	return hex.EncodeToString(sum[:16])
   427  }
   428  
   429  // validationErr returns a tagged validation error.
   430  func validationErr(format string, args ...any) error {
   431  	return errors.Reason(format, args...).
   432  		Tag(grpcutil.InvalidArgumentTag, cipderr.BadArgument).
   433  		Err()
   434  }