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 @@
+
+
{$i18n.t('File Decryption')}
+
+ +
+
+ + {#if RAGConfig.ENABLE_FILE_DECRYPTION} +
+
+
+ {$i18n.t('Decryption Endpoint URL')} +
+ +
+ +
+
{$i18n.t('API Key')}
+
+ +
+
+ +
+
{$i18n.t('Timeout (seconds)')}
+ +
+ + {#if !RAGConfig.FILE_DECRYPTION_ENDPOINT || !RAGConfig.FILE_DECRYPTION_API_KEY} +
+ {$i18n.t('Endpoint URL and API Key are required when file decryption is enabled.')} +
+ {/if} +
+ {/if} +
{$i18n.t('Allowed File Extensions')}
diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 65dc33c5bf..c174b31601 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -555,7 +555,15 @@ files = [...files, fileItem]; if (!$temporaryChatEnabled) { + let loadingToastId = null; + try { + // Show loading toast for decryption if enabled in config + if ($config?.file?.decryption_enabled ?? false) { + loadingToastId = toast.loading($i18n.t('Decrypting and uploading file...')); + } else { + loadingToastId = toast.loading($i18n.t('Uploading file...')); + } // If the file is an audio file, provide the language for STT. let metadata = null; if ( @@ -590,11 +598,22 @@ fileItem.url = `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`; files = files; + toast.success($i18n.t('File uploaded successfully.'), { id: loadingToastId }); } else { files = files.filter((item) => item?.itemId !== tempItemId); + toast.dismiss(loadingToastId); } } catch (e) { - toast.error(`${e}`); + toast.dismiss(loadingToastId); + const errMsg = typeof e === 'string' ? e : (e?.message || `${e}`); + if ( + errMsg.toLowerCase().includes('decryption') || + errMsg.toLowerCase().includes('azure function error') + ) { + toast.error($i18n.t('File decryption failed: {{error}}', { error: errMsg })); + } else { + toast.error(`${errMsg}`); + } files = files.filter((item) => item?.itemId !== tempItemId); } } else {