mcp-security-framework 1.2.2__py3-none-any.whl → 1.2.4__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.
@@ -71,7 +71,7 @@ from mcp_security_framework.schemas.responses import (
71
71
  )
72
72
 
73
73
  # Version information
74
- __version__ = "1.2.2"
74
+ __version__ = "1.2.3"
75
75
  __author__ = "Vasiliy Zdanovskiy"
76
76
  __email__ = "vasilyvz@gmail.com"
77
77
  __license__ = "MIT"
@@ -1953,6 +1953,11 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
1953
1953
  if not self.config.enabled:
1954
1954
  return
1955
1955
 
1956
+ # BUGFIX: Skip CA path validation if in CA creation mode
1957
+ if self.config.ca_creation_mode:
1958
+ self.logger.info("CA creation mode enabled, skipping CA path validation")
1959
+ return
1960
+
1956
1961
  if not self.config.ca_cert_path:
1957
1962
  raise CertificateConfigurationError("CA certificate path is required")
1958
1963
 
@@ -26,11 +26,10 @@ License: MIT
26
26
  import json
27
27
  import logging
28
28
  from pathlib import Path
29
- from typing import Dict, List, Optional, Set
29
+ from typing import Dict, List, Set
30
30
 
31
31
  from ..schemas.config import PermissionConfig
32
32
  from ..schemas.models import ValidationResult, ValidationStatus
33
- from ..utils.validation_utils import validate_configuration_file
34
33
 
35
34
 
36
35
  class PermissionManager:
@@ -227,10 +226,14 @@ class PermissionManager:
227
226
  if not user_roles:
228
227
  return set()
229
228
 
230
- # Validate that all roles exist
231
- invalid_roles = [role for role in user_roles if role not in self._roles]
232
- if invalid_roles:
233
- raise RoleNotFoundError(f"Invalid roles: {invalid_roles}")
229
+ # Validate that all roles exist (only if roles are loaded)
230
+ if self._roles: # Only validate if roles are loaded
231
+ invalid_roles = [role for role in user_roles if role not in self._roles]
232
+ if invalid_roles:
233
+ raise RoleNotFoundError(f"Invalid roles: {invalid_roles}")
234
+ else:
235
+ # If no roles are loaded, return empty set for any role
236
+ return set()
234
237
 
235
238
  # Create cache key
236
239
  cache_key = tuple(sorted(user_roles))
@@ -269,9 +272,15 @@ class PermissionManager:
269
272
  RoleNotFoundError: When either role is not found
270
273
  """
271
274
  if child_role not in self._roles:
275
+ # If no roles are loaded, return False
276
+ if not self._roles:
277
+ return False
272
278
  raise RoleNotFoundError(f"Child role not found: {child_role}")
273
279
 
274
280
  if parent_role not in self._roles:
281
+ # If no roles are loaded, return False
282
+ if not self._roles:
283
+ return False
275
284
  raise RoleNotFoundError(f"Parent role not found: {parent_role}")
276
285
 
277
286
  if child_role == parent_role:
@@ -473,6 +482,9 @@ class PermissionManager:
473
482
  RoleNotFoundError: When role is not found in configuration
474
483
  """
475
484
  if role not in self._roles:
485
+ # If no roles are loaded, return empty list
486
+ if not self._roles:
487
+ return []
476
488
  raise RoleNotFoundError(f"Role not found: {role}")
477
489
 
478
490
  return self._roles[role].get("permissions", []).copy()
@@ -563,6 +575,14 @@ class PermissionManager:
563
575
  def _load_roles_configuration(self) -> None:
564
576
  """Load roles configuration from file."""
565
577
  try:
578
+ # Handle null or empty roles_file
579
+ if not self.config.roles_file:
580
+ self.logger.warning(
581
+ "No roles file specified, using empty roles configuration"
582
+ )
583
+ self._roles = {}
584
+ return
585
+
566
586
  roles_file = Path(self.config.roles_file)
567
587
 
568
588
  if not roles_file.exists():
@@ -271,7 +271,8 @@ class CertificateConfig(BaseModel):
271
271
  default=False, description="Whether certificate management is enabled"
272
272
  )
273
273
  ca_creation_mode: bool = Field(
274
- default=False, description="Whether we are in CA creation mode (bypasses CA path validation)"
274
+ default=False,
275
+ description="Whether we are in CA creation mode (bypasses CA path validation)",
275
276
  )
