Skip to content

Commit 4f47941

Browse files
feat: add user custom schemas (#6)
## what - Add the concept of `custom_schemas` to `var.users` and `googleworkspace_user.defaults` - Custom schemas enable SSO between gsuite and AWS using SAML. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added support for specifying custom schemas for users, allowing multiple custom schema entries with configurable names and values. - **Documentation** - Updated documentation to describe the new custom schemas attribute and its usage. - **Tests** - Introduced new tests to validate the handling of user custom schemas and ensure correct behavior. - **Bug Fixes** - Added validation to ensure all custom schema values are JSON encodable, preventing configuration errors. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 8e540d1 commit 4f47941

File tree

6 files changed

+184
-3
lines changed

6 files changed

+184
-3
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ provider "googleworkspace" {
5151

5252
```hcl
5353
module "googleworkspace_users_groups" {
54-
source = "git::https://github.com/masterpointio/terraform-googleworkspace-users-groups-automation.git"
54+
source = "masterpointio/users-groups-automation/googleworkspace"
55+
version = "X.X.X"
5556
5657
users = {
5758
"first.last@example.com" = {
@@ -133,7 +134,7 @@ module "googleworkspace_users_groups" {
133134
| <a name="input_stage"></a> [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no |
134135
| <a name="input_tags"></a> [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).<br/>Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no |
135136
| <a name="input_tenant"></a> [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no |
136-
| <a name="input_users"></a> [users](#input\_users) | List of users | <pre>map(object({<br/> # addresses<br/> aliases : optional(list(string), []),<br/> archived : optional(bool, false),<br/> change_password_at_next_login : optional(bool),<br/> # custom_schemas<br/> # emails<br/> # external_ids<br/> family_name : string,<br/> given_name : string,<br/> groups : optional(map(object({<br/> role : optional(string, "MEMBER"),<br/> delivery_settings : optional(string, "ALL_MAIL"),<br/> type : optional(string, "USER"),<br/> })), {}),<br/> # ims<br/> include_in_global_address_list : optional(bool),<br/> ip_allowlist : optional(bool),<br/> is_admin : optional(bool),<br/> # keywords<br/> # languages<br/> # locations<br/> org_unit_path : optional(string),<br/> # organizations<br/> # phones<br/> # posix_accounts<br/> primary_email : string,<br/> recovery_email : optional(string),<br/> recovery_phone : optional(string),<br/> # relations<br/> # ssh_public_keys<br/> suspended : optional(bool),<br/> # timeouts<br/> # websites<br/><br/> # User attributes with unique constraints<br/><br/> # password and hash_function<br/> # If a hashFunction is specified, the password must be a valid hash key.<br/> # If it's not specified, the password should be in clear text and between<br/> # 8–100 ASCII characters.<br/> # https://developers.google.com/workspace/admin/directory/v1/guides/manage-users<br/> hash_function : optional(string),<br/> password : optional(string),<br/> }))</pre> | `{}` | no |
137+
| <a name="input_users"></a> [users](#input\_users) | List of users | <pre>map(object({<br/> # addresses<br/> aliases : optional(list(string), []),<br/> archived : optional(bool, false),<br/> change_password_at_next_login : optional(bool),<br/> custom_schemas : optional(list(object({<br/> schema_name : string,<br/> schema_values : optional(map(string), {}),<br/> })), []),<br/> # emails<br/> # external_ids<br/> family_name : string,<br/> given_name : string,<br/> groups : optional(map(object({<br/> role : optional(string, "MEMBER"),<br/> delivery_settings : optional(string, "ALL_MAIL"),<br/> type : optional(string, "USER"),<br/> })), {}),<br/> # ims<br/> include_in_global_address_list : optional(bool),<br/> ip_allowlist : optional(bool),<br/> is_admin : optional(bool),<br/> # keywords<br/> # languages<br/> # locations<br/> org_unit_path : optional(string),<br/> # organizations<br/> # phones<br/> # posix_accounts<br/> primary_email : string,<br/> recovery_email : optional(string),<br/> recovery_phone : optional(string),<br/> # relations<br/> # ssh_public_keys<br/> suspended : optional(bool),<br/> # timeouts<br/> # websites<br/><br/> # User attributes with unique constraints<br/><br/> # password and hash_function<br/> # If a hashFunction is specified, the password must be a valid hash key.<br/> # If it's not specified, the password should be in clear text and between<br/> # 8–100 ASCII characters.<br/> # https://developers.google.com/workspace/admin/directory/v1/guides/manage-users<br/> hash_function : optional(string),<br/> password : optional(string),<br/> }))</pre> | `{}` | no |
137138

138139
## Outputs
139140

examples/complete/main.tf

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,20 @@ module "googleworkspace" {
2929
role = "MEMBER"
3030
}
3131
}
32+
custom_schemas = [
33+
{
34+
schema_name = "AWS_SSO_for_Client123"
35+
schema_values = {
36+
"Role" = "[\"arn:aws:iam::111111111111:role/GoogleAppsAdmin\",\"arn:aws:iam::111111111111:saml-provider/GoogleApps\"]"
37+
}
38+
},
39+
{
40+
schema_name = "AWS_SSO_for_Client456"
41+
schema_values = {
42+
"Role" = "[\"arn:aws:iam::222222222222:role/xyz-identity-reader,arn:aws:iam::222222222222:saml-provider/xyz-identity-acme-gsuite\", \"arn:aws:iam::222222222222:role/xyz-identity-admin,arn:aws:iam::222222222222:saml-provider/xyz-identity-acme-gsuite\"]"
43+
}
44+
}
45+
]
3246
}
3347
}
3448

examples/import-existing-org/users.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ first.last@example.com:
99
primary_email: first.last@example.com
1010
family_name: Last
1111
given_name: First
12+
custom_schemas:
13+
- schema_name: AWS_SSO_for_Client123
14+
schema_values:
15+
Role: '["arn:aws:iam::111111111111:role/GoogleAppsAdmin","arn:aws:iam::111111111111:saml-provider/GoogleApps"]'
16+
- schema_name: AWS_SSO_for_Client456
17+
schema_values:
18+
Role: '["arn:aws:iam::222222222222:role/xyz-identity-reader,arn:aws:iam::222222222222:saml-provider/xyz-identity-acme-gsuite", "arn:aws:iam::222222222222:role/xyz-identity-admin,arn:aws:iam::222222222222:saml-provider/xyz-identity-acme-gsuite"]'
1219

1320
admin_first.admin_last@example.com:
1421
<<: *default_user

main.tf

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ resource "googleworkspace_user" "defaults" {
4242
recovery_phone = each.value.recovery_phone
4343
suspended = each.value.suspended
4444

45+
dynamic "custom_schemas" {
46+
for_each = lookup(each.value, "custom_schemas", [])
47+
content {
48+
schema_name = custom_schemas.value.schema_name
49+
schema_values = custom_schemas.value.schema_values
50+
}
51+
}
52+
4553
lifecycle {
4654
ignore_changes = [
4755
languages,

tests/variables_users.tftest.hcl

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,102 @@ run "hash_function_can_be_null_with_password_set" {
179179
}
180180
}
181181

182+
# -----------------------------------------------------------------------------
183+
# --- validate custom schemas
184+
# -----------------------------------------------------------------------------
185+
186+
187+
run "custom_schemas_success" {
188+
command = apply
189+
190+
providers = {
191+
googleworkspace = googleworkspace.mock
192+
}
193+
194+
variables {
195+
users = {
196+
"first.last@example.com" = {
197+
primary_email = "first.last@example.com"
198+
family_name = "Last"
199+
given_name = "First"
200+
custom_schemas = [
201+
{
202+
schema_name = "AWS_SSO_for_Client123"
203+
schema_values = {
204+
"Role" = "[\"arn:aws:iam::111111111111:role/GoogleAppsAdmin\",\"arn:aws:iam::111111111111:saml-provider/GoogleApps\"]"
205+
}
206+
},
207+
{
208+
schema_name = "AWS_SSO_for_Client456"
209+
schema_values = {
210+
"Role" = "[\"arn:aws:iam::222222222222:role/xyz-identity-reader,arn:aws:iam::222222222222:saml-provider/xyz-identity-acme-gsuite\", \"arn:aws:iam::222222222222:role/xyz-identity-admin,arn:aws:iam::222222222222:saml-provider/xyz-identity-acme-gsuite\"]"
211+
}
212+
}
213+
]
214+
}
215+
}
216+
}
217+
218+
# test that the rendered value is an encoded json string
219+
assert {
220+
condition = googleworkspace_user.defaults["first.last@example.com"].custom_schemas[1].schema_values["Role"] == "[\"arn:aws:iam::222222222222:role/xyz-identity-reader,arn:aws:iam::222222222222:saml-provider/xyz-identity-acme-gsuite\", \"arn:aws:iam::222222222222:role/xyz-identity-admin,arn:aws:iam::222222222222:saml-provider/xyz-identity-acme-gsuite\"]"
221+
error_message = "Expected rendered value to be encoded json string, got: ${googleworkspace_user.defaults["first.last@example.com"].custom_schemas[1].schema_values["Role"]}"
222+
}
223+
}
224+
225+
run "custom_schemas_output_verification" {
226+
command = apply
227+
228+
providers = {
229+
googleworkspace = googleworkspace.mock
230+
}
231+
232+
variables {
233+
users = {
234+
"schema.test@example.com" = {
235+
primary_email = "schema.test@example.com"
236+
family_name = "Test"
237+
given_name = "Schema"
238+
custom_schemas = [
239+
{
240+
schema_name = "AWS_SSO_Test"
241+
schema_values = {
242+
"Role" = "[\"arn:aws:iam::111111111111:role/TestRole\"]"
243+
"OtherKey" = "OtherValue"
244+
}
245+
},
246+
{
247+
schema_name = "Artibitrarily_Data"
248+
schema_values = {
249+
"ABC" = "123"
250+
"DEF" = "456"
251+
}
252+
}
253+
]
254+
}
255+
}
256+
}
257+
258+
assert {
259+
condition = googleworkspace_user.defaults["schema.test@example.com"].custom_schemas[0].schema_name == "AWS_SSO_Test"
260+
error_message = "Expected schema name 'AWS_SSO_Test', got: ${googleworkspace_user.defaults["schema.test@example.com"].custom_schemas[0].schema_name}"
261+
}
262+
263+
assert {
264+
condition = googleworkspace_user.defaults["schema.test@example.com"].custom_schemas[0].schema_values["Role"] == "[\"arn:aws:iam::111111111111:role/TestRole\"]"
265+
error_message = "Expected Role value to match, got: ${googleworkspace_user.defaults["schema.test@example.com"].custom_schemas[0].schema_values["Role"]}"
266+
}
267+
268+
assert {
269+
condition = googleworkspace_user.defaults["schema.test@example.com"].custom_schemas[0].schema_values["OtherKey"] == "OtherValue"
270+
error_message = "Expected OtherKey value to be 'OtherValue', got: ${googleworkspace_user.defaults["schema.test@example.com"].custom_schemas[0].schema_values["OtherKey"]}"
271+
}
272+
273+
assert {
274+
condition = googleworkspace_user.defaults["schema.test@example.com"].custom_schemas[1].schema_name == "Artibitrarily_Data"
275+
error_message = "Expected custom_schemas's input order to match output order"
276+
}
277+
}
182278

183279
# -----------------------------------------------------------------------------
184280
# --- validate groups
@@ -309,6 +405,46 @@ run "group_member_type_default_success" {
309405
}
310406
}
311407

408+
run "group_member_role_and_type_are_captilized" {
409+
command = apply
410+
411+
providers = {
412+
googleworkspace = googleworkspace.mock
413+
}
414+
415+
variables {
416+
users = {
417+
"user.type@example.com" = {
418+
primary_email = "user.type@example.com"
419+
family_name = "Type"
420+
given_name = "User"
421+
groups = {
422+
"test-group" = {
423+
role = "member"
424+
type = "user"
425+
}
426+
}
427+
}
428+
}
429+
groups = {
430+
"test-group" = {
431+
name = "Test Group"
432+
email = "test-group@example.com"
433+
}
434+
}
435+
}
436+
437+
assert {
438+
condition = googleworkspace_group_member.user_to_groups["test-group@example.com/user.type@example.com"].role == "MEMBER"
439+
error_message = "Expected role to be capitalized to 'MEMBER', got: ${googleworkspace_group_member.user_to_groups["test-group@example.com/user.type@example.com"].role}"
440+
}
441+
442+
assert {
443+
condition = googleworkspace_group_member.user_to_groups["test-group@example.com/user.type@example.com"].type == "USER"
444+
error_message = "Expected type to be capitalized to 'USER', got: ${googleworkspace_group_member.user_to_groups["test-group@example.com/user.type@example.com"].type}"
445+
}
446+
}
447+
312448
run "group_member_type_invalid" {
313449
command = plan
314450

variables.tf

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ variable "users" {
88
aliases : optional(list(string), []),
99
archived : optional(bool, false),
1010
change_password_at_next_login : optional(bool),
11-
# custom_schemas
11+
custom_schemas : optional(list(object({
12+
schema_name : string,
13+
schema_values : optional(map(string), {}),
14+
})), []),
1215
# emails
1316
# external_ids
1417
family_name : string,
@@ -109,6 +112,18 @@ variable "users" {
109112
]))
110113
error_message = "group type must be either 'USER', 'GROUP', or 'CUSTOMER'"
111114
}
115+
116+
# validate that schema_values's values can be JSON encoded (required by Google Workspace provider)
117+
validation {
118+
condition = alltrue(flatten([
119+
for user in var.users : [
120+
for schema in user.custom_schemas : [
121+
for key, value in schema.schema_values : can(jsonencode(value))
122+
]
123+
]
124+
]))
125+
error_message = "All values in custom schema values must be JSON encodable strings"
126+
}
112127
}
113128

114129
variable "groups" {

0 commit comments

Comments
 (0)