1
0
mirror of synced 2026-05-22 14:43:35 +00:00

Admin workflow - IAM PERMISSIONS md, utils module etc. (#1323)

* IAM PERMISSIONS md, utils module etc.

* IAM PERMISSIONS md, utils module etc.

* IAM PERMISSIONS md, utils module etc.

* IAM PERMISSIONS md, utils module etc.

* IAM PERMISSIONS md, utils module etc.

* IAM PERMISSIONS md, utils module etc.

* IAM PERMISSIONS md, utils module etc.
This commit is contained in:
Kamal Manchanda
2026-04-17 15:33:15 +05:30
committed by GitHub
parent 8adf5c5ccc
commit b35e6c8d43
4 changed files with 326 additions and 62 deletions
@@ -0,0 +1,266 @@
# IAM Permissions for Admin Approval Workflow sample
Create IAM user or role with the following permissions.
> **Before using these policies**, replace every occurrence of `YOUR_ACCOUNT_ID` with your 12-digit AWS account ID.
> Run the following command to find it:
> ```bash
> aws sts get-caller-identity --query Account --output text
> ```
> Then do a find-and-replace of `YOUR_ACCOUNT_ID` in the JSON below before attaching the policy.
## Policy for AWS Agent Registry access (Administrator)
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCreatingAndListingRegistries",
"Effect": "Allow",
"Action": [
"bedrock-agentcore:CreateRegistry",
"bedrock-agentcore:ListRegistries"
],
"Resource": ["arn:aws:bedrock-agentcore:*:YOUR_ACCOUNT_ID:*"]
},
{
"Sid": "AllowGetUpdateDeleteRegistry",
"Effect": "Allow",
"Action": [
"bedrock-agentcore:GetRegistry",
"bedrock-agentcore:UpdateRegistry",
"bedrock-agentcore:DeleteRegistry"
],
"Resource": ["arn:aws:bedrock-agentcore:*:YOUR_ACCOUNT_ID:registry/*"]
},
{
"Sid": "AllowCreatingAndListingRegistryRecords",
"Effect": "Allow",
"Action": [
"bedrock-agentcore:CreateRegistryRecord",
"bedrock-agentcore:ListRegistryRecords"
],
"Resource": ["arn:aws:bedrock-agentcore:*:YOUR_ACCOUNT_ID:registry/*"]
},
{
"Sid": "AllowRecordLevelOperations",
"Effect": "Allow",
"Action": [
"bedrock-agentcore:GetRegistryRecord",
"bedrock-agentcore:UpdateRegistryRecord",
"bedrock-agentcore:DeleteRegistryRecord",
"bedrock-agentcore:SubmitRegistryRecordForApproval"
],
"Resource": ["arn:aws:bedrock-agentcore:*:YOUR_ACCOUNT_ID:registry/*/record/*"]
},
{
"Sid": "AllowApproveRejectDeprecateRecords",
"Effect": "Allow",
"Action": ["bedrock-agentcore:UpdateRegistryRecordStatus"],
"Resource": ["arn:aws:bedrock-agentcore:*:YOUR_ACCOUNT_ID:registry/*/record/*"]
},
{
"Sid": "AdditionalPermissionForRegistryManagedWorkloadIdentity",
"Effect": "Allow",
"Action": ["bedrock-agentcore:*WorkloadIdentity"],
"Resource": ["arn:aws:bedrock-agentcore:*:YOUR_ACCOUNT_ID:workload-identity-directory/default/workload-identity/*"]
}
]
}
```
## Policy for AWS Agent Registry access (Publisher)
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowListingAllRegistries",
"Effect": "Allow",
"Action": ["bedrock-agentcore:ListRegistries"],
"Resource": ["arn:aws:bedrock-agentcore:*:YOUR_ACCOUNT_ID:*"]
},
{
"Sid": "AllowGetRegistry",
"Effect": "Allow",
"Action": ["bedrock-agentcore:GetRegistry"],
"Resource": ["arn:aws:bedrock-agentcore:*:YOUR_ACCOUNT_ID:registry/*"]
},
{
"Sid": "AllowCreatingAndListingRegistryRecords",
"Effect": "Allow",
"Action": [
"bedrock-agentcore:CreateRegistryRecord",
"bedrock-agentcore:ListRegistryRecords"
],
"Resource": ["arn:aws:bedrock-agentcore:*:YOUR_ACCOUNT_ID:registry/*"]
},
{
"Sid": "AllowRecordLevelOperations",
"Effect": "Allow",
"Action": [
"bedrock-agentcore:GetRegistryRecord",
"bedrock-agentcore:UpdateRegistryRecord",
"bedrock-agentcore:DeleteRegistryRecord",
"bedrock-agentcore:SubmitRegistryRecordForApproval"
],
"Resource": ["arn:aws:bedrock-agentcore:*:YOUR_ACCOUNT_ID:registry/*/record/*"]
}
]
}
```
## Permissions Required to deploy the required CI/CD stack such as DynamoDB and AWS Lambda etc.
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "STSCallerIdentity",
"Effect": "Allow",
"Action": ["sts:GetCallerIdentity"],
"Resource": "*"
},
{
"Sid": "CloudFormationValidate",
"Effect": "Allow",
"Action": ["cloudformation:ValidateTemplate"],
"Resource": "*"
},
{
"Sid": "CloudFormationStackManagement",
"Effect": "Allow",
"Action": [
"cloudformation:CreateStack",
"cloudformation:UpdateStack",
"cloudformation:DeleteStack",
"cloudformation:DescribeStacks",
"cloudformation:DescribeStackEvents",
"cloudformation:DescribeStackResources",
"cloudformation:GetTemplate",
"cloudformation:ListStackResources",
"cloudformation:CreateChangeSet",
"cloudformation:DescribeChangeSet",
"cloudformation:ExecuteChangeSet",
"cloudformation:DeleteChangeSet"
],
"Resource": "arn:aws:cloudformation:*:YOUR_ACCOUNT_ID:stack/*/*"
},
{
"Sid": "S3StagingBucketManagement",
"Effect": "Allow",
"Action": [
"s3:CreateBucket",
"s3:DeleteBucket",
"s3:HeadBucket",
"s3:PutBucketPublicAccessBlock",
"s3:GetBucketPublicAccessBlock",
"s3:ListBucket",
"s3:DeleteObject",
"s3:PutObject",
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::*",
"arn:aws:s3:::*/*"
]
},
{
"Sid": "LambdaFunctionManagement",
"Effect": "Allow",
"Action": [
"lambda:CreateFunction",
"lambda:UpdateFunctionCode",
"lambda:UpdateFunctionConfiguration",
"lambda:DeleteFunction",
"lambda:GetFunction",
"lambda:GetFunctionConfiguration",
"lambda:AddPermission",
"lambda:RemovePermission"
],
"Resource": "arn:aws:lambda:*:YOUR_ACCOUNT_ID:function:*"
},
{
"Sid": "LambdaLayerManagement",
"Effect": "Allow",
"Action": [
"lambda:PublishLayerVersion",
"lambda:DeleteLayerVersion",
"lambda:GetLayerVersion",
"lambda:ListLayerVersions"
],
"Resource": "arn:aws:lambda:*:YOUR_ACCOUNT_ID:layer:*"
},
{
"Sid": "IAMRoleManagement",
"Effect": "Allow",
"Action": [
"iam:CreateRole",
"iam:DeleteRole",
"iam:GetRole",
"iam:PassRole",
"iam:AttachRolePolicy",
"iam:DetachRolePolicy",
"iam:PutRolePolicy",
"iam:DeleteRolePolicy",
"iam:GetRolePolicy",
"iam:ListRolePolicies",
"iam:ListAttachedRolePolicies"
],
"Resource": "arn:aws:iam::YOUR_ACCOUNT_ID:role/*"
},
{
"Sid": "KMSCreateKey",
"Effect": "Allow",
"Action": ["kms:CreateKey"],
"Resource": "*"
},
{
"Sid": "KMSManageTaggedKeys",
"Effect": "Allow",
"Action": [
"kms:DescribeKey",
"kms:EnableKeyRotation",
"kms:GetKeyPolicy",
"kms:PutKeyPolicy",
"kms:ScheduleKeyDeletion",
"kms:CancelKeyDeletion",
"kms:TagResource",
"kms:UntagResource"
],
"Resource": "*"
},
{
"Sid": "DynamoDBTableManagement",
"Effect": "Allow",
"Action": [
"dynamodb:CreateTable",
"dynamodb:DeleteTable",
"dynamodb:DescribeTable",
"dynamodb:UpdateTable",
"dynamodb:DescribeContinuousBackups",
"dynamodb:DescribeTimeToLive"
],
"Resource": "arn:aws:dynamodb:*:YOUR_ACCOUNT_ID:table/*"
},
{
"Sid": "EventBridgeManagement",
"Effect": "Allow",
"Action": [
"events:PutRule",
"events:DeleteRule",
"events:DescribeRule",
"events:PutTargets",
"events:RemoveTargets",
"events:ListTargetsByRule"
],
"Resource": "arn:aws:events:*:YOUR_ACCOUNT_ID:rule/*"
}
]
}
```
@@ -67,7 +67,7 @@ As an Administrator, you can use the **AWS CLI** commands included in the notifi
## Prerequisites ## Prerequisites
- IAM credentials with appropriate permissions (see [`IAM_PERMISSIONS.md`](../../IAM_PERMISSIONS.md)). This tutorial requires both admin and publisher permissions. In addition, the following permissions are required to deploy and destroy the CI/CD stack: - IAM credentials with appropriate permissions (see [`IAM_PERMISSIONS.md`](./IAM_PERMISSIONS.md)). In addition to Agent Registry related operations, the following permissions are being used:
| Service | Permissions | | Service | Permissions |
|:--------|:------------| |:--------|:------------|
@@ -59,17 +59,19 @@
"metadata": {}, "metadata": {},
"source": [ "source": [
"## Prerequisites\n", "## Prerequisites\n",
"- IAM credentials with appropriate permissions (see [`IAM_PERMISSIONS.md`](../../IAM_PERMISSIONS.md)). This tutorial requires both admin and publisher permissions. In addition, the following permissions are required to deploy and destroy the CI/CD stack:\n", "- IAM credentials with appropriate permissions (see [`IAM_PERMISSIONS.md`](./IAM_PERMISSIONS.md)). In addition to Agent Registry related operations, the following permissions are being used:\n",
"\n", "\n",
" | Service | Permissions |\n", "| Service | Permissions |\n",
" |:--------|:------------|\n", "|:--------|:------------|\n",
" | **Amazon S3** | `CreateBucket`, `HeadBucket`, `PutPublicAccessBlock`, `DeleteBucket`, `ListBucket`, `PutObject`, `GetObject`, `DeleteObject` |\n", "| **Amazon S3** | `CreateBucket`, `HeadBucket`, `PutPublicAccessBlock`, `DeleteBucket`, `ListBucket`, `PutObject`, `GetObject`, `DeleteObject` |\n",
" | **AWS CloudFormation** | `CreateStack`, `UpdateStack`, `DeleteStack`, `DescribeStacks`, `CreateChangeSet`, `ExecuteChangeSet`, `DescribeChangeSet`, `DeleteChangeSet` |\n", "| **AWS CloudFormation** | `CreateStack`, `UpdateStack`, `DeleteStack`, `DescribeStacks`, `CreateChangeSet`, `ExecuteChangeSet`, `DescribeChangeSet`, `DeleteChangeSet` |\n",
" | **AWS Lambda** | `CreateFunction`, `UpdateFunctionCode`, `UpdateFunctionConfiguration`, `GetFunction`, `DeleteFunction`, `PublishLayerVersion`, `DeleteLayerVersion`, `AddPermission`, `RemovePermission` |\n", "| **AWS Lambda** | `CreateFunction`, `UpdateFunctionCode`, `UpdateFunctionConfiguration`, `GetFunction`, `DeleteFunction`, `PublishLayerVersion`, `DeleteLayerVersion`, `AddPermission`, `RemovePermission` |\n",
" | **AWS IAM** | `CreateRole`, `GetRole`, `DeleteRole`, `PassRole`, `AttachRolePolicy`, `DetachRolePolicy`, `PutRolePolicy`, `DeleteRolePolicy` |\n", "| **AWS IAM** | `CreateRole`, `GetRole`, `DeleteRole`, `PassRole`, `AttachRolePolicy`, `DetachRolePolicy`, `PutRolePolicy`, `DeleteRolePolicy` |\n",
" | **AWS EventBridge** | `PutRule`, `DescribeRule`, `DeleteRule`, `PutTargets`, `RemoveTargets` |\n", "| **AWS EventBridge** | `PutRule`, `DescribeRule`, `DeleteRule`, `PutTargets`, `RemoveTargets` |\n",
" | **Amazon DynamoDB** | `CreateTable`, `DeleteTable`, `DescribeTable` |\n", "| **Amazon DynamoDB** | `CreateTable`, `DeleteTable`, `DescribeTable` |\n",
" | **AWS CloudWatch Logs** | `CreateLogGroup`, `CreateLogStream`, `PutLogEvents`, `DeleteLogGroup` |- Python 3.9+ with `boto3` installed\n", "| **AWS CloudWatch Logs** | `CreateLogGroup`, `CreateLogStream`, `PutLogEvents`, `DeleteLogGroup` |\n",
"\n",
"- Python 3.9+ with `boto3` installed\n",
"\n", "\n",
"- [uv](https://docs.astral.sh/uv/getting-started/installation/) package manager (for installing python dependencies)\n", "- [uv](https://docs.astral.sh/uv/getting-started/installation/) package manager (for installing python dependencies)\n",
"- A Slack workspace with an [incoming webhook](https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/) configured. Note down the webhook URL and channel name.\n", "- A Slack workspace with an [incoming webhook](https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/) configured. Note down the webhook URL and channel name.\n",
@@ -98,6 +100,8 @@
"import subprocess\n", "import subprocess\n",
"import botocore.exceptions\n", "import botocore.exceptions\n",
"import time\n", "import time\n",
"import os\n",
"from utils import wait_for_registry_ready, wait_for_record_draft\n",
"\n", "\n",
"print(f\"Boto3 version: {boto3.__version__}\")\n", "print(f\"Boto3 version: {boto3.__version__}\")\n",
"\n", "\n",
@@ -159,19 +163,8 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"#wait for registry to get to Ready status (takes around ~2 minutes)\n", "# wait for registry to get to Ready status (takes around ~2 minutes)\n",
"registryStatus = 'Checking'\n", "wait_for_registry_ready(cp_client, REGISTRY_ID)"
"while registryStatus.lower() != 'ready':\n",
" getRegistry = cp_client.get_registry(registryId=REGISTRY_ID)\n",
"\n",
" registryStatus = getRegistry['status']\n",
"\n",
" if registryStatus.lower() == 'ready':\n",
" print(\"Verified: Registry is in Ready state\")\n",
" else:\n",
" print(f\"Registry is in {registryStatus} state. Waiting for it to be in Ready state\")\n",
"\n",
" time.sleep(10)"
] ]
}, },
{ {
@@ -307,19 +300,8 @@
"outputs": [], "outputs": [],
"source": [ "source": [
"# Wait for the record to be in Draft state\n", "# Wait for the record to be in Draft state\n",
"recordStatus = 'Checking'\n", "wait_for_record_draft(cp_client, REGISTRY_ID, A2A_RECORD_ID)\n",
"while recordStatus.lower() != 'draft':\n",
" getRegistryRecord = cp_client.get_registry_record(registryId=REGISTRY_ID, recordId=A2A_RECORD_ID)\n",
" recordStatus = getRegistryRecord['status']\n",
" metadata = getRegistryRecord.get(\"ResponseMetadata\", {})\n",
"\n", "\n",
" if recordStatus.lower() == 'draft':\n",
" print(\"Verified: Registry record is in Draft state. Ready to be submitted for Approval\")\n",
" print(f\"Metadata | RequestId: {metadata['HTTPHeaders']['x-amzn-requestid']}, Timestamp: {metadata['HTTPHeaders']['date']}\")\n",
" else:\n",
" print(f\"Registry record is in {recordStatus} state. Waiting for it to be in Draft state\")\n",
"\n",
" time.sleep(2)\n",
"submit_resp = cp_client.submit_registry_record_for_approval(registryId=REGISTRY_ID, recordId=A2A_RECORD_ID)\n", "submit_resp = cp_client.submit_registry_record_for_approval(registryId=REGISTRY_ID, recordId=A2A_RECORD_ID)\n",
"print(\"Record submitted for approval\")\n", "print(\"Record submitted for approval\")\n",
"\n", "\n",
@@ -381,19 +363,7 @@
"outputs": [], "outputs": [],
"source": [ "source": [
"# Wait for the record to be in Draft state\n", "# Wait for the record to be in Draft state\n",
"recordStatus = 'Checking'\n", "wait_for_record_draft(cp_client, REGISTRY_ID, MCP_RECORD_ID)\n",
"while recordStatus.lower() != 'draft':\n",
" getRegistryRecord = cp_client.get_registry_record(registryId=REGISTRY_ID, recordId=MCP_RECORD_ID)\n",
" recordStatus = getRegistryRecord['status']\n",
" metadata = getRegistryRecord.get(\"ResponseMetadata\", {})\n",
"\n",
" if recordStatus.lower() == 'draft':\n",
" print(\"Verified: Registry record is in Draft state. Ready to be submitted for Approval\")\n",
" print(f\"RequestId: {metadata['HTTPHeaders']['x-amzn-requestid']}, Timestamp: {metadata['HTTPHeaders']['date']}\")\n",
" else:\n",
" print(f\"Registry record is in {recordStatus} state. Waiting for it to be in Draft state\")\n",
"\n",
" time.sleep(2)\n",
"\n", "\n",
"#Submit for approval\n", "#Submit for approval\n",
"submit_resp = cp_client.submit_registry_record_for_approval(registryId=REGISTRY_ID, recordId=MCP_RECORD_ID)\n", "submit_resp = cp_client.submit_registry_record_for_approval(registryId=REGISTRY_ID, recordId=MCP_RECORD_ID)\n",
@@ -448,19 +418,7 @@
"outputs": [], "outputs": [],
"source": [ "source": [
"# Wait for the record to be in Draft state\n", "# Wait for the record to be in Draft state\n",
"recordStatus = 'Checking'\n", "wait_for_record_draft(cp_client, REGISTRY_ID, CUSTOM_RECORD_ID)\n",
"while recordStatus.lower() != 'draft':\n",
" getRegistryRecord = cp_client.get_registry_record(registryId=REGISTRY_ID, recordId=CUSTOM_RECORD_ID)\n",
" recordStatus = getRegistryRecord['status']\n",
" metadata = getRegistryRecord.get(\"ResponseMetadata\", {})\n",
"\n",
" if recordStatus.lower() == 'draft':\n",
" print(\"Verified: Registry record is in Draft state. Ready to be submitted for Approval\")\n",
" print(f\"RequestId: {metadata['HTTPHeaders']['x-amzn-requestid']}, Timestamp: {metadata['HTTPHeaders']['date']}\")\n",
" else:\n",
" print(f\"Registry record is in {recordStatus} state. Waiting for it to be in Draft state\")\n",
"\n",
" time.sleep(2)\n",
"\n", "\n",
"#Submit for approval\n", "#Submit for approval\n",
"submit_resp = cp_client.submit_registry_record_for_approval(registryId=REGISTRY_ID, recordId=CUSTOM_RECORD_ID)\n", "submit_resp = cp_client.submit_registry_record_for_approval(registryId=REGISTRY_ID, recordId=CUSTOM_RECORD_ID)\n",
@@ -0,0 +1,40 @@
"""Utility helpers for Agent Registry polling operations."""
import time
def wait_for_registry_ready(cp_client, registry_id, poll_interval=10):
"""Poll until the registry reaches READY status."""
status = "Checking"
while status.lower() != "ready":
resp = cp_client.get_registry(registryId=registry_id)
status = resp["status"]
if status.lower() == "ready":
print("Verified: Registry is in Ready state")
else:
print(f"Registry is in {status} state. Waiting for it to be in Ready state")
time.sleep(poll_interval)
def wait_for_record_draft(cp_client, registry_id, record_id, poll_interval=2):
"""Poll until the registry record reaches DRAFT status."""
status = "Checking"
while status.lower() != "draft":
resp = cp_client.get_registry_record(registryId=registry_id, recordId=record_id)
status = resp["status"]
metadata = resp.get("ResponseMetadata", {})
if status.lower() == "draft":
print(
"Verified: Registry record is in Draft state. "
"Ready to be submitted for Approval"
)
headers = metadata.get("HTTPHeaders", {})
request_id = headers.get("x-amzn-requestid", "")
date = headers.get("date", "")
print(f"RequestId: {request_id}, Timestamp: {date}")
else:
print(
f"Registry record is in {status} state. "
"Waiting for it to be in Draft state"
)
time.sleep(poll_interval)