Update demo (#1150)
* added full example of enterprise mcp platform with policy engine mcp server filtering based on user_tag, guardrail for PII data * fixed linting * fixed linting * fixing lint * fixing lint * fixinf ruff * FIXING RUFF * fixing ruff * fixed stack added missing lib files * fixing ruff * fixing ruff * Added scopes, audiences on MCP gateway Added ALB headers conditions Added ALB S3 logging Added ACG VPCe with Resource Based Policy Updated architecture diagram Updated readme Fixed interceptors * auto delete objets in s3 added dependency on VPCe * added path based routing updated cdk context remove print and use logger * fixed ruff * fixing ruff * enfore association --------- Signed-off-by: Anthony Bernabeu <bernabeu.anthony@gmail.com> Co-authored-by: brnaba-aws <brnaba@amazon.com>
This commit is contained in:
@@ -12,8 +12,8 @@ This CDK infrastructure deploys a complete MCP gateway solution that enables VS
|
|||||||
- **Policy-Based Access Control** with custom Cedar policies
|
- **Policy-Based Access Control** with custom Cedar policies
|
||||||
- **Request/Response Interception** for logging and transformation
|
- **Request/Response Interception** for logging and transformation
|
||||||
- **PII Protection** using Bedrock Guardrails
|
- **PII Protection** using Bedrock Guardrails
|
||||||
- **Flexible Deployment Options** (ALB or API Gateway)
|
|
||||||
- **Custom Domain Support** with SSL/TLS
|
- **Custom Domain Support** with SSL/TLS
|
||||||
|
- **ALB Access Logging** to S3 with encryption and lifecycle management
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -25,10 +25,8 @@ This CDK infrastructure deploys a complete MCP gateway solution that enables VS
|
|||||||
- **OAuth Clients**:
|
- **OAuth Clients**:
|
||||||
- VS Code Client (Authorization Code Grant with PKCE)
|
- VS Code Client (Authorization Code Grant with PKCE)
|
||||||
|
|
||||||
#### 2. **API Gateway Layer**
|
#### 2. **Application Load Balancer**
|
||||||
Choose between two deployment options:
|
Production-grade internet-facing ALB with custom domain, SSL/TLS, WAF, and access logging.
|
||||||
- **Application Load Balancer (ALB)**: Production-grade with custom domains and SSL/TLS
|
|
||||||
- **API Gateway HTTP API**: Serverless, cost-effective for development/testing
|
|
||||||
|
|
||||||
#### 3. **MCP Proxy Lambda**
|
#### 3. **MCP Proxy Lambda**
|
||||||
Central component that handles:
|
Central component that handles:
|
||||||
@@ -90,12 +88,12 @@ Example Lambda functions that implement MCP tools:
|
|||||||
| Amazon Cognito | OAuth 2.0 authentication and user management |
|
| Amazon Cognito | OAuth 2.0 authentication and user management |
|
||||||
| AWS Lambda | Serverless compute for proxy, MCP servers, and policy engine |
|
| AWS Lambda | Serverless compute for proxy, MCP servers, and policy engine |
|
||||||
| Amazon Bedrock AgentCore | MCP gateway and protocol handling |
|
| Amazon Bedrock AgentCore | MCP gateway and protocol handling |
|
||||||
| Application Load Balancer | Production routing with custom domains (optional) |
|
| Application Load Balancer | Internet-facing ALB with TLS, WAF, and access logging |
|
||||||
| API Gateway HTTP API | Serverless API endpoint (optional) |
|
| Amazon VPC | Network isolation with private subnets and VPC endpoints |
|
||||||
| Amazon VPC | Network isolation for ALB deployment |
|
|
||||||
| AWS IAM | Identity and access management |
|
| AWS IAM | Identity and access management |
|
||||||
| Amazon Route53 | DNS management for custom domains |
|
| Amazon Route53 | DNS management for custom domains |
|
||||||
| AWS Certificate Manager | SSL/TLS certificates |
|
| AWS Certificate Manager | SSL/TLS certificates |
|
||||||
|
| Amazon S3 | ALB access log storage (encrypted, 90-day lifecycle) |
|
||||||
| Bedrock Guardrails | Content filtering and PII protection |
|
| Bedrock Guardrails | Content filtering and PII protection |
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
@@ -116,7 +114,7 @@ Example Lambda functions that implement MCP tools:
|
|||||||
### 1. Install Dependencies
|
### 1. Install Dependencies
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd enterprise-mcp-infra/cdk
|
cd cdk
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -130,11 +128,8 @@ cdk bootstrap aws://ACCOUNT-ID/REGION
|
|||||||
|
|
||||||
Edit `cdk/cdk.context.json` to configure your deployment:
|
Edit `cdk/cdk.context.json` to configure your deployment:
|
||||||
|
|
||||||
#### Option A: ALB Deployment with Custom Domain
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"deploymentType": "ALB",
|
|
||||||
"domainName": "enterprise-mcp",
|
"domainName": "enterprise-mcp",
|
||||||
"hostedZoneName": "example.com",
|
"hostedZoneName": "example.com",
|
||||||
"hostedZoneId": "Z1234567890ABC",
|
"hostedZoneId": "Z1234567890ABC",
|
||||||
@@ -142,39 +137,14 @@ Edit `cdk/cdk.context.json` to configure your deployment:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Option B: API Gateway Deployment (Default URL)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"deploymentType": "API_GATEWAY",
|
|
||||||
"domainName": "",
|
|
||||||
"hostedZoneName": "",
|
|
||||||
"hostedZoneId": "",
|
|
||||||
"certificateArn": ""
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Option C: API Gateway with Custom Domain
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"deploymentType": "API_GATEWAY",
|
|
||||||
"domainName": "enterprise-mcp.example.com",
|
|
||||||
"hostedZoneName": "example.com",
|
|
||||||
"hostedZoneId": "Z1234567890ABC",
|
|
||||||
"certificateArn": "arn:aws:acm:region:account:certificate/xxx"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Configuration Parameters:**
|
**Configuration Parameters:**
|
||||||
|
|
||||||
| Parameter | Description | Required | Default |
|
| Parameter | Description | Required | Default |
|
||||||
|-----------|-------------|----------|---------|
|
|-----------|-------------|----------|---------|
|
||||||
| `deploymentType` | Deployment type: `ALB` or `API_GATEWAY` | Yes | `ALB` |
|
| `domainName` | Custom domain name (e.g., `enterprise-mcp`) | Yes | `""` |
|
||||||
| `domainName` | Custom domain name (e.g., `enterprise-mcp` for ALB, or full domain for API Gateway) | No (API Gateway only) | `""` |
|
| `hostedZoneName` | Route53 hosted zone name (e.g., `example.com`) | Yes | `""` |
|
||||||
| `hostedZoneName` | Route53 hosted zone name (e.g., `example.com`) | Only with custom domain | `""` |
|
| `hostedZoneId` | Route53 hosted zone ID (e.g., `Z1234567890ABC`) | Yes | `""` |
|
||||||
| `hostedZoneId` | Route53 hosted zone ID (e.g., `Z1234567890ABC`) | Only with custom domain | `""` |
|
| `certificateArn` | ACM certificate ARN for HTTPS | Yes | `""` |
|
||||||
| `certificateArn` | ACM certificate ARN for HTTPS | Only with custom domain | `""` |
|
|
||||||
|
|
||||||
### 4. Deploy the Stack
|
### 4. Deploy the Stack
|
||||||
|
|
||||||
@@ -182,11 +152,7 @@ Edit `cdk/cdk.context.json` to configure your deployment:
|
|||||||
cdk deploy
|
cdk deploy
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also override context values via command line:
|
> **Note:** The stack is pinned to `us-east-1` in `cdk/bin/enterprise-mcp-infra.ts`. Update the `region` value there if you need a different region.
|
||||||
|
|
||||||
```bash
|
|
||||||
cdk deploy -c deploymentType=API_GATEWAY
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Save CDK Outputs
|
### 5. Save CDK Outputs
|
||||||
|
|
||||||
@@ -208,25 +174,26 @@ EnterpriseMcpInfraStack.Gateway = agentcore-mcp-gateway-xxxxx
|
|||||||
|
|
||||||
#### Using the Automated Script (Recommended)
|
#### Using the Automated Script (Recommended)
|
||||||
|
|
||||||
1. **Edit** `enterprise-mcp-infra/scripts/script.py`:
|
1. **Edit** `scripts/script.py`:
|
||||||
- Replace the `output` variable content with your actual CDK outputs (from step 4)
|
- Replace the `output` variable content with your actual CDK outputs (from step 4)
|
||||||
- Customize the users list with your desired email addresses and passwords
|
- Customize the users list with your desired email addresses and passwords
|
||||||
|
|
||||||
2. **Run the script** to create users:
|
2. **Run the script** to create users:
|
||||||
```bash
|
```bash
|
||||||
cd enterprise-mcp-infra/scripts
|
cd scripts
|
||||||
python script.py
|
python script.py
|
||||||
```
|
```
|
||||||
|
|
||||||
The script will:
|
The script will:
|
||||||
- Parse the CDK outputs to extract the User Pool ID
|
- Parse the CDK outputs to extract the User Pool ID
|
||||||
- Create two default users: `vscode-admin@example.com` and `vscode-user@example.com`
|
- Create three default users (admin, regular, and read-only)
|
||||||
- Set permanent passwords (no need for password reset on first login)
|
- Set permanent passwords (no need for password reset on first login)
|
||||||
- Skip users that already exist
|
- Skip users that already exist
|
||||||
|
|
||||||
**Default Users Created:**
|
**Default Users Created:**
|
||||||
- `vscode-admin@example.com` / `TempPassword123!`
|
- `vscode-admin@example.com` / `TempPassword123!`
|
||||||
- `vscode-user@example.com` / `TempPassword1234!`
|
- `vscode-user@example.com` / `TempPassword1234!`
|
||||||
|
- `vscode-readonly@example.com` / `TempPassword1235!`
|
||||||
|
|
||||||
#### Manual User Creation (Alternative)
|
#### Manual User Creation (Alternative)
|
||||||
|
|
||||||
@@ -374,15 +341,26 @@ cdk deploy
|
|||||||
|
|
||||||
### Lambda Logs
|
### Lambda Logs
|
||||||
|
|
||||||
|
The CDK stack outputs the function names for the two most commonly debugged Lambdas (`ProxyLambdaName`, `PreTokenGenerationLambdaName`). For the others, look up the auto-generated name in the AWS Console (Lambda → Functions, filter by stack name) or use the AWS CLI:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# MCP Proxy Lambda
|
# MCP Proxy Lambda – name from CDK output: EnterpriseMcpInfraStack.ProxyLambdaName
|
||||||
aws logs tail /aws/lambda/<ProxyLambdaName> --follow
|
aws logs tail /aws/lambda/$(aws cloudformation describe-stacks \
|
||||||
|
--stack-name EnterpriseMcpInfraStack \
|
||||||
|
--query "Stacks[0].Outputs[?OutputKey=='ProxyLambdaName'].OutputValue" \
|
||||||
|
--output text) --follow
|
||||||
|
|
||||||
# Policy Engine Lambda
|
# Pre-Token Generation Lambda – name from CDK output: EnterpriseMcpInfraStack.PreTokenGenerationLambdaName
|
||||||
aws logs tail /aws/lambda/<PolicyEngineLambdaName> --follow
|
aws logs tail /aws/lambda/$(aws cloudformation describe-stacks \
|
||||||
|
--stack-name EnterpriseMcpInfraStack \
|
||||||
|
--query "Stacks[0].Outputs[?OutputKey=='PreTokenGenerationLambdaName'].OutputValue" \
|
||||||
|
--output text) --follow
|
||||||
|
|
||||||
# Interceptor Lambda
|
# Interceptor Lambda – look up name in AWS Console (filter by stack: EnterpriseMcpInfraStack)
|
||||||
aws logs tail /aws/lambda/<InterceptorLambdaName> --follow
|
# aws logs tail /aws/lambda/<McpInterceptorLambda-name> --follow
|
||||||
|
|
||||||
|
# Policy Engine Lambda – look up name in AWS Console (filter by stack: EnterpriseMcpInfraStack)
|
||||||
|
# aws logs tail /aws/lambda/<AgentCorePolicyEngine-PolicyFunction-name> --follow
|
||||||
```
|
```
|
||||||
|
|
||||||
### CloudWatch Insights Queries
|
### CloudWatch Insights Queries
|
||||||
@@ -401,24 +379,66 @@ fields @timestamp, method, path, statusCode
|
|||||||
| sort @timestamp desc
|
| sort @timestamp desc
|
||||||
```
|
```
|
||||||
|
|
||||||
## Security Considerations
|
## Security Posture
|
||||||
|
|
||||||
### Authentication
|
### Implemented
|
||||||
|
|
||||||
|
| Feature | Details |
|
||||||
|
|---|---|
|
||||||
|
| Cognito User Pool | Admin-only sign-up, strong password policy, Pre-Token Generation Lambda for audience/role claims |
|
||||||
|
| OAuth 2.0 | Authorization Code Grant with custom scopes (`mcp.read`, `mcp.write`) |
|
||||||
|
| JWT audience validation | Proxy Lambda validates `aud` claim before forwarding to AgentCore |
|
||||||
|
| AgentCore Cognito authorizer | Token verified a second time by AWS at the gateway level |
|
||||||
|
| Cedar policy engine | Fine-grained per-user tool access in ENFORCE mode |
|
||||||
|
| Bedrock Guardrails | PII masking (address, name, email) and blocking (credit card numbers) via interceptor |
|
||||||
|
| Lambda-in-VPC proxy | Private subnet, NAT egress only |
|
||||||
|
| VPC Interface Endpoint | AgentCore traffic stays on AWS private network, never crosses public internet |
|
||||||
|
| ALB TLS termination | TLS 1.2+ on custom domain via ACM certificate |
|
||||||
|
| ALB `dropInvalidHeaderFields` | Rejects malformed headers (request-smuggling mitigation) |
|
||||||
|
| ALB Host-header gating | Every forwarding rule requires Host header match; raw `*.elb` DNS returns 404 |
|
||||||
|
| HTTP → HTTPS redirect | Permanent redirect on port 80 |
|
||||||
|
| WAF WebACL | IP rate limit (1,000 req/5 min), AWS IP Reputation list, Core Rule Set (OWASP Top 10), Known Bad Inputs |
|
||||||
|
| WAF Bot Control | COMMON level in COUNT mode (switch to BLOCK after traffic validation) |
|
||||||
|
| Reserved Lambda concurrency | Caps on all functions to limit DoS blast radius |
|
||||||
|
| Gateway resource policy | Restricts `InvokeGateway` to the VPC |
|
||||||
|
| Shield Standard | Automatic L3/L4 DDoS protection on public ALBs |
|
||||||
|
| ALB access logging | S3 bucket with SSE, public access blocked, SSL enforced, 90-day lifecycle expiration |
|
||||||
|
| Redirect URI allowlist | `handle_callback` validates `redirect_uri` against registered Cognito callback URLs before issuing 302 redirects (prevents open-redirect / auth code theft) |
|
||||||
|
| Per-Lambda IAM roles | Four dedicated least-privilege roles: `preTokenLambdaRole` (Cognito trigger), `proxyLambdaRole` (VPC + AgentCore invoke), `interceptorLambdaRole` (Bedrock Guardrails only), `toolLambdaRole` (CloudWatch Logs only) |
|
||||||
|
|
||||||
|
### Not Implemented – Consider Before Production
|
||||||
|
|
||||||
|
| Feature | Details |
|
||||||
|
|---|---|
|
||||||
|
| Shield Advanced | L7 DDoS protection, SRT access, cost protection (subscription required) |
|
||||||
|
| Bot Control TARGETED | Higher inspection level for WAF Bot Control (additional cost) |
|
||||||
|
| CloudTrail / Security Hub | Centralized audit and security findings |
|
||||||
|
| ALB access-log Athena workgroup | Query access logs via Athena for forensic analysis |
|
||||||
|
| GuardDuty findings | Threat detection integration |
|
||||||
|
| MFA enforcement | Cognito User Pool is MFA-ready but not enforced (`mfa: cognito.Mfa.REQUIRED`) |
|
||||||
|
| Scoped IAM resources | Several policies use `Resource: "*"` — scope to specific ARNs |
|
||||||
|
| PKCE enforcement | Verify PKCE is enforced on the Cognito public client (no client secret) |
|
||||||
|
| Log encryption | Lambda CloudWatch logs use default settings (no KMS CMK encryption) |
|
||||||
|
| Log retention policy | Lambda CloudWatch log retention is indefinite by default |
|
||||||
|
|
||||||
|
### Additional Security Details
|
||||||
|
|
||||||
|
#### Authentication
|
||||||
- OAuth 2.0 with PKCE (Proof Key for Code Exchange)
|
- OAuth 2.0 with PKCE (Proof Key for Code Exchange)
|
||||||
- JWT tokens with custom claims
|
- JWT tokens with custom claims
|
||||||
- Secure token storage in VS Code
|
- Secure token storage in VS Code
|
||||||
|
|
||||||
### Authorization
|
#### Authorization
|
||||||
- Policy-based access control using Cedar
|
- Policy-based access control using Cedar
|
||||||
- User attribute injection via Lambda triggers
|
- User attribute injection via Lambda triggers
|
||||||
- Gateway-level authorization enforcement
|
- Gateway-level authorization enforcement
|
||||||
|
|
||||||
### Data Protection
|
#### Data Protection
|
||||||
- SSL/TLS encryption in transit
|
- SSL/TLS encryption in transit
|
||||||
- PII anonymization via Bedrock Guardrails
|
- PII anonymization via Bedrock Guardrails
|
||||||
- VPC isolation for ALB deployments
|
- VPC isolation with private subnets and VPC endpoints
|
||||||
|
|
||||||
### Secrets Management
|
#### Secrets Management
|
||||||
- Client secrets stored in environment variables
|
- Client secrets stored in environment variables
|
||||||
- OAuth tokens never exposed to logs
|
- OAuth tokens never exposed to logs
|
||||||
- IAM role-based access for Lambda functions
|
- IAM role-based access for Lambda functions
|
||||||
@@ -452,10 +472,12 @@ cdk destroy
|
|||||||
|
|
||||||
## Architecture Decisions
|
## Architecture Decisions
|
||||||
|
|
||||||
### Why Two Deployment Options?
|
### Why ALB?
|
||||||
|
|
||||||
- **ALB**: Production environments requiring custom domains, SSL/TLS, and fine-grained routing
|
- Production-grade with custom domains, SSL/TLS, and fine-grained routing
|
||||||
- **API Gateway**: Development/testing, serverless preference, cost optimization
|
- WAF WebACL integration for OWASP Top 10, rate limiting, and bot control
|
||||||
|
- Access logging to S3 for forensic analysis
|
||||||
|
- VPC integration for network isolation
|
||||||
|
|
||||||
### Why Lambda for MCP Servers?
|
### Why Lambda for MCP Servers?
|
||||||
|
|
||||||
|
|||||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 219 KiB After Width: | Height: | Size: 189 KiB |
+119
-10
@@ -1,14 +1,123 @@
|
|||||||
# Welcome to your CDK TypeScript project
|
# Enterprise MCP Gateway – CDK Infrastructure
|
||||||
|
|
||||||
This is a blank project for CDK development with TypeScript.
|
This CDK stack deploys an enterprise-grade MCP (Model Context Protocol) gateway backed by Amazon Bedrock AgentCore, using an Application Load Balancer (ALB) with full security hardening.
|
||||||
|
|
||||||
The `cdk.json` file tells the CDK Toolkit how to execute your app.
|
## Architecture Overview
|
||||||
|
|
||||||
## Useful commands
|
The stack provisions:
|
||||||
|
|
||||||
* `npm run build` compile typescript to js
|
- **Cognito User Pool** with OAuth 2.0 Authorization Code Grant, custom scopes (`mcp.read` / `mcp.write`), and a Pre-Token Generation Lambda for audience/role claim injection.
|
||||||
* `npm run watch` watch for changes and compile
|
- **AgentCore Gateway** with Cognito authorizer, Cedar policy engine (ENFORCE mode), and Bedrock Guardrails (PII masking/blocking).
|
||||||
* `npm run test` perform the jest unit tests
|
- **Lambda functions**: MCP proxy, weather tool, inventory tool, user details tool, interceptor, and pre-token generation.
|
||||||
* `npx cdk deploy` deploy this stack to your default AWS account/region
|
- **VPC** with private subnets, NAT gateway, and VPC Interface Endpoint for AgentCore.
|
||||||
* `npx cdk diff` compare deployed stack with current state
|
- **Internet-facing ALB** with TLS termination, WAF WebACL, and access logging.
|
||||||
* `npx cdk synth` emits the synthesized CloudFormation template
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- AWS CDK v2 installed (`npm install -g aws-cdk`)
|
||||||
|
- Node.js 18+
|
||||||
|
- An ACM certificate and Route 53 hosted zone for the custom domain
|
||||||
|
- Python 3.12 (for Lambda bundling)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Set CDK context variables in `cdk.context.json` or via `-c` flags:
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
|---|---|---|
|
||||||
|
| `domainName` | Custom domain name (e.g. `enterprise-mcp`) | `""` |
|
||||||
|
| `hostedZoneName` | Route 53 hosted zone name | `""` |
|
||||||
|
| `hostedZoneId` | Route 53 hosted zone ID | `""` |
|
||||||
|
| `certificateArn` | ACM certificate ARN | `""` |
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From the cdk/ directory
|
||||||
|
npm install
|
||||||
|
npx cdk synth
|
||||||
|
npx cdk deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** The stack is pinned to `us-east-1` in `bin/enterprise-mcp-infra.ts`. Update the `region` value there if you need a different region.
|
||||||
|
|
||||||
|
## Useful Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---|---|
|
||||||
|
| `npm run build` | Compile TypeScript to JS |
|
||||||
|
| `npm run watch` | Watch for changes and compile |
|
||||||
|
| `npm run test` | Run Jest unit tests |
|
||||||
|
| `npx cdk synth` | Emit the synthesized CloudFormation template |
|
||||||
|
| `npx cdk diff` | Compare deployed stack with current state |
|
||||||
|
| `npx cdk deploy` | Deploy this stack |
|
||||||
|
| `npx cdk destroy` | Tear down the stack |
|
||||||
|
|
||||||
|
## Security Posture
|
||||||
|
|
||||||
|
### Implemented
|
||||||
|
|
||||||
|
| Feature | Details |
|
||||||
|
|---|---|
|
||||||
|
| Cognito User Pool | Admin-only sign-up, strong password policy, Pre-Token Generation Lambda for audience/role claims |
|
||||||
|
| OAuth 2.0 | Authorization Code Grant with custom scopes (`mcp.read`, `mcp.write`) |
|
||||||
|
| JWT audience validation | Proxy Lambda validates `aud` claim before forwarding to AgentCore |
|
||||||
|
| AgentCore Cognito authorizer | Token verified a second time by AWS at the gateway level |
|
||||||
|
| Cedar policy engine | Fine-grained per-user tool access in ENFORCE mode |
|
||||||
|
| Bedrock Guardrails | PII masking (address, name, email) and blocking (credit card numbers) via interceptor |
|
||||||
|
| Lambda-in-VPC proxy | Private subnet, NAT egress only |
|
||||||
|
| VPC Interface Endpoint | AgentCore traffic stays on AWS private network, never crosses public internet |
|
||||||
|
| ALB TLS termination | TLS 1.2+ on custom domain via ACM certificate |
|
||||||
|
| ALB `dropInvalidHeaderFields` | Rejects malformed headers (request-smuggling mitigation) |
|
||||||
|
| ALB Host-header gating | Every forwarding rule requires Host header match; raw `*.elb` DNS returns 404 |
|
||||||
|
| HTTP → HTTPS redirect | Permanent redirect on port 80 |
|
||||||
|
| WAF WebACL | IP rate limit (1,000 req/5 min), AWS IP Reputation list, Core Rule Set (OWASP Top 10), Known Bad Inputs |
|
||||||
|
| WAF Bot Control | COMMON level in COUNT mode (switch to BLOCK after traffic validation) |
|
||||||
|
| Reserved Lambda concurrency | Caps on all functions to limit DoS blast radius |
|
||||||
|
| Gateway resource policy | Restricts `InvokeGateway` to the VPC |
|
||||||
|
| Shield Standard | Automatic L3/L4 DDoS protection on public ALBs |
|
||||||
|
| ALB access logging | S3 bucket with SSE, public access blocked, SSL enforced, 90-day lifecycle expiration |
|
||||||
|
| Redirect URI allowlist | `handle_callback` validates `redirect_uri` against registered Cognito callback URLs before issuing 302 redirects (prevents open-redirect / auth code theft) |
|
||||||
|
| Per-Lambda IAM roles | Four dedicated least-privilege roles: `preTokenLambdaRole` (Cognito trigger), `proxyLambdaRole` (VPC + AgentCore invoke), `interceptorLambdaRole` (Bedrock Guardrails only), `toolLambdaRole` (CloudWatch Logs only) |
|
||||||
|
|
||||||
|
### Not Implemented – Consider Before Production
|
||||||
|
|
||||||
|
| Feature | Details |
|
||||||
|
|---|---|
|
||||||
|
| Shield Advanced | L7 DDoS protection, SRT access, cost protection (subscription required) |
|
||||||
|
| Bot Control TARGETED | Higher inspection level for WAF Bot Control (additional cost) |
|
||||||
|
| CloudTrail / Security Hub | Centralized audit and security findings |
|
||||||
|
| ALB access-log Athena workgroup | Query access logs via Athena for forensic analysis |
|
||||||
|
| GuardDuty findings | Threat detection integration |
|
||||||
|
| MFA enforcement | Cognito User Pool is MFA-ready but not enforced (`mfa: cognito.Mfa.REQUIRED`) |
|
||||||
|
| Scoped IAM resources | Several policies use `Resource: "*"` — scope to specific ARNs |
|
||||||
|
| PKCE enforcement | Verify PKCE is enforced on the Cognito public client (no client secret) |
|
||||||
|
| Log encryption | Lambda CloudWatch logs use default settings (no KMS CMK encryption) |
|
||||||
|
| Log retention policy | Lambda CloudWatch log retention is indefinite by default |
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
cdk/
|
||||||
|
├── bin/
|
||||||
|
│ └── enterprise-mcp-infra.ts # CDK app entry point (region pinned to us-east-1)
|
||||||
|
├── lib/
|
||||||
|
│ ├── enterprise-mcp-infra-stack.ts # Main infrastructure stack
|
||||||
|
│ └── agentcore-policy-engine.ts # Cedar policy engine construct
|
||||||
|
├── lambda/
|
||||||
|
│ ├── mcp_proxy_lambda.py # MCP OAuth proxy Lambda
|
||||||
|
│ ├── pre_token_generation_lambda.py # Cognito pre-token generation trigger
|
||||||
|
│ ├── interceptor/
|
||||||
|
│ │ └── interceptor.py # Guardrails interceptor Lambda
|
||||||
|
│ ├── mcp-servers/
|
||||||
|
│ │ ├── weather/ # Weather tool Lambda
|
||||||
|
│ │ ├── inventory/ # Inventory tool Lambda
|
||||||
|
│ │ └── user_details/ # User details tool Lambda
|
||||||
|
│ └── agentcore-policy-engine/ # Policy engine custom resource Lambda
|
||||||
|
├── test/
|
||||||
|
│ └── enterprise-mcp-infra.test.ts # Jest tests
|
||||||
|
├── cdk.json
|
||||||
|
├── cdk.context.json
|
||||||
|
├── tsconfig.json
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|||||||
+1
-13
@@ -4,17 +4,5 @@ import { EnterpriseMcpInfraStack } from '../lib/enterprise-mcp-infra-stack';
|
|||||||
|
|
||||||
const app = new cdk.App();
|
const app = new cdk.App();
|
||||||
new EnterpriseMcpInfraStack(app, 'EnterpriseMcpInfraStack', {
|
new EnterpriseMcpInfraStack(app, 'EnterpriseMcpInfraStack', {
|
||||||
/* If you don't specify 'env', this stack will be environment-agnostic.
|
env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: 'us-east-1' },
|
||||||
* Account/Region-dependent features and context lookups will not work,
|
|
||||||
* but a single synthesized template can be deployed anywhere. */
|
|
||||||
|
|
||||||
/* Uncomment the next line to specialize this stack for the AWS Account
|
|
||||||
* and Region that are implied by the current CLI configuration. */
|
|
||||||
// env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
|
|
||||||
|
|
||||||
/* Uncomment the next line if you know exactly what Account and Region you
|
|
||||||
* want to deploy the stack to. */
|
|
||||||
// env: { account: '123456789012', region: 'us-east-1' },
|
|
||||||
|
|
||||||
/* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
|
|
||||||
});
|
});
|
||||||
|
|||||||
+4
-3
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"deploymentType": "API_GATEWAY",
|
"domainName": "example.com",
|
||||||
"domainName": "",
|
|
||||||
"hostedZoneName": "",
|
"hostedZoneName": "",
|
||||||
"hostedZoneId": "",
|
"hostedZoneId": "",
|
||||||
"certificateArn": ""
|
"certificateArn": "",
|
||||||
|
"mcpMetadataKey": "com.example/target"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+121
-19
@@ -9,6 +9,7 @@ logger.setLevel(logging.INFO)
|
|||||||
|
|
||||||
GUARDRAIL_ID = os.getenv("GUARDRAIL_ID", None)
|
GUARDRAIL_ID = os.getenv("GUARDRAIL_ID", None)
|
||||||
GUARDRAIL_VERSION = os.getenv("GUARDRAIL_VERSION", "1.0")
|
GUARDRAIL_VERSION = os.getenv("GUARDRAIL_VERSION", "1.0")
|
||||||
|
MCP_METADATA_KEY = os.getenv("MCP_METADATA_KEY", "com.example/target")
|
||||||
|
|
||||||
client = boto3.client("bedrock-runtime")
|
client = boto3.client("bedrock-runtime")
|
||||||
|
|
||||||
@@ -46,6 +47,95 @@ def lambda_handler(event, context):
|
|||||||
|
|
||||||
logger.info(f"Processing RESPONSE interceptor - MCP method: {mcp_method}")
|
logger.info(f"Processing RESPONSE interceptor - MCP method: {mcp_method}")
|
||||||
|
|
||||||
|
# === HANDLE TOOLS/LIST FILTERING BASED ON _meta ===
|
||||||
|
if mcp_method == "tools/list" and response_body:
|
||||||
|
logger.info("tools/list response detected in RESPONSE interceptor")
|
||||||
|
|
||||||
|
# Extract target filter from MCP _meta (spec-compliant)
|
||||||
|
target_filter = None
|
||||||
|
meta = request_body.get("_meta", {})
|
||||||
|
if isinstance(meta, dict):
|
||||||
|
target_filter = meta.get(MCP_METADATA_KEY)
|
||||||
|
|
||||||
|
if target_filter:
|
||||||
|
logger.info(
|
||||||
|
f"Target filter from _meta: {MCP_METADATA_KEY} = '{target_filter}'"
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"Will filter tools to only those starting with '{target_filter}___'"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
"No target filter in _meta - returning ALL tools (no filtering)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Filter tools if target filter is specified
|
||||||
|
if "result" in response_body and "tools" in response_body.get(
|
||||||
|
"result", {}
|
||||||
|
):
|
||||||
|
result = response_body["result"]
|
||||||
|
original_tools = result.get("tools", [])
|
||||||
|
|
||||||
|
logger.info(f"Original tools count: {len(original_tools)}")
|
||||||
|
|
||||||
|
if target_filter:
|
||||||
|
# Filter by gateway target name prefix (format: "target___tool")
|
||||||
|
filtered_tools = [
|
||||||
|
tool
|
||||||
|
for tool in original_tools
|
||||||
|
if tool.get("name", "").startswith(f"{target_filter}___")
|
||||||
|
]
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Filtered to {len(filtered_tools)} tools for target '{target_filter}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Log matched tools
|
||||||
|
if filtered_tools:
|
||||||
|
logger.info("Matched tools:")
|
||||||
|
for tool in filtered_tools:
|
||||||
|
logger.info(f" - {tool.get('name')}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"No tools matched target '{target_filter}'")
|
||||||
|
|
||||||
|
# Log filtering summary
|
||||||
|
removed = len(original_tools) - len(filtered_tools)
|
||||||
|
if removed > 0:
|
||||||
|
logger.info(
|
||||||
|
f"Filtered out {removed} tools not matching target"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create filtered response
|
||||||
|
filtered_body = {
|
||||||
|
"jsonrpc": response_body.get("jsonrpc", "2.0"),
|
||||||
|
"id": response_body.get("id"),
|
||||||
|
"result": {"tools": filtered_tools},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Preserve _meta from response if present
|
||||||
|
if "_meta" in response_body:
|
||||||
|
filtered_body["_meta"] = response_body["_meta"]
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"interceptorOutputVersion": "1.0",
|
||||||
|
"mcp": {
|
||||||
|
"transformedGatewayResponse": {
|
||||||
|
"body": filtered_body,
|
||||||
|
"statusCode": 200,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
logger.info("Returning filtered tools/list response")
|
||||||
|
return response
|
||||||
|
else:
|
||||||
|
# No filtering - log all tools and return unchanged
|
||||||
|
logger.info(
|
||||||
|
f"No filtering applied - returning all {len(original_tools)} tools"
|
||||||
|
)
|
||||||
|
logger.info("Available tools:")
|
||||||
|
for tool in original_tools:
|
||||||
|
logger.info(f" - {tool.get('name')}")
|
||||||
|
|
||||||
if mcp_method == "tools/call" and response_body:
|
if mcp_method == "tools/call" and response_body:
|
||||||
logger.info("tools/call response detected in RESPONSE interceptor")
|
logger.info("tools/call response detected in RESPONSE interceptor")
|
||||||
content = (
|
content = (
|
||||||
@@ -56,7 +146,7 @@ def lambda_handler(event, context):
|
|||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
if GUARDRAIL_ID:
|
if GUARDRAIL_ID:
|
||||||
response = client.apply_guardrail(
|
gr_response = client.apply_guardrail(
|
||||||
guardrailIdentifier=GUARDRAIL_ID,
|
guardrailIdentifier=GUARDRAIL_ID,
|
||||||
guardrailVersion=GUARDRAIL_VERSION,
|
guardrailVersion=GUARDRAIL_VERSION,
|
||||||
source="INPUT",
|
source="INPUT",
|
||||||
@@ -70,13 +160,16 @@ def lambda_handler(event, context):
|
|||||||
],
|
],
|
||||||
outputScope="FULL",
|
outputScope="FULL",
|
||||||
)
|
)
|
||||||
if response.get("action", None) == "GUARDRAIL_INTERVENED":
|
if gr_response.get("action", None) == "GUARDRAIL_INTERVENED":
|
||||||
logger.warning("Guardrail intervened on the content. Details:")
|
logger.warning("Guardrail intervened on the content. Details:")
|
||||||
logger.warning(response.get("outputs", [{}])[0].get("text", {}))
|
guardrail_text = gr_response.get("outputs", [{}])[0].get(
|
||||||
|
"text", ""
|
||||||
|
)
|
||||||
|
logger.warning(guardrail_text)
|
||||||
body_transformed = response_body
|
body_transformed = response_body
|
||||||
body_transformed["result"]["content"][0] = {
|
body_transformed["result"]["content"][0] = {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"text": response.get("outputs", [{}])[0].get("text", {}),
|
"text": guardrail_text,
|
||||||
}
|
}
|
||||||
statusCode = 403
|
statusCode = 403
|
||||||
response = {
|
response = {
|
||||||
@@ -135,7 +228,7 @@ def lambda_handler(event, context):
|
|||||||
if mcp_method == "tools/call" and request_body:
|
if mcp_method == "tools/call" and request_body:
|
||||||
# This is a REQUEST interceptor
|
# This is a REQUEST interceptor
|
||||||
if GUARDRAIL_ID:
|
if GUARDRAIL_ID:
|
||||||
response = client.apply_guardrail(
|
gr_response = client.apply_guardrail(
|
||||||
guardrailIdentifier=GUARDRAIL_ID,
|
guardrailIdentifier=GUARDRAIL_ID,
|
||||||
guardrailVersion=GUARDRAIL_VERSION,
|
guardrailVersion=GUARDRAIL_VERSION,
|
||||||
source="INPUT",
|
source="INPUT",
|
||||||
@@ -149,29 +242,38 @@ def lambda_handler(event, context):
|
|||||||
],
|
],
|
||||||
outputScope="FULL",
|
outputScope="FULL",
|
||||||
)
|
)
|
||||||
logger.info(f"Guardrail response: {response}")
|
logger.info(f"Guardrail response: {gr_response}")
|
||||||
|
|
||||||
if response.get("action", None) == "GUARDRAIL_INTERVENED":
|
if gr_response.get("action", None) == "GUARDRAIL_INTERVENED":
|
||||||
logger.warning("Guardrail intervened on the content. Details:")
|
logger.warning("Guardrail intervened on the content. Details:")
|
||||||
logger.warning(
|
guardrail_text = gr_response.get("outputs", [{}])[0].get(
|
||||||
json.dumps(
|
"text", "{}"
|
||||||
response.get("outputs", [{}])[0].get("text", {}),
|
)
|
||||||
indent=2,
|
logger.warning(guardrail_text)
|
||||||
|
|
||||||
|
# Parse the guardrail output back to a dict since the gateway
|
||||||
|
# expects body to be a JSON object, not a string
|
||||||
|
try:
|
||||||
|
transformed_body = json.loads(guardrail_text)
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
# If guardrail output isn't valid JSON, pass through original request
|
||||||
|
logger.error(
|
||||||
|
"Guardrail output is not valid JSON, passing through original request"
|
||||||
)
|
)
|
||||||
)
|
transformed_body = request_body
|
||||||
logger.info(
|
|
||||||
f"Interceptor response after guardrail intervention: {response}"
|
response = {
|
||||||
)
|
|
||||||
return {
|
|
||||||
"interceptorOutputVersion": "1.0",
|
"interceptorOutputVersion": "1.0",
|
||||||
"mcp": {
|
"mcp": {
|
||||||
"transformedGatewayRequest": {
|
"transformedGatewayRequest": {
|
||||||
"body": response.get("outputs", [{}])[0].get(
|
"body": transformed_body,
|
||||||
"text", {}
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
logger.info(
|
||||||
|
f"Interceptor response after guardrail intervention: {response}"
|
||||||
|
)
|
||||||
|
return response
|
||||||
else:
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Guardrail did not intervene. Passing through original request."
|
"Guardrail did not intervene. Passing through original request."
|
||||||
|
|||||||
+160
-48
@@ -11,15 +11,28 @@ import base64
|
|||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.error
|
import urllib.error
|
||||||
|
import logging
|
||||||
from botocore.auth import SigV4Auth
|
from botocore.auth import SigV4Auth
|
||||||
from botocore.awsrequest import AWSRequest
|
from botocore.awsrequest import AWSRequest
|
||||||
import boto3
|
import boto3
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logger = logging.getLogger()
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
# Configuration from environment variables
|
# Configuration from environment variables
|
||||||
GATEWAY_URL = os.environ.get("GATEWAY_URL", "")
|
GATEWAY_URL = os.environ.get("GATEWAY_URL", "")
|
||||||
COGNITO_DOMAIN = os.environ.get("COGNITO_DOMAIN", "")
|
COGNITO_DOMAIN = os.environ.get("COGNITO_DOMAIN", "")
|
||||||
CLIENT_ID = os.environ.get("CLIENT_ID", "")
|
CLIENT_ID = os.environ.get("CLIENT_ID", "")
|
||||||
CLIENT_SECRET = os.environ.get("CLIENT_SECRET", "")
|
CLIENT_SECRET = os.environ.get("CLIENT_SECRET", "")
|
||||||
|
CALLBACK_LAMBDA_URL = os.environ.get("CALLBACK_LAMBDA_URL", "")
|
||||||
|
RESOURCE_SERVER_ID = os.environ.get("RESOURCE_SERVER_ID", "")
|
||||||
|
MCP_METADATA_KEY = os.environ.get("MCP_METADATA_KEY", "com.example/target")
|
||||||
|
|
||||||
|
# Allowed redirect URIs for the OAuth callback, passed from CDK as a
|
||||||
|
# JSON-encoded list. Must match the Cognito client's registered callbackUrls
|
||||||
|
# to prevent open-redirect attacks.
|
||||||
|
ALLOWED_REDIRECT_URIS = json.loads(os.environ.get("ALLOWED_REDIRECT_URIS", "[]"))
|
||||||
|
|
||||||
|
|
||||||
def sign_request(request):
|
def sign_request(request):
|
||||||
@@ -43,7 +56,7 @@ def sign_request(request):
|
|||||||
|
|
||||||
def lambda_handler(event, context):
|
def lambda_handler(event, context):
|
||||||
"""Main Lambda handler - routes requests based on path."""
|
"""Main Lambda handler - routes requests based on path."""
|
||||||
print(f"Event: {json.dumps(event)}")
|
logger.debug(f"Event: {json.dumps(event)}")
|
||||||
|
|
||||||
# Support both ALB and API Gateway v2 (HTTP API) events
|
# Support both ALB and API Gateway v2 (HTTP API) events
|
||||||
# ALB uses: path, httpMethod
|
# ALB uses: path, httpMethod
|
||||||
@@ -53,7 +66,7 @@ def lambda_handler(event, context):
|
|||||||
"http", {}
|
"http", {}
|
||||||
).get("method", "GET")
|
).get("method", "GET")
|
||||||
|
|
||||||
print(f"Method: {method}, Path: {path}")
|
logger.debug(f"Method: {method}, Path: {path}")
|
||||||
|
|
||||||
if method == "OPTIONS":
|
if method == "OPTIONS":
|
||||||
return {
|
return {
|
||||||
@@ -79,7 +92,7 @@ def lambda_handler(event, context):
|
|||||||
return handle_token(event)
|
return handle_token(event)
|
||||||
elif path == "/register" and method == "POST":
|
elif path == "/register" and method == "POST":
|
||||||
return handle_dcr(event)
|
return handle_dcr(event)
|
||||||
elif path == "/mcp":
|
elif path == "/mcp" or path.endswith("/mcp"):
|
||||||
return proxy_to_gateway(event)
|
return proxy_to_gateway(event)
|
||||||
else:
|
else:
|
||||||
return {"statusCode": 404, "body": json.dumps({"error": "Not found"})}
|
return {"statusCode": 404, "body": json.dumps({"error": "Not found"})}
|
||||||
@@ -103,7 +116,13 @@ def handle_oauth_metadata(event):
|
|||||||
"authorization_endpoint": f"{api_url}/authorize",
|
"authorization_endpoint": f"{api_url}/authorize",
|
||||||
"token_endpoint": f"{api_url}/token",
|
"token_endpoint": f"{api_url}/token",
|
||||||
"registration_endpoint": f"{api_url}/register",
|
"registration_endpoint": f"{api_url}/register",
|
||||||
"scopes_supported": ["openid", "profile", "email"],
|
"scopes_supported": [
|
||||||
|
"openid",
|
||||||
|
"profile",
|
||||||
|
"email",
|
||||||
|
f"{RESOURCE_SERVER_ID}/mcp.read",
|
||||||
|
f"{RESOURCE_SERVER_ID}/mcp.write",
|
||||||
|
],
|
||||||
"response_types_supported": ["code"],
|
"response_types_supported": ["code"],
|
||||||
"grant_types_supported": ["authorization_code", "refresh_token"],
|
"grant_types_supported": ["authorization_code", "refresh_token"],
|
||||||
"token_endpoint_auth_methods_supported": ["none", "client_secret_post"],
|
"token_endpoint_auth_methods_supported": ["none", "client_secret_post"],
|
||||||
@@ -125,6 +144,13 @@ def handle_protected_resource_metadata(event):
|
|||||||
"resource": f"{api_url}/mcp",
|
"resource": f"{api_url}/mcp",
|
||||||
"authorization_servers": [api_url],
|
"authorization_servers": [api_url],
|
||||||
"bearer_methods_supported": ["header"],
|
"bearer_methods_supported": ["header"],
|
||||||
|
"scopes_supported": [
|
||||||
|
"openid",
|
||||||
|
"profile",
|
||||||
|
"email",
|
||||||
|
f"{RESOURCE_SERVER_ID}/mcp.read",
|
||||||
|
f"{RESOURCE_SERVER_ID}/mcp.write",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -135,40 +161,40 @@ def handle_authorize(event):
|
|||||||
Since Lambda is stateless, we encode the original redirect_uri in the state parameter
|
Since Lambda is stateless, we encode the original redirect_uri in the state parameter
|
||||||
so it survives across Lambda invocations.
|
so it survives across Lambda invocations.
|
||||||
"""
|
"""
|
||||||
print("=== HANDLE_AUTHORIZE DEBUG ===")
|
logger.debug("=== HANDLE_AUTHORIZE DEBUG ===")
|
||||||
params = event.get("queryStringParameters", {}) or {}
|
params = event.get("queryStringParameters", {}) or {}
|
||||||
print(f"Original params: {json.dumps(params)}")
|
logger.debug(f"Original params: {json.dumps(params)}")
|
||||||
|
|
||||||
# Remove unsupported parameters (Cognito doesn't support 'resource' parameter)
|
# Remove unsupported parameters (Cognito doesn't support 'resource' parameter)
|
||||||
if "resource" in params:
|
if "resource" in params:
|
||||||
print(f"Removing 'resource' parameter: {params['resource']}")
|
logger.debug(f"Removing 'resource' parameter: {params['resource']}")
|
||||||
params.pop("resource", None)
|
params.pop("resource", None)
|
||||||
|
|
||||||
# Fix scope parameter: convert + to spaces (Cognito expects space-separated scopes)
|
# Fix scope parameter: URL-decode and normalize spaces
|
||||||
if "scope" in params:
|
if "scope" in params:
|
||||||
# In URL encoding, + represents a space, so replace + with actual spaces
|
# URL-decode first (handles %2F etc.), then normalize + to spaces
|
||||||
params["scope"] = params["scope"].replace("+", " ")
|
params["scope"] = urllib.parse.unquote(params["scope"]).replace("+", " ")
|
||||||
print(f"Fixed scope parameter: {params['scope']}")
|
logger.debug(f"Fixed scope parameter: {params['scope']}")
|
||||||
|
|
||||||
# Override client_id
|
# Override client_id
|
||||||
print(f"Original client_id: {params.get('client_id', 'N/A')}")
|
logger.debug(f"Original client_id: {params.get('client_id', 'N/A')}")
|
||||||
params["client_id"] = CLIENT_ID
|
params["client_id"] = CLIENT_ID
|
||||||
print(f"Overridden client_id: {CLIENT_ID}")
|
logger.debug(f"Overridden client_id: {CLIENT_ID}")
|
||||||
|
|
||||||
# Encode original redirect_uri and state together in a new state parameter
|
# Encode original redirect_uri and state together in a new state parameter
|
||||||
original_redirect_uri = params.get("redirect_uri", "")
|
original_redirect_uri = params.get("redirect_uri", "")
|
||||||
original_state = params.get("state", "")
|
original_state = params.get("state", "")
|
||||||
|
|
||||||
print(f"Original redirect_uri (URL encoded): {original_redirect_uri}")
|
logger.debug(f"Original redirect_uri (URL encoded): {original_redirect_uri}")
|
||||||
print(f"Original state (URL encoded): {original_state}")
|
logger.debug(f"Original state (URL encoded): {original_state}")
|
||||||
|
|
||||||
if original_redirect_uri:
|
if original_redirect_uri:
|
||||||
# URL-decode both state and redirect_uri before storing
|
# URL-decode both state and redirect_uri before storing
|
||||||
decoded_state = urllib.parse.unquote(original_state)
|
decoded_state = urllib.parse.unquote(original_state)
|
||||||
decoded_redirect_uri = urllib.parse.unquote(original_redirect_uri)
|
decoded_redirect_uri = urllib.parse.unquote(original_redirect_uri)
|
||||||
|
|
||||||
print(f"Decoded state: {decoded_state}")
|
logger.debug(f"Decoded state: {decoded_state}")
|
||||||
print(f"Decoded redirect_uri: {decoded_redirect_uri}")
|
logger.debug(f"Decoded redirect_uri: {decoded_redirect_uri}")
|
||||||
|
|
||||||
# Create compound state: base64(json({original_state, original_redirect_uri}))
|
# Create compound state: base64(json({original_state, original_redirect_uri}))
|
||||||
compound_state = {
|
compound_state = {
|
||||||
@@ -180,18 +206,18 @@ def handle_authorize(event):
|
|||||||
).decode()
|
).decode()
|
||||||
params["state"] = encoded_state
|
params["state"] = encoded_state
|
||||||
|
|
||||||
print(f"Compound state created: {json.dumps(compound_state)}")
|
logger.debug(f"Compound state created: {json.dumps(compound_state)}")
|
||||||
print(f"Encoded state: {encoded_state}")
|
logger.debug(f"Encoded state: {encoded_state}")
|
||||||
|
|
||||||
# Replace redirect_uri with our callback
|
# Replace redirect_uri with our callback
|
||||||
api_url = get_api_url(event)
|
api_url = get_api_url(event)
|
||||||
params["redirect_uri"] = f"{api_url}/callback"
|
params["redirect_uri"] = f"{api_url}/callback"
|
||||||
print(f"New redirect_uri: {params['redirect_uri']}")
|
logger.debug(f"New redirect_uri: {params['redirect_uri']}")
|
||||||
|
|
||||||
print(f"Final params being sent to Cognito: {json.dumps(params)}")
|
logger.debug(f"Final params being sent to Cognito: {json.dumps(params)}")
|
||||||
redirect_url = f"{COGNITO_DOMAIN.rstrip('/')}/oauth2/authorize?{urllib.parse.urlencode(params)}"
|
redirect_url = f"{COGNITO_DOMAIN.rstrip('/')}/oauth2/authorize?{urllib.parse.urlencode(params)}"
|
||||||
print(f"Redirect URL: {redirect_url}")
|
logger.debug(f"Redirect URL: {redirect_url}")
|
||||||
print("=== END HANDLE_AUTHORIZE DEBUG ===")
|
logger.debug("=== END HANDLE_AUTHORIZE DEBUG ===")
|
||||||
|
|
||||||
return {"statusCode": 302, "headers": {"Location": redirect_url}, "body": ""}
|
return {"statusCode": 302, "headers": {"Location": redirect_url}, "body": ""}
|
||||||
|
|
||||||
@@ -206,10 +232,10 @@ def handle_callback(event):
|
|||||||
encoded_state = params.get("state", "")
|
encoded_state = params.get("state", "")
|
||||||
error = params.get("error", "")
|
error = params.get("error", "")
|
||||||
|
|
||||||
print("=== HANDLE_CALLBACK DEBUG ===")
|
logger.debug("=== HANDLE_CALLBACK DEBUG ===")
|
||||||
print(f"Code: {code}")
|
logger.debug(f"Code: {code}")
|
||||||
print(f"State (URL encoded): {encoded_state}")
|
logger.debug(f"State (URL encoded): {encoded_state}")
|
||||||
print(f"Error: {error}")
|
logger.debug(f"Error: {error}")
|
||||||
|
|
||||||
if error:
|
if error:
|
||||||
return json_response(400, {"error": error})
|
return json_response(400, {"error": error})
|
||||||
@@ -218,33 +244,55 @@ def handle_callback(event):
|
|||||||
try:
|
try:
|
||||||
# First, URL-decode the state parameter (Cognito sends it URL-encoded)
|
# First, URL-decode the state parameter (Cognito sends it URL-encoded)
|
||||||
encoded_state_clean = urllib.parse.unquote(encoded_state)
|
encoded_state_clean = urllib.parse.unquote(encoded_state)
|
||||||
print(f"State (URL decoded): {encoded_state_clean}")
|
logger.debug(f"State (URL decoded): {encoded_state_clean}")
|
||||||
|
|
||||||
# Handle any remaining URL encoding issues (spaces become + or %20)
|
# Handle any remaining URL encoding issues (spaces become + or %20)
|
||||||
encoded_state_clean = encoded_state_clean.replace(" ", "+")
|
encoded_state_clean = encoded_state_clean.replace(" ", "+")
|
||||||
|
|
||||||
# The state should now be proper base64, no padding needed
|
# The state should now be proper base64, no padding needed
|
||||||
print(f"State (ready for base64 decode): {encoded_state_clean}")
|
logger.debug(f"State (ready for base64 decode): {encoded_state_clean}")
|
||||||
print(f"State length: {len(encoded_state_clean)}")
|
logger.debug(f"State length: {len(encoded_state_clean)}")
|
||||||
|
|
||||||
decoded = base64.urlsafe_b64decode(encoded_state_clean).decode()
|
decoded = base64.urlsafe_b64decode(encoded_state_clean).decode()
|
||||||
print(f"Decoded JSON: {decoded}")
|
logger.debug(f"Decoded JSON: {decoded}")
|
||||||
|
|
||||||
compound_state = json.loads(decoded)
|
compound_state = json.loads(decoded)
|
||||||
original_state = compound_state.get("state", "")
|
original_state = compound_state.get("state", "")
|
||||||
original_redirect_uri = compound_state.get("redirect_uri", "")
|
original_redirect_uri = compound_state.get("redirect_uri", "")
|
||||||
|
|
||||||
print(f"Original state: {original_state}")
|
logger.debug(f"Original state: {original_state}")
|
||||||
print(f"Original redirect_uri: {original_redirect_uri}")
|
logger.debug(f"Original redirect_uri: {original_redirect_uri}")
|
||||||
print("=== END HANDLE_CALLBACK DEBUG ===")
|
logger.debug("=== END HANDLE_CALLBACK DEBUG ===")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error decoding state: {e}, state={encoded_state}")
|
logger.error(f"Error decoding state: {e}, state={encoded_state}")
|
||||||
print("=== END HANDLE_CALLBACK DEBUG (ERROR) ===")
|
logger.error("=== END HANDLE_CALLBACK DEBUG (ERROR) ===")
|
||||||
return json_response(400, {"error": "Invalid state parameter"})
|
return json_response(400, {"error": "Invalid state parameter"})
|
||||||
|
|
||||||
if not original_redirect_uri:
|
if not original_redirect_uri:
|
||||||
return json_response(400, {"error": "Missing redirect_uri in state"})
|
return json_response(400, {"error": "Missing redirect_uri in state"})
|
||||||
|
|
||||||
|
# Validate redirect_uri against the allowlist to prevent open-redirect attacks.
|
||||||
|
# A crafted state blob could otherwise redirect the authorization code to an
|
||||||
|
# attacker-controlled URL.
|
||||||
|
#
|
||||||
|
# Localhost URIs with any port are allowed because IDE clients (VS Code, Kiro)
|
||||||
|
# spin up an ephemeral local server on a random port for the OAuth callback.
|
||||||
|
normalized = original_redirect_uri.rstrip("/")
|
||||||
|
parsed = urllib.parse.urlparse(normalized)
|
||||||
|
is_localhost = parsed.scheme == "http" and parsed.hostname in (
|
||||||
|
"localhost",
|
||||||
|
"127.0.0.1",
|
||||||
|
)
|
||||||
|
allowed_normalized = [u.rstrip("/") for u in ALLOWED_REDIRECT_URIS]
|
||||||
|
if not is_localhost and normalized not in allowed_normalized:
|
||||||
|
logger.warning(
|
||||||
|
f"Rejected redirect_uri not in allowlist: {original_redirect_uri}"
|
||||||
|
)
|
||||||
|
logger.debug(f"Normalized redirect_uri: {normalized}")
|
||||||
|
logger.debug(f"Allowed URIs (raw): {ALLOWED_REDIRECT_URIS}")
|
||||||
|
logger.debug(f"Allowed URIs (normalized): {allowed_normalized}")
|
||||||
|
return json_response(400, {"error": "invalid_redirect_uri"})
|
||||||
|
|
||||||
# Forward to VS Code's callback with original state
|
# Forward to VS Code's callback with original state
|
||||||
forward_params = urllib.parse.urlencode({"code": code, "state": original_state})
|
forward_params = urllib.parse.urlencode({"code": code, "state": original_state})
|
||||||
forward_url = f"{original_redirect_uri}?{forward_params}"
|
forward_url = f"{original_redirect_uri}?{forward_params}"
|
||||||
@@ -302,19 +350,81 @@ def handle_dcr(event):
|
|||||||
|
|
||||||
|
|
||||||
def proxy_to_gateway(event):
|
def proxy_to_gateway(event):
|
||||||
"""Forward MCP requests to AgentCore Gateway."""
|
"""Forward MCP requests to AgentCore Gateway with optional target filtering."""
|
||||||
print("proxy_to_gateway")
|
logger.info("proxy_to_gateway")
|
||||||
path = event.get("path", "/")
|
path = event.get("path", "/")
|
||||||
method = event.get("httpMethod") or event.get("requestContext", {}).get(
|
method = event.get("httpMethod") or event.get("requestContext", {}).get(
|
||||||
"http", {}
|
"http", {}
|
||||||
).get("method", "GET")
|
).get("method", "GET")
|
||||||
headers = event.get("headers", {})
|
headers = event.get("headers", {})
|
||||||
body = event.get("body", "")
|
body = event.get("body", "")
|
||||||
print(f"Proxying to gateway - Method: {method}, Path: {path}")
|
logger.info(f"Proxying to gateway - Method: {method}, Path: {path}")
|
||||||
print(f"Headers: {json.dumps(headers)}")
|
logger.debug(f"Headers: {json.dumps(headers)}")
|
||||||
if event.get("isBase64Encoded") and body:
|
if event.get("isBase64Encoded") and body:
|
||||||
body = base64.b64decode(body)
|
body = base64.b64decode(body)
|
||||||
|
|
||||||
|
# === EXTRACT TARGET FROM PATH ===
|
||||||
|
# /mcp → no filter (return all tools)
|
||||||
|
# /gitlab/mcp → filter = "gitlab"
|
||||||
|
# /weather/mcp → filter = "weather"
|
||||||
|
target_filter = None
|
||||||
|
|
||||||
|
if path and path != "/mcp":
|
||||||
|
# Remove leading/trailing slashes and split
|
||||||
|
parts = path.strip("/").split("/")
|
||||||
|
|
||||||
|
# Check if path has format: <target>/mcp
|
||||||
|
if len(parts) == 2 and parts[-1] == "mcp":
|
||||||
|
target_filter = parts[0]
|
||||||
|
logger.info(f"Target filter extracted from path: '{target_filter}'")
|
||||||
|
elif len(parts) > 2 and parts[-1] == "mcp":
|
||||||
|
# Handle nested paths like /api/v1/gitlab/mcp
|
||||||
|
target_filter = parts[-2]
|
||||||
|
logger.info(f"Target filter extracted from nested path: '{target_filter}'")
|
||||||
|
else:
|
||||||
|
logger.debug(f"Path '{path}' does not match target pattern, no filtering")
|
||||||
|
else:
|
||||||
|
logger.debug("Default path '/mcp' - returning all tools (no filtering)")
|
||||||
|
|
||||||
|
# === INJECT INTO MCP _meta ONLY IF TARGET FILTER EXISTS ===
|
||||||
|
if method == "POST" and body:
|
||||||
|
try:
|
||||||
|
# Parse MCP JSON-RPC request
|
||||||
|
mcp_request = json.loads(body if isinstance(body, str) else body.decode())
|
||||||
|
|
||||||
|
# Only inject _meta if we have a target filter AND it's a tool-related method
|
||||||
|
if target_filter and mcp_request.get("method") in [
|
||||||
|
"tools/list",
|
||||||
|
"tools/call",
|
||||||
|
]:
|
||||||
|
# Ensure _meta exists
|
||||||
|
if "_meta" not in mcp_request:
|
||||||
|
mcp_request["_meta"] = {}
|
||||||
|
|
||||||
|
# Inject target filter using reverse DNS notation
|
||||||
|
mcp_request["_meta"][MCP_METADATA_KEY] = target_filter
|
||||||
|
|
||||||
|
logger.info(f"Injected _meta: {MCP_METADATA_KEY} = '{target_filter}'")
|
||||||
|
logger.debug(
|
||||||
|
f"Modified MCP request: {json.dumps(mcp_request, indent=2)}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if not target_filter:
|
||||||
|
logger.debug(
|
||||||
|
"No target filter - NOT injecting _meta (will return all tools)"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.debug(
|
||||||
|
f"Method '{mcp_request.get('method')}' - not injecting _meta"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Re-serialize (possibly modified) request
|
||||||
|
body = json.dumps(mcp_request).encode()
|
||||||
|
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logger.error(f"Failed to parse MCP request: {e}")
|
||||||
|
# Continue with original body if parsing fails
|
||||||
|
|
||||||
# target_url = f"{GATEWAY_URL.rstrip('/mcp')}{path}" if path != "/" else GATEWAY_URL
|
# target_url = f"{GATEWAY_URL.rstrip('/mcp')}{path}" if path != "/" else GATEWAY_URL
|
||||||
target_url = GATEWAY_URL
|
target_url = GATEWAY_URL
|
||||||
# Build request headers
|
# Build request headers
|
||||||
@@ -328,7 +438,7 @@ def proxy_to_gateway(event):
|
|||||||
if headers.get(h):
|
if headers.get(h):
|
||||||
req_headers[h.title()] = headers[h]
|
req_headers[h.title()] = headers[h]
|
||||||
|
|
||||||
print(json.dumps(req_headers))
|
logger.debug(json.dumps(req_headers))
|
||||||
try:
|
try:
|
||||||
if method == "POST" and body:
|
if method == "POST" and body:
|
||||||
data = body.encode() if isinstance(body, str) else body
|
data = body.encode() if isinstance(body, str) else body
|
||||||
@@ -354,7 +464,7 @@ def proxy_to_gateway(event):
|
|||||||
if auth:
|
if auth:
|
||||||
req.add_header("Authorization", auth)
|
req.add_header("Authorization", auth)
|
||||||
|
|
||||||
print(
|
logger.debug(
|
||||||
"{}\n{}\r\n{}\r\n\r\n{}".format(
|
"{}\n{}\r\n{}\r\n\r\n{}".format(
|
||||||
"-----------START-----------",
|
"-----------START-----------",
|
||||||
(req.method or "GET") + " " + req.full_url,
|
(req.method or "GET") + " " + req.full_url,
|
||||||
@@ -365,8 +475,8 @@ def proxy_to_gateway(event):
|
|||||||
|
|
||||||
with urllib.request.urlopen(req, timeout=60) as resp:
|
with urllib.request.urlopen(req, timeout=60) as resp:
|
||||||
resp_body = resp.read().decode()
|
resp_body = resp.read().decode()
|
||||||
print(resp_body)
|
logger.debug(resp_body)
|
||||||
print(resp.headers)
|
logger.debug(resp.headers)
|
||||||
resp_headers = {
|
resp_headers = {
|
||||||
"Content-Type": resp.headers.get("Content-Type", "application/json")
|
"Content-Type": resp.headers.get("Content-Type", "application/json")
|
||||||
}
|
}
|
||||||
@@ -387,7 +497,9 @@ def proxy_to_gateway(event):
|
|||||||
)
|
)
|
||||||
www_auth_rewritten = www_auth.replace(gateway_base, api_url)
|
www_auth_rewritten = www_auth.replace(gateway_base, api_url)
|
||||||
resp_headers["WWW-Authenticate"] = www_auth_rewritten
|
resp_headers["WWW-Authenticate"] = www_auth_rewritten
|
||||||
print(f"Rewrote WWW-Authenticate: {www_auth} -> {www_auth_rewritten}")
|
logger.debug(
|
||||||
|
f"Rewrote WWW-Authenticate: {www_auth} -> {www_auth_rewritten}"
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"statusCode": resp.status,
|
"statusCode": resp.status,
|
||||||
@@ -396,7 +508,7 @@ def proxy_to_gateway(event):
|
|||||||
}
|
}
|
||||||
except urllib.error.HTTPError as e:
|
except urllib.error.HTTPError as e:
|
||||||
error = e.read().decode()
|
error = e.read().decode()
|
||||||
print(f"Gateway error response: {error}")
|
logger.error(f"Gateway error response: {error}")
|
||||||
|
|
||||||
# Rewrite any Gateway URLs in error response body
|
# Rewrite any Gateway URLs in error response body
|
||||||
api_url = get_api_url(event)
|
api_url = get_api_url(event)
|
||||||
@@ -404,7 +516,7 @@ def proxy_to_gateway(event):
|
|||||||
gateway_base = GATEWAY_URL[:-4] if GATEWAY_URL.endswith("/mcp") else GATEWAY_URL
|
gateway_base = GATEWAY_URL[:-4] if GATEWAY_URL.endswith("/mcp") else GATEWAY_URL
|
||||||
error_rewritten = error.replace(gateway_base, api_url)
|
error_rewritten = error.replace(gateway_base, api_url)
|
||||||
if error != error_rewritten:
|
if error != error_rewritten:
|
||||||
print("Rewrote Gateway URL in error body")
|
logger.debug("Rewrote Gateway URL in error body")
|
||||||
|
|
||||||
resp_headers = {"Content-Type": "application/json"}
|
resp_headers = {"Content-Type": "application/json"}
|
||||||
|
|
||||||
@@ -413,7 +525,7 @@ def proxy_to_gateway(event):
|
|||||||
if www_auth:
|
if www_auth:
|
||||||
www_auth_rewritten = www_auth.replace(gateway_base, api_url)
|
www_auth_rewritten = www_auth.replace(gateway_base, api_url)
|
||||||
resp_headers["WWW-Authenticate"] = www_auth_rewritten
|
resp_headers["WWW-Authenticate"] = www_auth_rewritten
|
||||||
print(
|
logger.debug(
|
||||||
f"Rewrote WWW-Authenticate in error: {www_auth} -> {www_auth_rewritten}"
|
f"Rewrote WWW-Authenticate in error: {www_auth} -> {www_auth_rewritten}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+29
-1
@@ -1,9 +1,12 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
RESOURCE_SERVER_ID = os.environ.get("RESOURCE_SERVER_ID", "")
|
||||||
|
|
||||||
|
|
||||||
def lambda_handler(event, context):
|
def lambda_handler(event, context):
|
||||||
"""
|
"""
|
||||||
@@ -27,6 +30,10 @@ def lambda_handler(event, context):
|
|||||||
if email == "vscode-admin@example.com":
|
if email == "vscode-admin@example.com":
|
||||||
# Example: Set custom tag based on email
|
# Example: Set custom tag based on email
|
||||||
custom_tag = "admin_user"
|
custom_tag = "admin_user"
|
||||||
|
elif email == "vscode-readonly@example.com":
|
||||||
|
# Test user with limited scopes — only mcp.read, no mcp.write
|
||||||
|
# Used to verify the gateway rejects requests with insufficient scopes
|
||||||
|
custom_tag = "readonly_user"
|
||||||
else:
|
else:
|
||||||
custom_tag = "regular_user"
|
custom_tag = "regular_user"
|
||||||
|
|
||||||
@@ -74,13 +81,34 @@ def lambda_handler(event, context):
|
|||||||
"claimsToAddOrOverride"
|
"claimsToAddOrOverride"
|
||||||
] = {}
|
] = {}
|
||||||
|
|
||||||
# Add email and user_tag to access token
|
# Add email, user_tag, and aud to access token
|
||||||
event["response"]["claimsAndScopeOverrideDetails"]["accessTokenGeneration"][
|
event["response"]["claimsAndScopeOverrideDetails"]["accessTokenGeneration"][
|
||||||
"claimsToAddOrOverride"
|
"claimsToAddOrOverride"
|
||||||
]["email"] = email
|
]["email"] = email
|
||||||
event["response"]["claimsAndScopeOverrideDetails"]["accessTokenGeneration"][
|
event["response"]["claimsAndScopeOverrideDetails"]["accessTokenGeneration"][
|
||||||
"claimsToAddOrOverride"
|
"claimsToAddOrOverride"
|
||||||
]["user_tag"] = custom_tag
|
]["user_tag"] = custom_tag
|
||||||
|
# Inject the audience claim so the proxy Lambda and AgentCore Gateway
|
||||||
|
# can verify the token is scoped to this resource server.
|
||||||
|
# Cognito requires aud to match the current session's app client ID.
|
||||||
|
client_id = event.get("callerContext", {}).get("clientId", "")
|
||||||
|
if client_id:
|
||||||
|
event["response"]["claimsAndScopeOverrideDetails"]["accessTokenGeneration"][
|
||||||
|
"claimsToAddOrOverride"
|
||||||
|
]["aud"] = client_id
|
||||||
|
|
||||||
|
# For the readonly test user, suppress the mcp.write and mcp.read scopes so the
|
||||||
|
# gateway rejects write operations with insufficient_scope.
|
||||||
|
if custom_tag == "readonly_user":
|
||||||
|
event["response"]["claimsAndScopeOverrideDetails"]["accessTokenGeneration"][
|
||||||
|
"scopesToSuppress"
|
||||||
|
] = [
|
||||||
|
f"{RESOURCE_SERVER_ID}/mcp.write",
|
||||||
|
f"{RESOURCE_SERVER_ID}/mcp.read",
|
||||||
|
]
|
||||||
|
logger.info(
|
||||||
|
"Suppressed mcp.write and mcp.read scopes for readonly test user"
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Added custom claims to ID token and Access token: "
|
f"Added custom claims to ID token and Access token: "
|
||||||
|
|||||||
+7
@@ -73,6 +73,13 @@ export class AgentCorePolicyEngine extends Construct {
|
|||||||
resources: ['*'],
|
resources: ['*'],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
this.policyFunction.addToRolePolicy(
|
||||||
|
new PolicyStatement({
|
||||||
|
effect: Effect.ALLOW,
|
||||||
|
actions: ['iam:GetRole', 'iam:GetRolePolicy', 'iam:ListAttachedRolePolicies', 'iam:ListRolePolicies'],
|
||||||
|
resources: ["arn:aws:iam::*:role/*"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Grant permission to pass the gateway role if provided
|
// Grant permission to pass the gateway role if provided
|
||||||
if (props.gatewayRole) {
|
if (props.gatewayRole) {
|
||||||
|
|||||||
+666
-218
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user