fix: sync remote authorization revoke logout

This commit is contained in:
iamxvbaba 2026-07-24 21:39:00 +08:00
parent b4aaf57d6b
commit 0dcfaf0a65
11 changed files with 216 additions and 142 deletions

View file

@ -1328,18 +1328,7 @@ func (s *Service) ResetAuthorization(ctx context.Context, userID, hash int64) (d
if revoker, ok := s.auths.(authorizationRevoker); ok {
return revoker.RevokeByHash(ctx, userID, hash)
}
target, found, err := s.authorizationByHash(ctx, userID, hash)
if err != nil || !found {
return target, found, err
}
if err := s.deleteAuthKey(ctx, target.AuthKeyID); err != nil {
return target, true, err
}
deleted, found, err := s.auths.DeleteByHash(ctx, userID, hash)
if err != nil || !found {
return deleted, found, err
}
return deleted, true, nil
return s.auths.DeleteByHash(ctx, userID, hash)
}
func (s *Service) ResetAuthorizations(ctx context.Context, userID int64, keepAuthKeyID [8]byte) ([]domain.Authorization, error) {
@ -1349,54 +1338,7 @@ func (s *Service) ResetAuthorizations(ctx context.Context, userID int64, keepAut
if revoker, ok := s.auths.(authorizationRevoker); ok {
return revoker.RevokeByUserExcept(ctx, userID, keepAuthKeyID)
}
targets, err := s.authorizationsByUserExcept(ctx, userID, keepAuthKeyID)
if err != nil {
return nil, err
}
for _, a := range targets {
if err := s.deleteAuthKey(ctx, a.AuthKeyID); err != nil {
return nil, err
}
}
deleted, err := s.auths.DeleteByUserExcept(ctx, userID, keepAuthKeyID)
if err != nil {
return nil, err
}
return deleted, nil
}
func (s *Service) deleteAuthKey(ctx context.Context, authKeyID [8]byte) error {
if s == nil || s.authKeys == nil || authKeyID == ([8]byte{}) {
return nil
}
return s.authKeys.Delete(ctx, authKeyID)
}
func (s *Service) authorizationByHash(ctx context.Context, userID, hash int64) (domain.Authorization, bool, error) {
items, err := s.auths.ListByUser(ctx, userID)
if err != nil {
return domain.Authorization{}, false, err
}
for _, a := range items {
if a.Hash == hash {
return a, true, nil
}
}
return domain.Authorization{}, false, nil
}
func (s *Service) authorizationsByUserExcept(ctx context.Context, userID int64, keepAuthKeyID [8]byte) ([]domain.Authorization, error) {
items, err := s.auths.ListByUser(ctx, userID)
if err != nil {
return nil, err
}
out := make([]domain.Authorization, 0, len(items))
for _, a := range items {
if a.AuthKeyID != keepAuthKeyID {
out = append(out, a)
}
}
return out, nil
return s.auths.DeleteByUserExcept(ctx, userID, keepAuthKeyID)
}
func (s *Service) bind(ctx context.Context, auth domain.Authorization, userID int64) error {

View file

@ -535,7 +535,7 @@ func TestLogOutThenSignInSameAuthKeySwitchesUser(t *testing.T) {
}
}
func TestResetAuthorizationDeletesProtocolAuthKey(t *testing.T) {
func TestResetAuthorizationKeepsProtocolAuthKeyForRPCLogout(t *testing.T) {
ctx := context.Background()
authz := memory.NewAuthorizationStore()
keys := memory.NewAuthKeyStore()
@ -562,15 +562,15 @@ func TestResetAuthorizationDeletesProtocolAuthKey(t *testing.T) {
if err != nil || !found || deleted.AuthKeyID != key {
t.Fatalf("ResetAuthorization deleted=%x found=%v err=%v, want key %x", deleted.AuthKeyID, found, err, key)
}
if _, found, err := keys.Get(ctx, key); err != nil || found {
t.Fatalf("auth key after reset found=%v err=%v, want missing", found, err)
if _, found, err := keys.Get(ctx, key); err != nil || !found {
t.Fatalf("auth key after reset found=%v err=%v, want present for RPC 401", found, err)
}
if _, found, err := svc.UserID(ctx, key); err != nil || found {
t.Fatalf("user after reset found=%v err=%v, want missing", found, err)
}
}
func TestResetAuthorizationsDeletesOnlyRevokedProtocolAuthKeys(t *testing.T) {
func TestResetAuthorizationsKeepsRevokedProtocolAuthKeys(t *testing.T) {
ctx := context.Background()
authz := memory.NewAuthorizationStore()
keys := memory.NewAuthKeyStore()
@ -600,12 +600,18 @@ func TestResetAuthorizationsDeletesOnlyRevokedProtocolAuthKeys(t *testing.T) {
if err != nil || len(deleted) != 1 || deleted[0].AuthKeyID != revoked {
t.Fatalf("ResetAuthorizations deleted=%v err=%v, want revoked key", deleted, err)
}
if _, found, err := keys.Get(ctx, revoked); err != nil || found {
t.Fatalf("revoked auth key found=%v err=%v, want missing", found, err)
if _, found, err := keys.Get(ctx, revoked); err != nil || !found {
t.Fatalf("revoked auth key found=%v err=%v, want present for RPC 401", found, err)
}
if _, found, err := keys.Get(ctx, keep); err != nil || !found {
t.Fatalf("kept auth key found=%v err=%v, want present", found, err)
}
if _, found, err := svc.UserID(ctx, revoked); err != nil || found {
t.Fatalf("revoked user found=%v err=%v, want missing", found, err)
}
if got, found, err := svc.UserID(ctx, keep); err != nil || !found || got != u.ID {
t.Fatalf("kept user=%d found=%v err=%v, want %d", got, found, err, u.ID)
}
}
func TestSignUpWritesOfficialLoginMessage(t *testing.T) {

View file

@ -593,7 +593,8 @@ func (r *Router) onAccountResetAuthorization(ctx context.Context, hash int64) (b
}
r.revokeAuthKeySessions(deleted.AuthKeyID)
_ = r.clearAuthKeyState(ctx, deleted.AuthKeyID)
// P1 修复:撤销该会话销毁其 auth_key级联 discard 该设备绑定的活跃密聊并通知对端。
// 撤销该设备的业务授权后,级联 discard 其绑定的活跃密聊并通知对端。
// 协议 auth key 必须保留,供客户端重连后取得 AUTH_KEY_UNREGISTERED。
r.discardSecretChatsForAuthKey(ctx, businessAuthKeyInt64(deleted.AuthKeyID), userID)
return true, nil
}

View file

@ -594,7 +594,8 @@ func (r *Router) onAuthResetAuthorizations(ctx context.Context) (bool, error) {
for _, a := range deleted {
r.revokeAuthKeySessions(a.AuthKeyID)
_ = r.clearAuthKeyState(ctx, a.AuthKeyID)
// P1 修复:撤销其它会话同样销毁其 auth_key级联 discard 该设备绑定的活跃密聊并通知对端。
// 撤销其它会话会删除其业务 authorization协议 key 保留用于让客户端
// 重连后取得 AUTH_KEY_UNREGISTERED。密聊仍按设备授权边界 discard 并通知对端。
r.discardSecretChatsForAuthKey(ctx, businessAuthKeyInt64(a.AuthKeyID), userID)
}
return true, nil
@ -865,8 +866,8 @@ func (r *Router) onAuthLogOut(ctx context.Context) (*tg.AuthLoggedOut, error) {
r.presence.clearSession(key)
}
}
// P1 修复:登出销毁本设备 perm auth_key 后,级联 discard 其绑定的活跃密聊并通知对端
//(否则对端继续往死 auth_key 投递成静默死链。best-effort不阻断登出。
// 登出撤销本设备 authorization 后,级联 discard 其绑定的活跃密聊并通知对端
//(否则对端继续往已退出设备投递成静默死链。best-effort不阻断登出。
if userErr == nil && userID != 0 {
r.discardSecretChatsForAuthKey(ctx, businessAuthKeyInt64(id), userID)
}

View file

@ -12,6 +12,11 @@ import "github.com/iamxvbaba/td/tg"
func rpcAllowedWithoutAuthorization(id uint32) bool {
switch id {
case tg.AuthBindTempAuthKeyRequestTypeID,
// TWeb handles a 401 from a remotely revoked session by sending
// auth.logOut before it clears IndexedDB/local authorization state.
// This cleanup RPC is idempotent when no authorization remains; rejecting
// it with another 401 makes Web repeat its startup/logout cycle forever.
tg.AuthLogOutRequestTypeID,
tg.AuthExportLoginTokenRequestTypeID,
tg.AuthImportLoginTokenRequestTypeID,
tg.AuthAcceptLoginTokenRequestTypeID,

View file

@ -0,0 +1,112 @@
package rpc
import (
"context"
"testing"
"github.com/iamxvbaba/td/bin"
"github.com/iamxvbaba/td/clock"
"github.com/iamxvbaba/td/tg"
"github.com/iamxvbaba/td/tgerr"
"go.uber.org/zap/zaptest"
appauth "telesrv/internal/app/auth"
"telesrv/internal/domain"
"telesrv/internal/store"
"telesrv/internal/store/memory"
)
func TestAccountResetAuthorizationKeepsProtocolKeyAndReturnsRPC401(t *testing.T) {
ctx := context.Background()
currentAuthKeyID := [8]byte{0x71}
targetAuthKeyID := [8]byte{0x72}
const (
userID = int64(1000000001)
targetHash = int64(2026072401)
)
authKeys := memory.NewAuthKeyStore()
authorizations := memory.NewAuthorizationStore()
for _, authKeyID := range [][8]byte{currentAuthKeyID, targetAuthKeyID} {
if err := authKeys.Save(ctx, store.AuthKeyData{ID: authKeyID}); err != nil {
t.Fatalf("save auth key %x: %v", authKeyID, err)
}
}
authService := appauth.NewService(nil, authorizations, nil, authKeys, nil, "12345")
if err := authorizations.Bind(ctx, domain.Authorization{
AuthKeyID: currentAuthKeyID,
UserID: userID,
Hash: 2026072400,
}); err != nil {
t.Fatalf("bind current authorization: %v", err)
}
if err := authorizations.Bind(ctx, domain.Authorization{
AuthKeyID: targetAuthKeyID,
UserID: userID,
Hash: targetHash,
}); err != nil {
t.Fatalf("bind target authorization: %v", err)
}
r := New(Config{}, Deps{
Auth: authService,
Files: &fakeFiles{},
}, zaptest.NewLogger(t), clock.System)
var warmTarget bin.Buffer
if err := (&tg.UploadSaveFilePartRequest{
FileID: 1,
FilePart: 0,
Bytes: []byte{1},
}).Encode(&warmTarget); err != nil {
t.Fatalf("encode target warm-up RPC: %v", err)
}
if _, err := r.Dispatch(ctx, targetAuthKeyID, 101, &warmTarget); err != nil {
t.Fatalf("target warm-up RPC: %v", err)
}
var reset bin.Buffer
if err := (&tg.AccountResetAuthorizationRequest{Hash: targetHash}).Encode(&reset); err != nil {
t.Fatalf("encode account.resetAuthorization: %v", err)
}
if result, err := r.Dispatch(ctx, currentAuthKeyID, 102, &reset); err != nil {
t.Fatalf("account.resetAuthorization: %v", err)
} else if value, ok := dispatchCanonicalValue(result).(bool); !ok || !value {
t.Fatalf("account.resetAuthorization result = %#v, want true", dispatchCanonicalValue(result))
}
if _, found, err := authKeys.Get(ctx, targetAuthKeyID); err != nil || !found {
t.Fatalf("target protocol auth key found=%v err=%v, want retained", found, err)
}
if _, found, err := authorizations.ByAuthKey(ctx, targetAuthKeyID); err != nil || found {
t.Fatalf("target business authorization found=%v err=%v, want removed", found, err)
}
if current, found, err := authorizations.ByAuthKey(ctx, currentAuthKeyID); err != nil || !found || current.UserID != userID {
t.Fatalf("current authorization=%+v found=%v err=%v, want retained user %d", current, found, err, userID)
}
var afterRevoke bin.Buffer
if err := (&tg.UploadSaveFilePartRequest{
FileID: 1,
FilePart: 1,
Bytes: []byte{2},
}).Encode(&afterRevoke); err != nil {
t.Fatalf("encode target post-revoke RPC: %v", err)
}
if _, err := r.Dispatch(ctx, targetAuthKeyID, 103, &afterRevoke); !tgerr.Is(err, "AUTH_KEY_UNREGISTERED") {
t.Fatalf("target post-revoke RPC err=%v, want AUTH_KEY_UNREGISTERED", err)
}
var logout bin.Buffer
if err := (&tg.AuthLogOutRequest{}).Encode(&logout); err != nil {
t.Fatalf("encode target auth.logOut cleanup: %v", err)
}
if result, err := r.Dispatch(ctx, targetAuthKeyID, 104, &logout); err != nil {
t.Fatalf("target auth.logOut cleanup: %v", err)
} else if _, ok := dispatchCanonicalValue(result).(*tg.AuthLoggedOut); !ok {
t.Fatalf("target auth.logOut cleanup result=%#v, want *tg.AuthLoggedOut", dispatchCanonicalValue(result))
}
if _, found, err := authKeys.Get(ctx, targetAuthKeyID); err != nil || !found {
t.Fatalf("target protocol auth key after logout cleanup found=%v err=%v, want retained", found, err)
}
}

View file

@ -461,8 +461,9 @@ func (s *AuthorizationStore) DeleteByHash(_ context.Context, userID, hash int64)
return domain.Authorization{}, false, nil
}
// RevokeByHash mirrors PostgreSQL's protocol-key revocation boundary when this
// authorization projection is linked to an in-memory auth-key authority.
// RevokeByHash removes only the business authorization. The protocol auth key
// and any temp binding stay usable for MTProto decryption so a kicked client can
// reconnect and receive AUTH_KEY_UNREGISTERED from the RPC gate.
func (s *AuthorizationStore) RevokeByHash(ctx context.Context, userID, hash int64) (domain.Authorization, bool, error) {
s.linkMu.RLock()
defer s.linkMu.RUnlock()
@ -471,27 +472,17 @@ func (s *AuthorizationStore) RevokeByHash(ctx context.Context, userID, hash int6
}
s.authKeys.mu.Lock()
s.mu.Lock()
var (
targetID [8]byte
target domain.Authorization
found bool
)
for id, a := range s.m {
if a.UserID == userID && a.Hash == hash {
targetID, target, found = id, a, true
break
delete(s.m, id)
s.mu.Unlock()
s.authKeys.mu.Unlock()
return a, true, nil
}
}
if !found {
s.mu.Unlock()
s.authKeys.mu.Unlock()
return domain.Authorization{}, false, nil
}
deletedIDs := s.authKeys.deleteProtocolAuthKeyLocked(targetID)
s.authKeys.deleteAuthorizationMirrorsWithHeldLocked(deletedIDs, s)
s.mu.Unlock()
s.authKeys.mu.Unlock()
return target, true, nil
return domain.Authorization{}, false, nil
}
func (s *AuthorizationStore) DeleteByUserExcept(_ context.Context, userID int64, keepAuthKeyID [8]byte) ([]domain.Authorization, error) {
@ -517,18 +508,12 @@ func (s *AuthorizationStore) RevokeByUserExcept(ctx context.Context, userID int6
s.authKeys.mu.Lock()
s.mu.Lock()
out := make([]domain.Authorization, 0)
targets := make([][8]byte, 0)
for id, a := range s.m {
if a.UserID == userID && id != keepAuthKeyID {
out = append(out, a)
targets = append(targets, id)
delete(s.m, id)
}
}
deletedIDs := make([][8]byte, 0, len(targets))
for _, id := range targets {
deletedIDs = append(deletedIDs, s.authKeys.deleteProtocolAuthKeyLocked(id)...)
}
s.authKeys.deleteAuthorizationMirrorsWithHeldLocked(deletedIDs, s)
s.mu.Unlock()
s.authKeys.mu.Unlock()
return out, nil

View file

@ -505,7 +505,10 @@ FROM authorizations WHERE auth_key_id = $1 AND user_id = $2 FOR UPDATE`, id, use
if !found {
return nil, nil
}
if err := deleteRevocationTargetsTx(ctx, tx, []int64{id}); err != nil {
// Cancelling a pending account deletion deliberately retires the requester
// protocol identity; unlike remote device revocation, this path does not need
// to preserve the key for a client-visible RPC 401 transition.
if err := deleteProtocolAuthIdentitiesTx(ctx, tx, []int64{id}); err != nil {
return nil, err
}
return []domain.Authorization{a}, nil

View file

@ -195,8 +195,9 @@ WHERE auth_key_id = $1
//
// 同时清理把本 key 当作 perm key 的 temp auth key 行temp_auth_key_bindings.temp_auth_key_id
// 侧有外键 ON DELETE CASCADE删除 temp key 会自动清绑定perm_auth_key_id 侧由
// RESTRICT FK 防止悬空,因此被踢/销毁 perm key 时必须先把关联 temp key 一并删掉。否则 Web/上传连接用
// raw temp key 重连时仍能进入 RPC 层,只得到 AUTH_KEY_UNREGISTERED而不是连接层 404。
// RESTRICT FK 防止悬空,因此显式销毁 perm key 时必须先把关联 temp key 一并删掉。
// 远程撤销 authorization 不得调用本方法:被踢客户端必须保留协议 key重连进入 RPC
// 层后取得 AUTH_KEY_UNREGISTERED而不是只收到连接层 -404。
func (s *AuthKeyStore) Delete(ctx context.Context, id [8]byte) error {
return withAuthIdentityTx(ctx, s.db, "delete auth key", func(tx pgx.Tx) error {
return deleteAuthKeyTx(ctx, tx, authKeyIDToInt64(id))

View file

@ -252,9 +252,10 @@ RETURNING auth_key_id, user_id, hash, layer, device_model, platform, system_vers
return a, true, nil
}
// RevokeByHash 删除协议 auth_key 作为远程踢设备的持久化事实入口。
// authorizations 通过 FK cascade 删除update_states 没有 auth_keys FK必须显式清理
// 关联 temp auth key 也显式删除,避免 raw temp key 重连。
// RevokeByHash 是远程踢设备的持久化事实入口:删除业务 authorization 与
// device update state但保留 permanent/temp 协议 key 和 binding。这样被踢客户端
// 重连后仍可完成 MTProto 解密,并由 RPC gate 返回 AUTH_KEY_UNREGISTERED若先删除
// 协议 key客户端只能收到 transport -404无法可靠清理本地登录态。
func (s *AuthorizationStore) RevokeByHash(ctx context.Context, userID, hash int64) (domain.Authorization, bool, error) {
var (
a domain.Authorization
@ -315,7 +316,7 @@ FOR UPDATE`, candidate, userID, hash))
if !found {
return domain.Authorization{}, false, nil
}
if err := deleteRevocationTargetsTx(ctx, tx, []int64{candidate}); err != nil {
if err := deleteRevokedAuthorizationStateTx(ctx, tx, []int64{candidate}); err != nil {
return domain.Authorization{}, false, err
}
return a, true, nil
@ -349,7 +350,8 @@ RETURNING auth_key_id, user_id, hash, layer, device_model, platform, system_vers
return out, nil
}
// RevokeByUserExcept 批量删除协议 auth_key保留 keepAuthKeyID 对应的当前设备。
// RevokeByUserExcept 批量删除业务 authorization保留 keepAuthKeyID 对应的当前设备;
// 被撤销设备的协议 key/binding 保留,以便重连后取得 RPC 401 并完成客户端退出。
func (s *AuthorizationStore) RevokeByUserExcept(ctx context.Context, userID int64, keepAuthKeyID [8]byte) ([]domain.Authorization, error) {
var out []domain.Authorization
err := withAuthIdentityTx(ctx, s.db, "revoke authorizations by user", func(tx pgx.Tx) error {
@ -449,13 +451,38 @@ FOR UPDATE`, userID, keepAuthKeyID, candidates)
for i := range out {
targets[i] = authKeyIDToInt64(out[i].AuthKeyID)
}
if err := deleteRevocationTargetsTx(ctx, tx, targets); err != nil {
if err := deleteRevokedAuthorizationStateTx(ctx, tx, targets); err != nil {
return nil, err
}
return out, nil
}
func deleteRevocationTargetsTx(ctx context.Context, tx pgx.Tx, authKeyIDs []int64) error {
func deleteRevokedAuthorizationStateTx(ctx context.Context, tx pgx.Tx, authKeyIDs []int64) error {
if len(authKeyIDs) == 0 {
return nil
}
if _, err := tx.Exec(ctx, `
DELETE FROM update_states
WHERE auth_key_id = ANY($1::bigint[])`, authKeyIDs); err != nil {
return fmt.Errorf("delete revoked update states: %w", err)
}
tag, err := tx.Exec(ctx, `
DELETE FROM authorizations
WHERE auth_key_id = ANY($1::bigint[])`, authKeyIDs)
if err != nil {
return fmt.Errorf("delete revoked authorizations: %w", err)
}
if tag.RowsAffected() != int64(len(authKeyIDs)) {
return fmt.Errorf("delete revoked authorizations: deleted %d of %d locked targets", tag.RowsAffected(), len(authKeyIDs))
}
return nil
}
// deleteProtocolAuthIdentitiesTx permanently removes permanent identities and
// their derived temp keys. Remote account.resetAuthorization/
// auth.resetAuthorizations must not use this helper: those clients need the
// protocol key long enough to reconnect and receive an RPC-level 401.
func deleteProtocolAuthIdentitiesTx(ctx context.Context, tx pgx.Tx, authKeyIDs []int64) error {
if len(authKeyIDs) == 0 {
return nil
}
@ -466,21 +493,21 @@ WHERE auth_key_id IN (
FROM temp_auth_key_bindings
WHERE perm_auth_key_id = ANY($1::bigint[])
)`, authKeyIDs); err != nil {
return fmt.Errorf("delete revoked temporary auth keys: %w", err)
return fmt.Errorf("delete temporary auth keys: %w", err)
}
if _, err := tx.Exec(ctx, `
DELETE FROM update_states
WHERE auth_key_id = ANY($1::bigint[])`, authKeyIDs); err != nil {
return fmt.Errorf("delete revoked update states: %w", err)
return fmt.Errorf("delete protocol identity update states: %w", err)
}
tag, err := tx.Exec(ctx, `
DELETE FROM auth_keys
WHERE auth_key_id = ANY($1::bigint[])`, authKeyIDs)
if err != nil {
return fmt.Errorf("delete revoked permanent auth keys: %w", err)
return fmt.Errorf("delete permanent auth keys: %w", err)
}
if tag.RowsAffected() != int64(len(authKeyIDs)) {
return fmt.Errorf("delete revoked permanent auth keys: deleted %d of %d locked targets", tag.RowsAffected(), len(authKeyIDs))
return fmt.Errorf("delete permanent auth keys: deleted %d of %d locked targets", tag.RowsAffected(), len(authKeyIDs))
}
return nil
}

View file

@ -2,7 +2,6 @@ package postgres
import (
"context"
"errors"
"fmt"
"testing"
"time"
@ -13,7 +12,7 @@ import (
"telesrv/internal/store"
)
func TestAuthorizationStoreRevokeByHashDeletesProtocolKeyCascadePostgres(t *testing.T) {
func TestAuthorizationStoreRevokeByHashKeepsProtocolIdentityPostgres(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
userID := createRevokeTestUser(t, ctx, pool, "hash")
@ -43,14 +42,15 @@ func TestAuthorizationStoreRevokeByHashDeletesProtocolKeyCascadePostgres(t *test
if err := NewUpdateStateStore(pool).Save(ctx, perm, userID, domain.UpdateState{Pts: 11, Date: int(time.Now().Unix())}); err != nil {
t.Fatalf("save update state: %v", err)
}
if err := NewTempAuthKeyBindingStore(pool).Save(ctx, domain.TempAuthKeyBinding{
binding := domain.TempAuthKeyBinding{
TempAuthKeyID: temp,
PermAuthKeyID: authKeyIDToInt64(perm),
Nonce: 1,
TempSessionID: 2,
ExpiresAt: tempExpiry,
EncryptedMessage: []byte{1, 2, 3, 4},
}); err != nil {
}
if err := NewTempAuthKeyBindingStore(pool).Save(ctx, binding); err != nil {
t.Fatalf("save temp binding: %v", err)
}
@ -61,14 +61,14 @@ func TestAuthorizationStoreRevokeByHashDeletesProtocolKeyCascadePostgres(t *test
if deleted.AuthKeyID != perm || !deleted.PasswordPending {
t.Fatalf("deleted authorization = %+v, want perm key and password_pending", deleted)
}
assertRevokeTestMissingAuthKey(t, ctx, keys, perm)
assertRevokeTestMissingAuthKey(t, ctx, keys, temp)
assertRevokeTestPresentAuthKey(t, ctx, keys, perm)
assertRevokeTestPresentAuthKey(t, ctx, keys, temp)
assertRevokeTestNoAuthorization(t, ctx, auths, perm)
assertRevokeTestTableCount(t, ctx, pool, "update_states", "auth_key_id", authKeyIDToInt64(perm), 0)
assertRevokeTestTableCount(t, ctx, pool, "temp_auth_key_bindings", "temp_auth_key_id", authKeyIDToInt64(temp), 0)
assertTempIdentityBinding(t, ctx, NewTempAuthKeyBindingStore(pool), binding)
}
func TestAuthorizationStoreRevokeByUserExceptDeletesOnlyRevokedKeysPostgres(t *testing.T) {
func TestAuthorizationStoreRevokeByUserExceptKeepsRevokedProtocolIdentitiesPostgres(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
userID := createRevokeTestUser(t, ctx, pool, "bulk")
@ -87,14 +87,15 @@ func TestAuthorizationStoreRevokeByUserExceptDeletesOnlyRevokedKeysPostgres(t *t
}
tempExpiry := int(time.Now().Add(time.Hour).Unix())
saveRevokeTestAuthKey(t, ctx, keys, tempForTwo, tempExpiry)
if err := NewTempAuthKeyBindingStore(pool).Save(ctx, domain.TempAuthKeyBinding{
binding := domain.TempAuthKeyBinding{
TempAuthKeyID: tempForTwo,
PermAuthKeyID: authKeyIDToInt64(revokedTwo),
Nonce: 3,
TempSessionID: 4,
ExpiresAt: tempExpiry,
EncryptedMessage: []byte{5, 6, 7, 8},
}); err != nil {
}
if err := NewTempAuthKeyBindingStore(pool).Save(ctx, binding); err != nil {
t.Fatalf("save temp binding: %v", err)
}
@ -107,9 +108,12 @@ func TestAuthorizationStoreRevokeByUserExceptDeletesOnlyRevokedKeysPostgres(t *t
}
assertRevokeTestPresentAuthKey(t, ctx, keys, keep)
assertRevokeTestPresentAuthorization(t, ctx, auths, keep)
assertRevokeTestMissingAuthKey(t, ctx, keys, revokedOne)
assertRevokeTestMissingAuthKey(t, ctx, keys, revokedTwo)
assertRevokeTestMissingAuthKey(t, ctx, keys, tempForTwo)
assertRevokeTestPresentAuthKey(t, ctx, keys, revokedOne)
assertRevokeTestPresentAuthKey(t, ctx, keys, revokedTwo)
assertRevokeTestPresentAuthKey(t, ctx, keys, tempForTwo)
assertRevokeTestNoAuthorization(t, ctx, auths, revokedOne)
assertRevokeTestNoAuthorization(t, ctx, auths, revokedTwo)
assertTempIdentityBinding(t, ctx, NewTempAuthKeyBindingStore(pool), binding)
}
func TestAuthorizationStoreUpdateClientInfoMergesPostgres(t *testing.T) {
@ -155,7 +159,7 @@ func TestAuthorizationStoreUpdateClientInfoMergesPostgres(t *testing.T) {
}
}
func TestAuthorizationStoreRevokeByHashConcurrentTempBindLeavesNoDanglingStatePostgres(t *testing.T) {
func TestAuthorizationStoreRevokeByHashConcurrentTempBindKeepsProtocolIdentityPostgres(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
userID := createRevokeTestUser(t, ctx, pool, "bind-revoke-race")
@ -203,28 +207,17 @@ func TestAuthorizationStoreRevokeByHashConcurrentTempBindLeavesNoDanglingStatePo
close(start)
bindErr := <-bindResult
if bindErr != nil && !errors.Is(bindErr, store.ErrAuthKeyBindingInvalid) {
if bindErr != nil {
t.Fatalf("attempt %d bind/revoke race bind error = %v", attempt, bindErr)
}
revoked := <-revokeResults
if revoked.err != nil || !revoked.found {
t.Fatalf("attempt %d bind/revoke race found=%v err=%v", attempt, revoked.found, revoked.err)
}
if _, found, err := bindings.GetByTemp(ctx, temp); err != nil || found {
t.Fatalf("attempt %d dangling binding found=%v err=%v", attempt, found, err)
}
assertRevokeTestMissingAuthKey(t, ctx, keys, perm)
assertTempIdentityBinding(t, ctx, bindings, candidate)
assertRevokeTestPresentAuthKey(t, ctx, keys, perm)
assertRevokeTestPresentAuthKey(t, ctx, keys, temp)
assertRevokeTestNoAuthorization(t, ctx, auths, perm)
if bindErr == nil {
assertRevokeTestMissingAuthKey(t, ctx, keys, temp)
} else {
assertTempIdentityAuthKeyExpiry(t, ctx, keys, temp, tempExpiry)
assertRevokeTestNoAuthorization(t, ctx, auths, temp)
if err := keys.Delete(ctx, temp); err != nil {
t.Fatalf("attempt %d clean unbound loser temp: %v", attempt, err)
}
assertRevokeTestMissingAuthKey(t, ctx, keys, temp)
}
}
}
@ -520,12 +513,10 @@ func TestAuthorizationStoreRevokeByUserExceptPartiallySkipsTransferredCandidateP
t.Fatalf("old A state for transferred key found=%v err=%v, want absent", found, err)
}
assertRevokeTestMissingAuthKey(t, testCtx, keys, revoked)
assertRevokeTestMissingAuthKey(t, testCtx, keys, revokedTemp)
assertRevokeTestPresentAuthKey(t, testCtx, keys, revoked)
assertRevokeTestPresentAuthKey(t, testCtx, keys, revokedTemp)
assertRevokeTestNoAuthorization(t, testCtx, auths, revoked)
if _, found, err := bindings.GetByTemp(testCtx, revokedTemp); err != nil || found {
t.Fatalf("revoked temp binding found=%v err=%v, want absent", found, err)
}
assertTempIdentityBinding(t, testCtx, bindings, revokedBinding)
if _, found, err := states.Get(testCtx, revoked, userA); err != nil || found {
t.Fatalf("revoked A state found=%v err=%v, want absent", found, err)
}