feat: support for configuring private api key use

This commit is contained in:
Antti Pyykkönen 2024-11-19 16:14:52 +02:00
parent 5c4124ebe5
commit 979e6e5a79
7 changed files with 111 additions and 81 deletions

View File

@ -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:

View File

@ -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")
) )

View File

@ -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."

View File

@ -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,
**( **(

View File

@ -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

View File

@ -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 !enableApiKeyAuth}
{#if APIKey} <div class="mt-2 p-2 bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-lg">
<SensitiveInput value={APIKey} readOnly={true} /> {$i18n.t('Private API keys are disabled in this environment')}
</div>
{:else}
<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>
<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={() => {
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>
<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={() => { on:click={() => {
createAPIKeyHandler(); createAPIKeyHandler();
}} }}
> >
<svg <Plus strokeWidth="2" className=" size-3.5" />
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 {$i18n.t('Create new secret key')}</button
> >
{/if} {/if}
</div> </div>
{/if}
</div> </div>
</div> </div>
{/if} {/if}

View File

@ -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;