fixed udb rbac

This commit is contained in:
ValueOn AG 2026-04-12 10:43:30 +02:00
parent 091a3672de
commit 1b51ee3e1c
3 changed files with 35 additions and 34 deletions

View file

@ -956,7 +956,7 @@ class ComponentObjects:
mid = file.get("mandateId") mid = file.get("mandateId")
return not mid or (isinstance(mid, str) and not mid.strip()) return not mid or (isinstance(mid, str) and not mid.strip())
def getAllFiles(self, pagination: Optional[PaginationParams] = None) -> Union[List[FileItem], PaginatedResult]: def getAllFiles(self, pagination: Optional[PaginationParams] = None, recordFilter: Dict[str, Any] = None) -> Union[List[FileItem], PaginatedResult]:
""" """
Returns files visible to the current user based on RBAC scope rules. Returns files visible to the current user based on RBAC scope rules.
@ -999,44 +999,31 @@ class ComponentObjects:
continue continue
return fileItems return fileItems
folderFilter = None
hasFolderFilter = False
if pagination and pagination.filters and "folderId" in pagination.filters:
folderFilter = pagination.filters.pop("folderId")
hasFolderFilter = True
def _applyFolderFilter(files):
if not hasFolderFilter:
return files
if folderFilter is None:
return [f for f in files if not (f.get("folderId") if isinstance(f, dict) else getattr(f, "folderId", None))]
return [f for f in files if (f.get("folderId") if isinstance(f, dict) else getattr(f, "folderId", None)) == folderFilter]
if pagination is None: if pagination is None:
allFiles = getRecordsetWithRBAC( allFiles = getRecordsetWithRBAC(
self.db, FileItem, self.currentUser, self.db, FileItem, self.currentUser,
recordFilter=recordFilter,
mandateId=self.mandateId, mandateId=self.mandateId,
featureInstanceId=self.featureInstanceId, featureInstanceId=self.featureInstanceId,
) )
return _convertFileItems(_applyFolderFilter(allFiles)) return _convertFileItems(allFiles)
result = getRecordsetPaginatedWithRBAC( result = getRecordsetPaginatedWithRBAC(
self.db, FileItem, self.currentUser, self.db, FileItem, self.currentUser,
pagination=pagination, pagination=pagination,
recordFilter=recordFilter,
mandateId=self.mandateId, mandateId=self.mandateId,
featureInstanceId=self.featureInstanceId, featureInstanceId=self.featureInstanceId,
) )
if isinstance(result, PaginatedResult): if isinstance(result, PaginatedResult):
filtered = _applyFolderFilter(result.items)
return PaginatedResult( return PaginatedResult(
items=_convertFileItems(filtered), items=_convertFileItems(result.items),
totalItems=len(filtered), totalItems=result.totalItems,
totalPages=max(1, -(-len(filtered) // (pagination.pageSize or 20))), totalPages=result.totalPages,
) )
raw = result if isinstance(result, list) else [] return _convertFileItems(result if isinstance(result, list) else [])
return _convertFileItems(_applyFolderFilter(raw))
def getFile(self, fileId: str) -> Optional[FileItem]: def getFile(self, fileId: str) -> Optional[FileItem]:
"""Returns a file by ID if the current user has RBAC access (scope-based).""" """Returns a file by ID if the current user has RBAC access (scope-based)."""
@ -1337,13 +1324,14 @@ class ComponentObjects:
return folders[0] if folders else None return folders[0] if folders else None
def listFolders(self, parentId: Optional[str] = None) -> List[Dict[str, Any]]: def listFolders(self, parentId: Optional[str] = None) -> List[Dict[str, Any]]:
"""List folders for current user, optionally filtered by parentId. """List folders visible to the current user.
Each folder is enriched with ``fileCount`` (number of direct files Own folders are always returned. Other users' folders are only
visible to this user via RBAC scope rules).""" returned when they contain files visible to the current user.
recordFilter = {"sysCreatedBy": self.userId or ""} Each folder is enriched with ``fileCount``."""
recordFilter = {}
if parentId is not None: if parentId is not None:
recordFilter["parentId"] = parentId recordFilter["parentId"] = parentId
folders = self.db.getRecordset(FileFolder, recordFilter=recordFilter) folders = self.db.getRecordset(FileFolder, recordFilter=recordFilter if recordFilter else None)
if not folders: if not folders:
return folders return folders
@ -1351,11 +1339,7 @@ class ComponentObjects:
folderIds = [f["id"] for f in folders if f.get("id")] folderIds = [f["id"] for f in folders if f.get("id")]
fileCounts: Dict[str, int] = {} fileCounts: Dict[str, int] = {}
try: try:
# Count files per folder that the user can see (RBAC scope-aware).
# Own files are always counted; shared files (global/mandate/featureInstance)
# that happen to be in one of the user's folders are also counted.
from modules.interfaces.interfaceRbac import _buildFilesScopeWhereClause from modules.interfaces.interfaceRbac import _buildFilesScopeWhereClause
from modules.datamodels.datamodelUam import User as UserModel
scopeClause = _buildFilesScopeWhereClause( scopeClause = _buildFilesScopeWhereClause(
self.currentUser, "FileItem", self.db, self.currentUser, "FileItem", self.db,
self.mandateId, self.featureInstanceId, self.mandateId, self.featureInstanceId,
@ -1382,10 +1366,16 @@ class ComponentObjects:
except Exception as e: except Exception as e:
logger.warning(f"Could not count files per folder: {e}") logger.warning(f"Could not count files per folder: {e}")
userId = self.userId or ""
result = []
for folder in folders: for folder in folders:
folder["fileCount"] = fileCounts.get(folder.get("id", ""), 0) fc = fileCounts.get(folder.get("id", ""), 0)
folder["fileCount"] = fc
isOwn = folder.get("sysCreatedBy") == userId
if isOwn or fc > 0:
result.append(folder)
return folders return result
def createFolder(self, name: str, parentId: Optional[str] = None) -> Dict[str, Any]: def createFolder(self, name: str, parentId: Optional[str] = None) -> Dict[str, Any]:
"""Create a new folder with unique name validation.""" """Create a new folder with unique name validation."""

View file

@ -740,9 +740,15 @@ def buildRbacWhereClause(
# All records within the feature instance - only featureInstanceId filtering # All records within the feature instance - only featureInstanceId filtering
if readLevel == AccessLevel.ALL: if readLevel == AccessLevel.ALL:
namespaceAll = TABLE_NAMESPACE.get(table, "system")
# Files: scope-based context filtering applies even with ALL access
if namespaceAll == "files":
return _buildFilesScopeWhereClause(
currentUser, table, connector, mandateId, featureInstanceId,
baseConditions, baseValues,
)
# Chat / AI Workspace: even DATA read ALL must not list other users' rows in a # Chat / AI Workspace: even DATA read ALL must not list other users' rows in a
# shared featureInstance (stale RBAC rules or merged roles). Same as MY. # shared featureInstance (stale RBAC rules or merged roles). Same as MY.
namespaceAll = TABLE_NAMESPACE.get(table, "system")
if featureInstanceId and namespaceAll == "chat": if featureInstanceId and namespaceAll == "chat":
userIdFieldAll = "sysCreatedBy" userIdFieldAll = "sysCreatedBy"
if table == "UserInDB": if table == "UserInDB":

View file

@ -207,12 +207,17 @@ def get_files(
detail=f"Invalid pagination parameter: {str(e)}" detail=f"Invalid pagination parameter: {str(e)}"
) )
recordFilter = None
if paginationParams and paginationParams.filters and "folderId" in paginationParams.filters:
fVal = paginationParams.filters.pop("folderId")
recordFilter = {"folderId": fVal}
managementInterface = interfaceDbManagement.getInterface( managementInterface = interfaceDbManagement.getInterface(
currentUser, currentUser,
mandateId=str(context.mandateId) if context.mandateId else None, mandateId=str(context.mandateId) if context.mandateId else None,
featureInstanceId=str(context.featureInstanceId) if context.featureInstanceId else None featureInstanceId=str(context.featureInstanceId) if context.featureInstanceId else None
) )
result = managementInterface.getAllFiles(pagination=paginationParams) result = managementInterface.getAllFiles(pagination=paginationParams, recordFilter=recordFilter)
# If pagination was requested, result is PaginatedResult # If pagination was requested, result is PaginatedResult
# If no pagination, result is List[FileItem] # If no pagination, result is List[FileItem]