rbac module testing done
This commit is contained in:
parent
54246745a9
commit
6e6cf7012b
15 changed files with 209 additions and 561 deletions
135
docs/rbac_getrecordset_review.md
Normal file
135
docs/rbac_getrecordset_review.md
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
# RBAC getRecordset() Review
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Review of all `getRecordset()` calls in `interfaceDbChatObjects.py` and `interfaceDbComponentObjects.py` to determine which should be converted to `getRecordsetWithRBAC()`.
|
||||||
|
|
||||||
|
## Analysis Criteria
|
||||||
|
- **Convert to RBAC**: User-facing data that should respect access control
|
||||||
|
- **Keep as-is**: Internal/technical operations that don't need RBAC filtering
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## interfaceDbChatObjects.py
|
||||||
|
|
||||||
|
### Summary: **14 calls found - ALL should be converted to `getRecordsetWithRBAC()`**
|
||||||
|
|
||||||
|
All calls access user-facing data (ChatMessage, ChatDocument, ChatStat, ChatLog) and should respect RBAC even when:
|
||||||
|
- Used in cascade delete operations (after parent access is verified)
|
||||||
|
- Used to fetch child records (after parent access is verified)
|
||||||
|
- Used for existence checks
|
||||||
|
|
||||||
|
**Rationale**: RBAC should be applied at every data access point to ensure consistent security and prevent potential bypass scenarios.
|
||||||
|
|
||||||
|
### Detailed List:
|
||||||
|
|
||||||
|
1. **Line 760** - `deleteWorkflow()` - Cascade delete ChatStat
|
||||||
|
- **Action**: Convert to `getRecordsetWithRBAC(ChatStat, self.currentUser, recordFilter={"messageId": messageId})`
|
||||||
|
- **Reason**: Deleting related data should respect RBAC
|
||||||
|
|
||||||
|
2. **Line 765** - `deleteWorkflow()` - Cascade delete ChatDocument
|
||||||
|
- **Action**: Convert to `getRecordsetWithRBAC(ChatDocument, self.currentUser, recordFilter={"messageId": messageId})`
|
||||||
|
- **Reason**: Deleting related data should respect RBAC
|
||||||
|
|
||||||
|
3. **Line 773** - `deleteWorkflow()` - Cascade delete ChatStat (workflow level)
|
||||||
|
- **Action**: Convert to `getRecordsetWithRBAC(ChatStat, self.currentUser, recordFilter={"workflowId": workflowId})`
|
||||||
|
- **Reason**: Deleting related data should respect RBAC
|
||||||
|
|
||||||
|
4. **Line 778** - `deleteWorkflow()` - Cascade delete ChatLog
|
||||||
|
- **Action**: Convert to `getRecordsetWithRBAC(ChatLog, self.currentUser, recordFilter={"workflowId": workflowId})`
|
||||||
|
- **Reason**: Deleting related data should respect RBAC
|
||||||
|
|
||||||
|
5. **Line 821** - `getMessages()` - Fetch messages for workflow
|
||||||
|
- **Action**: Convert to `getRecordsetWithRBAC(ChatMessage, self.currentUser, recordFilter={"workflowId": workflowId})`
|
||||||
|
- **Reason**: Child records should still respect RBAC even if parent access is verified
|
||||||
|
|
||||||
|
6. **Line 1062** - `updateMessage()` - Check if message exists
|
||||||
|
- **Action**: Convert to `getRecordsetWithRBAC(ChatMessage, self.currentUser, recordFilter={"id": messageId})`
|
||||||
|
- **Reason**: Existence checks should respect RBAC
|
||||||
|
|
||||||
|
7. **Line 1167** - `deleteMessage()` - Cascade delete ChatStat
|
||||||
|
- **Action**: Convert to `getRecordsetWithRBAC(ChatStat, self.currentUser, recordFilter={"messageId": messageId})`
|
||||||
|
- **Reason**: Deleting related data should respect RBAC
|
||||||
|
|
||||||
|
8. **Line 1172** - `deleteMessage()` - Cascade delete ChatDocument
|
||||||
|
- **Action**: Convert to `getRecordsetWithRBAC(ChatDocument, self.currentUser, recordFilter={"messageId": messageId})`
|
||||||
|
- **Reason**: Deleting related data should respect RBAC
|
||||||
|
|
||||||
|
9. **Line 1199** - `deleteFileFromMessage()` - Get documents for message
|
||||||
|
- **Action**: Convert to `getRecordsetWithRBAC(ChatDocument, self.currentUser, recordFilter={"messageId": messageId})`
|
||||||
|
- **Reason**: Accessing related data should respect RBAC
|
||||||
|
|
||||||
|
10. **Line 1242** - `getDocuments()` - Get documents for message
|
||||||
|
- **Action**: Convert to `getRecordsetWithRBAC(ChatDocument, self.currentUser, recordFilter={"messageId": messageId})`
|
||||||
|
- **Reason**: Public method accessing user data should respect RBAC
|
||||||
|
|
||||||
|
11. **Line 1291** - `getLogs()` - Fetch logs for workflow
|
||||||
|
- **Action**: Convert to `getRecordsetWithRBAC(ChatLog, self.currentUser, recordFilter={"workflowId": workflowId})`
|
||||||
|
- **Reason**: Child records should still respect RBAC even if parent access is verified
|
||||||
|
|
||||||
|
12. **Line 1410** - `getStats()` - Fetch stats for workflow
|
||||||
|
- **Action**: Convert to `getRecordsetWithRBAC(ChatStat, self.currentUser, recordFilter={"workflowId": workflowId})`
|
||||||
|
- **Reason**: Child records should still respect RBAC even if parent access is verified
|
||||||
|
|
||||||
|
13. **Line 1460** - `getUnifiedChatData()` - Fetch messages for workflow
|
||||||
|
- **Action**: Convert to `getRecordsetWithRBAC(ChatMessage, self.currentUser, recordFilter={"workflowId": workflowId})`
|
||||||
|
- **Reason**: Child records should still respect RBAC even if parent access is verified
|
||||||
|
|
||||||
|
14. **Line 1501** - `getUnifiedChatData()` - Fetch logs for workflow
|
||||||
|
- **Action**: Convert to `getRecordsetWithRBAC(ChatLog, self.currentUser, recordFilter={"workflowId": workflowId})`
|
||||||
|
- **Reason**: Child records should still respect RBAC even if parent access is verified
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## interfaceDbComponentObjects.py
|
||||||
|
|
||||||
|
### Summary: **3 calls found - 1 keep as-is, 2 should be converted**
|
||||||
|
|
||||||
|
### Detailed List:
|
||||||
|
|
||||||
|
1. **Line 149** - `_initializeStandardPrompts()` - Check if prompts exist
|
||||||
|
- **Action**: **KEEP AS-IS** ✅
|
||||||
|
- **Reason**: This is initialization code that runs during bootstrap. It checks if any prompts exist to avoid re-initialization. Since this runs with root user context and is a system-level check, RBAC is not needed here.
|
||||||
|
|
||||||
|
2. **Line 947** - `deleteFile()` - Get FileData for deletion
|
||||||
|
- **Action**: **CONVERT** to `getRecordsetWithRBAC(FileData, self.currentUser, recordFilter={"id": fileId})`
|
||||||
|
- **Reason**: FileData stores binary data associated with FileItem. While it's a technical table, we should still respect RBAC for consistency and security. The file access was already checked via `getFile()`, but FileData access should also be RBAC-filtered.
|
||||||
|
|
||||||
|
3. **Line 1032** - `getFileData()` - Get FileData for reading
|
||||||
|
- **Action**: **CONVERT** to `getRecordsetWithRBAC(FileData, self.currentUser, recordFilter={"id": fileId})`
|
||||||
|
- **Reason**: FileData access should respect RBAC. The file access was already checked via `getFile()`, but FileData access should also be RBAC-filtered for consistency.
|
||||||
|
|
||||||
|
**Note on FileData**: FileData is a technical table storing binary file content. However, for consistency and security, RBAC should still be applied. If FileData doesn't have RBAC rules defined, the RBAC filter will effectively be a no-op (allowing access), but the pattern is consistent.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Priority
|
||||||
|
|
||||||
|
### High Priority (User-facing data access)
|
||||||
|
- All `interfaceDbChatObjects.py` calls (14 calls)
|
||||||
|
- `interfaceDbComponentObjects.py` FileData calls (2 calls)
|
||||||
|
|
||||||
|
### Low Priority (System initialization)
|
||||||
|
- `interfaceDbComponentObjects.py` Prompt initialization check (1 call) - Keep as-is
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. Convert all 14 calls in `interfaceDbChatObjects.py` to `getRecordsetWithRBAC()`
|
||||||
|
2. Convert 2 FileData calls in `interfaceDbComponentObjects.py` to `getRecordsetWithRBAC()`
|
||||||
|
3. Keep 1 Prompt initialization check as-is
|
||||||
|
4. Test all changes to ensure RBAC filtering works correctly
|
||||||
|
5. Verify cascade delete operations still work correctly with RBAC
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
After conversion, verify:
|
||||||
|
- [ ] Workflow deletion still works (cascade deletes)
|
||||||
|
- [ ] Message deletion still works (cascade deletes)
|
||||||
|
- [ ] File deletion still works (FileData cleanup)
|
||||||
|
- [ ] File reading still works (FileData access)
|
||||||
|
- [ ] Child record access (messages, logs, stats, documents) respects RBAC
|
||||||
|
- [ ] Users can only access data they have permission for
|
||||||
|
- [ ] No performance degradation from RBAC filtering
|
||||||
|
|
@ -13,11 +13,6 @@ class AuthAuthority(str, Enum):
|
||||||
GOOGLE = "google"
|
GOOGLE = "google"
|
||||||
MSFT = "msft"
|
MSFT = "msft"
|
||||||
|
|
||||||
class UserPrivilege(str, Enum): # TODO: TO remove, one new RBAC System is in place!
|
|
||||||
SYSADMIN = "sysadmin"
|
|
||||||
ADMIN = "admin"
|
|
||||||
USER = "user"
|
|
||||||
|
|
||||||
class ConnectionStatus(str, Enum):
|
class ConnectionStatus(str, Enum):
|
||||||
ACTIVE = "active"
|
ACTIVE = "active"
|
||||||
EXPIRED = "expired"
|
EXPIRED = "expired"
|
||||||
|
|
@ -152,7 +147,6 @@ class User(BaseModel):
|
||||||
{"value": "it", "label": {"en": "Italiano", "fr": "Italien"}},
|
{"value": "it", "label": {"en": "Italiano", "fr": "Italien"}},
|
||||||
]})
|
]})
|
||||||
enabled: bool = Field(default=True, description="Indicates whether the user is enabled", json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False})
|
enabled: bool = Field(default=True, description="Indicates whether the user is enabled", json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False})
|
||||||
privilege: UserPrivilege = Field(default=UserPrivilege.USER, description="Permission level (DEPRECATED: use roleLabels instead)", json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": "user.role"})
|
|
||||||
roleLabels: List[str] = Field(
|
roleLabels: List[str] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="List of role labels assigned to this user. All roles are opening roles (union) - if one role enables something, it is enabled.",
|
description="List of role labels assigned to this user. All roles are opening roles (union) - if one role enables something, it is enabled.",
|
||||||
|
|
@ -174,7 +168,6 @@ registerModelLabels(
|
||||||
"fullName": {"en": "Full Name", "fr": "Nom complet"},
|
"fullName": {"en": "Full Name", "fr": "Nom complet"},
|
||||||
"language": {"en": "Language", "fr": "Langue"},
|
"language": {"en": "Language", "fr": "Langue"},
|
||||||
"enabled": {"en": "Enabled", "fr": "Activé"},
|
"enabled": {"en": "Enabled", "fr": "Activé"},
|
||||||
"privilege": {"en": "Privilege", "fr": "Privilège"},
|
|
||||||
"roleLabels": {"en": "Role Labels", "fr": "Labels de rôle"},
|
"roleLabels": {"en": "Role Labels", "fr": "Labels de rôle"},
|
||||||
"authenticationAuthority": {"en": "Auth Authority", "fr": "Autorité d'authentification"},
|
"authenticationAuthority": {"en": "Auth Authority", "fr": "Autorité d'authentification"},
|
||||||
"mandateId": {"en": "Mandate ID", "fr": "ID de mandat"},
|
"mandateId": {"en": "Mandate ID", "fr": "ID de mandat"},
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ from modules.shared.configuration import APP_CONFIG
|
||||||
from modules.datamodels.datamodelUam import (
|
from modules.datamodels.datamodelUam import (
|
||||||
Mandate,
|
Mandate,
|
||||||
UserInDB,
|
UserInDB,
|
||||||
UserPrivilege,
|
|
||||||
AuthAuthority,
|
AuthAuthority,
|
||||||
)
|
)
|
||||||
from modules.datamodels.datamodelRbac import (
|
from modules.datamodels.datamodelRbac import (
|
||||||
|
|
@ -103,7 +102,6 @@ def initAdminUser(db: DatabaseConnector, mandateId: Optional[str]) -> Optional[s
|
||||||
fullName="Administrator",
|
fullName="Administrator",
|
||||||
enabled=True,
|
enabled=True,
|
||||||
language="en",
|
language="en",
|
||||||
privilege=UserPrivilege.SYSADMIN,
|
|
||||||
roleLabels=["sysadmin"],
|
roleLabels=["sysadmin"],
|
||||||
authenticationAuthority=AuthAuthority.LOCAL,
|
authenticationAuthority=AuthAuthority.LOCAL,
|
||||||
hashedPassword=_getPasswordHash(APP_CONFIG.get("APP_INIT_PASS_ADMIN_SECRET")),
|
hashedPassword=_getPasswordHash(APP_CONFIG.get("APP_INIT_PASS_ADMIN_SECRET")),
|
||||||
|
|
@ -140,7 +138,6 @@ def initEventUser(db: DatabaseConnector, mandateId: Optional[str]) -> Optional[s
|
||||||
fullName="Event",
|
fullName="Event",
|
||||||
enabled=True,
|
enabled=True,
|
||||||
language="en",
|
language="en",
|
||||||
privilege=UserPrivilege.SYSADMIN,
|
|
||||||
roleLabels=["sysadmin"],
|
roleLabels=["sysadmin"],
|
||||||
authenticationAuthority=AuthAuthority.LOCAL,
|
authenticationAuthority=AuthAuthority.LOCAL,
|
||||||
hashedPassword=_getPasswordHash(APP_CONFIG.get("APP_INIT_PASS_EVENT_SECRET")),
|
hashedPassword=_getPasswordHash(APP_CONFIG.get("APP_INIT_PASS_EVENT_SECRET")),
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ from modules.datamodels.datamodelUam import (
|
||||||
UserInDB,
|
UserInDB,
|
||||||
UserConnection,
|
UserConnection,
|
||||||
AuthAuthority,
|
AuthAuthority,
|
||||||
UserPrivilege,
|
|
||||||
ConnectionStatus,
|
ConnectionStatus,
|
||||||
)
|
)
|
||||||
from modules.datamodels.datamodelRbac import (
|
from modules.datamodels.datamodelRbac import (
|
||||||
|
|
@ -488,20 +487,21 @@ class AppObjects:
|
||||||
def getUser(self, userId: str) -> Optional[User]:
|
def getUser(self, userId: str) -> Optional[User]:
|
||||||
"""Returns a user by ID if user has access."""
|
"""Returns a user by ID if user has access."""
|
||||||
try:
|
try:
|
||||||
# Get all users
|
# Get users filtered by RBAC
|
||||||
users = self.db.getRecordset(UserInDB)
|
users = self.db.getRecordsetWithRBAC(
|
||||||
|
UserInDB,
|
||||||
|
self.currentUser,
|
||||||
|
recordFilter={"id": userId}
|
||||||
|
)
|
||||||
|
|
||||||
if not users:
|
if not users:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Find user by ID
|
|
||||||
for user_dict in users:
|
|
||||||
if user_dict.get("id") == userId:
|
|
||||||
# User already filtered by RBAC, just clean fields
|
# User already filtered by RBAC, just clean fields
|
||||||
|
user_dict = users[0]
|
||||||
cleanedUser = {k: v for k, v in user_dict.items() if not k.startswith("_")}
|
cleanedUser = {k: v for k, v in user_dict.items() if not k.startswith("_")}
|
||||||
return User(**cleanedUser)
|
return User(**cleanedUser)
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting user by ID: {str(e)}")
|
logger.error(f"Error getting user by ID: {str(e)}")
|
||||||
return None
|
return None
|
||||||
|
|
@ -542,7 +542,7 @@ class AppObjects:
|
||||||
fullName: str = None,
|
fullName: str = None,
|
||||||
language: str = "en",
|
language: str = "en",
|
||||||
enabled: bool = True,
|
enabled: bool = True,
|
||||||
privilege: UserPrivilege = UserPrivilege.USER,
|
roleLabels: List[str] = None,
|
||||||
authenticationAuthority: AuthAuthority = AuthAuthority.LOCAL,
|
authenticationAuthority: AuthAuthority = AuthAuthority.LOCAL,
|
||||||
externalId: str = None,
|
externalId: str = None,
|
||||||
externalUsername: str = None,
|
externalUsername: str = None,
|
||||||
|
|
@ -568,6 +568,10 @@ class AppObjects:
|
||||||
mandateId = self._getDefaultMandateId()
|
mandateId = self._getDefaultMandateId()
|
||||||
logger.warning(f"Using default mandate ID {mandateId} for new user {username}")
|
logger.warning(f"Using default mandate ID {mandateId} for new user {username}")
|
||||||
|
|
||||||
|
# Default roleLabels to ["user"] if not provided
|
||||||
|
if roleLabels is None or not roleLabels:
|
||||||
|
roleLabels = ["user"]
|
||||||
|
|
||||||
# Create user data using UserInDB model
|
# Create user data using UserInDB model
|
||||||
userData = UserInDB(
|
userData = UserInDB(
|
||||||
username=username,
|
username=username,
|
||||||
|
|
@ -576,7 +580,7 @@ class AppObjects:
|
||||||
language=language,
|
language=language,
|
||||||
mandateId=mandateId,
|
mandateId=mandateId,
|
||||||
enabled=enabled,
|
enabled=enabled,
|
||||||
privilege=privilege,
|
roleLabels=roleLabels,
|
||||||
authenticationAuthority=authenticationAuthority,
|
authenticationAuthority=authenticationAuthority,
|
||||||
hashedPassword=self._getPasswordHash(password) if password else None,
|
hashedPassword=self._getPasswordHash(password) if password else None,
|
||||||
connections=[],
|
connections=[],
|
||||||
|
|
@ -734,7 +738,11 @@ class AppObjects:
|
||||||
if not initialUserId:
|
if not initialUserId:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
users = self.db.getRecordset(UserInDB, recordFilter={"id": initialUserId})
|
users = self.db.getRecordsetWithRBAC(
|
||||||
|
UserInDB,
|
||||||
|
self.currentUser,
|
||||||
|
recordFilter={"id": initialUserId}
|
||||||
|
)
|
||||||
return users[0] if users else None
|
return users[0] if users else None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting initial user: {str(e)}")
|
logger.error(f"Error getting initial user: {str(e)}")
|
||||||
|
|
|
||||||
|
|
@ -757,12 +757,12 @@ class ChatObjects:
|
||||||
messageId = message.id
|
messageId = message.id
|
||||||
if messageId:
|
if messageId:
|
||||||
# Delete message stats
|
# Delete message stats
|
||||||
existing_stats = self.db.getRecordset(ChatStat, recordFilter={"messageId": messageId})
|
existing_stats = self.db.getRecordsetWithRBAC(ChatStat, self.currentUser, recordFilter={"messageId": messageId})
|
||||||
for stat in existing_stats:
|
for stat in existing_stats:
|
||||||
self.db.recordDelete(ChatStat, stat["id"])
|
self.db.recordDelete(ChatStat, stat["id"])
|
||||||
|
|
||||||
# Delete message documents (but NOT the files!)
|
# Delete message documents (but NOT the files!)
|
||||||
existing_docs = self.db.getRecordset(ChatDocument, recordFilter={"messageId": messageId})
|
existing_docs = self.db.getRecordsetWithRBAC(ChatDocument, self.currentUser, recordFilter={"messageId": messageId})
|
||||||
for doc in existing_docs:
|
for doc in existing_docs:
|
||||||
self.db.recordDelete(ChatDocument, doc["id"])
|
self.db.recordDelete(ChatDocument, doc["id"])
|
||||||
|
|
||||||
|
|
@ -770,12 +770,12 @@ class ChatObjects:
|
||||||
self.db.recordDelete(ChatMessage, messageId)
|
self.db.recordDelete(ChatMessage, messageId)
|
||||||
|
|
||||||
# 2. Delete workflow stats
|
# 2. Delete workflow stats
|
||||||
existing_stats = self.db.getRecordset(ChatStat, recordFilter={"workflowId": workflowId})
|
existing_stats = self.db.getRecordsetWithRBAC(ChatStat, self.currentUser, recordFilter={"workflowId": workflowId})
|
||||||
for stat in existing_stats:
|
for stat in existing_stats:
|
||||||
self.db.recordDelete(ChatStat, stat["id"])
|
self.db.recordDelete(ChatStat, stat["id"])
|
||||||
|
|
||||||
# 3. Delete workflow logs
|
# 3. Delete workflow logs
|
||||||
existing_logs = self.db.getRecordset(ChatLog, recordFilter={"workflowId": workflowId})
|
existing_logs = self.db.getRecordsetWithRBAC(ChatLog, self.currentUser, recordFilter={"workflowId": workflowId})
|
||||||
for log in existing_logs:
|
for log in existing_logs:
|
||||||
self.db.recordDelete(ChatLog, log["id"])
|
self.db.recordDelete(ChatLog, log["id"])
|
||||||
|
|
||||||
|
|
@ -818,7 +818,7 @@ class ChatObjects:
|
||||||
return PaginatedResult(items=[], totalItems=0, totalPages=0)
|
return PaginatedResult(items=[], totalItems=0, totalPages=0)
|
||||||
|
|
||||||
# Get messages for this workflow from normalized table
|
# Get messages for this workflow from normalized table
|
||||||
messages = self.db.getRecordset(ChatMessage, recordFilter={"workflowId": workflowId})
|
messages = self.db.getRecordsetWithRBAC(ChatMessage, self.currentUser, recordFilter={"workflowId": workflowId})
|
||||||
|
|
||||||
# Convert raw messages to dict format for sorting/filtering
|
# Convert raw messages to dict format for sorting/filtering
|
||||||
messageDicts = []
|
messageDicts = []
|
||||||
|
|
@ -1059,7 +1059,7 @@ class ChatObjects:
|
||||||
raise ValueError("messageId cannot be empty")
|
raise ValueError("messageId cannot be empty")
|
||||||
|
|
||||||
# Check if message exists in database
|
# Check if message exists in database
|
||||||
messages = self.db.getRecordset(ChatMessage, recordFilter={"id": messageId})
|
messages = self.db.getRecordsetWithRBAC(ChatMessage, self.currentUser, recordFilter={"id": messageId})
|
||||||
if not messages:
|
if not messages:
|
||||||
logger.warning(f"Message with ID {messageId} does not exist in database")
|
logger.warning(f"Message with ID {messageId} does not exist in database")
|
||||||
|
|
||||||
|
|
@ -1164,12 +1164,12 @@ class ChatObjects:
|
||||||
# CASCADE DELETE: Delete all related data first
|
# CASCADE DELETE: Delete all related data first
|
||||||
|
|
||||||
# 1. Delete message stats
|
# 1. Delete message stats
|
||||||
existing_stats = self.db.getRecordset(ChatStat, recordFilter={"messageId": messageId})
|
existing_stats = self.db.getRecordsetWithRBAC(ChatStat, self.currentUser, recordFilter={"messageId": messageId})
|
||||||
for stat in existing_stats:
|
for stat in existing_stats:
|
||||||
self.db.recordDelete(ChatStat, stat["id"])
|
self.db.recordDelete(ChatStat, stat["id"])
|
||||||
|
|
||||||
# 2. Delete message documents (but NOT the files!)
|
# 2. Delete message documents (but NOT the files!)
|
||||||
existing_docs = self.db.getRecordset(ChatDocument, recordFilter={"messageId": messageId})
|
existing_docs = self.db.getRecordsetWithRBAC(ChatDocument, self.currentUser, recordFilter={"messageId": messageId})
|
||||||
for doc in existing_docs:
|
for doc in existing_docs:
|
||||||
self.db.recordDelete(ChatDocument, doc["id"])
|
self.db.recordDelete(ChatDocument, doc["id"])
|
||||||
|
|
||||||
|
|
@ -1196,7 +1196,7 @@ class ChatObjects:
|
||||||
|
|
||||||
|
|
||||||
# Get documents for this message from normalized table
|
# Get documents for this message from normalized table
|
||||||
documents = self.db.getRecordset(ChatDocument, recordFilter={"messageId": messageId})
|
documents = self.db.getRecordsetWithRBAC(ChatDocument, self.currentUser, recordFilter={"messageId": messageId})
|
||||||
|
|
||||||
if not documents:
|
if not documents:
|
||||||
logger.warning(f"No documents found for message {messageId}")
|
logger.warning(f"No documents found for message {messageId}")
|
||||||
|
|
@ -1239,7 +1239,7 @@ class ChatObjects:
|
||||||
def getDocuments(self, messageId: str) -> List[ChatDocument]:
|
def getDocuments(self, messageId: str) -> List[ChatDocument]:
|
||||||
"""Returns documents for a message from normalized table."""
|
"""Returns documents for a message from normalized table."""
|
||||||
try:
|
try:
|
||||||
documents = self.db.getRecordset(ChatDocument, recordFilter={"messageId": messageId})
|
documents = self.db.getRecordsetWithRBAC(ChatDocument, self.currentUser, recordFilter={"messageId": messageId})
|
||||||
return [ChatDocument(**doc) for doc in documents]
|
return [ChatDocument(**doc) for doc in documents]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting message documents: {str(e)}")
|
logger.error(f"Error getting message documents: {str(e)}")
|
||||||
|
|
@ -1288,7 +1288,7 @@ class ChatObjects:
|
||||||
return PaginatedResult(items=[], totalItems=0, totalPages=0)
|
return PaginatedResult(items=[], totalItems=0, totalPages=0)
|
||||||
|
|
||||||
# Get logs for this workflow from normalized table
|
# Get logs for this workflow from normalized table
|
||||||
logs = self.db.getRecordset(ChatLog, recordFilter={"workflowId": workflowId})
|
logs = self.db.getRecordsetWithRBAC(ChatLog, self.currentUser, recordFilter={"workflowId": workflowId})
|
||||||
|
|
||||||
# Convert raw logs to dict format for sorting/filtering
|
# Convert raw logs to dict format for sorting/filtering
|
||||||
logDicts = []
|
logDicts = []
|
||||||
|
|
@ -1407,7 +1407,7 @@ class ChatObjects:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Get stats for this workflow from normalized table
|
# Get stats for this workflow from normalized table
|
||||||
stats = self.db.getRecordset(ChatStat, recordFilter={"workflowId": workflowId})
|
stats = self.db.getRecordsetWithRBAC(ChatStat, self.currentUser, recordFilter={"workflowId": workflowId})
|
||||||
|
|
||||||
if not stats:
|
if not stats:
|
||||||
return []
|
return []
|
||||||
|
|
@ -1457,7 +1457,7 @@ class ChatObjects:
|
||||||
items = []
|
items = []
|
||||||
|
|
||||||
# Get messages
|
# Get messages
|
||||||
messages = self.db.getRecordset(ChatMessage, recordFilter={"workflowId": workflowId})
|
messages = self.db.getRecordsetWithRBAC(ChatMessage, self.currentUser, recordFilter={"workflowId": workflowId})
|
||||||
for msg in messages:
|
for msg in messages:
|
||||||
# Apply timestamp filtering in Python
|
# Apply timestamp filtering in Python
|
||||||
msgTimestamp = parseTimestamp(msg.get("publishedAt"), default=getUtcTimestamp())
|
msgTimestamp = parseTimestamp(msg.get("publishedAt"), default=getUtcTimestamp())
|
||||||
|
|
@ -1498,7 +1498,7 @@ class ChatObjects:
|
||||||
})
|
})
|
||||||
|
|
||||||
# Get logs
|
# Get logs
|
||||||
logs = self.db.getRecordset(ChatLog, recordFilter={"workflowId": workflowId})
|
logs = self.db.getRecordsetWithRBAC(ChatLog, self.currentUser, recordFilter={"workflowId": workflowId})
|
||||||
for log in logs:
|
for log in logs:
|
||||||
# Apply timestamp filtering in Python
|
# Apply timestamp filtering in Python
|
||||||
logTimestamp = parseTimestamp(log.get("timestamp"), default=getUtcTimestamp())
|
logTimestamp = parseTimestamp(log.get("timestamp"), default=getUtcTimestamp())
|
||||||
|
|
|
||||||
|
|
@ -838,10 +838,11 @@ class ComponentObjects:
|
||||||
|
|
||||||
def _isfileNameUnique(self, fileName: str, excludeFileId: Optional[str] = None) -> bool:
|
def _isfileNameUnique(self, fileName: str, excludeFileId: Optional[str] = None) -> bool:
|
||||||
"""Checks if a fileName is unique for the current user."""
|
"""Checks if a fileName is unique for the current user."""
|
||||||
# Get all files for current user
|
# Get all files filtered by RBAC (will be filtered by user's access level)
|
||||||
files = self.db.getRecordset(FileItem, recordFilter={
|
files = self.db.getRecordsetWithRBAC(
|
||||||
"_createdBy": self.currentUser.id
|
FileItem,
|
||||||
})
|
self.currentUser
|
||||||
|
)
|
||||||
|
|
||||||
# Check if fileName exists (excluding the current file if updating)
|
# Check if fileName exists (excluding the current file if updating)
|
||||||
for file in files:
|
for file in files:
|
||||||
|
|
@ -930,16 +931,20 @@ class ComponentObjects:
|
||||||
if not self.checkRbacPermission(FileItem, "update", fileId):
|
if not self.checkRbacPermission(FileItem, "update", fileId):
|
||||||
raise PermissionError(f"No permission to delete file {fileId}")
|
raise PermissionError(f"No permission to delete file {fileId}")
|
||||||
|
|
||||||
# Check for other references to this file (by hash)
|
# Check for other references to this file (by hash) - use RBAC to only check files user has access to
|
||||||
fileHash = file.fileHash
|
fileHash = file.fileHash
|
||||||
if fileHash:
|
if fileHash:
|
||||||
otherReferences = [f for f in self.db.getRecordset(FileItem, recordFilter={"fileHash": fileHash})
|
allReferences = self.db.getRecordsetWithRBAC(
|
||||||
if f["id"] != fileId]
|
FileItem,
|
||||||
|
self.currentUser,
|
||||||
|
recordFilter={"fileHash": fileHash}
|
||||||
|
)
|
||||||
|
otherReferences = [f for f in allReferences if f["id"] != fileId]
|
||||||
|
|
||||||
# Only delete associated fileData if no other references exist
|
# Only delete associated fileData if no other references exist
|
||||||
if not otherReferences:
|
if not otherReferences:
|
||||||
try:
|
try:
|
||||||
fileDataEntries = self.db.getRecordset(FileData, recordFilter={"id": fileId})
|
fileDataEntries = self.db.getRecordsetWithRBAC(FileData, self.currentUser, recordFilter={"id": fileId})
|
||||||
if fileDataEntries:
|
if fileDataEntries:
|
||||||
self.db.recordDelete(FileData, fileId)
|
self.db.recordDelete(FileData, fileId)
|
||||||
logger.debug(f"FileData for file {fileId} deleted")
|
logger.debug(f"FileData for file {fileId} deleted")
|
||||||
|
|
@ -1024,7 +1029,7 @@ class ComponentObjects:
|
||||||
logger.warning(f"No access to file ID {fileId}")
|
logger.warning(f"No access to file ID {fileId}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
fileDataEntries = self.db.getRecordset(FileData, recordFilter={"id": fileId})
|
fileDataEntries = self.db.getRecordsetWithRBAC(FileData, self.currentUser, recordFilter={"id": fileId})
|
||||||
if not fileDataEntries:
|
if not fileDataEntries:
|
||||||
logger.warning(f"No data found for file ID {fileId}")
|
logger.warning(f"No data found for file ID {fileId}")
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
"""Migration modules for database schema and data migrations."""
|
|
||||||
|
|
@ -1,212 +0,0 @@
|
||||||
"""
|
|
||||||
Migration script to convert UAM (User Access Management) to RBAC (Role-Based Access Control).
|
|
||||||
|
|
||||||
This script:
|
|
||||||
1. Creates AccessRule table if it doesn't exist
|
|
||||||
2. Adds roleLabels column to User table if it doesn't exist
|
|
||||||
3. Converts User.privilege to User.roleLabels
|
|
||||||
4. Creates initial RBAC rules based on bootstrap logic
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import List, Dict, Any
|
|
||||||
from modules.connectors.connectorDbPostgre import DatabaseConnector
|
|
||||||
from modules.shared.configuration import APP_CONFIG
|
|
||||||
from modules.datamodels.datamodelUam import UserInDB, UserPrivilege
|
|
||||||
from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext
|
|
||||||
from modules.datamodels.datamodelUam import AccessLevel
|
|
||||||
from modules.interfaces.interfaceBootstrap import initRbacRules
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def migrateUamToRbac(db: DatabaseConnector, dryRun: bool = False) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Migrate from UAM to RBAC system.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: Database connector instance
|
|
||||||
dryRun: If True, only report what would be done without making changes
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary with migration results
|
|
||||||
"""
|
|
||||||
results = {
|
|
||||||
"schemaChanges": [],
|
|
||||||
"dataMigrations": [],
|
|
||||||
"rulesCreated": 0,
|
|
||||||
"usersUpdated": 0,
|
|
||||||
"errors": []
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Step 1: Ensure AccessRule table exists
|
|
||||||
logger.info("Step 1: Ensuring AccessRule table exists")
|
|
||||||
if not dryRun:
|
|
||||||
db._ensureTableExists(AccessRule)
|
|
||||||
results["schemaChanges"].append("AccessRule table ensured")
|
|
||||||
else:
|
|
||||||
results["schemaChanges"].append("Would ensure AccessRule table")
|
|
||||||
|
|
||||||
# Step 2: Add roleLabels column to UserInDB table if it doesn't exist
|
|
||||||
logger.info("Step 2: Adding roleLabels column to UserInDB table")
|
|
||||||
if not dryRun:
|
|
||||||
try:
|
|
||||||
with db.connection.cursor() as cursor:
|
|
||||||
# Check if column exists
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT column_name
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_name = 'UserInDB' AND column_name = 'roleLabels'
|
|
||||||
""")
|
|
||||||
columnExists = cursor.fetchone() is not None
|
|
||||||
|
|
||||||
if not columnExists:
|
|
||||||
cursor.execute('ALTER TABLE "UserInDB" ADD COLUMN "roleLabels" JSONB DEFAULT \'[]\'::jsonb')
|
|
||||||
db.connection.commit()
|
|
||||||
results["schemaChanges"].append("Added roleLabels column to UserInDB")
|
|
||||||
logger.info("Added roleLabels column to UserInDB table")
|
|
||||||
else:
|
|
||||||
results["schemaChanges"].append("roleLabels column already exists")
|
|
||||||
logger.info("roleLabels column already exists in UserInDB table")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error adding roleLabels column: {e}")
|
|
||||||
results["errors"].append(f"Error adding roleLabels column: {e}")
|
|
||||||
db.connection.rollback()
|
|
||||||
else:
|
|
||||||
results["schemaChanges"].append("Would add roleLabels column to UserInDB")
|
|
||||||
|
|
||||||
# Step 3: Convert User.privilege to User.roleLabels
|
|
||||||
logger.info("Step 3: Converting User.privilege to User.roleLabels")
|
|
||||||
if not dryRun:
|
|
||||||
try:
|
|
||||||
users = db.getRecordset(UserInDB)
|
|
||||||
updatedCount = 0
|
|
||||||
|
|
||||||
for user in users:
|
|
||||||
privilege = user.get("privilege")
|
|
||||||
roleLabels = user.get("roleLabels", [])
|
|
||||||
|
|
||||||
# Skip if already has roleLabels
|
|
||||||
if roleLabels and isinstance(roleLabels, list) and len(roleLabels) > 0:
|
|
||||||
logger.debug(f"User {user.get('id')} already has roleLabels: {roleLabels}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Convert privilege to roleLabels
|
|
||||||
if privilege == UserPrivilege.SYSADMIN.value:
|
|
||||||
newRoleLabels = ["sysadmin"]
|
|
||||||
elif privilege == UserPrivilege.ADMIN.value:
|
|
||||||
newRoleLabels = ["admin"]
|
|
||||||
elif privilege == UserPrivilege.USER.value:
|
|
||||||
newRoleLabels = ["user"]
|
|
||||||
else:
|
|
||||||
# Default to user if privilege is unknown
|
|
||||||
newRoleLabels = ["user"]
|
|
||||||
logger.warning(f"Unknown privilege '{privilege}' for user {user.get('id')}, defaulting to 'user'")
|
|
||||||
|
|
||||||
# Update user
|
|
||||||
user["roleLabels"] = newRoleLabels
|
|
||||||
db.recordModify(UserInDB, user["id"], user)
|
|
||||||
updatedCount += 1
|
|
||||||
logger.info(f"Updated user {user.get('id')} ({user.get('username')}): {privilege} -> {newRoleLabels}")
|
|
||||||
|
|
||||||
results["usersUpdated"] = updatedCount
|
|
||||||
logger.info(f"Updated {updatedCount} users with roleLabels")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error converting user privileges: {e}")
|
|
||||||
results["errors"].append(f"Error converting user privileges: {e}")
|
|
||||||
else:
|
|
||||||
# Dry run: count users that would be updated
|
|
||||||
users = db.getRecordset(UserInDB)
|
|
||||||
wouldUpdate = 0
|
|
||||||
for user in users:
|
|
||||||
roleLabels = user.get("roleLabels", [])
|
|
||||||
if not roleLabels or not isinstance(roleLabels, list) or len(roleLabels) == 0:
|
|
||||||
wouldUpdate += 1
|
|
||||||
results["usersUpdated"] = wouldUpdate
|
|
||||||
logger.info(f"Would update {wouldUpdate} users with roleLabels")
|
|
||||||
|
|
||||||
# Step 4: Create RBAC rules if they don't exist
|
|
||||||
logger.info("Step 4: Creating RBAC rules")
|
|
||||||
if not dryRun:
|
|
||||||
try:
|
|
||||||
existingRules = db.getRecordset(AccessRule)
|
|
||||||
if existingRules:
|
|
||||||
results["rulesCreated"] = len(existingRules)
|
|
||||||
results["dataMigrations"].append(f"RBAC rules already exist ({len(existingRules)} rules)")
|
|
||||||
logger.info(f"RBAC rules already exist ({len(existingRules)} rules)")
|
|
||||||
else:
|
|
||||||
# Initialize RBAC rules using bootstrap logic
|
|
||||||
initRbacRules(db)
|
|
||||||
newRules = db.getRecordset(AccessRule)
|
|
||||||
results["rulesCreated"] = len(newRules)
|
|
||||||
results["dataMigrations"].append(f"Created {len(newRules)} RBAC rules")
|
|
||||||
logger.info(f"Created {len(newRules)} RBAC rules")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error creating RBAC rules: {e}")
|
|
||||||
results["errors"].append(f"Error creating RBAC rules: {e}")
|
|
||||||
else:
|
|
||||||
existingRules = db.getRecordset(AccessRule)
|
|
||||||
if existingRules:
|
|
||||||
results["rulesCreated"] = len(existingRules)
|
|
||||||
results["dataMigrations"].append(f"RBAC rules already exist ({len(existingRules)} rules)")
|
|
||||||
else:
|
|
||||||
results["dataMigrations"].append("Would create RBAC rules")
|
|
||||||
|
|
||||||
logger.info("Migration completed successfully")
|
|
||||||
return results
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Migration failed: {e}")
|
|
||||||
results["errors"].append(f"Migration failed: {e}")
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def validateMigration(db: DatabaseConnector) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Validate that migration was successful.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: Database connector instance
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary with validation results
|
|
||||||
"""
|
|
||||||
validation = {
|
|
||||||
"valid": True,
|
|
||||||
"issues": []
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Check that AccessRule table exists
|
|
||||||
try:
|
|
||||||
rules = db.getRecordset(AccessRule)
|
|
||||||
if not rules:
|
|
||||||
validation["valid"] = False
|
|
||||||
validation["issues"].append("AccessRule table exists but has no rules")
|
|
||||||
except Exception as e:
|
|
||||||
validation["valid"] = False
|
|
||||||
validation["issues"].append(f"AccessRule table does not exist or is not accessible: {e}")
|
|
||||||
|
|
||||||
# Check that all users have roleLabels
|
|
||||||
users = db.getRecordset(UserInDB)
|
|
||||||
usersWithoutRoles = []
|
|
||||||
for user in users:
|
|
||||||
roleLabels = user.get("roleLabels", [])
|
|
||||||
if not roleLabels or not isinstance(roleLabels, list) or len(roleLabels) == 0:
|
|
||||||
usersWithoutRoles.append({
|
|
||||||
"id": user.get("id"),
|
|
||||||
"username": user.get("username"),
|
|
||||||
"privilege": user.get("privilege")
|
|
||||||
})
|
|
||||||
|
|
||||||
if usersWithoutRoles:
|
|
||||||
validation["valid"] = False
|
|
||||||
validation["issues"].append(f"{len(usersWithoutRoles)} users without roleLabels: {[u['username'] for u in usersWithoutRoles]}")
|
|
||||||
|
|
||||||
return validation
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
validation["valid"] = False
|
|
||||||
validation["issues"].append(f"Validation error: {e}")
|
|
||||||
return validation
|
|
||||||
|
|
@ -11,7 +11,7 @@ import logging
|
||||||
# Import interfaces and models
|
# Import interfaces and models
|
||||||
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
import modules.interfaces.interfaceDbChatObjects as interfaceDbChatObjects
|
||||||
from modules.security.auth import getCurrentUser, limiter
|
from modules.security.auth import getCurrentUser, limiter
|
||||||
from modules.datamodels.datamodelUam import User, UserPrivilege
|
from modules.datamodels.datamodelUam import User
|
||||||
|
|
||||||
# Configure logger
|
# Configure logger
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -30,11 +30,11 @@ router = APIRouter(
|
||||||
)
|
)
|
||||||
|
|
||||||
def requireSysadmin(currentUser: User):
|
def requireSysadmin(currentUser: User):
|
||||||
"""Require sysadmin privilege"""
|
"""Require sysadmin role"""
|
||||||
if currentUser.privilege != UserPrivilege.SYSADMIN:
|
if "sysadmin" not in (currentUser.roleLabels or []):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Sysadmin privilege required"
|
detail="Sysadmin role required"
|
||||||
)
|
)
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import modules.interfaces.interfaceDbAppObjects as interfaceDbAppObjects
|
||||||
from modules.security.auth import getCurrentUser, limiter, getCurrentUser
|
from modules.security.auth import getCurrentUser, limiter, getCurrentUser
|
||||||
|
|
||||||
# Import the attribute definition and helper functions
|
# Import the attribute definition and helper functions
|
||||||
from modules.datamodels.datamodelUam import User, UserPrivilege
|
from modules.datamodels.datamodelUam import User
|
||||||
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata
|
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata
|
||||||
|
|
||||||
# Configure logger
|
# Configure logger
|
||||||
|
|
@ -141,7 +141,7 @@ async def create_user(
|
||||||
fullName=user_data.fullName,
|
fullName=user_data.fullName,
|
||||||
language=user_data.language,
|
language=user_data.language,
|
||||||
enabled=user_data.enabled,
|
enabled=user_data.enabled,
|
||||||
privilege=user_data.privilege,
|
roleLabels=user_data.roleLabels if user_data.roleLabels else ["user"],
|
||||||
authenticationAuthority=user_data.authenticationAuthority
|
authenticationAuthority=user_data.authenticationAuthority
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -188,7 +188,7 @@ async def reset_user_password(
|
||||||
"""Reset user password (Admin only)"""
|
"""Reset user password (Admin only)"""
|
||||||
try:
|
try:
|
||||||
# Check if current user is admin
|
# Check if current user is admin
|
||||||
if currentUser.privilege != UserPrivilege.ADMIN:
|
if "admin" not in (currentUser.roleLabels or []) and "sysadmin" not in (currentUser.roleLabels or []):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Only administrators can reset passwords"
|
detail="Only administrators can reset passwords"
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,10 @@ router = APIRouter(
|
||||||
)
|
)
|
||||||
|
|
||||||
def _ensure_admin_scope(current_user: User, target_mandate_id: Optional[str] = None) -> None:
|
def _ensure_admin_scope(current_user: User, target_mandate_id: Optional[str] = None) -> None:
|
||||||
if current_user.privilege not in ("admin", "sysadmin"):
|
roleLabels = current_user.roleLabels or []
|
||||||
|
if "admin" not in roleLabels and "sysadmin" not in roleLabels:
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required")
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required")
|
||||||
if current_user.privilege == "admin":
|
if "admin" in roleLabels and "sysadmin" not in roleLabels:
|
||||||
if target_mandate_id and str(target_mandate_id) != str(current_user.mandateId):
|
if target_mandate_id and str(target_mandate_id) != str(current_user.mandateId):
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden for target mandate")
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden for target mandate")
|
||||||
|
|
||||||
|
|
@ -63,7 +64,8 @@ async def list_tokens(
|
||||||
recordFilter["connectionId"] = connectionId
|
recordFilter["connectionId"] = connectionId
|
||||||
if statusFilter:
|
if statusFilter:
|
||||||
recordFilter["status"] = statusFilter
|
recordFilter["status"] = statusFilter
|
||||||
if currentUser.privilege == "admin":
|
roleLabels = currentUser.roleLabels or []
|
||||||
|
if "admin" in roleLabels and "sysadmin" not in roleLabels:
|
||||||
recordFilter["mandateId"] = str(currentUser.mandateId)
|
recordFilter["mandateId"] = str(currentUser.mandateId)
|
||||||
|
|
||||||
tokens = appInterface.db.getRecordset(Token, recordFilter=recordFilter)
|
tokens = appInterface.db.getRecordset(Token, recordFilter=recordFilter)
|
||||||
|
|
@ -95,10 +97,11 @@ async def revoke_tokens_by_user(
|
||||||
target_mandate = target_user[0].get("mandateId") if target_user else None
|
target_mandate = target_user[0].get("mandateId") if target_user else None
|
||||||
_ensure_admin_scope(currentUser, target_mandate)
|
_ensure_admin_scope(currentUser, target_mandate)
|
||||||
|
|
||||||
|
roleLabels = currentUser.roleLabels or []
|
||||||
count = appInterface.revokeTokensByUser(
|
count = appInterface.revokeTokensByUser(
|
||||||
userId=userId,
|
userId=userId,
|
||||||
authority=AuthAuthority(authority) if authority else None,
|
authority=AuthAuthority(authority) if authority else None,
|
||||||
mandateId=None if currentUser.privilege == "sysadmin" else str(currentUser.mandateId),
|
mandateId=None if "sysadmin" in roleLabels else str(currentUser.mandateId),
|
||||||
revokedBy=currentUser.id,
|
revokedBy=currentUser.id,
|
||||||
reason=reason
|
reason=reason
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ from jose import jwt
|
||||||
from modules.security.auth import getCurrentUser, limiter, SECRET_KEY, ALGORITHM
|
from modules.security.auth import getCurrentUser, limiter, SECRET_KEY, ALGORITHM
|
||||||
from modules.security.jwtService import createAccessToken, createRefreshToken, setAccessTokenCookie, setRefreshTokenCookie, clearAccessTokenCookie, clearRefreshTokenCookie
|
from modules.security.jwtService import createAccessToken, createRefreshToken, setAccessTokenCookie, setRefreshTokenCookie, clearAccessTokenCookie, clearRefreshTokenCookie
|
||||||
from modules.interfaces.interfaceDbAppObjects import getInterface, getRootInterface
|
from modules.interfaces.interfaceDbAppObjects import getInterface, getRootInterface
|
||||||
from modules.datamodels.datamodelUam import User, UserInDB, AuthAuthority, UserPrivilege
|
from modules.datamodels.datamodelUam import User, UserInDB, AuthAuthority
|
||||||
from modules.datamodels.datamodelSecurity import Token
|
from modules.datamodels.datamodelSecurity import Token
|
||||||
|
|
||||||
# Configure logger
|
# Configure logger
|
||||||
|
|
@ -212,9 +212,8 @@ async def register_user(
|
||||||
appInterface.mandateId = defaultMandateId
|
appInterface.mandateId = defaultMandateId
|
||||||
|
|
||||||
# Create user with local authentication
|
# Create user with local authentication
|
||||||
# Set safe default privilege level for new registrations
|
# Set safe default role for new registrations
|
||||||
# New users are disabled by default and require admin approval
|
# New users are disabled by default and require admin approval
|
||||||
from modules.datamodels.datamodelUam import UserPrivilege
|
|
||||||
user = appInterface.createUser(
|
user = appInterface.createUser(
|
||||||
username=userData.username,
|
username=userData.username,
|
||||||
password=password,
|
password=password,
|
||||||
|
|
@ -222,7 +221,7 @@ async def register_user(
|
||||||
fullName=userData.fullName,
|
fullName=userData.fullName,
|
||||||
language=userData.language,
|
language=userData.language,
|
||||||
enabled=False, # New users are disabled by default
|
enabled=False, # New users are disabled by default
|
||||||
privilege=UserPrivilege.USER, # Always set to USER for new registrations
|
roleLabels=["user"], # Default role for new registrations
|
||||||
authenticationAuthority=AuthAuthority.LOCAL
|
authenticationAuthority=AuthAuthority.LOCAL
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -427,8 +427,12 @@ async def delete_workflow(
|
||||||
# Get service center
|
# Get service center
|
||||||
interfaceDbChat = getServiceChat(currentUser)
|
interfaceDbChat = getServiceChat(currentUser)
|
||||||
|
|
||||||
# Get raw workflow data from database to check permissions
|
# Check workflow access and permission using RBAC
|
||||||
workflows = interfaceDbChat.db.getRecordset(ChatWorkflow, recordFilter={"id": workflowId})
|
workflows = interfaceDbChat.db.getRecordsetWithRBAC(
|
||||||
|
ChatWorkflow,
|
||||||
|
currentUser,
|
||||||
|
recordFilter={"id": workflowId}
|
||||||
|
)
|
||||||
if not workflows:
|
if not workflows:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
|
|
||||||
|
|
@ -1,282 +0,0 @@
|
||||||
"""
|
|
||||||
Integration tests for UAM to RBAC migration.
|
|
||||||
Tests that migration correctly converts user privileges to roleLabels.
|
|
||||||
Uses real database connection for integration testing.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from modules.migration.migrateUamToRbac import migrateUamToRbac, validateMigration
|
|
||||||
from modules.datamodels.datamodelUam import UserInDB, UserPrivilege
|
|
||||||
from modules.connectors.connectorDbPostgre import DatabaseConnector
|
|
||||||
from modules.shared.configuration import APP_CONFIG
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="class")
|
|
||||||
def db():
|
|
||||||
"""Create real database connector for integration tests."""
|
|
||||||
dbHost = APP_CONFIG.get("DB_HOST", "localhost")
|
|
||||||
dbDatabase = APP_CONFIG.get("DB_DATABASE", "poweron_test")
|
|
||||||
dbUser = APP_CONFIG.get("DB_USER", "postgres")
|
|
||||||
dbPassword = APP_CONFIG.get("DB_PASSWORD", "")
|
|
||||||
dbPort = APP_CONFIG.get("DB_PORT", 5432)
|
|
||||||
|
|
||||||
db = DatabaseConnector(
|
|
||||||
dbHost=dbHost,
|
|
||||||
dbDatabase=dbDatabase,
|
|
||||||
dbUser=dbUser,
|
|
||||||
dbPassword=dbPassword,
|
|
||||||
dbPort=dbPort
|
|
||||||
)
|
|
||||||
yield db
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
|
|
||||||
class TestRbacMigration:
|
|
||||||
"""Test RBAC migration from UAM."""
|
|
||||||
|
|
||||||
def testMigrateUserPrivilegeToRoleLabels(self, db):
|
|
||||||
"""Test that user privileges are correctly converted to roleLabels."""
|
|
||||||
# Create test users with privileges but no roleLabels
|
|
||||||
testUsers = [
|
|
||||||
UserInDB(
|
|
||||||
id="migrate_test_user1",
|
|
||||||
username="migrate_admin",
|
|
||||||
privilege=UserPrivilege.SYSADMIN.value
|
|
||||||
),
|
|
||||||
UserInDB(
|
|
||||||
id="migrate_test_user2",
|
|
||||||
username="migrate_admin2",
|
|
||||||
privilege=UserPrivilege.ADMIN.value
|
|
||||||
),
|
|
||||||
UserInDB(
|
|
||||||
id="migrate_test_user3",
|
|
||||||
username="migrate_user1",
|
|
||||||
privilege=UserPrivilege.USER.value
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Create test users in database
|
|
||||||
for user in testUsers:
|
|
||||||
userData = user.model_dump()
|
|
||||||
# Ensure roleLabels is None/empty for migration test
|
|
||||||
userData["roleLabels"] = []
|
|
||||||
userData["id"] = user.id
|
|
||||||
db.recordCreate(UserInDB, userData)
|
|
||||||
|
|
||||||
# Run migration
|
|
||||||
results = migrateUamToRbac(db, dryRun=False)
|
|
||||||
|
|
||||||
# Check that users were updated
|
|
||||||
assert results["usersUpdated"] == 3
|
|
||||||
|
|
||||||
# Verify users were actually updated in database
|
|
||||||
users1 = db.getRecordset(UserInDB, recordFilter={"id": "migrate_test_user1"})
|
|
||||||
users2 = db.getRecordset(UserInDB, recordFilter={"id": "migrate_test_user2"})
|
|
||||||
users3 = db.getRecordset(UserInDB, recordFilter={"id": "migrate_test_user3"})
|
|
||||||
user1 = users1[0] if users1 else None
|
|
||||||
user2 = users2[0] if users2 else None
|
|
||||||
user3 = users3[0] if users3 else None
|
|
||||||
|
|
||||||
assert user1 is not None
|
|
||||||
assert "sysadmin" in user1.get("roleLabels", [])
|
|
||||||
|
|
||||||
assert user2 is not None
|
|
||||||
assert "admin" in user2.get("roleLabels", [])
|
|
||||||
|
|
||||||
assert user3 is not None
|
|
||||||
assert "user" in user3.get("roleLabels", [])
|
|
||||||
finally:
|
|
||||||
# Cleanup test users
|
|
||||||
for user in testUsers:
|
|
||||||
try:
|
|
||||||
db.recordDelete(UserInDB, user.id)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def testMigrationSkipsUsersWithExistingRoleLabels(self, db):
|
|
||||||
"""Test that migration skips users who already have roleLabels."""
|
|
||||||
# Create test users: one with roleLabels, one without
|
|
||||||
user1 = UserInDB(
|
|
||||||
id="skip_test_user1",
|
|
||||||
username="skip_admin",
|
|
||||||
privilege=UserPrivilege.SYSADMIN.value,
|
|
||||||
roleLabels=["sysadmin"] # Already migrated
|
|
||||||
)
|
|
||||||
user2 = UserInDB(
|
|
||||||
id="skip_test_user2",
|
|
||||||
username="skip_user1",
|
|
||||||
privilege=UserPrivilege.USER.value,
|
|
||||||
roleLabels=[] # Needs migration
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Create test users in database
|
|
||||||
user1Data = user1.model_dump()
|
|
||||||
user1Data["id"] = user1.id
|
|
||||||
user2Data = user2.model_dump()
|
|
||||||
user2Data["id"] = user2.id
|
|
||||||
db.recordCreate(UserInDB, user1Data)
|
|
||||||
db.recordCreate(UserInDB, user2Data)
|
|
||||||
|
|
||||||
# Run migration
|
|
||||||
results = migrateUamToRbac(db, dryRun=False)
|
|
||||||
|
|
||||||
# Only one user should be updated (user2)
|
|
||||||
assert results["usersUpdated"] == 1
|
|
||||||
|
|
||||||
# Verify user1 still has original roleLabels
|
|
||||||
users1 = db.getRecordset(UserInDB, recordFilter={"id": "skip_test_user1"})
|
|
||||||
updatedUser1 = users1[0] if users1 else None
|
|
||||||
assert updatedUser1 is not None
|
|
||||||
assert "sysadmin" in updatedUser1.get("roleLabels", [])
|
|
||||||
|
|
||||||
# Verify user2 was updated
|
|
||||||
users2 = db.getRecordset(UserInDB, recordFilter={"id": "skip_test_user2"})
|
|
||||||
updatedUser2 = users2[0] if users2 else None
|
|
||||||
assert updatedUser2 is not None
|
|
||||||
assert "user" in updatedUser2.get("roleLabels", [])
|
|
||||||
finally:
|
|
||||||
# Cleanup test users
|
|
||||||
try:
|
|
||||||
db.recordDelete(UserInDB, "skip_test_user1")
|
|
||||||
db.recordDelete(UserInDB, "skip_test_user2")
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def testDryRunMode(self, db):
|
|
||||||
"""Test that dry run mode doesn't make changes."""
|
|
||||||
# Create test user without roleLabels
|
|
||||||
testUser = UserInDB(
|
|
||||||
id="dryrun_test_user1",
|
|
||||||
username="dryrun_admin",
|
|
||||||
privilege=UserPrivilege.SYSADMIN.value,
|
|
||||||
roleLabels=[] # Needs migration
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Create test user in database
|
|
||||||
userData = testUser.model_dump()
|
|
||||||
userData["id"] = testUser.id
|
|
||||||
db.recordCreate(UserInDB, userData)
|
|
||||||
|
|
||||||
# Get original state
|
|
||||||
originalUsers = db.getRecordset(UserInDB, recordFilter={"id": "dryrun_test_user1"})
|
|
||||||
originalUser = originalUsers[0] if originalUsers else None
|
|
||||||
assert originalUser is not None
|
|
||||||
originalRoleLabels = originalUser.get("roleLabels", [])
|
|
||||||
|
|
||||||
# Run migration in dry run mode
|
|
||||||
results = migrateUamToRbac(db, dryRun=True)
|
|
||||||
|
|
||||||
# Should report what would be done
|
|
||||||
assert results["usersUpdated"] == 1
|
|
||||||
|
|
||||||
# Verify user was NOT actually updated
|
|
||||||
unchangedUsers = db.getRecordset(UserInDB, recordFilter={"id": "dryrun_test_user1"})
|
|
||||||
unchangedUser = unchangedUsers[0] if unchangedUsers else None
|
|
||||||
assert unchangedUser is not None
|
|
||||||
assert unchangedUser.get("roleLabels", []) == originalRoleLabels
|
|
||||||
finally:
|
|
||||||
# Cleanup test user
|
|
||||||
try:
|
|
||||||
db.recordDelete(UserInDB, "dryrun_test_user1")
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def testValidateMigrationSuccess(self, db):
|
|
||||||
"""Test validation passes when migration is successful."""
|
|
||||||
# Create test users with roleLabels (already migrated)
|
|
||||||
testUsers = [
|
|
||||||
UserInDB(
|
|
||||||
id="validate_test_user1",
|
|
||||||
username="validate_admin",
|
|
||||||
privilege=UserPrivilege.SYSADMIN.value,
|
|
||||||
roleLabels=["sysadmin"]
|
|
||||||
),
|
|
||||||
UserInDB(
|
|
||||||
id="validate_test_user2",
|
|
||||||
username="validate_admin2",
|
|
||||||
privilege=UserPrivilege.ADMIN.value,
|
|
||||||
roleLabels=["admin"]
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Create test users in database
|
|
||||||
for user in testUsers:
|
|
||||||
userData = user.model_dump()
|
|
||||||
userData["id"] = user.id
|
|
||||||
db.recordCreate(UserInDB, userData)
|
|
||||||
|
|
||||||
# Ensure AccessRule table exists (migration should have created it)
|
|
||||||
from modules.datamodels.datamodelRbac import AccessRule
|
|
||||||
db._ensureTableExists(AccessRule)
|
|
||||||
|
|
||||||
# Run validation
|
|
||||||
validation = validateMigration(db)
|
|
||||||
|
|
||||||
assert validation["valid"] == True
|
|
||||||
assert len(validation["issues"]) == 0
|
|
||||||
finally:
|
|
||||||
# Cleanup test users
|
|
||||||
for user in testUsers:
|
|
||||||
try:
|
|
||||||
db.recordDelete(UserInDB, user.id)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def testValidateMigrationFailsWithoutRoleLabels(self, db):
|
|
||||||
"""Test validation fails when users don't have roleLabels."""
|
|
||||||
# Create test users: one with roleLabels, one without, one with empty roleLabels
|
|
||||||
testUsers = [
|
|
||||||
UserInDB(
|
|
||||||
id="validate_fail_user1",
|
|
||||||
username="validate_fail_admin",
|
|
||||||
privilege=UserPrivilege.SYSADMIN.value,
|
|
||||||
roleLabels=["sysadmin"] # Has roleLabels
|
|
||||||
),
|
|
||||||
UserInDB(
|
|
||||||
id="validate_fail_user2",
|
|
||||||
username="validate_fail_user",
|
|
||||||
privilege=UserPrivilege.USER.value,
|
|
||||||
roleLabels=[] # Empty roleLabels
|
|
||||||
),
|
|
||||||
UserInDB(
|
|
||||||
id="validate_fail_user3",
|
|
||||||
username="validate_fail_user2",
|
|
||||||
privilege=UserPrivilege.USER.value
|
|
||||||
# Missing roleLabels field (will be None)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Create test users in database
|
|
||||||
for user in testUsers:
|
|
||||||
userData = user.model_dump()
|
|
||||||
userData["id"] = user.id
|
|
||||||
# For user3, explicitly set roleLabels to None or remove it
|
|
||||||
if user.id == "validate_fail_user3":
|
|
||||||
if "roleLabels" in userData:
|
|
||||||
del userData["roleLabels"]
|
|
||||||
db.recordCreate(UserInDB, userData)
|
|
||||||
|
|
||||||
# Ensure AccessRule table exists
|
|
||||||
from modules.datamodels.datamodelRbac import AccessRule
|
|
||||||
db._ensureTableExists(AccessRule)
|
|
||||||
|
|
||||||
# Run validation
|
|
||||||
validation = validateMigration(db)
|
|
||||||
|
|
||||||
assert validation["valid"] == False
|
|
||||||
assert len(validation["issues"]) > 0
|
|
||||||
# Check that validation found users without roleLabels
|
|
||||||
issuesStr = " ".join(validation["issues"])
|
|
||||||
assert "users without roleLabels" in issuesStr or "without roleLabels" in issuesStr
|
|
||||||
finally:
|
|
||||||
# Cleanup test users
|
|
||||||
for user in testUsers:
|
|
||||||
try:
|
|
||||||
db.recordDelete(UserInDB, user.id)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
@ -14,7 +14,7 @@ from modules.interfaces.interfaceBootstrap import (
|
||||||
createDefaultRoleRules,
|
createDefaultRoleRules,
|
||||||
createTableSpecificRules
|
createTableSpecificRules
|
||||||
)
|
)
|
||||||
from modules.datamodels.datamodelUam import UserInDB, Mandate, UserPrivilege, AuthAuthority
|
from modules.datamodels.datamodelUam import UserInDB, Mandate, AuthAuthority
|
||||||
from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext
|
from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext
|
||||||
from modules.datamodels.datamodelUam import AccessLevel
|
from modules.datamodels.datamodelUam import AccessLevel
|
||||||
|
|
||||||
|
|
@ -62,7 +62,6 @@ class TestRbacBootstrap:
|
||||||
assert isinstance(user, UserInDB)
|
assert isinstance(user, UserInDB)
|
||||||
assert user.username == "admin"
|
assert user.username == "admin"
|
||||||
assert "sysadmin" in user.roleLabels
|
assert "sysadmin" in user.roleLabels
|
||||||
assert user.privilege == UserPrivilege.SYSADMIN
|
|
||||||
|
|
||||||
def testInitEventUserCreatesWithSysadminRole(self):
|
def testInitEventUserCreatesWithSysadminRole(self):
|
||||||
"""Test that initEventUser creates user with sysadmin role."""
|
"""Test that initEventUser creates user with sysadmin role."""
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue