fix(auth): GitHub-only accounts go at GitHub's end; only the anonymous add an address
The full reading's M1 and L6: once GitHub IDs were forgotten at GITHUB_SIGN_IN_ENDS, an account that had signed in with GitHub alone counted as anonymous, so a stolen session and any mailbox gave it a first password and authenticator; and such accounts, with no way in left, were never pruned. Now: - at the end, the daily run that forgets the GitHub IDs first deletes the accounts that sign in with GitHub alone (no address, so never a password), with everything they have, members told (deletePerson); the app tells them for the three months before that their account goes then unless they set up a password (passwordSectionGitHubEndsDeletes, en/de); - password.set adds an address without GitHub only for someone anonymous where the session lives (anonymousHere, which a member tells the register, as it tells freshGitHubSignIn); anyone else is refused before any code is used. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
9d0d037c86
commit
5eb74639b6
19 changed files with 202 additions and 13 deletions
|
|
@ -113,6 +113,12 @@ void main() {
|
||||||
await openProfile(tester);
|
await openProfile(tester);
|
||||||
|
|
||||||
expect(find.textContaining('Sign-in with GitHub ends on'), findsOneWidget);
|
expect(find.textContaining('Sign-in with GitHub ends on'), findsOneWidget);
|
||||||
|
// Without an address, the account goes at the end (the full reading's
|
||||||
|
// M1).
|
||||||
|
expect(
|
||||||
|
find.textContaining('will be deleted then, unless you set up a password'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
expect(find.text('Set up a password'), findsOneWidget);
|
expect(find.text('Set up a password'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -231,5 +231,6 @@
|
||||||
"passwordResetsAccount": "Am {date} bekommt dein Konto ein neues Passwort und neue Schlüssel, und was mit den alten verschlüsselt ist, geht verloren: Jemand hat es mit einem Code per Mail angefordert. Warst du das nicht, sag es ab.",
|
"passwordResetsAccount": "Am {date} bekommt dein Konto ein neues Passwort und neue Schlüssel, und was mit den alten verschlüsselt ist, geht verloren: Jemand hat es mit einem Code per Mail angefordert. Warst du das nicht, sag es ab.",
|
||||||
"passwordResetsCallOff": "Absagen",
|
"passwordResetsCallOff": "Absagen",
|
||||||
"passwordCallOffTitle": "Zurücksetzen absagen",
|
"passwordCallOffTitle": "Zurücksetzen absagen",
|
||||||
"passwordCallOffIntro": "Dein Passwort und ein Code sagen jedes anstehende Zurücksetzen ab."
|
"passwordCallOffIntro": "Dein Passwort und ein Code sagen jedes anstehende Zurücksetzen ab.",
|
||||||
|
"passwordSectionGitHubEndsDeletes": "Die Anmeldung mit GitHub endet am {date}. Dann wird dein Konto mit allem darin gelöscht, es sei denn, du richtest vorher ein Passwort ein."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -499,5 +499,13 @@
|
||||||
},
|
},
|
||||||
"passwordResetsCallOff": "Call it off",
|
"passwordResetsCallOff": "Call it off",
|
||||||
"passwordCallOffTitle": "Call off the reset",
|
"passwordCallOffTitle": "Call off the reset",
|
||||||
"passwordCallOffIntro": "Your password and a code call off every reset waiting."
|
"passwordCallOffIntro": "Your password and a code call off every reset waiting.",
|
||||||
|
"passwordSectionGitHubEndsDeletes": "Sign-in with GitHub ends on {date}. Your account, with everything in it, will be deleted then, unless you set up a password before.",
|
||||||
|
"@passwordSectionGitHubEndsDeletes": {
|
||||||
|
"placeholders": {
|
||||||
|
"date": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1494,6 +1494,12 @@ abstract class HalkyonAppLocalizations {
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Your password and a code call off every reset waiting.'**
|
/// **'Your password and a code call off every reset waiting.'**
|
||||||
String get passwordCallOffIntro;
|
String get passwordCallOffIntro;
|
||||||
|
|
||||||
|
/// No description provided for @passwordSectionGitHubEndsDeletes.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Sign-in with GitHub ends on {date}. Your account, with everything in it, will be deleted then, unless you set up a password before.'**
|
||||||
|
String passwordSectionGitHubEndsDeletes(String date);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HalkyonAppLocalizationsDelegate
|
class _HalkyonAppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -869,4 +869,9 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get passwordCallOffIntro =>
|
String get passwordCallOffIntro =>
|
||||||
'Dein Passwort und ein Code sagen jedes anstehende Zurücksetzen ab.';
|
'Dein Passwort und ein Code sagen jedes anstehende Zurücksetzen ab.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String passwordSectionGitHubEndsDeletes(String date) {
|
||||||
|
return 'Die Anmeldung mit GitHub endet am $date. Dann wird dein Konto mit allem darin gelöscht, es sei denn, du richtest vorher ein Passwort ein.';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -852,6 +852,11 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get passwordCallOffIntro =>
|
String get passwordCallOffIntro =>
|
||||||
'Your password and a code call off every reset waiting.';
|
'Your password and a code call off every reset waiting.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String passwordSectionGitHubEndsDeletes(String date) {
|
||||||
|
return 'Sign-in with GitHub ends on $date. Your account, with everything in it, will be deleted then, unless you set up a password before.';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
||||||
|
|
|
||||||
|
|
@ -166,9 +166,12 @@ class _PasswordOfferState() extends State<_PasswordOffer> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(switch (security.gitHubEnds) {
|
Text(switch (security.gitHubEnds) {
|
||||||
final ends? => l10n.passwordSectionGitHubEnds(
|
final ends? =>
|
||||||
passwordMoment(context, ends),
|
(security.hasAddress
|
||||||
),
|
? l10n.passwordSectionGitHubEnds
|
||||||
|
: l10n.passwordSectionGitHubEndsDeletes)(
|
||||||
|
passwordMoment(context, ends),
|
||||||
|
),
|
||||||
null => l10n.passwordOfferBody,
|
null => l10n.passwordOfferBody,
|
||||||
}),
|
}),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,11 @@ class const _AccountSecurity() extends StatelessWidget {
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8),
|
padding: const EdgeInsets.only(top: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
l10n.passwordSectionGitHubEnds(
|
// Without an address, nothing is left to sign in with: the
|
||||||
|
// account goes at the end (the full reading's M1).
|
||||||
|
(security.hasAddress
|
||||||
|
? l10n.passwordSectionGitHubEnds
|
||||||
|
: l10n.passwordSectionGitHubEndsDeletes)(
|
||||||
passwordMoment(context, security.gitHubEnds!),
|
passwordMoment(context, security.gitHubEnds!),
|
||||||
),
|
),
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:clock/clock.dart';
|
import 'package:clock/clock.dart';
|
||||||
|
import 'package:halkyon_base_server/src/auth/account.dart';
|
||||||
import 'package:halkyon_base_server/src/auth/identified.dart';
|
import 'package:halkyon_base_server/src/auth/identified.dart';
|
||||||
import 'package:halkyon_base_server/src/generated/protocol.dart';
|
import 'package:halkyon_base_server/src/generated/protocol.dart';
|
||||||
import 'package:halkyon_base_server/src/password/password_config.dart';
|
import 'package:halkyon_base_server/src/password/password_config.dart';
|
||||||
|
|
@ -72,6 +73,13 @@ final _endsOf = Expando<DateTime>('GitHub sign-in ends');
|
||||||
/// at [now] ([gitHubSignInEnds]), and returns how many: nobody signs in with
|
/// at [now] ([gitHubSignInEnds]), and returns how many: nobody signs in with
|
||||||
/// them any more, so nothing is kept of them. Daily, with the pruning; on the
|
/// them any more, so nothing is kept of them. Daily, with the pruning; on the
|
||||||
/// register, which keeps them (ADR 0016).
|
/// register, which keeps them (ADR 0016).
|
||||||
|
///
|
||||||
|
/// First the accounts that sign in with GitHub alone (no address, so never
|
||||||
|
/// a password) go, with everything they have, every member told
|
||||||
|
/// (`deletePerson`): no way in is left to them, and an account nobody can
|
||||||
|
/// enter would stay for good, or be claimed by whoever holds a session of
|
||||||
|
/// it (the full reading's M1 and L6). The app tells them for the three
|
||||||
|
/// months before (ADR 0017, D2).
|
||||||
Future<int> forgetGitHubAccounts(
|
Future<int> forgetGitHubAccounts(
|
||||||
Session session, {
|
Session session, {
|
||||||
required DateTime now,
|
required DateTime now,
|
||||||
|
|
@ -80,12 +88,36 @@ Future<int> forgetGitHubAccounts(
|
||||||
if (ends == null || now.isBefore(ends)) {
|
if (ends == null || now.isBefore(ends)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
// SELECT * FROM "serverpod_auth_idp_github_account"
|
||||||
|
final gitHubAccounts = await GitHubAccount.db.find(session);
|
||||||
|
final gitHubPeople = {
|
||||||
|
for (final account in gitHubAccounts) account.authUserId,
|
||||||
|
};
|
||||||
|
if (gitHubPeople.isNotEmpty) {
|
||||||
|
// SELECT * FROM "email_identity" WHERE "authUserId" = ANY($1)
|
||||||
|
final withAddress = {
|
||||||
|
for (final identity in await EmailIdentity.db.find(
|
||||||
|
session,
|
||||||
|
where: (t) => t.authUserId.inSet(gitHubPeople),
|
||||||
|
))
|
||||||
|
identity.authUserId,
|
||||||
|
};
|
||||||
|
final gitHubOnly = gitHubPeople.difference(withAddress);
|
||||||
|
for (final person in gitHubOnly) {
|
||||||
|
await deletePerson(session, person);
|
||||||
|
}
|
||||||
|
session.log(
|
||||||
|
'Deleted ${gitHubOnly.length} accounts that signed in with GitHub '
|
||||||
|
'alone, at its end.',
|
||||||
|
);
|
||||||
|
}
|
||||||
// DELETE FROM "serverpod_auth_idp_github_account"
|
// DELETE FROM "serverpod_auth_idp_github_account"
|
||||||
final forgotten = await GitHubAccount.db.deleteWhere(
|
await GitHubAccount.db.deleteWhere(
|
||||||
session,
|
session,
|
||||||
where: (t) => t.id.notEquals(null),
|
where: (t) => t.id.notEquals(null),
|
||||||
);
|
);
|
||||||
return forgotten.length;
|
// Those deleted with their accounts counted too.
|
||||||
|
return gitHubAccounts.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The numeric GitHub ID that [code], from the app's GitHub sign-in, proves.
|
/// The numeric GitHub ID that [code], from the app's GitHub sign-in, proves.
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,14 @@ class PasswordEndpoint extends Endpoint {
|
||||||
request: request,
|
request: request,
|
||||||
code: code,
|
code: code,
|
||||||
freshGitHubSignIn: await _signedInWithGitHubJustNow(session),
|
freshGitHubSignIn: await _signedInWithGitHubJustNow(session),
|
||||||
|
// SELECT count(*) FROM "serverpod_auth_idp_anonymous_account"
|
||||||
|
// WHERE "authUserId" = $1
|
||||||
|
anonymousHere:
|
||||||
|
await AnonymousAccount.db.count(
|
||||||
|
session,
|
||||||
|
where: (t) => t.authUserId.equals(person),
|
||||||
|
) >
|
||||||
|
0,
|
||||||
);
|
);
|
||||||
if (isMember(session.serverpod)) {
|
if (isMember(session.serverpod)) {
|
||||||
await becomeIdentified(session, person);
|
await becomeIdentified(session, person);
|
||||||
|
|
|
||||||
|
|
@ -349,7 +349,11 @@ const gitHubSignInFreshFor = Duration(minutes: 10);
|
||||||
/// by a GitHub sign-in of the last [gitHubSignInFreshFor]
|
/// by a GitHub sign-in of the last [gitHubSignInFreshFor]
|
||||||
/// ([freshGitHubSignIn], which the server holding the session tells), with
|
/// ([freshGitHubSignIn], which the server holding the session tells), with
|
||||||
/// the address it adds proved as an anonymous person proves theirs
|
/// the address it adds proved as an anonymous person proves theirs
|
||||||
/// ([EmailCodePurpose.signUp]).
|
/// ([EmailCodePurpose.signUp]). Only someone anonymous where the session
|
||||||
|
/// lives ([anonymousHere], which that server tells) adds an address so
|
||||||
|
/// without either: anyone else has no way in left to prove (a GitHub ID
|
||||||
|
/// forgotten at its end, the full reading's M1), and a session of theirs
|
||||||
|
/// alone would make the account a thief's.
|
||||||
///
|
///
|
||||||
/// Refused ([PasswordRefusedException]) where the instance has no password
|
/// Refused ([PasswordRefusedException]) where the instance has no password
|
||||||
/// sign-in, for a setup out of range, without an address, for an address
|
/// sign-in, for a setup out of range, without an address, for an address
|
||||||
|
|
@ -357,7 +361,9 @@ const gitHubSignInFreshFor = Duration(minutes: 10);
|
||||||
/// confirmed authenticator ([PasswordRefusal.passwordExists]), without the
|
/// confirmed authenticator ([PasswordRefusal.passwordExists]), without the
|
||||||
/// code its own address takes ([PasswordRefusal.wrongCredentials]), and
|
/// code its own address takes ([PasswordRefusal.wrongCredentials]), and
|
||||||
/// for GitHub without a recent sign-in ([PasswordRefusal.signInAgain],
|
/// for GitHub without a recent sign-in ([PasswordRefusal.signInAgain],
|
||||||
/// before any code is used); a code's refusals as `proveAddressCode`'s, and
|
/// before any code is used), and for someone neither anonymous nor with a
|
||||||
|
/// way in ([PasswordRefusal.wrongCredentials], before any code is used); a
|
||||||
|
/// code's refusals as `proveAddressCode`'s, and
|
||||||
/// another address than the account's as email sign-in refuses it
|
/// another address than the account's as email sign-in refuses it
|
||||||
/// ([EmailSignInRefusedException]). Nothing changes then.
|
/// ([EmailSignInRefusedException]). Nothing changes then.
|
||||||
Future<void> setPassword(
|
Future<void> setPassword(
|
||||||
|
|
@ -367,6 +373,7 @@ Future<void> setPassword(
|
||||||
UuidValue? request,
|
UuidValue? request,
|
||||||
String? code,
|
String? code,
|
||||||
bool freshGitHubSignIn = false,
|
bool freshGitHubSignIn = false,
|
||||||
|
bool anonymousHere = false,
|
||||||
}) async {
|
}) async {
|
||||||
requirePasswordKeys(session.serverpod);
|
requirePasswordKeys(session.serverpod);
|
||||||
if (!isPasswordSetup(setup)) {
|
if (!isPasswordSetup(setup)) {
|
||||||
|
|
@ -405,6 +412,9 @@ Future<void> setPassword(
|
||||||
if (gitHub && !freshGitHubSignIn) {
|
if (gitHub && !freshGitHubSignIn) {
|
||||||
throw refusedFor(PasswordRefusal.signInAgain);
|
throw refusedFor(PasswordRefusal.signInAgain);
|
||||||
}
|
}
|
||||||
|
if (!gitHub && !anonymousHere) {
|
||||||
|
throw refusedFor(PasswordRefusal.wrongCredentials);
|
||||||
|
}
|
||||||
proven = request != null && code != null
|
proven = request != null && code != null
|
||||||
? await proveAddressCode(
|
? await proveAddressCode(
|
||||||
session,
|
session,
|
||||||
|
|
|
||||||
|
|
@ -152,8 +152,9 @@ abstract interface class AccountRegister {
|
||||||
/// Gives [person] the password [setup] describes, with the address [code]
|
/// Gives [person] the password [setup] describes, with the address [code]
|
||||||
/// proves for [request] (their own where they have one), and for one who
|
/// proves for [request] (their own where they have one), and for one who
|
||||||
/// signs in with GitHub alone a GitHub sign-in of theirs made recently
|
/// signs in with GitHub alone a GitHub sign-in of theirs made recently
|
||||||
/// ([freshGitHubSignIn], of the session asking). Refusals as
|
/// ([freshGitHubSignIn], of the session asking); someone with neither
|
||||||
/// `setPassword`'s.
|
/// only while anonymous where the session lives ([anonymousHere]).
|
||||||
|
/// Refusals as `setPassword`'s.
|
||||||
Future<void> setPassword(
|
Future<void> setPassword(
|
||||||
Session session, {
|
Session session, {
|
||||||
required UuidValue person,
|
required UuidValue person,
|
||||||
|
|
@ -161,6 +162,7 @@ abstract interface class AccountRegister {
|
||||||
UuidValue? request,
|
UuidValue? request,
|
||||||
String? code,
|
String? code,
|
||||||
bool freshGitHubSignIn = false,
|
bool freshGitHubSignIn = false,
|
||||||
|
bool anonymousHere = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// A password sign-in's first step for [address], the sign-ins begun from
|
/// A password sign-in's first step for [address], the sign-ins begun from
|
||||||
|
|
|
||||||
|
|
@ -233,8 +233,11 @@ Future<Map<String, Object?>?> _answer(
|
||||||
setup: arguments.model('setup', PasswordSetup.fromJson),
|
setup: arguments.model('setup', PasswordSetup.fromJson),
|
||||||
request: arguments.optionalId('request'),
|
request: arguments.optionalId('request'),
|
||||||
code: arguments.optionalString('code'),
|
code: arguments.optionalString('code'),
|
||||||
// The member tells it of its own session (the fourth reading).
|
// The member tells it of its own session (the fourth reading) and
|
||||||
|
// person (the full reading's M1): the register keeps no anonymous
|
||||||
|
// person of a member's.
|
||||||
freshGitHubSignIn: arguments.flag('freshGitHubSignIn'),
|
freshGitHubSignIn: arguments.flag('freshGitHubSignIn'),
|
||||||
|
anonymousHere: arguments.flag('anonymousHere'),
|
||||||
);
|
);
|
||||||
return const {};
|
return const {};
|
||||||
case RegisterOperation.beginPasswordSignIn:
|
case RegisterOperation.beginPasswordSignIn:
|
||||||
|
|
|
||||||
|
|
@ -423,6 +423,7 @@ final class LocalRegister implements AccountRegister {
|
||||||
UuidValue? request,
|
UuidValue? request,
|
||||||
String? code,
|
String? code,
|
||||||
bool freshGitHubSignIn = false,
|
bool freshGitHubSignIn = false,
|
||||||
|
bool anonymousHere = false,
|
||||||
}) => passwords.setPassword(
|
}) => passwords.setPassword(
|
||||||
session,
|
session,
|
||||||
person: person,
|
person: person,
|
||||||
|
|
@ -430,6 +431,7 @@ final class LocalRegister implements AccountRegister {
|
||||||
request: request,
|
request: request,
|
||||||
code: code,
|
code: code,
|
||||||
freshGitHubSignIn: freshGitHubSignIn,
|
freshGitHubSignIn: freshGitHubSignIn,
|
||||||
|
anonymousHere: anonymousHere,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -306,12 +306,14 @@ final class RemoteRegister implements AccountRegister {
|
||||||
UuidValue? request,
|
UuidValue? request,
|
||||||
String? code,
|
String? code,
|
||||||
bool freshGitHubSignIn = false,
|
bool freshGitHubSignIn = false,
|
||||||
|
bool anonymousHere = false,
|
||||||
}) => _ask(RegisterOperation.setPassword, {
|
}) => _ask(RegisterOperation.setPassword, {
|
||||||
'person': person.uuid,
|
'person': person.uuid,
|
||||||
'setup': setup.toJson(),
|
'setup': setup.toJson(),
|
||||||
'request': request?.uuid,
|
'request': request?.uuid,
|
||||||
'code': code,
|
'code': code,
|
||||||
'freshGitHubSignIn': freshGitHubSignIn,
|
'freshGitHubSignIn': freshGitHubSignIn,
|
||||||
|
'anonymousHere': anonymousHere,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -234,6 +234,35 @@ void main() {
|
||||||
});
|
});
|
||||||
expect(await forgetGitHubAccounts(session, now: ends), 1);
|
expect(await forgetGitHubAccounts(session, now: ends), 1);
|
||||||
expect(await GitHubAccount.db.count(session), 0);
|
expect(await GitHubAccount.db.count(session), 0);
|
||||||
|
// Ben signed in with GitHub alone and never set a password up: his
|
||||||
|
// account goes with it (the full reading's M1), no way in left.
|
||||||
|
expect(await AuthUser.db.findById(session, ben), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('at its end keeps the account of someone with an address too, '
|
||||||
|
'forgetting only the GitHub ID', () async {
|
||||||
|
final carl = (await AuthServices.instance.authUsers.create(
|
||||||
|
session,
|
||||||
|
scopes: {identified},
|
||||||
|
)).id;
|
||||||
|
await GitHubAccount.db.insertRow(
|
||||||
|
session,
|
||||||
|
GitHubAccount(userIdentifier: '3003', authUserId: carl),
|
||||||
|
);
|
||||||
|
await EmailIdentity.db.insertRow(
|
||||||
|
session,
|
||||||
|
EmailIdentity(
|
||||||
|
authUserId: carl,
|
||||||
|
addressHash: addressHash(session.serverpod, 'carl@example.org'),
|
||||||
|
createdAt: DateTime.utc(2026, 10),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final ends = DateTime.utc(2027, 1, 22);
|
||||||
|
useGitHubSignInEnds(session.serverpod, ends);
|
||||||
|
expect(await forgetGitHubAccounts(session, now: ends), 2);
|
||||||
|
expect(await AuthUser.db.findById(session, carl), isNotNull);
|
||||||
|
expect(await AuthUser.db.findById(session, ben), isNull);
|
||||||
|
expect(await GitHubAccount.db.count(session), 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -380,6 +380,43 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('is refused to someone neither anonymous nor with a way in (a '
|
||||||
|
'GitHub ID forgotten): a session alone and a code to any address '
|
||||||
|
'set nothing', () async {
|
||||||
|
// Anna, identified, her GitHub ID forgotten (the full reading's
|
||||||
|
// M1): a session of hers, stolen, outlived it.
|
||||||
|
await grantIdentified(session, anna);
|
||||||
|
await AnonymousAccount.db.deleteWhere(
|
||||||
|
session,
|
||||||
|
where: (t) => t.authUserId.equals(anna),
|
||||||
|
);
|
||||||
|
final thief = await PasswordDevice.make(seed: 99);
|
||||||
|
final code = await mailedCode('thief@example.org');
|
||||||
|
await expectLater(
|
||||||
|
endpoints.password.set(
|
||||||
|
asAnna(),
|
||||||
|
setup: thief.setup(),
|
||||||
|
request: code.request,
|
||||||
|
code: code.code,
|
||||||
|
),
|
||||||
|
refused(PasswordRefusal.wrongCredentials),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
await EmailIdentity.db.count(
|
||||||
|
session,
|
||||||
|
where: (t) => t.authUserId.equals(anna),
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
await AccountPassword.db.count(
|
||||||
|
session,
|
||||||
|
where: (t) => t.authUserId.equals(anna),
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('is refused without an address, keeping nothing', () async {
|
test('is refused without an address, keeping nothing', () async {
|
||||||
await expectLater(
|
await expectLater(
|
||||||
endpoints.password.set(asAnna(), setup: device.setup()),
|
endpoints.password.set(asAnna(), setup: device.setup()),
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,9 @@ final class _Register implements AccountRegister {
|
||||||
/// member's made just now.
|
/// member's made just now.
|
||||||
final freshGitHubSignIns = <UuidValue>{};
|
final freshGitHubSignIns = <UuidValue>{};
|
||||||
|
|
||||||
|
/// Those [setPassword] was told are anonymous on the member.
|
||||||
|
final anonymousHere_ = <UuidValue>{};
|
||||||
|
|
||||||
/// What a password sign-in proves; null refuses it.
|
/// What a password sign-in proves; null refuses it.
|
||||||
PasswordProof? proof;
|
PasswordProof? proof;
|
||||||
|
|
||||||
|
|
@ -240,12 +243,16 @@ final class _Register implements AccountRegister {
|
||||||
UuidValue? request,
|
UuidValue? request,
|
||||||
String? code,
|
String? code,
|
||||||
bool freshGitHubSignIn = false,
|
bool freshGitHubSignIn = false,
|
||||||
|
bool anonymousHere = false,
|
||||||
}) => _answer('setPassword', () {
|
}) => _answer('setPassword', () {
|
||||||
add(person);
|
add(person);
|
||||||
passwords.add(person);
|
passwords.add(person);
|
||||||
if (freshGitHubSignIn) {
|
if (freshGitHubSignIn) {
|
||||||
freshGitHubSignIns.add(person);
|
freshGitHubSignIns.add(person);
|
||||||
}
|
}
|
||||||
|
if (anonymousHere) {
|
||||||
|
anonymousHere_.add(person);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -850,6 +857,9 @@ void main() {
|
||||||
|
|
||||||
expect(register.passwords, {anna});
|
expect(register.passwords, {anna});
|
||||||
expect(register.freshGitHubSignIns, isEmpty);
|
expect(register.freshGitHubSignIns, isEmpty);
|
||||||
|
// Anna was anonymous here, which the register can't know (the
|
||||||
|
// full reading's M1).
|
||||||
|
expect(register.anonymousHere_, {anna});
|
||||||
expect(await scopesOf(anna), contains(identified.name));
|
expect(await scopesOf(anna), contains(identified.name));
|
||||||
expect(
|
expect(
|
||||||
await AnonymousAccount.db.count(
|
await AnonymousAccount.db.count(
|
||||||
|
|
|
||||||
|
|
@ -384,6 +384,7 @@ void main() {
|
||||||
remote.setPassword(
|
remote.setPassword(
|
||||||
session,
|
session,
|
||||||
person: annaOnTodo,
|
person: annaOnTodo,
|
||||||
|
anonymousHere: true,
|
||||||
setup: device.setup(),
|
setup: device.setup(),
|
||||||
request: request,
|
request: request,
|
||||||
code: 'AAAA-AAAA',
|
code: 'AAAA-AAAA',
|
||||||
|
|
@ -391,9 +392,22 @@ void main() {
|
||||||
refused(EmailSignInRefusal.wrongCode),
|
refused(EmailSignInRefusal.wrongCode),
|
||||||
);
|
);
|
||||||
expect(await AuthUser.db.findById(session, annaOnTodo), isNull);
|
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).
|
||||||
|
await expectLater(
|
||||||
|
remote.setPassword(
|
||||||
|
session,
|
||||||
|
person: annaOnTodo,
|
||||||
|
setup: device.setup(),
|
||||||
|
request: request,
|
||||||
|
code: mailbox.codeFor('anna@example.org'),
|
||||||
|
),
|
||||||
|
refused(PasswordRefusal.wrongCredentials),
|
||||||
|
);
|
||||||
await remote.setPassword(
|
await remote.setPassword(
|
||||||
session,
|
session,
|
||||||
person: annaOnTodo,
|
person: annaOnTodo,
|
||||||
|
anonymousHere: true,
|
||||||
setup: device.setup(),
|
setup: device.setup(),
|
||||||
request: request,
|
request: request,
|
||||||
code: mailbox.codeFor('anna@example.org'),
|
code: mailbox.codeFor('anna@example.org'),
|
||||||
|
|
@ -486,6 +500,7 @@ void main() {
|
||||||
await remote.setPassword(
|
await remote.setPassword(
|
||||||
session,
|
session,
|
||||||
person: annaOnTodo,
|
person: annaOnTodo,
|
||||||
|
anonymousHere: true,
|
||||||
setup: device.setup(),
|
setup: device.setup(),
|
||||||
request: request,
|
request: request,
|
||||||
code: mailbox.codeFor('anna@example.org'),
|
code: mailbox.codeFor('anna@example.org'),
|
||||||
|
|
@ -755,6 +770,7 @@ void main() {
|
||||||
await remote.setPassword(
|
await remote.setPassword(
|
||||||
session,
|
session,
|
||||||
person: annaOnTodo,
|
person: annaOnTodo,
|
||||||
|
anonymousHere: true,
|
||||||
setup: device.setup(),
|
setup: device.setup(),
|
||||||
request: request,
|
request: request,
|
||||||
code: mailbox.codeFor('anna@example.org'),
|
code: mailbox.codeFor('anna@example.org'),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue