From 62582b5e832c7d52caae3a072e419e8f511554c5 Mon Sep 17 00:00:00 2001 From: Abhishek Shroff Date: Sun, 1 Jun 2025 14:34:04 +0530 Subject: [PATCH] [client] Perform local permission check --- client/lib/libphylum/local_upload_errors.dart | 4 + .../repositories/resource_repository.dart | 40 +++- client/lib/ui/explorer/explorer_actions.dart | 3 +- client/lib/ui/explorer/explorer_view.dart | 3 +- client/lib/ui/explorer/paste_helpers.dart | 3 +- .../ui/explorer/resource_drop_and_drop.dart | 3 +- client/lib/ui/menu/menu_option.dart | 18 +- client/lib/util/upload_utils.dart | 181 ++++++++++-------- 8 files changed, 159 insertions(+), 96 deletions(-) diff --git a/client/lib/libphylum/local_upload_errors.dart b/client/lib/libphylum/local_upload_errors.dart index fd79d77b..b0d62b05 100644 --- a/client/lib/libphylum/local_upload_errors.dart +++ b/client/lib/libphylum/local_upload_errors.dart @@ -19,3 +19,7 @@ class DestinationCycleException { const DestinationCycleException(this.resource); } + +class PermissionException { + const PermissionException(); +} diff --git a/client/lib/libphylum/repositories/resource_repository.dart b/client/lib/libphylum/repositories/resource_repository.dart index e72361a5..a261df31 100644 --- a/client/lib/libphylum/repositories/resource_repository.dart +++ b/client/lib/libphylum/repositories/resource_repository.dart @@ -20,7 +20,7 @@ import 'package:phylum/libphylum/responses/responses.dart'; import 'package:phylum/libphylum/util/uuid.dart'; import 'package:phylum/util/permissions.dart'; -const performLocalNameCheck = true; +const performLocalChecks = true; const _remoteResourcesBoxName = 'remote_resources'; typedef CopyMoveFn = Future Function({ @@ -85,14 +85,19 @@ class ResourceRepository extends Repository { Future mkdir({ required String parent, required String name, - required NameConflictResolution conflictResolution, required Permission userPermission, - String? id, + required NameConflictResolution conflictResolution, + bool ensurePermission = true, }) async { - id ??= generateUuid(); + final id = generateUuid(); + String localName = name; String? deletedId; - if (performLocalNameCheck) { + if (performLocalChecks) { + if (ensurePermission && userPermission & permissionWrite == 0) { + throw PermissionException(); + } + var existing = await _account.db.getResourcesByName(parent: parent, name: name); if (existing.isNotEmpty) { switch (conflictResolution) { @@ -132,12 +137,17 @@ class ResourceRepository extends Repository { required Permission userPermission, XFile? file, String? path, - NameConflictResolution conflictResolution = nameConflictError, + required NameConflictResolution conflictResolution, + bool ensurePermission = true, }) async { String id = generateUuid(); String localName = name; String? deletedId; - if (performLocalNameCheck) { + if (performLocalChecks) { + if (ensurePermission && userPermission & permissionWrite == 0) { + throw PermissionException(); + } + var existing = await _account.db.getResourcesByName(parent: parent, name: name); if (existing.isNotEmpty) { switch (conflictResolution) { @@ -183,10 +193,16 @@ class ResourceRepository extends Repository { required String name, required String parent, required NameConflictResolution conflictResolution, + bool ensurePermission = true, }) async { String localName = name; String? deletedId; - if (performLocalNameCheck) { + if (performLocalChecks) { + final parentPermission = await _account.db.getResource(parent).then((p) => p?.userPermission ?? 0); + if (ensurePermission && parentPermission & permissionWrite == 0) { + throw PermissionException(); + } + final parents = await _account.db.parents(parent).get(); for (final p in parents) { if (p.id == resource.id) { @@ -231,6 +247,7 @@ class ResourceRepository extends Repository { required String parent, required String name, required NameConflictResolution conflictResolution, + bool ensurePermission = true, }) async { if (parent == resource.parent && name == resource.name) { name = '${basenameWithoutExtension(name)} - copy${extension(name)}'; @@ -239,7 +256,12 @@ class ResourceRepository extends Repository { String id = generateUuid(); String? deletedId; String localName = name; - if (performLocalNameCheck) { + if (performLocalChecks) { + final parentPermission = await _account.db.getResource(parent).then((p) => p?.userPermission ?? 0); + if (ensurePermission && parentPermission & permissionWrite == 0) { + throw PermissionException(); + } + var existing = await _account.db.getResourcesByName(parent: parent, name: name); if (existing.isNotEmpty) { switch (conflictResolution) { diff --git a/client/lib/ui/explorer/explorer_actions.dart b/client/lib/ui/explorer/explorer_actions.dart index aa4fee57..1e68e9ea 100644 --- a/client/lib/ui/explorer/explorer_actions.dart +++ b/client/lib/ui/explorer/explorer_actions.dart @@ -90,11 +90,12 @@ class ExplorerActions extends StatelessWidget { context, name, overwriteFn: (r) => nameConflictDelete, - (name, conflictResolution) => account.resourceRepository.move( + (name, conflictResolution, ensurePermission) => account.resourceRepository.move( resource: r, name: name, parent: r.parent!, conflictResolution: conflictResolution, + ensurePermission: ensurePermission, ), ); } diff --git a/client/lib/ui/explorer/explorer_view.dart b/client/lib/ui/explorer/explorer_view.dart index 2aac0213..369332cb 100644 --- a/client/lib/ui/explorer/explorer_view.dart +++ b/client/lib/ui/explorer/explorer_view.dart @@ -104,12 +104,13 @@ class _ExplorerViewState extends State { context, name, overwriteFn: (r) => r.dir ? nameConflictDelete : nameConflictOverwrite, - (name, conflictResolution) => account.resourceRepository.upload( + (name, conflictResolution, ensurePermission) => account.resourceRepository.upload( parent: dest, name: name, path: path, conflictResolution: conflictResolution, userPermission: userPermission, + ensurePermission: ensurePermission, ), ); } diff --git a/client/lib/ui/explorer/paste_helpers.dart b/client/lib/ui/explorer/paste_helpers.dart index 89f27bc0..917fd5f8 100644 --- a/client/lib/ui/explorer/paste_helpers.dart +++ b/client/lib/ui/explorer/paste_helpers.dart @@ -140,11 +140,12 @@ Future processPasteItems( context, r.name, overwriteFn: (r) => nameConflictDelete, - (name, conflictResolution) => copyMoveFn( + (name, conflictResolution, ensurePermission) => copyMoveFn( resource: r, name: name, parent: folderId, conflictResolution: conflictResolution, + ensurePermission: ensurePermission, ), ); } diff --git a/client/lib/ui/explorer/resource_drop_and_drop.dart b/client/lib/ui/explorer/resource_drop_and_drop.dart index e26ea434..9a5bc290 100644 --- a/client/lib/ui/explorer/resource_drop_and_drop.dart +++ b/client/lib/ui/explorer/resource_drop_and_drop.dart @@ -64,11 +64,12 @@ class _ResourceDragTargetState extends State { context, r.name, overwriteFn: (r) => nameConflictDelete, - (name, conflictResolution) => account.resourceRepository.move( + (name, conflictResolution, ensurePermission) => account.resourceRepository.move( resource: r, name: name, parent: widget.resourceId, conflictResolution: conflictResolution, + ensurePermission: ensurePermission, ), ); } diff --git a/client/lib/ui/menu/menu_option.dart b/client/lib/ui/menu/menu_option.dart index 8a71a84e..b135dc33 100644 --- a/client/lib/ui/menu/menu_option.dart +++ b/client/lib/ui/menu/menu_option.dart @@ -63,11 +63,12 @@ void handleOption(BuildContext context, Iterable resources, MenuOption context, name, overwriteFn: (r) => nameConflictDelete, - (name, conflictResolution) => account.resourceRepository.move( + (name, conflictResolution, ensurePermission) => account.resourceRepository.move( resource: r, name: name, parent: r.parent!, conflictResolution: conflictResolution, + ensurePermission: ensurePermission, ), ); } @@ -85,11 +86,12 @@ void handleOption(BuildContext context, Iterable resources, MenuOption context, r.name, overwriteFn: (r) => nameConflictDelete, - (name, conflictResolution) => account.resourceRepository.move( + (name, conflictResolution, ensurePermission) => account.resourceRepository.move( resource: r, name: name, parent: dest, conflictResolution: conflictResolution, + ensurePermission: ensurePermission, ), ); } @@ -107,12 +109,12 @@ void handleOption(BuildContext context, Iterable resources, MenuOption context, r.name, overwriteFn: (r) => nameConflictDelete, - (name, conflictResolution) => account.resourceRepository.copy( - resource: r, - name: name, - parent: dest, - conflictResolution: conflictResolution, - ), + (name, conflictResolution, ensurePermission) => account.resourceRepository.copy( + resource: r, + name: name, + parent: dest, + conflictResolution: conflictResolution, + ensurePermission: ensurePermission), ); } break; diff --git a/client/lib/util/upload_utils.dart b/client/lib/util/upload_utils.dart index a985fdce..96cab60d 100644 --- a/client/lib/util/upload_utils.dart +++ b/client/lib/util/upload_utils.dart @@ -38,11 +38,12 @@ Future createDirectory(BuildContext context, Resource parent, {String? pre context, name, overwriteFn: (r) => nameConflictDelete, - (name, conflictResolution) => account.resourceRepository.mkdir( + (name, conflictResolution, ensurePermission) => account.resourceRepository.mkdir( parent: parent.id, name: name, - conflictResolution: conflictResolution, userPermission: parent.userPermission, + conflictResolution: conflictResolution, + ensurePermission: ensurePermission, ), ); } @@ -78,12 +79,13 @@ Future uploadXFiles(BuildContext context, String folderId, Iterable context, p.basename(file.name), overwriteFn: (r) => r.dir ? nameConflictDelete : nameConflictOverwrite, - (name, conflictResolution) => account.resourceRepository.upload( + (name, conflictResolution, ensurePermission) => account.resourceRepository.upload( parent: folderId, name: name, file: file, - conflictResolution: conflictResolution, userPermission: userPermission, + conflictResolution: conflictResolution, + ensurePermission: ensurePermission, ), ); if (id == null && context.mounted) { @@ -135,11 +137,12 @@ Future uploadPath(BuildContext context, String folderId, Iterable context, p.basename(path), overwriteFn: (r) => nameConflictDelete, - (name, conflictResolution) => account.resourceRepository.mkdir( + (name, conflictResolution, ensurePermission) => account.resourceRepository.mkdir( parent: parent, name: name, - conflictResolution: conflictResolution, userPermission: userPermission, + conflictResolution: conflictResolution, + ensurePermission: ensurePermission, ), ); if (id == null) { @@ -163,12 +166,13 @@ Future uploadPath(BuildContext context, String folderId, Iterable context, p.basename(path), overwriteFn: (r) => r.dir ? nameConflictDelete : nameConflictOverwrite, - (name, conflictResolution) => account.resourceRepository.upload( + (name, conflictResolution, ensurePermission) => account.resourceRepository.upload( parent: parent, name: name, path: path, - conflictResolution: conflictResolution, userPermission: userPermission, + conflictResolution: conflictResolution, + ensurePermission: ensurePermission, ), ); if (id == null && context.mounted) { @@ -180,81 +184,108 @@ Future uploadPath(BuildContext context, String folderId, Iterable } } -// #TODO: handle permission errors as well Future handleLocalErrors( BuildContext context, String name, - Future Function(String name, NameConflictResolution conflictResolution) performAction, { + Future Function(String, NameConflictResolution, bool) performAction, { NameConflictResolution? Function(Resource)? overwriteFn, + NameConflictResolution conflictResolution = nameConflictError, + bool ensurePermission = true, }) async { - NameConflictResolution? conflictResolution = nameConflictError; - while (conflictResolution != null) { - try { - return await performAction(name, conflictResolution); - } on NameConflictException catch (e) { - if (!context.mounted) return null; - final overwrite = overwriteFn?.call(e.resource); - conflictResolution = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Name Conflict'), - content: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 360), - child: Text( - 'There is already another item with the name \'$name\' in this folder.', - softWrap: true, - ), - ), - actions: [ - if (overwrite != null) - TextButton( - style: TextButton.styleFrom(foregroundColor: Theme.of(context).colorScheme.error), - child: const Text('Overwrite'), - onPressed: () { - Navigator.of(context).pop(overwrite); - }, - ), - TextButton( - child: const Text('Change Name'), - onPressed: () { - Navigator.of(context).pop(nameConflictError); - }, - ), - ElevatedButton( - autofocus: true, - child: const Text('Auto Rename'), - onPressed: () { - Navigator.of(context).pop(nameConflictRename); - }, - ), - ], - ), - ); - if (conflictResolution == nameConflictError) { - if (!context.mounted) return null; - final newName = await showInputDialog( + try { + return await performAction(name, conflictResolution, ensurePermission); + } on PermissionException { + if (!context.mounted) return null; + final skip = await showAlertDialog( context, - title: 'Change Name', - labelText: 'New Name', - preset: name, - validate: validateName, - ); - if (!context.mounted) return null; - if (newName == null) { - return null; - } - name = newName; - } - } on DestinationCycleException catch (e) { - if (context.mounted) { - await showAlertDialog(context, message: 'Cannot move ${e.resource.name} into itself'); - } + title: 'Permissions Error', + message: 'You do not have permissions to write to this folder.', + positiveText: 'OK', + negativeText: 'Try Anyway', + ) ?? + true; + if (!skip && context.mounted) { + return handleLocalErrors( + context, + name, + performAction, + overwriteFn: overwriteFn, + conflictResolution: conflictResolution, + ensurePermission: false, + ); + } + } on NameConflictException catch (e) { + if (!context.mounted) return null; + final overwrite = overwriteFn?.call(e.resource); + final newResolution = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Name Conflict'), + content: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 360), + child: Text( + 'There is already another item with the name \'$name\' in this folder.', + softWrap: true, + ), + ), + actions: [ + if (overwrite != null) + TextButton( + style: TextButton.styleFrom(foregroundColor: Theme.of(context).colorScheme.error), + child: const Text('Overwrite'), + onPressed: () { + Navigator.of(context).pop(overwrite); + }, + ), + TextButton( + child: const Text('Change Name'), + onPressed: () { + Navigator.of(context).pop(nameConflictError); + }, + ), + ElevatedButton( + autofocus: true, + child: const Text('Auto Rename'), + onPressed: () { + Navigator.of(context).pop(nameConflictRename); + }, + ), + ], + ), + ); + if (newResolution == null) { return null; - } catch (e) { - if (context.mounted) { - await showAlertDialog(context, title: 'Unable to create $name', message: e.toString()); + } + if (newResolution == nameConflictError) { + if (!context.mounted) return null; + final newName = await showInputDialog( + context, + title: 'Change Name', + labelText: 'New Name', + preset: name, + validate: validateName, + ); + if (newName == null) { + return null; } - return null; + name = newName; + } + if (!context.mounted) return null; + return handleLocalErrors( + context, + name, + performAction, + overwriteFn: overwriteFn, + conflictResolution: newResolution, + ensurePermission: ensurePermission, + ); + } on DestinationCycleException catch (e) { + if (context.mounted) { + await showAlertDialog(context, message: 'Cannot move ${e.resource.name} into itself'); + } + } catch (e) { + if (context.mounted) { + await showAlertDialog(context, title: 'Unable to create $name', message: e.toString()); } }