mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat: 配置导出/导入支持 LDAP 和 OAuth Provider
- 新增配置格式 v2.1,支持导出/导入 LDAP 配置和 OAuth Providers - 导出时解密敏感字段(bind_password, client_secret, provider_ops credentials) - 导入时加密敏感字段后存储 - 前端支持预览和显示 LDAP/OAuth 导入结果
This commit is contained in:
@@ -1,11 +1,46 @@
|
|||||||
import apiClient from './client'
|
import apiClient from './client'
|
||||||
|
|
||||||
|
// LDAP 配置导出结构
|
||||||
|
export interface LDAPConfigExport {
|
||||||
|
server_url: string
|
||||||
|
bind_dn: string
|
||||||
|
bind_password?: string
|
||||||
|
base_dn: string
|
||||||
|
user_search_filter?: string
|
||||||
|
username_attr?: string
|
||||||
|
email_attr?: string
|
||||||
|
display_name_attr?: string
|
||||||
|
is_enabled?: boolean
|
||||||
|
is_exclusive?: boolean
|
||||||
|
use_starttls?: boolean
|
||||||
|
connect_timeout?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// OAuth Provider 导出结构
|
||||||
|
export interface OAuthProviderExport {
|
||||||
|
provider_type: string
|
||||||
|
display_name: string
|
||||||
|
client_id: string
|
||||||
|
client_secret?: string
|
||||||
|
authorization_url_override?: string | null
|
||||||
|
token_url_override?: string | null
|
||||||
|
userinfo_url_override?: string | null
|
||||||
|
scopes?: string[] | null
|
||||||
|
redirect_uri: string
|
||||||
|
frontend_callback_url: string
|
||||||
|
attribute_mapping?: any
|
||||||
|
extra_config?: any
|
||||||
|
is_enabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
// 配置导出数据结构
|
// 配置导出数据结构
|
||||||
export interface ConfigExportData {
|
export interface ConfigExportData {
|
||||||
version: string
|
version: string
|
||||||
exported_at: string
|
exported_at: string
|
||||||
global_models: GlobalModelExport[]
|
global_models: GlobalModelExport[]
|
||||||
providers: ProviderExport[]
|
providers: ProviderExport[]
|
||||||
|
ldap_config?: LDAPConfigExport | null
|
||||||
|
oauth_providers?: OAuthProviderExport[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户导出数据结构
|
// 用户导出数据结构
|
||||||
@@ -254,6 +289,8 @@ export interface ConfigImportResponse {
|
|||||||
endpoints: { created: number; updated: number; skipped: number }
|
endpoints: { created: number; updated: number; skipped: number }
|
||||||
keys: { created: number; updated: number; skipped: number }
|
keys: { created: number; updated: number; skipped: number }
|
||||||
models: { created: number; updated: number; skipped: number }
|
models: { created: number; updated: number; skipped: number }
|
||||||
|
ldap?: { created: number; updated: number; skipped: number }
|
||||||
|
oauth?: { created: number; updated: number; skipped: number }
|
||||||
errors: string[]
|
errors: string[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -538,6 +538,10 @@
|
|||||||
<li>
|
<li>
|
||||||
API Keys: {{ importPreview.providers?.reduce((sum: number, p: any) => sum + (p.api_keys?.length || 0), 0) }} 个
|
API Keys: {{ importPreview.providers?.reduce((sum: number, p: any) => sum + (p.api_keys?.length || 0), 0) }} 个
|
||||||
</li>
|
</li>
|
||||||
|
<li v-if="importPreview.ldap_config">LDAP 配置: 1 个</li>
|
||||||
|
<li v-if="importPreview.oauth_providers?.length">
|
||||||
|
OAuth Providers: {{ importPreview.oauth_providers.length }} 个
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -655,6 +659,26 @@
|
|||||||
跳过: {{ importResult.stats.models.skipped }}
|
跳过: {{ importResult.stats.models.skipped }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="importResult.stats.ldap">
|
||||||
|
<p class="font-medium">
|
||||||
|
LDAP 配置
|
||||||
|
</p>
|
||||||
|
<p class="text-muted-foreground">
|
||||||
|
创建: {{ importResult.stats.ldap.created }},
|
||||||
|
更新: {{ importResult.stats.ldap.updated }},
|
||||||
|
跳过: {{ importResult.stats.ldap.skipped }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="importResult.stats.oauth">
|
||||||
|
<p class="font-medium">
|
||||||
|
OAuth Providers
|
||||||
|
</p>
|
||||||
|
<p class="text-muted-foreground">
|
||||||
|
创建: {{ importResult.stats.oauth.created }},
|
||||||
|
更新: {{ importResult.stats.oauth.updated }},
|
||||||
|
跳过: {{ importResult.stats.oauth.skipped }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -1293,8 +1317,8 @@ function handleConfigFileSelect(event: Event) {
|
|||||||
const content = e.target?.result as string
|
const content = e.target?.result as string
|
||||||
const data = JSON.parse(content) as ConfigExportData
|
const data = JSON.parse(content) as ConfigExportData
|
||||||
|
|
||||||
// 验证版本
|
// 验证版本(支持 2.0 和 2.1)
|
||||||
if (data.version !== '2.0') {
|
if (!['2.0', '2.1'].includes(data.version)) {
|
||||||
error(`不支持的配置版本: ${data.version}`)
|
error(`不支持的配置版本: ${data.version}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ from sqlalchemy.orm import Session
|
|||||||
from src.api.base.admin_adapter import AdminApiAdapter
|
from src.api.base.admin_adapter import AdminApiAdapter
|
||||||
from src.api.base.pipeline import ApiRequestPipeline
|
from src.api.base.pipeline import ApiRequestPipeline
|
||||||
from src.core.exceptions import InvalidRequestException, NotFoundException, translate_pydantic_error
|
from src.core.exceptions import InvalidRequestException, NotFoundException, translate_pydantic_error
|
||||||
|
from src.core.logger import logger
|
||||||
from src.database import get_db
|
from src.database import get_db
|
||||||
from src.models.api import SystemSettingsRequest, SystemSettingsResponse
|
from src.models.api import SystemSettingsRequest, SystemSettingsResponse
|
||||||
from src.models.database import ApiKey, Provider, Usage, User
|
from src.models.database import ApiKey, Provider, Usage, User
|
||||||
@@ -731,6 +733,38 @@ class AdminGetApiFormatsAdapter(AdminApiAdapter):
|
|||||||
|
|
||||||
|
|
||||||
class AdminExportConfigAdapter(AdminApiAdapter):
|
class AdminExportConfigAdapter(AdminApiAdapter):
|
||||||
|
"""导出提供商和模型配置"""
|
||||||
|
|
||||||
|
# Provider Ops 中需要解密的敏感字段
|
||||||
|
SENSITIVE_CREDENTIALS = {
|
||||||
|
"api_key", "password", "session_token", "session_cookie",
|
||||||
|
"token_cookie", "auth_cookie", "cookie_string", "cookie"
|
||||||
|
}
|
||||||
|
|
||||||
|
def _decrypt_provider_config(self, config: dict, crypto_service) -> dict:
|
||||||
|
"""解密 Provider config 中的 provider_ops credentials"""
|
||||||
|
if not config:
|
||||||
|
return config
|
||||||
|
|
||||||
|
decrypted_config = copy.deepcopy(config)
|
||||||
|
|
||||||
|
# 解密 provider_ops.connector.credentials 中的敏感字段
|
||||||
|
provider_ops = decrypted_config.get("provider_ops")
|
||||||
|
if provider_ops and isinstance(provider_ops, dict):
|
||||||
|
connector = provider_ops.get("connector")
|
||||||
|
if connector and isinstance(connector, dict):
|
||||||
|
credentials = connector.get("credentials")
|
||||||
|
if credentials and isinstance(credentials, dict):
|
||||||
|
for field in self.SENSITIVE_CREDENTIALS:
|
||||||
|
if field in credentials and isinstance(credentials[field], str):
|
||||||
|
try:
|
||||||
|
credentials[field] = crypto_service.decrypt(credentials[field])
|
||||||
|
except Exception as e:
|
||||||
|
# 解密失败保持原值(可能本来就是明文)
|
||||||
|
logger.debug(f"解密 provider_ops credential '{field}' 失败: {e}")
|
||||||
|
|
||||||
|
return decrypted_config
|
||||||
|
|
||||||
async def handle(self, context): # type: ignore[override]
|
async def handle(self, context): # type: ignore[override]
|
||||||
"""导出提供商和模型配置(解密数据)"""
|
"""导出提供商和模型配置(解密数据)"""
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
@@ -841,6 +875,11 @@ class AdminExportConfigAdapter(AdminApiAdapter):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 解密 Provider config 中的 credentials
|
||||||
|
decrypted_provider_config = self._decrypt_provider_config(
|
||||||
|
provider.config, crypto_service
|
||||||
|
)
|
||||||
|
|
||||||
providers_data.append(
|
providers_data.append(
|
||||||
{
|
{
|
||||||
"name": provider.name,
|
"name": provider.name,
|
||||||
@@ -854,18 +893,77 @@ class AdminExportConfigAdapter(AdminApiAdapter):
|
|||||||
"concurrent_limit": provider.concurrent_limit,
|
"concurrent_limit": provider.concurrent_limit,
|
||||||
"max_retries": provider.max_retries,
|
"max_retries": provider.max_retries,
|
||||||
"proxy": provider.proxy,
|
"proxy": provider.proxy,
|
||||||
"config": provider.config,
|
"config": decrypted_provider_config,
|
||||||
"endpoints": endpoints_data,
|
"endpoints": endpoints_data,
|
||||||
"api_keys": keys_data,
|
"api_keys": keys_data,
|
||||||
"models": models_data,
|
"models": models_data,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 导出 LDAP 配置
|
||||||
|
from src.models.database import LDAPConfig
|
||||||
|
ldap_config = db.query(LDAPConfig).first()
|
||||||
|
ldap_data = None
|
||||||
|
if ldap_config:
|
||||||
|
# 解密绑定密码
|
||||||
|
bind_password = ""
|
||||||
|
if ldap_config.bind_password_encrypted:
|
||||||
|
try:
|
||||||
|
bind_password = crypto_service.decrypt(ldap_config.bind_password_encrypted)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"解密 LDAP bind_password 失败: {e}")
|
||||||
|
|
||||||
|
ldap_data = {
|
||||||
|
"server_url": ldap_config.server_url,
|
||||||
|
"bind_dn": ldap_config.bind_dn,
|
||||||
|
"bind_password": bind_password,
|
||||||
|
"base_dn": ldap_config.base_dn,
|
||||||
|
"user_search_filter": ldap_config.user_search_filter,
|
||||||
|
"username_attr": ldap_config.username_attr,
|
||||||
|
"email_attr": ldap_config.email_attr,
|
||||||
|
"display_name_attr": ldap_config.display_name_attr,
|
||||||
|
"is_enabled": ldap_config.is_enabled,
|
||||||
|
"is_exclusive": ldap_config.is_exclusive,
|
||||||
|
"use_starttls": ldap_config.use_starttls,
|
||||||
|
"connect_timeout": ldap_config.connect_timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 导出 OAuth Providers 配置
|
||||||
|
from src.models.database import OAuthProvider
|
||||||
|
oauth_providers = db.query(OAuthProvider).all()
|
||||||
|
oauth_data = []
|
||||||
|
for oauth in oauth_providers:
|
||||||
|
# 解密 client secret
|
||||||
|
client_secret = ""
|
||||||
|
if oauth.client_secret_encrypted:
|
||||||
|
try:
|
||||||
|
client_secret = crypto_service.decrypt(oauth.client_secret_encrypted)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"解密 OAuth '{oauth.provider_type}' client_secret 失败: {e}")
|
||||||
|
|
||||||
|
oauth_data.append({
|
||||||
|
"provider_type": oauth.provider_type,
|
||||||
|
"display_name": oauth.display_name,
|
||||||
|
"client_id": oauth.client_id,
|
||||||
|
"client_secret": client_secret,
|
||||||
|
"authorization_url_override": oauth.authorization_url_override,
|
||||||
|
"token_url_override": oauth.token_url_override,
|
||||||
|
"userinfo_url_override": oauth.userinfo_url_override,
|
||||||
|
"scopes": oauth.scopes,
|
||||||
|
"redirect_uri": oauth.redirect_uri,
|
||||||
|
"frontend_callback_url": oauth.frontend_callback_url,
|
||||||
|
"attribute_mapping": oauth.attribute_mapping,
|
||||||
|
"extra_config": oauth.extra_config,
|
||||||
|
"is_enabled": oauth.is_enabled,
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"version": "2.0",
|
"version": "2.1",
|
||||||
"exported_at": datetime.now(timezone.utc).isoformat(),
|
"exported_at": datetime.now(timezone.utc).isoformat(),
|
||||||
"global_models": global_models_data,
|
"global_models": global_models_data,
|
||||||
"providers": providers_data,
|
"providers": providers_data,
|
||||||
|
"ldap_config": ldap_data,
|
||||||
|
"oauth_providers": oauth_data,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -873,6 +971,36 @@ MAX_IMPORT_SIZE = 10 * 1024 * 1024 # 10MB
|
|||||||
|
|
||||||
|
|
||||||
class AdminImportConfigAdapter(AdminApiAdapter):
|
class AdminImportConfigAdapter(AdminApiAdapter):
|
||||||
|
"""导入提供商和模型配置"""
|
||||||
|
|
||||||
|
# Provider Ops 中需要加密的敏感字段
|
||||||
|
SENSITIVE_CREDENTIALS = {
|
||||||
|
"api_key", "password", "session_token", "session_cookie",
|
||||||
|
"token_cookie", "auth_cookie", "cookie_string", "cookie"
|
||||||
|
}
|
||||||
|
|
||||||
|
def _encrypt_provider_config(self, config: dict, crypto_service) -> dict:
|
||||||
|
"""加密 Provider config 中的 provider_ops credentials"""
|
||||||
|
if not config:
|
||||||
|
return config
|
||||||
|
|
||||||
|
encrypted_config = copy.deepcopy(config)
|
||||||
|
|
||||||
|
# 加密 provider_ops.connector.credentials 中的敏感字段
|
||||||
|
provider_ops = encrypted_config.get("provider_ops")
|
||||||
|
if provider_ops and isinstance(provider_ops, dict):
|
||||||
|
connector = provider_ops.get("connector")
|
||||||
|
if connector and isinstance(connector, dict):
|
||||||
|
credentials = connector.get("credentials")
|
||||||
|
if credentials and isinstance(credentials, dict):
|
||||||
|
for field in self.SENSITIVE_CREDENTIALS:
|
||||||
|
if field in credentials and isinstance(credentials[field], str):
|
||||||
|
value = credentials[field]
|
||||||
|
if value: # 只加密非空值
|
||||||
|
credentials[field] = crypto_service.encrypt(value)
|
||||||
|
|
||||||
|
return encrypted_config
|
||||||
|
|
||||||
async def handle(self, context): # type: ignore[override]
|
async def handle(self, context): # type: ignore[override]
|
||||||
"""导入提供商和模型配置"""
|
"""导入提供商和模型配置"""
|
||||||
import uuid
|
import uuid
|
||||||
@@ -889,15 +1017,17 @@ class AdminImportConfigAdapter(AdminApiAdapter):
|
|||||||
db = context.db
|
db = context.db
|
||||||
payload = context.ensure_json_body()
|
payload = context.ensure_json_body()
|
||||||
|
|
||||||
# 验证配置版本
|
# 验证配置版本(支持 2.0 和 2.1)
|
||||||
version = payload.get("version")
|
version = payload.get("version")
|
||||||
if version != "2.0":
|
if version not in ("2.0", "2.1"):
|
||||||
raise InvalidRequestException(f"不支持的配置版本: {version}")
|
raise InvalidRequestException(f"不支持的配置版本: {version}")
|
||||||
|
|
||||||
# 获取导入选项
|
# 获取导入选项
|
||||||
merge_mode = payload.get("merge_mode", "skip") # skip, overwrite, error
|
merge_mode = payload.get("merge_mode", "skip") # skip, overwrite, error
|
||||||
global_models_data = payload.get("global_models", [])
|
global_models_data = payload.get("global_models", [])
|
||||||
providers_data = payload.get("providers", [])
|
providers_data = payload.get("providers", [])
|
||||||
|
ldap_data = payload.get("ldap_config") # 2.1 新增
|
||||||
|
oauth_data = payload.get("oauth_providers", []) # 2.1 新增
|
||||||
|
|
||||||
stats = {
|
stats = {
|
||||||
"global_models": {"created": 0, "updated": 0, "skipped": 0},
|
"global_models": {"created": 0, "updated": 0, "skipped": 0},
|
||||||
@@ -905,6 +1035,8 @@ class AdminImportConfigAdapter(AdminApiAdapter):
|
|||||||
"endpoints": {"created": 0, "updated": 0, "skipped": 0},
|
"endpoints": {"created": 0, "updated": 0, "skipped": 0},
|
||||||
"keys": {"created": 0, "updated": 0, "skipped": 0},
|
"keys": {"created": 0, "updated": 0, "skipped": 0},
|
||||||
"models": {"created": 0, "updated": 0, "skipped": 0},
|
"models": {"created": 0, "updated": 0, "skipped": 0},
|
||||||
|
"ldap": {"created": 0, "updated": 0, "skipped": 0},
|
||||||
|
"oauth": {"created": 0, "updated": 0, "skipped": 0},
|
||||||
"errors": [],
|
"errors": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1006,7 +1138,10 @@ class AdminImportConfigAdapter(AdminApiAdapter):
|
|||||||
"max_retries", existing_provider.max_retries
|
"max_retries", existing_provider.max_retries
|
||||||
)
|
)
|
||||||
existing_provider.proxy = prov_data.get("proxy", existing_provider.proxy)
|
existing_provider.proxy = prov_data.get("proxy", existing_provider.proxy)
|
||||||
existing_provider.config = prov_data.get("config")
|
# 加密 provider_ops credentials 后再保存
|
||||||
|
existing_provider.config = self._encrypt_provider_config(
|
||||||
|
prov_data.get("config"), crypto_service
|
||||||
|
)
|
||||||
existing_provider.updated_at = datetime.now(timezone.utc)
|
existing_provider.updated_at = datetime.now(timezone.utc)
|
||||||
stats["providers"]["updated"] += 1
|
stats["providers"]["updated"] += 1
|
||||||
else:
|
else:
|
||||||
@@ -1015,6 +1150,11 @@ class AdminImportConfigAdapter(AdminApiAdapter):
|
|||||||
if prov_data.get("billing_type"):
|
if prov_data.get("billing_type"):
|
||||||
billing_type = ProviderBillingType(prov_data["billing_type"])
|
billing_type = ProviderBillingType(prov_data["billing_type"])
|
||||||
|
|
||||||
|
# 加密 provider_ops credentials 后再保存
|
||||||
|
encrypted_config = self._encrypt_provider_config(
|
||||||
|
prov_data.get("config"), crypto_service
|
||||||
|
)
|
||||||
|
|
||||||
new_provider = Provider(
|
new_provider = Provider(
|
||||||
id=str(uuid.uuid4()),
|
id=str(uuid.uuid4()),
|
||||||
name=prov_data["name"],
|
name=prov_data["name"],
|
||||||
@@ -1028,7 +1168,7 @@ class AdminImportConfigAdapter(AdminApiAdapter):
|
|||||||
concurrent_limit=prov_data.get("concurrent_limit"),
|
concurrent_limit=prov_data.get("concurrent_limit"),
|
||||||
max_retries=prov_data.get("max_retries"),
|
max_retries=prov_data.get("max_retries"),
|
||||||
proxy=prov_data.get("proxy"),
|
proxy=prov_data.get("proxy"),
|
||||||
config=prov_data.get("config"),
|
config=encrypted_config,
|
||||||
)
|
)
|
||||||
db.add(new_provider)
|
db.add(new_provider)
|
||||||
db.flush()
|
db.flush()
|
||||||
@@ -1280,6 +1420,162 @@ class AdminImportConfigAdapter(AdminApiAdapter):
|
|||||||
db.add(new_model)
|
db.add(new_model)
|
||||||
stats["models"]["created"] += 1
|
stats["models"]["created"] += 1
|
||||||
|
|
||||||
|
# 导入 LDAP 配置(2.1 新增)
|
||||||
|
if ldap_data:
|
||||||
|
from src.models.database import LDAPConfig
|
||||||
|
|
||||||
|
# 校验必填字段
|
||||||
|
required_ldap_fields = ["server_url", "bind_dn", "base_dn"]
|
||||||
|
missing = [f for f in required_ldap_fields if not ldap_data.get(f)]
|
||||||
|
if missing:
|
||||||
|
raise InvalidRequestException(f"LDAP 配置缺少必填字段: {', '.join(missing)}")
|
||||||
|
|
||||||
|
existing_ldap = db.query(LDAPConfig).first()
|
||||||
|
|
||||||
|
if existing_ldap:
|
||||||
|
if merge_mode == "skip":
|
||||||
|
stats["ldap"]["skipped"] += 1
|
||||||
|
elif merge_mode == "error":
|
||||||
|
raise InvalidRequestException("LDAP 配置已存在")
|
||||||
|
elif merge_mode == "overwrite":
|
||||||
|
existing_ldap.server_url = ldap_data.get("server_url", existing_ldap.server_url)
|
||||||
|
existing_ldap.bind_dn = ldap_data.get("bind_dn", existing_ldap.bind_dn)
|
||||||
|
# 加密绑定密码
|
||||||
|
if ldap_data.get("bind_password"):
|
||||||
|
existing_ldap.bind_password_encrypted = crypto_service.encrypt(
|
||||||
|
ldap_data["bind_password"]
|
||||||
|
)
|
||||||
|
existing_ldap.base_dn = ldap_data.get("base_dn", existing_ldap.base_dn)
|
||||||
|
existing_ldap.user_search_filter = ldap_data.get(
|
||||||
|
"user_search_filter", existing_ldap.user_search_filter
|
||||||
|
)
|
||||||
|
existing_ldap.username_attr = ldap_data.get(
|
||||||
|
"username_attr", existing_ldap.username_attr
|
||||||
|
)
|
||||||
|
existing_ldap.email_attr = ldap_data.get("email_attr", existing_ldap.email_attr)
|
||||||
|
existing_ldap.display_name_attr = ldap_data.get(
|
||||||
|
"display_name_attr", existing_ldap.display_name_attr
|
||||||
|
)
|
||||||
|
existing_ldap.is_enabled = ldap_data.get("is_enabled", existing_ldap.is_enabled)
|
||||||
|
existing_ldap.is_exclusive = ldap_data.get(
|
||||||
|
"is_exclusive", existing_ldap.is_exclusive
|
||||||
|
)
|
||||||
|
existing_ldap.use_starttls = ldap_data.get(
|
||||||
|
"use_starttls", existing_ldap.use_starttls
|
||||||
|
)
|
||||||
|
existing_ldap.connect_timeout = ldap_data.get(
|
||||||
|
"connect_timeout", existing_ldap.connect_timeout
|
||||||
|
)
|
||||||
|
existing_ldap.updated_at = datetime.now(timezone.utc)
|
||||||
|
stats["ldap"]["updated"] += 1
|
||||||
|
else:
|
||||||
|
# 创建新的 LDAP 配置
|
||||||
|
new_ldap = LDAPConfig(
|
||||||
|
server_url=ldap_data["server_url"],
|
||||||
|
bind_dn=ldap_data["bind_dn"],
|
||||||
|
bind_password_encrypted=(
|
||||||
|
crypto_service.encrypt(ldap_data["bind_password"])
|
||||||
|
if ldap_data.get("bind_password") else None
|
||||||
|
),
|
||||||
|
base_dn=ldap_data["base_dn"],
|
||||||
|
user_search_filter=ldap_data.get("user_search_filter", "(uid={username})"),
|
||||||
|
username_attr=ldap_data.get("username_attr", "uid"),
|
||||||
|
email_attr=ldap_data.get("email_attr", "mail"),
|
||||||
|
display_name_attr=ldap_data.get("display_name_attr", "cn"),
|
||||||
|
is_enabled=ldap_data.get("is_enabled", False),
|
||||||
|
is_exclusive=ldap_data.get("is_exclusive", False),
|
||||||
|
use_starttls=ldap_data.get("use_starttls", False),
|
||||||
|
connect_timeout=ldap_data.get("connect_timeout", 10),
|
||||||
|
)
|
||||||
|
db.add(new_ldap)
|
||||||
|
stats["ldap"]["created"] += 1
|
||||||
|
|
||||||
|
# 导入 OAuth Providers(2.1 新增)
|
||||||
|
if oauth_data:
|
||||||
|
from src.models.database import OAuthProvider
|
||||||
|
for oauth_item in oauth_data:
|
||||||
|
provider_type = oauth_item.get("provider_type")
|
||||||
|
if not provider_type:
|
||||||
|
stats["errors"].append("跳过无 provider_type 的 OAuth 配置")
|
||||||
|
continue
|
||||||
|
|
||||||
|
existing_oauth = (
|
||||||
|
db.query(OAuthProvider)
|
||||||
|
.filter(OAuthProvider.provider_type == provider_type)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
if existing_oauth:
|
||||||
|
if merge_mode == "skip":
|
||||||
|
stats["oauth"]["skipped"] += 1
|
||||||
|
elif merge_mode == "error":
|
||||||
|
raise InvalidRequestException(
|
||||||
|
f"OAuth Provider '{provider_type}' 已存在"
|
||||||
|
)
|
||||||
|
elif merge_mode == "overwrite":
|
||||||
|
existing_oauth.display_name = oauth_item.get(
|
||||||
|
"display_name", existing_oauth.display_name
|
||||||
|
)
|
||||||
|
existing_oauth.client_id = oauth_item.get(
|
||||||
|
"client_id", existing_oauth.client_id
|
||||||
|
)
|
||||||
|
# 加密 client_secret
|
||||||
|
if oauth_item.get("client_secret"):
|
||||||
|
existing_oauth.client_secret_encrypted = crypto_service.encrypt(
|
||||||
|
oauth_item["client_secret"]
|
||||||
|
)
|
||||||
|
existing_oauth.authorization_url_override = oauth_item.get(
|
||||||
|
"authorization_url_override"
|
||||||
|
)
|
||||||
|
existing_oauth.token_url_override = oauth_item.get("token_url_override")
|
||||||
|
existing_oauth.userinfo_url_override = oauth_item.get(
|
||||||
|
"userinfo_url_override"
|
||||||
|
)
|
||||||
|
existing_oauth.scopes = oauth_item.get("scopes")
|
||||||
|
existing_oauth.redirect_uri = oauth_item.get(
|
||||||
|
"redirect_uri", existing_oauth.redirect_uri
|
||||||
|
)
|
||||||
|
existing_oauth.frontend_callback_url = oauth_item.get(
|
||||||
|
"frontend_callback_url", existing_oauth.frontend_callback_url
|
||||||
|
)
|
||||||
|
existing_oauth.attribute_mapping = oauth_item.get("attribute_mapping")
|
||||||
|
existing_oauth.extra_config = oauth_item.get("extra_config")
|
||||||
|
existing_oauth.is_enabled = oauth_item.get(
|
||||||
|
"is_enabled", existing_oauth.is_enabled
|
||||||
|
)
|
||||||
|
existing_oauth.updated_at = datetime.now(timezone.utc)
|
||||||
|
stats["oauth"]["updated"] += 1
|
||||||
|
else:
|
||||||
|
# 创建新的 OAuth Provider - 校验必填字段
|
||||||
|
required_oauth_fields = ["client_id", "redirect_uri", "frontend_callback_url"]
|
||||||
|
missing = [f for f in required_oauth_fields if not oauth_item.get(f)]
|
||||||
|
if missing:
|
||||||
|
stats["errors"].append(
|
||||||
|
f"OAuth Provider '{provider_type}' 缺少必填字段: {', '.join(missing)}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_oauth = OAuthProvider(
|
||||||
|
provider_type=provider_type,
|
||||||
|
display_name=oauth_item.get("display_name", provider_type),
|
||||||
|
client_id=oauth_item["client_id"],
|
||||||
|
client_secret_encrypted=(
|
||||||
|
crypto_service.encrypt(oauth_item["client_secret"])
|
||||||
|
if oauth_item.get("client_secret") else None
|
||||||
|
),
|
||||||
|
authorization_url_override=oauth_item.get("authorization_url_override"),
|
||||||
|
token_url_override=oauth_item.get("token_url_override"),
|
||||||
|
userinfo_url_override=oauth_item.get("userinfo_url_override"),
|
||||||
|
scopes=oauth_item.get("scopes"),
|
||||||
|
redirect_uri=oauth_item["redirect_uri"],
|
||||||
|
frontend_callback_url=oauth_item["frontend_callback_url"],
|
||||||
|
attribute_mapping=oauth_item.get("attribute_mapping"),
|
||||||
|
extra_config=oauth_item.get("extra_config"),
|
||||||
|
is_enabled=oauth_item.get("is_enabled", False),
|
||||||
|
)
|
||||||
|
db.add(new_oauth)
|
||||||
|
stats["oauth"]["created"] += 1
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
# 失效缓存
|
# 失效缓存
|
||||||
|
|||||||
Reference in New Issue
Block a user