Files
sendico/frontend/pshared/lib/provider/permissions.dart
Arseni a2c05745ad A
2025-12-16 18:21:49 +03:00

195 lines
7.9 KiB
Dart

import 'dart:async';
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;
String? _loadedOrgRef;
//For permissions to auto-load when an organization is set, so the dashboard no longer hangs waiting for permissions to become ready.
void update(OrganizationsProvider venue) {
_organizations = venue;
// Trigger a reload when organization changes or when permissions were never loaded.
if (_organizations.isOrganizationSet &&
_loadedOrgRef != _organizations.current.id &&
!_userAccess.isLoading) {
unawaited(load());
}
}
// 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 {
if (!_organizations.isOrganizationSet) {
// Organization is not ready yet; skip loading until it becomes available.
return _userAccess.data;
}
_userAccess = _userAccess.copyWith(isLoading: true, error: null);
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;
_loadedOrgRef = orgRef;
} catch (e) {
_userAccess = _userAccess.copyWith(
error: e is Exception ? e : Exception(e.toString()),
isLoading: false,
);
}
notifyListeners();
return _userAccess.data;
}
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;
_loadedOrgRef = null;
notifyListeners();
}
Future<void> resetAsync() async {
reset();
}
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);
}