fix:修复租户停用后令牌还能用的bug

This commit is contained in:
chengqian committed 2026-07-27 17:45:47 +08:00
1 parent 17d8fadad7
commit d14e5b00e1
5 files changed
+135 -22

No files matched your search

+41
View File
@@ -0,0 +1,41 @@
package common
import (
"context"
"github.com/qianfree/team-api/internal/consts"
"github.com/qianfree/team-api/internal/dao"
)
// TenantPrincipal contains the current authorization state for a tenant user.
type TenantPrincipal struct {
Role string `json:"role"`
UserStatus string `json:"user_status"`
TenantStatus string `json:"tenant_status"`
}
// LoadTenantPrincipal loads a user's current role and both account statuses.
func LoadTenantPrincipal(ctx context.Context, userID, tenantID int64) (*TenantPrincipal, error) {
var principal *TenantPrincipal
err := dao.TntUsers.Ctx(ctx).As("u").
InnerJoin("tnt_tenants t ON t.id = u.tenant_id").
Where("u.id", userID).
Where("u.tenant_id", tenantID).
Fields("u.role, u.status AS user_status, t.status AS tenant_status").
Scan(&principal)
if err = IgnoreScanNoRows(err); err != nil {
return nil, err
}
return principal, nil
}
// ValidateTenantPrincipal verifies that both the user and tenant are active.
func ValidateTenantPrincipal(principal *TenantPrincipal) error {
if principal == nil || principal.UserStatus != "active" {
return consts.ErrUnauthorized
}
if principal.TenantStatus != "active" {
return consts.ErrTenantSuspended
}
return nil
}
@@ -0,0 +1,37 @@
package common
import (
"errors"
"testing"
"github.com/qianfree/team-api/internal/consts"
)
func TestValidateTenantPrincipal(t *testing.T) {
tests := []struct {
name string
principal *TenantPrincipal
wantErr error
}{
{name: "missing principal", wantErr: consts.ErrUnauthorized},
{name: "disabled user", principal: &TenantPrincipal{UserStatus: "disabled", TenantStatus: "active"}, wantErr: consts.ErrUnauthorized},
{name: "suspended tenant", principal: &TenantPrincipal{UserStatus: "active", TenantStatus: "suspended"}, wantErr: consts.ErrTenantSuspended},
{name: "closed tenant", principal: &TenantPrincipal{UserStatus: "active", TenantStatus: "closed"}, wantErr: consts.ErrTenantSuspended},
{name: "active principal", principal: &TenantPrincipal{UserStatus: "active", TenantStatus: "active"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateTenantPrincipal(tt.principal)
if tt.wantErr == nil {
if err != nil {
t.Fatalf("ValidateTenantPrincipal() error = %v", err)
}
return
}
if !errors.Is(err, tt.wantErr) {
t.Fatalf("ValidateTenantPrincipal() error = %v, want %v", err, tt.wantErr)
}
})
}
}
+12 -11
View File
@@ -564,6 +564,17 @@ func (s *sTenant) Refresh(ctx context.Context, req *v1.TenantRefreshReq) (*v1.Te
return nil, common.NewBusinessError(consts.CodeTokenRevoked, consts.MsgTokenRevoked)
}
principal, err := common.LoadTenantPrincipal(ctx, session.UserId, session.TenantId)
if err != nil {
return nil, err
}
if err = common.ValidateTenantPrincipal(principal); err != nil {
if err == consts.ErrTenantSuspended {
return nil, common.NewBusinessError(consts.CodeTenantSuspended, consts.MsgTenantSuspended)
}
return nil, common.NewUnauthorizedError(consts.MsgUnauthorized)
}
newRefreshToken, err := common.GenerateRefreshToken()
if err != nil {
return nil, err
@@ -577,17 +588,7 @@ func (s *sTenant) Refresh(ctx context.Context, req *v1.TenantRefreshReq) (*v1.Te
return nil, err
}
// Fetch current role from user table
var tntUser *entity.TntUsers
err = dao.TntUsers.Ctx(ctx).Where("id", session.UserId).Fields("role").Scan(&tntUser)
if err = common.IgnoreScanNoRows(err); err != nil {
return nil, err
}
if tntUser == nil {
return nil, common.NewUnauthorizedError("用户不存在")
}
tokenPair, err := common.GenerateTokenPair(ctx, session.UserId, "tenant", tntUser.Role, session.TenantId, session.Id, session.Jti)
tokenPair, err := common.GenerateTokenPair(ctx, session.UserId, "tenant", principal.Role, session.TenantId, session.Id, session.Jti)
if err != nil {
return nil, err
}
+9 -11
View File
@@ -1,13 +1,11 @@
package middleware
import (
"errors"
"strings"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/qianfree/team-api/internal/dao"
"github.com/qianfree/team-api/internal/model/entity"
"github.com/qianfree/team-api/internal/consts"
"github.com/qianfree/team-api/internal/logic/common"
"github.com/qianfree/team-api/internal/response"
@@ -107,17 +105,17 @@ func TenantAuth(r *ghttp.Request) {
return
}
// Verify user still exists and is active
var user *entity.TntUsers
err = dao.TntUsers.Ctx(r.Context()).
Where("id", claims.UserID).
Fields("status").
Scan(&user)
// Verify the user and tenant are still active using current database state.
principal, err := common.LoadTenantPrincipal(r.Context(), claims.UserID, claims.TenantID)
if err != nil {
response.ErrorMsg(r, consts.CodeUnauthorized, consts.MsgUnauthorized)
return
}
if user == nil || user.Status != "active" {
if err = common.ValidateTenantPrincipal(principal); err != nil {
if errors.Is(err, consts.ErrTenantSuspended) {
response.ErrorWithCode(r, 403, consts.CodeTenantSuspended, consts.MsgTenantSuspended)
return
}
response.ErrorMsg(r, consts.CodeUnauthorized, consts.MsgUnauthorized)
return
}
@@ -125,7 +123,7 @@ func TenantAuth(r *ghttp.Request) {
// Set auth context
r.SetCtxVar(CtxKeyUserID, claims.UserID)
r.SetCtxVar(CtxKeyUserType, claims.UserType)
r.SetCtxVar(CtxKeyRole, claims.Role)
r.SetCtxVar(CtxKeyRole, principal.Role)
r.SetCtxVar(CtxKeyTenantID, claims.TenantID)
r.SetCtxVar(CtxKeySessionID, claims.SessionID)
r.SetCtxVar(CtxKeyJti, claims.ID)
+36
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"testing"
"github.com/qianfree/team-api/internal/consts"
admintest "github.com/qianfree/team-api/tests/integration/admin/testinfra"
"github.com/qianfree/team-api/tests/integration/tenant/testinfra"
)
@@ -127,6 +128,41 @@ func TestTenantTokenRefresh(t *testing.T) {
sessionsResp.AssertSuccess(t)
}
func TestSuspendedTenantExistingTokensAreRejected(t *testing.T) {
regResult := testinfra.RegisterTestTenant(t)
adminClient := admintest.GetAuthedClient(t)
setStatus := func(status string) {
t.Helper()
resp := adminClient.Put(fmt.Sprintf("/api/admin/tenants/%d/status", regResult.Tenant.ID), map[string]any{
"status": status,
})
resp.AssertSuccess(t)
}
setStatus("suspended")
defer setStatus("active")
tenantClient := admintest.NewAPIClient(testinfra.DefaultBaseURL).WithToken(regResult.AccessToken)
accessResp := tenantClient.Get("/api/tenant/auth/sessions", map[string]string{
"page": "1",
"page_size": "10",
})
accessResp.AssertHTTPStatus(t, 403)
accessResp.AssertError(t, consts.CodeTenantSuspended)
publicClient := admintest.NewAPIClient(testinfra.DefaultBaseURL)
refreshResp := publicClient.Post("/api/tenant/auth/refresh", map[string]any{
"refresh_token": regResult.RefreshToken,
})
refreshResp.AssertError(t, consts.CodeTenantSuspended)
setStatus("active")
refreshAfterActivation := publicClient.Post("/api/tenant/auth/refresh", map[string]any{
"refresh_token": regResult.RefreshToken,
})
refreshAfterActivation.AssertSuccess(t)
}
func TestTenantChangePassword(t *testing.T) {
regResult := testinfra.RegisterTestTenant(t)
client := admintest.NewAPIClient(testinfra.DefaultBaseURL).WithToken(regResult.AccessToken)