1
+from typing import List
2
+
3
+from fastapi import APIRouter
4
+from fastapi import Depends
5
+from fastapi import HTTPException
6
+from fastapi import Security
7
+from loguru import logger
8
+from sqlalchemy.ext.asyncio import AsyncSession
9
+
10
+from app.auth.models.users import User
11
+from app.auth.utils import AuthHandler
12
+from app.db.db_session import get_db
13
+from app.incidents.middleware.tag_access import tag_access_handler
14
+from app.incidents.schema.db_operations import AlertTagItem
15
+from app.incidents.schema.db_operations import AllTagsResponse
16
+from app.incidents.schema.db_operations import RoleTagAccessResponse
17
+from app.incidents.schema.db_operations import TagAccessCreate
18
+from app.incidents.schema.db_operations import TagAccessSettingsItem
19
+from app.incidents.schema.db_operations import TagAccessSettingsResponse
20
+from app.incidents.schema.db_operations import TagAccessSettingsUpdate
21
+from app.incidents.schema.db_operations import UserEffectiveAccessResponse
22
+from app.incidents.schema.db_operations import UserTagAccessResponse
23
+from app.incidents.services import tag_access as tag_access_service
24
+
25
+tag_access_router = APIRouter()
26
+
27
+
28
+def _require_admin(current_user: User) -> User:
29
+ """Verify user has admin role."""
30
+ from app.auth.models.users import RoleEnum
31
+
32
+ if current_user.role_id != RoleEnum.admin.value:
33
+ raise HTTPException(status_code=403, detail="Admin access required")
34
+ return current_user
35
+
36
+
37
+def _tags_to_response(tags) -> List[AlertTagItem]:
38
+ """Convert AlertTag objects to response items."""
39
+ return [AlertTagItem(id=t.id, tag=t.tag) for t in tags]
40
+
41
+
42
+# ============================================
43
+# Settings Endpoints
44
+# ============================================
45
+
46
+
47
+@tag_access_router.get(
48
+ "/settings",
49
+ response_model=TagAccessSettingsResponse,
50
+ description="Get current tag access settings (admin only)",
51
+)
52
+async def get_tag_access_settings(
53
+ current_user: User = Security(AuthHandler().get_current_user),
54
+ db: AsyncSession = Depends(get_db),
55
+):
56
+ """Get current tag access settings."""
57
+ _require_admin(current_user)
58
+
59
+ settings = await tag_access_service.get_or_create_tag_settings(db)
60
+
61
+ # Get default tag name if set
62
+ default_tag_name = None
63
+ if settings.default_tag_id:
64
+ tag = await tag_access_service.get_tag_by_id(settings.default_tag_id, db)
65
+ if tag:
66
+ default_tag_name = tag.tag
67
+
68
+ return TagAccessSettingsResponse(
69
+ settings=TagAccessSettingsItem(
70
+ enabled=settings.enabled,
71
+ untagged_alert_behavior=settings.untagged_alert_behavior,
72
+ default_tag_id=settings.default_tag_id,
73
+ default_tag_name=default_tag_name,
74
+ ),
75
+ success=True,
76
+ message="Settings retrieved successfully",
77
+ )
78
+
79
+
80
+@tag_access_router.put(
81
+ "/settings",
82
+ response_model=TagAccessSettingsResponse,
83
+ description="Update tag access settings (admin only)",
84
+)
85
+async def update_tag_access_settings(
86
+ request: TagAccessSettingsUpdate,
87
+ current_user: User = Security(AuthHandler().get_current_user),
88
+ db: AsyncSession = Depends(get_db),
89
+):
90
+ """Update tag access settings."""
91
+ _require_admin(current_user)
92
+
93
+ # Validate default_tag_id exists if specified
94
+ if request.default_tag_id:
95
+ tag = await tag_access_service.get_tag_by_id(request.default_tag_id, db)
96
+ if not tag:
97
+ raise HTTPException(
98
+ status_code=404,
99
+ detail=f"Tag with id {request.default_tag_id} not found",
100
+ )
101
+
102
+ settings = await tag_access_service.update_tag_access_settings(
103
+ enabled=request.enabled,
104
+ untagged_alert_behavior=request.untagged_alert_behavior.value,
105
+ default_tag_id=request.default_tag_id,
106
+ updated_by=current_user.username,
107
+ db=db,
108
+ )
109
+
110
+ # Get default tag name if set
111
+ default_tag_name = None
112
+ if settings.default_tag_id:
113
+ tag = await tag_access_service.get_tag_by_id(settings.default_tag_id, db)
114
+ if tag:
115
+ default_tag_name = tag.tag
116
+
117
+ logger.info(f"Admin {current_user.username} updated tag access settings")
118
+
119
+ # Return with settings nested under 'settings' key to match schema
120
+ return TagAccessSettingsResponse(
121
+ settings=TagAccessSettingsItem(
122
+ enabled=settings.enabled,
123
+ untagged_alert_behavior=settings.untagged_alert_behavior,
124
+ default_tag_id=settings.default_tag_id,
125
+ default_tag_name=default_tag_name,
126
+ ),
127
+ success=True,
128
+ message="Settings updated successfully",
129
+ )
130
+
131
+
132
+# ============================================
133
+# Tag List Endpoints
134
+# ============================================
135
+
136
+
137
+@tag_access_router.get(
138
+ "/tags",
139
+ response_model=AllTagsResponse,
140
+ description="List all available tags",
141
+)
142
+async def list_all_tags(
143
+ current_user: User = Security(AuthHandler().get_current_user),
144
+ db: AsyncSession = Depends(get_db),
145
+):
146
+ """List all available tags."""
147
+ tags = await tag_access_service.get_all_tags(db)
148
+
149
+ return AllTagsResponse(
150
+ tags=_tags_to_response(tags),
151
+ success=True,
152
+ message=f"Retrieved {len(tags)} tags",
153
+ )
154
+
155
+
156
+# ============================================
157
+# User Tag Access Endpoints
158
+# ============================================
159
+
160
+
161
+@tag_access_router.get(
162
+ "/user/{user_id}",
163
+ response_model=UserTagAccessResponse,
164
+ description="Get tags assigned to a specific user (admin only)",
165
+)
166
+async def get_user_tag_access(
167
+ user_id: int,
168
+ current_user: User = Security(AuthHandler().get_current_user),
169
+ db: AsyncSession = Depends(get_db),
170
+):
171
+ """Get tags assigned to a specific user."""
172
+ _require_admin(current_user)
173
+
174
+ user = await tag_access_service.get_user_by_id(user_id, db)
175
+ if not user:
176
+ raise HTTPException(status_code=404, detail="User not found")
177
+
178
+ tags = await tag_access_service.get_user_accessible_tags(user_id, db)
179
+
180
+ return UserTagAccessResponse(
181
+ user_id=user_id,
182
+ username=user.username,
183
+ accessible_tags=_tags_to_response(tags),
184
+ success=True,
185
+ message="User tag access retrieved successfully",
186
+ )
187
+
188
+
189
+@tag_access_router.put(
190
+ "/user/{user_id}",
191
+ response_model=UserTagAccessResponse,
192
+ description="Set tags for a user - replaces existing (admin only)",
193
+)
194
+async def set_user_tag_access(
195
+ user_id: int,
196
+ request: TagAccessCreate,
197
+ current_user: User = Security(AuthHandler().get_current_user),
198
+ db: AsyncSession = Depends(get_db),
199
+):
200
+ """Set tags for a user (replaces existing)."""
201
+ _require_admin(current_user)
202
+
203
+ user = await tag_access_service.get_user_by_id(user_id, db)
204
+ if not user:
205
+ raise HTTPException(status_code=404, detail="User not found")
206
+
207
+ # Validate all tag IDs exist
208
+ for tag_id in request.tag_ids:
209
+ tag = await tag_access_service.get_tag_by_id(tag_id, db)
210
+ if not tag:
211
+ raise HTTPException(
212
+ status_code=404,
213
+ detail=f"Tag with id {tag_id} not found",
214
+ )
215
+
216
+ tags = await tag_access_service.set_user_tag_access(user_id, request.tag_ids, db)
217
+
218
+ logger.info(
219
+ f"Admin {current_user.username} set tag access for user {user.username}: {request.tag_ids}",
220
+ )
221
+
222
+ return UserTagAccessResponse(
223
+ user_id=user_id,
224
+ username=user.username,
225
+ accessible_tags=_tags_to_response(tags),
226
+ success=True,
227
+ message="User tag access updated successfully",
228
+ )
229
+
230
+
231
+@tag_access_router.post(
232
+ "/user/{user_id}/add",
233
+ response_model=UserTagAccessResponse,
234
+ description="Add tags to a user's access (admin only)",
235
+)
236
+async def add_user_tag_access(
237
+ user_id: int,
238
+ request: TagAccessCreate,
239
+ current_user: User = Security(AuthHandler().get_current_user),
240
+ db: AsyncSession = Depends(get_db),
241
+):
242
+ """Add tags to a user's access without removing existing."""
243
+ _require_admin(current_user)
244
+
245
+ user = await tag_access_service.get_user_by_id(user_id, db)
246
+ if not user:
247
+ raise HTTPException(status_code=404, detail="User not found")
248
+
249
+ # Validate all tag IDs exist
250
+ for tag_id in request.tag_ids:
251
+ tag = await tag_access_service.get_tag_by_id(tag_id, db)
252
+ if not tag:
253
+ raise HTTPException(
254
+ status_code=404,
255
+ detail=f"Tag with id {tag_id} not found",
256
+ )
257
+
258
+ tags = await tag_access_service.add_user_tag_access(user_id, request.tag_ids, db)
259
+
260
+ logger.info(
261
+ f"Admin {current_user.username} added tag access for user {user.username}: {request.tag_ids}",
262
+ )
263
+
264
+ return UserTagAccessResponse(
265
+ user_id=user_id,
266
+ username=user.username,
267
+ accessible_tags=_tags_to_response(tags),
268
+ success=True,
269
+ message="Tags added to user access successfully",
270
+ )
271
+
272
+
273
+@tag_access_router.post(
274
+ "/user/{user_id}/remove",
275
+ response_model=UserTagAccessResponse,
276
+ description="Remove tags from a user's access (admin only)",
277
+)
278
+async def remove_user_tag_access(
279
+ user_id: int,
280
+ request: TagAccessCreate,
281
+ current_user: User = Security(AuthHandler().get_current_user),
282
+ db: AsyncSession = Depends(get_db),
283
+):
284
+ """Remove specific tags from a user's access."""
285
+ _require_admin(current_user)
286
+
287
+ user = await tag_access_service.get_user_by_id(user_id, db)
288
+ if not user:
289
+ raise HTTPException(status_code=404, detail="User not found")
290
+
291
+ tags = await tag_access_service.remove_user_tag_access(user_id, request.tag_ids, db)
292
+
293
+ logger.info(
294
+ f"Admin {current_user.username} removed tag access for user {user.username}: {request.tag_ids}",
295
+ )
296
+
297
+ return UserTagAccessResponse(
298
+ user_id=user_id,
299
+ username=user.username,
300
+ accessible_tags=_tags_to_response(tags),
301
+ success=True,
302
+ message="Tags removed from user access successfully",
303
+ )
304
+
305
+
306
+# ============================================
307
+# Role Tag Access Endpoints
308
+# ============================================
309
+
310
+
311
+@tag_access_router.get(
312
+ "/role/{role_id}",
313
+ response_model=RoleTagAccessResponse,
314
+ description="Get tags assigned to a specific role (admin only)",
315
+)
316
+async def get_role_tag_access(
317
+ role_id: int,
318
+ current_user: User = Security(AuthHandler().get_current_user),
319
+ db: AsyncSession = Depends(get_db),
320
+):
321
+ """Get tags assigned to a specific role."""
322
+ _require_admin(current_user)
323
+
324
+ role = await tag_access_service.get_role_by_id(role_id, db)
325
+ if not role:
326
+ raise HTTPException(status_code=404, detail="Role not found")
327
+
328
+ tags = await tag_access_service.get_role_accessible_tags(role_id, db)
329
+
330
+ return RoleTagAccessResponse(
331
+ role_id=role_id,
332
+ role_name=role.name,
333
+ accessible_tags=_tags_to_response(tags),
334
+ success=True,
335
+ message="Role tag access retrieved successfully",
336
+ )
337
+
338
+
339
+@tag_access_router.put(
340
+ "/role/{role_id}",
341
+ response_model=RoleTagAccessResponse,
342
+ description="Set tags for a role - replaces existing (admin only)",
343
+)
344
+async def set_role_tag_access(
345
+ role_id: int,
346
+ request: TagAccessCreate,
347
+ current_user: User = Security(AuthHandler().get_current_user),
348
+ db: AsyncSession = Depends(get_db),
349
+):
350
+ """Set tags for a role (replaces existing)."""
351
+ _require_admin(current_user)
352
+
353
+ role = await tag_access_service.get_role_by_id(role_id, db)
354
+ if not role:
355
+ raise HTTPException(status_code=404, detail="Role not found")
356
+
357
+ # Validate all tag IDs exist
358
+ for tag_id in request.tag_ids:
359
+ tag = await tag_access_service.get_tag_by_id(tag_id, db)
360
+ if not tag:
361
+ raise HTTPException(
362
+ status_code=404,
363
+ detail=f"Tag with id {tag_id} not found",
364
+ )
365
+
366
+ tags = await tag_access_service.set_role_tag_access(role_id, request.tag_ids, db)
367
+
368
+ logger.info(
369
+ f"Admin {current_user.username} set tag access for role {role.name}: {request.tag_ids}",
370
+ )
371
+
372
+ return RoleTagAccessResponse(
373
+ role_id=role_id,
374
+ role_name=role.name,
375
+ accessible_tags=_tags_to_response(tags),
376
+ success=True,
377
+ message="Role tag access updated successfully",
378
+ )
379
+
380
+
381
+@tag_access_router.post(
382
+ "/role/{role_id}/add",
383
+ response_model=RoleTagAccessResponse,
384
+ description="Add tags to a role's access (admin only)",
385
+)
386
+async def add_role_tag_access(
387
+ role_id: int,
388
+ request: TagAccessCreate,
389
+ current_user: User = Security(AuthHandler().get_current_user),
390
+ db: AsyncSession = Depends(get_db),
391
+):
392
+ """Add tags to a role's access without removing existing."""
393
+ _require_admin(current_user)
394
+
395
+ role = await tag_access_service.get_role_by_id(role_id, db)
396
+ if not role:
397
+ raise HTTPException(status_code=404, detail="Role not found")
398
+
399
+ # Validate all tag IDs exist
400
+ for tag_id in request.tag_ids:
401
+ tag = await tag_access_service.get_tag_by_id(tag_id, db)
402
+ if not tag:
403
+ raise HTTPException(
404
+ status_code=404,
405
+ detail=f"Tag with id {tag_id} not found",
406
+ )
407
+
408
+ tags = await tag_access_service.add_role_tag_access(role_id, request.tag_ids, db)
409
+
410
+ logger.info(
411
+ f"Admin {current_user.username} added tag access for role {role.name}: {request.tag_ids}",
412
+ )
413
+
414
+ return RoleTagAccessResponse(
415
+ role_id=role_id,
416
+ role_name=role.name,
417
+ accessible_tags=_tags_to_response(tags),
418
+ success=True,
419
+ message="Tags added to role access successfully",
420
+ )
421
+
422
+
423
+@tag_access_router.post(
424
+ "/role/{role_id}/remove",
425
+ response_model=RoleTagAccessResponse,
426
+ description="Remove tags from a role's access (admin only)",
427
+)
428
+async def remove_role_tag_access(
429
+ role_id: int,
430
+ request: TagAccessCreate,
431
+ current_user: User = Security(AuthHandler().get_current_user),
432
+ db: AsyncSession = Depends(get_db),
433
+):
434
+ """Remove specific tags from a role's access."""
435
+ _require_admin(current_user)
436
+
437
+ role = await tag_access_service.get_role_by_id(role_id, db)
438
+ if not role:
439
+ raise HTTPException(status_code=404, detail="Role not found")
440
+
441
+ tags = await tag_access_service.remove_role_tag_access(role_id, request.tag_ids, db)
442
+
443
+ logger.info(
444
+ f"Admin {current_user.username} removed tag access for role {role.name}: {request.tag_ids}",
445
+ )
446
+
447
+ return RoleTagAccessResponse(
448
+ role_id=role_id,
449
+ role_name=role.name,
450
+ accessible_tags=_tags_to_response(tags),
451
+ success=True,
452
+ message="Tags removed from role access successfully",
453
+ )
454
+
455
+
456
+# ============================================
457
+# Current User Effective Access
458
+# ============================================
459
+
460
+
461
+@tag_access_router.get(
462
+ "/me",
463
+ response_model=UserEffectiveAccessResponse,
464
+ description="Get current user's effective access (customer + tags)",
465
+)
466
+async def get_my_effective_access(
467
+ current_user: User = Security(AuthHandler().get_current_user),
468
+ db: AsyncSession = Depends(get_db),
469
+):
470
+ """Get the current user's effective access (combines role + user-specific)."""
471
+ # Get tag RBAC status
472
+ tag_rbac_enabled = await tag_access_handler.is_tag_rbac_enabled(db)
473
+
474
+ # Get accessible tags
475
+ accessible_tag_ids = await tag_access_handler.get_user_accessible_tags(current_user, db)
476
+ is_unrestricted = "*" in accessible_tag_ids
477
+
478
+ if is_unrestricted:
479
+ # User has unrestricted access - show all tags
480
+ all_tags = await tag_access_service.get_all_tags(db)
481
+ tag_list = _tags_to_response(all_tags)
482
+ else:
483
+ # Get specific tags user can access
484
+ from sqlalchemy import select
485
+
486
+ from app.incidents.models import AlertTag
487
+
488
+ if accessible_tag_ids:
489
+ result = await db.execute(
490
+ select(AlertTag).where(AlertTag.id.in_(accessible_tag_ids)),
491
+ )
492
+ tags = result.scalars().all()
493
+ tag_list = _tags_to_response(tags)
494
+ else:
495
+ tag_list = []
496
+
497
+ # Get customer access (using existing middleware if available)
498
+ try:
499
+ from app.middleware.customer_access import customer_access_handler
500
+
501
+ accessible_customers = await customer_access_handler.get_user_accessible_customers(
502
+ current_user,
503
+ db,
504
+ )
505
+ customer_list = list(accessible_customers) if "*" not in accessible_customers else ["*"]
506
+ except ImportError:
507
+ customer_list = ["*"] # Fallback if customer access handler doesn't exist
508
+
509
+ # Get role name
510
+ role_name = None
511
+ if current_user.role_id:
512
+ role = await tag_access_service.get_role_by_id(current_user.role_id, db)
513
+ if role:
514
+ role_name = role.name
515
+
516
+ return UserEffectiveAccessResponse(
517
+ user_id=current_user.id,
518
+ username=current_user.username,
519
+ role_id=current_user.role_id,
520
+ role_name=role_name,
521
+ accessible_customers=customer_list,
522
+ accessible_tags=tag_list,
523
+ is_tag_unrestricted=is_unrestricted,
524
+ tag_rbac_enabled=tag_rbac_enabled,
525
+ success=True,
526
+ message="Effective access retrieved successfully",
527
+ )