iflow-mcp_enuno-unifi-mcp-server 0.2.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.
Files changed (81) hide show
  1. iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/METADATA +1282 -0
  2. iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/RECORD +81 -0
  3. iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/WHEEL +4 -0
  4. iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/entry_points.txt +2 -0
  5. iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/licenses/LICENSE +201 -0
  6. src/__init__.py +3 -0
  7. src/__main__.py +6 -0
  8. src/api/__init__.py +5 -0
  9. src/api/client.py +727 -0
  10. src/api/site_manager_client.py +176 -0
  11. src/cache.py +483 -0
  12. src/config/__init__.py +5 -0
  13. src/config/config.py +321 -0
  14. src/main.py +2234 -0
  15. src/models/__init__.py +126 -0
  16. src/models/acl.py +41 -0
  17. src/models/backup.py +272 -0
  18. src/models/client.py +74 -0
  19. src/models/device.py +53 -0
  20. src/models/dpi.py +50 -0
  21. src/models/firewall_policy.py +123 -0
  22. src/models/firewall_zone.py +28 -0
  23. src/models/network.py +62 -0
  24. src/models/qos_profile.py +458 -0
  25. src/models/radius.py +141 -0
  26. src/models/reference_data.py +34 -0
  27. src/models/site.py +59 -0
  28. src/models/site_manager.py +120 -0
  29. src/models/topology.py +138 -0
  30. src/models/traffic_flow.py +137 -0
  31. src/models/traffic_matching_list.py +56 -0
  32. src/models/voucher.py +42 -0
  33. src/models/vpn.py +73 -0
  34. src/models/wan.py +48 -0
  35. src/models/zbf_matrix.py +49 -0
  36. src/resources/__init__.py +8 -0
  37. src/resources/clients.py +111 -0
  38. src/resources/devices.py +102 -0
  39. src/resources/networks.py +93 -0
  40. src/resources/site_manager.py +64 -0
  41. src/resources/sites.py +86 -0
  42. src/tools/__init__.py +25 -0
  43. src/tools/acls.py +328 -0
  44. src/tools/application.py +42 -0
  45. src/tools/backups.py +1173 -0
  46. src/tools/client_management.py +505 -0
  47. src/tools/clients.py +203 -0
  48. src/tools/device_control.py +325 -0
  49. src/tools/devices.py +354 -0
  50. src/tools/dpi.py +241 -0
  51. src/tools/dpi_tools.py +89 -0
  52. src/tools/firewall.py +417 -0
  53. src/tools/firewall_policies.py +430 -0
  54. src/tools/firewall_zones.py +515 -0
  55. src/tools/network_config.py +388 -0
  56. src/tools/networks.py +190 -0
  57. src/tools/port_forwarding.py +263 -0
  58. src/tools/qos.py +1070 -0
  59. src/tools/radius.py +763 -0
  60. src/tools/reference_data.py +107 -0
  61. src/tools/site_manager.py +466 -0
  62. src/tools/site_vpn.py +95 -0
  63. src/tools/sites.py +187 -0
  64. src/tools/topology.py +406 -0
  65. src/tools/traffic_flows.py +1062 -0
  66. src/tools/traffic_matching_lists.py +371 -0
  67. src/tools/vouchers.py +249 -0
  68. src/tools/vpn.py +76 -0
  69. src/tools/wans.py +30 -0
  70. src/tools/wifi.py +498 -0
  71. src/tools/zbf_matrix.py +326 -0
  72. src/utils/__init__.py +88 -0
  73. src/utils/audit.py +213 -0
  74. src/utils/exceptions.py +114 -0
  75. src/utils/helpers.py +159 -0
  76. src/utils/logger.py +105 -0
  77. src/utils/sanitize.py +244 -0
  78. src/utils/validators.py +160 -0
  79. src/webhooks/__init__.py +6 -0
  80. src/webhooks/handlers.py +196 -0
  81. src/webhooks/receiver.py +290 -0
