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
9 changed files with 837 additions and 1353 deletions
|
|
@ -5,8 +5,8 @@
|
|||
// on top, and syncs and uploads only when a test says so.
|
||||
//
|
||||
// Needs `serverpod start` running; tool/check.sh skips these tests. Run them in
|
||||
// this package with `flutter test -t requires-server`. Each run signs in one
|
||||
// new anonymous person (at most 30 per IP and hour). That nobody else gets a
|
||||
// this package with `flutter test -t requires-server`. Each run makes one new
|
||||
// person on the development database (`signInNewPerson`). That nobody else gets a
|
||||
// person's file is the server's tests' (asset_test.dart).
|
||||
@Tags(['requires-server'])
|
||||
library;
|
||||
|
|
@ -26,7 +26,7 @@ void main() {
|
|||
final devices = <SyncDevice>[];
|
||||
|
||||
setUpAll(() async {
|
||||
alice = await signInAnonymously();
|
||||
alice = await signInNewPerson();
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
// 0001, rule 9), their revisions merged per field (ADR 0003, amended).
|
||||
//
|
||||
// Needs `serverpod start` running; tool/check.sh skips these tests. Run them in
|
||||
// this package with `flutter test -t requires-server`. Each run signs in one
|
||||
// new anonymous person (at most 30 per IP and hour).
|
||||
// this package with `flutter test -t requires-server`. Each run makes one new
|
||||
// person on the development database (`signInNewPerson`).
|
||||
@Tags(['requires-server'])
|
||||
library;
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ void main() {
|
|||
final devices = <SyncDevice>[];
|
||||
|
||||
setUpAll(() async {
|
||||
person = await signInAnonymously();
|
||||
person = await signInNewPerson();
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
// merged per field (ADR 0003), the answers as a log.
|
||||
//
|
||||
// Needs `serverpod start` running; tool/check.sh skips these tests. Run them in
|
||||
// this package with `flutter test -t requires-server`. Each run signs in one
|
||||
// new anonymous person (at most 30 per IP and hour).
|
||||
// this package with `flutter test -t requires-server`. Each run makes one new
|
||||
// person on the development database (`signInNewPerson`).
|
||||
@Tags(['requires-server'])
|
||||
library;
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ void main() {
|
|||
final devices = <SyncDevice>[];
|
||||
|
||||
setUpAll(() async {
|
||||
person = await signInAnonymously();
|
||||
person = await signInNewPerson();
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
// 0001, rule 9), their revisions merged title and body apart (ADR 0003).
|
||||
//
|
||||
// Needs `serverpod start` running; tool/check.sh skips these tests. Run them in
|
||||
// this package with `flutter test -t requires-server`. Each run signs in one
|
||||
// new anonymous person (at most 30 per IP and hour).
|
||||
// this package with `flutter test -t requires-server`. Each run makes one new
|
||||
// person on the development database (`signInNewPerson`).
|
||||
@Tags(['requires-server'])
|
||||
library;
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ void main() {
|
|||
final devices = <SyncDevice>[];
|
||||
|
||||
setUpAll(() async {
|
||||
person = await signInAnonymously();
|
||||
person = await signInNewPerson();
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
// its reading a log.
|
||||
//
|
||||
// Needs `serverpod start` running; tool/check.sh skips these tests. Run them in
|
||||
// this package with `flutter test -t requires-server`. Each run signs in one
|
||||
// new anonymous person (at most 30 per IP and hour).
|
||||
// this package with `flutter test -t requires-server`. Each run makes one new
|
||||
// person on the development database (`signInNewPerson`).
|
||||
@Tags(['requires-server'])
|
||||
library;
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ void main() {
|
|||
final devices = <SyncDevice>[];
|
||||
|
||||
setUpAll(() async {
|
||||
person = await signInAnonymously();
|
||||
person = await signInNewPerson();
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
|
|
|
|||
|
|
@ -8,12 +8,10 @@
|
|||
// repositories on top, and syncs only when a test says so.
|
||||
//
|
||||
// Needs `serverpod start` running; tool/check.sh skips these tests. Run them in
|
||||
// this package with `flutter test -t requires-server`. Each run signs in two
|
||||
// new anonymous people (at most 30 per IP and hour) and gives the author the
|
||||
// `identified` scope by SQL on the development database, on the person and
|
||||
// their sessions as linking GitHub does (`grantIdentified`), since a test
|
||||
// can't sign in to GitHub. Their studyset stays in the development
|
||||
// server's git repositories, withdrawn after its third release. Who may
|
||||
// this package with `flutter test -t requires-server`. Each run makes two new
|
||||
// people on the development database (`signInNewPerson`). Their studyset
|
||||
// stays in the development server's git repositories, withdrawn after its
|
||||
// third release. Who may
|
||||
// publish what, and what a release leaves out, are the server's tests'
|
||||
// (publish_test.dart).
|
||||
@Tags(['requires-server'])
|
||||
|
|
@ -50,22 +48,8 @@ void main() {
|
|||
final devices = <SyncDevice>[];
|
||||
|
||||
setUpAll(() async {
|
||||
author = await signInAnonymously();
|
||||
learner = await signInAnonymously();
|
||||
// As `grantIdentified` does once a GitHub account is linked: the scope
|
||||
// on the person, and on each of their sessions, which keep a copy.
|
||||
for (final table in [
|
||||
'serverpod_auth_core_user',
|
||||
'serverpod_auth_core_session',
|
||||
]) {
|
||||
await queryServer(
|
||||
'UPDATE "$table" '
|
||||
'SET "scopeNames" = ("scopeNames"::jsonb || \'["identified"]\')::json '
|
||||
'WHERE "${table == 'serverpod_auth_core_user' ? 'id' : 'authUserId'}" '
|
||||
'= @id:uuid',
|
||||
{'id': author.authUserId.uuid},
|
||||
);
|
||||
}
|
||||
author = await signInNewPerson();
|
||||
learner = await signInNewPerson();
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
// and repository on top, and syncs only when a test says so.
|
||||
//
|
||||
// Needs `serverpod start` running; tool/check.sh skips these tests. Run them in
|
||||
// this package with `flutter test -t requires-server`. Each run signs in two new
|
||||
// anonymous people (at most 30 per IP and hour).
|
||||
// this package with `flutter test -t requires-server`. Each run makes two new
|
||||
// people on the development database (`signInNewPerson`).
|
||||
@Tags(['requires-server'])
|
||||
library;
|
||||
|
||||
|
|
@ -25,8 +25,8 @@ void main() {
|
|||
final devices = <SyncDevice>[];
|
||||
|
||||
setUpAll(() async {
|
||||
alice = await signInAnonymously();
|
||||
bob = await signInAnonymously();
|
||||
alice = await signInNewPerson();
|
||||
bob = await signInNewPerson();
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_app/testing.dart';
|
||||
|
|
@ -41,14 +46,72 @@ import 'package:yaml/yaml.dart';
|
|||
|
||||
const _serverUrl = 'http://localhost:8080/';
|
||||
|
||||
/// A new anonymous person, as the app signs one in on first launch.
|
||||
Future<AuthSuccess> signInAnonymously() async {
|
||||
final client = protocol.Client(_serverUrl);
|
||||
try {
|
||||
return await client.modules.base.anonymousIdp.login();
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
/// A new person, `identified`, signed in on a device of their own: made by
|
||||
/// SQL on the development database, as every account has a way in since
|
||||
/// ADR 0020, D25, and these tests drive no sign-in. The session is made as
|
||||
/// Serverpod's server-side sessions make one (serverpod_auth_core v4.0.4):
|
||||
/// the token is `sas`, the session's ID and a secret, in URL-safe base64;
|
||||
/// the row keeps the scheme's version (1) before the SHA-512 of the secret,
|
||||
/// a salt and the server's pepper (`serverSideSessionKeyHashPepper` in its
|
||||
/// development passwords), and goes unused for 90 days at most, as the
|
||||
/// server's own do.
|
||||
Future<AuthSuccess> signInNewPerson() async {
|
||||
final random = Random.secure();
|
||||
Uint8List randomBytes(int length) => Uint8List.fromList([
|
||||
for (var i = 0; i < length; i++) random.nextInt(256),
|
||||
]);
|
||||
final passwords = loadYaml(
|
||||
File('${_serverDir()}/config/passwords.yaml').readAsStringSync(),
|
||||
) as YamlMap;
|
||||
final pepper =
|
||||
(passwords['development'] as YamlMap)['serverSideSessionKeyHashPepper']
|
||||
as String;
|
||||
final secret = randomBytes(32);
|
||||
final salt = randomBytes(16);
|
||||
final hash = Uint8List.fromList([
|
||||
1,
|
||||
...sha512.convert([...secret, ...salt, ...utf8.encode(pepper)]).bytes,
|
||||
]);
|
||||
|
||||
// INSERT INTO "serverpod_auth_core_user" … RETURNING "id"
|
||||
final person =
|
||||
(await queryServer(
|
||||
'INSERT INTO "serverpod_auth_core_user" '
|
||||
'("createdAt", "scopeNames", "blocked") '
|
||||
"VALUES (now() AT TIME ZONE 'UTC', '[\"identified\"]', false) "
|
||||
'RETURNING "id"',
|
||||
{},
|
||||
)).single.single!
|
||||
as String;
|
||||
// INSERT INTO "serverpod_auth_core_session" … RETURNING "id"
|
||||
final sessionId =
|
||||
(await queryServer(
|
||||
'INSERT INTO "serverpod_auth_core_session" '
|
||||
'("authUserId", "scopeNames", "createdAt", "lastUsedAt", '
|
||||
'"expireAfterUnusedFor", "sessionKeyHash", "sessionKeySalt", '
|
||||
'"method") '
|
||||
"VALUES (@person:uuid, '[\"identified\"]', "
|
||||
"now() AT TIME ZONE 'UTC', now() AT TIME ZONE 'UTC', "
|
||||
"@unused:int8, @hash:bytea, @salt:bytea, 'sync-test') "
|
||||
'RETURNING "id"',
|
||||
{
|
||||
'person': person,
|
||||
'unused': const Duration(days: 90).inMilliseconds,
|
||||
'hash': hash,
|
||||
'salt': salt,
|
||||
},
|
||||
)).single.single!
|
||||
as String;
|
||||
return AuthSuccess(
|
||||
authStrategy: 'session',
|
||||
token: base64Url.encode([
|
||||
...utf8.encode('sas'),
|
||||
...protocol.UuidValue.withValidation(sessionId).toBytes(),
|
||||
...secret,
|
||||
]),
|
||||
authUserId: protocol.UuidValue.withValidation(person),
|
||||
scopeNames: {'identified'},
|
||||
);
|
||||
}
|
||||
|
||||
/// Runs [sql] on the running development server's database, as its operator
|
||||
|
|
@ -71,11 +134,13 @@ Future<pg.Result> queryServer(
|
|||
}
|
||||
}
|
||||
|
||||
/// The folder of the server `serverpod start` runs, which a worktree's tests
|
||||
/// may take from another checkout: SYNC_TEST_SERVER_DIR names it.
|
||||
String _serverDir() =>
|
||||
Platform.environment['SYNC_TEST_SERVER_DIR'] ?? '../halkyon_learn_server';
|
||||
|
||||
Future<pg.Connection> _openServerDatabase() async {
|
||||
// A worktree's tests run against the server `serverpod start` runs, which
|
||||
// may be another checkout's: SYNC_TEST_SERVER_DIR names its server folder.
|
||||
final server =
|
||||
Platform.environment['SYNC_TEST_SERVER_DIR'] ?? '../halkyon_learn_server';
|
||||
final server = _serverDir();
|
||||
const settings = pg.ConnectionSettings(sslMode: pg.SslMode.disable);
|
||||
final development = loadYaml(
|
||||
File('$server/config/development.yaml').readAsStringSync(),
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
// test says so.
|
||||
//
|
||||
// Needs `serverpod start` running; tool/check.sh skips these tests. Run them in
|
||||
// this package with `flutter test -t requires-server`. Each run signs in one
|
||||
// new anonymous person (at most 30 per IP and hour).
|
||||
// this package with `flutter test -t requires-server`. Each run makes one new
|
||||
// person on the development database (`signInNewPerson`).
|
||||
@Tags(['requires-server'])
|
||||
library;
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ void main() {
|
|||
final devices = <SyncDevice>[];
|
||||
|
||||
setUpAll(() async {
|
||||
alice = await signInAnonymously();
|
||||
alice = await signInNewPerson();
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
|
|
|
|||
Loading…
Reference in a new issue