fix: restrict expired sessions and credential expansion (#14346)

This commit is contained in:
蘭
2026-11-08 22:40:12 +08:00
committed by GitHub
parent ab90f4bc2d
commit 038fe1625a
5 changed files with 131 additions and 41 deletions
+4
View File
@@ -35,6 +35,10 @@ func bindAPIKeyRequest(c *gin.Context, value interface{}) bool {
func apiKeyError(c *gin.Context, err error) {
var business buserr.BusinessError
if errors.As(err, &business) {
if business.Msg == "ErrPasswordExpired" {
helper.ErrorWithDetail(c, 313, business.Msg, nil)
return
}
helper.ErrorWithDetail(c, http.StatusBadRequest, business.Msg, business.Err)
return
}
+12
View File
@@ -408,6 +408,10 @@ func (b *BaseApi) GenerateApiKey(c *gin.Context) {
helper.BadAuth(c, "ErrApiConfigDisable", nil)
return
}
if err := appauth.RequireUnexpiredPassword(c, xpack.AuthProvider); err != nil {
apiKeyError(c, err)
return
}
apiKey, err := xpack.AuthProvider.GenerateApiKey(c)
if err != nil {
helper.InternalServer(c, err)
@@ -433,6 +437,10 @@ func (b *BaseApi) UpdateApiConfig(c *gin.Context) {
helper.BadAuth(c, "ErrApiConfigDisable", nil)
return
}
if err := appauth.RequireUnexpiredPassword(c, xpack.AuthProvider); err != nil {
apiKeyError(c, err)
return
}
var req dto.ApiInterfaceConfig
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
@@ -466,6 +474,10 @@ func (b *BaseApi) GetCurrentUser(c *gin.Context) {
}
if c.GetBool("API_AUTH") || appauth.HasAPICredentials(c) {
userInfo.ApiKey = ""
} else if expired, policyErr := appauth.IsPasswordExpired(c, xpack.AuthProvider); expired || policyErr != nil {
// Bootstrap must remain usable for password recovery. Policy failures
// never disclose a reusable legacy secret through this read endpoint.
userInfo.ApiKey = ""
}
c.Header("Cache-Control", "no-store")
helper.SuccessWithData(c, userInfo)
+59
View File
@@ -0,0 +1,59 @@
package auth
import (
"errors"
"strconv"
"time"
"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/utils/common"
"github.com/gin-gonic/gin"
)
type PasswordExpirationProvider interface {
ShouldCheckPasswordExpiration(*gin.Context) (bool, error)
LoadPasswordExpirationTime(*gin.Context) (string, error)
}
func IsPasswordExpired(c *gin.Context, provider PasswordExpirationProvider) (bool, error) {
daysText, err := repo.NewISettingRepo().GetValueByKey("ExpirationDays")
if err != nil {
return false, err
}
days, err := strconv.Atoi(daysText)
if err != nil || days < 0 {
return false, errors.New("invalid password expiration policy")
}
if days == 0 {
return false, nil
}
if provider == nil {
return false, errors.New("password expiration provider unavailable")
}
check, err := provider.ShouldCheckPasswordExpiration(c)
if err != nil || !check {
return false, err
}
deadline, err := provider.LoadPasswordExpirationTime(c)
if err != nil {
return false, err
}
expiresAt, err := time.ParseInLocation(constant.DateTimeLayout, deadline, common.LoadExpiredLocation())
if err != nil {
return false, err
}
return !time.Now().Before(expiresAt), nil
}
func RequireUnexpiredPassword(c *gin.Context, provider PasswordExpirationProvider) error {
expired, err := IsPasswordExpired(c, provider)
if err != nil {
return err
}
if expired {
return buserr.New("ErrPasswordExpired")
}
return nil
}
+22
View File
@@ -52,6 +52,11 @@ func (s *APIKeyService) owner(c *gin.Context) (auth.APIKeyOwner, error) {
return s.Provider.CurrentAPIKeyOwner(c)
}
func (s *APIKeyService) requireUnexpiredOwnerPassword(c *gin.Context) error {
policy, _ := s.Provider.(auth.PasswordExpirationProvider)
return auth.RequireUnexpiredPassword(c, policy)
}
func (s *APIKeyService) managementOwner(c *gin.Context) (auth.APIKeyOwner, error) {
if _, err := auth.RequireAPIKeyManagement(c); err != nil {
return auth.APIKeyOwner{}, err
@@ -171,6 +176,9 @@ func (s *APIKeyService) Create(c *gin.Context, req dto.APIKeyCreate) (*dto.APIKe
if err != nil {
return nil, err
}
if err := s.requireUnexpiredOwnerPassword(c); err != nil {
return nil, err
}
if req.RequestID == "" {
req.RequestID = uuid.NewString()
} else if _, err = uuid.Parse(req.RequestID); err != nil {
@@ -249,6 +257,9 @@ func (s *APIKeyService) Update(c *gin.Context, req dto.APIKeyUpdate) error {
if err != nil {
return err
}
if err := s.requireUnexpiredOwnerPassword(c); err != nil {
return err
}
if req.ID == "legacy" {
return s.updateLegacy(c, owner, req.APIKeyMutation, &req.APIKeyFields, "update", "")
}
@@ -280,6 +291,11 @@ func (s *APIKeyService) Status(c *gin.Context, req dto.APIKeyStatus) error {
if req.Status != constant.StatusEnable && req.Status != constant.StatusDisable {
return buserr.New("ErrInvalidParams")
}
if req.Status == constant.StatusEnable {
if err := s.requireUnexpiredOwnerPassword(c); err != nil {
return err
}
}
action := "disable"
if req.Status == constant.StatusEnable {
action = "enable"
@@ -343,6 +359,9 @@ func (s *APIKeyService) Reveal(c *gin.Context, req dto.APIKeyMutation) (*dto.API
if err != nil {
return nil, err
}
if err := s.requireUnexpiredOwnerPassword(c); err != nil {
return nil, err
}
if req.ID == "" || req.ID == "legacy" || req.Revision == 0 {
return nil, buserr.New("ErrInvalidParams")
}
@@ -511,6 +530,9 @@ func (s *APIKeyService) PrepareAppBinding(c *gin.Context, id string) (*auth.APIK
if err != nil {
return nil, err
}
if err := s.requireUnexpiredOwnerPassword(c); err != nil {
return nil, err
}
if id == "" {
id = "legacy"
}
+34 -41
View File
@@ -2,17 +2,13 @@ package middleware
import (
"net/http"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/core/app/api/v2/helper"
baseRepo "github.com/1Panel-dev/1Panel/core/app/repo"
"github.com/1Panel-dev/1Panel/core/app/auth"
"github.com/1Panel-dev/1Panel/core/buserr"
"github.com/1Panel-dev/1Panel/core/constant"
"github.com/1Panel-dev/1Panel/core/global"
psessionUtils "github.com/1Panel-dev/1Panel/core/init/session/psession"
"github.com/1Panel-dev/1Panel/core/utils/common"
"github.com/1Panel-dev/1Panel/core/utils/xpack"
"github.com/gin-gonic/gin"
)
@@ -31,15 +27,7 @@ func PasswordExpired() gin.HandlerFunc {
c.Next()
return
}
if strings.HasPrefix(c.Request.URL.Path, "/api/v2/core/auth") ||
c.Request.URL.Path == "/api/v2/core/settings/search" ||
c.Request.URL.Path == "/api/v2/core/settings/search/base" ||
c.Request.URL.Path == "/api/v2/core/xpack/settings/search" ||
c.Request.URL.Path == "/api/v2/core/xapp/verifyQRCode" ||
c.Request.URL.Path == "/api/v2/core/enterprise/users/info" ||
c.Request.URL.Path == "/api/v2/core/enterprise/licenses/info" ||
c.Request.URL.Path == "/api/v2/core/enterprise/licenses/status" ||
c.Request.URL.Path == "/api/v2/core/enterprise/licenses/upload" {
if passwordExpirationRecoveryRoute(c.Request.Method, c.Request.URL.Path) {
c.Next()
return
}
@@ -67,41 +55,46 @@ func PasswordExpired() gin.HandlerFunc {
helper.BadAuth(c, "ErrNotLogin", err)
return
}
settingRepo := baseRepo.NewISettingRepo()
expirationDays, err := settingRepo.GetValueByKey("ExpirationDays")
if err != nil {
helper.ErrorWithDetail(c, http.StatusInternalServerError, "ErrPasswordExpired", err)
return
}
expiredDays, _ := strconv.Atoi(expirationDays)
if expiredDays == 0 {
c.Next()
return
}
shouldCheck, err := xpack.AuthProvider.ShouldCheckPasswordExpiration(c)
if err != nil {
helper.ErrorWithDetail(c, http.StatusInternalServerError, "ErrPasswordExpired", err)
return
}
if !shouldCheck {
c.Next()
return
}
expirationTime, err := xpack.AuthProvider.LoadPasswordExpirationTime(c)
if err != nil {
helper.ErrorWithDetail(c, http.StatusInternalServerError, "ErrPasswordExpired", err)
return
}
expiredTime, err := time.ParseInLocation(constant.DateTimeLayout, expirationTime, common.LoadExpiredLocation())
expired, err := auth.IsPasswordExpired(c, xpack.AuthProvider)
if err != nil {
helper.ErrorWithDetail(c, http.StatusInternalServerError, "ErrPasswordExpired", err)
return
}
if time.Now().After(expiredTime) {
if expired {
helper.ErrorWithDetail(c, 313, "ErrPasswordExpired", err)
return
}
c.Next()
}
}
func passwordExpirationRecoveryRoute(method, path string) bool {
const core = "/api/v2/core"
switch method {
case http.MethodGet:
switch path {
case core + "/auth/captcha", core + "/auth/setting", core + "/auth/current", core + "/auth/ldap/status",
core + "/auth/oidc/status", core + "/auth/oidc/callback",
core + "/auth/saml2/status", core + "/auth/saml2/metadata", core + "/auth/saml2/logout",
core + "/xpack/settings/search", core + "/enterprise/users/info",
core + "/enterprise/licenses/info", core + "/enterprise/licenses/status":
return true
}
case http.MethodPost:
switch path {
case core + "/auth/login", core + "/auth/mfalogin", core + "/auth/logout",
core + "/auth/passkey/begin", core + "/auth/passkey/finish",
core + "/auth/current/update", core + "/auth/expired/reset",
core + "/auth/oidc/begin", core + "/auth/oidc/finish",
core + "/auth/saml2/begin", core + "/auth/saml2/acs", core + "/auth/saml2/finish", core + "/auth/saml2/logout",
core + "/settings/search/base", core + "/enterprise/licenses/upload", core + "/xapp/verifyQRCode",
core + "/auth/api/keys/search", core + "/auth/api/keys/status",
core + "/auth/api/keys/revoke", core + "/auth/api/keys/delete":
// The status service independently rejects Enable while allowing
// Disable. Metadata and credential revocation remain available.
return true
}
}
return false
}