595 lines
21 KiB
Go
595 lines
21 KiB
Go
package postgres
|
||
|
||
import (
|
||
"context"
|
||
"crypto/sha256"
|
||
"encoding/binary"
|
||
"errors"
|
||
"fmt"
|
||
|
||
"github.com/jackc/pgx/v5"
|
||
|
||
"telesrv/internal/domain"
|
||
"telesrv/internal/store"
|
||
"telesrv/internal/store/postgres/sqlcgen"
|
||
)
|
||
|
||
// AuthorizationStore 用 PostgreSQL 实现 store.AuthorizationStore。
|
||
type AuthorizationStore struct {
|
||
db sqlcgen.DBTX
|
||
q *sqlcgen.Queries
|
||
}
|
||
|
||
// NewAuthorizationStore 基于 pgx 连接池(或事务)创建 AuthorizationStore。
|
||
func NewAuthorizationStore(db sqlcgen.DBTX) *AuthorizationStore {
|
||
return &AuthorizationStore{db: db, q: sqlcgen.New(db)}
|
||
}
|
||
|
||
func (s *AuthorizationStore) Bind(ctx context.Context, a domain.Authorization) error {
|
||
if a.Hash == 0 {
|
||
a.Hash = authorizationHash(a.AuthKeyID)
|
||
}
|
||
err := withAuthIdentityTx(ctx, s.db, "bind authorization", func(tx pgx.Tx) error {
|
||
return bindAuthorization(ctx, tx, a)
|
||
})
|
||
if err != nil {
|
||
return fmt.Errorf("upsert authorization: %w", err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// bindAuthorization 把 auth_key→user 绑定和设备 update baseline 作为同一个状态边界提交。
|
||
//
|
||
// 锁顺序固定为:目标 user advisory/row → auth_keys 母行 →
|
||
// user_update_watermarks → user_update_retention → 目标 update_states。其中 watermark
|
||
// 与 retention 两个 row lock 的顺序和 pruneConfirmedUserPrefixTx 一致,使新授权的
|
||
// observed baseline 和 retained floor 不会交叉提交成静默空洞。母行锁又能在首次
|
||
// authorization 尚不存在时串行化同一
|
||
// raw auth key 的并发登录/换号。user 锁与账号 tombstone 使用同一顺序;因此 Bind
|
||
// 要么先提交并被随后删除事务撤销,要么等删除提交后看见 tombstone 并拒绝,不能在
|
||
// 删除事务枚举 authorization 之后重新绑定账号。
|
||
func bindAuthorization(ctx context.Context, db sqlcgen.DBTX, a domain.Authorization) error {
|
||
keyID := authKeyIDToInt64(a.AuthKeyID)
|
||
tx, ok := db.(pgx.Tx)
|
||
if !ok {
|
||
return fmt.Errorf("bind authorization requires a transaction")
|
||
}
|
||
if err := lockUsersForUpdate(ctx, tx, a.UserID); err != nil {
|
||
return fmt.Errorf("lock authorization user: %w", err)
|
||
}
|
||
var active bool
|
||
if err := db.QueryRow(ctx, `
|
||
SELECT deleted_at IS NULL
|
||
FROM users
|
||
WHERE id = $1
|
||
FOR UPDATE`, a.UserID).Scan(&active); err != nil {
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return domain.ErrUserNotFound
|
||
}
|
||
return fmt.Errorf("lock authorization user row: %w", err)
|
||
}
|
||
if !active {
|
||
return domain.ErrAccountDeleted
|
||
}
|
||
if err := lockPermanentAuthIdentities(ctx, tx, []int64{keyID}); err != nil {
|
||
return err
|
||
}
|
||
var (
|
||
lockedKeyID int64
|
||
expiresAt int
|
||
authLayer int
|
||
layerObservationID int64
|
||
)
|
||
if err := db.QueryRow(ctx, `
|
||
SELECT auth_key_id, expires_at, layer, layer_observation_id
|
||
FROM auth_keys
|
||
WHERE auth_key_id = $1
|
||
FOR UPDATE`, keyID).Scan(&lockedKeyID, &expiresAt, &authLayer, &layerObservationID); err != nil {
|
||
return fmt.Errorf("lock auth key for authorization: %w", err)
|
||
}
|
||
if expiresAt != 0 {
|
||
return store.ErrAuthKeyNotPermanent
|
||
}
|
||
if authLayer < 0 || layerObservationID < 0 || (layerObservationID > 0 && authLayer == 0) {
|
||
return fmt.Errorf(
|
||
"authorization auth-key layer invariant violation: auth key %x has layer %d observation %d",
|
||
a.AuthKeyID, authLayer, layerObservationID,
|
||
)
|
||
}
|
||
|
||
if _, err := db.Exec(ctx, `
|
||
INSERT INTO user_update_watermarks (user_id, contiguous_pts)
|
||
VALUES ($1, 0)
|
||
ON CONFLICT (user_id) DO NOTHING`, a.UserID); err != nil {
|
||
return fmt.Errorf("ensure authorization user update watermark: %w", err)
|
||
}
|
||
var currentPts int
|
||
if err := db.QueryRow(ctx, `
|
||
SELECT contiguous_pts
|
||
FROM user_update_watermarks
|
||
WHERE user_id = $1
|
||
FOR UPDATE`, a.UserID).Scan(¤tPts); err != nil {
|
||
return fmt.Errorf("lock authorization user update watermark: %w", err)
|
||
}
|
||
if _, err := db.Exec(ctx, `
|
||
INSERT INTO user_update_retention (user_id)
|
||
VALUES ($1)
|
||
ON CONFLICT (user_id) DO NOTHING`, a.UserID); err != nil {
|
||
return fmt.Errorf("ensure authorization user update retention: %w", err)
|
||
}
|
||
var retainedFloor int
|
||
if err := db.QueryRow(ctx, `
|
||
SELECT retained_through_pts
|
||
FROM user_update_retention
|
||
WHERE user_id = $1
|
||
FOR UPDATE`, a.UserID).Scan(&retainedFloor); err != nil {
|
||
return fmt.Errorf("lock authorization user update retention: %w", err)
|
||
}
|
||
if retainedFloor > currentPts {
|
||
return fmt.Errorf(
|
||
"authorization update baseline invariant violation: user %d retained floor %d exceeds contiguous watermark %d",
|
||
a.UserID, retainedFloor, currentPts,
|
||
)
|
||
}
|
||
|
||
// 每次 Bind 都是一次显式登录 baseline:delivered pts 推进到已锁定的账号连续水位;
|
||
// observed 只推进到已删除的 retained floor,不把 live tail 伪装成客户端确认。
|
||
// 历史遗留的 state 若超出账号 contiguous watermark,必须 fail-fast;不得用
|
||
// GREATEST 把非法 future cursor 保留下来。WHERE 也封住“预检后并发插入”的竞态。
|
||
tag, err := db.Exec(ctx, `
|
||
INSERT INTO update_states (auth_key_id, user_id, pts, qts, date, seq, observed_pts)
|
||
VALUES ($1, $2, $3, 0, EXTRACT(EPOCH FROM now())::int, 0, $4)
|
||
ON CONFLICT (auth_key_id, user_id) DO UPDATE SET
|
||
pts = GREATEST(update_states.pts, EXCLUDED.pts),
|
||
qts = GREATEST(update_states.qts, EXCLUDED.qts),
|
||
date = GREATEST(update_states.date, EXCLUDED.date),
|
||
seq = GREATEST(update_states.seq, EXCLUDED.seq),
|
||
observed_pts = GREATEST(update_states.observed_pts, EXCLUDED.observed_pts),
|
||
updated_at = now()
|
||
WHERE update_states.pts >= 0
|
||
AND update_states.pts <= $3
|
||
AND update_states.observed_pts <= $3`, keyID, a.UserID, currentPts, retainedFloor)
|
||
if err != nil {
|
||
return fmt.Errorf("upsert authorization update baseline: %w", err)
|
||
}
|
||
if tag.RowsAffected() != 1 {
|
||
return fmt.Errorf(
|
||
"authorization update baseline invariant violation: auth key %x user %d has pts or observed_pts outside contiguous watermark %d",
|
||
a.AuthKeyID, a.UserID, currentPts,
|
||
)
|
||
}
|
||
|
||
if _, err := db.Exec(ctx, `
|
||
DELETE FROM update_states
|
||
WHERE auth_key_id = $1
|
||
AND user_id <> $2`, keyID, a.UserID); err != nil {
|
||
return fmt.Errorf("delete stale cross-user update states: %w", err)
|
||
}
|
||
|
||
if _, err := db.Exec(ctx, `
|
||
INSERT INTO authorizations (auth_key_id, user_id, hash, layer, device_model, platform, system_version, api_id, app_version, ip, password_pending)
|
||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)
|
||
ON CONFLICT (auth_key_id) DO UPDATE SET
|
||
user_id = EXCLUDED.user_id,
|
||
hash = EXCLUDED.hash,
|
||
layer = EXCLUDED.layer,
|
||
device_model = EXCLUDED.device_model,
|
||
platform = EXCLUDED.platform,
|
||
system_version = EXCLUDED.system_version,
|
||
api_id = EXCLUDED.api_id,
|
||
app_version = EXCLUDED.app_version,
|
||
ip = EXCLUDED.ip,
|
||
password_pending = EXCLUDED.password_pending,
|
||
created_at = now(),
|
||
active_at = now()`,
|
||
keyID, a.UserID, a.Hash, int32(authLayer), a.DeviceModel, a.Platform, a.SystemVersion, int32(a.APIID), a.AppVersion, a.IP, a.PasswordPending,
|
||
); err != nil {
|
||
return fmt.Errorf("write authorization: %w", err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (s *AuthorizationStore) ByAuthKey(ctx context.Context, id [8]byte) (domain.Authorization, bool, error) {
|
||
row := s.db.QueryRow(ctx, `
|
||
SELECT user_id, hash, layer, device_model, platform, system_version, api_id, app_version, ip, password_pending, created_at, active_at
|
||
FROM authorizations WHERE auth_key_id = $1`, authKeyIDToInt64(id))
|
||
a := domain.Authorization{AuthKeyID: id}
|
||
if err := row.Scan(
|
||
&a.UserID, &a.Hash, &a.Layer, &a.DeviceModel, &a.Platform, &a.SystemVersion,
|
||
&a.APIID, &a.AppVersion, &a.IP, &a.PasswordPending, &a.CreatedAt, &a.ActiveAt,
|
||
); err != nil {
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return domain.Authorization{}, false, nil
|
||
}
|
||
return domain.Authorization{}, false, fmt.Errorf("get authorization: %w", err)
|
||
}
|
||
return a, true, nil
|
||
}
|
||
|
||
func (s *AuthorizationStore) UpdateClientInfo(ctx context.Context, id [8]byte, info domain.AuthKeyClientInfo) error {
|
||
if _, err := s.db.Exec(ctx, `
|
||
UPDATE authorizations SET
|
||
layer = CASE WHEN $2 > 0 THEN $2 ELSE layer END,
|
||
device_model = CASE WHEN $3 <> '' THEN $3 ELSE device_model END,
|
||
platform = CASE WHEN $4 <> '' THEN $4 ELSE platform END,
|
||
system_version = CASE WHEN $5 <> '' THEN $5 ELSE system_version END,
|
||
api_id = CASE WHEN $6 <> 0 THEN $6 ELSE api_id END,
|
||
app_version = CASE WHEN $7 <> '' THEN $7 ELSE app_version END,
|
||
active_at = now()
|
||
WHERE auth_key_id = $1`,
|
||
authKeyIDToInt64(id), int32(info.Layer), info.DeviceModel, info.Platform,
|
||
info.SystemVersion, int32(info.APIID), info.AppVersion,
|
||
); err != nil {
|
||
return fmt.Errorf("update authorization client info: %w", err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// MarkPasswordPassed atomically promotes only the pending identity whose
|
||
// password was just verified. A concurrent cross-user Bind must not let A's
|
||
// proof clear B's password_pending flag.
|
||
func (s *AuthorizationStore) MarkPasswordPassed(ctx context.Context, id [8]byte, expectedUserID int64) error {
|
||
tag, err := s.db.Exec(ctx, `
|
||
UPDATE authorizations SET password_pending = false, created_at = now(), active_at = now()
|
||
WHERE auth_key_id = $1 AND user_id = $2 AND password_pending`, authKeyIDToInt64(id), expectedUserID)
|
||
if err != nil {
|
||
return fmt.Errorf("mark authorization password passed: %w", err)
|
||
}
|
||
if tag.RowsAffected() != 1 {
|
||
return store.ErrAuthorizationStateChanged
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (s *AuthorizationStore) ListByUser(ctx context.Context, userID int64) ([]domain.Authorization, error) {
|
||
rows, err := s.q.ListAuthorizationsByUser(ctx, userID)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("list authorizations by user: %w", err)
|
||
}
|
||
out := make([]domain.Authorization, 0, len(rows))
|
||
for _, row := range rows {
|
||
out = append(out, authorizationFromRow(row))
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func (s *AuthorizationStore) Delete(ctx context.Context, id [8]byte) error {
|
||
if err := s.q.DeleteAuthorization(ctx, authKeyIDToInt64(id)); err != nil {
|
||
return fmt.Errorf("delete authorization: %w", err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (s *AuthorizationStore) DeleteByHash(ctx context.Context, userID, hash int64) (domain.Authorization, bool, error) {
|
||
row := s.db.QueryRow(ctx, `
|
||
DELETE FROM authorizations
|
||
WHERE user_id = $1 AND hash = $2
|
||
RETURNING auth_key_id, user_id, hash, layer, device_model, platform, system_version, api_id, app_version, ip, created_at, active_at`, userID, hash)
|
||
var a domain.Authorization
|
||
var authKeyID int64
|
||
if err := row.Scan(
|
||
&authKeyID, &a.UserID, &a.Hash, &a.Layer, &a.DeviceModel, &a.Platform, &a.SystemVersion,
|
||
&a.APIID, &a.AppVersion, &a.IP, &a.CreatedAt, &a.ActiveAt,
|
||
); err != nil {
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return domain.Authorization{}, false, nil
|
||
}
|
||
return domain.Authorization{}, false, fmt.Errorf("delete authorization by hash: %w", err)
|
||
}
|
||
a.AuthKeyID = authKeyIDFromInt64(authKeyID)
|
||
return a, true, nil
|
||
}
|
||
|
||
// 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
|
||
found bool
|
||
)
|
||
err := withAuthIdentityTx(ctx, s.db, "revoke authorization by hash", func(tx pgx.Tx) error {
|
||
var err error
|
||
a, found, err = revokeByHashTx(ctx, tx, userID, hash)
|
||
return err
|
||
})
|
||
if err != nil {
|
||
return domain.Authorization{}, false, err
|
||
}
|
||
return a, found, nil
|
||
}
|
||
|
||
// revokeByHashTx deliberately uses separate READ COMMITTED statements. The first
|
||
// lookup is only a candidate. Bind locks auth_keys before changing authorization
|
||
// ownership, so revocation must lock the same parent row and then re-read the
|
||
// owner/hash from a fresh statement snapshot. Otherwise an A->B re-login that
|
||
// commits while revoke waits can be deleted using A's stale target snapshot.
|
||
func revokeByHashTx(ctx context.Context, tx pgx.Tx, userID, hash int64) (domain.Authorization, bool, error) {
|
||
var candidate int64
|
||
if err := tx.QueryRow(ctx, `
|
||
SELECT auth_key_id
|
||
FROM authorizations
|
||
WHERE user_id = $1 AND hash = $2`, userID, hash).Scan(&candidate); err != nil {
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return domain.Authorization{}, false, nil
|
||
}
|
||
return domain.Authorization{}, false, fmt.Errorf("select revoke candidate by hash: %w", err)
|
||
}
|
||
if err := lockPermanentAuthIdentities(ctx, tx, []int64{candidate}); err != nil {
|
||
return domain.Authorization{}, false, err
|
||
}
|
||
|
||
var locked int64
|
||
if err := tx.QueryRow(ctx, `
|
||
SELECT auth_key_id
|
||
FROM auth_keys
|
||
WHERE auth_key_id = $1
|
||
FOR UPDATE`, candidate).Scan(&locked); err != nil {
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return domain.Authorization{}, false, nil
|
||
}
|
||
return domain.Authorization{}, false, fmt.Errorf("lock revoke auth key by hash: %w", err)
|
||
}
|
||
|
||
a, found, err := scanRevokedAuthorization(tx.QueryRow(ctx, `
|
||
SELECT auth_key_id, user_id, hash, layer, device_model, platform, system_version,
|
||
api_id, app_version, ip, password_pending, created_at, active_at
|
||
FROM authorizations
|
||
WHERE auth_key_id = $1 AND user_id = $2 AND hash = $3
|
||
FOR UPDATE`, candidate, userID, hash))
|
||
if err != nil {
|
||
return domain.Authorization{}, false, fmt.Errorf("revalidate revoke authorization by hash: %w", err)
|
||
}
|
||
if !found {
|
||
return domain.Authorization{}, false, nil
|
||
}
|
||
if err := deleteRevokedAuthorizationStateTx(ctx, tx, []int64{candidate}); err != nil {
|
||
return domain.Authorization{}, false, err
|
||
}
|
||
return a, true, nil
|
||
}
|
||
|
||
func (s *AuthorizationStore) DeleteByUserExcept(ctx context.Context, userID int64, keepAuthKeyID [8]byte) ([]domain.Authorization, error) {
|
||
rows, err := s.db.Query(ctx, `
|
||
DELETE FROM authorizations
|
||
WHERE user_id = $1 AND auth_key_id <> $2
|
||
RETURNING auth_key_id, user_id, hash, layer, device_model, platform, system_version, api_id, app_version, ip, created_at, active_at`, userID, authKeyIDToInt64(keepAuthKeyID))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("delete authorizations by user: %w", err)
|
||
}
|
||
defer rows.Close()
|
||
out := make([]domain.Authorization, 0)
|
||
for rows.Next() {
|
||
var a domain.Authorization
|
||
var authKeyID int64
|
||
if err := rows.Scan(
|
||
&authKeyID, &a.UserID, &a.Hash, &a.Layer, &a.DeviceModel, &a.Platform, &a.SystemVersion,
|
||
&a.APIID, &a.AppVersion, &a.IP, &a.CreatedAt, &a.ActiveAt,
|
||
); err != nil {
|
||
return nil, fmt.Errorf("scan deleted authorization: %w", err)
|
||
}
|
||
a.AuthKeyID = authKeyIDFromInt64(authKeyID)
|
||
out = append(out, a)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, fmt.Errorf("iterate deleted authorizations: %w", err)
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
// 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 {
|
||
var err error
|
||
out, err = revokeByUserExceptTx(ctx, tx, userID, authKeyIDToInt64(keepAuthKeyID))
|
||
return err
|
||
})
|
||
return out, err
|
||
}
|
||
|
||
func revokeByUserExceptTx(ctx context.Context, tx pgx.Tx, userID, keepAuthKeyID int64) ([]domain.Authorization, error) {
|
||
candidateRows, err := tx.Query(ctx, `
|
||
SELECT auth_key_id
|
||
FROM authorizations
|
||
WHERE user_id = $1 AND auth_key_id <> $2
|
||
ORDER BY auth_key_id`, userID, keepAuthKeyID)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("select revoke candidates by user: %w", err)
|
||
}
|
||
candidates := make([]int64, 0)
|
||
for candidateRows.Next() {
|
||
var id int64
|
||
if err := candidateRows.Scan(&id); err != nil {
|
||
candidateRows.Close()
|
||
return nil, fmt.Errorf("scan revoke candidate by user: %w", err)
|
||
}
|
||
candidates = append(candidates, id)
|
||
}
|
||
if err := candidateRows.Err(); err != nil {
|
||
candidateRows.Close()
|
||
return nil, fmt.Errorf("iterate revoke candidates by user: %w", err)
|
||
}
|
||
candidateRows.Close()
|
||
if len(candidates) == 0 {
|
||
return []domain.Authorization{}, nil
|
||
}
|
||
if err := lockPermanentAuthIdentities(ctx, tx, candidates); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Advisory keys are already all held in final int32-hash order. Parent rows
|
||
// are then locked by their real bigint IDs for deterministic batch behavior.
|
||
lockRows, err := tx.Query(ctx, `
|
||
SELECT auth_key_id
|
||
FROM auth_keys
|
||
WHERE auth_key_id = ANY($1::bigint[])
|
||
ORDER BY auth_key_id
|
||
FOR UPDATE`, candidates)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("lock revoke auth keys by user: %w", err)
|
||
}
|
||
for lockRows.Next() {
|
||
var ignored int64
|
||
if err := lockRows.Scan(&ignored); err != nil {
|
||
lockRows.Close()
|
||
return nil, fmt.Errorf("scan locked revoke auth key: %w", err)
|
||
}
|
||
}
|
||
if err := lockRows.Err(); err != nil {
|
||
lockRows.Close()
|
||
return nil, fmt.Errorf("iterate locked revoke auth keys: %w", err)
|
||
}
|
||
lockRows.Close()
|
||
|
||
// This is intentionally a new statement snapshot after all parent locks.
|
||
// Keys that changed owner while waiting are omitted and must remain intact.
|
||
rows, err := tx.Query(ctx, `
|
||
SELECT auth_key_id, user_id, hash, layer, device_model, platform, system_version,
|
||
api_id, app_version, ip, password_pending, created_at, active_at
|
||
FROM authorizations
|
||
WHERE user_id = $1
|
||
AND auth_key_id <> $2
|
||
AND auth_key_id = ANY($3::bigint[])
|
||
ORDER BY created_at, auth_key_id
|
||
FOR UPDATE`, userID, keepAuthKeyID, candidates)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("revalidate revoke authorizations by user: %w", err)
|
||
}
|
||
out := make([]domain.Authorization, 0)
|
||
for rows.Next() {
|
||
a, err := scanRevokedAuthorizationRow(rows)
|
||
if err != nil {
|
||
rows.Close()
|
||
return nil, err
|
||
}
|
||
out = append(out, a)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
rows.Close()
|
||
return nil, fmt.Errorf("iterate revoked authorizations: %w", err)
|
||
}
|
||
rows.Close()
|
||
if len(out) == 0 {
|
||
return out, nil
|
||
}
|
||
targets := make([]int64, len(out))
|
||
for i := range out {
|
||
targets[i] = authKeyIDToInt64(out[i].AuthKeyID)
|
||
}
|
||
if err := deleteRevokedAuthorizationStateTx(ctx, tx, targets); err != nil {
|
||
return nil, err
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
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
|
||
}
|
||
if _, err := tx.Exec(ctx, `
|
||
DELETE FROM auth_keys
|
||
WHERE auth_key_id IN (
|
||
SELECT temp_auth_key_id
|
||
FROM temp_auth_key_bindings
|
||
WHERE perm_auth_key_id = ANY($1::bigint[])
|
||
)`, authKeyIDs); err != nil {
|
||
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 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 permanent auth keys: %w", err)
|
||
}
|
||
if tag.RowsAffected() != int64(len(authKeyIDs)) {
|
||
return fmt.Errorf("delete permanent auth keys: deleted %d of %d locked targets", tag.RowsAffected(), len(authKeyIDs))
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func authorizationFromRow(row sqlcgen.Authorization) domain.Authorization {
|
||
return domain.Authorization{
|
||
AuthKeyID: authKeyIDFromInt64(row.AuthKeyID),
|
||
UserID: row.UserID,
|
||
Hash: row.Hash,
|
||
Layer: int(row.Layer),
|
||
DeviceModel: row.DeviceModel,
|
||
Platform: row.Platform,
|
||
SystemVersion: row.SystemVersion,
|
||
APIID: int(row.ApiID),
|
||
AppVersion: row.AppVersion,
|
||
IP: row.Ip,
|
||
CreatedAt: row.CreatedAt.Time,
|
||
ActiveAt: row.ActiveAt.Time,
|
||
}
|
||
}
|
||
|
||
type authorizationScanner interface {
|
||
Scan(dest ...any) error
|
||
}
|
||
|
||
func scanRevokedAuthorization(row authorizationScanner) (domain.Authorization, bool, error) {
|
||
a, err := scanRevokedAuthorizationRow(row)
|
||
if err != nil {
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return domain.Authorization{}, false, nil
|
||
}
|
||
return domain.Authorization{}, false, err
|
||
}
|
||
return a, true, nil
|
||
}
|
||
|
||
func scanRevokedAuthorizationRow(row authorizationScanner) (domain.Authorization, error) {
|
||
var a domain.Authorization
|
||
var authKeyID int64
|
||
if err := row.Scan(
|
||
&authKeyID, &a.UserID, &a.Hash, &a.Layer, &a.DeviceModel, &a.Platform, &a.SystemVersion,
|
||
&a.APIID, &a.AppVersion, &a.IP, &a.PasswordPending, &a.CreatedAt, &a.ActiveAt,
|
||
); err != nil {
|
||
return domain.Authorization{}, err
|
||
}
|
||
a.AuthKeyID = authKeyIDFromInt64(authKeyID)
|
||
return a, nil
|
||
}
|
||
|
||
func authorizationHash(authKeyID [8]byte) int64 {
|
||
sum := sha256.Sum256(authKeyID[:])
|
||
hash := int64(binary.LittleEndian.Uint64(sum[:8]))
|
||
if hash == 0 {
|
||
return 1
|
||
}
|
||
return hash
|
||
}
|