1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-01-19 00:09:16 +08:00

feat: Add expiration time setting for API key (#7584)

This commit is contained in:
2024-12-30 13:33:49 +08:00 committed by GitHub
parent c13387bc70
commit b467dfa3b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 106 additions and 20 deletions

View File

@ -70,6 +70,7 @@ type SettingInfo struct {
ApiInterfaceStatus string `json:"apiInterfaceStatus"` ApiInterfaceStatus string `json:"apiInterfaceStatus"`
ApiKey string `json:"apiKey"` ApiKey string `json:"apiKey"`
IpWhiteList string `json:"ipWhiteList"` IpWhiteList string `json:"ipWhiteList"`
ApiKeyValidityTime string `json:"apiKeyValidityTime"`
} }
type SettingUpdate struct { type SettingUpdate struct {
@ -240,4 +241,5 @@ type ApiInterfaceConfig struct {
ApiInterfaceStatus string `json:"apiInterfaceStatus"` ApiInterfaceStatus string `json:"apiInterfaceStatus"`
ApiKey string `json:"apiKey"` ApiKey string `json:"apiKey"`
IpWhiteList string `json:"ipWhiteList"` IpWhiteList string `json:"ipWhiteList"`
ApiKeyValidityTime string `json:"apiKeyValidityTime"`
} }

View File

@ -510,5 +510,9 @@ func (u *SettingService) UpdateApiConfig(req dto.ApiInterfaceConfig) error {
return err return err
} }
global.CONF.System.IpWhiteList = req.IpWhiteList 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 return nil
} }

View File

@ -30,4 +30,5 @@ type System struct {
ApiInterfaceStatus string `mapstructure:"api_interface_status"` ApiInterfaceStatus string `mapstructure:"api_interface_status"`
ApiKey string `mapstructure:"api_key"` ApiKey string `mapstructure:"api_key"`
IpWhiteList string `mapstructure:"ip_white_list"` IpWhiteList string `mapstructure:"ip_white_list"`
ApiKeyValidityTime string `mapstructure:"api_key_validity_time"`
} }

View File

