From 33cf47db036d12089fceebc61f7dab7fa454ad5f Mon Sep 17 00:00:00 2001 From: Chris Pates Date: Tue, 20 Jan 2026 15:26:39 +0000 Subject: [PATCH 01/27] update regex tennant id has changed length --- template.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template.yaml b/template.yaml index b6f007e7..1ad3044e 100644 --- a/template.yaml +++ b/template.yaml @@ -184,7 +184,7 @@ Parameters: 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-Za-z0-9]{11})-([A-Za-z0-9]{4})-([A-Za-z0-9]{4})-([A-Za-z0-9]{4})-([A-Za-z0-9]{12})/scim/v2/?)' + AllowedPattern: '(?!.*\s)|(https://scim.(us(-gov)?|ap|ca|cn|eu|il|me|mx|sa)-(central|(north|south)?(east|west)?)-([0-9]{1}).amazonaws.com/([A-Za-z0-9]{8,11})-([A-Za-z0-9]{4})-([A-Za-z0-9]{4})-([A-Za-z0-9]{4})-([A-Za-z0-9]{12})/scim/v2/?)' SCIMEndpointAccessToken: Type: String From fb55723333857784f27bbfedda184c41e6799bc1 Mon Sep 17 00:00:00 2001 From: Chris Pates Date: Tue, 10 Mar 2026 13:16:21 +0000 Subject: [PATCH 02/27] Update runtime to provided.al2023 --- template.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template.yaml b/template.yaml index 1ad3044e..5a6aa159 100644 --- a/template.yaml +++ b/template.yaml @@ -597,7 +597,7 @@ Resources: FunctionName: !If [SetFunctionName, !Ref FunctionName, !Ref AWS::NoValue] Description: "An instance of ssosync deplyed from the Serverless Application Repository, for details see http://https://github.com/awslabs/ssosync" Role: !If [RemoteSecrets, !GetAtt SSOSyncRoleRemote.Arn, !GetAtt SSOSyncRoleLocal.Arn] - Runtime: provided.al2 + Runtime: provided.al2023 Handler: bootstrap Architectures: - arm64 From f473ed7254678d35be4cd6d8070cb3ca0fb92cdf Mon Sep 17 00:00:00 2001 From: Chris Pates Date: Tue, 10 Mar 2026 17:47:42 +0000 Subject: [PATCH 03/27] 299 scheduleexpression allowedpattern rejects valid singular rate expressions (#303) ssue #, if available: #299 Description of changes: Refines the regex for rate: rate\((?:(?:\d{2,}+|[2-9]) (?:minutes|hours|days))|(?:1 (?:minute|hour|day))\) Allows for: rate(9 hours) rate(17 hours) rate(57 hours) rate(123 days) rate(1 hour) but rejects: rate(2 hour) rate(20 hour) rate(1 hours) By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. --- template.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template.yaml b/template.yaml index 5a6aa159..ffa095e9 100644 --- a/template.yaml +++ b/template.yaml @@ -96,7 +96,7 @@ Parameters: 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))*|\*)\))' + AllowedPattern: '(?!.*\s)|rate\((?:(?:\d{2,}+|[2-9]) (?:minutes|hours|days))|(?:1 (?:minute|hour|day))\)|(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 From a83442284e4c8f75173ff9ce4305fdd0385046a5 Mon Sep 17 00:00:00 2001 From: 44smkn Date: Mon, 9 Feb 2026 22:49:16 +0900 Subject: [PATCH 04/27] fix: Use `config.DefaultSyncMethod` as the default for `cfg.SyncMethod` --- cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index 037c2fca..03374b83 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -259,7 +259,7 @@ func configLambda() { // Handle environment variables for other settings cfg.LogLevel = getEnvStr("LOG_LEVEL", config.DefaultLogLevel) cfg.LogFormat = getEnvStr("LOG_FORMAT", config.DefaultLogFormat) - cfg.SyncMethod = getEnvStr("SYNC_METHOD", config.DefaultLogFormat) + cfg.SyncMethod = getEnvStr("SYNC_METHOD", config.DefaultSyncMethod) cfg.UserMatch = getEnvStr("USER_MATCH", "") cfg.GroupMatch = getEnvStr("GROUP_MATCH", "*") cfg.IgnoreGroups = getEnvStrs("IGNORE_GROUPS", []string{}) From b28cac2e4aaa9a0d08760e5e1a8c6fdb56f1ed81 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 02:26:07 +0000 Subject: [PATCH 05/27] Bump golang.org/x/crypto from 0.40.0 to 0.45.0 Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.40.0 to 0.45.0. - [Commits](https://github.com/golang/crypto/compare/v0.40.0...v0.45.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.45.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 0dc54754..d91374db 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/awslabs/ssosync -go 1.24 +go 1.24.0 require ( github.com/BurntSushi/toml v1.5.0 @@ -63,10 +63,10 @@ require ( go.opentelemetry.io/otel v1.37.0 // indirect go.opentelemetry.io/otel/metric v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect - golang.org/x/crypto v0.40.0 // indirect - golang.org/x/net v0.42.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.27.0 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect google.golang.org/grpc v1.74.2 // indirect google.golang.org/protobuf v1.36.7 // indirect diff --git a/go.sum b/go.sum index cf8bb08b..aa198d2d 100644 --- a/go.sum +++ b/go.sum @@ -144,33 +144,33 @@ go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mx go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= -golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= From 1b604388ee1ca668a001f8b90ab1009ed377564a Mon Sep 17 00:00:00 2001 From: Chris Pates Date: Wed, 11 Mar 2026 18:07:32 +0000 Subject: [PATCH 06/27] Delete users if inactive only (#304) * Feat: Delete users if inactive only * Update ScheduleExpression regex --------- Co-authored-by: Luis Moreira --- internal/sync.go | 30 +++++++++++++++++++++++++++++- template.yaml | 2 +- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/internal/sync.go b/internal/sync.go index c5d0583e..1e91f69e 100644 --- a/internal/sync.go +++ b/internal/sync.go @@ -90,7 +90,35 @@ func (s *syncGSuite) SyncUsers(query string) error { return err } - for _, u := range deletedUsers { + activeGoogleUsers, err := s.google.GetUsers(query, s.cfg.UserFilter) + if err != nil { + return err + } + + var newDeletedUsers = make([]*admin.User, 0) + for _, deleteUser := range deletedUsers { + isDeletedUserActive := false + log.WithFields(log.Fields{ + "email": deleteUser.PrimaryEmail, + }).Debug("Inspecting deleted user email") + for _, activeGoogleUser := range activeGoogleUsers { + if deleteUser.PrimaryEmail == activeGoogleUser.PrimaryEmail { + isDeletedUserActive = true + log.WithFields(log.Fields{ + "email": deleteUser.PrimaryEmail, + }).Debug("User is active again! Breaking loop...") + break + } + } + if !isDeletedUserActive { + log.WithFields(log.Fields{ + "email": deleteUser.PrimaryEmail, + }).Debug("Inactive user email") + newDeletedUsers = append(newDeletedUsers, deleteUser) + } + } + + for _, u := range newDeletedUsers { log.WithFields(log.Fields{ "email": u.PrimaryEmail, }).Info("deleting google user") diff --git a/template.yaml b/template.yaml index ffa095e9..5e8e4da6 100644 --- a/template.yaml +++ b/template.yaml @@ -96,7 +96,7 @@ Parameters: 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{2,}+|[2-9]) (?:minutes|hours|days))|(?:1 (?:minute|hour|day))\)|(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))*|\*)\))' + AllowedPattern: '(?!.*\s)|rate\((?:(?:(?:\d{2,}+|[2-9]) (?:minutes|hours|days))|(?:1 (?:minute|hour|day)))\)|(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 From f899c6dc60721265f432b8c1d2f904bc79f6eb9a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 02:12:15 +0000 Subject: [PATCH 07/27] Bump google.golang.org/grpc from 1.74.2 to 1.79.3 Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.74.2 to 1.79.3. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.74.2...v1.79.3) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-version: 1.79.3 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- go.mod | 29 +++++++++++---------- go.sum | 80 ++++++++++++++++++++++++++++++---------------------------- 2 files changed, 57 insertions(+), 52 deletions(-) diff --git a/go.mod b/go.mod index d91374db..4de30b90 100644 --- a/go.mod +++ b/go.mod @@ -17,15 +17,15 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.1 - github.com/stretchr/testify v1.10.0 - golang.org/x/oauth2 v0.30.0 + github.com/stretchr/testify v1.11.1 + golang.org/x/oauth2 v0.34.0 google.golang.org/api v0.246.0 ) require ( cloud.google.com/go/auth v0.16.4 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - cloud.google.com/go/compute/metadata v0.8.0 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.18.3 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.2 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.3 // indirect @@ -37,6 +37,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.32.0 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.36.0 // indirect github.com/aws/smithy-go v1.22.5 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect @@ -58,17 +59,17 @@ require ( github.com/spf13/pflag v1.0.7 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect - go.opentelemetry.io/otel v1.37.0 // indirect - go.opentelemetry.io/otel/metric v1.37.0 // indirect - go.opentelemetry.io/otel/trace v1.37.0 // indirect - golang.org/x/crypto v0.45.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.31.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect - google.golang.org/grpc v1.74.2 // indirect - google.golang.org/protobuf v1.36.7 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/grpc v1.79.3 // indirect + google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index aa198d2d..b20ffc9b 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ cloud.google.com/go/auth v0.16.4 h1:fXOAIQmkApVvcIn7Pc2+5J8QTMVbUGLscnSVNl11su8= cloud.google.com/go/auth v0.16.4/go.mod h1:j10ncYwjX/g3cdX7GpEzsdM+d+ZNsXAbb6qXA7p1Y5M= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA= -cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/aws/aws-lambda-go v1.49.0 h1:z4VhTqkFZPM3xpEtTqWqRqsRH4TZBMJqTkRiBPYLqIQ= @@ -42,6 +42,8 @@ github.com/aws/aws-secretsmanager-caching-go/v2 v2.1.1 h1:L1s6e1t52MWZ2S+96AF7Si github.com/aws/aws-secretsmanager-caching-go/v2 v2.1.1/go.mod h1:tI92REdzBEWATJHIqIVBk/L/9l6XPGj0Xallezr2fPQ= github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw= github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -99,8 +101,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.10.0 h1:FM8Cv6j2KqIhM2ZK7HZjm4mpj9NBktLgowT1aN9q5Cc= github.com/sagikazarmark/locafero v0.10.0/go.mod h1:Ieo3EUsjifvQu4NZwV5sPd4dwvu0OCgEQV7vjc9yDjw= @@ -123,72 +125,74 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.246.0 h1:H0ODDs5PnMZVZAEtdLMn2Ul2eQi7QNjqM2DIFp8TlTM= google.golang.org/api v0.246.0/go.mod h1:dMVhVcylamkirHdzEBAIQWUCgqY885ivNeZYd7VAVr8= google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= -google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= -google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= -google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= -google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= -google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From 28f19a0ac18907e0deb5e4f34a2d9272fdfccfe5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 20:44:13 +0000 Subject: [PATCH 08/27] Bump go.opentelemetry.io/otel from 1.39.0 to 1.41.0 Bumps [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go) from 1.39.0 to 1.41.0. - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.39.0...v1.41.0) --- updated-dependencies: - dependency-name: go.opentelemetry.io/otel dependency-version: 1.41.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 4de30b90..c9c0081c 100644 --- a/go.mod +++ b/go.mod @@ -61,9 +61,9 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect - go.opentelemetry.io/otel v1.39.0 // indirect - go.opentelemetry.io/otel/metric v1.39.0 // indirect - go.opentelemetry.io/otel/trace v1.39.0 // indirect + go.opentelemetry.io/otel v1.41.0 // indirect + go.opentelemetry.io/otel/metric v1.41.0 // indirect + go.opentelemetry.io/otel/trace v1.41.0 // indirect golang.org/x/crypto v0.46.0 // indirect golang.org/x/net v0.48.0 // indirect golang.org/x/sys v0.39.0 // indirect diff --git a/go.sum b/go.sum index b20ffc9b..1e956c1b 100644 --- a/go.sum +++ b/go.sum @@ -134,16 +134,16 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= -go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= -go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= -go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= -go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= +go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= -go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= -go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= From 52f5f793501df2d71fe837d45f39e64ff7818f80 Mon Sep 17 00:00:00 2001 From: Chris Pates Date: Thu, 30 Apr 2026 14:20:31 +0100 Subject: [PATCH 09/27] 309 precache field regex does not all for disabled (#312) * Edit the regex for the CloudFormation template for the PRECACHE_ORG_UNITS, to allow for a single OU path other than '/' * Updated handling of this environment variable so, if specified but and empty string or not set it disables precaching. * Updated handling of other optional comma separated string env_variables to have the same way. --- cmd/root.go | 60 ++++---- internal/config/config.go | 4 +- internal/sync.go | 317 +++++++++++++++++++++----------------- template.yaml | 4 +- 4 files changed, 209 insertions(+), 176 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 03374b83..5c39d75c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -174,6 +174,7 @@ func initConfig() { "sync_method", "region", "identity_store_id", + "dry_run", } for _, e := range appEnvVars { @@ -193,11 +194,11 @@ func initConfig() { // config logger logConfig(cfg) - if cfg.SyncSuspended { - cfg.UserFilter = " isArchived=false" - } else { - cfg.UserFilter = " isSuspended=false isArchived=false" - } + if cfg.SyncSuspended { + cfg.UserFilter = " isArchived=false" + } else { + cfg.UserFilter = " isSuspended=false isArchived=false" + } } @@ -208,25 +209,28 @@ func getEnvStr(key string, fallback string) string { log.WithField(key, valueStr).Info("EnvVar") return valueStr } - return fallback + return fallback } -func getEnvStrs (key string, fallback []string) []string { - if valueStr, ok := os.LookupEnv(key); ok { - log.WithField(key, valueStr).Info("EnvVar") - return strings.Split(valueStr, ",") - } - return fallback +func getEnvStrs(key string, fallback []string) []string { + if valueStr, ok := os.LookupEnv(key); ok { + log.WithField(key, valueStr).Info("EnvVar") + if valueStr == "" { + return nil + } + return strings.Split(valueStr, ",") + } + return fallback } -func getEnvBool (key string, fallback bool) bool { - if valueStr, ok := os.LookupEnv(key); ok { +func getEnvBool(key string, fallback bool) bool { + if valueStr, ok := os.LookupEnv(key); ok { log.WithField(key, valueStr).Info("EnvVar") - valueBool := strings.ToLower(valueStr) == "true" + valueBool := strings.ToLower(valueStr) == "true" log.WithField(key, valueBool).Info("config") - return valueBool - } - return fallback + return valueBool + } + return fallback } func configLambda() { @@ -255,17 +259,17 @@ func configLambda() { cfg.Region = getSecretFromCache(getEnvStr("REGION", "")) cfg.GoogleCredentials = getSecretFromCache(getEnvStr("GOOGLE_CREDENTIALS", "")) cfg.SCIMAccessToken = getSecretFromCache(getEnvStr("SCIM_ACCESS_TOKEN", "")) - + // Handle environment variables for other settings cfg.LogLevel = getEnvStr("LOG_LEVEL", config.DefaultLogLevel) cfg.LogFormat = getEnvStr("LOG_FORMAT", config.DefaultLogFormat) cfg.SyncMethod = getEnvStr("SYNC_METHOD", config.DefaultSyncMethod) cfg.UserMatch = getEnvStr("USER_MATCH", "") cfg.GroupMatch = getEnvStr("GROUP_MATCH", "*") - cfg.IgnoreGroups = getEnvStrs("IGNORE_GROUPS", []string{}) - cfg.IgnoreUsers = getEnvStrs("IGNORE_USERS", []string{}) - cfg.IncludeGroups = getEnvStrs("INCLUDE_GROUPS", []string{}) - cfg.PrecacheOrgUnits = getEnvStrs("PRECACHE_ORG_UNITS", strings.Split(config.DefaultPrecacheOrgUnits, ",")) + cfg.IgnoreGroups = getEnvStrs("IGNORE_GROUPS", nil) + cfg.IgnoreUsers = getEnvStrs("IGNORE_USERS", nil) + cfg.IncludeGroups = getEnvStrs("INCLUDE_GROUPS", nil) + cfg.PrecacheOrgUnits = getEnvStrs("PRECACHE_ORG_UNITS", nil) cfg.DryRun = getEnvBool("DRY_RUN", false) cfg.SyncSuspended = getEnvBool("SYNC_SUSPENDED", false) @@ -285,20 +289,20 @@ func addFlags(_ *cobra.Command, cfg *config.Config) { rootCmd.PersistentFlags().StringVarP(&cfg.LogFormat, "log-format", "", config.DefaultLogFormat, "log format") rootCmd.PersistentFlags().StringVarP(&cfg.LogLevel, "log-level", "", config.DefaultLogLevel, "log level") rootCmd.PersistentFlags().BoolVarP(&cfg.DryRun, "dry-run", "n", false, "Do *not* perform any actions, instead list what would happen") - rootCmd.PersistentFlags().BoolVarP(&cfg.SyncSuspended, "suspended", "", false, "included suspended users and their group memberships when syncing") + rootCmd.PersistentFlags().BoolVarP(&cfg.SyncSuspended, "suspended", "", false, "included suspended users and their group memberships when syncing") rootCmd.Flags().StringVarP(&cfg.SCIMAccessToken, "access-token", "t", "", "AWS SSO SCIM API Access Token") rootCmd.Flags().StringVarP(&cfg.SCIMEndpoint, "endpoint", "e", "", "AWS SSO SCIM API Endpoint") rootCmd.Flags().StringVarP(&cfg.GoogleCredentials, "google-credentials", "c", config.DefaultGoogleCredentials, "path to Google Workspace credentials file") rootCmd.Flags().StringVarP(&cfg.GoogleAdmin, "google-admin", "u", "", "Google Workspace admin user email") - rootCmd.Flags().StringSliceVar(&cfg.IgnoreUsers, "ignore-users", []string{}, "ignores these Google Workspace users") - rootCmd.Flags().StringSliceVar(&cfg.IgnoreGroups, "ignore-groups", []string{}, "ignores these Google Workspace groups") - rootCmd.Flags().StringSliceVar(&cfg.IncludeGroups, "include-groups", []string{}, "include only these Google Workspace groups, NOTE: only works when --sync-method 'users_groups'") + rootCmd.Flags().StringSliceVar(&cfg.IgnoreUsers, "ignore-users", nil, "ignores these Google Workspace users") + rootCmd.Flags().StringSliceVar(&cfg.IgnoreGroups, "ignore-groups", nil, "ignores these Google Workspace groups") + rootCmd.Flags().StringSliceVar(&cfg.IncludeGroups, "include-groups", nil, "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*' 'name=John Doe,email:admin*', to sync all users in the directory specify '*'. For query syntax and more examples 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*' 'name=AWS-Admins,email:aws*', to sync all groups (and their member users) specify '*'. For query syntax and more examples see: https://developers.google.com/admin-sdk/directory/v1/guides/search-groups") 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") - rootCmd.Flags().StringSliceVar(&cfg.PrecacheOrgUnits, "precache-ous", strings.Split(config.DefaultPrecacheOrgUnits, ","), "A common separated list of Google Workspace OrgUnitPathis e.g.'/', to precache all users within the organization or '/OU_1/OU 2,/OU3'. To disable and use caching on the fly, 'DISABLED'.") + rootCmd.Flags().StringSliceVar(&cfg.PrecacheOrgUnits, "precache-ous", nil, "A common separated list of Google Workspace OrgUnitPathis e.g.'/', to precache all users within the organization or '/OU_1/OU 2,/OU3'. Precaching is disabled by default.") } diff --git a/internal/config/config.go b/internal/config/config.go index 0f3cc01a..8bb0fcf4 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -60,8 +60,6 @@ const ( DefaultGoogleCredentials = "credentials.json" // DefaultSyncMethod is the default sync method to use. DefaultSyncMethod = "groups" - // DefaultPrecacheOrgUnits - DefaultPrecacheOrgUnits = "/" ) // New returns a new Config @@ -101,5 +99,5 @@ func (c *Config) Validate() error { return errors.New("sync method must be either 'groups' or 'users_groups'") } - return nil + return nil } diff --git a/internal/sync.go b/internal/sync.go index 1e91f69e..c1ee2dd8 100644 --- a/internal/sync.go +++ b/internal/sync.go @@ -588,6 +588,7 @@ func (s *syncGSuite) getGoogleGroupsAndUsers(queryGroups string, queryUsers stri gUserDetailCache := make(map[string]*admin.User) gGroupDetailCache := make(map[string]*admin.Group) gUniqUsers := make(map[string]*admin.User) + gGroups := make([]*admin.Group, 0) log.WithFields(log.Fields{ "func": funcName, @@ -613,66 +614,77 @@ func (s *syncGSuite) getGoogleGroupsAndUsers(queryGroups string, queryUsers stri } // Fetch Users - log.WithFields(log.Fields{ - "func": funcName, - "queryUsers": queryUsers, - "queryFilters": s.cfg.UserFilter, - }).Info("fetching userMatch") - - googleUsers, err := s.google.GetUsers(queryUsers, s.cfg.UserFilter) - if err != nil { + if queryUsers == "" { log.WithFields(log.Fields{ - "func": funcName, - "error": err, - }).Error("failed fetching userMatch from Google") - return nil, nil, nil, err - } + "func": funcName, + }).Info("Skipping fetch for userMatch") + } else { + log.WithFields(log.Fields{ + "func": funcName, + "queryUsers": queryUsers, + "queryFilters": s.cfg.UserFilter, + }).Info("fetching userMatch") - log.WithFields(log.Fields{ - "func": funcName, - }).Debug("process users from google, filtering as required") + googleUsers, err := s.google.GetUsers(queryUsers, s.cfg.UserFilter) + if err != nil { + log.WithFields(log.Fields{ + "func": funcName, + "error": err, + }).Error("failed fetching userMatch from Google") + return nil, nil, nil, err + } - for _, u := range googleUsers { log.WithFields(log.Fields{ "func": funcName, - "user": u, - }).Debug("process user") + }).Debug("process users from google, filtering as required") - // Remove any users that should be ignored - if s.ignoreUser(u.PrimaryEmail) { + for _, u := range googleUsers { log.WithFields(log.Fields{ - "func": funcName, - "user.Id": u.Id, - }).Info("ignoring user") - continue - } + "func": funcName, + "user": u, + }).Debug("process user") - if _, found := gUniqUsers[u.PrimaryEmail]; !found { - log.WithFields(log.Fields{ - "func": funcName, - "user.Id": u.Id, - }).Debug("adding user") - gUserDetailCache[u.PrimaryEmail] = u - gUniqUsers[u.PrimaryEmail] = gUserDetailCache[u.PrimaryEmail] - continue - } else { - log.WithFields(log.Fields{ - "func": funcName, - "user.Id": u.Id, - }).Debug("already existing") - continue + // Remove any users that should be ignored + if s.ignoreUser(u.PrimaryEmail) { + log.WithFields(log.Fields{ + "func": funcName, + "user.Id": u.Id, + }).Info("ignoring user") + continue + } + + if _, found := gUniqUsers[u.PrimaryEmail]; !found { + log.WithFields(log.Fields{ + "func": funcName, + "user.Id": u.Id, + }).Debug("adding user") + gUserDetailCache[u.PrimaryEmail] = u + gUniqUsers[u.PrimaryEmail] = gUserDetailCache[u.PrimaryEmail] + continue + } else { + log.WithFields(log.Fields{ + "func": funcName, + "user.Id": u.Id, + }).Debug("already existing") + continue + } } } // For larger directories this will reduce execution time and avoid throttling limits - // however if you have directory with 10s of 1000s of users you may want to down scope + // however if you have directory with 10,000+ users you may want to down scope // this to a specific OU path or disable by leaving empty. - if s.cfg.PrecacheOrgUnits[0] != "DISABLED" { + if s.cfg.PrecacheOrgUnits == nil { + log.WithFields(log.Fields{ + "func": funcName, + "OrgUnitPaths": s.cfg.PrecacheOrgUnits, + }).Info("Precaching DISABLED, caching on the fly") + } else { precacheQueries := "" log.WithFields(log.Fields{ "func": funcName, "OrgUnitPaths": s.cfg.PrecacheOrgUnits, - }).Info("Prechache users from these paths") + }).Info("Precache users from these paths") for _, orgUnitPath := range s.cfg.PrecacheOrgUnits { log.WithFields(log.Fields{ @@ -696,7 +708,7 @@ func (s *syncGSuite) getGoogleGroupsAndUsers(queryGroups string, queryUsers stri "queryFilters": s.cfg.UserFilter, }).Info("Precaching users") - googleUsers, err = s.google.GetUsers(precacheQueries, s.cfg.UserFilter) + googleUsers, err := s.google.GetUsers(precacheQueries, s.cfg.UserFilter) if err != nil { log.WithFields(log.Fields{ "func": funcName, @@ -732,126 +744,129 @@ func (s *syncGSuite) getGoogleGroupsAndUsers(queryGroups string, queryUsers stri } } } - } else { - log.WithFields(log.Fields{ - "func": funcName, - }).Info("Precaching DISABLED, caching on the fly") - } - - log.WithFields(log.Fields{ - "func": funcName, - "queryGroups": queryGroups, - }).Info("fetching groups") - - gGroups, err := s.google.GetGroups(queryGroups) - if err != nil { - log.WithFields(log.Fields{ - "func": funcName, - "error": err, - }).Error("failed fetching groups") - return nil, nil, nil, err } - filteredGoogleGroups := []*admin.Group{} - log.WithFields(log.Fields{ - "func": funcName, - }).Info("filter groups by ignoreList") - - for _, g := range gGroups { + // Fetch Users + if queryGroups == "" { log.WithFields(log.Fields{ - "func": funcName, - "group": g, - }).Debug("processing group") - - if s.ignoreGroup(g.Email) { - log.WithFields(log.Fields{ - "func": funcName, - "group.Id": g.Id, - }).Info("ignoring group") - continue - } - filteredGoogleGroups = append(filteredGoogleGroups, g) - } - gGroups = filteredGoogleGroups - - log.WithField("func", funcName).Info("fetch group memberships") - for _, g := range gGroups { + "func": funcName, + }).Info("Skipping fetch for groupMatch") + } else { log.WithFields(log.Fields{ - "func": funcName, - "group": g, - }).Debug("processing group") + "func": funcName, + "queryGroups": queryGroups, + }).Info("fetching groups") - if s.ignoreGroup(g.Email) { - log.WithFields(log.Fields{ - "func": funcName, - "group.Id": g.Id, - }).Info("skipping group") - continue - } - - log.WithField("func", funcName).Debug("fetch membership") - membersUsers, err := s.getGoogleUsersInGroup(g, gUserDetailCache, gGroupDetailCache) + gGroups, err = s.google.GetGroups(queryGroups) if err != nil { + log.WithFields(log.Fields{ + "func": funcName, + "error": err, + }).Error("failed fetching groups") return nil, nil, nil, err } - // If we've not seen the user email address before add it to the list of unique users - // also, we need to deduplicate the list of members. + filteredGoogleGroups := []*admin.Group{} log.WithFields(log.Fields{ - "func": funcName, - "group.Id": g.Id, - "membersUsers": membersUsers, - }).Debug("Process group membership") + "func": funcName, + }).Info("filter groups by ignoreList") - gUniqMembers := make(map[string]*admin.User) - for _, m := range membersUsers { + for _, g := range gGroups { log.WithFields(log.Fields{ - "func": funcName, - "group.Id": g.Id, - "member": m, - }).Debug("Processing member") + "func": funcName, + "group": g, + }).Debug("processing group") - if m == nil { + if s.ignoreGroup(g.Email) { log.WithFields(log.Fields{ - "func": funcName, - "group.Id": g.Id, - "member.Id": m.Id, - }).Error("nil user") + "func": funcName, + "group.Id": g.Id, + }).Info("ignoring group") continue } - if _, found := gUniqUsers[m.PrimaryEmail]; !found { + filteredGoogleGroups = append(filteredGoogleGroups, g) + } + gGroups = filteredGoogleGroups + + log.WithField("func", funcName).Info("fetch group memberships") + for _, g := range gGroups { + log.WithFields(log.Fields{ + "func": funcName, + "group": g, + }).Debug("processing group") + + if s.ignoreGroup(g.Email) { log.WithFields(log.Fields{ "func": funcName, "group.Id": g.Id, - "member": m.Id, - }).Debug("adding user to UniqueUsers") - gUniqUsers[m.PrimaryEmail] = gUserDetailCache[m.PrimaryEmail] + }).Info("skipping group") + continue } - if _, found := gUniqMembers[m.PrimaryEmail]; !found { + log.WithField("func", funcName).Debug("fetch membership") + membersUsers, err := s.getGoogleUsersInGroup(g, gUserDetailCache, gGroupDetailCache) + if err != nil { + return nil, nil, nil, err + } + + // If we've not seen the user email address before add it to the list of unique users + // also, we need to deduplicate the list of members. + log.WithFields(log.Fields{ + "func": funcName, + "group.Id": g.Id, + "membersUsers": membersUsers, + }).Debug("Process group membership") + + gUniqMembers := make(map[string]*admin.User) + for _, m := range membersUsers { log.WithFields(log.Fields{ "func": funcName, "group.Id": g.Id, - "member": m.Id, - }).Debug("adding user to group") - gUniqMembers[m.PrimaryEmail] = gUserDetailCache[m.PrimaryEmail] + "member": m, + }).Debug("Processing member") + + if m == nil { + log.WithFields(log.Fields{ + "func": funcName, + "group.Id": g.Id, + "member.Id": m.Id, + }).Error("nil user") + continue + } + if _, found := gUniqUsers[m.PrimaryEmail]; !found { + log.WithFields(log.Fields{ + "func": funcName, + "group.Id": g.Id, + "member": m.Id, + }).Debug("adding user to UniqueUsers") + gUniqUsers[m.PrimaryEmail] = gUserDetailCache[m.PrimaryEmail] + } + + if _, found := gUniqMembers[m.PrimaryEmail]; !found { + log.WithFields(log.Fields{ + "func": funcName, + "group.Id": g.Id, + "member": m.Id, + }).Debug("adding user to group") + gUniqMembers[m.PrimaryEmail] = gUserDetailCache[m.PrimaryEmail] + } } - } - log.WithFields(log.Fields{ - "func": funcName, - "group.Id": g.Id, - }).Debug("create gMembers") - gMembers := make([]*admin.User, 0) - for _, member := range gUniqMembers { - gMembers = append(gMembers, member) + log.WithFields(log.Fields{ + "func": funcName, + "group.Id": g.Id, + }).Debug("create gMembers") + gMembers := make([]*admin.User, 0) + for _, member := range gUniqMembers { + gMembers = append(gMembers, member) + } + log.WithFields(log.Fields{ + "func": funcName, + "group.Id": g.Id, + "gMembers": gMembers, + }).Debug("Finished group membership") + gGroupsUsers[g.Name] = gMembers } - log.WithFields(log.Fields{ - "func": funcName, - "group.Id": g.Id, - "gMembers": gMembers, - }).Debug("Finished group membership") - gGroupsUsers[g.Name] = gMembers } log.WithField("func", funcName).Debug("create gUsers") @@ -1088,14 +1103,18 @@ func DoSync(ctx context.Context, cfg *config.Config) error { return err } } else { - err = c.SyncUsers(cfg.UserMatch) - if err != nil { - return err + if cfg.UserMatch != "" { + err = c.SyncUsers(cfg.UserMatch) + if err != nil { + return err + } } - err = c.SyncGroups(cfg.GroupMatch) - if err != nil { - return err + if cfg.GroupMatch != "" { + err = c.SyncGroups(cfg.GroupMatch) + if err != nil { + return err + } } } @@ -1103,6 +1122,10 @@ func DoSync(ctx context.Context, cfg *config.Config) error { } func (s *syncGSuite) ignoreUser(name string) bool { + if s.cfg.IgnoreUsers == nil { + return false + } + if s.ignoreUsersSet == nil { s.ignoreUsersSet = make(map[string]struct{}, len(s.cfg.IgnoreUsers)) for _, u := range s.cfg.IgnoreUsers { @@ -1114,6 +1137,10 @@ func (s *syncGSuite) ignoreUser(name string) bool { } func (s *syncGSuite) ignoreGroup(name string) bool { + if s.cfg.IgnoreGroups == nil { + return false + } + if s.ignoreGroupsSet == nil { s.ignoreGroupsSet = make(map[string]struct{}, len(s.cfg.IgnoreGroups)) for _, g := range s.cfg.IgnoreGroups { @@ -1125,6 +1152,10 @@ func (s *syncGSuite) ignoreGroup(name string) bool { } func (s *syncGSuite) includeGroup(name string) bool { + if s.cfg.IncludeGroups == nil { + return false + } + if s.includeGroupsSet == nil { s.includeGroupsSet = make(map[string]struct{}, len(s.cfg.IncludeGroups)) for _, g := range s.cfg.IncludeGroups { diff --git a/template.yaml b/template.yaml index 5e8e4da6..c6e31f28 100644 --- a/template.yaml +++ b/template.yaml @@ -158,9 +158,9 @@ Parameters: PrecacheOrgUnits: Type: String Description: | - To reduce the volume of apis, ssosync precaches the users and groups from the Google directory. If you directory is large >10,000 users, you may wish to limit the organizational units that are precached. The default is the whole directory '/'. The parameter accpets a comma separate list of OrgUnitPaths, /OU1,/OU2/OU 3 in this example all users within the tree of /OU1 and /OU2/OU 3 would be precached but not from /OU2 itself. Precaching can be disable by leaving the field empty. + To reduce the volume of apis, ssosync precaches the users and groups from the Google directory. If you directory is large >10,000 users, you may wish to limit the organizational units that are precached. The default is the whole directory '/'. The parameter accpets a comma separate list of OrgUnitPaths, /OU1,/OU2/OU3 in this example all users within the tree of /OU1 and /OU2/OU3 would be precached but not from /OU2 itself. Precaching can be disable by leaving the field empty. Default: "/" - AllowedPattern: '(?!.*\s)|(^\/$)|(\/[a-zA-Z0-9_\/\- ]{1,50})(?:(,\/[a-zA-Z0-9_\/\- ]{1,50}))*' + AllowedPattern: '(?!.*\s)|(^\/$)|(\/[a-zA-Z0-9_\/\- ]{0,50})|(\/[a-zA-Z0-9_\/\- ]{0,50})(?:(,\/[a-zA-Z0-9_\/\- ]{1,50}))' # Secrets GoogleCredentials: From cc0c9e15ee33383121128b5172591934c5c33fd8 Mon Sep 17 00:00:00 2001 From: Chris Pates Date: Thu, 30 Apr 2026 19:10:19 +0100 Subject: [PATCH 10/27] Update template.yaml --- template.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template.yaml b/template.yaml index c6e31f28..126bdc3e 100644 --- a/template.yaml +++ b/template.yaml @@ -622,7 +622,7 @@ Resources: INCLUDE_GROUPS: !If [SetIncludeGroups, !Ref IncludeGroups, !Ref AWS::NoValue] DRY_RUN: !If [SetDryRun, 'True', 'False'] SYNC_SUSPENDED: !If [SetSyncSuspended, 'True', 'False'] - PRECACHE_ORG_UNITS: !If [DisablePrecaching, "DISABLED", !Ref PrecacheOrgUnits] + PRECACHE_ORG_UNITS: !If [DisablePrecaching, !Ref AWS::NoValue, !Ref PrecacheOrgUnits] Events: SyncScheduledEvent: From 53518d62cd467b3524e428e335a60ea921ed970c Mon Sep 17 00:00:00 2001 From: Chris Pates Date: Thu, 30 Apr 2026 19:10:19 +0100 Subject: [PATCH 11/27] Update template.yaml --- template.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template.yaml b/template.yaml index 5e8e4da6..5645212b 100644 --- a/template.yaml +++ b/template.yaml @@ -622,7 +622,7 @@ Resources: INCLUDE_GROUPS: !If [SetIncludeGroups, !Ref IncludeGroups, !Ref AWS::NoValue] DRY_RUN: !If [SetDryRun, 'True', 'False'] SYNC_SUSPENDED: !If [SetSyncSuspended, 'True', 'False'] - PRECACHE_ORG_UNITS: !If [DisablePrecaching, "DISABLED", !Ref PrecacheOrgUnits] + PRECACHE_ORG_UNITS: !If [DisablePrecaching, !Ref AWS::NoValue, !Ref PrecacheOrgUnits] Events: SyncScheduledEvent: From c6b623db5c3f717b7b7403205ccf93d985b10605 Mon Sep 17 00:00:00 2001 From: Chris Pates Date: Sat, 9 May 2026 19:14:08 +0100 Subject: [PATCH 12/27] 309 precache field regex does not all for disabled (#312) * Edit the regex for the CloudFormation template for the PRECACHE_ORG_UNITS, to allow for a single OU path other than '/' * Updated handling of this environment variable so, if specified but and empty string or not set it disables precaching. * Updated handling of other optional comma separated string env_variables to have the same way. --- cmd/root.go | 60 ++++---- internal/config/config.go | 4 +- internal/sync.go | 317 +++++++++++++++++++++----------------- template.yaml | 4 +- 4 files changed, 209 insertions(+), 176 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 03374b83..5c39d75c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -174,6 +174,7 @@ func initConfig() { "sync_method", "region", "identity_store_id", + "dry_run", } for _, e := range appEnvVars { @@ -193,11 +194,11 @@ func initConfig() { // config logger logConfig(cfg) - if cfg.SyncSuspended { - cfg.UserFilter = " isArchived=false" - } else { - cfg.UserFilter = " isSuspended=false isArchived=false" - } + if cfg.SyncSuspended { + cfg.UserFilter = " isArchived=false" + } else { + cfg.UserFilter = " isSuspended=false isArchived=false" + } } @@ -208,25 +209,28 @@ func getEnvStr(key string, fallback string) string { log.WithField(key, valueStr).Info("EnvVar") return valueStr } - return fallback + return fallback } -func getEnvStrs (key string, fallback []string) []string { - if valueStr, ok := os.LookupEnv(key); ok { - log.WithField(key, valueStr).Info("EnvVar") - return strings.Split(valueStr, ",") - } - return fallback +func getEnvStrs(key string, fallback []string) []string { + if valueStr, ok := os.LookupEnv(key); ok { + log.WithField(key, valueStr).Info("EnvVar") + if valueStr == "" { + return nil + } + return strings.Split(valueStr, ",") + } + return fallback } -func getEnvBool (key string, fallback bool) bool { - if valueStr, ok := os.LookupEnv(key); ok { +func getEnvBool(key string, fallback bool) bool { + if valueStr, ok := os.LookupEnv(key); ok { log.WithField(key, valueStr).Info("EnvVar") - valueBool := strings.ToLower(valueStr) == "true" + valueBool := strings.ToLower(valueStr) == "true" log.WithField(key, valueBool).Info("config") - return valueBool - } - return fallback + return valueBool + } + return fallback } func configLambda() { @@ -255,17 +259,17 @@ func configLambda() { cfg.Region = getSecretFromCache(getEnvStr("REGION", "")) cfg.GoogleCredentials = getSecretFromCache(getEnvStr("GOOGLE_CREDENTIALS", "")) cfg.SCIMAccessToken = getSecretFromCache(getEnvStr("SCIM_ACCESS_TOKEN", "")) - + // Handle environment variables for other settings cfg.LogLevel = getEnvStr("LOG_LEVEL", config.DefaultLogLevel) cfg.LogFormat = getEnvStr("LOG_FORMAT", config.DefaultLogFormat) cfg.SyncMethod = getEnvStr("SYNC_METHOD", config.DefaultSyncMethod) cfg.UserMatch = getEnvStr("USER_MATCH", "") cfg.GroupMatch = getEnvStr("GROUP_MATCH", "*") - cfg.IgnoreGroups = getEnvStrs("IGNORE_GROUPS", []string{}) - cfg.IgnoreUsers = getEnvStrs("IGNORE_USERS", []string{}) - cfg.IncludeGroups = getEnvStrs("INCLUDE_GROUPS", []string{}) - cfg.PrecacheOrgUnits = getEnvStrs("PRECACHE_ORG_UNITS", strings.Split(config.DefaultPrecacheOrgUnits, ",")) + cfg.IgnoreGroups = getEnvStrs("IGNORE_GROUPS", nil) + cfg.IgnoreUsers = getEnvStrs("IGNORE_USERS", nil) + cfg.IncludeGroups = getEnvStrs("INCLUDE_GROUPS", nil) + cfg.PrecacheOrgUnits = getEnvStrs("PRECACHE_ORG_UNITS", nil) cfg.DryRun = getEnvBool("DRY_RUN", false) cfg.SyncSuspended = getEnvBool("SYNC_SUSPENDED", false) @@ -285,20 +289,20 @@ func addFlags(_ *cobra.Command, cfg *config.Config) { rootCmd.PersistentFlags().StringVarP(&cfg.LogFormat, "log-format", "", config.DefaultLogFormat, "log format") rootCmd.PersistentFlags().StringVarP(&cfg.LogLevel, "log-level", "", config.DefaultLogLevel, "log level") rootCmd.PersistentFlags().BoolVarP(&cfg.DryRun, "dry-run", "n", false, "Do *not* perform any actions, instead list what would happen") - rootCmd.PersistentFlags().BoolVarP(&cfg.SyncSuspended, "suspended", "", false, "included suspended users and their group memberships when syncing") + rootCmd.PersistentFlags().BoolVarP(&cfg.SyncSuspended, "suspended", "", false, "included suspended users and their group memberships when syncing") rootCmd.Flags().StringVarP(&cfg.SCIMAccessToken, "access-token", "t", "", "AWS SSO SCIM API Access Token") rootCmd.Flags().StringVarP(&cfg.SCIMEndpoint, "endpoint", "e", "", "AWS SSO SCIM API Endpoint") rootCmd.Flags().StringVarP(&cfg.GoogleCredentials, "google-credentials", "c", config.DefaultGoogleCredentials, "path to Google Workspace credentials file") rootCmd.Flags().StringVarP(&cfg.GoogleAdmin, "google-admin", "u", "", "Google Workspace admin user email") - rootCmd.Flags().StringSliceVar(&cfg.IgnoreUsers, "ignore-users", []string{}, "ignores these Google Workspace users") - rootCmd.Flags().StringSliceVar(&cfg.IgnoreGroups, "ignore-groups", []string{}, "ignores these Google Workspace groups") - rootCmd.Flags().StringSliceVar(&cfg.IncludeGroups, "include-groups", []string{}, "include only these Google Workspace groups, NOTE: only works when --sync-method 'users_groups'") + rootCmd.Flags().StringSliceVar(&cfg.IgnoreUsers, "ignore-users", nil, "ignores these Google Workspace users") + rootCmd.Flags().StringSliceVar(&cfg.IgnoreGroups, "ignore-groups", nil, "ignores these Google Workspace groups") + rootCmd.Flags().StringSliceVar(&cfg.IncludeGroups, "include-groups", nil, "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*' 'name=John Doe,email:admin*', to sync all users in the directory specify '*'. For query syntax and more examples 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*' 'name=AWS-Admins,email:aws*', to sync all groups (and their member users) specify '*'. For query syntax and more examples see: https://developers.google.com/admin-sdk/directory/v1/guides/search-groups") 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") - rootCmd.Flags().StringSliceVar(&cfg.PrecacheOrgUnits, "precache-ous", strings.Split(config.DefaultPrecacheOrgUnits, ","), "A common separated list of Google Workspace OrgUnitPathis e.g.'/', to precache all users within the organization or '/OU_1/OU 2,/OU3'. To disable and use caching on the fly, 'DISABLED'.") + rootCmd.Flags().StringSliceVar(&cfg.PrecacheOrgUnits, "precache-ous", nil, "A common separated list of Google Workspace OrgUnitPathis e.g.'/', to precache all users within the organization or '/OU_1/OU 2,/OU3'. Precaching is disabled by default.") } diff --git a/internal/config/config.go b/internal/config/config.go index 0f3cc01a..8bb0fcf4 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -60,8 +60,6 @@ const ( DefaultGoogleCredentials = "credentials.json" // DefaultSyncMethod is the default sync method to use. DefaultSyncMethod = "groups" - // DefaultPrecacheOrgUnits - DefaultPrecacheOrgUnits = "/" ) // New returns a new Config @@ -101,5 +99,5 @@ func (c *Config) Validate() error { return errors.New("sync method must be either 'groups' or 'users_groups'") } - return nil + return nil } diff --git a/internal/sync.go b/internal/sync.go index 1e91f69e..c1ee2dd8 100644 --- a/internal/sync.go +++ b/internal/sync.go @@ -588,6 +588,7 @@ func (s *syncGSuite) getGoogleGroupsAndUsers(queryGroups string, queryUsers stri gUserDetailCache := make(map[string]*admin.User) gGroupDetailCache := make(map[string]*admin.Group) gUniqUsers := make(map[string]*admin.User) + gGroups := make([]*admin.Group, 0) log.WithFields(log.Fields{ "func": funcName, @@ -613,66 +614,77 @@ func (s *syncGSuite) getGoogleGroupsAndUsers(queryGroups string, queryUsers stri } // Fetch Users - log.WithFields(log.Fields{ - "func": funcName, - "queryUsers": queryUsers, - "queryFilters": s.cfg.UserFilter, - }).Info("fetching userMatch") - - googleUsers, err := s.google.GetUsers(queryUsers, s.cfg.UserFilter) - if err != nil { + if queryUsers == "" { log.WithFields(log.Fields{ - "func": funcName, - "error": err, - }).Error("failed fetching userMatch from Google") - return nil, nil, nil, err - } + "func": funcName, + }).Info("Skipping fetch for userMatch") + } else { + log.WithFields(log.Fields{ + "func": funcName, + "queryUsers": queryUsers, + "queryFilters": s.cfg.UserFilter, + }).Info("fetching userMatch") - log.WithFields(log.Fields{ - "func": funcName, - }).Debug("process users from google, filtering as required") + googleUsers, err := s.google.GetUsers(queryUsers, s.cfg.UserFilter) + if err != nil { + log.WithFields(log.Fields{ + "func": funcName, + "error": err, + }).Error("failed fetching userMatch from Google") + return nil, nil, nil, err + } - for _, u := range googleUsers { log.WithFields(log.Fields{ "func": funcName, - "user": u, - }).Debug("process user") + }).Debug("process users from google, filtering as required") - // Remove any users that should be ignored - if s.ignoreUser(u.PrimaryEmail) { + for _, u := range googleUsers { log.WithFields(log.Fields{ - "func": funcName, - "user.Id": u.Id, - }).Info("ignoring user") - continue - } + "func": funcName, + "user": u, + }).Debug("process user") - if _, found := gUniqUsers[u.PrimaryEmail]; !found { - log.WithFields(log.Fields{ - "func": funcName, - "user.Id": u.Id, - }).Debug("adding user") - gUserDetailCache[u.PrimaryEmail] = u - gUniqUsers[u.PrimaryEmail] = gUserDetailCache[u.PrimaryEmail] - continue - } else { - log.WithFields(log.Fields{ - "func": funcName, - "user.Id": u.Id, - }).Debug("already existing") - continue + // Remove any users that should be ignored + if s.ignoreUser(u.PrimaryEmail) { + log.WithFields(log.Fields{ + "func": funcName, + "user.Id": u.Id, + }).Info("ignoring user") + continue + } + + if _, found := gUniqUsers[u.PrimaryEmail]; !found { + log.WithFields(log.Fields{ + "func": funcName, + "user.Id": u.Id, + }).Debug("adding user") + gUserDetailCache[u.PrimaryEmail] = u + gUniqUsers[u.PrimaryEmail] = gUserDetailCache[u.PrimaryEmail] + continue + } else { + log.WithFields(log.Fields{ + "func": funcName, + "user.Id": u.Id, + }).Debug("already existing") + continue + } } } // For larger directories this will reduce execution time and avoid throttling limits - // however if you have directory with 10s of 1000s of users you may want to down scope + // however if you have directory with 10,000+ users you may want to down scope // this to a specific OU path or disable by leaving empty. - if s.cfg.PrecacheOrgUnits[0] != "DISABLED" { + if s.cfg.PrecacheOrgUnits == nil { + log.WithFields(log.Fields{ + "func": funcName, + "OrgUnitPaths": s.cfg.PrecacheOrgUnits, + }).Info("Precaching DISABLED, caching on the fly") + } else { precacheQueries := "" log.WithFields(log.Fields{ "func": funcName, "OrgUnitPaths": s.cfg.PrecacheOrgUnits, - }).Info("Prechache users from these paths") + }).Info("Precache users from these paths") for _, orgUnitPath := range s.cfg.PrecacheOrgUnits { log.WithFields(log.Fields{ @@ -696,7 +708,7 @@ func (s *syncGSuite) getGoogleGroupsAndUsers(queryGroups string, queryUsers stri "queryFilters": s.cfg.UserFilter, }).Info("Precaching users") - googleUsers, err = s.google.GetUsers(precacheQueries, s.cfg.UserFilter) + googleUsers, err := s.google.GetUsers(precacheQueries, s.cfg.UserFilter) if err != nil { log.WithFields(log.Fields{ "func": funcName, @@ -732,126 +744,129 @@ func (s *syncGSuite) getGoogleGroupsAndUsers(queryGroups string, queryUsers stri } } } - } else { - log.WithFields(log.Fields{ - "func": funcName, - }).Info("Precaching DISABLED, caching on the fly") - } - - log.WithFields(log.Fields{ - "func": funcName, - "queryGroups": queryGroups, - }).Info("fetching groups") - - gGroups, err := s.google.GetGroups(queryGroups) - if err != nil { - log.WithFields(log.Fields{ - "func": funcName, - "error": err, - }).Error("failed fetching groups") - return nil, nil, nil, err } - filteredGoogleGroups := []*admin.Group{} - log.WithFields(log.Fields{ - "func": funcName, - }).Info("filter groups by ignoreList") - - for _, g := range gGroups { + // Fetch Users + if queryGroups == "" { log.WithFields(log.Fields{ - "func": funcName, - "group": g, - }).Debug("processing group") - - if s.ignoreGroup(g.Email) { - log.WithFields(log.Fields{ - "func": funcName, - "group.Id": g.Id, - }).Info("ignoring group") - continue - } - filteredGoogleGroups = append(filteredGoogleGroups, g) - } - gGroups = filteredGoogleGroups - - log.WithField("func", funcName).Info("fetch group memberships") - for _, g := range gGroups { + "func": funcName, + }).Info("Skipping fetch for groupMatch") + } else { log.WithFields(log.Fields{ - "func": funcName, - "group": g, - }).Debug("processing group") + "func": funcName, + "queryGroups": queryGroups, + }).Info("fetching groups") - if s.ignoreGroup(g.Email) { - log.WithFields(log.Fields{ - "func": funcName, - "group.Id": g.Id, - }).Info("skipping group") - continue - } - - log.WithField("func", funcName).Debug("fetch membership") - membersUsers, err := s.getGoogleUsersInGroup(g, gUserDetailCache, gGroupDetailCache) + gGroups, err = s.google.GetGroups(queryGroups) if err != nil { + log.WithFields(log.Fields{ + "func": funcName, + "error": err, + }).Error("failed fetching groups") return nil, nil, nil, err } - // If we've not seen the user email address before add it to the list of unique users - // also, we need to deduplicate the list of members. + filteredGoogleGroups := []*admin.Group{} log.WithFields(log.Fields{ - "func": funcName, - "group.Id": g.Id, - "membersUsers": membersUsers, - }).Debug("Process group membership") + "func": funcName, + }).Info("filter groups by ignoreList") - gUniqMembers := make(map[string]*admin.User) - for _, m := range membersUsers { + for _, g := range gGroups { log.WithFields(log.Fields{ - "func": funcName, - "group.Id": g.Id, - "member": m, - }).Debug("Processing member") + "func": funcName, + "group": g, + }).Debug("processing group") - if m == nil { + if s.ignoreGroup(g.Email) { log.WithFields(log.Fields{ - "func": funcName, - "group.Id": g.Id, - "member.Id": m.Id, - }).Error("nil user") + "func": funcName, + "group.Id": g.Id, + }).Info("ignoring group") continue } - if _, found := gUniqUsers[m.PrimaryEmail]; !found { + filteredGoogleGroups = append(filteredGoogleGroups, g) + } + gGroups = filteredGoogleGroups + + log.WithField("func", funcName).Info("fetch group memberships") + for _, g := range gGroups { + log.WithFields(log.Fields{ + "func": funcName, + "group": g, + }).Debug("processing group") + + if s.ignoreGroup(g.Email) { log.WithFields(log.Fields{ "func": funcName, "group.Id": g.Id, - "member": m.Id, - }).Debug("adding user to UniqueUsers") - gUniqUsers[m.PrimaryEmail] = gUserDetailCache[m.PrimaryEmail] + }).Info("skipping group") + continue } - if _, found := gUniqMembers[m.PrimaryEmail]; !found { + log.WithField("func", funcName).Debug("fetch membership") + membersUsers, err := s.getGoogleUsersInGroup(g, gUserDetailCache, gGroupDetailCache) + if err != nil { + return nil, nil, nil, err + } + + // If we've not seen the user email address before add it to the list of unique users + // also, we need to deduplicate the list of members. + log.WithFields(log.Fields{ + "func": funcName, + "group.Id": g.Id, + "membersUsers": membersUsers, + }).Debug("Process group membership") + + gUniqMembers := make(map[string]*admin.User) + for _, m := range membersUsers { log.WithFields(log.Fields{ "func": funcName, "group.Id": g.Id, - "member": m.Id, - }).Debug("adding user to group") - gUniqMembers[m.PrimaryEmail] = gUserDetailCache[m.PrimaryEmail] + "member": m, + }).Debug("Processing member") + + if m == nil { + log.WithFields(log.Fields{ + "func": funcName, + "group.Id": g.Id, + "member.Id": m.Id, + }).Error("nil user") + continue + } + if _, found := gUniqUsers[m.PrimaryEmail]; !found { + log.WithFields(log.Fields{ + "func": funcName, + "group.Id": g.Id, + "member": m.Id, + }).Debug("adding user to UniqueUsers") + gUniqUsers[m.PrimaryEmail] = gUserDetailCache[m.PrimaryEmail] + } + + if _, found := gUniqMembers[m.PrimaryEmail]; !found { + log.WithFields(log.Fields{ + "func": funcName, + "group.Id": g.Id, + "member": m.Id, + }).Debug("adding user to group") + gUniqMembers[m.PrimaryEmail] = gUserDetailCache[m.PrimaryEmail] + } } - } - log.WithFields(log.Fields{ - "func": funcName, - "group.Id": g.Id, - }).Debug("create gMembers") - gMembers := make([]*admin.User, 0) - for _, member := range gUniqMembers { - gMembers = append(gMembers, member) + log.WithFields(log.Fields{ + "func": funcName, + "group.Id": g.Id, + }).Debug("create gMembers") + gMembers := make([]*admin.User, 0) + for _, member := range gUniqMembers { + gMembers = append(gMembers, member) + } + log.WithFields(log.Fields{ + "func": funcName, + "group.Id": g.Id, + "gMembers": gMembers, + }).Debug("Finished group membership") + gGroupsUsers[g.Name] = gMembers } - log.WithFields(log.Fields{ - "func": funcName, - "group.Id": g.Id, - "gMembers": gMembers, - }).Debug("Finished group membership") - gGroupsUsers[g.Name] = gMembers } log.WithField("func", funcName).Debug("create gUsers") @@ -1088,14 +1103,18 @@ func DoSync(ctx context.Context, cfg *config.Config) error { return err } } else { - err = c.SyncUsers(cfg.UserMatch) - if err != nil { - return err + if cfg.UserMatch != "" { + err = c.SyncUsers(cfg.UserMatch) + if err != nil { + return err + } } - err = c.SyncGroups(cfg.GroupMatch) - if err != nil { - return err + if cfg.GroupMatch != "" { + err = c.SyncGroups(cfg.GroupMatch) + if err != nil { + return err + } } } @@ -1103,6 +1122,10 @@ func DoSync(ctx context.Context, cfg *config.Config) error { } func (s *syncGSuite) ignoreUser(name string) bool { + if s.cfg.IgnoreUsers == nil { + return false + } + if s.ignoreUsersSet == nil { s.ignoreUsersSet = make(map[string]struct{}, len(s.cfg.IgnoreUsers)) for _, u := range s.cfg.IgnoreUsers { @@ -1114,6 +1137,10 @@ func (s *syncGSuite) ignoreUser(name string) bool { } func (s *syncGSuite) ignoreGroup(name string) bool { + if s.cfg.IgnoreGroups == nil { + return false + } + if s.ignoreGroupsSet == nil { s.ignoreGroupsSet = make(map[string]struct{}, len(s.cfg.IgnoreGroups)) for _, g := range s.cfg.IgnoreGroups { @@ -1125,6 +1152,10 @@ func (s *syncGSuite) ignoreGroup(name string) bool { } func (s *syncGSuite) includeGroup(name string) bool { + if s.cfg.IncludeGroups == nil { + return false + } + if s.includeGroupsSet == nil { s.includeGroupsSet = make(map[string]struct{}, len(s.cfg.IncludeGroups)) for _, g := range s.cfg.IncludeGroups { diff --git a/template.yaml b/template.yaml index 5645212b..126bdc3e 100644 --- a/template.yaml +++ b/template.yaml @@ -158,9 +158,9 @@ Parameters: PrecacheOrgUnits: Type: String Description: | - To reduce the volume of apis, ssosync precaches the users and groups from the Google directory. If you directory is large >10,000 users, you may wish to limit the organizational units that are precached. The default is the whole directory '/'. The parameter accpets a comma separate list of OrgUnitPaths, /OU1,/OU2/OU 3 in this example all users within the tree of /OU1 and /OU2/OU 3 would be precached but not from /OU2 itself. Precaching can be disable by leaving the field empty. + To reduce the volume of apis, ssosync precaches the users and groups from the Google directory. If you directory is large >10,000 users, you may wish to limit the organizational units that are precached. The default is the whole directory '/'. The parameter accpets a comma separate list of OrgUnitPaths, /OU1,/OU2/OU3 in this example all users within the tree of /OU1 and /OU2/OU3 would be precached but not from /OU2 itself. Precaching can be disable by leaving the field empty. Default: "/" - AllowedPattern: '(?!.*\s)|(^\/$)|(\/[a-zA-Z0-9_\/\- ]{1,50})(?:(,\/[a-zA-Z0-9_\/\- ]{1,50}))*' + AllowedPattern: '(?!.*\s)|(^\/$)|(\/[a-zA-Z0-9_\/\- ]{0,50})|(\/[a-zA-Z0-9_\/\- ]{0,50})(?:(,\/[a-zA-Z0-9_\/\- ]{1,50}))' # Secrets GoogleCredentials: From 77bab77fde6ebba4ab6d4ca4e5bfb6da363ac914 Mon Sep 17 00:00:00 2001 From: Chris Pates Date: Sat, 9 May 2026 19:30:44 +0100 Subject: [PATCH 13/27] Update Pipeline to type: V2 --- cicd/cloudformation/release.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/cicd/cloudformation/release.yaml b/cicd/cloudformation/release.yaml index 135c9906..fcb06733 100644 --- a/cicd/cloudformation/release.yaml +++ b/cicd/cloudformation/release.yaml @@ -198,6 +198,7 @@ Resources: Type: AWS::CodePipeline::Pipeline Properties: Name: SSOSync-Build + PipelineType: V2 RoleArn: !Sub ${CodePipelineRole.Arn} ArtifactStore: Type: S3 From ddafd84e7b549cc430b1ab96f7c7495ce441b680 Mon Sep 17 00:00:00 2001 From: Chris Pates Date: Thu, 21 May 2026 13:35:33 +0100 Subject: [PATCH 14/27] Configurable LogRetention and QuickStart, plus bugfixes (#316) Included in this change: Parameter PrecacheOrgUnits : When left empty will disable the pre-caching of both Groups and Users Parameter LogRetention : Added, config CloudWatch Log Group retention period, previously it defaulted to Indefinitely this default has been retain however, it is strongly recommended a more frugal option is selected. Parameter ScheduleExpression : When left empty will disable scheduling of the SSOSync lambda function. The default for this is unchanged Rate(15 minutes), where the lambda is being triggered by an external event or as part of CICD pipeline (such ac CodePipeline), this prevent concurrency limits being encountered. The Parameters page has also been re-grouped to be more intuitive. Quick Start templates: Added a simple template that can launched directly from CloudFormation, this can simply be launched from the repo. Currently, template creates a deployment in a single account with two nested stacks one for the secrets and the other for the lambda function. To update the deployment, download the latest version of the template and update the stack. * implement disabled schedule * Apply disable PreCache to Groups * Improve logging for precache activity * Remove Explicit RoleName * Added Log Retention Setting. * Tidy Up the Parameters page * Add QuickStart * Add job to update version strings in quickstart on release --- .github/workflows/release.yml | 6 + internal/sync.go | 42 +++--- quick-start/single-account.yaml | 247 ++++++++++++++++++++++++++++++++ template.yaml | 108 +++++++++++--- 4 files changed, 366 insertions(+), 37 deletions(-) create mode 100644 quick-start/single-account.yaml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bcf5b1d0..d741e590 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,6 +16,12 @@ jobs: - name: Check out code uses: actions/checkout@v4 + - name: Update version strings + run: | + SemanticVersion=$GITHUB_REF + SemanticVersion="${SemanticVersion//v/}" + sed -i '' -E "s/SemanticVersion: [0-9.]+/SemanticVersion: $SemanticVersion/g" quick-start/*.yaml template.yaml + - name: Setup Go uses: actions/setup-go@v5 with: diff --git a/internal/sync.go b/internal/sync.go index c1ee2dd8..7da1c5e3 100644 --- a/internal/sync.go +++ b/internal/sync.go @@ -596,21 +596,29 @@ func (s *syncGSuite) getGoogleGroupsAndUsers(queryGroups string, queryUsers stri "queryUsers": queryUsers, }).Debug("getGoogleGroupsAndUsers()") - // Precaching group data, this will speed up processing of nested groups, etc... - log.WithFields(log.Fields{ - "func": funcName, - }).Info("Precache all Groups from Google") - - googleGroups, err := s.google.GetGroups("*") - if err != nil { + // For larger directories this will reduce execution time and avoid throttling limits + // however if you have directory with 10,000+ users you may want to down scope + // this to a specific OU path or disable by leaving empty. + if s.cfg.PrecacheOrgUnits == nil { log.WithFields(log.Fields{ - "func": funcName, - "error": err, - }).Error("failed precaching groups from Google") - return nil, nil, nil, err - } - for _, g := range googleGroups { - gGroupDetailCache[g.Email] = g + "func": funcName, + }).Info("Precaching DISABLED, caching groups on the fly") + } else { + log.WithFields(log.Fields{ + "func": funcName, + }).Info("Precache all Groups from Google") + + googleGroups, err := s.google.GetGroups("*") + if err != nil { + log.WithFields(log.Fields{ + "func": funcName, + "error": err, + }).Error("failed precaching groups from Google") + return nil, nil, nil, err + } + for _, g := range googleGroups { + gGroupDetailCache[g.Email] = g + } } // Fetch Users @@ -678,7 +686,7 @@ func (s *syncGSuite) getGoogleGroupsAndUsers(queryGroups string, queryUsers stri log.WithFields(log.Fields{ "func": funcName, "OrgUnitPaths": s.cfg.PrecacheOrgUnits, - }).Info("Precaching DISABLED, caching on the fly") + }).Info("Precaching DISABLED, caching users on the fly") } else { precacheQueries := "" log.WithFields(log.Fields{ @@ -757,7 +765,7 @@ func (s *syncGSuite) getGoogleGroupsAndUsers(queryGroups string, queryUsers stri "queryGroups": queryGroups, }).Info("fetching groups") - gGroups, err = s.google.GetGroups(queryGroups) + gGroups, err := s.google.GetGroups(queryGroups) if err != nil { log.WithFields(log.Fields{ "func": funcName, @@ -1088,7 +1096,7 @@ func DoSync(ctx context.Context, cfg *config.Config) error { log.WithField("error", err).Warn("Problem performing test query against Identity Store") return err } - log.WithField("Groups", response).Info("Test call for groups successful") + log.WithField("Groups", response).Info("Test call to Identity Store successful") // Initialize sync client with // 1. SCIM API client diff --git a/quick-start/single-account.yaml b/quick-start/single-account.yaml new file mode 100644 index 00000000..d9440d2b --- /dev/null +++ b/quick-start/single-account.yaml @@ -0,0 +1,247 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: 'AWS::Serverless-2016-10-31' + +Description: + This CloudFormation template will deploy two instances of SSOSync from + the AWS Serverless Application Repository (SAR). One holding the Secrets + and the other the app itself. + +Metadata: + AWS::CloudFormation::Interface: + ParameterGroups: + - Label: + default: AWS IAM Identity Center (Successor to AWS Single Sign-On) + Parameters: + - SCIMEndpointUrl + - SCIMEndpointAccessToken + - IdentityStoreID + - Region + - Label: + default: Google Workspace Credentials + Parameters: + - GoogleAdminEmail + - GoogleCredentials + - Label: + default: Sync Configuration + Parameters: + - PrecacheOrgUnits + - GoogleUserMatch + - GoogleGroupMatch + - Label: + default: "Lambda Configuration" + Parameters: + - ScheduleExpression + - SyncSuspended + - DryRun + - MemorySize + - TimeOut + - Label: + default: Log Configuration + Parameters: + - LogLevel + - LogFormat + - LogRetention + + +Parameters: + ScheduleExpression: + Type: String + 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{2,}+|[2-9]) (?:minutes|hours|days))|(?:1 (?:minute|hour|day)))\)|(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))*|\*)\))' + + LogRetention: + Type: Number + Description: | + How long to retain log for, if you want to retain indefinitely, select 0 + Default: 365 + AllowedValues: + - 0 + - 1 + - 3 + - 5 + - 7 + - 14 + - 30 + - 60 + - 90 + - 120 + - 150 + - 180 + - 365 + - 400 + - 545 + - 731 + - 1096 + - 1827 + - 2192 + - 2557 + - 2922 + - 3288 + - 3653 + + LogLevel: + Type: String + Description: | + [required] Log level for Lambda function logging + Default: warn + AllowedValues: + - panic + - fatal + - error + - warn + - info + - debug + - trace + + LogFormat: + Type: String + Description: | + [required] Log format for Lambda function logging + Default: json + AllowedValues: + - json + - text + + MemorySize: + Type: Number + Description: | + [required] the amount of RAM allocated to the function, within range 128-10240MB. default is 128MB. + Default: 128 + MinValue: 128 + MaxValue: 10240 + + TimeOut: + Type: Number + Description: | + [required] Timeout for the Lambda function + Default: 300 + MinValue: 1 + MaxValue: 900 + + SyncSuspended: + Type: String + Description: | + If enabled suspended users that match either the GoogleUserMatch or GoogleGroupMatch criteria they will be replicated. + Default: ignore + AllowedValues: + - ignore + - sync + + DryRun: + Type: String + Description: | + Enabled Dry Run, means the lambda will execute highlighting the changes it would make to the Identity Store, will not making any changes. + Default: live + AllowedValues: + - live + - dry-run + + PrecacheOrgUnits: + Type: String + Description: | + To reduce the volume of apis, ssosync precaches the users and groups from the Google directory. If you directory is large >10,000 users, you may wish to limit the organizational units that are precached. The default is the whole directory '/'. The parameter accpets a comma separate list of OrgUnitPaths, /OU1,/OU2/OU3 in this example all users within the tree of /OU1 and /OU2/OU3 would be precached but not from /OU2 itself. Precaching can be disable by leaving the field empty. + Default: "/" + AllowedPattern: '(?!.*\s)|(^\/$)|(\/[a-zA-Z0-9_\/\- ]{0,50})|(\/[a-zA-Z0-9_\/\- ]{0,50})(?:(,\/[a-zA-Z0-9_\/\- ]{1,50}))' + +# Secrets + GoogleCredentials: + Type: String + 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 + 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 IAM Identity Center - SCIM Endpoint Url + Default: "" + AllowedPattern: '(?!.*\s)|(https://scim.(us(-gov)?|ap|ca|cn|eu|il|me|mx|sa)-(central|(north|south)?(east|west)?)-([0-9]{1}).amazonaws.com/([A-Za-z0-9]{8,11})-([A-Za-z0-9]{4})-([A-Za-z0-9]{4})-([A-Za-z0-9]{4})-([A-Za-z0-9]{12})/scim/v2/?)' + + SCIMEndpointAccessToken: + Type: String + Description: | + AWS IAM Identity Center - SCIM AccessToken + Default: "" + AllowedPattern: '(?!.*\s)|([0-9a-zA-Z/=+-\\]{500,620})' + NoEcho: true + + Region: + Type: String + 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 IAM Identity Center + Default: "" + AllowedPattern: '(?!.*\s)|d-[1-z0-9]{10}' + + GoogleUserMatch: + Type: String + Description: | + [optional] Google Workspace Users filter query parameter, a simple '*' denotes sync all users in the directory. example: 'name:John*,email:admin*', '*' or name=John Doe,email:admin*' see: https://developers.google.com/admin-sdk/directory/v1/guides/search-users, if left empty no users will be selected but if a pattern has been set for GroupMatch users that are members of the groups it matches will still be selected. + Default: "" + AllowedPattern: '(?!.*\s)|(\*)|((((name|Name|NAME)((:[a-zA-Z0-9\-_ ]{1,64}\*)|(=[a-zA-Z0-9\-_ ]{1,64})))|((email|Email|EMAIL)((:[a-zA-Z0-9.\-_]{1,64}\*)|(=([a-zA-Z0-9.\-_]{1,64})@([a-zA-Z0-9.\-]{5,260})))))(,(((name|Name|NAME)((:[a-zA-Z0-9\-_ ]{1,64}\*)|(=[a-zA-Z0-9\-_ ]{1,64})))|((email|Email|EMAIL)((:[a-zA-Z0-9.\-_]{1,64}\*)|(=([a-zA-Z0-9.\-_]{1,64})@([a-zA-Z0-9.\-]{5,260}))))))*)' + + GoogleGroupMatch: + Type: String + Description: | + [optional] Google Workspace Groups filter query parameter, a simple '*' denotes sync all groups (and any users that are members of those groups). example: 'name:Admin*,email:aws-*', 'name=Admins' or '*' see: https://developers.google.com/admin-sdk/directory/v1/guides/search-groups, if left empty no groups will be selected. + Default: "*" + AllowedPattern: '(?!.*\s)|(\*)|((((name|Name|NAME)((:[a-zA-Z0-9\-_ ]{1,64}\*)|(=[a-zA-Z0-9\-_ ]{1,64})))|((email|Email|EMAIL)((:[a-zA-Z0-9.\-_]{1,64}\*)|(=([a-zA-Z0-9.\-_]{1,64})@([a-zA-Z0-9.\-]{5,260})))))(,(((name|Name|NAME)((:[a-zA-Z0-9\-_ ]{1,64}\*)|(=[a-zA-Z0-9\-_ ]{1,64})))|((email|Email|EMAIL)((:[a-zA-Z0-9.\-_]{1,64}\*)|(=([a-zA-Z0-9.\-_]{1,64})@([a-zA-Z0-9.\-]{5,260}))))))*)' + + +Resources: + + Function: + Type: AWS::Serverless::Application + Properties: + Location: + ApplicationId: arn:aws:serverlessrepo:us-east-2:004480582608:applications/SSOSync + SemanticVersion: 1.0.0 + Parameters: + DeployPattern: App only + FunctionName: SSOSync + CrossStackConfig: !GetAtt [Secrets, Outputs.AppConfigLocal] + PrecacheOrgUnits: !Ref PrecacheOrgUnits + GoogleUserMatch: !Ref GoogleUserMatch + GoogleGroupMatch: !Ref GoogleGroupMatch + SyncMethod: groups + ScheduleExpression: !Ref ScheduleExpression + SyncSuspended: !Ref SyncSuspended + DryRun: !Ref DryRun + MemorySize: !Ref MemorySize + TimeOut: !Ref TimeOut + LogLevel: !Ref LogLevel + LogFormat: !Ref LogFormat + LogRetention: !Ref LogRetention + + Secrets: + Type: AWS::Serverless::Application + Properties: + Location: + ApplicationId: arn:aws:serverlessrepo:us-east-2:004480582608:applications/SSOSync + SemanticVersion: 1.0.0 + Parameters: + DeployPattern: Secrets only + SCIMEndpointUrl: !Ref SCIMEndpointUrl + SCIMEndpointAccessToken: !Ref SCIMEndpointAccessToken + IdentityStoreID: !Ref IdentityStoreID + Region: !Ref Region + GoogleAdminEmail: !Ref GoogleAdminEmail + GoogleCredentials: !Ref GoogleCredentials diff --git a/template.yaml b/template.yaml index 126bdc3e..8834b299 100644 --- a/template.yaml +++ b/template.yaml @@ -8,13 +8,14 @@ Metadata: default: Which pattern are we deploying? The app with secrets, the app but using existing secrets, or just the secrets. Parameters: - DeployPattern + - CrossStackConfig - Label: default: AWS IAM Identity Center (Successor to AWS Single Sign-On) Parameters: - SCIMEndpointUrl - SCIMEndpointAccessToken - - Region - IdentityStoreID + - Region - Label: default: Google Workspace Credentials Parameters: @@ -24,6 +25,7 @@ Metadata: default: Sync Configuration Parameters: - SyncMethod + - PrecacheOrgUnits - GoogleUserMatch - GoogleGroupMatch - IgnoreUsers @@ -36,13 +38,18 @@ Metadata: default: "Lambda Configuration" Parameters: - FunctionName - - LogLevel - - LogFormat - - TimeOut - ScheduleExpression - SyncSuspended - DryRun - - PrecacheOrgUnits + - MemorySize + - TimeOut + - Label: + default: Log Configuration + Parameters: + - LogLevel + - LogFormat + - LogRetention + AWS::ServerlessRepo::Application: Name: ssosync @@ -55,7 +62,7 @@ Metadata: Labels: [serverless, sso, lambda, scim] HomePageUrl: https://github.com/awslabs/ssosync # Update the semantic version and run sam publish to publish a new version of your app - SemanticVersion: 1.0.0-rc.10 + SemanticVersion: 1.0.0 # best practice is to use git tags for each release and link to the version tag as your source code URL SourceCodeUrl: https://github.com/awslabs/ssosync @@ -98,6 +105,36 @@ Parameters: Default: rate(15 minutes) AllowedPattern: '(?!.*\s)|rate\((?:(?:(?:\d{2,}+|[2-9]) (?:minutes|hours|days))|(?:1 (?:minute|hour|day)))\)|(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))*|\*)\))' + LogRetention: + Type: Number + Description: | + How long to retain log for, if you want to retain indefinitely, select 0 + Default: 0 + AllowedValues: + - 0 + - 1 + - 3 + - 5 + - 7 + - 14 + - 30 + - 60 + - 90 + - 120 + - 150 + - 180 + - 365 + - 400 + - 545 + - 731 + - 1096 + - 1827 + - 2192 + - 2557 + - 2922 + - 3288 + - 3653 + LogLevel: Type: String Description: | @@ -288,16 +325,32 @@ Conditions: - !Equals - !Ref SyncMethod - "users_groups" + SetLogRetention: !Not + - !Equals + - !Ref LogRetention + - 0 SetDryRun: !Equals - !Ref DryRun - "dry-run" SetSyncSuspended: !Equals - !Ref SyncSuspended - "sync" - OnSchedule: !Not - - !Equals + OnSchedule: !And + - !Not + - !Equals - !Ref ScheduleExpression - "" + - !Or + - !Equals + - !Ref DeployPattern + - "App for cross-account" + - !Equals + - !Ref DeployPattern + - "App + secrets" + - !Equals + - !Ref DeployPattern + - "App only" + CreateFunction: !Or - !Equals - !Ref DeployPattern @@ -462,7 +515,6 @@ Resources: Type: AWS::IAM::Role Condition: LocalSecrets Properties: - RoleName: SSOSyncAppRole AssumeRolePolicyDocument: Version: 2012-10-17 Statement: @@ -525,7 +577,6 @@ Resources: Type: AWS::IAM::Role Condition: RemoteSecrets Properties: - RoleName: SSOSyncAppRole AssumeRolePolicyDocument: Version: 2012-10-17 Statement: @@ -620,18 +671,35 @@ Resources: IGNORE_GROUPS: !If [SetIgnoreGroups, !Ref IgnoreGroups, !Ref AWS::NoValue] IGNORE_USERS: !If [SetIgnoreUsers, !Ref IgnoreUsers, !Ref AWS::NoValue] INCLUDE_GROUPS: !If [SetIncludeGroups, !Ref IncludeGroups, !Ref AWS::NoValue] - DRY_RUN: !If [SetDryRun, 'True', 'False'] - SYNC_SUSPENDED: !If [SetSyncSuspended, 'True', 'False'] + DRY_RUN: !If [SetDryRun, 'True', !Ref AWS::NoValue] + SYNC_SUSPENDED: !If [SetSyncSuspended, 'True', !Ref AWS::NoValue] PRECACHE_ORG_UNITS: !If [DisablePrecaching, !Ref AWS::NoValue, !Ref PrecacheOrgUnits] - Events: - SyncScheduledEvent: - Type: Schedule - Name: AWSSyncSchedule - Properties: - Enabled: !If [OnSchedule, true, true] - Schedule: !If [OnSchedule, !Ref ScheduleExpression, AWS::NoValue] - + PermissionForEventsToInvokeLambda: + Type: AWS::Lambda::Permission + Condition: OnSchedule + Properties: + FunctionName: !Ref SSOSyncFunction + Action: "lambda:InvokeFunction" + Principal: "events.amazonaws.com" + SourceArn: !GetAtt SyncScheduledEvent.Arn + + SyncScheduledEvent: + Type: AWS::Events::Rule + Condition: OnSchedule + Properties: + Name: AWSSyncSchedule + ScheduleExpression: !Ref ScheduleExpression + Targets: + - Id: SSOSync + Arn: !GetAtt SSOSyncFunction.Arn + + LogGroup: + Type: AWS::Logs::LogGroup + Condition: CreateFunction + Properties: + LogGroupName: !Sub /aws/lambda/${SSOSyncFunction} + RetentionInDays: !If [SetLogRetention, !Ref LogRetention, !Ref AWS::NoValue] KeyAlias: Type: AWS::KMS::Alias From eba2b1424200e54e941ff5768a82b630f4d5fc51 Mon Sep 17 00:00:00 2001 From: Chris Pates Date: Thu, 21 May 2026 14:19:17 +0100 Subject: [PATCH 15/27] fixes --- README.md | 7 +++++-- template.yaml | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d9dae385..71bcb0bc 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ SSO Sync is a powerful CLI tool and AWS Lambda function that enables automatic p ## ✨ Key Features -- **🔄 Bi-directional Sync**: Supports both `groups` and `users_groups` sync methods +- **🔄 Uni-directional Sync**: Supports both `groups` and `users_groups` sync methods - **🎯 Advanced Filtering**: Flexible user and group filtering with Google API query parameters - **🛡️ Dry-Run Mode**: Test synchronization without making actual changes - **⚡ High Performance**: Built with AWS SDK v2 for improved performance and reliability @@ -25,6 +25,9 @@ SSO Sync is a powerful CLI tool and AWS Lambda function that enables automatic p - **📈 Scalable**: Supports large directories with user caching and pagination ## 🚀 Quick Start +Use one of the quick start templates to simplify the your deployment. Use these templates, with the **Sync from Git** option, to make updates to your deployment far simpler. + +## 🚀 Step-by-Step Guide Want to dive straight in? Try this [hands-on lab](https://catalog.workshops.aws/control-tower/en-US/authentication-authorization/google-workspace) from the AWS Control Tower Workshop. The lab guides you through the complete setup process for both AWS and Google Workspace using the recommended Lambda deployment from the [AWS Serverless Application Repository](https://console.aws.amazon.com/lambda/home#/create/app?applicationId=arn:aws:serverlessrepo:us-east-2:004480582608:applications/SSOSync). @@ -455,4 +458,4 @@ This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENS --- -**Need help?** Check out our [Issues](https://github.com/awslabs/ssosync/issues) page or start a [Discussion](https://github.com/awslabs/ssosync/discussions). \ No newline at end of file +**Need help?** Check out our [Issues](https://github.com/awslabs/ssosync/issues) page or start a [Discussion](https://github.com/awslabs/ssosync/discussions). diff --git a/template.yaml b/template.yaml index 8834b299..7a566b0b 100644 --- a/template.yaml +++ b/template.yaml @@ -696,10 +696,10 @@ Resources: LogGroup: Type: AWS::Logs::LogGroup - Condition: CreateFunction + Condition: SetLogRetention Properties: LogGroupName: !Sub /aws/lambda/${SSOSyncFunction} - RetentionInDays: !If [SetLogRetention, !Ref LogRetention, !Ref AWS::NoValue] + RetentionInDays: !Ref LogRetention KeyAlias: Type: AWS::KMS::Alias From 6f271e1e53db9d23d7679df96d8bb4ec4ddd1a02 Mon Sep 17 00:00:00 2001 From: Chris Pates Date: Thu, 21 May 2026 14:21:17 +0100 Subject: [PATCH 16/27] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 71bcb0bc..1ce8ac15 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ SSO Sync is a powerful CLI tool and AWS Lambda function that enables automatic p - **📈 Scalable**: Supports large directories with user caching and pagination ## 🚀 Quick Start -Use one of the quick start templates to simplify the your deployment. Use these templates, with the **Sync from Git** option, to make updates to your deployment far simpler. +Use one of the [quick start](/quick-start/) templates to simplify the your deployment. Use these templates, with the **Sync from Git** option, to make updates to your deployment far simpler. ## 🚀 Step-by-Step Guide From 561a1c691b1ade4d5b6fa8824d0cb28815c2210d Mon Sep 17 00:00:00 2001 From: Chris Pates Date: Thu, 21 May 2026 19:21:56 +0100 Subject: [PATCH 17/27] Investigating Change in non-Delegated behavior --- cicd/cloudformation/testing.yaml | 6 ++++-- cicd/tests/account_execution/cli/buildspec.yml | 2 ++ cicd/tests/account_execution/lambda/buildspec.yml | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/cicd/cloudformation/testing.yaml b/cicd/cloudformation/testing.yaml index 02a131a5..1848efca 100644 --- a/cicd/cloudformation/testing.yaml +++ b/cicd/cloudformation/testing.yaml @@ -689,7 +689,8 @@ Resources: Type: LINUX_CONTAINER EnvironmentVariables: - Name: ExpectedExitState - Value: !If [DeployNonDelegated, 1, 0] + #Value: !If [DeployNonDelegated, 1, 0] + Value: 0 Artifacts: Name: SSOSync Type: CODEPIPELINE @@ -720,7 +721,8 @@ Resources: Type: LINUX_CONTAINER EnvironmentVariables: - Name: ExpectedResponse - Value: !If [DeployNonDelegated, "true", "false"] + # Value: !If [DeployNonDelegated, "true", "false"] + Value: "false" Artifacts: Name: SSOSync Type: CODEPIPELINE diff --git a/cicd/tests/account_execution/cli/buildspec.yml b/cicd/tests/account_execution/cli/buildspec.yml index 1db5489a..b3b13ac0 100644 --- a/cicd/tests/account_execution/cli/buildspec.yml +++ b/cicd/tests/account_execution/cli/buildspec.yml @@ -24,6 +24,8 @@ phases: - ./ssosync -t "${SCIMAccessToken}" -e "${SCIMEndpointUrl}" -u "${GoogleAdminEmail}" -i "${IdentityStoreID}" -r "${Region}" -s "groups" -g "name:AWS*"; ExitState=$? + - echo "${ExitState}" + - echo "${ExpectedExitState}" - | if expr "${ExitState}" : "${ExpectedExitState}" >/dev/null; then echo "We got what we expected" diff --git a/cicd/tests/account_execution/lambda/buildspec.yml b/cicd/tests/account_execution/lambda/buildspec.yml index b924ffab..fd224dc2 100644 --- a/cicd/tests/account_execution/lambda/buildspec.yml +++ b/cicd/tests/account_execution/lambda/buildspec.yml @@ -15,6 +15,8 @@ phases: # Execute the lambda - FunctionError=$(aws lambda invoke --function-name "SSOSyncFunction" response.json | jq 'has("FunctionError")') + - echo "${FunctionError}" + - echo "${ExpectedResponse}" - | if expr "${FunctionError}" : "${ExpectedResponse}" >/dev/null; then echo "We got what we expected" From 2a5dc8618674b498dcbf2b6ead01d2b848b3d85c Mon Sep 17 00:00:00 2001 From: Chris Pates Date: Thu, 21 May 2026 20:46:25 +0100 Subject: [PATCH 18/27] Revert "Investigating Change in non-Delegated behavior" This reverts commit 561a1c691b1ade4d5b6fa8824d0cb28815c2210d. --- cicd/cloudformation/testing.yaml | 6 ++---- cicd/tests/account_execution/cli/buildspec.yml | 2 -- cicd/tests/account_execution/lambda/buildspec.yml | 2 -- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/cicd/cloudformation/testing.yaml b/cicd/cloudformation/testing.yaml index 1848efca..02a131a5 100644 --- a/cicd/cloudformation/testing.yaml +++ b/cicd/cloudformation/testing.yaml @@ -689,8 +689,7 @@ Resources: Type: LINUX_CONTAINER EnvironmentVariables: - Name: ExpectedExitState - #Value: !If [DeployNonDelegated, 1, 0] - Value: 0 + Value: !If [DeployNonDelegated, 1, 0] Artifacts: Name: SSOSync Type: CODEPIPELINE @@ -721,8 +720,7 @@ Resources: Type: LINUX_CONTAINER EnvironmentVariables: - Name: ExpectedResponse - # Value: !If [DeployNonDelegated, "true", "false"] - Value: "false" + Value: !If [DeployNonDelegated, "true", "false"] Artifacts: Name: SSOSync Type: CODEPIPELINE diff --git a/cicd/tests/account_execution/cli/buildspec.yml b/cicd/tests/account_execution/cli/buildspec.yml index b3b13ac0..1db5489a 100644 --- a/cicd/tests/account_execution/cli/buildspec.yml +++ b/cicd/tests/account_execution/cli/buildspec.yml @@ -24,8 +24,6 @@ phases: - ./ssosync -t "${SCIMAccessToken}" -e "${SCIMEndpointUrl}" -u "${GoogleAdminEmail}" -i "${IdentityStoreID}" -r "${Region}" -s "groups" -g "name:AWS*"; ExitState=$? - - echo "${ExitState}" - - echo "${ExpectedExitState}" - | if expr "${ExitState}" : "${ExpectedExitState}" >/dev/null; then echo "We got what we expected" diff --git a/cicd/tests/account_execution/lambda/buildspec.yml b/cicd/tests/account_execution/lambda/buildspec.yml index fd224dc2..b924ffab 100644 --- a/cicd/tests/account_execution/lambda/buildspec.yml +++ b/cicd/tests/account_execution/lambda/buildspec.yml @@ -15,8 +15,6 @@ phases: # Execute the lambda - FunctionError=$(aws lambda invoke --function-name "SSOSyncFunction" response.json | jq 'has("FunctionError")') - - echo "${FunctionError}" - - echo "${ExpectedResponse}" - | if expr "${FunctionError}" : "${ExpectedResponse}" >/dev/null; then echo "We got what we expected" From 93f385a64fe332cbd2a0d5d46f83e5d3464d818c Mon Sep 17 00:00:00 2001 From: Chris Pates Date: Thu, 21 May 2026 20:46:36 +0100 Subject: [PATCH 19/27] Update sync.go --- internal/sync.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/sync.go b/internal/sync.go index 7da1c5e3..fae3e208 100644 --- a/internal/sync.go +++ b/internal/sync.go @@ -765,7 +765,7 @@ func (s *syncGSuite) getGoogleGroupsAndUsers(queryGroups string, queryUsers stri "queryGroups": queryGroups, }).Info("fetching groups") - gGroups, err := s.google.GetGroups(queryGroups) + googleGroups, err := s.google.GetGroups(queryGroups) if err != nil { log.WithFields(log.Fields{ "func": funcName, @@ -779,7 +779,7 @@ func (s *syncGSuite) getGoogleGroupsAndUsers(queryGroups string, queryUsers stri "func": funcName, }).Info("filter groups by ignoreList") - for _, g := range gGroups { + for _, g := range googleGroups { log.WithFields(log.Fields{ "func": funcName, "group": g, From 77a9357d1732f4e1360b5584d7812836d2c4f2d2 Mon Sep 17 00:00:00 2001 From: Chris Pates Date: Fri, 22 May 2026 10:37:54 +0100 Subject: [PATCH 20/27] Update workflows Adjust SemanticVersion update sed regex Update node actions to latest versions --- .github/workflows/main.yml | 2 +- .github/workflows/release.yml | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 35ac016f..9a266e17 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Go uses: actions/setup-go@v5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d741e590..0165bbca 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,12 +14,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Update version strings run: | - SemanticVersion=$GITHUB_REF - SemanticVersion="${SemanticVersion//v/}" + SemanticVersion="${GITHUB_REF#refs/tags/v}" sed -i '' -E "s/SemanticVersion: [0-9.]+/SemanticVersion: $SemanticVersion/g" quick-start/*.yaml template.yaml - name: Setup Go From 03578cb13e1bb6445f82e8ccc85552a351e1f950 Mon Sep 17 00:00:00 2001 From: Chris Pates Date: Fri, 22 May 2026 16:57:46 +0100 Subject: [PATCH 21/27] Update release.yml --- .github/workflows/release.yml | 36 ++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0165bbca..e7c89f7d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,7 +49,7 @@ jobs: needs: [ test ] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Unshallow run: git fetch --prune --unshallow @@ -71,3 +71,37 @@ jobs: run: make release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + quickstart: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v6 + + - name: Set SEMANTIC_VERSION + run : | + echo "SEMANTIC_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV + echo "env.SemanticVersion: ${{ env.SEMANTIC_VERSION }}" + + - name: 'SAR Template' + uses: lfreleng-actions/file-sed-regex-action@main + with: + flags: '-i -E' + regex: 's/SemanticVersion: [0-9.]+/SemanticVersion: ${{ env.SEMANTIC_VERSION }}/g' + path: 'template.yaml' + - name: 'Single Account' + uses: lfreleng-actions/file-sed-regex-action@main + with: + flags: '-i -E' + regex: 's/SemanticVersion: [0-9.]+/SemanticVersion: ${{ env.SEMANTIC_VERSION }}/g' + path: 'quick-start/single-account.yaml' + - name: Show Changes + run: | + grep SemanticVersion quick-start/*.yaml template.yaml + + - name: Commit updated Quick Start Templates + uses: stefanzweifel/git-auto-commit-action@v7 + with: + branch: ${{ github.event.release.target_commitish }} + commit_message: Update Quick Start templates + file_pattern: ./ From 94a0c052e8c2c1a7e244c18b5b28f6dee90d6762 Mon Sep 17 00:00:00 2001 From: Chris Pates Date: Fri, 22 May 2026 22:08:02 +0100 Subject: [PATCH 22/27] Update release.yml --- .github/workflows/release.yml | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e7c89f7d..bce85403 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -78,30 +78,23 @@ jobs: - name: Check out code uses: actions/checkout@v6 - - name: Set SEMANTIC_VERSION + - name: Fetch SEMANTIC_VERSION run : | echo "SEMANTIC_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV + - name: Show SemanticVersion echo "env.SemanticVersion: ${{ env.SEMANTIC_VERSION }}" - - name: 'SAR Template' - uses: lfreleng-actions/file-sed-regex-action@main - with: - flags: '-i -E' - regex: 's/SemanticVersion: [0-9.]+/SemanticVersion: ${{ env.SEMANTIC_VERSION }}/g' - path: 'template.yaml' - - name: 'Single Account' - uses: lfreleng-actions/file-sed-regex-action@main - with: - flags: '-i -E' - regex: 's/SemanticVersion: [0-9.]+/SemanticVersion: ${{ env.SEMANTIC_VERSION }}/g' - path: 'quick-start/single-account.yaml' - - name: Show Changes + - name: Show current SemanticVersion + run: grep SemanticVersion quick-start/*.yaml + - name: Update Templates run: | - grep SemanticVersion quick-start/*.yaml template.yaml + git grep -lr -e 'SemanticVersion' -- quick-start | xargs sed -i -E 's/SemanticVersion: [0-9.]+/SemanticVersion: ${{ env.SEMANTIC_VERSION }}/g' + - name: Show Templates SemanticVersion + run: grep SemanticVersion quick-start/*.yaml - name: Commit updated Quick Start Templates uses: stefanzweifel/git-auto-commit-action@v7 with: branch: ${{ github.event.release.target_commitish }} commit_message: Update Quick Start templates - file_pattern: ./ + file_pattern: quick-start From 033445c07e3800a919202f0906278d045aba0a57 Mon Sep 17 00:00:00 2001 From: Chris Pates Date: Fri, 22 May 2026 22:13:32 +0100 Subject: [PATCH 23/27] Update release.yml --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bce85403..d05571cc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -82,7 +82,7 @@ jobs: run : | echo "SEMANTIC_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV - name: Show SemanticVersion - echo "env.SemanticVersion: ${{ env.SEMANTIC_VERSION }}" + run: echo "env.SemanticVersion: ${{ env.SEMANTIC_VERSION }}" - name: Show current SemanticVersion run: grep SemanticVersion quick-start/*.yaml From 6a79623291b71b3fdda7eb8f448abc01de72b4a1 Mon Sep 17 00:00:00 2001 From: Chris Pates Date: Fri, 22 May 2026 22:19:00 +0100 Subject: [PATCH 24/27] Update release.yml --- .github/workflows/release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d05571cc..df083d92 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -82,7 +82,8 @@ jobs: run : | echo "SEMANTIC_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV - name: Show SemanticVersion - run: echo "env.SemanticVersion: ${{ env.SEMANTIC_VERSION }}" + run: | + echo "env.SemanticVersion: ${{ env.SEMANTIC_VERSION }}" - name: Show current SemanticVersion run: grep SemanticVersion quick-start/*.yaml From 43bd79a7bb2f2d26ad6b0953aa8989be8e67dc33 Mon Sep 17 00:00:00 2001 From: Chris Pates Date: Fri, 22 May 2026 22:22:33 +0100 Subject: [PATCH 25/27] Update release.yml --- .github/workflows/release.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index df083d92..875fcb06 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,11 +16,6 @@ jobs: - name: Check out code uses: actions/checkout@v6 - - name: Update version strings - run: | - SemanticVersion="${GITHUB_REF#refs/tags/v}" - sed -i '' -E "s/SemanticVersion: [0-9.]+/SemanticVersion: $SemanticVersion/g" quick-start/*.yaml template.yaml - - name: Setup Go uses: actions/setup-go@v5 with: From 46af4cfef88aa9ce7c1248575cf7172f546476d9 Mon Sep 17 00:00:00 2001 From: Chris Pates Date: Fri, 22 May 2026 22:34:56 +0100 Subject: [PATCH 26/27] Split workflows --- .github/workflows/quickstarts.yml | 39 +++++++++++++++++++++++++++++++ .github/workflows/release.yml | 28 ---------------------- 2 files changed, 39 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/quickstarts.yml diff --git a/.github/workflows/quickstarts.yml b/.github/workflows/quickstarts.yml new file mode 100644 index 00000000..bc46ec64 --- /dev/null +++ b/.github/workflows/quickstarts.yml @@ -0,0 +1,39 @@ +# .github/workflows/quickstarts.yaml +name: quickstarts + +on: release + +permissions: + contents: write + +jobs: + quickstart: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v6 + + - name: Fetch SEMANTIC_VERSION + run : | + echo "SEMANTIC_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV + + - name: Show SemanticVersion + run: | + echo "env.SemanticVersion: ${{ env.SEMANTIC_VERSION }}" + + - name: Show current SemanticVersion + run: grep SemanticVersion quick-start/*.yaml + + - name: Update Templates + run: | + git grep -lr -e 'SemanticVersion' -- quick-start | xargs sed -i -E 's/SemanticVersion: [0-9.]+/SemanticVersion: ${{ env.SEMANTIC_VERSION }}/g' + + - name: Show Templates SemanticVersion + run: grep SemanticVersion quick-start/*.yaml + + - name: Commit updated Quick Start Templates + uses: stefanzweifel/git-auto-commit-action@v7 + with: + branch: ${{ github.event.release.target_commitish }} + commit_message: Update Quick Start templates + file_pattern: quick-start diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 875fcb06..ecaf3499 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,31 +66,3 @@ jobs: run: make release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - quickstart: - runs-on: ubuntu-latest - steps: - - name: Check out code - uses: actions/checkout@v6 - - - name: Fetch SEMANTIC_VERSION - run : | - echo "SEMANTIC_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV - - name: Show SemanticVersion - run: | - echo "env.SemanticVersion: ${{ env.SEMANTIC_VERSION }}" - - - name: Show current SemanticVersion - run: grep SemanticVersion quick-start/*.yaml - - name: Update Templates - run: | - git grep -lr -e 'SemanticVersion' -- quick-start | xargs sed -i -E 's/SemanticVersion: [0-9.]+/SemanticVersion: ${{ env.SEMANTIC_VERSION }}/g' - - name: Show Templates SemanticVersion - run: grep SemanticVersion quick-start/*.yaml - - - name: Commit updated Quick Start Templates - uses: stefanzweifel/git-auto-commit-action@v7 - with: - branch: ${{ github.event.release.target_commitish }} - commit_message: Update Quick Start templates - file_pattern: quick-start From 7e0586d95b120ae8487b4af6d6fd6af113c16941 Mon Sep 17 00:00:00 2001 From: Evgeny Zislis Date: Tue, 2 Jun 2026 13:02:10 +0300 Subject: [PATCH 27/27] feat: wildcard support in ignore-users and ignore-groups Allow `*` in --ignore-users and --ignore-groups entries. Only `*` is special (matches any, possibly empty, substring); every other character including `?`, `[`, `]`, and `\` is matched literally. The matcher is hand-rolled rather than using path.Match so patterns cannot pick up extra metacharacter semantics. The ignore predicate is also consulted when picking AWS-only entries to delete, so a pattern like `*@contractors.example.com` protects matching AWS users from deletion even when they have no Google counterpart. Patterns are trimmed once and cached on the syncGSuite; empty entries are dropped. Inspired by awslabs/ssosync#306, rewritten to avoid path.Match, drop the redundant double-check in the deletion loops, and remove per-call debug log spam. --- README.md | 18 ++-- internal/sync.go | 99 ++++++++++++++++------ internal/sync_test.go | 186 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 271 insertions(+), 32 deletions(-) create mode 100644 internal/sync_test.go diff --git a/README.md b/README.md index 1ce8ac15..ca294be5 100644 --- a/README.md +++ b/README.md @@ -187,13 +187,21 @@ SSO Sync requires configuration from both Google Workspace and AWS sides. --group-match "*" \ --sync-method users_groups -# Ignore specific users/groups +# Ignore specific users/groups (entries may use '*' as a wildcard) ./ssosync \ --group-match "*" \ - --ignore-users "service@company.com,bot@company.com" \ - --ignore-groups "temp-group@company.com" + --ignore-users "service@company.com,bot@company.com,*@contractors.example.com" \ + --ignore-groups "temp-group@company.com,AWS*" ``` +Ignored users and groups are protected in both directions: they are skipped +when Google is enumerated, and they are also skipped when picking AWS +entries to delete. This means a pattern like `*@contractors.example.com` +acts as a deletion guard for accounts that exist only in AWS. + +Only `*` is special — it matches any (possibly empty) substring. Every +other character, including `?`, `[`, `]`, and `\`, is matched literally. + ### Environment Variables All CLI flags can be set via environment variables with the `SSOSYNC_` prefix: @@ -222,8 +230,8 @@ export SSOSYNC_DRY_RUN="true" | `--sync-method` | `SSOSYNC_SYNC_METHOD` | Sync method (`groups` or `users_groups`) | `groups` | | `--group-match` | `SSOSYNC_GROUP_MATCH` | Google Groups filter query | `*` | | `--user-match` | `SSOSYNC_USER_MATCH` | Google Users filter query | `""` | -| `--ignore-users` | `SSOSYNC_IGNORE_USERS` | Comma-separated list of users to ignore | `[]` | -| `--ignore-groups` | `SSOSYNC_IGNORE_GROUPS` | Comma-separated list of groups to ignore | `[]` | +| `--ignore-users` | `SSOSYNC_IGNORE_USERS` | Comma-separated list of users to ignore (supports `*` wildcard) | `[]` | +| `--ignore-groups` | `SSOSYNC_IGNORE_GROUPS` | Comma-separated list of groups to ignore (supports `*` wildcard) | `[]` | | `--include-groups` | `SSOSYNC_INCLUDE_GROUPS` | Include only these groups (users_groups method only) | `[]` | | `--dry-run` | `SSOSYNC_DRY_RUN` | Enable dry-run mode | `false` | | `--log-level` | `SSOSYNC_LOG_LEVEL` | Log level (debug, info, warn, error) | `info` | diff --git a/internal/sync.go b/internal/sync.go index fae3e208..9c178980 100644 --- a/internal/sync.go +++ b/internal/sync.go @@ -52,10 +52,10 @@ type syncGSuite struct { cfg *config.Config identityStore interfaces.IdentityStoreAPI - users map[string]*interfaces.User - ignoreUsersSet map[string]struct{} - ignoreGroupsSet map[string]struct{} - includeGroupsSet map[string]struct{} + users map[string]*interfaces.User + ignoreUserPatterns []string + ignoreGroupPatterns []string + includeGroupsSet map[string]struct{} } // New will create a new SyncGSuite object @@ -381,8 +381,8 @@ func (s *syncGSuite) SyncGroupsUsers(queryGroups string, queryUsers string) erro } // create list of changes by operations - addAWSUsers, delAWSUsers, updateAWSUsers, _ := getUserOperations(awsUsers, googleUsers) - addAWSGroups, delAWSGroups, equalAWSGroups := getGroupOperations(awsGroups, googleGroups) + addAWSUsers, delAWSUsers, updateAWSUsers, _ := getUserOperations(awsUsers, googleUsers, s.ignoreUser) + addAWSGroups, delAWSGroups, equalAWSGroups := getGroupOperations(awsGroups, googleGroups, s.ignoreGroup) log.Info("syncing changes") @@ -886,8 +886,11 @@ func (s *syncGSuite) getGoogleGroupsAndUsers(queryGroups string, queryUsers stri return gGroups, gUsers, gGroupsUsers, nil } -// getGroupOperations returns the groups of AWS that must be added, deleted and are equals -func getGroupOperations(awsGroups []*interfaces.Group, googleGroups []*admin.Group) (add []*interfaces.Group, delete []*interfaces.Group, equals []*interfaces.Group) { +// getGroupOperations returns the groups of AWS that must be added, deleted and are equals. +// ignoreGroup, if non-nil, is consulted before queueing an AWS-only group for deletion; +// matching groups are skipped so wildcard ignore patterns protect AWS groups that have +// no counterpart in Google. +func getGroupOperations(awsGroups []*interfaces.Group, googleGroups []*admin.Group, ignoreGroup func(string) bool) (add []*interfaces.Group, delete []*interfaces.Group, equals []*interfaces.Group) { log.Debug("getGroupOperations()") awsMap := make(map[string]*interfaces.Group) @@ -915,6 +918,10 @@ func getGroupOperations(awsGroups []*interfaces.Group, googleGroups []*admin.Gro // AWS Groups not found in Google for _, awsGroup := range awsGroups { if _, found := googleMap[awsGroup.DisplayName]; !found { + if ignoreGroup != nil && ignoreGroup(awsGroup.DisplayName) { + log.WithField("group", awsGroup.DisplayName).Info("skip delete: ignore list") + continue + } log.WithField("awsGroup", awsGroup).Debug("delete") delete = append(delete, aws.NewGroup(awsGroup.DisplayName)) } @@ -923,8 +930,11 @@ func getGroupOperations(awsGroups []*interfaces.Group, googleGroups []*admin.Gro return add, delete, equals } -// getUserOperations returns the users of AWS that must be added, deleted, updated and are equals -func getUserOperations(awsUsers []*interfaces.User, googleUsers []*admin.User) (add []*interfaces.User, delete []*interfaces.User, update []*interfaces.User, equals []*interfaces.User) { +// getUserOperations returns the users of AWS that must be added, deleted, updated and are equals. +// ignoreUser, if non-nil, is consulted before queueing an AWS-only user for deletion; +// matching users are skipped so wildcard ignore patterns protect AWS users that have no +// counterpart in Google. +func getUserOperations(awsUsers []*interfaces.User, googleUsers []*admin.User, ignoreUser func(string) bool) (add []*interfaces.User, delete []*interfaces.User, update []*interfaces.User, equals []*interfaces.User) { log.Debug("getUserOperations()") awsMap := make(map[string]*interfaces.User) @@ -968,6 +978,10 @@ func getUserOperations(awsUsers []*interfaces.User, googleUsers []*admin.User) ( // Google Users founds and not in aws for _, awsUser := range awsUsers { if _, found := googleMap[awsUser.Username]; !found { + if ignoreUser != nil && ignoreUser(awsUser.Username) { + log.WithField("user", awsUser.Username).Info("skip delete: ignore list") + continue + } log.WithFields(log.Fields{ "awsUser": awsUser, }).Debug("delete") @@ -1130,33 +1144,64 @@ func DoSync(ctx context.Context, cfg *config.Config) error { } func (s *syncGSuite) ignoreUser(name string) bool { - if s.cfg.IgnoreUsers == nil { - return false + if s.ignoreUserPatterns == nil { + s.ignoreUserPatterns = trimPatterns(s.cfg.IgnoreUsers) + } + return matchesAny(s.ignoreUserPatterns, strings.TrimSpace(name)) +} + +func (s *syncGSuite) ignoreGroup(name string) bool { + if s.ignoreGroupPatterns == nil { + s.ignoreGroupPatterns = trimPatterns(s.cfg.IgnoreGroups) } + return matchesAny(s.ignoreGroupPatterns, strings.TrimSpace(name)) +} - if s.ignoreUsersSet == nil { - s.ignoreUsersSet = make(map[string]struct{}, len(s.cfg.IgnoreUsers)) - for _, u := range s.cfg.IgnoreUsers { - s.ignoreUsersSet[u] = struct{}{} +// trimPatterns returns a copy of patterns with surrounding whitespace removed +// and empty entries dropped. It returns a non-nil (possibly empty) slice so the +// lazy-init check (`== nil`) doesn't retry on every call when the config has +// no patterns. +func trimPatterns(patterns []string) []string { + out := make([]string, 0, len(patterns)) + for _, p := range patterns { + if p = strings.TrimSpace(p); p != "" { + out = append(out, p) } } - _, exists := s.ignoreUsersSet[name] - return exists + return out } -func (s *syncGSuite) ignoreGroup(name string) bool { - if s.cfg.IgnoreGroups == nil { - return false +func matchesAny(patterns []string, name string) bool { + for _, p := range patterns { + if matchIgnorePattern(p, name) { + return true + } } + return false +} - if s.ignoreGroupsSet == nil { - s.ignoreGroupsSet = make(map[string]struct{}, len(s.cfg.IgnoreGroups)) - for _, g := range s.cfg.IgnoreGroups { - s.ignoreGroupsSet[g] = struct{}{} +// matchIgnorePattern reports whether name matches pattern. The only special +// character is '*', which matches any (possibly empty) substring. Every other +// character — including '?', '[', ']', and '\' — is matched literally. The +// matcher is deliberately narrower than path.Match / filepath.Match so that +// patterns cannot accidentally pick up extra metacharacter semantics. +func matchIgnorePattern(pattern, name string) bool { + if !strings.Contains(pattern, "*") { + return pattern == name + } + parts := strings.Split(pattern, "*") + if !strings.HasPrefix(name, parts[0]) { + return false + } + name = name[len(parts[0]):] + for _, p := range parts[1 : len(parts)-1] { + i := strings.Index(name, p) + if i < 0 { + return false } + name = name[i+len(p):] } - _, exists := s.ignoreGroupsSet[name] - return exists + return strings.HasSuffix(name, parts[len(parts)-1]) } func (s *syncGSuite) includeGroup(name string) bool { diff --git a/internal/sync_test.go b/internal/sync_test.go new file mode 100644 index 00000000..678dc251 --- /dev/null +++ b/internal/sync_test.go @@ -0,0 +1,186 @@ +// Copyright (c) 2020, Amazon.com, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 + +package internal + +import ( + "testing" + + "github.com/awslabs/ssosync/internal/config" + "github.com/awslabs/ssosync/internal/interfaces" + "github.com/stretchr/testify/assert" + admin "google.golang.org/api/admin/directory/v1" +) + +func TestMatchIgnorePattern(t *testing.T) { + cases := []struct { + pattern string + name string + want bool + }{ + // Exact match (no wildcard). + {"foo@example.com", "foo@example.com", true}, + {"foo@example.com", "bar@example.com", false}, + + // Leading wildcard (the *@domain case this PR is about). + {"*@example.com", "alice@example.com", true}, + {"*@example.com", "bob@example.com", true}, + {"*@example.com", "alice@other.com", false}, + {"*@example.com", "@example.com", true}, // '*' matches empty + {"*@example.com", "example.com", false}, // missing '@' + + // Trailing wildcard. + {"svc-*", "svc-prod", true}, + {"svc-*", "svc-", true}, + {"svc-*", "other", false}, + + // Middle wildcard. + {"a*z", "az", true}, + {"a*z", "abcz", true}, + {"a*z", "abc", false}, + {"a*z", "zab", false}, + + // Multiple wildcards. + {"*-temp-*", "x-temp-y", true}, + {"*-temp-*", "-temp-", true}, + {"*-temp-*", "temp", false}, + + // Edge patterns. + {"*", "anything", true}, + {"*", "", true}, + {"**", "anything", true}, + + // Other characters from path.Match are literal here. + {"foo?bar", "foo?bar", true}, // '?' is literal, not single-char wildcard + {"foo?bar", "fooXbar", false}, + {"[abc]", "[abc]", true}, // brackets are literal + {"[abc]", "a", false}, + {`a\*b`, `a\Xb`, true}, // pattern is literal 'a\' + wildcard + 'b' + {`a\*b`, "aXb", false}, // backslash is required literally + {`a\*b`, `a\b`, true}, // '*' matches the empty string + } + + for _, c := range cases { + got := matchIgnorePattern(c.pattern, c.name) + assert.Equalf(t, c.want, got, "matchIgnorePattern(%q, %q)", c.pattern, c.name) + } +} + +func TestIgnoreUserWildcard(t *testing.T) { + s := &syncGSuite{ + cfg: &config.Config{ + IgnoreUsers: []string{ + " *@internal.example.com ", // whitespace should be trimmed + "exact@example.com", + "", // empty entries should be dropped + }, + }, + } + + cases := []struct { + name string + want bool + }{ + {"alice@internal.example.com", true}, + {"bob@internal.example.com", true}, + {"exact@example.com", true}, + {"alice@external.example.com", false}, + {" alice@internal.example.com ", true}, // input whitespace trimmed + } + for _, c := range cases { + assert.Equalf(t, c.want, s.ignoreUser(c.name), "ignoreUser(%q)", c.name) + } +} + +func TestIgnoreGroupWildcard(t *testing.T) { + s := &syncGSuite{ + cfg: &config.Config{ + IgnoreGroups: []string{"AWS*", "exact-group"}, + }, + } + cases := []struct { + name string + want bool + }{ + {"AWSAccountFactory", true}, + {"AWSServiceRole", true}, + {"exact-group", true}, + {"OtherGroup", false}, + } + for _, c := range cases { + assert.Equalf(t, c.want, s.ignoreGroup(c.name), "ignoreGroup(%q)", c.name) + } +} + +func TestGetGroupOperationsRespectsIgnore(t *testing.T) { + ignore := func(name string) bool { + return name == "AWSReserved" || name == "ManualGroup" + } + + awsGroups := []*interfaces.Group{ + {DisplayName: "GroupInBoth"}, + {DisplayName: "AWSReserved"}, // ignored, AWS-only + {DisplayName: "ManualGroup"}, // ignored, AWS-only + {DisplayName: "DeleteMe"}, // not ignored, AWS-only -> delete + } + googleGroups := []*admin.Group{ + {Name: "GroupInBoth"}, + {Name: "NewGroup"}, + } + + add, del, eq := getGroupOperations(awsGroups, googleGroups, ignore) + + assert.Len(t, add, 1) + assert.Equal(t, "NewGroup", add[0].DisplayName) + assert.Len(t, del, 1) + assert.Equal(t, "DeleteMe", del[0].DisplayName) + assert.Len(t, eq, 1) + assert.Equal(t, "GroupInBoth", eq[0].DisplayName) +} + +func TestGetUserOperationsRespectsIgnore(t *testing.T) { + ignore := func(name string) bool { + return name == "ignored@example.com" + } + + awsUsers := []*interfaces.User{ + {Username: "user@example.com", Active: true}, + {Username: "delete-me@example.com"}, + {Username: "ignored@example.com"}, + } + googleUsers := []*admin.User{ + { + PrimaryEmail: "user@example.com", + Suspended: false, + Name: &admin.UserName{GivenName: "Test", FamilyName: "User"}, + }, + { + PrimaryEmail: "new-user@example.com", + Suspended: false, + Name: &admin.UserName{GivenName: "New", FamilyName: "User"}, + }, + } + + add, del, _, _ := getUserOperations(awsUsers, googleUsers, ignore) + + assert.Len(t, add, 1) + assert.Equal(t, "new-user@example.com", add[0].Username) + assert.Len(t, del, 1) + assert.Equal(t, "delete-me@example.com", del[0].Username) +} + +func TestGetOperationsNilIgnore(t *testing.T) { + // Passing nil should not panic and should not skip any deletes. + awsGroups := []*interfaces.Group{{DisplayName: "OnlyInAWS"}} + _, del, _ := getGroupOperations(awsGroups, nil, nil) + assert.Len(t, del, 1) + + awsUsers := []*interfaces.User{{Username: "only@aws"}} + _, delU, _, _ := getUserOperations(awsUsers, nil, nil) + assert.Len(t, delU, 1) +}