diff --git a/.gitignore b/.gitignore index 01b747b9..eda2ee02 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,5 @@ cloud_sql_proxy examples/cloud-operations/binauthz/tenant-setup.yaml examples/cloud-operations/binauthz/app/app.yaml env/ +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 00000000..70465c64 --- /dev/null +++ b/examples/cloud-operations/adfs/README.md @@ -0,0 +1,76 @@ +# AD FS + +This 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 00000000..e822a18f --- /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 00000000..bef01593 --- /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 00000000..96d2e3ba --- /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 00000000..5ba88d20 --- /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 00000000..38d26253 --- /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 00000000..f11f9fa0 --- /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 00000000..f95bc7f0 --- /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 00000000..ccbe99d2 --- /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 00000000..eeb6e1fc --- /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 00000000..4ca1d7f2 --- /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 00000000..6b846f41 --- /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 00000000..c5cca554 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 00000000..b7f8e920 --- /dev/null +++ b/examples/cloud-operations/adfs/main.tf @@ -0,0 +1,191 @@ +# 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" + ] +} + +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 00000000..c61460ba --- /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 00000000..8e67a549 --- /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 00000000..4a8b70f2 --- /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 00000000..7a118563 --- /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 00000000..6d6d1266 --- /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 00000000..2ddbe6e4 --- /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 00000000..a48a77e2 --- /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 00000000..7aeafc1d --- /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) == 16