diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 983db4e04b..97fc77f962 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -2710,6 +2710,28 @@ RAG_ALLOWED_FILE_EXTENSIONS = PersistentConfig( ], ) +# File decryption settings +ENABLE_FILE_DECRYPTION = PersistentConfig( + "ENABLE_FILE_DECRYPTION", + "file_decryption.enable", + os.environ.get("ENABLE_FILE_DECRYPTION", "False").lower() == "true", +) +FILE_DECRYPTION_ENDPOINT = PersistentConfig( + "FILE_DECRYPTION_ENDPOINT", + "file_decryption.endpoint", + os.environ.get("FILE_DECRYPTION_ENDPOINT", ""), +) +FILE_DECRYPTION_API_KEY = PersistentConfig( + "FILE_DECRYPTION_API_KEY", + "file_decryption.api_key", + os.environ.get("FILE_DECRYPTION_API_KEY", ""), +) +FILE_DECRYPTION_TIMEOUT = PersistentConfig( + "FILE_DECRYPTION_TIMEOUT", + "file_decryption.timeout", + int(os.environ.get("FILE_DECRYPTION_TIMEOUT", "30")), +) + RAG_EMBEDDING_ENGINE = PersistentConfig( "RAG_EMBEDDING_ENGINE", "rag.embedding_engine", diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 5609289166..ee21f479a9 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -248,6 +248,10 @@ from open_webui.config import ( RAG_AZURE_OPENAI_API_VERSION, RAG_OLLAMA_BASE_URL, RAG_OLLAMA_API_KEY, + ENABLE_FILE_DECRYPTION, + FILE_DECRYPTION_ENDPOINT, + FILE_DECRYPTION_API_KEY, + FILE_DECRYPTION_TIMEOUT, CHUNK_OVERLAP, CHUNK_SIZE, CONTENT_EXTRACTION_ENGINE, @@ -909,6 +913,11 @@ app.state.config.RAG_AZURE_OPENAI_API_VERSION = RAG_AZURE_OPENAI_API_VERSION app.state.config.RAG_OLLAMA_BASE_URL = RAG_OLLAMA_BASE_URL app.state.config.RAG_OLLAMA_API_KEY = RAG_OLLAMA_API_KEY +app.state.config.ENABLE_FILE_DECRYPTION = ENABLE_FILE_DECRYPTION +app.state.config.FILE_DECRYPTION_ENDPOINT = FILE_DECRYPTION_ENDPOINT +app.state.config.FILE_DECRYPTION_API_KEY = FILE_DECRYPTION_API_KEY +app.state.config.FILE_DECRYPTION_TIMEOUT = FILE_DECRYPTION_TIMEOUT + app.state.config.PDF_EXTRACT_IMAGES = PDF_EXTRACT_IMAGES app.state.config.YOUTUBE_LOADER_LANGUAGE = YOUTUBE_LOADER_LANGUAGE @@ -1903,6 +1912,7 @@ async def get_app_config(request: Request): "width": app.state.config.FILE_IMAGE_COMPRESSION_WIDTH, "height": app.state.config.FILE_IMAGE_COMPRESSION_HEIGHT, }, + "decryption_enabled": app.state.config.ENABLE_FILE_DECRYPTION, }, "permissions": {**app.state.config.USER_PERMISSIONS}, "google_drive": { diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index 08ffde1733..9aadf7b4a9 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -446,6 +446,11 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)): "TOP_K_RERANKER": request.app.state.config.TOP_K_RERANKER, "RELEVANCE_THRESHOLD": request.app.state.config.RELEVANCE_THRESHOLD, "HYBRID_BM25_WEIGHT": request.app.state.config.HYBRID_BM25_WEIGHT, + # File decryption settings + "ENABLE_FILE_DECRYPTION": request.app.state.config.ENABLE_FILE_DECRYPTION, + "FILE_DECRYPTION_ENDPOINT": request.app.state.config.FILE_DECRYPTION_ENDPOINT, + "FILE_DECRYPTION_API_KEY": request.app.state.config.FILE_DECRYPTION_API_KEY, + "FILE_DECRYPTION_TIMEOUT": request.app.state.config.FILE_DECRYPTION_TIMEOUT, # Content extraction settings "CONTENT_EXTRACTION_ENGINE": request.app.state.config.CONTENT_EXTRACTION_ENGINE, "PDF_EXTRACT_IMAGES": request.app.state.config.PDF_EXTRACT_IMAGES, @@ -682,6 +687,12 @@ class ConfigForm(BaseModel): ENABLE_GOOGLE_DRIVE_INTEGRATION: Optional[bool] = None ENABLE_ONEDRIVE_INTEGRATION: Optional[bool] = None + # File decryption settings + ENABLE_FILE_DECRYPTION: Optional[bool] = None + FILE_DECRYPTION_ENDPOINT: Optional[str] = None + FILE_DECRYPTION_API_KEY: Optional[str] = None + FILE_DECRYPTION_TIMEOUT: Optional[int] = None + # Web search settings web: Optional[WebConfig] = None @@ -997,6 +1008,28 @@ async def update_rag_config( else request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION ) + # File decryption settings + request.app.state.config.ENABLE_FILE_DECRYPTION = ( + form_data.ENABLE_FILE_DECRYPTION + if form_data.ENABLE_FILE_DECRYPTION is not None + else request.app.state.config.ENABLE_FILE_DECRYPTION + ) + request.app.state.config.FILE_DECRYPTION_ENDPOINT = ( + form_data.FILE_DECRYPTION_ENDPOINT + if form_data.FILE_DECRYPTION_ENDPOINT is not None + else request.app.state.config.FILE_DECRYPTION_ENDPOINT + ) + request.app.state.config.FILE_DECRYPTION_API_KEY = ( + form_data.FILE_DECRYPTION_API_KEY + if form_data.FILE_DECRYPTION_API_KEY is not None + else request.app.state.config.FILE_DECRYPTION_API_KEY + ) + request.app.state.config.FILE_DECRYPTION_TIMEOUT = ( + form_data.FILE_DECRYPTION_TIMEOUT + if form_data.FILE_DECRYPTION_TIMEOUT is not None + else request.app.state.config.FILE_DECRYPTION_TIMEOUT + ) + if form_data.web is not None: # Web search settings request.app.state.config.ENABLE_WEB_SEARCH = form_data.web.ENABLE_WEB_SEARCH @@ -1168,6 +1201,11 @@ async def update_rag_config( # Integration settings "ENABLE_GOOGLE_DRIVE_INTEGRATION": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, "ENABLE_ONEDRIVE_INTEGRATION": request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION, + # File decryption settings + "ENABLE_FILE_DECRYPTION": request.app.state.config.ENABLE_FILE_DECRYPTION, + "FILE_DECRYPTION_ENDPOINT": request.app.state.config.FILE_DECRYPTION_ENDPOINT, + "FILE_DECRYPTION_API_KEY": request.app.state.config.FILE_DECRYPTION_API_KEY, + "FILE_DECRYPTION_TIMEOUT": request.app.state.config.FILE_DECRYPTION_TIMEOUT, # Web search settings "web": { "ENABLE_WEB_SEARCH": request.app.state.config.ENABLE_WEB_SEARCH, diff --git a/backend/open_webui/storage/provider.py b/backend/open_webui/storage/provider.py index 4292e53827..1e1365dc2a 100644 --- a/backend/open_webui/storage/provider.py +++ b/backend/open_webui/storage/provider.py @@ -26,6 +26,10 @@ from open_webui.config import ( AZURE_STORAGE_KEY, STORAGE_PROVIDER, UPLOAD_DIR, + ENABLE_FILE_DECRYPTION, + FILE_DECRYPTION_ENDPOINT, + FILE_DECRYPTION_API_KEY, + FILE_DECRYPTION_TIMEOUT, ) from google.cloud import storage from google.cloud.exceptions import GoogleCloudError, NotFound @@ -35,11 +39,36 @@ from azure.storage.blob import BlobServiceClient from azure.core.exceptions import ResourceNotFoundError from open_webui.env import SRC_LOG_LEVELS +from open_webui.utils.decryption import ( + decrypt_file_via_azure, + DecryptionError, +) log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MAIN"]) +def _decrypt_content_if_enabled(filename: str, contents: bytes) -> bytes: + """Checks config and decrypts file contents if enabled.""" + + if ENABLE_FILE_DECRYPTION and FILE_DECRYPTION_ENDPOINT and FILE_DECRYPTION_API_KEY: + try: + decrypted_contents = decrypt_file_via_azure( + filename, + contents, + FILE_DECRYPTION_ENDPOINT, + FILE_DECRYPTION_API_KEY, + FILE_DECRYPTION_TIMEOUT, + ) + log.info(f"File {filename} decrypted successfully.") + return decrypted_contents + except Exception as e: + log.error(f"File decryption failed: {e}") + # Raise a specific error to be caught by the API layer + raise DecryptionError(f"File decryption failed: {e}") + return contents + + class StorageProvider(ABC): @abstractmethod def get_file(self, file_path: str) -> str: @@ -68,6 +97,10 @@ class LocalStorageProvider(StorageProvider): contents = file.read() if not contents: raise ValueError(ERROR_MESSAGES.EMPTY_CONTENT) + + # Decrypt contents if decryption is enabled in the config + contents = _decrypt_content_if_enabled(filename, contents) + file_path = f"{UPLOAD_DIR}/{filename}" with open(file_path, "wb") as f: f.write(contents) diff --git a/src/lib/components/admin/Settings/Documents.svelte b/src/lib/components/admin/Settings/Documents.svelte index 57a4f7b5f1..da192f996c 100644 --- a/src/lib/components/admin/Settings/Documents.svelte +++ b/src/lib/components/admin/Settings/Documents.svelte @@ -139,6 +139,14 @@ }; const submitHandler = async () => { + if ( + RAGConfig.ENABLE_FILE_DECRYPTION && + (!RAGConfig.FILE_DECRYPTION_ENDPOINT || !RAGConfig.FILE_DECRYPTION_API_KEY) + ) { + toast.error($i18n.t('Endpoint URL and API Key are required when file decryption is enabled.')); + return; + } + if ( RAGConfig.CONTENT_EXTRACTION_ENGINE === 'external' && RAGConfig.EXTERNAL_DOCUMENT_LOADER_URL === '' @@ -218,6 +226,10 @@ const res = await updateRAGConfig(localStorage.token, { ...RAGConfig, + ENABLE_FILE_DECRYPTION: RAGConfig.ENABLE_FILE_DECRYPTION ?? false, + FILE_DECRYPTION_ENDPOINT: RAGConfig.FILE_DECRYPTION_ENDPOINT ?? '', + FILE_DECRYPTION_API_KEY: RAGConfig.FILE_DECRYPTION_API_KEY ?? '', + FILE_DECRYPTION_TIMEOUT: RAGConfig.FILE_DECRYPTION_TIMEOUT ?? 30, ALLOWED_FILE_EXTENSIONS: RAGConfig.ALLOWED_FILE_EXTENSIONS.split(',') .map((ext) => ext.trim()) .filter((ext) => ext !== ''), @@ -1227,6 +1239,62 @@