pkg/cso/v1/validator.go (354 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package v1
import (
"fmt"
"strings"
semver "github.com/Masterminds/semver/v3"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/go-ozzo/ozzo-validation/v4/is"
"github.com/elastic/harp/pkg/sdk/types"
)
const (
// Local is used to describe local infrastructure provider.
reservedLocalProvider = "local"
// Global is used to replace region component to indicate non-region bound secret suffix.
reservedGlobalRegion = "global"
)
var validators = map[string]func([]string) error{
"meta": validateMeta,
"infra": validateInfra,
"platform": validatePlatform,
"product": validateProduct,
"app": validateApplication,
"artifact": validateArtifact,
}
// Validate path according to to CSO model
func Validate(path string) error {
// Validate path
if err := validation.Validate(path,
validation.Required,
is.PrintableASCII,
); err != nil {
return fmt.Errorf("unable to secret path: %w", err)
}
// Clean path first
cleanPath := Clean(path)
// Split path using '/'
parts := strings.Split(cleanPath, "/")
// Check path part count
if len(parts) < 2 {
return fmt.Errorf("invalid secret path, should contains more than 2 parts")
}
// Check validator according to given ring value
v, ok := validators[parts[0]]
if !ok {
return fmt.Errorf("invalid ring value (%s)", parts[0])
}
// Delegate to ring validator
return v(parts[1:])
}
// -----------------------------------------------------------------------------
func validateMeta(parts []string) error {
// Validate parts count
if len(parts) < 2 {
return fmt.Errorf("invalid part count for meta secret path")
}
// Validate accounts
if err := validation.Validate(parts[0],
validation.Required,
is.PrintableASCII,
); err != nil {
return fmt.Errorf("unable to validate meta path (%s): %w", parts[0], err)
}
// Meta has no constraints
return nil
}
// -----------------------------------------------------------------------------
var cloudProviderRegions = map[string]types.StringArray{
reservedLocalProvider: {},
"aws": {
"af-south-1",
"ap-east-1",
"ap-northeast-1",
"ap-northeast-2",
"ap-northeast-3",
"ap-south-1",
"ap-southeast-1",
"ap-southeast-2",
"ca-central-1",
"eu-central-1",
"eu-north-1",
"eu-south-1",
"eu-west-1",
"eu-west-2",
"eu-west-3",
"me-south-1",
"me-south-1",
"sa-east-1",
"us-east-1",
"us-east-2",
"us-west-1",
"us-west-2",
},
"aws-us-gov": {
"us-gov-east-1",
"us-gov-west-1",
},
"gcp": {
"asia-east1",
"asia-east2",
"asia-northeast1",
"asia-northeast2",
"asia-northeast3",
"asia-south1",
"asia-south2",
"asia-southeast1",
"asia-southeast2",
"australia-southeast1",
"australia-southeast2",
"europe-central2",
"europe-north1",
"europe-southwest1",
"europe-west1",
"europe-west12",
"europe-west2",
"europe-west3",
"europe-west4",
"europe-west6",
"europe-west8",
"europe-west9",
"me-central1",
"me-west1",
"northamerica-northeast1",
"northamerica-northeast2",
"southamerica-east1",
"southamerica-west1",
"us-central1",
"us-east1",
"us-east4",
"us-east5",
"us-south1",
"us-west1",
"us-west2",
"us-west3",
"us-west4",
},
"azure": {
"asia",
"asiapacific",
"australia",
"australiacentral",
"australiacentral2",
"australiaeast",
"australiasoutheast",
"brazil",
"brazilsouth",
"brazilsoutheast",
"canada",
"canadacentral",
"canadaeast",
"centralindia",
"centralus",
"centraluseuap",
"centralusstage",
"eastasia",
"eastasiastage",
"eastus",
"eastus2",
"eastus2euap",
"eastus2stage",
"eastusstage",
"europe",
"france",
"francecentral",
"francesouth",
"germany",
"germanynorth",
"germanywestcentral",
"global",
"india",
"japan",
"japaneast",
"japanwest",
"jioindiacentral",
"jioindiawest",
"korea",
"koreacentral",
"koreasouth",
"northcentralus",
"northcentralusstage",
"northeurope",
"norway",
"norwayeast",
"norwaywest",
"southafrica",
"southafricanorth",
"southafricawest",
"southcentralus",
"southcentralusstage",
"southeastasia",
"southeastasiastage",
"southindia",
"swedencentral",
"switzerland",
"switzerlandnorth",
"switzerlandwest",
"uae",
"uaecentral",
"uaenorth",
"uk",
"uksouth",
"ukwest",
"unitedstates",
"westcentralus",
"westeurope",
"westindia",
"westus",
"westus2",
"westus2stage",
"westus3",
"westusstage",
},
"azure-us-gov": {
"usgovarizona",
"usgovvirginia",
"usdodcentral",
"usdodeast",
"usgoviowa",
"usgovtexas",
},
"ibm": {
"au-syd",
"br-sao",
"brazil",
"ca-tor",
"chennai",
"dal",
"dallas",
"eu-de",
"eu-gb",
"fra",
"frankfurt",
"in-che",
"jp-osa",
"jp-tok",
"kr-seo",
"lon",
"london",
"osaka",
"seoul",
"syd",
"sydney",
"tok",
"tokyo",
"toronto",
"us-east",
"us-south",
"washingtondc",
"wdc",
},
}
func validateInfra(parts []string) error {
// Validate parts count
if len(parts) < 4 {
return fmt.Errorf("invalid part count for infrastructure secret path")
}
// Validate cloud provider
r, ok := cloudProviderRegions[parts[0]]
if !ok {
return fmt.Errorf("cloud provider (%s) not supported", r)
}
// Validate accounts
if err := validation.Validate(parts[1],
validation.Required,
is.PrintableASCII,
); err != nil {
return fmt.Errorf("unable to validate infrastructure cloud provider account (%s): %w", parts[1], err)
}
// Validate region if not local provider and not global region
if parts[0] != reservedLocalProvider && parts[2] != reservedGlobalRegion && !r.ContainsString(parts[2]) {
return fmt.Errorf("invalid region (%s) for account (%s) on cloud provider (%s)", parts[2], parts[1], parts[0])
}
// Infra has no more constraints
return nil
}
// -----------------------------------------------------------------------------
var platformQualityLevels = types.StringArray{"production", "staging", "qa", "dev"}
func validatePlatform(parts []string) error {
// Validate parts count
if len(parts) < 5 {
return fmt.Errorf("invalid part count for platform secret path")
}
// Validate quality grade level
if !platformQualityLevels.Contains(parts[0]) {
return fmt.Errorf("platform quality level (%s) is not supported", parts[0])
}
// Validate name
if err := validation.Validate(parts[1],
validation.Required,
is.PrintableASCII,
); err != nil {
return fmt.Errorf("unable to validate platform name (%s): %w", parts[1], err)
}
// Validate platform region
r := parts[2]
if r != reservedGlobalRegion {
regionFound := false
for _, regions := range cloudProviderRegions {
if regions.Contains(r) || regions.ContainsString(r) {
regionFound = true
break
}
}
if !regionFound {
return fmt.Errorf("unable to find a region matching (%s)", r)
}
}
// Validate accounts
if err := validation.Validate(parts[3],
validation.Required,
is.PrintableASCII,
); err != nil {
return fmt.Errorf("unable to validate platform service (%s): %w", parts[1], err)
}
// Platform has no more constraints
return nil
}
// -----------------------------------------------------------------------------
func validateProduct(parts []string) error {
// Validate parts count
if len(parts) < 3 {
return fmt.Errorf("invalid part count for product secret path")
}
// Extract product name
if err := validation.Validate(parts[0],
validation.Required,
is.PrintableASCII,
); err != nil {
return fmt.Errorf("unable to validate product name (%s): %w", parts[0], err)
}
// check version as a semver compliant version
if err := validateSemVer(parts[1]); err != nil {
return fmt.Errorf("invalid product (%s) version (%s), semver not compliant: %w", parts[0], parts[1], err)
}
// Product has no more constraints
return nil
}
// -----------------------------------------------------------------------------
func validateApplication(parts []string) error {
// Validate parts count
if len(parts) < 6 {
return fmt.Errorf("invalid part count for application secret path")
}
// Validate quality grade level
if !platformQualityLevels.Contains(parts[0]) {
return fmt.Errorf("application quality level (%s) is not supported", parts[0])
}
// Validate platform name
if err := validation.Validate(parts[1],
validation.Required,
is.PrintableASCII,
); err != nil {
return fmt.Errorf("unable to validate platform name (%s): %w", parts[1], err)
}
// Extract product name
if err := validation.Validate(parts[2],
validation.Required,
is.PrintableASCII,
); err != nil {
return fmt.Errorf("unable to validate product name (%s): %w", parts[2], err)
}
// check version as a semver compliant version
if err := validateSemVer(parts[3]); err != nil {
return fmt.Errorf("invalid product (%s) version (%s), semver not compliant: %w", parts[2], parts[3], err)
}
// Extract component
if err := validation.Validate(parts[4],
validation.Required,
is.PrintableASCII,
); err != nil {
return fmt.Errorf("invalid component (%s) for product (%s) version (%s), %w", parts[4], parts[3], parts[2], err)
}
// Product has no more constraints
return nil
}
// -----------------------------------------------------------------------------
func validateArtifact(parts []string) error {
// Validate parts count
if len(parts) < 2 {
return fmt.Errorf("invalid part count for artifact secret path")
}
// Validate type
if err := validation.Validate(parts[1],
validation.Required,
is.PrintableASCII,
); err != nil {
return fmt.Errorf("unable to validate artifact type (%s): %w", parts[1], err)
}
// Artifact has no more constraints
return nil
}
// -----------------------------------------------------------------------------
func validateSemVer(version string) error {
// Clean input
version = strings.TrimPrefix(strings.TrimSpace(strings.ToLower(version)), "v")
// check version as a semver compliant version
_, err := semver.NewVersion(version)
if err != nil {
return fmt.Errorf("version '%s' has not a valid semver syntax: %w", version, err)
}
// No error
return nil
}