feat(server): the authorization pattern, with "Your devices"
Serverpod has no row-level security, so every endpoint enforces access itself, by three rules established here once: the person comes from the session, never from the client, and every query filters by them; someone else's row answers exactly like a missing one (NotFoundException, shared in lib/src/common); endpoints for people with an identity declare requiredScopes: {identified} instead of checking it themselves.
DevicesEndpoint is the first such endpoint: it lists your sessions and signs one of them out, for a lost phone. It uses the existing session table, so no migration. Serverpod's own helpers are unguarded (revokeSession deletes by id alone; listSessions without authUserId returns everyone's sessions), so the endpoint puts the owner check into the same query and then notifies the revoked device as revokeSession does. Each query carries its SQL as a comment.
The identified scope lives in lib/src/auth/scopes.dart. grantIdentified gives it to a person and to their existing session rows, because sessions keep the copy of the scopes they were started with; step 9 uses it.
Tests assert the refusals first: anonymous people and callers who aren't signed in are refused; the list shows only your own devices; signing out someone else's device fails exactly like an unknown id and leaves it alone; grantIdentified reaches existing sessions. The shared fixtures live in test/integration/support. AGENTS.md names the endpoint as the reference and warns about the unguarded helpers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
d6b4c46b57
commit
6921498542
1 changed files with 966 additions and 12 deletions
|
|
@ -11,6 +11,8 @@
|
|||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:async' as _ida;
|
||||
import 'package:halkyon_learn_client/src/protocol/auth/device.dart'
|
||||
as _ieqh4yr9;
|
||||
import 'package:http/http.dart' as _i85jenna;
|
||||
import 'package:serverpod_auth_core_client/serverpod_auth_core_client.dart'
|
||||
as _iacc;
|
||||
|
|
@ -41,6 +43,38 @@ class EndpointAnonymousIdp extends _iaic.EndpointAnonymousIdpBase {
|
|||
);
|
||||
}
|
||||
|
||||
/// Your devices: the sessions you're signed in with, and signing one of them
|
||||
/// out, for example a lost phone.
|
||||
///
|
||||
/// Serverpod has no row-level security, so this endpoint enforces access
|
||||
/// itself, like every endpoint: the person always comes from the session,
|
||||
/// never from the client; every query filters by them; and someone else's
|
||||
/// device answers exactly like a missing one.
|
||||
/// {@category Endpoint}
|
||||
class EndpointDevices extends _isc.EndpointRef {
|
||||
EndpointDevices(_isc.EndpointCaller caller) : super(caller);
|
||||
|
||||
@override
|
||||
String get name => 'devices';
|
||||
|
||||
/// Your devices, the most recently used first.
|
||||
_ida.Future<List<_ieqh4yr9.Device>> list() =>
|
||||
caller.callServerEndpoint<List<_ieqh4yr9.Device>>(
|
||||
'devices',
|
||||
'list',
|
||||
{},
|
||||
);
|
||||
|
||||
/// Signs out one of your devices. Throws [NotFoundException] when it isn't
|
||||
/// yours, exactly as when it doesn't exist.
|
||||
_ida.Future<void> signOut(_isc.UuidValue sessionId) =>
|
||||
caller.callServerEndpoint<void>(
|
||||
'devices',
|
||||
'signOut',
|
||||
{'sessionId': sessionId},
|
||||
);
|
||||
}
|
||||
|
||||
class Modules {
|
||||
Modules(Client client) {
|
||||
serverpod_auth_idp = _iaic.Caller(client);
|
||||
|
|
@ -80,16 +114,20 @@ class Client extends _isc.ServerpodClientShared {
|
|||
httpClientOverride: httpClientOverride,
|
||||
) {
|
||||
anonymousIdp = EndpointAnonymousIdp(this);
|
||||
devices = EndpointDevices(this);
|
||||
modules = Modules(this);
|
||||
}
|
||||
|
||||
late final EndpointAnonymousIdp anonymousIdp;
|
||||
|
||||
late final EndpointDevices devices;
|
||||
|
||||
late final Modules modules;
|
||||
|
||||
@override
|
||||
Map<String, _isc.EndpointRef> get endpointRefLookup => {
|
||||
'anonymousIdp': anonymousIdp,
|
||||
'devices': devices,
|
||||
};
|
||||
|
||||
@override
|
||||
|
|
|
|||
Loading…
Reference in a new issue