Skip to content

Commit 7ce70a4

Browse files
authored
Lambda Function: Addition of Dry-Run, syncing of Suspended Users and Customer Precaching scope (#254)
* Use IncludeDerivedMembership in group memberships * Improve error messages * Make handling of suspended user configurable * Improve logging for precaching and group/user selection. * Add Templates parameters * Improve logging of EnvVars
1 parent a6cdbc6 commit 7ce70a4

File tree

10 files changed

+191
-104
lines changed

10 files changed

+191
-104
lines changed

cicd/account_execution/staging/stack.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,5 +73,7 @@ Resources:
7373
- '}}'
7474
SyncMethod: groups
7575
GoogleGroupMatch: !Ref GroupMatch
76-
LogLevel: info
76+
SyncSuspended: sync
77+
PrecacheOrgUnits: '/Catering/,/Maintenance/Vending Machines'
78+
LogLevel: debug
7779
LogFormat: json

cicd/build/package/buildspec.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ phases:
2828
# Create staging & release variants of the template.yaml
2929
- cp template.yaml staging.yaml
3030
- patch staging.yaml cicd/build/package/staging.patch
31-
- sam package --no-progressbar --template-file staging.yaml --s3-bucket ${S3Bucket} --output-template-file packaged-staging.yaml
31+
- sam package --force-upload --no-progressbar --template-file staging.yaml --s3-bucket ${S3Bucket} --output-template-file packaged-staging.yaml
3232

3333
- cp template.yaml release.yaml
3434
- patch release.yaml cicd/build/package/release.patch
35-
- sam package --no-progressbar --template-file release.yaml --s3-bucket ${S3Bucket} --output-template-file packaged-release.yaml
35+
- sam package --force-upload --no-progressbar --template-file release.yaml --s3-bucket ${S3Bucket} --output-template-file packaged-release.yaml
3636

3737
- ls packaged-staging.yaml
3838
- ls packaged-release.yaml

cicd/build/package/release.patch

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
--- template.yaml 2023-10-27 16:34:16
2-
+++ release.yaml 2023-10-27 16:34:37
3-
@@ -36,7 +36,7 @@
4-
- ScheduleExpression
1+
--- ../../../template.yaml 2025-08-14 14:37:47
2+
+++ release.yaml 2025-08-14 15:04:51
3+
@@ -45,7 +45,7 @@
4+
- PrecacheOrgUnits
55

66
AWS::ServerlessRepo::Application:
77
- Name: ssosync

cicd/build/package/staging.patch

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
--- template.yaml 2023-10-30 14:21:20
2-
+++ staging.yaml 2023-10-30 14:21:59
3-
@@ -38,7 +38,7 @@
4-
- ScheduleExpression
1+
--- ../../../template.yaml 2025-08-14 14:37:47
2+
+++ staging.yaml 2025-08-14 15:04:41
3+
@@ -45,7 +45,7 @@
4+
- PrecacheOrgUnits
55

66
AWS::ServerlessRepo::Application:
77
- Name: ssosync

cicd/cloudformation/developer.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,7 @@ Resources:
612612
- 'cloudformation:DeleteChangeSet'
613613
- 'cloudformation:DescribeChangeSet'
614614
- 'cloudformation:SetStackPolicy'
615+
- 'cloudformation:DescribeStackEvents'
615616
Resource:
616617
- '*'
617618
Effect: Allow

cmd/root.go

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,12 @@ func initConfig() {
191191
// config logger
192192
logConfig(cfg)
193193

194+
if cfg.SyncSuspended {
195+
cfg.UserFilter = " isArchived=false"
196+
} else {
197+
cfg.UserFilter = " isSuspended=false isArchived=false"
198+
}
199+
194200
}
195201

196202
func configLambda() {
@@ -237,62 +243,66 @@ func configLambda() {
237243
unwrap = os.Getenv("LOG_LEVEL")
238244
if len([]rune(unwrap)) != 0 {
239245
cfg.LogLevel = unwrap
240-
log.WithField("LogLevel", unwrap).Debug("from EnvVar")
246+
log.WithField("LogLevel", unwrap).Info("from EnvVar")
241247
}
242248

243249
unwrap = os.Getenv("LOG_FORMAT")
244250
if len([]rune(unwrap)) != 0 {
245251
cfg.LogFormat = unwrap
246-
log.WithField("LogFormay", unwrap).Debug("from EnvVar")
252+
log.WithField("LogFormat", unwrap).Info("from EnvVar")
247253
}
248254

249255
unwrap = os.Getenv("SYNC_METHOD")
250256
if len([]rune(unwrap)) != 0 {
251257
cfg.SyncMethod = unwrap
252-
log.WithField("SyncMethod", unwrap).Debug("from EnvVar")
258+
log.WithField("SyncMethod", unwrap).Info("from EnvVar")
253259
}
254260

255261
unwrap = os.Getenv("USER_MATCH")
256262
if len([]rune(unwrap)) != 0 {
257263
cfg.UserMatch = unwrap
258-
log.WithField("UserMatch", unwrap).Debug("from EnvVar")
264+
log.WithField("UserMatch", unwrap).Info("from EnvVar")
259265
}
260266

261267
unwrap = os.Getenv("GROUP_MATCH")
262268
if len([]rune(unwrap)) != 0 {
263269
cfg.GroupMatch = unwrap
264-
log.WithField("GroupMatch", unwrap).Debug("from EnvVar")
270+
log.WithField("GroupMatch", unwrap).Info("from EnvVar")
265271
}
266272

267273
unwrap = os.Getenv("IGNORE_GROUPS")
268274
if len([]rune(unwrap)) != 0 {
269275
cfg.IgnoreGroups = strings.Split(unwrap, ",")
270-
log.WithField("IgnoreGroups", unwrap).Debug("from EnvVar")
276+
log.WithField("IgnoreGroups", unwrap).Info("from EnvVar")
271277
}
272278

273279
unwrap = os.Getenv("IGNORE_USERS")
274280
if len([]rune(unwrap)) != 0 {
275281
cfg.IgnoreUsers = strings.Split(unwrap, ",")
276-
log.WithField("IgnoreUsers", unwrap).Debug("from EnvVar")
282+
log.WithField("IgnoreUsers", unwrap).Info("from EnvVar")
277283
}
278284

279285
unwrap = os.Getenv("INCLUDE_GROUPS")
280286
if len([]rune(unwrap)) != 0 {
281287
cfg.IncludeGroups = strings.Split(unwrap, ",")
282-
log.WithField("IncludeGroups", unwrap).Debug("from EnvVar")
288+
log.WithField("IncludeGroups", unwrap).Info("from EnvVar")
283289
}
284290

285-
unwrap = os.Getenv("PRECACHE_QUERIES")
291+
unwrap = os.Getenv("PRECACHE_ORG_UNITS")
286292
if len([]rune(unwrap)) != 0 {
287-
cfg.PrecacheQueries = unwrap
288-
log.WithField("PrecacheQueries", unwrap).Debug("from EnvVar")
289-
}
293+
cfg.PrecacheOrgUnits = strings.Split(unwrap, ",")
294+
log.WithField("PrecacheOrgUnits", unwrap).Info("from EnvVar")
295+
}
290296

291297
unwrap = os.Getenv("DRY_RUN")
292-
if len([]rune(unwrap)) != 0 {
293-
cfg.DryRun = strings.ToLower(unwrap) == "true"
294-
log.WithField("DryRun", unwrap).Debug("from EnvVar")
295-
}
298+
log.WithField("DRY_RUN", unwrap).Info("EnvVar")
299+
cfg.DryRun = strings.ToLower(unwrap) == "true"
300+
log.WithField("DryRun", cfg.DryRun).Info("config")
301+
302+
unwrap = os.Getenv("SYNC_SUSPENDED")
303+
log.WithField("SYNC_SUSPENDED", unwrap).Info("EnvVar")
304+
cfg.SyncSuspended = strings.ToLower(unwrap) == "true"
305+
log.WithField("SyncSuspended", cfg.SyncSuspended).Info("config")
296306
}
297307

298308
func addFlags(cmd *cobra.Command, cfg *config.Config) {
@@ -301,6 +311,7 @@ func addFlags(cmd *cobra.Command, cfg *config.Config) {
301311
rootCmd.PersistentFlags().StringVarP(&cfg.LogFormat, "log-format", "", config.DefaultLogFormat, "log format")
302312
rootCmd.PersistentFlags().StringVarP(&cfg.LogLevel, "log-level", "", config.DefaultLogLevel, "log level")
303313
rootCmd.PersistentFlags().BoolVarP(&cfg.DryRun, "dry-run", "n", false, "Do *not* perform any actions, instead list what would happen")
314+
rootCmd.PersistentFlags().BoolVarP(&cfg.SyncSuspended, "suspended", "", config.DefaultSyncSuspended, "included suspended users and their group memberships when syncing")
304315
rootCmd.Flags().StringVarP(&cfg.SCIMAccessToken, "access-token", "t", "", "AWS SSO SCIM API Access Token")
305316
rootCmd.Flags().StringVarP(&cfg.SCIMEndpoint, "endpoint", "e", "", "AWS SSO SCIM API Endpoint")
306317
rootCmd.Flags().StringVarP(&cfg.GoogleCredentials, "google-credentials", "c", config.DefaultGoogleCredentials, "path to Google Workspace credentials file")
@@ -309,11 +320,12 @@ func addFlags(cmd *cobra.Command, cfg *config.Config) {
309320
rootCmd.Flags().StringSliceVar(&cfg.IgnoreGroups, "ignore-groups", []string{}, "ignores these Google Workspace groups")
310321
rootCmd.Flags().StringSliceVar(&cfg.IncludeGroups, "include-groups", []string{}, "include only these Google Workspace groups, NOTE: only works when --sync-method 'users_groups'")
311322
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")
312-
rootCmd.Flags().StringVarP(&cfg.GroupMatch, "group-match", "g", "*", "Google Workspace Groups filter query parameter, example: 'name:Admin*' 'name=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")
323+
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")
313324
rootCmd.Flags().StringVarP(&cfg.SyncMethod, "sync-method", "s", config.DefaultSyncMethod, "Sync method to use (users_groups|groups)")
314325
rootCmd.Flags().StringVarP(&cfg.Region, "region", "r", "", "AWS Region where AWS SSO is enabled")
315326
rootCmd.Flags().StringVarP(&cfg.IdentityStoreID, "identity-store-id", "i", "", "Identifier of Identity Store in AWS SSO")
316-
rootCmd.Flags().StringVarP(&cfg.PrecacheQueries, "precache-queries", "p", config.DefaultPrecacheQueries, "Google Workspace Users filter queries parameter, example: 'OrgUnitPath=/ isSuspend=false isArchived=false', to precache all users within that Org Unit Path. For query syntax and more examples see: https://developers.google.com/admin-sdk/directory/v1/guides/search-users. To disable and use caching on the fly, 'DISABLED'.")
327+
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'.")
328+
317329
}
318330

319331
func logConfig(cfg *config.Config) {

internal/config/config.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,13 @@ type Config struct {
3838
// IdentityStoreID is the ID of the identity store
3939
IdentityStoreID string `mapstructure:"identity_store_id"`
4040
// Precaching queries as a comma separated list of query strings
41-
PrecacheQueries string
41+
PrecacheOrgUnits []string
4242
// DryRun flag, when set to true, no change will be made in the Identity Store
4343
DryRun bool
44+
// sync suspended user, if true suspended user and their group memberships are sync'd into IAM Identity Center
45+
SyncSuspended bool
46+
// User filter string
47+
UserFilter string
4448
}
4549

4650
const (
@@ -54,8 +58,10 @@ const (
5458
DefaultGoogleCredentials = "credentials.json"
5559
// DefaultSyncMethod is the default sync method to use.
5660
DefaultSyncMethod = "groups"
57-
// DefaultPrecacheQueries
58-
DefaultPrecacheQueries = "OrgUnitPath=/ isSuspended=false isArchived=false"
61+
// DefaultPrecacheOrgUnits
62+
DefaultPrecacheOrgUnits = "/"
63+
// DefaultSyncSuspended
64+
DefaultSyncSuspended = false
5965
)
6066

6167
// New returns a new Config

internal/google/client.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import (
2727

2828
// Client is the Interface for the Client
2929
type Client interface {
30-
GetUsers(string) ([]*admin.User, error)
30+
GetUsers(string, string) ([]*admin.User, error)
3131
GetDeletedUsers() ([]*admin.User, error)
3232
GetGroups(string) ([]*admin.Group, error)
3333
GetGroupMembers(*admin.Group) ([]*admin.Member, error)
@@ -84,7 +84,7 @@ func (c *client) GetGroupMembers(g *admin.Group) ([]*admin.Member, error) {
8484
m := make([]*admin.Member, 0)
8585
var err error
8686

87-
err = c.service.Members.List(g.Id).Pages(context.TODO(), func(members *admin.Members) error {
87+
err = c.service.Members.List(g.Id).IncludeDerivedMembership(true).Pages(context.TODO(), func(members *admin.Members) error {
8888
if err != nil {
8989
return err
9090
}
@@ -108,7 +108,7 @@ func (c *client) GetGroupMembers(g *admin.Group) ([]*admin.Member, error) {
108108
// manager='[email protected]'
109109
// orgName=Engineering orgTitle:Manager
110110
// EmploymentData.projects:'GeneGnomes'
111-
func (c *client) GetUsers(query string) ([]*admin.User, error) {
111+
func (c *client) GetUsers(query string, filter string) ([]*admin.User, error) {
112112
u := make([]*admin.User, 0)
113113
var err error
114114

@@ -119,7 +119,7 @@ func (c *client) GetUsers(query string) ([]*admin.User, error) {
119119

120120
// If we have wildcard then fetch all users
121121
if query == "*" {
122-
err = c.service.Users.List().Customer("my_customer").Pages(c.ctx, func(users *admin.Users) error {
122+
err = c.service.Users.List().Query(filter).Customer("my_customer").Pages(c.ctx, func(users *admin.Users) error {
123123
if err != nil {
124124
return err
125125
}
@@ -133,7 +133,7 @@ func (c *client) GetUsers(query string) ([]*admin.User, error) {
133133

134134
// Then call the api one query at a time, appending to our list
135135
for _, subQuery := range queries {
136-
err = c.service.Users.List().Query(subQuery).Customer("my_customer").Pages(c.ctx, func(users *admin.Users) error {
136+
err = c.service.Users.List().Query(subQuery + filter).Customer("my_customer").Pages(c.ctx, func(users *admin.Users) error {
137137
if err != nil {
138138
return err
139139
}

0 commit comments

Comments
 (0)