Files

510 lines
17 KiB
Go

package service
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"strings"
"sync"
"time"
"unicode/utf8"
"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/app/repo"
"github.com/1Panel-dev/1Panel/core/buserr"
"github.com/1Panel-dev/1Panel/core/constant"
"github.com/1Panel-dev/1Panel/core/global"
"github.com/1Panel-dev/1Panel/core/utils/apikey_audit"
"github.com/1Panel-dev/1Panel/core/utils/common"
"github.com/1Panel-dev/1Panel/core/utils/encrypt"
terminalsession "github.com/1Panel-dev/1Panel/core/utils/terminal_session"
"github.com/1Panel-dev/1Panel/core/utils/xpack"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
var apiKeyMutationMu sync.Mutex
type APIKeyService struct {
Provider auth.APIKeyOwnerProvider
RevokeTerminals terminalsession.RevokeFunc
}
func NewAPIKeyService() *APIKeyService {
provider, _ := xpack.AuthProvider.(auth.APIKeyOwnerProvider)
return &APIKeyService{Provider: provider, RevokeTerminals: xpack.AuthProvider.RevokeTerminalSessions}
}
func (s *APIKeyService) owner(c *gin.Context) (auth.APIKeyOwner, error) {
if _, err := auth.RequireAPIKeySession(c); err != nil {
return auth.APIKeyOwner{}, err
}
if s.Provider == nil {
return auth.APIKeyOwner{}, buserr.New("ErrApiConfigStatusInvalid")
}
return s.Provider.CurrentAPIKeyOwner(c)
}
func apiKeyItem(key model.APIKey) dto.APIKeyItem {
status := key.Status
if status != "Revoked" && key.ExpiresAt != nil && !time.Now().Before(*key.ExpiresAt) {
status = "Expired"
}
return dto.APIKeyItem{ID: key.ID, Kind: "apiKey", Name: key.Name, Description: key.Description, KeyHint: key.KeyHint, Status: status, IPWhiteList: key.IPWhiteList, APITrustedProxies: key.APITrustedProxies, APIKeyValidityTime: key.APIKeyValidityTime, ExpiresAt: key.ExpiresAt, AllowAppBinding: key.AllowAppBinding, Revision: key.Revision, CreatedAt: &key.CreatedAt}
}
func (s *APIKeyService) legacy(owner auth.APIKeyOwner) (dto.APIKeyItem, auth.APIAuthConfig, model.LegacyAPIKeyPolicy, string, error) {
config, err := s.Provider.LoadLegacyAPIKey(owner)
if err != nil {
return dto.APIKeyItem{}, config, model.LegacyAPIKeyPolicy{}, "", err
}
policy, err := auth.LoadLegacyAPIKeyPolicy(owner)
if err != nil {
return dto.APIKeyItem{}, config, policy, "", err
}
fingerprint, revision := auth.LegacyAPIKeyFingerprint(owner, config, policy)
item := dto.APIKeyItem{ID: "legacy", Kind: "legacy", Name: "Legacy API Key", KeyHint: keyHint(config.ApiKey), Status: config.ApiInterfaceStatus, IPWhiteList: config.IpWhiteList, APITrustedProxies: config.ApiTrustedProxies, APIKeyValidityTime: config.ApiKeyValidityTime, AllowAppBinding: policy.AllowAppBinding, Revision: revision}
return item, config, policy, fingerprint, nil
}
func (s *APIKeyService) Search(c *gin.Context, req dto.APIKeySearch) (*dto.APIKeyPage, error) {
owner, err := s.owner(c)
if err != nil {
return nil, err
}
if req.Page < 1 || req.PageSize < 1 || req.PageSize > 100 {
return nil, buserr.New("ErrInvalidParams")
}
keys, err := repo.NewAPIKeyRepo().List(owner.Type, owner.ID, req.ExcludeRevoked)
if err != nil {
return nil, err
}
items := make([]dto.APIKeyItem, 0, len(keys)+1)
legacy, config, _, _, err := s.legacy(owner)
if err != nil {
return nil, err
}
if config.ApiKey != "" {
items = append(items, legacy)
}
for _, key := range keys {
items = append(items, apiKeyItem(key))
}
result := &dto.APIKeyPage{Items: []dto.APIKeyItem{}, Total: len(items)}
start := (req.Page - 1) * req.PageSize
if start >= 0 && start < len(items) {
result.Items = items[start:min(start+req.PageSize, len(items))]
}
return result, nil
}
func validateAPIKeyFields(fields *dto.APIKeyFields, create bool) (*time.Time, error) {
fields.Name = strings.TrimSpace(fields.Name)
fields.Description = strings.TrimSpace(fields.Description)
if fields.Name == "" || utf8.RuneCountInString(fields.Name) > 64 || utf8.RuneCountInString(fields.Description) > 256 || fields.APIKeyValidityTime < 0 || fields.APIKeyValidityTime > 1440 || len(fields.IPWhiteList) > 4096 || len(fields.APITrustedProxies) > 4096 {
return nil, buserr.New("ErrInvalidParams")
}
ips, err := common.HandleIPList(fields.IPWhiteList)
if err != nil || len(ips) == 0 {
return nil, buserr.New("ErrInvalidParams")
}
fields.IPWhiteList = strings.Join(ips, "\n")
fields.APITrustedProxies, err = auth.NormalizeAPITrustedProxies(fields.APITrustedProxies)
if err != nil {
return nil, err
}
if len(fields.ExpiresAt) == 0 {
if !create {
return nil, buserr.New("ErrInvalidParams")
}
expires := time.Now().UTC().Add(90 * 24 * time.Hour)
return &expires, nil
}
if bytes.Equal(bytes.TrimSpace(fields.ExpiresAt), []byte("null")) {
return nil, nil
}
var expires time.Time
if err = json.Unmarshal(fields.ExpiresAt, &expires); err != nil || !expires.After(time.Now()) {
return nil, buserr.New("ErrInvalidParams")
}
return &expires, nil
}
func keyHint(secret string) string {
if len(secret) < 8 {
return ""
}
return secret[:4] + "••••" + secret[len(secret)-4:]
}
func (s *APIKeyService) Create(c *gin.Context, req dto.APIKeyCreate) (*dto.APIKeyCreated, error) {
owner, err := s.owner(c)
if err != nil {
return nil, err
}
if _, err = uuid.Parse(req.RequestID); err != nil {
return nil, buserr.New("ErrInvalidParams")
}
expires, err := validateAPIKeyFields(&req.APIKeyFields, true)
if err != nil {
return nil, err
}
encoded, _ := json.Marshal(req.APIKeyFields)
digest := sha256.Sum256(encoded)
requestHash := hex.EncodeToString(digest[:])
master, err := encrypt.APIKeyEncryptionKey()
if err != nil {
return nil, err
}
apiKeyMutationMu.Lock()
var key model.APIKey
var secret string
repeated := false
err = global.DB.Transaction(func(tx *gorm.DB) error {
q := repo.APIKeyOwnerQuery(tx, owner.Type, owner.ID)
err := q.Where("request_id = ?", req.RequestID).First(&key).Error
if err == nil {
if key.RequestHash != requestHash {
return buserr.New("ErrAPIKeyConflict")
}
repeated = true
return nil
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
if err = ensureAPIKeyName(tx, owner, req.Name, ""); err != nil {
return err
}
bytes := make([]byte, 16)
if _, err = rand.Read(bytes); err != nil {
return err
}
secret = hex.EncodeToString(bytes)
key = model.APIKey{ID: uuid.NewString(), OwnerType: owner.Type, OwnerID: owner.ID, Name: req.Name, ActiveName: &req.Name, Description: req.Description, RequestID: req.RequestID, RequestHash: requestHash, SecretVersion: 1, KeyHint: keyHint(secret), Status: constant.StatusEnable, IPWhiteList: req.IPWhiteList, APITrustedProxies: req.APITrustedProxies, APIKeyValidityTime: req.APIKeyValidityTime, ExpiresAt: expires, AllowAppBinding: req.AllowAppBinding, Revision: 1}
key.SecretCiphertext, err = encrypt.EncryptAPIKey(secret, key.ID, master)
if err != nil {
return err
}
return tx.Create(&key).Error
})
apiKeyMutationMu.Unlock()
if err != nil {
return nil, err
}
if !repeated {
recordAPIKeyEvent(c, owner, key.ID, key.Name, "create")
}
return &dto.APIKeyCreated{Item: apiKeyItem(key), APIKey: secret, AlreadyCreated: repeated}, nil
}
func ensureAPIKeyName(tx *gorm.DB, owner auth.APIKeyOwner, name, id string) error {
var count int64
err := repo.APIKeyOwnerQuery(tx.Model(&model.APIKey{}), owner.Type, owner.ID).Where("active_name = ? AND id <> ?", name, id).Count(&count).Error
if err != nil {
return err
}
if count > 0 {
return buserr.New("ErrAPIKeyNameExists")
}
return nil
}
func (s *APIKeyService) Update(c *gin.Context, req dto.APIKeyUpdate) error {
owner, err := s.owner(c)
if err != nil {
return err
}
if req.ID == "legacy" {
return s.updateLegacy(c, owner, req.APIKeyMutation, &req.APIKeyFields, "update", "")
}
expires, err := validateAPIKeyFields(&req.APIKeyFields, false)
if err != nil {
return err
}
return s.mutate(c, owner, req.APIKeyMutation, "update", func(tx *gorm.DB, key *model.APIKey) error {
if err := ensureAPIKeyName(tx, owner, req.Name, key.ID); err != nil {
return err
}
key.Name = req.Name
key.ActiveName = &req.Name
key.Description = req.Description
key.IPWhiteList = req.IPWhiteList
key.APITrustedProxies = req.APITrustedProxies
key.APIKeyValidityTime = req.APIKeyValidityTime
key.ExpiresAt = expires
key.AllowAppBinding = req.AllowAppBinding
return nil
})
}
func (s *APIKeyService) Status(c *gin.Context, req dto.APIKeyStatus) error {
owner, err := s.owner(c)
if err != nil {
return err
}
if req.Status != constant.StatusEnable && req.Status != constant.StatusDisable {
return buserr.New("ErrInvalidParams")
}
action := "disable"
if req.Status == constant.StatusEnable {
action = "enable"
}
if req.ID == "legacy" {
return s.updateLegacy(c, owner, req.APIKeyMutation, nil, action, req.Status)
}
return s.mutate(c, owner, req.APIKeyMutation, action, func(_ *gorm.DB, key *model.APIKey) error {
if req.Status == constant.StatusEnable && key.ExpiresAt != nil && !time.Now().Before(*key.ExpiresAt) {
return buserr.New("ErrApiConfigKeyExpired")
}
key.Status = req.Status
return nil
})
}
func (s *APIKeyService) Revoke(c *gin.Context, req dto.APIKeyMutation) error {
owner, err := s.owner(c)
if err != nil {
return err
}
if req.ID == "legacy" {
return s.updateLegacy(c, owner, req, nil, "revoke", constant.StatusDisable)
}
return s.mutate(c, owner, req, "revoke", func(_ *gorm.DB, key *model.APIKey) error {
now := time.Now()
key.Status = "Revoked"
key.RevokedAt = &now
key.ActiveName = nil
key.SecretCiphertext = ""
key.AllowAppBinding = false
return nil
})
}
func (s *APIKeyService) mutate(c *gin.Context, owner auth.APIKeyOwner, req dto.APIKeyMutation, action string, change func(*gorm.DB, *model.APIKey) error) error {
if req.ID == "" || req.Revision == 0 {
return buserr.New("ErrInvalidParams")
}
apiKeyMutationMu.Lock()
var key model.APIKey
closeTerminals := action == "disable" || action == "revoke"
err := global.DB.Transaction(func(tx *gorm.DB) error {
if err := repo.APIKeyOwnerQuery(tx, owner.Type, owner.ID).Where("id = ?", req.ID).First(&key).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return buserr.New("ErrAPIKeyNotFound")
}
return err
}
if key.Revision != req.Revision || key.Status == "Revoked" {
return buserr.New("ErrAPIKeyConflict")
}
before := key
if err := change(tx, &key); err != nil {
return err
}
closeTerminals = closeTerminals || before.IPWhiteList != key.IPWhiteList || before.APITrustedProxies != key.APITrustedProxies || before.APIKeyValidityTime != key.APIKeyValidityTime || (key.ExpiresAt != nil && (before.ExpiresAt == nil || key.ExpiresAt.Before(*before.ExpiresAt)))
key.Revision++
result := tx.Model(&model.APIKey{}).Where("id = ? AND revision = ?", key.ID, req.Revision).Select("*").Updates(&key)
if result.Error != nil {
return result.Error
}
if result.RowsAffected != 1 {
return buserr.New("ErrAPIKeyConflict")
}
return nil
})
apiKeyMutationMu.Unlock()
if err != nil {
return err
}
recordAPIKeyEvent(c, owner, key.ID, key.Name, action)
if closeTerminals {
s.revokeAPIKeyTerminals(c, owner, key.ID)
}
return nil
}
func (s *APIKeyService) updateLegacy(c *gin.Context, owner auth.APIKeyOwner, req dto.APIKeyMutation, fields *dto.APIKeyFields, action, status string) error {
return auth.WithLegacyAPIKeyMutation(func() error {
return s.updateLegacyLocked(c, owner, req, fields, action, status)
})
}
func (s *APIKeyService) updateLegacyLocked(c *gin.Context, owner auth.APIKeyOwner, req dto.APIKeyMutation, fields *dto.APIKeyFields, action, status string) error {
item, config, policy, _, err := s.legacy(owner)
if err != nil {
return err
}
if config.ApiKey == "" {
return buserr.New("ErrAPIKeyNotFound")
}
if req.Revision == 0 || req.Revision != item.Revision {
return buserr.New("ErrAPIKeyConflict")
}
previousConfig := config
if fields != nil {
fields.Name = "Legacy API Key"
legacyWindow := fields.APIKeyValidityTime
if legacyWindow > 1440 && legacyWindow == config.ApiKeyValidityTime {
fields.APIKeyValidityTime = 1440
}
expires, err := validateAPIKeyFields(fields, false)
fields.APIKeyValidityTime = legacyWindow
if err != nil {
return err
}
if expires != nil {
return buserr.New("ErrInvalidParams")
}
config.IpWhiteList = fields.IPWhiteList
config.ApiTrustedProxies = fields.APITrustedProxies
config.ApiKeyValidityTime = fields.APIKeyValidityTime
policy.AllowAppBinding = fields.AllowAppBinding
}
if status != "" {
config.ApiInterfaceStatus = status
}
if action == "revoke" {
config.ApiKey = ""
policy.AllowAppBinding = false
}
allowAppBinding := policy.AllowAppBinding
policy.AllowAppBinding = false
policy.Revision++
if err = global.DB.Clauses(clause.OnConflict{UpdateAll: true}).Create(&policy).Error; err != nil {
return err
}
if err = s.Provider.SaveLegacyAPIKey(owner, config); err != nil {
return err
}
if allowAppBinding {
policy.AllowAppBinding = true
policy.Revision++
if err = global.DB.Clauses(clause.OnConflict{UpdateAll: true}).Create(&policy).Error; err != nil {
return err
}
}
recordAPIKeyEvent(c, owner, "legacy", item.Name, action)
if action == "disable" || action == "revoke" || previousConfig.IpWhiteList != config.IpWhiteList || previousConfig.ApiTrustedProxies != config.ApiTrustedProxies || previousConfig.ApiKeyValidityTime != config.ApiKeyValidityTime {
s.revokeAPIKeyTerminals(c, owner, "legacy")
}
return nil
}
func recordAPIKeyEvent(c *gin.Context, owner auth.APIKeyOwner, id, name, action string) {
if err := apikey_audit.Record(c, apikey_audit.Event{OwnerType: owner.Type, OwnerID: owner.ID, KeyID: id, KeyName: name, Action: action, Status: constant.StatusSuccess}); err != nil && global.LOG != nil {
global.LOG.Errorf("API key audit failed: %v", err)
}
}
func (s *APIKeyService) AuditLegacyChange(c *gin.Context) {
owner, err := s.owner(c)
if err != nil {
if global.LOG != nil {
global.LOG.Errorf("legacy API key audit owner unavailable: %v", err)
}
return
}
recordAPIKeyEvent(c, owner, "legacy", "Legacy API Key", "update")
}
func (s *APIKeyService) revokeAPIKeyTerminals(c *gin.Context, owner auth.APIKeyOwner, id string) {
sessionID := "api-key:" + id
if id == "legacy" {
sessionID = terminalsession.APIAuthSessionID(owner.ID)
}
if err := terminalsession.RevokeWithRetry("auth_session", owner.ID, sessionID, s.RevokeTerminals); err != nil {
c.Set("API_KEY_TERMINAL_CLOSE_PENDING", true)
}
}
func (s *APIKeyService) PrepareAppBinding(c *gin.Context, id string) (*auth.APIKeyBinding, error) {
owner, err := s.owner(c)
if err != nil {
return nil, err
}
if id == "" {
id = "legacy"
}
if id == "legacy" {
item, config, _, fingerprint, err := s.legacy(owner)
if err != nil {
return nil, err
}
if config.ApiKey == "" || config.ApiInterfaceStatus != constant.StatusEnable {
return nil, buserr.New("ErrApiConfigStatusInvalid")
}
if !item.AllowAppBinding {
return nil, buserr.New("ErrAPIKeyAppBindingDisabled")
}
return &auth.APIKeyBinding{KeyID: id, KeyName: item.Name, Owner: owner, Revision: item.Revision, Fingerprint: fingerprint}, nil
}
key, err := repo.NewAPIKeyRepo().Get(id)
if err != nil || key.OwnerID != owner.ID || key.OwnerType != owner.Type {
return nil, buserr.New("ErrAPIKeyNotFound")
}
if err = auth.ValidateAPIKeyState(key, time.Now()); err != nil {
return nil, err
}
if !key.AllowAppBinding {
return nil, buserr.New("ErrAPIKeyAppBindingDisabled")
}
return &auth.APIKeyBinding{KeyID: key.ID, KeyName: key.Name, Owner: owner, Revision: key.Revision}, nil
}
func (s *APIKeyService) ResolveAppBinding(c *gin.Context, binding auth.APIKeyBinding) (string, error) {
if s.Provider == nil {
return "", buserr.New("ErrApiConfigStatusInvalid")
}
owner, err := s.Provider.ResolveAPIKeyOwner(c, binding.Owner.Type, binding.Owner.ID)
if err != nil {
return "", buserr.New("ErrApiConfigStatusInvalid")
}
if binding.KeyID == "legacy" {
item, config, _, fingerprint, err := s.legacy(owner)
if err != nil {
return "", err
}
if binding.Fingerprint == "" || fingerprint != binding.Fingerprint || binding.Revision != item.Revision {
return "", buserr.New("ErrAPIKeyConflict")
}
if config.ApiKey == "" || config.ApiInterfaceStatus != constant.StatusEnable {
return "", buserr.New("ErrApiConfigStatusInvalid")
}
if !item.AllowAppBinding {
return "", buserr.New("ErrAPIKeyAppBindingDisabled")
}
if !auth.IsIPInWhiteList(auth.GetAPIClientIP(c, config.ApiTrustedProxies), config.IpWhiteList) {
return "", buserr.New("ErrApiConfigIPInvalid")
}
c.Set("API_AUTH_CLIENT_IP", auth.GetAPIClientIP(c, config.ApiTrustedProxies))
return config.ApiKey, nil
}
key, err := repo.NewAPIKeyRepo().Get(binding.KeyID)
if err != nil || key.OwnerID != owner.ID || key.OwnerType != owner.Type {
return "", buserr.New("ErrAPIKeyNotFound")
}
if key.Revision != binding.Revision {
return "", buserr.New("ErrAPIKeyConflict")
}
if err = auth.ValidateAPIKeyState(key, time.Now()); err != nil {
return "", err
}
if !key.AllowAppBinding {
return "", buserr.New("ErrAPIKeyAppBindingDisabled")
}
if !auth.IsIPInWhiteList(auth.GetAPIClientIP(c, key.APITrustedProxies), key.IPWhiteList) {
return "", buserr.New("ErrApiConfigIPInvalid")
}
c.Set("API_AUTH_CLIENT_IP", auth.GetAPIClientIP(c, key.APITrustedProxies))
return auth.APIKeySecret(key)
}