AWSTemplateFormatVersion: '2010-09-09' Transform: - WorkSpaceDefaultRoleMacro Description: >- This template provisions an Amazon WorkSpace for the admin user of AWS Managed Microsoft AD. **WARNING** This template creates Amazon WorkSpace, SSM automation, Lambda function, and related resources. You'll be billed for the AWS resources used if you create a stack from this template. (qs-1t9sl8r26) Metadata: QuickStartDocumentation: EntrypointName: 'Parameters for provisioning WorkSpace in AWS Managed AD Directory Service' Order: '2' cfn-lint: config: ignore_checks: - E9101 - W9006 - W9002 - W9003 ignore_reason: - "Execution part SSM Automation" AWS::CloudFormation::Interface: ParameterGroups: - Label: default: WorkSpaces and WorkSpaces Directory configuration Parameters: - DirectoryID - DomainUser - DomainDNSName - Tenancy - EnableSelfService - EnableWorkDocs - BundleId - ComputeTypeName - RunningMode - UserVolumeEncryptionEnabled - RootVolumeEncryptionEnabled - VolumeEncryptionKey - UserVolumeSizeGib - RootVolumeSizeGib - VPCID - PrivateSubnet1ID - PrivateSubnet2ID - Label: default: VPC configuration Parameters: - VPCID - PrivateSubnet1ID - PrivateSubnet2ID ParameterLabels: DirectoryID: default: Directory ID DomainUser: default: Domain admin user DomainDNSName: default: DNS name for domain Tenancy: default: WorkSpaces VPC tenancy BundleId: default: Enter WorkSpace bundle ID ComputeTypeName: default: 'Workspaces compute type' RunningMode: default: WorkSpaces running mode EnableSelfService: default: Enable self-service on WorkSpace directory EnableWorkDocs: default: Enable WorkDocs on WorkSpace directory UserVolumeEncryptionEnabled: default: Encrypt WorkSpace user volume RootVolumeEncryptionEnabled: default: Encrypt WorkSpace user volume VolumeEncryptionKey: default: Enter WorkSpace KMS key to encrypt volumes UserVolumeSizeGib: default: Size of WorkSpace user volume storage RootVolumeSizeGib: default: Size of WorkSpace root volume storage QSS3BucketName: AllowedPattern: ^[0-9a-z]+([0-9a-z-\.]*[0-9a-z])*$ ConstraintDescription: >- The S3 bucket name can include numbers, lowercase letters, and hyphens (-), but it cannot start or end with a hyphen. Default: 'aws-quickstart' Description: >- Name of the S3 bucket for your copy of the deployment assets. Keep the default name unless you're customizing the template. Changing the name updates code references to point to a new location. MinLength: 3 MaxLength: 63 Type: 'String' QSS3KeyPrefix: AllowedPattern: ^([0-9a-zA-Z!-_\.\*'\(\)/]+/)*$ ConstraintDescription: >- The S3 key prefix can include numbers, lowercase letters, uppercase letters, hyphens (-), underscores (_), periods (.), asterisks (*), single quotes ('), open parenthesis ((), close parenthesis ()), and forward slashes (/). End the prefix with a forward slash. Default: quickstart-freeradius-mfa-workspaces/ Description: >- S3 key prefix that is used to simulate a folder for your copy of the deployment assets. Keep the default prefix unless you are customizing the template. Changing the prefix updates code references to point to a new location. Type: 'String' VPCID: default: VPC ID PrivateSubnet1ID: default: Private Subnet 1 ID PrivateSubnet2ID: default: Private Subnet 2 ID Parameters: DirectoryID: Default: 'd-xxxxxx' Description: Directory ID of AWS Managed ID. MaxLength: '15' MinLength: '5' Type: String DomainUser: Default: Admin Description: Domain admin user. Type: String DomainDNSName: Default: example.com Description: Domain DNS name. MaxLength: '25' MinLength: '5' Type: String EnableWorkDocs: AllowedValues: - 'true' - 'false' Default: 'false' Description: WorkDocs is enabled or disabled. If enabled and WorkDocs isn't available in the region, you'll receive an OperationNotSupportedException error. Set EnableWorkDocs to disabled and try again. Type: String EnableSelfService: AllowedValues: - 'true' - 'false' Default: 'true' Description: Self-service capabilities are enabled or disabled. Type: String Tenancy: AllowedValues: - 'DEDICATED' - 'SHARED' Default: 'SHARED' Description: WorkSpace directory is dedicated or shared. To use Bring Your Own License (BYOL) images, set this value to DEDICATED and enable your AWS account for BYOL. If your account hasn't been enabled for BYOL, you'll receive an InvalidParameterValuesException error. Type: String RootVolumeEncryptionEnabled: AllowedValues: - 'true' - 'false' Default: 'true' Description: Data stored on root volume is encrypted. Type: String UserVolumeEncryptionEnabled: AllowedValues: - 'true' - 'false' Default: 'true' Description: Data stored on user volume is encrypted. Type: String VolumeEncryptionKey: Type: String Default: alias/aws/workspaces Description: Symmetric AWS KMS key used to encrypt data stored on your WorkSpace. WorkSpaces doesn't support asymmetric KMS keys. Specify KMS Key ID. RootVolumeSizeGib: Default: '80' Description: Size of user storage. Refer to Modify Volume Sizes - https://docs.aws.amazon.com/workspaces/latest/adminguide/modify-workspaces.html#change_volume_sizes. Type: Number UserVolumeSizeGib: Default: '50' Description: Size of user storage. Refer to Modify Volume Sizes - https://docs.aws.amazon.com/workspaces/latest/adminguide/modify-workspaces.html#change_volume_sizes. Type: Number BundleId: Default: 'wsb-gm4d5tx2v' AllowedPattern: '^wsb-[0-9a-z]{8,63}$' Description: Identifier of bundle for WorkSpace. Use DescribeWorkspaceBundles to list available bundles. Type: String ComputeTypeName: Default: 'PERFORMANCE' AllowedValues: - VALUE - STANDARD - POWERPRO - POWER - PERFORMANCE - GRAPHICSPRO - GRAPHICS Description: Compute type. Valid values are VALUE | STANDARD | PERFORMANCE | POWER | GRAPHICS | POWERPRO | GRAPHICSPRO. Ensure bundle ID supports compute type selected. Type: String RunningMode: Default: 'AUTO_STOP' AllowedValues: - ALWAYS_ON - AUTO_STOP Description: Choose Workspaces running mode. Type: String MgmtServerNetBIOSName: AllowedPattern: '[a-zA-Z0-9\-]+' Default: MGMT1 Description: NetBIOS name of Management Server server (maximum of 15 characters). MaxLength: '15' MinLength: '1' Type: String VPCID: Default: '' Description: VPC ID for Workspaces. Type: String VPCCIDR: AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/(1[6-9]|2[0-8]))$ ConstraintDescription: CIDR block parameter (must be in the form x.x.x.x/16-28). Default: 10.0.0.0/16 Description: CIDR Block for VPC. Type: String ADServer1PrivateIP: AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$ Default: 172.31.16.20 Description: Fixed private IP for first AD Domain Controller located in Availability Zone 1. Type: String ADServer2PrivateIP: AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$ Default: 172.31.32.10 Description: Fixed private IP for second AD Domain Controller located in Availability Zone 2. Type: String PrivateSubnet1ID: Description: Choose ID of private subnet 1 in Availability Zone 1 (example-subnet-a0246dcd). Type: AWS::EC2::Subnet::Id PrivateSubnet2ID: Description: Choose ID of private subnet 2 in Availability Zone 2 (example-subnet-a0246dcd). Type: AWS::EC2::Subnet::Id ## SNS SNSDeliveryLambdaExecutionRoleExportName: Description: Name of export for execution role for SNS publisher Lambda. Type: String Default: '' SNSStackStatusTopicExportName: Description: Name of export of topic subscribed by user earlier and exported by parent stack. Type: String Default: '' SNSDeliveryLambdaExecutionRoleExportValue: Description: Execution role for SNS publisher Lambda. Type: String Default: '' SNSStackStatusTopicExportValue: Description: Topic subscribed by user earlier and exported by parent stack. Type: String Default: '' QSS3BucketName: AllowedPattern: ^[0-9a-z]+([0-9a-z-\.]*[0-9a-z])*$ ConstraintDescription: >- The S3 bucket name can include numbers, lowercase letters, and hyphens (-), but it cannot start or end with a hyphen. Default: 'aws-quickstart' Description: >- Name of the S3 bucket for your copy of the deployment assets. Keep the default name unless you are customizing the template. Changing the name updates code references to point to a new location. MinLength: 3 MaxLength: 63 Type: 'String' QSS3KeyPrefix: AllowedPattern: ^([0-9a-zA-Z!-_\.\*'\(\)/]+/)*$ ConstraintDescription: >- The S3 key prefix can include numbers, lowercase letters, uppercase letters, hyphens (-), underscores (_), periods (.), asterisks (*), single quotes ('), open parenthesis ((), close parenthesis ()), and forward slashes (/). End the prefix with a forward slash. Default: quickstart-freeradius-mfa-workspaces/ Description: >- S3 key prefix that is used to simulate a folder for your copy of the deployment assets. Keep the default prefix unless you are customizing the template. Changing the prefix updates code references to point to a new location. Type: 'String' Conditions: UsingDefaultBucket: !Equals [!Ref QSS3BucketName, 'aws-quickstart'] #CheckForThirdPrivateAZ: !Equals [!Ref PrivateSubnet3CIDR, ""] #CheckForThirdPublicAZ: !Equals [!Ref PublicSubnet3CIDR, ""] WorkspaceVolumeEncryptionCheck: !Or [!Equals [!Ref UserVolumeEncryptionEnabled, "true"], !Equals [!Ref RootVolumeEncryptionEnabled, "true"]] Resources: StoreDirectoryID: Type: AWS::SSM::Parameter Properties: Name: /ManagedAD/DirectoryID Type: String Value: !Ref 'DirectoryID' Description: AWS Microsoft AD ID. DomainUserBindDN: Type: AWS::SSM::Parameter Properties: Description: BindDN of domain user. Used for LinOTP LDAP connection. Name: /LinOTP/Config/MgAD/BindDN Type: String Value: 'CN=xxx,OU=xxxx,DC=xxx,DC=xxx' DomainUserBaseDN: Type: AWS::SSM::Parameter Properties: Description: BaseDN of domain user. Used for LinOTP LDAP connection. Name: /LinOTP/Config/MgAD/BaseDN Type: String Value: 'OU=xxxx,DC=xxx,DC=xxx' DNSServers: Type: AWS::SSM::Parameter Properties: Description: Directory Service DNS servers. Used for LinOTP LDAP connection. Name: /LinOTP/Config/MgAD/DNSServers Type: String Value: 'x.x.x.x' WorkSpacesMgdADSetupSSMDoc: Type: AWS::SSM::Document Properties: DocumentType: Automation Name: !Sub "WorkSpacesMgdADSSMDoc-${AWS::StackName}-${AWS::Region}" Tags: - Key: StackName Value: !Ref AWS::StackName Content: description: Configures AWS Managed AD for WorkSpaces. schemaVersion: '0.3' assumeRole: '{{AutomationAssumeRole}}' parameters: DirectoryId: type: String description: "(Required) ID of EC2 instance." DomainDNSName: type: String description: "(Required) Domain name ID of directory." DomainUser: description: 'Domain user.' type: 'String' default: 'JaneDoe' PrivateSubnet1ID: type: String description: "(Required) Private Subnet 1 ID of AWS Managed AD." PrivateSubnet2ID: type: String description: "(Required) Private Subnet 2 ID of AWS Managed AD." ADServer1PrivateIP: default: '10.0.1.10' description: 'Fixed private IP for first AD Domain Controller located in Availability Zone 1.' type: 'String' ADServer2PrivateIP: default: '10.0.2.10' description: 'Fixed private IP for second AD Domain Controller located in Availability Zone 2.' type: 'String' VPCID: type: String description: "(Required) VPC ID of AWS Managed AD." VPCCIDR: type: String description: "(Required) VPC CIDR of AWS Managed AD VPC." MgmtServerNetBIOSName: default: 'MGMT1' description: 'Management Server NetBIOS name (maximum of 15 characters).' type: 'String' Tenancy: type: String description: "(Optional) WorkSpace tenancy." default: SHARED EnableWorkDocs: default: false description: 'Enable WorkDocs on WorkSpace directory.' type: 'Boolean' EnableSelfService: default: true description: 'Enable self-service on WorkSpace directory.' type: 'Boolean' StackName: default: '' description: 'Stack name input for cfn resource signal.' type: 'String' AutomationAssumeRole: type: String description: "(Optional) ARN of role that allows automation to perform actions on your behalf. " default: '' mainSteps: - name: 'Mgmt1_InstanceId' action: aws:executeAwsApi onFailure: 'step:signalfailure' nextStep: 'UpdateMgtInstanceProfileRole' inputs: Service: ec2 Api: DescribeInstances Filters: - Name: 'tag:Name' Values: ['{{MgmtServerNetBIOSName}}'] - Name: 'instance-state-name' Values: ['running'] outputs: - Name: InstanceId Selector: '$.Reservations[0].Instances[0].InstanceId' Type: 'String' - Name: InstanceProfile Selector: '$.Reservations[0].Instances[0].IamInstanceProfile.Arn' Type: 'String' - name: 'UpdateMgtInstanceProfileRole' action: 'aws:executeScript' onFailure: 'step:signalfailure' nextStep: 'GetDirectoryServiceSG' inputs: Runtime: python3.8 InputPayload: InstanceProfile: '{{Mgmt1_InstanceId.InstanceProfile}}' Handler: script_handler Script: |- import boto3 import json import base64 from botocore.exceptions import ClientError iam_client = boto3.client(service_name="iam") def script_handler(events, context): instance_profile_arn = events['InstanceProfile'] instance_profile_split_array = instance_profile_arn.split('/') instance_profile_name = instance_profile_split_array[1] Instance_profile_object = iam_client.get_instance_profile(InstanceProfileName=instance_profile_name) roles_list = Instance_profile_object['InstanceProfile']['Roles'] role_name = "" for role in roles_list: role_name = role['RoleName'] print(role_name) response = iam_client.put_role_policy(RoleName=role_name,PolicyName='WS-QuickStart-SSM-SM-Policy',PolicyDocument='{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret"],"Resource":"*"},{"Effect":"Allow","Action":"ssm:PutParameter","Resource":["arn:aws:ssm:*:*:parameter/LinOTP/Config/MgAD/BaseDN","arn:aws:ssm:*:*:parameter/LinOTP/Config/MgAD/BindDN","arn:aws:ssm:*:*:parameter/LinOTP/Config/MgAD/DNSServers"]},{"Effect":"Allow","Action":["ec2:AuthorizeSecurityGroupEgress","ds:DescribeDirectories"],"Resource":"*"}]}') return response - name: 'GetDirectoryServiceSG' action: 'aws:executeAwsApi' onFailure: 'step:signalfailure' nextStep: 'UpdateDirectoryServiceSGEgress' inputs: Service: ds Api: DescribeDirectories DirectoryIds: - "{{DirectoryId}}" outputs: - Name: DSSecurityGroupID Type: 'String' Selector: '$.DirectoryDescriptions[0].VpcSettings.SecurityGroupId' - name: UpdateDirectoryServiceSGEgress action: 'aws:executeAwsApi' onFailure: 'step:signalfailure' nextStep: 'Sleep' inputs: Service: ec2 Api: AuthorizeSecurityGroupEgress GroupId: '{{GetDirectoryServiceSG.DSSecurityGroupID}}' IpPermissions: - IpProtocol: udp FromPort: 1812 ToPort: 1812 IpRanges: - CidrIp: '{{VPCCIDR}}' - name: "Sleep" action: "aws:sleep" inputs: Duration: "PT5M" - name: RegisterDSforWorkSpaces onFailure: 'step:signalfailure' nextStep: 'StoreADConfig' action: aws:executeAwsApi inputs: Service: workspaces Api: RegisterWorkspaceDirectory DirectoryId: "{{DirectoryId}}" EnableWorkDocs: '{{EnableWorkDocs}}' EnableSelfService: '{{EnableSelfService}}' SubnetIds: - "{{PrivateSubnet1ID}}" - "{{PrivateSubnet2ID}}" Tenancy: "{{Tenancy}}" # Save AD parameters in SSM Parameter Store - name: 'StoreADConfig' action: 'aws:runCommand' onFailure: 'step:signalfailure' nextStep: 'signalsuccess' inputs: DocumentName: 'AWS-RunPowerShellScript' InstanceIds: - '{{Mgmt1_InstanceId.InstanceId}}' TimeoutSeconds: 3600 Parameters: commands: - | $username = '{{DomainUser}}' $ADServer1PrivateIP = '{{ADServer1PrivateIP}}' $ADServer2PrivateIP = '{{ADServer2PrivateIP}}' function Write-UserADInfoToSSM { [CmdletBinding()] [Alias()] [OutputType([int])] Param ( [Parameter(Mandatory)] [string]$user, [Parameter(Mandatory)] [string]$AD1PrivateIP, [Parameter(Mandatory)] [string]$AD2PrivateIP ) Begin { try{ Write-Output 'Getting AD User Bind and Base DN information' $binddn = (Get-ADUser -Identity $user).distinguishedname $basedn = $binddn -replace '^.*?,(..=.*)$', '$1' } catch{ Write-Error -Message "Unable to get users information for the user: $user" -Category InvalidResult -targetobject $_ } } Process { try{ Write-Output "Writing BaseDN, BindDN and DNS Servers to SSM Parameter Store...." Write-SSMParameter -Name "/LinOTP/Config/MgAD/BaseDN" -Type "String" -Value $basedn -Overwrite $true Write-SSMParameter -Name "/LinOTP/Config/MgAD/BindDN" -Type "String" -Value $binddn -Overwrite $true Write-SSMParameter -Name "/LinOTP/Config/MgAD/DNSServers" -Type "StringList" -Value "$AD1PrivateIP $AD2PrivateIP" -Overwrite $true Write-Output "SSM Parameter values Saved Successfully!!!" $completed = $true } catch{ Write-Warning -Message "Unable to write to SSM Parameter Store, trying this action again in 15 Seconds" Start-Sleep -Seconds 15 } If(!$completed){ try{ Write-Output "Writing BaseDN, BindDN and DNS Servers to SSM Parameter Store...." Write-SSMParameter -Name "/LinOTP/Config/MgAD/BaseDN" -Type "String" -Value $basedn -Overwrite $true Write-SSMParameter -Name "/LinOTP/Config/MgAD/BindDN" -Type "String" -Value $binddn -Overwrite $true Write-SSMParameter -Name "/LinOTP/Config/MgAD/DNSServers" -Type "StringList" -Value "$AD1PrivateIP $AD2PrivateIP" -Overwrite $true Write-Output "SSM Parameter values Saved Successfully!!!" } catch{ Write-Output $_ Write-Error -Message "Unable to write to SSM Parameter Store" -ErrorAction Stop } } } } [string]$token = Invoke-RestMethod -Headers @{"X-aws-ec2-metadata-token-ttl-seconds" = "21600"} -Method PUT -Uri http://169.254.169.254/latest/api/token try { Invoke-RestMethod -Headers @{"X-aws-ec2-metadata-token" = $token} -Method GET -Uri http://169.254.169.254/latest/meta-data/iam/security-credentials } catch { Write-Host "StatusCode:" $_.Exception.Response.StatusCode.Value__ Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription Write-Error -Message "Unable to reach the EC2 metadata service" -Category MetadataError -targetobject $_ } Write-UserADInfoToSSM -user $username -AD1PrivateIP $ADServer1PrivateIP -AD2PrivateIP $ADServer2PrivateIP CloudWatchOutputConfig: CloudWatchOutputEnabled: 'true' CloudWatchLogGroupName: !Sub '/QuickStart/WorkSpaces/${AWS::StackName}' - name: 'signalsuccess' action: 'aws:executeAwsApi' isEnd: True inputs: Service: cloudformation Api: SignalResource LogicalResourceId: 'WaitCondition' StackName: !Sub '${AWS::StackName}' Status: SUCCESS UniqueId: AUTOMATION-SUCCEEDED # If any steps fails signals CFN of Failure - name: 'signalfailure' action: 'aws:executeAwsApi' inputs: Service: cloudformation Api: SignalResource LogicalResourceId: 'WaitCondition' StackName: !Sub '${AWS::StackName}' Status: FAILURE UniqueId: '{{Mgmt1_InstanceId.InstanceId}}' WaitCondition: DependsOn: SSMAutomationInvoker Type: AWS::CloudFormation::WaitCondition CreationPolicy: ResourceSignal: Count: 1 Timeout: PT15M SSMAutomationInvoker: Type: AWS::CloudFormation::CustomResource Properties: ServiceToken: !GetAtt SSMAutomationLambdaFunc.Arn AutomationAssumeRole: !GetAtt WSDSRole.Arn DirectoryId: !Ref DirectoryID DomainDNSName: !Ref DomainDNSName DomainUser: !Ref DomainUser MgmtServerNetBIOSName: !Ref MgmtServerNetBIOSName VPCID: !Ref VPCID VPCCIDR: !Ref VPCCIDR PrivateSubnet1ID: !Ref PrivateSubnet1ID PrivateSubnet2ID: !Ref PrivateSubnet2ID ADServer1PrivateIP: !Ref ADServer1PrivateIP ADServer2PrivateIP: !Ref ADServer2PrivateIP Tenancy: !Ref Tenancy EnableWorkDocs: !Ref EnableWorkDocs EnableSelfService: !Ref EnableSelfService DocumentName: !Ref WorkSpacesMgdADSetupSSMDoc SSMAutomationLambdaFunc: Type: 'AWS::Lambda::Function' Properties: Description: | Lambda function invoking SSM automation. Code: ZipFile: | import boto3 import json import time import cfnresponse import logging logger = logging.getLogger() logger.setLevel(logging.INFO) client = boto3.client('ssm') def lambda_handler(event, context): try: response_data = {} if event['RequestType'] == 'Delete': logger.info("Processing Delete event - " + json.dumps(event)) cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'}) elif event['RequestType'] == 'Update': logger.info("Processing UPDATE event - " + json.dumps(event)) cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'}) else: logger.info("Processing Create event - " + json.dumps(event)) document_name = event['ResourceProperties']['DocumentName'] domain_user = event['ResourceProperties']['DomainUser'] automation_assume_role = event['ResourceProperties']['AutomationAssumeRole'] vpc_id = event['ResourceProperties']['VPCID'] vpc_cidr = event['ResourceProperties']['VPCCIDR'] private_subnet1_id = event['ResourceProperties']['PrivateSubnet1ID'] private_subnet2_id = event['ResourceProperties']['PrivateSubnet2ID'] ad_server1_private_ip = event['ResourceProperties']['ADServer1PrivateIP'] ad_server2_private_ip = event['ResourceProperties']['ADServer2PrivateIP'] domain_dns_name = event['ResourceProperties']['DomainDNSName'] mgt_server1_net_bios_name = event['ResourceProperties']['MgmtServerNetBIOSName'] enable_workdocs = event['ResourceProperties']['EnableWorkDocs'] enable_self_service = event['ResourceProperties']['EnableSelfService'] tenancy = event['ResourceProperties']['Tenancy'] directory_id = event['ResourceProperties']['DirectoryId'] print(automation_assume_role) response = client.start_automation_execution( DocumentName=document_name,Parameters= { 'AutomationAssumeRole':[automation_assume_role], 'Tenancy':[tenancy], 'EnableSelfService':[enable_self_service], 'EnableWorkDocs':[enable_workdocs], 'DomainUser':[domain_user], 'VPCID':[vpc_id], 'VPCCIDR': [vpc_cidr], 'PrivateSubnet1ID':[private_subnet1_id], 'PrivateSubnet2ID':[private_subnet2_id], 'ADServer1PrivateIP':[ad_server1_private_ip], 'ADServer2PrivateIP':[ad_server1_private_ip], 'MgmtServerNetBIOSName':[mgt_server1_net_bios_name], 'DomainDNSName':[domain_dns_name], 'DirectoryId':[directory_id] }) if response['AutomationExecutionId']!= None: #response_data['Data']['executionID'] = response['AutomationExecutionId'] cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data) else: cfnresponse.send(event, context, cfnresponse.FAILED, {'Response': 'Failure'}) except Exception as e: print(e) #cfnresponse.send(event, context, cfnresponse.FAILED, {'Response': 'Failure'}) return response_data MemorySize: 128 Handler: index.lambda_handler Runtime: python3.8 Role: !Sub '${SSMAutomationInvokerLambdaExecutionRole.Arn}' LambdaPermission: Type: AWS::Lambda::Permission Properties: Action: 'lambda:InvokeFunction' Principal: cloudformation.amazonaws.com FunctionName: !GetAtt SSMAutomationLambdaFunc.Arn SourceArn: !Sub ${AWS::StackId} SourceAccount: !Sub ${AWS::AccountId} SSMAutomationInvokerLambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: / Policies: - PolicyName: Fn::Sub: ${AWS::StackName}-lambda-execution-role-policy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: - Fn::Sub: arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/* - Effect: Allow Action: - ssm:DescribeAutomationExecutions - ssm:DescribeAutomationStepExecutions - ssm:GetAutomationExecution - ssm:StartAutomationExecution Resource: - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${WorkSpacesMgdADSetupSSMDoc}:$DEFAULT - Effect: Allow Action: iam:PassRole Resource: !GetAtt WSDSRole.Arn WSDSRole: Type: AWS::IAM::Role Properties: RoleName: !Sub "WSDSRole-${AWS::StackName}-${AWS::Region}" Policies: - PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: s3:GetObject Resource: - !Sub arn:${AWS::Partition}:s3:::aws-ssm-${AWS::Region}/* - !Sub arn:${AWS::Partition}:s3:::aws-windows-downloads-${AWS::Region}/* - !Sub arn:${AWS::Partition}:s3:::amazon-ssm-${AWS::Region}/* - !Sub arn:${AWS::Partition}:s3:::amazon-ssm-packages-${AWS::Region}/* - !Sub arn:${AWS::Partition}:s3:::${AWS::Region}-birdwatcher-prod/* - !Sub arn:${AWS::Partition}:s3:::patch-baseline-snapshot-${AWS::Region}/* - !Sub arn:${AWS::Partition}:s3:::aws-ssm-distributor-file-${AWS::Region}/* - !Sub arn:${AWS::Partition}:s3:::aws-ssm-document-attachments-${AWS::Region}/* PolicyName: SSMAgent - PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: s3:ListBucket Resource: !Sub - 'arn:${AWS::Partition}:s3:::${S3Bucket}' - S3Bucket: !If [UsingDefaultBucket, !Sub '${QSS3BucketName}-${AWS::Region}', !Ref QSS3BucketName] - Effect: Allow Action: s3:GetObject Resource: !Sub - 'arn:${AWS::Partition}:s3:::${S3Bucket}/${QSS3KeyPrefix}*' - S3Bucket: !If [UsingDefaultBucket, !Sub '${QSS3BucketName}-${AWS::Region}', !Ref QSS3BucketName] - Effect: Allow Action: ssm:StartAutomationExecution Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${WorkSpacesMgdADSetupSSMDoc}:$DEFAULT - Effect: Allow Action: ssm:SendCommand Resource: - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:*:document/AWS-RunRemoteScript - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:*:document/AWS-RunPowerShellScript - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:instance/* - Effect: Allow Action: - iam:GetInstanceProfile - iam:PutRolePolicy Resource: - Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:instance-profile/*ADStack*MgmtStack*InstanceProfile* - Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:role/*ADStack*MgmtStack*InstanceRole* - Sid: ReadOperations Effect: Allow Action: - ec2:DescribeInstances - ssm:DescribeInstanceInformation - ssm:ListCommands - ssm:ListCommandInvocations Resource: '*' - Effect: Allow Action: cloudformation:SignalResource Resource: !Sub 'arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${AWS::StackName}/*' PolicyName: AWS-Mgmt-Quick-Start-Policy - PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ds:ConnectDirectory - ec2:DescribeSubnets - ec2:DescribeVpcs - ec2:DescribeNetworkInterfaces - ds:DescribeDirectories Resource: '*' - Effect: Allow Action: - ec2:CreateNetworkInterface - ec2:CreateSecurityGroup - ec2:AuthorizeSecurityGroupIngress - ec2:AuthorizeSecurityGroupEgress - ec2:DeleteSecurityGroup - ec2:RevokeSecurityGroupIngress - ec2:RevokeSecurityGroupEgress - ec2:DeleteNetworkInterface Resource: - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:network-interface/* - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:subnet/* - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:security-group/* - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:vpc/${VPCID} - Effect: Allow Action: - ds:DisableRadius - ds:EnableRadius - ds:UpdateRadius - ds:DeleteDirectory - ds:ConnectDirectory - ds:GetAuthorizedApplicationDetails - ds:ListAuthorizedApplications - ds:AuthorizeApplication - ds:UnauthorizeApplication Resource: - !Sub arn:${AWS::Partition}:ds:${AWS::Region}:${AWS::AccountId}:directory/* - Effect: Allow Action: - ec2:CreateTags - ec2:DeleteTags Resource: - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:*/* PolicyName: Create-ADConnector-Policy - PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: #- ds:* - kms:ListAliases - kms:ListKeys - ec2:DescribeInternetGateways - ec2:DescribeSecurityGroups - ec2:DescribeRouteTables - ec2:DescribeVpcs - ec2:DescribeSubnets - ec2:DescribeNetworkInterfaces - ec2:DescribeAvailabilityZones - workdocs:RegisterDirectory - workdocs:DeregisterDirectory - workdocs:AddUserToGroup - workspaces:DescribeWorkspaceDirectories Resource: '*' - Effect: Allow Action: - ssm:PutParameter - ssm:GetParameter Resource: - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${DomainUserBindDN} - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${DomainUserBaseDN} - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${DNSServers} - Effect: Allow Action: - iam:GetRole - iam:GetRolePolicy - iam:PassRole - iam:GetRole - iam:CreateRole - iam:PutRolePolicy Resource: - !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/* - Effect: Allow Action: - workspaces:RegisterWorkspaceDirectory - workspaces:DeregisterWorkspaceDirectory Resource: - !Sub arn:${AWS::Partition}:workspaces:${AWS::Region}:${AWS::AccountId}:directory/* - !Sub arn:${AWS::Partition}:workspaces:${AWS::Region}:${AWS::AccountId}:workspace/* - !Sub arn:${AWS::Partition}:workspaces:${AWS::Region}:${AWS::AccountId}:*/* - !Sub arn:${AWS::Partition}:workspaces:${AWS::Region}:${AWS::AccountId}:workspacebundle/* PolicyName: RegisterWorkSpace-Directory-Policy - PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - iam:PassRole Resource: - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:instance/* - !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/WSDSRole PolicyName: AWS-SSM-PassRole Path: / ManagedPolicyArns: - !Sub 'arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore' - !Sub 'arn:${AWS::Partition}:iam::aws:policy/CloudWatchAgentServerPolicy' Tags: - Key: StackName Value: !Ref AWS::StackName AssumeRolePolicyDocument: Statement: - Effect: Allow Action: sts:AssumeRole Principal: Service: - ec2.amazonaws.com - ssm.amazonaws.com - lambda.amazonaws.com Version: '2012-10-17' WorkSpace: DependsOn: - WaitCondition - CleanUpCustomResource Type: AWS::WorkSpaces::Workspace Properties: BundleId: !Ref 'BundleId' DirectoryId: !Ref 'DirectoryID' Tags: - Key: Name Value: !Ref 'DomainUser' UserName: !Ref 'DomainUser' WorkspaceProperties: ComputeTypeName: !Ref 'ComputeTypeName' RunningMode: !Ref 'RunningMode' RootVolumeSizeGib: !Ref 'RootVolumeSizeGib' UserVolumeSizeGib: !Ref 'UserVolumeSizeGib' VolumeEncryptionKey: !If [WorkspaceVolumeEncryptionCheck,!Ref 'VolumeEncryptionKey', !Ref AWS::NoValue] UserVolumeEncryptionEnabled: !Ref 'UserVolumeEncryptionEnabled' RootVolumeEncryptionEnabled: !Ref 'RootVolumeEncryptionEnabled' WorkSpaceRegCodeRetriever: Type: AWS::CloudFormation::CustomResource DependsOn: - WorkSpace Properties: ServiceToken: !GetAtt WorkSpaceRegCodeRetrieverLambdaFunction.Arn DirectoryID: !Ref 'DirectoryID' WorkSpaceRegCodeRetrieverLambdaFunction: Type: AWS::Lambda::Function Properties: FunctionName: !Sub ${AWS::StackName}-WSRegCode-Retriever Handler: index.handler Runtime: python3.8 Role: !GetAtt WorkSpaceRegCodeRetrieverLambdaExecutionRole.Arn Code: ZipFile: | import boto3 import json import logging import cfnresponse logger = logging.getLogger() logger.setLevel(logging.INFO) workspace_directory_client = boto3.client('workspaces') def handler(event, context): try: response_data = {} if event['RequestType'] == 'Create': logger.info("Processing Create event - " + json.dumps(event)) directory_id = event['ResourceProperties']['DirectoryID'] described_ws_directory = workspace_directory_client.describe_workspace_directories(DirectoryIds=[directory_id]) directories = described_ws_directory['Directories'] registration_code = '' for directory in directories: registration_code = directory['RegistrationCode'] cfnresponse.send(event, context, cfnresponse.SUCCESS, {'RegistrationCode': registration_code}) elif event['RequestType'] == 'Update': logger.info("Processing UPDATE event - " + json.dumps(event)) cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'}) else: logger.info("Processing UPDATE event - " + json.dumps(event)) cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'}) except Exception as e: print(e) cfnresponse.send(event, context, cfnresponse.FAILED, {'Response': 'Failure'}) return e WorkSpaceRegCodeRetrieverLambdaFunctionPermission: Type: AWS::Lambda::Permission Properties: Action: 'lambda:InvokeFunction' Principal: cloudformation.amazonaws.com FunctionName: !GetAtt WorkSpaceRegCodeRetrieverLambdaFunction.Arn SourceAccount: !Sub ${AWS::AccountId} SourceArn: !Sub ${AWS::StackId} WorkSpaceRegCodeRetrieverLambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: / Policies: - PolicyName: Fn::Sub: ${AWS::StackName}-regcode-retriever-policy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: - Fn::Sub: arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/* - Effect: Allow Action: - workspaces:DescribeWorkspaceDirectories - ds:DescribeDirectories Resource: - "*" ########### CLEAN-UP ############## CleanUpCustomResource: Type: AWS::CloudFormation::CustomResource Properties: MgmtServerNetBIOSName: !Ref MgmtServerNetBIOSName ServiceToken: !GetAtt CleanUpLambdaFunction.Arn WSDirectoryId: !Ref DirectoryID CleanUpLambdaFunction: Type: AWS::Lambda::Function Properties: FunctionName: !Sub ${AWS::StackName}-CleanUpFunction Handler: index.handler Runtime: python3.8 Timeout: 900 Role: !GetAtt CleanUpLambdaExecutionRole.Arn Code: ZipFile: | import boto3 import botocore import json import logging import cfnresponse import time logger = logging.getLogger() logger.setLevel(logging.INFO) iam_client = boto3.client('iam') ec2_client = boto3.client('ec2') workspace_directory_client = boto3.client('workspaces') ds_client = boto3.client('ds') def handler(event, context): try: response_data = {} if event['RequestType'] == 'Create': logger.info("Processing Create event - " + json.dumps(event)) cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'}) elif event['RequestType'] == 'Update': logger.info("Processing UPDATE event - " + json.dumps(event)) cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'}) elif event['RequestType'] == 'Delete': logger.info("Processing Delete event - " + json.dumps(event)) mgmnt_serv_net_bios = event['ResourceProperties']['MgmtServerNetBIOSName'] directory_id = event['ResourceProperties']['WSDirectoryId'] ## De-Register WorkSpace Directory logger.info("De-Registering Workspaces - ") de_register_wd = workspace_directory_client.deregister_workspace_directory(DirectoryId=directory_id) ## Timer to let workspaces de-register directory - describing directory itself doesn't return status of workspaces either ## no waiters currently supported for either workspaces/directory boto ## https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ds.html ## https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/workspaces.html time.sleep(10) ## Removing Inline Policy from main AD Role logger.info("Removing Inline Policy from main AD Role - ") described_instance = ec2_client.describe_instances(Filters=[{'Name':'tag:Name','Values':[mgmnt_serv_net_bios]},{'Name':'instance-state-name','Values':['running']}]) reservations = described_instance['Reservations'] instance_profile_arn = '' for reservation in reservations: instances = reservation['Instances'] for instance in instances: instance_profile_arn = instance['IamInstanceProfile']['Arn'] instance_profile_split_array = instance_profile_arn.split('/') instance_profile_name = instance_profile_split_array[1] instance_profile_object = iam_client.get_instance_profile(InstanceProfileName=instance_profile_name) roles_list = instance_profile_object['InstanceProfile']['Roles'] role_name = "" for role in roles_list: role_name = role['RoleName'] delete_inline_policy = iam_client.delete_role_policy(RoleName=role_name,PolicyName='WS-QuickStart-SSM-SM-Policy') ##Deleting any Managed Policies from Mgmt instance Role list_managed_policies = iam_client.list_attached_role_policies(RoleName=role_name) attached_managed_policies = list_managed_policies['AttachedPolicies'] if attached_managed_policies: for amp in attached_managed_policies: iam_client.detach_role_policy(RoleName=role_name,PolicyArn=amp['PolicyArn']) ## Removing policies from default WorkSpace Role created by Macro logger.info("Removing Inline/Managed Policy from workspace Default role - ") described_role = iam_client.get_role(RoleName='workspaces_DefaultRole') if described_role: described_role_tags = iam_client.list_role_tags(RoleName='workspaces_DefaultRole') tags = described_role_tags['Tags'] if tags: for tag in tags: if (tag['Key'] == 'Created_By' and tag['Value'] == 'WorkSpacesQuickstart_SC2') or (tag['Key'] == 'Created_By' and tag['Value'] == 'WorkSpacesQuickstart_AWS_managed_existing_directory'): logger.info('WorkSpace Tag Found') logger.info('WorkSpaces default Role was created by QuickStart - Hence removing its managed policies') iam_client.detach_role_policy(RoleName='workspaces_DefaultRole',PolicyArn='arn:aws:iam::aws:policy/AmazonWorkSpacesServiceAccess') iam_client.detach_role_policy(RoleName='workspaces_DefaultRole',PolicyArn='arn:aws:iam::aws:policy/AmazonWorkSpacesSelfServiceAccess') polcies_list = iam_client.list_role_policies(RoleName='workspaces_DefaultRole') policies_names = polcies_list['PolicyNames'] for policy_name in policies_names: if policy_name == 'SkyLightSelfServiceAccess' or policy_name == 'SkyLightServiceAccess': iam_client.delete_role_policy(RoleName='workspaces_DefaultRole',PolicyName=policy_name) else: logger.info('No tags found on the WorkSpace Default role') else: logger.info('No WorkSpace Default Role Found') logger.info("Sending SUCCESS Signal back to stack - ") cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data) else: logger.info("Sending SUCCESS Signal back to stack - ") cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data) except botocore.exceptions.ClientError as error: if error.response['Error']['Code'] == 'NoSuchEntityException': print('Error Message: {}'.format(err.response['Error']['Message'])) cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data) else: cfnresponse.send(event, context, cfnresponse.FAILED, {'Response': 'Failure'}) raise error CleanUpLambdaFunctionPermission: Type: AWS::Lambda::Permission Properties: Action: 'lambda:InvokeFunction' Principal: cloudformation.amazonaws.com FunctionName: !GetAtt CleanUpLambdaFunction.Arn SourceAccount: !Sub ${AWS::AccountId} SourceArn: !Sub ${AWS::StackId} CleanUpLambdaExecutionRole: DependsOn: - WaitCondition Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: / Policies: - PolicyName: Fn::Sub: ${AWS::StackName}-cleanup-lambda-execution-role-policy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: - Fn::Sub: arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/* - Effect: Allow Action: - iam:GetRole - iam:DeleteRolePolicy - iam:DetachRolePolicy - iam:ListAttachedRolePolicies Resource: - !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/* - Effect: Allow Action: - iam:GetRole - iam:GetPolicy - iam:DetachRolePolicy - iam:ListRoleTags - iam:ListRolePolicies - iam:DeleteRolePolicy Resource: - !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/workspaces_DefaultRole - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonWorkSpacesServiceAccess - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonWorkSpacesSelfServiceAccess - Effect: Allow Action: - ec2:DescribeInstances Resource: - '*' - Effect: Allow Action: - iam:GetInstanceProfile Resource: - Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:instance-profile/* - Effect: Allow Action: - ec2:ModifyInstanceAttribute Resource: - Fn::Sub: arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:security-group/* - Fn::Sub: arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:instance/* #- Fn::Sub: arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:security-group/${DomainControllersSGID} #- Fn::Sub: arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:security-group/${DirectoryServiceWorkSpaceSG} - Effect: Allow Action: - ec2:DeleteNetworkInterface - ec2:DeleteSecurityGroup Resource: - Fn::Sub: arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:security-group/* - Fn::Sub: arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:network-interface/* - Effect: Allow Action: - workspaces:DeregisterWorkspaceDirectory - workspaces:ModifyClientProperties - workspaces:ModifySelfservicePermissions - workspaces:ModifyWorkspaceAccessProperties - workspaces:ModifyWorkspaceCreationProperties - workspaces:RestoreWorkspace - ds:AuthorizeApplication - ds:UnauthorizeApplication - ds:ConnectDirectory - ds:DeleteDirectory - ds:UpdateRadius - ds:UpdateSettings - ds:DisableRadius - ds:EnableRadius - ds:RemoveIpRoutes - ds:VerifyTrust Resource: - !Join ['',['arn:',!Ref AWS::Partition,':workspaces:',!Ref AWS::Region,':', !Ref AWS::AccountId,':','directory/',!Ref DirectoryID]] - !Join ['',['arn:',!Ref AWS::Partition,':ds:',!Ref AWS::Region,':', !Ref AWS::AccountId,':','directory/',!Ref DirectoryID]] - Effect: Allow Action: - workspaces:DescribeWorkspaces - workspaces:DescribeTags - workspaces:DescribeWorkspaceDirectories - ds:DescribeDirectories - ec2:DescribeInternetGateways - ec2:DescribeSecurityGroups - ec2:DescribeRouteTables - ec2:DescribeVpcs - ec2:DescribeSubnets - ec2:DescribeNetworkInterfaces - ec2:DescribeAvailabilityZones - sns:ListSubscriptions - sns:ListTopics - sns:GetSubscriptionAttributes - kms:ListKeys - kms:ListAliases # - organizations:Describe* # - organizations:List* Resource: '*' - Effect: Allow Action: - sns:ListSubscriptionsByTopic - sns:GetTopicAttributes Resource: - Fn::Sub: arn:${AWS::Partition}:sns:${AWS::Region}:${AWS::AccountId}:* Outputs: WorkspacesRegistrationCode: Description: Registration code of launched WorkSpace. Value: !GetAtt WorkSpaceRegCodeRetriever.RegistrationCode Export: Name: AWS-QuickStart-WorkSpace-RegistrationCode WorkSpaceID: Description: WorkSpace ID for user WorkSpace. Value: !Ref 'WorkSpace' Export: Name: AWS-QuickStart-WorkSpaceID DomainUser: Description: WorkSpace users user name. Value: !Ref 'DomainUser' Export: Name: AWS-QuickStart-WorkSpace-Username SNSLambdaPublisherExecutionRole: Description: 'Role assumed by Lambda SNS publisher.' Value: !Ref SNSDeliveryLambdaExecutionRoleExportValue Export: Name: !Sub '${SNSDeliveryLambdaExecutionRoleExportName}' SNSStackStatusTopic: Description: 'Topic to publish emails to.' Value: !Ref SNSStackStatusTopicExportValue Export: Name: !Sub '${SNSStackStatusTopicExportName}'