Skip to content

Commit e8039b5

Browse files
feat(policy): add PolicyProvider and PolicyWidget for access control management
1 parent 9122d0f commit e8039b5

File tree

4 files changed

+200
-0
lines changed

4 files changed

+200
-0
lines changed

lib/flutter_policy_engine.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
library flutter_policy_engine;
22

33
export 'src/core/policy_manager.dart';
4+
export 'src/widgets/policy_widget.dart';
5+
export 'src/core/policy_provider.dart';

lib/src/core/policy_manager.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,4 +185,24 @@ class PolicyManager extends ChangeNotifier {
185185
rethrow;
186186
}
187187
}
188+
189+
/// Checks if the specified [role] has access to the given [content].
190+
///
191+
/// Returns `true` if the policy manager is initialized and the evaluator
192+
/// determines that the [role] is permitted to access the [content].
193+
/// Returns `false` if the policy manager is not initialized, the evaluator
194+
/// is not set, or if access is denied.
195+
///
196+
/// Logs an error if called before initialization or if the evaluator is missing.
197+
bool hasAccess(String role, String content) {
198+
if (!_isInitialized || _evaluator == null) {
199+
LogHandler.error(
200+
'Policy manager not initialized or evaluator not set',
201+
context: {'role': role, 'content': content},
202+
operation: 'policy_manager_access_check',
203+
);
204+
return false;
205+
}
206+
return _evaluator!.evaluate(role, content);
207+
}
188208
}

lib/src/core/policy_provider.dart

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import 'package:flutter/widgets.dart';
2+
import 'package:flutter_policy_engine/src/core/policy_manager.dart';
3+
4+
/// A widget that provides a [PolicyManager] instance to its descendant widgets.
5+
///
6+
/// This widget uses Flutter's [InheritedWidget] pattern to make the [PolicyManager]
7+
/// available throughout the widget tree without having to pass it explicitly
8+
/// through constructors.
9+
///
10+
/// Example usage:
11+
/// ```dart
12+
/// PolicyProvider(
13+
/// policyManager: myPolicyManager,
14+
/// child: MyApp(),
15+
/// )
16+
/// ```
17+
///
18+
/// To access the [PolicyManager] in descendant widgets:
19+
/// ```dart
20+
/// final policyManager = PolicyProvider.policyManagerOf(context);
21+
/// ```
22+
class PolicyProvider extends InheritedWidget {
23+
/// Creates a [PolicyProvider] widget.
24+
///
25+
/// The [policyManager] parameter is required and will be made available
26+
/// to all descendant widgets in the widget tree.
27+
///
28+
/// The [child] parameter is required and represents the widget subtree
29+
/// that will have access to the provided [PolicyManager].
30+
const PolicyProvider({
31+
required this.policyManager,
32+
required super.child,
33+
super.key,
34+
});
35+
36+
/// The [PolicyManager] instance that will be provided to descendant widgets.
37+
final PolicyManager policyManager;
38+
39+
/// Returns the nearest [PolicyProvider] widget in the widget tree.
40+
///
41+
/// This method uses [BuildContext.dependOnInheritedWidgetOfExactType] to
42+
/// establish a dependency on the [PolicyProvider] widget, which means
43+
/// the calling widget will rebuild when the [PolicyProvider] changes.
44+
///
45+
/// Returns `null` if no [PolicyProvider] is found in the widget tree.
46+
///
47+
/// Example:
48+
/// ```dart
49+
/// final provider = PolicyProvider.of(context);
50+
/// if (provider != null) {
51+
/// // Use provider.policyManager
52+
/// }
53+
/// ```
54+
static PolicyProvider? of(BuildContext context) {
55+
return context.dependOnInheritedWidgetOfExactType<PolicyProvider>();
56+
}
57+
58+
/// Returns the [PolicyManager] from the nearest [PolicyProvider] widget.
59+
///
60+
/// This is a convenience method that combines finding the [PolicyProvider]
61+
/// and accessing its [policyManager] property. It throws a [StateError]
62+
/// if no [PolicyProvider] is found in the widget tree.
63+
///
64+
/// This method establishes a dependency on the [PolicyProvider] widget,
65+
/// causing the calling widget to rebuild when the provider changes.
66+
///
67+
/// Throws:
68+
/// - [StateError] if no [PolicyProvider] is found in the widget tree.
69+
///
70+
/// Example:
71+
/// ```dart
72+
/// final policyManager = PolicyProvider.policyManagerOf(context);
73+
/// // Use policyManager directly
74+
/// ```
75+
static PolicyManager policyManagerOf(BuildContext context) {
76+
final provider = of(context);
77+
if (provider == null) {
78+
throw StateError('PolicyProvider not found in context');
79+
}
80+
return provider.policyManager;
81+
}
82+
83+
/// Determines whether this widget should notify its dependents of changes.
84+
///
85+
/// Returns `true` if the [policyManager] has changed, indicating that
86+
/// dependent widgets should rebuild. This ensures that widgets using
87+
/// the [PolicyManager] are updated when the manager instance changes.
88+
///
89+
/// The comparison is done by reference equality, so a new [PolicyManager]
90+
/// instance will trigger a rebuild even if it has the same configuration.
91+
@override
92+
bool updateShouldNotify(PolicyProvider oldWidget) {
93+
return policyManager != oldWidget.policyManager;
94+
}
95+
}

