Permissions
Comprehensive permission system for secure access control at multiple levels
Permissions and Security
The Vyuh Entity System provides a comprehensive permission system that ensures secure access control at multiple levels. This guide covers permission models, implementation patterns, and best practices for securing your entities.
Permission System Overview
The permission system consists of:
- Permission Model - Defines permissions and their scope
- Permission Guards - UI components that enforce permissions
- Permission Service - Core service for permission checks
- Permission Repository - Storage and retrieval of permissions
- Entity-Level Security - Fine-grained access control
Permission Model
Core Permission Structure
class Permission {
final String id;
final String resource; // Entity type or specific resource
final List<PermissionAction> actions;
final String? resourceId; // Optional: specific instance
final Map<String, dynamic>? conditions; // Optional: additional conditions
final DateTime? expiresAt; // Optional: time-based permissions
const Permission({
required this.id,
required this.resource,
required this.actions,
this.resourceId,
this.conditions,
this.expiresAt,
});
// Helper constructors
factory Permission.create(String resource, [String? resourceId]) {
return Permission(
id: const Uuid().v4(),
resource: resource,
actions: [PermissionAction.create],
resourceId: resourceId,
);
}
factory Permission.read(String resource, [String? resourceId]) {
return Permission(
id: const Uuid().v4(),
resource: resource,
actions: [PermissionAction.read],
resourceId: resourceId,
);
}
factory Permission.update(String resource, [String? resourceId]) {
return Permission(
id: const Uuid().v4(),
resource: resource,
actions: [PermissionAction.update],
resourceId: resourceId,
);
}
factory Permission.delete(String resource, [String? resourceId]) {
return Permission(
id: const Uuid().v4(),
resource: resource,
actions: [PermissionAction.delete],
resourceId: resourceId,
);
}
factory Permission.full(String resource, [String? resourceId]) {
return Permission(
id: const Uuid().v4(),
resource: resource,
actions: PermissionAction.values,
resourceId: resourceId,
);
}
}
enum PermissionAction {
create,
read,
update,
delete,
list,
export,
import,
approve,
publish,
archive,
restore,
manage, // Meta permission for managing permissions
}Permission Scopes
abstract class PermissionScope {
bool matches(String resource, String? resourceId);
}
class GlobalScope extends PermissionScope {
@override
bool matches(String resource, String? resourceId) => true;
}
class ResourceTypeScope extends PermissionScope {
final String resourceType;
ResourceTypeScope(this.resourceType);
@override
bool matches(String resource, String? resourceId) {
return resource == resourceType;
}
}
class ResourceInstanceScope extends PermissionScope {
final String resourceType;
final String instanceId;
ResourceInstanceScope(this.resourceType, this.instanceId);
@override
bool matches(String resource, String? resourceId) {
return resource == resourceType && resourceId == instanceId;
}
}
class ConditionalScope extends PermissionScope {
final String resourceType;
final bool Function(Map<String, dynamic>) condition;
ConditionalScope(this.resourceType, this.condition);
@override
bool matches(String resource, String? resourceId) {
if (resource != resourceType) return false;
// Evaluate condition with context
final context = {
'resourceId': resourceId,
'timestamp': DateTime.now(),
// Add more context as needed
};
return condition(context);
}
}Permission Guards
PermissionGuard Widget
Protect UI elements with permission checks:
PermissionGuard(
permissions: [Permission.create('users')],
fallback: const Text('You do not have permission to create users'),
child: ElevatedButton(
onPressed: () => context.go('/users/new'),
child: const Text('Add User'),
),
)Advanced Permission Guard
class PermissionGuard extends StatelessWidget {
final List<Permission> permissions;
final PermissionCheckMode mode; // all, any
final Widget child;
final Widget? fallback;
final Widget? loading;
final bool hideOnDenied;
final void Function()? onPermissionDenied;
const PermissionGuard({
required this.permissions,
required this.child,
this.mode = PermissionCheckMode.all,
this.fallback,
this.loading,
this.hideOnDenied = false,
this.onPermissionDenied,
});
@override
Widget build(BuildContext context) {
return FutureBuilder<bool>(
future: _checkPermissions(context),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return loading ?? const CircularProgressIndicator();
}
final hasPermission = snapshot.data ?? false;
if (!hasPermission) {
onPermissionDenied?.call();
if (hideOnDenied) {
return const SizedBox.shrink();
}
return fallback ?? _buildDefaultFallback(context);
}
return child;
},
);
}
Future<bool> _checkPermissions(BuildContext context) async {
final service = context.read<EntityPermissionService>();
switch (mode) {
case PermissionCheckMode.all:
for (final permission in permissions) {
if (!await service.hasPermission(permission)) {
return false;
}
}
return true;
case PermissionCheckMode.any:
for (final permission in permissions) {
if (await service.hasPermission(permission)) {
return true;
}
}
return false;
}
}
}Entity Action Guards
Protect entity actions:
class GuardedEntityAction<T extends EntityBase> extends EntityAction<T> {
final Permission permission;
GuardedEntityAction({
required String label,
required IconData icon,
required Future<void> Function(BuildContext, List<T>) onTap,
required this.permission,
}) : super(
label: label,
icon: icon,
onTap: (context, entities) async {
final hasPermission = await context
.read<EntityPermissionService>()
.hasPermission(permission);
if (!hasPermission) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Permission denied: ${permission.actions.join(", ")}'),
backgroundColor: Theme.of(context).colorScheme.error,
),
);
return;
}
await onTap(context, entities);
},
);
}Permission Service
EntityPermissionService
Core service for permission management:
class EntityPermissionService {
final UserPermissionsRepository repository;
final String? userId;
final Set<Permission> _cachedPermissions = {};
DateTime? _cacheTimestamp;
EntityPermissionService({
required this.repository,
this.userId,
});
Future<bool> hasPermission(Permission required) async {
final permissions = await _getPermissions();
return permissions.any((permission) =>
_permissionMatches(permission, required)
);
}
Future<bool> hasAllPermissions(List<Permission> required) async {
for (final permission in required) {
if (!await hasPermission(permission)) {
return false;
}
}
return true;
}
Future<bool> hasAnyPermission(List<Permission> required) async {
for (final permission in required) {
if (await hasPermission(permission)) {
return true;
}
}
return false;
}
Future<bool> canPerformAction(
String resource,
PermissionAction action, [
String? resourceId,
]) async {
final permission = Permission(
id: '',
resource: resource,
actions: [action],
resourceId: resourceId,
);
return hasPermission(permission);
}
bool _permissionMatches(Permission granted, Permission required) {
// Check resource match
if (granted.resource != required.resource &&
granted.resource != '*') {
return false;
}
// Check resource ID if specified
if (required.resourceId != null) {
if (granted.resourceId != null &&
granted.resourceId != required.resourceId &&
granted.resourceId != '*') {
return false;
}
}
// Check if permission is expired
if (granted.expiresAt != null &&
granted.expiresAt!.isBefore(DateTime.now())) {
return false;
}
// Check actions
for (final action in required.actions) {
if (!granted.actions.contains(action) &&
!granted.actions.contains(PermissionAction.manage)) {
return false;
}
}
// Check conditions if any
if (granted.conditions != null) {
// Evaluate conditions
return _evaluateConditions(granted.conditions!, required);
}
return true;
}
Future<Set<Permission>> _getPermissions() async {
// Check cache
if (_cacheTimestamp != null &&
DateTime.now().difference(_cacheTimestamp!) <
const Duration(minutes: 5)) {
return _cachedPermissions;
}
// Load from repository
final permissions = await repository.getUserPermissions(
userId ?? await _getCurrentUserId(),
);
_cachedPermissions.clear();
_cachedPermissions.addAll(permissions);
_cacheTimestamp = DateTime.now();
return _cachedPermissions;
}
void clearCache() {
_cachedPermissions.clear();
_cacheTimestamp = null;
}
}Permission Repository
abstract class UserPermissionsRepository {
Future<Set<Permission>> getUserPermissions(String userId);
Future<void> grantPermission(String userId, Permission permission);
Future<void> revokePermission(String userId, String permissionId);
Future<void> updatePermission(String userId, Permission permission);
}
class ApiPermissionsRepository implements UserPermissionsRepository {
final SharedApiClient client;
ApiPermissionsRepository({required this.client});
@override
Future<Set<Permission>> getUserPermissions(String userId) async {
final response = await client.get('/api/v1/users/$userId/permissions');
final List<dynamic> data = response.data['permissions'];
return data.map((json) => Permission.fromJson(json)).toSet();
}
@override
Future<void> grantPermission(String userId, Permission permission) async {
await client.post(
'/api/v1/users/$userId/permissions',
data: permission.toJson(),
);
}
@override
Future<void> revokePermission(String userId, String permissionId) async {
await client.delete('/api/v1/users/$userId/permissions/$permissionId');
}
@override
Future<void> updatePermission(String userId, Permission permission) async {
await client.put(
'/api/v1/users/$userId/permissions/${permission.id}',
data: permission.toJson(),
);
}
}Entity-Level Security
Securing Entity Operations
class SecureEntityApi<T extends EntityBase> extends EntityApi<T> {
final EntityPermissionService permissionService;
SecureEntityApi({
required super.client,
required this.permissionService,
});
@override
Future<List<T>> performList({
int? offset,
int? limit,
String? sortBy,
String? sortOrder,
String? search,
}) async {
// Check list permission
if (!await permissionService.canPerformAction(
T.toString().toLowerCase(),
PermissionAction.list,
)) {
throw PermissionDeniedException('Cannot list ${T.toString()}');
}
// Get base list
final entities = await super.performList(
offset: offset,
limit: limit,
sortBy: sortBy,
sortOrder: sortOrder,
search: search,
);
// Filter based on read permissions
final filtered = <T>[];
for (final entity in entities) {
if (await permissionService.canPerformAction(
T.toString().toLowerCase(),
PermissionAction.read,
entity.id,
)) {
filtered.add(entity);
}
}
return filtered;
}
@override
Future<T?> performGetById(String id) async {
if (!await permissionService.canPerformAction(
T.toString().toLowerCase(),
PermissionAction.read,
id,
)) {
throw PermissionDeniedException('Cannot read ${T.toString()} $id');
}
return super.performGetById(id);
}
@override
Future<T> performCreate(T entity) async {
if (!await permissionService.canPerformAction(
T.toString().toLowerCase(),
PermissionAction.create,
)) {
throw PermissionDeniedException('Cannot create ${T.toString()}');
}
return super.performCreate(entity);
}
@override
Future<T> performUpdate(String id, T entity) async {
if (!await permissionService.canPerformAction(
T.toString().toLowerCase(),
PermissionAction.update,
id,
)) {
throw PermissionDeniedException('Cannot update ${T.toString()} $id');
}
return super.performUpdate(id, entity);
}
@override
Future<void> performDelete(String id) async {
if (!await permissionService.canPerformAction(
T.toString().toLowerCase(),
PermissionAction.delete,
id,
)) {
throw PermissionDeniedException('Cannot delete ${T.toString()} $id');
}
return super.performDelete(id);
}
}Field-Level Security
class SecureEntityLayout<T extends EntityBase> extends EntityLayout<T> {
final Map<String, Permission> fieldPermissions;
@override
Widget build(BuildContext context, T entity) {
return Column(
children: [
// Always visible fields
Text('ID: ${entity.id}'),
// Permission-protected fields
PermissionGuard(
permissions: [fieldPermissions['email']!],
hideOnDenied: true,
child: Text('Email: ${entity.email}'),
),
PermissionGuard(
permissions: [fieldPermissions['salary']!],
fallback: const Text('Salary: [Hidden]'),
child: Text('Salary: \$${entity.salary}'),
),
],
);
}
}Role-Based Access Control (RBAC)
Role Definition
class Role {
final String id;
final String name;
final String description;
final Set<Permission> permissions;
final int priority; // For role hierarchy
const Role({
required this.id,
required this.name,
required this.description,
required this.permissions,
this.priority = 0,
});
// Predefined roles
static final admin = Role(
id: 'admin',
name: 'Administrator',
description: 'Full system access',
permissions: {
Permission(
id: 'admin-all',
resource: '*',
actions: PermissionAction.values,
),
},
priority: 100,
);
static final manager = Role(
id: 'manager',
name: 'Manager',
description: 'Manage users and content',
permissions: {
Permission.full('users'),
Permission.full('content'),
Permission.read('settings'),
},
priority: 50,
);
static final user = Role(
id: 'user',
name: 'User',
description: 'Basic user access',
permissions: {
Permission.read('content'),
Permission(
id: 'user-profile',
resource: 'users',
actions: [PermissionAction.read, PermissionAction.update],
// Can only access own profile
conditions: {'ownProfile': true},
),
},
priority: 10,
);
}Role-Based Permission Service
class RoleBasedPermissionService extends EntityPermissionService {
final RoleRepository roleRepository;
RoleBasedPermissionService({
required super.repository,
required this.roleRepository,
super.userId,
});
@override
Future<Set<Permission>> _getPermissions() async {
// Get direct permissions
final directPermissions = await super._getPermissions();
// Get role-based permissions
final userRoles = await roleRepository.getUserRoles(
userId ?? await _getCurrentUserId(),
);
final rolePermissions = <Permission>{};
for (final role in userRoles) {
rolePermissions.addAll(role.permissions);
}
// Combine permissions
return {...directPermissions, ...rolePermissions};
}
Future<bool> hasRole(String roleId) async {
final userRoles = await roleRepository.getUserRoles(
userId ?? await _getCurrentUserId(),
);
return userRoles.any((role) => role.id == roleId);
}
Future<bool> hasAnyRole(List<String> roleIds) async {
final userRoles = await roleRepository.getUserRoles(
userId ?? await _getCurrentUserId(),
);
return userRoles.any((role) => roleIds.contains(role.id));
}
}Dynamic Permissions
Attribute-Based Access Control (ABAC)
class AttributeBasedPermission extends Permission {
final Map<String, dynamic> attributes;
final String Function(Map<String, dynamic>) rule;
AttributeBasedPermission({
required super.id,
required super.resource,
required super.actions,
required this.attributes,
required this.rule,
});
bool evaluate(Map<String, dynamic> context) {
final combinedContext = {...attributes, ...context};
try {
// Evaluate rule with context
final result = rule(combinedContext);
return result == 'allow';
} catch (e) {
// Deny on error
return false;
}
}
}
// Example usage
final permission = AttributeBasedPermission(
id: 'location-based',
resource: 'equipment',
actions: [PermissionAction.update],
attributes: {
'allowedLocations': ['lab-1', 'lab-2'],
},
rule: (context) {
final userLocation = context['userLocation'];
final allowedLocations = context['allowedLocations'] as List;
return allowedLocations.contains(userLocation) ? 'allow' : 'deny';
},
);Time-Based Permissions
class TimeBasedPermission extends Permission {
final TimeOfDay? startTime;
final TimeOfDay? endTime;
final List<int>? allowedDays; // 1-7 (Monday-Sunday)
TimeBasedPermission({
required super.id,
required super.resource,
required super.actions,
this.startTime,
this.endTime,
this.allowedDays,
super.expiresAt,
});
bool isValidNow() {
final now = DateTime.now();
// Check expiration
if (expiresAt != null && now.isAfter(expiresAt!)) {
return false;
}
// Check day of week
if (allowedDays != null && !allowedDays!.contains(now.weekday)) {
return false;
}
// Check time of day
if (startTime != null || endTime != null) {
final currentTime = TimeOfDay.now();
if (startTime != null && _timeToMinutes(currentTime) < _timeToMinutes(startTime!)) {
return false;
}
if (endTime != null && _timeToMinutes(currentTime) > _timeToMinutes(endTime!)) {
return false;
}
}
return true;
}
int _timeToMinutes(TimeOfDay time) => time.hour * 60 + time.minute;
}Permission UI Components
Permission Manager
class PermissionManagerWidget extends StatefulWidget {
final String userId;
@override
State<PermissionManagerWidget> createState() => _PermissionManagerWidgetState();
}
class _PermissionManagerWidgetState extends State<PermissionManagerWidget> {
final Map<String, Set<PermissionAction>> _permissions = {};
@override
Widget build(BuildContext context) {
return Column(
children: [
// Entity type selector
DropdownButton<String>(
hint: const Text('Select Entity Type'),
items: _getEntityTypes().map((type) => DropdownMenuItem(
value: type,
child: Text(type),
)).toList(),
onChanged: (type) {
if (type != null) {
setState(() {
_permissions[type] ??= {};
});
}
},
),
// Permission grid
Expanded(
child: ListView(
children: _permissions.entries.map((entry) {
return ExpansionTile(
title: Text(entry.key),
children: PermissionAction.values.map((action) {
return CheckboxListTile(
title: Text(action.name),
value: entry.value.contains(action),
onChanged: (checked) {
setState(() {
if (checked ?? false) {
entry.value.add(action);
} else {
entry.value.remove(action);
}
});
},
);
}).toList(),
);
}).toList(),
),
),
// Save button
ElevatedButton(
onPressed: _savePermissions,
child: const Text('Save Permissions'),
),
],
);
}
Future<void> _savePermissions() async {
final permissions = <Permission>[];
_permissions.forEach((resource, actions) {
if (actions.isNotEmpty) {
permissions.add(Permission(
id: const Uuid().v4(),
resource: resource,
actions: actions.toList(),
));
}
});
// Save to repository
final repository = context.read<UserPermissionsRepository>();
for (final permission in permissions) {
await repository.grantPermission(widget.userId, permission);
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Permissions saved')),
);
}
}
}Permission Audit Trail
class PermissionAudit {
final String id;
final String userId;
final String action; // granted, revoked, updated
final Permission permission;
final String performedBy;
final DateTime timestamp;
final String? reason;
const PermissionAudit({
required this.id,
required this.userId,
required this.action,
required this.permission,
required this.performedBy,
required this.timestamp,
this.reason,
});
}
class PermissionAuditService {
final AuditRepository repository;
Future<void> logPermissionChange({
required String userId,
required String action,
required Permission permission,
required String performedBy,
String? reason,
}) async {
final audit = PermissionAudit(
id: const Uuid().v4(),
userId: userId,
action: action,
permission: permission,
performedBy: performedBy,
timestamp: DateTime.now(),
reason: reason,
);
await repository.save(audit);
}
Future<List<PermissionAudit>> getAuditTrail(String userId) async {
return repository.getByUserId(userId);
}
}Security Best Practices
1. Principle of Least Privilege
// Bad: Granting broad permissions
Permission.full('*') // Never do this
// Good: Grant specific permissions
Permission(
id: 'user-read-own',
resource: 'users',
actions: [PermissionAction.read],
conditions: {'ownProfile': true},
)2. Regular Permission Reviews
class PermissionReviewService {
Future<List<PermissionReviewItem>> reviewUserPermissions(
String userId,
) async {
final permissions = await repository.getUserPermissions(userId);
final reviews = <PermissionReviewItem>[];
for (final permission in permissions) {
// Check for overly broad permissions
if (permission.resource == '*') {
reviews.add(PermissionReviewItem(
permission: permission,
severity: ReviewSeverity.high,
message: 'Global permission detected',
));
}
// Check for expired permissions
if (permission.expiresAt != null &&
permission.expiresAt!.isBefore(DateTime.now())) {
reviews.add(PermissionReviewItem(
permission: permission,
severity: ReviewSeverity.medium,
message: 'Expired permission',
));
}
// Check for unused permissions
final lastUsed = await getLastUsedDate(permission);
if (lastUsed != null &&
DateTime.now().difference(lastUsed).inDays > 90) {
reviews.add(PermissionReviewItem(
permission: permission,
severity: ReviewSeverity.low,
message: 'Permission not used in 90 days',
));
}
}
return reviews;
}
}3. Permission Caching
class CachedPermissionService extends EntityPermissionService {
static const _cacheKey = 'permissions_cache';
static const _cacheDuration = Duration(minutes: 5);
@override
Future<Set<Permission>> _getPermissions() async {
// Check local cache first
final cached = await _loadFromCache();
if (cached != null) return cached;
// Load from repository
final permissions = await super._getPermissions();
// Cache the result
await _saveToCache(permissions);
return permissions;
}
Future<Set<Permission>?> _loadFromCache() async {
final prefs = await SharedPreferences.getInstance();
final data = prefs.getString(_cacheKey);
if (data == null) return null;
final cached = jsonDecode(data);
final timestamp = DateTime.parse(cached['timestamp']);
if (DateTime.now().difference(timestamp) > _cacheDuration) {
return null;
}
final List<dynamic> permissionData = cached['permissions'];
return permissionData
.map((json) => Permission.fromJson(json))
.toSet();
}
}Testing Permissions
class MockPermissionService extends EntityPermissionService {
final Set<Permission> mockPermissions;
MockPermissionService({required this.mockPermissions})
: super(
repository: MockPermissionRepository(),
userId: 'test-user',
);
@override
Future<Set<Permission>> _getPermissions() async {
return mockPermissions;
}
}
// Usage in tests
testWidgets('shows create button with permission', (tester) async {
await tester.pumpWidget(
Provider<EntityPermissionService>.value(
value: MockPermissionService(
mockPermissions: {Permission.create('users')},
),
child: const UserListView(),
),
);
expect(find.text('Add User'), findsOneWidget);
});
testWidgets('hides create button without permission', (tester) async {
await tester.pumpWidget(
Provider<EntityPermissionService>.value(
value: MockPermissionService(
mockPermissions: {Permission.read('users')},
),
child: const UserListView(),
),
);
expect(find.text('Add User'), findsNothing);
});Best Practices
- Always check permissions on both client and server
- Use specific permissions rather than wildcards
- Implement permission caching for performance
- Log all permission changes for audit trails
- Review permissions regularly
- Test permission logic thoroughly
- Provide clear error messages for permission denials
Next: Command Palette - Global search and navigation