瀏覽代碼

feat: support for configuring private api key use

Antti Pyykkönen 5 月之前
父節點
當前提交
979e6e5a79

+ 7 - 1
backend/open_webui/apps/webui/routers/auths.py

@@ -18,9 +18,10 @@ from open_webui.apps.webui.models.auths import (
     UserResponse,
     UserResponse,
 )
 )
 from open_webui.apps.webui.models.users import Users
 from open_webui.apps.webui.models.users import Users
-from open_webui.config import WEBUI_AUTH
+from open_webui.config import ENABLE_API_KEY_AUTH
 from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
 from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
 from open_webui.env import (
 from open_webui.env import (
+    WEBUI_AUTH,
     WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
     WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
     WEBUI_AUTH_TRUSTED_NAME_HEADER,
     WEBUI_AUTH_TRUSTED_NAME_HEADER,
     WEBUI_SESSION_COOKIE_SAME_SITE,
     WEBUI_SESSION_COOKIE_SAME_SITE,
@@ -734,6 +735,11 @@ async def update_ldap_config(
 # create api key
 # create api key
 @router.post("/api_key", response_model=ApiKey)
 @router.post("/api_key", response_model=ApiKey)
 async def create_api_key_(user=Depends(get_current_user)):
 async def create_api_key_(user=Depends(get_current_user)):
+    if not ENABLE_API_KEY_AUTH:
+        raise HTTPException(
+            status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.API_KEY_CREATION_NOT_ALLOWED
+        )
+
     api_key = create_api_key()
     api_key = create_api_key()
     success = Users.update_user_api_key_by_id(user.id, api_key)
     success = Users.update_user_api_key_by_id(user.id, api_key)
     if success:
     if success:

+ 4 - 0
backend/open_webui/config.py

@@ -265,6 +265,10 @@ class AppConfig:
 # WEBUI_AUTH (Required for security)
 # WEBUI_AUTH (Required for security)
 ####################################
 ####################################
 
 
+ENABLE_API_KEY_AUTH = (
+    os.environ.get("ENABLE_API_KEY_AUTH", "True").lower() == "true"
+)
+
 JWT_EXPIRES_IN = PersistentConfig(
 JWT_EXPIRES_IN = PersistentConfig(
     "JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "-1")
     "JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "-1")
 )
 )

+ 2 - 0
backend/open_webui/constants.py

@@ -62,6 +62,7 @@ class ERROR_MESSAGES(str, Enum):
     NOT_FOUND = "We could not find what you're looking for :/"
     NOT_FOUND = "We could not find what you're looking for :/"
     USER_NOT_FOUND = "We could not find what you're looking for :/"
     USER_NOT_FOUND = "We could not find what you're looking for :/"
     API_KEY_NOT_FOUND = "Oops! It looks like there's a hiccup. The API key is missing. Please make sure to provide a valid API key to access this feature."
     API_KEY_NOT_FOUND = "Oops! It looks like there's a hiccup. The API key is missing. Please make sure to provide a valid API key to access this feature."
+    API_KEY_NOT_ALLOWED = "Use of API key is not enabled in the environment."
 
 
     MALICIOUS = "Unusual activities detected, please try again in a few minutes."
     MALICIOUS = "Unusual activities detected, please try again in a few minutes."
 
 
@@ -75,6 +76,7 @@ class ERROR_MESSAGES(str, Enum):
     OPENAI_NOT_FOUND = lambda name="": "OpenAI API was not found"
     OPENAI_NOT_FOUND = lambda name="": "OpenAI API was not found"
     OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama"
     OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama"
     CREATE_API_KEY_ERROR = "Oops! Something went wrong while creating your API key. Please try again later. If the issue persists, contact support for assistance."
     CREATE_API_KEY_ERROR = "Oops! Something went wrong while creating your API key. Please try again later. If the issue persists, contact support for assistance."
+    API_KEY_CREATION_NOT_ALLOWED = "API key creation is not allowed in the environment."
 
 
     EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding."
     EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding."
 
 

+ 2 - 0
backend/open_webui/main.py

@@ -74,6 +74,7 @@ from open_webui.config import (
     ENABLE_ADMIN_EXPORT,
     ENABLE_ADMIN_EXPORT,
     ENABLE_OLLAMA_API,
     ENABLE_OLLAMA_API,
     ENABLE_OPENAI_API,
     ENABLE_OPENAI_API,
+    ENABLE_API_KEY_AUTH,
     ENABLE_TAGS_GENERATION,
     ENABLE_TAGS_GENERATION,
     ENV,
     ENV,
     FRONTEND_BUILD_DIR,
     FRONTEND_BUILD_DIR,
@@ -2427,6 +2428,7 @@ async def get_app_config(request: Request):
             "auth": WEBUI_AUTH,
             "auth": WEBUI_AUTH,
             "auth_trusted_header": bool(webui_app.state.AUTH_TRUSTED_EMAIL_HEADER),
             "auth_trusted_header": bool(webui_app.state.AUTH_TRUSTED_EMAIL_HEADER),
             "enable_ldap": webui_app.state.config.ENABLE_LDAP,
             "enable_ldap": webui_app.state.config.ENABLE_LDAP,
+            "enable_api_key_auth": ENABLE_API_KEY_AUTH,
             "enable_signup": webui_app.state.config.ENABLE_SIGNUP,
             "enable_signup": webui_app.state.config.ENABLE_SIGNUP,
             "enable_login_form": webui_app.state.config.ENABLE_LOGIN_FORM,
             "enable_login_form": webui_app.state.config.ENABLE_LOGIN_FORM,
             **(
             **(

+ 9 - 2
backend/open_webui/utils/utils.py

@@ -5,13 +5,11 @@ import jwt
 from datetime import UTC, datetime, timedelta
 from datetime import UTC, datetime, timedelta
 from typing import Optional, Union, List, Dict
 from typing import Optional, Union, List, Dict
 
 
-
 from open_webui.apps.webui.models.users import Users
 from open_webui.apps.webui.models.users import Users
 
 
 from open_webui.constants import ERROR_MESSAGES
 from open_webui.constants import ERROR_MESSAGES
 from open_webui.env import WEBUI_SECRET_KEY
 from open_webui.env import WEBUI_SECRET_KEY
 
 
-
 from fastapi import Depends, HTTPException, Request, Response, status
 from fastapi import Depends, HTTPException, Request, Response, status
 from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
 from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
 from passlib.context import CryptContext
 from passlib.context import CryptContext
@@ -75,10 +73,15 @@ def get_http_authorization_cred(auth_header: str):
     except Exception:
     except Exception:
         raise ValueError(ERROR_MESSAGES.INVALID_TOKEN)
         raise ValueError(ERROR_MESSAGES.INVALID_TOKEN)
 
 
+def get_api_key_auth_config():
+    from open_webui.config import ENABLE_API_KEY_AUTH
+    return ENABLE_API_KEY_AUTH
+
 
 
 def get_current_user(
 def get_current_user(
     request: Request,
     request: Request,
     auth_token: HTTPAuthorizationCredentials = Depends(bearer_security),
     auth_token: HTTPAuthorizationCredentials = Depends(bearer_security),
+    api_key_auth_enabled: bool = Depends(get_api_key_auth_config)
 ):
 ):
     token = None
     token = None
 
 
@@ -93,6 +96,10 @@ def get_current_user(
 
 
     # auth by api key
     # auth by api key
     if token.startswith("sk-"):
     if token.startswith("sk-"):
+        if not api_key_auth_enabled:
+            raise HTTPException(
+                status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.API_KEY_NOT_ALLOWED
+            )
         return get_current_user_by_api_key(token)
         return get_current_user_by_api_key(token)
 
 
     # auth by jwt token
     # auth by jwt token

+ 86 - 78
src/lib/components/chat/Settings/Account.svelte

@@ -2,7 +2,7 @@
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
 	import { onMount, getContext } from 'svelte';
 	import { onMount, getContext } from 'svelte';
 
 
-	import { user } from '$lib/stores';
+	import { user, config } from '$lib/stores';
 	import { updateUserProfile, createAPIKey, getAPIKey } from '$lib/apis/auths';
 	import { updateUserProfile, createAPIKey, getAPIKey } from '$lib/apis/auths';
 
 
 	import UpdatePassword from './Account/UpdatePassword.svelte';
 	import UpdatePassword from './Account/UpdatePassword.svelte';
@@ -27,6 +27,8 @@
 	let APIKey = '';
 	let APIKey = '';
 	let APIKeyCopied = false;
 	let APIKeyCopied = false;
 
 
+	$: enableApiKeyAuth = $config?.features.enable_api_key_auth ?? true;
+
 	let profileImageInputElement: HTMLInputElement;
 	let profileImageInputElement: HTMLInputElement;
 
 
 	const submitHandler = async () => {
 	const submitHandler = async () => {
@@ -306,90 +308,96 @@
 						<div class="self-center text-xs font-medium">{$i18n.t('API Key')}</div>
 						<div class="self-center text-xs font-medium">{$i18n.t('API Key')}</div>
 					</div>
 					</div>
 
 
-					<div class="flex mt-2">
-						{#if APIKey}
-							<SensitiveInput value={APIKey} readOnly={true} />
-
-							<button
-								class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg"
-								on:click={() => {
-									copyToClipboard(APIKey);
-									APIKeyCopied = true;
-									setTimeout(() => {
-										APIKeyCopied = false;
-									}, 2000);
-								}}
-							>
-								{#if APIKeyCopied}
-									<svg
-										xmlns="http://www.w3.org/2000/svg"
-										viewBox="0 0 20 20"
-										fill="currentColor"
-										class="w-4 h-4"
-									>
-										<path
-											fill-rule="evenodd"
-											d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
-											clip-rule="evenodd"
-										/>
-									</svg>
-								{:else}
-									<svg
-										xmlns="http://www.w3.org/2000/svg"
-										viewBox="0 0 16 16"
-										fill="currentColor"
-										class="w-4 h-4"
-									>
-										<path
-											fill-rule="evenodd"
-											d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z"
-											clip-rule="evenodd"
-										/>
-										<path
-											fill-rule="evenodd"
-											d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z"
-											clip-rule="evenodd"
-										/>
-									</svg>
-								{/if}
-							</button>
+					{#if !enableApiKeyAuth}
+						<div class="mt-2 p-2 bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-lg">
+							{$i18n.t('Private API keys are disabled in this environment')}
+						</div>
+					{:else}
+						<div class="flex mt-2">
+							{#if APIKey}
+								<SensitiveInput value={APIKey} readOnly={true} />
 
 
-							<Tooltip content={$i18n.t('Create new key')}>
 								<button
 								<button
-									class=" px-1.5 py-1 dark:hover:bg-gray-850transition rounded-lg"
+									class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg"
 									on:click={() => {
 									on:click={() => {
-										createAPIKeyHandler();
+										copyToClipboard(APIKey);
+										APIKeyCopied = true;
+										setTimeout(() => {
+											APIKeyCopied = false;
+										}, 2000);
 									}}
 									}}
 								>
 								>
-									<svg
-										xmlns="http://www.w3.org/2000/svg"
-										fill="none"
-										viewBox="0 0 24 24"
-										stroke-width="2"
-										stroke="currentColor"
-										class="size-4"
-									>
-										<path
-											stroke-linecap="round"
-											stroke-linejoin="round"
-											d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
-										/>
-									</svg>
+									{#if APIKeyCopied}
+										<svg
+											xmlns="http://www.w3.org/2000/svg"
+											viewBox="0 0 20 20"
+											fill="currentColor"
+											class="w-4 h-4"
+										>
+											<path
+												fill-rule="evenodd"
+												d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
+												clip-rule="evenodd"
+											/>
+										</svg>
+									{:else}
+										<svg
+											xmlns="http://www.w3.org/2000/svg"
+											viewBox="0 0 16 16"
+											fill="currentColor"
+											class="w-4 h-4"
+										>
+											<path
+												fill-rule="evenodd"
+												d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z"
+												clip-rule="evenodd"
+											/>
+											<path
+												fill-rule="evenodd"
+												d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z"
+												clip-rule="evenodd"
+											/>
+										</svg>
+									{/if}
 								</button>
 								</button>
-							</Tooltip>
-						{:else}
-							<button
-								class="flex gap-1.5 items-center font-medium px-3.5 py-1.5 rounded-lg bg-gray-100/70 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-850 transition"
-								on:click={() => {
-									createAPIKeyHandler();
-								}}
-							>
-								<Plus strokeWidth="2" className=" size-3.5" />
 
 
-								{$i18n.t('Create new secret key')}</button
-							>
-						{/if}
-					</div>
+								<Tooltip content={$i18n.t('Create new key')}>
+									<button
+										class=" px-1.5 py-1 dark:hover:bg-gray-850transition rounded-lg"
+										on:click={() => {
+											createAPIKeyHandler();
+										}}
+									>
+										<svg
+											xmlns="http://www.w3.org/2000/svg"
+											fill="none"
+											viewBox="0 0 24 24"
+											stroke-width="2"
+											stroke="currentColor"
+											class="size-4"
+										>
+											<path
+												stroke-linecap="round"
+												stroke-linejoin="round"
+												d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
+											/>
+										</svg>
+									</button>
+								</Tooltip>
+							{:else}
+								<button
+									class="flex gap-1.5 items-center font-medium px-3.5 py-1.5 rounded-lg bg-gray-100/70 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-850 transition"
+									on:click={() => {
+										createAPIKeyHandler();
+									}}
+								>
+									<Plus strokeWidth="2" className=" size-3.5" />
+
+									{$i18n.t('Create new secret key')}</button
+								>
+							{/if}
+						</div>
+					{/if}
 				</div>
 				</div>
 			</div>
 			</div>
 		{/if}
 		{/if}

+ 1 - 0
src/lib/stores/index.ts

@@ -172,6 +172,7 @@ type Config = {
 	features: {
 	features: {
 		auth: boolean;
 		auth: boolean;
 		auth_trusted_header: boolean;
 		auth_trusted_header: boolean;
+		enable_api_key_auth: boolean;
 		enable_signup: boolean;
 		enable_signup: boolean;
 		enable_login_form: boolean;
 		enable_login_form: boolean;
 		enable_web_search?: boolean;
 		enable_web_search?: boolean;