| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166 |
- package middleware
- import (
- "context"
- "errors"
- "net/http"
- "net/http/httptest"
- "testing"
- "github.com/Wei-Shaw/sub2api/internal/service"
- "github.com/gin-gonic/gin"
- "github.com/stretchr/testify/require"
- )
- type stubStepUpGrantChecker struct {
- granted bool
- err error
- }
- func (s stubStepUpGrantChecker) HasStepUpGrant(ctx context.Context, userID int64, sessionKey string) (bool, error) {
- return s.granted, s.err
- }
- type stubStepUpUserReader struct {
- user *service.User
- err error
- }
- func (s stubStepUpUserReader) GetByID(ctx context.Context, id int64) (*service.User, error) {
- return s.user, s.err
- }
- type stubStepUpSettingReader struct {
- enabled bool
- }
- func (s stubStepUpSettingReader) IsStepUpEnabled(ctx context.Context) bool {
- return s.enabled
- }
- // stepUpEnabled 功能开关开启的设置桩,供既有门控分支测试使用。
- var stepUpEnabled = stubStepUpSettingReader{enabled: true}
- func newStepUpTestContext(t *testing.T) (*gin.Context, *httptest.ResponseRecorder) {
- t.Helper()
- gin.SetMode(gin.TestMode)
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/sensitive", nil)
- return c, rec
- }
- func TestEnforceStepUpRejectsAdminAPIKey(t *testing.T) {
- c, rec := newStepUpTestContext(t)
- c.Set("auth_method", service.AuditAuthMethodAdminAPIKey)
- ok := enforceStepUp(c, stubStepUpGrantChecker{granted: true}, stubStepUpUserReader{user: &service.User{TotpEnabled: true}}, stepUpEnabled)
- require.False(t, ok)
- require.True(t, c.IsAborted())
- require.Equal(t, http.StatusForbidden, rec.Code)
- require.Contains(t, rec.Body.String(), "STEP_UP_ADMIN_API_KEY_FORBIDDEN")
- }
- func TestEnforceStepUpRequiresAuthSubject(t *testing.T) {
- c, rec := newStepUpTestContext(t)
- ok := enforceStepUp(c, stubStepUpGrantChecker{granted: true}, stubStepUpUserReader{user: &service.User{TotpEnabled: true}}, stepUpEnabled)
- require.False(t, ok)
- require.Equal(t, http.StatusUnauthorized, rec.Code)
- }
- func TestEnforceStepUpRequiresTotpEnabled(t *testing.T) {
- c, rec := newStepUpTestContext(t)
- c.Set(string(ContextKeyUser), AuthSubject{UserID: 1})
- ok := enforceStepUp(c, stubStepUpGrantChecker{granted: true}, stubStepUpUserReader{user: &service.User{ID: 1, TotpEnabled: false}}, stepUpEnabled)
- require.False(t, ok)
- require.Equal(t, http.StatusForbidden, rec.Code)
- require.Contains(t, rec.Body.String(), "STEP_UP_TOTP_NOT_ENABLED")
- }
- func TestEnforceStepUpFailsClosedOnGrantError(t *testing.T) {
- c, rec := newStepUpTestContext(t)
- c.Set(string(ContextKeyUser), AuthSubject{UserID: 1})
- ok := enforceStepUp(c, stubStepUpGrantChecker{err: errors.New("redis down")}, stubStepUpUserReader{user: &service.User{ID: 1, TotpEnabled: true}}, stepUpEnabled)
- require.False(t, ok)
- require.Equal(t, http.StatusServiceUnavailable, rec.Code)
- require.Contains(t, rec.Body.String(), "STEP_UP_UNAVAILABLE")
- }
- func TestEnforceStepUpRequiresGrant(t *testing.T) {
- c, rec := newStepUpTestContext(t)
- c.Set(string(ContextKeyUser), AuthSubject{UserID: 1})
- ok := enforceStepUp(c, stubStepUpGrantChecker{granted: false}, stubStepUpUserReader{user: &service.User{ID: 1, TotpEnabled: true}}, stepUpEnabled)
- require.False(t, ok)
- require.Equal(t, http.StatusForbidden, rec.Code)
- require.Contains(t, rec.Body.String(), "STEP_UP_REQUIRED")
- }
- func TestEnforceStepUpPassesWithGrant(t *testing.T) {
- c, _ := newStepUpTestContext(t)
- c.Set(string(ContextKeyUser), AuthSubject{UserID: 1})
- ok := enforceStepUp(c, stubStepUpGrantChecker{granted: true}, stubStepUpUserReader{user: &service.User{ID: 1, TotpEnabled: true}}, stepUpEnabled)
- require.True(t, ok)
- require.False(t, c.IsAborted())
- }
- // 功能开关关闭时:不论 TOTP/grant/凭证类型,一律放行(恢复门控引入前行为)。
- func TestEnforceStepUpDisabledSkipsAllChecks(t *testing.T) {
- disabled := stubStepUpSettingReader{enabled: false}
- t.Run("no totp, no grant", func(t *testing.T) {
- c, _ := newStepUpTestContext(t)
- c.Set(string(ContextKeyUser), AuthSubject{UserID: 1})
- ok := enforceStepUp(c, stubStepUpGrantChecker{granted: false}, stubStepUpUserReader{user: &service.User{ID: 1, TotpEnabled: false}}, disabled)
- require.True(t, ok)
- require.False(t, c.IsAborted())
- })
- t.Run("admin api key", func(t *testing.T) {
- c, _ := newStepUpTestContext(t)
- c.Set("auth_method", service.AuditAuthMethodAdminAPIKey)
- ok := enforceStepUp(c, stubStepUpGrantChecker{granted: false}, stubStepUpUserReader{user: nil, err: errors.New("should not be called")}, disabled)
- require.True(t, ok)
- require.False(t, c.IsAborted())
- })
- }
- // settings 为 nil 时保持门控(fail-closed),避免装配缺陷静默关闭安全控制。
- func TestEnforceStepUpNilSettingsFailsClosed(t *testing.T) {
- c, rec := newStepUpTestContext(t)
- c.Set(string(ContextKeyUser), AuthSubject{UserID: 1})
- ok := enforceStepUp(c, stubStepUpGrantChecker{granted: false}, stubStepUpUserReader{user: &service.User{ID: 1, TotpEnabled: true}}, nil)
- require.False(t, ok)
- require.Equal(t, http.StatusForbidden, rec.Code)
- require.Contains(t, rec.Body.String(), "STEP_UP_REQUIRED")
- }
- // EnforceStepUp 收到 nil *service.SettingService 时不得因 typed-nil 装箱绕过门控:
- // 未认证请求仍应被拦截(401),而不是当作"开关关闭"放行。
- func TestEnforceStepUpTypedNilSettingServiceFailsClosed(t *testing.T) {
- require.Nil(t, stepUpSettingsOrNil(nil))
- c, rec := newStepUpTestContext(t)
- ok := EnforceStepUp(c, nil, nil, nil)
- require.False(t, ok)
- require.Equal(t, http.StatusUnauthorized, rec.Code)
- }
|