Skip to content
This repository has been archived by the owner on Dec 6, 2024. It is now read-only.

Commit

Permalink
fix: Allow onboarding member account in non AppStream supported regio…
Browse files Browse the repository at this point in the history
…ns (#844)
  • Loading branch information
nguyen102 authored Jan 3, 2022
1 parent a125088 commit 93dc465
Show file tree
Hide file tree
Showing 6 changed files with 8,814 additions and 9,317 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Parameters:
Description: Please enter the IP range (CIDR notation) for the Workspace subnet. This value is only used if AppStream is enabled.
Type: String
Default: 10.0.64.0/19

AppStreamFleetDesiredInstances:
Description: The desired number of streaming instances.
Type: Number
Expand Down Expand Up @@ -121,7 +121,7 @@ Metadata:
Parameters:
- VpcCidr
- PublicSubnetCidr

Conditions:
isAppStream: !Equals
- !Ref EnableAppStream
Expand Down Expand Up @@ -372,8 +372,9 @@ Resources:
- cloudformation:DeleteStack
- cloudformation:DescribeStacks
- cloudformation:DescribeStackEvents
Resource: !Sub 'arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/SC-*'
Resource: !Sub 'arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/analysis-*' # ALB stack used this pattern
Resource:
- !Sub 'arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/SC-*'
- !Sub 'arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/analysis-*' # ALB stack used this pattern
- Effect: Allow
Action:
- sagemaker:CreatePresignedNotebookInstanceUrl
Expand Down Expand Up @@ -622,7 +623,7 @@ Resources:
Condition: isNotAppStream
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select [0, !GetAZs ]
AvailabilityZone: !Select [0, !GetAZs '' ]
CidrBlock: !Ref PublicSubnetCidr
MapPublicIpOnLaunch: true
Tags:
Expand Down Expand Up @@ -717,8 +718,6 @@ Resources:
Tags:
- Key: Name
Value: Private Workspace Subnet



PrivateWorkspaceRouteTable:
Type: 'AWS::EC2::RouteTable'
Expand Down Expand Up @@ -938,7 +937,7 @@ Resources:
IpProtocol: '-1'
- DestinationSecurityGroupId: !Ref WorkspaceSecurityGroup
IpProtocol: '-1'

AppStreamSecurityGroupEgress:
Type: AWS::EC2::SecurityGroupEgress
Condition: isAppStream
Expand Down Expand Up @@ -1049,7 +1048,7 @@ Outputs:
Description: Workspace subnet
Condition: isAppStream
Value: !Ref PrivateWorkspaceSubnet

AppStreamSecurityGroup:
Description: AppStream Security Group
Condition: isAppStream
Expand All @@ -1061,7 +1060,7 @@ Outputs:
Description: SageMaker Security Group
Condition: isAppStream
Value: !Ref SageMakerSecurityGroup

AppStreamFleet:
Description: AppStream Fleet
Condition: isAppStream
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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.
*/

const ServicesContainer = require('@aws-ee/base-services-container/lib/services-container');
const fs = require('fs');
const SettingsServiceMock = require('@aws-ee/base-services/lib/settings/env-settings-service');

const { yamlParse } = require('yaml-cfn');
const CfnTemplateService = require('../cfn-templates/cfn-template-service');

describe('cfn-template-service', () => {
let service = null;
let onboardAccount = null;
let settings = null;

beforeEach(async () => {
const pluginRegistryService = {
getPlugins: jest.fn(() => {
return [];
}),
initService: jest.fn(),
};

const container = new ServicesContainer();
container.register('settings', new SettingsServiceMock());
container.register('cfnTemplateService', new CfnTemplateService());
container.register('pluginRegistryService', pluginRegistryService);
await container.initServices();

// Get instance of the service we are testing
service = await container.find('cfnTemplateService');
settings = await container.find('settings');

// Grab cfn template for testing
onboardAccount = fs.readFileSync('./lib/__tests__/test-cfn-template.yml', 'utf8');
});

describe('getTemplate', () => {
it('should remove AppStream resources if AppStream is not enabled', async () => {
// BUILD
// Disable AppStream
settings.getBoolean = jest.fn(key => {
if (key === 'isAppStreamEnabled') {
return false;
}
throw new Error(`Unexpected setting: ${key}`);
});

await service.add({ name: 'onboard-account', yaml: onboardAccount });

// OPERATE
const updatedTemplate = await service.getTemplate('onboard-account');

// CHECK
expect(updatedTemplate).toBeDefined();

const parsedYaml = yamlParse(updatedTemplate);
expect(parsedYaml.Resources.PolicyCfnStatus).toBeDefined();
expect(parsedYaml.Outputs.CrossAccountEnvMgmtRoleArn).toBeDefined();
// Only 1 policy, because policy with IF statement for AppStream was removed
expect(parsedYaml.Resources.CrossAccountRoleEnvMgmt.Properties.Policies.length).toEqual(1);
expect(
parsedYaml.Resources.CrossAccountEnvMgmtPermissionsBoundary.Properties.PolicyDocument.Statement.length,
).toEqual(1);

// AppStream resources and outputs should not be defined
expect(parsedYaml.Resources.Route53HostedZone).toBeUndefined();
expect(parsedYaml.Resources.AppStreamFleet).toBeUndefined();
expect(parsedYaml.Resources.AppStreamStack).toBeUndefined();
expect(parsedYaml.Resources.AppStreamStackFleetAssociation).toBeUndefined();
expect(parsedYaml.Outputs.AppStreamFleet).toBeUndefined();
expect(parsedYaml.Outputs.AppStreamStack).toBeUndefined();
});

it('should NOT remove AppStream resources if AppStream is enabled', async () => {
// BUILD
// Enable AppStream
settings.getBoolean = jest.fn(key => {
if (key === 'isAppStreamEnabled') {
return true;
}
throw new Error(`Unexpected setting: ${key}`);
});

// OPERATE
await service.add({ name: 'onboard-account', yaml: onboardAccount });

// CHECK
const updatedTemplate = await service.getTemplate('onboard-account');
expect(updatedTemplate).toBeDefined();
expect(updatedTemplate).toEqual(onboardAccount);

// All resources should be available
const parsedYaml = yamlParse(updatedTemplate);
expect(parsedYaml.Resources.CrossAccountRoleEnvMgmt.Properties.Policies.length).toEqual(2);
expect(
parsedYaml.Resources.CrossAccountEnvMgmtPermissionsBoundary.Properties.PolicyDocument.Statement.length,
).toEqual(2);
expect(parsedYaml.Resources.PolicyCfnStatus).toBeDefined();
expect(parsedYaml.Resources.Route53HostedZone).toBeDefined();
expect(parsedYaml.Resources.AppStreamFleet).toBeDefined();
expect(parsedYaml.Resources.AppStreamStack).toBeDefined();
expect(parsedYaml.Resources.AppStreamStackFleetAssociation).toBeDefined();
expect(parsedYaml.Outputs.CrossAccountEnvMgmtRoleArn).toBeDefined();
expect(parsedYaml.Outputs.AppStreamFleet).toBeDefined();
expect(parsedYaml.Outputs.AppStreamStack).toBeDefined();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
AWSTemplateFormatVersion: 2010-09-09

Description: This stack provisions resources necessary to use this AWS account with Service Workbench.

Parameters:
EnableAppStream:
Type: String
AllowedValues: [true, false]
Description: Onboard this account to support AppStream

Resources:
Route53HostedZone:
Type: AWS::Route53::HostedZone
Condition: isAppStreamAndCustomDomain
Properties:
Name: !Ref DomainName
VPCs:
- VPCId: !Ref VPC
VPCRegion: !Ref "AWS::Region"

CrossAccountRoleEnvMgmt:
Type: 'AWS::IAM::Role'
Properties:
RoleName: !Join ['-', [Ref: Namespace, 'xacc-env-mgmt']]
Path: '/'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: 'Allow'
Principal:
AWS:
- !Join [':', ['arn:aws:iam:', Ref: CentralAccountId, 'root']]
- !Ref ApiHandlerArn
- !Ref WorkflowRoleArn
Action:
- 'sts:AssumeRole'
Condition:
StringEquals:
sts:ExternalId: !Ref ExternalId
Policies:
- PolicyName: iam-role-access
PolicyDocument:
Statement:
- Effect: Allow
Action:
- iam:CreateRole
- iam:TagRole
- iam:GetRolePolicy
- iam:PutRolePolicy
- iam:DeleteRolePolicy
- iam:ListRolePolicies
- iam:ListAttachedRolePolicies
- iam:UpdateAssumeRolePolicy
- iam:UpdateRoleDescription
- iam:AttachRolePolicy
- iam:DetachRolePolicy
Resource:
- !Sub 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${LaunchConstraintRolePrefix}LaunchConstraint'
- !Sub 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/*presigned-url-sagemaker-notebook-role'
- !If
- isAppStreamAndCustomDomain
- PolicyName: route53-access
PolicyDocument:
Statement:
- Effect: Allow
Action:
- route53:ChangeResourceRecordSets
Resource:
- !Sub 'arn:aws:route53:::hostedzone/${Route53HostedZone}'
- !Ref 'AWS::NoValue'

CrossAccountEnvMgmtPermissionsBoundary:
Type: AWS::IAM::ManagedPolicy
Properties:
Description: Permission boundary for cross account EnvMgmt role
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- iam:GetGroup
- iam:GetRole
- iam:GetUser
- iam:ListGroups
- iam:ListRoles
- iam:ListUsers
Resource: '*' # These non-mutating IAM actions cover the permissions in managed policy AWSServiceCatalogAdminFullAccess
- !If
- isAppStreamAndCustomDomain
- Effect: Allow
Action:
- route53:ChangeResourceRecordSets
Resource:
- !Sub 'arn:aws:route53:::hostedzone/${Route53HostedZone}'
- !Ref 'AWS::NoValue'

PolicyCfnStatus:
Type: AWS::IAM::ManagedPolicy
Properties:
Description: Allows main account to onboard and check status of aws accounts
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- cloudformation:DescribeStacks
- cloudformation:GetTemplate
Resource: !Sub 'arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/initial-stack*'

AppStreamFleet:
Type: 'AWS::AppStream::Fleet'
Condition: isAppStream
Properties:
ComputeCapacity:
DesiredInstances: !Ref AppStreamFleetDesiredInstances
Description: 'SWB AppStream Fleet'
DisconnectTimeoutInSeconds: !Ref AppStreamDisconnectTimeoutSeconds
DisplayName: 'SWB Fleet'
EnableDefaultInternetAccess: False
FleetType: !Ref AppStreamFleetType
IdleDisconnectTimeoutInSeconds: !Ref AppStreamIdleDisconnectTimeoutSeconds
ImageArn: !Sub 'arn:aws:appstream:${AWS::Region}:${CentralAccountId}:image/${AppStreamImageName}'
InstanceType: !Ref AppStreamInstanceType
MaxUserDurationInSeconds: !Ref AppStreamMaxUserDurationSeconds
Name: !Sub ${Namespace}-ServiceWorkbenchFleet
StreamView: 'APP'
VpcConfig:
SecurityGroupIds:
- !Ref AppStreamSecurityGroup
SubnetIds:
- !Ref PrivateAppStreamSubnet

AppStreamStack:
Type: 'AWS::AppStream::Stack'
Condition: isAppStream
Properties:
ApplicationSettings:
Enabled: False
Description: 'SWB AppStream Stack'
DisplayName: 'SWB Stack'
Name: !Sub ${Namespace}-ServiceWorkbenchStack
UserSettings:
- Action: 'CLIPBOARD_COPY_FROM_LOCAL_DEVICE'
Permission: 'ENABLED'
- Action: 'CLIPBOARD_COPY_TO_LOCAL_DEVICE'
Permission: 'DISABLED'
- Action: 'FILE_DOWNLOAD'
Permission: 'DISABLED'
- Action: 'FILE_UPLOAD'
Permission: 'DISABLED'
- Action: 'PRINTING_TO_LOCAL_DEVICE'
Permission: 'DISABLED'

AppStreamStackFleetAssociation:
Type: AWS::AppStream::StackFleetAssociation
Condition: isAppStream
Properties:
FleetName: !Ref AppStreamFleet
StackName: !Ref AppStreamStack

Outputs:
CrossAccountEnvMgmtRoleArn:
Description: The arn of the cross account role for environment management using AWS Service Catalog
Value: !GetAtt [CrossAccountRoleEnvMgmt, Arn]

#------------AppStream Output Below-------
AppStreamFleet:
Description: AppStream Fleet
Condition: isAppStream
Value: !Ref AppStreamFleet

AppStreamStack:
Description: AppStream Stack
Condition: isAppStream
Value: !Ref AppStreamStack
Loading

0 comments on commit 93dc465

Please sign in to comment.