From c15fabaa078fc5f2de60d0b79e5d8c29767555b7 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 31 Oct 2025 06:54:16 +0100 Subject: [PATCH 1/7] feat(rbac): add user management permissions - Add 'user.create' permission for creating new users (admin-only) - Add 'user.update' permission for updating any user's profile (admin-only) - Add 'user.delete' permission for deleting any user's account (admin-only) --- lib/src/rbac/permissions.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/src/rbac/permissions.dart b/lib/src/rbac/permissions.dart index 8bd383f9..31c63426 100644 --- a/lib/src/rbac/permissions.dart +++ b/lib/src/rbac/permissions.dart @@ -44,6 +44,13 @@ abstract class Permissions { // Allows deleting the authenticated user's own account static const String userDeleteOwned = 'user.delete_owned'; + // Allows creating a new user (admin-only). + static const String userCreate = 'user.create'; + // Allows updating any user's profile (admin-only). + static const String userUpdate = 'user.update'; + // Allows deleting any user's account (admin-only). + static const String userDelete = 'user.delete'; + // User App Settings Permissions (User-owned) static const String userAppSettingsReadOwned = 'user_app_settings.read_owned'; static const String userAppSettingsUpdateOwned = From fc79dae36708c1198376aa3ae02cb262e41cf407 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 31 Oct 2025 06:54:36 +0100 Subject: [PATCH 2/7] feat(rbac): extend dashboard admin permissions - Add user management permissions (create, update, delete) to dashboard admins - Update comment for userRead permission to clarify its purpose --- lib/src/rbac/role_permissions.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/src/rbac/role_permissions.dart b/lib/src/rbac/role_permissions.dart index a6660efa..f0cc81fc 100644 --- a/lib/src/rbac/role_permissions.dart +++ b/lib/src/rbac/role_permissions.dart @@ -68,7 +68,11 @@ final Set _dashboardAdminPermissions = { Permissions.languageCreate, Permissions.languageUpdate, Permissions.languageDelete, - Permissions.userRead, // Allows reading any user's profile + Permissions.userRead, // Allows reading any user's profile. + // Allow full user account management for admins. + Permissions.userCreate, + Permissions.userUpdate, + Permissions.userDelete, Permissions.remoteConfigCreate, Permissions.remoteConfigUpdate, Permissions.remoteConfigDelete, From 39501dfd9a7d44360f799142320f177dafd0a8ae Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 31 Oct 2025 06:55:03 +0100 Subject: [PATCH 3/7] feat(user): enhance user model permissions - Add create user permission for admins - Implement update and delete permissions for both admins and regular users - Add detailed comments explaining the permission logic --- lib/src/registry/model_registry.dart | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/src/registry/model_registry.dart b/lib/src/registry/model_registry.dart index 1e892840..bbe9ce31 100644 --- a/lib/src/registry/model_registry.dart +++ b/lib/src/registry/model_registry.dart @@ -281,17 +281,28 @@ final modelRegistry = >{ requiresOwnershipCheck: true, // Must be the owner requiresAuthentication: true, ), + // Admins can create users via the data endpoint. + // User creation via auth routes (e.g., sign-up) is separate. postPermission: const ModelActionPermission( - type: RequiredPermissionType - .unsupported, // User creation handled by auth routes + type: RequiredPermissionType.specificPermission, + permission: Permissions.userCreate, requiresAuthentication: true, ), + // An admin can update any user's roles. + // A regular user can update specific fields on their own profile + // (e.g., feedDecoratorStatus), which is handled by the updater logic + // in DataOperationRegistry. The ownership check ensures they can only + // access their own user object to begin with. putPermission: const ModelActionPermission( type: RequiredPermissionType.specificPermission, permission: Permissions.userUpdateOwned, // User can update their own requiresOwnershipCheck: true, // Must be the owner requiresAuthentication: true, ), + // An admin can delete any user. + // A regular user can delete their own account. + // The ownership check middleware is bypassed for admins, so this single + // config works for both roles. deletePermission: const ModelActionPermission( type: RequiredPermissionType.specificPermission, permission: Permissions.userDeleteOwned, // User can delete their own From 2ae6db40b13b1b123a2ddf8dd3e044116c415406 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 31 Oct 2025 06:56:07 +0100 Subject: [PATCH 4/7] feat(auth): implement user creation and update logic in registry Adds and refactors data operations for the user model. - Create: A new entry in _itemCreators is added for 'user', enabling user creation through the generic data endpoint. - Update: The updater for 'user' is refactored to be more secure and flexible. It now accepts a raw Map from the request body and selectively applies changes for appRole, dashboardRole, and feedDecoratorStatus. This approach prevents mass assignment vulnerabilities while allowing both admins and users to perform their permitted updates. --- lib/src/registry/data_operation_registry.dart | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/lib/src/registry/data_operation_registry.dart b/lib/src/registry/data_operation_registry.dart index c8cb2530..12599f7a 100644 --- a/lib/src/registry/data_operation_registry.dart +++ b/lib/src/registry/data_operation_registry.dart @@ -188,6 +188,11 @@ class DataOperationRegistry { item: item as Language, userId: uid, ), + // Handler for creating a new user. + 'user': (c, item, uid) => c.read>().create( + item: item as User, + userId: uid, + ), 'remote_config': (c, item, uid) => c .read>() .create(item: item as RemoteConfig, userId: uid), @@ -220,13 +225,44 @@ class DataOperationRegistry { 'language': (c, id, item, uid) => c .read>() .update(id: id, item: item as Language, userId: uid), + // Custom updater for the 'user' model. + // This updater handles two distinct use cases: + // 1. Admins updating user roles (`appRole`, `dashboardRole`). + // 2. Regular users updating their own `feedDecoratorStatus`. + // It accepts a raw Map as the `item` to prevent + // mass assignment vulnerabilities, only applying allowed fields. 'user': (c, id, item, uid) { final repo = c.read>(); final existingUser = c.read>().data as User; - final updatedUser = existingUser.copyWith( - feedDecoratorStatus: (item as User).feedDecoratorStatus, + final requestBody = item as Map; + + AppUserRole? newAppRole; + if (requestBody.containsKey('appRole')) { + newAppRole = AppUserRole.values.byName( + requestBody['appRole'] as String, + ); + } + + DashboardUserRole? newDashboardRole; + if (requestBody.containsKey('dashboardRole')) { + newDashboardRole = DashboardUserRole.values.byName( + requestBody['dashboardRole'] as String, + ); + } + + Map? newStatus; + if (requestBody.containsKey('feedDecoratorStatus')) { + newStatus = User.fromJson( + {'feedDecoratorStatus': requestBody['feedDecoratorStatus']}, + ).feedDecoratorStatus; + } + + final userWithUpdates = existingUser.copyWith( + appRole: newAppRole, + dashboardRole: newDashboardRole, + feedDecoratorStatus: newStatus, ); - return repo.update(id: id, item: updatedUser, userId: uid); + return repo.update(id: id, item: userWithUpdates, userId: uid); }, 'user_app_settings': (c, id, item, uid) => c .read>() From e7978b209779dee1d0785cb6309f46c0b81d4387 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 31 Oct 2025 06:57:58 +0100 Subject: [PATCH 5/7] feat(auth): add validation for user creation in POST handler Updates the _handlePost function in the generic data endpoint to include a specific validation check for modelName == 'user'. This ensures that any request to create a user includes a non-empty email field, preventing the creation of invalid user records. --- routes/api/v1/data/index.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/routes/api/v1/data/index.dart b/routes/api/v1/data/index.dart index ef002412..ab541e9d 100644 --- a/routes/api/v1/data/index.dart +++ b/routes/api/v1/data/index.dart @@ -117,6 +117,14 @@ Future _handlePost(RequestContext context) async { throw const BadRequestException('Missing or invalid request body.'); } + // For user creation, ensure the email field is present. + if (modelName == 'user') { + if (!requestBody.containsKey('email') || + (requestBody['email'] as String).isEmpty) { + throw const BadRequestException('Missing required field: "email".'); + } + } + final now = DateTime.now().toUtc().toIso8601String(); requestBody['id'] = ObjectId().oid; requestBody['createdAt'] = now; From 662bfe56e88e1056f5cf2a4f943b8acfebc9e04d Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 31 Oct 2025 06:58:19 +0100 Subject: [PATCH 6/7] feat(auth): adapt PUT handler for secure user updates Modifies the _handlePut function to introduce special handling for the 'user' model. When updating a user, the raw request body map is now passed directly to the _updateItem function, bypassing the standard fromJson deserialization. This change enables the custom user updater in the DataOperationRegistry to perform a secure, selective merge of updatable fields (like roles), preventing mass assignment vulnerabilities. For all other models, the behavior remains unchanged. --- routes/api/v1/data/[id]/index.dart | 48 +++++++++++++++++++----------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/routes/api/v1/data/[id]/index.dart b/routes/api/v1/data/[id]/index.dart index 4744c1d4..8b929b0f 100644 --- a/routes/api/v1/data/[id]/index.dart +++ b/routes/api/v1/data/[id]/index.dart @@ -68,27 +68,41 @@ Future _handlePut(RequestContext context, String id) async { requestBody['updatedAt'] = DateTime.now().toUtc().toIso8601String(); + // The item to be passed to the updater function. + // For 'user' updates, this will be the raw request body map to allow for + // secure, selective field merging in the DataOperationRegistry. + // For all other models, it's the deserialized object. dynamic itemToUpdate; - try { - itemToUpdate = modelConfig.fromJson(requestBody); - } on TypeError catch (e, s) { - _logger.warning('Deserialization TypeError in PUT /data/[id]', e, s); - throw const BadRequestException( - 'Invalid request body: Missing or invalid required field(s).', - ); - } - try { - final bodyItemId = modelConfig.getId(itemToUpdate); - if (bodyItemId != id) { - throw BadRequestException( - 'Bad Request: ID in request body ("$bodyItemId") does not match ID in path ("$id").', + if (modelName == 'user') { + // For user updates, we pass the raw map to the updater. + // This allows the updater to selectively apply fields, preventing mass + // assignment vulnerabilities. The ID check is also skipped as the request + // body for a user role update will not contain an ID. + _logger.finer('User model update: using raw request body for updater.'); + itemToUpdate = requestBody; + } else { + // For all other models, deserialize the body into a model instance. + try { + itemToUpdate = modelConfig.fromJson(requestBody); + } on TypeError catch (e, s) { + _logger.warning('Deserialization TypeError in PUT /data/[id]', e, s); + throw const BadRequestException( + 'Invalid request body: Missing or invalid required field(s).', ); } - } catch (e) { - // Ignore if getId throws, as the ID might not be in the body, - // which can be acceptable for some models. - _logger.info('Could not get ID from PUT body: $e'); + + // Validate that the ID in the body matches the ID in the path. + try { + final bodyItemId = modelConfig.getId(itemToUpdate); + if (bodyItemId != id) { + throw BadRequestException( + 'Bad Request: ID in request body ("$bodyItemId") does not match ID in path ("$id").', + ); + } + } catch (e) { + _logger.info('Could not get ID from PUT body: $e'); + } } if (modelName == 'user_content_preferences') { From 6f81b7f155953a286a1729e837614763053e063e Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 31 Oct 2025 07:13:53 +0100 Subject: [PATCH 7/7] refactor(auth): add robust validation for role updates Adds try-catch blocks around the byName() enum parsing for appRole and dashboardRole in the user updater logic. This prevents unhandled ArgumentError exceptions when a client provides an invalid role string. Instead of causing a 500 Internal Server Error, the API will now correctly return a 400 Bad Request with a clear error message, improving client-side error handling and API robustness. --- lib/src/registry/data_operation_registry.dart | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/lib/src/registry/data_operation_registry.dart b/lib/src/registry/data_operation_registry.dart index 12599f7a..b51146d4 100644 --- a/lib/src/registry/data_operation_registry.dart +++ b/lib/src/registry/data_operation_registry.dart @@ -238,16 +238,28 @@ class DataOperationRegistry { AppUserRole? newAppRole; if (requestBody.containsKey('appRole')) { - newAppRole = AppUserRole.values.byName( - requestBody['appRole'] as String, - ); + try { + newAppRole = AppUserRole.values.byName( + requestBody['appRole'] as String, + ); + } on ArgumentError { + throw BadRequestException( + 'Invalid value for "appRole": "${requestBody['appRole']}".', + ); + } } DashboardUserRole? newDashboardRole; if (requestBody.containsKey('dashboardRole')) { - newDashboardRole = DashboardUserRole.values.byName( - requestBody['dashboardRole'] as String, - ); + try { + newDashboardRole = DashboardUserRole.values.byName( + requestBody['dashboardRole'] as String, + ); + } on ArgumentError { + throw BadRequestException( + 'Invalid value for "dashboardRole": "${requestBody['dashboardRole']}".', + ); + } } Map? newStatus;