awslabs.eks-mcp-server 0.1.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.
@@ -0,0 +1,359 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
4
+ # with the License. A copy of the License is located at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES
9
+ # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
10
+ # and limitations under the License.
11
+
12
+ """IAM handler for the EKS MCP Server."""
13
+
14
+ import json
15
+ from awslabs.eks_mcp_server.aws_helper import AwsHelper
16
+ from awslabs.eks_mcp_server.logging_helper import LogLevel, log_with_request_id
17
+ from awslabs.eks_mcp_server.models import (
18
+ AddInlinePolicyResponse,
19
+ PolicySummary,
20
+ RoleDescriptionResponse,
21
+ )
22
+ from mcp.server.fastmcp import Context
23
+ from mcp.types import TextContent
24
+ from pydantic import Field
25
+ from typing import Any, Dict, List, Union
26
+
27
+
28
+ class IAMHandler:
29
+ """Handler for AWS IAM operations in the EKS MCP Server.
30
+
31
+ This class provides tools for managing IAM roles and policies, including
32
+ describing roles with their attached policies and adding inline permissions
33
+ to policies.
34
+ """
35
+
36
+ def __init__(self, mcp, allow_write: bool = False):
37
+ """Initialize the IAM handler.
38
+
39
+ Args:
40
+ mcp: The MCP server instance
41
+ allow_write: Whether to enable write access (default: False)
42
+ """
43
+ self.mcp = mcp
44
+ self.iam_client = AwsHelper.create_boto3_client('iam')
45
+ self.allow_write = allow_write
46
+
47
+ # Register tools
48
+ self.mcp.tool(name='add_inline_policy')(self.add_inline_policy)
49
+ self.mcp.tool(name='get_policies_for_role')(self.get_policies_for_role)
50
+
51
+ async def get_policies_for_role(
52
+ self,
53
+ ctx: Context,
54
+ role_name: str = Field(
55
+ ...,
56
+ description='Name of the IAM role to get policies for. The role must exist in your AWS account.',
57
+ ),
58
+ ) -> RoleDescriptionResponse:
59
+ """Get all policies attached to an IAM role.
60
+
61
+ This tool retrieves all policies associated with an IAM role, providing a comprehensive view
62
+ of the role's permissions and trust relationships. It helps you understand the current
63
+ permissions, identify missing or excessive permissions, troubleshoot EKS cluster issues,
64
+ and verify trust relationships for service roles.
65
+
66
+ ## Requirements
67
+ - The role must exist in your AWS account
68
+ - Valid AWS credentials with permissions to read IAM role information
69
+
70
+ ## Response Information
71
+ The response includes role ARN, assume role policy document (trust relationships),
72
+ role description, managed policies with their documents, and inline policies with
73
+ their documents.
74
+
75
+ ## Usage Tips
76
+ - Use this tool before adding new permissions to understand existing access
77
+ - Check the assume role policy to verify which services or roles can assume this role
78
+ - Look for overly permissive policies that might pose security risks
79
+ - Use with add_inline_policy to implement least-privilege permissions
80
+
81
+ Args:
82
+ ctx: The MCP context
83
+ role_name: Name of the IAM role to get policies for
84
+
85
+ Returns:
86
+ RoleDescriptionResponse: Detailed information about the role's policies
87
+ """
88
+ try:
89
+ log_with_request_id(ctx, LogLevel.INFO, f'Describing IAM role: {role_name}')
90
+
91
+ # Get role details
92
+ role_response = self.iam_client.get_role(RoleName=role_name)
93
+ role = role_response['Role']
94
+
95
+ # Get attached managed policies
96
+ managed_policies = self._get_managed_policies(ctx, role_name)
97
+
98
+ # Get inline policies
99
+ inline_policies = self._get_inline_policies(ctx, role_name)
100
+
101
+ # Parse the assume role policy document if it's a string, otherwise use it directly
102
+ if isinstance(role['AssumeRolePolicyDocument'], str):
103
+ assume_role_policy_document = json.loads(role['AssumeRolePolicyDocument'])
104
+ else:
105
+ assume_role_policy_document = role['AssumeRolePolicyDocument']
106
+
107
+ # Create the response
108
+ return RoleDescriptionResponse(
109
+ isError=False,
110
+ content=[
111
+ TextContent(
112
+ type='text',
113
+ text=f'Successfully retrieved details for IAM role: {role_name}',
114
+ )
115
+ ],
116
+ role_arn=role['Arn'],
117
+ assume_role_policy_document=assume_role_policy_document,
118
+ description=role.get('Description'),
119
+ managed_policies=managed_policies,
120
+ inline_policies=inline_policies,
121
+ )
122
+ except Exception as e:
123
+ error_message = f'Failed to describe IAM role: {str(e)}'
124
+ log_with_request_id(ctx, LogLevel.ERROR, error_message)
125
+
126
+ # Return a response with error status
127
+ return RoleDescriptionResponse(
128
+ isError=True,
129
+ content=[TextContent(type='text', text=error_message)],
130
+ role_arn='',
131
+ assume_role_policy_document={},
132
+ description=None,
133
+ managed_policies=[],
134
+ inline_policies=[],
135
+ )
136
+
137
+ async def add_inline_policy(
138
+ self,
139
+ ctx: Context,
140
+ policy_name: str = Field(
141
+ ..., description='Name of the inline policy to create. Must be unique within the role.'
142
+ ),
143
+ role_name: str = Field(
144
+ ..., description='Name of the IAM role to add the policy to. The role must exist.'
145
+ ),
146
+ permissions: Union[Dict[str, Any], List[Dict[str, Any]]] = Field(
147
+ ...,
148
+ description="""Permissions to include in the policy as IAM policy statements in JSON format.
149
+ Can be either a single statement object or an array of statement objects.""",
150
+ ),
151
+ ) -> AddInlinePolicyResponse:
152
+ """Add a new inline policy to an IAM role.
153
+
154
+ This tool creates a new inline policy with the specified permissions and adds it to an IAM role.
155
+ Inline policies are embedded within the role and cannot be attached to multiple roles. Commonly used
156
+ for granting EKS clusters access to AWS services, enabling worker nodes to access resources, and
157
+ configuring permissions for CloudWatch logging and ECR access.
158
+
159
+ ## Requirements
160
+ - The server must be run with the `--allow-write` flag
161
+ - The role must exist in your AWS account
162
+ - The policy name must be unique within the role
163
+ - You cannot modify existing policies with this tool
164
+
165
+ ## Permission Format
166
+ The permissions parameter can be either a single policy statement or a list of statements.
167
+
168
+ ### Single Statement Example
169
+ ```json
170
+ {
171
+ "Effect": "Allow",
172
+ "Action": ["s3:GetObject", "s3:PutObject"],
173
+ "Resource": "arn:aws:s3:::example-bucket/*"
174
+ }
175
+ ```
176
+
177
+ ## Usage Tips
178
+ - Follow the principle of least privilege by granting only necessary permissions
179
+ - Use specific resources rather than "*" whenever possible
180
+ - Consider using conditions to further restrict permissions
181
+ - Group related permissions into logical policies with descriptive names
182
+
183
+ Args:
184
+ ctx: The MCP context
185
+ policy_name: Name of the new inline policy to create
186
+ role_name: Name of the role to add the policy to
187
+ permissions: Permissions to include in the policy (in JSON format)
188
+
189
+ Returns:
190
+ AddInlinePolicyResponse: Information about the created policy
191
+ """
192
+ try:
193
+ # Check if write access is disabled
194
+ if not self.allow_write:
195
+ error_message = 'Adding inline policies requires --allow-write flag'
196
+ log_with_request_id(ctx, LogLevel.ERROR, error_message)
197
+ return AddInlinePolicyResponse(
198
+ isError=True,
199
+ content=[TextContent(type='text', text=error_message)],
200
+ policy_name=policy_name,
201
+ role_name=role_name,
202
+ permissions_added={},
203
+ )
204
+
205
+ # Create the inline policy
206
+ return self._create_inline_policy(ctx, role_name, policy_name, permissions)
207
+
208
+ except Exception as e:
209
+ error_message = f'Failed to create inline policy: {str(e)}'
210
+ log_with_request_id(ctx, LogLevel.ERROR, error_message)
211
+
212
+ # Return a response with error status
213
+ return AddInlinePolicyResponse(
214
+ isError=True,
215
+ content=[TextContent(type='text', text=error_message)],
216
+ policy_name=policy_name,
217
+ role_name=role_name,
218
+ permissions_added={},
219
+ )
220
+
221
+ def _get_managed_policies(self, ctx, role_name):
222
+ """Get managed policies attached to a role.
223
+
224
+ Args:
225
+ ctx: The MCP context
226
+ role_name: Name of the IAM role
227
+
228
+ Returns:
229
+ List of PolicySummary objects
230
+ """
231
+ managed_policies = []
232
+ managed_policies_response = self.iam_client.list_attached_role_policies(RoleName=role_name)
233
+
234
+ for policy in managed_policies_response.get('AttachedPolicies', []):
235
+ policy_arn = policy['PolicyArn']
236
+ policy_details = self.iam_client.get_policy(PolicyArn=policy_arn)['Policy']
237
+
238
+ # Get the policy version details to get the policy document
239
+ policy_version = None
240
+ try:
241
+ policy_version_response = self.iam_client.get_policy_version(
242
+ PolicyArn=policy_arn, VersionId=policy_details.get('DefaultVersionId', 'v1')
243
+ )
244
+ policy_version = policy_version_response.get('PolicyVersion', {})
245
+ except Exception as e:
246
+ log_with_request_id(
247
+ ctx, LogLevel.WARNING, f'Failed to get policy version: {str(e)}'
248
+ )
249
+
250
+ managed_policies.append(
251
+ PolicySummary(
252
+ policy_type='Managed',
253
+ description=policy_details.get('Description'),
254
+ policy_document=policy_version.get('Document') if policy_version else None,
255
+ )
256
+ )
257
+
258
+ return managed_policies
259
+
260
+ def _get_inline_policies(self, ctx, role_name):
261
+ """Get inline policies embedded in a role.
262
+
263
+ Args:
264
+ ctx: The MCP context
265
+ role_name: Name of the IAM role
266
+
267
+ Returns:
268
+ List of PolicySummary objects
269
+ """
270
+ inline_policies = []
271
+ inline_policies_response = self.iam_client.list_role_policies(RoleName=role_name)
272
+
273
+ for policy_name in inline_policies_response.get('PolicyNames', []):
274
+ policy_response = self.iam_client.get_role_policy(
275
+ RoleName=role_name, PolicyName=policy_name
276
+ )
277
+
278
+ inline_policies.append(
279
+ PolicySummary(
280
+ policy_type='Inline',
281
+ description=None,
282
+ policy_document=policy_response.get('PolicyDocument'),
283
+ )
284
+ )
285
+
286
+ return inline_policies
287
+
288
+ def _create_inline_policy(self, ctx, role_name, policy_name, permissions):
289
+ """Create a new inline policy with the specified permissions.
290
+
291
+ Args:
292
+ ctx: The MCP context
293
+ role_name: Name of the role
294
+ policy_name: Name of the new policy to create
295
+ permissions: Permissions to include in the policy
296
+
297
+ Returns:
298
+ AddInlinePolicyResponse: Information about the created policy
299
+ """
300
+ log_with_request_id(
301
+ ctx,
302
+ LogLevel.INFO,
303
+ f'Creating new inline policy {policy_name} in role {role_name}',
304
+ )
305
+
306
+ # Check if the policy already exists
307
+ try:
308
+ self.iam_client.get_role_policy(RoleName=role_name, PolicyName=policy_name)
309
+ # If we get here, the policy exists
310
+ error_message = f'Policy {policy_name} already exists in role {role_name}. Cannot modify existing policies.'
311
+ log_with_request_id(ctx, LogLevel.ERROR, error_message)
312
+ return AddInlinePolicyResponse(
313
+ isError=True,
314
+ content=[TextContent(type='text', text=error_message)],
315
+ policy_name=policy_name,
316
+ role_name=role_name,
317
+ permissions_added={},
318
+ )
319
+ except self.iam_client.exceptions.NoSuchEntityException:
320
+ # Policy doesn't exist, we can create it
321
+ pass
322
+
323
+ # Create a new policy document
324
+ policy_document = {'Version': '2012-10-17', 'Statement': []}
325
+
326
+ # Add the permissions to the policy document
327
+ self._add_permissions_to_document(policy_document, permissions)
328
+
329
+ # Create the policy
330
+ self.iam_client.put_role_policy(
331
+ RoleName=role_name, PolicyName=policy_name, PolicyDocument=json.dumps(policy_document)
332
+ )
333
+
334
+ return AddInlinePolicyResponse(
335
+ isError=False,
336
+ content=[
337
+ TextContent(
338
+ type='text',
339
+ text=f'Successfully created new inline policy {policy_name} in role {role_name}',
340
+ )
341
+ ],
342
+ policy_name=policy_name,
343
+ role_name=role_name,
344
+ permissions_added=permissions,
345
+ )
346
+
347
+ def _add_permissions_to_document(self, policy_document, permissions):
348
+ """Add permissions to a policy document.
349
+
350
+ Args:
351
+ policy_document: Policy document to modify
352
+ permissions: Permissions to add
353
+ """
354
+ if isinstance(permissions, dict):
355
+ # Single statement
356
+ policy_document['Statement'].append(permissions)
357
+ elif isinstance(permissions, list):
358
+ # Multiple statements
359
+ policy_document['Statement'].extend(permissions)