Add support for Workforce Identity to organization module and org setup stage (#3530)

* module-level support

* fast stage 0

* fix inventory, add outputs/tfvars
This commit is contained in:
Ludovico Magnocavallo
2025-11-17 08:00:30 +01:00
committed by GitHub
parent 5270586a8e
commit 87ed19bc47
9 changed files with 684 additions and 1 deletions

View File

@@ -32,6 +32,7 @@ To manage organization policies, the `orgpolicy.googleapis.com` service should b
- [Custom Security Health Analytics Modules Factory](#custom-security-health-analytics-modules-factory)
- [Tags](#tags)
- [Tags Factory](#tags-factory)
- [Workforce Identity](#workforce-identity)
- [Files](#files)
- [Variables](#variables)
- [Outputs](#outputs)
@@ -556,7 +557,6 @@ cloudkmKeyRotationPeriod:
## Tags
Refer to the [Creating and managing tags](https://cloud.google.com/resource-manager/docs/tags/tags-creating-and-managing) documentation for details on usage.
```hcl
@@ -708,6 +708,77 @@ values:
```
## Workforce Identity
A Workforce Identity pool and providers can be created via the `workforce_identity_config` variable.
Auto-population of provider attributes is supported via the `attribute_mapping_template` provider attribute. Currently only `azuread` and `okta` are supported.
```hcl
module "org" {
source = "./fabric/modules/organization"
organization_id = var.organization_id
workforce_identity_config = {
# optional, defaults to 'default'
pool_name = "test-pool"
providers = {
saml-basic = {
attribute_mapping_template = "azuread"
identity_provider = {
saml = {
idp_metadata_xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>..."
}
}
}
saml-full = {
attribute_mapping = {
"google.subject" = "assertion.sub"
}
identity_provider = {
saml = {
idp_metadata_xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>..."
}
}
oauth2_client_config = {
extra_attributes = {
issuer_uri = "https://login.microsoftonline.com/abcdef/v2.0"
client_id = "client-id"
client_secret = "client-secret"
attributes_type = "AZURE_AD_GROUPS_ID"
query_filter = "mail:gcp"
}
}
}
oidc-full = {
attribute_mapping = {
"google.subject" = "assertion.sub"
}
identity_provider = {
oidc = {
issuer_uri = "https://sts.windows.net/abcd01234/"
client_id = "https://analysis.windows.net/powerbi/connector/GoogleBigQuery"
client_secret = "client-secret"
web_sso_config = {
response_type = "CODE"
assertion_claims_behavior = "MERGE_USER_INFO_OVER_ID_TOKEN_CLAIMS"
}
}
}
oauth2_client_config = {
extra_attributes = {
issuer_uri = "https://login.microsoftonline.com/abcd01234/v2.0"
client_id = "client-id"
client_secret = "client-secret"
attributes_type = "AZURE_AD_GROUPS_MAIL"
}
}
}
}
}
}
# tftest modules=1 resources=4 inventory=wfif.yaml
```
<!-- TFDOC OPTS files:1 -->
<!-- BEGIN TFDOC -->
## Files
@@ -715,6 +786,7 @@ values:
| name | description | resources |
|---|---|---|
| [iam.tf](./iam.tf) | IAM bindings. | <code>google_organization_iam_binding</code> · <code>google_organization_iam_custom_role</code> · <code>google_organization_iam_member</code> |
| [identity-providers.tf](./identity-providers.tf) | Workforce Identity Federation provider definitions. | <code>google_iam_workforce_pool</code> · <code>google_iam_workforce_pool_provider</code> |
| [logging.tf](./logging.tf) | Log sinks and data access logs. | <code>google_bigquery_dataset_iam_member</code> · <code>google_logging_organization_exclusion</code> · <code>google_logging_organization_settings</code> · <code>google_logging_organization_sink</code> · <code>google_organization_iam_audit_config</code> · <code>google_project_iam_member</code> · <code>google_pubsub_topic_iam_member</code> · <code>google_storage_bucket_iam_member</code> |
| [main.tf](./main.tf) | Module-level locals and resources. | <code>google_compute_firewall_policy_association</code> · <code>google_essential_contacts_contact</code> |
| [org-policy-custom-constraints.tf](./org-policy-custom-constraints.tf) | None | <code>google_org_policy_custom_constraint</code> |
@@ -759,6 +831,7 @@ values:
| [tag_bindings](variables-tags.tf#L82) | Tag bindings for this organization, in key => tag value id format. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [tags](variables-tags.tf#L89) | Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | <code title="map&#40;object&#40;&#123;&#10; description &#61; optional&#40;string, &#34;Managed by the Terraform organization module.&#34;&#41;&#10; iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; iam_bindings &#61; optional&#40;map&#40;object&#40;&#123;&#10; members &#61; list&#40;string&#41;&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; iam_bindings_additive &#61; optional&#40;map&#40;object&#40;&#123;&#10; member &#61; string&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; id &#61; optional&#40;string&#41;&#10; values &#61; optional&#40;map&#40;object&#40;&#123;&#10; description &#61; optional&#40;string, &#34;Managed by the Terraform organization module.&#34;&#41;&#10; iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; iam_bindings &#61; optional&#40;map&#40;object&#40;&#123;&#10; members &#61; list&#40;string&#41;&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; iam_bindings_additive &#61; optional&#40;map&#40;object&#40;&#123;&#10; member &#61; string&#10; role &#61; string&#10; condition &#61; optional&#40;object&#40;&#123;&#10; expression &#61; string&#10; title &#61; string&#10; description &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; id &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [tags_config](variables-tags.tf#L154) | Fine-grained control on tag resource and IAM creation. | <code title="object&#40;&#123;&#10; force_context_ids &#61; optional&#40;bool, false&#41;&#10; ignore_iam &#61; optional&#40;bool, false&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [workforce_identity_config](variables.tf#L136) | Workforce Identity Federation pools. | <code title="object&#40;&#123;&#10; pool_name &#61; optional&#40;string, &#34;default&#34;&#41;&#10; providers &#61; optional&#40;map&#40;object&#40;&#123;&#10; description &#61; optional&#40;string&#41;&#10; display_name &#61; optional&#40;string&#41;&#10; attribute_condition &#61; optional&#40;string&#41;&#10; attribute_mapping &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; attribute_mapping_template &#61; optional&#40;string&#41;&#10; disabled &#61; optional&#40;bool, false&#41;&#10; identity_provider &#61; object&#40;&#123;&#10; oidc &#61; optional&#40;object&#40;&#123;&#10; issuer_uri &#61; string&#10; client_id &#61; string&#10; client_secret &#61; optional&#40;string&#41;&#10; jwks_json &#61; optional&#40;string&#41;&#10; web_sso_config &#61; optional&#40;object&#40;&#123;&#10; response_type &#61; optional&#40;string, &#34;CODE&#34;&#41;&#10; assertion_claims_behavior &#61; optional&#40;string, &#34;ONLY_ID_TOKEN_CLAIMS&#34;&#41;&#10; additional_scopes &#61; optional&#40;list&#40;string&#41;&#41;&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#41;&#10; saml &#61; optional&#40;object&#40;&#123;&#10; idp_metadata_xml &#61; string&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#10; oauth2_client_config &#61; optional&#40;object&#40;&#123;&#10; extended_attributes &#61; optional&#40;object&#40;&#123;&#10; issuer_uri &#61; string&#10; client_id &#61; string&#10; client_secret &#61; string&#10; attributes_type &#61; optional&#40;string&#41;&#10; query_filter &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10; extra_attributes &#61; optional&#40;object&#40;&#123;&#10; issuer_uri &#61; string&#10; client_id &#61; string&#10; client_secret &#61; string&#10; attributes_type &#61; optional&#40;string&#41;&#10; query_filter &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
## Outputs
@@ -777,4 +850,5 @@ values:
| [sink_writer_identities](outputs.tf#L101) | Writer identities created for each sink. | |
| [tag_keys](outputs.tf#L109) | Tag key resources. | |
| [tag_values](outputs.tf#L118) | Tag value resources. | |
| [workforce_identity_provider_names](outputs.tf#L126) | Workforce Identity provider names. | |
<!-- END TFDOC -->

View File

@@ -0,0 +1,168 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
# tfdoc:file:description Workforce Identity Federation provider definitions.
locals {
wfif_attribute_mappings = {
azuread = {
"google.subject" = "assertion.subject"
"google.display_name" = "assertion.attributes.userprincipalname[0]"
"google.groups" = "assertion.attributes.groups"
"attribute.first_name" = "assertion.attributes.givenname[0]"
"attribute.last_name" = "assertion.attributes.surname[0]"
"attribute.user_email" = "assertion.attributes.mail[0]"
}
okta = {
"google.subject" = "assertion.subject"
"google.display_name" = "assertion.subject"
"google.groups" = "assertion.attributes.groups"
"attribute.first_name" = "assertion.attributes.firstName[0]"
"attribute.last_name" = "assertion.attributes.lastName[0]"
"attribute.user_email" = "assertion.attributes.email[0]"
}
}
}
resource "google_iam_workforce_pool" "default" {
count = var.workforce_identity_config == null ? 0 : 1
parent = "organizations/${var.organization_id}"
location = "global"
workforce_pool_id = var.workforce_identity_config.pool_name
}
resource "google_iam_workforce_pool_provider" "default" {
for_each = try(var.workforce_identity_config.providers, {})
provider_id = each.key
attribute_condition = each.value.attribute_condition
description = each.value.description
disabled = each.value.disabled
display_name = each.value.display_name
attribute_mapping = merge(
try(local.wfif_attribute_mappings[each.value.attribute_mapping_template], {}),
each.value.attribute_mapping
)
location = google_iam_workforce_pool.default[0].location
workforce_pool_id = google_iam_workforce_pool.default[0].workforce_pool_id
dynamic "saml" {
for_each = each.value.identity_provider.saml == null ? [] : [""]
content {
idp_metadata_xml = each.value.identity_provider.saml.idp_metadata_xml
}
}
dynamic "oidc" {
for_each = each.value.identity_provider.oidc == null ? [] : [""]
content {
issuer_uri = each.value.identity_provider.oidc.issuer_uri
client_id = each.value.identity_provider.oidc.client_id
jwks_json = each.value.identity_provider.oidc.jwks_json
dynamic "client_secret" {
for_each = (
each.value.identity_provider.oidc.client_secret == null ? [] : [""]
)
content {
value {
plain_text = each.value.identity_provider.oidc.client_secret
}
}
}
dynamic "web_sso_config" {
for_each = (
each.value.identity_provider.oidc.web_sso_config == null ? [] : [""]
)
content {
response_type = (
each.value.identity_provider.oidc.web_sso_config.response_type
)
assertion_claims_behavior = (
each.value.identity_provider.oidc.web_sso_config.assertion_claims_behavior
)
additional_scopes = (
each.value.identity_provider.oidc.web_sso_config.additional_scopes
)
}
}
}
}
dynamic "extra_attributes_oauth2_client" {
for_each = (
try(each.value.oauth2_client_config.extra_attributes, null) == null ? [] : [""]
)
content {
issuer_uri = (
each.value.oauth2_client_config.extra_attributes.issuer_uri
)
client_id = (
each.value.oauth2_client_config.extra_attributes.client_id
)
attributes_type = (
each.value.oauth2_client_config.extra_attributes.attributes_type
)
dynamic "client_secret" {
for_each = (
each.value.oauth2_client_config.extra_attributes.client_secret == null ? [] : [""]
)
content {
value {
plain_text = each.value.oauth2_client_config.extra_attributes.client_secret
}
}
}
dynamic "query_parameters" {
for_each = (
each.value.oauth2_client_config.extra_attributes.query_filter == null ? [] : [""]
)
content {
filter = each.value.oauth2_client_config.extra_attributes.query_filter
}
}
}
}
dynamic "extended_attributes_oauth2_client" {
for_each = (
try(each.value.oauth2_client_config.extended_attributes, null) == null ? [] : [""]
)
content {
issuer_uri = (
each.value.oauth2_client_config.extended_attributes.issuer_uri
)
client_id = (
each.value.oauth2_client_config.extended_attributes.client_id
)
attributes_type = (
each.value.oauth2_client_config.extended_attributes.attributes_type
)
dynamic "client_secret" {
for_each = (
each.value.oauth2_client_config.extended_attributes.client_secret == null ? [] : [""]
)
content {
value {
plain_text = each.value.oauth2_client_config.extended_attributes.client_secret
}
}
}
dynamic "query_parameters" {
for_each = (
each.value.oauth2_client_config.extended_attributes.query_filter == null ? [] : [""]
)
content {
filter = each.value.oauth2_client_config.extended_attributes.query_filter
}
}
}
}
}

View File

@@ -123,3 +123,9 @@ output "tag_values" {
}
}
output "workforce_identity_provider_names" {
description = "Workforce Identity provider names."
value = {
for k, v in google_iam_workforce_pool_provider.default : k => v.name
}
}

View File

@@ -132,3 +132,115 @@ variable "organization_id" {
error_message = "The organization_id must in the form organizations/nnn."
}
}
variable "workforce_identity_config" {
description = "Workforce Identity Federation pools."
type = object({
pool_name = optional(string, "default")
providers = optional(map(object({
description = optional(string)
display_name = optional(string)
attribute_condition = optional(string)
attribute_mapping = optional(map(string), {})
attribute_mapping_template = optional(string)
disabled = optional(bool, false)
identity_provider = object({
oidc = optional(object({
issuer_uri = string
client_id = string
client_secret = optional(string)
jwks_json = optional(string)
web_sso_config = optional(object({
# TODO: validation
response_type = optional(string, "CODE")
assertion_claims_behavior = optional(string, "ONLY_ID_TOKEN_CLAIMS")
additional_scopes = optional(list(string))
}))
}))
saml = optional(object({
idp_metadata_xml = string
}))
})
oauth2_client_config = optional(object({
extended_attributes = optional(object({
issuer_uri = string
client_id = string
client_secret = string
attributes_type = optional(string)
query_filter = optional(string)
}))
extra_attributes = optional(object({
issuer_uri = string
client_id = string
client_secret = string
attributes_type = optional(string)
query_filter = optional(string)
}))
}), {})
})), {})
})
nullable = true
default = null
validation {
condition = alltrue([
for v in try(var.workforce_identity_config.providers, {}) : contains(
["azuread", "okta"],
coalesce(v.attribute_mapping_template, "azuread")
)
])
error_message = "Supported mapping templates are: azuread, okta."
}
validation {
condition = alltrue([
for v in try(var.workforce_identity_config.providers, {}) : (
(try(v.identity_provider.oidc, null) == null ? 0 : 1) +
(try(v.identity_provider.saml, null) == null ? 0 : 1)
) == 1
])
error_message = "Only one of identity_provider.oidc or identity_provider.saml can be defined."
}
validation {
condition = alltrue([
for v in try(var.workforce_identity_config.providers, {}) : contains(
["CODE", "ID_TOKEN"],
coalesce(try(
v.identity_provider.oidc.web_sso_config.response_type, null
), "CODE")
)
])
error_message = "Invalid OIDC web SSO config response type."
}
validation {
condition = alltrue([
for v in try(var.workforce_identity_config.providers, {}) : contains(
["MERGE_USER_INFO_OVER_ID_TOKEN_CLAIMS", "ONLY_ID_TOKEN_CLAIMS"],
coalesce(try(
v.identity_provider.oidc.web_sso_config.assertion_claims_behavior, null
), "MERGE_USER_INFO_OVER_ID_TOKEN_CLAIMS")
)
])
error_message = "Invalid OIDC web SSO config assertion claims behavior."
}
validation {
condition = alltrue([
for v in try(var.workforce_identity_config.providers, {}) : contains(
["AZURE_AD_GROUPS_MAIL", "AZURE_AD_GROUPS_ID"],
coalesce(try(
v.oauth2_client_config.extended_attributes.attributes_type, null
), "AZURE_AD_GROUPS_MAIL")
)
])
error_message = "Invalid AzureAD attribute type in OAuth 2.0 client extended attributes.."
}
validation {
condition = alltrue([
for v in try(var.workforce_identity_config.providers, {}) : contains(
["AZURE_AD_GROUPS_MAIL", "AZURE_AD_GROUPS_ID"],
coalesce(try(
v.oauth2_client_config.extra_attributes.attributes_type, null
), "AZURE_AD_GROUPS_MAIL")
)
])
error_message = "Invalid AzureAD attribute type in OAuth 2.0 client extra attributes.."
}
}