import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; import 'package:pshared/api/requests/change_role.dart'; import 'package:pshared/models/permissions/access.dart'; import 'package:pshared/models/permissions/action.dart' as perm; import 'package:pshared/models/permissions/data/permission.dart'; import 'package:pshared/models/permissions/data/policy.dart'; import 'package:pshared/models/permissions/data/role.dart'; import 'package:pshared/models/permissions/descriptions/policy.dart'; import 'package:pshared/models/permissions/descriptions/role.dart'; import 'package:pshared/models/permissions/effect.dart'; import 'package:pshared/models/resources.dart'; import 'package:pshared/provider/organizations.dart'; import 'package:pshared/provider/resource.dart'; import 'package:pshared/service/permissions.dart'; class PermissionsProvider extends ChangeNotifier { Resource _userAccess = Resource(data: null, isLoading: false, error: null); late OrganizationsProvider _organizations; bool _isLoaded = false; bool _errorHandled = false; void update(OrganizationsProvider venue) { _organizations = venue; } // Generic wrapper to perform service calls and reload state Future _performServiceCall(Future Function() operation) async { try { await operation(); return await load(); } catch (e) { _userAccess = _userAccess.copyWith( error: e is Exception ? e : Exception(e.toString()), isLoading: false, ); notifyListeners(); return _userAccess.data; } } /// Load the [UserAccess] for the current venue. Future load() async { _userAccess = _userAccess.copyWith(isLoading: true, error: null); _errorHandled = false; notifyListeners(); try { final orgRef = _organizations.current.id; final access = await PermissionsService.load(orgRef); _userAccess = _userAccess.copyWith(data: access, isLoading: false); if (canRead(ResourceType.roles)) { final allAccess = await PermissionsService.loadAll(orgRef); _userAccess = _userAccess.copyWith(data: allAccess, isLoading: false); } _isLoaded = true; } catch (e) { _userAccess = _userAccess.copyWith( error: e is Exception ? e : Exception(e.toString()), isLoading: false, ); } notifyListeners(); return _userAccess.data; } bool get hasUnhandledError => error != null && !_errorHandled; void markErrorHandled() { _errorHandled = true; } Future changeRole(String accountRef, String newRoleDescRef) async { final currentRole = roles.firstWhereOrNull((r) => r.accountRef == accountRef); final currentDesc = currentRole != null ? roleDescriptions.firstWhereOrNull((d) => d.storable.id == currentRole.descriptionRef) : null; if (currentRole == null || currentDesc == null || currentDesc.storable.id == newRoleDescRef) { return _userAccess.data; } return _performServiceCall(() => PermissionsService.changeRole( _organizations.current.id, ChangeRole(accountRef: accountRef, newRoleDescriptionRef: newRoleDescRef), )); } Future deleteRoleDescription(String descRef) { return _performServiceCall(() => PermissionsService.deleteRoleDescription(descRef)); } Future createPermissions(List policies) { return _performServiceCall(() => PermissionsService.createPolicies(policies)); } Future deletePermissions(List policies) { return _performServiceCall(() => PermissionsService.deletePolicies(policies)); } Future changePermissions(List add, List remove) { return _performServiceCall(() => PermissionsService.changePolicies(add, remove)); } // -- Data getters -- Set extractResourceTypes(Iterable descriptions) => descriptions.expand((policy) => policy.resourceTypes ?? []).toSet(); Set get resources => Set.unmodifiable(extractResourceTypes(policyDescriptions)); Set getRoleResources(String roleDescRef) => Set.unmodifiable( extractResourceTypes( getRolePermissions(roleDescRef) .map((p) => getPolicyDescription(p.policy.descriptionRef)) .whereType(), ), ); String? getPolicyDescriptionRef(ResourceType resource) => policyDescriptions.firstWhereOrNull((p) => p.resourceTypes?.contains(resource) ?? false)?.storable.id; List get policyDescriptions => List.unmodifiable(_userAccess.data?.descriptions.policies ?? []); List get roleDescriptions => List.unmodifiable(_userAccess.data?.descriptions.roles ?? []); List get permissions => List.unmodifiable(_userAccess.data?.permissions.permissions ?? []); List get policies => List.unmodifiable(_userAccess.data?.permissions.policies ?? []); List get roles => List.unmodifiable(_userAccess.data?.permissions.roles ?? []); Role? getRole(String accountRef) => roles.firstWhereOrNull((r) => r.accountRef == accountRef); RoleDescription? getRoleDescription(String descRef) => roleDescriptions.firstWhereOrNull((d) => d.storable.id == descRef); List getRoles(String accountRef) => roles.where((r) => r.accountRef == accountRef).toList(); List getRolePolicies(String roleRef) => policies.where((p) => p.roleDescriptionRef == roleRef).toList(); List getRolePermissions(String descRef) => permissions.where((p) => p.policy.roleDescriptionRef == descRef).toList(); PolicyDescription? getPolicyDescription(String policyRef) => policyDescriptions.firstWhereOrNull((p) => p.storable.id == policyRef); // -- Permission checks -- bool get isLoading => _userAccess.isLoading; bool get isReady => !_userAccess.isLoading && error == null && _isLoaded; Exception? get error => _userAccess.error; bool _hasMatchingPermission( PolicyDescription pd, Effect effect, perm.Action? action, { Object? objectRef, }) => permissions.firstWhereOrNull( (p) => p.policy.descriptionRef == pd.storable.id && p.policy.effect.effect == effect && (action == null || p.policy.effect.action == action) && (p.policy.objectRef == null || p.policy.objectRef == objectRef), ) != null; bool canAccessResource( ResourceType resource, { perm.Action? action, Object? objectRef, }) { final orgId = _organizations.current.id; final pd = policyDescriptions.firstWhereOrNull( (policy) => (policy.resourceTypes?.contains(resource) ?? false) && (policy.organizationRef == null || policy.organizationRef == orgId), ); if (pd == null) return false; if (_hasMatchingPermission(pd, Effect.deny, action, objectRef: objectRef)) return false; return _hasMatchingPermission(pd, Effect.allow, action, objectRef: objectRef); } void reset() { _userAccess = Resource(data: null, isLoading: false, error: null); _isLoaded = false; notifyListeners(); } bool canRead(ResourceType r, {Object? objectRef}) => canAccessResource(r, action: perm.Action.read, objectRef: objectRef); bool canUpdate(ResourceType r, {Object? objectRef}) => canAccessResource(r, action: perm.Action.update, objectRef: objectRef); bool canDelete(ResourceType r, {Object? objectRef}) => canAccessResource(r, action: perm.Action.delete, objectRef: objectRef); bool canCreate(ResourceType r) => canAccessResource(r, action: perm.Action.create); }