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 a2911353..338fdd4c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -19,15 +19,16 @@ import ( "context" "fmt" "os" + "regexp" - "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" @@ -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; %v", awsGroupMatch, compileErr) + } + }, RunE: func(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -66,9 +77,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 +88,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() { @@ -167,6 +178,7 @@ func initConfig() { "include_groups", "user_match", "group_match", + "aws_group_match", "sync_method", "region", "identity_store_id", @@ -246,6 +258,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 820f3a27..22bee392 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -17,13 +17,17 @@ 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 ... SCIMAccessToken string `mapstructure:"scim_access_token"` // IsLambda ... IsLambda bool - // IsLambdaRunningInCodePipeline ... + // IsLambdaRunningInCodePipeline ... IsLambdaRunningInCodePipeline bool // Ignore users ... IgnoreUsers []string `mapstructure:"ignore_users"` @@ -50,6 +54,8 @@ const ( DefaultGoogleCredentials = "credentials.json" // DefaultSyncMethod is the default sync method to use. DefaultSyncMethod = "groups" + // DefaultAwsGroupMatch is the default regex for AWS group name to be matched. + DefaultAwsGroupMatch = ".*" ) // New returns a new Config @@ -59,6 +65,7 @@ func New() *Config { LogLevel: DefaultLogLevel, LogFormat: DefaultLogFormat, SyncMethod: DefaultSyncMethod, + AwsGroupMatch: DefaultAwsGroupMatch, GoogleCredentials: DefaultGoogleCredentials, } } diff --git a/internal/fac/extensions.go b/internal/fac/extensions.go new file mode 100644 index 00000000..cf245916 --- /dev/null +++ b/internal/fac/extensions.go @@ -0,0 +1,49 @@ +// Package fac contains custom functions for additional operations on data. +package fac + +import ( + "errors" + "fmt" + "regexp" + + "github.com/awslabs/ssosync/internal/aws" + log "github.com/sirupsen/logrus" +) + +// NoAWSGroupsErr indicates no AWS groups were received. +var NoAWSGroupsErr = errors.New("received no AWS groups") + +// BadRegexError represents a regex compilation error. +type BadRegexError struct { + Message string + Err error +} + +func (e BadRegexError) 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 len(awsGroups) == 0 { + return nil, NoAWSGroupsErr + } + + awsGroupRegex, err := regexp.Compile(matchRegex) + if err != nil { + return nil, BadRegexError{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 { + log.Infof("AWS group %s will not be included in sync", group.DisplayName) + continue + } + + 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..f61f15f5 --- /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: NoAWSGroupsErr, + }, + { + name: "returns an error when input groups nil", + awsGroupMatch: "aws-group-*", + inputGroups: []*aws.Group{}, + expectedErr: NoAWSGroupsErr, + }, + { + name: "returns an error when regex invalid", + awsGroupMatch: "[^0-1", + inputGroups: []*aws.Group{{DisplayName: "aws-group-A"}}, + expectedErr: BadRegexError{ + 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 22f0330d..a44e70e9 100644 --- a/internal/sync.go +++ b/internal/sync.go @@ -23,6 +23,7 @@ import ( "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" @@ -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 @@ -269,7 +270,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: @@ -289,9 +291,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 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 @@ -319,6 +321,16 @@ func (s *syncGSuite) SyncGroupsUsers(query string) error { 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; %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 { + awsGroups = onlyAWSGroupsFromGoogle + } + log.Info("get existing aws users") awsUsers, err := s.GetUsers() if err != nil { @@ -542,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") @@ -584,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 { @@ -757,7 +769,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 } diff --git a/template.yaml b/template.yaml index 6837017f..4daf39b0 100644 --- a/template.yaml +++ b/template.yaml @@ -32,6 +32,10 @@ Metadata: 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: @@ -186,6 +190,12 @@ Parameters: 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: | @@ -552,6 +562,7 @@ Resources: 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: !If [SetIgnoreGroups, !Ref IgnoreGroups, !Ref AWS::NoValue] SSOSYNC_IGNORE_USERS: !If [SetIgnoreUsers, !Ref IgnoreUsers, !Ref AWS::NoValue]