Skip to content

Commit

Permalink
feat: Add API interface authentication function (#7146)
Browse files Browse the repository at this point in the history
  • Loading branch information
lan-yonghui authored Nov 21, 2024
1 parent c2fd02a commit 2859772
Show file tree
Hide file tree
Showing 29 changed files with 884 additions and 88 deletions.
49 changes: 49 additions & 0 deletions backend/app/api/v1/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,3 +342,52 @@ func (b *BaseApi) MFABind(c *gin.Context) {

helper.SuccessWithData(c, nil)
}

// @Tags System Setting
// @Summary generate api key
// @Description 生成 API 接口密钥
// @Accept json
// @Success 200
// @Security ApiKeyAuth
// @Router /settings/api/config/generate/key [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"生成 API 接口密钥","formatEN":"generate api key"}
func (b *BaseApi) GenerateApiKey(c *gin.Context) {
panelToken := c.GetHeader("1Panel-Token")
if panelToken != "" {
helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrApiConfigDisable, nil)
return
}
apiKey, err := settingService.GenerateApiKey()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, apiKey)
}

// @Tags System Setting
// @Summary Update api config
// @Description 更新 API 接口配置
// @Accept json
// @Param request body dto.ApiInterfaceConfig true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /settings/api/config/update [post]
// @x-panel-log {"bodyKeys":["ipWhiteList"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 API 接口配置 => IP 白名单: [ipWhiteList]","formatEN":"update api config => IP White List: [ipWhiteList]"}
func (b *BaseApi) UpdateApiConfig(c *gin.Context) {
panelToken := c.GetHeader("1Panel-Token")
if panelToken != "" {
helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrApiConfigDisable, nil)
return
}
var req dto.ApiInterfaceConfig
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}

if err := settingService.UpdateApiConfig(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
10 changes: 10 additions & 0 deletions backend/app/dto/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ type SettingInfo struct {
ProxyUser string `json:"proxyUser"`
ProxyPasswd string `json:"proxyPasswd"`
ProxyPasswdKeep string `json:"proxyPasswdKeep"`

ApiInterfaceStatus string `json:"apiInterfaceStatus"`
ApiKey string `json:"apiKey"`
IpWhiteList string `json:"ipWhiteList"`
}

type SettingUpdate struct {
Expand Down Expand Up @@ -231,3 +235,9 @@ type XpackHideMenu struct {
Path string `json:"path,omitempty"`
Children []XpackHideMenu `json:"children,omitempty"`
}

type ApiInterfaceConfig struct {
ApiInterfaceStatus string `json:"apiInterfaceStatus"`
ApiKey string `json:"apiKey"`
IpWhiteList string `json:"ipWhiteList"`
}
27 changes: 27 additions & 0 deletions backend/app/service/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ type ISettingService interface {
UpdateSSL(c *gin.Context, req dto.SSLUpdate) error
LoadFromCert() (*dto.SSLInfo, error)
HandlePasswordExpired(c *gin.Context, old, new string) error
GenerateApiKey() (string, error)
UpdateApiConfig(req dto.ApiInterfaceConfig) error
}

func NewISettingService() ISettingService {
Expand Down Expand Up @@ -485,3 +487,28 @@ func checkCertValid() error {

return nil
}

func (u *SettingService) GenerateApiKey() (string, error) {
apiKey := common.RandStr(32)
if err := settingRepo.Update("ApiKey", apiKey); err != nil {
return global.CONF.System.ApiKey, err
}
global.CONF.System.ApiKey = apiKey
return apiKey, nil
}

func (u *SettingService) UpdateApiConfig(req dto.ApiInterfaceConfig) error {
if err := settingRepo.Update("ApiInterfaceStatus", req.ApiInterfaceStatus); err != nil {
return err
}
global.CONF.System.ApiInterfaceStatus = req.ApiInterfaceStatus
if err := settingRepo.Update("ApiKey", req.ApiKey); err != nil {
return err
}
global.CONF.System.ApiKey = req.ApiKey
if err := settingRepo.Update("IpWhiteList", req.IpWhiteList); err != nil {
return err
}
global.CONF.System.IpWhiteList = req.IpWhiteList
return nil
}
51 changes: 27 additions & 24 deletions backend/configs/system.go
Original file line number Diff line number Diff line change
@@ -1,28 +1,31 @@
package configs

type System struct {
Port string `mapstructure:"port"`
Ipv6 string `mapstructure:"ipv6"`
BindAddress string `mapstructure:"bindAddress"`
SSL string `mapstructure:"ssl"`
DbFile string `mapstructure:"db_file"`
DbPath string `mapstructure:"db_path"`
LogPath string `mapstructure:"log_path"`
DataDir string `mapstructure:"data_dir"`
TmpDir string `mapstructure:"tmp_dir"`
Cache string `mapstructure:"cache"`
Backup string `mapstructure:"backup"`
EncryptKey string `mapstructure:"encrypt_key"`
BaseDir string `mapstructure:"base_dir"`
Mode string `mapstructure:"mode"`
RepoUrl string `mapstructure:"repo_url"`
Version string `mapstructure:"version"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
Entrance string `mapstructure:"entrance"`
IsDemo bool `mapstructure:"is_demo"`
AppRepo string `mapstructure:"app_repo"`
ChangeUserInfo string `mapstructure:"change_user_info"`
OneDriveID string `mapstructure:"one_drive_id"`
OneDriveSc string `mapstructure:"one_drive_sc"`
Port string `mapstructure:"port"`
Ipv6 string `mapstructure:"ipv6"`
BindAddress string `mapstructure:"bindAddress"`
SSL string `mapstructure:"ssl"`
DbFile string `mapstructure:"db_file"`
DbPath string `mapstructure:"db_path"`
LogPath string `mapstructure:"log_path"`
DataDir string `mapstructure:"data_dir"`
TmpDir string `mapstructure:"tmp_dir"`
Cache string `mapstructure:"cache"`
Backup string `mapstructure:"backup"`
EncryptKey string `mapstructure:"encrypt_key"`
BaseDir string `mapstructure:"base_dir"`
Mode string `mapstructure:"mode"`
RepoUrl string `mapstructure:"repo_url"`
Version string `mapstructure:"version"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
Entrance string `mapstructure:"entrance"`
IsDemo bool `mapstructure:"is_demo"`
AppRepo string `mapstructure:"app_repo"`
ChangeUserInfo string `mapstructure:"change_user_info"`
OneDriveID string `mapstructure:"one_drive_id"`
OneDriveSc string `mapstructure:"one_drive_sc"`
ApiInterfaceStatus string `mapstructure:"api_interface_status"`
ApiKey string `mapstructure:"api_key"`
IpWhiteList string `mapstructure:"ip_white_list"`
}
28 changes: 16 additions & 12 deletions backend/constant/errs.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,22 @@ 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"
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"
)

// app
Expand Down
4 changes: 4 additions & 0 deletions backend/i18n/lang/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ ErrStructTransform: "Type conversion failure: {{ .detail }}"
ErrNotLogin: "User is not Login: {{ .detail }}"
ErrPasswordExpired: "The current password has expired: {{ .detail }}"
ErrNotSupportType: "The system does not support the current type: {{ .detail }}"
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 }}"

