feat(tenant): optional legacy auto-create API key on tenant creation

Add a tenant.auto_create_api_key setting (env
WEKNORA_TENANT_AUTO_CREATE_API_KEY, default false) that restores the
pre-break-change behaviour of minting a full_access API key when a
tenant is created and returning its plaintext token in the create
response (data.api_key).

- system_setting: register tenant.auto_create_api_key bool spec
- handler: gate auto key creation behind the setting; embed the token
  via tenantWithAPIKey without re-adding a persisted api_key column;
  key-creation failures log a warning and do not fail tenant creation
- client: add optional Tenant.APIKey field populated only on create
- docs/env: document the switch (.env.example, docker-compose,
  tenant.md, regenerated swagger)
This commit is contained in:
wizardchen
2026-07-10 15:46:12 +08:00
committed by lyingbug
parent 38aacc3113
commit b4b63a0c1f
9 changed files with 307 additions and 10 deletions
+8
View File
@@ -527,6 +527,14 @@ WEKNORA_SANDBOX_TIMEOUT=60
# < 0 :彻底关闭限额(不建议在共享部署中使用)
# WEKNORA_TENANT_MAX_OWNED_PER_USER=
# 创建租户时是否自动生成一个全量权限(full_access)的 API Key
# 并在「创建租户」接口的响应中返回其明文 token(字段 api_key)。
# 用于兼容旧版本「创建租户即下发默认 API Key」的行为(破坏性变更的回退开关)。
# - true :创建租户时自动创建 full_access API Key 并返回 token
# - false(默认):不自动创建,需通过 API Key 管理页/接口显式创建
# 也可在系统设置中以 tenant.auto_create_api_key 实时调整,无需重启。
# WEKNORA_TENANT_AUTO_CREATE_API_KEY=false
# APK 镜像源设置(可选)
APK_MIRROR_ARG=mirrors.tencent.com
+5
View File
@@ -42,6 +42,11 @@ type Tenant struct {
StorageQuota int64 `yaml:"storage_quota" json:"storage_quota" gorm:"default:10737418240"`
// Storage used (Bytes)
StorageUsed int64 `yaml:"storage_used" json:"storage_used" gorm:"default:0"`
// APIKey is only populated by CreateTenant when the server has
// tenant.auto_create_api_key (env WEKNORA_TENANT_AUTO_CREATE_API_KEY)
// enabled: it carries the plaintext token of an auto-created full_access
// key. Empty otherwise. Save it on receipt — it is never returned again.
APIKey string `yaml:"api_key,omitempty" json:"api_key,omitempty"`
// Creation timestamp
CreatedAt time.Time `yaml:"created_at" json:"created_at"`
// Last update timestamp
+3
View File
@@ -190,8 +190,11 @@ services:
# - WEKNORA_TENANT_ENABLE_RBAC: 是否启用租户角色强制鉴权(true / false),默认 true
# - WEKNORA_TENANT_MAX_OWNED_PER_USER: 单个非超管自助创建租户的上限
# >0 强制限额;=0 走 handler 默认;<0 关闭限额(不建议共享部署使用)
# - WEKNORA_TENANT_AUTO_CREATE_API_KEY: 创建租户时自动生成 full_access API Key 并在响应中返回 token
# (兼容旧版默认下发 API Key 行为,破坏性变更回退开关;true / false,默认 false
- WEKNORA_TENANT_ENABLE_RBAC=${WEKNORA_TENANT_ENABLE_RBAC:-}
- WEKNORA_TENANT_MAX_OWNED_PER_USER=${WEKNORA_TENANT_MAX_OWNED_PER_USER:-}
- WEKNORA_TENANT_AUTO_CREATE_API_KEY=${WEKNORA_TENANT_AUTO_CREATE_API_KEY:-}
- WEKNORA_ASYNQ_CONCURRENCY=${WEKNORA_ASYNQ_CONCURRENCY:-32}
- WEKNORA_WIKI_ASYNQ_CONCURRENCY=${WEKNORA_WIKI_ASYNQ_CONCURRENCY:-16}
- WEKNORA_MODEL_MAX_CONCURRENCY=${WEKNORA_MODEL_MAX_CONCURRENCY:-32}
+25 -3
View File
@@ -110,7 +110,9 @@ curl --location 'http://localhost:8080/api/v1/tenants/search?keyword=weknora&pag
## POST `/tenants` - 创建新租户
创建一个新的租户。**不会**自动发放 API Key;请在创建后通过 `POST /tenants/:id/api-keys` 创建密钥。从旧版本升级时,原有 `tenants.api_key` 会迁移到 `tenant_api_keys` 表并继续可用,直至被吊销。
创建一个新的租户。**默认不会**自动发放 API Key;请在创建后通过 `POST /tenants/:id/api-keys` 创建密钥。从旧版本升级时,原有 `tenants.api_key` 会迁移到 `tenant_api_keys` 表并继续可用,直至被吊销。
> **兼容旧行为(可选)**:如需恢复旧版「创建租户即下发默认 API Key」的行为,可将系统设置 `tenant.auto_create_api_key` 置为 `true`(或设置环境变量 `WEKNORA_TENANT_AUTO_CREATE_API_KEY=true`)。开启后,创建租户会自动生成一个 `full_access` 权限的 API Key,并在响应体 `data.api_key` 中返回其明文 token(仅本次创建响应返回,请妥善保存)。默认 `false`。
**参数说明(请求体)**:
@@ -146,7 +148,7 @@ curl --location 'http://localhost:8080/api/v1/tenants' \
}'
```
**响应**:
**响应**(默认,不含 API Key:
```json
{
@@ -154,7 +156,6 @@ curl --location 'http://localhost:8080/api/v1/tenants' \
"id": 10000,
"name": "weknora",
"description": "weknora tenants",
"api_key": "sk-aaLRAgvCRJcmtiL2vLMeB1FB5UV0Q-qB7DlTE1pJ9KA93XZG",
"status": "active",
"retriever_engines": {
"engines": [
@@ -179,6 +180,27 @@ curl --location 'http://localhost:8080/api/v1/tenants' \
}
```
当开启 `tenant.auto_create_api_key`(或 `WEKNORA_TENANT_AUTO_CREATE_API_KEY=true`)时,响应的 `data` 中会额外包含 `api_key` 字段(`full_access` 密钥的明文 token):
```json
{
"data": {
"id": 10000,
"name": "weknora",
"description": "weknora tenants",
"api_key": "sk-aaLRAgvCRJcmtiL2vLMeB1FB5UV0Q-qB7DlTE1pJ9KA93XZG",
"status": "active",
"business": "wechat",
"storage_quota": 10737418240,
"storage_used": 0,
"created_at": "2025-08-11T20:37:28.396980093+08:00",
"updated_at": "2025-08-11T20:37:28.396980301+08:00",
"deleted_at": null
},
"success": true
}
```
## GET `/tenants/:id` - 获取指定租户信息
获取指定 ID 的租户详情。只能访问自己所属租户;访问其他租户需要跨租户权限,否则返回 403。
+64 -2
View File
@@ -3956,6 +3956,50 @@ const docTemplate = `{
}
}
},
"/knowledge-bases/{id}/duplicate": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "创建一个只包含设置的新知识库副本,不复制知识、FAQ 内容、分块、索引、Wiki 页面、分享或置顶状态",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"知识库"
],
"summary": "创建知识库副本",
"parameters": [
{
"type": "string",
"description": "源知识库 ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"201": {
"description": "创建后的知识库副本",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "请求参数错误",
"schema": {
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError"
}
}
}
}
},
"/knowledge-bases/{id}/faq/entries": {
"get": {
"security": [
@@ -11494,7 +11538,7 @@ const docTemplate = `{
"Bearer": []
}
],
"description": "创建新的租户。任意已登录用户均可调用以建立自己的新工作区,\n调用方会被自动设为该租户的 Owner。跨租户超管仍可像以前一样\n通过本接口创建任意租户。",
"description": "创建新的租户。任意已登录用户均可调用以建立自己的新工作区,\n调用方会被自动设为该租户的 Owner。跨租户超管仍可像以前一样\n通过本接口创建任意租户。\n当 tenant.auto_create_api_key(或 WEKNORA_TENANT_AUTO_CREATE_API_KEY\n开启时,会自动创建一个 full_access API Key,并在响应体的 data.api_key 字段返回其明文 token。",
"consumes": [
"application/json"
],
@@ -11518,7 +11562,7 @@ const docTemplate = `{
],
"responses": {
"201": {
"description": "创建的租户",
"description": "创建的租户(可选含 api_key",
"schema": {
"type": "object",
"additionalProperties": true
@@ -16198,6 +16242,10 @@ const docTemplate = `{
"interface_type": {
"type": "string"
},
"max_concurrency": {
"description": "MaxConcurrency caps concurrent in-flight BACKGROUND (ingestion /\nenrichment) calls to THIS specific model, keyed by model ID and shared\nacross all replicas. 0 (the default) means \"fall back to the\nprocess-wide model.max_concurrency\". Interactive user-facing calls are\nnever gated. Only chat / vlm / embedding honour this (see limiter.Gate).",
"type": "integer"
},
"parameter_size": {
"description": "Ollama model parameter size (e.g., \"7B\", \"13B\", \"70B\")",
"type": "string"
@@ -20348,9 +20396,23 @@ const docTemplate = `{
"type": "string"
}
},
"mentioned_items": {
"description": "Optional scoped tag mentions",
"type": "array",
"items": {
"$ref": "#/definitions/internal_handler_session.MentionedItemRequest"
}
},
"query": {
"description": "Query text to search for",
"type": "string"
},
"tag_ids": {
"description": "Tag IDs for filtering within a single KB",
"type": "array",
"items": {
"type": "string"
}
}
}
},
+64 -2
View File
@@ -3949,6 +3949,50 @@
}
}
},
"/knowledge-bases/{id}/duplicate": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "创建一个只包含设置的新知识库副本,不复制知识、FAQ 内容、分块、索引、Wiki 页面、分享或置顶状态",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"知识库"
],
"summary": "创建知识库副本",
"parameters": [
{
"type": "string",
"description": "源知识库 ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"201": {
"description": "创建后的知识库副本",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "请求参数错误",
"schema": {
"$ref": "#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError"
}
}
}
}
},
"/knowledge-bases/{id}/faq/entries": {
"get": {
"security": [
@@ -11487,7 +11531,7 @@
"Bearer": []
}
],
"description": "创建新的租户。任意已登录用户均可调用以建立自己的新工作区,\n调用方会被自动设为该租户的 Owner。跨租户超管仍可像以前一样\n通过本接口创建任意租户。",
"description": "创建新的租户。任意已登录用户均可调用以建立自己的新工作区,\n调用方会被自动设为该租户的 Owner。跨租户超管仍可像以前一样\n通过本接口创建任意租户。\n当 tenant.auto_create_api_key(或 WEKNORA_TENANT_AUTO_CREATE_API_KEY\n开启时,会自动创建一个 full_access API Key,并在响应体的 data.api_key 字段返回其明文 token。",
"consumes": [
"application/json"
],
@@ -11511,7 +11555,7 @@
],
"responses": {
"201": {
"description": "创建的租户",
"description": "创建的租户(可选含 api_key",
"schema": {
"type": "object",
"additionalProperties": true
@@ -16191,6 +16235,10 @@
"interface_type": {
"type": "string"
},
"max_concurrency": {
"description": "MaxConcurrency caps concurrent in-flight BACKGROUND (ingestion /\nenrichment) calls to THIS specific model, keyed by model ID and shared\nacross all replicas. 0 (the default) means \"fall back to the\nprocess-wide model.max_concurrency\". Interactive user-facing calls are\nnever gated. Only chat / vlm / embedding honour this (see limiter.Gate).",
"type": "integer"
},
"parameter_size": {
"description": "Ollama model parameter size (e.g., \"7B\", \"13B\", \"70B\")",
"type": "string"
@@ -20341,9 +20389,23 @@
"type": "string"
}
},
"mentioned_items": {
"description": "Optional scoped tag mentions",
"type": "array",
"items": {
"$ref": "#/definitions/internal_handler_session.MentionedItemRequest"
}
},
"query": {
"description": "Query text to search for",
"type": "string"
},
"tag_ids": {
"description": "Tag IDs for filtering within a single KB",
"type": "array",
"items": {
"type": "string"
}
}
}
},
+49 -1
View File
@@ -2061,6 +2061,14 @@ definitions:
type: object
interface_type:
type: string
max_concurrency:
description: |-
MaxConcurrency caps concurrent in-flight BACKGROUND (ingestion /
enrichment) calls to THIS specific model, keyed by model ID and shared
across all replicas. 0 (the default) means "fall back to the
process-wide model.max_concurrency". Interactive user-facing calls are
never gated. Only chat / vlm / embedding honour this (see limiter.Gate).
type: integer
parameter_size:
description: Ollama model parameter size (e.g., "7B", "13B", "70B")
type: string
@@ -5123,9 +5131,19 @@ definitions:
items:
type: string
type: array
mentioned_items:
description: Optional scoped tag mentions
items:
$ref: '#/definitions/internal_handler_session.MentionedItemRequest'
type: array
query:
description: Query text to search for
type: string
tag_ids:
description: Tag IDs for filtering within a single KB
items:
type: string
type: array
required:
- query
type: object
@@ -7609,6 +7627,34 @@ paths:
summary: 更新知识库
tags:
- 知识库
/knowledge-bases/{id}/duplicate:
post:
consumes:
- application/json
description: 创建一个只包含设置的新知识库副本,不复制知识、FAQ 内容、分块、索引、Wiki 页面、分享或置顶状态
parameters:
- description: 源知识库 ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"201":
description: 创建后的知识库副本
schema:
additionalProperties: true
type: object
"400":
description: 请求参数错误
schema:
$ref: '#/definitions/github_com_Tencent_WeKnora_internal_errors.AppError'
security:
- Bearer: []
summary: 创建知识库副本
tags:
- 知识库
/knowledge-bases/{id}/faq/entries:
delete:
consumes:
@@ -12490,6 +12536,8 @@ paths:
创建新的租户。任意已登录用户均可调用以建立自己的新工作区,
调用方会被自动设为该租户的 Owner。跨租户超管仍可像以前一样
通过本接口创建任意租户。
当 tenant.auto_create_api_key(或 WEKNORA_TENANT_AUTO_CREATE_API_KEY
开启时,会自动创建一个 full_access API Key,并在响应体的 data.api_key 字段返回其明文 token。
parameters:
- description: 租户信息
in: body
@@ -12501,7 +12549,7 @@ paths:
- application/json
responses:
"201":
description: 创建的租户
description: 创建的租户(可选含 api_key
schema:
additionalProperties: true
type: object
@@ -166,6 +166,23 @@ var registry = map[string]settingSpec{
"仅在创建时读取,修改后只对之后新建的租户生效,不会回写已存在的租户。" +
"0 或负数表示使用内置默认值 10GB。",
},
// tenant.auto_create_api_key restores the legacy behaviour where creating
// a tenant also minted a full-access API key and returned its plaintext
// token in the create response. Newer versions stopped doing this (keys
// are created explicitly via tenant_api_keys), which is a breaking change
// for integrations that relied on the create response carrying a key.
// Deployments that need the old behaviour set this to true (or the
// WEKNORA_TENANT_AUTO_CREATE_API_KEY env var). Default false keeps the
// current, safer no-implicit-key behaviour. Read at create time only.
"tenant.auto_create_api_key": {
Type: "bool",
EnvName: "WEKNORA_TENANT_AUTO_CREATE_API_KEY",
Default: false,
Category: "tenant",
Description: "创建租户时是否自动生成一个全量权限(full_access)的 API Key,并在创建接口的响应中返回其明文 token。" +
"用于兼容旧版本「创建租户即下发默认 API Key」的行为(属于破坏性变更的回退开关)。" +
"每次创建租户时实时读取,修改后立即生效。默认 false(不自动创建,需通过 API Key 管理显式创建)。",
},
// asynq.concurrency is the asynq worker pool size (parallel in-flight
// tasks). Read once when the asynq server starts — changing it in the
// UI requires a process restart to take effect. Mirrors
+72 -2
View File
@@ -2,6 +2,7 @@ package handler
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
@@ -202,11 +203,13 @@ func (h *TenantHandler) resolveMaxOwnedTenantsPerUser(ctx context.Context) int {
// @Description 创建新的租户。任意已登录用户均可调用以建立自己的新工作区,
// @Description 调用方会被自动设为该租户的 Owner。跨租户超管仍可像以前一样
// @Description 通过本接口创建任意租户。
// @Description 当 tenant.auto_create_api_key(或 WEKNORA_TENANT_AUTO_CREATE_API_KEY
// @Description 开启时,会自动创建一个 full_access API Key,并在响应体的 data.api_key 字段返回其明文 token。
// @Tags 租户管理
// @Accept json
// @Produce json
// @Param request body handler.createTenantRequest true "租户信息"
// @Success 201 {object} map[string]interface{} "创建的租户"
// @Success 201 {object} map[string]interface{} "创建的租户(可选含 api_key"
// @Failure 400 {object} errors.AppError "请求参数错误"
// @Security Bearer
// @Router /tenants [post]
@@ -405,12 +408,79 @@ func (h *TenantHandler) CreateTenant(c *gin.Context) {
createdTenant.ID,
secutils.SanitizeForLog(createdTenant.Name),
)
// data carries the created tenant. When the legacy auto-create-key
// behaviour is enabled we embed the plaintext token as data.api_key so
// the response shape mirrors the pre-break-change behaviour integrations
// relied on.
var data any = createdTenant
// Optional legacy compatibility: mint a full-access API key on tenant
// creation and return its plaintext token, gated by the
// tenant.auto_create_api_key setting (env WEKNORA_TENANT_AUTO_CREATE_API_KEY).
// Default off — modern deployments create keys explicitly via
// tenant_api_keys. Failing to create the convenience key must NOT fail
// the whole tenant creation (the tenant is fully usable without a key);
// we log a warning and return the tenant as usual.
if h.autoCreateTenantAPIKey(ctx) && h.apiKeyService != nil {
result, keyErr := h.apiKeyService.CreateAPIKey(ctx, interfaces.TenantAPIKeyCreateRequest{
TenantID: createdTenant.ID,
Name: "default",
FullAccess: true,
})
if keyErr != nil {
logger.Errorf(ctx,
"Auto-create default API key failed for tenant %d: %v — returning tenant without key",
createdTenant.ID, keyErr)
} else if merged, mErr := tenantWithAPIKey(createdTenant, result.Token); mErr != nil {
// Round-trip failure is unexpected; degrade gracefully by
// returning the tenant without embedding the key rather than
// failing the whole request.
logger.Errorf(ctx, "Failed to embed api_key into tenant response for tenant %d: %v",
createdTenant.ID, mErr)
} else {
data = merged
}
}
c.JSON(http.StatusCreated, gin.H{
"success": true,
"data": createdTenant,
"data": data,
})
}
// tenantWithAPIKey returns the tenant serialized as a map with an extra
// api_key field, so the create response can embed the plaintext token inside
// data (mirroring the pre-break-change shape) without adding a persisted
// api_key column back onto types.Tenant.
func tenantWithAPIKey(tenant *types.Tenant, token string) (map[string]any, error) {
raw, err := json.Marshal(tenant)
if err != nil {
return nil, err
}
m := map[string]any{}
if err := json.Unmarshal(raw, &m); err != nil {
return nil, err
}
m["api_key"] = token
return m, nil
}
// autoCreateTenantAPIKey resolves whether tenant creation should also mint a
// full-access API key (legacy compatibility). 3-tier resolver:
// system_settings DB row > WEKNORA_TENANT_AUTO_CREATE_API_KEY env > false.
func (h *TenantHandler) autoCreateTenantAPIKey(ctx context.Context) bool {
if h.systemSettingSvc == nil {
return false
}
return h.systemSettingSvc.GetBool(
ctx,
"tenant.auto_create_api_key",
"WEKNORA_TENANT_AUTO_CREATE_API_KEY",
false,
)
}
// GetTenant godoc
// @Summary 获取租户详情
// @Description 根据ID获取租户详情