awslabs.iam-mcp-server 1.0.1__py3-none-any.whl → 1.0.2__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/iam_mcp_server/__init__.py +1 -1
- awslabs/iam_mcp_server/context.py +5 -0
- awslabs/iam_mcp_server/models.py +93 -0
- awslabs/iam_mcp_server/server.py +844 -17
- {awslabs_iam_mcp_server-1.0.1.dist-info → awslabs_iam_mcp_server-1.0.2.dist-info}/METADATA +212 -2
- awslabs_iam_mcp_server-1.0.2.dist-info/RECORD +13 -0
- awslabs_iam_mcp_server-1.0.1.dist-info/RECORD +0 -13
- {awslabs_iam_mcp_server-1.0.1.dist-info → awslabs_iam_mcp_server-1.0.2.dist-info}/WHEEL +0 -0
- {awslabs_iam_mcp_server-1.0.1.dist-info → awslabs_iam_mcp_server-1.0.2.dist-info}/entry_points.txt +0 -0
- {awslabs_iam_mcp_server-1.0.1.dist-info → awslabs_iam_mcp_server-1.0.2.dist-info}/licenses/LICENSE +0 -0
- {awslabs_iam_mcp_server-1.0.1.dist-info → awslabs_iam_mcp_server-1.0.2.dist-info}/licenses/NOTICE +0 -0
awslabs/iam_mcp_server/models.py
CHANGED
|
@@ -70,6 +70,16 @@ class IamPolicy(BaseModel):
|
|
|
70
70
|
update_date: str = Field(..., description='The date and time when the policy was last updated')
|
|
71
71
|
|
|
72
72
|
|
|
73
|
+
class IamGroup(BaseModel):
|
|
74
|
+
"""IAM Group model."""
|
|
75
|
+
|
|
76
|
+
group_name: str = Field(..., description='The name of the IAM group')
|
|
77
|
+
group_id: str = Field(..., description='The unique identifier for the group')
|
|
78
|
+
arn: str = Field(..., description='The Amazon Resource Name (ARN) of the group')
|
|
79
|
+
path: str = Field(..., description='The path to the group')
|
|
80
|
+
create_date: str = Field(..., description='The date and time when the group was created')
|
|
81
|
+
|
|
82
|
+
|
|
73
83
|
class AccessKey(BaseModel):
|
|
74
84
|
"""IAM Access Key model."""
|
|
75
85
|
|
|
@@ -195,3 +205,86 @@ class PolicySimulationResponse(BaseModel):
|
|
|
195
205
|
is_truncated: bool = Field(False, description='Whether the response is truncated')
|
|
196
206
|
marker: Optional[str] = Field(None, description='Marker for pagination')
|
|
197
207
|
policy_source_arn: str = Field(..., description='ARN of the principal that was simulated')
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class GroupDetailsResponse(BaseModel):
|
|
211
|
+
"""Response model for detailed group information."""
|
|
212
|
+
|
|
213
|
+
group: IamGroup = Field(..., description='Group details')
|
|
214
|
+
users: List[str] = Field(default_factory=list, description='List of user names in the group')
|
|
215
|
+
attached_policies: List[AttachedPolicy] = Field(
|
|
216
|
+
default_factory=list, description='List of attached managed policies'
|
|
217
|
+
)
|
|
218
|
+
inline_policies: List[str] = Field(
|
|
219
|
+
default_factory=list, description='List of inline policy names'
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class GroupsListResponse(BaseModel):
|
|
224
|
+
"""Response model for listing groups."""
|
|
225
|
+
|
|
226
|
+
groups: List[IamGroup] = Field(..., description='List of IAM groups')
|
|
227
|
+
is_truncated: bool = Field(False, description='Whether the response is truncated')
|
|
228
|
+
marker: Optional[str] = Field(None, description='Marker for pagination')
|
|
229
|
+
count: int = Field(..., description='Number of groups returned')
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class CreateGroupResponse(BaseModel):
|
|
233
|
+
"""Response model for creating a group."""
|
|
234
|
+
|
|
235
|
+
group: IamGroup = Field(..., description='Created group details')
|
|
236
|
+
message: str = Field(..., description='Success message')
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class GroupMembershipResponse(BaseModel):
|
|
240
|
+
"""Response model for group membership operations."""
|
|
241
|
+
|
|
242
|
+
message: str = Field(..., description='Operation result message')
|
|
243
|
+
group_name: str = Field(..., description='The name of the group')
|
|
244
|
+
user_name: str = Field(..., description='The name of the user')
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class GroupPolicyAttachmentResponse(BaseModel):
|
|
248
|
+
"""Response model for group policy attachment operations."""
|
|
249
|
+
|
|
250
|
+
message: str = Field(..., description='Operation result message')
|
|
251
|
+
group_name: str = Field(..., description='The name of the group')
|
|
252
|
+
policy_arn: str = Field(..., description='The ARN of the policy')
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class InlinePolicy(BaseModel):
|
|
256
|
+
"""Inline Policy model."""
|
|
257
|
+
|
|
258
|
+
policy_name: str = Field(..., description='The name of the inline policy')
|
|
259
|
+
policy_document: str = Field(..., description='The policy document in JSON format')
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class InlinePolicyResponse(BaseModel):
|
|
263
|
+
"""Response model for inline policy operations."""
|
|
264
|
+
|
|
265
|
+
policy_name: str = Field(..., description='The name of the policy')
|
|
266
|
+
policy_document: str = Field(..., description='The policy document in JSON format')
|
|
267
|
+
user_name: Optional[str] = Field(None, description='The name of the user (for user policies)')
|
|
268
|
+
role_name: Optional[str] = Field(None, description='The name of the role (for role policies)')
|
|
269
|
+
message: str = Field(..., description='Operation result message')
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class InlinePolicyListResponse(BaseModel):
|
|
273
|
+
"""Response model for listing inline policies."""
|
|
274
|
+
|
|
275
|
+
policy_names: List[str] = Field(..., description='List of inline policy names')
|
|
276
|
+
user_name: Optional[str] = Field(None, description='The name of the user (for user policies)')
|
|
277
|
+
role_name: Optional[str] = Field(None, description='The name of the role (for role policies)')
|
|
278
|
+
count: int = Field(..., description='Number of policies returned')
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class ManagedPolicyResponse(BaseModel):
|
|
282
|
+
"""Response model for managed policy document operations."""
|
|
283
|
+
|
|
284
|
+
policy_arn: str = Field(..., description='The ARN of the managed policy')
|
|
285
|
+
policy_name: str = Field(..., description='The name of the policy')
|
|
286
|
+
version_id: str = Field(..., description='The version ID of the policy')
|
|
287
|
+
policy_document: str = Field(..., description='The policy document in JSON format')
|
|
288
|
+
is_default_version: bool = Field(..., description='Whether this is the default version')
|
|
289
|
+
create_date: str = Field(..., description='The date and time when this version was created')
|
|
290
|
+
message: str = Field(..., description='Operation result message')
|
awslabs/iam_mcp_server/server.py
CHANGED
|
@@ -22,8 +22,17 @@ from awslabs.iam_mcp_server.errors import IamClientError, IamValidationError, ha
|
|
|
22
22
|
from awslabs.iam_mcp_server.models import (
|
|
23
23
|
AccessKey,
|
|
24
24
|
AttachedPolicy,
|
|
25
|
+
CreateGroupResponse,
|
|
25
26
|
CreateUserResponse,
|
|
27
|
+
GroupDetailsResponse,
|
|
28
|
+
GroupMembershipResponse,
|
|
29
|
+
GroupPolicyAttachmentResponse,
|
|
30
|
+
GroupsListResponse,
|
|
31
|
+
IamGroup,
|
|
26
32
|
IamUser,
|
|
33
|
+
InlinePolicyListResponse,
|
|
34
|
+
InlinePolicyResponse,
|
|
35
|
+
ManagedPolicyResponse,
|
|
27
36
|
UserDetailsResponse,
|
|
28
37
|
UsersListResponse,
|
|
29
38
|
)
|
|
@@ -45,10 +54,17 @@ mcp = FastMCP(
|
|
|
45
54
|
1. **User Management**: Create, list, update, and delete IAM users
|
|
46
55
|
2. **Role Management**: Create, list, update, and delete IAM roles
|
|
47
56
|
3. **Policy Management**: Create, list, update, and delete IAM policies
|
|
48
|
-
4. **
|
|
49
|
-
5. **
|
|
50
|
-
6. **
|
|
51
|
-
7. **
|
|
57
|
+
4. **Inline Policy Management**: Full CRUD operations for user and role inline policies
|
|
58
|
+
5. **Group Management**: Create, list, update, and delete IAM groups
|
|
59
|
+
6. **Permission Management**: Attach/detach policies to users, roles, and groups
|
|
60
|
+
7. **Access Key Management**: Create, list, and delete access keys for users
|
|
61
|
+
8. **Security Analysis**: Analyze permissions, find unused resources, and security recommendations
|
|
62
|
+
|
|
63
|
+
## Inline Policy Management:
|
|
64
|
+
- **User Inline Policies**: Create, retrieve, update, delete, and list inline policies for users
|
|
65
|
+
- **Role Inline Policies**: Create, retrieve, update, delete, and list inline policies for roles
|
|
66
|
+
- **Policy Validation**: Automatic JSON validation for policy documents
|
|
67
|
+
- **Security Best Practices**: Built-in guidance for policy creation and management
|
|
52
68
|
|
|
53
69
|
## Security Best Practices:
|
|
54
70
|
- Always follow the principle of least privilege
|
|
@@ -56,6 +72,8 @@ mcp = FastMCP(
|
|
|
56
72
|
- Use roles instead of users for applications
|
|
57
73
|
- Enable MFA where possible
|
|
58
74
|
- Review and audit permissions regularly
|
|
75
|
+
- Prefer managed policies over inline policies for reusable permissions
|
|
76
|
+
- Test policies using simulate_principal_policy before applying
|
|
59
77
|
|
|
60
78
|
## Usage Requirements:
|
|
61
79
|
- Requires valid AWS credentials with appropriate IAM permissions
|
|
@@ -180,7 +198,7 @@ async def get_user(
|
|
|
180
198
|
inline_policies = inline_policies_response.get('PolicyNames', [])
|
|
181
199
|
|
|
182
200
|
# Get groups
|
|
183
|
-
groups_response = iam.
|
|
201
|
+
groups_response = iam.list_groups_for_user(UserName=user_name)
|
|
184
202
|
groups = [group['GroupName'] for group in groups_response.get('Groups', [])]
|
|
185
203
|
|
|
186
204
|
# Get access keys
|
|
@@ -321,7 +339,7 @@ async def delete_user(
|
|
|
321
339
|
|
|
322
340
|
if force:
|
|
323
341
|
# Remove from all groups
|
|
324
|
-
groups = iam.
|
|
342
|
+
groups = iam.list_groups_for_user(UserName=user_name)
|
|
325
343
|
for group in groups.get('Groups', []):
|
|
326
344
|
iam.remove_user_from_group(GroupName=group['GroupName'], UserName=user_name)
|
|
327
345
|
|
|
@@ -542,6 +560,63 @@ async def list_policies(
|
|
|
542
560
|
raise handle_iam_error(e)
|
|
543
561
|
|
|
544
562
|
|
|
563
|
+
@mcp.tool()
|
|
564
|
+
async def get_managed_policy_document(
|
|
565
|
+
policy_arn: str = Field(description='The ARN of the managed policy'),
|
|
566
|
+
version_id: Optional[str] = Field(
|
|
567
|
+
description='The version ID of the policy (defaults to current version)', default=None
|
|
568
|
+
),
|
|
569
|
+
) -> ManagedPolicyResponse:
|
|
570
|
+
"""Retrieve the policy document for a managed policy.
|
|
571
|
+
|
|
572
|
+
This tool retrieves the policy document for a specific managed policy version.
|
|
573
|
+
Use this to examine the actual permissions and wildcards in managed policies.
|
|
574
|
+
|
|
575
|
+
Args:
|
|
576
|
+
policy_arn: The ARN of the managed policy
|
|
577
|
+
version_id: Optional version ID (defaults to current version)
|
|
578
|
+
|
|
579
|
+
Returns:
|
|
580
|
+
ManagedPolicyResponse containing the policy document and details
|
|
581
|
+
"""
|
|
582
|
+
try:
|
|
583
|
+
logger.info(f'Getting managed policy document for: {policy_arn}')
|
|
584
|
+
|
|
585
|
+
if not policy_arn:
|
|
586
|
+
raise IamValidationError('Policy ARN is required')
|
|
587
|
+
|
|
588
|
+
iam = get_iam_client()
|
|
589
|
+
|
|
590
|
+
# Build parameters for the API call
|
|
591
|
+
kwargs = {'PolicyArn': policy_arn}
|
|
592
|
+
if version_id:
|
|
593
|
+
kwargs['VersionId'] = version_id
|
|
594
|
+
|
|
595
|
+
response = iam.get_policy_version(**kwargs)
|
|
596
|
+
policy_version = response['PolicyVersion']
|
|
597
|
+
|
|
598
|
+
# Extract policy name from ARN
|
|
599
|
+
policy_name = policy_arn.split('/')[-1]
|
|
600
|
+
|
|
601
|
+
result = ManagedPolicyResponse(
|
|
602
|
+
policy_arn=policy_arn,
|
|
603
|
+
policy_name=policy_name,
|
|
604
|
+
version_id=policy_version['VersionId'],
|
|
605
|
+
policy_document=json.dumps(policy_version['Document'], indent=2),
|
|
606
|
+
is_default_version=policy_version['IsDefaultVersion'],
|
|
607
|
+
create_date=policy_version['CreateDate'].isoformat(),
|
|
608
|
+
message=f'Successfully retrieved managed policy document for {policy_name}',
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
logger.info(f'Successfully retrieved managed policy document for: {policy_arn}')
|
|
612
|
+
return result
|
|
613
|
+
|
|
614
|
+
except Exception as e:
|
|
615
|
+
error = handle_iam_error(e)
|
|
616
|
+
logger.error(f'Error getting managed policy document: {error}')
|
|
617
|
+
raise error
|
|
618
|
+
|
|
619
|
+
|
|
545
620
|
@mcp.tool()
|
|
546
621
|
async def attach_user_policy(
|
|
547
622
|
user_name: str = Field(description='The name of the IAM user'),
|
|
@@ -738,6 +813,761 @@ async def simulate_principal_policy(
|
|
|
738
813
|
raise handle_iam_error(e)
|
|
739
814
|
|
|
740
815
|
|
|
816
|
+
# Group Management Tools
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
@mcp.tool()
|
|
820
|
+
async def list_groups(
|
|
821
|
+
path_prefix: Optional[str] = Field(
|
|
822
|
+
None, description='Path prefix to filter groups (e.g., "/division_abc/")'
|
|
823
|
+
),
|
|
824
|
+
max_items: int = Field(100, description='Maximum number of groups to return'),
|
|
825
|
+
) -> GroupsListResponse:
|
|
826
|
+
"""List IAM groups in the account.
|
|
827
|
+
|
|
828
|
+
This tool retrieves a list of IAM groups from your AWS account with optional filtering.
|
|
829
|
+
Use this to get an overview of all groups or find specific groups by path prefix.
|
|
830
|
+
|
|
831
|
+
## Usage Tips:
|
|
832
|
+
- Use path_prefix to filter groups by organizational structure
|
|
833
|
+
- Adjust max_items to control response size for large accounts
|
|
834
|
+
- Results may be paginated for accounts with many groups
|
|
835
|
+
|
|
836
|
+
Args:
|
|
837
|
+
path_prefix: Optional path prefix to filter groups
|
|
838
|
+
max_items: Maximum number of groups to return
|
|
839
|
+
|
|
840
|
+
Returns:
|
|
841
|
+
GroupsListResponse containing list of groups and metadata
|
|
842
|
+
"""
|
|
843
|
+
if Context.is_readonly():
|
|
844
|
+
# List operations are allowed in read-only mode
|
|
845
|
+
pass
|
|
846
|
+
|
|
847
|
+
try:
|
|
848
|
+
iam = get_iam_client()
|
|
849
|
+
|
|
850
|
+
kwargs: Dict[str, Union[int, str]] = {'MaxItems': max_items}
|
|
851
|
+
if path_prefix:
|
|
852
|
+
kwargs['PathPrefix'] = path_prefix
|
|
853
|
+
|
|
854
|
+
response = iam.list_groups(**kwargs)
|
|
855
|
+
|
|
856
|
+
groups = []
|
|
857
|
+
for group_data in response.get('Groups', []):
|
|
858
|
+
group = IamGroup(
|
|
859
|
+
group_name=group_data['GroupName'],
|
|
860
|
+
group_id=group_data['GroupId'],
|
|
861
|
+
arn=group_data['Arn'],
|
|
862
|
+
path=group_data['Path'],
|
|
863
|
+
create_date=group_data['CreateDate'].isoformat(),
|
|
864
|
+
)
|
|
865
|
+
groups.append(group)
|
|
866
|
+
|
|
867
|
+
return GroupsListResponse(
|
|
868
|
+
groups=groups,
|
|
869
|
+
is_truncated=response.get('IsTruncated', False),
|
|
870
|
+
marker=response.get('Marker'),
|
|
871
|
+
count=len(groups),
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
except Exception as e:
|
|
875
|
+
raise handle_iam_error(e)
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
@mcp.tool()
|
|
879
|
+
async def get_group(
|
|
880
|
+
group_name: str = Field(description='The name of the IAM group to retrieve'),
|
|
881
|
+
) -> GroupDetailsResponse:
|
|
882
|
+
"""Get detailed information about a specific IAM group.
|
|
883
|
+
|
|
884
|
+
This tool retrieves comprehensive information about an IAM group including
|
|
885
|
+
group members, attached policies, and inline policies. Use this to get
|
|
886
|
+
a complete picture of a group's configuration and membership.
|
|
887
|
+
|
|
888
|
+
## Usage Tips:
|
|
889
|
+
- Use this after list_groups to get detailed information about specific groups
|
|
890
|
+
- Review attached policies to understand group permissions
|
|
891
|
+
- Check group members to see who has these permissions
|
|
892
|
+
|
|
893
|
+
Args:
|
|
894
|
+
group_name: The name of the IAM group
|
|
895
|
+
|
|
896
|
+
Returns:
|
|
897
|
+
GroupDetailsResponse containing comprehensive group information
|
|
898
|
+
"""
|
|
899
|
+
if Context.is_readonly():
|
|
900
|
+
# Get operations are allowed in read-only mode
|
|
901
|
+
pass
|
|
902
|
+
|
|
903
|
+
try:
|
|
904
|
+
iam = get_iam_client()
|
|
905
|
+
|
|
906
|
+
# Get group details and members
|
|
907
|
+
group_response = iam.get_group(GroupName=group_name)
|
|
908
|
+
group_data = group_response['Group']
|
|
909
|
+
|
|
910
|
+
group = IamGroup(
|
|
911
|
+
group_name=group_data['GroupName'],
|
|
912
|
+
group_id=group_data['GroupId'],
|
|
913
|
+
arn=group_data['Arn'],
|
|
914
|
+
path=group_data['Path'],
|
|
915
|
+
create_date=group_data['CreateDate'].isoformat(),
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
# Get group members
|
|
919
|
+
users = [user['UserName'] for user in group_response.get('Users', [])]
|
|
920
|
+
|
|
921
|
+
# Get attached managed policies
|
|
922
|
+
attached_policies_response = iam.list_attached_group_policies(GroupName=group_name)
|
|
923
|
+
attached_policies = [
|
|
924
|
+
AttachedPolicy(policy_name=policy['PolicyName'], policy_arn=policy['PolicyArn'])
|
|
925
|
+
for policy in attached_policies_response.get('AttachedPolicies', [])
|
|
926
|
+
]
|
|
927
|
+
|
|
928
|
+
# Get inline policies
|
|
929
|
+
inline_policies_response = iam.list_group_policies(GroupName=group_name)
|
|
930
|
+
inline_policies = inline_policies_response.get('PolicyNames', [])
|
|
931
|
+
|
|
932
|
+
return GroupDetailsResponse(
|
|
933
|
+
group=group,
|
|
934
|
+
users=users,
|
|
935
|
+
attached_policies=attached_policies,
|
|
936
|
+
inline_policies=inline_policies,
|
|
937
|
+
)
|
|
938
|
+
|
|
939
|
+
except Exception as e:
|
|
940
|
+
raise handle_iam_error(e)
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
@mcp.tool()
|
|
944
|
+
async def create_group(
|
|
945
|
+
group_name: str = Field(description='The name of the new IAM group'),
|
|
946
|
+
path: str = Field('/', description='The path for the group'),
|
|
947
|
+
) -> CreateGroupResponse:
|
|
948
|
+
"""Create a new IAM group.
|
|
949
|
+
|
|
950
|
+
This tool creates a new IAM group in your AWS account. The group will be created
|
|
951
|
+
without any permissions by default - you'll need to attach policies separately.
|
|
952
|
+
|
|
953
|
+
## Security Best Practices:
|
|
954
|
+
- Use descriptive group names that indicate the group's purpose
|
|
955
|
+
- Set appropriate paths for organizational structure
|
|
956
|
+
- Follow the principle of least privilege when assigning permissions later
|
|
957
|
+
|
|
958
|
+
Args:
|
|
959
|
+
group_name: The name of the new IAM group
|
|
960
|
+
path: The path for the group (default: '/')
|
|
961
|
+
|
|
962
|
+
Returns:
|
|
963
|
+
CreateGroupResponse containing the created group details
|
|
964
|
+
"""
|
|
965
|
+
if Context.is_readonly():
|
|
966
|
+
raise IamValidationError('Cannot create group in read-only mode')
|
|
967
|
+
|
|
968
|
+
try:
|
|
969
|
+
iam = get_iam_client()
|
|
970
|
+
|
|
971
|
+
response = iam.create_group(GroupName=group_name, Path=path)
|
|
972
|
+
|
|
973
|
+
group_data = response['Group']
|
|
974
|
+
group = IamGroup(
|
|
975
|
+
group_name=group_data['GroupName'],
|
|
976
|
+
group_id=group_data['GroupId'],
|
|
977
|
+
arn=group_data['Arn'],
|
|
978
|
+
path=group_data['Path'],
|
|
979
|
+
create_date=group_data['CreateDate'].isoformat(),
|
|
980
|
+
)
|
|
981
|
+
|
|
982
|
+
return CreateGroupResponse(
|
|
983
|
+
group=group, message=f'Successfully created IAM group: {group_name}'
|
|
984
|
+
)
|
|
985
|
+
|
|
986
|
+
except Exception as e:
|
|
987
|
+
raise handle_iam_error(e)
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
@mcp.tool()
|
|
991
|
+
async def delete_group(
|
|
992
|
+
group_name: str = Field(description='The name of the IAM group to delete'),
|
|
993
|
+
force: bool = Field(
|
|
994
|
+
False, description='Force delete by removing all members and policies first'
|
|
995
|
+
),
|
|
996
|
+
) -> Dict[str, str]:
|
|
997
|
+
"""Delete an IAM group.
|
|
998
|
+
|
|
999
|
+
Args:
|
|
1000
|
+
group_name: The name of the IAM group to delete
|
|
1001
|
+
force: If True, removes all members and attached policies first
|
|
1002
|
+
|
|
1003
|
+
Returns:
|
|
1004
|
+
Dictionary containing deletion status
|
|
1005
|
+
"""
|
|
1006
|
+
if Context.is_readonly():
|
|
1007
|
+
raise IamValidationError('Cannot delete group in read-only mode')
|
|
1008
|
+
|
|
1009
|
+
try:
|
|
1010
|
+
iam = get_iam_client()
|
|
1011
|
+
|
|
1012
|
+
if force:
|
|
1013
|
+
# Remove all users from the group
|
|
1014
|
+
group_response = iam.get_group(GroupName=group_name)
|
|
1015
|
+
for user in group_response.get('Users', []):
|
|
1016
|
+
iam.remove_user_from_group(GroupName=group_name, UserName=user['UserName'])
|
|
1017
|
+
|
|
1018
|
+
# Detach all managed policies
|
|
1019
|
+
attached_policies = iam.list_attached_group_policies(GroupName=group_name)
|
|
1020
|
+
for policy in attached_policies.get('AttachedPolicies', []):
|
|
1021
|
+
iam.detach_group_policy(GroupName=group_name, PolicyArn=policy['PolicyArn'])
|
|
1022
|
+
|
|
1023
|
+
# Delete all inline policies
|
|
1024
|
+
inline_policies = iam.list_group_policies(GroupName=group_name)
|
|
1025
|
+
for policy_name in inline_policies.get('PolicyNames', []):
|
|
1026
|
+
iam.delete_group_policy(GroupName=group_name, PolicyName=policy_name)
|
|
1027
|
+
|
|
1028
|
+
# Delete the group
|
|
1029
|
+
iam.delete_group(GroupName=group_name)
|
|
1030
|
+
|
|
1031
|
+
return {'message': f'Successfully deleted IAM group: {group_name}'}
|
|
1032
|
+
|
|
1033
|
+
except Exception as e:
|
|
1034
|
+
raise handle_iam_error(e)
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
@mcp.tool()
|
|
1038
|
+
async def add_user_to_group(
|
|
1039
|
+
group_name: str = Field(description='The name of the IAM group'),
|
|
1040
|
+
user_name: str = Field(description='The name of the IAM user'),
|
|
1041
|
+
) -> GroupMembershipResponse:
|
|
1042
|
+
"""Add a user to an IAM group.
|
|
1043
|
+
|
|
1044
|
+
Args:
|
|
1045
|
+
group_name: The name of the IAM group
|
|
1046
|
+
user_name: The name of the IAM user
|
|
1047
|
+
|
|
1048
|
+
Returns:
|
|
1049
|
+
GroupMembershipResponse containing operation status
|
|
1050
|
+
"""
|
|
1051
|
+
if Context.is_readonly():
|
|
1052
|
+
raise IamValidationError('Cannot add user to group in read-only mode')
|
|
1053
|
+
|
|
1054
|
+
try:
|
|
1055
|
+
iam = get_iam_client()
|
|
1056
|
+
iam.add_user_to_group(GroupName=group_name, UserName=user_name)
|
|
1057
|
+
|
|
1058
|
+
return GroupMembershipResponse(
|
|
1059
|
+
message=f'Successfully added user {user_name} to group {group_name}',
|
|
1060
|
+
group_name=group_name,
|
|
1061
|
+
user_name=user_name,
|
|
1062
|
+
)
|
|
1063
|
+
|
|
1064
|
+
except Exception as e:
|
|
1065
|
+
raise handle_iam_error(e)
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
@mcp.tool()
|
|
1069
|
+
async def remove_user_from_group(
|
|
1070
|
+
group_name: str = Field(description='The name of the IAM group'),
|
|
1071
|
+
user_name: str = Field(description='The name of the IAM user'),
|
|
1072
|
+
) -> GroupMembershipResponse:
|
|
1073
|
+
"""Remove a user from an IAM group.
|
|
1074
|
+
|
|
1075
|
+
Args:
|
|
1076
|
+
group_name: The name of the IAM group
|
|
1077
|
+
user_name: The name of the IAM user
|
|
1078
|
+
|
|
1079
|
+
Returns:
|
|
1080
|
+
GroupMembershipResponse containing operation status
|
|
1081
|
+
"""
|
|
1082
|
+
if Context.is_readonly():
|
|
1083
|
+
raise IamValidationError('Cannot remove user from group in read-only mode')
|
|
1084
|
+
|
|
1085
|
+
try:
|
|
1086
|
+
iam = get_iam_client()
|
|
1087
|
+
iam.remove_user_from_group(GroupName=group_name, UserName=user_name)
|
|
1088
|
+
|
|
1089
|
+
return GroupMembershipResponse(
|
|
1090
|
+
message=f'Successfully removed user {user_name} from group {group_name}',
|
|
1091
|
+
group_name=group_name,
|
|
1092
|
+
user_name=user_name,
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
except Exception as e:
|
|
1096
|
+
raise handle_iam_error(e)
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
@mcp.tool()
|
|
1100
|
+
async def attach_group_policy(
|
|
1101
|
+
group_name: str = Field(description='The name of the IAM group'),
|
|
1102
|
+
policy_arn: str = Field(description='The ARN of the policy to attach'),
|
|
1103
|
+
) -> GroupPolicyAttachmentResponse:
|
|
1104
|
+
"""Attach a managed policy to an IAM group.
|
|
1105
|
+
|
|
1106
|
+
Args:
|
|
1107
|
+
group_name: The name of the IAM group
|
|
1108
|
+
policy_arn: The ARN of the policy to attach
|
|
1109
|
+
|
|
1110
|
+
Returns:
|
|
1111
|
+
GroupPolicyAttachmentResponse containing operation status
|
|
1112
|
+
"""
|
|
1113
|
+
if Context.is_readonly():
|
|
1114
|
+
raise IamValidationError('Cannot attach policy to group in read-only mode')
|
|
1115
|
+
|
|
1116
|
+
try:
|
|
1117
|
+
iam = get_iam_client()
|
|
1118
|
+
iam.attach_group_policy(GroupName=group_name, PolicyArn=policy_arn)
|
|
1119
|
+
|
|
1120
|
+
return GroupPolicyAttachmentResponse(
|
|
1121
|
+
message=f'Successfully attached policy {policy_arn} to group {group_name}',
|
|
1122
|
+
group_name=group_name,
|
|
1123
|
+
policy_arn=policy_arn,
|
|
1124
|
+
)
|
|
1125
|
+
|
|
1126
|
+
except Exception as e:
|
|
1127
|
+
raise handle_iam_error(e)
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
@mcp.tool()
|
|
1131
|
+
async def detach_group_policy(
|
|
1132
|
+
group_name: str = Field(description='The name of the IAM group'),
|
|
1133
|
+
policy_arn: str = Field(description='The ARN of the policy to detach'),
|
|
1134
|
+
) -> GroupPolicyAttachmentResponse:
|
|
1135
|
+
"""Detach a managed policy from an IAM group.
|
|
1136
|
+
|
|
1137
|
+
Args:
|
|
1138
|
+
group_name: The name of the IAM group
|
|
1139
|
+
policy_arn: The ARN of the policy to detach
|
|
1140
|
+
|
|
1141
|
+
Returns:
|
|
1142
|
+
GroupPolicyAttachmentResponse containing operation status
|
|
1143
|
+
"""
|
|
1144
|
+
if Context.is_readonly():
|
|
1145
|
+
raise IamValidationError('Cannot detach policy from group in read-only mode')
|
|
1146
|
+
|
|
1147
|
+
try:
|
|
1148
|
+
iam = get_iam_client()
|
|
1149
|
+
iam.detach_group_policy(GroupName=group_name, PolicyArn=policy_arn)
|
|
1150
|
+
|
|
1151
|
+
return GroupPolicyAttachmentResponse(
|
|
1152
|
+
message=f'Successfully detached policy {policy_arn} from group {group_name}',
|
|
1153
|
+
group_name=group_name,
|
|
1154
|
+
policy_arn=policy_arn,
|
|
1155
|
+
)
|
|
1156
|
+
|
|
1157
|
+
except Exception as e:
|
|
1158
|
+
raise handle_iam_error(e)
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
# Inline Policy Management Tools
|
|
1162
|
+
|
|
1163
|
+
|
|
1164
|
+
@mcp.tool()
|
|
1165
|
+
async def put_user_policy(
|
|
1166
|
+
user_name: str = Field(description='The name of the IAM user'),
|
|
1167
|
+
policy_name: str = Field(description='The name of the inline policy'),
|
|
1168
|
+
policy_document: Union[str, dict] = Field(
|
|
1169
|
+
description='The policy document in JSON format (string or dict)'
|
|
1170
|
+
),
|
|
1171
|
+
) -> InlinePolicyResponse:
|
|
1172
|
+
"""Create or update an inline policy for an IAM user.
|
|
1173
|
+
|
|
1174
|
+
This tool creates a new inline policy or updates an existing one for the specified user.
|
|
1175
|
+
Inline policies are directly embedded in a single user, role, or group and have a one-to-one
|
|
1176
|
+
relationship with the identity.
|
|
1177
|
+
|
|
1178
|
+
## Security Best Practices:
|
|
1179
|
+
- Follow the principle of least privilege when creating policies
|
|
1180
|
+
- Use managed policies for common permissions that can be reused
|
|
1181
|
+
- Regularly review and audit inline policies
|
|
1182
|
+
- Test policies using simulate_principal_policy before applying
|
|
1183
|
+
|
|
1184
|
+
Args:
|
|
1185
|
+
user_name: The name of the IAM user
|
|
1186
|
+
policy_name: The name of the inline policy
|
|
1187
|
+
policy_document: The policy document in JSON format
|
|
1188
|
+
|
|
1189
|
+
Returns:
|
|
1190
|
+
InlinePolicyResponse containing the policy details and operation status
|
|
1191
|
+
"""
|
|
1192
|
+
try:
|
|
1193
|
+
logger.info(f'Creating/updating inline policy {policy_name} for user: {user_name}')
|
|
1194
|
+
|
|
1195
|
+
# Check if server is in read-only mode
|
|
1196
|
+
if Context.is_readonly():
|
|
1197
|
+
raise IamClientError(
|
|
1198
|
+
'Cannot create/update inline policy: server is running in read-only mode'
|
|
1199
|
+
)
|
|
1200
|
+
|
|
1201
|
+
if not user_name or not policy_name:
|
|
1202
|
+
raise IamValidationError('User name and policy name are required')
|
|
1203
|
+
|
|
1204
|
+
iam = get_iam_client()
|
|
1205
|
+
|
|
1206
|
+
# Handle both string and dict types
|
|
1207
|
+
if isinstance(policy_document, dict):
|
|
1208
|
+
policy_doc = json.dumps(policy_document)
|
|
1209
|
+
else:
|
|
1210
|
+
policy_doc = policy_document
|
|
1211
|
+
# Validate JSON
|
|
1212
|
+
try:
|
|
1213
|
+
json.loads(policy_doc)
|
|
1214
|
+
except json.JSONDecodeError:
|
|
1215
|
+
raise IamValidationError('Invalid JSON in policy_document')
|
|
1216
|
+
|
|
1217
|
+
iam.put_user_policy(UserName=user_name, PolicyName=policy_name, PolicyDocument=policy_doc)
|
|
1218
|
+
|
|
1219
|
+
result = InlinePolicyResponse(
|
|
1220
|
+
policy_name=policy_name,
|
|
1221
|
+
policy_document=policy_doc,
|
|
1222
|
+
user_name=user_name,
|
|
1223
|
+
role_name=None,
|
|
1224
|
+
message=f'Successfully created/updated inline policy {policy_name} for user {user_name}',
|
|
1225
|
+
)
|
|
1226
|
+
|
|
1227
|
+
logger.info(
|
|
1228
|
+
f'Successfully created/updated inline policy {policy_name} for user: {user_name}'
|
|
1229
|
+
)
|
|
1230
|
+
return result
|
|
1231
|
+
|
|
1232
|
+
except Exception as e:
|
|
1233
|
+
error = handle_iam_error(e)
|
|
1234
|
+
logger.error(f'Error creating/updating inline policy: {error}')
|
|
1235
|
+
raise error
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
@mcp.tool()
|
|
1239
|
+
async def get_user_policy(
|
|
1240
|
+
user_name: str = Field(description='The name of the IAM user'),
|
|
1241
|
+
policy_name: str = Field(description='The name of the inline policy'),
|
|
1242
|
+
) -> InlinePolicyResponse:
|
|
1243
|
+
"""Retrieve an inline policy for an IAM user.
|
|
1244
|
+
|
|
1245
|
+
This tool retrieves the policy document for a specific inline policy attached to a user.
|
|
1246
|
+
|
|
1247
|
+
Args:
|
|
1248
|
+
user_name: The name of the IAM user
|
|
1249
|
+
policy_name: The name of the inline policy
|
|
1250
|
+
|
|
1251
|
+
Returns:
|
|
1252
|
+
InlinePolicyResponse containing the policy document and details
|
|
1253
|
+
"""
|
|
1254
|
+
try:
|
|
1255
|
+
logger.info(f'Getting inline policy {policy_name} for user: {user_name}')
|
|
1256
|
+
|
|
1257
|
+
if not user_name or not policy_name:
|
|
1258
|
+
raise IamValidationError('User name and policy name are required')
|
|
1259
|
+
|
|
1260
|
+
iam = get_iam_client()
|
|
1261
|
+
|
|
1262
|
+
response = iam.get_user_policy(UserName=user_name, PolicyName=policy_name)
|
|
1263
|
+
|
|
1264
|
+
result = InlinePolicyResponse(
|
|
1265
|
+
policy_name=response['PolicyName'],
|
|
1266
|
+
policy_document=response['PolicyDocument'],
|
|
1267
|
+
user_name=response['UserName'],
|
|
1268
|
+
role_name=None,
|
|
1269
|
+
message=f'Successfully retrieved inline policy {policy_name} for user {user_name}',
|
|
1270
|
+
)
|
|
1271
|
+
|
|
1272
|
+
logger.info(f'Successfully retrieved inline policy {policy_name} for user: {user_name}')
|
|
1273
|
+
return result
|
|
1274
|
+
|
|
1275
|
+
except Exception as e:
|
|
1276
|
+
error = handle_iam_error(e)
|
|
1277
|
+
logger.error(f'Error getting inline policy: {error}')
|
|
1278
|
+
raise error
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
@mcp.tool()
|
|
1282
|
+
async def delete_user_policy(
|
|
1283
|
+
user_name: str = Field(description='The name of the IAM user'),
|
|
1284
|
+
policy_name: str = Field(description='The name of the inline policy to delete'),
|
|
1285
|
+
) -> Dict[str, Any]:
|
|
1286
|
+
"""Delete an inline policy from an IAM user.
|
|
1287
|
+
|
|
1288
|
+
This tool removes an inline policy from the specified user. The policy document
|
|
1289
|
+
will be permanently deleted and cannot be recovered.
|
|
1290
|
+
|
|
1291
|
+
Args:
|
|
1292
|
+
user_name: The name of the IAM user
|
|
1293
|
+
policy_name: The name of the inline policy to delete
|
|
1294
|
+
|
|
1295
|
+
Returns:
|
|
1296
|
+
Dictionary containing deletion status
|
|
1297
|
+
"""
|
|
1298
|
+
try:
|
|
1299
|
+
logger.info(f'Deleting inline policy {policy_name} from user: {user_name}')
|
|
1300
|
+
|
|
1301
|
+
# Check if server is in read-only mode
|
|
1302
|
+
if Context.is_readonly():
|
|
1303
|
+
raise IamClientError(
|
|
1304
|
+
'Cannot delete inline policy: server is running in read-only mode'
|
|
1305
|
+
)
|
|
1306
|
+
|
|
1307
|
+
if not user_name or not policy_name:
|
|
1308
|
+
raise IamValidationError('User name and policy name are required')
|
|
1309
|
+
|
|
1310
|
+
iam = get_iam_client()
|
|
1311
|
+
|
|
1312
|
+
iam.delete_user_policy(UserName=user_name, PolicyName=policy_name)
|
|
1313
|
+
|
|
1314
|
+
result = {
|
|
1315
|
+
'message': f'Successfully deleted inline policy {policy_name} from user {user_name}',
|
|
1316
|
+
'user_name': user_name,
|
|
1317
|
+
'policy_name': policy_name,
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
logger.info(f'Successfully deleted inline policy {policy_name} from user: {user_name}')
|
|
1321
|
+
return result
|
|
1322
|
+
|
|
1323
|
+
except Exception as e:
|
|
1324
|
+
error = handle_iam_error(e)
|
|
1325
|
+
logger.error(f'Error deleting inline policy: {error}')
|
|
1326
|
+
raise error
|
|
1327
|
+
|
|
1328
|
+
|
|
1329
|
+
# Role Inline Policy Management Tools
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
@mcp.tool()
|
|
1333
|
+
async def put_role_policy(
|
|
1334
|
+
role_name: str = Field(description='The name of the IAM role'),
|
|
1335
|
+
policy_name: str = Field(description='The name of the inline policy'),
|
|
1336
|
+
policy_document: Union[str, dict] = Field(
|
|
1337
|
+
description='The policy document in JSON format (string or dict)'
|
|
1338
|
+
),
|
|
1339
|
+
) -> InlinePolicyResponse:
|
|
1340
|
+
"""Create or update an inline policy for an IAM role.
|
|
1341
|
+
|
|
1342
|
+
This tool creates a new inline policy or updates an existing one for the specified role.
|
|
1343
|
+
Inline policies are directly embedded in a single user, role, or group and have a one-to-one
|
|
1344
|
+
relationship with the identity.
|
|
1345
|
+
|
|
1346
|
+
Args:
|
|
1347
|
+
role_name: The name of the IAM role
|
|
1348
|
+
policy_name: The name of the inline policy
|
|
1349
|
+
policy_document: The policy document in JSON format
|
|
1350
|
+
|
|
1351
|
+
Returns:
|
|
1352
|
+
InlinePolicyResponse containing the policy details and operation status
|
|
1353
|
+
"""
|
|
1354
|
+
try:
|
|
1355
|
+
logger.info(f'Creating/updating inline policy {policy_name} for role: {role_name}')
|
|
1356
|
+
|
|
1357
|
+
# Check if server is in read-only mode
|
|
1358
|
+
if Context.is_readonly():
|
|
1359
|
+
raise IamClientError(
|
|
1360
|
+
'Cannot create/update inline policy: server is running in read-only mode'
|
|
1361
|
+
)
|
|
1362
|
+
|
|
1363
|
+
if not role_name or not policy_name:
|
|
1364
|
+
raise IamValidationError('Role name and policy name are required')
|
|
1365
|
+
|
|
1366
|
+
iam = get_iam_client()
|
|
1367
|
+
|
|
1368
|
+
# Handle both string and dict types
|
|
1369
|
+
if isinstance(policy_document, dict):
|
|
1370
|
+
policy_doc = json.dumps(policy_document)
|
|
1371
|
+
else:
|
|
1372
|
+
policy_doc = policy_document
|
|
1373
|
+
# Validate JSON
|
|
1374
|
+
try:
|
|
1375
|
+
json.loads(policy_doc)
|
|
1376
|
+
except json.JSONDecodeError:
|
|
1377
|
+
raise IamValidationError('Invalid JSON in policy_document')
|
|
1378
|
+
|
|
1379
|
+
iam.put_role_policy(RoleName=role_name, PolicyName=policy_name, PolicyDocument=policy_doc)
|
|
1380
|
+
|
|
1381
|
+
result = InlinePolicyResponse(
|
|
1382
|
+
policy_name=policy_name,
|
|
1383
|
+
policy_document=policy_doc,
|
|
1384
|
+
user_name=None,
|
|
1385
|
+
role_name=role_name,
|
|
1386
|
+
message=f'Successfully created/updated inline policy {policy_name} for role {role_name}',
|
|
1387
|
+
)
|
|
1388
|
+
|
|
1389
|
+
logger.info(
|
|
1390
|
+
f'Successfully created/updated inline policy {policy_name} for role: {role_name}'
|
|
1391
|
+
)
|
|
1392
|
+
return result
|
|
1393
|
+
|
|
1394
|
+
except Exception as e:
|
|
1395
|
+
error = handle_iam_error(e)
|
|
1396
|
+
logger.error(f'Error creating/updating inline policy: {error}')
|
|
1397
|
+
raise error
|
|
1398
|
+
|
|
1399
|
+
|
|
1400
|
+
@mcp.tool()
|
|
1401
|
+
async def get_role_policy(
|
|
1402
|
+
role_name: str = Field(description='The name of the IAM role'),
|
|
1403
|
+
policy_name: str = Field(description='The name of the inline policy'),
|
|
1404
|
+
) -> InlinePolicyResponse:
|
|
1405
|
+
"""Retrieve an inline policy for an IAM role.
|
|
1406
|
+
|
|
1407
|
+
This tool retrieves the policy document for a specific inline policy attached to a role.
|
|
1408
|
+
|
|
1409
|
+
Args:
|
|
1410
|
+
role_name: The name of the IAM role
|
|
1411
|
+
policy_name: The name of the inline policy
|
|
1412
|
+
|
|
1413
|
+
Returns:
|
|
1414
|
+
InlinePolicyResponse containing the policy document and details
|
|
1415
|
+
"""
|
|
1416
|
+
try:
|
|
1417
|
+
logger.info(f'Getting inline policy {policy_name} for role: {role_name}')
|
|
1418
|
+
|
|
1419
|
+
if not role_name or not policy_name:
|
|
1420
|
+
raise IamValidationError('Role name and policy name are required')
|
|
1421
|
+
|
|
1422
|
+
iam = get_iam_client()
|
|
1423
|
+
|
|
1424
|
+
response = iam.get_role_policy(RoleName=role_name, PolicyName=policy_name)
|
|
1425
|
+
|
|
1426
|
+
result = InlinePolicyResponse(
|
|
1427
|
+
policy_name=response['PolicyName'],
|
|
1428
|
+
policy_document=response['PolicyDocument'],
|
|
1429
|
+
user_name=None,
|
|
1430
|
+
role_name=response['RoleName'],
|
|
1431
|
+
message=f'Successfully retrieved inline policy {policy_name} for role {role_name}',
|
|
1432
|
+
)
|
|
1433
|
+
|
|
1434
|
+
logger.info(f'Successfully retrieved inline policy {policy_name} for role: {role_name}')
|
|
1435
|
+
return result
|
|
1436
|
+
|
|
1437
|
+
except Exception as e:
|
|
1438
|
+
error = handle_iam_error(e)
|
|
1439
|
+
logger.error(f'Error getting inline policy: {error}')
|
|
1440
|
+
raise error
|
|
1441
|
+
|
|
1442
|
+
|
|
1443
|
+
@mcp.tool()
|
|
1444
|
+
async def delete_role_policy(
|
|
1445
|
+
role_name: str = Field(description='The name of the IAM role'),
|
|
1446
|
+
policy_name: str = Field(description='The name of the inline policy to delete'),
|
|
1447
|
+
) -> Dict[str, Any]:
|
|
1448
|
+
"""Delete an inline policy from an IAM role.
|
|
1449
|
+
|
|
1450
|
+
This tool removes an inline policy from the specified role. The policy document
|
|
1451
|
+
will be permanently deleted and cannot be recovered.
|
|
1452
|
+
|
|
1453
|
+
Args:
|
|
1454
|
+
role_name: The name of the IAM role
|
|
1455
|
+
policy_name: The name of the inline policy to delete
|
|
1456
|
+
|
|
1457
|
+
Returns:
|
|
1458
|
+
Dictionary containing deletion status
|
|
1459
|
+
"""
|
|
1460
|
+
try:
|
|
1461
|
+
logger.info(f'Deleting inline policy {policy_name} from role: {role_name}')
|
|
1462
|
+
|
|
1463
|
+
# Check if server is in read-only mode
|
|
1464
|
+
if Context.is_readonly():
|
|
1465
|
+
raise IamClientError(
|
|
1466
|
+
'Cannot delete inline policy: server is running in read-only mode'
|
|
1467
|
+
)
|
|
1468
|
+
|
|
1469
|
+
if not role_name or not policy_name:
|
|
1470
|
+
raise IamValidationError('Role name and policy name are required')
|
|
1471
|
+
|
|
1472
|
+
iam = get_iam_client()
|
|
1473
|
+
|
|
1474
|
+
iam.delete_role_policy(RoleName=role_name, PolicyName=policy_name)
|
|
1475
|
+
|
|
1476
|
+
result = {
|
|
1477
|
+
'message': f'Successfully deleted inline policy {policy_name} from role {role_name}',
|
|
1478
|
+
'role_name': role_name,
|
|
1479
|
+
'policy_name': policy_name,
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
logger.info(f'Successfully deleted inline policy {policy_name} from role: {role_name}')
|
|
1483
|
+
return result
|
|
1484
|
+
|
|
1485
|
+
except Exception as e:
|
|
1486
|
+
error = handle_iam_error(e)
|
|
1487
|
+
logger.error(f'Error deleting inline policy: {error}')
|
|
1488
|
+
raise error
|
|
1489
|
+
|
|
1490
|
+
|
|
1491
|
+
@mcp.tool()
|
|
1492
|
+
async def list_user_policies(
|
|
1493
|
+
user_name: str = Field(description='The name of the IAM user'),
|
|
1494
|
+
) -> InlinePolicyListResponse:
|
|
1495
|
+
"""List all inline policies for an IAM user.
|
|
1496
|
+
|
|
1497
|
+
This tool retrieves the names of all inline policies attached to the specified user.
|
|
1498
|
+
|
|
1499
|
+
Args:
|
|
1500
|
+
user_name: The name of the IAM user
|
|
1501
|
+
|
|
1502
|
+
Returns:
|
|
1503
|
+
InlinePolicyListResponse containing the list of policy names
|
|
1504
|
+
"""
|
|
1505
|
+
try:
|
|
1506
|
+
logger.info(f'Listing inline policies for user: {user_name}')
|
|
1507
|
+
|
|
1508
|
+
if not user_name:
|
|
1509
|
+
raise IamValidationError('User name is required')
|
|
1510
|
+
|
|
1511
|
+
iam = get_iam_client()
|
|
1512
|
+
|
|
1513
|
+
response = iam.list_user_policies(UserName=user_name)
|
|
1514
|
+
|
|
1515
|
+
result = InlinePolicyListResponse(
|
|
1516
|
+
policy_names=response.get('PolicyNames', []),
|
|
1517
|
+
user_name=user_name,
|
|
1518
|
+
role_name=None,
|
|
1519
|
+
count=len(response.get('PolicyNames', [])),
|
|
1520
|
+
)
|
|
1521
|
+
|
|
1522
|
+
logger.info(f'Successfully listed {result.count} inline policies for user: {user_name}')
|
|
1523
|
+
return result
|
|
1524
|
+
|
|
1525
|
+
except Exception as e:
|
|
1526
|
+
error = handle_iam_error(e)
|
|
1527
|
+
logger.error(f'Error listing inline policies: {error}')
|
|
1528
|
+
raise error
|
|
1529
|
+
|
|
1530
|
+
|
|
1531
|
+
@mcp.tool()
|
|
1532
|
+
async def list_role_policies(
|
|
1533
|
+
role_name: str = Field(description='The name of the IAM role'),
|
|
1534
|
+
) -> InlinePolicyListResponse:
|
|
1535
|
+
"""List all inline policies for an IAM role.
|
|
1536
|
+
|
|
1537
|
+
This tool retrieves the names of all inline policies attached to the specified role.
|
|
1538
|
+
|
|
1539
|
+
Args:
|
|
1540
|
+
role_name: The name of the IAM role
|
|
1541
|
+
|
|
1542
|
+
Returns:
|
|
1543
|
+
InlinePolicyListResponse containing the list of policy names
|
|
1544
|
+
"""
|
|
1545
|
+
try:
|
|
1546
|
+
logger.info(f'Listing inline policies for role: {role_name}')
|
|
1547
|
+
|
|
1548
|
+
if not role_name:
|
|
1549
|
+
raise IamValidationError('Role name is required')
|
|
1550
|
+
|
|
1551
|
+
iam = get_iam_client()
|
|
1552
|
+
|
|
1553
|
+
response = iam.list_role_policies(RoleName=role_name)
|
|
1554
|
+
|
|
1555
|
+
result = InlinePolicyListResponse(
|
|
1556
|
+
policy_names=response.get('PolicyNames', []),
|
|
1557
|
+
user_name=None,
|
|
1558
|
+
role_name=role_name,
|
|
1559
|
+
count=len(response.get('PolicyNames', [])),
|
|
1560
|
+
)
|
|
1561
|
+
|
|
1562
|
+
logger.info(f'Successfully listed {result.count} inline policies for role: {role_name}')
|
|
1563
|
+
return result
|
|
1564
|
+
|
|
1565
|
+
except Exception as e:
|
|
1566
|
+
error = handle_iam_error(e)
|
|
1567
|
+
logger.error(f'Error listing inline policies: {error}')
|
|
1568
|
+
raise error
|
|
1569
|
+
|
|
1570
|
+
|
|
741
1571
|
def main():
|
|
742
1572
|
"""Run the MCP server with CLI argument support."""
|
|
743
1573
|
parser = argparse.ArgumentParser(
|
|
@@ -745,23 +1575,20 @@ def main():
|
|
|
745
1575
|
)
|
|
746
1576
|
parser.add_argument(
|
|
747
1577
|
'--readonly',
|
|
748
|
-
action=
|
|
749
|
-
help='
|
|
750
|
-
default=False,
|
|
1578
|
+
action='store_true',
|
|
1579
|
+
help='Run server in read-only mode (prevents all mutating operations)',
|
|
751
1580
|
)
|
|
752
|
-
parser.add_argument('--region', help='AWS region to use for operations')
|
|
753
1581
|
|
|
754
1582
|
args = parser.parse_args()
|
|
755
1583
|
|
|
756
|
-
#
|
|
757
|
-
Context.initialize(readonly=args.readonly, region=args.region)
|
|
758
|
-
|
|
759
|
-
if args.region:
|
|
760
|
-
logger.info(f'Using AWS region: {args.region}')
|
|
761
|
-
|
|
1584
|
+
# Set read-only mode if specified
|
|
762
1585
|
if args.readonly:
|
|
763
|
-
|
|
1586
|
+
Context.set_readonly(True)
|
|
1587
|
+
logger.info('Server started in READ-ONLY mode - all mutating operations are disabled')
|
|
1588
|
+
else:
|
|
1589
|
+
logger.info('Server started in FULL ACCESS mode')
|
|
764
1590
|
|
|
1591
|
+
# Run the MCP server
|
|
765
1592
|
mcp.run()
|
|
766
1593
|
|
|
767
1594
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: awslabs.iam-mcp-server
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: An AWS Labs Model Context Protocol (MCP) server for managing AWS IAM resources including users, roles, policies, and permissions
|
|
5
5
|
Project-URL: homepage, https://awslabs.github.io/mcp/
|
|
6
6
|
Project-URL: docs, https://awslabs.github.io/mcp/servers/iam-mcp-server/
|
|
@@ -37,7 +37,9 @@ A Model Context Protocol (MCP) server for comprehensive AWS Identity and Access
|
|
|
37
37
|
### Core IAM Management
|
|
38
38
|
- **User Management**: Create, list, retrieve, and delete IAM users
|
|
39
39
|
- **Role Management**: Create, list, and manage IAM roles with trust policies
|
|
40
|
+
- **Group Management**: Create, list, retrieve, and delete IAM groups with member management
|
|
40
41
|
- **Policy Management**: List and manage IAM policies (managed and inline)
|
|
42
|
+
- **Inline Policy Management**: Full CRUD operations for user and role inline policies
|
|
41
43
|
- **Permission Management**: Attach/detach policies to users and roles
|
|
42
44
|
- **Access Key Management**: Create and delete access keys for users
|
|
43
45
|
- **Security Simulation**: Test policy permissions before applying them
|
|
@@ -104,6 +106,16 @@ The AWS credentials used by this server need the following IAM permissions:
|
|
|
104
106
|
"iam:GetRole",
|
|
105
107
|
"iam:CreateRole",
|
|
106
108
|
"iam:DeleteRole",
|
|
109
|
+
"iam:ListGroups",
|
|
110
|
+
"iam:GetGroup",
|
|
111
|
+
"iam:CreateGroup",
|
|
112
|
+
"iam:DeleteGroup",
|
|
113
|
+
"iam:AddUserToGroup",
|
|
114
|
+
"iam:RemoveUserFromGroup",
|
|
115
|
+
"iam:AttachGroupPolicy",
|
|
116
|
+
"iam:DetachGroupPolicy",
|
|
117
|
+
"iam:ListAttachedGroupPolicies",
|
|
118
|
+
"iam:ListGroupPolicies",
|
|
107
119
|
"iam:ListPolicies",
|
|
108
120
|
"iam:GetPolicy",
|
|
109
121
|
"iam:CreatePolicy",
|
|
@@ -116,13 +128,18 @@ The AWS credentials used by this server need the following IAM permissions:
|
|
|
116
128
|
"iam:ListAttachedRolePolicies",
|
|
117
129
|
"iam:ListUserPolicies",
|
|
118
130
|
"iam:ListRolePolicies",
|
|
131
|
+
"iam:GetUserPolicy",
|
|
132
|
+
"iam:GetRolePolicy",
|
|
133
|
+
"iam:PutUserPolicy",
|
|
134
|
+
"iam:PutRolePolicy",
|
|
119
135
|
"iam:GetGroupsForUser",
|
|
120
136
|
"iam:ListAccessKeys",
|
|
121
137
|
"iam:CreateAccessKey",
|
|
122
138
|
"iam:DeleteAccessKey",
|
|
123
139
|
"iam:SimulatePrincipalPolicy",
|
|
124
140
|
"iam:RemoveUserFromGroup",
|
|
125
|
-
"iam:DeleteUserPolicy"
|
|
141
|
+
"iam:DeleteUserPolicy",
|
|
142
|
+
"iam:DeleteRolePolicy"
|
|
126
143
|
],
|
|
127
144
|
"Resource": "*"
|
|
128
145
|
}
|
|
@@ -304,6 +321,63 @@ Create a new IAM role with a trust policy.
|
|
|
304
321
|
- `max_session_duration` (optional): Maximum session duration in seconds (default: 3600)
|
|
305
322
|
- `permissions_boundary` (optional): ARN of the permissions boundary policy
|
|
306
323
|
|
|
324
|
+
### Group Management
|
|
325
|
+
|
|
326
|
+
#### `list_groups`
|
|
327
|
+
List IAM groups in the account with optional filtering.
|
|
328
|
+
|
|
329
|
+
**Parameters:**
|
|
330
|
+
- `path_prefix` (optional): Path prefix to filter groups (e.g., "/division_abc/")
|
|
331
|
+
- `max_items` (optional): Maximum number of groups to return (default: 100)
|
|
332
|
+
|
|
333
|
+
#### `get_group`
|
|
334
|
+
Get detailed information about a specific IAM group including members, attached policies, and inline policies.
|
|
335
|
+
|
|
336
|
+
**Parameters:**
|
|
337
|
+
- `group_name`: The name of the IAM group to retrieve
|
|
338
|
+
|
|
339
|
+
#### `create_group`
|
|
340
|
+
Create a new IAM group.
|
|
341
|
+
|
|
342
|
+
**Parameters:**
|
|
343
|
+
- `group_name`: The name of the new IAM group
|
|
344
|
+
- `path` (optional): The path for the group (default: "/")
|
|
345
|
+
|
|
346
|
+
#### `delete_group`
|
|
347
|
+
Delete an IAM group with optional force cleanup.
|
|
348
|
+
|
|
349
|
+
**Parameters:**
|
|
350
|
+
- `group_name`: The name of the IAM group to delete
|
|
351
|
+
- `force` (optional): Force delete by removing all members and policies first (default: false)
|
|
352
|
+
|
|
353
|
+
#### `add_user_to_group`
|
|
354
|
+
Add a user to an IAM group.
|
|
355
|
+
|
|
356
|
+
**Parameters:**
|
|
357
|
+
- `group_name`: The name of the IAM group
|
|
358
|
+
- `user_name`: The name of the IAM user
|
|
359
|
+
|
|
360
|
+
#### `remove_user_from_group`
|
|
361
|
+
Remove a user from an IAM group.
|
|
362
|
+
|
|
363
|
+
**Parameters:**
|
|
364
|
+
- `group_name`: The name of the IAM group
|
|
365
|
+
- `user_name`: The name of the IAM user
|
|
366
|
+
|
|
367
|
+
#### `attach_group_policy`
|
|
368
|
+
Attach a managed policy to an IAM group.
|
|
369
|
+
|
|
370
|
+
**Parameters:**
|
|
371
|
+
- `group_name`: The name of the IAM group
|
|
372
|
+
- `policy_arn`: The ARN of the policy to attach
|
|
373
|
+
|
|
374
|
+
#### `detach_group_policy`
|
|
375
|
+
Detach a managed policy from an IAM group.
|
|
376
|
+
|
|
377
|
+
**Parameters:**
|
|
378
|
+
- `group_name`: The name of the IAM group
|
|
379
|
+
- `policy_arn`: The ARN of the policy to detach
|
|
380
|
+
|
|
307
381
|
### Policy Management
|
|
308
382
|
|
|
309
383
|
#### `list_policies`
|
|
@@ -357,6 +431,64 @@ Simulate IAM policy evaluation for a principal to test permissions.
|
|
|
357
431
|
- `resource_arns` (optional): List of resource ARNs to test against
|
|
358
432
|
- `context_entries` (optional): Context entries for the simulation
|
|
359
433
|
|
|
434
|
+
### Inline Policy Management
|
|
435
|
+
|
|
436
|
+
#### `put_user_policy`
|
|
437
|
+
Create or update an inline policy for an IAM user.
|
|
438
|
+
|
|
439
|
+
**Parameters:**
|
|
440
|
+
- `user_name`: The name of the IAM user
|
|
441
|
+
- `policy_name`: The name of the inline policy
|
|
442
|
+
- `policy_document`: The policy document in JSON format (string or dict)
|
|
443
|
+
|
|
444
|
+
#### `get_user_policy`
|
|
445
|
+
Retrieve an inline policy for an IAM user.
|
|
446
|
+
|
|
447
|
+
**Parameters:**
|
|
448
|
+
- `user_name`: The name of the IAM user
|
|
449
|
+
- `policy_name`: The name of the inline policy
|
|
450
|
+
|
|
451
|
+
#### `delete_user_policy`
|
|
452
|
+
Delete an inline policy from an IAM user.
|
|
453
|
+
|
|
454
|
+
**Parameters:**
|
|
455
|
+
- `user_name`: The name of the IAM user
|
|
456
|
+
- `policy_name`: The name of the inline policy to delete
|
|
457
|
+
|
|
458
|
+
#### `list_user_policies`
|
|
459
|
+
List all inline policies for an IAM user.
|
|
460
|
+
|
|
461
|
+
**Parameters:**
|
|
462
|
+
- `user_name`: The name of the IAM user
|
|
463
|
+
|
|
464
|
+
#### `put_role_policy`
|
|
465
|
+
Create or update an inline policy for an IAM role.
|
|
466
|
+
|
|
467
|
+
**Parameters:**
|
|
468
|
+
- `role_name`: The name of the IAM role
|
|
469
|
+
- `policy_name`: The name of the inline policy
|
|
470
|
+
- `policy_document`: The policy document in JSON format (string or dict)
|
|
471
|
+
|
|
472
|
+
#### `get_role_policy`
|
|
473
|
+
Retrieve an inline policy for an IAM role.
|
|
474
|
+
|
|
475
|
+
**Parameters:**
|
|
476
|
+
- `role_name`: The name of the IAM role
|
|
477
|
+
- `policy_name`: The name of the inline policy
|
|
478
|
+
|
|
479
|
+
#### `delete_role_policy`
|
|
480
|
+
Delete an inline policy from an IAM role.
|
|
481
|
+
|
|
482
|
+
**Parameters:**
|
|
483
|
+
- `role_name`: The name of the IAM role
|
|
484
|
+
- `policy_name`: The name of the inline policy to delete
|
|
485
|
+
|
|
486
|
+
#### `list_role_policies`
|
|
487
|
+
List all inline policies for an IAM role.
|
|
488
|
+
|
|
489
|
+
**Parameters:**
|
|
490
|
+
- `role_name`: The name of the IAM role
|
|
491
|
+
|
|
360
492
|
## Usage Examples
|
|
361
493
|
|
|
362
494
|
### Basic User Management
|
|
@@ -398,6 +530,30 @@ role = await create_role(
|
|
|
398
530
|
)
|
|
399
531
|
```
|
|
400
532
|
|
|
533
|
+
### Group Management
|
|
534
|
+
```python
|
|
535
|
+
# Create a new group
|
|
536
|
+
group = await create_group(
|
|
537
|
+
group_name="Developers",
|
|
538
|
+
path="/teams/"
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
# Add users to the group
|
|
542
|
+
await add_user_to_group(
|
|
543
|
+
group_name="Developers",
|
|
544
|
+
user_name="john.doe"
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
# Attach a policy to the group
|
|
548
|
+
await attach_group_policy(
|
|
549
|
+
group_name="Developers",
|
|
550
|
+
policy_arn="arn:aws:iam::123456789012:policy/DeveloperPolicy"
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
# Get group details including members
|
|
554
|
+
group_details = await get_group(group_name="Developers")
|
|
555
|
+
```
|
|
556
|
+
|
|
401
557
|
### Policy Management
|
|
402
558
|
```python
|
|
403
559
|
# List customer managed policies
|
|
@@ -420,6 +576,58 @@ simulation = await simulate_principal_policy(
|
|
|
420
576
|
)
|
|
421
577
|
```
|
|
422
578
|
|
|
579
|
+
### Inline Policy Management
|
|
580
|
+
```python
|
|
581
|
+
# Create an inline policy for a user
|
|
582
|
+
policy_document = {
|
|
583
|
+
"Version": "2012-10-17",
|
|
584
|
+
"Statement": [
|
|
585
|
+
{
|
|
586
|
+
"Effect": "Allow",
|
|
587
|
+
"Action": ["s3:GetObject", "s3:PutObject"],
|
|
588
|
+
"Resource": "arn:aws:s3:::my-bucket/*"
|
|
589
|
+
}
|
|
590
|
+
]
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
await put_user_policy(
|
|
594
|
+
user_name="developer",
|
|
595
|
+
policy_name="S3AccessPolicy",
|
|
596
|
+
policy_document=policy_document
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
# Retrieve an inline policy
|
|
600
|
+
policy = await get_user_policy(
|
|
601
|
+
user_name="developer",
|
|
602
|
+
policy_name="S3AccessPolicy"
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
# List all inline policies for a user
|
|
606
|
+
policies = await list_user_policies(user_name="developer")
|
|
607
|
+
|
|
608
|
+
# Create an inline policy for a role
|
|
609
|
+
await put_role_policy(
|
|
610
|
+
role_name="EC2-S3-Access-Role",
|
|
611
|
+
policy_name="S3ReadOnlyPolicy",
|
|
612
|
+
policy_document={
|
|
613
|
+
"Version": "2012-10-17",
|
|
614
|
+
"Statement": [
|
|
615
|
+
{
|
|
616
|
+
"Effect": "Allow",
|
|
617
|
+
"Action": "s3:GetObject",
|
|
618
|
+
"Resource": "*"
|
|
619
|
+
}
|
|
620
|
+
]
|
|
621
|
+
}
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
# Delete an inline policy
|
|
625
|
+
await delete_user_policy(
|
|
626
|
+
user_name="developer",
|
|
627
|
+
policy_name="S3AccessPolicy"
|
|
628
|
+
)
|
|
629
|
+
```
|
|
630
|
+
|
|
423
631
|
## Security Best Practices
|
|
424
632
|
|
|
425
633
|
1. **Principle of Least Privilege**: Always grant the minimum permissions necessary
|
|
@@ -429,6 +637,8 @@ simulation = await simulate_principal_policy(
|
|
|
429
637
|
5. **Enable MFA**: Use multi-factor authentication where possible
|
|
430
638
|
6. **Permissions Boundaries**: Use permissions boundaries to set maximum permissions
|
|
431
639
|
7. **Policy Simulation**: Test policies before applying them to production
|
|
640
|
+
8. **Prefer Managed Policies**: Use managed policies over inline policies for reusable permissions
|
|
641
|
+
9. **Inline Policy Guidelines**: Use inline policies only for permissions unique to a single identity
|
|
432
642
|
|
|
433
643
|
## Error Handling
|
|
434
644
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
awslabs/__init__.py,sha256=dzKGHmVYP-680lDPkXJrDGVhMSb0BD6_Vfu83qcA1aA,675
|
|
2
|
+
awslabs/iam_mcp_server/__init__.py,sha256=EkXrr_NxCPkjPWqoMupbYspWGNRVClM5ycmO42mvEZM,673
|
|
3
|
+
awslabs/iam_mcp_server/aws_client.py,sha256=13lAde77Q3DdOHDqE7YvF93SjdV-S9c_2uy3ChrjpaA,2994
|
|
4
|
+
awslabs/iam_mcp_server/context.py,sha256=Xmxrp6B-T_2OS3WEp2K1VjBHQjIo6dpvbx6V_j8ZXFo,1752
|
|
5
|
+
awslabs/iam_mcp_server/errors.py,sha256=Co6-rVod81qaAl__Ob5ouGly33SBhfpF61am2P5BfmE,5818
|
|
6
|
+
awslabs/iam_mcp_server/models.py,sha256=G7Ft_4MIXtDpGyFkdc8nJ693ZG6cWiticqHn2T1gUBE,12535
|
|
7
|
+
awslabs/iam_mcp_server/server.py,sha256=ApKVC0rvxSYzIsS5GiLYn8saaBv7XzY-xWmDyqjc-BM,54476
|
|
8
|
+
awslabs_iam_mcp_server-1.0.2.dist-info/METADATA,sha256=XTpZQKEcJyz-O54yrClndiZT_n-cYCP0h1uxYRCYbIE,19927
|
|
9
|
+
awslabs_iam_mcp_server-1.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
10
|
+
awslabs_iam_mcp_server-1.0.2.dist-info/entry_points.txt,sha256=B9fOJVT7l2NUQ1RHLUiskRtB5wADSpSI95cI9gU6dO0,78
|
|
11
|
+
awslabs_iam_mcp_server-1.0.2.dist-info/licenses/LICENSE,sha256=CeipvOyAZxBGUsFoaFqwkx54aPnIKEtm9a5u2uXxEws,10142
|
|
12
|
+
awslabs_iam_mcp_server-1.0.2.dist-info/licenses/NOTICE,sha256=Qls-8qZMuKGG2WDBsLbJOBNuQxTkSI6ij_7HTi7I4ys,86
|
|
13
|
+
awslabs_iam_mcp_server-1.0.2.dist-info/RECORD,,
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
awslabs/__init__.py,sha256=dzKGHmVYP-680lDPkXJrDGVhMSb0BD6_Vfu83qcA1aA,675
|
|
2
|
-
awslabs/iam_mcp_server/__init__.py,sha256=tnJPNAmwa84Ny3QliNkzX5cLfDTMrM3bXKa9QGfXrLo,665
|
|
3
|
-
awslabs/iam_mcp_server/aws_client.py,sha256=13lAde77Q3DdOHDqE7YvF93SjdV-S9c_2uy3ChrjpaA,2994
|
|
4
|
-
awslabs/iam_mcp_server/context.py,sha256=X28PtpwuvU1NyWIc8cBx8tyZimGfirXRegyasygBUd8,1620
|
|
5
|
-
awslabs/iam_mcp_server/errors.py,sha256=Co6-rVod81qaAl__Ob5ouGly33SBhfpF61am2P5BfmE,5818
|
|
6
|
-
awslabs/iam_mcp_server/models.py,sha256=ftCkBwW4IKk0g1pmspI7tXpWEZTu3zSs7nx0DwQvOfI,8424
|
|
7
|
-
awslabs/iam_mcp_server/server.py,sha256=8OHhdRMwxE9YlYlynbA-V33ChLCQudrxWsLHk7UVPKo,26615
|
|
8
|
-
awslabs_iam_mcp_server-1.0.1.dist-info/METADATA,sha256=i8PWiTrIXj9wYS5docL-O2U4OOgECuFas4UlYq76cBg,14297
|
|
9
|
-
awslabs_iam_mcp_server-1.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
10
|
-
awslabs_iam_mcp_server-1.0.1.dist-info/entry_points.txt,sha256=B9fOJVT7l2NUQ1RHLUiskRtB5wADSpSI95cI9gU6dO0,78
|
|
11
|
-
awslabs_iam_mcp_server-1.0.1.dist-info/licenses/LICENSE,sha256=CeipvOyAZxBGUsFoaFqwkx54aPnIKEtm9a5u2uXxEws,10142
|
|
12
|
-
awslabs_iam_mcp_server-1.0.1.dist-info/licenses/NOTICE,sha256=Qls-8qZMuKGG2WDBsLbJOBNuQxTkSI6ij_7HTi7I4ys,86
|
|
13
|
-
awslabs_iam_mcp_server-1.0.1.dist-info/RECORD,,
|
|
File without changes
|
{awslabs_iam_mcp_server-1.0.1.dist-info → awslabs_iam_mcp_server-1.0.2.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{awslabs_iam_mcp_server-1.0.1.dist-info → awslabs_iam_mcp_server-1.0.2.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{awslabs_iam_mcp_server-1.0.1.dist-info → awslabs_iam_mcp_server-1.0.2.dist-info}/licenses/NOTICE
RENAMED
|
File without changes
|