Skip to content

Commit

Permalink
Adds validation logic for secrets aliases and IDs (Conjur var paths)
Browse files Browse the repository at this point in the history
  • Loading branch information
diverdane committed Oct 28, 2021
1 parent 1a6f421 commit 3fdbaa0
Show file tree
Hide file tree
Showing 6 changed files with 523 additions and 65 deletions.
112 changes: 64 additions & 48 deletions pkg/secrets/pushtofile/secret_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,27 @@ const secretGroupFileFormatPrefix = "conjur.org/secret-file-format."

const defaultFilePermissions os.FileMode = 0664

// Secret represents an application secret that has been retrieved from
// Conjur.
type Secret struct {
Alias string
Value string
}

// SecretGroup incorporates all of the information about a secret group
// that has been parsed from that secret group's Annotations.
type SecretGroup struct {
Name string
FilePath string
FileTemplate string
FileFormat string
PolicyPathPrefix string
FilePermissions os.FileMode
SecretSpecs []SecretSpec
Name string
FilePath string
FileTemplate string
FileFormat string
PolicyPathPrefix string
FilePermissions os.FileMode
SecretSpecs []SecretSpec
}

// ResolvedSecretSpecs resolves all of the secret paths for a secret
// group by prepending each path with that group's policy path prefix.
func (s *SecretGroup) ResolvedSecretSpecs() []SecretSpec {
if len(s.PolicyPathPrefix) == 0 {
return s.SecretSpecs
Expand Down Expand Up @@ -161,49 +167,15 @@ func NewSecretGroups(annotations map[string]string) ([]*SecretGroup, []error) {
var sgs []*SecretGroup

var errors []error
for k, v := range annotations {
if strings.HasPrefix(k, secretGroupPrefix) {
groupName := strings.TrimPrefix(k, secretGroupPrefix)
secretSpecs, err := NewSecretSpecs([]byte(v))
if err != nil {
// Accumulate errors
err = fmt.Errorf(
`cannot create secret specs from annotation "%s": %s`,
k,
err,
)
errors = append(errors, err)
for key := range annotations {
if strings.HasPrefix(key, secretGroupPrefix) {
groupName := strings.TrimPrefix(key, secretGroupPrefix)
sg, errs := newSecretGroup(annotations, groupName)
if errs != nil {
errors = append(errors, errs...)
continue
}

fileTemplate := annotations[secretGroupFileTemplatePrefix+groupName]
filePath := annotations[secretGroupFilePathPrefix+groupName]
fileFormat := annotations[secretGroupFileFormatPrefix+groupName]
policyPathPrefix := annotations[secretGroupPolicyPathPrefix+groupName]

if len(fileFormat) > 0 {
_, err := FileTemplateForFormat(fileFormat, secretSpecs)
if err != nil {
// Accumulate errors
err = fmt.Errorf(
`unable to process file format annotation %q for group: %s`,
fileFormat,
err,
)
errors = append(errors, err)
continue
}
}

sgs = append(sgs, &SecretGroup{
Name: groupName,
FilePath: filePath,
FileTemplate: fileTemplate,
FileFormat: fileFormat,
FilePermissions: defaultFilePermissions,
PolicyPathPrefix: policyPathPrefix,
SecretSpecs: secretSpecs,
})
sgs = append(sgs, sg)
}
}

Expand All @@ -218,3 +190,47 @@ func NewSecretGroups(annotations map[string]string) ([]*SecretGroup, []error) {

return sgs, nil
}

func newSecretGroup(annotations map[string]string, groupName string) (*SecretGroup, []error) {
groupSecrets := annotations[secretGroupPrefix+groupName]
secretSpecs, err := NewSecretSpecs([]byte(groupSecrets))
if err != nil {
err = fmt.Errorf(
`cannot create secret specs from annotation "%s": %s`,
secretGroupPrefix+groupName,
err,
)
return nil, []error{err}

}
if errors := validateSecretPaths(secretSpecs, groupName); err != nil {
return nil, errors
}

fileTemplate := annotations[secretGroupFileTemplatePrefix+groupName]
filePath := annotations[secretGroupFilePathPrefix+groupName]
fileFormat := annotations[secretGroupFileFormatPrefix+groupName]
policyPathPrefix := annotations[secretGroupPolicyPathPrefix+groupName]

if len(fileFormat) > 0 {
_, err := FileTemplateForFormat(fileFormat, secretSpecs)
if err != nil {
err = fmt.Errorf(
`unable to process file format annotation %q for group: %s`,
fileFormat,
err,
)
return nil, []error{err}
}
}

return &SecretGroup{
Name: groupName,
FilePath: filePath,
FileTemplate: fileTemplate,
FileFormat: fileFormat,
FilePermissions: defaultFilePermissions,
PolicyPathPrefix: policyPathPrefix,
SecretSpecs: secretSpecs,
}, nil
}
43 changes: 43 additions & 0 deletions pkg/secrets/pushtofile/secret_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,19 @@ import (
"gopkg.in/yaml.v3"
)

const (
maxConjurVarNameLen = 126
)

// SecretSpec specifies a secret to be retrieved from Conjur by defining
// its alias (i.e. the name of the secret from an application's perspective)
// and its variable path in Conjur.
type SecretSpec struct {
Alias string
Path string
}

// MarshalYAML is a custom marshaller for SecretSpec.
func (t SecretSpec) MarshalYAML() (interface{}, error) {
return map[string]string{t.Alias: t.Path}, nil
}
Expand Down Expand Up @@ -68,6 +76,8 @@ func (t *SecretSpec) unmarshalFromMap(node *yaml.Node) error {
return nil
}

// NewSecretSpecs creates a slice of SecretSpec structs by unmarshalling
// a YAML representation of secret specifications.
func NewSecretSpecs(raw []byte) ([]SecretSpec, error) {
var secretSpecs []SecretSpec
err := yaml.Unmarshal(raw, &secretSpecs)
Expand All @@ -77,3 +87,36 @@ func NewSecretSpecs(raw []byte) ([]SecretSpec, error) {

return secretSpecs, nil
}

func validateSecretPaths(secretSpecs []SecretSpec, groupName string) []error {
var errors []error
for _, secretSpec := range secretSpecs {
if err := validateSecretPath(secretSpec.Path, groupName); err != nil {
errors = append(errors, err)
}
}
return errors
}

func validateSecretPath(path, groupName string) error {
// The Conjur variable path must not be null
if path == "" {
return fmt.Errorf("Secret group %s: null Conjur variable path", groupName)
}

// The Conjur variable path must not end with slash character
varName := path[strings.LastIndex(path, "/")+1:]
if varName == "" {
return fmt.Errorf("Secret group %s: the Conjur variable path '%s' has a trailing '/'",
groupName, path)
}

// The Conjur variable name (the last word in the Conjur variable path)
// must be no longer than 126 characters.
if len(varName) > maxConjurVarNameLen {
return fmt.Errorf("Secret group %s: the Conjur variable name '%s' is longer than %d characters",
groupName, varName, maxConjurVarNameLen)
}

return nil
}
98 changes: 95 additions & 3 deletions pkg/secrets/pushtofile/secret_spec_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package pushtofile

import (
"fmt"
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

const (
validConjurPath1 = "valid/conjur/variable/path"
validConjurPath2 = "another/valid/conjur/variable/path"
)

type secretsSpecTestCase struct {
description string
contents string
Expand All @@ -19,7 +26,7 @@ func (tc secretsSpecTestCase) Run(t *testing.T) {
})
}

func assertGoodSecretSpecs(expectedResult []SecretSpec) func (*testing.T, []SecretSpec, error) {
func assertGoodSecretSpecs(expectedResult []SecretSpec) func(*testing.T, []SecretSpec, error) {
return func(t *testing.T, result []SecretSpec, err error) {
if !assert.NoError(t, err) {
return
Expand All @@ -43,12 +50,12 @@ var secretsSpecTestCases = []secretsSpecTestCase{
assert: assertGoodSecretSpecs(
[]SecretSpec{
{
Path: "dev/openshift/api-url",
Alias: "api-url",
Path: "dev/openshift/api-url",
},
{
Path: "dev/openshift/password",
Alias: "admin-password",
Path: "dev/openshift/password",
},
},
),
Expand Down Expand Up @@ -107,3 +114,88 @@ func TestNewSecretSpecs(t *testing.T) {
tc.Run(t)
}
}

func generateLongName(len int, char byte) string {
var b strings.Builder
b.Grow(len)
for i := 0; i < len; i++ {
b.WriteByte(byte(char))
}
return b.String()
}

func TestValidateSecretSpecPaths(t *testing.T) {
repeatChar := []byte("a")[0]
maxLenConjurVarName := generateLongName(maxConjurVarNameLen, repeatChar)

type assertFunc func(*testing.T, []error, string)

assertNoErrors := func() assertFunc {
return func(t *testing.T, errors []error, desc string) {
assert.Len(t, errors, 0, desc)
}
}

assertErrorsContain := func(expErrStrs ...string) assertFunc {
return func(t *testing.T, errors []error, desc string) {
assert.Len(t, errors, len(expErrStrs), desc)
for i, expErrStr := range expErrStrs {
assert.Contains(t, errors[i].Error(), expErrStr, desc)
}
}
}

testCases := []struct {
description string
path1 string
path2 string
assert assertFunc
}{
{
"valid Conjur paths",
validConjurPath1,
validConjurPath2,
assertNoErrors(),
}, {
"null Conjur path and valid Conjur path",
"",
validConjurPath2,
assertErrorsContain("null Conjur variable path"),
}, {
"Conjur path with trailing '/' and valid Conjur path",
validConjurPath1 + "/",
validConjurPath2,
assertErrorsContain("has a trailing '/'"),
}, {
"Conjur path with max len var name and valid Conjur path",
validConjurPath1 + "/" + maxLenConjurVarName,
validConjurPath2,
assertNoErrors(),
}, {
"Conjur path with oversized var name and valid Conjur path",
validConjurPath1 + "/" + maxLenConjurVarName + "a",
validConjurPath2,
assertErrorsContain(fmt.Sprintf(
"is longer than %d characters", maxConjurVarNameLen)),
}, {
"Two Conjur paths with trailing '/'",
validConjurPath1 + "/",
validConjurPath2 + "/",
assertErrorsContain("has a trailing '/'", "has a trailing '/'"),
},
}

for _, tc := range testCases {
// Set up test case
secretSpecs := []SecretSpec{
{Alias: "foo", Path: tc.path1},
{Alias: "bar", Path: tc.path2},
}

// Run test case
err := validateSecretPaths(secretSpecs, "some-group-name")

// Check result
tc.assert(t, err, tc.description)
}
}
10 changes: 4 additions & 6 deletions pkg/secrets/pushtofile/standard_templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,10 @@ func (s standardTemplate) ValidateAlias(alias string) error {
}

var standardTemplates = map[string]standardTemplate{
"yaml": {template: yamlTemplate, validateAlias: func(alias string) error {
return nil
}},
"json": {template: jsonTemplate},
"dotenv": {template: dotenvTemplate},
"bash": {template: bashTemplate},
"yaml": {template: yamlTemplate, validateAlias: validateYAMLKey},
"json": {template: jsonTemplate, validateAlias: validateJSONKey},
"dotenv": {template: dotenvTemplate, validateAlias: validateBashVarName},
"bash": {template: bashTemplate, validateAlias: validateBashVarName},
}

// FileTemplateForFormat returns the template for a file format, after ensuring the
Expand Down
Loading

0 comments on commit 3fdbaa0

Please sign in to comment.