From b467dfa3b29adfc72db9a01564ad71d13f1d5be6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=98=AD?= <81747598+lan-yonghui@users.noreply.github.com> Date: Mon, 30 Dec 2024 13:33:49 +0800 Subject: [PATCH] feat: Add expiration time setting for API key (#7584) --- backend/app/dto/setting.go | 2 ++ backend/app/service/setting.go | 4 +++ backend/configs/system.go | 1 + backend/constant/errs.go | 33 ++++++++++--------- backend/i18n/lang/en.yaml | 1 + backend/i18n/lang/ru.yaml | 1 + backend/i18n/lang/zh-Hant.yaml | 1 + backend/i18n/lang/zh.yaml | 1 + backend/init/hook/hook.go | 5 +++ backend/init/migration/migrate.go | 1 + backend/init/migration/migrations/v_1_10.go | 10 ++++++ backend/middleware/session.go | 30 ++++++++++++++--- frontend/src/api/interface/setting.ts | 2 ++ frontend/src/lang/modules/en.ts | 4 +++ frontend/src/lang/modules/ru.ts | 4 +++ frontend/src/lang/modules/tw.ts | 3 ++ frontend/src/lang/modules/zh.ts | 3 ++ .../setting/panel/api-interface/index.vue | 16 +++++++++ frontend/src/views/setting/panel/index.vue | 4 +++ 19 files changed, 106 insertions(+), 20 deletions(-) diff --git a/backend/app/dto/setting.go b/backend/app/dto/setting.go index ac15e7c60..254c83803 100644 --- a/backend/app/dto/setting.go +++ b/backend/app/dto/setting.go @@ -70,6 +70,7 @@ type SettingInfo struct { ApiInterfaceStatus string `json:"apiInterfaceStatus"` ApiKey string `json:"apiKey"` IpWhiteList string `json:"ipWhiteList"` + ApiKeyValidityTime string `json:"apiKeyValidityTime"` } type SettingUpdate struct { @@ -240,4 +241,5 @@ type ApiInterfaceConfig struct { ApiInterfaceStatus string `json:"apiInterfaceStatus"` ApiKey string `json:"apiKey"` IpWhiteList string `json:"ipWhiteList"` + ApiKeyValidityTime string `json:"apiKeyValidityTime"` } diff --git a/backend/app/service/setting.go b/backend/app/service/setting.go index 1e05d458f..0049d0335 100644 --- a/backend/app/service/setting.go +++ b/backend/app/service/setting.go @@ -510,5 +510,9 @@ func (u *SettingService) UpdateApiConfig(req dto.ApiInterfaceConfig) error { return err } global.CONF.System.IpWhiteList = req.IpWhiteList + if err := settingRepo.Update("ApiKeyValidityTime", req.ApiKeyValidityTime); err != nil { + return err + } + global.CONF.System.ApiKeyValidityTime = req.ApiKeyValidityTime return nil } diff --git a/backend/configs/system.go b/backend/configs/system.go index 1f13b2618..c603d8cbb 100644 --- a/backend/configs/system.go +++ b/backend/configs/system.go @@ -30,4 +30,5 @@ type System struct { ApiInterfaceStatus string `mapstructure:"api_interface_status"` ApiKey string `mapstructure:"api_key"` IpWhiteList string `mapstructure:"ip_white_list"` + ApiKeyValidityTime string `mapstructure:"api_key_validity_time"` } diff --git a/backend/constant/errs.go b/backend/constant/errs.go index a76e8b3f3..943d23037 100644 --- a/backend/constant/errs.go +++ b/backend/constant/errs.go @@ -37,22 +37,23 @@ var ( // api var ( - ErrTypeInternalServer = "ErrInternalServer" - ErrTypeInvalidParams = "ErrInvalidParams" - ErrTypeNotLogin = "ErrNotLogin" - ErrTypePasswordExpired = "ErrPasswordExpired" - ErrNameIsExist = "ErrNameIsExist" - ErrDemoEnvironment = "ErrDemoEnvironment" - ErrCmdIllegal = "ErrCmdIllegal" - ErrXpackNotFound = "ErrXpackNotFound" - ErrXpackNotActive = "ErrXpackNotActive" - ErrXpackLost = "ErrXpackLost" - ErrXpackTimeout = "ErrXpackTimeout" - ErrXpackOutOfDate = "ErrXpackOutOfDate" - ErrApiConfigStatusInvalid = "ErrApiConfigStatusInvalid" - ErrApiConfigKeyInvalid = "ErrApiConfigKeyInvalid" - ErrApiConfigIPInvalid = "ErrApiConfigIPInvalid" - ErrApiConfigDisable = "ErrApiConfigDisable" + ErrTypeInternalServer = "ErrInternalServer" + ErrTypeInvalidParams = "ErrInvalidParams" + ErrTypeNotLogin = "ErrNotLogin" + ErrTypePasswordExpired = "ErrPasswordExpired" + ErrNameIsExist = "ErrNameIsExist" + ErrDemoEnvironment = "ErrDemoEnvironment" + ErrCmdIllegal = "ErrCmdIllegal" + ErrXpackNotFound = "ErrXpackNotFound" + ErrXpackNotActive = "ErrXpackNotActive" + ErrXpackLost = "ErrXpackLost" + ErrXpackTimeout = "ErrXpackTimeout" + ErrXpackOutOfDate = "ErrXpackOutOfDate" + ErrApiConfigStatusInvalid = "ErrApiConfigStatusInvalid" + ErrApiConfigKeyInvalid = "ErrApiConfigKeyInvalid" + ErrApiConfigIPInvalid = "ErrApiConfigIPInvalid" + ErrApiConfigDisable = "ErrApiConfigDisable" + ErrApiConfigKeyTimeInvalid = "ErrApiConfigKeyTimeInvalid" ) // app diff --git a/backend/i18n/lang/en.yaml b/backend/i18n/lang/en.yaml index 6b816d6bd..6034c2888 100644 --- a/backend/i18n/lang/en.yaml +++ b/backend/i18n/lang/en.yaml @@ -12,6 +12,7 @@ ErrApiConfigStatusInvalid: "API Interface access prohibited: {{ .detail }}" ErrApiConfigKeyInvalid: "API Interface key error: {{ .detail }}" ErrApiConfigIPInvalid: "API Interface IP is not on the whitelist: {{ .detail }}" ErrApiConfigDisable: "This interface prohibits the use of API Interface calls: {{ .detail }}" +ErrApiConfigKeyTimeInvalid: "API Interface timestamp error: {{ .detail }}" #common ErrNameIsExist: "Name already exists" diff --git a/backend/i18n/lang/ru.yaml b/backend/i18n/lang/ru.yaml index 62eb5fb94..7f5b2e0d7 100644 --- a/backend/i18n/lang/ru.yaml +++ b/backend/i18n/lang/ru.yaml @@ -12,6 +12,7 @@ ErrApiConfigStatusInvalid: "Доступ к API интерфейсу запре ErrApiConfigKeyInvalid: "Ошибка ключа API интерфейса: {{ .detail }}" ErrApiConfigIPInvalid: "IP API интерфейса отсутствует в белом списке: {{ .detail }}" ErrApiConfigDisable: "Этот интерфейс запрещает использование вызовов API интерфейса: {{ .detail }}" +ErrApiConfigKeyTimeInvalid: "Ошибка временной метки интерфейса API: {{ .detail }}" #common ErrNameIsExist: "Имя уже существует" diff --git a/backend/i18n/lang/zh-Hant.yaml b/backend/i18n/lang/zh-Hant.yaml index 5a11509ec..f34185838 100644 --- a/backend/i18n/lang/zh-Hant.yaml +++ b/backend/i18n/lang/zh-Hant.yaml @@ -12,6 +12,7 @@ ErrApiConfigStatusInvalid: "API 介面禁止訪問: {{ .detail }}" ErrApiConfigKeyInvalid: "API 介面金鑰錯誤: {{ .detail }}" ErrApiConfigIPInvalid: "呼叫 API 介面 IP 不在白名單: {{ .detail }}" ErrApiConfigDisable: "此介面禁止使用 API 介面呼叫: {{ .detail }}" +ErrApiConfigKeyTimeInvalid: "API 介面時間戳記錯誤: {{ .detail }}" #common ErrNameIsExist: "名稱已存在" diff --git a/backend/i18n/lang/zh.yaml b/backend/i18n/lang/zh.yaml index 72fabe4c7..9b6d1e777 100644 --- a/backend/i18n/lang/zh.yaml +++ b/backend/i18n/lang/zh.yaml @@ -12,6 +12,7 @@ ErrApiConfigStatusInvalid: "API 接口禁止访问: {{ .detail }}" ErrApiConfigKeyInvalid: "API 接口密钥错误: {{ .detail }}" ErrApiConfigIPInvalid: "调用 API 接口 IP 不在白名单: {{ .detail }}" ErrApiConfigDisable: "此接口禁止使用 API 接口调用: {{ .detail }}" +ErrApiConfigKeyTimeInvalid: "API 接口时间戳错误: {{ .detail }}" #common ErrNameIsExist: "名称已存在" diff --git a/backend/init/hook/hook.go b/backend/init/hook/hook.go index b0334966f..6157a5211 100644 --- a/backend/init/hook/hook.go +++ b/backend/init/hook/hook.go @@ -77,6 +77,11 @@ func Init() { global.LOG.Errorf("load service ip white list from setting failed, err: %v", err) } global.CONF.System.IpWhiteList = ipWhiteListSetting.Value + apiKeyValidityTimeSetting, err := settingRepo.Get(settingRepo.WithByKey("ApiKeyValidityTime")) + if err != nil { + global.LOG.Errorf("load service api key validity time from setting failed, err: %v", err) + } + global.CONF.System.ApiKeyValidityTime = apiKeyValidityTimeSetting.Value } handleUserInfo(global.CONF.System.ChangeUserInfo, settingRepo) diff --git a/backend/init/migration/migrate.go b/backend/init/migration/migrate.go index 52c243fea..0a3660643 100644 --- a/backend/init/migration/migrate.go +++ b/backend/init/migration/migrate.go @@ -98,6 +98,7 @@ func Init() { migrations.AddAutoRestart, migrations.AddApiInterfaceConfig, + migrations.AddApiKeyValidityTime, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/backend/init/migration/migrations/v_1_10.go b/backend/init/migration/migrations/v_1_10.go index e0e97e7bf..c234f0901 100644 --- a/backend/init/migration/migrations/v_1_10.go +++ b/backend/init/migration/migrations/v_1_10.go @@ -350,3 +350,13 @@ var AddApiInterfaceConfig = &gormigrate.Migration{ return nil }, } + +var AddApiKeyValidityTime = &gormigrate.Migration{ + ID: "20241226-add-api-key-validity-time", + Migrate: func(tx *gorm.DB) error { + if err := tx.Create(&model.Setting{Key: "ApiKeyValidityTime", Value: "120"}).Error; err != nil { + return err + } + return nil + }, +} diff --git a/backend/middleware/session.go b/backend/middleware/session.go index 965259c13..51d535edc 100644 --- a/backend/middleware/session.go +++ b/backend/middleware/session.go @@ -3,15 +3,15 @@ package middleware import ( "crypto/md5" "encoding/hex" - "net" - "strconv" - "strings" - "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" "github.com/1Panel-dev/1Panel/backend/app/repo" "github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/global" "github.com/gin-gonic/gin" + "net" + "strconv" + "strings" + "time" ) func SessionAuth() gin.HandlerFunc { @@ -25,6 +25,11 @@ func SessionAuth() gin.HandlerFunc { if panelToken != "" || panelTimestamp != "" { if global.CONF.System.ApiInterfaceStatus == "enable" { clientIP := c.ClientIP() + if !isValid1PanelTimestamp(panelTimestamp) { + helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrApiConfigKeyTimeInvalid, nil) + return + } + if !isValid1PanelToken(panelToken, panelTimestamp) { helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrApiConfigKeyInvalid, nil) return @@ -63,6 +68,23 @@ func SessionAuth() gin.HandlerFunc { } } +func isValid1PanelTimestamp(panelTimestamp string) bool { + apiKeyValidityTime := global.CONF.System.ApiKeyValidityTime + apiTime, err := strconv.Atoi(apiKeyValidityTime) + if err != nil { + return false + } + panelTime, err := strconv.ParseInt(panelTimestamp, 10, 64) + if err != nil { + return false + } + nowTime := time.Now().Unix() + if panelTime > nowTime { + return false + } + return apiTime == 0 || nowTime-panelTime <= int64(apiTime*60) +} + func isValid1PanelToken(panelToken string, panelTimestamp string) bool { system1PanelToken := global.CONF.System.ApiKey if panelToken == GenerateMD5("1panel"+system1PanelToken+panelTimestamp) { diff --git a/frontend/src/api/interface/setting.ts b/frontend/src/api/interface/setting.ts index 88ad99539..cde776038 100644 --- a/frontend/src/api/interface/setting.ts +++ b/frontend/src/api/interface/setting.ts @@ -61,6 +61,7 @@ export namespace Setting { apiInterfaceStatus: string; apiKey: string; ipWhiteList: string; + apiKeyValidityTime: number; } export interface SettingUpdate { key: string; @@ -193,5 +194,6 @@ export namespace Setting { apiInterfaceStatus: string; apiKey: string; ipWhiteList: string; + apiKeyValidityTime: number; } } diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index f6db94189..101c8e794 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -1431,6 +1431,10 @@ 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.', + apiKeyValidityTime: 'Validity period of interface key', + apiKeyValidityTimeEgs: 'Validity period of interface key (in minutes)', + apiKeyValidityTimeHelper: + 'The interface timestamp is valid if its difference from the current timestamp (in minutes) is within the allowed range. A value of 0 disables verification.', apiKeyReset: 'Interface key reset', apiKeyResetHelper: 'the associated key service will become invalid. Please add a new key to the service', confDockerProxy: 'Configure docker proxy', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index 607874d69..212858561 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -1437,6 +1437,10 @@ const message = { ipWhiteList: 'Белый список IP', ipWhiteListEgs: 'По одному в строке. Например,\n172.161.10.111\n172.161.10.0/24', ipWhiteListHelper: 'IP-адреса из белого списка могут получить доступ к API.', + apiKeyValidityTime: 'Срок действия ключа интерфейса', + apiKeyValidityTimeEgs: 'Срок действия ключа интерфейса (в единицах)', + apiKeyValidityTimeHelper: + 'Интерфейс времени метки между текущей меткой времени на момент запроса действителен (в единицах), установлен как 0, не проводится проверка метки времени', apiKeyReset: 'Сброс ключа интерфейса', apiKeyResetHelper: 'связанный ключевой сервис станет недействительным. Пожалуйста, добавьте новый ключ к сервису', diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts index 6789c404c..322043fa5 100644 --- a/frontend/src/lang/modules/tw.ts +++ b/frontend/src/lang/modules/tw.ts @@ -1357,6 +1357,9 @@ const message = { ipWhiteList: 'IP白名單', ipWhiteListEgs: '當存在多個 IP 時,需要換行顯示,例:\n172.16.10.111 \n172.16.10.0/24', ipWhiteListHelper: '必需在 IP 白名單清單中的 IP 才能瀏覽面板 API 介面', + apiKeyValidityTime: '介面金鑰有效期', + apiKeyValidityTimeEgs: '介面金鑰有效期(組織分)', + apiKeyValidityTimeHelper: '介面時間戳記到請求時的當前時間戳之間有效(組織分),設定為0時,不做時間戳記校驗', apiKeyReset: '介面密鑰重設', apiKeyResetHelper: '重設密鑰後,已關聯密鑰服務將失效,請重新新增新密鑰至服務。', confDockerProxy: '配寘 Docker 代理', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 8ac88cb41..4941f8e23 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -1358,6 +1358,9 @@ const message = { ipWhiteList: 'IP 白名单', ipWhiteListEgs: '当存在多个 IP 时,需要换行显示,例: \n172.16.10.111 \n172.16.10.0/24', ipWhiteListHelper: '必需在 IP 白名单列表中的 IP 才能访问面板 API 接口', + apiKeyValidityTime: '接口密钥有效期', + apiKeyValidityTimeEgs: '接口密钥有效期(单位分)', + apiKeyValidityTimeHelper: '接口时间戳到请求时的当前时间戳之间有效(单位分),设置为 0 时,不做时间戳校验', apiKeyReset: '接口密钥重置', apiKeyResetHelper: '重置密钥后,已关联密钥服务将失效,请重新添加新密钥至服务。', confDockerProxy: '配置 Docker 代理', diff --git a/frontend/src/views/setting/panel/api-interface/index.vue b/frontend/src/views/setting/panel/api-interface/index.vue index aa606b38a..cbf2b1e7f 100644 --- a/frontend/src/views/setting/panel/api-interface/index.vue +++ b/frontend/src/views/setting/panel/api-interface/index.vue @@ -65,6 +65,17 @@ /> {{ $t('setting.ipWhiteListHelper') }} + + + + + + {{ $t('setting.apiKeyValidityTimeHelper') }} + + @@ -103,17 +114,20 @@ const form = reactive({ apiKey: '', ipWhiteList: '', apiInterfaceStatus: '', + apiKeyValidityTime: 120, }); const rules = reactive({ ipWhiteList: [Rules.requiredInput, { validator: checkIPs, trigger: 'blur' }], apiKey: [Rules.requiredInput], + apiKeyValidityTime: [Rules.requiredInput, Rules.integerNumberWith0], }); interface DialogProps { apiInterfaceStatus: string; apiKey: string; ipWhiteList: string; + apiKeyValidityTime: number; } function checkIPs(rule: any, value: any, callback: any) { @@ -146,6 +160,7 @@ const acceptParams = async (params: DialogProps): Promise => { }); } form.ipWhiteList = params.ipWhiteList; + form.apiKeyValidityTime = params.apiKeyValidityTime; drawerVisible.value = true; }; @@ -179,6 +194,7 @@ const onSave = async (formEl: FormInstance | undefined) => { apiKey: form.apiKey, ipWhiteList: form.ipWhiteList, apiInterfaceStatus: form.apiInterfaceStatus, + apiKeyValidityTime: form.apiKeyValidityTime, }; loading.value = true; await updateApiConfig(param) diff --git a/frontend/src/views/setting/panel/index.vue b/frontend/src/views/setting/panel/index.vue index c737b2934..3883077a2 100644 --- a/frontend/src/views/setting/panel/index.vue +++ b/frontend/src/views/setting/panel/index.vue @@ -285,6 +285,7 @@ const form = reactive({ apiInterfaceStatus: 'disable', apiKey: '', ipWhiteList: '', + apiKeyValidityTime: 120, proHideMenus: ref(i18n.t('setting.unSetting')), hideMenuList: '', @@ -353,6 +354,7 @@ const search = async () => { form.apiInterfaceStatus = res.data.apiInterfaceStatus; form.apiKey = res.data.apiKey; form.ipWhiteList = res.data.ipWhiteList; + form.apiKeyValidityTime = res.data.apiKeyValidityTime; const json: Node = JSON.parse(res.data.xpackHideMenu); const checkedTitles = getCheckedTitles(json); @@ -428,6 +430,7 @@ const onChangeApiInterfaceStatus = async () => { apiInterfaceStatus: form.apiInterfaceStatus, apiKey: form.apiKey, ipWhiteList: form.ipWhiteList, + apiKeyValidityTime: form.apiKeyValidityTime, }); return; } @@ -442,6 +445,7 @@ const onChangeApiInterfaceStatus = async () => { apiKey: form.apiKey, ipWhiteList: form.ipWhiteList, apiInterfaceStatus: form.apiInterfaceStatus, + apiKeyValidityTime: form.apiKeyValidityTime, }; await updateApiConfig(param) .then(() => {