From 301f71c9dfa6a85a214bab57bcf41ef91b204db8 Mon Sep 17 00:00:00 2001 From: Julien Duchesne Date: Tue, 11 Jun 2019 20:59:58 -0400 Subject: [PATCH] Add tagging logic. Allows syncing creds only on certain targets --- credentials/credentials.go | 46 ++++++++++- credentials/credentials_test.go | 141 ++++++++++++++++++++++++++++++++ go.mod | 1 + sync/config.go | 3 + targets/targets.go | 5 ++ 5 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 credentials/credentials_test.go diff --git a/credentials/credentials.go b/credentials/credentials.go index f12fad1..13c0cd8 100644 --- a/credentials/credentials.go +++ b/credentials/credentials.go @@ -12,15 +12,24 @@ import ( type Credentials interface { BaseValidate() bool GetID() string + ShouldSync(targetTags map[string]string) bool ToString(bool) string Validate() bool } +type targetTagsMatcher struct { + DoMatch map[string]interface{} `mapstructure:"do_match"` + DontMatch map[string]interface{} `mapstructure:"dont_match"` +} + // Base defines that fields that are common to all types of credentials type Base struct { ID string Description string - CredType string + TargetTags targetTagsMatcher `mapstructure:"target_tags"` + + // Field set by constructor + CredType string // For multi-value fields. Such as SSM Value string @@ -50,6 +59,32 @@ func (credBase *Base) GetID() string { return credBase.ID } +func (credBase *Base) ShouldSync(targetTags map[string]string) bool { + findMatch := func(match map[string]interface{}) bool { + for key, value := range match { + for tagKey, tag := range targetTags { + if key != tagKey { + continue + } + if valueAsString, ok := value.(string); ok { + if valueAsString == tag { + return true + } + } else if valueAsList, ok := value.([]string); ok { + if listContainsElement(valueAsList, tag) { + return true + } + } else { + log.Warningf("%s ignored. Its value should either be a string or a list of string", key) + } + } + } + return false + } + + return !findMatch(credBase.TargetTags.DontMatch) && (len(credBase.TargetTags.DoMatch) == 0 || findMatch(credBase.TargetTags.DoMatch)) +} + // ParseCredentials transforms a list of maps into a list of Credentials // The credentials type is determined by the `type` attribute func ParseCredentials(credentialsMaps []map[string]interface{}) ([]Credentials, error) { @@ -91,3 +126,12 @@ func ParseSingleCredentials(credentialsMap map[string]interface{}) (Credentials, } return credentials, nil } + +func listContainsElement(list []string, element string) bool { + for _, listElement := range list { + if listElement == element { + return true + } + } + return false +} diff --git a/credentials/credentials_test.go b/credentials/credentials_test.go new file mode 100644 index 0000000..e093dfa --- /dev/null +++ b/credentials/credentials_test.go @@ -0,0 +1,141 @@ +package credentials + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestShouldSyncCredentials(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + wantedTargetTags targetTagsMatcher + targetTags map[string]string + expected bool + }{ + { + name: "No tags", + wantedTargetTags: targetTagsMatcher{ + DoMatch: map[string]interface{}{}, + DontMatch: map[string]interface{}{}, + }, + targetTags: map[string]string{}, + expected: true, + }, + { + name: "No filter", + wantedTargetTags: targetTagsMatcher{ + DoMatch: map[string]interface{}{}, + DontMatch: map[string]interface{}{}, + }, + targetTags: map[string]string{ + "MyTag": "MyValue", + }, + expected: true, + }, + { + name: "Match", + wantedTargetTags: targetTagsMatcher{ + DoMatch: map[string]interface{}{ + "MyFirstTag": "MyValue", + "MyTag": "MyValue", + }, + DontMatch: map[string]interface{}{}, + }, + targetTags: map[string]string{ + "MyTag": "MyValue", + }, + expected: true, + }, + { + name: "Match List Item", + wantedTargetTags: targetTagsMatcher{ + DoMatch: map[string]interface{}{ + "MyTag": []string{"FirstValue", "MyValue"}, + }, + DontMatch: map[string]interface{}{}, + }, + targetTags: map[string]string{ + "MyTag": "MyValue", + }, + expected: true, + }, + { + name: "List Without Matches", + wantedTargetTags: targetTagsMatcher{ + DoMatch: map[string]interface{}{ + "MyTag": []string{"FirstValue", "SecondValue"}, + }, + DontMatch: map[string]interface{}{}, + }, + targetTags: map[string]string{ + "MyTag": "MyValue", + }, + expected: false, + }, + { + name: "String Doesnt Match", + wantedTargetTags: targetTagsMatcher{ + DoMatch: map[string]interface{}{ + "MyTag": "AValue", + }, + DontMatch: map[string]interface{}{}, + }, + targetTags: map[string]string{ + "MyTag": "MyValue", + }, + expected: false, + }, + { + name: "String that shouldn't match", + wantedTargetTags: targetTagsMatcher{ + DoMatch: map[string]interface{}{}, + DontMatch: map[string]interface{}{ + "MyTag": "MyValue", + }, + }, + targetTags: map[string]string{ + "MyTag": "MyValue", + }, + expected: false, + }, + { + name: "String in list that shouldn't match", + wantedTargetTags: targetTagsMatcher{ + DoMatch: map[string]interface{}{}, + DontMatch: map[string]interface{}{ + "MyTag": []string{"Test", "MyValue"}, + }, + }, + targetTags: map[string]string{ + "MyTag": "MyValue", + }, + expected: false, + }, + { + name: "Match and exclude", + wantedTargetTags: targetTagsMatcher{ + DoMatch: map[string]interface{}{ + "MyTag": "Value", + }, + DontMatch: map[string]interface{}{ + "MyOtherTag": []string{"Test", "MyValue"}, + }, + }, + targetTags: map[string]string{ + "MyTag": "Value", + "MyOtherTag": "MyValue", + }, + expected: false, + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + credentials := &Base{TargetTags: tt.wantedTargetTags} + assert.Equal(t, tt.expected, credentials.ShouldSync(tt.targetTags)) + }) + } +} diff --git a/go.mod b/go.mod index cb6adc9..82aad41 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/sirupsen/logrus v1.4.1 github.com/spf13/cobra v0.0.3 github.com/spf13/pflag v1.0.3 // indirect + github.com/stretchr/testify v1.2.2 golang.org/x/net v0.0.0-20190522155817-f3200d17e092 // indirect gopkg.in/yaml.v2 v2.2.2 ) diff --git a/sync/config.go b/sync/config.go index 551556e..53fda84 100644 --- a/sync/config.go +++ b/sync/config.go @@ -78,6 +78,9 @@ func syncCredentials(target targets.Target, credentialsList []credentials.Creden credChannel <- true go func(cred credentials.Credentials) { defer func() { <-credChannel }() + if !cred.ShouldSync(target.GetTags()) { + return + } log.Infof("[%s] Syncing %s", target.GetName(), cred.GetID()) if err := target.UpdateCredentials(cred); err != nil { message := fmt.Sprintf("Failed to send credential %s to %s: %v", cred.GetID(), target.GetName(), err) diff --git a/targets/targets.go b/targets/targets.go index fb07fcd..5aa501e 100644 --- a/targets/targets.go +++ b/targets/targets.go @@ -11,6 +11,7 @@ import ( type Target interface { BaseValidateConfiguration() bool GetName() string + GetTags() map[string]string Initialize([]credentials.Credentials) error ToString() string UpdateListOfCredentials([]credentials.Credentials) error @@ -40,6 +41,10 @@ func (targetBase *Base) BaseValidateConfiguration() bool { return true } +func (targetBase *Base) GetTags() map[string]string { + return targetBase.Tags +} + type Configuration struct { JenkinsTargets []*JenkinsTarget `mapstructure:"jenkins"` }