def test_with_no_stack_type(self): mock_response = make_describe_stacks_response(MOCK_STACK_DESCRIPTION) with mock_aws.patch_client( 'cloudformation', 'describe_stacks', return_value=mock_response) as mock_describe_stacks: with self.assertRaisesRegexp(RuntimeError, MOCK_STACK_ARN): stack_info.get_stack_info(MOCK_STACK_ARN) mock_describe_stacks.client_factory.assert_called_once_with( 'cloudformation', region_name=MOCK_REGION) mock_describe_stacks.assert_called_once_with( StackName=MOCK_STACK_ARN)
def _add_built_in_settings(settings, stack_arn): stack = stack_info.get_stack_info(stack_arn) if stack.stack_type != stack.STACK_TYPE_RESOURCE_GROUP: return access_stack = stack.deployment.deployment_access if access_stack: pool = access_stack.resources.get_by_logical_id( "PlayerAccessIdentityPool", "Custom::CognitoIdentityPool", True) if pool: settings["CloudCanvas::IdentityPool"] = pool.physical_id print 'Adding setting CloudCanvas::IdentityPool = {}'.format( settings["CloudCanvas::IdentityPool"]) else: print 'Skipping setting CloudCanvas::IdentityPool: PlayerAccessIdentityPool not found.' else: print 'Skipping setting CloudCanvas::IdentityPool: access stack not found.' project_service_lambda = stack.deployment.project.resources.get_by_logical_id( "ServiceLambda", "AWS::Lambda::Function", True) if project_service_lambda: settings[ "CloudCanvas::ServiceLambda"] = project_service_lambda.physical_id print 'Adding setting CloudCanvas::ServiceLambda = {}'.format( settings["CloudCanvas::ServiceLambda"]) else: print 'Skipping setting CloudCanvas::ServiceLambda: resource not found.' settings["CloudCanvas::DeploymentName"] = stack.deployment.deployment_name
def handler(event, context): '''Entry point for the Custom::AccessControl resource handler.''' props = properties.load( event, { 'ConfigurationBucket': properties.String(), # Currently not used 'ConfigurationKey': properties.String() } ) # Depend on unique upload id in key to force Cloud Formation to call handler # Validate RequestType request_type = event['RequestType'] if request_type not in ['Create', 'Update', 'Delete']: raise RuntimeError('Unexpected request type: {}'.format(request_type)) # Get stack_info for the AccessControl resource's stack. stack_arn = event['StackId'] stack = stack_info.get_stack_info(stack_arn) # Physical ID is always the same. physical_resource_id = aws_utils.get_stack_name_from_stack_arn( stack_arn) + '-' + event['LogicalResourceId'] # The AccessControl resource has no output values. data = {} # Accumlate problems encountered so we can give a full report. problems = ProblemList() # Apply access control as determined by the Cloud Canvas stack type. if stack.stack_type == stack.STACK_TYPE_RESOURCE_GROUP: were_changes = _apply_resource_group_access_control( request_type, stack, problems) elif stack.stack_type == stack.STACK_TYPE_DEPLOYMENT_ACCESS: were_changes = _apply_deployment_access_control( request_type, stack, problems) elif stack.stack_type == stack.STACK_TYPE_PROJECT: were_changes = _apply_project_access_control(request_type, stack, problems) else: raise RuntimeError( 'The Custom::AccessControl resource can only be used in resource group, deployment access, or project stack templates.' ) # If there were any problems, provide an error message with all the details. if problems: raise RuntimeError( 'Found invalid AccessControl metadata:\n {}'.format(problems)) # If there were changes, wait a few seconds for them to propagate if were_changes: print 'Delaying {} seconds for change propagation'.format( PROPAGATION_DELAY_SECONDS) time.sleep(PROPAGATION_DELAY_SECONDS) # Successful execution. custom_resource_response.succeed(event, context, data, physical_resource_id)
def _get_project_service_lambda_arn(stack_arn): stack = stack_info.get_stack_info(stack_arn) if stack.stack_type == stack.STACK_TYPE_RESOURCE_GROUP: project_service_lambda = stack.deployment.project.resources.get_by_logical_id( "ServiceLambda", "AWS::Lambda::Function", True) if project_service_lambda: return project_service_lambda.resource_arn return None
def test_with_deployment_stack(self): mock_response = make_describe_stacks_response( make_stack_description(stack_info.StackInfo.STACK_TYPE_DEPLOYMENT)) with mock_aws.patch_client( 'cloudformation', 'describe_stacks', return_value=mock_response) as mock_describe_stacks: result = stack_info.get_stack_info(MOCK_STACK_ARN) self.assertIsInstance(result, stack_info.DeploymentInfo) self.assertEqual(result.stack_type, stack_info.StackInfo.STACK_TYPE_DEPLOYMENT) mock_describe_stacks.client_factory.assert_called_once_with( 'cloudformation', region_name=MOCK_REGION) mock_describe_stacks.assert_called_once_with( StackName=MOCK_STACK_ARN)
def validate_identity_metadata(stack_arn, user_pool_logical_id, client_names): client_names_set = set(client_names) stack = stack_info.get_stack_info(stack_arn) user_pool = stack.resources.get_by_logical_id(user_pool_logical_id) for identity in user_pool.metadata.get('CloudCanvas', {}).get('Identities', []): if 'IdentityPoolLogicalName' not in identity: raise RuntimeError( 'Missing IdentityPoolLogicalName in Identities metadata') client_app = identity.get('ClientApp') if not client_app: raise RuntimeError('Missing ClientApp in Identities metadata') if client_app not in client_names_set: raise RuntimeError( 'ClientApp {} is not in the list of ClientApps'.format( client_app))
def get_identity_mappings(stack_arn, updated_resources={}): # Collect a list of stacks to search for resource metadata. stacks_to_search = [] stack = stack_info.get_stack_info(stack_arn) if stack.stack_type == stack_info.StackInfo.STACK_TYPE_DEPLOYMENT_ACCESS or stack.stack_type == stack_info.StackInfo.STACK_TYPE_RESOURCE_GROUP: # Pools can be linked between resource groups and the deployment access stack. stacks_to_search.extend(stack.deployment.resource_groups) if stack.deployment.deployment_access: stacks_to_search.append(stack.deployment.deployment_access) elif stack.stack_type == stack.STACK_TYPE_PROJECT: # The project stack can have pools that aren't linked to a deployment. stacks_to_search.append(stack) # Fetch the stack descriptions and collect information from Custom::CognitoIdentityPool and Custom::CognitoUserPool resources. identity_pool_mappings = [] idp_by_pool_name = {} for stack in stacks_to_search: for resource in stack.resources: if resource.type == 'Custom::CognitoIdentityPool': identity_pool_mappings.append({ 'identity_pool_resource': resource, }) print 'Found CognitoIdentityPool {}.{}'.format( stack.stack_name, resource.logical_id) elif resource.type == 'Custom::CognitoUserPool': identities = resource.metadata.get('CloudCanvas', {}).get('Identities', []) updated_resource = updated_resources.get( stack.stack_arn, {}).get(resource.logical_id, {}) physical_id = updated_resource.get('physical_id', resource.physical_id) # Skip user pools that haven't been created yet, they will be linked later on creation. if physical_id: for identity in identities: pool_name = identity.get('IdentityPoolLogicalName') client_app = identity.get('ClientApp') if not client_app: raise RuntimeError( 'Missing ClientApp in Identities metadata for stack {} resource {}' .format(stack.stack_name, resource.logical_id)) # Get the client id from the updated_resources parameter if possible, otherwise get it from the pool. client_id = updated_resource.get( 'client_apps', {}).get('client_app', {}).get('client_id') if not client_id: client_id = user_pool.get_client_id( physical_id, client_app) if not client_id: # A client may be missing if there's more than one pool updating at the same time and the list of clients is changing. print 'Unable to find client named {} in user pool with physical id {} defined in stack {} resource {}'.format( client_app, physical_id, stack.stack_name, resource.logical_id) else: pools = idp_by_pool_name.get(pool_name, []) pools.append({ 'ClientId': client_id, 'ProviderName': user_pool.get_provider_name(physical_id), 'ServerSideTokenCheck': True }) idp_by_pool_name[pool_name] = pools print 'Found CognitoUserPool {}.{} mapped to {} with client id {}'.format( stack.stack_name, resource.logical_id, pool_name, client_id) # Combine the user pool mappings with the identity pool mappings. for mapping in identity_pool_mappings: mapping['providers'] = idp_by_pool_name.get( mapping['identity_pool_resource'].logical_id, []) return identity_pool_mappings
def create_access_control_role(id_data, stack_arn, logical_role_name, assume_role_service, delay_for_propagation=True, default_policy=None): '''Create an IAM Role for a resource group stack. Developers typlically do not have the IAM permissions necessary to create roles, either directly or indirectly via Cloud Formation. However, many resource require that a role be specified (e.g. the role assumed when a Lambda function is invoked). To overcome this delema, custom resource handlers can use this method and act as a proxies for directly defining roles in resource-group-template.json files. The Custom::AcessControl resource handler will manage the policies attached to this role. Args: resource_group_stack_arn: Identifies the stack that "owns" the role. logical_role_name: Appended to the stack name to construct the actual role name. assume_role_service: Identifies the AWS service that is allowed to assume the role. default_policy (named): An IAM policy document that will be attached to the role. Returns: The ARN of the created role. ''' role_name = get_access_control_role_name(stack_arn, logical_role_name) owning_stack_info = stack_info.get_stack_info(stack_arn) deployment_name = "NONE" resource_group_name = "NONE" project_name = "NONE" if owning_stack_info.stack_type == stack_info.StackInfo.STACK_TYPE_RESOURCE_GROUP: deployment_name = owning_stack_info.deployment.deployment_name resource_group_name = owning_stack_info.resource_group_name project_name = owning_stack_info.deployment.project.project_name elif owning_stack_info.stack_type == stack_info.StackInfo.STACK_TYPE_DEPLOYMENT: deployment_name = owning_stack_info.deployment.deployment_name project_name = owning_stack_info.project.project_name elif owning_stack_info.stack_type == stack_info.StackInfo.STACK_TYPE_PROJECT: project_name = owning_stack_info.project_name path = '/{project_name}/{deployment_name}/{resource_group_name}/{logical_role_name}/AccessControl/'.format( project_name=project_name, deployment_name=deployment_name, resource_group_name=resource_group_name, logical_role_name=logical_role_name) assume_role_policy_document = ASSUME_ROLE_POLICY_DOCUMENT.replace( '{assume_role_service}', assume_role_service) try: res = iam.create_role( RoleName=role_name, AssumeRolePolicyDocument=assume_role_policy_document, Path=path) role_arn = res['Role']['Arn'] except ClientError as e: if e.response["Error"]["Code"] != 'EntityAlreadyExists': raise e res = iam.get_role(RoleName=role_name) role_arn = res['Role']['Arn'] if default_policy: iam.put_role_policy(RoleName=role_name, PolicyName='Default', PolicyDocument=default_policy) set_id_data_abstract_role_mapping(id_data, logical_role_name, role_name) # Allow time for the role to propagate before lambda tries to assume # it, which lambda tries to do when the function is created. if delay_for_propagation: time.sleep(PROPAGATION_DELAY_SECONDS) return role_arn
def handler(event, context): request_type = event['RequestType'] logical_resource_id = event['LogicalResourceId'] logical_role_name = logical_resource_id owning_stack_info = stack_info.get_stack_info(event['StackId']) rest_api_resource_name = owning_stack_info.stack_name + '-' + logical_resource_id id_data = aws_utils.get_data_from_custom_physical_resource_id( event.get('PhysicalResourceId', None)) response_data = {} if request_type == 'Create': props = properties.load(event, PROPERTY_SCHEMA) role_arn = role_utils.create_access_control_role( id_data, owning_stack_info.stack_arn, logical_role_name, API_GATEWAY_SERVICE_NAME) swagger_content = get_configured_swagger_content( owning_stack_info, props, role_arn, rest_api_resource_name) rest_api_id = create_api_gateway(props, swagger_content) response_data['Url'] = get_api_url(rest_api_id, owning_stack_info.region) id_data['RestApiId'] = rest_api_id elif request_type == 'Update': rest_api_id = id_data.get('RestApiId', None) if not rest_api_id: raise RuntimeError( 'No RestApiId found in id_data: {}'.format(id_data)) props = properties.load(event, PROPERTY_SCHEMA) role_arn = role_utils.get_access_control_role_arn( id_data, logical_role_name) swagger_content = get_configured_swagger_content( owning_stack_info, props, role_arn, rest_api_resource_name) update_api_gateway(rest_api_id, props, swagger_content) response_data['Url'] = get_api_url(rest_api_id, owning_stack_info.region) elif request_type == 'Delete': if not id_data: # The will be no data in the id if Cloud Formation cancels a resource creation # (due to a failure in another resource) before it processes the resource create # response. Appearently Cloud Formation has an internal temporary id for the # resource and uses it for the delete request. # # Unfortunalty there isn't a good way to deal with this case. We don't have the # id data, so we can't clean up the things it identifies. At best we can allow the # stack cleanup to continue, leaving the rest API behind and role behind. print 'WARNING: No id_data provided on delete.'.format(id_data) else: rest_api_id = id_data.get('RestApiId', None) if not rest_api_id: raise RuntimeError( 'No RestApiId found in id_data: {}'.format(id_data)) delete_api_gateway(rest_api_id) del id_data['RestApiId'] role_utils.delete_access_control_role(id_data, logical_role_name) else: raise RuntimeError('Invalid RequestType: {}'.format(request_type)) physical_resource_id = aws_utils.construct_custom_physical_resource_id_with_data( event['StackId'], logical_resource_id, id_data) custom_resource_response.succeed(event, context, response_data, physical_resource_id)