fix(files): Enforce DELETE action permission for file deletion (#4275)

Previously, file deletion only checked READ permission via the
_lookup_file_id() method. This meant any user with READ access to a file
could also delete it, making it impossible to configure read-only file
access.

This change adds an 'action' parameter to fetch_all() and fetch_one() in
AuthorizedSqlStore, defaulting to Action.READ for backward
compatibility. The openai_delete_file() method now passes Action.DELETE,
ensuring proper RBAC enforcement.

With this fix, access policies can now distinguish between Users who can
read/list files but not delete them

Closes: #4274

Signed-off-by: Derek Higgins <derekh@redhat.com>
This commit is contained in:
Derek Higgins 2025-12-02 17:56:59 +00:00 committed by GitHub
parent ee107aadd6
commit 4ff0c25c52
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 20 additions and 10 deletions

View file

@ -11,6 +11,7 @@ from typing import Annotated
from fastapi import Depends, File, Form, Response, UploadFile
from llama_stack.core.access_control.datatypes import Action
from llama_stack.core.datatypes import AccessRule
from llama_stack.core.id_generation import generate_object_id
from llama_stack.core.storage.sqlstore.authorized_sqlstore import AuthorizedSqlStore
@ -72,12 +73,12 @@ class LocalfsFilesImpl(Files):
"""Get the filesystem path for a file ID."""
return Path(self.config.storage_dir) / file_id
async def _lookup_file_id(self, file_id: str) -> tuple[OpenAIFileObject, Path]:
async def _lookup_file_id(self, file_id: str, action: Action = Action.READ) -> tuple[OpenAIFileObject, Path]:
"""Look up a OpenAIFileObject and filesystem path from its ID."""
if not self.sql_store:
raise RuntimeError("Files provider not initialized")
row = await self.sql_store.fetch_one("openai_files", where={"id": file_id})
row = await self.sql_store.fetch_one("openai_files", where={"id": file_id}, action=action)
if not row:
raise ResourceNotFoundError(file_id, "File", "client.files.list()")
@ -188,7 +189,7 @@ class LocalfsFilesImpl(Files):
async def openai_delete_file(self, file_id: str) -> OpenAIFileDeleteResponse:
"""Delete a file."""
# Delete physical file
_, file_path = await self._lookup_file_id(file_id)
_, file_path = await self._lookup_file_id(file_id, action=Action.DELETE)
if file_path.exists():
file_path.unlink()