276
277
  ca_cert_path: Optional[str] = Field(
277
278
  default=None, description="Path to CA certificate"
@@ -390,7 +391,17 @@ class PermissionConfig(BaseModel):
390
391
  @classmethod
391
392
  def validate_roles_file(cls, v):
392
393
  """Validate roles file path."""
393
- if v is not None and not Path(v).exists():
394
+ # Allow None, empty string, null values, or whitespace-only strings
395
+ if (
396
+ v is None
397
+ or v == ""
398
+ or v == "null"
399
+ or (isinstance(v, str) and v.strip() == "")
400
+ ):
401
+ return None
402
+
403
+ # If a path is provided, check if it exists
404
+ if v and not Path(v).exists():
394
405
  raise ValueError(f"Roles file does not exist: {v}")
395
406
  return v
396
407
 
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-security-framework
3
- Version: 1.2.2
4
- Summary: Universal security framework for microservices with SSL/TLS, authentication, authorization, and rate limiting. Requires cryptography>=42.0.0 for certificate operations.
3
+ Version: 1.2.4
4
+ Summary: Universal security framework for microservices with SSL/TLS, authentication, authorization, and rate limiting. Fixed MCP Proxy Adapter JSON-RPC authentication issue with null roles_file. Requires cryptography>=42.0.0 for certificate operations.
5
5
  Author-email: Vasiliy Zdanovskiy <vasilyvz@gmail.com>
6
6
  Maintainer-email: Vasiliy Zdanovskiy <vasilyvz@gmail.com>
7
7
  License: MIT
@@ -1,12 +1,12 @@
1
- mcp_security_framework/__init__.py,sha256=C9V1poQA2xvUsTjT2DulR-ePlspAJ1ouYFLZ-GgfsuE,3172
1
+ mcp_security_framework/__init__.py,sha256=ljFr1tiyy5JeHkk8a77Z3YuC6usyvIwlS01XuL1bsp8,3172
2
2
  mcp_security_framework/constants.py,sha256=k7NMSrgc83Cci8aoilybQxdC7jir7J-mVFE_EpqVrDk,5307
3
3
  mcp_security_framework/cli/__init__.py,sha256=plpWdiWMp2dcLvUuGwXynRg5CDjz8YKnNTBn7lcta08,369
4
4
  mcp_security_framework/cli/cert_cli.py,sha256=LdZ3SYKM3e3dP5LsVR5Y0OENtlG0ENu64aHefHjuiN8,23818
5
5
  mcp_security_framework/cli/security_cli.py,sha256=Thine_Zzfesz7j29y2k_XZFYUK5YSrhCc6w2FilgEiE,28486
6
6
  mcp_security_framework/core/__init__.py,sha256=LiX8_M5qWiTXccJFjSLxup9emhklp-poq57SvznsKEg,1729
7
7
  mcp_security_framework/core/auth_manager.py,sha256=GqGAW83Qg1_z2HJ0-FEVTmlli_DBSOPOap2jJMEU1_k,39882
8
- mcp_security_framework/core/cert_manager.py,sha256=RJgZxElPgMV15Lj_N2uCyO7Ofb9z-MclM1hf8wzFwSc,88882
9
- mcp_security_framework/core/permission_manager.py,sha256=SADS_oXpwp9MhXHKJMCsvjEq8KWcz7vPYL05Yr-zfio,26478
8
+ mcp_security_framework/core/cert_manager.py,sha256=F3rWpqi-YZtaCt3g-KpoqJ1WY22TGaVLEkacKKTrHxw,89094
9
+ mcp_security_framework/core/permission_manager.py,sha256=P6ENqC6sCH4ig_DBfMGALsw-ooRrXJGvQjPyLZrKo9k,27228
10
10
  mcp_security_framework/core/rate_limiter.py,sha256=6qjVBxK2YHouSxQuCcbr0PBpRqA5toQss_Ce178RElY,20682
11
11
  mcp_security_framework/core/security_manager.py,sha256=mAF-5znqxin-MSSgXISB7t1kTkqHltEqGzzmlLAhRGs,37766
12
12
  mcp_security_framework/core/ssl_manager.py,sha256=SXuN5PMTAnMNz04CEKzHbxRKjzF-VqvS-QCFhV-wFeo,29133
@@ -29,7 +29,7 @@ mcp_security_framework/middleware/mtls_middleware.py,sha256=WSyWIk1fCN96hkofODKj
29
29
  mcp_security_framework/middleware/rate_limit_middleware.py,sha256=deCwwigI0Pt7pBUnk2jDurI9ZyjujWTsexEWWndXm3g,13177
30
30
  mcp_security_framework/middleware/security_middleware.py,sha256=PQ251Fr2UrYVPgGfhXq6QJyqK2tRk0WCIg9_FBvfVkg,16844
31
31
  mcp_security_framework/schemas/__init__.py,sha256=lefkbRlbj2ICfasSj51MQ04o3z1YycnbnknSJCFfXbU,2590
32
- mcp_security_framework/schemas/config.py,sha256=I2bBypkNkE3d8Zv5IPeduggFUfIUA5Pyi56NmT1a_5g,27385
32
+ mcp_security_framework/schemas/config.py,sha256=AQEQ8260ZwP9QpTdmpQ9EbU13P7TEyPAA5B48TWssp8,27687
33
33
  mcp_security_framework/schemas/models.py,sha256=Izjy3I55zjMVLsVZpXZ0M4aK3SCks9sC2U1cbxrXYeI,28439
34
34
  mcp_security_framework/schemas/responses.py,sha256=nVXaqF5GTSprXTa_wiUEu38nvSw9WAXtKViAJNbO-Xg,23206
35
35
  mcp_security_framework/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -45,8 +45,9 @@ tests/test_cli/test_cert_cli.py,sha256=Rm7z-20VAvnmYKY3sgxS-qVNks1vbniQJSpSxjsx_
45
45
  tests/test_cli/test_security_cli.py,sha256=Bpd31IPJSUl_V1Xzy74ZCOvQpwlbj8Da83C46T8Jewg,25569
46
46
  tests/test_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
47
  tests/test_core/test_auth_manager.py,sha256=7Z2DLfJLqKtiwX5Q-lR85hN6NxHbE2Q_FT7IsoyKPQk,22568
48
- tests/test_core/test_cert_manager.py,sha256=4YMAkRedkAZW3PEZYEbo1PyrzMntUrKfl7arPsHXDCE,36356
48
+ tests/test_core/test_cert_manager.py,sha256=AVaJlWrWTFMO4MqxzVx39QAwB5zbYhVHQwdYUgNNplk,38091
49
49
  tests/test_core/test_permission_manager.py,sha256=0XeghWXZqVpKyyRuhuDu1dkLUSwuZaFWkRQxQhkkFVI,14966
50
+ tests/test_core/test_permission_manager_null_roles.py,sha256=ajYFpkWgJqtamOU1eI3HlO0WiMdDpPB8br2rp2VjxCY,10403
50
51
  tests/test_core/test_rate_limiter.py,sha256=YzzlhlxZm-A7YGMiIV8LXDA0zmb_6uRF9GRx9s21Q0U,22544
51
52
  tests/test_core/test_security_manager.py,sha256=C5uPFALAkitmHbi-L8xF1OyfOmVHQSq1g-PLkwl_LDU,35007
52
53
  tests/test_core/test_ssl_manager.py,sha256=Vm_Nw4SoVro_iwPPc_uD9CwzXpVBkGyVH7EqDtHawvU,20362
@@ -69,6 +70,7 @@ tests/test_middleware/test_flask_middleware.py,sha256=JqWr5MknE6AvnUUf2Cr0ME6l_w
69
70
  tests/test_middleware/test_security_middleware.py,sha256=J69rVgsnohQp2ucUnGRyWCWZxt6RF2tQ9vQNLFlDXEg,19199
70
71
  tests/test_schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
71
72
  tests/test_schemas/test_config.py,sha256=m0TKYPXKC2QdkVmUc7UPEF3yOANL5Ee1v93DZswSMvk,31347
73
+ tests/test_schemas/test_config_null_roles.py,sha256=xF7d1-moagxWp3TSNERMVrp6g9vzRqR-t9mMgfzK0xI,9491
72
74
  tests/test_schemas/test_models.py,sha256=bBeZOPqveuVJuEi_BTVWdVsdj08JXJTEFwvBM4eFRVU,34311
73
75
  tests/test_schemas/test_responses.py,sha256=ZSbO7A3ThPBovTXO8PFF-2ONWAjJx2dMOoV2lQIfd8s,40774
74
76
  tests/test_schemas/test_serialization.py,sha256=jCugAyrdD6Mw1U7Kxni9oTukarZmMMl6KUcl6cq_NTk,18599
@@ -78,8 +80,8 @@ tests/test_utils/test_crypto_utils.py,sha256=yEb4hzG6-irj2DPoXY0DUboJfbeR87ussgT
78
80
  tests/test_utils/test_datetime_compat.py,sha256=n8S4X5HN-_ejSNpgymDXRyZkmxhnyxwwjxFPdX23I40,5656
79
81
  tests/test_utils/test_unitid_compat.py,sha256=MWh03A4FwzQyZa20PKHEWz4W03YtARwBOd_1JbABznQ,25544
80
82
  tests/test_utils/test_validation_utils.py,sha256=lus_wHJ2WyVnBGQ28S7dSv78uWcCIuLhn5uflJw-uGw,18569
81
- mcp_security_framework-1.2.2.dist-info/METADATA,sha256=JSoJc0AxNbssfj8K6IyR-JCfySC8L9pmys2d7PnLj9o,11771
82
- mcp_security_framework-1.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
83
- mcp_security_framework-1.2.2.dist-info/entry_points.txt,sha256=qBh92fVDmd1m2f3xeW0hTu3Ksg8QfGJyV8UEkdA2itg,142
84
- mcp_security_framework-1.2.2.dist-info/top_level.txt,sha256=ifUiGrTDcD574MXSOoAN2rp2wpUvWlb4jD9LTUgDWCA,29
85
- mcp_security_framework-1.2.2.dist-info/RECORD,,
83
+ mcp_security_framework-1.2.4.dist-info/METADATA,sha256=j3FYSacZFO8OwGfkn-B6om8z1ZcATSmUZrGxnIZLDCk,11847
84
+ mcp_security_framework-1.2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
85
+ mcp_security_framework-1.2.4.dist-info/entry_points.txt,sha256=qBh92fVDmd1m2f3xeW0hTu3Ksg8QfGJyV8UEkdA2itg,142
86
+ mcp_security_framework-1.2.4.dist-info/top_level.txt,sha256=ifUiGrTDcD574MXSOoAN2rp2wpUvWlb4jD9LTUgDWCA,29
87
+ mcp_security_framework-1.2.4.dist-info/RECORD,,
@@ -676,6 +676,50 @@ class TestCertificateManager:
676
676
 
677
677
  assert "CA certificate and key paths are required" in str(exc_info.value)
678
678
 
679
+ def test_ca_creation_mode_bypasses_validation(self):
680
+ """Test that CA creation mode bypasses CA path validation in CertificateManager."""
681
+ config = CertificateConfig(
682
+ enabled=True,
683
+ ca_creation_mode=True,
684
+ cert_storage_path=self.temp_dir,
685
+ key_storage_path=self.temp_dir
686
+ )
687
+
688
+ # This should not raise an exception
689
+ cert_manager = CertificateManager(config)
690
+ assert cert_manager is not None
691
+ assert cert_manager.config.ca_creation_mode is True
692
+
693
+ def test_ca_creation_mode_with_ca_paths(self):
694
+ """Test that CA creation mode works even with CA paths provided."""
695
+ config = CertificateConfig(
696
+ enabled=True,
697
+ ca_creation_mode=True,
698
+ ca_cert_path=self.ca_cert_path,
699
+ ca_key_path=self.ca_key_path,
700
+ cert_storage_path=self.temp_dir,
701
+ key_storage_path=self.temp_dir
702
+ )
703
+
704
+ # This should not raise an exception
705
+ cert_manager = CertificateManager(config)
706
+ assert cert_manager is not None
707
+ assert cert_manager.config.ca_creation_mode is True
708
+ assert cert_manager.config.ca_cert_path == self.ca_cert_path
709
+ assert cert_manager.config.ca_key_path == self.ca_key_path
710
+
711
+ def test_normal_mode_requires_ca_paths(self):
712
+ """Test that normal mode requires CA paths."""
713
+ with pytest.raises(ValidationError) as exc_info:
714
+ CertificateConfig(
715
+ enabled=True,
716
+ ca_creation_mode=False,
717
+ cert_storage_path=self.temp_dir,
718
+ key_storage_path=self.temp_dir
719
+ )
720
+
721
+ assert "ca_creation_mode=True" in str(exc_info.value)
722
+
679
723
  def test_create_output_directory(self):
680
724
  """Test automatic output directory creation."""
681
725
  # Create new temp directory
@@ -0,0 +1,265 @@
1
+ """
2
+ Tests for PermissionManager with null roles_file
3
+
4
+ This module contains tests for the PermissionManager when roles_file
5
+ is set to null or None, ensuring graceful handling of this configuration.
6
+
7
+ Author: Vasiliy Zdanovskiy
8
+ email: vasilyvz@gmail.com
9
+ """
10
+
11
+ import pytest
12
+ from unittest.mock import patch
13
+ from mcp_security_framework.core.permission_manager import PermissionManager
14
+ from mcp_security_framework.schemas.config import PermissionConfig
15
+ from mcp_security_framework.schemas.models import ValidationResult, ValidationStatus
16
+
17
+
18
+ class TestPermissionManagerNullRoles:
19
+ """Test suite for PermissionManager with null roles_file."""
20
+
21
+ def test_permission_manager_with_null_roles_file(self):
22
+ """Test PermissionManager initialization with null roles_file."""
23
+ config = PermissionConfig(enabled=True, roles_file=None, default_role="guest")
24
+
25
+ # Should not raise an exception
26
+ perm_manager = PermissionManager(config)
27
+
28
+ # Should have empty roles
29
+ assert perm_manager._roles == {}
30
+
31
+ # Should be able to validate access with default role
32
+ result = perm_manager.validate_access(["guest"], ["read:public"])
33
+ assert isinstance(result, ValidationResult)
34
+
35
+ def test_permission_manager_with_empty_roles_file(self):
36
+ """Test PermissionManager initialization with empty roles_file."""
37
+ config = PermissionConfig(enabled=True, roles_file="", default_role="guest")
38
+
39
+ # Should not raise an exception
40
+ perm_manager = PermissionManager(config)
41
+
42
+ # Should have empty roles
43
+ assert perm_manager._roles == {}
44
+
45
+ def test_permission_manager_with_string_null_roles_file(self):
46
+ """Test PermissionManager initialization with string 'null' roles_file."""
47
+ config = PermissionConfig(enabled=True, roles_file="null", default_role="guest")
48
+
49
+ # Should not raise an exception
50
+ perm_manager = PermissionManager(config)
51
+
52
+ # Should have empty roles
53
+ assert perm_manager._roles == {}
54
+
55
+ def test_permission_manager_validation_with_empty_roles(self):
56
+ """Test permission validation when no roles are loaded."""
57
+ config = PermissionConfig(enabled=True, roles_file=None, default_role="guest")
58
+
59
+ perm_manager = PermissionManager(config)
60
+
61
+ # Test validation with guest role
62
+ result = perm_manager.validate_access(["guest"], ["read:public"])
63
+ assert isinstance(result, ValidationResult)
64
+
65
+ # Test validation with unknown role
66
+ result = perm_manager.validate_access(["unknown"], ["read:public"])
67
+ assert isinstance(result, ValidationResult)
68
+ assert result.status == ValidationStatus.INVALID
69
+
70
+ def test_permission_manager_get_effective_permissions_empty_roles(self):
71
+ """Test getting effective permissions when no roles are loaded."""
72
+ config = PermissionConfig(enabled=True, roles_file=None, default_role="guest")
73
+
74
+ perm_manager = PermissionManager(config)
75
+
76
+ # Should return empty set for any role
77
+ permissions = perm_manager.get_effective_permissions(["guest"])
78
+ assert permissions == set()
79
+
80
+ permissions = perm_manager.get_effective_permissions(["admin"])
81
+ assert permissions == set()
82
+
83
+ def test_permission_manager_export_roles_config_empty(self):
84
+ """Test exporting roles configuration when no roles are loaded."""
85
+ config = PermissionConfig(enabled=True, roles_file=None, default_role="guest")
86
+
87
+ perm_manager = PermissionManager(config)
88
+
89
+ # Should export empty configuration
90
+ exported = perm_manager.export_roles_config()
91
+ assert "roles" in exported
92
+ assert exported["roles"] == {}
93
+ assert "permissions" in exported
94
+ assert exported["permissions"] == {}
95
+
96
+ def test_permission_manager_cache_operations_empty_roles(self):
97
+ """Test cache operations when no roles are loaded."""
98
+ config = PermissionConfig(
99
+ enabled=True,
100
+ roles_file=None,
101
+ default_role="guest",
102
+ permission_cache_enabled=True,
103
+ )
104
+
105
+ perm_manager = PermissionManager(config)
106
+
107
+ # Cache operations should work without errors
108
+ perm_manager.clear_cache()
109
+ perm_manager.clear_permission_cache()
110
+
111
+ # Cache should be empty
112
+ assert len(perm_manager._permission_cache) == 0
113
+
114
+ def test_permission_manager_role_operations_empty_roles(self):
115
+ """Test role operations when no roles are loaded."""
116
+ config = PermissionConfig(enabled=True, roles_file=None, default_role="guest")
117
+
118
+ perm_manager = PermissionManager(config)
119
+
120
+ # Role operations should work without errors
121
+ roles = perm_manager.get_all_roles()
122
+ assert roles == []
123
+
124
+ # Getting role permissions should work (returns empty for unknown roles)
125
+ permissions = perm_manager.get_role_permissions("test_role")
126
+ assert permissions == []
127
+
128
+ def test_permission_manager_hierarchy_operations_empty_roles(self):
129
+ """Test hierarchy operations when no roles are loaded."""
130
+ config = PermissionConfig(enabled=True, roles_file=None, default_role="guest")
131
+
132
+ perm_manager = PermissionManager(config)
133
+
134
+ # Hierarchy operations should work without errors
135
+ hierarchy = perm_manager.get_role_hierarchy()
136
+ assert hierarchy == {}
137
+
138
+ # Checking hierarchy should work
139
+ is_hierarchy = perm_manager.check_role_hierarchy("child", "parent")
140
+ assert is_hierarchy is False
141
+
142
+ def test_permission_manager_permission_operations_empty_roles(self):
143
+ """Test permission operations when no roles are loaded."""
144
+ config = PermissionConfig(enabled=True, roles_file=None, default_role="guest")
145
+
146
+ perm_manager = PermissionManager(config)
147
+
148
+ # Permission operations should work without errors
149
+ # Note: get_all_permissions doesn't exist, so we test role permissions instead
150
+ permissions = perm_manager.get_role_permissions("guest")
151
+ assert permissions == []
152
+
153
+ def test_permission_manager_validation_with_wildcards_empty_roles(self):
154
+ """Test wildcard permission validation when no roles are loaded."""
155
+ config = PermissionConfig(
156
+ enabled=True,
157
+ roles_file=None,
158
+ default_role="guest",
159
+ wildcard_permissions=True,
160
+ )
161
+
162
+ perm_manager = PermissionManager(config)
163
+
164
+ # Wildcard validation should work without errors
165
+ result = perm_manager.validate_access(["guest"], ["*"])
166
+ assert isinstance(result, ValidationResult)
167
+
168
+ result = perm_manager.validate_access(["guest"], ["read:*"])
169
+ assert isinstance(result, ValidationResult)
170
+
171
+ def test_permission_manager_strict_mode_empty_roles(self):
172
+ """Test strict mode validation when no roles are loaded."""
173
+ config = PermissionConfig(
174
+ enabled=True, roles_file=None, default_role="guest", strict_mode=True
175
+ )
176
+
177
+ perm_manager = PermissionManager(config)
178
+
179
+ # Strict mode validation should work without errors
180
+ result = perm_manager.validate_access(["guest"], ["read:public"])
181
+ assert isinstance(result, ValidationResult)
182
+
183
+ result = perm_manager.validate_access(["unknown"], ["read:public"])
184
+ assert isinstance(result, ValidationResult)
185
+ assert result.status == ValidationStatus.INVALID
186
+
187
+ def test_permission_manager_cache_ttl_empty_roles(self):
188
+ """Test cache TTL operations when no roles are loaded."""
189
+ config = PermissionConfig(
190
+ enabled=True,
191
+ roles_file=None,
192
+ default_role="guest",
193
+ permission_cache_enabled=True,
194
+ permission_cache_ttl=300,
195
+ )
196
+
197
+ perm_manager = PermissionManager(config)
198
+
199
+ # Cache TTL operations should work without errors
200
+ assert perm_manager._cache_enabled is True
201
+
202
+ # Cache should be empty but functional
203
+ permissions = perm_manager.get_effective_permissions(["guest"])
204
+ assert permissions == set()
205
+
206
+ def test_permission_manager_error_handling_empty_roles(self):
207
+ """Test error handling when no roles are loaded."""
208
+ config = PermissionConfig(enabled=True, roles_file=None, default_role="guest")
209
+
210
+ perm_manager = PermissionManager(config)
211
+
212
+ # Error handling should work without errors
213
+ try:
214
+ perm_manager.validate_access([], ["read:public"])
215
+ except Exception as e:
216
+ pytest.fail(f"validate_access raised an exception: {e}")
217
+
218
+ try:
219
+ perm_manager.validate_access(["guest"], [])
220
+ except Exception as e:
221
+ pytest.fail(f"validate_access raised an exception: {e}")
222
+
223
+ def test_permission_manager_logging_empty_roles(self):
224
+ """Test logging when no roles are loaded."""
225
+ config = PermissionConfig(enabled=True, roles_file=None, default_role="guest")
226
+
227
+ # Should not raise an exception during initialization
228
+ with patch(
229
+ "mcp_security_framework.core.permission_manager.logging.getLogger"
230
+ ) as mock_logger:
231
+ mock_logger_instance = mock_logger.return_value
232
+ perm_manager = PermissionManager(config)
233
+
234
+ # Should log warning about empty roles configuration
235
+ mock_logger_instance.warning.assert_called_with(
236
+ "No roles file specified, using empty roles configuration"
237
+ )
238
+
239
+ def test_permission_manager_integration_empty_roles(self):
240
+ """Test full integration when no roles are loaded."""
241
+ config = PermissionConfig(
242
+ enabled=True,
243
+ roles_file=None,
244
+ default_role="guest",
245
+ permission_cache_enabled=True,
246
+ wildcard_permissions=True,
247
+ strict_mode=True,
248
+ )
249
+
250
+ # Should initialize without errors
251
+ perm_manager = PermissionManager(config)
252
+
253
+ # All operations should work
254
+ assert perm_manager._roles == {}
255
+ assert perm_manager._hierarchy == {}
256
+ assert perm_manager._permission_cache == {}
257
+ assert perm_manager._cache_enabled is True
258
+
259
+ # Validation should work
260
+ result = perm_manager.validate_access(["guest"], ["read:public"])
261
+ assert isinstance(result, ValidationResult)
262
+
263
+ # Cache operations should work
264
+ perm_manager.clear_cache()
265
+ assert len(perm_manager._permission_cache) == 0
@@ -0,0 +1,252 @@
1
+ """
2
+ Tests for PermissionConfig with null roles_file
3
+
4
+ This module contains tests for the PermissionConfig validation
5
+ when roles_file is set to null, None, or empty values.
6
+
7
+ Author: Vasiliy Zdanovskiy
8
+ email: vasilyvz@gmail.com
9
+ """
10
+
11
+ import pytest
12
+ from mcp_security_framework.schemas.config import PermissionConfig
13
+
14
+
15
+ class TestPermissionConfigNullRoles:
16
+ """Test suite for PermissionConfig with null roles_file."""
17
+
18
+ def test_permission_config_with_none_roles_file(self):
19
+ """Test PermissionConfig with None roles_file."""
20
+ config = PermissionConfig(enabled=True, roles_file=None, default_role="guest")
21
+
22
+ assert config.roles_file is None
23
+ assert config.enabled is True
24
+ assert config.default_role == "guest"
25
+
26
+ def test_permission_config_with_empty_roles_file(self):
27
+ """Test PermissionConfig with empty string roles_file."""
28
+ config = PermissionConfig(enabled=True, roles_file="", default_role="guest")
29
+
30
+ assert config.roles_file is None
31
+ assert config.enabled is True
32
+ assert config.default_role == "guest"
33
+
34
+ def test_permission_config_with_string_null_roles_file(self):
35
+ """Test PermissionConfig with string 'null' roles_file."""
36
+ config = PermissionConfig(enabled=True, roles_file="null", default_role="guest")
37
+
38
+ assert config.roles_file is None
39
+ assert config.enabled is True
40
+ assert config.default_role == "guest"
41
+
42
+ def test_permission_config_with_valid_roles_file(self):
43
+ """Test PermissionConfig with valid roles_file."""
44
+ # Create a temporary file for testing
45
+ import tempfile
46
+ import os
47
+
48
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
49
+ f.write('{"roles": {}}')
50
+ temp_file = f.name
51
+
52
+ try:
53
+ config = PermissionConfig(
54
+ enabled=True, roles_file=temp_file, default_role="guest"
55
+ )
56
+
57
+ assert config.roles_file == temp_file
58
+ assert config.enabled is True
59
+ assert config.default_role == "guest"
60
+ finally:
61
+ os.unlink(temp_file)
62
+
63
+ def test_permission_config_with_nonexistent_roles_file(self):
64
+ """Test PermissionConfig with nonexistent roles_file."""
65
+ with pytest.raises(ValueError, match="Roles file does not exist"):
66
+ PermissionConfig(
67
+ enabled=True,
68
+ roles_file="/nonexistent/path/roles.json",
69
+ default_role="guest",
70
+ )
71
+
72
+ def test_permission_config_default_values(self):
73
+ """Test PermissionConfig default values."""
74
+ config = PermissionConfig()
75
+
76
+ assert config.enabled is True
77
+ assert config.roles_file is None
78
+ assert config.default_role == "guest"
79
+ assert config.admin_role == "admin"
80
+ assert config.role_hierarchy == {}
81
+ assert config.permission_cache_enabled is True
82
+ assert config.permission_cache_ttl == 300
83
+ assert config.wildcard_permissions is False
84
+ assert config.strict_mode is True
85
+ assert config.roles is None
86
+
87
+ def test_permission_config_with_all_null_values(self):
88
+ """Test PermissionConfig with all null-like values."""
89
+ config = PermissionConfig(
90
+ enabled=True,
91
+ roles_file=None,
92
+ default_role="guest",
93
+ admin_role="admin",
94
+ role_hierarchy={},
95
+ permission_cache_enabled=True,
96
+ permission_cache_ttl=300,
97
+ wildcard_permissions=False,
98
+ strict_mode=True,
99
+ roles=None,
100
+ )
101
+
102
+ assert config.roles_file is None
103
+ assert config.roles is None
104
+ assert config.role_hierarchy == {}
105
+
106
+ def test_permission_config_validation_edge_cases(self):
107
+ """Test PermissionConfig validation with edge cases."""
108
+ # Test with whitespace-only string
109
+ config = PermissionConfig(enabled=True, roles_file=" ", default_role="guest")
110
+
111
+ # Should be treated as empty and converted to None
112
+ assert config.roles_file is None
113
+
114
+ def test_permission_config_with_roles_dict(self):
115
+ """Test PermissionConfig with inline roles dictionary."""
116
+ config = PermissionConfig(
117
+ enabled=True,
118
+ roles_file=None,
119
+ default_role="guest",
120
+ roles={
121
+ "admin": ["*"],
122
+ "user": ["read:*", "write:own"],
123
+ "guest": ["read:public"],
124
+ },
125
+ )
126
+
127
+ assert config.roles_file is None
128
+ assert config.roles is not None
129
+ assert "admin" in config.roles
130
+ assert "user" in config.roles
131
+ assert "guest" in config.roles
132
+
133
+ def test_permission_config_serialization(self):
134
+ """Test PermissionConfig serialization with null values."""
135
+ config = PermissionConfig(enabled=True, roles_file=None, default_role="guest")
136
+
137
+ # Should serialize without errors
138
+ config_dict = config.model_dump()
139
+ assert "roles_file" in config_dict
140
+ assert config_dict["roles_file"] is None
141
+
142
+ # Should deserialize without errors
143
+ config_dict["roles_file"] = None
144
+ new_config = PermissionConfig(**config_dict)
145
+ assert new_config.roles_file is None
146
+
147
+ def test_permission_config_json_serialization(self):
148
+ """Test PermissionConfig JSON serialization with null values."""
149
+ config = PermissionConfig(enabled=True, roles_file=None, default_role="guest")
150
+
151
+ # Should serialize to JSON without errors
152
+ json_str = config.model_dump_json()
153
+ assert "roles_file" in json_str
154
+ assert "null" in json_str
155
+
156
+ # Should deserialize from JSON without errors
157
+ import json
158
+
159
+ config_dict = json.loads(json_str)
160
+ assert config_dict["roles_file"] is None
161
+
162
+ def test_permission_config_validation_chain(self):
163
+ """Test PermissionConfig validation chain with null values."""
164
+ # Test that validation works with null values
165
+ config = PermissionConfig(
166
+ enabled=True,
167
+ roles_file=None,
168
+ default_role="guest",
169
+ admin_role="admin",
170
+ role_hierarchy={},
171
+ permission_cache_enabled=True,
172
+ permission_cache_ttl=300,
173
+ wildcard_permissions=False,
174
+ strict_mode=True,
175
+ roles=None,
176
+ )
177
+
178
+ # All validations should pass
179
+ assert config.enabled is True
180
+ assert config.roles_file is None
181
+ assert config.default_role == "guest"
182
+ assert config.admin_role == "admin"
183
+ assert config.role_hierarchy == {}
184
+ assert config.permission_cache_enabled is True
185
+ assert config.permission_cache_ttl == 300
186
+ assert config.wildcard_permissions is False
187
+ assert config.strict_mode is True
188
+ assert config.roles is None
189
+
190
+ def test_permission_config_field_validation(self):
191
+ """Test PermissionConfig field validation with null values."""
192
+ # Test that all fields can be set to null/None where appropriate
193
+ config = PermissionConfig(
194
+ enabled=True,
195
+ roles_file=None, # Can be None
196
+ default_role="guest", # Cannot be None
197
+ admin_role="admin", # Cannot be None
198
+ role_hierarchy={}, # Cannot be None, but can be empty
199
+ permission_cache_enabled=True, # Cannot be None
200
+ permission_cache_ttl=300, # Cannot be None
201
+ wildcard_permissions=False, # Cannot be None
202
+ strict_mode=True, # Cannot be None
203
+ roles=None, # Can be None
204
+ )
205
+
206
+ # All fields should be valid
207
+ assert config.roles_file is None
208
+ assert config.roles is None
209
+ assert config.role_hierarchy == {}
210
+ assert config.enabled is True
211
+ assert config.default_role == "guest"
212
+ assert config.admin_role == "admin"
213
+ assert config.permission_cache_enabled is True
214
+ assert config.permission_cache_ttl == 300
215
+ assert config.wildcard_permissions is False
216
+ assert config.strict_mode is True
217
+
218
+ def test_permission_config_equality(self):
219
+ """Test PermissionConfig equality with null values."""
220
+ config1 = PermissionConfig(enabled=True, roles_file=None, default_role="guest")
221
+
222
+ config2 = PermissionConfig(enabled=True, roles_file=None, default_role="guest")
223
+
224
+ # Should be equal
225
+ assert config1 == config2
226
+
227
+ # Test with different null representations
228
+ config3 = PermissionConfig(enabled=True, roles_file="", default_role="guest")
229
+
230
+ # Should be equal (empty string converted to None)
231
+ assert config1 == config3
232
+
233
+ def test_permission_config_copy(self):
234
+ """Test PermissionConfig copy with null values."""
235
+ config = PermissionConfig(enabled=True, roles_file=None, default_role="guest")
236
+
237
+ # Should copy without errors
238
+ config_copy = config.model_copy()
239
+ assert config_copy == config
240
+ assert config_copy.roles_file is None
241
+ assert config_copy.default_role == "guest"
242
+
243
+ def test_permission_config_mutability(self):
244
+ """Test PermissionConfig mutability with null values."""
245
+ config = PermissionConfig(enabled=True, roles_file=None, default_role="guest")
246
+
247
+ # Pydantic models are mutable by default
248
+ config.roles_file = "new_value"
249
+ assert config.roles_file == "new_value"
250
+
251
+ config.default_role = "new_role"
252
+ assert config.default_role == "new_role"