Skip to content

Commit

Permalink
feat: add validation for secret block (#126)
Browse files Browse the repository at this point in the history
  • Loading branch information
Neal authored Jan 11, 2021
1 parent 2000583 commit cd87586
Show file tree
Hide file tree
Showing 19 changed files with 551 additions and 42 deletions.
8 changes: 7 additions & 1 deletion yaml/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,14 @@ func (b *Build) Validate(pipeline []byte) error {
invalid = fmt.Errorf("%w: %s", invalid, "stages and steps provided")
}

// validate the secrets block provided
err := b.Secrets.Validate(pipeline)
if err != nil {
invalid = fmt.Errorf("%v: %w", invalid, err)
}

// validate the services block provided
err := b.Services.Validate(pipeline)
err = b.Services.Validate(pipeline)
if err != nil {
invalid = fmt.Errorf("%v: %w", invalid, err)
}
Expand Down
276 changes: 276 additions & 0 deletions yaml/secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ package yaml
import (
"errors"
"fmt"
"regexp"
"strings"

"github.com/docker/distribution/reference"
"github.com/go-vela/types/constants"
"github.com/go-vela/types/pipeline"
"github.com/go-vela/types/raw"
"github.com/goccy/go-yaml"
)

// nolint:lll // jsonschema will cause long lines
Expand Down Expand Up @@ -221,3 +224,276 @@ func (s *StepSecretSlice) UnmarshalYAML(unmarshal func(interface{}) error) error

return errors.New("failed to unmarshal StepSecretSlice")
}

// Validate lints if the secrets configuration is valid.
func (s *SecretSlice) Validate(pipeline []byte) error {
invalid, isInvalid := errors.New("invalid secret block found"), false

// iterate through each secret and linting yaml tags
for i, secret := range *s {
// check required name field
if len(secret.Name) == 0 && secret.Origin.Empty() {
path, err := yaml.PathString(fmt.Sprintf("$.secrets[%d]", i))
if err != nil {
return fmt.Errorf("failed compile: unable to path index: %w", err)
}

source, err := path.AnnotateSource(pipeline, true)
if err != nil {
return fmt.Errorf("failed compile: unable to annotate: %w", err)
}

invalid = fmt.Errorf("%w: %s", invalid,
fmt.Sprintf("no name provided:\n%s\n ", string(source)))
isInvalid = true
}

// check if the engine is not a "native" or "vault"
if len(secret.Engine) != 0 {
if !strings.EqualFold(secret.Engine, constants.DriverNative) &&
!strings.EqualFold(secret.Engine, constants.DriverVault) {
path, err := yaml.PathString(fmt.Sprintf("$.secrets[%d].engine", i))
if err != nil {
return fmt.Errorf("failed compile: unable to path index: %w", err)
}

source, err := path.AnnotateSource(pipeline, true)
if err != nil {
return fmt.Errorf("failed compile: unable to annotate: %w", err)
}

invalid = fmt.Errorf("%w: %s", invalid,
fmt.Sprintf("invalid engine value:\n%s\n ", string(source)))
isInvalid = true
}
}

// allocate variables for secret type checks
var (
bad bool
err error
)

// validate secret by type
switch {
case strings.EqualFold(secret.Type, constants.SecretRepo):
bad, err = secret.validateRepo(pipeline, i)
case strings.EqualFold(secret.Type, constants.SecretOrg):
bad, err = secret.validateOrg(pipeline, i)
case strings.EqualFold(secret.Type, constants.SecretShared):
bad, err = secret.validateShared(pipeline, i)
case !secret.Origin.Empty():
bad, err = secret.validatePlugin(pipeline, i)
}

// check if we need to append a user yaml error
if bad {
invalid = fmt.Errorf("%v: %v", invalid, err)
isInvalid = true
}

// check if the compiler has failed from bad yaml
if strings.HasPrefix(err.Error(), "failed compile:") {
return err
}
}

// check if only default error exists
if isInvalid {
return invalid
}

return nil
}

// validateRepo is a helper function to lint secrets of type "repo".
//
// this function is used to check the fields of secret with the explicit
// definition yaml style.
func (s *Secret) validateRepo(pipeline []byte, i int) (bool, error) {
invalid, isInvalid := errors.New("invalid secret"), false

// check if a key was provided for explicit definition
// when the key == name than we have an implicit definition
if len(s.Key) != 0 && s.Key != s.Name {
match, err := regexp.MatchString(`.+\/.+\/.+`, s.Key)
if err != nil {
return false, fmt.Errorf("unable to execute regex on %s: %w", s.Key, err)
}

// provide anotated error message when bad syntax is detected
if !match {
path, err := yaml.PathString(fmt.Sprintf("$.secrets[%d].key", i))
if err != nil {
return false, fmt.Errorf("failed compile: unable to path index: %w", err)
}

source, err := path.AnnotateSource(pipeline, true)
if err != nil {
return false, fmt.Errorf("failed compile: unable to annotate: %w", err)
}

invalid = fmt.Errorf("%w: %s", invalid,
fmt.Sprintf("invalid key value:\n%s\n ", string(source)))
isInvalid = true
}
}

return isInvalid, invalid
}

