open-webui/backend/open_webui/models/user_model_credentials.py

331 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
用户私有模型凭据管理 - 数据模型层
功能:
1. 允许用户保存自己的 LLM API 凭据OpenAI/Claude/自建等)
2. 提供凭据的增删改查操作
3. 自动掩码 API Key 防止泄露
4. 支持多 ProviderOpenAI/Ollama/兼容接口等)
核心设计:
- 每个用户可以添加多个私有模型配置
- 存储 base_url、api_key 等信息
- 返回给前端时自动掩码 API Key仅显示前 4 位和后 4 位)
- 所有操作都强制用户隔离(通过 user_id 校验)
建表参考SQLite 默认路径 backend/data/webui.db
cd /home/gaofeng/open-webui-next/backend
source .venv/bin/activate
sqlite3 data/webui.db "
CREATE TABLE IF NOT EXISTS user_model_credential (
id TEXT PRIMARY KEY,
user_id TEXT,
name TEXT,
model_id TEXT NOT NULL,
base_url TEXT,
api_key TEXT NOT NULL,
config TEXT,
created_at INTEGER,
updated_at INTEGER
);
CREATE INDEX IF NOT EXISTS ix_user_model_credential_user_id ON user_model_credential (user_id);
"
"""
import time
from typing import Optional
from open_webui.internal.db import Base, JSONField, get_db
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, String, Text
####################
# DB Schema - 数据库表定义
####################
class UserModelCredential(Base):
"""
用户私有模型凭据表 - 存储每个用户自己配置的 LLM API 凭据
字段说明:
- id: 凭据唯一标识(纳秒时间戳)
- user_id: 所属用户 ID外键索引
- name: 用户可读名称(如 "我的 GPT-4"
- model_id: 上游模型名称/ID"gpt-4", "claude-3-opus"
- base_url: API 基础地址(如 "https://api.openai.com/v1"
- api_key: API 密钥(明文存储,返回时掩码)
- config: 扩展配置JSON可存 organization、headers 等)
- created_at/updated_at: 创建/更新时间戳
"""
__tablename__ = "user_model_credential"
id = Column(String, primary_key=True)
user_id = Column(String, index=True)
name = Column(String, nullable=True) # 用户可读名称
model_id = Column(String, nullable=False) # 上游模型名/ID
base_url = Column(Text, nullable=True)
api_key = Column(Text, nullable=False)
config = Column(JSONField, nullable=True) # 预留自定义字段如org、headers等
created_at = Column(BigInteger)
updated_at = Column(BigInteger)
class UserModelCredentialModel(BaseModel):
"""
用户私有模型凭据数据模型 - 内部使用的完整数据模型
用途:数据库操作和内部逻辑使用的完整模型(包含明文 api_key
注意:不应直接返回给前端,应使用 UserModelCredentialResponse
"""
id: str
user_id: str
name: Optional[str] = None
model_id: str
base_url: Optional[str] = None
api_key: str # 明文 API Key仅内部使用
config: Optional[dict] = None
created_at: int
updated_at: int
model_config = ConfigDict(from_attributes=True)
class UserModelCredentialResponse(BaseModel):
"""
用户私有模型凭据响应模型 - 返回给前端的安全模型
用途API 响应时使用,将 api_key 替换为掩码后的 api_key_masked
安全设计:前端永远看不到完整的 API Key只能看到前 4 位和后 4 位
"""
id: str
user_id: str
name: Optional[str] = None
model_id: str
base_url: Optional[str] = None
api_key_masked: str # 掩码后的 API Key如 "sk-1***5678"
config: Optional[dict] = None
created_at: int
updated_at: int
model_config = ConfigDict(from_attributes=True)
class UserModelCredentialForm(BaseModel):
"""
用户私有模型凭据表单模型 - 前端提交的数据格式
用途:创建/更新凭据时前端提交的数据
字段:不包含 id、user_id、时间戳由后端自动生成
"""
name: Optional[str] = None
model_id: str
base_url: Optional[str] = None
api_key: str # 前端提交时是明文,后端直接存储
config: Optional[dict] = None
def mask_api_key(key: str) -> str:
"""
掩码 API Key - 保护敏感信息
规则:
- 空字符串:返回空
- 长度 ≤ 8全部替换为 *
- 长度 > 8保留前 4 位和后 4 位,中间用 * 填充
示例:
- "sk-1234567890abcdef""sk-1**********cdef"
- "short""*****"
- """"
"""
if not key:
return ""
if len(key) <= 8:
return "*" * len(key)
return f"{key[:4]}{'*' * (len(key) - 8)}{key[-4:]}"
class UserModelCredentialsTable:
"""
用户私有模型凭据数据访问层 - CRUD 操作
功能:
1. 增insert_new_credential - 创建新凭据
2. 删delete_credential_by_id_and_user_id - 删除凭据(用户隔离)
3. 改update_credential_by_id_and_user_id - 更新凭据(用户隔离)
4. 查get_credentials_by_user_id - 获取用户的所有凭据
get_credential_by_id_and_user_id - 获取单个凭据(用户隔离)
安全设计:
- 所有操作都强制校验 user_id防止跨用户访问
- 更新/删除/查询单条时,必须同时匹配 id 和 user_id
"""
def insert_new_credential(
self, user_id: str, form: UserModelCredentialForm
) -> Optional[UserModelCredentialModel]:
"""
创建新的用户私有模型凭据
流程:
1. 生成唯一 ID纳秒时间戳确保唯一性
2. 关联当前用户 ID
3. 保存表单数据model_id、base_url、api_key 等)
4. 设置创建时间和更新时间
5. 持久化到数据库
Args:
user_id: 当前用户 ID
form: 前端提交的凭据表单数据
Returns:
UserModelCredentialModel: 创建成功的凭据对象
"""
with get_db() as db:
now = int(time.time())
# 使用纳秒时间戳生成唯一 ID
cred = UserModelCredential(
**{
"id": str(time.time_ns()),
"user_id": user_id,
"name": form.name,
"model_id": form.model_id,
"base_url": form.base_url,
"api_key": form.api_key, # 明文存储
"config": form.config,
"created_at": now,
"updated_at": now,
}
)
db.add(cred)
db.commit()
db.refresh(cred)
return UserModelCredentialModel.model_validate(cred)
def update_credential_by_id_and_user_id(
self, cred_id: str, user_id: str, form: UserModelCredentialForm
) -> Optional[UserModelCredentialModel]:
"""
更新用户私有模型凭据
安全校验:
- 凭据必须存在
- 凭据必须属于当前用户user_id 匹配)
- 不满足条件返回 None
更新策略:
- 仅更新前端提交的字段exclude_none=True
- 自动更新 updated_at 时间戳
- created_at 不会被修改
Args:
cred_id: 凭据 ID
user_id: 当前用户 ID
form: 更新的数据
Returns:
UserModelCredentialModel: 更新后的凭据对象,权限不足返回 None
"""
with get_db() as db:
# === 1. 权限校验:凭据必须存在且属于当前用户 ===
cred = db.get(UserModelCredential, cred_id)
if not cred or cred.user_id != user_id:
return None
# === 2. 更新字段:仅更新提交的非空字段 ===
for field, value in form.model_dump(exclude_none=True).items():
setattr(cred, field, value)
cred.updated_at = int(time.time())
# === 3. 持久化 ===
db.commit()
db.refresh(cred)
return UserModelCredentialModel.model_validate(cred)
def delete_credential_by_id_and_user_id(
self, cred_id: str, user_id: str
) -> bool:
"""
删除用户私有模型凭据
安全校验:
- 凭据必须存在
- 凭据必须属于当前用户user_id 匹配)
- 不满足条件返回 False
Args:
cred_id: 凭据 ID
user_id: 当前用户 ID
Returns:
bool: 删除成功返回 True权限不足或不存在返回 False
"""
with get_db() as db:
# === 1. 权限校验:凭据必须存在且属于当前用户 ===
cred = db.get(UserModelCredential, cred_id)
if not cred or cred.user_id != user_id:
return False
# === 2. 删除凭据 ===
db.delete(cred)
db.commit()
return True
def get_credentials_by_user_id(
self, user_id: str
) -> list[UserModelCredentialModel]:
"""
获取用户的所有私有模型凭据
用途:前端模型选择器展示用户的所有私有模型
返回:用户创建的所有凭据列表(包含明文 api_key
Args:
user_id: 用户 ID
Returns:
list[UserModelCredentialModel]: 用户的所有凭据列表,无数据返回空列表
"""
with get_db() as db:
# 查询该用户的所有凭据
creds = (
db.query(UserModelCredential).filter_by(user_id=user_id).all()
)
return (
[UserModelCredentialModel.model_validate(c) for c in creds]
if creds
else []
)
def get_credential_by_id_and_user_id(
self, cred_id: str, user_id: str
) -> Optional[UserModelCredentialModel]:
"""
获取单个用户私有模型凭据
安全校验:
- 凭据必须存在
- 凭据必须属于当前用户user_id 匹配)
- 不满足条件返回 None
用途:查看/编辑凭据详情时使用
Args:
cred_id: 凭据 ID
user_id: 当前用户 ID
Returns:
UserModelCredentialModel: 凭据对象,权限不足返回 None
"""
with get_db() as db:
# === 1. 权限校验:凭据必须存在且属于当前用户 ===
cred = db.get(UserModelCredential, cred_id)
if not cred or cred.user_id != user_id:
return None
return UserModelCredentialModel.model_validate(cred)
UserModelCredentials = UserModelCredentialsTable()