fix:修复租户停用后令牌还能用的bug
This commit is contained in:
1 parent
17d8fadad7
commit
d14e5b00e1
5 files changed
+135
-22
No files matched your search
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user