Skip to content

Commit

Permalink
Merge pull request #692 from overmindtech/ssm-param-store
Browse files Browse the repository at this point in the history
SSM param store
  • Loading branch information
dylanratcliffe authored Dec 6, 2024
2 parents 2f29942 + 6b5ccfd commit 4d75c89
Show file tree
Hide file tree
Showing 16 changed files with 658 additions and 25 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ This source requires the following IAM Policy
"sns:Get*",
"sns:List*",
"sqs:Get*",
"sqs:List*"
"sqs:List*",
"ssm:Describe*",
"ssm:Get*",
"ssm:ListTagsForResource"
],
"Resource": "*"
}
Expand Down
24 changes: 24 additions & 0 deletions adapterhelpers/describe_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,22 @@ type DescribeOnlyAdapter[Input InputType, Output OutputType, ClientStruct Client
// unset then a search request will default to searching by ARN
InputMapperSearch func(ctx context.Context, client ClientStruct, scope string, query string) (Input, error)

// A PostSearchFilter, if set, will be called after the search has been
// completed. This can be used to filter the results of the search before
// they are returned to the user, based on the query. This is used in
// situations where the underlying API doesn't allow for granular enough
// searching to match a given query string, and we need to apply some
// additional filtering to the response.
//
// A good example if this is allowing users to search using ARNs that
// contain IAM-Style wildcards. Since IAM is enforced *after* a query is
// run, most APIs don't provide detailed enough search options to completely
// replicate this functionality in the query, and instead we need to filter
// the results ourselves.
//
// This will only be applied when the InputMapperSearch function is also set
PostSearchFilter func(ctx context.Context, query string, items []*sdp.Item) ([]*sdp.Item, error)

// A function that returns a paginator for this API. If this is nil, we will
// assume that the API is not paginated e.g.
// https://aws.github.io/aws-sdk-go-v2/docs/making-requests/#using-paginators
Expand Down Expand Up @@ -363,6 +379,14 @@ func (s *DescribeOnlyAdapter[Input, Output, ClientStruct, Options]) searchCustom
return nil, err
}

if s.PostSearchFilter != nil {
items, err = s.PostSearchFilter(ctx, query, items)
if err != nil {
err = s.processError(err, ck)
return nil, err
}
}

for _, item := range items {
s.cache.StoreItem(item, s.cacheDuration(), ck)
}
Expand Down
16 changes: 16 additions & 0 deletions adapterhelpers/describe_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,22 @@ func TestSearchCustom(t *testing.T) {
if items[0].UniqueAttributeValue() != "custom" {
t.Errorf("expected item to be 'custom', got %v", items[0].UniqueAttributeValue())
}

t.Run("with a post-search filter", func(t *testing.T) {
s.PostSearchFilter = func(ctx context.Context, query string, items []*sdp.Item) ([]*sdp.Item, error) {
return nil, nil
}

items, err := s.Search(context.Background(), "account-id.region", "bar", false)

if err != nil {
t.Fatal(err)
}

if len(items) != 0 {
t.Errorf("expected 0 item, got %v", len(items))
}
})
}

func TestNoInputMapper(t *testing.T) {
Expand Down
52 changes: 52 additions & 0 deletions adapterhelpers/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net/http"
"regexp"
"slices"
"strings"
"testing"
Expand Down Expand Up @@ -98,6 +99,57 @@ func (a *ARN) Type() string {
return a.Resource[:separatorLocation]
}

// Matches checks if the IAM wildcards included in the ARN match another ARN
// using the logic that IAM uses. For example if the ARN is
// "arn:aws:s3:::amzn-s3-demo-bucket/*" then it will match
// "arn:aws:s3:::amzn-s3-demo-bucket/thing" but not
// "arn:aws:s3:::some-other-bucket/object"
func (a *ARN) IAMWildcardMatches(arn string) bool {
targetARN, err := ParseARN(arn)
if err != nil {
return false
}

// You can't use a wildcard in the service segment
if a.Service != targetARN.Service {
return false
}

// Convert * wildcard to regex pattern and escape other special chars
convertToPattern := func(s string) string {
// Escape regex special chars except * and ?
special := []string{".", "+", "^", "$", "(", ")", "[", "]", "{", "}", "|"}
escaped := s
for _, ch := range special {
escaped = strings.ReplaceAll(escaped, ch, "\\"+ch)
}
// Convert * to .* and ? to . for regex
escaped = strings.ReplaceAll(escaped, "*", ".*")
escaped = strings.ReplaceAll(escaped, "?", ".")
return "^" + escaped + "$"
}

// Check each component using pattern matching
components := []struct {
pattern string
target string
}{
{a.Region, targetARN.Region},
{a.AccountID, targetARN.AccountID},
{a.Resource, targetARN.Resource},
}

for _, c := range components {
pattern := convertToPattern(c.pattern)
matched, err := regexp.MatchString(pattern, c.target)
if err != nil || !matched {
return false
}
}

return true
}

// ParseARN Parses an ARN and tries to determine the resource ID from it. The
// logic is that the resource ID will be the last component when separated by
// slashes or colons: https://devopscube.com/aws-arn-guide/
Expand Down
129 changes: 129 additions & 0 deletions adapterhelpers/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,132 @@ func TestParseARN(t *testing.T) {
}
})
}

