mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-03 07:06:36 +08:00
fix(auth): register CAP scope, 400 on captcha, 403 for permission
Navigating from login reused a send_email_code token on register. Captcha failure used 401 so the client stored /register as the post-login target and never left the page. Permission denials now return 403, and the API client no longer wipes the session on 401.
This commit is contained in:
@@ -30,11 +30,9 @@ const (
|
||||
errExternalAccountBindingIncomplete = "外部帐号绑定信息不完整"
|
||||
errExternalAccountAlreadyBoundToAnother = "该外部帐号已被其他用户绑定"
|
||||
errExternalAccountBindingIDRequired = "外部帐号绑定记录 ID 不能为空"
|
||||
errAdminRequired = "无权访问"
|
||||
//nolint:gosec // error message, not hardcoded credentials
|
||||
errTokenAdminRequired = "令牌无管理员权限"
|
||||
errBannedAccount = "账号已被封禁"
|
||||
errUnAuthorized = "未登录"
|
||||
errInsufficientPermission = "权限不足"
|
||||
errBannedAccount = "账号已被封禁"
|
||||
errUnAuthorized = "未登录"
|
||||
)
|
||||
|
||||
// Service 层与鉴权中间件内部错误文案(保持与重构前逐字一致)
|
||||
|
||||
@@ -162,15 +162,13 @@ func AdminRequired() gin.HandlerFunc {
|
||||
isTokenAuth, _ := ginutil.GetFromContext[bool](c, contracts.AuthTokenAuthKey)
|
||||
isTokenAdmin, _ := ginutil.GetFromContext[bool](c, contracts.AuthTokenAdminKey)
|
||||
|
||||
// 如果是通过 Token 鉴权,要求该 Token 具备管理员权限或者用户本身为管理员
|
||||
// Logged-in but lacking admin permission is 403, not 401/404.
|
||||
if isTokenAuth && !isTokenAdmin && !user.IsAdmin {
|
||||
response.AbortNotFound(c, errTokenAdminRequired)
|
||||
response.AbortForbidden(c, errInsufficientPermission)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是通过 Session 鉴权,直接检查用户的 is_admin 属性
|
||||
if !isTokenAuth && !user.IsAdmin {
|
||||
response.AbortNotFound(c, errAdminRequired)
|
||||
response.AbortForbidden(c, errInsufficientPermission)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ type testUser struct {
|
||||
ID uint64 `gorm:"primaryKey"`
|
||||
Username string
|
||||
IsActive bool
|
||||
IsAdmin bool
|
||||
LastLoginAt time.Time
|
||||
}
|
||||
|
||||
|
||||
@@ -399,3 +399,30 @@ func TestAuthWhitelistMiddleware(t *testing.T) {
|
||||
engine.ServeHTTP(w2, req2)
|
||||
assert.Equal(t, http.StatusUnauthorized, w2.Code)
|
||||
}
|
||||
|
||||
func TestAdminRequiredReturnsForbiddenForLoggedInNonAdmin(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupTestDB(t)
|
||||
require.NoError(t, db.Create(&testUser{ID: 42, Username: "member", IsActive: true, IsAdmin: false}).Error)
|
||||
|
||||
svc := newTestAuthService(t, db)
|
||||
mw, ok := svc.RequireAdminMiddleware().(gin.HandlerFunc)
|
||||
require.True(t, ok)
|
||||
|
||||
engine := newSessionEngine()
|
||||
engine.Use(func(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
session.Set(auth.UserIDKey, uint64(42))
|
||||
require.NoError(t, session.Save())
|
||||
c.Next()
|
||||
})
|
||||
engine.Use(mw)
|
||||
engine.GET("/admin-only", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, response.OK("ok"))
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
engine.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/admin-only", nil))
|
||||
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "权限不足")
|
||||
}
|
||||
|
||||
@@ -17,19 +17,19 @@ func VerifyMiddleware(mgr *Manager, scope string) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
if mgr == nil {
|
||||
response.AbortUnauthorized(c, errCapTokenInvalidOrExpired)
|
||||
response.AbortBadRequest(c, errCapTokenInvalidOrExpired)
|
||||
return
|
||||
}
|
||||
|
||||
token := c.GetHeader("X-Cap-Token")
|
||||
if token == "" {
|
||||
response.AbortUnauthorized(c, errCapTokenMissing)
|
||||
response.AbortBadRequest(c, errCapTokenMissing)
|
||||
return
|
||||
}
|
||||
|
||||
valid, err := mgr.VerifyToken(c.Request.Context(), token, scope)
|
||||
if err != nil || !valid {
|
||||
response.AbortUnauthorized(c, errCapTokenInvalidOrExpired)
|
||||
response.AbortBadRequest(c, errCapTokenInvalidOrExpired)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cap
|
||||
|
||||
import (
|
||||
"Wavelet/pkg/response"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestVerifyMiddlewareMissingTokenIsBadRequest(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
restore := InstallTestRuntimeSettings(RuntimeSettings{LoginEnabled: true})
|
||||
t.Cleanup(restore)
|
||||
|
||||
engine := gin.New()
|
||||
engine.Use(response.ErrorHandlerMiddleware())
|
||||
engine.POST("/register", VerifyMiddleware(GetDefaultManager(), "register"), func(c *gin.Context) {
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/register", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
engine.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Errorf("VerifyMiddleware() status = %d, want %d", rec.Code, http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"Wavelet/core"
|
||||
"Wavelet/core/contracts"
|
||||
"Wavelet/core/extpoints"
|
||||
"Wavelet/pkg/ginutil"
|
||||
"Wavelet/plugins/domain/upload/filesrv"
|
||||
"Wavelet/plugins/domain/upload/handler"
|
||||
"Wavelet/plugins/domain/upload/shared"
|
||||
@@ -66,13 +67,25 @@ func (p *Plugin) Apply(ctx *core.Context) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
// 0. Resolve auth service for middleware
|
||||
var authSvc contracts.AuthService
|
||||
if err := core.Using[contracts.AuthService](ctx, func(svc contracts.AuthService) { authSvc = svc }); err != nil {
|
||||
return err
|
||||
denyAuth := ginutil.AuthUnavailable()
|
||||
loginMW := func(c *gin.Context) {
|
||||
if svc := shared.GetAuthService(c.Request.Context()); svc != nil {
|
||||
if mw, ok := svc.RequireAuthMiddleware().(gin.HandlerFunc); ok && mw != nil {
|
||||
mw(c)
|
||||
return
|
||||
}
|
||||
}
|
||||
denyAuth(c)
|
||||
}
|
||||
adminMW := func(c *gin.Context) {
|
||||
if svc := shared.GetAuthService(c.Request.Context()); svc != nil {
|
||||
if mw, ok := svc.RequireAdminMiddleware().(gin.HandlerFunc); ok && mw != nil {
|
||||
mw(c)
|
||||
return
|
||||
}
|
||||
}
|
||||
denyAuth(c)
|
||||
}
|
||||
loginMW := authSvc.RequireAuthMiddleware().(gin.HandlerFunc)
|
||||
adminMW := authSvc.RequireAdminMiddleware().(gin.HandlerFunc)
|
||||
|
||||
// 0a. Register migrations
|
||||
ctx.Migrations().Register("upload", uploadMigrations)
|
||||
|
||||
Reference in New Issue
Block a user