#common
ErrNameIsExist: "Name is already exist"
Expand Down
4 changes: 4 additions & 0 deletions backend/i18n/lang/zh-Hant.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ ErrStructTransform: "類型轉換失敗: {{ .detail }}"
ErrNotLogin: "用戶未登入: {{ .detail }}"
ErrPasswordExpired: "當前密碼已過期: {{ .detail }}"
ErrNotSupportType: "系統暫不支持當前類型: {{ .detail }}"
ErrApiConfigStatusInvalid: "API 接口禁止訪問: {{ .detail }}"
ErrApiConfigKeyInvalid: "API 接口密钥錯誤: {{ .detail }}"
ErrApiConfigIPInvalid: "调用 API 接口 IP 不在白名单: {{ .detail }}"
ErrApiConfigDisable: "此接口禁止使用 API 接口調用: {{ .detail }}"

#common
ErrNameIsExist: "名稱已存在"
Expand Down
4 changes: 4 additions & 0 deletions backend/i18n/lang/zh.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ ErrStructTransform: "类型转换失败: {{ .detail }}"
ErrNotLogin: "用户未登录: {{ .detail }}"
ErrPasswordExpired: "当前密码已过期: {{ .detail }}"
ErrNotSupportType: "系统暂不支持当前类型: {{ .detail }}"
ErrApiConfigStatusInvalid: "API 接口禁止访问: {{ .detail }}"
ErrApiConfigKeyInvalid: "API 接口密钥错误: {{ .detail }}"
ErrApiConfigIPInvalid: "调用 API 接口 IP 不在白名单: {{ .detail }}"
ErrApiConfigDisable: "此接口禁止使用 API 接口调用: {{ .detail }}"

#common
ErrNameIsExist: "名称已存在"
Expand Down
18 changes: 18 additions & 0 deletions backend/init/hook/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,24 @@ func Init() {
global.LOG.Fatalf("init service before start failed, err: %v", err)
}

