[client] Perform local permission check

This commit is contained in:
Abhishek Shroff
2025-06-01 14:34:04 +05:30
parent f52fcb818d
commit 62582b5e83
8 changed files with 159 additions and 96 deletions

View File

@@ -19,3 +19,7 @@ class DestinationCycleException {
const DestinationCycleException(this.resource);
}
class PermissionException {
const PermissionException();
}

View File

@@ -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<String> Function({
@@ -85,14 +85,19 @@ class ResourceRepository extends Repository<PhylumAccount, Resource> {
Future<String> 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<PhylumAccount, Resource> {
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<PhylumAccount, Resource> {
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<PhylumAccount, Resource> {
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<PhylumAccount, Resource> {
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) {

View File

@@ -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,
),
);
}

View File

@@ -104,12 +104,13 @@ class _ExplorerViewState extends State<ExplorerView> {
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,
),
);
}

View File

@@ -140,11 +140,12 @@ Future<void> processPasteItems(
context,
r.name,
overwriteFn: (r) => nameConflictDelete,
(name, conflictResolution) => copyMoveFn(
(name, conflictResolution, ensurePermission) => copyMoveFn(
resource: r,
name: name,
parent: folderId,
conflictResolution: conflictResolution,
ensurePermission: ensurePermission,
),
);
}

View File

@@ -64,11 +64,12 @@ class _ResourceDragTargetState extends State<ResourceDragTarget> {
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,
),
);
}

View File

@@ -63,11 +63,12 @@ void handleOption(BuildContext context, Iterable<Resource> 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<Resource> 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<Resource> 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;

View File

@@ -38,11 +38,12 @@ Future<void> 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<void> uploadXFiles(BuildContext context, String folderId, Iterable<XFile>
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<void> uploadPath(BuildContext context, String folderId, Iterable<String>
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<void> uploadPath(BuildContext context, String folderId, Iterable<String>
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<void> uploadPath(BuildContext context, String folderId, Iterable<String>
}
}
// #TODO: handle permission errors as well
Future<String?> handleLocalErrors(
BuildContext context,
String name,
Future<String> Function(String name, NameConflictResolution conflictResolution) performAction, {
Future<String> 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<NameConflictResolution>(
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: <Widget>[
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<NameConflictResolution>(
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: <Widget>[
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());
}
}