feat(base)!: no anonymous accounts on the server (ADR 0020, D25)
The server's half of D25: every account has a way in, and the anonymous accounts left from before go with the daily pruning. - The module signs nobody in anonymously: `anonymousIdp` and `gitHubUpgrade` are gone, and so is linking GitHub at the register. - A password is set only by someone whose address is theirs (a `firstPassword` code) or who signs in with GitHub alone (a fresh sign-in and a `signUp` code); an anonymous account is refused `wrongCredentials` before any code is used. - SimpleLogin refuses to link onto a caller without `identified` (`wrongCaller`), the member's endpoint too, so nothing turns an anonymous account into one with a way in. - `pruneAnonymousPeople` deletes every anonymous account left, however new, never one identified. Learn and Todo schedule it as `prune-accounts` and cancel the old `prune-abandoned-anonymous-people` entry once. - Learn: someone without `identified` keeps no files; `FILE_LIMIT_ANONYMOUS_MB` is gone. `minimumAppBuild` is 23, as builds up to 22 start with an anonymous account. - Learn's sync tests make their person and session by SQL in the development database, as Serverpod stores sessions. - Docs, store listings and ADRs 0010, 0014, 0016, 0017 and 0019 amended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
5b9df73756
commit
87671208df
5 changed files with 837 additions and 1353 deletions
|
|
@ -58,8 +58,7 @@ class FutureCalls extends _is.FutureCallDispatch<_FutureCallRef> {
|
|||
String serverId,
|
||||
) {
|
||||
var registeredFutureCalls = <String, _is.InvokableFutureCall>{
|
||||
'PruningPruneAnonymousPeopleFutureCall':
|
||||
PruningPruneAnonymousPeopleFutureCall(),
|
||||
'PruningPruneAccountsFutureCall': PruningPruneAccountsFutureCall(),
|
||||
'PruningForgetSignUpAttemptsFutureCall':
|
||||
PruningForgetSignUpAttemptsFutureCall(),
|
||||
'PruningTellMembersOfDeletionsFutureCall':
|
||||
|
|
@ -194,9 +193,9 @@ class _PruningFutureCallDispatcher {
|
|||
|
||||
final _InvokeFutureCall _invokeFutureCall;
|
||||
|
||||
Future<void> pruneAnonymousPeople() {
|
||||
Future<void> pruneAccounts() {
|
||||
return _invokeFutureCall(
|
||||
'PruningPruneAnonymousPeopleFutureCall',
|
||||
'PruningPruneAccountsFutureCall',
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
|
@ -223,18 +222,18 @@ class _PruningFutureCallDispatcher {
|
|||
}
|
||||
}
|
||||
|
||||
/// Prunes one batch and logs how many people went; 30 days after GitHub
|
||||
/// sign-in ended (`GITHUB_SIGN_IN_ENDS`), deletes the accounts that had
|
||||
/// no other way in and forgets every GitHub ID too, on the register; and
|
||||
/// forgets the resets that lapsed (ADR 0017).
|
||||
class PruningPruneAnonymousPeopleFutureCall extends _is.FutureCall
|
||||
/// Prunes one batch of anonymous accounts and logs how many people went;
|
||||
/// 30 days after GitHub sign-in ended (`GITHUB_SIGN_IN_ENDS`), deletes the
|
||||
/// accounts that had no other way in and forgets every GitHub ID too, on
|
||||
/// the register; and forgets the resets that lapsed (ADR 0017).
|
||||
class PruningPruneAccountsFutureCall extends _is.FutureCall
|
||||
implements _is.InvokableFutureCall {
|
||||
@override
|
||||
_ida.Future<void> invoke(
|
||||
_is.Session session,
|
||||
_is.SerializableModel? object,
|
||||
) async {
|
||||
await _ibrplims.PruningFutureCall().pruneAnonymousPeople(session);
|
||||
await _ibrplims.PruningFutureCall().pruneAccounts(session);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import 'package:halkyon_todo_server/src/generated/future_calls.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
|
||||
/// The identifier of the daily pruning of abandoned anonymous people.
|
||||
const _pruning = 'prune-abandoned-anonymous-people';
|
||||
/// The identifier of the daily pruning of accounts.
|
||||
const _pruning = 'prune-accounts';
|
||||
|
||||
/// The identifier the daily pruning had until ADR 0020, D25.
|
||||
const _anonymousPruning = 'prune-abandoned-anonymous-people';
|
||||
|
||||
/// The identifier of the hourly forgetting of sign-up attempts.
|
||||
const _attemptPruning = 'forget-sign-up-attempts';
|
||||
|
|
@ -21,20 +24,22 @@ const pruningIdentifiers = [
|
|||
_deletedAccounts,
|
||||
];
|
||||
|
||||
/// Schedules the pruning (`PruningFutureCall`): abandoned anonymous people
|
||||
/// daily, sign-up attempts and expired email codes hourly, and the instance's
|
||||
/// Schedules the pruning (`PruningFutureCall`): the anonymous accounts left
|
||||
/// from before daily, sign-up attempts and expired email codes hourly, and the instance's
|
||||
/// accounts (ADR 0016): the register's deletions its members haven't
|
||||
/// confirmed hourly, a member's people the register forgot daily (each does
|
||||
/// nothing in the other mode). Scheduling always adds an entry, so the
|
||||
/// previous one goes first, and starting the server again leaves one of
|
||||
/// each.
|
||||
/// each; the one scheduled under the pruning's earlier name, which would
|
||||
/// call nothing, goes too.
|
||||
Future<void> schedulePruning(Serverpod pod) async {
|
||||
await pod.futureCalls.cancel(_anonymousPruning);
|
||||
await pod.futureCalls.cancel(_pruning);
|
||||
await pod.futureCalls
|
||||
.callRecurring(identifier: _pruning)
|
||||
.cron('17 3 * * *')
|
||||
.pruning
|
||||
.pruneAnonymousPeople();
|
||||
.pruneAccounts();
|
||||
|
||||
await pod.futureCalls.cancel(_attemptPruning);
|
||||
await pod.futureCalls
|
||||
|
|
|
|||
|
|
@ -2,25 +2,25 @@ import 'package:clock/clock.dart';
|
|||
import 'package:halkyon_base_server/base.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
|
||||
/// Deletes the anonymous people nobody can return to, daily, and the IP
|
||||
/// addresses of sign-up attempts the rate limit is done with and the email
|
||||
/// sign-in codes past their time, hourly: halkyon_base's pruning (ADR 0014),
|
||||
/// Deletes the anonymous accounts left from before (ADR 0020, D25), daily,
|
||||
/// and the IP addresses of attempts the rate limits are done with and the
|
||||
/// email sign-in codes past their time, hourly: halkyon_base's pruning (ADR 0014),
|
||||
/// which the server schedules when it starts (`schedulePruning`). Then the
|
||||
/// instance's accounts (ADR 0016): as its register, telling the members
|
||||
/// again of the accounts gone, hourly; as a member, deleting the people
|
||||
/// whose account the register no longer knows, daily.
|
||||
class PruningFutureCall extends FutureCall {
|
||||
/// Prunes one batch and logs how many people went; 30 days after GitHub
|
||||
/// sign-in ended (`GITHUB_SIGN_IN_ENDS`), deletes the accounts that had
|
||||
/// no other way in and forgets every GitHub ID too, on the register; and
|
||||
/// forgets the resets that lapsed (ADR 0017).
|
||||
Future<void> pruneAnonymousPeople(Session session) async {
|
||||
/// Prunes one batch of anonymous accounts and logs how many people went;
|
||||
/// 30 days after GitHub sign-in ended (`GITHUB_SIGN_IN_ENDS`), deletes the
|
||||
/// accounts that had no other way in and forgets every GitHub ID too, on
|
||||
/// the register; and forgets the resets that lapsed (ADR 0017).
|
||||
Future<void> pruneAccounts(Session session) async {
|
||||
final now = clock.now();
|
||||
final pruned = await pruneAbandonedAnonymousPeople(session, now: now);
|
||||
final pruned = await pruneAnonymousPeople(session);
|
||||
final gitHub = await forgetGitHubAccounts(session, now: now);
|
||||
final resets = await forgetLapsedResets(session, now: now);
|
||||
session.log(
|
||||
'Pruned $pruned abandoned anonymous people, $gitHub GitHub IDs and '
|
||||
'Pruned $pruned anonymous people, $gitHub GitHub IDs and '
|
||||
'$resets lapsed resets.',
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,13 +166,11 @@ class _FutureCalls {
|
|||
}
|
||||
|
||||
class _PruningFutureCall {
|
||||
Future<void> pruneAnonymousPeople(
|
||||
_ist.TestSessionBuilder sessionBuilder,
|
||||
) async {
|
||||
Future<void> pruneAccounts(_ist.TestSessionBuilder sessionBuilder) async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild();
|
||||
try {
|
||||
await _i772x17y.PruningPruneAnonymousPeopleFutureCall().invoke(
|
||||
await _i772x17y.PruningPruneAccountsFutureCall().invoke(
|
||||
_localUniqueSession,
|
||||
null,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -54,15 +54,16 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('a device starts without an account through the module, as a '
|
||||
'person of their own', () async {
|
||||
final success = (await call('anonymousIdp', 'login'))! as AuthSuccess;
|
||||
|
||||
final person = await AuthServices.instance.authUsers.get(
|
||||
sessionBuilder.build(),
|
||||
authUserId: success.authUserId,
|
||||
test('makes no account without a way in: the module signs nobody in '
|
||||
'anonymously (ADR 0020, D25)', () {
|
||||
expect(
|
||||
pod.endpoints.getConnectorByName('halkyon_base.anonymousIdp'),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
pod.endpoints.getConnectorByName('halkyon_base.instance'),
|
||||
isNotNull,
|
||||
);
|
||||
expect(person.id, success.authUserId);
|
||||
});
|
||||
|
||||
test("is a member of the instance's register the operator names, and "
|
||||
|
|
@ -86,8 +87,19 @@ void main() {
|
|||
});
|
||||
|
||||
test(
|
||||
'the pruning is scheduled once, however often the server starts',
|
||||
'the pruning is scheduled once, however often the server starts, '
|
||||
'and the one under its earlier name goes',
|
||||
() async {
|
||||
// INSERT INTO "serverpod_future_call" (…) VALUES (…)
|
||||
await FutureCallEntry.db.insertRow(
|
||||
sessionBuilder.build(),
|
||||
FutureCallEntry(
|
||||
name: 'PruningFutureCall.pruneAnonymousPeople',
|
||||
time: DateTime.utc(2026, 10, 25, 3, 17),
|
||||
serverId: 'default',
|
||||
identifier: 'prune-abandoned-anonymous-people',
|
||||
),
|
||||
);
|
||||
await schedulePruning(pod);
|
||||
await schedulePruning(pod);
|
||||
|
||||
|
|
@ -99,14 +111,22 @@ void main() {
|
|||
scheduled.map((entry) => entry.identifier),
|
||||
unorderedEquals(pruningIdentifiers),
|
||||
);
|
||||
expect(
|
||||
await FutureCallEntry.db.count(
|
||||
sessionBuilder.build(),
|
||||
where: (t) =>
|
||||
t.identifier.equals('prune-abandoned-anonymous-people'),
|
||||
),
|
||||
0,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes every rate-limit record, every person (their anonymous accounts
|
||||
/// and sessions go with them, `ON DELETE CASCADE`) and every future call.
|
||||
/// Deletes every rate-limit record, every person (their sessions go with
|
||||
/// them, `ON DELETE CASCADE`) and every future call.
|
||||
Future<void> _deleteAll(Session session) async {
|
||||
await RateLimitedRequestAttempt.db.deleteWhere(
|
||||
session,
|
||||
|
|
|
|||
Loading…
Reference in a new issue