fix(auth): members learn of ended sessions in the background
The full reading's L5: every change or reset of a password told the members inside the request, up to 500 notices one after another with a timeout of 10 seconds each, so a member out of reach held the change up for minutes. Now announceSessionsEnded tells the members of this account's notices alone, in the background on a session of its own; those not confirmed wait for the hourly run as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
5eb74639b6
commit
76291e6ab4
3 changed files with 74 additions and 8 deletions
|
|
@ -1,8 +1,11 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:halkyon_base_server/src/generated/protocol.dart';
|
||||
import 'package:halkyon_base_server/src/halkyon_base.dart';
|
||||
import 'package:halkyon_base_server/src/register/accounts_mode.dart';
|
||||
import 'package:halkyon_base_server/src/register/deletion_notices.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
|
||||
|
|
@ -80,21 +83,55 @@ Future<DateTime> endSessionsEverywhere(
|
|||
|
||||
/// What follows [endSessionsEverywhere] once its transaction holds: the
|
||||
/// account's devices here learn that their sessions ended, and the members
|
||||
/// are told.
|
||||
/// are told, in the background (the full reading's L5: told inside the
|
||||
/// request, a member out of reach held a change of password up for its
|
||||
/// timeout, and the other notices waiting for minutes), this account's
|
||||
/// notices first; those not confirmed are told again hourly
|
||||
/// ([deliverSessionNotices], with the pruning).
|
||||
Future<void> announceSessionsEnded(Session session, UuidValue account) async {
|
||||
await session.messages.authenticationRevoked(
|
||||
account.uuid,
|
||||
RevokedAuthenticationUser(),
|
||||
);
|
||||
await deliverSessionNotices(session);
|
||||
final delivery = _deliverInBackground(session.serverpod, account);
|
||||
_deliveries.add(delivery);
|
||||
unawaited(delivery.whenComplete(() => _deliveries.remove(delivery)));
|
||||
}
|
||||
|
||||
/// The deliveries [announceSessionsEnded] began, until they end.
|
||||
final _deliveries = <Future<void>>{};
|
||||
|
||||
/// Tells the members of [account]'s ended sessions, on a session of its
|
||||
/// own: the request's may close first.
|
||||
Future<void> _deliverInBackground(Serverpod pod, UuidValue account) async {
|
||||
final session = await pod.createSession(enableLogging: false);
|
||||
try {
|
||||
await deliverSessionNotices(session, account: account);
|
||||
} on Object catch (error) {
|
||||
// Its type only: what a member answered tells nobody here more.
|
||||
session.log(
|
||||
'Telling members of ended sessions failed: ${error.runtimeType}',
|
||||
level: LogLevel.warning,
|
||||
);
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// Waits for the deliveries [announceSessionsEnded] began: tests do.
|
||||
@visibleForTesting
|
||||
Future<void> sessionNoticesDelivered() => Future.wait([..._deliveries]);
|
||||
|
||||
/// Tells the members the sessions ended that they haven't confirmed yet, and
|
||||
/// returns how many confirmed; the others stay, counted, for the next time
|
||||
/// (the register's hourly pruning, and at once after each change), those
|
||||
/// tried least first, 500 at a time. Notices to a member the operator no
|
||||
/// longer names are dropped.
|
||||
Future<int> deliverSessionNotices(Session session, {DateTime? now}) async {
|
||||
/// (the register's hourly pruning, and after each change [account]'s alone,
|
||||
/// in the background), those tried least first, 500 at a time. Notices to a
|
||||
/// member the operator no longer names are dropped.
|
||||
Future<int> deliverSessionNotices(
|
||||
Session session, {
|
||||
DateTime? now,
|
||||
UuidValue? account,
|
||||
}) async {
|
||||
final members = switch (HalkyonBase.maybeOf(session.serverpod)?.accounts) {
|
||||
HoldsRegister(:final members) => {for (final member in members) '$member'},
|
||||
_ => const <String>{},
|
||||
|
|
@ -105,10 +142,11 @@ Future<int> deliverSessionNotices(Session session, {DateTime? now}) async {
|
|||
where: (t) => t.member.notInSet(members),
|
||||
);
|
||||
final notifier = MemberNotifier.of(session.serverpod);
|
||||
// SELECT … FROM "sessions_ended_notice" ORDER BY "attempts", "id"
|
||||
// LIMIT 500
|
||||
// SELECT … FROM "sessions_ended_notice" [WHERE "account" = $1]
|
||||
// ORDER BY "attempts", "id" LIMIT 500
|
||||
final notices = await SessionsEndedNotice.db.find(
|
||||
session,
|
||||
where: account == null ? null : (t) => t.account.equals(account),
|
||||
orderByList: (t) => [t.attempts.asc(), t.id.asc()],
|
||||
limit: 500,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
|
|
@ -7,6 +8,8 @@ import 'package:halkyon_base_server/src/password/password_endpoint.dart'
|
|||
show changesBegunPerSession;
|
||||
import 'package:halkyon_base_server/src/password/passwords.dart'
|
||||
show writeNewPassword;
|
||||
import 'package:halkyon_base_server/src/register/sessions_ended.dart'
|
||||
show sessionNoticesDelivered;
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
|
@ -24,6 +27,10 @@ final class _Members implements MemberNotifier {
|
|||
final sessionsTold = <(Uri, UuidValue, DateTime, UuidValue?)>[];
|
||||
bool answering = true;
|
||||
|
||||
/// What a member's answer waits for, as one out of reach keeps it
|
||||
/// waiting; none by default.
|
||||
Future<void>? answerAfter;
|
||||
|
||||
@override
|
||||
Future<bool> accountGone(Uri member, UuidValue account) async => answering;
|
||||
|
||||
|
|
@ -34,6 +41,7 @@ final class _Members implements MemberNotifier {
|
|||
required DateTime endedBefore,
|
||||
UuidValue? keepSession,
|
||||
}) async {
|
||||
await answerAfter;
|
||||
sessionsTold.add((member, account, endedBefore, keepSession));
|
||||
return answering;
|
||||
}
|
||||
|
|
@ -241,6 +249,7 @@ void main() {
|
|||
final sessions = await sessionsOf(anna);
|
||||
expect(sessions, contains(annasPhone));
|
||||
expect(sessions, isNot(contains(annasLaptop)));
|
||||
await sessionNoticesDelivered();
|
||||
final (member, account, endedBefore, kept) =
|
||||
members.sessionsTold.first;
|
||||
expect(member, _todo);
|
||||
|
|
@ -307,6 +316,7 @@ void main() {
|
|||
kept,
|
||||
);
|
||||
expect(await sessionsOf(anna), {annasPhone, annasLaptop});
|
||||
await sessionNoticesDelivered();
|
||||
expect(members.sessionsTold, isEmpty);
|
||||
});
|
||||
|
||||
|
|
@ -566,10 +576,23 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('is done without waiting for the members: one out of reach '
|
||||
"holds no change up (the full reading's L5)", () async {
|
||||
final reached = Completer<void>();
|
||||
members.answerAfter = reached.future;
|
||||
await change();
|
||||
expect(members.sessionsTold, isEmpty);
|
||||
reached.complete();
|
||||
await sessionNoticesDelivered();
|
||||
expect(members.sessionsTold, hasLength(1));
|
||||
expect(await SessionsEndedNotice.db.count(session), 0);
|
||||
});
|
||||
|
||||
test('tells a member that missed it again, hourly, until it '
|
||||
'confirms', () async {
|
||||
members.answering = false;
|
||||
await change();
|
||||
await sessionNoticesDelivered();
|
||||
expect(await SessionsEndedNotice.db.count(session), 1);
|
||||
|
||||
members.answering = true;
|
||||
|
|
@ -1042,6 +1065,7 @@ void main() {
|
|||
expect(sessions, hasLength(1));
|
||||
expect(sessions, isNot(contains(annasPhone)));
|
||||
expect(sessions, isNot(contains(annasLaptop)));
|
||||
await sessionNoticesDelivered();
|
||||
expect(members.sessionsTold.single.$2, anna);
|
||||
expect(members.sessionsTold.single.$4, isNull);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import 'package:halkyon_base_server/src/register/deletion_notices.dart';
|
|||
import 'package:halkyon_base_server/src/register/instance_api.dart';
|
||||
import 'package:halkyon_base_server/src/register/register_protocol.dart';
|
||||
import 'package:halkyon_base_server/src/register/remote_register.dart';
|
||||
import 'package:halkyon_base_server/src/register/sessions_ended.dart'
|
||||
show sessionNoticesDelivered;
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
|
|
@ -677,6 +679,7 @@ void main() {
|
|||
(account: annaOnTodo, endedBefore: now),
|
||||
);
|
||||
// Every member is told to end the account's sessions too.
|
||||
await sessionNoticesDelivered();
|
||||
expect(members.sessionsTold.single, (_todo, annaOnTodo, now, null));
|
||||
|
||||
final forReset = await remote.beginPasswordSignIn(
|
||||
|
|
@ -809,6 +812,7 @@ void main() {
|
|||
password: device.newPassword(),
|
||||
keepSession: annasPhone,
|
||||
);
|
||||
await sessionNoticesDelivered();
|
||||
expect(members.sessionsTold.single, (
|
||||
_todo,
|
||||
annaOnTodo,
|
||||
|
|
|
|||
Loading…
Reference in a new issue