awslabs.iam-mcp-server 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- awslabs/__init__.py +17 -0
- awslabs/iam_mcp_server/__init__.py +17 -0
- awslabs/iam_mcp_server/aws_client.py +84 -0
- awslabs/iam_mcp_server/context.py +50 -0
- awslabs/iam_mcp_server/errors.py +150 -0
- awslabs/iam_mcp_server/models.py +197 -0
- awslabs/iam_mcp_server/server.py +769 -0
- awslabs_iam_mcp_server-1.0.1.dist-info/METADATA +482 -0
- awslabs_iam_mcp_server-1.0.1.dist-info/RECORD +13 -0
- awslabs_iam_mcp_server-1.0.1.dist-info/WHEEL +4 -0
- awslabs_iam_mcp_server-1.0.1.dist-info/entry_points.txt +2 -0
- awslabs_iam_mcp_server-1.0.1.dist-info/licenses/LICENSE +175 -0
- awslabs_iam_mcp_server-1.0.1.dist-info/licenses/NOTICE +2 -0
|
@@ -0,0 +1,769 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""AWS IAM MCP Server implementation."""
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
from awslabs.iam_mcp_server.aws_client import get_iam_client
|
|
20
|
+
from awslabs.iam_mcp_server.context import Context
|
|
21
|
+
from awslabs.iam_mcp_server.errors import IamClientError, IamValidationError, handle_iam_error
|
|
22
|
+
from awslabs.iam_mcp_server.models import (
|
|
23
|
+
AccessKey,
|
|
24
|
+
AttachedPolicy,
|
|
25
|
+
CreateUserResponse,
|
|
26
|
+
IamUser,
|
|
27
|
+
UserDetailsResponse,
|
|
28
|
+
UsersListResponse,
|
|
29
|
+
)
|
|
30
|
+
from loguru import logger
|
|
31
|
+
from mcp.server.fastmcp import FastMCP
|
|
32
|
+
from mcp.types import CallToolResult
|
|
33
|
+
from pydantic import Field
|
|
34
|
+
from typing import Any, Dict, List, Optional, Union
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
mcp = FastMCP(
|
|
38
|
+
'awslabs.iam-mcp-server',
|
|
39
|
+
instructions="""
|
|
40
|
+
# AWS IAM MCP Server
|
|
41
|
+
|
|
42
|
+
This MCP server provides comprehensive AWS Identity and Access Management (IAM) capabilities:
|
|
43
|
+
|
|
44
|
+
## Core Features:
|
|
45
|
+
1. **User Management**: Create, list, update, and delete IAM users
|
|
46
|
+
2. **Role Management**: Create, list, update, and delete IAM roles
|
|
47
|
+
3. **Policy Management**: Create, list, update, and delete IAM policies
|
|
48
|
+
4. **Group Management**: Create, list, update, and delete IAM groups
|
|
49
|
+
5. **Permission Management**: Attach/detach policies to users, roles, and groups
|
|
50
|
+
6. **Access Key Management**: Create, list, and delete access keys for users
|
|
51
|
+
7. **Security Analysis**: Analyze permissions, find unused resources, and security recommendations
|
|
52
|
+
|
|
53
|
+
## Security Best Practices:
|
|
54
|
+
- Always follow the principle of least privilege
|
|
55
|
+
- Regularly rotate access keys
|
|
56
|
+
- Use roles instead of users for applications
|
|
57
|
+
- Enable MFA where possible
|
|
58
|
+
- Review and audit permissions regularly
|
|
59
|
+
|
|
60
|
+
## Usage Requirements:
|
|
61
|
+
- Requires valid AWS credentials with appropriate IAM permissions
|
|
62
|
+
- Some operations may be restricted in read-only mode
|
|
63
|
+
- Always test policy changes in a safe environment first
|
|
64
|
+
""",
|
|
65
|
+
dependencies=['pydantic', 'loguru', 'boto3', 'botocore'],
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@mcp.tool()
|
|
70
|
+
async def list_users(
|
|
71
|
+
ctx: CallToolResult,
|
|
72
|
+
path_prefix: Optional[str] = Field(
|
|
73
|
+
description='Path prefix to filter users (e.g., "/division_abc/")', default=None
|
|
74
|
+
),
|
|
75
|
+
max_items: int = Field(description='Maximum number of users to return', default=100),
|
|
76
|
+
) -> UsersListResponse:
|
|
77
|
+
"""List IAM users in the account.
|
|
78
|
+
|
|
79
|
+
This tool retrieves a list of IAM users from your AWS account with optional filtering.
|
|
80
|
+
Use this to get an overview of all users or find specific users by path prefix.
|
|
81
|
+
|
|
82
|
+
## Usage Tips:
|
|
83
|
+
- Use path_prefix to filter users by organizational structure
|
|
84
|
+
- Adjust max_items to control response size for large accounts
|
|
85
|
+
- Results may be paginated for accounts with many users
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
ctx: MCP context for error reporting
|
|
89
|
+
path_prefix: Optional path prefix to filter users
|
|
90
|
+
max_items: Maximum number of users to return
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
UsersListResponse containing list of users and metadata
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
logger.info(f"Listing IAM users with path_prefix='{path_prefix}', max_items={max_items}")
|
|
97
|
+
|
|
98
|
+
iam = get_iam_client()
|
|
99
|
+
|
|
100
|
+
kwargs: Dict[str, Any] = {'MaxItems': max_items}
|
|
101
|
+
if path_prefix:
|
|
102
|
+
kwargs['PathPrefix'] = path_prefix
|
|
103
|
+
|
|
104
|
+
response = iam.list_users(**kwargs)
|
|
105
|
+
|
|
106
|
+
users = []
|
|
107
|
+
for user in response.get('Users', []):
|
|
108
|
+
users.append(
|
|
109
|
+
IamUser(
|
|
110
|
+
user_name=user['UserName'],
|
|
111
|
+
user_id=user['UserId'],
|
|
112
|
+
arn=user['Arn'],
|
|
113
|
+
path=user['Path'],
|
|
114
|
+
create_date=user['CreateDate'].isoformat(),
|
|
115
|
+
password_last_used=user.get('PasswordLastUsed', '').isoformat()
|
|
116
|
+
if user.get('PasswordLastUsed')
|
|
117
|
+
else None,
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
result = UsersListResponse(
|
|
122
|
+
users=users,
|
|
123
|
+
is_truncated=response.get('IsTruncated', False),
|
|
124
|
+
marker=response.get('Marker'),
|
|
125
|
+
count=len(users),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
logger.info(f'Successfully listed {len(users)} IAM users')
|
|
129
|
+
return result
|
|
130
|
+
|
|
131
|
+
except Exception as e:
|
|
132
|
+
error = handle_iam_error(e)
|
|
133
|
+
logger.error(f'Error listing users: {error}')
|
|
134
|
+
raise error
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@mcp.tool()
|
|
138
|
+
async def get_user(
|
|
139
|
+
ctx: CallToolResult, user_name: str = Field(description='The name of the IAM user to retrieve')
|
|
140
|
+
) -> UserDetailsResponse:
|
|
141
|
+
"""Get detailed information about a specific IAM user.
|
|
142
|
+
|
|
143
|
+
This tool retrieves comprehensive information about an IAM user including
|
|
144
|
+
attached policies, group memberships, and access keys. Use this to get
|
|
145
|
+
a complete picture of a user's permissions and configuration.
|
|
146
|
+
|
|
147
|
+
## Usage Tips:
|
|
148
|
+
- Use this after list_users to get detailed information about specific users
|
|
149
|
+
- Review attached policies to understand user permissions
|
|
150
|
+
- Check access keys to identify potential security issues
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
ctx: MCP context for error reporting
|
|
154
|
+
user_name: The name of the IAM user
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
UserDetailsResponse containing comprehensive user information
|
|
158
|
+
"""
|
|
159
|
+
try:
|
|
160
|
+
logger.info(f'Getting details for IAM user: {user_name}')
|
|
161
|
+
|
|
162
|
+
if not user_name:
|
|
163
|
+
raise IamValidationError('User name is required')
|
|
164
|
+
|
|
165
|
+
iam = get_iam_client()
|
|
166
|
+
|
|
167
|
+
# Get user details
|
|
168
|
+
user_response = iam.get_user(UserName=user_name)
|
|
169
|
+
user = user_response['User']
|
|
170
|
+
|
|
171
|
+
# Get attached policies
|
|
172
|
+
attached_policies_response = iam.list_attached_user_policies(UserName=user_name)
|
|
173
|
+
attached_policies = [
|
|
174
|
+
AttachedPolicy(policy_name=policy['PolicyName'], policy_arn=policy['PolicyArn'])
|
|
175
|
+
for policy in attached_policies_response.get('AttachedPolicies', [])
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
# Get inline policies
|
|
179
|
+
inline_policies_response = iam.list_user_policies(UserName=user_name)
|
|
180
|
+
inline_policies = inline_policies_response.get('PolicyNames', [])
|
|
181
|
+
|
|
182
|
+
# Get groups
|
|
183
|
+
groups_response = iam.get_groups_for_user(UserName=user_name)
|
|
184
|
+
groups = [group['GroupName'] for group in groups_response.get('Groups', [])]
|
|
185
|
+
|
|
186
|
+
# Get access keys
|
|
187
|
+
access_keys_response = iam.list_access_keys(UserName=user_name)
|
|
188
|
+
access_keys = [
|
|
189
|
+
AccessKey(
|
|
190
|
+
access_key_id=key['AccessKeyId'],
|
|
191
|
+
status=key['Status'],
|
|
192
|
+
create_date=key['CreateDate'].isoformat(),
|
|
193
|
+
)
|
|
194
|
+
for key in access_keys_response.get('AccessKeyMetadata', [])
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
user_details = IamUser(
|
|
198
|
+
user_name=user['UserName'],
|
|
199
|
+
user_id=user['UserId'],
|
|
200
|
+
arn=user['Arn'],
|
|
201
|
+
path=user['Path'],
|
|
202
|
+
create_date=user['CreateDate'].isoformat(),
|
|
203
|
+
password_last_used=user.get('PasswordLastUsed', '').isoformat()
|
|
204
|
+
if user.get('PasswordLastUsed')
|
|
205
|
+
else None,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
result = UserDetailsResponse(
|
|
209
|
+
user=user_details,
|
|
210
|
+
attached_policies=attached_policies,
|
|
211
|
+
inline_policies=inline_policies,
|
|
212
|
+
groups=groups,
|
|
213
|
+
access_keys=access_keys,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
logger.info(f'Successfully retrieved details for user: {user_name}')
|
|
217
|
+
return result
|
|
218
|
+
|
|
219
|
+
except Exception as e:
|
|
220
|
+
error = handle_iam_error(e)
|
|
221
|
+
logger.error(f'Error getting user details: {error}')
|
|
222
|
+
raise error
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@mcp.tool()
|
|
226
|
+
async def create_user(
|
|
227
|
+
ctx: CallToolResult,
|
|
228
|
+
user_name: str = Field(description='The name of the new IAM user'),
|
|
229
|
+
path: str = Field(description='The path for the user', default='/'),
|
|
230
|
+
permissions_boundary: Optional[str] = Field(
|
|
231
|
+
description='ARN of the permissions boundary policy', default=None
|
|
232
|
+
),
|
|
233
|
+
) -> CreateUserResponse:
|
|
234
|
+
"""Create a new IAM user.
|
|
235
|
+
|
|
236
|
+
This tool creates a new IAM user in your AWS account. The user will be created
|
|
237
|
+
without any permissions by default - you'll need to attach policies separately.
|
|
238
|
+
|
|
239
|
+
## Security Best Practices:
|
|
240
|
+
- Use descriptive user names that indicate the user's role or purpose
|
|
241
|
+
- Set appropriate paths for organizational structure
|
|
242
|
+
- Consider using permissions boundaries to limit maximum permissions
|
|
243
|
+
- Follow the principle of least privilege when assigning permissions later
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
ctx: MCP context for error reporting
|
|
247
|
+
user_name: The name of the new IAM user
|
|
248
|
+
path: The path for the user (default: '/')
|
|
249
|
+
permissions_boundary: Optional ARN of the permissions boundary policy
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
CreateUserResponse containing the created user details
|
|
253
|
+
"""
|
|
254
|
+
try:
|
|
255
|
+
logger.info(f'Creating IAM user: {user_name}')
|
|
256
|
+
|
|
257
|
+
# Check if server is in read-only mode
|
|
258
|
+
if Context.is_readonly():
|
|
259
|
+
raise IamClientError('Cannot create user: server is running in read-only mode')
|
|
260
|
+
|
|
261
|
+
if not user_name:
|
|
262
|
+
raise IamValidationError('User name is required')
|
|
263
|
+
|
|
264
|
+
iam = get_iam_client()
|
|
265
|
+
|
|
266
|
+
kwargs = {'UserName': user_name, 'Path': path}
|
|
267
|
+
|
|
268
|
+
if permissions_boundary:
|
|
269
|
+
kwargs['PermissionsBoundary'] = permissions_boundary
|
|
270
|
+
|
|
271
|
+
response = iam.create_user(**kwargs)
|
|
272
|
+
user = response['User']
|
|
273
|
+
|
|
274
|
+
user_details = IamUser(
|
|
275
|
+
user_name=user['UserName'],
|
|
276
|
+
user_id=user['UserId'],
|
|
277
|
+
arn=user['Arn'],
|
|
278
|
+
path=user['Path'],
|
|
279
|
+
create_date=user['CreateDate'].isoformat(),
|
|
280
|
+
password_last_used=user.get('PasswordLastUsed').isoformat()
|
|
281
|
+
if user.get('PasswordLastUsed')
|
|
282
|
+
else None,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
result = CreateUserResponse(
|
|
286
|
+
user=user_details, message=f'Successfully created user: {user_name}'
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
logger.info(f'Successfully created IAM user: {user_name}')
|
|
290
|
+
return result
|
|
291
|
+
|
|
292
|
+
except Exception as e:
|
|
293
|
+
error = handle_iam_error(e)
|
|
294
|
+
logger.error(f'Error creating user: {error}')
|
|
295
|
+
raise error
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@mcp.tool()
|
|
299
|
+
async def delete_user(
|
|
300
|
+
user_name: str = Field(description='The name of the IAM user to delete'),
|
|
301
|
+
force: bool = Field(
|
|
302
|
+
description='Force delete user by removing all attached policies, groups, and access keys first',
|
|
303
|
+
default=False,
|
|
304
|
+
),
|
|
305
|
+
) -> Dict[str, Any]:
|
|
306
|
+
"""Delete an IAM user.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
user_name: The name of the IAM user to delete
|
|
310
|
+
force: If True, removes all attached policies, groups, and access keys first
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Dictionary containing deletion status
|
|
314
|
+
"""
|
|
315
|
+
try:
|
|
316
|
+
# Check if server is in read-only mode
|
|
317
|
+
if Context.is_readonly():
|
|
318
|
+
raise IamClientError('Cannot delete user: server is running in read-only mode')
|
|
319
|
+
|
|
320
|
+
iam = get_iam_client()
|
|
321
|
+
|
|
322
|
+
if force:
|
|
323
|
+
# Remove from all groups
|
|
324
|
+
groups = iam.get_groups_for_user(UserName=user_name)
|
|
325
|
+
for group in groups.get('Groups', []):
|
|
326
|
+
iam.remove_user_from_group(GroupName=group['GroupName'], UserName=user_name)
|
|
327
|
+
|
|
328
|
+
# Detach all managed policies
|
|
329
|
+
attached_policies = iam.list_attached_user_policies(UserName=user_name)
|
|
330
|
+
for policy in attached_policies.get('AttachedPolicies', []):
|
|
331
|
+
iam.detach_user_policy(UserName=user_name, PolicyArn=policy['PolicyArn'])
|
|
332
|
+
|
|
333
|
+
# Delete all inline policies
|
|
334
|
+
inline_policies = iam.list_user_policies(UserName=user_name)
|
|
335
|
+
for policy_name in inline_policies.get('PolicyNames', []):
|
|
336
|
+
iam.delete_user_policy(UserName=user_name, PolicyName=policy_name)
|
|
337
|
+
|
|
338
|
+
# Delete all access keys
|
|
339
|
+
access_keys = iam.list_access_keys(UserName=user_name)
|
|
340
|
+
for key in access_keys.get('AccessKeyMetadata', []):
|
|
341
|
+
iam.delete_access_key(UserName=user_name, AccessKeyId=key['AccessKeyId'])
|
|
342
|
+
|
|
343
|
+
# Delete the user
|
|
344
|
+
iam.delete_user(UserName=user_name)
|
|
345
|
+
|
|
346
|
+
return {'Message': f'Successfully deleted user: {user_name}', 'ForcedCleanup': force}
|
|
347
|
+
|
|
348
|
+
except Exception as e:
|
|
349
|
+
raise handle_iam_error(e)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@mcp.tool()
|
|
353
|
+
async def list_roles(
|
|
354
|
+
path_prefix: Optional[str] = Field(
|
|
355
|
+
description='Path prefix to filter roles (e.g., "/service-role/")', default=None
|
|
356
|
+
),
|
|
357
|
+
max_items: int = Field(description='Maximum number of roles to return', default=100),
|
|
358
|
+
) -> Dict[str, Any]:
|
|
359
|
+
"""List IAM roles in the account.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
path_prefix: Optional path prefix to filter roles
|
|
363
|
+
max_items: Maximum number of roles to return
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
Dictionary containing list of roles and metadata
|
|
367
|
+
"""
|
|
368
|
+
try:
|
|
369
|
+
iam = get_iam_client()
|
|
370
|
+
|
|
371
|
+
kwargs: Dict[str, Any] = {'MaxItems': max_items}
|
|
372
|
+
if path_prefix:
|
|
373
|
+
kwargs['PathPrefix'] = path_prefix
|
|
374
|
+
|
|
375
|
+
response = iam.list_roles(**kwargs)
|
|
376
|
+
|
|
377
|
+
roles = []
|
|
378
|
+
for role in response.get('Roles', []):
|
|
379
|
+
roles.append(
|
|
380
|
+
{
|
|
381
|
+
'RoleName': role['RoleName'],
|
|
382
|
+
'RoleId': role['RoleId'],
|
|
383
|
+
'Arn': role['Arn'],
|
|
384
|
+
'Path': role['Path'],
|
|
385
|
+
'CreateDate': role['CreateDate'].isoformat(),
|
|
386
|
+
'AssumeRolePolicyDocument': role.get('AssumeRolePolicyDocument'),
|
|
387
|
+
'Description': role.get('Description'),
|
|
388
|
+
'MaxSessionDuration': role.get('MaxSessionDuration'),
|
|
389
|
+
}
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
'Roles': roles,
|
|
394
|
+
'IsTruncated': response.get('IsTruncated', False),
|
|
395
|
+
'Marker': response.get('Marker'),
|
|
396
|
+
'Count': len(roles),
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
except Exception as e:
|
|
400
|
+
raise handle_iam_error(e)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
@mcp.tool()
|
|
404
|
+
async def create_role(
|
|
405
|
+
role_name: str = Field(description='The name of the new IAM role'),
|
|
406
|
+
assume_role_policy_document: Union[str, dict] = Field(
|
|
407
|
+
description='The trust policy document in JSON format (string or dict)'
|
|
408
|
+
),
|
|
409
|
+
path: str = Field(description='The path for the role', default='/'),
|
|
410
|
+
description: Optional[str] = Field(description='Description of the role', default=None),
|
|
411
|
+
max_session_duration: int = Field(
|
|
412
|
+
description='Maximum session duration in seconds (3600-43200)', default=3600
|
|
413
|
+
),
|
|
414
|
+
permissions_boundary: Optional[str] = Field(
|
|
415
|
+
description='ARN of the permissions boundary policy', default=None
|
|
416
|
+
),
|
|
417
|
+
) -> Dict[str, Any]:
|
|
418
|
+
"""Create a new IAM role.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
role_name: The name of the new IAM role
|
|
422
|
+
assume_role_policy_document: The trust policy document in JSON format
|
|
423
|
+
path: The path for the role (default: '/')
|
|
424
|
+
description: Optional description of the role
|
|
425
|
+
max_session_duration: Maximum session duration in seconds
|
|
426
|
+
permissions_boundary: Optional ARN of the permissions boundary policy
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
Dictionary containing the created role details
|
|
430
|
+
"""
|
|
431
|
+
try:
|
|
432
|
+
# Check if server is in read-only mode
|
|
433
|
+
if Context.is_readonly():
|
|
434
|
+
raise IamClientError('Cannot create role: server is running in read-only mode')
|
|
435
|
+
|
|
436
|
+
iam = get_iam_client()
|
|
437
|
+
|
|
438
|
+
# Handle both string and dict types
|
|
439
|
+
if isinstance(assume_role_policy_document, dict):
|
|
440
|
+
policy_document = json.dumps(assume_role_policy_document)
|
|
441
|
+
else:
|
|
442
|
+
policy_document = assume_role_policy_document
|
|
443
|
+
# Validate JSON
|
|
444
|
+
try:
|
|
445
|
+
json.loads(policy_document)
|
|
446
|
+
except json.JSONDecodeError:
|
|
447
|
+
raise Exception('Invalid JSON in assume_role_policy_document')
|
|
448
|
+
|
|
449
|
+
kwargs = {
|
|
450
|
+
'RoleName': role_name,
|
|
451
|
+
'AssumeRolePolicyDocument': policy_document,
|
|
452
|
+
'Path': path,
|
|
453
|
+
'MaxSessionDuration': max_session_duration,
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if description:
|
|
457
|
+
kwargs['Description'] = description
|
|
458
|
+
if permissions_boundary:
|
|
459
|
+
kwargs['PermissionsBoundary'] = permissions_boundary
|
|
460
|
+
|
|
461
|
+
response = iam.create_role(**kwargs)
|
|
462
|
+
role = response['Role']
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
'Role': {
|
|
466
|
+
'RoleName': role['RoleName'],
|
|
467
|
+
'RoleId': role['RoleId'],
|
|
468
|
+
'Arn': role['Arn'],
|
|
469
|
+
'Path': role['Path'],
|
|
470
|
+
'CreateDate': role['CreateDate'].isoformat(),
|
|
471
|
+
'AssumeRolePolicyDocument': role.get('AssumeRolePolicyDocument'),
|
|
472
|
+
'Description': role.get('Description'),
|
|
473
|
+
'MaxSessionDuration': role.get('MaxSessionDuration'),
|
|
474
|
+
},
|
|
475
|
+
'Message': f'Successfully created role: {role_name}',
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
except Exception as e:
|
|
479
|
+
raise handle_iam_error(e)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
@mcp.tool()
|
|
483
|
+
async def list_policies(
|
|
484
|
+
scope: str = Field(
|
|
485
|
+
description='Scope of policies to list: "All", "AWS", or "Local"', default='Local'
|
|
486
|
+
),
|
|
487
|
+
only_attached: bool = Field(
|
|
488
|
+
description='Only return policies that are attached to a user, group, or role',
|
|
489
|
+
default=False,
|
|
490
|
+
),
|
|
491
|
+
path_prefix: Optional[str] = Field(description='Path prefix to filter policies', default=None),
|
|
492
|
+
max_items: int = Field(description='Maximum number of policies to return', default=100),
|
|
493
|
+
) -> Dict[str, Any]:
|
|
494
|
+
"""List IAM policies in the account.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
scope: Scope of policies to list ("All", "AWS", or "Local")
|
|
498
|
+
only_attached: Only return policies that are attached
|
|
499
|
+
path_prefix: Optional path prefix to filter policies
|
|
500
|
+
max_items: Maximum number of policies to return
|
|
501
|
+
|
|
502
|
+
Returns:
|
|
503
|
+
Dictionary containing list of policies and metadata
|
|
504
|
+
"""
|
|
505
|
+
try:
|
|
506
|
+
iam = get_iam_client()
|
|
507
|
+
|
|
508
|
+
kwargs = {'Scope': scope, 'OnlyAttached': only_attached, 'MaxItems': max_items}
|
|
509
|
+
if path_prefix:
|
|
510
|
+
kwargs['PathPrefix'] = path_prefix
|
|
511
|
+
|
|
512
|
+
response = iam.list_policies(**kwargs)
|
|
513
|
+
|
|
514
|
+
policies = []
|
|
515
|
+
for policy in response.get('Policies', []):
|
|
516
|
+
policies.append(
|
|
517
|
+
{
|
|
518
|
+
'PolicyName': policy['PolicyName'],
|
|
519
|
+
'PolicyId': policy['PolicyId'],
|
|
520
|
+
'Arn': policy['Arn'],
|
|
521
|
+
'Path': policy['Path'],
|
|
522
|
+
'DefaultVersionId': policy['DefaultVersionId'],
|
|
523
|
+
'AttachmentCount': policy['AttachmentCount'],
|
|
524
|
+
'PermissionsBoundaryUsageCount': policy.get(
|
|
525
|
+
'PermissionsBoundaryUsageCount', 0
|
|
526
|
+
),
|
|
527
|
+
'IsAttachable': policy['IsAttachable'],
|
|
528
|
+
'Description': policy.get('Description'),
|
|
529
|
+
'CreateDate': policy['CreateDate'].isoformat(),
|
|
530
|
+
'UpdateDate': policy['UpdateDate'].isoformat(),
|
|
531
|
+
}
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
return {
|
|
535
|
+
'Policies': policies,
|
|
536
|
+
'IsTruncated': response.get('IsTruncated', False),
|
|
537
|
+
'Marker': response.get('Marker'),
|
|
538
|
+
'Count': len(policies),
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
except Exception as e:
|
|
542
|
+
raise handle_iam_error(e)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
@mcp.tool()
|
|
546
|
+
async def attach_user_policy(
|
|
547
|
+
user_name: str = Field(description='The name of the IAM user'),
|
|
548
|
+
policy_arn: str = Field(description='The ARN of the policy to attach'),
|
|
549
|
+
) -> Dict[str, Any]:
|
|
550
|
+
"""Attach a managed policy to an IAM user.
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
user_name: The name of the IAM user
|
|
554
|
+
policy_arn: The ARN of the policy to attach
|
|
555
|
+
|
|
556
|
+
Returns:
|
|
557
|
+
Dictionary containing attachment status
|
|
558
|
+
"""
|
|
559
|
+
try:
|
|
560
|
+
# Check if server is in read-only mode
|
|
561
|
+
if Context.is_readonly():
|
|
562
|
+
raise IamClientError('Cannot attach policy: server is running in read-only mode')
|
|
563
|
+
|
|
564
|
+
iam = get_iam_client()
|
|
565
|
+
|
|
566
|
+
iam.attach_user_policy(UserName=user_name, PolicyArn=policy_arn)
|
|
567
|
+
|
|
568
|
+
return {
|
|
569
|
+
'Message': f'Successfully attached policy {policy_arn} to user {user_name}',
|
|
570
|
+
'UserName': user_name,
|
|
571
|
+
'PolicyArn': policy_arn,
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
except Exception as e:
|
|
575
|
+
raise handle_iam_error(e)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
@mcp.tool()
|
|
579
|
+
async def detach_user_policy(
|
|
580
|
+
user_name: str = Field(description='The name of the IAM user'),
|
|
581
|
+
policy_arn: str = Field(description='The ARN of the policy to detach'),
|
|
582
|
+
) -> Dict[str, Any]:
|
|
583
|
+
"""Detach a managed policy from an IAM user.
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
user_name: The name of the IAM user
|
|
587
|
+
policy_arn: The ARN of the policy to detach
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
Dictionary containing detachment status
|
|
591
|
+
"""
|
|
592
|
+
try:
|
|
593
|
+
# Check if server is in read-only mode
|
|
594
|
+
if Context.is_readonly():
|
|
595
|
+
raise IamClientError('Cannot detach policy: server is running in read-only mode')
|
|
596
|
+
|
|
597
|
+
iam = get_iam_client()
|
|
598
|
+
|
|
599
|
+
iam.detach_user_policy(UserName=user_name, PolicyArn=policy_arn)
|
|
600
|
+
|
|
601
|
+
return {
|
|
602
|
+
'Message': f'Successfully detached policy {policy_arn} from user {user_name}',
|
|
603
|
+
'UserName': user_name,
|
|
604
|
+
'PolicyArn': policy_arn,
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
except Exception as e:
|
|
608
|
+
raise handle_iam_error(e)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
@mcp.tool()
|
|
612
|
+
async def create_access_key(
|
|
613
|
+
user_name: str = Field(description='The name of the IAM user'),
|
|
614
|
+
) -> Dict[str, Any]:
|
|
615
|
+
"""Create a new access key for an IAM user.
|
|
616
|
+
|
|
617
|
+
Args:
|
|
618
|
+
user_name: The name of the IAM user
|
|
619
|
+
|
|
620
|
+
Returns:
|
|
621
|
+
Dictionary containing the new access key details
|
|
622
|
+
"""
|
|
623
|
+
try:
|
|
624
|
+
# Check if server is in read-only mode
|
|
625
|
+
if Context.is_readonly():
|
|
626
|
+
raise IamClientError('Cannot create access key: server is running in read-only mode')
|
|
627
|
+
|
|
628
|
+
iam = get_iam_client()
|
|
629
|
+
|
|
630
|
+
response = iam.create_access_key(UserName=user_name)
|
|
631
|
+
access_key = response['AccessKey']
|
|
632
|
+
|
|
633
|
+
return {
|
|
634
|
+
'AccessKey': {
|
|
635
|
+
'AccessKeyId': access_key['AccessKeyId'],
|
|
636
|
+
'SecretAccessKey': access_key['SecretAccessKey'],
|
|
637
|
+
'Status': access_key['Status'],
|
|
638
|
+
'UserName': access_key['UserName'],
|
|
639
|
+
'CreateDate': access_key['CreateDate'].isoformat(),
|
|
640
|
+
},
|
|
641
|
+
'Message': f'Successfully created access key for user: {user_name}',
|
|
642
|
+
'Warning': 'Store the SecretAccessKey securely - it cannot be retrieved again!',
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
except Exception as e:
|
|
646
|
+
raise handle_iam_error(e)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
@mcp.tool()
|
|
650
|
+
async def delete_access_key(
|
|
651
|
+
user_name: str = Field(description='The name of the IAM user'),
|
|
652
|
+
access_key_id: str = Field(description='The access key ID to delete'),
|
|
653
|
+
) -> Dict[str, Any]:
|
|
654
|
+
"""Delete an access key for an IAM user.
|
|
655
|
+
|
|
656
|
+
Args:
|
|
657
|
+
user_name: The name of the IAM user
|
|
658
|
+
access_key_id: The access key ID to delete
|
|
659
|
+
|
|
660
|
+
Returns:
|
|
661
|
+
Dictionary containing deletion status
|
|
662
|
+
"""
|
|
663
|
+
try:
|
|
664
|
+
# Check if server is in read-only mode
|
|
665
|
+
if Context.is_readonly():
|
|
666
|
+
raise IamClientError('Cannot delete access key: server is running in read-only mode')
|
|
667
|
+
|
|
668
|
+
iam = get_iam_client()
|
|
669
|
+
|
|
670
|
+
iam.delete_access_key(UserName=user_name, AccessKeyId=access_key_id)
|
|
671
|
+
|
|
672
|
+
return {
|
|
673
|
+
'Message': f'Successfully deleted access key {access_key_id} for user {user_name}',
|
|
674
|
+
'UserName': user_name,
|
|
675
|
+
'AccessKeyId': access_key_id,
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
except Exception as e:
|
|
679
|
+
raise handle_iam_error(e)
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
@mcp.tool()
|
|
683
|
+
async def simulate_principal_policy(
|
|
684
|
+
policy_source_arn: str = Field(description='ARN of the user or role to simulate'),
|
|
685
|
+
action_names: List[str] = Field(description='List of actions to simulate'),
|
|
686
|
+
resource_arns: Optional[List[str]] = Field(
|
|
687
|
+
description='List of resource ARNs to test against', default=None
|
|
688
|
+
),
|
|
689
|
+
context_entries: Optional[Dict[str, str]] = Field(
|
|
690
|
+
description='Context entries for the simulation', default=None
|
|
691
|
+
),
|
|
692
|
+
) -> Dict[str, Any]:
|
|
693
|
+
"""Simulate IAM policy evaluation for a principal.
|
|
694
|
+
|
|
695
|
+
Args:
|
|
696
|
+
policy_source_arn: ARN of the user or role to simulate
|
|
697
|
+
action_names: List of actions to simulate
|
|
698
|
+
resource_arns: Optional list of resource ARNs to test against
|
|
699
|
+
context_entries: Optional context entries for the simulation
|
|
700
|
+
|
|
701
|
+
Returns:
|
|
702
|
+
Dictionary containing simulation results
|
|
703
|
+
"""
|
|
704
|
+
try:
|
|
705
|
+
iam = get_iam_client()
|
|
706
|
+
|
|
707
|
+
kwargs = {'PolicySourceArn': policy_source_arn, 'ActionNames': action_names}
|
|
708
|
+
|
|
709
|
+
if resource_arns:
|
|
710
|
+
kwargs['ResourceArns'] = resource_arns
|
|
711
|
+
if context_entries:
|
|
712
|
+
kwargs['ContextEntries'] = [
|
|
713
|
+
{'ContextKeyName': k, 'ContextKeyValues': [v]} for k, v in context_entries.items()
|
|
714
|
+
]
|
|
715
|
+
|
|
716
|
+
response = iam.simulate_principal_policy(**kwargs)
|
|
717
|
+
|
|
718
|
+
results = []
|
|
719
|
+
for result in response.get('EvaluationResults', []):
|
|
720
|
+
results.append(
|
|
721
|
+
{
|
|
722
|
+
'EvalActionName': result['EvalActionName'],
|
|
723
|
+
'EvalResourceName': result.get('EvalResourceName', '*'),
|
|
724
|
+
'EvalDecision': result['EvalDecision'],
|
|
725
|
+
'MatchedStatements': result.get('MatchedStatements', []),
|
|
726
|
+
'MissingContextValues': result.get('MissingContextValues', []),
|
|
727
|
+
}
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
return {
|
|
731
|
+
'EvaluationResults': results,
|
|
732
|
+
'IsTruncated': response.get('IsTruncated', False),
|
|
733
|
+
'Marker': response.get('Marker'),
|
|
734
|
+
'PolicySourceArn': policy_source_arn,
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
except Exception as e:
|
|
738
|
+
raise handle_iam_error(e)
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def main():
|
|
742
|
+
"""Run the MCP server with CLI argument support."""
|
|
743
|
+
parser = argparse.ArgumentParser(
|
|
744
|
+
description='An AWS Labs Model Context Protocol (MCP) server for comprehensive AWS IAM management'
|
|
745
|
+
)
|
|
746
|
+
parser.add_argument(
|
|
747
|
+
'--readonly',
|
|
748
|
+
action=argparse.BooleanOptionalAction,
|
|
749
|
+
help='Prevents the MCP server from performing mutating operations',
|
|
750
|
+
default=False,
|
|
751
|
+
)
|
|
752
|
+
parser.add_argument('--region', help='AWS region to use for operations')
|
|
753
|
+
|
|
754
|
+
args = parser.parse_args()
|
|
755
|
+
|
|
756
|
+
# Initialize context with configuration
|
|
757
|
+
Context.initialize(readonly=args.readonly, region=args.region)
|
|
758
|
+
|
|
759
|
+
if args.region:
|
|
760
|
+
logger.info(f'Using AWS region: {args.region}')
|
|
761
|
+
|
|
762
|
+
if args.readonly:
|
|
763
|
+
logger.info('Running in read-only mode - mutating operations will be disabled')
|
|
764
|
+
|
|
765
|
+
mcp.run()
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
if __name__ == '__main__':
|
|
769
|
+
main()
|