diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 49ab1a9aad..30f740d35e 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -2250,6 +2250,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 595d551d75..10f5b8e925 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -223,6 +223,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, @@ -818,6 +822,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 @@ -1616,6 +1625,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 fac5706f03..a8e15a6a10 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -397,6 +397,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, @@ -611,6 +616,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 @@ -889,6 +900,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 @@ -1045,6 +1078,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 41a92fafe9..a000ec7045 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 993cc6553f..e31d489e14 100644 --- a/src/lib/components/admin/Settings/Documents.svelte +++ b/src/lib/components/admin/Settings/Documents.svelte @@ -136,6 +136,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 === '' @@ -192,6 +200,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 !== ''), @@ -1058,6 +1070,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 147f84220a..107053a50b 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -533,7 +533,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 ( @@ -568,11 +576,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 {