added ability to check shared devices
This commit is contained in:
parent
7d0daef923
commit
72db66d4aa
14 changed files with 456 additions and 23 deletions
|
|
@ -157,6 +157,33 @@ type AuthorizationRow struct {
|
|||
ActiveAt time.Time
|
||||
}
|
||||
|
||||
// SharedDeviceAccount is one account whose authorizations matched a
|
||||
// SharedDeviceGroup's device fingerprint.
|
||||
type SharedDeviceAccount struct {
|
||||
UserID int64
|
||||
Phone string
|
||||
Username string
|
||||
FirstName string
|
||||
LastName string
|
||||
ActiveAt time.Time
|
||||
}
|
||||
|
||||
// SharedDeviceGroup is a device fingerprint (device_model + system_version +
|
||||
// platform + ip) shared by more than one distinct account's authorizations --
|
||||
// a heuristic multi-accounting signal, not proof: device_model/system_version
|
||||
// are client-reported and spoofable, and ip alone collides naturally behind
|
||||
// NAT, shared wifi, or carrier CGNAT. Treat this as a lead to investigate, not
|
||||
// a verdict.
|
||||
type SharedDeviceGroup struct {
|
||||
DeviceModel string
|
||||
SystemVersion string
|
||||
Platform string
|
||||
IP string
|
||||
AccountCount int
|
||||
LastActiveAt time.Time
|
||||
Accounts []SharedDeviceAccount
|
||||
}
|
||||
|
||||
type AuditLogRow struct {
|
||||
ID int64
|
||||
CommandID string
|
||||
|
|
@ -787,6 +814,88 @@ LIMIT $3`, beforeActiveUS, beforeID, limit+1)
|
|||
return out, hasMore, nil
|
||||
}
|
||||
|
||||
// ListSharedDeviceGroups pages through device fingerprints (device_model +
|
||||
// system_version + platform + ip) that more than one distinct account has
|
||||
// authorized from -- see SharedDeviceGroup's doc comment on why this is a
|
||||
// heuristic, not a verdict. Pagination is plain offset/limit over the
|
||||
// aggregated group list (not the raw authorizations table): the number of
|
||||
// *groups* is expected to stay small relative to total session count, so an
|
||||
// offset scan over the pre-aggregated CTE is cheap even though offset
|
||||
// pagination over raw rows elsewhere in this file deliberately uses a keyset
|
||||
// cursor instead.
|
||||
func (s *readStore) ListSharedDeviceGroups(ctx context.Context, offset, limit int) ([]SharedDeviceGroup, bool, error) {
|
||||
if limit <= 0 {
|
||||
limit = accountListDefaultLimit
|
||||
}
|
||||
if limit > accountListMaxLimit {
|
||||
limit = accountListMaxLimit
|
||||
}
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
WITH device_groups AS (
|
||||
SELECT device_model, system_version, platform, ip,
|
||||
count(DISTINCT user_id)::int AS account_count,
|
||||
max(active_at) AS last_active_at
|
||||
FROM authorizations
|
||||
WHERE device_model <> ''
|
||||
GROUP BY device_model, system_version, platform, ip
|
||||
HAVING count(DISTINCT user_id) > 1
|
||||
ORDER BY max(active_at) DESC, device_model, system_version, platform, ip
|
||||
LIMIT $1 OFFSET $2
|
||||
),
|
||||
members AS (
|
||||
SELECT DISTINCT ON (a.device_model, a.system_version, a.platform, a.ip, a.user_id)
|
||||
a.device_model, a.system_version, a.platform, a.ip, a.user_id, a.active_at
|
||||
FROM authorizations a
|
||||
JOIN device_groups g ON a.device_model = g.device_model AND a.system_version = g.system_version
|
||||
AND a.platform = g.platform AND a.ip = g.ip
|
||||
ORDER BY a.device_model, a.system_version, a.platform, a.ip, a.user_id, a.active_at DESC
|
||||
)
|
||||
SELECT g.device_model, g.system_version, g.platform, g.ip, g.account_count, g.last_active_at,
|
||||
m.user_id, m.active_at, u.phone, u.username, u.first_name, u.last_name
|
||||
FROM device_groups g
|
||||
JOIN members m ON m.device_model = g.device_model AND m.system_version = g.system_version
|
||||
AND m.platform = g.platform AND m.ip = g.ip
|
||||
JOIN users u ON u.id = m.user_id
|
||||
ORDER BY g.last_active_at DESC, g.device_model, g.system_version, g.platform, g.ip, m.active_at DESC`,
|
||||
limit+1, offset)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("list shared device groups: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
groups := make([]SharedDeviceGroup, 0, limit+1)
|
||||
for rows.Next() {
|
||||
var (
|
||||
deviceModel, systemVersion, platform, ip string
|
||||
accountCount int
|
||||
lastActiveAt time.Time
|
||||
acc SharedDeviceAccount
|
||||
)
|
||||
if err := rows.Scan(&deviceModel, &systemVersion, &platform, &ip, &accountCount, &lastActiveAt,
|
||||
&acc.UserID, &acc.ActiveAt, &acc.Phone, &acc.Username, &acc.FirstName, &acc.LastName); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if len(groups) == 0 {
|
||||
groups = append(groups, SharedDeviceGroup{DeviceModel: deviceModel, SystemVersion: systemVersion, Platform: platform, IP: ip, AccountCount: accountCount, LastActiveAt: lastActiveAt})
|
||||
} else if last := &groups[len(groups)-1]; last.DeviceModel != deviceModel || last.SystemVersion != systemVersion || last.Platform != platform || last.IP != ip {
|
||||
groups = append(groups, SharedDeviceGroup{DeviceModel: deviceModel, SystemVersion: systemVersion, Platform: platform, IP: ip, AccountCount: accountCount, LastActiveAt: lastActiveAt})
|
||||
}
|
||||
last := &groups[len(groups)-1]
|
||||
last.Accounts = append(last.Accounts, acc)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
hasMore := len(groups) > limit
|
||||
if hasMore {
|
||||
groups = groups[:limit]
|
||||
}
|
||||
return groups, hasMore, nil
|
||||
}
|
||||
|
||||
func (s *readStore) AccountDetail(ctx context.Context, userID int64) (AccountDetail, error) {
|
||||
var out AccountDetail
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue