Skip to content

Commit d9ab40b

Browse files
authored
Feature multi select (#176)
* Implement multi-pattern support for groups and users * adjust regex for multi-pattern * Allow - and ' ' in names * UPDATE envVar creation logic for Match * Update template.yaml * Update template.yaml * Adding support for '*' to sync all and empty to sync nothing. * Improvements to Filtering UserMatch now considered in addition to GroupMatch. Improved filtering for external users proper handling of nested groups. * Improve logging Added dump of envVars Corrected copy&paste error in log message. * Adding user detail caching To reduce repeated calls to the directory api, prefetch all users and use when processing groups. * Update README.md
1 parent a2930a1 commit d9ab40b

File tree

5 files changed

+197
-79
lines changed

5 files changed

+197
-79
lines changed

README.md

+10-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
SSO Sync will run on any platform that Go can build for. It is available in the [AWS Serverless Application Repository](https://console.aws.amazon.com/lambda/home#/create/app?applicationId=arn:aws:serverlessrepo:us-east-2:004480582608:applications/SSOSync)
1212

1313
> [!CAUTION]
14-
> When using ssosync with an instance or IAM Identity Center integrated with AWS Control Tower. AWS Control Tower creates a number of groups and users (directly via the Identity Store API), when an external identity provider is configured these users and groups are can not be used to log in. However it is important to remember that because ssosync implemements a uni-directional sync it will make the IAM Identity Store match the subset of your Google Workspaces directory you specify, including removing these groups and users created by AWS Control Tower. There is a PFR [#88 - ssosync deletes Control Tower groups](https://github.com/awslabs/ssosync/issues/88) to implement an option to ignore these users and groups, hopefully this will be implemented in version 3.x.
14+
> When using ssosync with an instance of IAM Identity Center integrated with AWS Control Tower. AWS Control Tower creates a number of groups and users (directly via the Identity Store API), when an external identity provider is configured these users and groups are can not be used to log in. However it is important to remember that because ssosync implemements a uni-directional sync it will make the IAM Identity Store match the subset of your Google Workspaces directory you specify, including removing these groups and users created by AWS Control Tower. There is a PFR [#88 - ssosync deletes Control Tower groups](https://github.com/awslabs/ssosync/issues/88) to implement an option to ignore these users and groups, hopefully this will be implemented in version 3.x.
1515
1616
> [!WARNING]
1717
> There are breaking changes for versions `>= 0.02`
@@ -30,6 +30,13 @@ SSO Sync will run on any platform that Go can build for. It is available in the
3030
> [!IMPORTANT]
3131
> `>= 2.1.0` switched to using `provided.al2` powered by ARM64 instances.
3232
33+
> [!Info]
34+
> As of `v2.2.0` multiple query patterns are supported for both Group and User matching, simply separate each query with a `,`. For full sync of groups and/or users specify '*' in the relevant match field.
35+
> User match and group match can now be used in combination with the sync method of groups.
36+
> Nested groups will now be flattened into the top level groups.
37+
> external users are ignored.
38+
> User details are now cached to reduce the number of api calls and improve execution times on large directories.
39+
3340
## Why?
3441

3542
As per the [AWS SSO](https://aws.amazon.com/single-sign-on/) Homepage:
@@ -146,15 +153,15 @@ Flags:
146153
-e, --endpoint string AWS SSO SCIM API Endpoint
147154
-u, --google-admin string Google Workspace admin user email
148155
-c, --google-credentials string path to Google Workspace credentials file (default "credentials.json")
149-
-g, --group-match string Google Workspace Groups filter query parameter, example: 'name:Admin* email:aws-*', see: https://developers.google.com/admin-sdk/directory/v1/guides/search-groups
156+
-g, --group-match string 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
150157
-h, --help help for ssosync
151158
--ignore-groups strings ignores these Google Workspace groups
152159
--ignore-users strings ignores these Google Workspace users
153160
--include-groups strings include only these Google Workspace groups, NOTE: only works when --sync-method 'users_groups'
154161
--log-format string log format (default "text")
155162
--log-level string log level (default "info")
156163
-s, --sync-method string Sync method to use (users_groups|groups) (default "groups")
157-
-m, --user-match string Google Workspace Users filter query parameter, example: 'name:John* email:admin*', see: https://developers.google.com/admin-sdk/directory/v1/guides/search-users
164+
-m, --user-match string 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
158165
-v, --version version for ssosync
159166
-r, --region AWS region where identity store exists
160167
-i, --identity-store-id AWS Identity Store ID

cmd/root.go

+16-8
Original file line numberDiff line numberDiff line change
@@ -199,78 +199,86 @@ func configLambda() {
199199

200200
unwrap, err := secrets.GoogleAdminEmail(os.Getenv("GOOGLE_ADMIN"))
201201
if err != nil {
202-
log.Fatalf(errors.Wrap(err, "cannot read config").Error())
202+
log.Fatalf(errors.Wrap(err, "cannot read config: GOOGLE_ADMIN").Error())
203203
}
204204
cfg.GoogleAdmin = unwrap
205205

206206
unwrap, err = secrets.GoogleCredentials(os.Getenv("GOOGLE_CREDENTIALS"))
207207
if err != nil {
208-
log.Fatalf(errors.Wrap(err, "cannot read config").Error())
208+
log.Fatalf(errors.Wrap(err, "cannot read config: GOOGLE_CREDENTIALS").Error())
209209
}
210210
cfg.GoogleCredentials = unwrap
211211

212212
unwrap, err = secrets.SCIMAccessToken(os.Getenv("SCIM_ACCESS_TOKEN"))
213213
if err != nil {
214-
log.Fatalf(errors.Wrap(err, "cannot read config").Error())
214+
log.Fatalf(errors.Wrap(err, "cannot read config: SCIM_ACCESS_TOKEN").Error())
215215
}
216216
cfg.SCIMAccessToken = unwrap
217217

218218
unwrap, err = secrets.SCIMEndpointUrl(os.Getenv("SCIM_ENDPOINT"))
219219
if err != nil {
220-
log.Fatalf(errors.Wrap(err, "cannot read config").Error())
220+
log.Fatalf(errors.Wrap(err, "cannot read config: SCIM_ENDPOINT").Error())
221221
}
222222
cfg.SCIMEndpoint = unwrap
223223

224224
unwrap, err = secrets.Region(os.Getenv("REGION"))
225225
if err != nil {
226-
log.Fatalf(errors.Wrap(err, "cannot read config").Error())
226+
log.Fatalf(errors.Wrap(err, "cannot read config: REGION").Error())
227227
}
228228
cfg.Region = unwrap
229229

230230
unwrap, err = secrets.IdentityStoreID(os.Getenv("IDENTITY_STORE_ID"))
231231
if err != nil {
232-
log.Fatalf(errors.Wrap(err, "cannot read config").Error())
232+
log.Fatalf(errors.Wrap(err, "cannot read config: IDENTITY_STORE_ID").Error())
233233
}
234234
cfg.IdentityStoreID = unwrap
235235

236236
unwrap = os.Getenv("LOG_LEVEL")
237237
if len([]rune(unwrap)) != 0 {
238238
cfg.LogLevel = unwrap
239+
log.WithField("LogLevel", unwrap).Debug("from EnvVar")
239240
}
240241

241242
unwrap = os.Getenv("LOG_FORMAT")
242243
if len([]rune(unwrap)) != 0 {
243244
cfg.LogFormat = unwrap
245+
log.WithField("LogFormay", unwrap).Debug("from EnvVar")
244246
}
245247

246248
unwrap = os.Getenv("SYNC_METHOD")
247249
if len([]rune(unwrap)) != 0 {
248250
cfg.SyncMethod = unwrap
251+
log.WithField("SyncMethod", unwrap).Debug("from EnvVar")
249252
}
250253

251254
unwrap = os.Getenv("USER_MATCH")
252255
if len([]rune(unwrap)) != 0 {
253256
cfg.UserMatch = unwrap
257+
log.WithField("UserMatch", unwrap).Debug("from EnvVar")
254258
}
255259

256260
unwrap = os.Getenv("GROUP_MATCH")
257261
if len([]rune(unwrap)) != 0 {
258262
cfg.GroupMatch = unwrap
263+
log.WithField("GroupMatch", unwrap).Debug("from EnvVar")
259264
}
260265

261266
unwrap = os.Getenv("IGNORE_GROUPS")
262267
if len([]rune(unwrap)) != 0 {
263268
cfg.IgnoreGroups = strings.Split(unwrap, ",")
269+
log.WithField("IgnoreGroups", unwrap).Debug("from EnvVar")
264270
}
265271

266272
unwrap = os.Getenv("IGNORE_USERS")
267273
if len([]rune(unwrap)) != 0 {
268274
cfg.IgnoreUsers = strings.Split(unwrap, ",")
275+
log.WithField("IgnoreUsers", unwrap).Debug("from EnvVar")
269276
}
270277

271278
unwrap = os.Getenv("INCLUDE_GROUPS")
272279
if len([]rune(unwrap)) != 0 {
273280
cfg.IncludeGroups = strings.Split(unwrap, ",")
281+
log.WithField("IncludeGroups", unwrap).Debug("from EnvVar")
274282
}
275283

276284
}
@@ -287,8 +295,8 @@ func addFlags(cmd *cobra.Command, cfg *config.Config) {
287295
rootCmd.Flags().StringSliceVar(&cfg.IgnoreUsers, "ignore-users", []string{}, "ignores these Google Workspace users")
288296
rootCmd.Flags().StringSliceVar(&cfg.IgnoreGroups, "ignore-groups", []string{}, "ignores these Google Workspace groups")
289297
rootCmd.Flags().StringSliceVar(&cfg.IncludeGroups, "include-groups", []string{}, "include only these Google Workspace groups, NOTE: only works when --sync-method 'users_groups'")
290-
rootCmd.Flags().StringVarP(&cfg.UserMatch, "user-match", "m", "", "Google Workspace Users filter query parameter, example: 'name:John* email:admin*', see: https://developers.google.com/admin-sdk/directory/v1/guides/search-users")
291-
rootCmd.Flags().StringVarP(&cfg.GroupMatch, "group-match", "g", "", "Google Workspace Groups filter query parameter, example: 'name:Admin* email:aws-*', see: https://developers.google.com/admin-sdk/directory/v1/guides/search-groups")
298+
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")
299+
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")
292300
rootCmd.Flags().StringVarP(&cfg.SyncMethod, "sync-method", "s", config.DefaultSyncMethod, "Sync method to use (users_groups|groups)")
293301
rootCmd.Flags().StringVarP(&cfg.Region, "region", "r", "", "AWS Region where AWS SSO is enabled")
294302
rootCmd.Flags().StringVarP(&cfg.IdentityStoreID, "identity-store-id", "i", "", "Identifier of Identity Store in AWS SSO")

internal/google/client.go

+41-15
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package google
1717

1818
import (
1919
"context"
20+
"strings"
2021

2122
"golang.org/x/oauth2/google"
2223
admin "google.golang.org/api/admin/directory/v1"
@@ -100,20 +101,33 @@ func (c *client) GetUsers(query string) ([]*admin.User, error) {
100101
u := make([]*admin.User, 0)
101102
var err error
102103

103-
if query != "" {
104-
err = c.service.Users.List().Query(query).Customer("my_customer").Pages(c.ctx, func(users *admin.Users) error {
105-
u = append(u, users.Users...)
106-
return nil
107-
})
104+
// If we have an empty query, return nothing.
105+
if query == "" {
106+
return u, err
107+
}
108108

109-
} else {
110-
err = c.service.Users.List().Customer("my_customer").Pages(c.ctx, func(users *admin.Users) error {
109+
// If we have wildcard then fetch all users
110+
if query == "*" {
111+
err = c.service.Users.List().Customer("my_customer").Pages(c.ctx, func(users *admin.Users) error {
112+
u = append(u, users.Users...)
113+
return nil
114+
})
115+
return u, err
116+
}
117+
118+
// The Google api doesn't support multi-part queries, but we do so we need to split into an array of query strings
119+
queries := strings.Split(query, ",")
120+
121+
// Then call the api one query at a time, appending to our list
122+
for _, subQuery := range queries {
123+
err = c.service.Users.List().Query(subQuery).Customer("my_customer").Pages(c.ctx, func(users *admin.Users) error {
111124
u = append(u, users.Users...)
112125
return nil
113126
})
114127
}
115-
116128
return u, err
129+
130+
117131
}
118132

119133
// GetGroups will get the groups from Google's Admin API
@@ -133,17 +147,29 @@ func (c *client) GetGroups(query string) ([]*admin.Group, error) {
133147
g := make([]*admin.Group, 0)
134148
var err error
135149

136-
if query != "" {
137-
err = c.service.Groups.List().Customer("my_customer").Query(query).Pages(context.TODO(), func(groups *admin.Groups) error {
138-
g = append(g, groups.Groups...)
139-
return nil
140-
})
141-
} else {
150+
// If we have an empty query, then we are not looking for groups
151+
if query == "" {
152+
return g, err
153+
}
154+
155+
// If we have wildcard then fetch all groups
156+
if query == "*" {
142157
err = c.service.Groups.List().Customer("my_customer").Pages(context.TODO(), func(groups *admin.Groups) error {
158+
g = append(g, groups.Groups...)
159+
return nil
160+
})
161+
return g, err
162+
}
163+
164+
// The Google api doesn't support multi-part queries, but we do so we need to split into an array of query strings
165+
queries := strings.Split(query, ",")
166+
167+
// Then call the api one query at a time, appending to our list
168+
for _, subQuery := range queries {
169+
err = c.service.Groups.List().Customer("my_customer").Query(subQuery).Pages(context.TODO(), func(groups *admin.Groups) error {
143170
g = append(g, groups.Groups...)
144171
return nil
145172
})
146-
147173
}
148174
return g, err
149175
}

0 commit comments

Comments
 (0)