From 563df3da7142e1fcf548c0d28a4691257b714a00 Mon Sep 17 00:00:00 2001 From: ssongliu Date: Wed, 29 Jul 2026 16:48:59 +0800 Subject: [PATCH] fix: support trusted proxies for API allowlist (#13409) --- agent/cmd/server/docs/x-log.json | 14 +- core/app/api/v2/auth.go | 9 +- core/app/auth/api_auth.go | 124 ++++++- core/app/auth/auth.go | 7 + core/app/dto/auth.go | 2 + core/cmd/server/docs/docs.go | 319 +++++++++++++++++- core/cmd/server/docs/swagger.json | 319 +++++++++++++++++- core/cmd/server/docs/x-log.json | 14 +- core/init/migration/migrate.go | 1 + core/init/migration/migrations/init.go | 17 + frontend/src/api/interface/auth.ts | 2 + frontend/src/lang/modules/en.ts | 3 + frontend/src/lang/modules/es-es.ts | 3 + frontend/src/lang/modules/fa.ts | 3 + frontend/src/lang/modules/ja.ts | 3 + frontend/src/lang/modules/ko.ts | 3 + frontend/src/lang/modules/lo.ts | 3 + frontend/src/lang/modules/ms.ts | 3 + frontend/src/lang/modules/pt-br.ts | 3 + frontend/src/lang/modules/ru.ts | 3 + frontend/src/lang/modules/tr.ts | 3 + frontend/src/lang/modules/zh-Hant.ts | 3 + frontend/src/lang/modules/zh.ts | 3 + .../Sidebar/components/user-info/index.vue | 23 +- 24 files changed, 863 insertions(+), 24 deletions(-) diff --git a/agent/cmd/server/docs/x-log.json b/agent/cmd/server/docs/x-log.json index 2d60faeeb..989c6b670 100644 --- a/agent/cmd/server/docs/x-log.json +++ b/agent/cmd/server/docs/x-log.json @@ -816,12 +816,13 @@ }, "/core/auth/api/update": { "bodyKeys": [ - "ipWhiteList" + "ipWhiteList", + "apiTrustedProxies" ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "更新 API 接口配置 =\u003e IP 白名单: [ipWhiteList]", - "formatEN": "update api config =\u003e IP White List: [ipWhiteList]" + "formatZH": "更新 API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]", + "formatEN": "update api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]" }, "/core/auth/expired/reset": { "bodyKeys": [], @@ -1577,7 +1578,8 @@ "/core/enterprise/users/api/update": { "bodyKeys": [ "id", - "ipWhiteList" + "ipWhiteList", + "apiTrustedProxies" ], "paramKeys": [], "beforeFunctions": [ @@ -1590,8 +1592,8 @@ "output_value": "name" } ], - "formatZH": "更新用户 [name] API 接口配置 =\u003e IP 白名单: [ipWhiteList]", - "formatEN": "update user [name] api config =\u003e IP White List: [ipWhiteList]" + "formatZH": "更新用户 [name] API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]", + "formatEN": "update user [name] api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]" }, "/core/enterprise/users/del": { "bodyKeys": [ diff --git a/core/app/api/v2/auth.go b/core/app/api/v2/auth.go index 4f8fb98bd..52b8cdcf7 100644 --- a/core/app/api/v2/auth.go +++ b/core/app/api/v2/auth.go @@ -7,6 +7,7 @@ import ( "path" "github.com/1Panel-dev/1Panel/core/app/api/v2/helper" + appauth "github.com/1Panel-dev/1Panel/core/app/auth" "github.com/1Panel-dev/1Panel/core/app/dto" "github.com/1Panel-dev/1Panel/core/app/model" "github.com/1Panel-dev/1Panel/core/buserr" @@ -435,7 +436,7 @@ func (b *BaseApi) GenerateApiKey(c *gin.Context) { // @Security ApiKeyAuth // @Security Timestamp // @Router /core/auth/api/update [post] -// @x-panel-log {"bodyKeys":["ipWhiteList"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 API 接口配置 => IP 白名单: [ipWhiteList]","formatEN":"update api config => IP White List: [ipWhiteList]"} +// @x-panel-log {"bodyKeys":["ipWhiteList","apiTrustedProxies"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 API 接口配置 => IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]","formatEN":"update api config => IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]"} func (b *BaseApi) UpdateApiConfig(c *gin.Context) { panelToken := c.GetHeader("1Panel-Token") if panelToken != "" { @@ -446,6 +447,12 @@ func (b *BaseApi) UpdateApiConfig(c *gin.Context) { if err := helper.CheckBindAndValidate(&req, c); err != nil { return } + trustedProxies, err := appauth.NormalizeAPITrustedProxies(req.ApiTrustedProxies) + if err != nil { + helper.BadRequest(c, err) + return + } + req.ApiTrustedProxies = trustedProxies if err := xpack.AuthProvider.UpdateApiConfig(c, req); err != nil { helper.InternalServer(c, err) diff --git a/core/app/auth/api_auth.go b/core/app/auth/api_auth.go index 69f1b3920..002f67aa5 100644 --- a/core/app/auth/api_auth.go +++ b/core/app/auth/api_auth.go @@ -6,6 +6,7 @@ import ( "crypto/sha256" "encoding/hex" "errors" + "fmt" "net" "strconv" "strings" @@ -24,6 +25,7 @@ type APIAuthConfig struct { ApiInterfaceStatus string ApiKey string IpWhiteList string + ApiTrustedProxies string ApiKeyValidityTime int } @@ -66,7 +68,7 @@ func APIAuthMiddleware(loadConfig APIAuthConfigLoader, onSuccess APIAuthSuccessH helper.BadAuth(c, "ErrApiConfigKeyInvalid", nil) return } - if !isIPInWhiteList(c.ClientIP(), config.IpWhiteList) { + if !isIPInWhiteList(GetAPIClientIP(c, config.ApiTrustedProxies), config.IpWhiteList) { helper.BadAuth(c, "ErrApiConfigIPInvalid", nil) return } @@ -92,6 +94,9 @@ func LoadAPIAuthConfig(_ *gin.Context) (APIAuthConfig, error) { if config.IpWhiteList, err = settingRepo.GetValueByKey("IpWhiteList"); err != nil { return config, err } + if config.ApiTrustedProxies, err = settingRepo.GetValueByKey("ApiTrustedProxies"); err != nil { + return config, err + } apiValidity, err := settingRepo.GetValueByKey("ApiKeyValidityTime") if err != nil { return config, err @@ -102,6 +107,123 @@ func LoadAPIAuthConfig(_ *gin.Context) (APIAuthConfig, error) { return config, nil } +func GetAPIClientIP(c *gin.Context, trustedProxies string) string { + remoteAddr := common.GetRealClientIP(c) + remoteIP := net.ParseIP(remoteAddr) + if remoteIP == nil { + return remoteAddr + } + + proxies, err := parseAPITrustedProxies(trustedProxies) + if err != nil { + if global.LOG != nil { + global.LOG.Errorf("Failed to parse API trusted proxies: %v", err) + } + return remoteAddr + } + if !isIPInNetworks(remoteIP, proxies) { + return remoteAddr + } + + forwardedFor := strings.Join(c.Request.Header.Values("X-Forwarded-For"), ",") + if strings.TrimSpace(forwardedFor) != "" { + clientIP, ok := clientIPFromForwardedFor(forwardedFor, proxies) + if !ok { + return remoteAddr + } + return clientIP + } + + realIPValue := strings.Join(c.Request.Header.Values("X-Real-IP"), ",") + realIP := net.ParseIP(strings.TrimSpace(realIPValue)) + if realIP == nil { + return remoteAddr + } + return realIP.String() +} + +func NormalizeAPITrustedProxies(value string) (string, error) { + lines := strings.Split(value, "\n") + normalized := make([]string, 0, len(lines)) + for _, line := range lines { + item := strings.TrimSpace(line) + if item == "" { + continue + } + if ip := net.ParseIP(item); ip != nil { + normalized = append(normalized, ip.String()) + continue + } + _, ipNet, err := net.ParseCIDR(item) + if err != nil { + return "", fmt.Errorf("invalid API trusted proxy entry %q: %w", item, err) + } + ones, _ := ipNet.Mask.Size() + if ones == 0 { + return "", fmt.Errorf("invalid API trusted proxy entry %q: unrestricted CIDR is not allowed", item) + } + normalized = append(normalized, ipNet.String()) + } + return strings.Join(normalized, "\n"), nil +} + +func parseAPITrustedProxies(value string) ([]*net.IPNet, error) { + normalized, err := NormalizeAPITrustedProxies(value) + if err != nil { + return nil, err + } + if normalized == "" { + return []*net.IPNet{}, nil + } + + lines := strings.Split(normalized, "\n") + proxies := make([]*net.IPNet, 0, len(lines)) + for _, item := range lines { + if ip := net.ParseIP(item); ip != nil { + bits := 128 + if ip.To4() != nil { + bits = 32 + ip = ip.To4() + } + proxies = append(proxies, &net.IPNet{IP: ip, Mask: net.CIDRMask(bits, bits)}) + continue + } + _, ipNet, err := net.ParseCIDR(item) + if err != nil { + return nil, err + } + proxies = append(proxies, ipNet) + } + return proxies, nil +} + +func clientIPFromForwardedFor(value string, trustedProxies []*net.IPNet) (string, bool) { + items := strings.Split(value, ",") + ips := make([]net.IP, len(items)) + for i, item := range items { + ip := net.ParseIP(strings.TrimSpace(item)) + if ip == nil { + return "", false + } + ips[i] = ip + } + for i := len(ips) - 1; i >= 0; i-- { + if i == 0 || !isIPInNetworks(ips[i], trustedProxies) { + return ips[i].String(), true + } + } + return "", false +} + +func isIPInNetworks(ip net.IP, networks []*net.IPNet) bool { + for _, network := range networks { + if network.Contains(ip) { + return true + } + } + return false +} + func IsValid1PanelTimestamp(panelTimestamp string, apiKeyValidityTime int) bool { apiTime := apiKeyValidityTime if apiTime < 0 { diff --git a/core/app/auth/auth.go b/core/app/auth/auth.go index 21bbc1509..9a00ead27 100644 --- a/core/app/auth/auth.go +++ b/core/app/auth/auth.go @@ -311,6 +311,10 @@ func GenerateApiKey() (string, error) { } func UpdateApiConfig(req dto.ApiInterfaceConfig) error { settingRepo := repo.NewISettingRepo() + trustedProxies, err := NormalizeAPITrustedProxies(req.ApiTrustedProxies) + if err != nil { + return err + } if err := settingRepo.UpdateOrCreate("ApiInterfaceStatus", req.ApiInterfaceStatus); err != nil { return err } @@ -320,6 +324,9 @@ func UpdateApiConfig(req dto.ApiInterfaceConfig) error { if err := settingRepo.UpdateOrCreate("IpWhiteList", req.IpWhiteList); err != nil { return err } + if err := settingRepo.UpdateOrCreate("ApiTrustedProxies", trustedProxies); err != nil { + return err + } if err := settingRepo.UpdateOrCreate("ApiKeyValidityTime", strconv.Itoa(req.ApiKeyValidityTime)); err != nil { return err } diff --git a/core/app/dto/auth.go b/core/app/dto/auth.go index 2e46a743b..5a871849a 100644 --- a/core/app/dto/auth.go +++ b/core/app/dto/auth.go @@ -57,6 +57,7 @@ type ApiInterfaceConfig struct { ApiInterfaceStatus string `json:"apiInterfaceStatus"` ApiKey string `json:"apiKey"` IpWhiteList string `json:"ipWhiteList"` + ApiTrustedProxies string `json:"apiTrustedProxies"` ApiKeyValidityTime int `json:"apiKeyValidityTime"` } @@ -71,6 +72,7 @@ type CurrentUserInfo struct { ApiInterfaceStatus string `json:"apiInterfaceStatus"` ApiKey string `json:"apiKey"` IpWhiteList string `json:"ipWhiteList"` + ApiTrustedProxies string `json:"apiTrustedProxies"` ApiKeyValidityTime int `json:"apiKeyValidityTime"` Role string `json:"role"` diff --git a/core/cmd/server/docs/docs.go b/core/cmd/server/docs/docs.go index dbcce4993..a4dd91a1b 100644 --- a/core/cmd/server/docs/docs.go +++ b/core/cmd/server/docs/docs.go @@ -2120,6 +2120,158 @@ const docTemplate = `{ ] } }, + "/ai/agents/plugins/install": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.AgentPluginMarketInstallReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Install an OpenClaw marketplace plugin", + "tags": [ + "AI" + ] + } + }, + "/ai/agents/plugins/list": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.AgentPluginsReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/dto.AgentPluginItem" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "List OpenClaw plugins", + "tags": [ + "AI" + ] + } + }, + "/ai/agents/plugins/operate": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.AgentPluginOperateReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Operate an OpenClaw plugin", + "tags": [ + "AI" + ] + } + }, + "/ai/agents/plugins/search": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.AgentPluginSearchReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/dto.AgentPluginSearchItem" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Search OpenClaw plugins", + "tags": [ + "AI" + ] + } + }, "/ai/agents/remark": { "post": { "consumes": [ @@ -9257,10 +9409,11 @@ const docTemplate = `{ "x-panel-log": { "BeforeFunctions": [], "bodyKeys": [ - "ipWhiteList" + "ipWhiteList", + "apiTrustedProxies" ], - "formatEN": "update api config =\u003e IP White List: [ipWhiteList]", - "formatZH": "更新 API 接口配置 =\u003e IP 白名单: [ipWhiteList]", + "formatEN": "update api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]", + "formatZH": "更新 API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]", "paramKeys": [] } } @@ -30404,6 +30557,143 @@ const docTemplate = `{ ], "type": "object" }, + "dto.AgentPluginItem": { + "properties": { + "enabled": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "origin": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "type": "object" + }, + "dto.AgentPluginMarketInstallReq": { + "properties": { + "agentId": { + "type": "integer" + }, + "package": { + "maxLength": 200, + "type": "string" + }, + "taskID": { + "type": "string" + }, + "version": { + "maxLength": 100, + "type": "string" + } + }, + "required": [ + "agentId", + "package", + "taskID", + "version" + ], + "type": "object" + }, + "dto.AgentPluginOperateReq": { + "properties": { + "agentId": { + "type": "integer" + }, + "operate": { + "enum": [ + "enable", + "disable", + "update", + "uninstall" + ], + "type": "string" + }, + "pluginId": { + "maxLength": 200, + "type": "string" + }, + "taskID": { + "type": "string" + } + }, + "required": [ + "agentId", + "operate", + "pluginId", + "taskID" + ], + "type": "object" + }, + "dto.AgentPluginSearchItem": { + "properties": { + "categories": { + "items": { + "type": "string" + }, + "type": "array" + }, + "channel": { + "type": "string" + }, + "description": { + "type": "string" + }, + "downloads": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "official": { + "type": "boolean" + }, + "package": { + "type": "string" + }, + "pluginId": { + "type": "string" + }, + "score": { + "type": "number" + }, + "verificationTier": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "type": "object" + }, + "dto.AgentPluginSearchReq": { + "properties": { + "agentId": { + "type": "integer" + }, + "keyword": { + "maxLength": 100, + "type": "string" + }, + "limit": { + "maximum": 100, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "agentId", + "keyword" + ], + "type": "object" + }, "dto.AgentPluginStatus": { "properties": { "currentVersion": { @@ -30473,6 +30763,17 @@ const docTemplate = `{ ], "type": "object" }, + "dto.AgentPluginsReq": { + "properties": { + "agentId": { + "type": "integer" + } + }, + "required": [ + "agentId" + ], + "type": "object" + }, "dto.AgentQQBotBot": { "properties": { "accountId": { @@ -31518,6 +31819,9 @@ const docTemplate = `{ "apiKeyValidityTime": { "type": "integer" }, + "apiTrustedProxies": { + "type": "string" + }, "ipWhiteList": { "type": "string" } @@ -33560,6 +33864,15 @@ const docTemplate = `{ "apiKeyValidityTime": { "type": "integer" }, + "apiTrustedProxies": { + "type": "string" + }, + "authSource": { + "type": "string" + }, + "authSourceStatus": { + "type": "string" + }, "complexitySetting": { "type": "string" }, diff --git a/core/cmd/server/docs/swagger.json b/core/cmd/server/docs/swagger.json index dced38cc2..d814d9cfb 100644 --- a/core/cmd/server/docs/swagger.json +++ b/core/cmd/server/docs/swagger.json @@ -2116,6 +2116,158 @@ ] } }, + "/ai/agents/plugins/install": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.AgentPluginMarketInstallReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Install an OpenClaw marketplace plugin", + "tags": [ + "AI" + ] + } + }, + "/ai/agents/plugins/list": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.AgentPluginsReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/dto.AgentPluginItem" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "List OpenClaw plugins", + "tags": [ + "AI" + ] + } + }, + "/ai/agents/plugins/operate": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.AgentPluginOperateReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Operate an OpenClaw plugin", + "tags": [ + "AI" + ] + } + }, + "/ai/agents/plugins/search": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.AgentPluginSearchReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/dto.AgentPluginSearchItem" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Search OpenClaw plugins", + "tags": [ + "AI" + ] + } + }, "/ai/agents/remark": { "post": { "consumes": [ @@ -9253,10 +9405,11 @@ "x-panel-log": { "BeforeFunctions": [], "bodyKeys": [ - "ipWhiteList" + "ipWhiteList", + "apiTrustedProxies" ], - "formatEN": "update api config =\u003e IP White List: [ipWhiteList]", - "formatZH": "更新 API 接口配置 =\u003e IP 白名单: [ipWhiteList]", + "formatEN": "update api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]", + "formatZH": "更新 API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]", "paramKeys": [] } } @@ -30400,6 +30553,143 @@ ], "type": "object" }, + "dto.AgentPluginItem": { + "properties": { + "enabled": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "origin": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "type": "object" + }, + "dto.AgentPluginMarketInstallReq": { + "properties": { + "agentId": { + "type": "integer" + }, + "package": { + "maxLength": 200, + "type": "string" + }, + "taskID": { + "type": "string" + }, + "version": { + "maxLength": 100, + "type": "string" + } + }, + "required": [ + "agentId", + "package", + "taskID", + "version" + ], + "type": "object" + }, + "dto.AgentPluginOperateReq": { + "properties": { + "agentId": { + "type": "integer" + }, + "operate": { + "enum": [ + "enable", + "disable", + "update", + "uninstall" + ], + "type": "string" + }, + "pluginId": { + "maxLength": 200, + "type": "string" + }, + "taskID": { + "type": "string" + } + }, + "required": [ + "agentId", + "operate", + "pluginId", + "taskID" + ], + "type": "object" + }, + "dto.AgentPluginSearchItem": { + "properties": { + "categories": { + "items": { + "type": "string" + }, + "type": "array" + }, + "channel": { + "type": "string" + }, + "description": { + "type": "string" + }, + "downloads": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "official": { + "type": "boolean" + }, + "package": { + "type": "string" + }, + "pluginId": { + "type": "string" + }, + "score": { + "type": "number" + }, + "verificationTier": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "type": "object" + }, + "dto.AgentPluginSearchReq": { + "properties": { + "agentId": { + "type": "integer" + }, + "keyword": { + "maxLength": 100, + "type": "string" + }, + "limit": { + "maximum": 100, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "agentId", + "keyword" + ], + "type": "object" + }, "dto.AgentPluginStatus": { "properties": { "currentVersion": { @@ -30469,6 +30759,17 @@ ], "type": "object" }, + "dto.AgentPluginsReq": { + "properties": { + "agentId": { + "type": "integer" + } + }, + "required": [ + "agentId" + ], + "type": "object" + }, "dto.AgentQQBotBot": { "properties": { "accountId": { @@ -31514,6 +31815,9 @@ "apiKeyValidityTime": { "type": "integer" }, + "apiTrustedProxies": { + "type": "string" + }, "ipWhiteList": { "type": "string" } @@ -33556,6 +33860,15 @@ "apiKeyValidityTime": { "type": "integer" }, + "apiTrustedProxies": { + "type": "string" + }, + "authSource": { + "type": "string" + }, + "authSourceStatus": { + "type": "string" + }, "complexitySetting": { "type": "string" }, diff --git a/core/cmd/server/docs/x-log.json b/core/cmd/server/docs/x-log.json index 2d60faeeb..989c6b670 100644 --- a/core/cmd/server/docs/x-log.json +++ b/core/cmd/server/docs/x-log.json @@ -816,12 +816,13 @@ }, "/core/auth/api/update": { "bodyKeys": [ - "ipWhiteList" + "ipWhiteList", + "apiTrustedProxies" ], "paramKeys": [], "beforeFunctions": [], - "formatZH": "更新 API 接口配置 =\u003e IP 白名单: [ipWhiteList]", - "formatEN": "update api config =\u003e IP White List: [ipWhiteList]" + "formatZH": "更新 API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]", + "formatEN": "update api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]" }, "/core/auth/expired/reset": { "bodyKeys": [], @@ -1577,7 +1578,8 @@ "/core/enterprise/users/api/update": { "bodyKeys": [ "id", - "ipWhiteList" + "ipWhiteList", + "apiTrustedProxies" ], "paramKeys": [], "beforeFunctions": [ @@ -1590,8 +1592,8 @@ "output_value": "name" } ], - "formatZH": "更新用户 [name] API 接口配置 =\u003e IP 白名单: [ipWhiteList]", - "formatEN": "update user [name] api config =\u003e IP White List: [ipWhiteList]" + "formatZH": "更新用户 [name] API 接口配置 =\u003e IP 白名单: [ipWhiteList], API 可信代理: [apiTrustedProxies]", + "formatEN": "update user [name] api config =\u003e IP Allowlist: [ipWhiteList], API Trusted Proxies: [apiTrustedProxies]" }, "/core/enterprise/users/del": { "bodyKeys": [ diff --git a/core/init/migration/migrate.go b/core/init/migration/migrate.go index 411affc92..31c085c0f 100644 --- a/core/init/migration/migrate.go +++ b/core/init/migration/migrate.go @@ -51,6 +51,7 @@ func Init() { migrations.AddLoginLogUser, migrations.AddAlertAuditUser, migrations.AddMenuAccordionSetting, + migrations.AddAPITrustedProxiesSetting, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/core/init/migration/migrations/init.go b/core/init/migration/migrations/init.go index e54279d8f..d18b1c837 100644 --- a/core/init/migration/migrations/init.go +++ b/core/init/migration/migrations/init.go @@ -183,6 +183,9 @@ var InitSetting = &gormigrate.Migration{ if err := tx.Create(&model.Setting{Key: "IpWhiteList", Value: ""}).Error; err != nil { return err } + if err := tx.Create(&model.Setting{Key: "ApiTrustedProxies", Value: ""}).Error; err != nil { + return err + } if err := tx.Create(&model.Setting{Key: "ApiKeyValidityTime", Value: "120"}).Error; err != nil { return err } @@ -1282,3 +1285,17 @@ var AddMenuAccordionSetting = &gormigrate.Migration{ return nil }, } + +var AddAPITrustedProxiesSetting = &gormigrate.Migration{ + ID: "20260729-add-api-trusted-proxies-setting", + Migrate: func(tx *gorm.DB) error { + var setting model.Setting + if err := tx.Where("key = ?", "ApiTrustedProxies").First(&setting).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return tx.Create(&model.Setting{Key: "ApiTrustedProxies", Value: ""}).Error + } + return err + } + return nil + }, +} diff --git a/frontend/src/api/interface/auth.ts b/frontend/src/api/interface/auth.ts index 73bb14dbd..67f0ae1b5 100644 --- a/frontend/src/api/interface/auth.ts +++ b/frontend/src/api/interface/auth.ts @@ -61,6 +61,7 @@ export namespace Login { apiInterfaceStatus: string; apiKey: string; ipWhiteList: string; + apiTrustedProxies: string; apiKeyValidityTime: number; } export interface AuthInfoUpdate { @@ -86,6 +87,7 @@ export namespace Login { apiInterfaceStatus: string; apiKey: string; ipWhiteList: string; + apiTrustedProxies: string; apiKeyValidityTime: number; } export interface PasswordUpdate { diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index a5ff75a94..f1fae8696 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -2758,6 +2758,9 @@ const message = { ipWhiteList: 'IP allowlist', ipWhiteListEgs: 'One per line. For example,\n172.161.10.111\n172.161.10.0/24', ipWhiteListHelper: 'IPs within the allowlist can access the API,0.0.0.0/0 (all IPv4), ::/0 (all IPv6)', + apiTrustedProxies: 'API trusted proxies', + apiTrustedProxiesHelper: + 'When using a reverse proxy, enter the IP address or network of the proxy server to correctly obtain the client IP. Otherwise, leave this blank. 0.0.0.0/0 and ::/0 are not supported.', apiKeyValidityTime: 'Validity period of interface key', apiKeyValidityTimeEgs: 'Validity period of interface key (in minutes)', apiKeyValidityTimeHelper: diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index b63f98211..30a1d995a 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -2807,6 +2807,9 @@ const message = { ipWhiteListEgs: 'Una por línea. Por ejemplo,\n172.161.10.111\n172.161.10.0/24', ipWhiteListHelper: 'Las IP en la lista blanca pueden acceder a la API, 0.0.0.0/0 (todas IPv4), ::/0 (todas IPv6)', + apiTrustedProxies: 'Proxies de confianza de la API', + apiTrustedProxiesHelper: + 'Al usar un proxy inverso, introduce la IP o la red del servidor proxy para obtener correctamente la IP del cliente. Si no lo usas, deja este campo vacío. No se admiten 0.0.0.0/0 ni ::/0.', apiKeyValidityTime: 'Periodo de validez de la clave de interfaz', apiKeyValidityTimeEgs: 'Periodo de validez de la clave de interfaz (en minutos)', apiKeyValidityTimeHelper: diff --git a/frontend/src/lang/modules/fa.ts b/frontend/src/lang/modules/fa.ts index edb07c590..b5719a420 100644 --- a/frontend/src/lang/modules/fa.ts +++ b/frontend/src/lang/modules/fa.ts @@ -2733,6 +2733,9 @@ const message = { ipWhiteListEgs: 'هر خط یک IP. مثلاً،\n172.161.10.111\n172.161.10.0/24', ipWhiteListHelper: 'IP‌های موجود در لیست سفید می‌توانند به API دسترسی داشته باشند، 0.0.0.0/0 (همه IPv4)، ::/0 (همه IPv6)', + apiTrustedProxies: 'پروکسی‌های قابل اعتماد API', + apiTrustedProxiesHelper: + 'هنگام استفاده از پروکسی معکوس، IP یا شبکه سرور پروکسی را وارد کنید تا IP کلاینت به‌درستی دریافت شود. در صورت عدم استفاده، این بخش را خالی بگذارید. 0.0.0.0/0 و ::/0 پشتیبانی نمی‌شوند.', apiKeyValidityTime: 'مدت اعتبار کلید رابط', apiKeyValidityTimeEgs: 'مدت اعتبار کلید رابط (به دقیقه)', apiKeyValidityTimeHelper: diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 03dde3e6e..7ca51217a 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -2755,6 +2755,9 @@ const message = { ipWhiteList: 'IP AllowList', ipWhiteListEgs: '1行に1つ。たとえば、 n172.161.10.111 n172.161.10.0/24', ipWhiteListHelper: 'AllowList内のIPSはAPIにアクセスできます、0.0.0.0/0(すべての IPv4)、::/0(すべての IPv6)', + apiTrustedProxies: 'API信頼済みプロキシ', + apiTrustedProxiesHelper: + 'リバースプロキシを使用する場合は、クライアントIPを正しく取得するために、プロキシサーバーのIPまたはネットワークを入力してください。使用しない場合は空欄にしてください。0.0.0.0/0と::/0は指定できません。', apiKeyReset: 'インターフェイスキーリセット', apiKeyResetHelper: '関連するキーサービスは無効になります。サービスに新しいキーを追加してください', confDockerProxy: 'Dockerプロキシを構成します', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index fe2fc2092..2284fe18b 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -2706,6 +2706,9 @@ const message = { ipWhiteList: 'IP 허용 목록', ipWhiteListEgs: '한 줄에 하나씩 입력하십시오. 예:\n172.161.10.111\n172.161.10.0/24', ipWhiteListHelper: '허용 목록에 있는 IP만 API 에 접근할 수 있습니다. 0.0.0.0/0(모든 IPv4), ::/0(모든 IPv6)', + apiTrustedProxies: 'API 신뢰 프록시', + apiTrustedProxiesHelper: + '리버스 프록시를 사용하는 경우 클라이언트 IP를 올바르게 가져오려면 프록시 서버의 IP 또는 네트워크를 입력하세요. 사용하지 않는 경우 비워 두세요. 0.0.0.0/0 및 ::/0은 지원되지 않습니다.', apiKeyValidityTime: '인터페이스 키 유효 기간', apiKeyValidityTimeEgs: '인터페이스 키 유효 기간 (분 단위)', apiKeyValidityTimeHelper: diff --git a/frontend/src/lang/modules/lo.ts b/frontend/src/lang/modules/lo.ts index 306ab5bca..5ed6abdfa 100644 --- a/frontend/src/lang/modules/lo.ts +++ b/frontend/src/lang/modules/lo.ts @@ -2696,6 +2696,9 @@ const message = { ipWhiteList: 'IP allowlist', ipWhiteListEgs: 'ໜຶ່ງລາຍການຕໍ່ແຖວ. ຕົວຢ່າງ:\n172.161.10.111\n172.161.10.0/24', ipWhiteListHelper: 'IP ໃນ allowlist ສາມາດເຂົ້າເຖິງ API ໄດ້, 0.0.0.0/0 (ທຸກ IPv4), ::/0 (ທຸກ IPv6)', + apiTrustedProxies: 'ພຣັອກຊີ API ທີ່ເຊື່ອຖື', + apiTrustedProxiesHelper: + 'ເມື່ອໃຊ້ reverse proxy, ໃຫ້ໃສ່ IP ຫຼືເຄືອຂ່າຍຂອງ proxy server ເພື່ອໃຫ້ໄດ້ client IP ຢ່າງຖືກຕ້ອງ. ຖ້າບໍ່ໃຊ້ໃຫ້ປ່ອຍຫວ່າງ. ບໍ່ຮອງຮັບ 0.0.0.0/0 ແລະ ::/0.', apiKeyValidityTime: 'ໄລຍະເວລາທີ່ API key ໃຊ້ງານໄດ້', apiKeyValidityTimeEgs: 'ໄລຍະເວລາທີ່ API key ໃຊ້ງານໄດ້ (ນາທີ)', apiKeyValidityTimeHelper: diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index f9a66c423..6925ec1e1 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -2803,6 +2803,9 @@ const message = { ipWhiteList: 'Senarai putih IP', ipWhiteListEgs: 'Satu per baris. Contoh,\n172.161.10.111\n172.161.10.0/24', ipWhiteListHelper: 'IP dalam senarai putih boleh mengakses API, 0.0.0.0/0 (semua IPv4), ::/0 (semua IPv6)', + apiTrustedProxies: 'Proksi dipercayai API', + apiTrustedProxiesHelper: + 'Apabila menggunakan proksi songsang, masukkan IP atau rangkaian pelayan proksi untuk mendapatkan IP klien dengan betul. Jika tidak digunakan, biarkan kosong. 0.0.0.0/0 dan ::/0 tidak disokong.', apiKeyValidityTime: 'Tempoh sah kunci antara muka', apiKeyValidityTimeEgs: 'Tempoh sah kunci antara muka (dalam minit)', apiKeyValidityTimeHelper: diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 30a91f35e..60bbe4c71 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -2919,6 +2919,9 @@ const message = { ipWhiteListEgs: 'Um por linha. Exemplo: \n172.161.10.111\n172.161.10.0/24', ipWhiteListHelper: 'IPs na lista de permitidos podem acessar a API, 0.0.0.0/0 (todos os IPv4), ::/0 (todos os IPv6)', + apiTrustedProxies: 'Proxies confiáveis da API', + apiTrustedProxiesHelper: + 'Ao usar um proxy reverso, insira o IP ou a rede do servidor proxy para obter corretamente o IP do cliente. Caso contrário, deixe este campo em branco. 0.0.0.0/0 e ::/0 não são suportados.', apiKeyReset: 'Redefinir chave da interface', apiKeyResetHelper: 'O serviço associado à chave se tornará inválido. Por favor, adicione uma nova chave ao serviço', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index 9d5287f0e..7e52bce1d 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -2778,6 +2778,9 @@ const message = { ipWhiteListEgs: 'По одному в строке. Например,\n172.161.10.111\n172.161.10.0/24', ipWhiteListHelper: 'IP-адреса из белого списка могут получить доступ к API, 0.0.0.0/0 (все IPv4), ::/0 (все IPv6)', + apiTrustedProxies: 'Доверенные прокси API', + apiTrustedProxiesHelper: + 'При использовании обратного прокси укажите IP-адрес или сеть прокси-сервера, чтобы корректно определить IP клиента. Если прокси не используется, оставьте поле пустым. 0.0.0.0/0 и ::/0 не поддерживаются.', apiKeyValidityTime: 'Срок действия ключа интерфейса', apiKeyValidityTimeEgs: 'Срок действия ключа интерфейса (в единицах)', apiKeyValidityTimeHelper: diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index f459938b5..b0f96224d 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -2786,6 +2786,9 @@ const message = { ipWhiteList: 'IP izin listesi', ipWhiteListEgs: 'Her satıra bir tane. Örneğin,\n172.161.10.111\n172.161.10.0/24', ipWhiteListHelper: 'İzin listesindeki IP’ler API’ye erişebilir, 0.0.0.0/0 (tüm IPv4), ::/0 (tüm IPv6)', + apiTrustedProxies: 'API güvenilir proxyleri', + apiTrustedProxiesHelper: + 'Ters proxy kullanırken istemci IP adresini doğru şekilde almak için proxy sunucusunun IP adresini veya ağını girin. Kullanmıyorsanız boş bırakın. 0.0.0.0/0 ve ::/0 desteklenmez.', apiKeyValidityTime: 'Arayüz anahtarının geçerlilik süresi', apiKeyValidityTimeEgs: 'Arayüz anahtarının geçerlilik süresi (dakika cinsinden)', apiKeyValidityTimeHelper: diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 0a9ef0bf1..3a0777598 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -2587,6 +2587,9 @@ const message = { ipWhiteListEgs: '當存在多個 IP 時,需要換行顯示,例:\n172.16.10.111 \n172.16.10.0/24', ipWhiteListHelper: '必需在 IP 白名單清單中的 IP 才能存取面板 API 介面,0.0.0.0/0(所有 IPv4),::/0(所有 IPv6)', + apiTrustedProxies: 'API 可信代理', + apiTrustedProxiesHelper: + '使用反向代理時,請填寫代理伺服器的 IP 或網段,以便正確取得用戶端 IP;不使用時請留空。不支援 0.0.0.0/0 和 ::/0。', apiKeyValidityTime: '介面金鑰有效期', apiKeyValidityTimeEgs: '介面金鑰有效期(組織分)', apiKeyValidityTimeHelper: '介面時間戳記到請求時的目前時間戳之間有效(組織分),設定為0時,不做時間戳記校驗', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 4ea2e1959..8ae25d1d1 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -2582,6 +2582,9 @@ const message = { ipWhiteListEgs: '当存在多个 IP 时,需要换行显示,例: \n172.16.10.111 \n172.16.10.0/24', ipWhiteListHelper: '必需在 IP 白名单列表中的 IP 才能访问面板 API 接口,0.0.0.0/0(所有 IPv4),::/0(所有 IPv6)', + apiTrustedProxies: 'API 可信代理', + apiTrustedProxiesHelper: + '使用反向代理时,请填写代理服务器的 IP 或网段,以便正确获取客户端 IP;不使用时请留空。不支持 0.0.0.0/0 和 ::/0。', apiKeyValidityTime: '接口密钥有效期', apiKeyValidityTimeEgs: '接口密钥有效期(单位分)', apiKeyValidityTimeHelper: '接口时间戳到请求时的当前时间戳之间有效(单位分),设置为 0 时,不做时间戳校验', diff --git a/frontend/src/layout/components/Sidebar/components/user-info/index.vue b/frontend/src/layout/components/Sidebar/components/user-info/index.vue index 5db385ae1..ff842b9a3 100644 --- a/frontend/src/layout/components/Sidebar/components/user-info/index.vue +++ b/frontend/src/layout/components/Sidebar/components/user-info/index.vue @@ -356,6 +356,15 @@ /> {{ $t('setting.ipWhiteListHelper') }} + + + {{ $t('setting.apiTrustedProxiesHelper') }} + @@ -446,6 +455,7 @@ const form = reactive({ apiInterfaceStatus: 'Disable', apiKey: '', ipWhiteList: '', + apiTrustedProxies: '', apiKeyValidityTime: 120, }); const mfaForm = reactive({ @@ -512,6 +522,7 @@ const mfaRules = reactive({ }); const apiRules = reactive({ ipWhiteList: [Rules.requiredInput, { validator: checkIPs, trigger: 'blur' }], + apiTrustedProxies: [{ validator: checkIPs, trigger: 'blur' }], apiKey: [Rules.requiredInput], apiKeyValidityTime: [Rules.requiredInput, Rules.integerNumberWith0], }); @@ -523,7 +534,7 @@ const getUserFormFields = () => { } return fields; }; -const apiFormFields = ['apiKey', 'ipWhiteList', 'apiKeyValidityTime']; +const apiFormFields = ['apiKey', 'ipWhiteList', 'apiTrustedProxies', 'apiKeyValidityTime']; const openDrawer = async () => { if (!props.currentUser) { @@ -538,6 +549,7 @@ const syncApiConfig = (currentUser: Login.AuthInfo) => { form.apiInterfaceStatus = currentUser.apiInterfaceStatus || 'Disable'; form.apiKey = currentUser.apiKey; form.ipWhiteList = currentUser.ipWhiteList; + form.apiTrustedProxies = currentUser.apiTrustedProxies || ''; form.apiKeyValidityTime = currentUser.apiKeyValidityTime; savedApiStatus.value = form.apiInterfaceStatus; }; @@ -586,9 +598,10 @@ const resetApiKey = async () => { }; function checkIPs(rule: any, value: any, callback: any) { - if (form.ipWhiteList !== '') { - let addr = form.ipWhiteList.split('\n'); - for (const item of addr) { + if (value !== '') { + let addr = value.split('\n'); + for (const rawItem of addr) { + const item = rawItem.trim(); if (item === '') { continue; } @@ -898,6 +911,7 @@ const onSaveApi = async (formEl: FormInstance | undefined) => { const param = { apiKey: form.apiKey, ipWhiteList: form.ipWhiteList, + apiTrustedProxies: form.apiTrustedProxies, apiInterfaceStatus: form.apiInterfaceStatus, apiKeyValidityTime: form.apiKeyValidityTime, }; @@ -972,6 +986,7 @@ const handleApi = async () => { let param = { apiKey: form.apiKey, ipWhiteList: form.ipWhiteList, + apiTrustedProxies: form.apiTrustedProxies, apiInterfaceStatus: form.apiInterfaceStatus, apiKeyValidityTime: form.apiKeyValidityTime, };