|
| 1 | +#!/bin/bash |
| 2 | +set -e |
| 3 | + |
| 4 | +echo "==============================================" |
| 5 | +echo "13-mcp-policy.sh - MCP Gateway Policy Deployment" |
| 6 | +echo "==============================================" |
| 7 | + |
| 8 | +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 9 | + |
| 10 | +source ~/environment/.envrc 2>/dev/null || true |
| 11 | + |
| 12 | +if [ -z "${GATEWAY_ID}" ]; then |
| 13 | + echo "Error: Missing GATEWAY_ID. Run 06-mcp-gateway.sh first." |
| 14 | + exit 1 |
| 15 | +fi |
| 16 | + |
| 17 | +ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text --no-cli-pager) |
| 18 | +AWS_REGION=$(aws configure get region --no-cli-pager) |
| 19 | +GATEWAY_ARN="arn:aws:bedrock-agentcore:${AWS_REGION}:${ACCOUNT_ID}:gateway/${GATEWAY_ID}" |
| 20 | + |
| 21 | +echo "Gateway: ${GATEWAY_ID}" |
| 22 | + |
| 23 | +VENV_DIR="/tmp/policy_venv" |
| 24 | +[ ! -d "${VENV_DIR}" ] && python3 -m venv "${VENV_DIR}" && "${VENV_DIR}/bin/pip" install -q boto3 |
| 25 | + |
| 26 | +"${VENV_DIR}/bin/python3" << EOF |
| 27 | +import boto3, time, secrets, json |
| 28 | +
|
| 29 | +client = boto3.client('bedrock-agentcore-control', region_name='${AWS_REGION}') |
| 30 | +iam = boto3.client('iam', region_name='${AWS_REGION}') |
| 31 | +
|
| 32 | +GATEWAY_ARN = "${GATEWAY_ARN}" |
| 33 | +ACCOUNT_ID = "${ACCOUNT_ID}" |
| 34 | +
|
| 35 | +# Get or create policy engine |
| 36 | +engines = client.list_policy_engines() |
| 37 | +existing = [e for e in engines.get('policyEngines', []) if e['name'].startswith('BackofficePolicyEngine') and e['status'] == 'ACTIVE'] |
| 38 | +if existing: |
| 39 | + ENGINE_ID = existing[0]['policyEngineId'] |
| 40 | +else: |
| 41 | + result = client.create_policy_engine(name=f"BackofficePolicyEngine_{secrets.token_hex(5)}") |
| 42 | + ENGINE_ID = result['policyEngineId'] |
| 43 | + while client.get_policy_engine(policyEngineId=ENGINE_ID)['status'] != 'ACTIVE': |
| 44 | + time.sleep(3) |
| 45 | +print(f"Policy Engine: {ENGINE_ID}") |
| 46 | +
|
| 47 | +# Cleanup |
| 48 | +for p in client.list_policies(policyEngineId=ENGINE_ID).get('policies', []): |
| 49 | + try: client.delete_policy(policyEngineId=ENGINE_ID, policyId=p['policyId']); time.sleep(1) |
| 50 | + except: pass |
| 51 | +
|
| 52 | +# Try combined policy first |
| 53 | +COMBINED = f'''forbid( |
| 54 | + principal, |
| 55 | + action in [ |
| 56 | + AgentCore::Action::"backoffice___cancelTrip", |
| 57 | + AgentCore::Action::"backoffice___deleteExpense" |
| 58 | + ], |
| 59 | + resource == AgentCore::Gateway::"{GATEWAY_ARN}" |
| 60 | +);''' |
| 61 | +
|
| 62 | +print("Creating forbid policy...") |
| 63 | +result = client.create_policy( |
| 64 | + policyEngineId=ENGINE_ID, |
| 65 | + name="ForbidDangerousOperations", |
| 66 | + definition={'cedar': {'statement': COMBINED}}, |
| 67 | + validationMode='IGNORE_ALL_FINDINGS' |
| 68 | +) |
| 69 | +
|
| 70 | +success = False |
| 71 | +for _ in range(15): |
| 72 | + p = client.get_policy(policyEngineId=ENGINE_ID, policyId=result['policyId']) |
| 73 | + if p['status'] == 'ACTIVE': |
| 74 | + print(" ✅ Combined policy active") |
| 75 | + success = True |
| 76 | + break |
| 77 | + elif 'FAILED' in p['status']: |
| 78 | + print(" ⚠️ Combined failed, creating separate policies...") |
| 79 | + client.delete_policy(policyEngineId=ENGINE_ID, policyId=result['policyId']) |
| 80 | + break |
| 81 | + time.sleep(2) |
| 82 | +
|
| 83 | +# Fallback to separate policies |
| 84 | +if not success: |
| 85 | + for action in ["cancelTrip", "deleteExpense"]: |
| 86 | + policy = f'''forbid( |
| 87 | + principal, |
| 88 | + action == AgentCore::Action::"backoffice___{action}", |
| 89 | + resource == AgentCore::Gateway::"{GATEWAY_ARN}" |
| 90 | +);''' |
| 91 | + result = client.create_policy( |
| 92 | + policyEngineId=ENGINE_ID, |
| 93 | + name=f"Forbid{action}", |
| 94 | + definition={'cedar': {'statement': policy}}, |
| 95 | + validationMode='IGNORE_ALL_FINDINGS' |
| 96 | + ) |
| 97 | + for _ in range(10): |
| 98 | + p = client.get_policy(policyEngineId=ENGINE_ID, policyId=result['policyId']) |
| 99 | + if p['status'] == 'ACTIVE': |
| 100 | + print(f" ✅ Forbid{action}: active") |
| 101 | + break |
| 102 | + elif 'FAILED' in p['status']: |
| 103 | + print(f" ⚠️ Forbid{action}: failed (tool not in schema)") |
| 104 | + break |
| 105 | + time.sleep(2) |
| 106 | +
|
| 107 | +# IAM |
| 108 | +iam.put_role_policy( |
| 109 | + RoleName='mcp-gateway-role', |
| 110 | + PolicyName='PolicyEngineAccess', |
| 111 | + PolicyDocument=json.dumps({ |
| 112 | + "Version": "2012-10-17", |
| 113 | + "Statement": [{"Effect": "Allow", "Action": [ |
| 114 | + "bedrock-agentcore:GetPolicyEngine", "bedrock-agentcore:IsAuthorized", |
| 115 | + "bedrock-agentcore:AuthorizeAction", "bedrock-agentcore:AuthorizeActions" |
| 116 | + ], "Resource": f"arn:aws:bedrock-agentcore:${AWS_REGION}:{ACCOUNT_ID}:*"}] |
| 117 | + }) |
| 118 | +) |
| 119 | +
|
| 120 | +# Attach to gateway |
| 121 | +gateway = client.get_gateway(gatewayIdentifier="${GATEWAY_ID}") |
| 122 | +ENGINE_ARN = f"arn:aws:bedrock-agentcore:${AWS_REGION}:{ACCOUNT_ID}:policy-engine/{ENGINE_ID}" |
| 123 | +client.update_gateway( |
| 124 | + gatewayIdentifier="${GATEWAY_ID}", |
| 125 | + name=gateway['name'], roleArn=gateway['roleArn'], |
| 126 | + protocolType=gateway['protocolType'], authorizerType=gateway['authorizerType'], |
| 127 | + policyEngineConfiguration={'arn': ENGINE_ARN, 'mode': 'ENFORCE'} |
| 128 | +) |
| 129 | +while client.get_gateway(gatewayIdentifier="${GATEWAY_ID}")['status'] != 'READY': |
| 130 | + time.sleep(2) |
| 131 | +print("✅ Gateway ready") |
| 132 | +EOF |
| 133 | + |
| 134 | +echo "" |
| 135 | +echo "Done! Forbidden: cancelTrip, deleteExpense" |
0 commit comments