租户库共库分表实现
This commit is contained in:
@@ -196,7 +196,7 @@ func (a *audit) run() {
|
|||||||
a.testHTTP("5", "5.qr-resolve", "二维码解析", "POST", "/v1/im/qrcodes/resolve", map[string]any{"token": a.meQR, "purpose": a.meQRPurpose}, a.tokenB, true, []int{200}, nil)
|
a.testHTTP("5", "5.qr-resolve", "二维码解析", "POST", "/v1/im/qrcodes/resolve", map[string]any{"token": a.meQR, "purpose": a.meQRPurpose}, a.tokenB, true, []int{200}, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
a.testHTTP("6", "6.create", "创建群", "POST", "/v1/im/groups", map[string]any{"name": "联调群" + suffix, "description": "自动化测试群", "member_user_ids": []string{a.userB}, "avatar_file_id": ""}, a.tokenA, true, []int{200}, func(r imResp) string {
|
a.testHTTP("6", "6.create", "创建群", "POST", "/v1/im/groups", map[string]any{"name": "auditgroup" + suffix, "description": "audit group", "member_user_ids": []string{a.userB}, "avatar_file_id": ""}, a.tokenA, true, []int{200}, func(r imResp) string {
|
||||||
a.groupTopic = asString(r.Data["topic"])
|
a.groupTopic = asString(r.Data["topic"])
|
||||||
a.groupVer = asInt(r.Data["version"])
|
a.groupVer = asInt(r.Data["version"])
|
||||||
return fmt.Sprintf("topic=%s version=%d", a.groupTopic, a.groupVer)
|
return fmt.Sprintf("topic=%s version=%d", a.groupTopic, a.groupVer)
|
||||||
@@ -451,6 +451,7 @@ func (a *audit) wsRegister(username, phone, password, challenge, verification st
|
|||||||
"scheme": "basic",
|
"scheme": "basic",
|
||||||
"username": username,
|
"username": username,
|
||||||
"phone": phone,
|
"phone": phone,
|
||||||
|
"registration_type": 1,
|
||||||
"secret": secret,
|
"secret": secret,
|
||||||
"login": true,
|
"login": true,
|
||||||
"cred": []map[string]any{{
|
"cred": []map[string]any{{
|
||||||
|
|||||||
@@ -642,6 +642,7 @@ func (a *audit) register(username, phone, password string) (string, string, erro
|
|||||||
"scheme": "basic",
|
"scheme": "basic",
|
||||||
"username": username,
|
"username": username,
|
||||||
"phone": phone,
|
"phone": phone,
|
||||||
|
"registration_type": 1,
|
||||||
"secret": secret,
|
"secret": secret,
|
||||||
"login": true,
|
"login": true,
|
||||||
"cred": []map[string]any{{
|
"cred": []map[string]any{{
|
||||||
@@ -724,8 +725,8 @@ func (a *audit) createFriendship() (string, error) {
|
|||||||
|
|
||||||
func (a *audit) createGroup(suffix string) (string, error) {
|
func (a *audit) createGroup(suffix string) (string, error) {
|
||||||
status, r, err := a.call("POST", "/v1/im/groups", map[string]any{
|
status, r, err := a.call("POST", "/v1/im/groups", map[string]any{
|
||||||
"name": "WS协议群" + suffix,
|
"name": "auditgroup" + suffix,
|
||||||
"description": "WS audit group",
|
"description": "audit group",
|
||||||
"member_user_ids": []string{a.userB},
|
"member_user_ids": []string{a.userB},
|
||||||
"avatar_file_id": "",
|
"avatar_file_id": "",
|
||||||
}, a.tokenA)
|
}, a.tokenA)
|
||||||
@@ -829,6 +830,7 @@ func (a *audit) openBasicLoggedIn(name, username, password string) (*wsClient, e
|
|||||||
if err = c.write(map[string]any{"login": map[string]any{
|
if err = c.write(map[string]any{"login": map[string]any{
|
||||||
"id": loginID,
|
"id": loginID,
|
||||||
"scheme": "basic",
|
"scheme": "basic",
|
||||||
|
"login_type": 1,
|
||||||
"secret": secret,
|
"secret": secret,
|
||||||
}}); err != nil {
|
}}); err != nil {
|
||||||
_ = c.conn.Close()
|
_ = c.conn.Close()
|
||||||
|
|||||||
+58
-6
@@ -4294,7 +4294,7 @@ type verificationCodeCache struct {
|
|||||||
Action string `json:"action,omitempty"`
|
Action string `json:"action,omitempty"`
|
||||||
PhoneHash string `json:"phone_hash"`
|
PhoneHash string `json:"phone_hash"`
|
||||||
PhoneMask string `json:"phone_mask,omitempty"`
|
PhoneMask string `json:"phone_mask,omitempty"`
|
||||||
TargetUser types.Uid `json:"target_user,omitempty"`
|
TargetUser string `json:"target_user,omitempty"`
|
||||||
CodeHash string `json:"code_hash"`
|
CodeHash string `json:"code_hash"`
|
||||||
VerificationTokenHash *string `json:"verification_token_hash,omitempty"`
|
VerificationTokenHash *string `json:"verification_token_hash,omitempty"`
|
||||||
State types.VerificationState `json:"state"`
|
State types.VerificationState `json:"state"`
|
||||||
@@ -4320,7 +4320,7 @@ func verificationCodeCacheFromType(vc *types.VerificationCode) verificationCodeC
|
|||||||
Action: vc.Action,
|
Action: vc.Action,
|
||||||
PhoneHash: vc.PhoneHash,
|
PhoneHash: vc.PhoneHash,
|
||||||
PhoneMask: vc.PhoneMask,
|
PhoneMask: vc.PhoneMask,
|
||||||
TargetUser: vc.TargetUser,
|
TargetUser: vc.TargetUser.UserId(),
|
||||||
CodeHash: vc.CodeHash,
|
CodeHash: vc.CodeHash,
|
||||||
VerificationTokenHash: vc.VerificationTokenHash,
|
VerificationTokenHash: vc.VerificationTokenHash,
|
||||||
State: vc.State,
|
State: vc.State,
|
||||||
@@ -4344,7 +4344,7 @@ func (vc verificationCodeCache) toType() *types.VerificationCode {
|
|||||||
Action: vc.Action,
|
Action: vc.Action,
|
||||||
PhoneHash: vc.PhoneHash,
|
PhoneHash: vc.PhoneHash,
|
||||||
PhoneMask: vc.PhoneMask,
|
PhoneMask: vc.PhoneMask,
|
||||||
TargetUser: vc.TargetUser,
|
TargetUser: types.ParseUserId(vc.TargetUser),
|
||||||
CodeHash: vc.CodeHash,
|
CodeHash: vc.CodeHash,
|
||||||
VerificationTokenHash: vc.VerificationTokenHash,
|
VerificationTokenHash: vc.VerificationTokenHash,
|
||||||
State: vc.State,
|
State: vc.State,
|
||||||
@@ -4405,6 +4405,58 @@ type UserSecurityPersistenceInterface interface {
|
|||||||
|
|
||||||
type userSecurityMapper struct{}
|
type userSecurityMapper struct{}
|
||||||
|
|
||||||
|
type actionTokenCache struct {
|
||||||
|
ID int64 `json:"id,omitempty"`
|
||||||
|
TenantID types.TenantID `json:"tenant_id"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
TokenID string `json:"token_id"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
Target string `json:"target,omitempty"`
|
||||||
|
TokenHash string `json:"token_hash"`
|
||||||
|
State int16 `json:"state"`
|
||||||
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
|
ConsumedAt *time.Time `json:"consumed_at,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func actionTokenCacheFromType(token *types.ActionToken) actionTokenCache {
|
||||||
|
if token == nil {
|
||||||
|
return actionTokenCache{}
|
||||||
|
}
|
||||||
|
return actionTokenCache{
|
||||||
|
ID: token.ID,
|
||||||
|
TenantID: token.TenantID,
|
||||||
|
UserID: token.UserID.UserId(),
|
||||||
|
TokenID: token.TokenID,
|
||||||
|
Action: token.Action,
|
||||||
|
Target: token.Target,
|
||||||
|
TokenHash: token.TokenHash,
|
||||||
|
State: token.State,
|
||||||
|
ExpiresAt: token.ExpiresAt,
|
||||||
|
ConsumedAt: token.ConsumedAt,
|
||||||
|
CreatedAt: token.CreatedAt,
|
||||||
|
UpdatedAt: token.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (token actionTokenCache) toType() *types.ActionToken {
|
||||||
|
return &types.ActionToken{
|
||||||
|
ID: token.ID,
|
||||||
|
TenantID: token.TenantID,
|
||||||
|
UserID: types.ParseUserId(token.UserID),
|
||||||
|
TokenID: token.TokenID,
|
||||||
|
Action: token.Action,
|
||||||
|
Target: token.Target,
|
||||||
|
TokenHash: token.TokenHash,
|
||||||
|
State: token.State,
|
||||||
|
ExpiresAt: token.ExpiresAt,
|
||||||
|
ConsumedAt: token.ConsumedAt,
|
||||||
|
CreatedAt: token.CreatedAt,
|
||||||
|
UpdatedAt: token.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Security is a singleton for user security metadata.
|
// Security is a singleton for user security metadata.
|
||||||
var Security UserSecurityPersistenceInterface
|
var Security UserSecurityPersistenceInterface
|
||||||
|
|
||||||
@@ -4461,7 +4513,7 @@ func (userSecurityMapper) CreateActionToken(tenantID types.TenantID, token *type
|
|||||||
now := types.TimeNow()
|
now := types.TimeNow()
|
||||||
token.CreatedAt = now
|
token.CreatedAt = now
|
||||||
token.UpdatedAt = now
|
token.UpdatedAt = now
|
||||||
return CacheObject(tenantID, cacheNamespaceActionToken, actionTokenCacheID(token.UserID, token.TokenID, token.Action, token.Target), token)
|
return CacheObject(tenantID, cacheNamespaceActionToken, actionTokenCacheID(token.UserID, token.TokenID, token.Action, token.Target), actionTokenCacheFromType(token))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (userSecurityMapper) ConsumeActionToken(tenantID types.TenantID, uid types.Uid, token, action, target string, now time.Time) (*types.ActionToken, error) {
|
func (userSecurityMapper) ConsumeActionToken(tenantID types.TenantID, uid types.Uid, token, action, target string, now time.Time) (*types.ActionToken, error) {
|
||||||
@@ -4476,7 +4528,7 @@ func (userSecurityMapper) ConsumeActionToken(tenantID types.TenantID, uid types.
|
|||||||
return nil, types.ErrMalformed
|
return nil, types.ErrMalformed
|
||||||
}
|
}
|
||||||
cacheID := actionTokenCacheID(uid, tokenID, action, target)
|
cacheID := actionTokenCacheID(uid, tokenID, action, target)
|
||||||
var cached types.ActionToken
|
var cached actionTokenCache
|
||||||
ok, err := GetCachedObject(tenantID, cacheNamespaceActionToken, cacheID, &cached)
|
ok, err := GetCachedObject(tenantID, cacheNamespaceActionToken, cacheID, &cached)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -4490,7 +4542,7 @@ func (userSecurityMapper) ConsumeActionToken(tenantID types.TenantID, uid types.
|
|||||||
if err = DeleteCachedObject(tenantID, cacheNamespaceActionToken, cacheID); err != nil {
|
if err = DeleteCachedObject(tenantID, cacheNamespaceActionToken, cacheID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &cached, nil
|
return cached.toType(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (userSecurityMapper) GetIndependentPassword(tenantID types.TenantID, uid types.Uid, purpose string) (*types.IndependentPassword, error) {
|
func (userSecurityMapper) GetIndependentPassword(tenantID types.TenantID, uid types.Uid, purpose string) (*types.IndependentPassword, error) {
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ func TestVerificationCodeCachePreservesInternalFields(t *testing.T) {
|
|||||||
Action: "signup",
|
Action: "signup",
|
||||||
PhoneHash: "phone-hash",
|
PhoneHash: "phone-hash",
|
||||||
PhoneMask: "+861****000",
|
PhoneMask: "+861****000",
|
||||||
|
TargetUser: types.ParseUserId("usrR25C_ZgHzyg"),
|
||||||
CodeHash: "code-hash",
|
CodeHash: "code-hash",
|
||||||
VerificationTokenHash: &tokenHash,
|
VerificationTokenHash: &tokenHash,
|
||||||
State: types.VerificationStateVerified,
|
State: types.VerificationStateVerified,
|
||||||
@@ -81,6 +82,7 @@ func TestVerificationCodeCachePreservesInternalFields(t *testing.T) {
|
|||||||
}
|
}
|
||||||
if !strings.Contains(string(cachePayload), `"code_hash":"code-hash"`) ||
|
if !strings.Contains(string(cachePayload), `"code_hash":"code-hash"`) ||
|
||||||
!strings.Contains(string(cachePayload), `"phone_hash":"phone-hash"`) ||
|
!strings.Contains(string(cachePayload), `"phone_hash":"phone-hash"`) ||
|
||||||
|
!strings.Contains(string(cachePayload), `"target_user":"usrR25C_ZgHzyg"`) ||
|
||||||
!strings.Contains(string(cachePayload), `"max_attempts":5`) {
|
!strings.Contains(string(cachePayload), `"max_attempts":5`) {
|
||||||
t.Fatalf("cache verification JSON missing internal fields: %s", cachePayload)
|
t.Fatalf("cache verification JSON missing internal fields: %s", cachePayload)
|
||||||
}
|
}
|
||||||
@@ -91,8 +93,55 @@ func TestVerificationCodeCachePreservesInternalFields(t *testing.T) {
|
|||||||
}
|
}
|
||||||
got := cached.toType()
|
got := cached.toType()
|
||||||
if got.CodeHash != vc.CodeHash || got.PhoneHash != vc.PhoneHash ||
|
if got.CodeHash != vc.CodeHash || got.PhoneHash != vc.PhoneHash ||
|
||||||
got.State != vc.State || got.Attempts != vc.Attempts || got.MaxAttempts != vc.MaxAttempts ||
|
got.TargetUser != vc.TargetUser || got.State != vc.State || got.Attempts != vc.Attempts || got.MaxAttempts != vc.MaxAttempts ||
|
||||||
got.VerificationTokenHash == nil || *got.VerificationTokenHash != tokenHash {
|
got.VerificationTokenHash == nil || *got.VerificationTokenHash != tokenHash {
|
||||||
t.Fatalf("cache round trip lost fields: got %#v", got)
|
t.Fatalf("cache round trip lost fields: got %#v", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestActionTokenCachePreservesInternalFields(t *testing.T) {
|
||||||
|
now := time.Now().UTC().Round(time.Millisecond)
|
||||||
|
consumedAt := now.Add(time.Minute)
|
||||||
|
token := &types.ActionToken{
|
||||||
|
ID: 9,
|
||||||
|
TenantID: 7,
|
||||||
|
UserID: types.ParseUserId("usrR25C_ZgHzyg"),
|
||||||
|
TokenID: "token-id",
|
||||||
|
Action: "close_youth_mode",
|
||||||
|
Target: "youth_mode",
|
||||||
|
TokenHash: "token-hash",
|
||||||
|
State: 1,
|
||||||
|
ExpiresAt: now.Add(5 * time.Minute),
|
||||||
|
ConsumedAt: &consumedAt,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: consumedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
publicPayload, err := json.Marshal(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if strings.Contains(string(publicPayload), "token-hash") || strings.Contains(string(publicPayload), "token-id") {
|
||||||
|
t.Fatalf("public action token JSON leaked internal fields: %s", publicPayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
cachePayload, err := json.Marshal(actionTokenCacheFromType(token))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(cachePayload), `"token_hash":"token-hash"`) ||
|
||||||
|
!strings.Contains(string(cachePayload), `"token_id":"token-id"`) ||
|
||||||
|
!strings.Contains(string(cachePayload), `"state":1`) {
|
||||||
|
t.Fatalf("cache action token JSON missing internal fields: %s", cachePayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cached actionTokenCache
|
||||||
|
if err := json.Unmarshal(cachePayload, &cached); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got := cached.toType()
|
||||||
|
if got.TokenHash != token.TokenHash || got.TokenID != token.TokenID ||
|
||||||
|
got.UserID != token.UserID || got.State != token.State || got.Action != token.Action {
|
||||||
|
t.Fatalf("cache round trip lost fields: got %#v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user