mcp-eregistrations-bpa 0.8.5__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.

Potentially problematic release.


This version of mcp-eregistrations-bpa might be problematic. Click here for more details.

Files changed (66) hide show
  1. mcp_eregistrations_bpa/__init__.py +121 -0
  2. mcp_eregistrations_bpa/__main__.py +6 -0
  3. mcp_eregistrations_bpa/arazzo/__init__.py +21 -0
  4. mcp_eregistrations_bpa/arazzo/expression.py +379 -0
  5. mcp_eregistrations_bpa/audit/__init__.py +56 -0
  6. mcp_eregistrations_bpa/audit/context.py +66 -0
  7. mcp_eregistrations_bpa/audit/logger.py +236 -0
  8. mcp_eregistrations_bpa/audit/models.py +131 -0
  9. mcp_eregistrations_bpa/auth/__init__.py +64 -0
  10. mcp_eregistrations_bpa/auth/callback.py +391 -0
  11. mcp_eregistrations_bpa/auth/cas.py +409 -0
  12. mcp_eregistrations_bpa/auth/oidc.py +252 -0
  13. mcp_eregistrations_bpa/auth/permissions.py +162 -0
  14. mcp_eregistrations_bpa/auth/token_manager.py +348 -0
  15. mcp_eregistrations_bpa/bpa_client/__init__.py +84 -0
  16. mcp_eregistrations_bpa/bpa_client/client.py +740 -0
  17. mcp_eregistrations_bpa/bpa_client/endpoints.py +193 -0
  18. mcp_eregistrations_bpa/bpa_client/errors.py +276 -0
  19. mcp_eregistrations_bpa/bpa_client/models.py +203 -0
  20. mcp_eregistrations_bpa/config.py +349 -0
  21. mcp_eregistrations_bpa/db/__init__.py +21 -0
  22. mcp_eregistrations_bpa/db/connection.py +64 -0
  23. mcp_eregistrations_bpa/db/migrations.py +168 -0
  24. mcp_eregistrations_bpa/exceptions.py +39 -0
  25. mcp_eregistrations_bpa/py.typed +0 -0
  26. mcp_eregistrations_bpa/rollback/__init__.py +19 -0
  27. mcp_eregistrations_bpa/rollback/manager.py +616 -0
  28. mcp_eregistrations_bpa/server.py +152 -0
  29. mcp_eregistrations_bpa/tools/__init__.py +372 -0
  30. mcp_eregistrations_bpa/tools/actions.py +155 -0
  31. mcp_eregistrations_bpa/tools/analysis.py +352 -0
  32. mcp_eregistrations_bpa/tools/audit.py +399 -0
  33. mcp_eregistrations_bpa/tools/behaviours.py +1042 -0
  34. mcp_eregistrations_bpa/tools/bots.py +627 -0
  35. mcp_eregistrations_bpa/tools/classifications.py +575 -0
  36. mcp_eregistrations_bpa/tools/costs.py +765 -0
  37. mcp_eregistrations_bpa/tools/debug_strategies.py +351 -0
  38. mcp_eregistrations_bpa/tools/debugger.py +1230 -0
  39. mcp_eregistrations_bpa/tools/determinants.py +2235 -0
  40. mcp_eregistrations_bpa/tools/document_requirements.py +670 -0
  41. mcp_eregistrations_bpa/tools/export.py +899 -0
  42. mcp_eregistrations_bpa/tools/fields.py +162 -0
  43. mcp_eregistrations_bpa/tools/form_errors.py +36 -0
  44. mcp_eregistrations_bpa/tools/formio_helpers.py +971 -0
  45. mcp_eregistrations_bpa/tools/forms.py +1269 -0
  46. mcp_eregistrations_bpa/tools/jsonlogic_builder.py +466 -0
  47. mcp_eregistrations_bpa/tools/large_response.py +163 -0
  48. mcp_eregistrations_bpa/tools/messages.py +523 -0
  49. mcp_eregistrations_bpa/tools/notifications.py +241 -0
  50. mcp_eregistrations_bpa/tools/registration_institutions.py +680 -0
  51. mcp_eregistrations_bpa/tools/registrations.py +897 -0
  52. mcp_eregistrations_bpa/tools/role_status.py +447 -0
  53. mcp_eregistrations_bpa/tools/role_units.py +400 -0
  54. mcp_eregistrations_bpa/tools/roles.py +1236 -0
  55. mcp_eregistrations_bpa/tools/rollback.py +335 -0
  56. mcp_eregistrations_bpa/tools/services.py +674 -0
  57. mcp_eregistrations_bpa/tools/workflows.py +2487 -0
  58. mcp_eregistrations_bpa/tools/yaml_transformer.py +991 -0
  59. mcp_eregistrations_bpa/workflows/__init__.py +28 -0
  60. mcp_eregistrations_bpa/workflows/loader.py +440 -0
  61. mcp_eregistrations_bpa/workflows/models.py +336 -0
  62. mcp_eregistrations_bpa-0.8.5.dist-info/METADATA +965 -0
  63. mcp_eregistrations_bpa-0.8.5.dist-info/RECORD +66 -0
  64. mcp_eregistrations_bpa-0.8.5.dist-info/WHEEL +4 -0
  65. mcp_eregistrations_bpa-0.8.5.dist-info/entry_points.txt +2 -0
  66. mcp_eregistrations_bpa-0.8.5.dist-info/licenses/LICENSE +86 -0
