fix(auth): a member asks the register whose account it is before a deletion's proof counts
The reading of the full reading's fixes found (B1) that a member asked the register knownAccounts inside the owner-proof try, so ten tries to delete while the register was out of reach ended the owner's session, though no proof ever left the member; and that a person anonymous on a member could no longer delete their account while the register was down. The member now asks first, outside the try, which wraps only what checks the proof. While the register is out of reach, a person still anonymous here (becoming identified ends that) is none of its and is deleted here; anyone else waits for the register. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
b0e1557153
commit
9c2b1cd6e0
3 changed files with 81 additions and 16 deletions
|
|
@ -74,8 +74,6 @@ class EndpointAccount extends _isc.EndpointRef {
|
||||||
{'proof': proof},
|
{'proof': proof},
|
||||||
);
|
);
|
||||||
|
|
||||||
/// How the caller signs in besides their sessions: with GitHub, with an
|
|
||||||
/// email address, both, or neither (anonymous).
|
|
||||||
_ida.Future<_ifld8s4r.SignInWays> signInWays() =>
|
_ida.Future<_ifld8s4r.SignInWays> signInWays() =>
|
||||||
caller.callServerEndpoint<_ifld8s4r.SignInWays>(
|
caller.callServerEndpoint<_ifld8s4r.SignInWays>(
|
||||||
'halkyon_base.account',
|
'halkyon_base.account',
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:halkyon_base_server/src/password/key_proof.dart';
|
||||||
import 'package:halkyon_base_server/src/password/owner_proof_tries.dart';
|
import 'package:halkyon_base_server/src/password/owner_proof_tries.dart';
|
||||||
import 'package:halkyon_base_server/src/password/password_change.dart';
|
import 'package:halkyon_base_server/src/password/password_change.dart';
|
||||||
import 'package:halkyon_base_server/src/register/account_register.dart';
|
import 'package:halkyon_base_server/src/register/account_register.dart';
|
||||||
|
import 'package:halkyon_base_server/src/register/register_protocol.dart';
|
||||||
import 'package:serverpod/serverpod.dart';
|
import 'package:serverpod/serverpod.dart';
|
||||||
import 'package:serverpod_auth_idp_server/core.dart';
|
import 'package:serverpod_auth_idp_server/core.dart';
|
||||||
|
|
||||||
|
|
@ -29,21 +30,23 @@ class AccountEndpoint extends Endpoint {
|
||||||
instanceUrl: deviceInstanceUrlOn(pod, proof.instanceUrl),
|
instanceUrl: deviceInstanceUrlOn(pod, proof.instanceUrl),
|
||||||
);
|
);
|
||||||
// The proof counts for this session (`asOwnerProofTry`), as every
|
// The proof counts for this session (`asOwnerProofTry`), as every
|
||||||
// change of how the account is secured does.
|
// change of how the account is secured does: only around what checks
|
||||||
await asOwnerProofTry(session, bound, () async {
|
// it, so asking the register whose account it is spends no try (the
|
||||||
if (!isMember(pod)) {
|
// reading of the fixes' B1).
|
||||||
await requireOwnerToDelete(session, person, bound);
|
if (!isMember(pod)) {
|
||||||
return;
|
await asOwnerProofTry(
|
||||||
}
|
session,
|
||||||
// An account of the register's whatever this session's scopes say
|
bound,
|
||||||
// (the full reading's L7: a person the register linked whose
|
() => requireOwnerToDelete(session, person, bound),
|
||||||
// becoming identified here failed was deleted here on the session
|
);
|
||||||
// alone): the register decides, with the proof it takes.
|
} else if (await _registersAccount(session, person)) {
|
||||||
final register = AccountRegister.of(pod);
|
final register = AccountRegister.of(pod);
|
||||||
if ((await register.knownAccounts(session, {person})).contains(person)) {
|
await asOwnerProofTry(
|
||||||
await register.deleteAccount(session, person, proof: bound);
|
session,
|
||||||
}
|
bound,
|
||||||
});
|
() => register.deleteAccount(session, person, proof: bound),
|
||||||
|
);
|
||||||
|
}
|
||||||
await deletePerson(session, person);
|
await deletePerson(session, person);
|
||||||
await session.messages.authenticationRevoked(
|
await session.messages.authenticationRevoked(
|
||||||
person.uuid,
|
person.uuid,
|
||||||
|
|
@ -53,6 +56,35 @@ class AccountEndpoint extends Endpoint {
|
||||||
|
|
||||||
/// How the caller signs in besides their sessions: with GitHub, with an
|
/// How the caller signs in besides their sessions: with GitHub, with an
|
||||||
/// email address, both, or neither (anonymous).
|
/// email address, both, or neither (anonymous).
|
||||||
|
/// Whether [person], on a member, is an account of the register's,
|
||||||
|
/// whatever this session's scopes say (the full reading's L7: a person
|
||||||
|
/// the register linked whose becoming identified here failed was deleted
|
||||||
|
/// here on the session alone): the register decides, with the proof it
|
||||||
|
/// takes. While the register is out of reach, a person still anonymous
|
||||||
|
/// here (becoming identified ends that) is none of its, and is deleted
|
||||||
|
/// here; anyone else waits for it ([RegisterUnavailableException]).
|
||||||
|
static Future<bool> _registersAccount(
|
||||||
|
Session session,
|
||||||
|
UuidValue person,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
return (await AccountRegister.of(
|
||||||
|
session.serverpod,
|
||||||
|
).knownAccounts(session, {person})).contains(person);
|
||||||
|
} on RegisterUnavailableException {
|
||||||
|
// SELECT count(*) FROM "serverpod_auth_idp_anonymous_account"
|
||||||
|
// WHERE "authUserId" = $1
|
||||||
|
if (await AnonymousAccount.db.count(
|
||||||
|
session,
|
||||||
|
where: (t) => t.authUserId.equals(person),
|
||||||
|
) >
|
||||||
|
0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<SignInWays> signInWays(Session session) => AccountRegister.of(
|
Future<SignInWays> signInWays(Session session) => AccountRegister.of(
|
||||||
session.serverpod,
|
session.serverpod,
|
||||||
).signInWaysOf(session, session.authenticated!.authUserId);
|
).signInWaysOf(session, session.authenticated!.authUserId);
|
||||||
|
|
|
||||||
|
|
@ -1060,6 +1060,41 @@ void main() {
|
||||||
);
|
);
|
||||||
expect(hostDeleted, isEmpty);
|
expect(hostDeleted, isEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('asks the register first: tries while it is out of reach count '
|
||||||
|
"for nothing, as no proof left here (the reading of the fixes' "
|
||||||
|
'B1)', () async {
|
||||||
|
register.failing = RegisterUnavailableException('test');
|
||||||
|
DeletionProof proof() => DeletionProof(
|
||||||
|
challenge: const Uuid().v7obj(),
|
||||||
|
clientValue: ByteData(384),
|
||||||
|
clientProof: ByteData(32),
|
||||||
|
totpCode: '123456',
|
||||||
|
instanceUrl: testInstanceUrl,
|
||||||
|
);
|
||||||
|
for (var i = 0; i < 11; i++) {
|
||||||
|
await expectLater(
|
||||||
|
endpoints.account.delete(asBen(), proof: proof()),
|
||||||
|
throwsA(isA<RegisterUnavailableException>()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
expect(register.asked.toSet(), {'knownAccounts'});
|
||||||
|
expect(
|
||||||
|
await ServerSideSession.db.findById(session, bensLaptop),
|
||||||
|
isNotNull,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('of a person anonymous here deletes it here while the register '
|
||||||
|
'is out of reach: becoming identified ends being anonymous, so '
|
||||||
|
"none is the register's", () async {
|
||||||
|
register.failing = RegisterUnavailableException('test');
|
||||||
|
|
||||||
|
await endpoints.account.delete(asAnna());
|
||||||
|
|
||||||
|
expect(await AuthUser.db.findById(session, anna), isNull);
|
||||||
|
expect(hostDeleted, [anna]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('told by the register that an account is gone', () {
|
group('told by the register that an account is gone', () {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue