mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-11-15 08:05:47 +00:00
fix: restrict expired sessions and credential expansion (#14346)
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user