AWS Cloud Operations & Migrations Blog

Tracking AWS Service Catalog products provisioned by individual SAML users

To manage access to the AWS Cloud, many companies prefer Enterprise Federation over AWS Identity and Access Management (IAM) users. Identity federation provides single sign-on (SSO) to access AWS accounts using credentials from the corporate directory. This method of accessing AWS allows companies to utilize their existing identity solutions, such as Active Directory (AD) or Active Directory Federation Services, by mapping users to IAM roles.

Another option for managing access to AWS is to use AWS Service Catalog. In this blog I’ll show you how to set up AWS Service Catalog to grant users IAM roles for launching AWS resources.

AWS Service Catalog allows an organization to create a portfolio of products that can be provisioned by users. This method mitigates the need to grant user permissions to AWS resources and only grants permissions to the service catalog and specific products.



This diagram shows how users can access products through AWS Service Catalog after they have access to an appropriate IAM role. However, we need a way to distinguish users because multiple users can belong to the same AD group.

One way to identify each user is to add the user name parameter to the product template. But this method doesn’t guarantee that the value entered by the user will be correct or match the user name in Active Directory.

A better way to accomplish this is to programmatically add the user name to each product template. Let’s take a look at how to accomplish this using AWS Service Catalog.

Solution Overview

This Solution Overview diagram shows the architecture of the proposed solution:

    1. The user provisions a product after authenticating to AWS Service Catalog.
    2. AWS Service Catalog launches an AWS CloudFormation template in response to the user’s request.
    3. An AWS Lambda function is invoked based on the Amazon CloudWatch rule triggered by the CloudFormation CreateStack event.
    4. The Lambda function reads the Active Directory User Name and CloudFormation stack ID from the event record and stores this information in an Amazon DynamoDB database.
    5. The CloudFormation template provisions a custom resource that invokes the AWS Lambda function.
    6. The Lambda function reads the user name from the Amazon DynamoDB record associated with the CloudFormation stack ID and returns this information back to the CloudFormation template.

Prerequisites

Before you begin implementing this solution, be sure to do the following:

    1. 1. Install the AWS CLI:

https://aws.amazon.com/cli/

    .
    2. Install Python 2.7 (including pip).

Implementation

Step 1: Create an Amazon DynamoDB table

An Amazon DynamoDB table will be used as central location to store the user name and CloudFormation stack ID.

aws dynamodb create-table --table-name sc-track-user --attribute-definitions AttributeName=CFStackid,AttributeType=S --key-schema AttributeName=CFStackid,KeyType=HASH --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5

Step 2: Create an IAM role

Create an IAM role that will be associated with both AWS Lambda functions to grant permission to Amazon DynamoDB table.

aws iam create-role --role-name sc-lambda-role --assume-role-policy-document "{\"Version\": \"2012-10-17\",\"Statement\":[{\"Effect\": \"Allow\",\"Principal\":{\"Service\": \"lambda.amazonaws.com\"},\"Action\": \"sts:AssumeRole\"}]}"

Step 3: Create an IAM policy

1. Create new file name called lambda-access-policy.json and add following context to the file:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AccessDynamoDB",
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem"
      ],
      "Resource": [
        "arn:aws:dynamodb:us-east-1:{your AWS Account No}:table/sc-track-user"
      ]
    },
    {
      "Sid": "CreateCWLogs",
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream"
      ],
      "Resource": "*"
    },
    {
      "Sid": "WriteCWLog",
      "Action": [
        "logs:PutLogEvents"
      ],
      "Resource": [
        "arn:aws:logs:us-east-1: {AWS Account No}:log-group:/aws/lambda/sc-add-user-id:*:*",
        "arn:aws:logs:us-east-1: {AWS Account No}:log-group:/aws/lambda/sc-get-user-id:*:*"
      ],
      "Effect": "Allow"
    }
  ]
}

2. Replace {AWS Account No} with your AWS account number.
3. Execute the following CLI command:

aws iam put-role-policy --role-name sc-lambda-role --policy-name Lambda-DynamoDB-CloudWatch --policy-document file://lambda-access-policy.json

Step 4: Create an AWS Lambda function

Create a Lambda function to store the user name and CloudFormation stack ID.
1. Create a new file name called adduser.py.
2. Add the following code to the file:

import boto3

def lambda_handler(event, context):
    dynamodb = boto3.resource('dynamodb',region_name='us-east-1')
    table = dynamodb.Table('sc-track-user')
    stackId = event['detail']['responseElements']['stackId']
    Id = (stackId.split('/'))[-1]
    UserArn = event['detail']['userIdentity']['arn']
    UserAID = (UserArn.split('/'))[-1]
    table.put_item(
        Item={
            'CFStackid' : Id,
            'User' : UserAID
        })
    return ''

3. Zip file as adduser.zip.
4. Run the following command:

aws lambda create-function --function-name sc-add-user-id --runtime python2.7 --role arn:aws:iam::{AWS Account No}:role/sc-lambda-role --handler adduser.lambda_handler --timeout 30 --zip-file "fileb://adduser.zip"

Note: Before running this command change {AWS Account No} to the correct account number.

Step 5: Create an Amazon CloudWatch Event

An Amazon CloudWatch Event will invoke a Lambda function each time a new CloudFormation stack is created.

1. Create the CloudWatch Event:

