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 }