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
@@ -0,0 +1,430 @@
1
+ """Firewall policies management tools for UniFi v2 API."""
2
+
3
+ from typing import Any
4
+
5
+ from ..api.client import UniFiClient
6
+ from ..config import APIType, Settings
7
+ from ..models.firewall_policy import FirewallPolicy, FirewallPolicyCreate
8
+ from ..utils import ResourceNotFoundError, get_logger, log_audit
9
+
10
+ logger = get_logger(__name__)
11
+
12
+
13
+ def _ensure_local_api(settings: Settings) -> None:
14
+ """Ensure the UniFi controller is accessed via the local API for v2 endpoints."""
15
+ if settings.api_type != APIType.LOCAL:
16
+ raise NotImplementedError(
17
+ "Firewall policies (v2 API) are only available when UNIFI_API_TYPE='local'. "
18
+ "Please configure a local UniFi gateway connection to use these tools."
19
+ )
20
+
21
+
22
+ async def list_firewall_policies(
23
+ site_id: str,
24
+ settings: Settings,
25
+ ) -> list[dict[str, Any]]:
26
+ """List all firewall policies (Traffic & Firewall Rules) for a site.
27
+
28
+ This tool fetches firewall policies from the UniFi v2 API endpoint.
29
+ Only available with local gateway API (api_type="local").
30
+
31
+ Args:
32
+ site_id: Site identifier (default: "default")
33
+ settings: Application settings
34
+
35
+ Returns:
36
+ List of firewall policy objects
37
+
38
+ Raises:
39
+ NotImplementedError: When using cloud API (v2 endpoints require local access)
40
+ APIError: When API request fails
41
+
42
+ Note:
43
+ Cloud API does not support v2 endpoints. Configure UNIFI_API_TYPE=local
44
+ and UNIFI_LOCAL_HOST to use this tool.
45
+ """
46
+ _ensure_local_api(settings)
47
+
48
+ async with UniFiClient(settings) as client:
49
+ logger.info(f"Listing firewall policies for site {site_id}")
50
+
51
+ if not client.is_authenticated:
52
+ await client.authenticate()
53
+
54
+ endpoint = f"{settings.get_v2_api_path(site_id)}/firewall-policies"
55
+ response = await client.get(endpoint)
56
+
57
+ policies_data = response if isinstance(response, list) else response.get("data", [])
58
+
59
+ return [FirewallPolicy(**policy).model_dump() for policy in policies_data]
60
+
61
+
62
+ async def get_firewall_policy(
63
+ policy_id: str,
64
+ site_id: str,
65
+ settings: Settings,
66
+ ) -> dict[str, Any]:
67
+ """Get a specific firewall policy by ID.
68
+
69
+ Retrieves detailed information about a single firewall policy
70
+ from the v2 API endpoint.
71
+
72
+ Args:
73
+ policy_id: The firewall policy ID
74
+ site_id: Site identifier (default: "default")
75
+ settings: Application settings
76
+
77
+ Returns:
78
+ Firewall policy object
79
+
80
+ Raises:
81
+ NotImplementedError: When using cloud API (v2 endpoints require local access)
82
+ ResourceNotFoundError: If policy not found
83
+ APIError: When API request fails
84
+
85
+ Note:
86
+ Cloud API does not support v2 endpoints. Configure UNIFI_API_TYPE=local
87
+ and UNIFI_LOCAL_HOST to use this tool.
88
+
89
+ Example:
90
+ >>> policy = await get_firewall_policy(
91
+ ... "682a0e42220317278bb0b2cb",
92
+ ... "default",
93
+ ... settings
94
+ ... )
95
+ >>> print(f"{policy['name']}: {policy['action']}")
96
+ """
97
+ _ensure_local_api(settings)
98
+
99
+ async with UniFiClient(settings) as client:
100
+ logger.info(f"Getting firewall policy {policy_id} for site {site_id}")
101
+
102
+ if not client.is_authenticated:
103
+ await client.authenticate()
104
+
105
+ endpoint = f"{settings.get_v2_api_path(site_id)}/firewall-policies/{policy_id}"
106
+
107
+ try:
108
+ response = await client.get(endpoint)
109
+ except ResourceNotFoundError as err:
110
+ raise ResourceNotFoundError("firewall_policy", policy_id) from err
111
+
112
+ # Handle both wrapped and unwrapped responses
113
+ if isinstance(response, dict) and "data" in response:
114
+ data = response["data"]
115
+ else:
116
+ data = response
117
+
118
+ if not data:
119
+ raise ResourceNotFoundError("firewall_policy", policy_id)
120
+
121
+ return FirewallPolicy(**data).model_dump()
122
+
123
+
124
+ async def create_firewall_policy(
125
+ name: str,
126
+ action: str,
127
+ site_id: str,
128
+ settings: Settings,
129
+ source_zone_id: str | None = None,
130
+ destination_zone_id: str | None = None,
131
+ source_matching_target: str = "ANY",
132
+ destination_matching_target: str = "ANY",
133
+ protocol: str = "all",
134
+ enabled: bool = True,
135
+ description: str | None = None,
136
+ confirm: bool = False,
137
+ dry_run: bool = False,
138
+ ) -> dict[str, Any]:
139
+ """Create a new firewall policy (Traffic & Firewall Rule).
140
+
141
+ Only available with local gateway API (api_type="local").
142
+ Requires confirm=True to execute. Use dry_run=True to preview.
143
+
144
+ Args:
145
+ name: Policy name
146
+ action: ALLOW or BLOCK
147
+ site_id: Site identifier
148
+ settings: Application settings
149
+ source_zone_id: Source zone ID
150
+ destination_zone_id: Destination zone ID
151
+ source_matching_target: ANY, IP, NETWORK, REGION, or CLIENT
152
+ destination_matching_target: ANY, IP, NETWORK, or REGION
153
+ protocol: all, tcp, udp, tcp_udp, or icmpv6
154
+ enabled: Whether policy is active
155
+ description: Optional description
156
+ confirm: REQUIRED True for mutating operations
157
+ dry_run: Preview changes without applying
158
+
159
+ Returns:
160
+ Created firewall policy object or dry-run preview
161
+
162
+ Raises:
163
+ ValueError: If confirm not True or invalid action
164
+ NotImplementedError: When using cloud API
165
+ """
166
+ _ensure_local_api(settings)
167
+
168
+ valid_actions = ["ALLOW", "BLOCK"]
169
+ action_upper = action.upper()
170
+ if action_upper not in valid_actions:
171
+ raise ValueError(f"Invalid action '{action}'. Must be one of: {valid_actions}")
172
+
173
+ source_config: dict[str, Any] = {"matching_target": source_matching_target.upper()}
174
+ if source_zone_id:
175
+ source_config["zone_id"] = source_zone_id
176
+
177
+ destination_config: dict[str, Any] = {"matching_target": destination_matching_target.upper()}
178
+ if destination_zone_id:
179
+ destination_config["zone_id"] = destination_zone_id
180
+
181
+ policy_data = FirewallPolicyCreate(
182
+ name=name,
183
+ action=action_upper,
184
+ enabled=enabled,
185
+ protocol=protocol,
186
+ source=source_config,
187
+ destination=destination_config,
188
+ description=description,
189
+ )
190
+
191
+ parameters = {
192
+ "site_id": site_id,
193
+ "name": name,
194
+ "action": action_upper,
195
+ "enabled": enabled,
196
+ }
197
+
198
+ if dry_run:
199
+ logger.info(f"DRY RUN: Would create firewall policy '{name}' in site '{site_id}'")
200
+ log_audit(
201
+ operation="create_firewall_policy",
202
+ parameters=parameters,
203
+ result="dry_run",
204
+ site_id=site_id,
205
+ dry_run=True,
206
+ )
207
+ return {
208
+ "status": "dry_run",
209
+ "message": f"Would create firewall policy '{name}'",
210
+ "policy": policy_data.model_dump(exclude_none=True),
211
+ }
212
+
213
+ if not confirm:
214
+ raise ValueError(
215
+ "This operation requires confirm=True to execute. "
216
+ "Use dry_run=True to preview changes first."
217
+ )
218
+
219
+ try:
220
+ async with UniFiClient(settings) as client:
221
+ logger.info(f"Creating firewall policy '{name}' for site {site_id}")
222
+
223
+ if not client.is_authenticated:
224
+ await client.authenticate()
225
+
226
+ endpoint = f"{settings.get_v2_api_path(site_id)}/firewall-policies"
227
+ response = await client.post(
228
+ endpoint, json_data=policy_data.model_dump(exclude_none=True)
229
+ )
230
+
231
+ if isinstance(response, dict) and "data" in response:
232
+ data = response["data"]
233
+ else:
234
+ data = response
235
+
236
+ logger.info(f"Created firewall policy '{name}' in site '{site_id}'")
237
+ log_audit(
238
+ operation="create_firewall_policy",
239
+ parameters=parameters,
240
+ result="success",
241
+ site_id=site_id,
242
+ )
243
+
244
+ return FirewallPolicy(**data).model_dump()
245
+
246
+ except Exception as e:
247
+ logger.error(f"Failed to create firewall policy '{name}': {e}")
248
+ log_audit(
249
+ operation="create_firewall_policy",
250
+ parameters=parameters,
251
+ result="failed",
252
+ site_id=site_id,
253
+ )
254
+ raise
255
+
256
+
257
+ async def update_firewall_policy(
258
+ policy_id: str,
259
+ site_id: str = "default",
260
+ settings: Settings = None,
261
+ name: str | None = None,
262
+ action: str | None = None,
263
+ enabled: bool | None = None,
264
+ confirm: bool = False,
265
+ dry_run: bool = False,
266
+ ) -> dict[str, Any]:
267
+ """Update an existing firewall policy.
268
+
269
+ Only provided fields are updated (partial update).
270
+
271
+ Args:
272
+ policy_id: ID of policy to update
273
+ site_id: Site identifier
274
+ settings: Application settings
275
+ name: New policy name (optional)
276
+ action: New action ALLOW/BLOCK (optional)
277
+ enabled: Enable/disable (optional)
278
+ confirm: REQUIRED True for mutating operations
279
+ dry_run: Preview changes without applying
280
+
281
+ Returns:
282
+ Updated policy object
283
+
284
+ Raises:
285
+ NotImplementedError: When using cloud API (v2 endpoints require local access)
286
+ ValueError: If confirmation not provided
287
+ ResourceNotFoundError: If policy not found
288
+ """
289
+ _ensure_local_api(settings)
290
+
291
+ if not dry_run and not confirm:
292
+ raise ValueError(
293
+ "This operation requires confirm=True to execute. "
294
+ "Use dry_run=True to preview changes first."
295
+ )
296
+
297
+ # Build update payload with only provided fields
298
+ update_data: dict[str, Any] = {}
299
+ if name is not None:
300
+ update_data["name"] = name
301
+ if action is not None:
302
+ action_upper = action.upper()
303
+ if action_upper not in ["ALLOW", "BLOCK"]:
304
+ raise ValueError(f"Invalid action '{action}'. Must be ALLOW or BLOCK.")
305
+ update_data["action"] = action_upper
306
+ if enabled is not None:
307
+ update_data["enabled"] = enabled
308
+
309
+ if dry_run:
310
+ logger.info(f"DRY RUN: Would update firewall policy {policy_id}")
311
+ return {
312
+ "status": "dry_run",
313
+ "policy_id": policy_id,
314
+ "changes": update_data,
315
+ }
316
+
317
+ async with UniFiClient(settings) as client:
318
+ logger.info(f"Updating firewall policy {policy_id} for site {site_id}")
319
+
320
+ if not client.is_authenticated:
321
+ await client.authenticate()
322
+
323
+ endpoint = f"{settings.get_v2_api_path(site_id)}/firewall-policies/{policy_id}"
324
+
325
+ try:
326
+ response = await client.put(endpoint, json_data=update_data)
327
+ except ResourceNotFoundError as err:
328
+ raise ResourceNotFoundError("firewall_policy", policy_id) from err
329
+
330
+ if isinstance(response, dict) and "data" in response:
331
+ data = response["data"]
332
+ else:
333
+ data = response
334
+
335
+ logger.info(f"Updated firewall policy {policy_id}")
336
+ log_audit(
337
+ operation="update_firewall_policy",
338
+ parameters={"policy_id": policy_id, "site_id": site_id, **update_data},
339
+ result="success",
340
+ site_id=site_id,
341
+ )
342
+
343
+ return FirewallPolicy(**data).model_dump()
344
+
345
+
346
+ async def delete_firewall_policy(
347
+ policy_id: str,
348
+ site_id: str = "default",
349
+ settings: Settings = None,
350
+ confirm: bool = False,
351
+ dry_run: bool = False,
352
+ ) -> dict[str, Any]:
353
+ """Delete a firewall policy.
354
+
355
+ Warning: Cannot delete predefined system rules.
356
+
357
+ Args:
358
+ policy_id: ID of policy to delete
359
+ site_id: Site identifier
360
+ settings: Application settings
361
+ confirm: REQUIRED True for destructive operations
362
+ dry_run: Preview deletion without applying
363
+
364
+ Returns:
365
+ Confirmation of deletion
366
+
367
+ Raises:
368
+ NotImplementedError: When using cloud API (v2 endpoints require local access)
369
+ ValueError: If confirmation not provided or attempting to delete predefined rule
370
+ ResourceNotFoundError: If policy not found
371
+ """
372
+ _ensure_local_api(settings)
373
+
374
+ if not dry_run and not confirm:
375
+ raise ValueError("This operation deletes a firewall policy. Pass confirm=True to proceed.")
376
+
377
+ async with UniFiClient(settings) as client:
378
+ logger.info(f"Deleting firewall policy {policy_id} from site {site_id}")
379
+
380
+ if not client.is_authenticated:
381
+ await client.authenticate()
382
+
383
+ endpoint = f"{settings.get_v2_api_path(site_id)}/firewall-policies/{policy_id}"
384
+
385
+ try:
386
+ policy_response = await client.get(endpoint)
387
+ except ResourceNotFoundError as err:
388
+ raise ResourceNotFoundError("firewall_policy", policy_id) from err
389
+
390
+ if isinstance(policy_response, dict) and "data" in policy_response:
391
+ policy_data = policy_response["data"]
392
+ else:
393
+ policy_data = policy_response
394
+
395
+ if not policy_data:
396
+ raise ResourceNotFoundError("firewall_policy", policy_id)
397
+
398
+ policy = FirewallPolicy(**policy_data)
399
+
400
+ if policy.predefined:
401
+ raise ValueError(
402
+ f"Cannot delete predefined system rule '{policy.name}' (id={policy_id}). "
403
+ "Predefined rules are managed by the UniFi system."
404
+ )
405
+
406
+ if dry_run:
407
+ logger.info(f"DRY RUN: Would delete firewall policy {policy_id}")
408
+ return {
409
+ "status": "dry_run",
410
+ "policy_id": policy_id,
411
+ "action": "would_delete",
412
+ "policy": policy.model_dump(),
413
+ }
414
+
415
+ await client.delete(endpoint)
416
+
417
+ log_audit(
418
+ operation="delete_firewall_policy",
419
+ parameters={"policy_id": policy_id, "site_id": site_id},
420
+ result="success",
421
+ site_id=site_id,
422
+ )
423
+
424
+ logger.info(f"Deleted firewall policy {policy_id} from site {site_id}")
425
+
426
+ return {
427
+ "status": "success",
428
+ "policy_id": policy_id,
429
+ "action": "deleted",
430
+ }