Skip to content

Commit ee00d12

Browse files
authored
Merge pull request #57 from flutter-news-app-full-source-code/Implement-Configurable-Authentication-for-/api/v1/data-Generic-Route
Implement configurable authentication for `/api/v1/data` generic route
2 parents 2c63e2c + c8455e1 commit ee00d12

File tree

7 files changed

+274
-102
lines changed

7 files changed

+274
-102
lines changed

lib/src/middlewares/authorization_middleware.dart

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@ final _log = Logger('AuthorizationMiddleware');
99
/// {@template authorization_middleware}
1010
/// Middleware to enforce role-based permissions and model-specific access rules.
1111
///
12-
/// This middleware reads the authenticated [User], the requested `modelName`,
13-
/// the `HttpMethod`, and the `ModelConfig` from the request context. It then
14-
/// determines the required permission based on the `ModelConfig` metadata for
15-
/// the specific HTTP method and checks if the authenticated user has that
16-
/// permission using the [PermissionService].
12+
/// This middleware reads the authenticated [User] (which may be null for
13+
/// public routes), the requested `modelName`, the `HttpMethod`, and the
14+
/// `ModelConfig` from the request context. It then determines the required
15+
/// permission based on the `ModelConfig` metadata for the specific HTTP method
16+
/// and checks if the authenticated user has that permission using the
17+
/// [PermissionService].
1718
///
18-
/// If the user does not have the required permission, it throws a
19-
/// [ForbiddenException], which should be caught by the 'errorHandler' middleware.
19+
/// If the user does not have the required permission, or if authentication is
20+
/// required but no user is present, it throws a [ForbiddenException] or
21+
/// [UnauthorizedException], which should be caught by the 'errorHandler' middleware.
2022
///
2123
/// This middleware runs *after* authentication and model validation.
2224
/// It does NOT perform instance-level ownership checks; those are handled
@@ -27,8 +29,9 @@ Middleware authorizationMiddleware() {
2729
return (handler) {
2830
return (context) async {
2931
// Read dependencies from the context.
30-
// User is guaranteed non-null by requireAuthentication() middleware.
31-
final user = context.read<User>();
32+
// User is now read as nullable, as _conditionalAuthenticationMiddleware
33+
// might provide null for public routes.
34+
final user = context.read<User?>();
3235
final permissionService = context.read<PermissionService>();
3336
final modelName = context.read<String>(); // Provided by data/_middleware
3437
final modelConfig = context
@@ -64,23 +67,33 @@ Middleware authorizationMiddleware() {
6467
);
6568
}
6669

67-
// Perform the permission check based on the configuration type
70+
// Handle authentication requirement based on ModelConfig ---
71+
if (requiredPermissionConfig.requiresAuthentication && user == null) {
72+
_log.warning(
73+
'Authorization required but no valid user found. Denying access.',
74+
);
75+
throw const UnauthorizedException('Authentication required.');
76+
}
77+
78+
// If authentication is not required, or if user is present, proceed with permission checks.
79+
// All subsequent checks must now handle a potentially null 'user' if 'requiresAuthentication' is false.
6880
switch (requiredPermissionConfig.type) {
6981
case RequiredPermissionType.none:
70-
// No specific permission required (beyond authentication if applicable)
71-
// This case is primarily for documentation/completeness if a route
72-
// group didn't require authentication, but the /data route does.
73-
// For the /data route, 'none' effectively means 'authenticated users allowed'.
82+
// No specific permission required.
83+
// If user is null, it's a public route. If user is not null, they are authenticated.
7484
break;
7585
case RequiredPermissionType.adminOnly:
76-
// Requires the user to be an admin
77-
if (!permissionService.isAdmin(user)) {
86+
// Requires the user to be an admin.
87+
// If user is null here, it means requiresAuthentication was false,
88+
// but adminOnly implies authentication. This is a misconfiguration
89+
// or an attempt to access an admin-only resource publicly.
90+
if (user == null || !permissionService.isAdmin(user)) {
7891
throw const ForbiddenException(
7992
'Only administrators can perform this action.',
8093
);
8194
}
8295
case RequiredPermissionType.specificPermission:
83-
// Requires a specific permission string
96+
// Requires a specific permission string.
8497
final permission = requiredPermissionConfig.permission;
8598
if (permission == null) {
8699
// This indicates a configuration error in ModelRegistry
@@ -92,19 +105,21 @@ Middleware authorizationMiddleware() {
92105
'Internal Server Error: Authorization configuration error.',
93106
);
94107
}
95-
if (!permissionService.hasPermission(user, permission)) {
108+
// If user is null here, it means requiresAuthentication was false,
109+
// but specificPermission implies authentication. This is a misconfiguration
110+
// or an attempt to access a protected resource publicly.
111+
if (user == null ||
112+
!permissionService.hasPermission(user, permission)) {
96113
throw const ForbiddenException(
97114
'You do not have permission to perform this action.',
98115
);
99116
}
100117
case RequiredPermissionType.unsupported:
101118
// This action is explicitly marked as not supported via this generic route.
102-
// Return Method Not Allowed.
103119
_log.warning(
104120
'Action for model "$modelName", method "$method" is marked as '
105121
'unsupported via generic route.',
106122
);
107-
// Throw ForbiddenException to be caught by the errorHandler
108123
throw ForbiddenException(
109124
'Method "$method" is not supported for model "$modelName" '
110125
'via this generic data endpoint.',

lib/src/middlewares/ownership_check_middleware.dart

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import 'package:core/core.dart';
22
import 'package:dart_frog/dart_frog.dart';
33
import 'package:flutter_news_app_api_server_full_source_code/src/rbac/permission_service.dart';
44
import 'package:flutter_news_app_api_server_full_source_code/src/registry/model_registry.dart';
5+
import 'package:logging/logging.dart';
6+
7+
final _log = Logger('OwnershipCheckMiddleware');
58

69
/// A wrapper class to provide a fetched item into the request context.
710
///
@@ -33,8 +36,9 @@ class FetchedItem<T> {
3336
Middleware ownershipCheckMiddleware() {
3437
return (handler) {
3538
return (context) async {
39+
_log.finer('Entering ownershipCheckMiddleware.');
3640
final modelConfig = context.read<ModelConfig<dynamic>>();
37-
final user = context.read<User>();
41+
final user = context.read<User?>();
3842
final permissionService = context.read<PermissionService>();
3943
final method = context.request.method;
4044

@@ -49,20 +53,47 @@ Middleware ownershipCheckMiddleware() {
4953
permission = modelConfig.deletePermission;
5054
default:
5155
// For any other methods, no ownership check is performed.
56+
_log.finer(
57+
'Method "$method" does not require ownership check. Skipping.',
58+
);
5259
return handler(context);
5360
}
5461

5562
// If no ownership check is required for this action, or if the user is
5663
// an admin (who bypasses ownership checks), proceed immediately.
5764
if (!permission.requiresOwnershipCheck ||
58-
permissionService.isAdmin(user)) {
65+
(user != null && permissionService.isAdmin(user))) {
66+
_log.finer(
67+
'Ownership check not required or user is admin. Skipping ownership check.',
68+
);
5969
return handler(context);
6070
}
6171

6272
// At this point, an ownership check is required for a non-admin user.
73+
_log.finer(
74+
'Ownership check required for model "${context.read<String>()}", '
75+
'method "$method". User is not admin.',
76+
);
77+
78+
// If an ownership check IS required, we must have an authenticated user.
79+
// If user is null here, it means a public route is configured to require
80+
// an ownership check, which is a misconfiguration.
81+
if (user == null) {
82+
_log.warning(
83+
'Ownership check required but no authenticated user found. '
84+
'This indicates a configuration error.',
85+
);
86+
throw const OperationFailedException(
87+
'Internal Server Error: Ownership check required for unauthenticated access.',
88+
);
89+
}
6390

6491
// Ensure the model is configured to support ownership checks.
6592
if (modelConfig.getOwnerId == null) {
93+
_log.severe(
94+
'Configuration Error: Model "${context.read<String>()}" requires '
95+
'ownership check but getOwnerId is null.',
96+
);
6697
throw const OperationFailedException(
6798
'Internal Server Error: Model configuration error for ownership check.',
6899
);
@@ -76,10 +107,15 @@ Middleware ownershipCheckMiddleware() {
76107
// Compare the item's owner ID with the authenticated user's ID.
77108
final itemOwnerId = modelConfig.getOwnerId!(item);
78109
if (itemOwnerId != user.id) {
110+
_log.warning(
111+
'Ownership check failed for user ${user.id} on item with owner ID '
112+
'$itemOwnerId. Denying access.',
113+
);
79114
throw const ForbiddenException(
80115
'You do not have permission to access this item.',
81116
);
82117
}
118+
_log.finer('Ownership check passed for user ${user.id}.');
83119

84120
// If the ownership check passes, proceed to the final route handler.
85121
return handler(context);

lib/src/providers/countries_client_provider.dart

Lines changed: 0 additions & 39 deletions
This file was deleted.

0 commit comments

Comments
 (0)