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 ADDED
@@ -0,0 +1,17 @@
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 Labs MCP Servers package."""
16
+
17
+ __version__ = '1.0.0'
@@ -0,0 +1,17 @@
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."""
16
+
17
+ __version__ = '1.0.0'
@@ -0,0 +1,84 @@
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 client utilities for the IAM MCP Server."""
16
+
17
+ import boto3
18
+ from awslabs.iam_mcp_server.context import Context
19
+ from botocore.config import Config
20
+ from loguru import logger
21
+ from typing import Any, Optional
22
+
23
+
24
+ def get_iam_client(region: Optional[str] = None) -> Any:
25
+ """Get an IAM client with proper configuration.
26
+
27
+ Args:
28
+ region: Optional AWS region override
29
+
30
+ Returns:
31
+ Configured IAM client
32
+
33
+ Raises:
34
+ Exception: If client creation fails
35
+ """
36
+ try:
37
+ # Use provided region, context region, or default
38
+ client_region = region or Context.get_region()
39
+
40
+ # Add user agent to identify this MCP server in AWS logs
41
+ config = Config(user_agent_extra='awslabs-iam-mcp-server/1.0.0')
42
+
43
+ if client_region:
44
+ logger.debug(f'Creating IAM client for region: {client_region}')
45
+ return boto3.client('iam', region_name=client_region, config=config)
46
+ else:
47
+ logger.debug('Creating IAM client with default region')
48
+ return boto3.client('iam', config=config)
49
+
50
+ except Exception as e:
51
+ logger.error(f'Failed to create IAM client: {e}')
52
+ raise Exception(f'Failed to create IAM client: {str(e)}')
53
+
54
+
55
+ def get_aws_client(service_name: str, region: Optional[str] = None) -> Any:
56
+ """Get a generic AWS client for any service.
57
+
58
+ Args:
59
+ service_name: Name of the AWS service (e.g., 'iam', 's3', 'ec2')
60
+ region: Optional AWS region override
61
+
62
+ Returns:
63
+ Configured AWS client
64
+
65
+ Raises:
66
+ Exception: If client creation fails
67
+ """
68
+ try:
69
+ # Use provided region, context region, or default
70
+ client_region = region or Context.get_region()
71
+
72
+ # Add user agent to identify this MCP server in AWS logs
73
+ config = Config(user_agent_extra='awslabs-iam-mcp-server/1.0.0')
74
+
75
+ if client_region:
76
+ logger.debug(f'Creating {service_name} client for region: {client_region}')
77
+ return boto3.client(service_name, region_name=client_region, config=config)
78
+ else:
79
+ logger.debug(f'Creating {service_name} client with default region')
80
+ return boto3.client(service_name, config=config)
81
+
82
+ except Exception as e:
83
+ logger.error(f'Failed to create {service_name} client: {e}')
84
+ raise Exception(f'Failed to create {service_name} client: {str(e)}')
@@ -0,0 +1,50 @@
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
+ """Context management for the AWS IAM MCP Server."""
16
+
17
+ from typing import Optional
18
+
19
+
20
+ class Context:
21
+ """Context class for managing server state and configuration."""
22
+
23
+ _readonly: bool = False
24
+ _region: Optional[str] = None
25
+
26
+ @classmethod
27
+ def initialize(cls, readonly: bool = False, region: Optional[str] = None):
28
+ """Initialize the context with configuration options.
29
+
30
+ Args:
31
+ readonly: Whether to run in read-only mode (prevents mutations)
32
+ region: AWS region to use for operations
33
+ """
34
+ cls._readonly = readonly
35
+ cls._region = region
36
+
37
+ @classmethod
38
+ def is_readonly(cls) -> bool:
39
+ """Check if the server is running in read-only mode."""
40
+ return cls._readonly
41
+
42
+ @classmethod
43
+ def get_region(cls) -> Optional[str]:
44
+ """Get the configured AWS region."""
45
+ return cls._region
46
+
47
+ @classmethod
48
+ def set_region(cls, region: str):
49
+ """Set the AWS region."""
50
+ cls._region = region
@@ -0,0 +1,150 @@
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
+ """Error handling utilities for the AWS IAM MCP Server."""
16
+
17
+ from botocore.exceptions import ClientError as BotoClientError
18
+
19
+
20
+ class IamMcpError(Exception):
21
+ """Base exception for IAM MCP Server errors."""
22
+
23
+ def __init__(self, message: str, error_code: str = 'IamMcpError'):
24
+ """Initialize the IAM MCP error.
25
+
26
+ Args:
27
+ message: Error message
28
+ error_code: Error code identifier
29
+ """
30
+ self.message = message
31
+ self.error_code = error_code
32
+ super().__init__(self.message)
33
+
34
+
35
+ class IamClientError(IamMcpError):
36
+ """Exception for IAM client-side errors."""
37
+
38
+ def __init__(self, message: str):
39
+ """Initialize the IAM client error.
40
+
41
+ Args:
42
+ message: Error message
43
+ """
44
+ super().__init__(message, 'IamClientError')
45
+
46
+
47
+ class IamPermissionError(IamMcpError):
48
+ """Exception for IAM permission-related errors."""
49
+
50
+ def __init__(self, message: str):
51
+ """Initialize the IAM permission error.
52
+
53
+ Args:
54
+ message: Error message
55
+ """
56
+ super().__init__(message, 'IamPermissionError')
57
+
58
+
59
+ class IamResourceNotFoundError(IamMcpError):
60
+ """Exception for IAM resource not found errors."""
61
+
62
+ def __init__(self, message: str):
63
+ """Initialize the IAM resource not found error.
64
+
65
+ Args:
66
+ message: Error message
67
+ """
68
+ super().__init__(message, 'IamResourceNotFoundError')
69
+
70
+
71
+ class IamValidationError(IamMcpError):
72
+ """Exception for IAM validation errors."""
73
+
74
+ def __init__(self, message: str):
75
+ """Initialize the IAM validation error.
76
+
77
+ Args:
78
+ message: Error message
79
+ """
80
+ super().__init__(message, 'IamValidationError')
81
+
82
+
83
+ def handle_iam_error(error: Exception) -> IamMcpError:
84
+ """Handle IAM-specific errors and return standardized error responses.
85
+
86
+ Args:
87
+ error: The exception that was raised
88
+
89
+ Returns:
90
+ Standardized IAM MCP error
91
+ """
92
+ if isinstance(error, BotoClientError):
93
+ error_code = error.response.get('Error', {}).get('Code', 'Unknown')
94
+ error_message = error.response.get('Error', {}).get('Message', str(error))
95
+
96
+ # Handle common AWS IAM error patterns
97
+ if error_code in ['AccessDenied', 'AccessDeniedException']:
98
+ return IamPermissionError(
99
+ f'Access denied: {error_message}. Please check your AWS credentials and IAM permissions.'
100
+ )
101
+ elif error_code in ['NoSuchEntity', 'NoSuchEntityException']:
102
+ return IamResourceNotFoundError(f'Resource not found: {error_message}')
103
+ elif error_code in ['EntityAlreadyExists', 'EntityAlreadyExistsException']:
104
+ return IamClientError(f'Resource already exists: {error_message}')
105
+ elif error_code in ['InvalidInput', 'InvalidInputException', 'ValidationException']:
106
+ return IamValidationError(f'Invalid input: {error_message}')
107
+ elif error_code in ['LimitExceeded', 'LimitExceededException']:
108
+ return IamClientError(f'Limit exceeded: {error_message}')
109
+ elif error_code in ['ServiceFailure', 'ServiceFailureException']:
110
+ return IamMcpError(f'AWS service failure: {error_message}', 'ServiceFailure')
111
+ elif error_code in ['Throttling', 'ThrottlingException']:
112
+ return IamMcpError(f'Request throttled: {error_message}', 'Throttling')
113
+ elif error_code in ['IncompleteSignature']:
114
+ return IamClientError(
115
+ 'Incomplete signature. The request signature does not conform to AWS standards.'
116
+ )
117
+ elif error_code in ['InvalidAction']:
118
+ return IamClientError(
119
+ 'Invalid action. The action or operation requested is invalid. Verify that the action is typed correctly.'
120
+ )
121
+ elif error_code in ['InvalidClientTokenId']:
122
+ return IamClientError(
123
+ 'Invalid client token ID. The X.509 certificate or AWS access key ID provided does not exist in our records.'
124
+ )
125
+ elif error_code in ['NotAuthorized']:
126
+ return IamPermissionError(
127
+ 'Not authorized. The request signature we calculated does not match the signature you provided.'
128
+ )
129
+ elif error_code in ['RequestExpired']:
130
+ return IamClientError(
131
+ 'Request expired. The request must be submitted within a valid time frame.'
132
+ )
133
+ elif error_code in ['SignatureDoesNotMatch']:
134
+ return IamClientError(
135
+ 'Signature does not match. The request signature we calculated does not match the signature you provided.'
136
+ )
137
+ elif error_code in ['TokenRefreshRequired']:
138
+ return IamClientError(
139
+ 'Token refresh required. The AWS access token needs to be refreshed.'
140
+ )
141
+ else:
142
+ return IamMcpError(f'AWS IAM Error ({error_code}): {error_message}', error_code)
143
+
144
+ elif isinstance(error, IamMcpError):
145
+ # Already a handled IAM MCP error, return as-is
146
+ return error
147
+
148
+ else:
149
+ # Generic error handling
150
+ return IamMcpError(f'Unexpected error: {str(error)}', 'UnexpectedError')
@@ -0,0 +1,197 @@
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
+ """Data models for the AWS IAM MCP Server."""
16
+
17
+ from pydantic import BaseModel, Field
18
+ from typing import Any, Dict, List, Optional
19
+
20
+
21
+ class IamUser(BaseModel):
22
+ """IAM User model."""
23
+
24
+ user_name: str = Field(..., description='The name of the IAM user')
25
+ user_id: str = Field(..., description='The unique identifier for the user')
26
+ arn: str = Field(..., description='The Amazon Resource Name (ARN) of the user')
27
+ path: str = Field(..., description='The path to the user')
28
+ create_date: str = Field(..., description='The date and time when the user was created')
29
+ password_last_used: Optional[str] = Field(
30
+ None, description="The date and time when the user's password was last used"
31
+ )
32
+
33
+
34
+ class IamRole(BaseModel):
35
+ """IAM Role model."""
36
+
37
+ role_name: str = Field(..., description='The name of the IAM role')
38
+ role_id: str = Field(..., description='The unique identifier for the role')
39
+ arn: str = Field(..., description='The Amazon Resource Name (ARN) of the role')
40
+ path: str = Field(..., description='The path to the role')
41
+ create_date: str = Field(..., description='The date and time when the role was created')
42
+ assume_role_policy_document: Optional[str] = Field(
43
+ None, description='The trust policy document'
44
+ )
45
+ description: Optional[str] = Field(None, description='The description of the role')
46
+ max_session_duration: Optional[int] = Field(
47
+ None, description='Maximum session duration in seconds'
48
+ )
49
+
50
+
51
+ class IamPolicy(BaseModel):
52
+ """IAM Policy model."""
53
+
54
+ policy_name: str = Field(..., description='The name of the policy')
55
+ policy_id: str = Field(..., description='The unique identifier for the policy')
56
+ arn: str = Field(..., description='The Amazon Resource Name (ARN) of the policy')
57
+ path: str = Field(..., description='The path to the policy')
58
+ default_version_id: str = Field(
59
+ ..., description='The identifier for the default version of the policy'
60
+ )
61
+ attachment_count: int = Field(..., description='The number of entities attached to the policy')
62
+ permissions_boundary_usage_count: int = Field(
63
+ 0, description='The number of entities using this policy as a permissions boundary'
64
+ )
65
+ is_attachable: bool = Field(
66
+ ..., description='Whether the policy can be attached to users, groups, or roles'
67
+ )
68
+ description: Optional[str] = Field(None, description='The description of the policy')
69
+ create_date: str = Field(..., description='The date and time when the policy was created')
70
+ update_date: str = Field(..., description='The date and time when the policy was last updated')
71
+
72
+
73
+ class AccessKey(BaseModel):
74
+ """IAM Access Key model."""
75
+
76
+ access_key_id: str = Field(..., description='The access key ID')
77
+ status: str = Field(..., description='The status of the access key (Active or Inactive)')
78
+ create_date: str = Field(..., description='The date and time when the access key was created')
79
+
80
+
81
+ class AttachedPolicy(BaseModel):
82
+ """Attached Policy model."""
83
+
84
+ policy_name: str = Field(..., description='The name of the policy')
85
+ policy_arn: str = Field(..., description='The ARN of the policy')
86
+
87
+
88
+ class UserDetailsResponse(BaseModel):
89
+ """Response model for detailed user information."""
90
+
91
+ user: IamUser = Field(..., description='User details')
92
+ attached_policies: List[AttachedPolicy] = Field(
93
+ default_factory=list, description='List of attached managed policies'
94
+ )
95
+ inline_policies: List[str] = Field(
96
+ default_factory=list, description='List of inline policy names'
97
+ )
98
+ groups: List[str] = Field(
99
+ default_factory=list, description='List of group names the user belongs to'
100
+ )
101
+ access_keys: List[AccessKey] = Field(
102
+ default_factory=list, description='List of access keys for the user'
103
+ )
104
+
105
+
106
+ class UsersListResponse(BaseModel):
107
+ """Response model for listing users."""
108
+
109
+ users: List[IamUser] = Field(..., description='List of IAM users')
110
+ is_truncated: bool = Field(False, description='Whether the response is truncated')
111
+ marker: Optional[str] = Field(None, description='Marker for pagination')
112
+ count: int = Field(..., description='Number of users returned')
113
+
114
+
115
+ class RolesListResponse(BaseModel):
116
+ """Response model for listing roles."""
117
+
118
+ roles: List[IamRole] = Field(..., description='List of IAM roles')
119
+ is_truncated: bool = Field(False, description='Whether the response is truncated')
120
+ marker: Optional[str] = Field(None, description='Marker for pagination')
121
+ count: int = Field(..., description='Number of roles returned')
122
+
123
+
124
+ class PoliciesListResponse(BaseModel):
125
+ """Response model for listing policies."""
126
+
127
+ policies: List[IamPolicy] = Field(..., description='List of IAM policies')
128
+ is_truncated: bool = Field(False, description='Whether the response is truncated')
129
+ marker: Optional[str] = Field(None, description='Marker for pagination')
130
+ count: int = Field(..., description='Number of policies returned')
131
+
132
+
133
+ class CreateUserResponse(BaseModel):
134
+ """Response model for creating a user."""
135
+
136
+ user: IamUser = Field(..., description='Created user details')
137
+ message: str = Field(..., description='Success message')
138
+
139
+
140
+ class CreateRoleResponse(BaseModel):
141
+ """Response model for creating a role."""
142
+
143
+ role: IamRole = Field(..., description='Created role details')
144
+ message: str = Field(..., description='Success message')
145
+
146
+
147
+ class CreateAccessKeyResponse(BaseModel):
148
+ """Response model for creating an access key."""
149
+
150
+ access_key_id: str = Field(..., description='The access key ID')
151
+ secret_access_key: str = Field(..., description='The secret access key')
152
+ status: str = Field(..., description='The status of the access key')
153
+ user_name: str = Field(..., description='The name of the user')
154
+ create_date: str = Field(..., description='The date and time when the access key was created')
155
+ message: str = Field(..., description='Success message')
156
+ warning: str = Field(..., description='Security warning about storing the secret key')
157
+
158
+
159
+ class OperationResponse(BaseModel):
160
+ """Generic response model for operations."""
161
+
162
+ message: str = Field(..., description='Operation result message')
163
+ details: Optional[Dict[str, Any]] = Field(None, description='Additional operation details')
164
+
165
+
166
+ class PolicyAttachmentResponse(BaseModel):
167
+ """Response model for policy attachment operations."""
168
+
169
+ message: str = Field(..., description='Operation result message')
170
+ user_name: Optional[str] = Field(None, description='The name of the user')
171
+ role_name: Optional[str] = Field(None, description='The name of the role')
172
+ policy_arn: str = Field(..., description='The ARN of the policy')
173
+
174
+
175
+ class SimulationResult(BaseModel):
176
+ """Model for policy simulation result."""
177
+
178
+ eval_action_name: str = Field(..., description='The action that was evaluated')
179
+ eval_resource_name: str = Field(..., description='The resource that was evaluated')
180
+ eval_decision: str = Field(..., description='The result of the evaluation (Allow, Deny, etc.)')
181
+ matched_statements: List[Dict[str, Any]] = Field(
182
+ default_factory=list, description='Statements that matched'
183
+ )
184
+ missing_context_values: List[str] = Field(
185
+ default_factory=list, description='Context values that were missing'
186
+ )
187
+
188
+
189
+ class PolicySimulationResponse(BaseModel):
190
+ """Response model for policy simulation."""
191
+
192
+ evaluation_results: List[SimulationResult] = Field(
193
+ ..., description='List of evaluation results'
194
+ )
195
+ is_truncated: bool = Field(False, description='Whether the response is truncated')
196
+ marker: Optional[str] = Field(None, description='Marker for pagination')
197
+ policy_source_arn: str = Field(..., description='ARN of the principal that was simulated')