Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
a6a6a8b
initial implementation
raymondeah Oct 22, 2025
09977f6
handle delete conflict exception
raymondeah Oct 22, 2025
fd5f548
remove standard inline policy
raymondeah Oct 22, 2025
5290d7e
add managed policy prefix to allowed resources in lambda execution role
raymondeah Oct 22, 2025
e6a3d12
use inline policy for standard permissions
raymondeah Oct 22, 2025
d7bef90
use inline policy for standard permissions
raymondeah Oct 22, 2025
a0da5e9
readd lambda trigger
raymondeah Oct 22, 2025
f420918
add missing param
raymondeah Oct 22, 2025
f0d6ea9
fix typo
raymondeah Oct 22, 2025
d4bdd7f
add PutRolePolicy permission to lambda execution role
raymondeah Oct 22, 2025
7768d8e
fix typo
raymondeah Oct 22, 2025
4e818fe
cleanup lambda code
raymondeah Oct 24, 2025
40da360
fix typo
raymondeah Oct 24, 2025
ffdd521
fix typo
raymondeah Oct 24, 2025
af8bb4a
cleanup comments
raymondeah Oct 24, 2025
2e8670f
Merge branch 'master' into ray.eah/awscore-221-quickstart-remove-iam-…
raymondeah Oct 24, 2025
83265b3
only attach resource collection permissions when specified
raymondeah Oct 24, 2025
06a2413
add lambda permission to delete inline policy:
raymondeah Oct 24, 2025
9f0ff5a
fix invalid parameter
raymondeah Oct 24, 2025
b1d4043
bump version
raymondeah Oct 24, 2025
951625d
bump changelog
raymondeah Oct 24, 2025
f602af1
update python runtime
raymondeah Oct 24, 2025
2532775
Update aws_quickstart/CHANGELOG.md
raymondeah Oct 24, 2025
5a4a3d8
Merge branch 'master' into ray.eah/awscore-221-quickstart-remove-iam-…
raymondeah Jan 9, 2026
bb366e7
use built in chunking in api layer
raymondeah Jan 9, 2026
1f63450
align python code
raymondeah Jan 9, 2026
84e257e
fix typo
raymondeah Jan 9, 2026
611a30d
fix typo
raymondeah Jan 9, 2026
3f92cb0
separate deletion behavior for standard and resource collection permi…
raymondeah Jan 9, 2026
468d5ea
update changelog
raymondeah Jan 9, 2026
358a642
Merge branch 'master' into ray.eah/awscore-221-quickstart-remove-iam-…
raymondeah Feb 9, 2026
b3e9522
use aws partition when constructing ARNs
raymondeah Feb 9, 2026
5c632ca
fix version numbers
raymondeah Feb 9, 2026
41fda05
sync both python codes
raymondeah Feb 9, 2026
a1fb1b7
add proper try catch to api request error handling
raymondeah Feb 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions aws_quickstart/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# 4.4.0 (February 9, 2026)

- Remove all enumerated IAM permissions from the datadog_integration_role stack
- Add Lambda function to retrieve and attach the equivalent IAM permissions at stack creation/update time

# 4.3.0 (December 26, 2025)

### Features
Expand Down
159 changes: 159 additions & 0 deletions aws_quickstart/attach_integration_permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import json
import logging
from urllib.request import Request
import urllib.error
import urllib.request
import cfnresponse
import boto3

LOGGER = logging.getLogger()
LOGGER.setLevel(logging.INFO)
API_CALL_SOURCE_HEADER_VALUE = "cfn-quickstart"
POLICY_NAME_STANDARD = "DatadogAWSIntegrationPolicy"
BASE_POLICY_PREFIX_RESOURCE_COLLECTION = "datadog-aws-integration-resource-collection-permissions"
STANDARD_PERMISSIONS_API_URL = "https://api.datadoghq.com/api/v2/integration/aws/iam_permissions/standard"
RESOURCE_COLLECTION_PERMISSIONS_API_URL = "https://api.datadoghq.com/api/v2/integration/aws/iam_permissions/resource_collection?chunked=true"

class DatadogAPIError(Exception):
pass

def fetch_permissions_from_datadog(api_url):
"""Fetch permissions from Datadog API"""
headers = {
"Dd-Aws-Api-Call-Source": API_CALL_SOURCE_HEADER_VALUE,
}
request = Request(api_url, headers=headers)
request.get_method = lambda: "GET"