src/tools/firewall.py ADDED
@@ -0,0 +1,417 @@
1
+ """Firewall rules management MCP tools."""
2
+
3
+ from typing import Any
4
+
5
+ from ..api import UniFiClient
6
+ from ..config import Settings
7
+ from ..utils import (
8
+ ResourceNotFoundError,
9
+ get_logger,
10
+ log_audit,
11
+ validate_confirmation,
12
+ validate_limit_offset,
13
+ validate_site_id,
14
+ )
15
+
16
+
17
+ async def list_firewall_rules(
18
+ site_id: str,
19
+ settings: Settings,
20
+ limit: int | None = None,
21
+ offset: int | None = None,
22
+ ) -> list[dict[str, Any]]:
23
+ """List all firewall rules in a site (read-only).
24
+
25
+ Args:
26
+ site_id: Site identifier
27
+ settings: Application settings
28
+ limit: Maximum number of rules to return
29
+ offset: Number of rules to skip
30
+
31
+ Returns:
32
+ List of firewall rule dictionaries
33
+ """
34
+ site_id = validate_site_id(site_id)
35
+ limit, offset = validate_limit_offset(limit, offset)
36
+ logger = get_logger(__name__, settings.log_level)
37
+
38
+ async with UniFiClient(settings) as client:
39
+ await client.authenticate()
40
+
41
+ response = await client.get(f"/ea/sites/{site_id}/rest/firewallrule")
42
+ # Client now auto-unwraps the "data" field, so response is the actual data
43
+ rules_data: list[dict[str, Any]] = (
44
+ response if isinstance(response, list) else response.get("data", [])
45
+ )
46
+
47
+ # Apply pagination
48
+ paginated = rules_data[offset : offset + limit]
49
+
50
+ logger.info(f"Retrieved {len(paginated)} firewall rules for site '{site_id}'")
51
+ return paginated
52
+
53
+
54
+ async def create_firewall_rule(
55
+ site_id: str,
56
+ name: str,
57
+ action: str,
58
+ settings: Settings,
59
+ source: str | None = None,
60
+ destination: str | None = None,
61
+ protocol: str | None = None,
62
+ port: int | None = None,
63
+ enabled: bool = True,
64
+ ruleset: str = "WAN_IN",
65
+ rule_index: int = 2000,
66
+ confirm: bool = False,
67
+ dry_run: bool = False,
68
+ ) -> dict[str, Any]:
69
+ """Create a new firewall rule.
70
+
71
+ Args:
72
+ site_id: Site identifier
73
+ name: Rule name
74
+ action: Action to take (accept, drop, reject)
75
+ settings: Application settings
76
+ source: Source network/IP (CIDR notation)
77
+ destination: Destination network/IP (CIDR notation)
78
+ protocol: Protocol (tcp, udp, icmp, all)
79
+ port: Destination port number
80
+ enabled: Enable the rule immediately
81
+ ruleset: Ruleset to apply rule to (WAN_IN, WAN_OUT, LAN_IN, etc.)
82
+ rule_index: Position in firewall chain (higher = lower priority)
83
+ confirm: Confirmation flag (must be True to execute)
84
+ dry_run: If True, validate but don't create the rule
85
+
86
+ Returns:
87
+ Created firewall rule dictionary or dry-run result
88
+
89
+ Raises:
90
+ ConfirmationRequiredError: If confirm is not True
91
+ ValidationError: If validation fails
92
+ """
93
+ site_id = validate_site_id(site_id)
94
+ validate_confirmation(confirm, "firewall operation")
95
+ logger = get_logger(__name__, settings.log_level)
96
+
97
+ # Validate action
98
+ valid_actions = ["accept", "drop", "reject"]
99
+ if action.lower() not in valid_actions:
100
+ raise ValueError(f"Invalid action '{action}'. Must be one of: {valid_actions}")
101
+
102
+ # Validate protocol if provided
103
+ if protocol:
104
+ valid_protocols = ["tcp", "udp", "icmp", "all"]
105
+ if protocol.lower() not in valid_protocols:
106
+ raise ValueError(f"Invalid protocol '{protocol}'. Must be one of: {valid_protocols}")
107
+
108
+ # Build rule data with required defaults
109
+ rule_data = {
110
+ "name": name,
111
+ "action": action.lower(),
112
+ "enabled": enabled,
113
+ "ruleset": ruleset,
114
+ "rule_index": rule_index,
115
+ # Required default fields
116
+ "setting_preference": "auto",
117
+ "src_networkconf_type": "NETv4",
118
+ "dst_networkconf_type": "NETv4",
119
+ "state_new": False,
120
+ "state_established": False,
121
+ "state_invalid": False,
122
+ "state_related": False,
123
+ "logging": False,
124
+ "protocol_match_excepted": False,
125
+ }
126
+
127
+ if source:
128
+ rule_data["src_address"] = source
129
+
130
+ if destination:
131
+ rule_data["dst_address"] = destination
132
+
133
+ if protocol:
134
+ rule_data["protocol"] = protocol.lower()
135
+
136
+ if port is not None:
137
+ rule_data["dst_port"] = port
138
+
139
+ # Log parameters for audit
140
+ parameters = {
141
+ "site_id": site_id,
142
+ "name": name,
143
+ "action": action,
144
+ "source": source,
145
+ "destination": destination,
146
+ "protocol": protocol,
147
+ "port": port,
148
+ "enabled": enabled,
149
+ }
150
+
151
+ if dry_run:
152
+ logger.info(f"DRY RUN: Would create firewall rule '{name}' in site '{site_id}'")
153
+ log_audit(
154
+ operation="create_firewall_rule",
155
+ parameters=parameters,
156
+ result="dry_run",
157
+ site_id=site_id,
158
+ dry_run=True,
159
+ )
160
+ return {"dry_run": True, "would_create": rule_data}
161
+
162
+ try:
163
+ async with UniFiClient(settings) as client:
164
+ await client.authenticate()
165
+
166
+ response = await client.post(
167
+ f"/ea/sites/{site_id}/rest/firewallrule", json_data=rule_data
168
+ )
169
+ # Client now auto-unwraps the "data" field, so response is the actual data
170
+ if isinstance(response, list):
171
+ created_rule: dict[str, Any] = response[0]
172
+ else:
173
+ data_list = response.get("data", [{}])
174
+ created_rule = data_list[0] if isinstance(data_list, list) else {}
175
+
176
+ logger.info(f"Created firewall rule '{name}' in site '{site_id}'")
177
+ log_audit(
178
+ operation="create_firewall_rule",
179
+ parameters=parameters,
180
+ result="success",
181
+ site_id=site_id,
182
+ )
183
+
184
+ return created_rule
185
+
186
+ except Exception as e:
187
+ logger.error(f"Failed to create firewall rule '{name}': {e}")
188
+ log_audit(
189
+ operation="create_firewall_rule",
190
+ parameters=parameters,
191
+ result="failed",
192
+ site_id=site_id,
193
+ )
194
+ raise
195
+
196
+
197
+ async def update_firewall_rule(
198
+ site_id: str,
199
+ rule_id: str,
200
+ settings: Settings,
201
+ name: str | None = None,
202
+ action: str | None = None,
203
+ source: str | None = None,
204
+ destination: str | None = None,
205
+ protocol: str | None = None,
206
+ port: int | None = None,
207
+ enabled: bool | None = None,
208
+ confirm: bool = False,
209
+ dry_run: bool = False,
210
+ ) -> dict[str, Any]:
211
+ """Update an existing firewall rule.
212
+
213
+ Args:
214
+ site_id: Site identifier
215
+ rule_id: Firewall rule ID
216
+ settings: Application settings
217
+ name: New rule name
218
+ action: New action (accept, drop, reject)
219
+ source: New source network/IP
220
+ destination: New destination network/IP
221
+ protocol: New protocol (tcp, udp, icmp, all)
222
+ port: New destination port
223
+ enabled: Enable/disable the rule
224
+ confirm: Confirmation flag (must be True to execute)
225
+ dry_run: If True, validate but don't update the rule
226
+
227
+ Returns:
228
+ Updated firewall rule dictionary or dry-run result
229
+
230
+ Raises:
231
+ ConfirmationRequiredError: If confirm is not True
232
+ ResourceNotFoundError: If rule not found
233
+ """
234
+ site_id = validate_site_id(site_id)
235
+ validate_confirmation(confirm, "firewall operation")
236
+ logger = get_logger(__name__, settings.log_level)
237
+
238
+ # Validate action if provided
239
+ if action:
240
+ valid_actions = ["accept", "drop", "reject"]
241
+ if action.lower() not in valid_actions:
242
+ raise ValueError(f"Invalid action '{action}'. Must be one of: {valid_actions}")
243
+
244
+ # Validate protocol if provided
245
+ if protocol:
246
+ valid_protocols = ["tcp", "udp", "icmp", "all"]
247
+ if protocol.lower() not in valid_protocols:
248
+ raise ValueError(f"Invalid protocol '{protocol}'. Must be one of: {valid_protocols}")
249
+
250
+ parameters = {
251
+ "site_id": site_id,
252
+ "rule_id": rule_id,
253
+ "name": name,
254
+ "action": action,
255
+ "source": source,
256
+ "destination": destination,
257
+ "protocol": protocol,
258
+ "port": port,
259
+ "enabled": enabled,
260
+ }
261
+
262
+ if dry_run:
263
+ logger.info(f"DRY RUN: Would update firewall rule '{rule_id}' in site '{site_id}'")
264
+ log_audit(
265
+ operation="update_firewall_rule",
266
+ parameters=parameters,
267
+ result="dry_run",
268
+ site_id=site_id,
269
+ dry_run=True,
270
+ )
271
+ return {"dry_run": True, "would_update": parameters}
272
+
273
+ try:
274
+ async with UniFiClient(settings) as client:
275
+ await client.authenticate()
276
+
277
+ # Get existing rule
278
+ response = await client.get(f"/ea/sites/{site_id}/rest/firewallrule")
279
+ # Client now auto-unwraps the "data" field, so response is the actual data
280
+ rules_data: list[dict[str, Any]] = (
281
+ response if isinstance(response, list) else response.get("data", [])
282
+ )
283
+
284
+ existing_rule = None
285
+ for rule in rules_data:
286
+ if rule.get("_id") == rule_id:
287
+ existing_rule = rule
288
+ break
289
+
290
+ if not existing_rule:
291
+ raise ResourceNotFoundError("firewall_rule", rule_id)
292
+
293
+ # Build update data
294
+ update_data = existing_rule.copy()
295
+
296
+ if name is not None:
297
+ update_data["name"] = name
298
+ if action is not None:
299
+ update_data["action"] = action.lower()
300
+ if source is not None:
301
+ update_data["src_address"] = source
302
+ if destination is not None:
303
+ update_data["dst_address"] = destination
304
+ if protocol is not None:
305
+ update_data["protocol"] = protocol.lower()
306
+ if port is not None:
307
+ update_data["dst_port"] = port
308
+ if enabled is not None:
309
+ update_data["enabled"] = enabled
310
+
311
+ response = await client.put(
312
+ f"/ea/sites/{site_id}/rest/firewallrule/{rule_id}", json_data=update_data
313
+ )
314
+ # Client now auto-unwraps the "data" field, so response is the actual data
315
+ if isinstance(response, list):
316
+ updated_rule: dict[str, Any] = response[0]
317
+ else:
318
+ data_list = response.get("data", [{}])
319
+ updated_rule = data_list[0] if isinstance(data_list, list) else {}
320
+
321
+ logger.info(f"Updated firewall rule '{rule_id}' in site '{site_id}'")
322
+ log_audit(
323
+ operation="update_firewall_rule",
324
+ parameters=parameters,
325
+ result="success",
326
+ site_id=site_id,
327
+ )
328
+
329
+ return updated_rule
330
+
331
+ except Exception as e:
332
+ logger.error(f"Failed to update firewall rule '{rule_id}': {e}")
333
+ log_audit(
334
+ operation="update_firewall_rule",
335
+ parameters=parameters,
336
+ result="failed",
337
+ site_id=site_id,
338
+ )
339
+ raise
340
+
341
+
342
+ async def delete_firewall_rule(
343
+ site_id: str,
344
+ rule_id: str,
345
+ settings: Settings,
346
+ confirm: bool = False,
347
+ dry_run: bool = False,
348
+ ) -> dict[str, Any]:
349
+ """Delete a firewall rule.
350
+
351
+ Args:
352
+ site_id: Site identifier
353
+ rule_id: Firewall rule ID
354
+ settings: Application settings
355
+ confirm: Confirmation flag (must be True to execute)
356
+ dry_run: If True, validate but don't delete the rule
357
+
358
+ Returns:
359
+ Deletion result dictionary
360
+
361
+ Raises:
362
+ ConfirmationRequiredError: If confirm is not True
363
+ ResourceNotFoundError: If rule not found
364
+ """
365
+ site_id = validate_site_id(site_id)
366
+ validate_confirmation(confirm, "firewall operation")
367
+ logger = get_logger(__name__, settings.log_level)
368
+
369
+ parameters = {"site_id": site_id, "rule_id": rule_id}
370
+
371
+ if dry_run:
372
+ logger.info(f"DRY RUN: Would delete firewall rule '{rule_id}' from site '{site_id}'")
373
+ log_audit(
374
+ operation="delete_firewall_rule",
375
+ parameters=parameters,
376
+ result="dry_run",
377
+ site_id=site_id,
378
+ dry_run=True,
379
+ )
380
+ return {"dry_run": True, "would_delete": rule_id}
381
+
382
+ try:
383
+ async with UniFiClient(settings) as client:
384
+ await client.authenticate()
385
+
386
+ # Verify rule exists before deleting
387
+ response = await client.get(f"/ea/sites/{site_id}/rest/firewallrule")
388
+ # Client now auto-unwraps the "data" field, so response is the actual data
389
+ rules_data: list[dict[str, Any]] = (
390
+ response if isinstance(response, list) else response.get("data", [])
391
+ )
392
+
393
+ rule_exists = any(rule.get("_id") == rule_id for rule in rules_data)
394
+ if not rule_exists:
395
+ raise ResourceNotFoundError("firewall_rule", rule_id)
396
+
397
+ response = await client.delete(f"/ea/sites/{site_id}/rest/firewallrule/{rule_id}")
398
+
399
+ logger.info(f"Deleted firewall rule '{rule_id}' from site '{site_id}'")
400
+ log_audit(
401
+ operation="delete_firewall_rule",
402
+ parameters=parameters,
403
+ result="success",
404
+ site_id=site_id,
405
+ )
406
+
407
+ return {"success": True, "deleted_rule_id": rule_id}
408
+
409
+ except Exception as e:
410
+ logger.error(f"Failed to delete firewall rule '{rule_id}': {e}")
411
+ log_audit(
412
+ operation="delete_firewall_rule",
413
+ parameters=parameters,
414
+ result="failed",
415
+ site_id=site_id,
416
+ )
417
+ raise