auth: allow certain googlers to vacuum commits
diff --git a/app_dart/bin/gae_server.dart b/app_dart/bin/gae_server.dart index 7c65385..3037ca9 100644 --- a/app_dart/bin/gae_server.dart +++ b/app_dart/bin/gae_server.dart
@@ -69,6 +69,10 @@ final firebaseJwtValidator = FirebaseJwtValidator(cache: cache); const cronAuthentication = DashboardCronAuthentication(); + final googlerAuthentication = DashboardFirebaseAdminAuthentication( + validator: firebaseJwtValidator, + firestore: firestore, + ); final firebaseAuthentication = DashboardFirebaseAuthentication( cache: cache, validator: firebaseJwtValidator, @@ -82,6 +86,7 @@ final cronAuthProvider = ChainOfAuthentication.forProviders([ cronAuthentication, + googlerAuthentication, ]); final dashboardAuthProvider = ChainOfAuthentication.forProviders([ cronAuthentication,
diff --git a/app_dart/lib/src/model/firestore/admin_account.dart b/app_dart/lib/src/model/firestore/admin_account.dart new file mode 100644 index 0000000..4ef7657 --- /dev/null +++ b/app_dart/lib/src/model/firestore/admin_account.dart
@@ -0,0 +1,54 @@ +// Copyright 2024 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:googleapis/firestore/v1.dart' as g; +import 'package:path/path.dart' as p; + +import '../../service/firestore.dart'; +import 'base.dart'; + +/// Describes an authenticated account. +final class AdminAccount extends AppDocument<AdminAccount> { + /// Description of the document in Firestore. + static final metadata = AppDocumentMetadata( + collectionId: 'admin_accounts', + fromDocument: AdminAccount.fromDocument, + ); + + @override + AppDocumentMetadata<AdminAccount> get runtimeMetadata => + AdminAccount.metadata; + + /// Retrieves the account by email. + /// + /// Returns `null` if the account does not exist. + static Future<AdminAccount?> getByEmail( + FirestoreService firestore, { + required String email, + }) async { + final document = await firestore.getDocumentOrNull( + p.posix.join(kDatabase, 'documents', metadata.collectionId, email), + ); + return document == null ? null : AdminAccount.fromDocument(document); + } + + /// Creates a new account with the given [email]. + factory AdminAccount({required String email}) { + return AdminAccount.fromDocument( + g.Document( + name: p.posix.join( + kDatabase, + 'documents', + metadata.collectionId, + email, + ), + ), + ); + } + + AdminAccount.fromDocument(super.document); + + /// Email address of the account. + String get email => p.posix.basename(name!); +}
diff --git a/app_dart/lib/src/request_handling/dashboard_authentication.dart b/app_dart/lib/src/request_handling/dashboard_authentication.dart index ac1549d..e72018a 100644 --- a/app_dart/lib/src/request_handling/dashboard_authentication.dart +++ b/app_dart/lib/src/request_handling/dashboard_authentication.dart
@@ -11,6 +11,7 @@ import '../foundation/providers.dart'; import '../foundation/typedefs.dart'; import '../model/firestore/account.dart'; +import '../model/firestore/admin_account.dart'; import '../model/google/token_info.dart'; import '../service/firebase_jwt_validator.dart'; import 'exceptions.dart'; @@ -160,6 +161,59 @@ } } +/// Firebase authentication for engprod @google.com +class DashboardFirebaseAdminAuthentication implements AuthenticationProvider { + DashboardFirebaseAdminAuthentication({ + required FirebaseJwtValidator validator, + required FirestoreService firestore, + ClientContext Function() clientContextProvider = + Providers.serviceScopeContext, + }) : _validator = validator, + _firestore = firestore, + _clientContextProvider = clientContextProvider; + + /// Provides the App Engine client context as part of the + /// [AuthenticatedContext]. + /// + /// This is guaranteed to be non-null. + final ClientContextProvider _clientContextProvider; + final FirebaseJwtValidator _validator; + final FirestoreService _firestore; + + /// Attempt to validate a JWT as a Firebase token with email @google.com. + @override + Future<AuthenticatedContext> authenticate(Request request) async { + try { + if (request.header('X-Flutter-IdToken') case final idTokenFromHeader?) { + final token = await _validator.decodeAndVerify(idTokenFromHeader); + log.info('authed with firebase: ${token.email}'); + return await authenticateFirebase( + token, + clientContext: _clientContextProvider(), + ); + } + } on JwtException { + // do nothing while in transition + } + throw const Unauthenticated('Not a Firebase token'); + } + + @visibleForTesting + Future<AuthenticatedContext> authenticateFirebase( + TokenInfo token, { + required ClientContext clientContext, + }) async { + if (token.email case final email?) { + if (await AdminAccount.getByEmail(_firestore, email: email) != null) { + return AuthenticatedContext(clientContext: clientContext, email: email); + } + } + throw Unauthenticated( + '${token.email} is not authorized to access the dashboard', + ); + } +} + class DashboardCronAuthentication implements AuthenticationProvider { const DashboardCronAuthentication({ ClientContextProvider clientContextProvider = Providers.serviceScopeContext,