Vyuh CDX

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:

  1. Permission Model - Defines permissions and their scope
  2. Permission Guards - UI components that enforce permissions
  3. Permission Service - Core service for permission checks
  4. Permission Repository - Storage and retrieval of permissions
  5. 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

  1. Always check permissions on both client and server
  2. Use specific permissions rather than wildcards
  3. Implement permission caching for performance
  4. Log all permission changes for audit trails
  5. Review permissions regularly
  6. Test permission logic thoroughly
  7. Provide clear error messages for permission denials

Next: Command Palette - Global search and navigation