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 955391255ed04e504fbd36b9cfef61413011986c Mon Sep 17 00:00:00 2001 From: Evgeny Zislis Date: Tue, 20 Jan 2026 12:28:49 +0200 Subject: [PATCH 27/27] fix: skip users without email to prevent sync failures - Add validation to skip users with empty PrimaryEmail in SyncUsers - Add validation in getGoogleGroupsAndUsers for user matches - Add validation for group members without email - Log warnings for skipped users with their names for tracking Fixes fatal error when syncing users without email addresses --- internal/sync.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/internal/sync.go b/internal/sync.go index fae3e208..3d6bd11c 100644 --- a/internal/sync.go +++ b/internal/sync.go @@ -157,6 +157,11 @@ func (s *syncGSuite) SyncUsers(query string) error { } for _, u := range googleUsers { + if u.PrimaryEmail == "" { + log.WithField("name", u.Name.FullName).Warn("skipping user without email") + continue + } + if s.ignoreUser(u.PrimaryEmail) { continue } @@ -652,6 +657,14 @@ func (s *syncGSuite) getGoogleGroupsAndUsers(queryGroups string, queryUsers stri "user": u, }).Debug("process user") + if u.PrimaryEmail == "" { + log.WithFields(log.Fields{ + "func": funcName, + "name": u.Name.FullName, + }).Warn("skipping user without email") + continue + } + // Remove any users that should be ignored if s.ignoreUser(u.PrimaryEmail) { log.WithFields(log.Fields{ @@ -735,6 +748,14 @@ func (s *syncGSuite) getGoogleGroupsAndUsers(queryGroups string, queryUsers stri "user": u, }).Debug("process user") + if u.PrimaryEmail == "" { + log.WithFields(log.Fields{ + "func": funcName, + "name": u.Name.FullName, + }).Warn("skipping user without email") + continue + } + if _, found := gUniqUsers[u.PrimaryEmail]; !found { log.WithFields(log.Fields{ "func": funcName, @@ -841,6 +862,14 @@ func (s *syncGSuite) getGoogleGroupsAndUsers(queryGroups string, queryUsers stri }).Error("nil user") continue } + if m.PrimaryEmail == "" { + log.WithFields(log.Fields{ + "func": funcName, + "group.Id": g.Id, + "name": m.Name.FullName, + }).Warn("skipping group member without email") + continue + } if _, found := gUniqUsers[m.PrimaryEmail]; !found { log.WithFields(log.Fields{ "func": funcName,