feat(base)!: no anonymous accounts on the server (ADR 0020, D25)
All checks were successful
CI / check (push) Successful in 30m13s
CI / deploy (push) Has been skipped

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:
Tizian 「ティツィアーン」 2026-10-24 16:07:27 +02:00
commit 87671208df
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -435,56 +435,62 @@ void main() {
expect(mailbox.sent, hasLength(11));
});
test("a code proves the address for a member's password, and a "
'wrong one refuses it, keeping nothing', () async {
test("a code to a member's person's address proves it for their "
'password, a wrong one refusing it, and someone without a way '
'in sets none, before the code is used', () async {
usePasswordKeys(pod, testPasswordKeys);
addTearDown(() => usePasswordKeys(pod, null));
final device = await PasswordDevice.make();
final annaOnTodo = const Uuid().v7obj();
final annaOnTodo = await newPersonWithAddress(
session,
'anna@example.org',
);
final request = await remote.sendSignInCode(
session,
address: 'Anna@example.org',
language: 'de',
network: '203.0.113.7',
purpose: EmailCodePurpose.signUp,
purpose: EmailCodePurpose.firstPassword,
);
Future<int> annasPasswords() => AccountPassword.db.count(
session,
where: (t) => t.authUserId.equals(annaOnTodo),
);
await expectLater(
remote.setPassword(
session,
person: annaOnTodo,
anonymousHere: true,
setup: device.setup(),
request: request,
code: 'AAAA-AAAA',
),
refused(EmailSignInRefusal.wrongCode),
);
expect(await AuthUser.db.findById(session, annaOnTodo), isNull);
// Without the member saying Anna is anonymous there, refused
// before her code is used (the full reading's M1).
expect(await annasPasswords(), 0);
// Someone without a way in (an anonymous account left from
// before, ADR 0020, D25, or a GitHub ID forgotten, the full
// reading's M1) is refused before the code is used.
final carlOnTodo = const Uuid().v7obj();
await expectLater(
remote.setPassword(
session,
person: annaOnTodo,
person: carlOnTodo,
setup: device.setup(),
request: request,
code: mailbox.codeFor('anna@example.org'),
),
refused(PasswordRefusal.wrongCredentials),
);
expect(await AuthUser.db.findById(session, carlOnTodo), isNull);
await remote.setPassword(
session,
person: annaOnTodo,
anonymousHere: true,
setup: device.setup(),
request: request,
code: mailbox.codeFor('anna@example.org'),
);
expect(
(await AuthUser.db.findById(session, annaOnTodo))!.scopeNames,
contains(identified.name),
);
expect(await annasPasswords(), 1);
expect(await remote.knownAccounts(session, {annaOnTodo, ben}), {
annaOnTodo,
ben,
@ -493,41 +499,11 @@ void main() {
});
group('GitHub', () {
test("links a member's person, who becomes an account under the "
'same ID, found by it afterwards', () async {
final annaOnTodo = const Uuid().v7obj();
await remote.linkGitHub(
session,
person: annaOnTodo,
gitHubUserId: '2002',
);
expect(await remote.accountWithGitHub(session, '2002'), annaOnTodo);
test('finds the account a GitHub account signs into, and nobody for '
'one nobody has', () async {
expect(await remote.accountWithGitHub(session, '1001'), ben);
expect(await remote.accountWithGitHub(session, '3003'), isNull);
expect(
(await AuthUser.db.findById(session, annaOnTodo))!.scopeNames,
contains(identified.name),
);
});
test(
"refuses another person's GitHub account, keeping nothing",
() async {
final annaOnTodo = const Uuid().v7obj();
await expectLater(
remote.linkGitHub(
session,
person: annaOnTodo,
gitHubUserId: '1001',
),
refused(GitHubSignInRefusal.takenByAnotherPerson),
);
expect(await AuthUser.db.findById(session, annaOnTodo), isNull);
},
);
});
group('SimpleLogin (ADR 0020)', () {
@ -1109,18 +1085,20 @@ void main() {
'register refuses', () async {
final device = await PasswordDevice.make();
await atNow(() async {
final annaOnTodo = const Uuid().v7obj();
final annaOnTodo = await newPersonWithAddress(
session,
'anna@example.org',
);
final request = await remote.sendSignInCode(
session,
address: 'anna@example.org',
language: 'en',
network: '203.0.113.7',
purpose: EmailCodePurpose.signUp,
purpose: EmailCodePurpose.firstPassword,
);
await remote.setPassword(
session,
person: annaOnTodo,
anonymousHere: true,
setup: device.setup(),
request: request,
code: mailbox.codeFor('anna@example.org'),
@ -1409,19 +1387,21 @@ void main() {
() async {
final device = await PasswordDevice.make();
await atNow(() async {
final annaOnTodo = const Uuid().v7obj();
final annaOnTodo = await newPersonWithAddress(
session,
'anna@example.org',
);
final annasPhone = const Uuid().v7obj();
final request = await remote.sendSignInCode(
session,
address: 'anna@example.org',
language: 'en',
network: '203.0.113.7',
purpose: EmailCodePurpose.signUp,
purpose: EmailCodePurpose.firstPassword,
);
await remote.setPassword(
session,
person: annaOnTodo,
anonymousHere: true,
setup: device.setup(),
request: request,
code: mailbox.codeFor('anna@example.org'),
@ -1691,8 +1671,8 @@ void main() {
expect(members.told, [(_todo, gone)]);
});
test('tells nobody of an anonymous person, a member never having '
'had them', () async {
test('tells nobody of an anonymous account left from before, a member '
'never having had it', () async {
final anna = await newAnonymousPerson(session);
await remote.deleteAccount(session, anna);