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.
- mcp_eregistrations_bpa/__init__.py +121 -0
- mcp_eregistrations_bpa/__main__.py +6 -0
- mcp_eregistrations_bpa/arazzo/__init__.py +21 -0
- mcp_eregistrations_bpa/arazzo/expression.py +379 -0
- mcp_eregistrations_bpa/audit/__init__.py +56 -0
- mcp_eregistrations_bpa/audit/context.py +66 -0
- mcp_eregistrations_bpa/audit/logger.py +236 -0
- mcp_eregistrations_bpa/audit/models.py +131 -0
- mcp_eregistrations_bpa/auth/__init__.py +64 -0
- mcp_eregistrations_bpa/auth/callback.py +391 -0
- mcp_eregistrations_bpa/auth/cas.py +409 -0
- mcp_eregistrations_bpa/auth/oidc.py +252 -0
- mcp_eregistrations_bpa/auth/permissions.py +162 -0
- mcp_eregistrations_bpa/auth/token_manager.py +348 -0
- mcp_eregistrations_bpa/bpa_client/__init__.py +84 -0
- mcp_eregistrations_bpa/bpa_client/client.py +740 -0
- mcp_eregistrations_bpa/bpa_client/endpoints.py +193 -0
- mcp_eregistrations_bpa/bpa_client/errors.py +276 -0
- mcp_eregistrations_bpa/bpa_client/models.py +203 -0
- mcp_eregistrations_bpa/config.py +349 -0
- mcp_eregistrations_bpa/db/__init__.py +21 -0
- mcp_eregistrations_bpa/db/connection.py +64 -0
- mcp_eregistrations_bpa/db/migrations.py +168 -0
- mcp_eregistrations_bpa/exceptions.py +39 -0
- mcp_eregistrations_bpa/py.typed +0 -0
- mcp_eregistrations_bpa/rollback/__init__.py +19 -0
- mcp_eregistrations_bpa/rollback/manager.py +616 -0
- mcp_eregistrations_bpa/server.py +152 -0
- mcp_eregistrations_bpa/tools/__init__.py +372 -0
- mcp_eregistrations_bpa/tools/actions.py +155 -0
- mcp_eregistrations_bpa/tools/analysis.py +352 -0
- mcp_eregistrations_bpa/tools/audit.py +399 -0
- mcp_eregistrations_bpa/tools/behaviours.py +1042 -0
- mcp_eregistrations_bpa/tools/bots.py +627 -0
- mcp_eregistrations_bpa/tools/classifications.py +575 -0
- mcp_eregistrations_bpa/tools/costs.py +765 -0
- mcp_eregistrations_bpa/tools/debug_strategies.py +351 -0
- mcp_eregistrations_bpa/tools/debugger.py +1230 -0
- mcp_eregistrations_bpa/tools/determinants.py +2235 -0
- mcp_eregistrations_bpa/tools/document_requirements.py +670 -0
- mcp_eregistrations_bpa/tools/export.py +899 -0
- mcp_eregistrations_bpa/tools/fields.py +162 -0
- mcp_eregistrations_bpa/tools/form_errors.py +36 -0
- mcp_eregistrations_bpa/tools/formio_helpers.py +971 -0
- mcp_eregistrations_bpa/tools/forms.py +1269 -0
- mcp_eregistrations_bpa/tools/jsonlogic_builder.py +466 -0
- mcp_eregistrations_bpa/tools/large_response.py +163 -0
- mcp_eregistrations_bpa/tools/messages.py +523 -0
- mcp_eregistrations_bpa/tools/notifications.py +241 -0
- mcp_eregistrations_bpa/tools/registration_institutions.py +680 -0
- mcp_eregistrations_bpa/tools/registrations.py +897 -0
- mcp_eregistrations_bpa/tools/role_status.py +447 -0
- mcp_eregistrations_bpa/tools/role_units.py +400 -0
- mcp_eregistrations_bpa/tools/roles.py +1236 -0
- mcp_eregistrations_bpa/tools/rollback.py +335 -0
- mcp_eregistrations_bpa/tools/services.py +674 -0
- mcp_eregistrations_bpa/tools/workflows.py +2487 -0
- mcp_eregistrations_bpa/tools/yaml_transformer.py +991 -0
- mcp_eregistrations_bpa/workflows/__init__.py +28 -0
- mcp_eregistrations_bpa/workflows/loader.py +440 -0
- mcp_eregistrations_bpa/workflows/models.py +336 -0
- mcp_eregistrations_bpa-0.8.5.dist-info/METADATA +965 -0
- mcp_eregistrations_bpa-0.8.5.dist-info/RECORD +66 -0
- mcp_eregistrations_bpa-0.8.5.dist-info/WHEEL +4 -0
- mcp_eregistrations_bpa-0.8.5.dist-info/entry_points.txt +2 -0
- mcp_eregistrations_bpa-0.8.5.dist-info/licenses/LICENSE +86 -0
|
@@ -0,0 +1,1236 @@
|
|
|
1
|
+
"""MCP tools for BPA role operations.
|
|
2
|
+
|
|
3
|
+
This module provides tools for listing, retrieving, creating, updating,
|
|
4
|
+
and deleting BPA roles. Roles are access control entities that define
|
|
5
|
+
user permissions within a service.
|
|
6
|
+
|
|
7
|
+
Write operations follow the audit-before-write pattern:
|
|
8
|
+
1. Validate parameters (pre-flight, no audit record if validation fails)
|
|
9
|
+
2. Create PENDING audit record
|
|
10
|
+
3. Execute BPA API call
|
|
11
|
+
4. Update audit record to SUCCESS or FAILED
|
|
12
|
+
|
|
13
|
+
API Endpoints used:
|
|
14
|
+
- GET /service/{service_id}/role - List roles for a service
|
|
15
|
+
- GET /role/{role_id} - Get role by ID
|
|
16
|
+
- POST /service/{service_id}/role - Create role within service
|
|
17
|
+
- PUT /role - Update role
|
|
18
|
+
- DELETE /role/{role_id} - Delete role
|
|
19
|
+
- POST /role/{role_id}/role_institution - Assign institution to role
|
|
20
|
+
- POST /role/{role_id}/role_registration - Assign registration to role
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from mcp.server.fastmcp.exceptions import ToolError
|
|
28
|
+
|
|
29
|
+
from mcp_eregistrations_bpa.audit.context import (
|
|
30
|
+
NotAuthenticatedError,
|
|
31
|
+
get_current_user_email,
|
|
32
|
+
)
|
|
33
|
+
from mcp_eregistrations_bpa.audit.logger import AuditLogger
|
|
34
|
+
from mcp_eregistrations_bpa.bpa_client import BPAClient
|
|
35
|
+
from mcp_eregistrations_bpa.bpa_client.errors import (
|
|
36
|
+
BPAClientError,
|
|
37
|
+
BPANotFoundError,
|
|
38
|
+
translate_error,
|
|
39
|
+
)
|
|
40
|
+
from mcp_eregistrations_bpa.tools.large_response import large_response_handler
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"role_list",
|
|
44
|
+
"role_get",
|
|
45
|
+
"role_create",
|
|
46
|
+
"role_update",
|
|
47
|
+
"role_delete",
|
|
48
|
+
"roleinstitution_create",
|
|
49
|
+
"roleregistration_create",
|
|
50
|
+
"register_role_tools",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _transform_role_response(data: dict[str, Any]) -> dict[str, Any]:
|
|
55
|
+
"""Transform role API response from camelCase to snake_case.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
data: Raw API response with camelCase keys.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
dict: Transformed response with snake_case keys.
|
|
62
|
+
"""
|
|
63
|
+
return {
|
|
64
|
+
# Core fields
|
|
65
|
+
"id": data.get("id"),
|
|
66
|
+
"name": data.get("name"),
|
|
67
|
+
"short_name": data.get("shortName"),
|
|
68
|
+
"role_type": data.get("type"),
|
|
69
|
+
"assigned_to": data.get("assignedTo"),
|
|
70
|
+
"description": data.get("description"),
|
|
71
|
+
"service_id": data.get("serviceId"),
|
|
72
|
+
# Workflow configuration
|
|
73
|
+
"start_role": data.get("startRole", False),
|
|
74
|
+
"visible_for_applicant": data.get("visibleForApplicant", True),
|
|
75
|
+
"sort_order_number": data.get("sortOrderNumber", 0),
|
|
76
|
+
"used_in_flow": data.get("usedInFlow", False),
|
|
77
|
+
# Permissions
|
|
78
|
+
"allow_to_confirm_payments": data.get("allowToConfirmPayments", False),
|
|
79
|
+
"allow_access_to_financial_reports": data.get(
|
|
80
|
+
"allowAccessToFinancialReports", False
|
|
81
|
+
),
|
|
82
|
+
# Linked entities (read-only)
|
|
83
|
+
"registrations": data.get("registrations", []),
|
|
84
|
+
"role_institutions": data.get("roleInstitutions", []),
|
|
85
|
+
"statuses": data.get("statuses", []),
|
|
86
|
+
# BotRole-specific fields (only present for BotRole type)
|
|
87
|
+
"repeat_until_successful": data.get("repeatUntilSuccessful"),
|
|
88
|
+
"repeat_in_minutes": data.get("repeatInMinutes"),
|
|
89
|
+
"repeat_in_hours": data.get("repeatInHours"),
|
|
90
|
+
"repeat_in_days": data.get("repeatInDays"),
|
|
91
|
+
"duration_in_minutes": data.get("durationInMinutes"),
|
|
92
|
+
"duration_in_hours": data.get("durationInHours"),
|
|
93
|
+
"duration_in_days": data.get("durationInDays"),
|
|
94
|
+
"bot_role_bots": data.get("botRoleBots"),
|
|
95
|
+
# Audit fields
|
|
96
|
+
"created_by": data.get("createdBy"),
|
|
97
|
+
"created_when": data.get("createdWhen"),
|
|
98
|
+
"last_changed_by": data.get("lastChangedBy"),
|
|
99
|
+
"last_changed_when": data.get("lastChangedWhen"),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@large_response_handler(
|
|
104
|
+
threshold_bytes=50 * 1024, # 50KB threshold for list tools
|
|
105
|
+
navigation={
|
|
106
|
+
"list_all": "jq '.roles'",
|
|
107
|
+
"find_by_type": "jq '.roles[] | select(.role_type == \"UserRole\")'",
|
|
108
|
+
"find_by_name": "jq '.roles[] | select(.name | contains(\"search\"))'",
|
|
109
|
+
"start_roles": "jq '.roles[] | select(.start_role == true)'",
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
async def role_list(service_id: str | int) -> dict[str, Any]:
|
|
113
|
+
"""List all roles for a BPA service.
|
|
114
|
+
|
|
115
|
+
Returns roles configured for the specified service.
|
|
116
|
+
Large responses (>50KB) are saved to file with navigation hints.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
service_id: The service ID to list roles for (required).
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
dict with roles, service_id, total.
|
|
123
|
+
"""
|
|
124
|
+
if not service_id:
|
|
125
|
+
raise ToolError(
|
|
126
|
+
"Cannot list roles: 'service_id' is required. "
|
|
127
|
+
"Use 'service_list' to find valid service IDs."
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
async with BPAClient() as client:
|
|
132
|
+
try:
|
|
133
|
+
roles_data = await client.get_list(
|
|
134
|
+
"/service/{service_id}/role",
|
|
135
|
+
path_params={"service_id": service_id},
|
|
136
|
+
resource_type="role",
|
|
137
|
+
)
|
|
138
|
+
except BPANotFoundError:
|
|
139
|
+
raise ToolError(
|
|
140
|
+
f"Service '{service_id}' not found. "
|
|
141
|
+
"Use 'service_list' to see available services."
|
|
142
|
+
)
|
|
143
|
+
except ToolError:
|
|
144
|
+
raise
|
|
145
|
+
except BPAClientError as e:
|
|
146
|
+
raise translate_error(e, resource_type="role")
|
|
147
|
+
|
|
148
|
+
# Transform to consistent output format
|
|
149
|
+
roles = [_transform_role_response(role) for role in roles_data]
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
"roles": roles,
|
|
153
|
+
"service_id": service_id,
|
|
154
|
+
"total": len(roles),
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
async def _resolve_destination_role_names(
|
|
159
|
+
client: BPAClient, statuses: list[dict[str, Any]]
|
|
160
|
+
) -> list[dict[str, Any]]:
|
|
161
|
+
"""Resolve destination IDs in statuses to role names.
|
|
162
|
+
|
|
163
|
+
For each status with a destinationId, fetches the destination role
|
|
164
|
+
to get its name. Uses caching to avoid duplicate lookups.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
client: Active BPAClient connection.
|
|
168
|
+
statuses: List of status objects from role response.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
list: Enhanced status objects with destination_role_name field.
|
|
172
|
+
"""
|
|
173
|
+
if not statuses:
|
|
174
|
+
return []
|
|
175
|
+
|
|
176
|
+
# Build mapping of destination_id -> role_name (with caching)
|
|
177
|
+
destination_ids: set[str] = set()
|
|
178
|
+
for status in statuses:
|
|
179
|
+
dest_id = status.get("destinationId")
|
|
180
|
+
if dest_id:
|
|
181
|
+
destination_ids.add(dest_id)
|
|
182
|
+
|
|
183
|
+
# Fetch role names for all unique destination IDs
|
|
184
|
+
id_to_name: dict[str, str | None] = {}
|
|
185
|
+
for dest_id in destination_ids:
|
|
186
|
+
try:
|
|
187
|
+
dest_role = await client.get(
|
|
188
|
+
"/role/{role_id}",
|
|
189
|
+
path_params={"role_id": dest_id},
|
|
190
|
+
resource_type="role",
|
|
191
|
+
resource_id=dest_id,
|
|
192
|
+
)
|
|
193
|
+
id_to_name[dest_id] = dest_role.get("name")
|
|
194
|
+
except BPANotFoundError:
|
|
195
|
+
# Graceful fallback: role may have been deleted
|
|
196
|
+
id_to_name[dest_id] = None
|
|
197
|
+
except BPAClientError:
|
|
198
|
+
# Other errors: still graceful fallback
|
|
199
|
+
id_to_name[dest_id] = None
|
|
200
|
+
|
|
201
|
+
# Enhance statuses with resolved names
|
|
202
|
+
enhanced_statuses = []
|
|
203
|
+
for status in statuses:
|
|
204
|
+
enhanced = {
|
|
205
|
+
"name": status.get("name"),
|
|
206
|
+
"type": status.get("type"),
|
|
207
|
+
}
|
|
208
|
+
dest_id = status.get("destinationId")
|
|
209
|
+
if dest_id:
|
|
210
|
+
enhanced["destination_id"] = dest_id
|
|
211
|
+
enhanced["destination_role_name"] = id_to_name.get(dest_id)
|
|
212
|
+
enhanced_statuses.append(enhanced)
|
|
213
|
+
|
|
214
|
+
return enhanced_statuses
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
async def role_get(role_id: str | int) -> dict[str, Any]:
|
|
218
|
+
"""Get details of a BPA role by ID.
|
|
219
|
+
|
|
220
|
+
Returns complete role details with resolved destination role names.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
role_id: The unique identifier of the role.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
dict: Complete role details including:
|
|
227
|
+
- id, name, description
|
|
228
|
+
- service_id: The parent service ID
|
|
229
|
+
- statuses: Array with destination_id and destination_role_name resolved
|
|
230
|
+
"""
|
|
231
|
+
if not role_id:
|
|
232
|
+
raise ToolError(
|
|
233
|
+
"Cannot get role: 'role_id' is required. "
|
|
234
|
+
"Use 'role_list' with service_id to find valid role IDs."
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
async with BPAClient() as client:
|
|
239
|
+
try:
|
|
240
|
+
role_data = await client.get(
|
|
241
|
+
"/role/{role_id}",
|
|
242
|
+
path_params={"role_id": role_id},
|
|
243
|
+
resource_type="role",
|
|
244
|
+
resource_id=role_id,
|
|
245
|
+
)
|
|
246
|
+
except BPANotFoundError:
|
|
247
|
+
raise ToolError(
|
|
248
|
+
f"Role '{role_id}' not found. "
|
|
249
|
+
"Use 'role_list' with service_id to see available roles."
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Resolve destination IDs in statuses to role names
|
|
253
|
+
raw_statuses = role_data.get("statuses", [])
|
|
254
|
+
enhanced_statuses = await _resolve_destination_role_names(
|
|
255
|
+
client, raw_statuses
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
except ToolError:
|
|
259
|
+
raise
|
|
260
|
+
except BPAClientError as e:
|
|
261
|
+
raise translate_error(e, resource_type="role", resource_id=role_id)
|
|
262
|
+
|
|
263
|
+
result = _transform_role_response(role_data)
|
|
264
|
+
# Override statuses with enhanced version containing resolved names
|
|
265
|
+
result["statuses"] = enhanced_statuses
|
|
266
|
+
return result
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _validate_role_create_params(
|
|
270
|
+
service_id: str | int,
|
|
271
|
+
name: str,
|
|
272
|
+
short_name: str,
|
|
273
|
+
assigned_to: str,
|
|
274
|
+
role_type: str,
|
|
275
|
+
description: str | None,
|
|
276
|
+
start_role: bool,
|
|
277
|
+
visible_for_applicant: bool,
|
|
278
|
+
sort_order_number: int,
|
|
279
|
+
allow_to_confirm_payments: bool,
|
|
280
|
+
allow_access_to_financial_reports: bool,
|
|
281
|
+
# BotRole-specific parameters
|
|
282
|
+
repeat_until_successful: bool | None,
|
|
283
|
+
repeat_in_minutes: int | None,
|
|
284
|
+
repeat_in_hours: int | None,
|
|
285
|
+
repeat_in_days: int | None,
|
|
286
|
+
duration_in_minutes: int | None,
|
|
287
|
+
duration_in_hours: int | None,
|
|
288
|
+
duration_in_days: int | None,
|
|
289
|
+
) -> dict[str, Any]:
|
|
290
|
+
"""Validate role_create parameters (pre-flight).
|
|
291
|
+
|
|
292
|
+
Returns validated params dict or raises ToolError if invalid.
|
|
293
|
+
No audit record is created for validation failures.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
service_id: Parent service ID (required).
|
|
297
|
+
name: Role name (required).
|
|
298
|
+
short_name: Short name for the role (required by BPA API).
|
|
299
|
+
assigned_to: Role pool assignment string (e.g., "processing").
|
|
300
|
+
role_type: Role type - "UserRole" for humans, "BotRole" for automation.
|
|
301
|
+
description: Role description (optional).
|
|
302
|
+
start_role: Whether this is the workflow entry point.
|
|
303
|
+
visible_for_applicant: Whether visible to applicants.
|
|
304
|
+
sort_order_number: Ordering in workflow.
|
|
305
|
+
allow_to_confirm_payments: Payment confirmation permission (UserRole only).
|
|
306
|
+
allow_access_to_financial_reports: Financial reports permission (UserRole only).
|
|
307
|
+
repeat_until_successful: Retry on failure (BotRole only).
|
|
308
|
+
repeat_in_minutes: Retry interval in minutes (BotRole only).
|
|
309
|
+
repeat_in_hours: Retry interval in hours (BotRole only).
|
|
310
|
+
repeat_in_days: Retry interval in days (BotRole only).
|
|
311
|
+
duration_in_minutes: Execution timeout in minutes (BotRole only).
|
|
312
|
+
duration_in_hours: Execution timeout in hours (BotRole only).
|
|
313
|
+
duration_in_days: Execution timeout in days (BotRole only).
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
dict: Validated parameters ready for API call.
|
|
317
|
+
|
|
318
|
+
Raises:
|
|
319
|
+
ToolError: If validation fails.
|
|
320
|
+
"""
|
|
321
|
+
errors = []
|
|
322
|
+
|
|
323
|
+
if not service_id:
|
|
324
|
+
errors.append("'service_id' is required")
|
|
325
|
+
|
|
326
|
+
if not name or not name.strip():
|
|
327
|
+
errors.append("'name' is required and cannot be empty")
|
|
328
|
+
|
|
329
|
+
if name and len(name.strip()) > 255:
|
|
330
|
+
errors.append("'name' must be 255 characters or less")
|
|
331
|
+
|
|
332
|
+
if not short_name or not short_name.strip():
|
|
333
|
+
errors.append("'short_name' is required and cannot be empty")
|
|
334
|
+
|
|
335
|
+
if short_name and len(short_name.strip()) > 50:
|
|
336
|
+
errors.append("'short_name' must be 50 characters or less")
|
|
337
|
+
|
|
338
|
+
if not assigned_to or not assigned_to.strip():
|
|
339
|
+
errors.append("'assigned_to' is required and cannot be empty")
|
|
340
|
+
elif len(assigned_to.strip()) < 2:
|
|
341
|
+
errors.append("'assigned_to' must be at least 2 characters")
|
|
342
|
+
elif len(assigned_to.strip()) > 255:
|
|
343
|
+
errors.append("'assigned_to' must be 255 characters or less")
|
|
344
|
+
|
|
345
|
+
valid_role_types = ["UserRole", "BotRole"]
|
|
346
|
+
if role_type not in valid_role_types:
|
|
347
|
+
errors.append(f"'role_type' must be one of: {', '.join(valid_role_types)}")
|
|
348
|
+
|
|
349
|
+
if errors:
|
|
350
|
+
error_msg = "; ".join(errors)
|
|
351
|
+
raise ToolError(f"Cannot create role: {error_msg}. Check required fields.")
|
|
352
|
+
|
|
353
|
+
params: dict[str, Any] = {
|
|
354
|
+
"name": name.strip(),
|
|
355
|
+
"shortName": short_name.strip(),
|
|
356
|
+
"type": role_type,
|
|
357
|
+
"assignedTo": assigned_to.strip(),
|
|
358
|
+
# Workflow configuration
|
|
359
|
+
"startRole": start_role,
|
|
360
|
+
"visibleForApplicant": visible_for_applicant,
|
|
361
|
+
"sortOrderNumber": sort_order_number,
|
|
362
|
+
}
|
|
363
|
+
if description:
|
|
364
|
+
params["description"] = description.strip()
|
|
365
|
+
|
|
366
|
+
# Add role-type-specific parameters
|
|
367
|
+
if role_type == "UserRole":
|
|
368
|
+
# UserRole-specific permissions
|
|
369
|
+
params["allowToConfirmPayments"] = allow_to_confirm_payments
|
|
370
|
+
params["allowAccessToFinancialReports"] = allow_access_to_financial_reports
|
|
371
|
+
elif role_type == "BotRole":
|
|
372
|
+
# BotRole-specific automation configuration
|
|
373
|
+
if repeat_until_successful is not None:
|
|
374
|
+
params["repeatUntilSuccessful"] = repeat_until_successful
|
|
375
|
+
if repeat_in_minutes is not None:
|
|
376
|
+
params["repeatInMinutes"] = repeat_in_minutes
|
|
377
|
+
if repeat_in_hours is not None:
|
|
378
|
+
params["repeatInHours"] = repeat_in_hours
|
|
379
|
+
if repeat_in_days is not None:
|
|
380
|
+
params["repeatInDays"] = repeat_in_days
|
|
381
|
+
if duration_in_minutes is not None:
|
|
382
|
+
params["durationInMinutes"] = duration_in_minutes
|
|
383
|
+
if duration_in_hours is not None:
|
|
384
|
+
params["durationInHours"] = duration_in_hours
|
|
385
|
+
if duration_in_days is not None:
|
|
386
|
+
params["durationInDays"] = duration_in_days
|
|
387
|
+
|
|
388
|
+
return params
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
async def role_create(
|
|
392
|
+
service_id: str | int,
|
|
393
|
+
name: str,
|
|
394
|
+
short_name: str,
|
|
395
|
+
assigned_to: str = "processing",
|
|
396
|
+
role_type: str = "UserRole",
|
|
397
|
+
description: str | None = None,
|
|
398
|
+
start_role: bool = False,
|
|
399
|
+
visible_for_applicant: bool = True,
|
|
400
|
+
sort_order_number: int = 0,
|
|
401
|
+
allow_to_confirm_payments: bool = False,
|
|
402
|
+
allow_access_to_financial_reports: bool = False,
|
|
403
|
+
# BotRole-specific parameters
|
|
404
|
+
repeat_until_successful: bool | None = None,
|
|
405
|
+
repeat_in_minutes: int | None = None,
|
|
406
|
+
repeat_in_hours: int | None = None,
|
|
407
|
+
repeat_in_days: int | None = None,
|
|
408
|
+
duration_in_minutes: int | None = None,
|
|
409
|
+
duration_in_hours: int | None = None,
|
|
410
|
+
duration_in_days: int | None = None,
|
|
411
|
+
) -> dict[str, Any]:
|
|
412
|
+
"""Create role in a service. Audited write operation.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
service_id: Parent service ID.
|
|
416
|
+
name: Role name.
|
|
417
|
+
short_name: Short name (required by BPA).
|
|
418
|
+
assigned_to: Role pool (default: "processing").
|
|
419
|
+
role_type: "UserRole" or "BotRole" (default: "UserRole").
|
|
420
|
+
description: Optional description.
|
|
421
|
+
start_role: Workflow entry point (default: False).
|
|
422
|
+
visible_for_applicant: Visible to applicants (default: True).
|
|
423
|
+
sort_order_number: Workflow position (default: 0).
|
|
424
|
+
allow_to_confirm_payments: Payment permission (UserRole only).
|
|
425
|
+
allow_access_to_financial_reports: Reports permission (UserRole only).
|
|
426
|
+
repeat_until_successful: Retry on failure (BotRole only).
|
|
427
|
+
repeat_in_minutes/hours/days: Retry interval (BotRole only).
|
|
428
|
+
duration_in_minutes/hours/days: Execution timeout (BotRole only).
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
dict with role details, service_id, audit_id.
|
|
432
|
+
"""
|
|
433
|
+
# Pre-flight validation (no audit record for validation failures)
|
|
434
|
+
validated_params = _validate_role_create_params(
|
|
435
|
+
service_id,
|
|
436
|
+
name,
|
|
437
|
+
short_name,
|
|
438
|
+
assigned_to,
|
|
439
|
+
role_type,
|
|
440
|
+
description,
|
|
441
|
+
start_role,
|
|
442
|
+
visible_for_applicant,
|
|
443
|
+
sort_order_number,
|
|
444
|
+
allow_to_confirm_payments,
|
|
445
|
+
allow_access_to_financial_reports,
|
|
446
|
+
# BotRole-specific parameters
|
|
447
|
+
repeat_until_successful,
|
|
448
|
+
repeat_in_minutes,
|
|
449
|
+
repeat_in_hours,
|
|
450
|
+
repeat_in_days,
|
|
451
|
+
duration_in_minutes,
|
|
452
|
+
duration_in_hours,
|
|
453
|
+
duration_in_days,
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
# Get authenticated user for audit (before any API calls)
|
|
457
|
+
try:
|
|
458
|
+
user_email = get_current_user_email()
|
|
459
|
+
except NotAuthenticatedError as e:
|
|
460
|
+
raise ToolError(str(e))
|
|
461
|
+
|
|
462
|
+
# Use single BPAClient connection for all operations
|
|
463
|
+
try:
|
|
464
|
+
async with BPAClient() as client:
|
|
465
|
+
# Verify parent service exists before creating audit record
|
|
466
|
+
try:
|
|
467
|
+
await client.get(
|
|
468
|
+
"/service/{id}",
|
|
469
|
+
path_params={"id": service_id},
|
|
470
|
+
resource_type="service",
|
|
471
|
+
resource_id=service_id,
|
|
472
|
+
)
|
|
473
|
+
except BPANotFoundError:
|
|
474
|
+
raise ToolError(
|
|
475
|
+
f"Cannot create role: Service '{service_id}' not found. "
|
|
476
|
+
"Use 'service_list' to see available services."
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
# Create audit record BEFORE API call (audit-before-write pattern)
|
|
480
|
+
audit_logger = AuditLogger()
|
|
481
|
+
audit_id = await audit_logger.record_pending(
|
|
482
|
+
user_email=user_email,
|
|
483
|
+
operation_type="create",
|
|
484
|
+
object_type="role",
|
|
485
|
+
params={
|
|
486
|
+
"service_id": str(service_id),
|
|
487
|
+
**validated_params,
|
|
488
|
+
},
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
try:
|
|
492
|
+
role_data = await client.post(
|
|
493
|
+
"/service/{service_id}/role",
|
|
494
|
+
path_params={"service_id": service_id},
|
|
495
|
+
json=validated_params,
|
|
496
|
+
resource_type="role",
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
# Save rollback state (for create, save ID to enable deletion)
|
|
500
|
+
created_id = role_data.get("id")
|
|
501
|
+
await audit_logger.save_rollback_state(
|
|
502
|
+
audit_id=audit_id,
|
|
503
|
+
object_type="role",
|
|
504
|
+
object_id=str(created_id),
|
|
505
|
+
previous_state={
|
|
506
|
+
"id": created_id,
|
|
507
|
+
"name": role_data.get("name"),
|
|
508
|
+
"shortName": role_data.get("shortName"),
|
|
509
|
+
"type": role_data.get("type"),
|
|
510
|
+
"assignedTo": role_data.get("assignedTo"),
|
|
511
|
+
"description": role_data.get("description"),
|
|
512
|
+
"serviceId": str(service_id),
|
|
513
|
+
# Workflow configuration
|
|
514
|
+
"startRole": role_data.get("startRole"),
|
|
515
|
+
"visibleForApplicant": role_data.get("visibleForApplicant"),
|
|
516
|
+
"sortOrderNumber": role_data.get("sortOrderNumber"),
|
|
517
|
+
# UserRole permissions
|
|
518
|
+
"allowToConfirmPayments": role_data.get(
|
|
519
|
+
"allowToConfirmPayments"
|
|
520
|
+
),
|
|
521
|
+
"allowAccessToFinancialReports": role_data.get(
|
|
522
|
+
"allowAccessToFinancialReports"
|
|
523
|
+
),
|
|
524
|
+
# BotRole-specific fields
|
|
525
|
+
"repeatUntilSuccessful": role_data.get("repeatUntilSuccessful"),
|
|
526
|
+
"repeatInMinutes": role_data.get("repeatInMinutes"),
|
|
527
|
+
"repeatInHours": role_data.get("repeatInHours"),
|
|
528
|
+
"repeatInDays": role_data.get("repeatInDays"),
|
|
529
|
+
"durationInMinutes": role_data.get("durationInMinutes"),
|
|
530
|
+
"durationInHours": role_data.get("durationInHours"),
|
|
531
|
+
"durationInDays": role_data.get("durationInDays"),
|
|
532
|
+
"_operation": "create", # Marker for rollback to DELETE
|
|
533
|
+
},
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
# Mark audit as success
|
|
537
|
+
await audit_logger.mark_success(
|
|
538
|
+
audit_id,
|
|
539
|
+
result={
|
|
540
|
+
"role_id": role_data.get("id"),
|
|
541
|
+
"name": role_data.get("name"),
|
|
542
|
+
"service_id": str(service_id),
|
|
543
|
+
},
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
result = _transform_role_response(role_data)
|
|
547
|
+
result["service_id"] = service_id # Ensure service_id is always set
|
|
548
|
+
result["audit_id"] = audit_id
|
|
549
|
+
return result
|
|
550
|
+
|
|
551
|
+
except BPAClientError as e:
|
|
552
|
+
# Mark audit as failed
|
|
553
|
+
await audit_logger.mark_failed(audit_id, str(e))
|
|
554
|
+
raise translate_error(e, resource_type="role")
|
|
555
|
+
|
|
556
|
+
except ToolError:
|
|
557
|
+
raise
|
|
558
|
+
except BPAClientError as e:
|
|
559
|
+
raise translate_error(e, resource_type="service", resource_id=service_id)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _validate_role_update_params(
|
|
563
|
+
role_id: str | int,
|
|
564
|
+
name: str | None,
|
|
565
|
+
short_name: str | None,
|
|
566
|
+
assigned_to: str | None,
|
|
567
|
+
description: str | None,
|
|
568
|
+
start_role: bool | None,
|
|
569
|
+
visible_for_applicant: bool | None,
|
|
570
|
+
sort_order_number: int | None,
|
|
571
|
+
allow_to_confirm_payments: bool | None,
|
|
572
|
+
allow_access_to_financial_reports: bool | None,
|
|
573
|
+
) -> dict[str, Any]:
|
|
574
|
+
"""Validate role_update parameters (pre-flight).
|
|
575
|
+
|
|
576
|
+
Returns validated params dict or raises ToolError if invalid.
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
role_id: ID of role to update (required).
|
|
580
|
+
name: New name (optional).
|
|
581
|
+
short_name: New short name (optional).
|
|
582
|
+
assigned_to: New role pool assignment string (optional).
|
|
583
|
+
description: New description (optional).
|
|
584
|
+
start_role: Whether this is the workflow entry point (optional).
|
|
585
|
+
visible_for_applicant: Whether visible to applicants (optional).
|
|
586
|
+
sort_order_number: Ordering in workflow (optional).
|
|
587
|
+
allow_to_confirm_payments: Payment confirmation permission (optional).
|
|
588
|
+
allow_access_to_financial_reports: Financial reports permission (optional).
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
dict: Validated parameters ready for API call.
|
|
592
|
+
|
|
593
|
+
Raises:
|
|
594
|
+
ToolError: If validation fails.
|
|
595
|
+
"""
|
|
596
|
+
errors = []
|
|
597
|
+
|
|
598
|
+
if not role_id:
|
|
599
|
+
errors.append("'role_id' is required")
|
|
600
|
+
|
|
601
|
+
if name is not None and not name.strip():
|
|
602
|
+
errors.append("'name' cannot be empty when provided")
|
|
603
|
+
|
|
604
|
+
if name and len(name.strip()) > 255:
|
|
605
|
+
errors.append("'name' must be 255 characters or less")
|
|
606
|
+
|
|
607
|
+
if short_name is not None and not short_name.strip():
|
|
608
|
+
errors.append("'short_name' cannot be empty when provided")
|
|
609
|
+
|
|
610
|
+
if short_name and len(short_name.strip()) > 50:
|
|
611
|
+
errors.append("'short_name' must be 50 characters or less")
|
|
612
|
+
|
|
613
|
+
if assigned_to is not None and not assigned_to.strip():
|
|
614
|
+
errors.append("'assigned_to' cannot be empty when provided")
|
|
615
|
+
|
|
616
|
+
# At least one field must be provided for update
|
|
617
|
+
all_none = (
|
|
618
|
+
name is None
|
|
619
|
+
and short_name is None
|
|
620
|
+
and assigned_to is None
|
|
621
|
+
and description is None
|
|
622
|
+
and start_role is None
|
|
623
|
+
and visible_for_applicant is None
|
|
624
|
+
and sort_order_number is None
|
|
625
|
+
and allow_to_confirm_payments is None
|
|
626
|
+
and allow_access_to_financial_reports is None
|
|
627
|
+
)
|
|
628
|
+
if all_none:
|
|
629
|
+
errors.append("At least one field must be provided for update")
|
|
630
|
+
|
|
631
|
+
if errors:
|
|
632
|
+
error_msg = "; ".join(errors)
|
|
633
|
+
raise ToolError(f"Cannot update role: {error_msg}. Check required fields.")
|
|
634
|
+
|
|
635
|
+
params: dict[str, Any] = {"id": role_id}
|
|
636
|
+
if name is not None:
|
|
637
|
+
params["name"] = name.strip()
|
|
638
|
+
if short_name is not None:
|
|
639
|
+
params["shortName"] = short_name.strip()
|
|
640
|
+
if assigned_to is not None:
|
|
641
|
+
params["assignedTo"] = assigned_to.strip()
|
|
642
|
+
if description is not None:
|
|
643
|
+
params["description"] = description.strip()
|
|
644
|
+
# Workflow configuration
|
|
645
|
+
if start_role is not None:
|
|
646
|
+
params["startRole"] = start_role
|
|
647
|
+
if visible_for_applicant is not None:
|
|
648
|
+
params["visibleForApplicant"] = visible_for_applicant
|
|
649
|
+
if sort_order_number is not None:
|
|
650
|
+
params["sortOrderNumber"] = sort_order_number
|
|
651
|
+
# Permissions
|
|
652
|
+
if allow_to_confirm_payments is not None:
|
|
653
|
+
params["allowToConfirmPayments"] = allow_to_confirm_payments
|
|
654
|
+
if allow_access_to_financial_reports is not None:
|
|
655
|
+
params["allowAccessToFinancialReports"] = allow_access_to_financial_reports
|
|
656
|
+
|
|
657
|
+
return params
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
async def role_update(
|
|
661
|
+
role_id: str | int,
|
|
662
|
+
name: str | None = None,
|
|
663
|
+
short_name: str | None = None,
|
|
664
|
+
assigned_to: str | None = None,
|
|
665
|
+
description: str | None = None,
|
|
666
|
+
start_role: bool | None = None,
|
|
667
|
+
visible_for_applicant: bool | None = None,
|
|
668
|
+
sort_order_number: int | None = None,
|
|
669
|
+
allow_to_confirm_payments: bool | None = None,
|
|
670
|
+
allow_access_to_financial_reports: bool | None = None,
|
|
671
|
+
) -> dict[str, Any]:
|
|
672
|
+
"""Update an existing BPA role. Audited write operation.
|
|
673
|
+
|
|
674
|
+
Args:
|
|
675
|
+
role_id: Role ID to update.
|
|
676
|
+
name: New name (optional).
|
|
677
|
+
short_name: New short name (optional).
|
|
678
|
+
assigned_to: New role pool (optional).
|
|
679
|
+
description: New description (optional).
|
|
680
|
+
start_role: Workflow entry point (optional).
|
|
681
|
+
visible_for_applicant: Visible to applicants (optional).
|
|
682
|
+
sort_order_number: Workflow position (optional).
|
|
683
|
+
allow_to_confirm_payments: Payment permission (optional).
|
|
684
|
+
allow_access_to_financial_reports: Reports permission (optional).
|
|
685
|
+
|
|
686
|
+
Returns:
|
|
687
|
+
dict with updated role, previous_state, audit_id.
|
|
688
|
+
"""
|
|
689
|
+
# Pre-flight validation (no audit record for validation failures)
|
|
690
|
+
validated_params = _validate_role_update_params(
|
|
691
|
+
role_id,
|
|
692
|
+
name,
|
|
693
|
+
short_name,
|
|
694
|
+
assigned_to,
|
|
695
|
+
description,
|
|
696
|
+
start_role,
|
|
697
|
+
visible_for_applicant,
|
|
698
|
+
sort_order_number,
|
|
699
|
+
allow_to_confirm_payments,
|
|
700
|
+
allow_access_to_financial_reports,
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
# Get authenticated user for audit
|
|
704
|
+
try:
|
|
705
|
+
user_email = get_current_user_email()
|
|
706
|
+
except NotAuthenticatedError as e:
|
|
707
|
+
raise ToolError(str(e))
|
|
708
|
+
|
|
709
|
+
# Use single BPAClient connection for all operations
|
|
710
|
+
try:
|
|
711
|
+
async with BPAClient() as client:
|
|
712
|
+
# Capture current state for rollback BEFORE making changes
|
|
713
|
+
try:
|
|
714
|
+
previous_state = await client.get(
|
|
715
|
+
"/role/{role_id}",
|
|
716
|
+
path_params={"role_id": role_id},
|
|
717
|
+
resource_type="role",
|
|
718
|
+
resource_id=role_id,
|
|
719
|
+
)
|
|
720
|
+
except BPANotFoundError:
|
|
721
|
+
raise ToolError(
|
|
722
|
+
f"Role '{role_id}' not found. "
|
|
723
|
+
"Use 'role_list' with service_id to see available roles."
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
# Merge provided changes with current state for full object PUT
|
|
727
|
+
full_params = {
|
|
728
|
+
"id": role_id,
|
|
729
|
+
"name": validated_params.get("name", previous_state.get("name")),
|
|
730
|
+
"shortName": validated_params.get(
|
|
731
|
+
"shortName", previous_state.get("shortName")
|
|
732
|
+
),
|
|
733
|
+
"assignedTo": validated_params.get(
|
|
734
|
+
"assignedTo", previous_state.get("assignedTo")
|
|
735
|
+
),
|
|
736
|
+
"description": validated_params.get(
|
|
737
|
+
"description", previous_state.get("description")
|
|
738
|
+
),
|
|
739
|
+
"serviceId": previous_state.get("serviceId"),
|
|
740
|
+
# Workflow configuration
|
|
741
|
+
"startRole": validated_params.get(
|
|
742
|
+
"startRole", previous_state.get("startRole", False)
|
|
743
|
+
),
|
|
744
|
+
"visibleForApplicant": validated_params.get(
|
|
745
|
+
"visibleForApplicant",
|
|
746
|
+
previous_state.get("visibleForApplicant", True),
|
|
747
|
+
),
|
|
748
|
+
"sortOrderNumber": validated_params.get(
|
|
749
|
+
"sortOrderNumber", previous_state.get("sortOrderNumber", 0)
|
|
750
|
+
),
|
|
751
|
+
# Permissions
|
|
752
|
+
"allowToConfirmPayments": validated_params.get(
|
|
753
|
+
"allowToConfirmPayments",
|
|
754
|
+
previous_state.get("allowToConfirmPayments", False),
|
|
755
|
+
),
|
|
756
|
+
"allowAccessToFinancialReports": validated_params.get(
|
|
757
|
+
"allowAccessToFinancialReports",
|
|
758
|
+
previous_state.get("allowAccessToFinancialReports", False),
|
|
759
|
+
),
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
# Create audit record BEFORE API call (audit-before-write pattern)
|
|
763
|
+
audit_logger = AuditLogger()
|
|
764
|
+
audit_id = await audit_logger.record_pending(
|
|
765
|
+
user_email=user_email,
|
|
766
|
+
operation_type="update",
|
|
767
|
+
object_type="role",
|
|
768
|
+
object_id=str(role_id),
|
|
769
|
+
params={
|
|
770
|
+
"changes": validated_params,
|
|
771
|
+
},
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
# Save rollback state for undo capability
|
|
775
|
+
await audit_logger.save_rollback_state(
|
|
776
|
+
audit_id=audit_id,
|
|
777
|
+
object_type="role",
|
|
778
|
+
object_id=str(role_id),
|
|
779
|
+
previous_state={
|
|
780
|
+
"id": previous_state.get("id"),
|
|
781
|
+
"name": previous_state.get("name"),
|
|
782
|
+
"shortName": previous_state.get("shortName"),
|
|
783
|
+
"type": previous_state.get("type"),
|
|
784
|
+
"assignedTo": previous_state.get("assignedTo"),
|
|
785
|
+
"description": previous_state.get("description"),
|
|
786
|
+
"serviceId": previous_state.get("serviceId"),
|
|
787
|
+
# Workflow configuration
|
|
788
|
+
"startRole": previous_state.get("startRole"),
|
|
789
|
+
"visibleForApplicant": previous_state.get("visibleForApplicant"),
|
|
790
|
+
"sortOrderNumber": previous_state.get("sortOrderNumber"),
|
|
791
|
+
# UserRole permissions
|
|
792
|
+
"allowToConfirmPayments": previous_state.get(
|
|
793
|
+
"allowToConfirmPayments"
|
|
794
|
+
),
|
|
795
|
+
"allowAccessToFinancialReports": previous_state.get(
|
|
796
|
+
"allowAccessToFinancialReports"
|
|
797
|
+
),
|
|
798
|
+
# BotRole-specific fields
|
|
799
|
+
"repeatUntilSuccessful": previous_state.get(
|
|
800
|
+
"repeatUntilSuccessful"
|
|
801
|
+
),
|
|
802
|
+
"repeatInMinutes": previous_state.get("repeatInMinutes"),
|
|
803
|
+
"repeatInHours": previous_state.get("repeatInHours"),
|
|
804
|
+
"repeatInDays": previous_state.get("repeatInDays"),
|
|
805
|
+
"durationInMinutes": previous_state.get("durationInMinutes"),
|
|
806
|
+
"durationInHours": previous_state.get("durationInHours"),
|
|
807
|
+
"durationInDays": previous_state.get("durationInDays"),
|
|
808
|
+
},
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
try:
|
|
812
|
+
role_data = await client.put(
|
|
813
|
+
"/role",
|
|
814
|
+
json=full_params,
|
|
815
|
+
resource_type="role",
|
|
816
|
+
resource_id=role_id,
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
# Mark audit as success
|
|
820
|
+
await audit_logger.mark_success(
|
|
821
|
+
audit_id,
|
|
822
|
+
result={
|
|
823
|
+
"role_id": role_data.get("id"),
|
|
824
|
+
"name": role_data.get("name"),
|
|
825
|
+
"changes_applied": {
|
|
826
|
+
k: v for k, v in validated_params.items() if k != "id"
|
|
827
|
+
},
|
|
828
|
+
},
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
result = _transform_role_response(role_data)
|
|
832
|
+
result["previous_state"] = {
|
|
833
|
+
"name": previous_state.get("name"),
|
|
834
|
+
"short_name": previous_state.get("shortName"),
|
|
835
|
+
"assigned_to": previous_state.get("assignedTo"),
|
|
836
|
+
"description": previous_state.get("description"),
|
|
837
|
+
# Workflow configuration
|
|
838
|
+
"start_role": previous_state.get("startRole"),
|
|
839
|
+
"visible_for_applicant": previous_state.get("visibleForApplicant"),
|
|
840
|
+
"sort_order_number": previous_state.get("sortOrderNumber"),
|
|
841
|
+
# Permissions
|
|
842
|
+
"allow_to_confirm_payments": previous_state.get(
|
|
843
|
+
"allowToConfirmPayments"
|
|
844
|
+
),
|
|
845
|
+
"allow_access_to_financial_reports": previous_state.get(
|
|
846
|
+
"allowAccessToFinancialReports"
|
|
847
|
+
),
|
|
848
|
+
}
|
|
849
|
+
result["audit_id"] = audit_id
|
|
850
|
+
return result
|
|
851
|
+
|
|
852
|
+
except BPAClientError as e:
|
|
853
|
+
# Mark audit as failed
|
|
854
|
+
await audit_logger.mark_failed(audit_id, str(e))
|
|
855
|
+
raise translate_error(e, resource_type="role", resource_id=role_id)
|
|
856
|
+
|
|
857
|
+
except ToolError:
|
|
858
|
+
raise
|
|
859
|
+
except BPAClientError as e:
|
|
860
|
+
raise translate_error(e, resource_type="role", resource_id=role_id)
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
def _validate_role_delete_params(role_id: str | int) -> None:
|
|
864
|
+
"""Validate role_delete parameters (pre-flight).
|
|
865
|
+
|
|
866
|
+
Raises ToolError if validation fails.
|
|
867
|
+
|
|
868
|
+
Args:
|
|
869
|
+
role_id: Role ID to delete (required).
|
|
870
|
+
|
|
871
|
+
Raises:
|
|
872
|
+
ToolError: If validation fails.
|
|
873
|
+
"""
|
|
874
|
+
if not role_id:
|
|
875
|
+
raise ToolError(
|
|
876
|
+
"Cannot delete role: 'role_id' is required. "
|
|
877
|
+
"Use 'role_list' with service_id to find valid role IDs."
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
async def role_delete(role_id: str | int) -> dict[str, Any]:
|
|
882
|
+
"""Delete a BPA role. Audited write operation.
|
|
883
|
+
|
|
884
|
+
Known Issue: BPA may return "Camunda publish problem" - contact administrator.
|
|
885
|
+
|
|
886
|
+
Args:
|
|
887
|
+
role_id: Role ID to delete.
|
|
888
|
+
|
|
889
|
+
Returns:
|
|
890
|
+
dict with deleted (bool), role_id, deleted_role, audit_id.
|
|
891
|
+
"""
|
|
892
|
+
# Pre-flight validation (no audit record for validation failures)
|
|
893
|
+
_validate_role_delete_params(role_id)
|
|
894
|
+
|
|
895
|
+
# Get authenticated user for audit
|
|
896
|
+
try:
|
|
897
|
+
user_email = get_current_user_email()
|
|
898
|
+
except NotAuthenticatedError as e:
|
|
899
|
+
raise ToolError(str(e))
|
|
900
|
+
|
|
901
|
+
# Use single BPAClient connection for all operations
|
|
902
|
+
try:
|
|
903
|
+
async with BPAClient() as client:
|
|
904
|
+
# Capture current state for rollback BEFORE making changes
|
|
905
|
+
try:
|
|
906
|
+
previous_state = await client.get(
|
|
907
|
+
"/role/{role_id}",
|
|
908
|
+
path_params={"role_id": role_id},
|
|
909
|
+
resource_type="role",
|
|
910
|
+
resource_id=role_id,
|
|
911
|
+
)
|
|
912
|
+
except BPANotFoundError:
|
|
913
|
+
raise ToolError(
|
|
914
|
+
f"Role '{role_id}' not found. "
|
|
915
|
+
"Use 'role_list' with service_id to see available roles."
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
# Normalize previous_state to snake_case for consistency
|
|
919
|
+
normalized_previous_state = {
|
|
920
|
+
"id": previous_state.get("id"),
|
|
921
|
+
"name": previous_state.get("name"),
|
|
922
|
+
"assigned_to": previous_state.get("assignedTo"),
|
|
923
|
+
"description": previous_state.get("description"),
|
|
924
|
+
"service_id": previous_state.get("serviceId"),
|
|
925
|
+
# Workflow configuration
|
|
926
|
+
"start_role": previous_state.get("startRole"),
|
|
927
|
+
"visible_for_applicant": previous_state.get("visibleForApplicant"),
|
|
928
|
+
"sort_order_number": previous_state.get("sortOrderNumber"),
|
|
929
|
+
# Permissions
|
|
930
|
+
"allow_to_confirm_payments": previous_state.get(
|
|
931
|
+
"allowToConfirmPayments"
|
|
932
|
+
),
|
|
933
|
+
"allow_access_to_financial_reports": previous_state.get(
|
|
934
|
+
"allowAccessToFinancialReports"
|
|
935
|
+
),
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
# Create audit record BEFORE API call (audit-before-write pattern)
|
|
939
|
+
audit_logger = AuditLogger()
|
|
940
|
+
audit_id = await audit_logger.record_pending(
|
|
941
|
+
user_email=user_email,
|
|
942
|
+
operation_type="delete",
|
|
943
|
+
object_type="role",
|
|
944
|
+
object_id=str(role_id),
|
|
945
|
+
params={},
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
# Save rollback state for undo capability (recreate on rollback)
|
|
949
|
+
await audit_logger.save_rollback_state(
|
|
950
|
+
audit_id=audit_id,
|
|
951
|
+
object_type="role",
|
|
952
|
+
object_id=str(role_id),
|
|
953
|
+
previous_state={
|
|
954
|
+
"id": previous_state.get("id"),
|
|
955
|
+
"name": previous_state.get("name"),
|
|
956
|
+
"shortName": previous_state.get("shortName"),
|
|
957
|
+
"type": previous_state.get("type"),
|
|
958
|
+
"assignedTo": previous_state.get("assignedTo"),
|
|
959
|
+
"description": previous_state.get("description"),
|
|
960
|
+
"serviceId": previous_state.get("serviceId"),
|
|
961
|
+
# Workflow configuration
|
|
962
|
+
"startRole": previous_state.get("startRole"),
|
|
963
|
+
"visibleForApplicant": previous_state.get("visibleForApplicant"),
|
|
964
|
+
"sortOrderNumber": previous_state.get("sortOrderNumber"),
|
|
965
|
+
# UserRole permissions
|
|
966
|
+
"allowToConfirmPayments": previous_state.get(
|
|
967
|
+
"allowToConfirmPayments"
|
|
968
|
+
),
|
|
969
|
+
"allowAccessToFinancialReports": previous_state.get(
|
|
970
|
+
"allowAccessToFinancialReports"
|
|
971
|
+
),
|
|
972
|
+
# BotRole-specific fields
|
|
973
|
+
"repeatUntilSuccessful": previous_state.get(
|
|
974
|
+
"repeatUntilSuccessful"
|
|
975
|
+
),
|
|
976
|
+
"repeatInMinutes": previous_state.get("repeatInMinutes"),
|
|
977
|
+
"repeatInHours": previous_state.get("repeatInHours"),
|
|
978
|
+
"repeatInDays": previous_state.get("repeatInDays"),
|
|
979
|
+
"durationInMinutes": previous_state.get("durationInMinutes"),
|
|
980
|
+
"durationInHours": previous_state.get("durationInHours"),
|
|
981
|
+
"durationInDays": previous_state.get("durationInDays"),
|
|
982
|
+
},
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
try:
|
|
986
|
+
await client.delete(
|
|
987
|
+
"/role/{role_id}",
|
|
988
|
+
path_params={"role_id": role_id},
|
|
989
|
+
resource_type="role",
|
|
990
|
+
resource_id=role_id,
|
|
991
|
+
)
|
|
992
|
+
|
|
993
|
+
# Mark audit as success
|
|
994
|
+
await audit_logger.mark_success(
|
|
995
|
+
audit_id,
|
|
996
|
+
result={
|
|
997
|
+
"deleted": True,
|
|
998
|
+
"role_id": str(role_id),
|
|
999
|
+
},
|
|
1000
|
+
)
|
|
1001
|
+
|
|
1002
|
+
return {
|
|
1003
|
+
"deleted": True,
|
|
1004
|
+
"role_id": str(role_id), # Normalize to string for consistency
|
|
1005
|
+
"deleted_role": {
|
|
1006
|
+
"id": normalized_previous_state["id"],
|
|
1007
|
+
"name": normalized_previous_state["name"],
|
|
1008
|
+
"service_id": normalized_previous_state["service_id"],
|
|
1009
|
+
},
|
|
1010
|
+
"audit_id": audit_id,
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
except BPAClientError as e:
|
|
1014
|
+
# Mark audit as failed
|
|
1015
|
+
await audit_logger.mark_failed(audit_id, str(e))
|
|
1016
|
+
raise translate_error(e, resource_type="role", resource_id=role_id)
|
|
1017
|
+
|
|
1018
|
+
except ToolError:
|
|
1019
|
+
raise
|
|
1020
|
+
except BPAClientError as e:
|
|
1021
|
+
raise translate_error(e, resource_type="role", resource_id=role_id)
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
async def roleinstitution_create(
|
|
1025
|
+
role_id: str | int,
|
|
1026
|
+
institution_id: str,
|
|
1027
|
+
) -> dict[str, Any]:
|
|
1028
|
+
"""Assign institution to a role. Audited write operation. Required for publishing.
|
|
1029
|
+
|
|
1030
|
+
Args:
|
|
1031
|
+
role_id: Role to assign institution to.
|
|
1032
|
+
institution_id: Institution to assign.
|
|
1033
|
+
|
|
1034
|
+
Returns:
|
|
1035
|
+
dict with id, role_id, institution_id, audit_id.
|
|
1036
|
+
"""
|
|
1037
|
+
# Pre-flight validation (no audit if these fail)
|
|
1038
|
+
if not role_id:
|
|
1039
|
+
raise ToolError(
|
|
1040
|
+
"Cannot create role institution: 'role_id' is required. "
|
|
1041
|
+
"Use 'role_list' with service_id to find valid role IDs."
|
|
1042
|
+
)
|
|
1043
|
+
if not institution_id:
|
|
1044
|
+
raise ToolError(
|
|
1045
|
+
"Cannot create role institution: 'institution_id' is required. "
|
|
1046
|
+
"Use 'institution_discover' to find valid institution IDs."
|
|
1047
|
+
)
|
|
1048
|
+
|
|
1049
|
+
# Get user email for audit
|
|
1050
|
+
try:
|
|
1051
|
+
user_email = get_current_user_email()
|
|
1052
|
+
except NotAuthenticatedError as e:
|
|
1053
|
+
raise ToolError(str(e))
|
|
1054
|
+
|
|
1055
|
+
audit_logger = AuditLogger()
|
|
1056
|
+
|
|
1057
|
+
try:
|
|
1058
|
+
async with BPAClient() as client:
|
|
1059
|
+
# Verify role exists (no audit if not found)
|
|
1060
|
+
try:
|
|
1061
|
+
await client.get(
|
|
1062
|
+
"/role/{role_id}",
|
|
1063
|
+
path_params={"role_id": role_id},
|
|
1064
|
+
resource_type="role",
|
|
1065
|
+
resource_id=role_id,
|
|
1066
|
+
)
|
|
1067
|
+
except BPANotFoundError:
|
|
1068
|
+
raise ToolError(
|
|
1069
|
+
f"Role '{role_id}' not found. "
|
|
1070
|
+
"Use 'role_list' with service_id to see available roles."
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1073
|
+
# Create PENDING audit record
|
|
1074
|
+
audit_id = await audit_logger.record_pending(
|
|
1075
|
+
user_email=user_email,
|
|
1076
|
+
operation_type="link",
|
|
1077
|
+
object_type="role_institution",
|
|
1078
|
+
params={
|
|
1079
|
+
"role_id": str(role_id),
|
|
1080
|
+
"institution_id": institution_id,
|
|
1081
|
+
},
|
|
1082
|
+
)
|
|
1083
|
+
|
|
1084
|
+
# Execute API call - body is the raw institution_id string
|
|
1085
|
+
try:
|
|
1086
|
+
result = await client.post(
|
|
1087
|
+
"/role/{role_id}/role_institution",
|
|
1088
|
+
path_params={"role_id": role_id},
|
|
1089
|
+
content=institution_id, # Raw string body
|
|
1090
|
+
resource_type="role_institution",
|
|
1091
|
+
)
|
|
1092
|
+
|
|
1093
|
+
# Mark audit as success
|
|
1094
|
+
await audit_logger.mark_success(
|
|
1095
|
+
audit_id,
|
|
1096
|
+
result={
|
|
1097
|
+
"role_id": str(role_id),
|
|
1098
|
+
"institution_id": institution_id,
|
|
1099
|
+
},
|
|
1100
|
+
)
|
|
1101
|
+
|
|
1102
|
+
# Transform response
|
|
1103
|
+
return {
|
|
1104
|
+
"id": result.get("id") if isinstance(result, dict) else None,
|
|
1105
|
+
"role_id": str(role_id),
|
|
1106
|
+
"institution_id": institution_id,
|
|
1107
|
+
"audit_id": audit_id,
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
except BPAClientError as e:
|
|
1111
|
+
await audit_logger.mark_failed(audit_id, str(e))
|
|
1112
|
+
raise translate_error(e, resource_type="role_institution")
|
|
1113
|
+
|
|
1114
|
+
except ToolError:
|
|
1115
|
+
raise
|
|
1116
|
+
except BPAClientError as e:
|
|
1117
|
+
raise translate_error(e, resource_type="role", resource_id=role_id)
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
async def roleregistration_create(
|
|
1121
|
+
role_id: str | int,
|
|
1122
|
+
registration_id: str | int,
|
|
1123
|
+
) -> dict[str, Any]:
|
|
1124
|
+
"""Assign registration to a role. Audited write operation. Required for publishing.
|
|
1125
|
+
|
|
1126
|
+
Args:
|
|
1127
|
+
role_id: Role to assign registration to.
|
|
1128
|
+
registration_id: Registration to assign.
|
|
1129
|
+
|
|
1130
|
+
Returns:
|
|
1131
|
+
dict with id, role_id, registration_id, audit_id.
|
|
1132
|
+
"""
|
|
1133
|
+
# Pre-flight validation (no audit if these fail)
|
|
1134
|
+
if not role_id:
|
|
1135
|
+
raise ToolError(
|
|
1136
|
+
"Cannot create role registration: 'role_id' is required. "
|
|
1137
|
+
"Use 'role_list' with service_id to find valid role IDs."
|
|
1138
|
+
)
|
|
1139
|
+
if not registration_id:
|
|
1140
|
+
raise ToolError(
|
|
1141
|
+
"Cannot create role registration: 'registration_id' is required. "
|
|
1142
|
+
"Use 'registration_list' with service_id to find valid registration IDs."
|
|
1143
|
+
)
|
|
1144
|
+
|
|
1145
|
+
# Get user email for audit
|
|
1146
|
+
try:
|
|
1147
|
+
user_email = get_current_user_email()
|
|
1148
|
+
except NotAuthenticatedError as e:
|
|
1149
|
+
raise ToolError(str(e))
|
|
1150
|
+
|
|
1151
|
+
audit_logger = AuditLogger()
|
|
1152
|
+
|
|
1153
|
+
try:
|
|
1154
|
+
async with BPAClient() as client:
|
|
1155
|
+
# Verify role exists (no audit if not found)
|
|
1156
|
+
try:
|
|
1157
|
+
await client.get(
|
|
1158
|
+
"/role/{role_id}",
|
|
1159
|
+
path_params={"role_id": role_id},
|
|
1160
|
+
resource_type="role",
|
|
1161
|
+
resource_id=role_id,
|
|
1162
|
+
)
|
|
1163
|
+
except BPANotFoundError:
|
|
1164
|
+
raise ToolError(
|
|
1165
|
+
f"Role '{role_id}' not found. "
|
|
1166
|
+
"Use 'role_list' with service_id to see available roles."
|
|
1167
|
+
)
|
|
1168
|
+
|
|
1169
|
+
# Create PENDING audit record
|
|
1170
|
+
audit_id = await audit_logger.record_pending(
|
|
1171
|
+
user_email=user_email,
|
|
1172
|
+
operation_type="link",
|
|
1173
|
+
object_type="role_registration",
|
|
1174
|
+
params={
|
|
1175
|
+
"role_id": str(role_id),
|
|
1176
|
+
"registration_id": str(registration_id),
|
|
1177
|
+
},
|
|
1178
|
+
)
|
|
1179
|
+
|
|
1180
|
+
# Execute API call - body is the raw registration_id string
|
|
1181
|
+
try:
|
|
1182
|
+
result = await client.post(
|
|
1183
|
+
"/role/{role_id}/role_registration",
|
|
1184
|
+
path_params={"role_id": role_id},
|
|
1185
|
+
content=str(
|
|
1186
|
+
registration_id
|
|
1187
|
+
), # Raw string body like role_institution
|
|
1188
|
+
resource_type="role_registration",
|
|
1189
|
+
)
|
|
1190
|
+
|
|
1191
|
+
# Mark audit as success
|
|
1192
|
+
await audit_logger.mark_success(
|
|
1193
|
+
audit_id,
|
|
1194
|
+
result={
|
|
1195
|
+
"role_id": str(role_id),
|
|
1196
|
+
"registration_id": str(registration_id),
|
|
1197
|
+
},
|
|
1198
|
+
)
|
|
1199
|
+
|
|
1200
|
+
# Transform response
|
|
1201
|
+
return {
|
|
1202
|
+
"id": result.get("id") if isinstance(result, dict) else None,
|
|
1203
|
+
"role_id": str(role_id),
|
|
1204
|
+
"registration_id": str(registration_id),
|
|
1205
|
+
"audit_id": audit_id,
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
except BPAClientError as e:
|
|
1209
|
+
await audit_logger.mark_failed(audit_id, str(e))
|
|
1210
|
+
raise translate_error(e, resource_type="role_registration")
|
|
1211
|
+
|
|
1212
|
+
except ToolError:
|
|
1213
|
+
raise
|
|
1214
|
+
except BPAClientError as e:
|
|
1215
|
+
raise translate_error(e, resource_type="role", resource_id=role_id)
|
|
1216
|
+
|
|
1217
|
+
|
|
1218
|
+
def register_role_tools(mcp: Any) -> None:
|
|
1219
|
+
"""Register role tools with the MCP server.
|
|
1220
|
+
|
|
1221
|
+
Args:
|
|
1222
|
+
mcp: The FastMCP server instance.
|
|
1223
|
+
"""
|
|
1224
|
+
# Read operations
|
|
1225
|
+
mcp.tool()(role_list)
|
|
1226
|
+
mcp.tool()(role_get)
|
|
1227
|
+
# Write operations (audit-before-write pattern)
|
|
1228
|
+
mcp.tool()(role_create)
|
|
1229
|
+
mcp.tool()(role_update)
|
|
1230
|
+
# Role assignment tools (required for publishing)
|
|
1231
|
+
mcp.tool()(roleinstitution_create)
|
|
1232
|
+
mcp.tool()(roleregistration_create)
|
|
1233
|
+
# NOTE: role_delete disabled due to Camunda server-side 404 error.
|
|
1234
|
+
# The BPA server returns "Camunda publish problem" when deleting roles.
|
|
1235
|
+
# Re-enable when the server-side issue is resolved.
|
|
1236
|
+
# mcp.tool()(role_delete)
|