@ -37,22 +37,23 @@ var (
// api // api
var ( var (
ErrTypeInternalServer = "ErrInternalServer" ErrTypeInternalServer = "ErrInternalServer"
ErrTypeInvalidParams = "ErrInvalidParams" ErrTypeInvalidParams = "ErrInvalidParams"
ErrTypeNotLogin = "ErrNotLogin" ErrTypeNotLogin = "ErrNotLogin"
ErrTypePasswordExpired = "ErrPasswordExpired" ErrTypePasswordExpired = "ErrPasswordExpired"
ErrNameIsExist = "ErrNameIsExist" ErrNameIsExist = "ErrNameIsExist"
ErrDemoEnvironment = "ErrDemoEnvironment" ErrDemoEnvironment = "ErrDemoEnvironment"
ErrCmdIllegal = "ErrCmdIllegal" ErrCmdIllegal = "ErrCmdIllegal"
ErrXpackNotFound = "ErrXpackNotFound" ErrXpackNotFound = "ErrXpackNotFound"
ErrXpackNotActive = "ErrXpackNotActive" ErrXpackNotActive = "ErrXpackNotActive"
ErrXpackLost = "ErrXpackLost" ErrXpackLost = "ErrXpackLost"
ErrXpackTimeout = "ErrXpackTimeout" ErrXpackTimeout = "ErrXpackTimeout"
ErrXpackOutOfDate = "ErrXpackOutOfDate" ErrXpackOutOfDate = "ErrXpackOutOfDate"
ErrApiConfigStatusInvalid = "ErrApiConfigStatusInvalid" ErrApiConfigStatusInvalid = "ErrApiConfigStatusInvalid"
ErrApiConfigKeyInvalid = "ErrApiConfigKeyInvalid" ErrApiConfigKeyInvalid = "ErrApiConfigKeyInvalid"
ErrApiConfigIPInvalid = "ErrApiConfigIPInvalid" ErrApiConfigIPInvalid = "ErrApiConfigIPInvalid"
ErrApiConfigDisable = "ErrApiConfigDisable" ErrApiConfigDisable = "ErrApiConfigDisable"
ErrApiConfigKeyTimeInvalid = "ErrApiConfigKeyTimeInvalid"
) )
// app // app

View File

@ -12,6 +12,7 @@ ErrApiConfigStatusInvalid: "API Interface access prohibited: {{ .detail }}"
ErrApiConfigKeyInvalid: "API Interface key error: {{ .detail }}" ErrApiConfigKeyInvalid: "API Interface key error: {{ .detail }}"
ErrApiConfigIPInvalid: "API Interface IP is not on the whitelist: {{ .detail }}" ErrApiConfigIPInvalid: "API Interface IP is not on the whitelist: {{ .detail }}"
ErrApiConfigDisable: "This interface prohibits the use of API Interface calls: {{ .detail }}" ErrApiConfigDisable: "This interface prohibits the use of API Interface calls: {{ .detail }}"
ErrApiConfigKeyTimeInvalid: "API Interface timestamp error: {{ .detail }}"
#common #common
ErrNameIsExist: "Name already exists" ErrNameIsExist: "Name already exists"

View File

@ -12,6 +12,7 @@ ErrApiConfigStatusInvalid: "Доступ к API интерфейсу запре
ErrApiConfigKeyInvalid: "Ошибка ключа API интерфейса: {{ .detail }}" ErrApiConfigKeyInvalid: "Ошибка ключа API интерфейса: {{ .detail }}"
ErrApiConfigIPInvalid: "IP API интерфейса отсутствует в белом списке: {{ .detail }}" ErrApiConfigIPInvalid: "IP API интерфейса отсутствует в белом списке: {{ .detail }}"
ErrApiConfigDisable: "Этот интерфейс запрещает использование вызовов API интерфейса: {{ .detail }}" ErrApiConfigDisable: "Этот интерфейс запрещает использование вызовов API интерфейса: {{ .detail }}"
ErrApiConfigKeyTimeInvalid: "Ошибка временной метки интерфейса API: {{ .detail }}"
#common #common
ErrNameIsExist: "Имя уже существует" ErrNameIsExist: "Имя уже существует"

View File

@ -12,6 +12,7 @@ ErrApiConfigStatusInvalid: "API 介面禁止訪問: {{ .detail }}"
ErrApiConfigKeyInvalid: "API 介面金鑰錯誤: {{ .detail }}" ErrApiConfigKeyInvalid: "API 介面金鑰錯誤: {{ .detail }}"
ErrApiConfigIPInvalid: "呼叫 API 介面 IP 不在白名單: {{ .detail }}" ErrApiConfigIPInvalid: "呼叫 API 介面 IP 不在白名單: {{ .detail }}"
ErrApiConfigDisable: "此介面禁止使用 API 介面呼叫: {{ .detail }}" ErrApiConfigDisable: "此介面禁止使用 API 介面呼叫: {{ .detail }}"
ErrApiConfigKeyTimeInvalid: "API 介面時間戳記錯誤: {{ .detail }}"
#common #common
ErrNameIsExist: "名稱已存在" ErrNameIsExist: "名稱已存在"

View File

@ -12,6 +12,7 @@ ErrApiConfigStatusInvalid: "API 接口禁止访问: {{ .detail }}"
ErrApiConfigKeyInvalid: "API 接口密钥错误: {{ .detail }}" ErrApiConfigKeyInvalid: "API 接口密钥错误: {{ .detail }}"
ErrApiConfigIPInvalid: "调用 API 接口 IP 不在白名单: {{ .detail }}" ErrApiConfigIPInvalid: "调用 API 接口 IP 不在白名单: {{ .detail }}"
ErrApiConfigDisable: "此接口禁止使用 API 接口调用: {{ .detail }}" ErrApiConfigDisable: "此接口禁止使用 API 接口调用: {{ .detail }}"
ErrApiConfigKeyTimeInvalid: "API 接口时间戳错误: {{ .detail }}"
#common #common
ErrNameIsExist: "名称已存在" ErrNameIsExist: "名称已存在"

View File

@ -77,6 +77,11 @@ func Init() {
global.LOG.Errorf("load service ip white list from setting failed, err: %v", err) global.LOG.Errorf("load service ip white list from setting failed, err: %v", err)
} }
global.CONF.System.IpWhiteList = ipWhiteListSetting.Value 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) handleUserInfo(global.CONF.System.ChangeUserInfo, settingRepo)