apiInterfaceStatusSetting, err := settingRepo.Get(settingRepo.WithByKey("ApiInterfaceStatus"))
if err != nil {
global.LOG.Errorf("load service api interface from setting failed, err: %v", err)
}
global.CONF.System.ApiInterfaceStatus = apiInterfaceStatusSetting.Value
if apiInterfaceStatusSetting.Value == "enable" {
apiKeySetting, err := settingRepo.Get(settingRepo.WithByKey("ApiKey"))
if err != nil {
global.LOG.Errorf("load service api key from setting failed, err: %v", err)
}
global.CONF.System.ApiKey = apiKeySetting.Value
ipWhiteListSetting, err := settingRepo.Get(settingRepo.WithByKey("IpWhiteList"))
if err != nil {
global.LOG.Errorf("load service ip white list from setting failed, err: %v", err)
}
global.CONF.System.IpWhiteList = ipWhiteListSetting.Value
}

handleUserInfo(global.CONF.System.ChangeUserInfo, settingRepo)

handleCronjobStatus()
Expand Down
1 change: 1 addition & 0 deletions backend/init/migration/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ func Init() {
migrations.AddComposeColumn,

migrations.AddAutoRestart,
migrations.AddApiInterfaceConfig,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)
Expand Down
16 changes: 16 additions & 0 deletions backend/init/migration/migrations/v_1_10.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,3 +334,19 @@ var AddAutoRestart = &gormigrate.Migration{
return nil
},
}

var AddApiInterfaceConfig = &gormigrate.Migration{
ID: "202411-add-api-interface-config",
Migrate: func(tx *gorm.DB) error {
if err := tx.Create(&model.Setting{Key: "ApiInterfaceStatus", Value: "disable"}).Error; err != nil {
return err
}
if err := tx.Create(&model.Setting{Key: "ApiKey", Value: ""}).Error; err != nil {
return err
}
if err := tx.Create(&model.Setting{Key: "IpWhiteList", Value: ""}).Error; err != nil {
return err
}
return nil
},
}
61 changes: 61 additions & 0 deletions backend/middleware/session.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
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"
Expand All @@ -16,6 +20,28 @@ func SessionAuth() gin.HandlerFunc {
c.Next()
return
}
panelToken := c.GetHeader("1Panel-Token")
panelTimestamp := c.GetHeader("1Panel-Timestamp")
if panelToken != "" || panelTimestamp != "" {
if global.CONF.System.ApiInterfaceStatus == "enable" {
clientIP := c.ClientIP()
if !isValid1PanelToken(panelToken, panelTimestamp) {
helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrApiConfigKeyInvalid, nil)
return
}

if !isIPInWhiteList(clientIP) {
helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrApiConfigIPInvalid, nil)
return
}
c.Next()
return
} else {
helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrApiConfigStatusInvalid, nil)
return
}
}

sId, err := c.Cookie(constant.SessionName)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrTypeNotLogin, nil)
Expand All @@ -36,3 +62,38 @@ func SessionAuth() gin.HandlerFunc {
c.Next()
}
}

func isValid1PanelToken(panelToken string, panelTimestamp string) bool {
system1PanelToken := global.CONF.System.ApiKey
if GenerateMD5("1panel"+panelToken+panelTimestamp) == GenerateMD5("1panel"+system1PanelToken+panelTimestamp) {
return true
}
return false
}

func isIPInWhiteList(clientIP string) bool {
ipWhiteString := global.CONF.System.IpWhiteList
ipWhiteList := strings.Split(ipWhiteString, "\n")
for _, cidr := range ipWhiteList {
if cidr == "0.0.0.0" {
return true
}
_, ipNet, err := net.ParseCIDR(cidr)
if err != nil {
if cidr == clientIP {
return true
}
continue
}
if ipNet.Contains(net.ParseIP(clientIP)) {
return true
}
}
return false
}

func GenerateMD5(input string) string {
hash := md5.New()
hash.Write([]byte(input))
return hex.EncodeToString(hash.Sum(nil))
}
2 changes: 2 additions & 0 deletions backend/router/ro_setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,7 @@ func (s *SettingRouter) InitRouter(Router *gin.RouterGroup) {
settingRouter.POST("/upgrade/notes", baseApi.GetNotesByVersion)
settingRouter.GET("/upgrade", baseApi.GetUpgradeInfo)
settingRouter.GET("/basedir", baseApi.LoadBaseDir)
settingRouter.POST("/api/config/generate/key", baseApi.GenerateApiKey)
settingRouter.POST("/api/config/update", baseApi.UpdateApiConfig)
}
}
Loading

0 comments on commit 2859772

Please sign in to comment.