diff --git a/.gitignore b/.gitignore
index f2ac5f40dd..026b2b87cc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,3 +31,5 @@ fast/stages/**/globals.auto.tfvars.json
cloud_sql_proxy
examples/cloud-operations/binauthz/tenant-setup.yaml
examples/cloud-operations/binauthz/app/app.yaml
+examples/cloud-operations/adfs/ansible/vars/vars.yaml
+examples/cloud-operations/adfs/ansible/gssh.sh
diff --git a/examples/cloud-operations/adfs/README.md b/examples/cloud-operations/adfs/README.md
new file mode 100644
index 0000000000..281eb5f0ce
--- /dev/null
+++ b/examples/cloud-operations/adfs/README.md
@@ -0,0 +1,78 @@
+# AD FS
+
+The folowing example does the following:
+
+Terraform:
+
+- (Optional) Creates a project.
+- (Optional) Creates a VPC.
+- Sets up managed AD
+- Creates a server where AD FS will be installed. This machine will also act as admin workstation for AD.
+- Exposes AD FS using GLB.
+
+Ansible:
+
+- Installs the required Windows features and joins the computer to the AD domain.
+- Provisions some tests users, groups and group memberships in AD. The data to provision is in the ifles directory of the ad-provisioning ansible role. There is script available in the scripts/ad-provisioning folder that you can use to generate an alternative users or memberships file.
+- Installs AD FS
+
+In addition to this, we also include a Powershell script that facilitates the configuration required for Anthos when authenticating users with AD FS as IdP.
+
+The diagram below depicts the architecture of the example:
+
+![Architecture](architecture.png)
+
+## Running the example
+
+Clone this repository or [open it in cloud shell](https://ssh.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2Fterraform-google-modules%2Fcloud-foundation-fabric&cloudshell_print=cloud-shell-readme.txt&cloudshell_working_dir=examples%2Fcloud-operations%2Fadfs), then go through the following steps to create resources:
+
+* `terraform init`
+* `terraform apply -var project_id=my-project-id -var ad_dns_domain_name=my-domain.org -var adfs_dns_domain_name=adfs.my-domain.org`
+
+Once the resources have been created, do the following:
+
+1. Create an A record to point the AD FS DNS domain name to the public IP address returned after the terraform configuration was applied.
+
+2. Run the ansible playbook
+
+ ansible-playbook playbook.yaml
+
+# Testing the example
+
+1. In your browser open the following URL:
+
+ https://adfs.my-domain.org/adfs/ls/IdpInitiatedSignOn.aspx
+
+2. Enter the username and password of one of the users provisioned. The username has to be in the format: username@my-domain.org
+
+3. Verify that you have successfuly signed in.
+
+Once done testing, you can clean up resources by running `terraform destroy`.
+
+
+## Variables
+
+| name | description | type | required | default |
+|---|---|:---:|:---:|:---:|
+| [ad_dns_domain_name](variables.tf#L44) | AD DNS domain name. | string
| ✓ | |
+| [adfs_dns_domain_name](variables.tf#L49) | ADFS DNS domain name. | string
| ✓ | |
+| [project_id](variables.tf#L24) | Host project ID. | string
| ✓ | |
+| [ad_ip_cidr_block](variables.tf#L90) | Managed AD IP CIDR block. | string
| | "10.0.0.0/24"
|
+| [disk_size](variables.tf#L54) | Disk size. | number
| | 50
|
+| [disk_type](variables.tf#L60) | Disk type. | string
| | "pd-ssd"
|
+| [image](variables.tf#L66) | Image. | string
| | "projects/windows-cloud/global/images/family/windows-2022"
|
+| [instance_type](variables.tf#L72) | Instance type. | string
| | "n1-standard-2"
|
+| [network_config](variables.tf#L35) | Network configuration | object({…})
| | null
|
+| [prefix](variables.tf#L29) | Prefix for the resources created. | string
| | null
|
+| [project_create](variables.tf#L15) | Parameters for the creation of the new project. | object({…})
| | null
|
+| [region](variables.tf#L78) | Region. | string
| | "europe-west1"
|
+| [subnet_ip_cidr_block](variables.tf#L96) | Subnet IP CIDR block. | string
| | "10.0.1.0/28"
|
+| [zone](variables.tf#L84) | Zone. | string
| | "europe-west1-c"
|
+
+## Outputs
+
+| name | description | sensitive |
+|---|---|:---:|
+| [ip_address](outputs.tf#L15) | IP address. | |
+
+
diff --git a/examples/cloud-operations/adfs/ansible/ansible.cfg b/examples/cloud-operations/adfs/ansible/ansible.cfg
new file mode 100644
index 0000000000..e822a18f59
--- /dev/null
+++ b/examples/cloud-operations/adfs/ansible/ansible.cfg
@@ -0,0 +1,8 @@
+[defaults]
+inventory = inventory/hosts.ini
+
+[ssh_connection]
+pipelining = True
+ssh_executable = ./gssh.sh
+transfer_method = piped
+
diff --git a/examples/cloud-operations/adfs/ansible/inventory/hosts.ini b/examples/cloud-operations/adfs/ansible/inventory/hosts.ini
new file mode 100644
index 0000000000..bef015937d
--- /dev/null
+++ b/examples/cloud-operations/adfs/ansible/inventory/hosts.ini
@@ -0,0 +1 @@
+adfs ansible_connection=ssh ansible_shell_type=powershell
\ No newline at end of file
diff --git a/examples/cloud-operations/adfs/ansible/playbook.yaml b/examples/cloud-operations/adfs/ansible/playbook.yaml
new file mode 100644
index 0000000000..96d2e3baa4
--- /dev/null
+++ b/examples/cloud-operations/adfs/ansible/playbook.yaml
@@ -0,0 +1,53 @@
+# Copyright 2022 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
+#
+# https://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.
+
+- name: Prepare
+ hosts: adfs
+ gather_facts: yes
+ vars_files:
+ - vars/vars.yaml
+ roles:
+ - role: server-setup
+
+- name: Provision organizational units users, groups and memberships
+ hosts: adfs
+ gather_facts: no
+ vars_files:
+ - vars/vars.yaml
+ vars:
+ ansible_become: yes
+ ansible_become_method: runas
+ ansible_become_user: "SetupAdmin@{{ ad_dns_domain_name }}"
+ ansible_become_password: "{{ setupadmin_password }}"
+ roles:
+ - role: ad-provisioning
+
+- name: Install AD FS
+ hosts: adfs
+ gather_facts: no
+ vars_files:
+ - vars/vars.yaml
+ vars:
+ ansible_become: yes
+ ansible_become_method: runas
+ adfssvc_password: "{{ lookup('ansible.builtin.password', '~/.adfssvc-password.txt chars=ascii_letters,digits') }}"
+ roles:
+ - role: adfs-prerequisites
+ vars:
+ ansible_become_user: "SetupAdmin@{{ ad_dns_domain_name }}"
+ ansible_become_password: "{{ setupadmin_password }}"
+ - role: adfs-installation
+ vars:
+ ansible_become_user: "adfssvc@{{ ad_dns_domain_name }}"
+ ansible_become_password: "{{ adfssvc_password }}"
\ No newline at end of file
diff --git a/examples/cloud-operations/adfs/ansible/roles/ad-provisioning/files/groups.json b/examples/cloud-operations/adfs/ansible/roles/ad-provisioning/files/groups.json
new file mode 100644
index 0000000000..5ba88d2088
--- /dev/null
+++ b/examples/cloud-operations/adfs/ansible/roles/ad-provisioning/files/groups.json
@@ -0,0 +1,8 @@
+[
+ "gcp-billing-admins",
+ "gcp-devops",
+ "gcp-network-admins",
+ "gcp-organization-admins",
+ "gcp-security-admins",
+ "gcp-support"
+]
\ No newline at end of file
diff --git a/examples/cloud-operations/adfs/ansible/roles/ad-provisioning/files/memberships.json b/examples/cloud-operations/adfs/ansible/roles/ad-provisioning/files/memberships.json
new file mode 100644
index 0000000000..38d26253d6
--- /dev/null
+++ b/examples/cloud-operations/adfs/ansible/roles/ad-provisioning/files/memberships.json
@@ -0,0 +1,82 @@
+[
+ {
+ "group": "gcp-devops",
+ "member": "pamela.reed"
+ },
+ {
+ "group": "gcp-devops",
+ "member": "joshua.banks"
+ },
+ {
+ "group": "gcp-devops",
+ "member": "clayton.espinoza"
+ },
+ {
+ "group": "gcp-devops",
+ "member": "maureen.morgan"
+ },
+ {
+ "group": "gcp-network-admins",
+ "member": "pamela.reed"
+ },
+ {
+ "group": "gcp-network-admins",
+ "member": "william.bowen"
+ },
+ {
+ "group": "gcp-network-admins",
+ "member": "clayton.espinoza"
+ },
+ {
+ "group": "gcp-network-admins",
+ "member": "stacy.holland"
+ },
+ {
+ "group": "gcp-network-admins",
+ "member": "joshua.banks"
+ },
+ {
+ "group": "gcp-network-admins",
+ "member": "charlene.mckenzie"
+ },
+ {
+ "group": "gcp-network-admins",
+ "member": "lisa.harris"
+ },
+ {
+ "group": "gcp-organization-admins",
+ "member": "maureen.morgan"
+ },
+ {
+ "group": "gcp-organization-admins",
+ "member": "pamela.reed"
+ },
+ {
+ "group": "gcp-support",
+ "member": "maureen.morgan"
+ },
+ {
+ "group": "gcp-support",
+ "member": "pamela.reed"
+ },
+ {
+ "group": "gcp-support",
+ "member": "lisa.harris"
+ },
+ {
+ "group": "gcp-support",
+ "member": "tina.ferguson"
+ },
+ {
+ "group": "gcp-support",
+ "member": "stacy.holland"
+ },
+ {
+ "group": "gcp-support",
+ "member": "william.bowen"
+ },
+ {
+ "group": "gcp-support",
+ "member": "clayton.espinoza"
+ }
+]
\ No newline at end of file
diff --git a/examples/cloud-operations/adfs/ansible/roles/ad-provisioning/files/users.json b/examples/cloud-operations/adfs/ansible/roles/ad-provisioning/files/users.json
new file mode 100644
index 0000000000..f11f9fa0e5
--- /dev/null
+++ b/examples/cloud-operations/adfs/ansible/roles/ad-provisioning/files/users.json
@@ -0,0 +1,56 @@
+[
+ {
+ "first_name": "Pamela",
+ "last_name": "Reed",
+ "username": "pamela.reed",
+ "password": "Ig_17BbZVu"
+ },
+ {
+ "first_name": "Charlene",
+ "last_name": "Mckenzie",
+ "username": "charlene.mckenzie",
+ "password": "$y0IsMLPy5"
+ },
+ {
+ "first_name": "William",
+ "last_name": "Bowen",
+ "username": "william.bowen",
+ "password": "y882QxMHE@"
+ },
+ {
+ "first_name": "Joshua",
+ "last_name": "Banks",
+ "username": "joshua.banks",
+ "password": ")00+LN!r0$"
+ },
+ {
+ "first_name": "Clayton",
+ "last_name": "Espinoza",
+ "username": "clayton.espinoza",
+ "password": "gIf@52FqUY"
+ },
+ {
+ "first_name": "Stacy",
+ "last_name": "Holland",
+ "username": "stacy.holland",
+ "password": "da4PLSQDb^"
+ },
+ {
+ "first_name": "Maureen",
+ "last_name": "Morgan",
+ "username": "maureen.morgan",
+ "password": "V)c2Vfc%i#"
+ },
+ {
+ "first_name": "Lisa",
+ "last_name": "Harris",
+ "username": "lisa.harris",
+ "password": "0@1Oid71co"
+ },
+ {
+ "first_name": "Tina",
+ "last_name": "Ferguson",
+ "username": "tina.ferguson",
+ "password": "+f#0C#_oi6"
+ }
+]
\ No newline at end of file
diff --git a/examples/cloud-operations/adfs/ansible/roles/ad-provisioning/tasks/main.yaml b/examples/cloud-operations/adfs/ansible/roles/ad-provisioning/tasks/main.yaml
new file mode 100644
index 0000000000..f95bc7f0cc
--- /dev/null
+++ b/examples/cloud-operations/adfs/ansible/roles/ad-provisioning/tasks/main.yaml
@@ -0,0 +1,58 @@
+# Copyright 2022 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
+#
+# https://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.
+
+- name: Read files
+ set_fact:
+ ad_users: "{{ lookup('file','users.json') | from_json }}"
+ ad_groups: "{{ lookup('file','groups.json') | from_json }}"
+ ad_memberships: "{{ lookup('file','memberships.json') | from_json }}"
+
+- name: Create organizational units
+ community.windows.win_domain_ou:
+ name: "{{ item }}"
+ path: "{{ cloud_path }}"
+ state: present
+ protected: true
+ with_items:
+ - "Users"
+ - "Groups"
+
+- name: Create users
+ community.windows.win_domain_user:
+ name: "{{ item.username }}"
+ firstname: "{{ item.first_name }}"
+ surname: "{{ item.last_name }}"
+ email: "{{ item.username }}@{{ ad_dns_domain_name }}"
+ sam_account_name: "{{ item.username }}"
+ upn: "{{ item.username }}@{{ ad_dns_domain_name }}"
+ password: "{{ item.password }}"
+ path: "OU=Users,{{ cloud_path }}"
+ state: present
+ with_items: "{{ ad_users }}"
+
+- name: Create groups
+ community.windows.win_domain_group:
+ name: "{{ item }}"
+ path: "OU=Groups,{{ cloud_path }}"
+ scope: global
+ state: present
+ with_items: "{{ ad_groups }}"
+
+- name: Create memberships
+ community.windows.win_domain_group_membership:
+ name: "{{ item.group }}"
+ members:
+ - "{{ item.member }}"
+ state: present
+ with_items: "{{ ad_memberships }}"
\ No newline at end of file
diff --git a/examples/cloud-operations/adfs/ansible/roles/adfs-installation/tasks/main.yaml b/examples/cloud-operations/adfs/ansible/roles/adfs-installation/tasks/main.yaml
new file mode 100644
index 0000000000..ccbe99d201
--- /dev/null
+++ b/examples/cloud-operations/adfs/ansible/roles/adfs-installation/tasks/main.yaml
@@ -0,0 +1,104 @@
+# Copyright 2022 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
+#
+# https://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.
+
+- name: Create server certificate
+ ansible.windows.win_powershell:
+ script: |
+ $Certificate = Get-ChildItem -Path Cert:\LocalMachine\My | Where-Object {$_.Subject -eq "CN={{ adfs_dns_domain_name }}"}
+ if(-not $Certificate) {
+ $Certificate = New-SelfSignedCertificate `
+ -Subject {{ adfs_dns_domain_name }} `
+ -KeyAlgorithm RSA `
+ -KeyLength 2048 `
+ -KeyExportPolicy NonExportable `
+ -KeyUsage DigitalSignature, KeyEncipherment `
+ -Provider 'Microsoft Platform Crypto Provider' `
+ -NotAfter (Get-Date).AddDays(365) `
+ -Type SSLServerAuthentication `
+ -CertStoreLocation 'Cert:\LocalMachine\My' `
+ -DnsName {{ adfs_dns_domain_name }}
+ }
+ $Certificate.Thumbprint
+ register: server_cert
+
+- name: Create token signing certificate
+ ansible.windows.win_powershell:
+ script: |
+ $Certificate = Get-ChildItem -Path Cert:\LocalMachine\My | Where-Object {$_.Subject -eq "CN=ADFS Signing"}
+ if(-not $Certificate) {
+ $Certificate = New-SelfSignedCertificate `
+ -Subject "ADFS Signing" `
+ -KeyAlgorithm RSA `
+ -KeyLength 2048 `
+ -KeyExportPolicy NonExportable `
+ -KeyUsage DigitalSignature, KeyEncipherment `
+ -Provider 'Microsoft RSA SChannel Cryptographic Provider' `
+ -NotAfter (Get-Date).AddDays(365) `
+ -DnsName {{ adfs_dns_domain_name }} `
+ -CertStoreLocation 'Cert:\LocalMachine\My'
+ }
+ $Certificate.Thumbprint
+ register: token_signing_cert
+
+- name: Create AD FS DKM container
+ ansible.windows.win_powershell:
+ script: |
+ $DkmContainer = Get-ADObject -LDAPFilter "(Objectclass=container)" -SearchBase "CN=ADFS Data,{{ cloud_path }}" -SearchScope 1
+ if(-not $DkmContainer) {
+ $DkmContainer.DistinguishedName
+ $Name = (New-Guid).Guid
+ $DkmContainer = New-ADObject `
+ -Name $Name `
+ -Type Container `
+ -Path "CN=ADFS Data,{{ cloud_path }}" `
+ -PassThru
+ }
+ $DkmContainer.DistinguishedName
+ register: adfs_dkm_container
+
+- name: Install ADFS
+ ansible.windows.win_powershell:
+ script: |
+ try {
+ $AdfsFarm = Get-AdfsFarmInformation
+ } catch [System.ServiceModel.EndpointNotFoundException] {
+ $AdfsCredential = New-Object `
+ -TypeName System.Management.Automation.PSCredential `
+ -ArgumentList "$env:userdomain\adfssvc", (ConvertTo-SecureString {{ adfssvc_password }} -AsPlainText -Force)
+ Install-ADFSFarm `
+ -CertificateThumbprint {{ server_cert.output[0] }} `
+ -SigningCertificateThumbprint {{ token_signing_cert.output[0] }} `
+ -DecryptionCertificateThumbprint {{ token_signing_cert.output[0] }}`
+ -FederationServiceName {{ adfs_dns_domain_name }} `
+ -ServiceAccountCredential $AdfsCredential `
+ -OverwriteConfiguration `
+ -AdminConfiguration @{"DKMContainerDn"="{{ adfs_dkm_container.output[0] }}"}
+ }
+ no_log: yes
+
+- name: Configure TLS
+ ansible.windows.win_powershell:
+ script: |
+ netsh http show sslcert ipport=0.0.0.0:443
+ if($LastExitCode -gt 0) {
+ netsh http add sslcert ipport=0.0.0.0:443 certhash={{ server_cert.output[0] }} appid="{5d89a20c-beab-4389-9447-324788eb944a}" certstorename=MY
+ }
+
+- name: Restart computer
+ ansible.windows.win_reboot:
+
+- name: Enable the Idp-Initiated Sign on page
+ ansible.windows.win_powershell:
+ script: |
+ Set-AdfsProperties -EnableIdpInitiatedSignonPage $true
\ No newline at end of file
diff --git a/examples/cloud-operations/adfs/ansible/roles/adfs-prerequisites/tasks/main.yaml b/examples/cloud-operations/adfs/ansible/roles/adfs-prerequisites/tasks/main.yaml
new file mode 100644
index 0000000000..eeb6e1fc39
--- /dev/null
+++ b/examples/cloud-operations/adfs/ansible/roles/adfs-prerequisites/tasks/main.yaml
@@ -0,0 +1,45 @@
+# Copyright 2022 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
+#
+# https://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.
+
+- name: Create AD FS service user
+ community.windows.win_domain_user:
+ name: "adfssvc"
+ password: "{{ adfssvc_password }}"
+ spn: "http/{{ adfs_dns_domain_name }}"
+ path: "OU=Users,{{ cloud_path }}"
+ state: present
+
+- name: Add AD FS service user to local Administrators group
+ ansible.windows.win_group_membership:
+ name: Administrators
+ members:
+ - "adfssvc@{{ ad_dns_domain_name }}"
+ state: present
+
+- name: Create AD FS Data container
+ ansible.windows.win_powershell:
+ script: |
+ try {
+ Get-ADObject -Identity "CN=ADFS Data,{{ cloud_path }}"
+ } catch [Microsoft.ActiveDirectory.Management.ADIdentityResolutionException] {
+ New-ADObject `
+ -Name "ADFS Data" `
+ -Type Container `
+ -Path "{{ cloud_path }}"
+ }
+
+- name: Grant the AD FS user full control on the container
+ ansible.windows.win_powershell:
+ script: |
+ dsacls.exe "CN=ADFS Data,{{ cloud_path }}" /G $env:userdomain\adfssvc:GA /I:T
\ No newline at end of file
diff --git a/examples/cloud-operations/adfs/ansible/roles/anthos/tasks/main.yaml b/examples/cloud-operations/adfs/ansible/roles/anthos/tasks/main.yaml
new file mode 100644
index 0000000000..4ca1d7f21a
--- /dev/null
+++ b/examples/cloud-operations/adfs/ansible/roles/anthos/tasks/main.yaml
@@ -0,0 +1,67 @@
+# Copyright 2022 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
+#
+# https://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.
+
+$ApplicationGroup = Get-AdfsApplicationGroup -Name Anthos
+
+$ApplicationGroupName = "Anthos"
+$ApplicationGroupIdentifier = (New-Guid).Guid
+New-AdfsApplicationGroup -Name $ApplicationGroupName `
+-ApplicationGroupIdentifier $ApplicationGroupIdentifier
+
+$ServerApplicationName = "$ApplicationGroupName Server App"
+$ServerApplicationIdentifier = (New-Guid).Guid
+$RelyingPartyTrustName = "Anthos"
+$RelyingPartyTrustIdentifier = (New-Guid).Guid
+$RedirectURI1 = "http://localhost:1025/callback"
+$RedirectURI2 = "https://console.cloud.google.com/kubernetes/oidc"
+
+$ADFSApp = Add-AdfsServerApplication -Name $ServerApplicationName `
+-ApplicationGroupIdentifier $ApplicationGroupIdentifier `
+-RedirectUri $RedirectURI1,$RedirectURI2 `
+-Identifier $ServerApplicationIdentifier `
+-GenerateClientSecret
+
+$IssuanceTransformRules = @'
+@RuleTemplate = "LdapClaims"
+@RuleName = "groups"
+c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", Issuer == "AD AUTHORITY"]
+=> issue(store = "Active Directory", types = ("http://schemas.xmlsoap.org/claims/Group"), query = ";tokenGroups(domainQualifiedName);{0}", param = c.Value);
+'@
+
+Add-AdfsRelyingPartyTrust -Name $RelyingPartyTrustName `
+-Identifier $RelyingPartyTrustIdentifier `
+-AccessControlPolicyName "Permit everyone" `
+-IssuanceTransformRules "$IssuanceTransformRules"
+
+Grant-ADFSApplicationPermission -ClientRoleIdentifier $ServerApplicationIdentifier `
+-ServerRoleIdentifier $RelyingPartyTrustIdentifier `
+-ScopeName "allatclaims", "openid"
+
+$ClientId = $ADFSApp.Identifier
+$ClientSecret = $ADFSApp.ClientSecret
+
+@"
+authentication:
+ oidc:
+ clientID: $ADFSApp.Identifier
+ clientSecret: $ADFSApp.ClientSecret
+ extraParams: resource=$RelyingPartyTrustIdentifier
+ group: groups
+ groupPrefix: ""
+ issuerURI: https://{{ adfs_dns_domain_name }}/adfs
+ kubectlRedirectURL: $RedirectURI1
+ scopes: openid
+ username: upn
+ usernamePrefix: ""
+"@
\ No newline at end of file
diff --git a/examples/cloud-operations/adfs/ansible/roles/server-setup/tasks/main.yaml b/examples/cloud-operations/adfs/ansible/roles/server-setup/tasks/main.yaml
new file mode 100644
index 0000000000..6b846f41a0
--- /dev/null
+++ b/examples/cloud-operations/adfs/ansible/roles/server-setup/tasks/main.yaml
@@ -0,0 +1,86 @@
+# Copyright 2022 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
+#
+# https://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.
+
+- name: Install Windows features
+ ansible.windows.win_feature:
+ name: "{{ item.feature }}"
+ include_mamangement_tools: "{{ item.include_management_tools }}"
+ state: present
+ with_items:
+ - { "feature": "RSAT-AD-Tools", "include_management_tools": false }
+ - { "feature": "GPMC", "include_management_tools": false }
+ - { "feature": "RSAT-DNS-Server", "include_management_tools": false }
+ - { "feature": "ADFS-Federation", "include_management_tools": true }
+ - { "feature": "RSAT-AD-PowerShell", "include_management_tools": false }
+ - { "feature": "RSAT-ADDS-Tools", "include_management_tools": false }
+
+- name: Check if SetupAdmin password has already been reset
+ stat:
+ path: ~/.setupadmin-password.txt
+ register: setupadmin_password_file_check
+ delegate_to: localhost
+
+- name: Set AD SetupAdmin password fact
+ set_fact:
+ setupadmin_password: "{{ lookup('file', '~/.setupadmin-password.txt') }}"
+ no_log: true
+ when: setupadmin_password_file_check.stat.exists
+ delegate_to: localhost
+
+- name: Reset AD deletegated admin password
+ shell: >
+ gcloud active-directory domains reset-admin-password {{ ad_dns_domain_name }}
+ --project={{ project_id }}
+ --quiet
+ --format "value(password)"
+ register: setupadmin_password_reset
+ no_log: yes
+ when: not setupadmin_password_file_check.stat.exists
+ delegate_to: localhost
+
+- name: Set AD SetupAdmin password fact
+ set_fact:
+ setupadmin_password: "{{ setupadmin_password_reset.stdout }}"
+ no_log: yes
+ when: not setupadmin_password_file_check.stat.exists
+
+- name: Creating a file setupadmin password
+ copy:
+ dest: ~/.setupadmin-password.txt
+ content: "{{ setupadmin_password }}"
+ when: not setupadmin_password_file_check.stat.exists
+ delegate_to: localhost
+
+- name: Add computer to domain
+ ansible.windows.win_domain_membership:
+ dns_domain_name: "{{ ad_dns_domain_name }}"
+ domain_admin_user: "SetupAdmin@{{ ad_dns_domain_name }}"
+ domain_admin_password: "{{ setupadmin_password }}"
+ state: domain
+ register: domain_state
+
+- name: Restart computer
+ ansible.windows.win_reboot:
+ when: domain_state.reboot_required
+
+- name: Get Domain info
+ community.windows.win_domain_object_info:
+ filter: ObjectClass -eq 'domain'
+ domain_username: "SetupAdmin@{{ ad_dns_domain_name }}"
+ domain_password: "{{ setupadmin_password }}"
+ register: ad_domain
+
+- name: Set facts
+ set_fact:
+ cloud_path: "OU=Cloud,{{ ad_domain.objects[0].DistinguishedName }}"
diff --git a/examples/cloud-operations/adfs/architecture.png b/examples/cloud-operations/adfs/architecture.png
new file mode 100644
index 0000000000..c5cca554e9
Binary files /dev/null and b/examples/cloud-operations/adfs/architecture.png differ
diff --git a/examples/cloud-operations/adfs/main.tf b/examples/cloud-operations/adfs/main.tf
new file mode 100644
index 0000000000..24952d6b1c
--- /dev/null
+++ b/examples/cloud-operations/adfs/main.tf
@@ -0,0 +1,200 @@
+# Copyright 2022 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
+#
+# https://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.
+
+locals {
+ prefix = (var.prefix == null || var.prefix == "") ? "" : "${var.prefix}-"
+}
+
+module "project" {
+ source = "../../../modules/project"
+ billing_account = (var.project_create != null
+ ? var.project_create.billing_account_id
+ : null
+ )
+ parent = (var.project_create != null
+ ? var.project_create.parent
+ : null
+ )
+ prefix = var.project_create == null ? null : var.prefix
+ name = var.project_id
+ services = [
+ "compute.googleapis.com",
+ "dns.googleapis.com",
+ "managedidentities.googleapis.com"
+ ]
+ policy_list = {
+ "constraints/compute.restrictLoadBalancerCreationForTypes" = {
+ inherit_from_parent = null
+ suggested_value = null
+ status = true
+ values = ["EXTERNAL_HTTP_HTTPS"]
+ }
+ }
+}
+
+module "vpc" {
+ count = var.network_config == null ? 1 : 0
+ source = "../../../modules/net-vpc"
+ project_id = module.project.project_id
+ name = "${local.prefix}vpc"
+ subnets = [
+ {
+ ip_cidr_range = var.subnet_ip_cidr_block
+ name = "subnet"
+ region = var.region
+ secondary_ip_range = null
+ }
+ ]
+}
+
+resource "google_active_directory_domain" "ad_domain" {
+ project = module.project.project_id
+ domain_name = var.ad_dns_domain_name
+ locations = [var.region]
+ authorized_networks = [module.vpc[0].network.id]
+ reserved_ip_range = var.ad_ip_cidr_block
+}
+
+module "server" {
+ source = "../../../modules/compute-vm"
+ project_id = module.project.project_id
+ zone = var.zone
+ name = "adfs"
+ instance_type = var.instance_type
+ network_interfaces = [{
+ network = var.network_config == null ? module.vpc[0].self_link : var.network_config.network
+ subnetwork = var.network_config == null ? module.vpc[0].subnet_self_links["${var.region}/subnet"] : var.network_config.subnet
+ nat = false
+ addresses = null
+ }]
+ metadata = {
+ # Enables OpenSSH in the Windows instance
+ sysprep-specialize-script-cmd = "googet -noconfirm=true update && googet -noconfirm=true install google-compute-engine-ssh"
+ enable-windows-ssh = "TRUE"
+ # Set the default OpenSSH shell to Powershell
+ windows-startup-script-ps1 = < issue(store = "Active Directory", types = ("http://schemas.xmlsoap.org/claims/Group"), query = ";tokenGroups(domainQualifiedName);{0}", param = c.Value);
+'@
+
+Add-AdfsRelyingPartyTrust -Name $RelyingPartyTrustName `
+-Identifier $RelyingPartyTrustIdentifier `
+-AccessControlPolicyName "Permit everyone" `
+-IssuanceTransformRules "$IssuanceTransformRules"
+
+Grant-ADFSApplicationPermission -ClientRoleIdentifier $ServerApplicationIdentifier `
+-ServerRoleIdentifier $RelyingPartyTrustIdentifier `
+-ScopeName "allatclaims", "openid"
+
+@"
+authentication:
+ oidc:
+ clientID: $($ADFSApp.Identifier)
+ clientSecret: $($ADFSApp.ClientSecret)
+ extraParams: resource=$RelyingPartyTrustIdentifier
+ group: groups
+ groupPrefix: ""
+ issuerURI: https://$DnsName/adfs
+ kubectlRedirectURL: $RedirectURI1
+ scopes: openid
+ username: upn
+ usernamePrefix: ""
+"@
\ No newline at end of file
diff --git a/examples/cloud-operations/adfs/templates/gssh.sh.tpl b/examples/cloud-operations/adfs/templates/gssh.sh.tpl
new file mode 100644
index 0000000000..c61460ba2d
--- /dev/null
+++ b/examples/cloud-operations/adfs/templates/gssh.sh.tpl
@@ -0,0 +1,30 @@
+#!/bin/bash
+#
+# Copyright 2022 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
+#
+# https://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.
+
+host="$${@: -2: 1}"
+cmd="$${@: -1: 1}"
+
+gcloud_args="
+--tunnel-through-iap
+--zone=${zone}
+--project=${project_id}
+--quiet
+--no-user-output-enabled
+--
+-C
+"
+
+exec gcloud compute ssh "$host" $gcloud_args "$cmd"
\ No newline at end of file
diff --git a/examples/cloud-operations/adfs/templates/vars.yaml.tpl b/examples/cloud-operations/adfs/templates/vars.yaml.tpl
new file mode 100644
index 0000000000..8e67a54943
--- /dev/null
+++ b/examples/cloud-operations/adfs/templates/vars.yaml.tpl
@@ -0,0 +1,17 @@
+# Copyright 2022 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
+#
+# https://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.
+
+project_id: ${project_id}
+ad_dns_domain_name: ${ad_dns_domain_name}
+adfs_dns_domain_name: ${adfs_dns_domain_name}
\ No newline at end of file
diff --git a/examples/cloud-operations/adfs/variables.tf b/examples/cloud-operations/adfs/variables.tf
new file mode 100644
index 0000000000..4a8b70f260
--- /dev/null
+++ b/examples/cloud-operations/adfs/variables.tf
@@ -0,0 +1,100 @@
+# Copyright 2022 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
+#
+# https://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.
+
+variable "project_create" {
+ description = "Parameters for the creation of the new project."
+ type = object({
+ billing_account_id = string
+ parent = string
+ })
+ default = null
+}
+
+variable "project_id" {
+ description = "Host project ID."
+ type = string
+}
+
+variable "prefix" {
+ description = "Prefix for the resources created."
+ type = string
+ default = null
+}
+
+variable "network_config" {
+ description = "Network configuration"
+ type = object({
+ network = string
+ subnet = string
+ })
+ default = null
+}
+
+variable "ad_dns_domain_name" {
+ description = "AD DNS domain name."
+ type = string
+}
+
+variable "adfs_dns_domain_name" {
+ description = "ADFS DNS domain name."
+ type = string
+}
+
+variable "disk_size" {
+ description = "Disk size."
+ type = number
+ default = 50
+}
+
+variable "disk_type" {
+ description = "Disk type."
+ type = string
+ default = "pd-ssd"
+}
+
+variable "image" {
+ description = "Image."
+ type = string
+ default = "projects/windows-cloud/global/images/family/windows-2022"
+}
+
+variable "instance_type" {
+ description = "Instance type."
+ type = string
+ default = "n1-standard-2"
+}
+
+variable "region" {
+ description = "Region."
+ type = string
+ default = "europe-west1"
+}
+
+variable "zone" {
+ description = "Zone."
+ type = string
+ default = "europe-west1-c"
+}
+
+variable "ad_ip_cidr_block" {
+ description = "Managed AD IP CIDR block."
+ type = string
+ default = "10.0.0.0/24"
+}
+
+variable "subnet_ip_cidr_block" {
+ description = "Subnet IP CIDR block."
+ type = string
+ default = "10.0.1.0/28"
+}
\ No newline at end of file
diff --git a/examples/cloud-operations/adfs/versions.tf b/examples/cloud-operations/adfs/versions.tf
new file mode 100644
index 0000000000..7a118563ed
--- /dev/null
+++ b/examples/cloud-operations/adfs/versions.tf
@@ -0,0 +1,32 @@
+# Copyright 2022 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
+#
+# https://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.
+
+terraform {
+ required_version = ">= 1.1.0"
+ required_providers {
+ local = {
+ version = ">= 2.2.3"
+ }
+ google = {
+ source = "hashicorp/google"
+ version = ">= 4.17.0"
+ }
+ google-beta = {
+ source = "hashicorp/google-beta"
+ version = ">= 4.17.0"
+ }
+ }
+}
+
+
diff --git a/tests/examples/cloud_operations/adfs/__init__.py b/tests/examples/cloud_operations/adfs/__init__.py
new file mode 100644
index 0000000000..6d6d1266c3
--- /dev/null
+++ b/tests/examples/cloud_operations/adfs/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022 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.
diff --git a/tests/examples/cloud_operations/adfs/fixture/main.tf b/tests/examples/cloud_operations/adfs/fixture/main.tf
new file mode 100644
index 0000000000..2ddbe6e4aa
--- /dev/null
+++ b/tests/examples/cloud_operations/adfs/fixture/main.tf
@@ -0,0 +1,23 @@
+/**
+ * Copyright 2022 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.
+ */
+
+module "test" {
+ source = "../../../../../examples/cloud-operations/adfs"
+ project_create = var.project_create
+ project_id = var.project_id
+ ad_dns_domain_name = var.ad_dns_domain_name
+ adfs_dns_domain_name = var.adfs_dns_domain_name
+}
diff --git a/tests/examples/cloud_operations/adfs/fixture/variables.tf b/tests/examples/cloud_operations/adfs/fixture/variables.tf
new file mode 100644
index 0000000000..a48a77e21e
--- /dev/null
+++ b/tests/examples/cloud_operations/adfs/fixture/variables.tf
@@ -0,0 +1,103 @@
+# Copyright 2022 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
+#
+# https://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.
+
+# Copyright 2022 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
+#
+# https://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.
+
+variable "project_create" {
+ type = object({
+ billing_account_id = string
+ parent = string
+ })
+ default = null
+}
+
+variable "project_id" {
+ type = string
+ default = "my-project"
+}
+
+variable "prefix" {
+ type = string
+ default = null
+}
+
+variable "network_config" {
+ type = object({
+ network = string
+ subnet = string
+ })
+ default = null
+}
+
+variable "ad_dns_domain_name" {
+ type = string
+ default = "example.com"
+}
+
+variable "adfs_dns_domain_name" {
+ type = string
+ default = "adfs.example.com"
+}
+
+variable "disk_size" {
+ type = number
+ default = 50
+}
+
+variable "disk_type" {
+ type = string
+ default = "pd-ssd"
+}
+
+variable "image" {
+ type = string
+ default = "projects/windows-cloud/global/images/family/windows-2022"
+}
+
+variable "instance_type" {
+ type = string
+ default = "n1-standard-2"
+}
+
+variable "region" {
+ type = string
+ default = "europe-west1"
+}
+
+variable "zone" {
+ type = string
+ default = "europe-west1-c"
+}
+
+variable "ad_ip_cidr_block" {
+ type = string
+ default = "10.0.0.0/24"
+}
+
+variable "subnet_ip_cidr_block" {
+ type = string
+ default = "10.0.1.0/28"
+}
diff --git a/tests/examples/cloud_operations/adfs/test_plan.py b/tests/examples/cloud_operations/adfs/test_plan.py
new file mode 100644
index 0000000000..4cb97aeff0
--- /dev/null
+++ b/tests/examples/cloud_operations/adfs/test_plan.py
@@ -0,0 +1,19 @@
+# Copyright 2022 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.
+
+def test_resources(e2e_plan_runner):
+ "Test that plan works and the numbers of resources is as expected."
+ modules, resources = e2e_plan_runner()
+ assert len(modules) == 4
+ assert len(resources) == 17