try:
response = urllib.request.urlopen(request)
except urllib.error.HTTPError as e:
error_body = json.loads(e.read())
error_message = error_body.get('errors', ['Unknown error'])[0]
raise DatadogAPIError(f"Datadog API error: {error_message}") from e

json_response = json.loads(response.read())
return json_response["data"]["attributes"]["permissions"]

def cleanup_existing_policies(iam_client, role_name, account_id, partition, max_policies=10):
# Remove resource collection permissions
for i in range(max_policies):
policy_name = f"{BASE_POLICY_PREFIX_RESOURCE_COLLECTION}-{i+1}"
policy_arn = f"arn:{partition}:iam::{account_id}:policy/{policy_name}"
try:
iam_client.detach_role_policy(
RoleName=role_name,
PolicyArn=policy_arn
)
except iam_client.exceptions.NoSuchEntityException:
pass
except Exception as e:
LOGGER.error(f"Error detaching policy {policy_name}: {str(e)}")

try:
iam_client.delete_policy(
PolicyArn=policy_arn
)
except iam_client.exceptions.NoSuchEntityException:
pass
except iam_client.exceptions.DeleteConflictException:
LOGGER.warning(f"Policy {policy_name} still attached, skipping delete")
except Exception as e:
LOGGER.error(f"Error deleting policy {policy_name}: {str(e)}")

# Remove standard permissions
try:
iam_client.delete_role_policy(
RoleName=role_name,
PolicyName=POLICY_NAME_STANDARD
)
except iam_client.exceptions.NoSuchEntityException:
pass
except Exception as e:
LOGGER.error(f"Error deleting inline policy {POLICY_NAME_STANDARD}: {str(e)}")

def attach_standard_permissions(iam_client, role_name):
permissions = fetch_permissions_from_datadog(STANDARD_PERMISSIONS_API_URL)
policy_document = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": permissions,
"Resource": "*"
}
]
}

iam_client.put_role_policy(
RoleName=role_name,
PolicyName=POLICY_NAME_STANDARD,
PolicyDocument=json.dumps(policy_document, separators=(',', ':'))
)

def attach_resource_collection_permissions(iam_client, role_name):
permission_chunks = fetch_permissions_from_datadog(RESOURCE_COLLECTION_PERMISSIONS_API_URL)

# Create and attach new policies
for i, chunk in enumerate(permission_chunks):
# Create policy
policy_name = f"{BASE_POLICY_PREFIX_RESOURCE_COLLECTION}-{i+1}"
policy_document = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": chunk,
"Resource": "*"
}
]
}
policy_json = json.dumps(policy_document, separators=(',', ':'))
policy_size = len(policy_json)
LOGGER.info(f"Creating policy {policy_name} with {len(chunk)} permissions ({policy_size} characters)")
policy = iam_client.create_policy(
PolicyName=policy_name,
PolicyDocument=policy_json
)

# Attach policy to role
iam_client.attach_role_policy(
RoleName=role_name,
PolicyArn=policy['Policy']['Arn']
)

def handle_delete(event, context, role_name, account_id, partition):
"""Handle stack deletion."""
iam_client = boto3.client('iam')
try:
cleanup_existing_policies(iam_client, role_name, account_id, partition)
cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData={})
except Exception as e:
LOGGER.error(f"Error deleting policy: {str(e)}")
cfnresponse.send(event, context, cfnresponse.FAILED, responseData={"Message": str(e)})

def handle_create_update(event, context, role_name, account_id, partition, should_install_security_audit_policy):
"""Handle stack creation or update."""
try:
iam_client = boto3.client('iam')
cleanup_existing_policies(iam_client, role_name, account_id, partition)
attach_standard_permissions(iam_client, role_name)
if should_install_security_audit_policy:
attach_resource_collection_permissions(iam_client, role_name)
cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData={})
except Exception as e:
LOGGER.error(f"Error creating/attaching policy: {str(e)}")
cfnresponse.send(event, context, cfnresponse.FAILED, responseData={"Message": str(e)})

def handler(event, context):
LOGGER.info("Event received: %s", json.dumps(event))

role_name = event['ResourceProperties']['DatadogIntegrationRole']
account_id = event['ResourceProperties']['AccountId']
partition = event['ResourceProperties'].get('Partition', 'aws')
should_install_security_audit_policy = str(event['ResourceProperties']['ResourceCollectionPermissions']).lower() == 'true'

if event['RequestType'] == 'Delete':
handle_delete(event, context, role_name, account_id, partition)
else:
handle_create_update(event, context, role_name, account_id, partition, should_install_security_audit_policy)
Loading
Loading