From aefe69e220c4f944d7df7aa197cba49089f1305e Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 16:06:11 +0100 Subject: [PATCH 1/3] fix(auth): enhance dashboard login security - Added role verification for dashboard logins. - Prevents non-admin access via code verification. - Improved security against unauthorized access. - Closed loophole in existing authentication flow. - Added logging for successful/failed verifications. --- lib/src/services/auth_service.dart | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/lib/src/services/auth_service.dart b/lib/src/services/auth_service.dart index dc38237..f6672e9 100644 --- a/lib/src/services/auth_service.dart +++ b/lib/src/services/auth_service.dart @@ -157,6 +157,25 @@ class AuthService { final existingUser = await _findUserByEmail(email); if (existingUser != null) { user = existingUser; + // If this is a dashboard login, re-verify the user's dashboard role. + // This closes the loophole where a non-admin user could request a code + // via the app flow and then use it to log into the dashboard. + if (isDashboardLogin) { + final hasRequiredRole = + user.dashboardRole == DashboardUserRole.admin || + user.dashboardRole == DashboardUserRole.publisher; + + if (!hasRequiredRole) { + _log.warning( + 'Dashboard login failed: User ${user.id} lacks required roles ' + 'during code verification.', + ); + throw const ForbiddenException( + 'Your account does not have the required permissions to sign in.', + ); + } + _log.info('Dashboard user ${user.id} re-verified successfully.'); + } } else { // User not found. if (isDashboardLogin) { From 384267ddc3300901597962d01107b686e5e739eb Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 16:15:10 +0100 Subject: [PATCH 2/3] feat(auth): implement permission-based dashboard login - Added PermissionService to AuthService - Created dashboard login permission - Updated login logic to use PermissionService - Improved logging for permission checks - Refactored role-based checks to permission checks --- lib/src/config/app_dependencies.dart | 3 ++- lib/src/rbac/permissions.dart | 3 +++ lib/src/rbac/role_permissions.dart | 1 + lib/src/services/auth_service.dart | 32 ++++++++++++++++------------ 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/lib/src/config/app_dependencies.dart b/lib/src/config/app_dependencies.dart index 2ae1b7e..b48d7e5 100644 --- a/lib/src/config/app_dependencies.dart +++ b/lib/src/config/app_dependencies.dart @@ -178,10 +178,12 @@ class AppDependencies { ); verificationCodeStorageService = InMemoryVerificationCodeStorageService(); + permissionService = const PermissionService(); authService = AuthService( userRepository: userRepository, authTokenService: authTokenService, verificationCodeStorageService: verificationCodeStorageService, + permissionService: permissionService, emailRepository: emailRepository, userAppSettingsRepository: userAppSettingsRepository, userContentPreferencesRepository: userContentPreferencesRepository, @@ -193,7 +195,6 @@ class AppDependencies { topicRepository: topicRepository, sourceRepository: sourceRepository, ); - permissionService = const PermissionService(); userPreferenceLimitService = DefaultUserPreferenceLimitService( remoteConfigRepository: remoteConfigRepository, log: Logger('DefaultUserPreferenceLimitService'), diff --git a/lib/src/rbac/permissions.dart b/lib/src/rbac/permissions.dart index 53687c0..9d17c47 100644 --- a/lib/src/rbac/permissions.dart +++ b/lib/src/rbac/permissions.dart @@ -54,4 +54,7 @@ abstract class Permissions { static const String remoteConfigRead = 'remote_config.read'; static const String remoteConfigUpdate = 'remote_config.update'; static const String remoteConfigDelete = 'remote_config.delete'; + + // Dashboard Permissions + static const String dashboardLogin = 'dashboard.login'; } diff --git a/lib/src/rbac/role_permissions.dart b/lib/src/rbac/role_permissions.dart index 9e26b9e..951e500 100644 --- a/lib/src/rbac/role_permissions.dart +++ b/lib/src/rbac/role_permissions.dart @@ -33,6 +33,7 @@ final Set _dashboardPublisherPermissions = { Permissions.headlineCreate, Permissions.headlineUpdate, Permissions.headlineDelete, + Permissions.dashboardLogin, }; final Set _dashboardAdminPermissions = { diff --git a/lib/src/services/auth_service.dart b/lib/src/services/auth_service.dart index f6672e9..d1a0cfc 100644 --- a/lib/src/services/auth_service.dart +++ b/lib/src/services/auth_service.dart @@ -1,3 +1,5 @@ +import 'package:ht_api/src/rbac/permission_service.dart'; +import 'package:ht_api/src/rbac/permissions.dart'; import 'package:ht_api/src/services/auth_token_service.dart'; import 'package:ht_api/src/services/verification_code_storage_service.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; @@ -21,12 +23,14 @@ class AuthService { required HtEmailRepository emailRepository, required HtDataRepository userAppSettingsRepository, required HtDataRepository - userContentPreferencesRepository, + userContentPreferencesRepository, + required PermissionService permissionService, required Uuid uuidGenerator, required Logger log, }) : _userRepository = userRepository, _authTokenService = authTokenService, _verificationCodeStorageService = verificationCodeStorageService, + _permissionService = permissionService, _emailRepository = emailRepository, _userAppSettingsRepository = userAppSettingsRepository, _userContentPreferencesRepository = userContentPreferencesRepository, @@ -39,7 +43,8 @@ class AuthService { final HtEmailRepository _emailRepository; final HtDataRepository _userAppSettingsRepository; final HtDataRepository - _userContentPreferencesRepository; + _userContentPreferencesRepository; + final PermissionService _permissionService; final Logger _log; final Uuid _uuid; @@ -77,13 +82,13 @@ class AuthService { ); } - final hasRequiredRole = - user.dashboardRole == DashboardUserRole.admin || - user.dashboardRole == DashboardUserRole.publisher; - - if (!hasRequiredRole) { + // Use the PermissionService to check for the specific dashboard login permission. + if (!_permissionService.hasPermission( + user, + Permissions.dashboardLogin, + )) { _log.warning( - 'Dashboard login failed: User ${user.id} lacks required roles.', + 'Dashboard login failed: User ${user.id} lacks required permission (${Permissions.dashboardLogin}).', ); throw const ForbiddenException( 'Your account does not have the required permissions to sign in.', @@ -161,13 +166,12 @@ class AuthService { // This closes the loophole where a non-admin user could request a code // via the app flow and then use it to log into the dashboard. if (isDashboardLogin) { - final hasRequiredRole = - user.dashboardRole == DashboardUserRole.admin || - user.dashboardRole == DashboardUserRole.publisher; - - if (!hasRequiredRole) { + if (!_permissionService.hasPermission( + user, + Permissions.dashboardLogin, + )) { _log.warning( - 'Dashboard login failed: User ${user.id} lacks required roles ' + 'Dashboard login failed: User ${user.id} lacks required permission ' 'during code verification.', ); throw const ForbiddenException( From 9e89851ac15ee08c2c673e1b934f0668683f677f Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 13 Jul 2025 16:20:00 +0100 Subject: [PATCH 3/3] feat(rbac): Add user preference bypass permission - Added `userPreferenceBypassLimits` permission - Updated role permissions to include new permission - Implemented permission check in limit service - Modified `app_dependencies.dart` to inject `PermissionService` - Updated `DefaultUserPreferenceLimitService` to use new permission --- lib/src/config/app_dependencies.dart | 1 + lib/src/rbac/permissions.dart | 4 ++++ lib/src/rbac/role_permissions.dart | 1 + ...default_user_preference_limit_service.dart | 19 +++++++++++++++---- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/lib/src/config/app_dependencies.dart b/lib/src/config/app_dependencies.dart index b48d7e5..be39ea8 100644 --- a/lib/src/config/app_dependencies.dart +++ b/lib/src/config/app_dependencies.dart @@ -197,6 +197,7 @@ class AppDependencies { ); userPreferenceLimitService = DefaultUserPreferenceLimitService( remoteConfigRepository: remoteConfigRepository, + permissionService: permissionService, log: Logger('DefaultUserPreferenceLimitService'), ); diff --git a/lib/src/rbac/permissions.dart b/lib/src/rbac/permissions.dart index 9d17c47..54ece71 100644 --- a/lib/src/rbac/permissions.dart +++ b/lib/src/rbac/permissions.dart @@ -57,4 +57,8 @@ abstract class Permissions { // Dashboard Permissions static const String dashboardLogin = 'dashboard.login'; + + // User Preference Permissions + static const String userPreferenceBypassLimits = + 'user_preference.bypass_limits'; } diff --git a/lib/src/rbac/role_permissions.dart b/lib/src/rbac/role_permissions.dart index 951e500..5222d47 100644 --- a/lib/src/rbac/role_permissions.dart +++ b/lib/src/rbac/role_permissions.dart @@ -51,6 +51,7 @@ final Set _dashboardAdminPermissions = { Permissions.remoteConfigCreate, Permissions.remoteConfigUpdate, Permissions.remoteConfigDelete, + Permissions.userPreferenceBypassLimits, }; /// Defines the mapping between user roles (both app and dashboard) and the diff --git a/lib/src/services/default_user_preference_limit_service.dart b/lib/src/services/default_user_preference_limit_service.dart index 74082a8..4f21aee 100644 --- a/lib/src/services/default_user_preference_limit_service.dart +++ b/lib/src/services/default_user_preference_limit_service.dart @@ -1,3 +1,5 @@ +import 'package:ht_api/src/rbac/permission_service.dart'; +import 'package:ht_api/src/rbac/permissions.dart'; import 'package:ht_api/src/services/user_preference_limit_service.dart'; import 'package:ht_data_repository/ht_data_repository.dart'; import 'package:ht_shared/ht_shared.dart'; @@ -11,11 +13,14 @@ class DefaultUserPreferenceLimitService implements UserPreferenceLimitService { /// {@macro default_user_preference_limit_service} const DefaultUserPreferenceLimitService({ required HtDataRepository remoteConfigRepository, + required PermissionService permissionService, required Logger log, }) : _remoteConfigRepository = remoteConfigRepository, + _permissionService = permissionService, _log = log; final HtDataRepository _remoteConfigRepository; + final PermissionService _permissionService; final Logger _log; // Assuming a fixed ID for the RemoteConfig document @@ -34,8 +39,11 @@ class DefaultUserPreferenceLimitService implements UserPreferenceLimitService { ); final limits = remoteConfig.userPreferenceConfig; - // Admins have no limits. - if (user.dashboardRole == DashboardUserRole.admin) { + // Users with the bypass permission (e.g., admins) have no limits. + if (_permissionService.hasPermission( + user, + Permissions.userPreferenceBypassLimits, + )) { return; } @@ -94,8 +102,11 @@ class DefaultUserPreferenceLimitService implements UserPreferenceLimitService { ); final limits = remoteConfig.userPreferenceConfig; - // Admins have no limits. - if (user.dashboardRole == DashboardUserRole.admin) { + // Users with the bypass permission (e.g., admins) have no limits. + if (_permissionService.hasPermission( + user, + Permissions.userPreferenceBypassLimits, + )) { return; }