iam-policy-validator 1.14.7__py3-none-any.whl → 1.15.0__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.
- {iam_policy_validator-1.14.7.dist-info → iam_policy_validator-1.15.0.dist-info}/METADATA +16 -11
- {iam_policy_validator-1.14.7.dist-info → iam_policy_validator-1.15.0.dist-info}/RECORD +41 -28
- iam_policy_validator-1.15.0.dist-info/entry_points.txt +4 -0
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +2 -0
- iam_validator/checks/action_validation.py +91 -27
- iam_validator/checks/not_action_not_resource.py +163 -0
- iam_validator/checks/resource_validation.py +132 -81
- iam_validator/checks/wildcard_resource.py +136 -6
- iam_validator/commands/__init__.py +3 -0
- iam_validator/commands/cache.py +66 -24
- iam_validator/commands/completion.py +94 -15
- iam_validator/commands/mcp.py +210 -0
- iam_validator/commands/query.py +489 -65
- iam_validator/core/aws_service/__init__.py +5 -1
- iam_validator/core/aws_service/cache.py +20 -0
- iam_validator/core/aws_service/fetcher.py +180 -11
- iam_validator/core/aws_service/storage.py +14 -6
- iam_validator/core/aws_service/validators.py +32 -41
- iam_validator/core/check_registry.py +100 -35
- iam_validator/core/config/aws_global_conditions.py +13 -0
- iam_validator/core/config/check_documentation.py +104 -51
- iam_validator/core/config/config_loader.py +39 -3
- iam_validator/core/config/defaults.py +6 -0
- iam_validator/core/constants.py +11 -4
- iam_validator/core/models.py +39 -14
- iam_validator/mcp/__init__.py +162 -0
- iam_validator/mcp/models.py +118 -0
- iam_validator/mcp/server.py +2928 -0
- iam_validator/mcp/session_config.py +319 -0
- iam_validator/mcp/templates/__init__.py +79 -0
- iam_validator/mcp/templates/builtin.py +856 -0
- iam_validator/mcp/tools/__init__.py +72 -0
- iam_validator/mcp/tools/generation.py +888 -0
- iam_validator/mcp/tools/org_config_tools.py +263 -0
- iam_validator/mcp/tools/query.py +395 -0
- iam_validator/mcp/tools/validation.py +376 -0
- iam_validator/sdk/__init__.py +2 -0
- iam_validator/sdk/policy_utils.py +31 -5
- iam_policy_validator-1.14.7.dist-info/entry_points.txt +0 -2
- {iam_policy_validator-1.14.7.dist-info → iam_policy_validator-1.15.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.14.7.dist-info → iam_policy_validator-1.15.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""Session configuration management for MCP server.
|
|
2
|
+
|
|
3
|
+
This module provides session-scoped configuration management using the core
|
|
4
|
+
ValidatorConfig system. It stores the validator configuration for the MCP
|
|
5
|
+
session lifetime, enabling consistent validation across tool calls.
|
|
6
|
+
|
|
7
|
+
Example usage:
|
|
8
|
+
# Set session config from a YAML file
|
|
9
|
+
SessionConfigManager.load_from_file("/path/to/config.yaml")
|
|
10
|
+
|
|
11
|
+
# Or set from YAML content
|
|
12
|
+
SessionConfigManager.load_from_yaml(yaml_content)
|
|
13
|
+
|
|
14
|
+
# Or set from a dictionary
|
|
15
|
+
SessionConfigManager.set_config({"settings": {"fail_on_severity": ["error", "critical"]}})
|
|
16
|
+
|
|
17
|
+
# Get the current config
|
|
18
|
+
config = SessionConfigManager.get_config()
|
|
19
|
+
if config:
|
|
20
|
+
# Use config for validation
|
|
21
|
+
check_config = config.get_check_config("wildcard_action")
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
import yaml
|
|
27
|
+
|
|
28
|
+
from iam_validator.core.config.config_loader import ValidatorConfig
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SessionConfigManager:
|
|
32
|
+
"""Manages session-scoped configuration for MCP tools.
|
|
33
|
+
|
|
34
|
+
This class provides session-scoped storage for ValidatorConfig.
|
|
35
|
+
The config is stored as a class variable and persists for the lifetime
|
|
36
|
+
of the MCP session.
|
|
37
|
+
|
|
38
|
+
The configuration uses the same schema as the CLI validator, so you can
|
|
39
|
+
use the same YAML configuration files for both CLI and MCP usage.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
_session_config: ValidatorConfig | None = None
|
|
43
|
+
_config_source: str = "none"
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def set_config(cls, config_dict: dict[str, Any], source: str = "session") -> ValidatorConfig:
|
|
47
|
+
"""Set the session configuration from a dictionary.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
config_dict: Configuration dictionary (same format as YAML config files)
|
|
51
|
+
source: Source identifier ("session", "yaml", "file")
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
The created ValidatorConfig instance
|
|
55
|
+
"""
|
|
56
|
+
cls._session_config = ValidatorConfig(config_dict, use_defaults=True)
|
|
57
|
+
cls._config_source = source
|
|
58
|
+
return cls._session_config
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def get_config(cls) -> ValidatorConfig | None:
|
|
62
|
+
"""Get the current session configuration.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Current ValidatorConfig, or None if not set
|
|
66
|
+
"""
|
|
67
|
+
return cls._session_config
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def get_config_source(cls) -> str:
|
|
71
|
+
"""Get the source of the current configuration.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Source identifier: "session", "yaml", "file", or "none"
|
|
75
|
+
"""
|
|
76
|
+
return cls._config_source
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def clear_config(cls) -> bool:
|
|
80
|
+
"""Clear the session configuration.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
True if config was cleared, False if no config was set
|
|
84
|
+
"""
|
|
85
|
+
had_config = cls._session_config is not None
|
|
86
|
+
cls._session_config = None
|
|
87
|
+
cls._config_source = "none"
|
|
88
|
+
return had_config
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def has_config(cls) -> bool:
|
|
92
|
+
"""Check if a session configuration is set.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
True if config is set, False otherwise
|
|
96
|
+
"""
|
|
97
|
+
return cls._session_config is not None
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def load_from_yaml(cls, yaml_content: str) -> tuple[ValidatorConfig, list[str]]:
|
|
101
|
+
"""Load session configuration from YAML content.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
yaml_content: YAML string containing configuration
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Tuple of (ValidatorConfig, list of warnings)
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
ValueError: If YAML parsing or validation fails
|
|
111
|
+
"""
|
|
112
|
+
warnings: list[str] = []
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
config_dict = yaml.safe_load(yaml_content)
|
|
116
|
+
except yaml.YAMLError as e:
|
|
117
|
+
raise ValueError(f"Invalid YAML: {e}") from e
|
|
118
|
+
|
|
119
|
+
if not isinstance(config_dict, dict):
|
|
120
|
+
raise ValueError("YAML content must be a dictionary")
|
|
121
|
+
|
|
122
|
+
# Support legacy "organization" key for backwards compatibility
|
|
123
|
+
if "organization" in config_dict:
|
|
124
|
+
org_config = config_dict.pop("organization")
|
|
125
|
+
# Merge organization settings into settings
|
|
126
|
+
if "settings" not in config_dict:
|
|
127
|
+
config_dict["settings"] = {}
|
|
128
|
+
config_dict["settings"].update(org_config)
|
|
129
|
+
warnings.append("Migrated 'organization' key to 'settings'")
|
|
130
|
+
|
|
131
|
+
# Extract custom_instructions if present (MCP-specific setting)
|
|
132
|
+
if "custom_instructions" in config_dict:
|
|
133
|
+
custom_instructions = config_dict.pop("custom_instructions")
|
|
134
|
+
if isinstance(custom_instructions, str) and custom_instructions.strip():
|
|
135
|
+
CustomInstructionsManager.set_instructions(custom_instructions, source="config")
|
|
136
|
+
warnings.append("Loaded custom instructions from config")
|
|
137
|
+
|
|
138
|
+
config = cls.set_config(config_dict, source="yaml")
|
|
139
|
+
return config, warnings
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def load_from_file(cls, file_path: str) -> tuple[ValidatorConfig, list[str]]:
|
|
143
|
+
"""Load session configuration from a YAML file.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
file_path: Path to YAML configuration file
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Tuple of (ValidatorConfig, list of warnings)
|
|
150
|
+
|
|
151
|
+
Raises:
|
|
152
|
+
ValueError: If file reading or parsing fails
|
|
153
|
+
FileNotFoundError: If file doesn't exist
|
|
154
|
+
"""
|
|
155
|
+
from pathlib import Path
|
|
156
|
+
|
|
157
|
+
path = Path(file_path)
|
|
158
|
+
if not path.exists():
|
|
159
|
+
raise FileNotFoundError(f"Configuration file not found: {file_path}")
|
|
160
|
+
|
|
161
|
+
yaml_content = path.read_text()
|
|
162
|
+
config, warnings = cls.load_from_yaml(yaml_content)
|
|
163
|
+
cls._config_source = "file"
|
|
164
|
+
return config, warnings
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def merge_conditions(
|
|
168
|
+
base_conditions: dict[str, Any] | None,
|
|
169
|
+
required_conditions: dict[str, Any],
|
|
170
|
+
) -> dict[str, Any]:
|
|
171
|
+
"""Merge required conditions into base conditions.
|
|
172
|
+
|
|
173
|
+
This performs a deep merge of condition blocks, combining operators
|
|
174
|
+
and their nested conditions appropriately.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
base_conditions: Existing conditions (may be None)
|
|
178
|
+
required_conditions: Required conditions to merge in
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Merged conditions dictionary
|
|
182
|
+
"""
|
|
183
|
+
if not required_conditions:
|
|
184
|
+
return base_conditions or {}
|
|
185
|
+
|
|
186
|
+
if not base_conditions:
|
|
187
|
+
return required_conditions.copy()
|
|
188
|
+
|
|
189
|
+
result = base_conditions.copy()
|
|
190
|
+
|
|
191
|
+
for operator, conditions in required_conditions.items():
|
|
192
|
+
if operator in result:
|
|
193
|
+
# Merge conditions under the same operator
|
|
194
|
+
if isinstance(result[operator], dict) and isinstance(conditions, dict):
|
|
195
|
+
result[operator] = {**result[operator], **conditions}
|
|
196
|
+
else:
|
|
197
|
+
# Can't merge non-dict values, required takes precedence
|
|
198
|
+
result[operator] = conditions
|
|
199
|
+
else:
|
|
200
|
+
result[operator] = conditions
|
|
201
|
+
|
|
202
|
+
return result
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class CustomInstructionsManager:
|
|
206
|
+
"""Manages custom LLM instructions for the MCP server.
|
|
207
|
+
|
|
208
|
+
Custom instructions are appended to the default MCP server instructions,
|
|
209
|
+
allowing organizations to add their own policy generation guidelines.
|
|
210
|
+
|
|
211
|
+
Instructions can be set via:
|
|
212
|
+
- YAML config file (custom_instructions key)
|
|
213
|
+
- Environment variable (IAM_VALIDATOR_MCP_INSTRUCTIONS)
|
|
214
|
+
- CLI argument (--instructions or --instructions-file)
|
|
215
|
+
- MCP tool (set_custom_instructions)
|
|
216
|
+
|
|
217
|
+
Example YAML config:
|
|
218
|
+
custom_instructions: |
|
|
219
|
+
## Organization-Specific Rules
|
|
220
|
+
- All policies must include aws:PrincipalOrgID condition
|
|
221
|
+
- Use resource tags for access control where possible
|
|
222
|
+
- S3 buckets must have encryption conditions
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
_custom_instructions: str | None = None
|
|
226
|
+
_instructions_source: str = "none"
|
|
227
|
+
|
|
228
|
+
@classmethod
|
|
229
|
+
def set_instructions(cls, instructions: str, source: str = "api") -> None:
|
|
230
|
+
"""Set custom instructions.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
instructions: Custom instructions text (markdown supported)
|
|
234
|
+
source: Source identifier ("api", "env", "file", "config")
|
|
235
|
+
"""
|
|
236
|
+
stripped = instructions.strip() if instructions else ""
|
|
237
|
+
cls._custom_instructions = stripped if stripped else None
|
|
238
|
+
cls._instructions_source = source if cls._custom_instructions else "none"
|
|
239
|
+
|
|
240
|
+
@classmethod
|
|
241
|
+
def get_instructions(cls) -> str | None:
|
|
242
|
+
"""Get custom instructions.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Custom instructions string, or None if not set
|
|
246
|
+
"""
|
|
247
|
+
return cls._custom_instructions
|
|
248
|
+
|
|
249
|
+
@classmethod
|
|
250
|
+
def get_source(cls) -> str:
|
|
251
|
+
"""Get the source of custom instructions.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Source identifier: "api", "env", "file", "config", or "none"
|
|
255
|
+
"""
|
|
256
|
+
return cls._instructions_source
|
|
257
|
+
|
|
258
|
+
@classmethod
|
|
259
|
+
def clear_instructions(cls) -> bool:
|
|
260
|
+
"""Clear custom instructions.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
True if instructions were cleared, False if none were set
|
|
264
|
+
"""
|
|
265
|
+
had_instructions = cls._custom_instructions is not None
|
|
266
|
+
cls._custom_instructions = None
|
|
267
|
+
cls._instructions_source = "none"
|
|
268
|
+
return had_instructions
|
|
269
|
+
|
|
270
|
+
@classmethod
|
|
271
|
+
def has_instructions(cls) -> bool:
|
|
272
|
+
"""Check if custom instructions are set.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
True if instructions are set, False otherwise
|
|
276
|
+
"""
|
|
277
|
+
return cls._custom_instructions is not None
|
|
278
|
+
|
|
279
|
+
@classmethod
|
|
280
|
+
def load_from_env(cls) -> bool:
|
|
281
|
+
"""Load custom instructions from environment variable.
|
|
282
|
+
|
|
283
|
+
Checks IAM_VALIDATOR_MCP_INSTRUCTIONS environment variable.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
True if instructions were loaded, False otherwise
|
|
287
|
+
"""
|
|
288
|
+
import os
|
|
289
|
+
|
|
290
|
+
env_instructions = os.environ.get("IAM_VALIDATOR_MCP_INSTRUCTIONS")
|
|
291
|
+
if env_instructions:
|
|
292
|
+
cls.set_instructions(env_instructions, source="env")
|
|
293
|
+
return True
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
@classmethod
|
|
297
|
+
def load_from_file(cls, file_path: str) -> None:
|
|
298
|
+
"""Load custom instructions from a file.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
file_path: Path to file containing instructions (markdown, txt)
|
|
302
|
+
|
|
303
|
+
Raises:
|
|
304
|
+
FileNotFoundError: If file doesn't exist
|
|
305
|
+
"""
|
|
306
|
+
from pathlib import Path
|
|
307
|
+
|
|
308
|
+
path = Path(file_path)
|
|
309
|
+
if not path.exists():
|
|
310
|
+
raise FileNotFoundError(f"Instructions file not found: {file_path}")
|
|
311
|
+
|
|
312
|
+
cls.set_instructions(path.read_text(), source="file")
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
__all__ = [
|
|
316
|
+
"SessionConfigManager",
|
|
317
|
+
"CustomInstructionsManager",
|
|
318
|
+
"merge_conditions",
|
|
319
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""IAM policy templates for MCP server.
|
|
2
|
+
|
|
3
|
+
This package provides built-in policy templates that can be used to generate
|
|
4
|
+
common IAM policies with variable substitution. Templates are validated
|
|
5
|
+
through the security enforcement layer before being returned.
|
|
6
|
+
|
|
7
|
+
Available templates:
|
|
8
|
+
- s3-read-only: S3 bucket read-only access
|
|
9
|
+
- s3-read-write: S3 bucket read-write access
|
|
10
|
+
- lambda-basic-execution: Basic Lambda execution role
|
|
11
|
+
- lambda-s3-trigger: Lambda with S3 event trigger permissions
|
|
12
|
+
- dynamodb-crud: DynamoDB table CRUD operations
|
|
13
|
+
- cloudwatch-logs: CloudWatch Logs write permissions
|
|
14
|
+
- secrets-manager-read: Secrets Manager read access
|
|
15
|
+
- kms-encrypt-decrypt: KMS key encryption/decryption
|
|
16
|
+
- ec2-describe: EC2 describe-only permissions
|
|
17
|
+
- ecs-task-execution: ECS task execution role
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from .builtin import get_template as _get_template
|
|
21
|
+
from .builtin import list_templates as _list_templates
|
|
22
|
+
from .builtin import render_template as _render_template
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def list_templates() -> list[str]:
|
|
26
|
+
"""List all available template names.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
List of template names that can be used with load_template()
|
|
30
|
+
"""
|
|
31
|
+
templates = _list_templates()
|
|
32
|
+
return [t["name"] for t in templates]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_template_variables(template_name: str) -> list[str]:
|
|
36
|
+
"""Get the required variables for a template.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
template_name: Name of the template
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
List of variable names required by the template
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ValueError: If template_name is not found
|
|
46
|
+
"""
|
|
47
|
+
template = _get_template(template_name)
|
|
48
|
+
if not template:
|
|
49
|
+
raise ValueError(
|
|
50
|
+
f"Template '{template_name}' not found. "
|
|
51
|
+
f"Available templates: {', '.join(list_templates())}"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return [var["name"] for var in template["variables"]]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def load_template(template_name: str, variables: dict[str, str] | None = None) -> dict:
|
|
58
|
+
"""Load a template and substitute variables.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
template_name: Name of the template to load
|
|
62
|
+
variables: Dictionary of variable values to substitute
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Policy dictionary with variables substituted
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
ValueError: If template_name is not found or required variables are missing
|
|
69
|
+
"""
|
|
70
|
+
if variables is None:
|
|
71
|
+
variables = {}
|
|
72
|
+
return _render_template(template_name, variables)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
__all__ = [
|
|
76
|
+
"list_templates",
|
|
77
|
+
"get_template_variables",
|
|
78
|
+
"load_template",
|
|
79
|
+
]
|