[client] Fix permission view

This commit is contained in:
Abhishek Shroff
2025-04-06 03:40:37 +05:30
parent 7883ffcbda
commit 410ccab5a7
5 changed files with 120 additions and 63 deletions
@@ -3,7 +3,6 @@ import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:offtheline/offtheline.dart';
import 'package:phylum/libphylum/db/db.dart';
import 'package:phylum/util/permissions.dart';
class UpdatePermissionsChange extends LocalChange<Resource> {
@override
@@ -25,21 +24,29 @@ class UpdatePermissionsChange extends LocalChange<Resource> {
@override
Resource? apply(Resource? data) {
if (data == null) return null;
var p = data.grants.parseGrantsMap();
if (p.containsKey(username) && p[username] == permission) {
var p = data.grants == null ? const <String, Map>{} : (jsonDecode(data.grants!) as Map).cast<String, Map>();
if ((p[username]?['p'] ?? 0) == permission) {
return data;
}
if (permission == 0) {
p.remove(username);
} else if (p.isEmpty) {
// Empty means const map, so we can't add to it
p = {username: permission};
p = {
username: <String, int>{
'p': permission,
't': DateTime.now().millisecondsSinceEpoch,
},
};
} else {
p[username] = permission;
p[username] = <String, int>{
'p': permission,
't': DateTime.now().millisecondsSinceEpoch,
};
}
final serialized = p.isEmpty ? null : json.encode(p);
return data.copyWith(
permissions: Value(serialized),
grants: Value(serialized),
modified: Value(timestamp),
);
}
+29 -2
View File
@@ -1639,7 +1639,7 @@ abstract class _$AppDatabase extends GeneratedDatabase {
Selectable<ParentsResult> parents(String id) {
return customSelect(
'WITH RECURSIVE parents (id, parent, name, permissions, grants) AS (SELECT id, parent, name, permissions, grants FROM resources WHERE id = ?1 UNION ALL SELECT r.id, r.parent, r.name, r.permissions, r.grants FROM resources AS r JOIN parents AS p ON r.id = p.parent) SELECT id, name, permissions, grants FROM parents',
'WITH RECURSIVE parents (id, parent, name, permissions) AS (SELECT id, parent, name, permissions FROM resources WHERE id = ?1 UNION ALL SELECT r.id, r.parent, r.name, r.permissions FROM resources AS r JOIN parents AS p ON r.id = p.parent) SELECT id, name, permissions FROM parents',
variables: [
Variable<String>(id)
],
@@ -1649,7 +1649,23 @@ abstract class _$AppDatabase extends GeneratedDatabase {
id: row.read<String>('id'),
name: row.read<String>('name'),
permissions: row.readNullable<String>('permissions'),
));
}
Selectable<PermissionsResult> permissions(String id) {
return customSelect(
'SELECT r.name, r.permissions, r.grants, p.permissions AS inherited_permissions FROM resources AS r JOIN resources AS p ON r.parent = p.id WHERE r.id = ?1',
variables: [
Variable<String>(id)
],
readsFrom: {
resources,
}).map((QueryRow row) => PermissionsResult(
name: row.read<String>('name'),
permissions: row.readNullable<String>('permissions'),
grants: row.readNullable<String>('grants'),
inheritedPermissions:
row.readNullable<String>('inherited_permissions'),
));
}
@@ -3063,12 +3079,23 @@ class ParentsResult {
final String id;
final String name;
final String? permissions;
final String? grants;
ParentsResult({
required this.id,
required this.name,
this.permissions,
});
}
class PermissionsResult {
final String name;
final String? permissions;
final String? grants;
final String? inheritedPermissions;
PermissionsResult({
required this.name,
this.permissions,
this.grants,
this.inheritedPermissions,
});
}
@@ -28,6 +28,10 @@ extension ResourceHelpers on AppDatabase {
return parents(id).watch();
}
Stream<PermissionsResult?> watchPermissions(String id) {
return permissions(id).watchSingleOrNull();
}
Future<void> markResourceAccess(String id) {
return recents.insertOnConflictUpdate(Recent(resourceId: id, accessed: DateTime.now()));
}
+9 -4
View File
@@ -15,13 +15,18 @@ CREATE TABLE IF NOT EXISTS resources(
last_refresh DATETIME
);
parents: WITH RECURSIVE parents(id, parent, name, permissions, grants) AS (
SELECT id, parent, name, permissions, grants
parents: WITH RECURSIVE parents(id, parent, name, permissions) AS (
SELECT id, parent, name, permissions
FROM resources
WHERE id = :id
UNION ALL
SELECT r.id, r.parent, r.name, r.permissions, r.grants
SELECT r.id, r.parent, r.name, r.permissions
FROM resources r
JOIN parents p
ON r.id = p.parent
) SELECT id, name, permissions, grants from parents;
) SELECT id, name, permissions from parents;
permissions: SELECT r.name, r.permissions, r.grants, p.permissions as inherited_permissions
FROM resources r
JOIN resources p ON r.parent = p.id
WhERE r.id = :id;
@@ -2,7 +2,6 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:phylum/libphylum/actions/action_resource_share.dart';
import 'package:phylum/libphylum/db/db.dart';
import 'package:phylum/libphylum/db/resource_helpers.dart';
import 'package:phylum/libphylum/phylum_account.dart';
import 'package:phylum/util/dialogs.dart';
@@ -19,20 +18,23 @@ class ResourcePermissionsView extends StatefulWidget {
}
class _ResourcePermissionsViewState extends State<ResourcePermissionsView> {
late String resourceName;
Map<String, Permission>? inheritedPermissions;
String? resourceName;
Map<String, Permission>? permissions;
Map<String, Permission>? grants;
Permission? userPermission;
StreamSubscription? sub;
StreamSubscription<List<ParentsResult>>? sub;
@override
void initState() {
super.initState();
final account = context.read<PhylumAccount>();
sub = account.db.watchParents(widget.resourceId).listen((data) {
sub = account.db.watchPermissions(widget.resourceId).listen((data) {
setState(() {
resourceName = data.first.name;
inheritedPermissions = data.length > 1 ? data[1].permissions.parsePermissionMap() : const {};
permissions = data.first.grants.parseGrantsMap();
resourceName = data?.name;
userPermission = data?.permissions.parsePermissionMap()[account.userName] ?? 0;
permissions = data?.inheritedPermissions.parsePermissionMap();
grants = data?.grants.parseGrantsMap();
});
});
}
@@ -45,14 +47,17 @@ class _ResourcePermissionsViewState extends State<ResourcePermissionsView> {
@override
Widget build(BuildContext context) {
final grants = this.grants;
final permissions = this.permissions;
final inheritedPermissions = this.inheritedPermissions;
if (permissions == null || inheritedPermissions == null) return const CircularProgressIndicator();
if (grants == null || permissions == null) return const CircularProgressIndicator();
final account = context.read<PhylumAccount>();
final repository = account.userRepository;
final userPermissions = permissions[account.userName] ?? 0;
final entries = permissions.entries.toList(growable: false)
final grantEntries = grants.entries.toList(growable: false)
..sort(((a, b) {
return a.value == b.value ? a.key.compareTo(b.key) : a.value.compareTo(b.value);
}));
final permissionEntries = permissions.entries.toList(growable: false)
..sort(((a, b) {
return a.value == b.value ? a.key.compareTo(b.key) : a.value.compareTo(b.value);
}));
@@ -60,16 +65,15 @@ class _ResourcePermissionsViewState extends State<ResourcePermissionsView> {
return DropdownButtonHideUnderline(
child: Column(
children: [
if (userPermissions & permissionShare != 0)
if (userPermission! & permissionShare != 0)
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: ElevatedButton.icon(
label: Text('Add User'),
icon: Icon(Icons.person_add),
onPressed: () async {
final users = account.userRepository.users.entries
.where((e) => !inheritedPermissions.containsKey(e.key) && !permissions.containsKey(e.key))
.map((e) => e.value);
final users =
account.userRepository.users.entries.where((e) => !grants.containsKey(e.key)).map((e) => e.value);
final user = await showOptionsDialogBuilder(
context, users.toList(growable: false), (user) => ListTile(title: Text(user.display)),
filterList: (u, q) {
@@ -86,105 +90,115 @@ class _ResourcePermissionsViewState extends State<ResourcePermissionsView> {
permissionSetReadWrite,
permissionSetReadWriteShare,
],
(p) => ListTile(leading: Icon(_getIcon(p)), title: Text(_getText(p))),
(p) => ListTile(leading: Icon(p.icon), title: Text(p.text)),
);
if (permission == null || !context.mounted) return;
account.addAction(ResourceShareAction(
resourceId: widget.resourceId,
resourceName: resourceName,
resourceName: resourceName!,
username: user.username,
permission: permission,
));
},
),
),
for (final e in entries)
for (final e in grantEntries)
ListTile(
visualDensity: VisualDensity.adaptivePlatformDensity,
title: Text(repository.getUserDisplayName(e.key)),
trailing: DropdownButton<Permission>(
padding: EdgeInsets.symmetric(horizontal: 6, vertical: 6),
items: _createPermissionsItems(theme, e.value, inheritedPermissions[e.key] ?? 0),
items: _createPermissionsItems(theme, e.value),
value: e.value,
onChanged: (userPermissions & permissionShare) == 0
onChanged: (userPermission! & permissionShare) == 0
? null
: (value) {
if (value == null || value == e.value) return;
if (value == inheritedPermissions[e.key]) {
if (value == permissions[e.key]) {
value = 0;
}
account.addAction(ResourceShareAction(
resourceId: widget.resourceId,
resourceName: resourceName,
resourceName: resourceName!,
username: e.key,
permission: value,
));
},
),
// trailing: _getPermissionIcon(e.value),
),
ListTile(
visualDensity: VisualDensity.compact,
dense: true,
title: const Text('Inherited Permissions', style: TextStyle(fontWeight: FontWeight.bold)),
),
for (final e in permissionEntries)
ListTile(
visualDensity: VisualDensity.adaptivePlatformDensity,
leading: Icon(e.value.icon),
title: Text(repository.getUserDisplayName(e.key)),
subtitle: Text(e.value.text),
),
],
),
);
}
List<DropdownMenuItem<Permission>> _createPermissionsItems(ThemeData theme, Permission p, Permission inherited) {
List<DropdownMenuItem<Permission>> _createPermissionsItems(ThemeData theme, Permission p) {
return [
_buildDropdownMenuItem(permissionSetNone, inherited, theme),
_buildDropdownMenuItem(permissionSetReadOnly, inherited, theme),
_buildDropdownMenuItem(permissionSetReadWrite, inherited, theme),
_buildDropdownMenuItem(permissionSetReadWriteShare, inherited, theme),
if (p == permissionSetAll) _buildDropdownMenuItem(permissionSetAll, inherited, theme),
_buildDropdownMenuItem(permissionSetNone, theme),
_buildDropdownMenuItem(permissionSetReadOnly, theme),
_buildDropdownMenuItem(permissionSetReadWrite, theme),
_buildDropdownMenuItem(permissionSetReadWriteShare, theme),
if (p == permissionSetAll) _buildDropdownMenuItem(permissionSetAll, theme),
if (p != permissionSetNone &&
p != permissionSetReadOnly &&
p != permissionSetReadWrite &&
p != permissionSetReadWriteShare &&
p != permissionSetAll)
_buildDropdownMenuItem(p, inherited, theme),
_buildDropdownMenuItem(p, theme),
];
}
DropdownMenuItem<Permission> _buildDropdownMenuItem(Permission p, Permission inherited, ThemeData theme) {
bool enabled = (p | inherited) == p || (p | inherited) != inherited;
final iconColor = enabled ? theme.listTileTheme.iconColor : theme.disabledColor;
final textColor = enabled ? theme.listTileTheme.tileColor : theme.disabledColor;
DropdownMenuItem<Permission> _buildDropdownMenuItem(Permission p, ThemeData theme) {
final iconColor = theme.listTileTheme.iconColor;
final textColor = theme.listTileTheme.tileColor;
return DropdownMenuItem(
value: p,
enabled: enabled,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Icon(_getIcon(p), color: iconColor),
child: Icon(p.icon, color: iconColor),
),
Text(
_getText(p),
p.text,
style: TextStyle(color: textColor),
),
],
),
);
}
}
IconData _getIcon(Permission p) {
if (p == 0) return Icons.block;
if (p == -1) return Icons.shield_outlined;
if (p == permissionSetReadOnly) return Icons.visibility;
if (p == permissionSetReadWrite) return Icons.edit;
if (p == permissionSetReadWriteShare) return Icons.person_add;
extension on Permission {
IconData get icon {
if (this == 0) return Icons.block;
if (this == -1) return Icons.shield_outlined;
if (this == permissionSetReadOnly) return Icons.visibility;
if (this == permissionSetReadWrite) return Icons.edit;
if (this == permissionSetReadWriteShare) return Icons.person_add;
return Icons.settings;
}
String _getText(Permission p) {
if (p == 0) return 'None';
if (p == -1) return 'Admin';
if (p == permissionSetReadOnly) return 'Viewer';
if (p == permissionSetReadWrite) return 'Collaborator';
if (p == permissionSetReadWriteShare) return 'Editor';
String get text {
if (this == 0) return 'None';
if (this == -1) return 'Admin';
if (this == permissionSetReadOnly) return 'Viewer';
if (this == permissionSetReadWrite) return 'Collaborator';
if (this == permissionSetReadWriteShare) return 'Editor';
return 'Custom';
}
}