func TestIAMWildcardMatches(t *testing.T) {
tests := []struct {
Name string
ARN string
ShouldMatch []string
ShouldNotMatch []string
}{
{
Name: "ARN with no wildcards",
ARN: "arn:aws:iam::123456789:user/Bob",
ShouldMatch: []string{
"arn:aws:iam::123456789:user/Bob",
},
ShouldNotMatch: []string{
"arn:aws:iam::123456789:user/Alice",
"arn:aws:iam::123456789:role/Bob",
"arn:aws:iam::123456789:role/Alice",
},
},
{
Name: "Complex multi-wildcard ARN",
// The asterisk (*) character can expand to replace everything
// within a segment, including characters like a forward slash (/)
// that may otherwise appear to be a delimiter within a given
// service namespace. For example, consider the following Amazon S3
// ARN as the same wildcard expansion logic applies to all services.
ARN: "arn:aws:s3:::amzn-s3-demo-bucket/*/test/*",
// The wildcards in the ARN apply to all of the following objects in
// the bucket, not only the first object listed.
ShouldMatch: []string{
"arn:aws:s3:::amzn-s3-demo-bucket/1/test/object.jpg",
"arn:aws:s3:::amzn-s3-demo-bucket/1/2/test/object.jpg",
"arn:aws:s3:::amzn-s3-demo-bucket/1/2/test/3/object.jpg ",
"arn:aws:s3:::amzn-s3-demo-bucket/1/2/3/test/4/object.jpg",
"arn:aws:s3:::amzn-s3-demo-bucket/1///test///object.jpg",
"arn:aws:s3:::amzn-s3-demo-bucket/1/test/.jpg",
"arn:aws:s3:::amzn-s3-demo-bucket//test/object.jpg",
"arn:aws:s3:::amzn-s3-demo-bucket/1/test/",
},
// Consider the last two objects in the previous list. An Amazon S3
// object name can begin or end with the conventional delimiter
// forward slash (/) character. While / works as a delimiter, there
// is no specific significance when this character is used within a
// resource ARN. It is treated the same as any other valid
// character. The ARN would not match the following objects:
ShouldNotMatch: []string{
"arn:aws:s3:::amzn-s3-demo-bucket/1-test/object.jpg",
"arn:aws:s3:::amzn-s3-demo-bucket/test/object.jpg",
"arn:aws:s3:::amzn-s3-demo-bucket/1/2/test.jpg",
},
},
{
Name: "* at the end",
ARN: "arn:aws:s3:::amzn-s3-demo-bucket/*",
ShouldMatch: []string{
"arn:aws:s3:::amzn-s3-demo-bucket/1/test/object.jpg",
"arn:aws:s3:::amzn-s3-demo-bucket/1/2/test/object.jpg",
"arn:aws:s3:::amzn-s3-demo-bucket/1/2/test/3/object.jpg ",
"arn:aws:s3:::amzn-s3-demo-bucket/1/2/3/test/4/object.jpg",
"arn:aws:s3:::amzn-s3-demo-bucket/1///test///object.jpg",
"arn:aws:s3:::amzn-s3-demo-bucket/1/test/.jpg",
"arn:aws:s3:::amzn-s3-demo-bucket//test/object.jpg",
"arn:aws:s3:::amzn-s3-demo-bucket/1/test/",
"arn:aws:s3:::amzn-s3-demo-bucket/1-test/object.jpg",
"arn:aws:s3:::amzn-s3-demo-bucket/test/object.jpg",
"arn:aws:s3:::amzn-s3-demo-bucket/1/2/test.jpg",
},
},
{
Name: "ARN using a ? wildcard",
ARN: "arn:aws:s3:::amzn-s3-demo-bucket/??",
ShouldMatch: []string{
"arn:aws:s3:::amzn-s3-demo-bucket/11",
"arn:aws:s3:::amzn-s3-demo-bucket/ab",
"arn:aws:s3:::amzn-s3-demo-bucket///",
},
ShouldNotMatch: []string{
"arn:aws:s3:::amzn-s3-demo-bucket/1/2",
"arn:aws:s3:::amzn-s3-demo-bucket/1/2/3",
},
},
{
Name: "ARN using a ? wildcard in the middle",
ARN: "arn:aws:s3:::amzn-s3-demo-bucket/1?/2",
ShouldMatch: []string{
"arn:aws:s3:::amzn-s3-demo-bucket/1a/2",
"arn:aws:s3:::amzn-s3-demo-bucket/1b/2",
},
ShouldNotMatch: []string{
"arn:aws:s3:::amzn-s3-demo-bucket/1/2",
"arn:aws:s3:::amzn-s3-demo-bucket/1/2/3",
},
},
{
Name: "ARN using a ? and * wildcard",
ARN: "arn:aws:s3:::amzn-s3-demo-bucket/1?/2*",
ShouldMatch: []string{
"arn:aws:s3:::amzn-s3-demo-bucket/1a/234567890",
"arn:aws:s3:::amzn-s3-demo-bucket/1b/2c",
},
ShouldNotMatch: []string{
"arn:aws:s3:::amzn-s3-demo-bucket/1/2",
"arn:aws:s3:::amzn-s3-demo-bucket/1/2/3",
},
},
}

for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
a, err := ParseARN(test.ARN)
if err != nil {
t.Fatal(err)
}

for _, match := range test.ShouldMatch {
if !a.IAMWildcardMatches(match) {
t.Errorf("expected %v to match %v", a.String(), match)
}
}

for _, match := range test.ShouldNotMatch {
if a.IAMWildcardMatches(match) {
t.Errorf("expected %v to not match %v", a.String(), match)
}
}
})
}
}
3 changes: 2 additions & 1 deletion adapters/sns-data-protection-policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ func TestGetDataProtectionPolicyFunc(t *testing.T) {
}