@@ -0,0 +1,236 @@
1
+ """Audit logger for tracking BPA operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from mcp_eregistrations_bpa.audit.models import AuditEntry, AuditStatus
10
+ from mcp_eregistrations_bpa.db import get_connection
11
+
12
+
13
+ class AuditEntryNotFoundError(Exception):
14
+ """Raised when an audit entry is not found."""
15
+
16
+ pass
17
+
18
+
19
+ class AuditEntryImmutableError(Exception):
20
+ """Raised when attempting to modify a finalized audit entry."""
21
+
22
+ pass
23
+
24
+
25
+ class AuditLogger:
26
+ """Append-only audit logger for BPA operations.
27
+
28
+ This logger implements the audit-before-write pattern:
29
+ 1. Call record_pending() BEFORE executing the BPA operation
30
+ 2. Call mark_success() or mark_failed() AFTER the operation completes
31
+
32
+ The audit log is append-only (NFR5): entries can only be created and
33
+ their status updated, never deleted or modified otherwise.
34
+ """
35
+
36
+ def __init__(self, db_path: Path | None = None):
37
+ """Initialize the audit logger.
38
+
39
+ Args:
40
+ db_path: Optional path to SQLite database. If None, uses default.
41
+ """
42
+ self._db_path = db_path
43
+
44
+ async def record_pending(
45
+ self,
46
+ user_email: str,
47
+ operation_type: str,
48
+ object_type: str,
49
+ params: dict[str, Any],
50
+ object_id: str | None = None,
51
+ ) -> str:
52
+ """Record a pending operation BEFORE execution.
53
+
54
+ This method MUST be called before executing any BPA write operation.
55
+ It creates an audit entry with status='pending' that will be updated
56
+ after the operation completes.
57
+
58
+ Args:
59
+ user_email: Email of the user performing the operation
60
+ operation_type: Type (create, update, delete, link, unlink)
61
+ object_type: Type (service, registration, field, determinant, action)
62
+ params: Parameters passed to the operation
63
+ object_id: Optional ID of the object being operated on
64
+
65
+ Returns:
66
+ The audit entry ID (UUID string) for use with mark_success/mark_failed
67
+ """
68
+ entry = AuditEntry.create(
69
+ user_email=user_email,
70
+ operation_type=operation_type,
71
+ object_type=object_type,
72
+ params=params,
73
+ object_id=object_id,
74
+ )
75
+
76
+ async with get_connection(self._db_path) as conn:
77
+ await conn.execute(
78
+ """
79
+ INSERT INTO audit_logs (
80
+ id, timestamp, user_email, operation_type, object_type,
81
+ object_id, params, status, result, rollback_state_id
82
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
83
+ """,
84
+ (
85
+ entry.id,
86
+ entry.timestamp,
87
+ entry.user_email,
88
+ entry.operation_type,
89
+ entry.object_type,
90
+ entry.object_id,
91
+ json.dumps(entry.params),
92
+ entry.status.value,
93
+ None,
94
+ None,
95
+ ),
96
+ )
97
+ await conn.commit()
98
+
99
+ return entry.id
100
+
101
+ async def mark_success(self, audit_id: str, result: dict[str, Any]) -> None:
102
+ """Mark operation as successful with result summary.
103
+
104
+ Args:
105
+ audit_id: The audit entry ID returned by record_pending()
106
+ result: Summary of the operation result (e.g., created object ID)
107
+
108
+ Raises:
109
+ AuditEntryNotFoundError: If audit_id does not exist.
110
+ AuditEntryImmutableError: If entry is not in PENDING status.
111
+ """
112
+ async with get_connection(self._db_path) as conn:
113
+ # Check current status (append-only enforcement)
114
+ cursor = await conn.execute(
115
+ "SELECT status FROM audit_logs WHERE id = ?",
116
+ (audit_id,),
117
+ )
118
+ row = await cursor.fetchone()
119
+ if row is None:
120
+ raise AuditEntryNotFoundError(f"Audit entry '{audit_id}' not found")
121
+ if row["status"] != AuditStatus.PENDING.value:
122
+ raise AuditEntryImmutableError(
123
+ f"Cannot modify audit entry with status '{row['status']}'. "
124
+ "Only PENDING entries can be updated."
125
+ )
126
+
127
+ await conn.execute(
128
+ "UPDATE audit_logs SET status = ?, result = ? WHERE id = ?",
129
+ (AuditStatus.SUCCESS.value, json.dumps(result), audit_id),
130
+ )
131
+ await conn.commit()
132
+
133
+ async def mark_failed(self, audit_id: str, error_message: str) -> None:
134
+ """Mark operation as failed with error details.
135
+
136
+ Args:
137
+ audit_id: The audit entry ID returned by record_pending()
138
+ error_message: Description of the error that occurred
139
+
140
+ Raises:
141
+ AuditEntryNotFoundError: If audit_id does not exist.
142
+ AuditEntryImmutableError: If entry is not in PENDING status.
143
+ """
144
+ async with get_connection(self._db_path) as conn:
145
+ # Check current status (append-only enforcement)
146
+ cursor = await conn.execute(
147
+ "SELECT status FROM audit_logs WHERE id = ?",
148
+ (audit_id,),
149
+ )
150
+ row = await cursor.fetchone()
151
+ if row is None:
152
+ raise AuditEntryNotFoundError(f"Audit entry '{audit_id}' not found")
153
+ if row["status"] != AuditStatus.PENDING.value:
154
+ raise AuditEntryImmutableError(
155
+ f"Cannot modify audit entry with status '{row['status']}'. "
156
+ "Only PENDING entries can be updated."
157
+ )
158
+
159
+ await conn.execute(
160
+ "UPDATE audit_logs SET status = ?, result = ? WHERE id = ?",
161
+ (
162
+ AuditStatus.FAILED.value,
163
+ json.dumps({"error": error_message}),
164
+ audit_id,
165
+ ),
166
+ )
167
+ await conn.commit()
168
+
169
+ async def save_rollback_state(
170
+ self,
171
+ audit_id: str,
172
+ object_type: str,
173
+ object_id: str,
174
+ previous_state: dict[str, Any],
175
+ ) -> str:
176
+ """Save rollback state for an audit entry.
177
+
178
+ This enables rollback capability for update/delete operations by
179
+ storing the previous state of the object before changes were made.
180
+
181
+ Args:
182
+ audit_id: The audit entry ID to associate the rollback state with.
183
+ object_type: Type of object (service, registration, etc.).
184
+ object_id: ID of the object.
185
+ previous_state: The object state before the operation.
186
+
187
+ Returns:
188
+ The rollback state ID (UUID string).
189
+ """
190
+ import uuid
191
+
192
+ rollback_state_id = str(uuid.uuid4())
193
+
194
+ async with get_connection(self._db_path) as conn:
195
+ # Insert rollback state
196
+ await conn.execute(
197
+ """
198
+ INSERT INTO rollback_states (
199
+ id, audit_log_id, object_type, object_id, previous_state
200
+ ) VALUES (?, ?, ?, ?, ?)
201
+ """,
202
+ (
203
+ rollback_state_id,
204
+ audit_id,
205
+ object_type,
206
+ object_id,
207
+ json.dumps(previous_state),
208
+ ),
209
+ )
210
+ # Update audit entry with rollback_state_id
211
+ await conn.execute(
212
+ "UPDATE audit_logs SET rollback_state_id = ? WHERE id = ?",
213
+ (rollback_state_id, audit_id),
214
+ )
215
+ await conn.commit()
216
+
217
+ return rollback_state_id
218
+
219
+ async def get_entry(self, audit_id: str) -> AuditEntry | None:
220
+ """Retrieve audit entry by ID.
221
+
222
+ Args:
223
+ audit_id: The audit entry ID to retrieve
224
+
225
+ Returns:
226
+ The AuditEntry if found, None otherwise
227
+ """
228
+ async with get_connection(self._db_path) as conn:
229
+ cursor = await conn.execute(
230
+ "SELECT * FROM audit_logs WHERE id = ?",
231
+ (audit_id,),
232
+ )
233
+ row = await cursor.fetchone()
234
+ if row is None:
235
+ return None
236
+ return AuditEntry.from_row(dict(row))
@@ -0,0 +1,131 @@
1
+ """Audit models for tracking BPA operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import uuid
7
+ from dataclasses import dataclass
8
+ from datetime import UTC, datetime
9
+ from enum import Enum
10
+ from typing import Any
11
+
12
+
13
+ class AuditStatus(str, Enum):
14
+ """Status of an audit log entry."""
15
+
16
+ PENDING = "pending"
17
+ SUCCESS = "success"
18
+ FAILED = "failed"
19
+
20
+
21
+ class OperationType(str, Enum):
22
+ """Type of operation being audited."""
23
+
24
+ CREATE = "create"
25
+ UPDATE = "update"
26
+ DELETE = "delete"
27
+ LINK = "link"
28
+ UNLINK = "unlink"
29
+
30
+
31
+ class ObjectType(str, Enum):
32
+ """Type of object being operated on."""
33
+
34
+ SERVICE = "service"
35
+ REGISTRATION = "registration"
36
+ FIELD = "field"
37
+ DETERMINANT = "determinant"
38
+ ACTION = "action"
39
+
40
+
41
+ @dataclass
42
+ class AuditEntry:
43
+ """Represents a single audit log entry for a BPA operation.
44
+
45
+ Audit entries are created BEFORE operations execute (with status='pending')
46
+ and updated AFTER completion (with status='success' or 'failed').
47
+ """
48
+
49
+ id: str
50
+ timestamp: str
51
+ user_email: str
52
+ operation_type: str
53
+ object_type: str
54
+ params: dict[str, Any]
55
+ status: AuditStatus = AuditStatus.PENDING
56
+ object_id: str | None = None
57
+ result: dict[str, Any] | None = None
58
+ rollback_state_id: str | None = None
59
+
60
+ @classmethod
61
+ def create(
62
+ cls,
63
+ user_email: str,
64
+ operation_type: str,
65
+ object_type: str,
66
+ params: dict[str, Any],
67
+ object_id: str | None = None,
68
+ ) -> AuditEntry:
69
+ """Factory method for creating new audit entries.
70
+
71
+ Args:
72
+ user_email: Email of the user performing the operation
73
+ operation_type: Type (create, update, delete, link, unlink)
74
+ object_type: Type (service, registration, field, determinant, action)
75
+ params: Parameters passed to the operation
76
+ object_id: Optional ID of the object being operated on
77
+
78
+ Returns:
79
+ A new AuditEntry with generated UUID and timestamp
80
+ """
81
+ return cls(
82
+ id=str(uuid.uuid4()),
83
+ timestamp=datetime.now(UTC).isoformat(),
84
+ user_email=user_email,
85
+ operation_type=operation_type,
86
+ object_type=object_type,
87
+ params=params,
88
+ object_id=object_id,
89
+ )
90
+
91
+ def to_dict(self) -> dict[str, Any]:
92
+ """Convert to dict for database storage.
93
+
94
+ Returns:
95
+ Dictionary with all fields, params/result serialized as JSON strings
96
+ """
97
+ return {
98
+ "id": self.id,
99
+ "timestamp": self.timestamp,
100
+ "user_email": self.user_email,
101
+ "operation_type": self.operation_type,
102
+ "object_type": self.object_type,
103
+ "object_id": self.object_id,
104
+ "params": json.dumps(self.params),
105
+ "status": self.status.value,
106
+ "result": json.dumps(self.result) if self.result else None,
107
+ "rollback_state_id": self.rollback_state_id,
108
+ }
109
+
110
+ @classmethod
111
+ def from_row(cls, row: dict[str, Any]) -> AuditEntry:
112
+ """Create from database row.
113
+
114
+ Args:
115
+ row: Dictionary with database column values
116
+
117
+ Returns:
118
+ AuditEntry instance with deserialized data
119
+ """
120
+ return cls(
121
+ id=row["id"],
122
+ timestamp=row["timestamp"],
123
+ user_email=row["user_email"],
124
+ operation_type=row["operation_type"],
125
+ object_type=row["object_type"],
126
+ object_id=row["object_id"],
127
+ params=json.loads(row["params"]),
128
+ status=AuditStatus(row["status"]),
129
+ result=json.loads(row["result"]) if row["result"] else None,
130
+ rollback_state_id=row["rollback_state_id"],
131
+ )
@@ -0,0 +1,64 @@
1
+ """Authentication module for Keycloak OIDC and CAS.
2
+
3
+ This module provides authentication for both:
4
+ - Keycloak: OIDC/PKCE authentication (modern BPA systems)
5
+ - CAS: OAuth2 Basic Auth authentication (legacy BPA systems)
6
+
7
+ Both flows include browser-based login, token management, automatic refresh,
8
+ and permission enforcement.
9
+
10
+ Permission enforcement usage in MCP tools:
11
+ @mcp.tool()
12
+ async def service_create(name: str) -> dict[str, object]:
13
+ # Ensure write permission before proceeding
14
+ access_token = await ensure_write_permission()
15
+ # ... use access_token for BPA API call
16
+ """
17
+
18
+ from mcp_eregistrations_bpa.auth.callback import CallbackServer
19
+ from mcp_eregistrations_bpa.auth.cas import perform_cas_browser_login
20
+ from mcp_eregistrations_bpa.auth.oidc import (
21
+ OIDCConfig,
22
+ build_authorization_url,
23
+ discover_oidc_config,
24
+ generate_pkce_pair,
25
+ generate_state,
26
+ perform_browser_login,
27
+ )
28
+ from mcp_eregistrations_bpa.auth.permissions import (
29
+ PERMISSION_SERVICE_DESIGNER,
30
+ PERMISSION_VIEWER,
31
+ WRITE_PERMISSIONS,
32
+ check_permission,
33
+ ensure_authenticated,
34
+ ensure_write_permission,
35
+ )
36
+ from mcp_eregistrations_bpa.auth.token_manager import (
37
+ TokenManager,
38
+ TokenResponse,
39
+ exchange_code_for_tokens,
40
+ )
41
+
42
+ __all__ = [
43
+ # OIDC (Keycloak)
44
+ "OIDCConfig",
45
+ "discover_oidc_config",
46
+ "generate_pkce_pair",
47
+ "generate_state",
48
+ "build_authorization_url",
49
+ "perform_browser_login",
50
+ "CallbackServer",
51
+ # CAS (Legacy)
52
+ "perform_cas_browser_login",
53
+ # Token management
54
+ "TokenManager",
55
+ "TokenResponse",
56
+ "exchange_code_for_tokens",
57
+ # Permissions
58
+ "PERMISSION_VIEWER",
59
+ "PERMISSION_SERVICE_DESIGNER",
60
+ "WRITE_PERMISSIONS",
61
+ "ensure_authenticated",
62
+ "ensure_write_permission",
63
+ "check_permission",
64
+ ]