// validateOrg is a helper function to lint secrets of type "org".
// nolint:dupl // ignoring dupl to make clearly define which function owns linting a secret type
func (s *Secret) validateOrg(pipeline []byte, i int) (bool, error) {
invalid, isInvalid := errors.New("invalid secret"), false

// check if a key was provided
match, err := regexp.MatchString(`.+\/.+`, s.Key)
if err != nil {
return false, fmt.Errorf("unable to execute regex on %s: %w", s.Key, err)
}

// provide anotated error message when bad syntax is detected
if !match {
if strings.EqualFold(s.Name, s.Key) {
path, err := yaml.PathString(fmt.Sprintf("$.secrets[%d]", i))
if err != nil {
return false, fmt.Errorf("failed compile: unable to path index: %w", err)
}

source, err := path.AnnotateSource(pipeline, true)
if err != nil {
return false, fmt.Errorf("failed compile: unable to annotate: %w", err)
}

invalid = fmt.Errorf("%v: %s", invalid,
fmt.Sprintf("no key provided:\n%s\n ", string(source)))
isInvalid = true
} else {
path, err := yaml.PathString(fmt.Sprintf("$.secrets[%d].key", i))
if err != nil {
return false, fmt.Errorf("failed compile: unable to path index: %w", err)
}

source, err := path.AnnotateSource(pipeline, true)
if err != nil {
return false, fmt.Errorf("failed compile: unable to annotate: %w", err)
}

invalid = fmt.Errorf("%v: %s", invalid,
fmt.Sprintf("invalid key value:\n%s\n ", string(source)))
isInvalid = true
}
}

return isInvalid, invalid
}

// validateShared is a helper function to lint secrets of type "shared".
// nolint:dupl // ignoring dupl to make clearly define which function owns linting a secret type
func (s *Secret) validateShared(pipeline []byte, i int) (bool, error) {
invalid, isInvalid := errors.New("invalid secret"), false

// check if a key was provided
match, err := regexp.MatchString(`.+\/.+\/.+`, s.Key)
if err != nil {
return false, fmt.Errorf("unable to execute regex on %s: %w", s.Key, err)
}

// provide anotated error message when bad syntax is detected
if !match {
if strings.EqualFold(s.Name, s.Key) {
path, err := yaml.PathString(fmt.Sprintf("$.secrets[%d]", i))
if err != nil {
return false, fmt.Errorf("failed compile: unable to path index: %w", err)
}

source, err := path.AnnotateSource(pipeline, true)
if err != nil {
return false, fmt.Errorf("failed compile: unable to annotate: %w", err)
}

invalid = fmt.Errorf("%v: %s", invalid,
fmt.Sprintf("no key provided:\n%s\n ", string(source)))
isInvalid = true
} else {
path, err := yaml.PathString(fmt.Sprintf("$.secrets[%d].key", i))
if err != nil {
return false, fmt.Errorf("failed compile: unable to path index: %w", err)
}

source, err := path.AnnotateSource(pipeline, true)
if err != nil {
return false, fmt.Errorf("failed compile: unable to annotate: %w", err)
}

invalid = fmt.Errorf("%v: %s", invalid,
fmt.Sprintf("invalid key value:\n%s\n ", string(source)))
isInvalid = true
}
}

return isInvalid, invalid
}

// validatePlugin is a helper function to lint secret plugin fields.
func (s *Secret) validatePlugin(pipeline []byte, i int) (bool, error) {
invalid, isInvalid := errors.New("invalid secret plugin"), false

// check required fields
if len(s.Origin.Name) == 0 {
path, err := yaml.PathString(fmt.Sprintf("$.secrets[%d]", i))
if err != nil {
return false, fmt.Errorf("failed compile: unable to path index: %w", err)
}

source, err := path.AnnotateSource(pipeline, true)
if err != nil {
return false, fmt.Errorf("failed compile: unable to annotate: %w", err)
}

invalid = fmt.Errorf("%w: %s", invalid,
fmt.Sprintf("no name provided:\n%s\n ", string(source)))
isInvalid = true
}

if len(s.Origin.Image) == 0 {
path, err := yaml.PathString(fmt.Sprintf("$.secrets[%d]", i))
if err != nil {
return false, fmt.Errorf("failed compile: unable to path index: %w", err)
}

source, err := path.AnnotateSource(pipeline, true)
if err != nil {
return false, fmt.Errorf("failed compile: unable to annotate: %w", err)
}

invalid = fmt.Errorf("%w: %s", invalid,
fmt.Errorf("no image provided %s:\n%s\n ", s.Origin.Name, string(source)))
isInvalid = true
} else {
// parse the image provided into a
// named, fully qualified reference
//
// https://pkg.go.dev/github.com/docker/distribution/reference?tab=doc#ParseAnyReference
_, err := reference.ParseAnyReference(s.Origin.Image)
if err != nil {
// output error with YAML source
path, err := yaml.PathString(fmt.Sprintf("$.secrets[%d].origin.image", i))
if err != nil {
return false, fmt.Errorf("failed compile: unable to path index: %w", err)
}

source, err := path.AnnotateSource(pipeline, true)
if err != nil {
return false, fmt.Errorf("failed compile: unable to annotate: %w", err)
}

invalid = fmt.Errorf("%w: %s", invalid,
fmt.Errorf("invalid image value %s:\n%s\n ", s.Origin.Image, string(source)))
isInvalid = true
}
}

return isInvalid, invalid
}
Loading

0 comments on commit cd87586

Please sign in to comment.