Skip to content

feat: add user custom schemas #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ provider "googleworkspace" {

```hcl
module "googleworkspace_users_groups" {
source = "git::https://github.com/masterpointio/terraform-googleworkspace-users-groups-automation.git"
source = "masterpointio/users-groups-automation/googleworkspace"
version = "X.X.X"

users = {
"first.last@example.com" = {
Expand Down Expand Up @@ -133,7 +134,7 @@ module "googleworkspace_users_groups" {
| <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 |
| <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 |
| <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 |
| <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 |
| <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 |

## Outputs

Expand Down
14 changes: 14 additions & 0 deletions examples/complete/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ module "googleworkspace" {
role = "MEMBER"
}
}
custom_schemas = [
{
schema_name = "AWS_SSO_for_Client123"
schema_values = {
"Role" = "[\"arn:aws:iam::111111111111:role/GoogleAppsAdmin\",\"arn:aws:iam::111111111111:saml-provider/GoogleApps\"]"
}
},
{
schema_name = "AWS_SSO_for_Client456"
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\"]"
}
}
]
}
}

Expand Down
7 changes: 7 additions & 0 deletions examples/import-existing-org/users.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ first.last@example.com:
primary_email: first.last@example.com
family_name: Last
given_name: First
custom_schemas:
- schema_name: AWS_SSO_for_Client123
schema_values:
Role: '["arn:aws:iam::111111111111:role/GoogleAppsAdmin","arn:aws:iam::111111111111:saml-provider/GoogleApps"]'
- schema_name: AWS_SSO_for_Client456
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"]'

admin_first.admin_last@example.com:
<<: *default_user
Expand Down
8 changes: 8 additions & 0 deletions main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ resource "googleworkspace_user" "defaults" {
recovery_phone = each.value.recovery_phone
suspended = each.value.suspended

dynamic "custom_schemas" {
for_each = lookup(each.value, "custom_schemas", [])
content {
schema_name = custom_schemas.value.schema_name
schema_values = custom_schemas.value.schema_values
}
}

lifecycle {
ignore_changes = [
languages,
Expand Down
136 changes: 136 additions & 0 deletions tests/variables_users.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,102 @@ run "hash_function_can_be_null_with_password_set" {
}
}

# -----------------------------------------------------------------------------
# --- validate custom schemas
# -----------------------------------------------------------------------------


run "custom_schemas_success" {
command = apply

providers = {
googleworkspace = googleworkspace.mock
}

variables {
users = {
"first.last@example.com" = {
primary_email = "first.last@example.com"
family_name = "Last"
given_name = "First"
custom_schemas = [
{
schema_name = "AWS_SSO_for_Client123"
schema_values = {
"Role" = "[\"arn:aws:iam::111111111111:role/GoogleAppsAdmin\",\"arn:aws:iam::111111111111:saml-provider/GoogleApps\"]"
}
},
{
schema_name = "AWS_SSO_for_Client456"
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\"]"
}
}
]
}
}
}

# test that the rendered value is an encoded json string
assert {
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\"]"
error_message = "Expected rendered value to be encoded json string, got: ${googleworkspace_user.defaults["first.last@example.com"].custom_schemas[1].schema_values["Role"]}"
}
}

run "custom_schemas_output_verification" {
command = apply

providers = {
googleworkspace = googleworkspace.mock
}

variables {
users = {
"schema.test@example.com" = {
primary_email = "schema.test@example.com"
family_name = "Test"
given_name = "Schema"
custom_schemas = [
{
schema_name = "AWS_SSO_Test"
schema_values = {
"Role" = "[\"arn:aws:iam::111111111111:role/TestRole\"]"
"OtherKey" = "OtherValue"
}
},
{
schema_name = "Artibitrarily_Data"
schema_values = {
"ABC" = "123"
"DEF" = "456"
}
}
]
}
}
}

assert {
condition = googleworkspace_user.defaults["schema.test@example.com"].custom_schemas[0].schema_name == "AWS_SSO_Test"
error_message = "Expected schema name 'AWS_SSO_Test', got: ${googleworkspace_user.defaults["schema.test@example.com"].custom_schemas[0].schema_name}"
}

assert {
condition = googleworkspace_user.defaults["schema.test@example.com"].custom_schemas[0].schema_values["Role"] == "[\"arn:aws:iam::111111111111:role/TestRole\"]"
error_message = "Expected Role value to match, got: ${googleworkspace_user.defaults["schema.test@example.com"].custom_schemas[0].schema_values["Role"]}"
}

assert {
condition = googleworkspace_user.defaults["schema.test@example.com"].custom_schemas[0].schema_values["OtherKey"] == "OtherValue"
error_message = "Expected OtherKey value to be 'OtherValue', got: ${googleworkspace_user.defaults["schema.test@example.com"].custom_schemas[0].schema_values["OtherKey"]}"
}

assert {
condition = googleworkspace_user.defaults["schema.test@example.com"].custom_schemas[1].schema_name == "Artibitrarily_Data"
error_message = "Expected custom_schemas's input order to match output order"
}
}

# -----------------------------------------------------------------------------
# --- validate groups
Expand Down Expand Up @@ -309,6 +405,46 @@ run "group_member_type_default_success" {
}
}

run "group_member_role_and_type_are_captilized" {
command = apply

providers = {
googleworkspace = googleworkspace.mock
}

variables {
users = {
"user.type@example.com" = {
primary_email = "user.type@example.com"
family_name = "Type"
given_name = "User"
groups = {
"test-group" = {
role = "member"
type = "user"
}
}
}
}
groups = {
"test-group" = {
name = "Test Group"
email = "test-group@example.com"
}
}
}

assert {
condition = googleworkspace_group_member.user_to_groups["test-group@example.com/user.type@example.com"].role == "MEMBER"
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}"
}

assert {
condition = googleworkspace_group_member.user_to_groups["test-group@example.com/user.type@example.com"].type == "USER"
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}"
}
}

run "group_member_type_invalid" {
command = plan

Expand Down
17 changes: 16 additions & 1 deletion variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ variable "users" {
aliases : optional(list(string), []),
archived : optional(bool, false),
change_password_at_next_login : optional(bool),
# custom_schemas
custom_schemas : optional(list(object({
schema_name : string,
schema_values : optional(map(string), {}),
})), []),
# emails
# external_ids
family_name : string,
Expand Down Expand Up @@ -109,6 +112,18 @@ variable "users" {
]))
error_message = "group type must be either 'USER', 'GROUP', or 'CUSTOMER'"
}

# validate that schema_values's values can be JSON encoded (required by Google Workspace provider)
validation {
condition = alltrue(flatten([
for user in var.users : [
for schema in user.custom_schemas : [
for key, value in schema.schema_values : can(jsonencode(value))
]
]
]))
error_message = "All values in custom schema values must be JSON encodable strings"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah that's why we have weird strings. Okay. Good to know -- Glad we have this validation as documentation.

}
}

variable "groups" {
Expand Down