View File

@ -98,6 +98,7 @@ func Init() {
migrations.AddAutoRestart, migrations.AddAutoRestart,
migrations.AddApiInterfaceConfig, migrations.AddApiInterfaceConfig,
migrations.AddApiKeyValidityTime,
}) })
if err := m.Migrate(); err != nil { if err := m.Migrate(); err != nil {
global.LOG.Error(err) global.LOG.Error(err)

View File

@ -350,3 +350,13 @@ var AddApiInterfaceConfig = &gormigrate.Migration{
return nil 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
},
}

View File

@ -3,15 +3,15 @@ package middleware
import ( import (
"crypto/md5" "crypto/md5"
"encoding/hex" "encoding/hex"
"net"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/repo" "github.com/1Panel-dev/1Panel/backend/app/repo"
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/global"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net"
"strconv"
"strings"
"time"
) )
func SessionAuth() gin.HandlerFunc { func SessionAuth() gin.HandlerFunc {
@ -25,6 +25,11 @@ func SessionAuth() gin.HandlerFunc {
if panelToken != "" || panelTimestamp != "" { if panelToken != "" || panelTimestamp != "" {
if global.CONF.System.ApiInterfaceStatus == "enable" { if global.CONF.System.ApiInterfaceStatus == "enable" {
clientIP := c.ClientIP() clientIP := c.ClientIP()
if !isValid1PanelTimestamp(panelTimestamp) {
helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrApiConfigKeyTimeInvalid, nil)
return
}
if !isValid1PanelToken(panelToken, panelTimestamp) { if !isValid1PanelToken(panelToken, panelTimestamp) {
helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrApiConfigKeyInvalid, nil) helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrApiConfigKeyInvalid, nil)
return 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 { func isValid1PanelToken(panelToken string, panelTimestamp string) bool {
system1PanelToken := global.CONF.System.ApiKey system1PanelToken := global.CONF.System.ApiKey
if panelToken == GenerateMD5("1panel"+system1PanelToken+panelTimestamp) { if panelToken == GenerateMD5("1panel"+system1PanelToken+panelTimestamp) {

View File

@ -61,6 +61,7 @@ export namespace Setting {
apiInterfaceStatus: string; apiInterfaceStatus: string;
apiKey: string; apiKey: string;
ipWhiteList: string; ipWhiteList: string;
apiKeyValidityTime: number;
} }
export interface SettingUpdate { export interface SettingUpdate {
key: string; key: string;
@ -193,5 +194,6 @@ export namespace Setting {
apiInterfaceStatus: string; apiInterfaceStatus: string;
apiKey: string; apiKey: string;
ipWhiteList: string; ipWhiteList: string;
apiKeyValidityTime: number;
} }
} }

View File

@ -1431,6 +1431,10 @@ const message = {
ipWhiteList: 'IP allowlist', ipWhiteList: 'IP allowlist',
ipWhiteListEgs: 'One per line. For example,\n172.161.10.111\n172.161.10.0/24', ipWhiteListEgs: 'One per line. For example,\n172.161.10.111\n172.161.10.0/24',
ipWhiteListHelper: 'IPs within the allowlist can access the API.', 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', apiKeyReset: 'Interface key reset',
apiKeyResetHelper: 'the associated key service will become invalid. Please add a new key to the service', apiKeyResetHelper: 'the associated key service will become invalid. Please add a new key to the service',
confDockerProxy: 'Configure docker proxy', confDockerProxy: 'Configure docker proxy',

View File

@ -1437,6 +1437,10 @@ const message = {
ipWhiteList: 'Белый список IP', ipWhiteList: 'Белый список IP',
ipWhiteListEgs: 'По одному в строке. Например,\n172.161.10.111\n172.161.10.0/24', ipWhiteListEgs: 'По одному в строке. Например,\n172.161.10.111\n172.161.10.0/24',
ipWhiteListHelper: 'IP-адреса из белого списка могут получить доступ к API.', ipWhiteListHelper: 'IP-адреса из белого списка могут получить доступ к API.',
apiKeyValidityTime: 'Срок действия ключа интерфейса',
apiKeyValidityTimeEgs: 'Срок действия ключа интерфейса (в единицах)',
apiKeyValidityTimeHelper:
'Интерфейс времени метки между текущей меткой времени на момент запроса действителен (в единицах), установлен как 0, не проводится проверка метки времени',
apiKeyReset: 'Сброс ключа интерфейса', apiKeyReset: 'Сброс ключа интерфейса',
apiKeyResetHelper: apiKeyResetHelper:
'связанный ключевой сервис станет недействительным. Пожалуйста, добавьте новый ключ к сервису', 'связанный ключевой сервис станет недействительным. Пожалуйста, добавьте новый ключ к сервису',

View File

@ -1357,6 +1357,9 @@ const message = {
ipWhiteList: 'IP白名單', ipWhiteList: 'IP白名單',
ipWhiteListEgs: '當存在多個 IP 需要換行顯示\n172.16.10.111 \n172.16.10.0/24', ipWhiteListEgs: '當存在多個 IP 需要換行顯示\n172.16.10.111 \n172.16.10.0/24',
ipWhiteListHelper: '必需在 IP 白名單清單中的 IP 才能瀏覽面板 API 介面', ipWhiteListHelper: '必需在 IP 白名單清單中的 IP 才能瀏覽面板 API 介面',
apiKeyValidityTime: '介面金鑰有效期',
apiKeyValidityTimeEgs: '介面金鑰有效期組織分',
apiKeyValidityTimeHelper: '介面時間戳記到請求時的當前時間戳之間有效組織分設定為0時不做時間戳記校驗',
apiKeyReset: '介面密鑰重設', apiKeyReset: '介面密鑰重設',
apiKeyResetHelper: '重設密鑰後已關聯密鑰服務將失效請重新新增新密鑰至服務', apiKeyResetHelper: '重設密鑰後已關聯密鑰服務將失效請重新新增新密鑰至服務',
confDockerProxy: '配寘 Docker 代理', confDockerProxy: '配寘 Docker 代理',

View File

@ -1358,6 +1358,9 @@ const message = {
ipWhiteList: 'IP 白名单', ipWhiteList: 'IP 白名单',
ipWhiteListEgs: '当存在多个 IP 需要换行显示 \n172.16.10.111 \n172.16.10.0/24', ipWhiteListEgs: '当存在多个 IP 需要换行显示 \n172.16.10.111 \n172.16.10.0/24',
ipWhiteListHelper: '必需在 IP 白名单列表中的 IP 才能访问面板 API 接口', ipWhiteListHelper: '必需在 IP 白名单列表中的 IP 才能访问面板 API 接口',
apiKeyValidityTime: '接口密钥有效期',
apiKeyValidityTimeEgs: '接口密钥有效期单位分',
apiKeyValidityTimeHelper: '接口时间戳到请求时的当前时间戳之间有效单位分设置为 0 不做时间戳校验',
apiKeyReset: '接口密钥重置', apiKeyReset: '接口密钥重置',
apiKeyResetHelper: '重置密钥后已关联密钥服务将失效请重新添加新密钥至服务', apiKeyResetHelper: '重置密钥后已关联密钥服务将失效请重新添加新密钥至服务',
confDockerProxy: '配置 Docker 代理', confDockerProxy: '配置 Docker 代理',

View File

@ -65,6 +65,17 @@
/> />
<span class="input-help">{{ $t('setting.ipWhiteListHelper') }}</span> <span class="input-help">{{ $t('setting.ipWhiteListHelper') }}</span>
</el-form-item> </el-form-item>
<el-form-item :label="$t('setting.apiKeyValidityTime')" prop="apiKeyValidityTime">
<el-input
:placeholder="$t('setting.apiKeyValidityTimeEgs')"
v-model="form.apiKeyValidityTime"
>
<template #append>{{ $t('commons.units.minute') }}</template>
</el-input>
<span class="input-help">
{{ $t('setting.apiKeyValidityTimeHelper') }}
</span>
</el-form-item>
</el-col> </el-col>
</el-row> </el-row>
</el-form> </el-form>
@ -103,17 +114,20 @@ const form = reactive({
apiKey: '', apiKey: '',
ipWhiteList: '', ipWhiteList: '',
apiInterfaceStatus: '', apiInterfaceStatus: '',
apiKeyValidityTime: 120,
}); });
const rules = reactive({ const rules = reactive({
ipWhiteList: [Rules.requiredInput, { validator: checkIPs, trigger: 'blur' }], ipWhiteList: [Rules.requiredInput, { validator: checkIPs, trigger: 'blur' }],
apiKey: [Rules.requiredInput], apiKey: [Rules.requiredInput],
apiKeyValidityTime: [Rules.requiredInput, Rules.integerNumberWith0],
}); });
interface DialogProps { interface DialogProps {
apiInterfaceStatus: string; apiInterfaceStatus: string;
apiKey: string; apiKey: string;
ipWhiteList: string; ipWhiteList: string;
apiKeyValidityTime: number;
} }
function checkIPs(rule: any, value: any, callback: any) { function checkIPs(rule: any, value: any, callback: any) {
@ -146,6 +160,7 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
}); });
} }
form.ipWhiteList = params.ipWhiteList; form.ipWhiteList = params.ipWhiteList;
form.apiKeyValidityTime = params.apiKeyValidityTime;
drawerVisible.value = true; drawerVisible.value = true;
}; };
@ -179,6 +194,7 @@ const onSave = async (formEl: FormInstance | undefined) => {
apiKey: form.apiKey, apiKey: form.apiKey,
ipWhiteList: form.ipWhiteList, ipWhiteList: form.ipWhiteList,
apiInterfaceStatus: form.apiInterfaceStatus, apiInterfaceStatus: form.apiInterfaceStatus,
apiKeyValidityTime: form.apiKeyValidityTime,
}; };
loading.value = true; loading.value = true;
await updateApiConfig(param) await updateApiConfig(param)

View File

@ -285,6 +285,7 @@ const form = reactive({
apiInterfaceStatus: 'disable', apiInterfaceStatus: 'disable',
apiKey: '', apiKey: '',
ipWhiteList: '', ipWhiteList: '',
apiKeyValidityTime: 120,
proHideMenus: ref(i18n.t('setting.unSetting')), proHideMenus: ref(i18n.t('setting.unSetting')),
hideMenuList: '', hideMenuList: '',
@ -353,6 +354,7 @@ const search = async () => {
form.apiInterfaceStatus = res.data.apiInterfaceStatus; form.apiInterfaceStatus = res.data.apiInterfaceStatus;
form.apiKey = res.data.apiKey; form.apiKey = res.data.apiKey;
form.ipWhiteList = res.data.ipWhiteList; form.ipWhiteList = res.data.ipWhiteList;
form.apiKeyValidityTime = res.data.apiKeyValidityTime;
const json: Node = JSON.parse(res.data.xpackHideMenu); const json: Node = JSON.parse(res.data.xpackHideMenu);
const checkedTitles = getCheckedTitles(json); const checkedTitles = getCheckedTitles(json);
@ -428,6 +430,7 @@ const onChangeApiInterfaceStatus = async () => {
apiInterfaceStatus: form.apiInterfaceStatus, apiInterfaceStatus: form.apiInterfaceStatus,
apiKey: form.apiKey, apiKey: form.apiKey,
ipWhiteList: form.ipWhiteList, ipWhiteList: form.ipWhiteList,
apiKeyValidityTime: form.apiKeyValidityTime,
}); });
return; return;
} }
@ -442,6 +445,7 @@ const onChangeApiInterfaceStatus = async () => {
apiKey: form.apiKey, apiKey: form.apiKey,
ipWhiteList: form.ipWhiteList, ipWhiteList: form.ipWhiteList,
apiInterfaceStatus: form.apiInterfaceStatus, apiInterfaceStatus: form.apiInterfaceStatus,
apiKeyValidityTime: form.apiKeyValidityTime,
}; };
await updateApiConfig(param) await updateApiConfig(param)
.then(() => { .then(() => {