From 8982f68f294966a46c83de389e846cca881943e1 Mon Sep 17 00:00:00 2001 From: Dan Corne Date: Tue, 12 Sep 2023 09:31:30 +0100 Subject: [PATCH 1/5] Add config AwsGroupMatch to filter which AWS groups get compared This is potentially dangerous for the reason in the comment, but useful for our purposes and we just need something that works. --- cmd/root.go | 1 + internal/config/config.go | 7 +++++++ internal/sync.go | 33 ++++++++++++++++++++++----------- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index bb7113b3..2e60a5bd 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -167,6 +167,7 @@ func initConfig() { "include_groups", "user_match", "group_match", + "aws_group_match", "sync_method", "region", "identity_store_id", diff --git a/internal/config/config.go b/internal/config/config.go index 820f3a27..e0207a33 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -17,6 +17,10 @@ type Config struct { UserMatch string `mapstructure:"user_match"` // GroupFilter ... GroupMatch string `mapstructure:"group_match"` + // AwsGroupMatch is a regex to filter which AWS groups are included in sync + // Careful this covers all Google groups filtered by GroupMatch or SSOSync will + // persistently try to recreate groups. + AwsGroupMatch string `mapstructure:"aws_group_match"` // SCIMEndpoint .... SCIMEndpoint string `mapstructure:"scim_endpoint"` // SCIMAccessToken ... @@ -50,6 +54,8 @@ const ( DefaultGoogleCredentials = "credentials.json" // DefaultSyncMethod is the default sync method to use. DefaultSyncMethod = "groups" + // Default + DefaultAwsMatch = ".*" ) // New returns a new Config @@ -59,6 +65,7 @@ func New() *Config { LogLevel: DefaultLogLevel, LogFormat: DefaultLogFormat, SyncMethod: DefaultSyncMethod, + AwsGroupMatch: DefaultAwsMatch, GoogleCredentials: DefaultGoogleCredentials, } } diff --git a/internal/sync.go b/internal/sync.go index 7df5fede..a1cfca39 100644 --- a/internal/sync.go +++ b/internal/sync.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" "io/ioutil" + "regexp" "github.com/awslabs/ssosync/internal/aws" "github.com/awslabs/ssosync/internal/config" @@ -38,7 +39,7 @@ import ( type SyncGSuite interface { SyncUsers(string) error SyncGroups(string) error - SyncGroupsUsers(string) error + SyncGroupsUsers(string, string) error } // SyncGSuite is an object type that will synchronize real users and groups @@ -279,13 +280,13 @@ func (s *syncGSuite) SyncGroups(query string) error { // name:Admin* email:aws-* // email:aws-* // process workflow: -// 1) delete users in aws, these were deleted in google -// 2) update users in aws, these were updated in google -// 3) add users in aws, these were added in google -// 4) add groups in aws and add its members, these were added in google -// 5) validate equals aws an google groups members -// 6) delete groups in aws, these were deleted in google -func (s *syncGSuite) SyncGroupsUsers(query string) error { +// 1. delete users in aws, these were deleted in google +// 2. update users in aws, these were updated in google +// 3. add users in aws, these were added in google +// 4. add groups in aws and add its members, these were added in google +// 5. validate equals aws an google groups members +// 6. delete groups in aws, these were deleted in google +func (s *syncGSuite) SyncGroupsUsers(query, aws_group_query string) error { log.WithField("query", query).Info("get google groups") googleGroups, err := s.google.GetGroups(query) @@ -309,7 +310,7 @@ func (s *syncGSuite) SyncGroupsUsers(query string) error { } log.Info("get existing aws groups") - awsGroups, err := s.GetGroups() + awsGroups, err := s.GetGroups(aws_group_query) if err != nil { log.Error("error getting aws groups") return err @@ -755,7 +756,7 @@ func DoSync(ctx context.Context, cfg *config.Config) error { log.WithField("sync_method", cfg.SyncMethod).Info("syncing") if cfg.SyncMethod == config.DefaultSyncMethod { - err = c.SyncGroupsUsers(cfg.GroupMatch) + err = c.SyncGroupsUsers(cfg.GroupMatch, cfg.AwsGroupMatch) if err != nil { return err } @@ -805,9 +806,12 @@ func (s *syncGSuite) includeGroup(name string) bool { } var awsGroups []*aws.Group +var awsGroupFilter *regexp.Regexp -func (s *syncGSuite) GetGroups() ([]*aws.Group, error) { +func (s *syncGSuite) GetGroups(name_regex string) ([]*aws.Group, error) { awsGroups = make([]*aws.Group, 0) + log.Infof("Getting AWS groups matching regex %s", name_regex) + awsGroupFilter = regexp.MustCompile(name_regex) err := s.identityStoreClient.ListGroupsPages( &identitystore.ListGroupsInput{IdentityStoreId: &s.cfg.IdentityStoreID}, @@ -824,6 +828,13 @@ func (s *syncGSuite) GetGroups() ([]*aws.Group, error) { func ListGroupsPagesCallbackFn(page *identitystore.ListGroupsOutput, lastPage bool) bool { // Loop through each Group returned for _, group := range page.Groups { + + // Remove any groups that don't match our query name pattern. + if awsGroupFilter.FindStringIndex(*group.DisplayName) == nil { + log.Infof("Skipped group not matching pattern: %s", *group.DisplayName) + continue + } + // Convert to native Group object awsGroups = append(awsGroups, &aws.Group{ ID: *group.GroupId, From 6873714fd8117034bbc7c18ef1d76abeef10d054 Mon Sep 17 00:00:00 2001 From: AgnesG Date: Wed, 7 Feb 2024 13:36:45 +0000 Subject: [PATCH 2/5] Exclude AWS groups that don't match the filter --- cmd/root.go | 123 +++--- go.mod | 42 +- go.sum | 8 - internal/config/config.go | 10 +- internal/fac/extensions.go | 45 ++ internal/fac/extensions_test.go | 83 ++++ internal/sync.go | 35 +- template.yaml | 712 ++++++++++++++++++++++++++++---- 8 files changed, 876 insertions(+), 182 deletions(-) create mode 100644 internal/fac/extensions.go create mode 100644 internal/fac/extensions_test.go diff --git a/cmd/root.go b/cmd/root.go index 2e60a5bd..5f96398d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -20,14 +20,14 @@ import ( "fmt" "os" - "github.com/awslabs/ssosync/internal" - "github.com/awslabs/ssosync/internal/config" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-lambda-go/events" - "github.com/aws/aws-sdk-go/service/codepipeline" "github.com/aws/aws-lambda-go/lambda" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/codepipeline" "github.com/aws/aws-sdk-go/service/secretsmanager" + "github.com/awslabs/ssosync/internal" + "github.com/awslabs/ssosync/internal/config" "github.com/pkg/errors" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -66,9 +66,9 @@ Complete documentation is available at https://github.com/awslabs/ssosync`, // running inside of AWS Lambda, we use the Lambda // execution path. func Execute() { - if cfg.IsLambda { - log.Info("Executing as Lambda") - lambda.Start(Handler) + if cfg.IsLambda { + log.Info("Executing as Lambda") + lambda.Start(Handler) } if err := rootCmd.Execute(); err != nil { @@ -77,60 +77,60 @@ func Execute() { } func Handler(ctx context.Context, event events.CodePipelineEvent) (string, error) { - log.Debug(event) - err := rootCmd.Execute() - s := session.Must(session.NewSession()) - cpl := codepipeline.New(s) - - cfg.IsLambdaRunningInCodePipeline = len(event.CodePipelineJob.ID) > 0 - - if cfg.IsLambdaRunningInCodePipeline { - log.Info("Lambda has been invoked by CodePipeline") - - if err != nil { - // notify codepipeline and mark its job execution as Failure - log.Fatalf(errors.Wrap(err, "Notifying CodePipeline and mark its job execution as Failure").Error()) - jobID := event.CodePipelineJob.ID - if len(jobID) == 0 { - panic("CodePipeline Job ID is not set") - } - // mark the job as Failure. - cplFailure := &codepipeline.PutJobFailureResultInput{ - JobId: aws.String(jobID), - FailureDetails: &codepipeline.FailureDetails{ - Message: aws.String(err.Error()), - Type: aws.String("JobFailed"), - }, - } - _, cplErr := cpl.PutJobFailureResult(cplFailure) - if cplErr != nil { - log.Fatalf(errors.Wrap(err, "Failed to update CodePipeline jobID status").Error()) - } - return "Failure", err - } else { - log.Info("Notifying CodePipeline and mark its job execution as Success") - jobID := event.CodePipelineJob.ID - if len(jobID) == 0 { - panic("CodePipeline Job ID is not set") - } - // mark the job as Success. - cplSuccess := &codepipeline.PutJobSuccessResultInput{ - JobId: aws.String(jobID), - } - _, cplErr := cpl.PutJobSuccessResult(cplSuccess) - if cplErr != nil { - log.Fatalf(errors.Wrap(err, "Failed to update CodePipeline jobID status").Error()) - } - return "Success", nil - } - } else { - if err != nil { - log.Fatalf(errors.Wrap(err, "Notifying Lambda and mark this execution as Failure").Error()) - return "Failure", err - } else { - return "Success", nil - } - } + log.Debug(event) + err := rootCmd.Execute() + s := session.Must(session.NewSession()) + cpl := codepipeline.New(s) + + cfg.IsLambdaRunningInCodePipeline = len(event.CodePipelineJob.ID) > 0 + + if cfg.IsLambdaRunningInCodePipeline { + log.Info("Lambda has been invoked by CodePipeline") + + if err != nil { + // notify codepipeline and mark its job execution as Failure + log.Fatalf(errors.Wrap(err, "Notifying CodePipeline and mark its job execution as Failure").Error()) + jobID := event.CodePipelineJob.ID + if len(jobID) == 0 { + panic("CodePipeline Job ID is not set") + } + // mark the job as Failure. + cplFailure := &codepipeline.PutJobFailureResultInput{ + JobId: aws.String(jobID), + FailureDetails: &codepipeline.FailureDetails{ + Message: aws.String(err.Error()), + Type: aws.String("JobFailed"), + }, + } + _, cplErr := cpl.PutJobFailureResult(cplFailure) + if cplErr != nil { + log.Fatalf(errors.Wrap(err, "Failed to update CodePipeline jobID status").Error()) + } + return "Failure", err + } else { + log.Info("Notifying CodePipeline and mark its job execution as Success") + jobID := event.CodePipelineJob.ID + if len(jobID) == 0 { + panic("CodePipeline Job ID is not set") + } + // mark the job as Success. + cplSuccess := &codepipeline.PutJobSuccessResultInput{ + JobId: aws.String(jobID), + } + _, cplErr := cpl.PutJobSuccessResult(cplSuccess) + if cplErr != nil { + log.Fatalf(errors.Wrap(err, "Failed to update CodePipeline jobID status").Error()) + } + return "Success", nil + } + } else { + if err != nil { + log.Fatalf(errors.Wrap(err, "Notifying Lambda and mark this execution as Failure").Error()) + return "Failure", err + } else { + return "Success", nil + } + } } func init() { @@ -247,6 +247,7 @@ func addFlags(cmd *cobra.Command, cfg *config.Config) { rootCmd.Flags().StringSliceVar(&cfg.IncludeGroups, "include-groups", []string{}, "include only these Google Workspace groups, NOTE: only works when --sync-method 'users_groups'") rootCmd.Flags().StringVarP(&cfg.UserMatch, "user-match", "m", "", "Google Workspace Users filter query parameter, example: 'name:John* email:admin*', see: https://developers.google.com/admin-sdk/directory/v1/guides/search-users") rootCmd.Flags().StringVarP(&cfg.GroupMatch, "group-match", "g", "", "Google Workspace Groups filter query parameter, example: 'name:Admin* email:aws-*', see: https://developers.google.com/admin-sdk/directory/v1/guides/search-groups") + rootCmd.Flags().StringVarP(&cfg.AwsGroupMatch, "aws-group-match", "a", config.DefaultAwsGroupMatch, "Filter to select specific AWS groups for syncing with Google Workspace, by default selects all. Works only in `groups` sync mode.") rootCmd.Flags().StringVarP(&cfg.SyncMethod, "sync-method", "s", config.DefaultSyncMethod, "Sync method to use (users_groups|groups)") rootCmd.Flags().StringVarP(&cfg.Region, "region", "r", "", "AWS Region where AWS SSO is enabled") rootCmd.Flags().StringVarP(&cfg.IdentityStoreID, "identity-store-id", "i", "", "Identifier of Identity Store in AWS SSO") diff --git a/go.mod b/go.mod index d3483bf0..ce0869bf 100644 --- a/go.mod +++ b/go.mod @@ -1,27 +1,51 @@ module github.com/awslabs/ssosync -go 1.16 +go 1.21 require ( github.com/BurntSushi/toml v1.0.0 github.com/aws/aws-lambda-go v1.23.0 github.com/aws/aws-sdk-go v1.44.102 - github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/golang/mock v1.5.0 - github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.0 - github.com/magiconair/properties v1.8.5 // indirect - github.com/mitchellh/mapstructure v1.4.1 // indirect - github.com/pelletier/go-toml v1.9.0 // indirect github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.8.1 - github.com/spf13/afero v1.6.0 // indirect - github.com/spf13/cast v1.3.1 // indirect github.com/spf13/cobra v1.1.3 - github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/viper v1.7.1 github.com/stretchr/testify v1.7.0 golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c google.golang.org/api v0.46.0 +) + +require ( + cloud.google.com/go v0.81.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/googleapis/gax-go/v2 v2.0.5 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/magiconair/properties v1.8.5 // indirect + github.com/mitchellh/mapstructure v1.4.1 // indirect + github.com/pelletier/go-toml v1.9.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/afero v1.6.0 // indirect + github.com/spf13/cast v1.3.1 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.2.0 // indirect + go.opencensus.io v0.23.0 // indirect + golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect + golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect + golang.org/x/text v0.3.7 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20210429181445-86c259c2b4ab // indirect + google.golang.org/grpc v1.37.0 // indirect + google.golang.org/protobuf v1.26.0 // indirect gopkg.in/ini.v1 v1.62.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect ) diff --git a/go.sum b/go.sum index b9305ba4..d6bf2953 100644 --- a/go.sum +++ b/go.sum @@ -38,7 +38,6 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= @@ -51,8 +50,6 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aws/aws-lambda-go v1.23.0 h1:Vjwow5COkFJp7GePkk9kjAo/DyX36b7wVPKwseQZbRo= github.com/aws/aws-lambda-go v1.23.0/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XOkbv4w9SGLU= -github.com/aws/aws-sdk-go v1.38.36 h1:MiqzQY/IOFTX/jmGse7ThafD0eyOC4TrCLv2KY1v+bI= -github.com/aws/aws-sdk-go v1.38.36/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.44.102 h1:6tUCTGL2UDbFZae1TLGk8vTgeXuzkb8KbAe2FiAeKHc= github.com/aws/aws-sdk-go v1.44.102/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -409,8 +406,6 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210508051633-16afe75a6701 h1:lQVgcB3+FoAXOb20Dp6zTzAIrpj1k/yOOBN7s+Zv1rA= -golang.org/x/net v0.0.0-20210508051633-16afe75a6701/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -481,8 +476,6 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210507161434-a76c4d0a0096 h1:5PbJGn5Sp3GEUjJ61aYbUP6RIo3Z3r2E4Tv9y2z8UHo= -golang.org/x/sys v0.0.0-20210507161434-a76c4d0a0096/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -495,7 +488,6 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= diff --git a/internal/config/config.go b/internal/config/config.go index e0207a33..22bee392 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -17,7 +17,7 @@ type Config struct { UserMatch string `mapstructure:"user_match"` // GroupFilter ... GroupMatch string `mapstructure:"group_match"` - // AwsGroupMatch is a regex to filter which AWS groups are included in sync + // AwsGroupMatch is a regex to filter which AWS groups are included in sync. // Careful this covers all Google groups filtered by GroupMatch or SSOSync will // persistently try to recreate groups. AwsGroupMatch string `mapstructure:"aws_group_match"` @@ -27,7 +27,7 @@ type Config struct { SCIMAccessToken string `mapstructure:"scim_access_token"` // IsLambda ... IsLambda bool - // IsLambdaRunningInCodePipeline ... + // IsLambdaRunningInCodePipeline ... IsLambdaRunningInCodePipeline bool // Ignore users ... IgnoreUsers []string `mapstructure:"ignore_users"` @@ -54,8 +54,8 @@ const ( DefaultGoogleCredentials = "credentials.json" // DefaultSyncMethod is the default sync method to use. DefaultSyncMethod = "groups" - // Default - DefaultAwsMatch = ".*" + // DefaultAwsGroupMatch is the default regex for AWS group name to be matched. + DefaultAwsGroupMatch = ".*" ) // New returns a new Config @@ -65,7 +65,7 @@ func New() *Config { LogLevel: DefaultLogLevel, LogFormat: DefaultLogFormat, SyncMethod: DefaultSyncMethod, - AwsGroupMatch: DefaultAwsMatch, + AwsGroupMatch: DefaultAwsGroupMatch, GoogleCredentials: DefaultGoogleCredentials, } } diff --git a/internal/fac/extensions.go b/internal/fac/extensions.go new file mode 100644 index 00000000..23849df1 --- /dev/null +++ b/internal/fac/extensions.go @@ -0,0 +1,45 @@ +// Package fac contains custom functions for additional operations on data. +package fac + +import ( + "errors" + "fmt" + "regexp" + + "github.com/awslabs/ssosync/internal/aws" +) + +// ErrNoAWSGroups indicates no AWS groups were received. +var ErrNoAWSGroups = errors.New("received no AWS groups") + +// ErrorBadRegex represents a regex compilation error. +type ErrorBadRegex struct { + Message string + Err error +} + +func (e ErrorBadRegex) Error() string { + return e.Message +} + +// MatchAWSGroups will filter out the AWS groups that don't match the regex. +// Returns an error on failure, a list of AWS groups that match on success. +func MatchAWSGroups(awsGroups []*aws.Group, matchRegex string) ([]*aws.Group, error) { + if awsGroups == nil || len(awsGroups) == 0 { + return nil, ErrNoAWSGroups + } + + awsGroupRegex, err := regexp.Compile(matchRegex) + if err != nil { + return nil, ErrorBadRegex{Message: fmt.Sprintf("can't compile regex %s", matchRegex), Err: err} + } + + matchedGroups := make([]*aws.Group, 0) + for _, group := range awsGroups { + if awsGroupRegex.FindStringIndex(group.DisplayName) != nil { + matchedGroups = append(matchedGroups, group) + } + } + + return matchedGroups, nil +} diff --git a/internal/fac/extensions_test.go b/internal/fac/extensions_test.go new file mode 100644 index 00000000..16f5ab24 --- /dev/null +++ b/internal/fac/extensions_test.go @@ -0,0 +1,83 @@ +package fac + +import ( + "regexp/syntax" + "testing" + + "github.com/awslabs/ssosync/internal/aws" + "github.com/awslabs/ssosync/internal/config" + "github.com/stretchr/testify/assert" +) + +func TestMatchAWSGroups(t *testing.T) { + tests := []struct { + name string + awsGroupMatch string + inputGroups []*aws.Group + expectedGroups []*aws.Group + expectedErr error + }{ + { + name: "correctly matches all groups with default group match", + awsGroupMatch: config.DefaultAwsGroupMatch, + inputGroups: []*aws.Group{ + {DisplayName: "aws-group-A"}, + {DisplayName: "aws-group-B"}, + {DisplayName: "aws-group-C"}, + {DisplayName: "aws-meow-meow"}, + }, + expectedGroups: []*aws.Group{ + {DisplayName: "aws-group-A"}, + {DisplayName: "aws-group-B"}, + {DisplayName: "aws-group-C"}, + {DisplayName: "aws-meow-meow"}, + }, + expectedErr: nil, + }, + { + name: "correctly matches selected groups", + awsGroupMatch: "aws-group-*", + inputGroups: []*aws.Group{ + {DisplayName: "aws-group-A"}, + {DisplayName: "aws-group-B"}, + {DisplayName: "aws-group-C"}, + {DisplayName: "aws-meow-meow"}, + }, + expectedGroups: []*aws.Group{ + {DisplayName: "aws-group-A"}, + {DisplayName: "aws-group-B"}, + {DisplayName: "aws-group-C"}, + }, + expectedErr: nil, + }, + { + name: "returns an error when input groups empty", + awsGroupMatch: "aws-group-*", + inputGroups: []*aws.Group{}, + expectedErr: ErrNoAWSGroups, + }, + { + name: "returns an error when input groups nil", + awsGroupMatch: "aws-group-*", + inputGroups: []*aws.Group{}, + expectedErr: ErrNoAWSGroups, + }, + { + name: "returns an error when regex invalid", + awsGroupMatch: "[^0-1", + inputGroups: []*aws.Group{{DisplayName: "aws-group-A"}}, + expectedErr: ErrorBadRegex{ + Message: "can't compile regex [^0-1", + Err: &syntax.Error{Code: syntax.ErrMissingBracket, Expr: "[^0-1"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + groups, err := MatchAWSGroups(tt.inputGroups, tt.awsGroupMatch) + assert.Equal(t, tt.expectedErr, err) + assert.Equal(t, tt.expectedGroups, groups) + }) + } +} diff --git a/internal/sync.go b/internal/sync.go index a1cfca39..c2a06351 100644 --- a/internal/sync.go +++ b/internal/sync.go @@ -20,10 +20,10 @@ import ( "errors" "fmt" "io/ioutil" - "regexp" "github.com/awslabs/ssosync/internal/aws" "github.com/awslabs/ssosync/internal/config" + "github.com/awslabs/ssosync/internal/fac" "github.com/awslabs/ssosync/internal/google" "github.com/hashicorp/go-retryablehttp" @@ -268,7 +268,8 @@ func (s *syncGSuite) SyncGroups(query string) error { } // SyncGroupsUsers will sync groups and its members from Google -> AWS SSO SCIM -// allowing filter groups base on google api filter query parameter +// allowing filtering Google groups based on google api filter query parameter +// and including only those groups from AWS that match the awsGroupMatch filter // References: // * https://developers.google.com/admin-sdk/directory/v1/guides/search-groups // query possible values: @@ -286,9 +287,9 @@ func (s *syncGSuite) SyncGroups(query string) error { // 4. add groups in aws and add its members, these were added in google // 5. validate equals aws an google groups members // 6. delete groups in aws, these were deleted in google -func (s *syncGSuite) SyncGroupsUsers(query, aws_group_query string) error { - +func (s *syncGSuite) SyncGroupsUsers(query, awsGroupMatch string) error { log.WithField("query", query).Info("get google groups") + googleGroups, err := s.google.GetGroups(query) if err != nil { return err @@ -310,12 +311,20 @@ func (s *syncGSuite) SyncGroupsUsers(query, aws_group_query string) error { } log.Info("get existing aws groups") - awsGroups, err := s.GetGroups(aws_group_query) + awsGroups, err := s.GetGroups() if err != nil { log.Error("error getting aws groups") return err } + log.Info("prune AWS groups that don't originate from Google") + onlyAWSGroupsFromGoogle, matchErr := fac.MatchAWSGroups(awsGroups, awsGroupMatch) + if err != nil { + log.Errorf("error filtering AWS groups by %s", matchErr) + } else { + awsGroups = onlyAWSGroupsFromGoogle + } + log.Info("get existing aws users") awsUsers, err := s.GetUsers() if err != nil { @@ -806,12 +815,9 @@ func (s *syncGSuite) includeGroup(name string) bool { } var awsGroups []*aws.Group -var awsGroupFilter *regexp.Regexp -func (s *syncGSuite) GetGroups(name_regex string) ([]*aws.Group, error) { +func (s *syncGSuite) GetGroups() ([]*aws.Group, error) { awsGroups = make([]*aws.Group, 0) - log.Infof("Getting AWS groups matching regex %s", name_regex) - awsGroupFilter = regexp.MustCompile(name_regex) err := s.identityStoreClient.ListGroupsPages( &identitystore.ListGroupsInput{IdentityStoreId: &s.cfg.IdentityStoreID}, @@ -828,13 +834,6 @@ func (s *syncGSuite) GetGroups(name_regex string) ([]*aws.Group, error) { func ListGroupsPagesCallbackFn(page *identitystore.ListGroupsOutput, lastPage bool) bool { // Loop through each Group returned for _, group := range page.Groups { - - // Remove any groups that don't match our query name pattern. - if awsGroupFilter.FindStringIndex(*group.DisplayName) == nil { - log.Infof("Skipped group not matching pattern: %s", *group.DisplayName) - continue - } - // Convert to native Group object awsGroups = append(awsGroups, &aws.Group{ ID: *group.GroupId, @@ -878,8 +877,8 @@ func ConvertSdkUserObjToNative(user *identitystore.User) *aws.User { for _, email := range user.Emails { if email.Value == nil || email.Type == nil || email.Primary == nil { - // This must be a user created by AWS Control Tower - // Need feature development to make how these users are treated + // This must be a user created by AWS Control Tower + // Need feature development to make how these users are treated // configurable. continue } diff --git a/template.yaml b/template.yaml index 54b53ab9..4daf39b0 100644 --- a/template.yaml +++ b/template.yaml @@ -5,26 +5,45 @@ Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: - default: "General" + default: Which pattern are we deploying? The app with secrets, the app but using existing secrets, or just the secrets. + Parameters: + - DeployPattern + - Label: + default: AWS IAM Identity Center (Successor to AWS Single Sign-On) Parameters: - - GoogleCredentials - - GoogleAdminEmail - SCIMEndpointUrl - SCIMEndpointAccessToken - Region - IdentityStoreID - Label: - default: "Advanced Configuration" + default: Google Workspace Credentials + Parameters: + - GoogleAdminEmail + - GoogleCredentials + - Label: + default: Sync Configuration Parameters: - SyncMethod - GoogleUserMatch - GoogleGroupMatch - - LogLevel - - LogFormat - - ScheduleExpression - IgnoreUsers - IgnoreGroups + - Label: + default: "Configuration options for users_groups Mode only" + Parameters: - IncludeGroups + - Label: + default: "Configuration options for groups Mode only" + Parameters: + - AwsGroupMatch + - Label: + default: "Lambda Configuration" + Parameters: + - FunctionName + - LogLevel + - LogFormat + - TimeOut + - ScheduleExpression AWS::ServerlessRepo::Application: Name: ssosync @@ -42,13 +61,48 @@ Metadata: SourceCodeUrl: https://github.com/awslabs/ssosync/tree/1.0.0-rc.10 Parameters: + DeployPattern: + Type: String + Description: | + App + secrets (default); you provide the values for the secrets and the everything is setup. + App only; Deploys the app and to use the secrets you provide the arn for, that exist in this account. + App for cross-account; Deploys the app and to use the secrets you provide the arn for, also requires details of the KMS key used to encrypt them. + Secrets only; Just creates the secrets. + Secrets for cross-account; Just creates the secrets, and encrypts them with a KMS key and share them with the target account. + Default: App + secrets + AllowedValues: + - App + secrets + - App only + - App for cross-account + - Secrets only + - Secrets for cross-account + + CrossStackConfig: + Type: String + Description: | + [App for cross-account] this is the AppConfig from the ouputs of the Secrets for cross-account stack. + [Secrets for cross-account] this is the AWS account id into which you will be deplying the SSOSync app? + Default: "" + AllowedPattern: '(?!.*\s)|(\d{12})|(arn:aws:secretsmanager:((us(-gov)?|ap|ca|cn|eu|sa)-(central|(north|south)?(east|west)?)-\d):[0-9]{8,12}:secret:[a-zA-Z0-9/_+=.@-]{1,512})(,arn:aws:secretsmanager:((us(-gov)?|ap|ca|cn|eu|sa)-(central|(north|south)?(east|west)?)-\d):[0-9]{8,12}:secret:[a-zA-Z0-9/_+=.@-]{1,512}){5}(,arn:aws:kms:((us(-gov)?|ap|ca|cn|eu|sa)-(central|(north|south)?(east|west)?)-\d):[0-9]{12}:key/[a-zA-Z0-9/_+=.@-]{1,512})?' + + FunctionName: + Type: String + Description: | + [optional] Specify the name you want to us for this deployment, if you want to trigger SSOSync as part of a pipeline or other automation this will ensure a consistent arn to call. leave empty for default behaviour. + Default: "" + AllowedPattern: '(?!.*\s)|[a-zA-Z0-9-_]{1,140}' + ScheduleExpression: Type: String - Description: Schedule for trigger the execution of ssosync (see CloudWatch schedule expressions) + Description: | + [optional] Schedule for trigger the execution of ssosync (see CloudWatch schedule expressions), leave empty if you want to trigger execution by another method such as AWS CodePipeline. Default: rate(15 minutes) + AllowedPattern: '(?!.*\s)|rate\(\d{1,3} (minutes|hours|days)\)|(cron\((([0-9]|[1-5][0-9]|60)|\d\/([0-9]|[1-5][0-9]|60)|\*) (([0-9]|[1][0-9]|[2][0-3])|(\d\/([0-9]|[1][0-9]|[2][0-3]))|(([0-9]|[1][0-9]|[2][0-3])-([0-9]|[1][0-9]|[2][0-3]))|\*) (([1-9]|[1-2][0-9]|[3][0-1])|\d\/([1-9]|[1-2][0-9]|[3][0-1])|[1-5]W|L|\*|\?) (([1-9]|[1][1-2])|(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)|((JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV)-(FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))|(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV)(,(FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)){0,11}|\d\/([0-9]|[1][0-2])|\?|\*) ((MON|TUE|WED|THU|FRI|SAT|SUN)|(MON|TUE|WED|THU|FRI|SAT)-(TUE|WED|THU|FRI|SAT|SUN)|(MON|TUE|WED|THU|FRI|SAT)(,(TUE|WED|THU|FRI|SAT|SUN)){0,6}|[1-7]L|[1-7]#[1-5]|\?|\*) ((19[7-9][0-9]|2[0-1]\d\d)|(19[7-9][0-9]|2[0-1]\d\d)-(19[7-9][0-9]|2[0-1]\d\d)|(19[7-9][0-9]|2[0-1]\d\d)(,(19[7-9][0-9]|2[0-1]\d\d))*|\*)\))' + LogLevel: Type: String - Description: Log level for Lambda function logging + Description: | + [required] Log level for Lambda function logging Default: warn AllowedValues: - panic @@ -58,166 +112,662 @@ Parameters: - info - debug - trace + LogFormat: Type: String - Description: Log format for Lambda function logging + Description: | + [required] Log format for Lambda function logging Default: json AllowedValues: - json - text + + TimeOut: + Type: Number + Description: | + [required] Timeout for the Lambda function + Default: 300 + MinValue: 1 + MaxValue: 900 + +# Secrets GoogleCredentials: Type: String - Description: Credentials to log into Google (content of credentials.json) + Description: | + Credentials to log into Google (content of credentials.json) + Default: "" + AllowedPattern: '(?!.*\s)|(\{(\s)*(".*")(\s)*:(\s)*(".*")(\s)*\})' NoEcho: true + GoogleAdminEmail: Type: String - Description: Google Admin email + Description: | + Google Admin email + Default: "" + AllowedPattern: '(?!.*\s)|(([a-zA-Z0-9.+=_-]{0,61})@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)' NoEcho: true + SCIMEndpointUrl: Type: String - Description: AWS SSO SCIM Endpoint Url - NoEcho: true + Description: | + AWS IAM Identity Center - SCIM Endpoint Url + Default: "" + AllowedPattern: '(?!.*\s)|(https://scim.(us(-gov)?|ap|ca|cn|eu|sa)-(central|(north|south)?(east|west)?)-([0-9]{1}).amazonaws.com/(.*)-([a-z0-9]{4})-([a-z0-9]{4})-([a-z0-9]{12})/scim/v2/)' + SCIMEndpointAccessToken: Type: String - Description: AWS SSO SCIM AccessToken + Description: | + AWS IAM Identity Center - SCIM AccessToken + Default: "" + AllowedPattern: '(?!.*\s)|([0-9a-zA-Z/=+-\\]{500,600})' NoEcho: true + Region: Type: String - Description: AWS Region where AWS SSO is enabled - NoEcho: true + Description: | + AWS Region where AWS IAM Identity Center is enabled + Default: "" + AllowedPattern: '(?!.*\s)|(us(-gov)?|ap|ca|cn|eu|sa)-(central|(north|south)?(east|west)?)-\d' + IdentityStoreID: Type: String - Description: Identifier of Identity Store in AWS SSO - NoEcho: true + Description: | + Identifier of Identity Store in AWS IAM Identity Center + Default: "" + AllowedPattern: '(?!.*\s)|d-[1-z0-9]{10}' + GoogleUserMatch: Type: String Description: | - Google Workspace user filter query parameter, example: 'name:John* email:admin*', see: https://developers.google.com/admin-sdk/directory/v1/guides/search-users - Default: '*' + [optional] Google Workspace user filter query parameter, example: 'name:John* email:admin*', see: https://developers.google.com/admin-sdk/directory/v1/guides/search-users + Default: "" + AllowedPattern: '(?!.*\s)|(name|Name|NAME)(:([a-zA-Z0-9]{1,64})(\*))|(name|Name|NAME)(=([a-zA-Z0-9 ]{1,64}))|(email|Email|EMAIL)(:([a-zA-Z0-9.\-_]{1,64})(\*))|(email|Email|EMAIL)(=([a-zA-Z0-9.\-_]{1,64})@([a-zA-Z0-9.\-]{5,260}))' + GoogleGroupMatch: Type: String Description: | - Google Workspace group filter query parameter, example: 'name:Admin* email:aws-*', see: https://developers.google.com/admin-sdk/directory/v1/guides/search-groups + [optional] Google Workspace group filter query parameter, example: 'name:Admin* email:aws-*', see: https://developers.google.com/admin-sdk/directory/v1/guides/search-groups Default: 'name:AWS*' + AllowedPattern: '(?!.*\s)|(name|Name|NAME)(:([a-zA-Z0-9]{1,64})\*)|(name|Name|NAME)(=([a-zA-Z0-9 ]{1,64}))|(email|Email|EMAIL)(:([a-zA-Z0-9.\-_]{1,64})\*)|(email|Email|EMAIL)(=([a-zA-Z0-9.\-_]{1,64})@([a-zA-Z0-9.\-]{5,260}))' + + AwsGroupMatch: + Type: String + Description: | + [optional] Regex to match AWS groups to be included in sync. + Default: '.*' + IgnoreGroups: Type: String Description: | - Ignore these Google Workspace groups - Default: 'none' + [optional] Ignore these Google Workspace groups, leave empty if not required + Default: "" + AllowedPattern: '(?!.*\s)|([0-9a-zA-Z\-= _]*)(,[0-9a-zA-Z\-=@. _]*)*' + IgnoreUsers: Type: String Description: | - Ignore these Google Workspace users - Default: 'none' + [optional] Ignore these Google Workspace users, leave empty if not required + Default: "" + AllowedPattern: '(?!.*\s)|([0-9a-zA-Z\-= _]*)(,[0-9a-zA-Z\-=@. _]*)*' + IncludeGroups: Type: String Description: | - Include only these Google Workspace groups. (Only applicable for SyncMethod user_groups) - Default: '*' + [optional] Include only these Google Workspace groups, leave empty if not required. (Only applicable for SyncMethod user_groups) + Default: "" + AllowedPattern: '(?!.*\s)|([0-9a-zA-Z\-= _]*)(,[0-9a-zA-Z\-=@. _]*)*' + SyncMethod: Type: String - Description: Sync method to use + Description: Sync method to use Default: groups AllowedValues: - groups - users_groups +Conditions: + SetFunctionName: !Not + - !Equals + - !Ref FunctionName + - "" + SetGoogleUserMatch: !Or + - !Not + - !Equals + - !Ref GoogleUserMatch + - "" + - !Equals + - !Ref SyncMethod + - "users_groups" + SetGoogleGroupMatch: !Or + - !Not + - !Equals + - !Ref GoogleGroupMatch + - "" + - !Equals + - !Ref SyncMethod + - "users_groups" + SetIgnoreGroups: !Not + - !Equals + - !Ref IgnoreGroups + - "" + SetIgnoreUsers: !Not + - !Equals + - !Ref IgnoreUsers + - "" + SetIncludeGroups: !Or + - !Not + - !Equals + - !Ref IncludeGroups + - "" + - !Equals + - !Ref SyncMethod + - "groups" + OnSchedule: !Not + - !Equals + - !Ref ScheduleExpression + - "" + CreateFunction: !Or + - !Equals + - !Ref DeployPattern + - "App for cross-account" + - !Equals + - !Ref DeployPattern + - "App + secrets" + - !Equals + - !Ref DeployPattern + - "App only" + CreateSecrets: !Or + - !Equals + - !Ref DeployPattern + - "App + secrets" + - !Equals + - !Ref DeployPattern + - "Secrets only" + - !Equals + - !Ref DeployPattern + - "Secrets for cross-account" + CreateKey: !Equals + - !Ref DeployPattern + - "Secrets for cross-account" + RemoteSecrets: !Equals + - !Ref DeployPattern + - "App for cross-account" + LocalSecrets: !Or + - !Equals + - !Ref DeployPattern + - "App + secrets" + - !Equals + - !Ref DeployPattern + - "App only" + OutputFunction: !Or + - !Equals + - !Ref DeployPattern + - "App + secrets" + - !Equals + - !Ref DeployPattern + - "App only" + - !Equals + - !Ref DeployPattern + - "App for cross-account" + OutputSecrets: !Equals + - !Ref DeployPattern + - "Secrets only" + +Rules: + SecretValues: + RuleCondition: !Or + - !Equals + - !Ref DeployPattern + - "App + secrets" + - !Equals + - !Ref DeployPattern + - "Secrets only" + - !Equals + - !Ref DeployPattern + - "Secrets for cross-account" + Assertions: + - Assert: !Not + - !Equals + - !Ref GoogleCredentials + - "" + AssertDescription: 'The contents of the Credentials.json is required for this deployment type.' + - Assert: !Not + - !Equals + - !Ref GoogleAdminEmail + - "" + AssertDescription: 'The email address of a directory admin is required for this deployment type.' + - Assert: !Not + - !Equals + - !Ref SCIMEndpointUrl + - "" + AssertDescription: 'The SCIM url from IAM Identity Center is required for this deployment type.' + - Assert: !Not + - !Equals + - !Ref SCIMEndpointAccessToken + - "" + AssertDescription: 'The SCIM Access Token is required for this deployment type.' + - Assert: !Not + - !Equals + - !Ref Region + - "" + AssertDescription: 'The Region in which IAM Identity Center is deployed is required for this deployment type.' + - Assert: !Not + - !Equals + - !Ref IdentityStoreID + - "" + AssertDescription: 'The Identity Store Id for IAM Identity Center is required for this deployment type.' + LocalSecrets: + RuleCondition: !Or + - !Equals + - !Ref DeployPattern + - "App + secrets" + - !Equals + - !Ref DeployPattern + - "Secrets only" + Assertions: + - Assert: !Equals + - !Ref CrossStackConfig + - "" + AssertDescription: 'Do not provide a Cross Stack Configuration for this deployment type.' + CrossSecrets: + RuleCondition: !Equals + - !Ref DeployPattern + - "Secrets for cross-account" + Assertions: + - Assert: !Not + - !Equals + - !Ref CrossStackConfig + - "" + AssertDescription: 'The AWS account id, of the account where you intend to deploy the app.' + + CrossApp: + RuleCondition: !Or + - !Equals + - !Ref DeployPattern + - "App" + - !Equals + - !Ref DeployPattern + - "App for cross-account" + Assertions: + - Assert: !Equals + - !Ref GoogleCredentials + - "" + AssertDescription: 'A value for GoogleCredentials is not required for this deployment type.' + - Assert: !Equals + - !Ref GoogleAdminEmail + - "" + AssertDescription: 'A value for GoogleAdminEmail is not required for this deployment type.' + - Assert: !Equals + - !Ref SCIMEndpointUrl + - "" + AssertDescription: 'A value for SCIMEndpointUrl is not required for this deployment type.' + - Assert: !Equals + - !Ref SCIMEndpointAccessToken + - "" + AssertDescription: 'A value for SCIMEndpointAccessToken is not required for this deployment type.' + - Assert: !Equals + - !Ref Region + - "" + AssertDescription: 'A value for Region is not required for this deployment type.' + - Assert: !Equals + - !Ref IdentityStoreID + - "" + AssertDescription: 'A value for IdentityStoreID is not required for this deployment type.' + - Assert: !Not + - !Equals + - !Ref CrossStackConfig + - "" + AssertDescription: 'AppConfig copied from the outputs of the secrets stack is required for this deployment type.' Resources: + + SSOSyncRoleLocal: + Type: AWS::IAM::Role + Condition: LocalSecrets + Properties: + RoleName: SSOSyncAppRole + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Action: + - sts:AssumeRole + Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Path: / + ManagedPolicyArns: + - arn:aws:iam::aws:policy/AWSLambdaExecute + Policies: + - PolicyName: SSOSyncAppPolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Sid: SSMGetParameterPolicy + Effect: Allow + Action: + - "secretsmanager:Get*" + Resource: + - !If [CreateSecrets, !Ref SecretGoogleCredentials, !Select [0, !Split [',', !Ref CrossStackConfig]]] + - !If [CreateSecrets, !Ref SecretGoogleAdminEmail, !Select [1, !Split [',', !Ref CrossStackConfig]]] + - !If [CreateSecrets, !Ref SecretSCIMEndpoint, !Select [2, !Split [',', !Ref CrossStackConfig]]] + - !If [CreateSecrets, !Ref SecretSCIMAccessToken, !Select [3, !Split [',', !Ref CrossStackConfig]]] + - !If [CreateSecrets, !Ref SecretRegion, !Select [4, !Split [',', !Ref CrossStackConfig]]] + - !If [CreateSecrets, !Ref SecretIdentityStoreID, !Select [5, !Split [',', !Ref CrossStackConfig]]] + - Sid: IdentityStoreAccesPolicy + Effect: Allow + Action: + - "identitystore:DeleteUser" + - "identitystore:CreateGroup" + - "identitystore:CreateGroupMembership" + - "identitystore:ListGroups" + - "identitystore:ListUsers" + - "identitystore:ListGroupMemberships" + - "identitystore:IsMemberInGroups" + - "identitystore:GetGroupMembershipId" + - "identitystore:DeleteGroupMembership" + - "identitystore:DeleteGroup" + Resource: + - "*" + - Sid: CodePipelinePolicy + Effect: Allow + Action: + - codepipeline:PutJobSuccessResult + - codepipeline:PutJobFailureResult + Resource: "*" + + SSOSyncRoleRemote: + Type: AWS::IAM::Role + Condition: RemoteSecrets + Properties: + RoleName: SSOSyncAppRole + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Action: + - sts:AssumeRole + Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Path: / + ManagedPolicyArns: + - arn:aws:iam::aws:policy/AWSLambdaExecute + Policies: + - PolicyName: SSOSyncAppPolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Sid: SSMGetParameterPolicy + Effect: Allow + Action: + - "secretsmanager:Get*" + Resource: + - !Select [0, !Split [',', !Ref CrossStackConfig]] # GoogleCredentials + - !Select [1, !Split [',', !Ref CrossStackConfig]] # GoogleAdminEmail + - !Select [2, !Split [',', !Ref CrossStackConfig]] # SCIMEndpointUrl + - !Select [3, !Split [',', !Ref CrossStackConfig]] # SCIMEndpointAccessToken + - !Select [4, !Split [',', !Ref CrossStackConfig]] # Region + - !Select [5, !Split [',', !Ref CrossStackConfig]] # IdentityStoreID + - Sid: KMSDecryptPolicy + Effect: Allow + Action: + - kms:Decrypt + Resource: + - !Select [6, !Split [',', !Ref CrossStackConfig]] # KMSSecret + - Sid: IdentityStoreAccesPolicy + Effect: Allow + Action: + - "identitystore:DeleteUser" + - "identitystore:CreateGroup" + - "identitystore:CreateGroupMembership" + - "identitystore:ListGroups" + - "identitystore:ListUsers" + - "identitystore:ListGroupMemberships" + - "identitystore:IsMemberInGroups" + - "identitystore:GetGroupMembershipId" + - "identitystore:DeleteGroupMembership" + - "identitystore:DeleteGroup" + Resource: + - "*" + - Sid: CodePipelinePolicy + Effect: Allow + Action: + - codepipeline:PutJobSuccessResult + - codepipeline:PutJobFailureResult + Resource: "*" + SSOSyncFunction: Type: AWS::Serverless::Function + Condition: CreateFunction Properties: - Runtime: go1.x - Handler: dist/ssosync_linux_amd64_v1/ssosync - Timeout: 300 + FunctionName: !If [SetFunctionName, !Ref FunctionName, !Ref AWS::NoValue] + Role: !If [RemoteSecrets, !GetAtt SSOSyncRoleRemote.Arn, !GetAtt SSOSyncRoleLocal.Arn] + Runtime: provided.al2 + Handler: bootstrap + Architectures: + - arm64 + Timeout: !Ref TimeOut Environment: Variables: SSOSYNC_LOG_LEVEL: !Ref LogLevel SSOSYNC_LOG_FORMAT: !Ref LogFormat - SSOSYNC_GOOGLE_CREDENTIALS: !Ref AWSGoogleCredentialsSecret - SSOSYNC_GOOGLE_ADMIN: !Ref AWSGoogleAdminEmail - SSOSYNC_SCIM_ENDPOINT: !Ref AWSSCIMEndpointSecret - SSOSYNC_SCIM_ACCESS_TOKEN: !Ref AWSSCIMAccessTokenSecret - SSOSYNC_REGION: !Ref AWSRegionSecret - SSOSYNC_IDENTITY_STORE_ID: !Ref AWSIdentityStoreIDSecret - SSOSYNC_USER_MATCH: !Ref GoogleUserMatch - SSOSYNC_GROUP_MATCH: !Ref GoogleGroupMatch + SSOSYNC_GOOGLE_CREDENTIALS: !If [CreateSecrets, !Ref SecretGoogleCredentials, !Select [0, !Split [',', !Ref CrossStackConfig]]] + SSOSYNC_GOOGLE_ADMIN: !If [CreateSecrets, !Ref SecretGoogleAdminEmail, !Select [1, !Split [',', !Ref CrossStackConfig]]] + SSOSYNC_SCIM_ENDPOINT: !If [CreateSecrets, !Ref SecretSCIMEndpoint, !Select [2, !Split [',', !Ref CrossStackConfig]]] + SSOSYNC_SCIM_ACCESS_TOKEN: !If [CreateSecrets, !Ref SecretSCIMAccessToken, !Select [3, !Split [',', !Ref CrossStackConfig]]] + SSOSYNC_REGION: !If [CreateSecrets, !Ref SecretRegion, !Select [4, !Split [',', !Ref CrossStackConfig]]] + SSOSYNC_IDENTITY_STORE_ID: !If [CreateSecrets, !Ref SecretIdentityStoreID, !Select [5, !Split [',', !Ref CrossStackConfig]]] + SSOSYNC_USER_MATCH: !If [SetGoogleUserMatch, !Ref GoogleUserMatch, !Ref AWS::NoValue] + SSOSYNC_GROUP_MATCH: !If [SetGoogleGroupMatch, !Ref GoogleGroupMatch, !Ref AWS::NoValue] + SSOSYNC_AWS_GROUP_MATCH: !Ref AWSGroupMatch SSOSYNC_SYNC_METHOD: !Ref SyncMethod - SSOSYNC_IGNORE_GROUPS: !Ref IgnoreGroups - SSOSYNC_IGNORE_USERS: !Ref IgnoreUsers - SSOSYNC_INCLUDE_GROUPS: !Ref IncludeGroups - Policies: - - Statement: - - Sid: SSMGetParameterPolicy - Effect: Allow - Action: - - "secretsmanager:Get*" - Resource: - - !Ref AWSGoogleCredentialsSecret - - !Ref AWSGoogleAdminEmail - - !Ref AWSSCIMEndpointSecret - - !Ref AWSSCIMAccessTokenSecret - - !Ref AWSRegionSecret - - !Ref AWSIdentityStoreIDSecret - - Version: '2012-10-17' - Statement: - - Sid: IdentityStoreAccesPolicy - Effect: Allow - Action: - - "identitystore:DeleteUser" - - "identitystore:CreateGroup" - - "identitystore:CreateGroupMembership" - - "identitystore:ListGroups" - - "identitystore:ListUsers" - - "identitystore:ListGroupMemberships" - - "identitystore:IsMemberInGroups" - - "identitystore:GetGroupMembershipId" - - "identitystore:DeleteGroupMembership" - - "identitystore:DeleteGroup" - Resource: - - "*" + SSOSYNC_IGNORE_GROUPS: !If [SetIgnoreGroups, !Ref IgnoreGroups, !Ref AWS::NoValue] + SSOSYNC_IGNORE_USERS: !If [SetIgnoreUsers, !Ref IgnoreUsers, !Ref AWS::NoValue] + SSOSYNC_INCLUDE_GROUPS: !If [SetIncludeGroups, !Ref IncludeGroups, !Ref AWS::NoValue] Events: SyncScheduledEvent: Type: Schedule Name: AWSSyncSchedule Properties: - Enabled: true - Schedule: !Ref ScheduleExpression + Enabled: !If [OnSchedule, false, true] + Schedule: !If [OnSchedule, !Ref ScheduleExpression, "rate(15 minutes)"] + - AWSGoogleCredentialsSecret: + KeyAlias: + Type: AWS::KMS::Alias + Condition: CreateKey + Properties: + AliasName: alias/SSOSync + TargetKeyId: !Ref KeyForSecrets + + KeyForSecrets: + Type: AWS::KMS::Key + Condition: CreateKey + Properties: + Description: Key for protecting SSOSync Secrets in cross-account deployment + Enabled: true + KeySpec: SYMMETRIC_DEFAULT + KeyUsage: ENCRYPT_DECRYPT + MultiRegion: false + PendingWindowInDays: 7 + KeyPolicy: + Version: 2012-10-17 + Id: key-default-1 + Statement: + - Sid: Enable IAM User Permissions + Effect: Allow + Principal: + AWS: !Sub arn:aws:iam::${AWS::AccountId}:root + Action: 'kms:*' + Resource: '*' + - Sid: AppRole in Other account + Effect: Allow + Principal: + AWS: !Sub arn:aws:iam::${CrossStackConfig}:root + Action: + - kms:Decrypt + - kms:DescribeKey + Resource: '*' + + SecretGoogleCredentials: Type: "AWS::SecretsManager::Secret" + Condition: CreateSecrets Properties: Name: SSOSyncGoogleCredentials SecretString: !Ref GoogleCredentials + KmsKeyId: !If [CreateKey, !Ref KeyAlias, alias/aws/secretsmanager] + + SecretGoogleCredentialsPolicy: + Type: AWS::SecretsManager::ResourcePolicy + Condition: CreateKey + Properties: + SecretId: !Ref SecretGoogleCredentials + ResourcePolicy: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + AWS: !Sub arn:aws:iam::${CrossStackConfig}:root + Action: + - secretsmanager:GetSecretValue + Resource: '*' - AWSGoogleAdminEmail: + SecretGoogleAdminEmail: Type: "AWS::SecretsManager::Secret" + Condition: CreateSecrets Properties: Name: SSOSyncGoogleAdminEmail SecretString: !Ref GoogleAdminEmail + KmsKeyId: !If [CreateKey, !Ref KeyAlias, alias/aws/secretsmanager] - AWSSCIMEndpointSecret: # This can be moved to custom provider + SecretGoogleAdminEmailPolicy: + Type: AWS::SecretsManager::ResourcePolicy + Condition: CreateKey + Properties: + SecretId: !Ref SecretGoogleAdminEmail + ResourcePolicy: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + AWS: !Sub arn:aws:iam::${CrossStackConfig}:root + Action: + - secretsmanager:GetSecretValue + Resource: '*' + + SecretSCIMEndpoint: # This can be moved to custom provider Type: "AWS::SecretsManager::Secret" + Condition: CreateSecrets Properties: Name: SSOSyncSCIMEndpointUrl SecretString: !Ref SCIMEndpointUrl + KmsKeyId: !If [CreateKey, !Ref KeyAlias, alias/aws/secretsmanager] - AWSSCIMAccessTokenSecret: # This can be moved to custom provider + SecretSCIMEndpointPolicy: + Type: AWS::SecretsManager::ResourcePolicy + Condition: CreateKey + Properties: + SecretId: !Ref SecretSCIMEndpoint + ResourcePolicy: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + AWS: !Sub arn:aws:iam::${CrossStackConfig}:root + Action: + - secretsmanager:GetSecretValue + Resource: '*' + + SecretSCIMAccessToken: # This can be moved to custom provider Type: "AWS::SecretsManager::Secret" + Condition: CreateSecrets Properties: Name: SSOSyncSCIMAccessToken SecretString: !Ref SCIMEndpointAccessToken + KmsKeyId: !If [CreateKey, !Ref KeyAlias, alias/aws/secretsmanager] - AWSRegionSecret: + SecretSCIMAccessTokenPolicy: + Type: AWS::SecretsManager::ResourcePolicy + Condition: CreateKey + Properties: + SecretId: !Ref SecretSCIMAccessToken + ResourcePolicy: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + AWS: !Sub arn:aws:iam::${CrossStackConfig}:root + Action: + - secretsmanager:GetSecretValue + Resource: '*' + + SecretRegion: Type: "AWS::SecretsManager::Secret" + Condition: CreateSecrets Properties: Name: SSOSyncRegion SecretString: !Ref Region + KmsKeyId: !If [CreateKey, !Ref KeyAlias, alias/aws/secretsmanager] - AWSIdentityStoreIDSecret: + SecretRegionPolicy: + Type: AWS::SecretsManager::ResourcePolicy + Condition: CreateKey + Properties: + SecretId: !Ref SecretRegion + ResourcePolicy: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + AWS: !Sub arn:aws:iam::${CrossStackConfig}:root + Action: + - secretsmanager:GetSecretValue + Resource: '*' + + SecretIdentityStoreID: Type: "AWS::SecretsManager::Secret" + Condition: CreateSecrets Properties: Name: SSOSyncIdentityStoreID SecretString: !Ref IdentityStoreID + KmsKeyId: !If [CreateKey, !Ref KeyAlias, alias/aws/secretsmanager] + + SecretIdentityStoreIDPolicy: + Type: AWS::SecretsManager::ResourcePolicy + Condition: CreateKey + Properties: + SecretId: !Ref SecretIdentityStoreID + ResourcePolicy: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + AWS: !Sub arn:aws:iam::${CrossStackConfig}:root + Action: + - secretsmanager:GetSecretValue + Resource: '*' + +Outputs: + FunctionArn: + Condition: OutputFunction + Description: "The Arn of the deployed lambda function" + Value: !GetAtt SSOSyncFunction.Arn + Export: + Name: FunctionARN + + AppConfigLocal: + Condition: OutputSecrets + Description: "The Comma Separated list of secrets ARNs to copy and paste into the CrossStackConfig field of the App only stack." + Value: !Sub ${SecretGoogleCredentials},${SecretGoogleAdminEmail},${SecretSCIMEndpoint},${SecretSCIMAccessToken},${SecretRegion},${SecretIdentityStoreID} + Export: + Name: AppConfig + + AppConfigRemote: + Condition: CreateKey + Description: "The Comma Separated list of Secrets and KMS Key ARNs to copy and paste into the CrossStackConfig field of the app for cross-account stack." + Value: !Sub ${SecretGoogleCredentials},${SecretGoogleAdminEmail},${SecretSCIMEndpoint},${SecretSCIMAccessToken},${SecretRegion},${SecretIdentityStoreID},arn:aws:kms:${AWS::Region}:${AWS::AccountId}:key/${KeyForSecrets} + Export: + Name: AppConfig From eafae9a1a344fae2f325972ea8566e554d4ba048 Mon Sep 17 00:00:00 2001 From: AgnesG Date: Wed, 7 Feb 2024 16:02:37 +0000 Subject: [PATCH 3/5] Add a log statement for exluded AWS group --- internal/fac/extensions.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/internal/fac/extensions.go b/internal/fac/extensions.go index 23849df1..10626530 100644 --- a/internal/fac/extensions.go +++ b/internal/fac/extensions.go @@ -7,6 +7,7 @@ import ( "regexp" "github.com/awslabs/ssosync/internal/aws" + log "github.com/sirupsen/logrus" ) // ErrNoAWSGroups indicates no AWS groups were received. @@ -25,7 +26,7 @@ func (e ErrorBadRegex) Error() string { // MatchAWSGroups will filter out the AWS groups that don't match the regex. // Returns an error on failure, a list of AWS groups that match on success. func MatchAWSGroups(awsGroups []*aws.Group, matchRegex string) ([]*aws.Group, error) { - if awsGroups == nil || len(awsGroups) == 0 { + if len(awsGroups) == 0 { return nil, ErrNoAWSGroups } @@ -36,9 +37,12 @@ func MatchAWSGroups(awsGroups []*aws.Group, matchRegex string) ([]*aws.Group, er matchedGroups := make([]*aws.Group, 0) for _, group := range awsGroups { - if awsGroupRegex.FindStringIndex(group.DisplayName) != nil { - matchedGroups = append(matchedGroups, group) + if awsGroupRegex.FindStringIndex(group.DisplayName) == nil { + log.Infof("AWS group %s will not be included in sync", group.DisplayName) + continue } + + matchedGroups = append(matchedGroups, group) } return matchedGroups, nil From fdf45108682b61bf2324730aad3a13891339df79 Mon Sep 17 00:00:00 2001 From: AgnesG Date: Thu, 8 Feb 2024 11:21:25 +0000 Subject: [PATCH 4/5] Add flag validation to prevent lambda execution with incorrect settings --- .github/workflows/release.yml | 40 +++++++++------------------------ cmd/root.go | 11 +++++++++ internal/fac/extensions.go | 14 ++++++------ internal/fac/extensions_test.go | 6 ++--- internal/sync.go | 2 ++ 5 files changed, 34 insertions(+), 39 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d16140be..ec97d4e7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,48 +4,30 @@ name: release on: push: tags: - - '*' + - 'v[0-9]+.[0-9]+.[0-9]+*' -jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Check out code into the Go module directory - uses: actions/checkout@v3 - - - name: Setup go - uses: actions/setup-go@v4 - with: - go-version: '1.20.x' - - - name: Install staticcheck - run: go install honnef.co/go/tools/cmd/staticcheck@latest - - - name: Run staticcheck - run: staticcheck ./... - - - name: Run Tests - run: go test -p 1 -cover -race -v ./... +permissions: + contents: write +jobs: release: runs-on: ubuntu-latest - needs: [ test ] steps: - name: Checkout - uses: actions/checkout@v3 - - - name: Unshallow - run: git fetch --prune --unshallow + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true - name: Set up Go uses: actions/setup-go@v4 with: - go-version: '1.20.x' + go-version: '1.21.x' - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v4 + uses: goreleaser/goreleaser-action@v5 with: version: latest - args: release --rm-dist + args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/cmd/root.go b/cmd/root.go index 8139d360..c63b2874 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -19,6 +19,7 @@ import ( "context" "fmt" "os" + "regexp" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" @@ -50,6 +51,16 @@ var rootCmd = &cobra.Command{ Long: `A command line tool to enable you to synchronise your Google Apps (Google Workspace) users to AWS Single Sign-on (AWS SSO) Complete documentation is available at https://github.com/awslabs/ssosync`, + PreRun: func(cmd *cobra.Command, args []string) { + awsGroupMatch, flagErr := cmd.Flags().GetString("aws-group-match") + if flagErr != nil { + log.Fatal("flag `aws-group-match` does not exist", flagErr) + } + _, compileErr := regexp.Compile(awsGroupMatch) + if compileErr != nil { + log.Fatalf("invalid aws-group-match flag value %s", awsGroupMatch, compileErr) + } + }, RunE: func(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithCancel(context.Background()) defer cancel() diff --git a/internal/fac/extensions.go b/internal/fac/extensions.go index 10626530..cf245916 100644 --- a/internal/fac/extensions.go +++ b/internal/fac/extensions.go @@ -10,16 +10,16 @@ import ( log "github.com/sirupsen/logrus" ) -// ErrNoAWSGroups indicates no AWS groups were received. -var ErrNoAWSGroups = errors.New("received no AWS groups") +// NoAWSGroupsErr indicates no AWS groups were received. +var NoAWSGroupsErr = errors.New("received no AWS groups") -// ErrorBadRegex represents a regex compilation error. -type ErrorBadRegex struct { +// BadRegexError represents a regex compilation error. +type BadRegexError struct { Message string Err error } -func (e ErrorBadRegex) Error() string { +func (e BadRegexError) Error() string { return e.Message } @@ -27,12 +27,12 @@ func (e ErrorBadRegex) Error() string { // Returns an error on failure, a list of AWS groups that match on success. func MatchAWSGroups(awsGroups []*aws.Group, matchRegex string) ([]*aws.Group, error) { if len(awsGroups) == 0 { - return nil, ErrNoAWSGroups + return nil, NoAWSGroupsErr } awsGroupRegex, err := regexp.Compile(matchRegex) if err != nil { - return nil, ErrorBadRegex{Message: fmt.Sprintf("can't compile regex %s", matchRegex), Err: err} + return nil, BadRegexError{Message: fmt.Sprintf("can't compile regex %s", matchRegex), Err: err} } matchedGroups := make([]*aws.Group, 0) diff --git a/internal/fac/extensions_test.go b/internal/fac/extensions_test.go index 16f5ab24..29d17057 100644 --- a/internal/fac/extensions_test.go +++ b/internal/fac/extensions_test.go @@ -54,19 +54,19 @@ func TestMatchAWSGroups(t *testing.T) { name: "returns an error when input groups empty", awsGroupMatch: "aws-group-*", inputGroups: []*aws.Group{}, - expectedErr: ErrNoAWSGroups, + expectedErr: NoAWSGroupsErr, }, { name: "returns an error when input groups nil", awsGroupMatch: "aws-group-*", inputGroups: []*aws.Group{}, - expectedErr: ErrNoAWSGroups, + expectedErr: NoAWSGroupsErr, }, { name: "returns an error when regex invalid", awsGroupMatch: "[^0-1", inputGroups: []*aws.Group{{DisplayName: "aws-group-A"}}, - expectedErr: ErrorBadRegex{ + expectedErr: BadRegexError{ Message: "can't compile regex [^0-1", Err: &syntax.Error{Code: syntax.ErrMissingBracket, Expr: "[^0-1"}, }, diff --git a/internal/sync.go b/internal/sync.go index 23a6a22f..f2218126 100644 --- a/internal/sync.go +++ b/internal/sync.go @@ -325,6 +325,8 @@ func (s *syncGSuite) SyncGroupsUsers(query, awsGroupMatch string) error { onlyAWSGroupsFromGoogle, matchErr := fac.MatchAWSGroups(awsGroups, awsGroupMatch) if err != nil { log.Errorf("error filtering AWS groups by %s", matchErr) + // Will continue with the full group which will delete the non Google groups. + // This flow is prevented by adding pre-run flag validation. } else { awsGroups = onlyAWSGroupsFromGoogle } From 0a5f9cb6243a20b190bb038c362fe3bb80dc2356 Mon Sep 17 00:00:00 2001 From: AgnesG Date: Fri, 9 Feb 2024 15:27:20 +0000 Subject: [PATCH 5/5] Google groups to be consistently referenced by Email --- cmd/root.go | 2 +- internal/fac/extensions_test.go | 4 ++-- internal/sync.go | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index c63b2874..338fdd4c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -58,7 +58,7 @@ Complete documentation is available at https://github.com/awslabs/ssosync`, } _, compileErr := regexp.Compile(awsGroupMatch) if compileErr != nil { - log.Fatalf("invalid aws-group-match flag value %s", awsGroupMatch, compileErr) + log.Fatalf("invalid aws-group-match flag value %s; %v", awsGroupMatch, compileErr) } }, RunE: func(cmd *cobra.Command, args []string) error { diff --git a/internal/fac/extensions_test.go b/internal/fac/extensions_test.go index 29d17057..f61f15f5 100644 --- a/internal/fac/extensions_test.go +++ b/internal/fac/extensions_test.go @@ -36,7 +36,7 @@ func TestMatchAWSGroups(t *testing.T) { }, { name: "correctly matches selected groups", - awsGroupMatch: "aws-group-*", + awsGroupMatch: "aws-group-.*", inputGroups: []*aws.Group{ {DisplayName: "aws-group-A"}, {DisplayName: "aws-group-B"}, @@ -52,7 +52,7 @@ func TestMatchAWSGroups(t *testing.T) { }, { name: "returns an error when input groups empty", - awsGroupMatch: "aws-group-*", + awsGroupMatch: "aws-group-.*", inputGroups: []*aws.Group{}, expectedErr: NoAWSGroupsErr, }, diff --git a/internal/sync.go b/internal/sync.go index f2218126..a44e70e9 100644 --- a/internal/sync.go +++ b/internal/sync.go @@ -324,7 +324,7 @@ func (s *syncGSuite) SyncGroupsUsers(query, awsGroupMatch string) error { log.Info("prune AWS groups that don't originate from Google") onlyAWSGroupsFromGoogle, matchErr := fac.MatchAWSGroups(awsGroups, awsGroupMatch) if err != nil { - log.Errorf("error filtering AWS groups by %s", matchErr) + log.Errorf("error filtering AWS groups by %s; %v", awsGroupMatch, matchErr) // Will continue with the full group which will delete the non Google groups. // This flow is prevented by adding pre-run flag validation. } else { @@ -554,7 +554,7 @@ func (s *syncGSuite) getGoogleGroupsAndUsers(googleGroups []*admin.Group) ([]*ad for _, g := range googleGroups { - log := log.WithFields(log.Fields{"group": g.Name}) + log := log.WithFields(log.Fields{"group": g.Email}) if s.ignoreGroup(g.Email) { log.Debug("ignoring group") @@ -596,7 +596,7 @@ func (s *syncGSuite) getGoogleGroupsAndUsers(googleGroups []*admin.Group) ([]*ad gUniqUsers[m.Email] = u[0] } } - gGroupsUsers[g.Name] = membersUsers + gGroupsUsers[g.Email] = membersUsers } for _, user := range gUniqUsers {