From d14e5b00e17610eadf627a8aae5637c5421c304c Mon Sep 17 00:00:00 2001 From: chengqian Date: Mon, 27 Jul 2026 17:45:47 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E7=A7=9F=E6=88=B7?= =?UTF-8?q?=E5=81=9C=E7=94=A8=E5=90=8E=E4=BB=A4=E7=89=8C=E8=BF=98=E8=83=BD?= =?UTF-8?q?=E7=94=A8=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/logic/common/tenant_access.go | 41 +++++++++++++++++++++ internal/logic/common/tenant_access_test.go | 37 +++++++++++++++++++ internal/logic/tenant/auth.go | 23 ++++++------ internal/middleware/tenant_auth.go | 20 +++++----- tests/integration/tenant/auth_test.go | 36 ++++++++++++++++++ 5 files changed, 135 insertions(+), 22 deletions(-) create mode 100644 internal/logic/common/tenant_access.go create mode 100644 internal/logic/common/tenant_access_test.go diff --git a/internal/logic/common/tenant_access.go b/internal/logic/common/tenant_access.go new file mode 100644 index 0000000..cf1e616 --- /dev/null +++ b/internal/logic/common/tenant_access.go @@ -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 +} diff --git a/internal/logic/common/tenant_access_test.go b/internal/logic/common/tenant_access_test.go new file mode 100644 index 0000000..ca2efd3 --- /dev/null +++ b/internal/logic/common/tenant_access_test.go @@ -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) + } + }) + } +} diff --git a/internal/logic/tenant/auth.go b/internal/logic/tenant/auth.go index 01e36e6..1dfafcc 100644 --- a/internal/logic/tenant/auth.go +++ b/internal/logic/tenant/auth.go @@ -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 } diff --git a/internal/middleware/tenant_auth.go b/internal/middleware/tenant_auth.go index 9723d88..824a2d2 100644 --- a/internal/middleware/tenant_auth.go +++ b/internal/middleware/tenant_auth.go @@ -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) diff --git a/tests/integration/tenant/auth_test.go b/tests/integration/tenant/auth_test.go index 8058320..3d8e1d2 100644 --- a/tests/integration/tenant/auth_test.go +++ b/tests/integration/tenant/auth_test.go @@ -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)