aws events put-rule --name "sc-add-user" --event-pattern "{\"source\":[\"aws.cloudformation\"],\"detail-type\":[\"AWS API Call via CloudTrail\"],\"detail\":{\"eventSource\":[\"cloudformation.amazonaws.com\"],\"eventName\":[\"CreateStack\"]}}"

2. Add the Lambda function as the target to the event.

aws events put-targets --rule sc-add-user --targets "Id"="LambdaFunction","Arn"="arn:aws:lambda:us-east-1:{AWS Account No}:function:sc-add-user-id"

3. Grant permission for your event to invoke the Lambda function.

aws lambda add-permission --function-name sc-add-user-id --statement-id LamdaPermission-10 --action lambda:InvokeFunction --principal events.amazonaws.com --source-arn arn:aws:events:us-east-1: {AWS Account No}:rule/sc-add-user

Note: Before running commands change {AWS Account No} to the correct account number, where you need to.

Step 6: Call the AWS Lambda function

This Lambda function will be called by CloudFormation template to retrieve user name from DynamoDB table

1. Create a new file name: getuser.py
2. Add the following code to the file:

import json
import requests
import os
import boto3
from botocore.exceptions import ClientError
import time

from requests.auth import HTTPBasicAuth
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

dynamodb = boto3.resource('dynamodb')

###########################################################################
# Lambda Handler
def lambda_handler(event, context):

    stackId = event['ResourceProperties']['stackId']
    Id = (stackId.split('/'))[-1]
    responseStatus = 'SUCCESS'
    responseData = {}
    responseData["Id"]  = get_user_aid(Id)
    sendResponse(event, context, responseStatus, responseData)

#Send Response back to CF
def sendResponse(event, context, responseStatus, responseData):
    responseBody = {'Status': responseStatus,
                    'Reason': 'See the details in CloudWatch Log Stream: ' + context.log_stream_name,
                    'PhysicalResourceId': responseData["Id"],
                    'StackId': event['StackId'],
                    'RequestId': event['RequestId'],
                    'LogicalResourceId': event['LogicalResourceId'],
                    'Data': responseData}
    try:
        req = requests.put(event['ResponseURL'], data=json.dumps(responseBody))
        if req.status_code != 200:
            print(req.text)
            raise Exception('Received non 200 response while sending response to CFN.')
        return
    except requests.exceptions.RequestException as e:
        print(e)
        raise

###########################################################################
# Get User AID
def get_user_aid(stackId):

    IValue = ''
    table = dynamodb.Table('sc-track-user')
    counter = 0

    while not IValue:
        try:
            response = table.get_item(
                Key={
                    'CFStackid': stackId
                }
            )

            if 'Item' in response:
                IValue = response['Item']['User']
            else:
                time.sleep(5)

        except ClientError as e:
            print(e.response['Error']['Message'])


    return(IValue)

3. The code for our Lambda function requires a requests Python module that is not included in the standard Python library. To install the requests module, use the command that follows. Note that $PWD is the location of the getuser.py file..:

pip install -t "$PWD" requests

4. Zip getuser.py along with all modules installed in the previous step as getuser.zip.
5. Execute the following command:

aws lambda create-function --function-name sc-get-user-id --runtime python2.7 --role arn:aws:iam::{AWS Account No}:role/sc-lambda-role --handler getuser.lambda_handler --timeout 120 --zip-file "fileb://getuser.zip

Step 7: Modify the CloudFormation template

In the final step, we need to add a custom resource to product CloudFormation templates to retrieve the name of the user who provisions the product.

Here is an example of a CloudFormation template for a product:

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Description": "test-get-user-aid",
  "Resources": {
    "UserAID": {
      "Type": "Custom::SCUserAID",
      "Properties": {
        "ServiceToken": {
          "Fn::Join": [ "",  
			[ "arn:aws:lambda:", { "Ref": "AWS::Region" }, ":", { "Ref": "AWS::AccountId" }, ":function:sc-get-user-id" ]
          ] },
        "stackId": {
          "Ref": "AWS::StackId"
        }
      }
    }
  },
  "Outputs": {
    "Param": {
      "Value": {
        "Fn::GetAtt": [
          "UserAID",
          "Id"
        ]
      }
    }
  }
}

The user name returned by our Lambda function can now be used to tag AWS resources launched through this process.

Use cases

Beside proper tagging resource with correct user name there are examples where described solution come handy. Here are couple of examples:

Custom DNS

In many cases, provisioning products using the AWS Service Catalog requires access over DNS rather than an IP address– for example an HTTPS connection. Since multiple users can launch the same product, the DNS name cannot be hard coded in the CloudFormation template but should be generated dynamically during product provisioning. In such cases, the user ID could be used as a prefix to the DNS name.

AWS Service Catalog product access control

When users authenticate to AWS through federation, access to AWS Service Catalog is managed through an IAM role. In such cases, an organization might require excluding access to AWS Service Catalog products) for certain users. This can be accomplished by creating an Amazon DynamoDB table with a list of users who have access to products and then modifying the sc-get-user-id Lambda function so the function will query the table and return status FAILED when user doesn’t have permission to provision a given product. The name of the product can be passed to the Lambda function as additional parameter directly from CloudFormation template.

About the Author


Remek Hetman is a Senior Cloud Infrastructure Architect with the Amazon Web Services ProServe team. He works with AWS Enterprise customers providing technical guidance and assistance for Infrastructure, DevOps, and big data to help them make the best use of AWS services. Outside of work he enjoys spending time actively as well as pursing his passion – astronomy.