lib/src/widgets/policy_widget.dart

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import 'package:flutter/widgets.dart';
2+
import 'package:flutter_policy_engine/src/core/policy_provider.dart';
3+
import 'package:flutter_policy_engine/src/exceptions/i_policy_sdk_exceptions.dart';
4+
5+
/// A widget that conditionally displays its [child] based on policy access control.
6+
///
7+
/// [PolicyWidget] checks if the given [role] has access to the specified [content]
8+
/// using the nearest [PolicyProvider] in the widget tree. If access is granted,
9+
/// [child] is rendered. If access is denied, [fallback] is rendered (or an empty
10+
/// [SizedBox] if [fallback] is null), and [onAccessDenied] is called if provided.
11+
///
12+
/// If a [IPolicySDKException] is thrown during access evaluation, an assertion
13+
/// error is thrown in debug mode; in release mode, access is denied silently.
14+
///
15+
/// Example usage:
16+
/// ```dart
17+
/// PolicyWidget(
18+
/// role: 'admin',
19+
/// content: 'dashboard',
20+
/// child: DashboardWidget(),
21+
/// fallback: AccessDeniedWidget(),
22+
/// onAccessDenied: () => log('Access denied'),
23+
/// )
24+
/// ```
25+
class PolicyWidget extends StatelessWidget {
26+
/// Creates a [PolicyWidget].
27+
///
28+
/// [role] is the user or entity role to check.
29+
/// [content] is the resource or content identifier to check access for.
30+
/// [child] is the widget to display if access is granted.
31+
/// [fallback] is the widget to display if access is denied (optional).
32+
/// [onAccessDenied] is a callback invoked when access is denied (optional).
33+
const PolicyWidget({
34+
required this.role,
35+
required this.content,
36+
required this.child,
37+
this.fallback,
38+
this.onAccessDenied,
39+
super.key,
40+
});
41+
42+
/// The role to check for access.
43+
final String role;
44+
45+
/// The content or resource identifier to check access for.
46+
final String content;
47+
48+
/// The widget to display if access is granted.
49+
final Widget child;
50+
51+
/// The widget to display if access is denied. If null, an empty [SizedBox] is shown.
52+
final Widget? fallback;
53+
54+
/// Callback invoked when access is denied.
55+
final VoidCallback? onAccessDenied;
56+
57+
@override
58+
Widget build(BuildContext context) {
59+
final policyManager = PolicyProvider.policyManagerOf(context);
60+
61+
try {
62+
final hasAccess = policyManager.hasAccess(role, content);
63+
64+
if (hasAccess) {
65+
return child;
66+
} else {
67+
onAccessDenied?.call();
68+
return fallback ?? const SizedBox.shrink();
69+
}
70+
} catch (e) {
71+
if (e is IPolicySDKException) {
72+
assert(() {
73+
throw FlutterError('Error en PolicyWidget: ${e.message}');
74+
}());
75+
76+
// On production, deny access silently
77+
onAccessDenied?.call();
78+
return fallback ?? const SizedBox.shrink();
79+
}
80+
rethrow;
81+
}
82+
}
83+
}

0 commit comments

Comments
 (0)