Files
sendico/frontend/pshared/lib/provider/permissions.dart
2025-12-11 17:41:25 +03:00

183 lines
7.4 KiB
Dart

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> _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<UserAccess?> _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<UserAccess?> 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<UserAccess?> 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<UserAccess?> deleteRoleDescription(String descRef) {
return _performServiceCall(() => PermissionsService.deleteRoleDescription(descRef));
}
Future<UserAccess?> createPermissions(List<Policy> policies) {
return _performServiceCall(() => PermissionsService.createPolicies(policies));
}
Future<UserAccess?> deletePermissions(List<Policy> policies) {
return _performServiceCall(() => PermissionsService.deletePolicies(policies));
}
Future<UserAccess?> changePermissions(List<Policy> add, List<Policy> remove) {
return _performServiceCall(() => PermissionsService.changePolicies(add, remove));
}
// -- Data getters --
Set<ResourceType> extractResourceTypes(Iterable<PolicyDescription> descriptions) => descriptions.expand((policy) => policy.resourceTypes ?? <ResourceType>[]).toSet();
Set<ResourceType> get resources => Set.unmodifiable(extractResourceTypes(policyDescriptions));
Set<ResourceType> getRoleResources(String roleDescRef) => Set.unmodifiable(
extractResourceTypes(
getRolePermissions(roleDescRef)
.map((p) => getPolicyDescription(p.policy.descriptionRef))
.whereType<PolicyDescription>(),
),
);
String? getPolicyDescriptionRef(ResourceType resource) => policyDescriptions.firstWhereOrNull((p) => p.resourceTypes?.contains(resource) ?? false)?.storable.id;
List<PolicyDescription> get policyDescriptions => List.unmodifiable(_userAccess.data?.descriptions.policies ?? []);
List<RoleDescription> get roleDescriptions => List.unmodifiable(_userAccess.data?.descriptions.roles ?? []);
List<Permission> get permissions => List.unmodifiable(_userAccess.data?.permissions.permissions ?? []);
List<Policy> get policies => List.unmodifiable(_userAccess.data?.permissions.policies ?? []);
List<Role> 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<Role> getRoles(String accountRef) => roles.where((r) => r.accountRef == accountRef).toList();
List<Policy> getRolePolicies(String roleRef) => policies.where((p) => p.roleDescriptionRef == roleRef).toList();
List<Permission> 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);
}