func TestNewSNSDataProtectionPolicyAdapter(t *testing.T) {
client, account, region := GetAutoConfig(t)
config, account, region := adapterhelpers.GetAutoConfig(t)
client := sns.NewFromConfig(config)

adapter := NewSNSDataProtectionPolicyAdapter(client, account, region)

Expand Down
3 changes: 2 additions & 1 deletion adapters/sns-endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ func TestGetEndpointFunc(t *testing.T) {
}

func TestNewSNSEndpointAdapter(t *testing.T) {
client, account, region := GetAutoConfig(t)
config, account, region := adapterhelpers.GetAutoConfig(t)
client := sns.NewFromConfig(config)

adapter := NewSNSEndpointAdapter(client, account, region)

Expand Down
3 changes: 2 additions & 1 deletion adapters/sns-platform-application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ func TestGetPlatformApplicationFunc(t *testing.T) {
}

func TestNewSNSPlatformApplicationAdapter(t *testing.T) {
client, account, region := GetAutoConfig(t)
config, account, region := adapterhelpers.GetAutoConfig(t)
client := sns.NewFromConfig(config)

adapter := NewSNSPlatformApplicationAdapter(client, account, region)

Expand Down
3 changes: 2 additions & 1 deletion adapters/sns-subscription_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ func TestSNSGetFunc(t *testing.T) {
}

func TestNewSNSSubscriptionAdapter(t *testing.T) {
client, account, region := GetAutoConfig(t)
config, account, region := adapterhelpers.GetAutoConfig(t)
client := sns.NewFromConfig(config)

adapter := NewSNSSubscriptionAdapter(client, account, region)

Expand Down
3 changes: 2 additions & 1 deletion adapters/sns-topic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ func TestGetTopicFunc(t *testing.T) {
}

func TestNewSNSTopicAdapter(t *testing.T) {
client, account, region := GetAutoConfig(t)
config, account, region := adapterhelpers.GetAutoConfig(t)
client := sns.NewFromConfig(config)

adapter := NewSNSTopicAdapter(client, account, region)

Expand Down
15 changes: 0 additions & 15 deletions adapters/sns_test.go

This file was deleted.

Loading

0 comments on commit 4d75c89

Please sign in to comment.