From 68e56058ab8ffb7f9c1b25f7bba85131fda7f6e4 Mon Sep 17 00:00:00 2001 From: Miren Esnaola Date: Mon, 13 Jun 2022 02:55:57 +0200 Subject: [PATCH] AD FS example --- .gitignore | 2 + examples/cloud-operations/adfs/README.md | 76 +++++++ .../cloud-operations/adfs/ansible/ansible.cfg | 8 + .../adfs/ansible/inventory/hosts.ini | 1 + .../adfs/ansible/playbook.yaml | 53 +++++ .../roles/ad-provisioning/files/groups.json | 8 + .../ad-provisioning/files/memberships.json | 82 ++++++++ .../roles/ad-provisioning/files/users.json | 56 +++++ .../roles/ad-provisioning/tasks/main.yaml | 58 ++++++ .../roles/adfs-installation/tasks/main.yaml | 104 ++++++++++ .../roles/adfs-prerequisites/tasks/main.yaml | 45 +++++ .../adfs/ansible/roles/anthos/tasks/main.yaml | 67 ++++++ .../roles/server-setup/tasks/main.yaml | 86 ++++++++ .../cloud-operations/adfs/architecture.png | Bin 0 -> 33217 bytes examples/cloud-operations/adfs/main.tf | 191 ++++++++++++++++++ examples/cloud-operations/adfs/outputs.tf | 18 ++ .../adfs/scripts/ad-provisioning/main.py | 98 +++++++++ .../scripts/ad-provisioning/requirements.txt | 3 + .../cloud-operations/adfs/scripts/anthos.ps1 | 66 ++++++ .../adfs/templates/gssh.sh.tpl | 30 +++ .../adfs/templates/vars.yaml.tpl | 17 ++ examples/cloud-operations/adfs/variables.tf | 100 +++++++++ examples/cloud-operations/adfs/versions.tf | 32 +++ .../cloud_operations/adfs/__init__.py | 13 ++ .../cloud_operations/adfs/fixture/main.tf | 23 +++ .../adfs/fixture/variables.tf | 103 ++++++++++ .../cloud_operations/adfs/test_plan.py | 19 ++ 27 files changed, 1359 insertions(+) create mode 100644 examples/cloud-operations/adfs/README.md create mode 100644 examples/cloud-operations/adfs/ansible/ansible.cfg create mode 100644 examples/cloud-operations/adfs/ansible/inventory/hosts.ini create mode 100644 examples/cloud-operations/adfs/ansible/playbook.yaml create mode 100644 examples/cloud-operations/adfs/ansible/roles/ad-provisioning/files/groups.json create mode 100644 examples/cloud-operations/adfs/ansible/roles/ad-provisioning/files/memberships.json create mode 100644 examples/cloud-operations/adfs/ansible/roles/ad-provisioning/files/users.json create mode 100644 examples/cloud-operations/adfs/ansible/roles/ad-provisioning/tasks/main.yaml create mode 100644 examples/cloud-operations/adfs/ansible/roles/adfs-installation/tasks/main.yaml create mode 100644 examples/cloud-operations/adfs/ansible/roles/adfs-prerequisites/tasks/main.yaml create mode 100644 examples/cloud-operations/adfs/ansible/roles/anthos/tasks/main.yaml create mode 100644 examples/cloud-operations/adfs/ansible/roles/server-setup/tasks/main.yaml create mode 100644 examples/cloud-operations/adfs/architecture.png create mode 100644 examples/cloud-operations/adfs/main.tf create mode 100644 examples/cloud-operations/adfs/outputs.tf create mode 100644 examples/cloud-operations/adfs/scripts/ad-provisioning/main.py create mode 100644 examples/cloud-operations/adfs/scripts/ad-provisioning/requirements.txt create mode 100644 examples/cloud-operations/adfs/scripts/anthos.ps1 create mode 100644 examples/cloud-operations/adfs/templates/gssh.sh.tpl create mode 100644 examples/cloud-operations/adfs/templates/vars.yaml.tpl create mode 100644 examples/cloud-operations/adfs/variables.tf create mode 100644 examples/cloud-operations/adfs/versions.tf create mode 100644 tests/examples/cloud_operations/adfs/__init__.py create mode 100644 tests/examples/cloud_operations/adfs/fixture/main.tf create mode 100644 tests/examples/cloud_operations/adfs/fixture/variables.tf create mode 100644 tests/examples/cloud_operations/adfs/test_plan.py 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 0000000000000000000000000000000000000000..c5cca554e9850bf9066ad1980c554eec0a4fa3a0 GIT binary patch literal 33217 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 CZ19o* literal 0 HcmV?d00001 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