mcp-security-framework 1.1.0__py3-none-any.whl → 1.1.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.
- mcp_security_framework/__init__.py +26 -15
- mcp_security_framework/cli/__init__.py +1 -1
- mcp_security_framework/cli/cert_cli.py +233 -197
- mcp_security_framework/cli/security_cli.py +324 -234
- mcp_security_framework/constants.py +21 -27
- mcp_security_framework/core/auth_manager.py +41 -22
- mcp_security_framework/core/cert_manager.py +210 -147
- mcp_security_framework/core/permission_manager.py +9 -9
- mcp_security_framework/core/rate_limiter.py +2 -2
- mcp_security_framework/core/security_manager.py +284 -229
- mcp_security_framework/examples/__init__.py +6 -0
- mcp_security_framework/examples/comprehensive_example.py +349 -279
- mcp_security_framework/examples/django_example.py +247 -206
- mcp_security_framework/examples/fastapi_example.py +315 -283
- mcp_security_framework/examples/flask_example.py +274 -203
- mcp_security_framework/examples/gateway_example.py +304 -237
- mcp_security_framework/examples/microservice_example.py +258 -189
- mcp_security_framework/examples/standalone_example.py +255 -230
- mcp_security_framework/examples/test_all_examples.py +151 -135
- mcp_security_framework/middleware/__init__.py +46 -55
- mcp_security_framework/middleware/auth_middleware.py +62 -63
- mcp_security_framework/middleware/fastapi_auth_middleware.py +119 -118
- mcp_security_framework/middleware/fastapi_middleware.py +156 -148
- mcp_security_framework/middleware/flask_auth_middleware.py +160 -147
- mcp_security_framework/middleware/flask_middleware.py +183 -157
- mcp_security_framework/middleware/mtls_middleware.py +106 -117
- mcp_security_framework/middleware/rate_limit_middleware.py +105 -101
- mcp_security_framework/middleware/security_middleware.py +109 -124
- mcp_security_framework/schemas/config.py +2 -1
- mcp_security_framework/schemas/models.py +18 -6
- mcp_security_framework/utils/cert_utils.py +14 -8
- mcp_security_framework/utils/datetime_compat.py +116 -0
- {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.2.dist-info}/METADATA +4 -3
- mcp_security_framework-1.1.2.dist-info/RECORD +84 -0
- tests/conftest.py +63 -66
- tests/test_cli/test_cert_cli.py +184 -146
- tests/test_cli/test_security_cli.py +274 -247
- tests/test_core/test_cert_manager.py +24 -10
- tests/test_core/test_security_manager.py +2 -2
- tests/test_examples/test_comprehensive_example.py +190 -137
- tests/test_examples/test_fastapi_example.py +124 -101
- tests/test_examples/test_flask_example.py +124 -101
- tests/test_examples/test_standalone_example.py +73 -80
- tests/test_integration/test_auth_flow.py +213 -197
- tests/test_integration/test_certificate_flow.py +180 -149
- tests/test_integration/test_fastapi_integration.py +108 -111
- tests/test_integration/test_flask_integration.py +141 -140
- tests/test_integration/test_standalone_integration.py +290 -259
- tests/test_middleware/test_fastapi_auth_middleware.py +195 -174
- tests/test_middleware/test_fastapi_middleware.py +147 -132
- tests/test_middleware/test_flask_auth_middleware.py +260 -202
- tests/test_middleware/test_flask_middleware.py +201 -179
- tests/test_middleware/test_security_middleware.py +145 -130
- tests/test_utils/test_datetime_compat.py +147 -0
- mcp_security_framework-1.1.0.dist-info/RECORD +0 -82
- {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.2.dist-info}/WHEEL +0 -0
- {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.2.dist-info}/entry_points.txt +0 -0
- {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.2.dist-info}/top_level.txt +0 -0
@@ -24,50 +24,57 @@ Version: 1.0.0
|
|
24
24
|
License: MIT
|
25
25
|
"""
|
26
26
|
|
27
|
-
import click
|
28
27
|
import json
|
29
28
|
import os
|
30
|
-
from pathlib import Path
|
31
|
-
from typing import Optional, List
|
32
29
|
from datetime import datetime
|
30
|
+
from pathlib import Path
|
31
|
+
from typing import List, Optional
|
32
|
+
|
33
|
+
import click
|
33
34
|
|
34
35
|
from ..core.security_manager import SecurityManager
|
35
|
-
from ..schemas.config import
|
36
|
+
from ..schemas.config import (
|
37
|
+
AuthConfig,
|
38
|
+
PermissionConfig,
|
39
|
+
RateLimitConfig,
|
40
|
+
SecurityConfig,
|
41
|
+
SSLConfig,
|
42
|
+
)
|
36
43
|
from ..schemas.models import AuthResult, AuthStatus
|
37
44
|
|
38
45
|
|
39
46
|
@click.group()
|
40
|
-
@click.option(
|
41
|
-
|
42
|
-
|
43
|
-
|
47
|
+
@click.option(
|
48
|
+
"--config", "-c", "config_path", help="Path to security configuration file"
|
49
|
+
)
|
50
|
+
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
|
44
51
|
@click.pass_context
|
45
52
|
def security_cli(ctx, config_path: Optional[str], verbose: bool):
|
46
53
|
"""
|
47
54
|
Security Management CLI
|
48
|
-
|
55
|
+
|
49
56
|
Manage security operations including authentication, authorization,
|
50
57
|
and security configuration.
|
51
58
|
"""
|
52
59
|
ctx.ensure_object(dict)
|
53
|
-
ctx.obj[
|
54
|
-
|
60
|
+
ctx.obj["verbose"] = verbose
|
61
|
+
|
55
62
|
# Load configuration
|
56
63
|
if config_path and os.path.exists(config_path):
|
57
|
-
with open(config_path,
|
64
|
+
with open(config_path, "r") as f:
|
58
65
|
config_data = json.load(f)
|
59
|
-
ctx.obj[
|
66
|
+
ctx.obj["config"] = SecurityConfig(**config_data)
|
60
67
|
else:
|
61
68
|
# Use default configuration
|
62
|
-
ctx.obj[
|
69
|
+
ctx.obj["config"] = SecurityConfig(
|
63
70
|
auth=AuthConfig(enabled=True),
|
64
71
|
rate_limit=RateLimitConfig(enabled=True),
|
65
72
|
ssl=SSLConfig(enabled=False),
|
66
|
-
permissions=PermissionConfig(enabled=True)
|
73
|
+
permissions=PermissionConfig(enabled=True),
|
67
74
|
)
|
68
|
-
|
75
|
+
|
69
76
|
# Create security manager
|
70
|
-
ctx.obj[
|
77
|
+
ctx.obj["security_manager"] = SecurityManager(ctx.obj["config"])
|
71
78
|
|
72
79
|
|
73
80
|
@security_cli.group()
|
@@ -75,7 +82,7 @@ def security_cli(ctx, config_path: Optional[str], verbose: bool):
|
|
75
82
|
def auth(ctx):
|
76
83
|
"""
|
77
84
|
Authentication operations.
|
78
|
-
|
85
|
+
|
79
86
|
Manage authentication including API keys, user authentication,
|
80
87
|
and authentication testing.
|
81
88
|
"""
|
@@ -83,92 +90,88 @@ def auth(ctx):
|
|
83
90
|
|
84
91
|
|
85
92
|
@auth.command()
|
86
|
-
@click.option(
|
87
|
-
|
88
|
-
@click.option('--api-key', '-k', required=True,
|
89
|
-
help='API key value')
|
93
|
+
@click.option("--username", "-u", required=True, help="Username for the API key")
|
94
|
+
@click.option("--api-key", "-k", required=True, help="API key value")
|
90
95
|
@click.pass_context
|
91
96
|
def add_api_key(ctx, username: str, api_key: str):
|
92
97
|
"""
|
93
98
|
Add an API key for a user.
|
94
|
-
|
99
|
+
|
95
100
|
This command adds an API key to the authentication system
|
96
101
|
for the specified user.
|
97
102
|
"""
|
98
103
|
try:
|
99
|
-
security_manager = ctx.obj[
|
100
|
-
verbose = ctx.obj[
|
101
|
-
|
104
|
+
security_manager = ctx.obj["security_manager"]
|
105
|
+
verbose = ctx.obj["verbose"]
|
106
|
+
|
102
107
|
if verbose:
|
103
108
|
click.echo(f"Adding API key for user: {username}")
|
104
|
-
|
109
|
+
|
105
110
|
# Add API key
|
106
111
|
success = security_manager.auth_manager.add_api_key(username, api_key)
|
107
|
-
|
112
|
+
|
108
113
|
if success:
|
109
114
|
click.echo(f"✅ API key added successfully for user: {username}")
|
110
115
|
else:
|
111
116
|
click.echo(f"❌ Failed to add API key for user: {username}", err=True)
|
112
117
|
raise click.Abort()
|
113
|
-
|
118
|
+
|
114
119
|
except Exception as e:
|
115
120
|
click.echo(f"❌ Failed to add API key: {str(e)}", err=True)
|
116
121
|
raise click.Abort()
|
117
122
|
|
118
123
|
|
119
124
|
@auth.command()
|
120
|
-
@click.option(
|
121
|
-
help='Username to remove API key for')
|
125
|
+
@click.option("--username", "-u", required=True, help="Username to remove API key for")
|
122
126
|
@click.pass_context
|
123
127
|
def remove_api_key(ctx, username: str):
|
124
128
|
"""
|
125
129
|
Remove an API key for a user.
|
126
|
-
|
130
|
+
|
127
131
|
This command removes an API key from the authentication system
|
128
132
|
for the specified user.
|
129
133
|
"""
|
130
134
|
try:
|
131
|
-
security_manager = ctx.obj[
|
132
|
-
verbose = ctx.obj[
|
133
|
-
|
135
|
+
security_manager = ctx.obj["security_manager"]
|
136
|
+
verbose = ctx.obj["verbose"]
|
137
|
+
|
134
138
|
if verbose:
|
135
139
|
click.echo(f"Removing API key for user: {username}")
|
136
|
-
|
140
|
+
|
137
141
|
# Remove API key
|
138
142
|
success = security_manager.auth_manager.remove_api_key(username)
|
139
|
-
|
143
|
+
|
140
144
|
if success:
|
141
145
|
click.echo(f"✅ API key removed successfully for user: {username}")
|
142
146
|
else:
|
143
147
|
click.echo(f"❌ Failed to remove API key for user: {username}", err=True)
|
144
148
|
raise click.Abort()
|
145
|
-
|
149
|
+
|
146
150
|
except Exception as e:
|
147
151
|
click.echo(f"❌ Failed to remove API key: {str(e)}", err=True)
|
148
152
|
raise click.Abort()
|
149
153
|
|
150
154
|
|
151
155
|
@auth.command()
|
152
|
-
@click.option(
|
153
|
-
help='API key to test')
|
156
|
+
@click.option("--api-key", "-k", required=True, help="API key to test")
|
154
157
|
@click.pass_context
|
155
158
|
def test_api_key(ctx, api_key: str):
|
156
159
|
"""
|
157
160
|
Test API key authentication.
|
158
|
-
|
161
|
+
|
159
162
|
This command tests if an API key is valid and returns
|
160
163
|
authentication information.
|
161
164
|
"""
|
162
165
|
try:
|
163
|
-
security_manager = ctx.obj[
|
164
|
-
verbose = ctx.obj[
|
165
|
-
|
166
|
+
security_manager = ctx.obj["security_manager"]
|
167
|
+
verbose = ctx.obj["verbose"]
|
168
|
+
|
166
169
|
if verbose:
|
167
170
|
click.echo(f"Testing API key authentication")
|
168
|
-
|
171
|
+
|
169
172
|
# Test API key
|
170
173
|
auth_result = security_manager.auth_manager.authenticate_api_key(api_key)
|
171
|
-
|
174
|
+
|
172
175
|
if auth_result.is_valid:
|
173
176
|
click.echo(f"✅ API key authentication successful!")
|
174
177
|
click.echo(f" Username: {auth_result.username}")
|
@@ -178,33 +181,32 @@ def test_api_key(ctx, api_key: str):
|
|
178
181
|
click.echo(f"❌ API key authentication failed!", err=True)
|
179
182
|
click.echo(f" Error: {auth_result.error_message}")
|
180
183
|
raise click.Abort()
|
181
|
-
|
184
|
+
|
182
185
|
except Exception as e:
|
183
186
|
click.echo(f"❌ API key authentication failed: {str(e)}", err=True)
|
184
187
|
raise click.Abort()
|
185
188
|
|
186
189
|
|
187
190
|
@auth.command()
|
188
|
-
@click.option(
|
189
|
-
help='JWT token to test')
|
191
|
+
@click.option("--token", "-t", required=True, help="JWT token to test")
|
190
192
|
@click.pass_context
|
191
193
|
def test_jwt(ctx, token: str):
|
192
194
|
"""
|
193
195
|
Test JWT token authentication.
|
194
|
-
|
196
|
+
|
195
197
|
This command tests if a JWT token is valid and returns
|
196
198
|
authentication information.
|
197
199
|
"""
|
198
200
|
try:
|
199
|
-
security_manager = ctx.obj[
|
200
|
-
verbose = ctx.obj[
|
201
|
-
|
201
|
+
security_manager = ctx.obj["security_manager"]
|
202
|
+
verbose = ctx.obj["verbose"]
|
203
|
+
|
202
204
|
if verbose:
|
203
205
|
click.echo(f"Testing JWT token authentication")
|
204
|
-
|
206
|
+
|
205
207
|
# Test JWT token
|
206
208
|
auth_result = security_manager.auth_manager.authenticate_jwt_token(token)
|
207
|
-
|
209
|
+
|
208
210
|
if auth_result.is_valid:
|
209
211
|
click.echo(f"✅ JWT token authentication successful!")
|
210
212
|
click.echo(f" Username: {auth_result.username}")
|
@@ -214,7 +216,7 @@ def test_jwt(ctx, token: str):
|
|
214
216
|
click.echo(f"❌ JWT token authentication failed!", err=True)
|
215
217
|
click.echo(f" Error: {auth_result.error_message}")
|
216
218
|
raise click.Abort()
|
217
|
-
|
219
|
+
|
218
220
|
except Exception as e:
|
219
221
|
click.echo(f"❌ JWT token authentication failed: {str(e)}", err=True)
|
220
222
|
raise click.Abort()
|
@@ -225,7 +227,7 @@ def test_jwt(ctx, token: str):
|
|
225
227
|
def permissions(ctx):
|
226
228
|
"""
|
227
229
|
Permission management operations.
|
228
|
-
|
230
|
+
|
229
231
|
Manage permissions including role assignment, permission validation,
|
230
232
|
and permission testing.
|
231
233
|
"""
|
@@ -233,37 +235,43 @@ def permissions(ctx):
|
|
233
235
|
|
234
236
|
|
235
237
|
@permissions.command()
|
236
|
-
@click.option(
|
237
|
-
|
238
|
-
|
239
|
-
|
238
|
+
@click.option(
|
239
|
+
"--username", "-u", required=True, help="Username to check permissions for"
|
240
|
+
)
|
241
|
+
@click.option(
|
242
|
+
"--permissions",
|
243
|
+
"-p",
|
244
|
+
multiple=True,
|
245
|
+
required=True,
|
246
|
+
help="Permissions to check (can be specified multiple times)",
|
247
|
+
)
|
240
248
|
@click.pass_context
|
241
249
|
def check(ctx, username: str, permissions: tuple):
|
242
250
|
"""
|
243
251
|
Check user permissions.
|
244
|
-
|
252
|
+
|
245
253
|
This command checks if a user has the specified permissions.
|
246
254
|
"""
|
247
255
|
try:
|
248
|
-
security_manager = ctx.obj[
|
249
|
-
verbose = ctx.obj[
|
256
|
+
security_manager = ctx.obj["security_manager"]
|
257
|
+
verbose = ctx.obj["verbose"]
|
250
258
|
permissions_list = list(permissions)
|
251
|
-
|
259
|
+
|
252
260
|
if verbose:
|
253
261
|
click.echo(f"Checking permissions for user: {username}")
|
254
262
|
click.echo(f"Required permissions: {', '.join(permissions_list)}")
|
255
|
-
|
263
|
+
|
256
264
|
# Get user roles
|
257
265
|
user_roles = security_manager.auth_manager._get_user_roles(username)
|
258
|
-
|
266
|
+
|
259
267
|
if verbose:
|
260
268
|
click.echo(f"User roles: {', '.join(user_roles)}")
|
261
|
-
|
269
|
+
|
262
270
|
# Check permissions
|
263
271
|
has_permissions = security_manager.permission_manager.validate_access(
|
264
272
|
user_roles, permissions_list
|
265
273
|
)
|
266
|
-
|
274
|
+
|
267
275
|
if has_permissions.is_valid:
|
268
276
|
click.echo(f"✅ User has all required permissions!")
|
269
277
|
click.echo(f" Username: {username}")
|
@@ -273,41 +281,42 @@ def check(ctx, username: str, permissions: tuple):
|
|
273
281
|
click.echo(f"❌ User does not have required permissions!", err=True)
|
274
282
|
click.echo(f" Username: {username}")
|
275
283
|
click.echo(f" Roles: {', '.join(user_roles)}")
|
276
|
-
click.echo(
|
284
|
+
click.echo(
|
285
|
+
f" Missing permissions: {', '.join(has_permissions.missing_permissions)}"
|
286
|
+
)
|
277
287
|
raise click.Abort()
|
278
|
-
|
288
|
+
|
279
289
|
except Exception as e:
|
280
290
|
click.echo(f"❌ Permission check failed: {str(e)}", err=True)
|
281
291
|
raise click.Abort()
|
282
292
|
|
283
293
|
|
284
294
|
@permissions.command()
|
285
|
-
@click.option(
|
286
|
-
help='Role name')
|
295
|
+
@click.option("--role", "-r", required=True, help="Role name")
|
287
296
|
@click.pass_context
|
288
297
|
def list_role_permissions(ctx, role: str):
|
289
298
|
"""
|
290
299
|
List permissions for a role.
|
291
|
-
|
300
|
+
|
292
301
|
This command displays all permissions assigned to a specific role.
|
293
302
|
"""
|
294
303
|
try:
|
295
|
-
security_manager = ctx.obj[
|
296
|
-
verbose = ctx.obj[
|
297
|
-
|
304
|
+
security_manager = ctx.obj["security_manager"]
|
305
|
+
verbose = ctx.obj["verbose"]
|
306
|
+
|
298
307
|
if verbose:
|
299
308
|
click.echo(f"Listing permissions for role: {role}")
|
300
|
-
|
309
|
+
|
301
310
|
# Get role permissions
|
302
311
|
permissions = security_manager.permission_manager.get_role_permissions(role)
|
303
|
-
|
312
|
+
|
304
313
|
if permissions:
|
305
314
|
click.echo(f"Permissions for role '{role}':")
|
306
315
|
for permission in permissions:
|
307
316
|
click.echo(f" - {permission}")
|
308
317
|
else:
|
309
318
|
click.echo(f"No permissions found for role: {role}")
|
310
|
-
|
319
|
+
|
311
320
|
except Exception as e:
|
312
321
|
click.echo(f"❌ Failed to list role permissions: {str(e)}", err=True)
|
313
322
|
raise click.Abort()
|
@@ -318,7 +327,7 @@ def list_role_permissions(ctx, role: str):
|
|
318
327
|
def rate_limit(ctx):
|
319
328
|
"""
|
320
329
|
Rate limiting operations.
|
321
|
-
|
330
|
+
|
322
331
|
Manage rate limiting including checking limits, resetting limits,
|
323
332
|
and monitoring rate limit status.
|
324
333
|
"""
|
@@ -326,90 +335,96 @@ def rate_limit(ctx):
|
|
326
335
|
|
327
336
|
|
328
337
|
@rate_limit.command()
|
329
|
-
@click.option(
|
330
|
-
|
338
|
+
@click.option(
|
339
|
+
"--identifier",
|
340
|
+
"-i",
|
341
|
+
required=True,
|
342
|
+
help="Rate limit identifier (IP, user ID, etc.)",
|
343
|
+
)
|
331
344
|
@click.pass_context
|
332
345
|
def check(ctx, identifier: str):
|
333
346
|
"""
|
334
347
|
Check rate limit status.
|
335
|
-
|
348
|
+
|
336
349
|
This command checks the current rate limit status for an identifier.
|
337
350
|
"""
|
338
351
|
try:
|
339
|
-
security_manager = ctx.obj[
|
340
|
-
verbose = ctx.obj[
|
341
|
-
|
352
|
+
security_manager = ctx.obj["security_manager"]
|
353
|
+
verbose = ctx.obj["verbose"]
|
354
|
+
|
342
355
|
if verbose:
|
343
356
|
click.echo(f"Checking rate limit for identifier: {identifier}")
|
344
|
-
|
357
|
+
|
345
358
|
# Check rate limit
|
346
359
|
is_allowed = security_manager.rate_limiter.check_rate_limit(identifier)
|
347
|
-
|
360
|
+
|
348
361
|
if is_allowed:
|
349
362
|
click.echo(f"✅ Rate limit check passed for: {identifier}")
|
350
363
|
else:
|
351
364
|
click.echo(f"❌ Rate limit exceeded for: {identifier}", err=True)
|
352
365
|
raise click.Abort()
|
353
|
-
|
366
|
+
|
354
367
|
except Exception as e:
|
355
368
|
click.echo(f"❌ Rate limit check failed: {str(e)}", err=True)
|
356
369
|
raise click.Abort()
|
357
370
|
|
358
371
|
|
359
372
|
@rate_limit.command()
|
360
|
-
@click.option(
|
361
|
-
|
373
|
+
@click.option(
|
374
|
+
"--identifier", "-i", required=True, help="Rate limit identifier to reset"
|
375
|
+
)
|
362
376
|
@click.pass_context
|
363
377
|
def reset(ctx, identifier: str):
|
364
378
|
"""
|
365
379
|
Reset rate limit for an identifier.
|
366
|
-
|
380
|
+
|
367
381
|
This command resets the rate limit counter for an identifier.
|
368
382
|
"""
|
369
383
|
try:
|
370
|
-
security_manager = ctx.obj[
|
371
|
-
verbose = ctx.obj[
|
372
|
-
|
384
|
+
security_manager = ctx.obj["security_manager"]
|
385
|
+
verbose = ctx.obj["verbose"]
|
386
|
+
|
373
387
|
if verbose:
|
374
388
|
click.echo(f"Resetting rate limit for identifier: {identifier}")
|
375
|
-
|
389
|
+
|
376
390
|
# Reset rate limit
|
377
391
|
security_manager.rate_limiter.reset_rate_limit(identifier)
|
378
|
-
|
392
|
+
|
379
393
|
click.echo(f"✅ Rate limit reset successfully for: {identifier}")
|
380
|
-
|
394
|
+
|
381
395
|
except Exception as e:
|
382
396
|
click.echo(f"❌ Failed to reset rate limit: {str(e)}", err=True)
|
383
397
|
raise click.Abort()
|
384
398
|
|
385
399
|
|
386
400
|
@rate_limit.command()
|
387
|
-
@click.option(
|
388
|
-
|
401
|
+
@click.option(
|
402
|
+
"--identifier", "-i", required=True, help="Rate limit identifier to get status for"
|
403
|
+
)
|
389
404
|
@click.pass_context
|
390
405
|
def status(ctx, identifier: str):
|
391
406
|
"""
|
392
407
|
Get rate limit status for an identifier.
|
393
|
-
|
408
|
+
|
394
409
|
This command displays detailed rate limit status information.
|
395
410
|
"""
|
396
411
|
try:
|
397
|
-
security_manager = ctx.obj[
|
398
|
-
verbose = ctx.obj[
|
399
|
-
|
412
|
+
security_manager = ctx.obj["security_manager"]
|
413
|
+
verbose = ctx.obj["verbose"]
|
414
|
+
|
400
415
|
if verbose:
|
401
416
|
click.echo(f"Getting rate limit status for identifier: {identifier}")
|
402
|
-
|
417
|
+
|
403
418
|
# Get rate limit status
|
404
419
|
status = security_manager.rate_limiter.get_rate_limit_status(identifier)
|
405
|
-
|
420
|
+
|
406
421
|
click.echo(f"Rate Limit Status for '{identifier}':")
|
407
422
|
click.echo(f" Current Count: {status.current_count}")
|
408
423
|
click.echo(f" Limit: {status.limit}")
|
409
424
|
click.echo(f" Window Start: {status.window_start}")
|
410
425
|
click.echo(f" Window End: {status.window_end}")
|
411
426
|
click.echo(f" Is Allowed: {status.is_allowed}")
|
412
|
-
|
427
|
+
|
413
428
|
except Exception as e:
|
414
429
|
click.echo(f"❌ Failed to get rate limit status: {str(e)}", err=True)
|
415
430
|
raise click.Abort()
|
@@ -420,7 +435,7 @@ def status(ctx, identifier: str):
|
|
420
435
|
def config(ctx):
|
421
436
|
"""
|
422
437
|
Configuration management operations.
|
423
|
-
|
438
|
+
|
424
439
|
Manage security configuration including validation, export,
|
425
440
|
and configuration testing.
|
426
441
|
"""
|
@@ -432,35 +447,37 @@ def config(ctx):
|
|
432
447
|
def validate(ctx):
|
433
448
|
"""
|
434
449
|
Validate security configuration.
|
435
|
-
|
450
|
+
|
436
451
|
This command validates the current security configuration
|
437
452
|
and reports any issues.
|
438
453
|
"""
|
439
454
|
try:
|
440
|
-
config = ctx.obj[
|
441
|
-
verbose = ctx.obj[
|
442
|
-
|
455
|
+
config = ctx.obj["config"]
|
456
|
+
verbose = ctx.obj["verbose"]
|
457
|
+
|
443
458
|
if verbose:
|
444
459
|
click.echo("Validating security configuration...")
|
445
|
-
|
460
|
+
|
446
461
|
# Validate configuration
|
447
462
|
issues = []
|
448
|
-
|
463
|
+
|
449
464
|
# Check authentication configuration
|
450
465
|
if config.auth and config.auth.enabled:
|
451
466
|
if not config.auth.methods:
|
452
467
|
issues.append("Authentication enabled but no methods specified")
|
453
|
-
|
468
|
+
|
454
469
|
# Check rate limiting configuration
|
455
470
|
if config.rate_limit and config.rate_limit.enabled:
|
456
471
|
if config.rate_limit.default_requests_per_minute <= 0:
|
457
|
-
issues.append(
|
458
|
-
|
472
|
+
issues.append(
|
473
|
+
"Invalid rate limit: requests per minute must be positive"
|
474
|
+
)
|
475
|
+
|
459
476
|
# Check SSL configuration
|
460
477
|
if config.ssl and config.ssl.enabled:
|
461
478
|
if not config.ssl.cert_file or not config.ssl.key_file:
|
462
479
|
issues.append("SSL enabled but certificate or key file not specified")
|
463
|
-
|
480
|
+
|
464
481
|
if issues:
|
465
482
|
click.echo(f"❌ Configuration validation failed!", err=True)
|
466
483
|
for issue in issues:
|
@@ -468,65 +485,68 @@ def validate(ctx):
|
|
468
485
|
raise click.Abort()
|
469
486
|
else:
|
470
487
|
click.echo(f"✅ Configuration validation passed!")
|
471
|
-
|
488
|
+
|
472
489
|
except Exception as e:
|
473
490
|
click.echo(f"❌ Configuration validation failed: {str(e)}", err=True)
|
474
491
|
raise click.Abort()
|
475
492
|
|
476
493
|
|
477
494
|
@config.command()
|
478
|
-
@click.option(
|
479
|
-
|
495
|
+
@click.option(
|
496
|
+
"--output", "-o", type=click.Path(), help="Output file path (default: stdout)"
|
497
|
+
)
|
480
498
|
@click.pass_context
|
481
499
|
def export(ctx, output: Optional[str]):
|
482
500
|
"""
|
483
501
|
Export security configuration.
|
484
|
-
|
502
|
+
|
485
503
|
This command exports the current security configuration
|
486
504
|
to JSON format.
|
487
505
|
"""
|
488
506
|
try:
|
489
|
-
config = ctx.obj[
|
490
|
-
verbose = ctx.obj[
|
491
|
-
|
507
|
+
config = ctx.obj["config"]
|
508
|
+
verbose = ctx.obj["verbose"]
|
509
|
+
|
492
510
|
if verbose:
|
493
511
|
click.echo("Exporting security configuration...")
|
494
|
-
|
512
|
+
|
495
513
|
# Export configuration
|
496
514
|
config_json = config.model_dump_json(indent=2)
|
497
|
-
|
515
|
+
|
498
516
|
if output:
|
499
|
-
with open(output,
|
517
|
+
with open(output, "w") as f:
|
500
518
|
f.write(config_json)
|
501
519
|
click.echo(f"✅ Configuration exported to: {output}")
|
502
520
|
else:
|
503
521
|
click.echo(config_json)
|
504
|
-
|
522
|
+
|
505
523
|
except Exception as e:
|
506
524
|
click.echo(f"❌ Failed to export configuration: {str(e)}", err=True)
|
507
525
|
raise click.Abort()
|
508
526
|
|
509
527
|
|
510
528
|
@security_cli.command()
|
511
|
-
@click.option(
|
512
|
-
|
513
|
-
|
514
|
-
|
529
|
+
@click.option(
|
530
|
+
"--output", "-o", type=click.Path(), help="Output file path for roles configuration"
|
531
|
+
)
|
532
|
+
@click.option(
|
533
|
+
"--template", "-t", is_flag=True, help="Generate template roles configuration"
|
534
|
+
)
|
515
535
|
@click.pass_context
|
516
536
|
def generate_roles(ctx, output: Optional[str], template: bool):
|
517
537
|
"""
|
518
538
|
Generate roles configuration file.
|
519
|
-
|
539
|
+
|
520
540
|
This command generates a roles configuration file with default
|
521
541
|
roles and permissions or a template for customization.
|
522
542
|
"""
|
523
543
|
try:
|
524
|
-
security_manager = ctx.obj[
|
525
|
-
verbose = ctx.obj[
|
526
|
-
|
544
|
+
security_manager = ctx.obj["security_manager"]
|
545
|
+
verbose = ctx.obj["verbose"]
|
546
|
+
|
527
547
|
if verbose:
|
528
548
|
click.echo("Generating roles configuration...")
|
529
|
-
|
549
|
+
|
530
550
|
if template:
|
531
551
|
# Generate template roles configuration
|
532
552
|
template_roles = {
|
@@ -534,31 +554,31 @@ def generate_roles(ctx, output: Optional[str], template: bool):
|
|
534
554
|
"admin": {
|
535
555
|
"description": "Administrator role with full access",
|
536
556
|
"permissions": ["*"],
|
537
|
-
"parent_roles": []
|
557
|
+
"parent_roles": [],
|
538
558
|
},
|
539
559
|
"user": {
|
540
560
|
"description": "Standard user role",
|
541
561
|
"permissions": ["read:own", "write:own"],
|
542
|
-
"parent_roles": []
|
562
|
+
"parent_roles": [],
|
543
563
|
},
|
544
564
|
"guest": {
|
545
565
|
"description": "Guest role with limited access",
|
546
566
|
"permissions": ["read:public"],
|
547
|
-
"parent_roles": []
|
548
|
-
}
|
567
|
+
"parent_roles": [],
|
568
|
+
},
|
549
569
|
},
|
550
570
|
"permissions": {
|
551
571
|
"read:own": "Read own resources",
|
552
572
|
"write:own": "Write own resources",
|
553
573
|
"read:public": "Read public resources",
|
554
|
-
"*": "All permissions (wildcard)"
|
555
|
-
}
|
574
|
+
"*": "All permissions (wildcard)",
|
575
|
+
},
|
556
576
|
}
|
557
|
-
|
577
|
+
|
558
578
|
roles_json = json.dumps(template_roles, indent=2)
|
559
|
-
|
579
|
+
|
560
580
|
if output:
|
561
|
-
with open(output,
|
581
|
+
with open(output, "w") as f:
|
562
582
|
f.write(roles_json)
|
563
583
|
click.echo(f"✅ Template roles configuration generated: {output}")
|
564
584
|
else:
|
@@ -566,40 +586,46 @@ def generate_roles(ctx, output: Optional[str], template: bool):
|
|
566
586
|
else:
|
567
587
|
# Generate current roles configuration
|
568
588
|
roles_config = security_manager.permission_manager.export_roles_config()
|
569
|
-
|
589
|
+
|
570
590
|
if output:
|
571
|
-
with open(output,
|
591
|
+
with open(output, "w") as f:
|
572
592
|
json.dump(roles_config, f, indent=2)
|
573
593
|
click.echo(f"✅ Current roles configuration exported: {output}")
|
574
594
|
else:
|
575
595
|
click.echo(json.dumps(roles_config, indent=2))
|
576
|
-
|
596
|
+
|
577
597
|
except Exception as e:
|
578
598
|
click.echo(f"❌ Failed to generate roles configuration: {str(e)}", err=True)
|
579
599
|
raise click.Abort()
|
580
600
|
|
581
601
|
|
582
602
|
@security_cli.command()
|
583
|
-
@click.option(
|
584
|
-
|
585
|
-
|
586
|
-
|
603
|
+
@click.option(
|
604
|
+
"--output", "-o", type=click.Path(), help="Output file path for audit report"
|
605
|
+
)
|
606
|
+
@click.option(
|
607
|
+
"--format",
|
608
|
+
"-f",
|
609
|
+
type=click.Choice(["json", "text"]),
|
610
|
+
default="text",
|
611
|
+
help="Output format for audit report",
|
612
|
+
)
|
587
613
|
@click.pass_context
|
588
614
|
def security_audit(ctx, output: Optional[str], format: str):
|
589
615
|
"""
|
590
616
|
Perform security audit.
|
591
|
-
|
617
|
+
|
592
618
|
This command performs a comprehensive security audit of the
|
593
619
|
system configuration and components.
|
594
620
|
"""
|
595
621
|
try:
|
596
|
-
config = ctx.obj[
|
597
|
-
security_manager = ctx.obj[
|
598
|
-
verbose = ctx.obj[
|
599
|
-
|
622
|
+
config = ctx.obj["config"]
|
623
|
+
security_manager = ctx.obj["security_manager"]
|
624
|
+
verbose = ctx.obj["verbose"]
|
625
|
+
|
600
626
|
if verbose:
|
601
627
|
click.echo("Performing security audit...")
|
602
|
-
|
628
|
+
|
603
629
|
# Perform security audit
|
604
630
|
audit_results = {
|
605
631
|
"timestamp": datetime.now().isoformat(),
|
@@ -607,65 +633,111 @@ def security_audit(ctx, output: Optional[str], format: str):
|
|
607
633
|
"authentication": {
|
608
634
|
"enabled": config.auth.enabled if config.auth else False,
|
609
635
|
"methods": config.auth.methods if config.auth else [],
|
610
|
-
"issues": []
|
636
|
+
"issues": [],
|
611
637
|
},
|
612
638
|
"rate_limiting": {
|
613
|
-
"enabled":
|
614
|
-
|
615
|
-
|
639
|
+
"enabled": (
|
640
|
+
config.rate_limit.enabled if config.rate_limit else False
|
641
|
+
),
|
642
|
+
"default_limit": (
|
643
|
+
config.rate_limit.default_requests_per_minute
|
644
|
+
if config.rate_limit
|
645
|
+
else None
|
646
|
+
),
|
647
|
+
"issues": [],
|
616
648
|
},
|
617
649
|
"ssl_tls": {
|
618
650
|
"enabled": config.ssl.enabled if config.ssl else False,
|
619
651
|
"cert_file": config.ssl.cert_file if config.ssl else None,
|
620
652
|
"key_file": config.ssl.key_file if config.ssl else None,
|
621
|
-
"issues": []
|
653
|
+
"issues": [],
|
622
654
|
},
|
623
655
|
"permissions": {
|
624
|
-
"enabled":
|
625
|
-
|
626
|
-
|
627
|
-
|
656
|
+
"enabled": (
|
657
|
+
config.permissions.enabled if config.permissions else False
|
658
|
+
),
|
659
|
+
"roles_file": (
|
660
|
+
config.permissions.roles_file if config.permissions else None
|
661
|
+
),
|
662
|
+
"issues": [],
|
663
|
+
},
|
628
664
|
},
|
629
665
|
"components": {
|
630
666
|
"auth_manager": {
|
631
|
-
"status":
|
632
|
-
|
667
|
+
"status": (
|
668
|
+
"✅ Initialized"
|
669
|
+
if security_manager.auth_manager
|
670
|
+
else "❌ Not Initialized"
|
671
|
+
),
|
672
|
+
"api_keys_count": (
|
673
|
+
len(security_manager.auth_manager.api_keys)
|
674
|
+
if security_manager.auth_manager
|
675
|
+
else 0
|
676
|
+
),
|
633
677
|
},
|
634
678
|
"permission_manager": {
|
635
|
-
"status":
|
636
|
-
|
679
|
+
"status": (
|
680
|
+
"✅ Initialized"
|
681
|
+
if security_manager.permission_manager
|
682
|
+
else "❌ Not Initialized"
|
683
|
+
),
|
684
|
+
"roles_count": (
|
685
|
+
len(security_manager.permission_manager.roles)
|
686
|
+
if security_manager.permission_manager
|
687
|
+
else 0
|
688
|
+
),
|
637
689
|
},
|
638
690
|
"rate_limiter": {
|
639
|
-
"status":
|
691
|
+
"status": (
|
692
|
+
"✅ Initialized"
|
693
|
+
if security_manager.rate_limiter
|
694
|
+
else "❌ Not Initialized"
|
695
|
+
)
|
640
696
|
},
|
641
697
|
"ssl_manager": {
|
642
|
-
"status":
|
698
|
+
"status": (
|
699
|
+
"✅ Initialized"
|
700
|
+
if security_manager.ssl_manager
|
701
|
+
else "❌ Not Initialized"
|
702
|
+
)
|
643
703
|
},
|
644
704
|
"cert_manager": {
|
645
|
-
"status":
|
646
|
-
|
705
|
+
"status": (
|
706
|
+
"✅ Initialized"
|
707
|
+
if security_manager.cert_manager
|
708
|
+
else "❌ Not Initialized"
|
709
|
+
)
|
710
|
+
},
|
647
711
|
},
|
648
|
-
"recommendations": []
|
712
|
+
"recommendations": [],
|
649
713
|
}
|
650
|
-
|
714
|
+
|
651
715
|
# Add security recommendations
|
652
716
|
if not config.auth.enabled:
|
653
|
-
audit_results["recommendations"].append(
|
654
|
-
|
717
|
+
audit_results["recommendations"].append(
|
718
|
+
"Enable authentication for better security"
|
719
|
+
)
|
720
|
+
|
655
721
|
if not config.rate_limit.enabled:
|
656
|
-
audit_results["recommendations"].append(
|
657
|
-
|
722
|
+
audit_results["recommendations"].append(
|
723
|
+
"Enable rate limiting to prevent abuse"
|
724
|
+
)
|
725
|
+
|
658
726
|
if not config.ssl.enabled:
|
659
|
-
audit_results["recommendations"].append(
|
660
|
-
|
727
|
+
audit_results["recommendations"].append(
|
728
|
+
"Enable SSL/TLS for secure communication"
|
729
|
+
)
|
730
|
+
|
661
731
|
if not config.permissions.enabled:
|
662
|
-
audit_results["recommendations"].append(
|
663
|
-
|
732
|
+
audit_results["recommendations"].append(
|
733
|
+
"Enable permissions for access control"
|
734
|
+
)
|
735
|
+
|
664
736
|
# Output audit results
|
665
|
-
if format ==
|
737
|
+
if format == "json":
|
666
738
|
audit_json = json.dumps(audit_results, indent=2)
|
667
739
|
if output:
|
668
|
-
with open(output,
|
740
|
+
with open(output, "w") as f:
|
669
741
|
f.write(audit_json)
|
670
742
|
click.echo(f"✅ Security audit report saved: {output}")
|
671
743
|
else:
|
@@ -676,47 +748,57 @@ def security_audit(ctx, output: Optional[str], format: str):
|
|
676
748
|
click.echo("=" * 50)
|
677
749
|
click.echo(f"Timestamp: {audit_results['timestamp']}")
|
678
750
|
click.echo()
|
679
|
-
|
751
|
+
|
680
752
|
click.echo("Configuration:")
|
681
753
|
click.echo("-" * 20)
|
682
|
-
auth_config = audit_results[
|
683
|
-
click.echo(
|
684
|
-
|
754
|
+
auth_config = audit_results["configuration"]["authentication"]
|
755
|
+
click.echo(
|
756
|
+
f"Authentication: {'✅ Enabled' if auth_config['enabled'] else '❌ Disabled'}"
|
757
|
+
)
|
758
|
+
if auth_config["methods"]:
|
685
759
|
click.echo(f" Methods: {', '.join(auth_config['methods'])}")
|
686
|
-
|
687
|
-
rate_config = audit_results[
|
688
|
-
click.echo(
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
760
|
+
|
761
|
+
rate_config = audit_results["configuration"]["rate_limiting"]
|
762
|
+
click.echo(
|
763
|
+
f"Rate Limiting: {'✅ Enabled' if rate_config['enabled'] else '❌ Disabled'}"
|
764
|
+
)
|
765
|
+
if rate_config["default_limit"]:
|
766
|
+
click.echo(
|
767
|
+
f" Default Limit: {rate_config['default_limit']} requests/minute"
|
768
|
+
)
|
769
|
+
|
770
|
+
ssl_config = audit_results["configuration"]["ssl_tls"]
|
771
|
+
click.echo(
|
772
|
+
f"SSL/TLS: {'✅ Enabled' if ssl_config['enabled'] else '❌ Disabled'}"
|
773
|
+
)
|
774
|
+
|
775
|
+
perm_config = audit_results["configuration"]["permissions"]
|
776
|
+
click.echo(
|
777
|
+
f"Permissions: {'✅ Enabled' if perm_config['enabled'] else '❌ Disabled'}"
|
778
|
+
)
|
779
|
+
|
698
780
|
click.echo()
|
699
781
|
click.echo("Components:")
|
700
782
|
click.echo("-" * 20)
|
701
|
-
for name, status in audit_results[
|
783
|
+
for name, status in audit_results["components"].items():
|
702
784
|
click.echo(f"{name.replace('_', ' ').title()}: {status['status']}")
|
703
|
-
if
|
785
|
+
if "api_keys_count" in status:
|
704
786
|
click.echo(f" API Keys: {status['api_keys_count']}")
|
705
|
-
if
|
787
|
+
if "roles_count" in status:
|
706
788
|
click.echo(f" Roles: {status['roles_count']}")
|
707
|
-
|
708
|
-
if audit_results[
|
789
|
+
|
790
|
+
if audit_results["recommendations"]:
|
709
791
|
click.echo()
|
710
792
|
click.echo("Recommendations:")
|
711
793
|
click.echo("-" * 20)
|
712
|
-
for rec in audit_results[
|
794
|
+
for rec in audit_results["recommendations"]:
|
713
795
|
click.echo(f"• {rec}")
|
714
|
-
|
796
|
+
|
715
797
|
if output:
|
716
|
-
with open(output,
|
798
|
+
with open(output, "w") as f:
|
717
799
|
f.write(json.dumps(audit_results, indent=2))
|
718
800
|
click.echo(f"\n✅ Detailed audit report saved: {output}")
|
719
|
-
|
801
|
+
|
720
802
|
except Exception as e:
|
721
803
|
click.echo(f"❌ Failed to perform security audit: {str(e)}", err=True)
|
722
804
|
raise click.Abort()
|
@@ -727,46 +809,54 @@ def security_audit(ctx, output: Optional[str], format: str):
|
|
727
809
|
def status(ctx):
|
728
810
|
"""
|
729
811
|
Display security status.
|
730
|
-
|
812
|
+
|
731
813
|
This command displays the current status of all security
|
732
814
|
components and their configuration.
|
733
815
|
"""
|
734
816
|
try:
|
735
|
-
config = ctx.obj[
|
736
|
-
security_manager = ctx.obj[
|
737
|
-
verbose = ctx.obj[
|
738
|
-
|
817
|
+
config = ctx.obj["config"]
|
818
|
+
security_manager = ctx.obj["security_manager"]
|
819
|
+
verbose = ctx.obj["verbose"]
|
820
|
+
|
739
821
|
if verbose:
|
740
822
|
click.echo("Getting security status...")
|
741
|
-
|
823
|
+
|
742
824
|
click.echo("Security Status:")
|
743
825
|
click.echo("=" * 50)
|
744
|
-
|
826
|
+
|
745
827
|
# Authentication status
|
746
828
|
auth_enabled = config.auth.enabled if config.auth else False
|
747
829
|
auth_methods = config.auth.methods if config.auth else []
|
748
830
|
click.echo(f"Authentication: {'✅ Enabled' if auth_enabled else '❌ Disabled'}")
|
749
831
|
if auth_methods:
|
750
832
|
click.echo(f" Methods: {', '.join(auth_methods)}")
|
751
|
-
|
833
|
+
|
752
834
|
# Rate limiting status
|
753
835
|
rate_limit_enabled = config.rate_limit.enabled if config.rate_limit else False
|
754
|
-
click.echo(
|
836
|
+
click.echo(
|
837
|
+
f"Rate Limiting: {'✅ Enabled' if rate_limit_enabled else '❌ Disabled'}"
|
838
|
+
)
|
755
839
|
if rate_limit_enabled and config.rate_limit:
|
756
|
-
click.echo(
|
757
|
-
|
840
|
+
click.echo(
|
841
|
+
f" Default Limit: {config.rate_limit.default_requests_per_minute} requests/minute"
|
842
|
+
)
|
843
|
+
|
758
844
|
# SSL/TLS status
|
759
845
|
ssl_enabled = config.ssl.enabled if config.ssl else False
|
760
846
|
click.echo(f"SSL/TLS: {'✅ Enabled' if ssl_enabled else '❌ Disabled'}")
|
761
|
-
|
847
|
+
|
762
848
|
# Permissions status
|
763
|
-
permissions_enabled =
|
764
|
-
|
765
|
-
|
849
|
+
permissions_enabled = (
|
850
|
+
config.permissions.enabled if config.permissions else False
|
851
|
+
)
|
852
|
+
click.echo(
|
853
|
+
f"Permissions: {'✅ Enabled' if permissions_enabled else '❌ Disabled'}"
|
854
|
+
)
|
855
|
+
|
766
856
|
# Component status
|
767
857
|
click.echo("\nComponent Status:")
|
768
858
|
click.echo("-" * 30)
|
769
|
-
|
859
|
+
|
770
860
|
# Check if components are properly initialized
|
771
861
|
components = [
|
772
862
|
("Auth Manager", security_manager.auth_manager),
|
@@ -775,17 +865,17 @@ def status(ctx):
|
|
775
865
|
("SSL Manager", security_manager.ssl_manager),
|
776
866
|
("Certificate Manager", security_manager.cert_manager),
|
777
867
|
]
|
778
|
-
|
868
|
+
|
779
869
|
for name, component in components:
|
780
870
|
if component:
|
781
871
|
click.echo(f"{name}: ✅ Initialized")
|
782
872
|
else:
|
783
873
|
click.echo(f"{name}: ❌ Not Initialized")
|
784
|
-
|
874
|
+
|
785
875
|
except Exception as e:
|
786
876
|
click.echo(f"❌ Failed to get security status: {str(e)}", err=True)
|
787
877
|
raise click.Abort()
|
788
878
|
|
789
879
|
|
790
|
-
if __name__ ==
|
880
|
+
if __name__ == "__main__":
|
791
881
|
security_cli()
|