diff --git a/lib/src/config/app_dependencies.dart b/lib/src/config/app_dependencies.dart index 2ae1b7e..be39ea8 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,9 +195,9 @@ class AppDependencies { topicRepository: topicRepository, sourceRepository: sourceRepository, ); - permissionService = const PermissionService(); userPreferenceLimitService = DefaultUserPreferenceLimitService( remoteConfigRepository: remoteConfigRepository, + permissionService: permissionService, log: Logger('DefaultUserPreferenceLimitService'), ); diff --git a/lib/src/rbac/permissions.dart b/lib/src/rbac/permissions.dart index 53687c0..54ece71 100644 --- a/lib/src/rbac/permissions.dart +++ b/lib/src/rbac/permissions.dart @@ -54,4 +54,11 @@ 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'; + + // 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 9e26b9e..5222d47 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 = { @@ -50,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/auth_service.dart b/lib/src/services/auth_service.dart index dc38237..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.', @@ -157,6 +162,24 @@ 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) { + if (!_permissionService.hasPermission( + user, + Permissions.dashboardLogin, + )) { + _log.warning( + 'Dashboard login failed: User ${user.id} lacks required permission ' + '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) { 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; }