diff --git a/README.md b/README.md index 25d10f6..3edb64f 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,158 @@ -# flutter_policy_engine +# Flutter Policy Engine -A lightweight, extensible policy engine for Flutter. Define, manage, and evaluate access control rules declaratively using ABAC (Attribute-Based Access Control) or RBAC (Role-Based Access Control) models. +[![Pub Version](https://img.shields.io/pub/v/flutter_policy_engine)](https://pub.dev/packages/flutter_policy_engine) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Flutter](https://img.shields.io/badge/Flutter-3.4.1+-blue.svg)](https://flutter.dev) -## Features +A lightweight, extensible policy engine for Flutter applications. Define, manage, and evaluate access control rules declaratively using **ABAC** (Attribute-Based Access Control) or **RBAC** (Role-Based Access Control) models with a clean, intuitive API. -- Attribute-Based and Role-Based Access Control (ABAC/RBAC) -- Declarative rule definitions -- Extensible and modular design -- Easy integration with Flutter apps +## โœจ Features -## Installation +- **๐Ÿ” Dual Access Control Models**: Support for both Role-Based (RBAC) and Attribute-Based (ABAC) access control +- **๐ŸŽฏ Declarative Policy Definitions**: Define access rules using simple, readable configurations +- **๐Ÿ—๏ธ Modular Architecture**: Extensible design with clear separation of concerns +- **โšก Lightweight & Fast**: Minimal overhead with efficient policy evaluation +- **๐Ÿ”„ Real-time Updates**: Dynamic policy updates without app restarts +- **๐ŸŽจ Flutter-Native**: Built specifically for Flutter with widget integration +- **๐Ÿ“ฑ Easy Integration**: Simple setup with minimal boilerplate code +- **๐Ÿงช Comprehensive Testing**: Full test coverage with examples -Ensure you have [Flutter](https://docs.flutter.dev/get-started/install) and [Dart](https://dart.dev/get-dart) installed. [FVM](https://fvm.app/) is recommended for managing Flutter versions. +## ๐Ÿš€ Quick Start + +### Installation + +Add the package to your `pubspec.yaml`: + +```yaml +dependencies: + flutter_policy_engine: ^1.0.1 +``` + +Run the installation: ```bash -# Clone the repository and enter the directory -git clone -cd flutter_policy_engine +flutter pub get +``` -# Run the setup script (requires bash, Flutter, Node.js, and npm) -./setup.sh +### Basic Usage + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_policy_engine/flutter_policy_engine.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Initialize policy manager + final policyManager = PolicyManager(); + await policyManager.initialize({ + "admin": ["dashboard", "users", "settings", "reports"], + "manager": ["dashboard", "users", "reports"], + "user": ["dashboard"], + "guest": ["login"] + }); + + runApp(MyApp(policyManager: policyManager)); +} + +class MyApp extends StatelessWidget { + final PolicyManager policyManager; + + const MyApp({super.key, required this.policyManager}); + + @override + Widget build(BuildContext context) { + return PolicyProvider( + policyManager: policyManager, + child: MaterialApp( + title: 'Policy Engine Demo', + home: HomePage(), + ), + ); + } +} + +class HomePage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Dashboard')), + body: Column( + children: [ + // Only show for admin and manager roles + PolicyWidget( + role: "admin", + content: "users", + child: UserManagementCard(), + fallback: AccessDeniedWidget(), + ), + + // Show for all authenticated users + PolicyWidget( + role: "user", + content: "dashboard", + child: DashboardCard(), + ), + ], + ), + ); + } +} ``` -The setup script will: +## ๐Ÿ“š Core Concepts + +### Policy Manager + +The central orchestrator that manages all access control logic: + +```dart +final policyManager = PolicyManager(); + +// Initialize with role definitions +await policyManager.initialize({ + "admin": ["dashboard", "users", "settings"], + "user": ["dashboard"], +}); + +// Check access programmatically +bool hasAccess = policyManager.evaluateAccess("admin", "users"); // true +bool canAccess = policyManager.evaluateAccess("user", "settings"); // false +``` + +### Policy Widget + +Conditionally render content based on user roles: + +```dart +PolicyWidget( + role: "admin", + content: "settings", + child: SettingsPage(), + fallback: AccessDeniedWidget(), +) +``` + +### Role Management + +Create and manage roles dynamically: + +```dart +// Add a new role +await policyManager.addRole("moderator", ["dashboard", "comments"]); -- Check for Flutter and FVM -- Install the Flutter version from `.fvm/fvm_config.json` (if present) -- Install Dart/Flutter dependencies (`pub get`) -- Install Node.js dependencies (if `package.json` exists) -- Initialize Husky for git hooks (if present) -- Add `.fvm/` to `.gitignore` if needed +// Update existing role +await policyManager.updateRole("user", ["dashboard", "profile"]); -## Testing +// Remove a role +await policyManager.removeRole("guest"); +``` + +## ๐Ÿงช Testing ### Local Testing -Run tests with coverage locally: +Run tests with coverage: ```bash # Using the provided script (recommended) @@ -48,20 +165,57 @@ genhtml coverage/lcov.info -o coverage/html open coverage/html/index.html ``` -## Contributing +### Example App + +Explore the interactive example app: + +```bash +cd example +flutter run +``` + +The example includes: + +- Basic policy demonstrations +- Role management interface +- Real-time policy updates +- Access control scenarios + +## ๐Ÿ“š Documentation + +- **[Quick Start Guide](docs/quick-start.mdx)** - Get up and running in minutes +- **[Core Concepts](docs/core-concepts/)** - Deep dive into policy management +- **[Examples](docs/examples/)** - Practical usage examples + +## ๐Ÿค Contributing + +We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details. + +### Development Setup + +```bash +# Clone the repository +git clone https://github.com/aspicas/flutter_policy_engine.git +cd flutter_policy_engine + +# Run the setup script +./setup.sh + +# Run tests +./scripts/test_with_coverage.sh +``` -Contributions are welcome! Please: +### Code Style -- Follow the existing code style and patterns -- Write clear commit messages (Commitlint and Husky are enabled) -- Add or update tests for new features or bug fixes -- Ensure all tests pass before submitting a pull request -- Open a pull request with a clear description +- Follow the existing code patterns and style +- Write clear commit messages (Commitlint enabled) +- Add tests for new features +- Ensure all tests pass before submitting PRs -## License +## ๐Ÿ“„ License MIT ยฉ 2025 David Alejandro Garcia Ruiz --- -> **Tip:** If you use VSCode, restart your terminal after setup to ensure FVM is properly detected. +> **๐Ÿ’ก Tip**: If you use VSCode, restart your terminal after setup to ensure FVM is properly detected. diff --git a/docs.json b/docs.json new file mode 100644 index 0000000..8b6c271 --- /dev/null +++ b/docs.json @@ -0,0 +1,51 @@ +{ + "name": "Flutter Policy Engine", + "description": "A comprehensive policy management and access control system for Flutter applications", + "sidebar": [ + { + "group": "Getting Started", + "pages": [ + { + "title": "Introduction", + "href": "/", + "icon": "rocket" + }, + { + "title": "Quick Start", + "href": "/quick-start", + "icon": "play" + }, + { + "title": "Installation", + "href": "/installation", + "icon": "download" + } + ] + }, + { + "group": "Core Concepts", + "pages": [ + { + "title": "Policy Management", + "href": "/core-concepts/policy-management", + "icon": "shield" + } + ] + }, + { + "group": "Examples", + "pages": [ + { + "title": "Basic Policy Demo", + "href": "/examples/basic-policy-demo", + "icon": "code" + }, + { + "title": "Role Management Demo", + "href": "/examples/role-management-demo", + "icon": "settings" + } + ] + } + ] +} diff --git a/docs/core-concepts/policy-management.mdx b/docs/core-concepts/policy-management.mdx new file mode 100644 index 0000000..36812a0 --- /dev/null +++ b/docs/core-concepts/policy-management.mdx @@ -0,0 +1,210 @@ +--- +title: Policy Management +description: Understanding policy management in the Flutter Policy Engine +--- + +# Policy Management + +Policy management is the core concept of the Flutter Policy Engine. It involves defining, organizing, and enforcing access control policies that determine what content and features users can access based on their roles. + +## ๐ŸŽฏ What is Policy Management? + +Policy management is the systematic approach to: + +- **Defining Access Rules**: Establishing who can access what resources +- **Organizing Permissions**: Structuring permissions in a logical hierarchy +- **Enforcing Policies**: Applying access control rules consistently +- **Managing Changes**: Updating policies as requirements evolve + +## ๐Ÿ—๏ธ Policy Architecture + +The Flutter Policy Engine uses a hierarchical policy architecture: + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Policy Manager โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Role Storage โ”‚ โ”‚ Policy Evaluatorโ”‚ โ”‚ Role Manager โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ + โ–ผ โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Role Model โ”‚ โ”‚ Policy Widget โ”‚ +โ”‚ - Name โ”‚ โ”‚ - Role-based rendering โ”‚ +โ”‚ - Permissions โ”‚ โ”‚ - Access control โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +## ๐Ÿ”‘ Core Components + +### 1. Policy Manager + +The `PolicyManager` is the central orchestrator that: + +- **Initializes Policies**: Sets up the initial role and permission structure +- **Evaluates Access**: Determines if a role has access to specific content +- **Manages Roles**: Handles role creation, updates, and removal +- **Provides Context**: Makes policy information available to the widget tree + +```dart +final policyManager = PolicyManager(); + +// Initialize with role definitions +await policyManager.initialize({ + "admin": ["LoginPage", "Dashboard", "UserManagement", "Settings"], + "user": ["LoginPage", "Dashboard"], + "guest": ["LoginPage"] +}); +``` + +### 2. Role Model + +A `Role` represents a user category with specific permissions: + +```dart +class Role { + final String name; + final List allowedContent; + + Role({required this.name, required this.allowedContent}); +} +``` + +**Key Properties**: + +- **name**: Unique identifier for the role +- **allowedContent**: List of content types the role can access + +### 3. Policy Evaluator + +The policy evaluator determines access rights: + +```dart +// Check if a role has access to specific content +bool hasAccess = policyManager.evaluateAccess(role, content); +``` + +## ๐Ÿ“‹ Policy Definition + +### Basic Policy Structure + +Policies are defined as a map of roles to their allowed content: + +```dart +final policies = { + "admin": ["LoginPage", "Dashboard", "UserManagement", "Settings"], + "user": ["LoginPage", "Dashboard"], + "guest": ["LoginPage"] +}; +``` + +### Content Types + +Content types represent different features or sections of your app: + +```dart +// Common content types +const contentTypes = [ + "LoginPage", // Authentication pages + "Dashboard", // Main dashboard + "UserManagement", // User administration + "Settings", // Application settings + "Reports", // Analytics and reports + "Billing", // Payment and billing + "Support", // Customer support +]; +``` + +### Role Hierarchy + +Consider implementing a role hierarchy for complex permission systems: + +```dart +// Hierarchical role structure +final roleHierarchy = { + "super_admin": ["admin", "user", "guest"], + "admin": ["user", "guest"], + "user": ["guest"], + "guest": [] +}; + +// Inherited permissions +class HierarchicalRole extends Role { + final List inheritedRoles; + + HierarchicalRole({ + required String name, + required List allowedContent, + required this.inheritedRoles, + }) : super(name: name, allowedContent: allowedContent); +} +``` + +## ๐Ÿ”„ Policy Lifecycle + +### 1. Initialization + +```dart +Future initializePolicies() async { + final policyManager = PolicyManager(); + + // Define initial policies + final policies = { + "admin": ["LoginPage", "Dashboard", "UserManagement", "Settings"], + "user": ["LoginPage", "Dashboard"], + "guest": ["LoginPage"] + }; + + // Initialize the policy manager + await policyManager.initialize(policies); +} +``` + +### 2. Policy Evaluation + +```dart +// Evaluate access for a specific role and content +bool canAccess = policyManager.hasAccess("user", "Dashboard"); +``` + +### 3. Policy Updates + +```dart +// Add a new role +final newRole = Role(name: "moderator", allowedContent: ["LoginPage", "Dashboard"]); +await policyManager.addRole(newRole); + +// Update existing role +final updatedRole = Role(name: "user", allowedContent: ["LoginPage", "Dashboard", "Settings"]); +await policyManager.updateRole(updatedRole); + +// Remove a role +await policyManager.removeRole("guest"); +``` + +## ๐Ÿ”„ Best Practices + +### 1. Policy Design + +- **Keep it Simple**: Start with basic roles and add complexity as needed +- **Use Clear Names**: Use descriptive role and content names +- **Document Policies**: Maintain clear documentation of policy structure +- **Test Thoroughly**: Test all policy combinations + +### 2. Performance + +- **Cache Policies**: Cache frequently accessed policy data +- **Optimize Queries**: Use efficient data structures for policy lookups +- **Monitor Performance**: Track policy evaluation performance + +### 3. Security + +- **Validate Inputs**: Always validate policy data +- **Encrypt Sensitive Data**: Encrypt policy data when appropriate +- **Audit Access**: Log all policy-related operations +- **Regular Reviews**: Regularly review and update policies + +## ๐Ÿ“š Next Steps + +- **[Basic Policy Demo](/examples/basic-policy-demo)**: Simple role-based access control demonstration diff --git a/docs/examples/basic-policy-demo.mdx b/docs/examples/basic-policy-demo.mdx new file mode 100644 index 0000000..f35e607 --- /dev/null +++ b/docs/examples/basic-policy-demo.mdx @@ -0,0 +1,293 @@ +--- +title: Basic Policy Demo +description: Simple role-based access control demonstration +--- + +# Basic Policy Demo + +This example demonstrates the fundamental usage of the Flutter Policy Engine with a simple role-based access control system. The demo shows how to initialize policies, switch between roles, and conditionally render content based on permissions. + +## ๐ŸŽฏ Demo Overview + +The Basic Policy Demo includes: + +- **Role Selection**: Interactive buttons to switch between different user roles +- **Policy Evaluation**: Real-time display of access permissions for different content +- **Visual Feedback**: Color-coded cards showing granted (green) or denied (red) access +- **Dynamic Updates**: UI updates immediately when roles change + +## ๐Ÿ“ฑ Complete Demo Code + +```dart +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:flutter_policy_engine/flutter_policy_engine.dart'; +import 'package:flutter_policy_engine_example/demo_content.dart'; + +class PolicyEngineDemo extends StatefulWidget { + const PolicyEngineDemo({super.key}); + + @override + State createState() { + return _PolicyEngineDemoState(); + } +} + +class _PolicyEngineDemoState extends State { + late PolicyManager policyManager; + bool _isInitialized = false; + + @override + void initState() { + super.initState(); + _initializePolicyManager(); + } + + Future _initializePolicyManager() async { + policyManager = PolicyManager(); + final policies = { + "admin": ["LoginPage", "Dashboard", "UserManagement", "Settings"], + "user": ["LoginPage", "Dashboard"], + "guest": ["LoginPage"] + }; + try { + await policyManager.initialize(policies); + setState(() { + _isInitialized = true; + }); + } catch (e) { + log(e.toString()); + setState(() { + _isInitialized = false; + }); + } + } + + @override + Widget build(BuildContext context) { + if (!_isInitialized) { + return Scaffold( + appBar: AppBar( + title: const Text('Policy Engine Demo'), + ), + body: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Loading policies...'), + ], + ), + ), + ); + } + return PolicyProvider( + policyManager: policyManager, + child: Scaffold( + appBar: AppBar( + title: const Text('Policy Engine Demo'), + ), + body: const DemoContent(), + ), + ); + } +} +``` + +## ๐ŸŽจ Demo Content Widget + +The `DemoContent` widget provides the interactive interface for testing policies: + +```dart +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:flutter_policy_engine/flutter_policy_engine.dart'; + +class DemoContent extends StatefulWidget { + const DemoContent({super.key}); + + @override + State createState() => _DemoContentState(); +} + +class _DemoContentState extends State { + String _currentRole = 'guest'; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Current Role: $_currentRole', + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 16), + + // Role selector + Row( + children: [ + _buildRoleButton('guest'), + const SizedBox(width: 8), + _buildRoleButton('user'), + const SizedBox(width: 8), + _buildRoleButton('admin'), + ], + ), + + const SizedBox(height: 32), + + // Policy examples + _buildPolicyExample('LoginPage', 'Login Page'), + _buildPolicyExample('Dashboard', 'Dashboard'), + _buildPolicyExample('UserManagement', 'User Management'), + _buildPolicyExample('Settings', 'Settings'), + + const SizedBox(height: 32), + ], + ), + ); + } + + Widget _buildRoleButton(String role) { + return ElevatedButton( + onPressed: () { + setState(() { + _currentRole = role; + }); + }, + style: ElevatedButton.styleFrom( + backgroundColor: _currentRole == role ? Colors.blue : Colors.grey, + ), + child: Text(role), + ); + } + + Widget _buildPolicyExample(String content, String displayName) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('$displayName:'), + const SizedBox(height: 4), + PolicyWidget( + role: _currentRole, + content: content, + fallback: Card( + color: Colors.red[100], + child: Padding( + padding: const EdgeInsets.all(16), + child: Text('Access denied for $displayName'), + ), + ), + onAccessDenied: () { + log('Access denied for $_currentRole to $content'); + }, + child: Card( + color: Colors.green[100], + child: Padding( + padding: const EdgeInsets.all(16), + child: Text('Access granted to $displayName'), + ), + ), + ), + ], + ), + ); + } +} +``` + +## ๐Ÿ”ง Key Components Explained + +### 1. Policy Initialization + +```dart +final policies = { + "admin": ["LoginPage", "Dashboard", "UserManagement", "Settings"], + "user": ["LoginPage", "Dashboard"], + "guest": ["LoginPage"] +}; +``` + +This defines three roles with different permission levels: + +- **admin**: Full access to all features +- **user**: Access to login and dashboard +- **guest**: Limited to login page only + +### 2. PolicyProvider Setup + +```dart +PolicyProvider( + policyManager: policyManager, + child: Scaffold( + // Your app content + ), +) +``` + +The `PolicyProvider` makes the policy manager available to all child widgets in the widget tree. + +### 3. PolicyWidget Usage + +```dart +PolicyWidget( + role: _currentRole, + content: "Dashboard", + child: Card( + color: Colors.green[100], + child: Text('Access granted to Dashboard'), + ), + fallback: Card( + color: Colors.red[100], + child: Text('Access denied for Dashboard'), + ), +) +``` + +The `PolicyWidget` conditionally renders content based on the user's role and the requested content type. + +## ๐ŸŽฎ How to Use the Demo + +1. **Launch the Demo**: Run the example app and navigate to "Basic Policy Demo" +2. **Select Roles**: Click on different role buttons (guest, user, admin) +3. **Observe Changes**: Watch how the content cards change color based on permissions +4. **Test Permissions**: Notice which features are accessible for each role + +## ๐Ÿ“Š Expected Behavior + +| Role | Login Page | Dashboard | User Management | Settings | +| ----- | ---------- | --------- | --------------- | -------- | +| Guest | โœ… Green | โŒ Red | โŒ Red | โŒ Red | +| User | โœ… Green | โœ… Green | โŒ Red | โŒ Red | +| Admin | โœ… Green | โœ… Green | โœ… Green | โœ… Green | + +## ๐Ÿ” Key Learning Points + +1. **Role-Based Access**: Different roles have different permission levels +2. **Dynamic Evaluation**: Policies are evaluated in real-time as roles change +3. **Visual Feedback**: Clear visual indicators show access status +4. **Fallback Handling**: Graceful handling when access is denied +5. **Logging**: Access denials are logged for debugging + +## ๐Ÿš€ Running the Demo + +To run this demo: + +1. Navigate to the example directory +2. Run `flutter pub get` +3. Execute `flutter run` +4. Select "Basic Policy Demo" from the main menu + +## ๐Ÿ”„ Next Steps + +After exploring the basic demo: + +- **[Role Management Demo](/examples/role-management-demo)**: Dynamic role management capabilities diff --git a/docs/examples/role-management-demo.mdx b/docs/examples/role-management-demo.mdx new file mode 100644 index 0000000..f84e694 --- /dev/null +++ b/docs/examples/role-management-demo.mdx @@ -0,0 +1,611 @@ +--- +title: Role Management Demo +description: Dynamic role creation, updating, and removal demonstration +--- + +# Role Management Demo + +This advanced example demonstrates the dynamic role management capabilities of the Flutter Policy Engine. The demo shows how to create, update, and remove roles at runtime, providing a comprehensive testing environment for policy management operations. + +## ๐ŸŽฏ Demo Overview + +The Role Management Demo provides: + +- **Dynamic Role Creation**: Add new roles with custom permissions +- **Role Updates**: Modify existing role permissions +- **Role Removal**: Delete roles from the system +- **Real-time Testing**: Test policy changes immediately +- **Interactive UI**: User-friendly forms for role management +- **Status Feedback**: Clear feedback for all operations + +## ๐Ÿ“ฑ Complete Demo Code + +```dart +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:flutter_policy_engine/flutter_policy_engine.dart'; + +class RoleManagementDemo extends StatefulWidget { + const RoleManagementDemo({super.key}); + + @override + State createState() => _RoleManagementDemoState(); +} + +class _RoleManagementDemoState extends State { + late PolicyManager policyManager; + bool _isInitialized = false; + String _selectedRole = 'guest'; + + // Form controllers for role management + final TextEditingController _roleNameController = TextEditingController(); + final TextEditingController _permissionsController = TextEditingController(); + + // Status messages + String _statusMessage = ''; + bool _isSuccess = true; + + @override + void initState() { + super.initState(); + _initializePolicyManager(); + } + + @override + void dispose() { + _roleNameController.dispose(); + _permissionsController.dispose(); + super.dispose(); + } + + Future _initializePolicyManager() async { + policyManager = PolicyManager(); + final policies = { + "admin": ["LoginPage", "Dashboard", "UserManagement", "Settings"], + "user": ["LoginPage", "Dashboard"], + "guest": ["LoginPage"] + }; + try { + await policyManager.initialize(policies); + setState(() { + _isInitialized = true; + _selectedRole = 'guest'; + }); + } catch (e) { + log(e.toString()); + setState(() { + _isInitialized = false; + _statusMessage = 'Failed to initialize: $e'; + _isSuccess = false; + }); + } + } + + void _showStatus(String message, bool isSuccess) { + setState(() { + _statusMessage = message; + _isSuccess = isSuccess; + }); + + // Clear status after 3 seconds + Future.delayed(const Duration(seconds: 3), () { + if (mounted) { + setState(() { + _statusMessage = ''; + }); + } + }); + } + + Future _addRole() async { + final roleName = _roleNameController.text.trim(); + final permissionsText = _permissionsController.text.trim(); + + if (roleName.isEmpty) { + _showStatus('Role name cannot be empty', false); + return; + } + + try { + final permissions = permissionsText.isEmpty + ? [] + : permissionsText.split(',').map((e) => e.trim()).toList(); + + final role = Role(name: roleName, allowedContent: permissions); + await policyManager.addRole(role); + + _showStatus('Role "$roleName" added successfully', true); + _roleNameController.clear(); + _permissionsController.clear(); + + // Update selected role if it's the new one + setState(() { + _selectedRole = roleName; + }); + } catch (e) { + _showStatus('Failed to add role: $e', false); + } + } + + Future _updateRole() async { + final roleName = _roleNameController.text.trim(); + final permissionsText = _permissionsController.text.trim(); + + if (roleName.isEmpty) { + _showStatus('Role name cannot be empty', false); + return; + } + + try { + final permissions = permissionsText.isEmpty + ? [] + : permissionsText.split(',').map((e) => e.trim()).toList(); + + final role = Role(name: roleName, allowedContent: permissions); + await policyManager.updateRole(role); + + _showStatus('Role "$roleName" updated successfully', true); + _roleNameController.clear(); + _permissionsController.clear(); + } catch (e) { + _showStatus('Failed to update role: $e', false); + } + } + + Future _removeRole() async { + final roleName = _roleNameController.text.trim(); + + if (roleName.isEmpty) { + _showStatus('Role name cannot be empty', false); + return; + } + + try { + await policyManager.removeRole(roleName); + + _showStatus('Role "$roleName" removed successfully', true); + _roleNameController.clear(); + _permissionsController.clear(); + + // Reset selected role if it was removed + if (_selectedRole == roleName) { + setState(() { + _selectedRole = 'guest'; + }); + } + } catch (e) { + _showStatus('Failed to remove role: $e', false); + } + } + + void _selectRole(String roleName) { + setState(() { + _selectedRole = roleName; + _roleNameController.text = roleName; + + // Get current permissions for the role + final role = policyManager.getRole(roleName); + if (role != null) { + _permissionsController.text = role.allowedContent.join(', '); + } + }); + } + + @override + Widget build(BuildContext context) { + if (!_isInitialized) { + return Scaffold( + appBar: AppBar( + title: const Text('Role Management Demo'), + ), + body: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Loading policies...'), + ], + ), + ), + ); + } + + return PolicyProvider( + policyManager: policyManager, + child: Scaffold( + appBar: AppBar( + title: const Text('Role Management Demo'), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Status message + if (_statusMessage.isNotEmpty) + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: _isSuccess ? Colors.green[100] : Colors.red[100], + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: _isSuccess ? Colors.green : Colors.red, + ), + ), + child: Text( + _statusMessage, + style: TextStyle( + color: _isSuccess ? Colors.green[800] : Colors.red[800], + ), + ), + ), + + // Current roles section + _buildCurrentRolesSection(), + + const SizedBox(height: 32), + + // Role management forms + _buildRoleManagementForms(), + + const SizedBox(height: 32), + + // Policy testing section + _buildPolicyTestingSection(), + ], + ), + ), + ), + ); + } + + Widget _buildCurrentRolesSection() { + final roles = policyManager.getAllRoles(); + + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Current Roles', + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 16), + Wrap( + spacing: 8, + runSpacing: 8, + children: roles.map((role) { + final isSelected = _selectedRole == role.name; + return GestureDetector( + onTap: () => _selectRole(role.name), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + decoration: BoxDecoration( + color: isSelected ? Colors.blue : Colors.grey[200], + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: isSelected ? Colors.blue : Colors.grey[300]!, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + role.name, + style: TextStyle( + color: isSelected ? Colors.white : Colors.black, + fontWeight: FontWeight.bold, + ), + ), + if (role.allowedContent.isNotEmpty) + Text( + 'Permissions: ${role.allowedContent.join(', ')}', + style: TextStyle( + color: isSelected ? Colors.white70 : Colors.grey[600], + fontSize: 12, + ), + ), + ], + ), + ), + ); + }).toList(), + ), + ], + ), + ), + ); + } + + Widget _buildRoleManagementForms() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Role Management', + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 16), + + // Role name input + TextField( + controller: _roleNameController, + decoration: const InputDecoration( + labelText: 'Role Name', + hintText: 'Enter role name (e.g., moderator)', + border: OutlineInputBorder(), + ), + ), + + const SizedBox(height: 16), + + // Permissions input + TextField( + controller: _permissionsController, + decoration: const InputDecoration( + labelText: 'Permissions', + hintText: 'Enter permissions separated by commas (e.g., LoginPage, Dashboard, Settings)', + border: OutlineInputBorder(), + ), + maxLines: 2, + ), + + const SizedBox(height: 24), + + // Action buttons + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: _addRole, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + ), + child: const Text('Add Role'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton( + onPressed: _updateRole, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange, + foregroundColor: Colors.white, + ), + child: const Text('Update Role'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton( + onPressed: _removeRole, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + ), + child: const Text('Remove Role'), + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildPolicyTestingSection() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Policy Testing', + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 8), + Text( + 'Selected Role: $_selectedRole', + style: Theme.of(context).textTheme.bodyLarge, + ), + const SizedBox(height: 16), + + // Policy widgets for testing + _buildPolicyTest('LoginPage', 'Login Page'), + _buildPolicyTest('Dashboard', 'Dashboard'), + _buildPolicyTest('UserManagement', 'User Management'), + _buildPolicyTest('Settings', 'Settings'), + ], + ), + ), + ); + } + + Widget _buildPolicyTest(String content, String displayName) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('$displayName:'), + const SizedBox(height: 4), + PolicyWidget( + role: _selectedRole, + content: content, + fallback: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.red[100], + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.red), + ), + child: Text('Access denied for $displayName'), + ), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.green[100], + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.green), + ), + child: Text('Access granted to $displayName'), + ), + ), + ], + ), + ); + } +} +``` + +## ๐Ÿ”ง Key Features Explained + +### 1. Dynamic Role Management + +The demo provides three main operations: + +#### Add Role + +```dart +Future _addRole() async { + final roleName = _roleNameController.text.trim(); + final permissionsText = _permissionsController.text.trim(); + + final permissions = permissionsText.split(',').map((e) => e.trim()).toList(); + final role = Role(name: roleName, allowedContent: permissions); + await policyManager.addRole(role); +} +``` + +#### Update Role + +```dart +Future _updateRole() async { + final roleName = _roleNameController.text.trim(); + final permissionsText = _permissionsController.text.trim(); + + final permissions = permissionsText.split(',').map((e) => e.trim()).toList(); + final role = Role(name: roleName, allowedContent: permissions); + await policyManager.updateRole(role); +} +``` + +#### Remove Role + +```dart +Future _removeRole() async { + final roleName = _roleNameController.text.trim(); + await policyManager.removeRole(roleName); +} +``` + +### 2. Interactive Role Selection + +```dart +void _selectRole(String roleName) { + setState(() { + _selectedRole = roleName; + _roleNameController.text = roleName; + + // Get current permissions for the role + final role = policyManager.getRole(roleName); + if (role != null) { + _permissionsController.text = role.allowedContent.join(', '); + } + }); +} +``` + +### 3. Status Feedback System + +```dart +void _showStatus(String message, bool isSuccess) { + setState(() { + _statusMessage = message; + _isSuccess = isSuccess; + }); + + // Clear status after 3 seconds + Future.delayed(const Duration(seconds: 3), () { + if (mounted) { + setState(() { + _statusMessage = ''; + }); + } + }); +} +``` + +## ๐ŸŽฎ How to Use the Demo + +### Adding a New Role + +1. **Enter Role Name**: Type a new role name (e.g., "moderator") +2. **Enter Permissions**: Add comma-separated permissions (e.g., "LoginPage, Dashboard, UserManagement") +3. **Click "Add Role"**: The new role appears in the Current Roles section +4. **Test the Role**: Select the new role to test its permissions + +### Updating an Existing Role + +1. **Select a Role**: Click on an existing role in the Current Roles section +2. **Modify Permissions**: Update the permissions in the text field +3. **Click "Update Role"**: The role's permissions are updated +4. **Test Changes**: Verify the changes in the Policy Testing section + +### Removing a Role + +1. **Enter Role Name**: Type the name of the role to remove +2. **Click "Remove Role"**: The role is deleted from the system +3. **Verify Removal**: The role no longer appears in Current Roles + +## ๐Ÿ“Š Testing Scenarios + +### Scenario 1: Create Moderator Role + +- **Role Name**: moderator +- **Permissions**: LoginPage, Dashboard, UserManagement +- **Expected Result**: Access to login, dashboard, and user management, but not settings + +### Scenario 2: Update User Role + +- **Original**: LoginPage, Dashboard +- **Updated**: LoginPage, Dashboard, Settings +- **Expected Result**: User role now has access to settings + +### Scenario 3: Remove Guest Role + +- **Action**: Remove guest role +- **Expected Result**: Guest role disappears, cannot be selected + +## ๐Ÿ” Key Learning Points + +1. **Dynamic Operations**: Roles can be created, updated, and removed at runtime +2. **Real-time Updates**: Policy changes take effect immediately +3. **Error Handling**: Comprehensive error handling with user feedback +4. **Form Validation**: Input validation prevents invalid operations +5. **State Management**: Proper state management for UI updates +6. **User Experience**: Intuitive interface with clear visual feedback + +## ๐Ÿš€ Running the Demo + +To run this demo: + +1. Navigate to the example directory +2. Run `flutter pub get` +3. Execute `flutter run` +4. Select "Role Management Demo" from the main menu diff --git a/docs/index.mdx b/docs/index.mdx new file mode 100644 index 0000000..1ef8368 --- /dev/null +++ b/docs/index.mdx @@ -0,0 +1,111 @@ +--- +title: Flutter Policy Engine +description: A comprehensive policy management and access control system for Flutter applications +--- + +# Flutter Policy Engine + +A powerful and flexible policy management system designed specifically for Flutter applications. The Flutter Policy Engine provides comprehensive role-based access control (RBAC) capabilities with an intuitive API and seamless integration with Flutter widgets. + +## ๐Ÿš€ Key Features + +- **Role-Based Access Control**: Define roles with specific permissions and content access +- **Dynamic Policy Management**: Add, update, and remove roles at runtime +- **Widget Integration**: Built-in `PolicyWidget` for easy UI integration +- **Memory Storage**: Efficient in-memory policy storage with extensible architecture +- **Error Handling**: Comprehensive exception handling and validation +- **Type Safety**: Full Dart type safety with null safety support + +## ๐Ÿ—๏ธ Architecture Overview + +The Flutter Policy Engine is built with a modular architecture that separates concerns and promotes maintainability: + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ PolicyWidget โ”‚ โ”‚ PolicyProvider โ”‚ โ”‚ PolicyManager โ”‚ +โ”‚ (UI Layer) โ”‚โ—„โ”€โ”€โ–บโ”‚ (Context) โ”‚โ—„โ”€โ”€โ–บโ”‚ (Core Logic) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Policy Storage โ”‚ + โ”‚ (Memory/Extensible)โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +## ๐Ÿ“ฆ Core Components + +### PolicyManager + +The central orchestrator that handles policy initialization, role management, and access evaluation. + +### PolicyProvider + +A Flutter widget that provides policy context to the widget tree, enabling child widgets to access policy information. + +### PolicyWidget + +A conditional widget that renders content based on the current user's role and permissions. + +### Role Model + +A data structure representing a role with its associated permissions and allowed content. + +## ๐ŸŽฏ Quick Example + +```dart +// Initialize the policy manager +final policyManager = PolicyManager(); +await policyManager.initialize({ + "admin": ["LoginPage", "Dashboard", "UserManagement", "Settings"], + "user": ["LoginPage", "Dashboard"], + "guest": ["LoginPage"] +}); + +// Wrap your app with PolicyProvider +PolicyProvider( + policyManager: policyManager, + child: MaterialApp( + home: MyHomePage(), + ), +); + +// Use PolicyWidget for conditional rendering +PolicyWidget( + role: "admin", + content: "UserManagement", + child: UserManagementPage(), + fallback: AccessDeniedWidget(), +) +``` + +## ๐Ÿ”ง Getting Started + +1. **Installation**: Add the package to your `pubspec.yaml` +2. **Quick Start**: Follow the basic setup guide +3. **Examples**: Explore the interactive demos +4. **API Reference**: Dive into the detailed API documentation + +## ๐Ÿ“š Documentation Sections + +- **[Quick Start](/quick-start)**: Get up and running in minutes +- **[Core Concepts](/core-concepts/policy-management)**: Understand the fundamental concepts +- **[Examples](/examples/basic-policy-demo)**: Interactive examples and demos +- **[API Reference](/api/policy-manager)**: Complete API documentation +- **[Advanced Topics](/advanced/best-practices)**: Best practices and advanced usage + +## ๐ŸŽฎ Interactive Demos + +The Flutter Policy Engine includes comprehensive example applications that demonstrate: + +- **Basic Policy Demo**: Simple role-based access control +- **Role Management Demo**: Dynamic role creation and management +- **Interactive Testing**: Real-time policy testing and validation + +## ๐Ÿค Contributing + +We welcome contributions! Please see our [Contributing Guide](https://github.com/your-repo/flutter_policy_engine/blob/main/CONTRIBUTING.md) for details. + +## ๐Ÿ“„ License + +This project is licensed under the MIT License - see the [LICENSE](https://github.com/your-repo/flutter_policy_engine/blob/main/LICENSE) file for details. diff --git a/docs/installation.mdx b/docs/installation.mdx new file mode 100644 index 0000000..ee59a0e --- /dev/null +++ b/docs/installation.mdx @@ -0,0 +1,198 @@ +--- +title: Installation +description: Complete installation guide for Flutter Policy Engine +--- + +# Installation Guide + +This guide covers everything you need to install and set up the Flutter Policy Engine in your project. + +## ๐Ÿ“‹ Prerequisites + +Before installing Flutter Policy Engine, ensure you have: + +- **Flutter SDK**: Version 3.0.0 or higher +- **Dart SDK**: Version 2.17.0 or higher +- **Android Studio** or **VS Code** with Flutter extensions +- **Git** for version control + +### Check Your Flutter Version + +```bash +flutter --version +``` + +If you need to update Flutter: + +```bash +flutter upgrade +``` + +## ๐Ÿ“ฆ Package Installation + +### 1. Add to pubspec.yaml + +Add the Flutter Policy Engine dependency to your `pubspec.yaml` file: + +```yaml +dependencies: + flutter: + sdk: flutter + flutter_policy_engine: ^1.0.0 +``` + +### 2. Install Dependencies + +Run the following command to install the package: + +```bash +flutter pub get +``` + +### 3. Verify Installation + +Check that the package was installed correctly: + +```bash +flutter pub deps +``` + +You should see `flutter_policy_engine` listed in the dependencies. + +## ๐Ÿ”ง Platform-Specific Setup + +### Android + +No additional setup required for Android. The package works out of the box with Android apps. + +### iOS + +No additional setup required for iOS. The package works out of the box with iOS apps. + +### Web + +For Flutter web applications, ensure you have web support enabled: + +```bash +flutter config --enable-web +``` + +### Desktop (Windows, macOS, Linux) + +For desktop applications, ensure you have desktop support enabled: + +```bash +# For Windows +flutter config --enable-windows-desktop + +# For macOS +flutter config --enable-macos-desktop + +# For Linux +flutter config --enable-linux-desktop +``` + +## ๐Ÿงช Testing the Installation + +Create a simple test to verify the installation: + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_policy_engine/flutter_policy_engine.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Test basic functionality + final policyManager = PolicyManager(); + await policyManager.initialize({ + "test": ["test_content"] + }); + + print("Flutter Policy Engine installed successfully!"); + + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Installation Test', + home: Scaffold( + appBar: AppBar(title: Text('Installation Test')), + body: Center( + child: Text('Flutter Policy Engine is working!'), + ), + ), + ); + } +} +``` + +## ๐Ÿ”„ Updating the Package + +To update to the latest version: + +```bash +flutter pub upgrade flutter_policy_engine +``` + +To update to a specific version: + +```bash +flutter pub add flutter_policy_engine:^1.1.0 +``` + +## ๐Ÿ—‘๏ธ Uninstalling + +To remove the package from your project: + +1. Remove the dependency from `pubspec.yaml` +2. Run `flutter pub get` +3. Remove any import statements from your code + +## ๐Ÿ› Common Installation Issues + +### Issue: Package not found + +**Solution**: Ensure you have a stable internet connection and try: + +```bash +flutter pub cache clean +flutter pub get +``` + +### Issue: Version conflicts + +**Solution**: Check for version conflicts in your `pubspec.yaml`: + +```bash +flutter pub deps --style=tree +``` + +### Issue: Flutter version compatibility + +**Solution**: Update Flutter to the latest stable version: + +```bash +flutter upgrade +flutter doctor +``` + +## ๐Ÿ“š Next Steps + +After successful installation: + +1. **[Quick Start](/quick-start)**: Get up and running in minutes +2. **[Examples](/examples/basic-policy-demo)**: Explore working examples +3. **[API Reference](/api/policy-manager)**: Learn about the available APIs + +## ๐Ÿค Getting Help + +If you encounter installation issues: + +1. Check the [Flutter documentation](https://flutter.dev/docs) +2. Review the [pub.dev package page](https://pub.dev/packages/flutter_policy_engine) +3. Open an issue on the [GitHub repository](https://github.com/your-repo/flutter_policy_engine) +4. Check the [troubleshooting guide](/advanced/error-handling) diff --git a/docs/next-steps.mdx b/docs/next-steps.mdx new file mode 100644 index 0000000..126a78b --- /dev/null +++ b/docs/next-steps.mdx @@ -0,0 +1,371 @@ +--- +title: Next Steps +description: What to do after getting started with Flutter Policy Engine +--- + +# Next Steps + +Congratulations! You've successfully set up the Flutter Policy Engine. Here's what you can do next to make the most of this powerful policy management system. + +## ๐Ÿš€ Immediate Next Steps + +### 1. Explore the Examples + +Start by running the example applications to see the Flutter Policy Engine in action: + +```bash +cd example +flutter run +``` + +**Try these demos**: + +- **[Basic Policy Demo](/examples/basic-policy-demo)**: Simple role-based access control +- **[Role Management Demo](/examples/role-management-demo)**: Dynamic role creation and management +- **[Interactive Testing](/examples/interactive-testing)**: Comprehensive testing tools + +### 2. Integrate into Your App + +Begin integrating the Flutter Policy Engine into your own application: + +```dart +// 1. Initialize the policy manager +final policyManager = PolicyManager(); +await policyManager.initialize({ + "admin": ["LoginPage", "Dashboard", "UserManagement", "Settings"], + "user": ["LoginPage", "Dashboard"], + "guest": ["LoginPage"] +}); + +// 2. Wrap your app with PolicyProvider +PolicyProvider( + policyManager: policyManager, + child: MaterialApp( + home: MyHomePage(), + ), +); + +// 3. Use PolicyWidget for conditional rendering +PolicyWidget( + role: currentUserRole, + content: "Dashboard", + child: DashboardPage(), + fallback: AccessDeniedWidget(), +) +``` + +### 3. Customize Your Policies + +Define policies that match your application's needs: + +```dart +// Example: E-commerce app policies +final ecommercePolicies = { + "customer": ["Browse", "Cart", "Checkout", "Orders"], + "vendor": ["Browse", "Cart", "Checkout", "Orders", "Inventory", "Analytics"], + "admin": ["Browse", "Cart", "Checkout", "Orders", "Inventory", "Analytics", "UserManagement", "Settings"], + "guest": ["Browse"] +}; +``` + +## ๐Ÿ“š Learning Path + +### Beginner Level + +1. **[Quick Start](/quick-start)**: Master the basics +2. **[Core Concepts](/core-concepts/policy-management)**: Understand policy management +3. **[Basic Examples](/examples/basic-policy-demo)**: Run simple demos + +### Intermediate Level + +4. **[Role Management](/examples/role-management-demo)**: Learn dynamic role management +5. **[Interactive Testing](/examples/interactive-testing)**: Master testing techniques +6. **[API Reference](/api/policy-manager)**: Explore the complete API + +### Advanced Level + +7. **[Best Practices](/advanced/best-practices)**: Learn production patterns +8. **[Custom Storage](/advanced/custom-storage)**: Implement custom storage +9. **[Error Handling](/advanced/error-handling)**: Master error handling + +## ๐ŸŽฏ Common Use Cases + +### 1. User Authentication & Authorization + +```dart +class AuthService { + final PolicyManager policyManager; + + Future loginUser(String username, String password) async { + // Authenticate user + final user = await authenticateUser(username, password); + + // Set user role based on authentication result + final role = user.isAdmin ? "admin" : "user"; + + // Use role for policy evaluation + return role; + } +} +``` + +### 2. Feature Flags + +```dart +PolicyWidget( + role: currentUserRole, + content: "BetaFeature", + child: BetaFeatureWidget(), + fallback: Container(), // Hide feature for unauthorized users +) +``` + +### 3. Content Management + +```dart +PolicyWidget( + role: currentUserRole, + content: "PremiumContent", + child: PremiumContentWidget(), + fallback: UpgradePromptWidget(), +) +``` + +## ๐Ÿ”ง Advanced Integration + +### 1. State Management Integration + +```dart +// With Provider +class UserProvider extends ChangeNotifier { + String _currentRole = 'guest'; + final PolicyManager _policyManager; + + UserProvider(this._policyManager); + + String get currentRole => _currentRole; + + void setRole(String role) { + _currentRole = role; + notifyListeners(); + } + + bool hasAccess(String content) { + return _policyManager.evaluateAccess(_currentRole, content); + } +} +``` + +### 2. Navigation Integration + +```dart +class PolicyAwareNavigator { + final PolicyManager policyManager; + final String currentRole; + + PolicyAwareNavigator(this.policyManager, this.currentRole); + + void navigateToContent(BuildContext context, String content, Widget page) { + if (policyManager.evaluateAccess(currentRole, content)) { + Navigator.push(context, MaterialPageRoute(builder: (context) => page)); + } else { + showAccessDeniedDialog(context); + } + } +} +``` + +### 3. API Integration + +```dart +class PolicyAwareApiClient { + final PolicyManager policyManager; + final String currentRole; + + PolicyAwareApiClient(this.policyManager, this.currentRole); + + Future getData(String endpoint) async { + // Check if user has access to this endpoint + if (!policyManager.evaluateAccess(currentRole, endpoint)) { + throw AccessDeniedException('Access denied to $endpoint'); + } + + // Proceed with API call + return await http.get(Uri.parse(endpoint)); + } +} +``` + +## ๐Ÿงช Testing Your Implementation + +### 1. Unit Tests + +```dart +void main() { + group('Policy Manager Tests', () { + late PolicyManager policyManager; + + setUp(() async { + policyManager = PolicyManager(); + await policyManager.initialize({ + "admin": ["LoginPage", "Dashboard"], + "user": ["LoginPage"], + }); + }); + + test('admin should have access to Dashboard', () { + expect(policyManager.evaluateAccess("admin", "Dashboard"), isTrue); + }); + + test('user should not have access to Dashboard', () { + expect(policyManager.evaluateAccess("user", "Dashboard"), isFalse); + }); + }); +} +``` + +### 2. Widget Tests + +```dart +void main() { + testWidgets('PolicyWidget shows content for authorized role', (tester) async { + final policyManager = PolicyManager(); + await policyManager.initialize({ + "admin": ["Dashboard"], + }); + + await tester.pumpWidget( + PolicyProvider( + policyManager: policyManager, + child: MaterialApp( + home: PolicyWidget( + role: "admin", + content: "Dashboard", + child: Text('Dashboard Content'), + fallback: Text('Access Denied'), + ), + ), + ), + ); + + expect(find.text('Dashboard Content'), findsOneWidget); + expect(find.text('Access Denied'), findsNothing); + }); +} +``` + +## ๐Ÿš€ Performance Optimization + +### 1. Policy Caching + +```dart +class CachedPolicyManager { + final PolicyManager _policyManager; + final Map _accessCache = {}; + + CachedPolicyManager(this._policyManager); + + bool evaluateAccess(String role, String content) { + final cacheKey = '${role}_$content'; + + if (_accessCache.containsKey(cacheKey)) { + return _accessCache[cacheKey]!; + } + + final result = _policyManager.evaluateAccess(role, content); + _accessCache[cacheKey] = result; + + return result; + } +} +``` + +### 2. Lazy Loading + +```dart +class LazyPolicyWidget extends StatefulWidget { + final String role; + final String content; + final Widget Function() childBuilder; + final Widget fallback; + + @override + _LazyPolicyWidgetState createState() => _LazyPolicyWidgetState(); +} + +class _LazyPolicyWidgetState extends State { + bool _hasAccess = false; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _checkAccess(); + } + + Future _checkAccess() async { + // Simulate async policy check + await Future.delayed(Duration(milliseconds: 100)); + final hasAccess = PolicyManager.of(context).evaluateAccess(widget.role, widget.content); + + setState(() { + _hasAccess = hasAccess; + _isLoading = false; + }); + } + + @override + Widget build(BuildContext context) { + if (_isLoading) { + return CircularProgressIndicator(); + } + + return _hasAccess ? widget.childBuilder() : widget.fallback; + } +} +``` + +## ๐Ÿ”„ Community & Support + +### 1. Get Help + +- **GitHub Issues**: Report bugs and request features +- **Documentation**: Comprehensive guides and examples +- **Community**: Join discussions and share experiences + +### 2. Contribute + +- **Code Contributions**: Submit pull requests +- **Documentation**: Help improve the docs +- **Examples**: Share your use cases and implementations + +### 3. Stay Updated + +- **Releases**: Follow new releases and features +- **Blog**: Read about best practices and tips +- **Newsletter**: Get updates delivered to your inbox + +## ๐Ÿ“ˆ Production Checklist + +Before deploying to production: + +- [ ] **Policy Validation**: Ensure all policies are properly validated +- [ ] **Error Handling**: Implement comprehensive error handling +- [ ] **Performance Testing**: Test with realistic data volumes +- [ ] **Security Review**: Review security implications +- [ ] **Monitoring**: Set up monitoring and alerting +- [ ] **Backup Strategy**: Plan for policy data backup +- [ ] **Rollback Plan**: Prepare for policy rollbacks if needed + +## ๐ŸŽฏ What's Next? + +Choose your path: + +- **Build Something**: Start integrating into your app +- **Learn More**: Dive deeper into the documentation +- **Contribute**: Help improve the project +- **Share**: Tell others about your experience + +The Flutter Policy Engine is designed to grow with your needs. Start simple and add complexity as your application evolves. + +Happy coding! ๐Ÿš€ diff --git a/docs/quick-start.mdx b/docs/quick-start.mdx new file mode 100644 index 0000000..1b66a31 --- /dev/null +++ b/docs/quick-start.mdx @@ -0,0 +1,235 @@ +--- +title: Quick Start +description: Get up and running with Flutter Policy Engine in minutes +--- + +# Quick Start Guide + +Get started with Flutter Policy Engine in just a few minutes. This guide will walk you through the basic setup and show you how to implement role-based access control in your Flutter app. + +## ๐Ÿ“ฆ Installation + +Add the Flutter Policy Engine to your `pubspec.yaml`: + +```yaml +dependencies: + flutter_policy_engine: ^1.0.0 +``` + +Then run: + +```bash +flutter pub get +``` + +## ๐Ÿš€ Basic Setup + +### 1. Import the Package + +```dart +import 'package:flutter_policy_engine/flutter_policy_engine.dart'; +``` + +### 2. Initialize Policy Manager + +Create a `PolicyManager` instance and initialize it with your role definitions: + +```dart +final policyManager = PolicyManager(); + +// Define your roles and their permissions +final policies = { + "admin": ["LoginPage", "Dashboard", "UserManagement", "Settings"], + "user": ["LoginPage", "Dashboard"], + "guest": ["LoginPage"] +}; + +// Initialize the policy manager +await policyManager.initialize(policies); +``` + +### 3. Wrap Your App with PolicyProvider + +Wrap your main app widget with `PolicyProvider` to make the policy manager available throughout your widget tree: + +```dart +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return PolicyProvider( + policyManager: policyManager, + child: MaterialApp( + title: 'My App', + home: MyHomePage(), + ), + ); + } +} +``` + +### 4. Use PolicyWidget for Conditional Rendering + +Use `PolicyWidget` to conditionally render content based on user roles: + +```dart +PolicyWidget( + role: "admin", + content: "UserManagement", + child: UserManagementPage(), + fallback: AccessDeniedWidget(), +) +``` + +## ๐Ÿ“ฑ Complete Example + +Here's a complete working example that demonstrates the basic functionality: + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_policy_engine/flutter_policy_engine.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Initialize policy manager + final policyManager = PolicyManager(); + await policyManager.initialize({ + "admin": ["LoginPage", "Dashboard", "UserManagement", "Settings"], + "user": ["LoginPage", "Dashboard"], + "guest": ["LoginPage"] + }); + + runApp(MyApp(policyManager: policyManager)); +} + +class MyApp extends StatelessWidget { + final PolicyManager policyManager; + + const MyApp({Key? key, required this.policyManager}) : super(key: key); + + @override + Widget build(BuildContext context) { + return PolicyProvider( + policyManager: policyManager, + child: MaterialApp( + title: 'Policy Engine Demo', + theme: ThemeData(primarySwatch: Colors.blue), + home: HomePage(), + ), + ); + } +} + +class HomePage extends StatefulWidget { + @override + _HomePageState createState() => _HomePageState(); +} + +class _HomePageState extends State { + String currentRole = 'guest'; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Policy Engine Demo')), + body: Padding( + padding: EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Current Role: $currentRole', + style: Theme.of(context).textTheme.headlineSmall), + SizedBox(height: 16), + + // Role selector + Row( + children: ['guest', 'user', 'admin'].map((role) => + Padding( + padding: EdgeInsets.only(right: 8), + child: ElevatedButton( + onPressed: () => setState(() => currentRole = role), + child: Text(role), + ), + ) + ).toList(), + ), + + SizedBox(height: 32), + + // Policy widgets + PolicyWidget( + role: currentRole, + content: "Dashboard", + child: Card( + color: Colors.green[100], + child: Padding( + padding: EdgeInsets.all(16), + child: Text('Dashboard Access Granted'), + ), + ), + fallback: Card( + color: Colors.red[100], + child: Padding( + padding: EdgeInsets.all(16), + child: Text('Dashboard Access Denied'), + ), + ), + ), + + SizedBox(height: 16), + + PolicyWidget( + role: currentRole, + content: "UserManagement", + child: Card( + color: Colors.green[100], + child: Padding( + padding: EdgeInsets.all(16), + child: Text('User Management Access Granted'), + ), + ), + fallback: Card( + color: Colors.red[100], + child: Padding( + padding: EdgeInsets.all(16), + child: Text('User Management Access Denied'), + ), + ), + ), + ], + ), + ), + ); + } +} +``` + +## ๐ŸŽฏ What You've Learned + +In this quick start guide, you've learned how to: + +1. **Install** the Flutter Policy Engine package +2. **Initialize** a policy manager with role definitions +3. **Provide** policy context to your app +4. **Use** PolicyWidget for conditional rendering +5. **Test** different roles and permissions + +## ๐Ÿ”„ Next Steps + +Now that you have the basics working, explore these next topics: + +- **[Policy Management](/core-concepts/policy-management)**: Learn about dynamic role management +- **[Role-Based Access Control](/core-concepts/role-based-access)**: Understand the RBAC concepts +- **[Examples](/examples/basic-policy-demo)**: See more complex examples +- **[API Reference](/api/policy-manager)**: Dive into the complete API documentation + +## ๐Ÿ› Troubleshooting + +If you encounter any issues: + +1. **Make sure** you've run `flutter pub get` +2. **Check** that you're using the latest version of Flutter +3. **Verify** your role definitions are properly formatted +4. **Ensure** PolicyProvider wraps your app correctly + +For more help, check the [Error Handling](/advanced/error-handling) guide or open an issue on GitHub. diff --git a/example/ROLE_MANAGEMENT_TEST.md b/example/ROLE_MANAGEMENT_TEST.md new file mode 100644 index 0000000..6db6cf4 --- /dev/null +++ b/example/ROLE_MANAGEMENT_TEST.md @@ -0,0 +1,91 @@ +# Role Management Test + +This dynamic test screen allows you to interactively test the role management operations from the Flutter Policy Engine. + +## Features + +### 1. Current Roles Display + +- Shows all currently available roles in the system +- Displays permissions for each role +- Click on any role to select it for testing or editing + +### 2. Role Management Operations + +Test the three main role management functions: + +#### Add Role + +- Enter a new role name and permissions +- Permissions should be comma-separated (e.g., "LoginPage, Dashboard, Settings") +- Click "Add Role" to create the new role + +#### Update Role + +- Select an existing role or enter a role name +- Modify the permissions as needed +- Click "Update Role" to apply changes + +#### Remove Role + +- Enter the name of the role to remove +- Click "Remove Role" to delete it from the system + +### 3. Policy Testing + +- Select any role from the available roles +- See real-time policy evaluation for different content types: + - Login Page + - Dashboard + - User Management + - Settings +- Green cards indicate access granted +- Red cards indicate access denied + +## Usage Examples + +### Adding a New Role + +1. Enter role name: "moderator" +2. Enter permissions: "LoginPage, Dashboard, UserManagement" +3. Click "Add Role" +4. The new role appears in the Current Roles list + +### Testing Access Control + +1. Select "admin" role +2. Observe that all content types show green (access granted) +3. Switch to "guest" role +4. Observe that only Login Page shows green, others show red (access denied) + +### Updating Permissions + +1. Click on "user" role in the Current Roles list +2. The form will be populated with current permissions +3. Add "Settings" to the permissions field +4. Click "Update Role" +5. Test the updated permissions in the Policy Testing section + +## Technical Details + +This test screen demonstrates: + +- `PolicyManager.addRole()` - Adding new roles +- `PolicyManager.removeRole()` - Removing existing roles +- `PolicyManager.updateRole()` - Updating role permissions +- `PolicyWidget` - Real-time policy evaluation +- Error handling and user feedback +- Dynamic UI updates based on policy changes + +## Error Handling + +The test includes comprehensive error handling: + +- Validation for empty role names +- Try-catch blocks for all operations +- User-friendly status messages +- Automatic status message clearing after 3 seconds + +## Integration + +This test screen is accessible from the main app home screen and provides a comprehensive way to validate the role management functionality of the Flutter Policy Engine. diff --git a/example/lib/main.dart b/example/lib/main.dart index e51e6db..f6e03cd 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_policy_engine_example/policy_engine_demo.dart'; +import 'package:flutter_policy_engine_example/role_management_demo.dart'; void main() { runApp(const MyApp()); @@ -16,7 +17,90 @@ class MyApp extends StatelessWidget { colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), - home: const PolicyEngineDemo(), + home: const HomeScreen(), + ); + } +} + +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Flutter Policy Engine Demo'), + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + const Icon( + Icons.security, + size: 64, + color: Colors.blue, + ), + const SizedBox(height: 16), + Text( + 'Flutter Policy Engine', + style: Theme.of(context).textTheme.headlineMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'Interactive demo and testing tools', + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + const SizedBox(height: 24), + ElevatedButton.icon( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const PolicyEngineDemo(), + ), + ); + }, + icon: const Icon(Icons.play_arrow), + label: const Text('Basic Policy Demo'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.all(16), + ), + ), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const RoleManagementDemo(), + ), + ); + }, + icon: const Icon(Icons.admin_panel_settings), + label: const Text('Role Management Demo'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.all(16), + backgroundColor: Colors.orange, + foregroundColor: Colors.white, + ), + ), + ], + ), + ), ); } } diff --git a/example/lib/role_management_demo.dart b/example/lib/role_management_demo.dart new file mode 100644 index 0000000..aee8cd6 --- /dev/null +++ b/example/lib/role_management_demo.dart @@ -0,0 +1,417 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:flutter_policy_engine/flutter_policy_engine.dart'; + +class RoleManagementDemo extends StatefulWidget { + const RoleManagementDemo({super.key}); + + @override + State createState() => _RoleManagementDemoState(); +} + +class _RoleManagementDemoState extends State { + late PolicyManager policyManager; + bool _isInitialized = false; + String _selectedRole = 'guest'; + + // Form controllers for role management + final TextEditingController _roleNameController = TextEditingController(); + final TextEditingController _permissionsController = TextEditingController(); + + // Status messages + String _statusMessage = ''; + bool _isSuccess = true; + + @override + void initState() { + super.initState(); + _initializePolicyManager(); + } + + @override + void dispose() { + _roleNameController.dispose(); + _permissionsController.dispose(); + super.dispose(); + } + + Future _initializePolicyManager() async { + policyManager = PolicyManager(); + final policies = { + "admin": ["LoginPage", "Dashboard", "UserManagement", "Settings"], + "user": ["LoginPage", "Dashboard"], + "guest": ["LoginPage"] + }; + try { + await policyManager.initialize(policies); + setState(() { + _isInitialized = true; + _selectedRole = 'guest'; + }); + } catch (e) { + log(e.toString()); + setState(() { + _isInitialized = false; + _statusMessage = 'Failed to initialize: $e'; + _isSuccess = false; + }); + } + } + + void _showStatus(String message, bool isSuccess) { + setState(() { + _statusMessage = message; + _isSuccess = isSuccess; + }); + + // Clear status after 3 seconds + Future.delayed(const Duration(seconds: 3), () { + if (mounted) { + setState(() { + _statusMessage = ''; + }); + } + }); + } + + Future _addRole() async { + final roleName = _roleNameController.text.trim(); + final permissionsText = _permissionsController.text.trim(); + + if (roleName.isEmpty) { + _showStatus('Role name cannot be empty', false); + return; + } + + try { + final permissions = permissionsText.isEmpty + ? [] + : permissionsText.split(',').map((e) => e.trim()).toList(); + + final role = Role(name: roleName, allowedContent: permissions); + await policyManager.addRole(role); + + _showStatus('Role "$roleName" added successfully', true); + _roleNameController.clear(); + _permissionsController.clear(); + + // Update selected role if it's the new one + setState(() { + _selectedRole = roleName; + }); + } catch (e) { + _showStatus('Failed to add role: $e', false); + } + } + + Future _removeRole() async { + final roleName = _roleNameController.text.trim(); + + if (roleName.isEmpty) { + _showStatus('Role name cannot be empty', false); + return; + } + + try { + await policyManager.removeRole(roleName); + _showStatus('Role "$roleName" removed successfully', true); + _roleNameController.clear(); + _permissionsController.clear(); + + // Update selected role if the removed one was selected + if (_selectedRole == roleName) { + setState(() { + _selectedRole = 'guest'; + }); + } + } catch (e) { + _showStatus('Failed to remove role: $e', false); + } + } + + Future _updateRole() async { + final roleName = _roleNameController.text.trim(); + final permissionsText = _permissionsController.text.trim(); + + if (roleName.isEmpty) { + _showStatus('Role name cannot be empty', false); + return; + } + + try { + final permissions = permissionsText.isEmpty + ? [] + : permissionsText.split(',').map((e) => e.trim()).toList(); + + final role = Role(name: roleName, allowedContent: permissions); + await policyManager.updateRole(roleName, role); + + _showStatus('Role "$roleName" updated successfully', true); + _roleNameController.clear(); + _permissionsController.clear(); + } catch (e) { + _showStatus('Failed to update role: $e', false); + } + } + + List _getAvailableRoles() { + return policyManager.roles.keys.toList(); + } + + @override + Widget build(BuildContext context) { + if (!_isInitialized) { + return Scaffold( + appBar: AppBar( + title: const Text('Role Management Demo'), + ), + body: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Loading policies...'), + ], + ), + ), + ); + } + + return Scaffold( + appBar: AppBar( + title: const Text('Role Management Demo'), + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Status message + if (_statusMessage.isNotEmpty) + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: _isSuccess ? Colors.green[100] : Colors.red[100], + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: _isSuccess ? Colors.green : Colors.red, + ), + ), + child: Text( + _statusMessage, + style: TextStyle( + color: _isSuccess ? Colors.green[800] : Colors.red[800], + fontWeight: FontWeight.w500, + ), + ), + ), + + // Current roles section + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Current Roles', + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 12), + ..._getAvailableRoles().map((roleName) { + final role = policyManager.roles[roleName]; + return ListTile( + title: Text(roleName), + subtitle: Text( + 'Permissions: ${role?.allowedContent.join(', ') ?? 'None'}'), + trailing: roleName == _selectedRole + ? const Icon(Icons.check_circle, + color: Colors.green) + : null, + onTap: () { + setState(() { + _selectedRole = roleName; + _roleNameController.text = roleName; + _permissionsController.text = + role?.allowedContent.join(', ') ?? ''; + }); + }, + ); + }), + ], + ), + ), + ), + + const SizedBox(height: 24), + + // Role management form + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Role Management', + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 16), + TextField( + controller: _roleNameController, + decoration: const InputDecoration( + labelText: 'Role Name', + hintText: 'Enter role name', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 16), + TextField( + controller: _permissionsController, + decoration: const InputDecoration( + labelText: 'Permissions', + hintText: 'Enter permissions separated by commas', + border: OutlineInputBorder(), + ), + maxLines: 2, + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: _addRole, + icon: const Icon(Icons.add), + label: const Text('Add Role'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton.icon( + onPressed: _updateRole, + icon: const Icon(Icons.edit), + label: const Text('Update Role'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange, + foregroundColor: Colors.white, + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton.icon( + onPressed: _removeRole, + icon: const Icon(Icons.delete), + label: const Text('Remove Role'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + ), + ), + ), + ], + ), + ], + ), + ), + ), + + const SizedBox(height: 24), + + // Policy testing section + PolicyProvider( + policyManager: policyManager, + child: Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Policy Testing', + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 12), + + Text('Selected Role: $_selectedRole'), + const SizedBox(height: 16), + + // Role selector + Wrap( + spacing: 8, + children: _getAvailableRoles().map((role) { + return ChoiceChip( + label: Text(role), + selected: _selectedRole == role, + onSelected: (selected) { + if (selected) { + setState(() { + _selectedRole = role; + }); + } + }, + ); + }).toList(), + ), + + const SizedBox(height: 16), + + // Policy examples + _buildPolicyExample('LoginPage', 'Login Page'), + _buildPolicyExample('Dashboard', 'Dashboard'), + _buildPolicyExample('UserManagement', 'User Management'), + _buildPolicyExample('Settings', 'Settings'), + ], + ), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildPolicyExample(String content, String displayName) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('$displayName:'), + const SizedBox(height: 4), + PolicyWidget( + role: _selectedRole, + content: content, + fallback: Card( + color: Colors.red[100], + child: Padding( + padding: const EdgeInsets.all(12), + child: Text('Access denied for $displayName'), + ), + ), + onAccessDenied: () { + log('Access denied for $_selectedRole to $content'); + }, + child: Card( + color: Colors.green[100], + child: Padding( + padding: const EdgeInsets.all(12), + child: Text('Access granted to $displayName'), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/flutter_policy_engine.dart b/lib/flutter_policy_engine.dart index 160efed..68ffa89 100644 --- a/lib/flutter_policy_engine.dart +++ b/lib/flutter_policy_engine.dart @@ -3,3 +3,4 @@ library flutter_policy_engine; export 'src/core/policy_manager.dart'; export 'src/widgets/policy_widget.dart'; export 'src/core/policy_provider.dart'; +export 'src/models/role.dart'; diff --git a/lib/src/core/policy_manager.dart b/lib/src/core/policy_manager.dart index 1aa5921..be084ef 100644 --- a/lib/src/core/policy_manager.dart +++ b/lib/src/core/policy_manager.dart @@ -3,7 +3,7 @@ import 'package:flutter_policy_engine/src/core/interfaces/i_policy_evaluator.dar import 'package:flutter_policy_engine/src/core/interfaces/i_policy_storage.dart'; import 'package:flutter_policy_engine/src/core/memory_policy_storage.dart'; import 'package:flutter_policy_engine/src/core/role_evaluator.dart'; -import 'package:flutter_policy_engine/src/models/policy.dart'; +import 'package:flutter_policy_engine/src/models/role.dart'; import 'package:flutter_policy_engine/src/utils/json_handler.dart'; import 'package:flutter_policy_engine/src/utils/log_handler.dart'; @@ -40,7 +40,7 @@ class PolicyManager extends ChangeNotifier { IPolicyEvaluator? _evaluator; /// Internal cache of loaded policies, keyed by policy identifier. - Map _policies = {}; + Map _roles = {}; /// Indicates whether the policy manager has been initialized with policy data. bool _isInitialized = false; @@ -54,7 +54,7 @@ class PolicyManager extends ChangeNotifier { /// /// The returned map cannot be modified directly. Use [initialize] to update /// the policy collection. - Map get policies => Map.unmodifiable(_policies); + Map get roles => Map.unmodifiable(_roles); /// Initializes the policy manager with policy data from JSON. /// @@ -62,7 +62,7 @@ class PolicyManager extends ChangeNotifier { /// This method should be called before using any policy-related functionality. /// /// [jsonPolicies] should be a map where keys are policy identifiers and values - /// are JSON representations of [Policy] objects. + /// are JSON representations of [Role] objects. /// /// Throws: /// - [JsonParseException] if policy parsing fails completely @@ -118,30 +118,30 @@ class PolicyManager extends ChangeNotifier { } // Create the policy and add to valid policies - final policy = Policy( - roleName: key, + final role = Role( + name: key, allowedContent: value.cast(), ); - validPolicies[key] = policy.toJson(); + validPolicies[key] = role.toJson(); } - _policies = JsonHandler.parseMap( + _roles = JsonHandler.parseMap( validPolicies, - (json) => Policy.fromJson(json), + (json) => Role.fromJson(json), context: 'policy_manager', allowPartialSuccess: true, ); // Only create evaluator if we have at least some policies - if (_policies.isNotEmpty) { - _evaluator = RoleEvaluator(_policies); - await _storage.savePolicies(_policies); + if (_roles.isNotEmpty) { + _evaluator = RoleEvaluator(_roles); + await _storage.savePolicies(_roles); _isInitialized = true; LogHandler.info( 'Policy manager initialized successfully', context: { - 'loaded_policies': _policies.length, + 'loaded_policies': _roles.length, 'total_policies': jsonPolicies.length, }, operation: 'policy_manager_initialized', @@ -194,4 +194,80 @@ class PolicyManager extends ChangeNotifier { } return _evaluator!.evaluate(role, content); } + + /// Adds a new role to the policy manager. + /// + /// Adds the specified [role] to the internal policy cache and updates the + /// evaluator with the new role configuration. The role is also persisted + /// to storage and listeners are notified of the change. + /// + /// If a role with the same name already exists, it will be overwritten. + /// + /// [role] must not be null and should have a valid name. + /// + /// Throws: + /// - [ArgumentError] if [role] is null or has an invalid name + /// - Storage-related exceptions if persistence fails + Future addRole(Role role) async { + if (role.name.isEmpty) { + throw ArgumentError('Role name cannot be empty'); + } + + _roles[role.name] = role; + _evaluator = RoleEvaluator(_roles); + await _storage.savePolicies(_roles); + notifyListeners(); + } + + /// Removes a role from the policy manager. + /// + /// Removes the role identified by [roleName] from the internal policy cache + /// and updates the evaluator with the modified role configuration. The + /// updated policy state is persisted to storage and listeners are notified + /// of the change. + /// + /// If no role exists with the specified [roleName], the operation completes + /// successfully without any changes. + /// + /// [roleName] must not be null or empty. + /// + /// Throws: + /// - [ArgumentError] if [roleName] is null or empty + /// - Storage-related exceptions if persistence fails + Future removeRole(String roleName) async { + if (roleName.isEmpty) { + throw ArgumentError('Role name cannot be empty'); + } + + _roles.remove(roleName); + _evaluator = RoleEvaluator(_roles); + await _storage.savePolicies(_roles); + notifyListeners(); + } + + /// Updates an existing role in the policy manager. + /// + /// Replaces the role identified by [roleName] with the new [role] configuration. + /// The evaluator is updated with the modified role configuration, the updated + /// policy state is persisted to storage, and listeners are notified of the change. + /// + /// If no role exists with the specified [roleName], a new role is added instead. + /// This method effectively combines the functionality of [addRole] and [removeRole]. + /// + /// [roleName] must not be null or empty. + /// [role] must not be null and should have a valid name. + /// + /// Throws: + /// - [ArgumentError] if [roleName] is null/empty or [role] is null/invalid + /// - Storage-related exceptions if persistence fails + Future updateRole(String roleName, Role role) async { + if (roleName.isEmpty && role.name.isEmpty) { + throw ArgumentError('Role name cannot be empty'); + } + + _roles[roleName] = role; + _evaluator = RoleEvaluator(_roles); + await _storage.savePolicies(_roles); + notifyListeners(); + } } diff --git a/lib/src/core/role_evaluator.dart b/lib/src/core/role_evaluator.dart index cfdc3f4..e0a8e88 100644 --- a/lib/src/core/role_evaluator.dart +++ b/lib/src/core/role_evaluator.dart @@ -1,5 +1,5 @@ import 'package:flutter_policy_engine/src/core/interfaces/i_policy_evaluator.dart'; -import 'package:flutter_policy_engine/src/models/policy.dart'; +import 'package:flutter_policy_engine/src/models/role.dart'; import 'package:meta/meta.dart'; /// A policy evaluator that determines access permissions based on role-based policies. @@ -8,7 +8,7 @@ import 'package:meta/meta.dart'; /// role-based access control functionality. It evaluates whether content is /// allowed for a specific role by checking against predefined policies. /// -/// Each role is associated with a [Policy] that defines the rules for content +/// Each role is associated with a [Role] that defines the rules for content /// evaluation. If no policy exists for a given role, access is denied by default. /// /// Example usage: @@ -25,7 +25,7 @@ class RoleEvaluator implements IPolicyEvaluator { /// Creates a new [RoleEvaluator] with the specified policies. /// /// The [policies] parameter should contain a mapping of role names to their - /// corresponding [Policy] objects. Each policy defines the rules for content + /// corresponding [Role] objects. Each policy defines the rules for content /// evaluation for that specific role. /// /// Throws an [ArgumentError] if [policies] is null. @@ -35,8 +35,8 @@ class RoleEvaluator implements IPolicyEvaluator { /// /// This map contains all available policies that can be evaluated by this /// evaluator. The key is the role name and the value is the corresponding - /// [Policy] object. - final Map _policies; + /// [Role] object. + final Map _policies; /// Evaluates whether the specified content is allowed for the given role. /// @@ -45,11 +45,11 @@ class RoleEvaluator implements IPolicyEvaluator { /// /// Returns `true` if: /// - A policy exists for the [roleName] - /// - The policy's [Policy.isContentAllowed] method returns `true` for the [content] + /// - The policy's [Role.isContentAllowed] method returns `true` for the [content] /// /// Returns `false` if: /// - No policy exists for the [roleName] - /// - The policy's [Policy.isContentAllowed] method returns `false` for the [content] + /// - The policy's [Role.isContentAllowed] method returns `false` for the [content] /// /// Parameters: /// - [roleName]: The name of the role to evaluate permissions for diff --git a/lib/src/models/policy.dart b/lib/src/models/role.dart similarity index 83% rename from lib/src/models/policy.dart rename to lib/src/models/role.dart index 2bfd2c7..1b40281 100644 --- a/lib/src/models/policy.dart +++ b/lib/src/models/role.dart @@ -17,17 +17,17 @@ import 'package:collection/collection.dart'; /// ); /// ``` @immutable -class Policy { +class Role { /// Creates a new policy with the specified role name and allowed content. /// - /// The [roleName] identifies the role this policy applies to. + /// The [name] identifies the role this policy applies to. /// The [allowedContent] list contains the content types or actions that are /// permitted for this role. The [metadata] provides additional configuration /// options and defaults to an empty map if not specified. /// - /// Throws an [ArgumentError] if [roleName] is empty or [allowedContent] is null. - const Policy({ - required this.roleName, + /// Throws an [ArgumentError] if [name] is empty or [allowedContent] is null. + const Role({ + required this.name, required this.allowedContent, this.metadata = const {}, }); @@ -35,7 +35,7 @@ class Policy { /// The name of the role this policy applies to. /// /// This should be a unique identifier for the role within your application. - final String roleName; + final String name; /// List of content types or actions that are allowed for this role. /// @@ -52,7 +52,7 @@ class Policy { /// Creates a copy of this policy with the given fields replaced by new values. /// - /// Returns a new [Policy] instance with the same values as this one, + /// Returns a new [Role] instance with the same values as this one, /// except for the fields that are explicitly provided in the parameters. /// /// Example: @@ -62,13 +62,13 @@ class Policy { /// metadata: {'priority': 'low'}, /// ); /// ``` - Policy copyWith({ - String? roleName, + Role copyWith({ + String? name, List? allowedContent, Map? metadata, }) => - Policy( - roleName: roleName ?? this.roleName, + Role( + name: name ?? this.name, allowedContent: allowedContent ?? this.allowedContent, metadata: metadata ?? this.metadata, ); @@ -88,15 +88,15 @@ class Policy { /// Compares this policy with another object for equality. /// - /// Two policies are considered equal if they have the same [roleName] and + /// Two policies are considered equal if they have the same [name] and /// the same [allowedContent] (regardless of order). The [metadata] is not /// considered in the equality comparison. @override bool operator ==(Object other) { if (identical(this, other)) return true; - if (other is! Policy) return false; + if (other is! Role) return false; - if (roleName != other.roleName) return false; + if (name != other.name) return false; if (allowedContent.length != other.allowedContent.length) return false; // Sort both lists to ensure order-independent comparison (same as hashCode) @@ -108,12 +108,12 @@ class Policy { /// Returns the hash code for this policy. /// - /// The hash code is based on the [roleName] and [allowedContent] fields. + /// The hash code is based on the [name] and [allowedContent] fields. /// The allowedContent is sorted to ensure consistent hash codes regardless of order. @override int get hashCode { final sortedContent = List.from(allowedContent)..sort(); - return Object.hash(roleName, const ListEquality().hash(sortedContent)); + return Object.hash(name, const ListEquality().hash(sortedContent)); } /// Returns a string representation of this policy. @@ -121,9 +121,9 @@ class Policy { /// The string includes the role name, allowed content, and metadata. @override String toString() => - 'Policy(roleName: $roleName, allowedContent: $allowedContent, metadata: $metadata)'; + 'Policy(roleName: $name, allowedContent: $allowedContent, metadata: $metadata)'; - /// Creates a [Policy] instance from a JSON map. + /// Creates a [Role] instance from a JSON map. /// /// The JSON map should contain: /// - `roleName`: A string representing the role name @@ -141,12 +141,12 @@ class Policy { /// }; /// final policy = Policy.fromJson(json); /// ``` - factory Policy.fromJson(Map json) { - final roleName = json['roleName']; + factory Role.fromJson(Map json) { + final name = json['roleName']; final allowedContent = json['allowedContent']; final metadata = json['metadata']; - if (roleName == null || roleName is! String) { + if (name == null || name is! String) { throw ArgumentError('roleName must be a non-null string'); } if (allowedContent == null || allowedContent is! List) { @@ -156,8 +156,8 @@ class Policy { throw ArgumentError('All allowedContent items must be strings'); } - return Policy( - roleName: roleName, + return Role( + name: name, allowedContent: allowedContent.cast(), metadata: metadata is Map ? metadata : {}, @@ -179,7 +179,7 @@ class Policy { /// // } /// ``` Map toJson() => { - 'roleName': roleName, + 'roleName': name, 'allowedContent': allowedContent, 'metadata': metadata, }; diff --git a/test/core/integration_test.dart b/test/core/integration_test.dart index 25cde98..0e66528 100644 --- a/test/core/integration_test.dart +++ b/test/core/integration_test.dart @@ -24,15 +24,15 @@ void main() { await policyManager.initialize(jsonPolicies); expect(policyManager.isInitialized, isTrue); - expect(policyManager.policies.length, equals(3)); + expect(policyManager.roles.length, equals(3)); // Verify policies were created correctly - expect(policyManager.policies['admin']!.roleName, equals('admin')); - expect(policyManager.policies['admin']!.allowedContent, + expect(policyManager.roles['admin']!.name, equals('admin')); + expect(policyManager.roles['admin']!.allowedContent, containsAll(['read', 'write', 'delete'])); - expect(policyManager.policies['user']!.allowedContent, - containsAll(['read'])); - expect(policyManager.policies['guest']!.allowedContent, isEmpty); + expect( + policyManager.roles['user']!.allowedContent, containsAll(['read'])); + expect(policyManager.roles['guest']!.allowedContent, isEmpty); }); test('should persist policies to storage during initialization', @@ -59,7 +59,7 @@ void main() { }; await policyManager.initialize(initialPolicies); - expect(policyManager.policies.length, equals(2)); + expect(policyManager.roles.length, equals(2)); // Updated policies final updatedPolicies = { @@ -68,12 +68,12 @@ void main() { }; await policyManager.initialize(updatedPolicies); - expect(policyManager.policies.length, equals(2)); - expect(policyManager.policies['admin']!.allowedContent, + expect(policyManager.roles.length, equals(2)); + expect(policyManager.roles['admin']!.allowedContent, containsAll(['read', 'write', 'delete'])); - expect(policyManager.policies['moderator']!.allowedContent, + expect(policyManager.roles['moderator']!.allowedContent, containsAll(['read', 'write'])); - expect(policyManager.policies['user'], isNull); // Should be removed + expect(policyManager.roles['user'], isNull); // Should be removed }); }); @@ -88,7 +88,7 @@ void main() { await policyManager.initialize(jsonPolicies); // Create evaluator with the loaded policies - final evaluator = RoleEvaluator(policyManager.policies); + final evaluator = RoleEvaluator(policyManager.roles); // Test admin permissions expect(evaluator.evaluate('admin', 'read'), isTrue); @@ -115,7 +115,7 @@ void main() { }; await policyManager.initialize(jsonPolicies); - final evaluator = RoleEvaluator(policyManager.policies); + final evaluator = RoleEvaluator(policyManager.roles); expect(evaluator.evaluate('admin', 'read'), isTrue); expect(evaluator.evaluate('admin', 'READ'), isFalse); @@ -128,7 +128,7 @@ void main() { }; await policyManager.initialize(jsonPolicies); - final evaluator = RoleEvaluator(policyManager.policies); + final evaluator = RoleEvaluator(policyManager.roles); expect(evaluator.evaluate('admin', 'read@domain'), isTrue); expect(evaluator.evaluate('admin', 'write-file'), isTrue); @@ -149,12 +149,12 @@ void main() { // Verify storage has the same data final storedPolicies = await storage.loadPolicies(); - expect(storedPolicies.length, equals(policyManager.policies.length)); + expect(storedPolicies.length, equals(policyManager.roles.length)); // Clear storage and verify manager is unaffected await storage.clearPolicies(); expect(await storage.loadPolicies(), isEmpty); - expect(policyManager.policies.length, + expect(policyManager.roles.length, equals(2)); // Manager still has policies }); @@ -264,7 +264,7 @@ void main() { // Should initialize successfully but skip invalid policies expect(policyManager.isInitialized, isTrue); - expect(policyManager.policies, isEmpty); + expect(policyManager.roles, isEmpty); }); test('should handle partial failures gracefully', () async { @@ -278,10 +278,10 @@ void main() { // Should still initialize with valid policies expect(policyManager.isInitialized, isTrue); - expect(policyManager.policies.length, equals(2)); - expect(policyManager.policies['admin'], isNotNull); - expect(policyManager.policies['guest'], isNotNull); - expect(policyManager.policies['user'], isNull); + expect(policyManager.roles.length, equals(2)); + expect(policyManager.roles['admin'], isNotNull); + expect(policyManager.roles['guest'], isNotNull); + expect(policyManager.roles['user'], isNull); }); test('should handle empty policy set', () async { @@ -290,7 +290,7 @@ void main() { await policyManager.initialize(emptyPolicies); expect(policyManager.isInitialized, isTrue); - expect(policyManager.policies, isEmpty); + expect(policyManager.roles, isEmpty); }); }); @@ -306,7 +306,7 @@ void main() { stopwatch.stop(); expect(policyManager.isInitialized, isTrue); - expect(policyManager.policies.length, equals(1000)); + expect(policyManager.roles.length, equals(1000)); expect(stopwatch.elapsedMilliseconds, lessThan(5000)); // Should complete within 5 seconds }); @@ -318,7 +318,7 @@ void main() { }; await policyManager.initialize(jsonPolicies); - final evaluator = RoleEvaluator(policyManager.policies); + final evaluator = RoleEvaluator(policyManager.roles); const iterations = 10000; final stopwatch = Stopwatch()..start(); @@ -350,7 +350,7 @@ void main() { }; await policyManager.initialize(webAppPolicies); - final evaluator = RoleEvaluator(policyManager.policies); + final evaluator = RoleEvaluator(policyManager.roles); // Test role hierarchy expect(evaluator.evaluate('super_admin', 'manage_roles'), isTrue); @@ -369,7 +369,7 @@ void main() { }; await policyManager.initialize(fileSystemPolicies); - final evaluator = RoleEvaluator(policyManager.policies); + final evaluator = RoleEvaluator(policyManager.roles); // Test Unix-like permissions expect(evaluator.evaluate('root', 'chown'), isTrue); @@ -386,7 +386,7 @@ void main() { }; await policyManager.initialize(apiPolicies); - final evaluator = RoleEvaluator(policyManager.policies); + final evaluator = RoleEvaluator(policyManager.roles); // Test REST API permissions expect(evaluator.evaluate('admin', 'DELETE'), isTrue); diff --git a/test/core/policy_manager_test.dart b/test/core/policy_manager_test.dart index d995628..d6e717c 100644 --- a/test/core/policy_manager_test.dart +++ b/test/core/policy_manager_test.dart @@ -3,7 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_policy_engine/src/core/policy_manager.dart'; import 'package:flutter_policy_engine/src/core/interfaces/i_policy_evaluator.dart'; import 'package:flutter_policy_engine/src/core/interfaces/i_policy_storage.dart'; -import 'package:flutter_policy_engine/src/models/policy.dart'; +import 'package:flutter_policy_engine/src/models/role.dart'; import 'package:flutter_policy_engine/src/utils/json_handler.dart'; import 'package:flutter_policy_engine/src/exceptions/json_parse_exception.dart'; @@ -65,11 +65,10 @@ class MockPolicyEvaluator implements IPolicyEvaluator { } } -class ThrowingPolicy extends Policy { - const ThrowingPolicy( - {required super.roleName, required super.allowedContent}); +class ThrowingPolicy extends Role { + const ThrowingPolicy({required super.name, required super.allowedContent}); - static Policy fromJson(Map json) { + static Role fromJson(Map json) { throw StateError('Forced error in fromJson'); } } @@ -95,7 +94,7 @@ void main() { final manager = PolicyManager(); expect(manager, isA()); expect(manager.isInitialized, isFalse); - expect(manager.policies, isEmpty); + expect(manager.roles, isEmpty); }); test('should create instance with custom storage and evaluator', () { @@ -105,7 +104,7 @@ void main() { ); expect(manager, isA()); expect(manager.isInitialized, isFalse); - expect(manager.policies, isEmpty); + expect(manager.roles, isEmpty); }); test('should extend ChangeNotifier', () { @@ -119,13 +118,13 @@ void main() { }); test('should have empty policies by default', () { - expect(policyManager.policies, isEmpty); + expect(policyManager.roles, isEmpty); }); test('should return unmodifiable policies map', () { expect( - () => policyManager.policies['test'] = - const Policy(roleName: 'test', allowedContent: ['read']), + () => policyManager.roles['test'] = + const Role(name: 'test', allowedContent: ['read']), throwsA(isA())); }); }); @@ -140,13 +139,13 @@ void main() { await policyManager.initialize(jsonPolicies); expect(policyManager.isInitialized, isTrue); - expect(policyManager.policies.length, equals(2)); - expect(policyManager.policies['admin'], isA()); - expect(policyManager.policies['user'], isA()); - expect(policyManager.policies['admin']!.allowedContent, + expect(policyManager.roles.length, equals(2)); + expect(policyManager.roles['admin'], isA()); + expect(policyManager.roles['user'], isA()); + expect(policyManager.roles['admin']!.allowedContent, containsAll(['read', 'write', 'delete'])); - expect(policyManager.policies['user']!.allowedContent, - containsAll(['read'])); + expect( + policyManager.roles['user']!.allowedContent, containsAll(['read'])); }); test('should handle empty policies gracefully', () async { @@ -155,7 +154,7 @@ void main() { await policyManager.initialize(jsonPolicies); expect(policyManager.isInitialized, isTrue); - expect(policyManager.policies, isEmpty); + expect(policyManager.roles, isEmpty); }); test('should handle single policy', () async { @@ -166,9 +165,9 @@ void main() { await policyManager.initialize(jsonPolicies); expect(policyManager.isInitialized, isTrue); - expect(policyManager.policies.length, equals(1)); - expect(policyManager.policies['admin']!.roleName, equals('admin')); - expect(policyManager.policies['admin']!.allowedContent, + expect(policyManager.roles.length, equals(1)); + expect(policyManager.roles['admin']!.name, equals('admin')); + expect(policyManager.roles['admin']!.allowedContent, containsAll(['read', 'write'])); }); @@ -181,7 +180,7 @@ void main() { await policyManager.initialize(jsonPolicies); expect(mockStorage.storedPolicies.length, equals(1)); - expect(mockStorage.storedPolicies['admin'], isA()); + expect(mockStorage.storedPolicies['admin'], isA()); }); test('should notify listeners after initialization', () async { @@ -219,7 +218,7 @@ void main() { // Should initialize successfully but skip invalid policies expect(policyManager.isInitialized, isTrue); - expect(policyManager.policies, isEmpty); + expect(policyManager.roles, isEmpty); }); test('should handle null values in JSON data gracefully', () async { @@ -231,7 +230,7 @@ void main() { // Should initialize successfully but skip null policies expect(policyManager.isInitialized, isTrue); - expect(policyManager.policies, isEmpty); + expect(policyManager.roles, isEmpty); }); test( @@ -247,10 +246,10 @@ void main() { // Should still initialize with valid policies expect(policyManager.isInitialized, isTrue); - expect(policyManager.policies.length, equals(2)); - expect(policyManager.policies['admin'], isNotNull); - expect(policyManager.policies['guest'], isNotNull); - expect(policyManager.policies['user'], isNull); + expect(policyManager.roles.length, equals(2)); + expect(policyManager.roles['admin'], isNotNull); + expect(policyManager.roles['guest'], isNotNull); + expect(policyManager.roles['user'], isNull); }); test('should create RoleEvaluator when policies are available', () async { @@ -273,20 +272,19 @@ void main() { await policyManager.initialize(jsonPolicies); expect(policyManager.isInitialized, isTrue); - expect(policyManager.policies, isEmpty); + expect(policyManager.roles, isEmpty); }); test('should handle exception in Policy.fromJson during initialization', () async { // Prepare a validPolicies map that will be passed to parseMap final validPolicies = { - 'admin': - const Policy(roleName: 'admin', allowedContent: ['read', 'write']) - .toJson(), + 'admin': const Role(name: 'admin', allowedContent: ['read', 'write']) + .toJson(), }; expect( - () => JsonHandler.parseMap( + () => JsonHandler.parseMap( validPolicies, (json) => ThrowingPolicy.fromJson(json), allowPartialSuccess: false, @@ -306,7 +304,7 @@ void main() { await policyManager.initialize(jsonPolicies); expect(policyManager.isInitialized, isTrue); - expect(policyManager.policies.length, equals(1000)); + expect(policyManager.roles.length, equals(1000)); }); test('should handle policies with empty allowed content', () async { @@ -317,7 +315,7 @@ void main() { await policyManager.initialize(jsonPolicies); expect(policyManager.isInitialized, isTrue); - expect(policyManager.policies['admin']!.allowedContent, isEmpty); + expect(policyManager.roles['admin']!.allowedContent, isEmpty); }); test('should handle policies with duplicate content', () async { @@ -328,7 +326,7 @@ void main() { await policyManager.initialize(jsonPolicies); expect(policyManager.isInitialized, isTrue); - expect(policyManager.policies['admin']!.allowedContent, + expect(policyManager.roles['admin']!.allowedContent, containsAll(['read', 'write'])); }); }); @@ -362,7 +360,7 @@ void main() { await Future.wait(futures); expect(policyManager.isInitialized, isTrue); - expect(policyManager.policies.length, equals(1)); + expect(policyManager.roles.length, equals(1)); }); test('should handle policies with non-string content items', () async { @@ -374,7 +372,7 @@ void main() { // Should initialize successfully but skip invalid policies expect(policyManager.isInitialized, isTrue); - expect(policyManager.policies, isEmpty); + expect(policyManager.roles, isEmpty); }); test('should handle policies with non-list values', () async { @@ -386,7 +384,7 @@ void main() { // Should initialize successfully but skip invalid policies expect(policyManager.isInitialized, isTrue); - expect(policyManager.policies, isEmpty); + expect(policyManager.roles, isEmpty); }); test('should handle complete initialization failure gracefully', @@ -401,7 +399,7 @@ void main() { // Should still mark as initialized even with no valid policies expect(policyManager.isInitialized, isTrue); - expect(policyManager.policies, isEmpty); + expect(policyManager.roles, isEmpty); }); test('should handle hasAccess when not initialized', () { @@ -448,5 +446,447 @@ void main() { ); }); }); + + group('addRole', () { + test('should add a new role successfully', () async { + await policyManager.initialize({}); + + const newRole = Role(name: 'editor', allowedContent: ['read', 'write']); + await policyManager.addRole(newRole); + + expect(policyManager.roles['editor'], equals(newRole)); + expect(policyManager.roles.length, equals(1)); + }); + + test('should overwrite existing role with same name', () async { + await policyManager.initialize({ + 'admin': ['read', 'write'], + }); + + const updatedRole = + Role(name: 'admin', allowedContent: ['read', 'write', 'delete']); + await policyManager.addRole(updatedRole); + + expect(policyManager.roles['admin'], equals(updatedRole)); + expect(policyManager.roles['admin']!.allowedContent, + containsAll(['read', 'write', 'delete'])); + expect(policyManager.roles.length, equals(1)); + }); + + test('should throw ArgumentError for role with empty name', () async { + await policyManager.initialize({}); + + const invalidRole = Role(name: '', allowedContent: ['read']); + + expect( + () => policyManager.addRole(invalidRole), + throwsA(isA()), + ); + }); + + test('should save role to storage after adding', () async { + await policyManager.initialize({}); + + const newRole = Role(name: 'editor', allowedContent: ['read', 'write']); + await policyManager.addRole(newRole); + + expect(mockStorage.storedPolicies['editor'], equals(newRole)); + }); + + test('should notify listeners after adding role', () async { + await policyManager.initialize({}); + + bool listenerCalled = false; + policyManager.addListener(() { + listenerCalled = true; + }); + + const newRole = Role(name: 'editor', allowedContent: ['read', 'write']); + await policyManager.addRole(newRole); + + expect(listenerCalled, isTrue); + }); + + test('should update evaluator after adding role', () async { + await policyManager.initialize({}); + + const newRole = Role(name: 'editor', allowedContent: ['read', 'write']); + await policyManager.addRole(newRole); + + // The evaluator should be updated and functional + expect(policyManager.hasAccess('editor', 'read'), isTrue); + expect(policyManager.hasAccess('editor', 'write'), isTrue); + expect(policyManager.hasAccess('editor', 'delete'), isFalse); + }); + + test('should handle storage errors when adding role', () async { + await policyManager.initialize({}); + mockStorage.setShouldThrowOnSave(true); + + const newRole = Role(name: 'editor', allowedContent: ['read', 'write']); + + expect( + () => policyManager.addRole(newRole), + throwsA(isA()), + ); + }); + + test('should handle multiple role additions', () async { + await policyManager.initialize({}); + + const role1 = + Role(name: 'admin', allowedContent: ['read', 'write', 'delete']); + const role2 = Role(name: 'user', allowedContent: ['read']); + const role3 = Role(name: 'guest', allowedContent: ['read']); + + await policyManager.addRole(role1); + await policyManager.addRole(role2); + await policyManager.addRole(role3); + + expect(policyManager.roles.length, equals(3)); + expect(policyManager.roles['admin'], equals(role1)); + expect(policyManager.roles['user'], equals(role2)); + expect(policyManager.roles['guest'], equals(role3)); + }); + }); + + group('removeRole', () { + test('should remove existing role successfully', () async { + await policyManager.initialize({ + 'admin': ['read', 'write'], + 'user': ['read'], + }); + + await policyManager.removeRole('admin'); + + expect(policyManager.roles['admin'], isNull); + expect(policyManager.roles['user'], isNotNull); + expect(policyManager.roles.length, equals(1)); + }); + + test('should complete successfully when removing non-existent role', + () async { + await policyManager.initialize({ + 'admin': ['read', 'write'], + }); + + await policyManager.removeRole('non-existent'); + + expect(policyManager.roles['admin'], isNotNull); + expect(policyManager.roles.length, equals(1)); + }); + + test('should throw ArgumentError for empty role name', () async { + await policyManager.initialize({ + 'admin': ['read', 'write'], + }); + + expect( + () => policyManager.removeRole(''), + throwsA(isA()), + ); + }); + + test('should save updated policies to storage after removal', () async { + await policyManager.initialize({ + 'admin': ['read', 'write'], + 'user': ['read'], + }); + + await policyManager.removeRole('admin'); + + expect(mockStorage.storedPolicies['admin'], isNull); + expect(mockStorage.storedPolicies['user'], isNotNull); + expect(mockStorage.storedPolicies.length, equals(1)); + }); + + test('should notify listeners after removing role', () async { + await policyManager.initialize({ + 'admin': ['read', 'write'], + 'user': ['read'], + }); + + bool listenerCalled = false; + policyManager.addListener(() { + listenerCalled = true; + }); + + await policyManager.removeRole('admin'); + + expect(listenerCalled, isTrue); + }); + + test('should update evaluator after removing role', () async { + await policyManager.initialize({ + 'admin': ['read', 'write'], + 'user': ['read'], + }); + + await policyManager.removeRole('admin'); + + // The evaluator should be updated and reflect the removal + expect(policyManager.hasAccess('admin', 'read'), isFalse); + expect(policyManager.hasAccess('user', 'read'), isTrue); + }); + + test('should handle storage errors when removing role', () async { + await policyManager.initialize({ + 'admin': ['read', 'write'], + }); + + mockStorage.setShouldThrowOnSave(true); + + expect( + () => policyManager.removeRole('admin'), + throwsA(isA()), + ); + }); + + test('should handle removing last role', () async { + await policyManager.initialize({ + 'admin': ['read', 'write'], + }); + + await policyManager.removeRole('admin'); + + expect(policyManager.roles, isEmpty); + expect(policyManager.hasAccess('admin', 'read'), isFalse); + }); + + test('should handle multiple role removals', () async { + await policyManager.initialize({ + 'admin': ['read', 'write'], + 'user': ['read'], + 'guest': ['read'], + }); + + await policyManager.removeRole('admin'); + await policyManager.removeRole('user'); + + expect(policyManager.roles.length, equals(1)); + expect(policyManager.roles['guest'], isNotNull); + expect(policyManager.roles['admin'], isNull); + expect(policyManager.roles['user'], isNull); + }); + }); + + group('updateRole', () { + test('should update existing role successfully', () async { + await policyManager.initialize({ + 'admin': ['read', 'write'], + }); + + const updatedRole = + Role(name: 'admin', allowedContent: ['read', 'write', 'delete']); + await policyManager.updateRole('admin', updatedRole); + + expect(policyManager.roles['admin'], equals(updatedRole)); + expect(policyManager.roles['admin']!.allowedContent, + containsAll(['read', 'write', 'delete'])); + }); + + test('should add new role when updating non-existent role', () async { + await policyManager.initialize({ + 'admin': ['read', 'write'], + }); + + const newRole = Role(name: 'editor', allowedContent: ['read', 'write']); + await policyManager.updateRole('editor', newRole); + + expect(policyManager.roles['editor'], equals(newRole)); + expect(policyManager.roles.length, equals(2)); + expect(policyManager.roles['admin'], isNotNull); + }); + + test('should throw ArgumentError for empty role name', () async { + await policyManager.initialize({ + 'admin': ['read', 'write'], + }); + + const role = Role(name: '', allowedContent: ['read']); + + expect( + () => policyManager.updateRole('', role), + throwsA(isA()), + ); + }); + + test('should throw ArgumentError for role with empty name', () async { + await policyManager.initialize({ + 'admin': ['read', 'write'], + }); + + const invalidRole = Role(name: '', allowedContent: ['read']); + + expect( + () => policyManager.updateRole('', invalidRole), + throwsA(isA()), + ); + }); + + test('should save updated policies to storage', () async { + await policyManager.initialize({ + 'admin': ['read', 'write'], + }); + + const updatedRole = + Role(name: 'admin', allowedContent: ['read', 'write', 'delete']); + await policyManager.updateRole('admin', updatedRole); + + expect(mockStorage.storedPolicies['admin'], equals(updatedRole)); + }); + + test('should notify listeners after updating role', () async { + await policyManager.initialize({ + 'admin': ['read', 'write'], + }); + + bool listenerCalled = false; + policyManager.addListener(() { + listenerCalled = true; + }); + + const updatedRole = + Role(name: 'admin', allowedContent: ['read', 'write', 'delete']); + await policyManager.updateRole('admin', updatedRole); + + expect(listenerCalled, isTrue); + }); + + test('should update evaluator after updating role', () async { + await policyManager.initialize({ + 'admin': ['read', 'write'], + }); + + const updatedRole = + Role(name: 'admin', allowedContent: ['read', 'write', 'delete']); + await policyManager.updateRole('admin', updatedRole); + + // The evaluator should be updated and reflect the changes + expect(policyManager.hasAccess('admin', 'read'), isTrue); + expect(policyManager.hasAccess('admin', 'write'), isTrue); + expect(policyManager.hasAccess('admin', 'delete'), isTrue); + expect(policyManager.hasAccess('admin', 'execute'), isFalse); + }); + + test('should handle storage errors when updating role', () async { + await policyManager.initialize({ + 'admin': ['read', 'write'], + }); + + mockStorage.setShouldThrowOnSave(true); + const updatedRole = + Role(name: 'admin', allowedContent: ['read', 'write', 'delete']); + + expect( + () => policyManager.updateRole('admin', updatedRole), + throwsA(isA()), + ); + }); + + test('should handle updating role with different name in parameter', + () async { + await policyManager.initialize({ + 'admin': ['read', 'write'], + }); + + const updatedRole = Role( + name: 'superadmin', allowedContent: ['read', 'write', 'delete']); + await policyManager.updateRole('admin', updatedRole); + + // Should update the role at 'admin' key with the new role data + expect(policyManager.roles['admin'], equals(updatedRole)); + expect(policyManager.roles['admin']!.name, equals('superadmin')); + expect( + policyManager.roles['superadmin'], isNull); // Key remains 'admin' + }); + + test('should handle multiple role updates', () async { + await policyManager.initialize({ + 'admin': ['read', 'write'], + 'user': ['read'], + }); + + const updatedAdmin = + Role(name: 'admin', allowedContent: ['read', 'write', 'delete']); + const updatedUser = + Role(name: 'user', allowedContent: ['read', 'write']); + + await policyManager.updateRole('admin', updatedAdmin); + await policyManager.updateRole('user', updatedUser); + + expect(policyManager.roles['admin'], equals(updatedAdmin)); + expect(policyManager.roles['user'], equals(updatedUser)); + expect(policyManager.roles.length, equals(2)); + }); + }); + + group('Listener notifications', () { + test('should notify listeners on addRole', () async { + await policyManager.initialize({}); + + int notificationCount = 0; + policyManager.addListener(() { + notificationCount++; + }); + + const role = Role(name: 'admin', allowedContent: ['read']); + await policyManager.addRole(role); + + expect(notificationCount, equals(1)); + }); + + test('should notify listeners on removeRole', () async { + await policyManager.initialize({ + 'admin': ['read', 'write'], + }); + + int notificationCount = 0; + policyManager.addListener(() { + notificationCount++; + }); + + await policyManager.removeRole('admin'); + + expect(notificationCount, equals(1)); + }); + + test('should notify listeners on updateRole', () async { + await policyManager.initialize({ + 'admin': ['read', 'write'], + }); + + int notificationCount = 0; + policyManager.addListener(() { + notificationCount++; + }); + + const updatedRole = + Role(name: 'admin', allowedContent: ['read', 'write', 'delete']); + await policyManager.updateRole('admin', updatedRole); + + expect(notificationCount, equals(1)); + }); + + test('should notify multiple listeners', () async { + await policyManager.initialize({}); + + int notificationCount1 = 0; + int notificationCount2 = 0; + + policyManager.addListener(() { + notificationCount1++; + }); + policyManager.addListener(() { + notificationCount2++; + }); + + const role = Role(name: 'admin', allowedContent: ['read']); + await policyManager.addRole(role); + + expect(notificationCount1, equals(1)); + expect(notificationCount2, equals(1)); + }); + }); }); } diff --git a/test/core/role_evaluator_test.dart b/test/core/role_evaluator_test.dart index db148fe..04734f5 100644 --- a/test/core/role_evaluator_test.dart +++ b/test/core/role_evaluator_test.dart @@ -1,26 +1,26 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_policy_engine/src/core/role_evaluator.dart'; -import 'package:flutter_policy_engine/src/models/policy.dart'; +import 'package:flutter_policy_engine/src/models/role.dart'; void main() { group('RoleEvaluator', () { - late Map policies; + late Map policies; late RoleEvaluator evaluator; setUp(() { policies = { - 'admin': const Policy( - roleName: 'admin', + 'admin': const Role( + name: 'admin', allowedContent: ['read', 'write', 'delete'], metadata: {'level': 'high'}, ), - 'user': const Policy( - roleName: 'user', + 'user': const Role( + name: 'user', allowedContent: ['read'], metadata: {'level': 'normal'}, ), - 'guest': const Policy( - roleName: 'guest', + 'guest': const Role( + name: 'guest', allowedContent: [], metadata: {'level': 'low'}, ), @@ -85,8 +85,8 @@ void main() { }); test('should handle duplicate content in allowed list', () { - const duplicatePolicy = Policy( - roleName: 'duplicate_role', + const duplicatePolicy = Role( + name: 'duplicate_role', allowedContent: ['read', 'read', 'write'], ); const duplicateEvaluator = @@ -99,8 +99,8 @@ void main() { }); test('should handle special characters in content', () { - const specialPolicy = Policy( - roleName: 'special_role', + const specialPolicy = Role( + name: 'special_role', allowedContent: ['read@domain', 'write-file', 'delete_user'], ); const specialEvaluator = RoleEvaluator({'special_role': specialPolicy}); @@ -114,8 +114,8 @@ void main() { }); test('should handle whitespace in content', () { - const whitespacePolicy = Policy( - roleName: 'whitespace_role', + const whitespacePolicy = Role( + name: 'whitespace_role', allowedContent: ['read file', 'write document', 'delete record'], ); const whitespaceEvaluator = @@ -136,8 +136,8 @@ void main() { group('Edge cases', () { test('should handle very long content strings', () { final longContent = 'a' * 10000; - final longPolicy = Policy( - roleName: 'long_role', + final longPolicy = Role( + name: 'long_role', allowedContent: [longContent], ); final longEvaluator = RoleEvaluator({'long_role': longPolicy}); @@ -149,8 +149,8 @@ void main() { test('should handle very long role names', () { final longRoleName = 'a' * 1000; - final longRolePolicy = Policy( - roleName: longRoleName, + final longRolePolicy = Role( + name: longRoleName, allowedContent: const ['read'], ); final longRoleEvaluator = RoleEvaluator({longRoleName: longRolePolicy}); @@ -161,8 +161,8 @@ void main() { test('should handle large number of allowed content items', () { final largeContentList = List.generate(1000, (i) => 'content_$i'); - final largePolicy = Policy( - roleName: 'large_role', + final largePolicy = Role( + name: 'large_role', allowedContent: largeContentList, ); final largeEvaluator = RoleEvaluator({'large_role': largePolicy}); @@ -174,8 +174,8 @@ void main() { }); test('should handle unicode characters in content', () { - const unicodePolicy = Policy( - roleName: 'unicode_role', + const unicodePolicy = Role( + name: 'unicode_role', allowedContent: ['cafรฉ', 'naรฏve', 'rรฉsumรฉ', 'รผber'], ); const unicodeEvaluator = RoleEvaluator({'unicode_role': unicodePolicy}); @@ -188,8 +188,8 @@ void main() { }); test('should handle numbers as content', () { - const numberPolicy = Policy( - roleName: 'number_role', + const numberPolicy = Role( + name: 'number_role', allowedContent: ['123', '456', '789'], ); const numberEvaluator = RoleEvaluator({'number_role': numberPolicy}); @@ -201,8 +201,8 @@ void main() { }); test('should handle mixed content types', () { - const mixedPolicy = Policy( - roleName: 'mixed_role', + const mixedPolicy = Role( + name: 'mixed_role', allowedContent: ['read', '123', 'cafรฉ', 'read@domain', ''], ); const mixedEvaluator = RoleEvaluator({'mixed_role': mixedPolicy}); @@ -231,10 +231,10 @@ void main() { }); test('should handle evaluation with large policy set', () { - final largePolicies = {}; + final largePolicies = {}; for (int i = 0; i < 1000; i++) { - largePolicies['role_$i'] = Policy( - roleName: 'role_$i', + largePolicies['role_$i'] = Role( + name: 'role_$i', allowedContent: const ['read', 'write'], ); } @@ -256,8 +256,8 @@ void main() { group('Integration scenarios', () { test('should handle typical RBAC scenario', () { final rbacPolicies = { - 'super_admin': const Policy( - roleName: 'super_admin', + 'super_admin': const Role( + name: 'super_admin', allowedContent: [ 'read', 'write', @@ -266,20 +266,20 @@ void main() { 'manage_users' ], ), - 'admin': const Policy( - roleName: 'admin', + 'admin': const Role( + name: 'admin', allowedContent: ['read', 'write', 'delete'], ), - 'moderator': const Policy( - roleName: 'moderator', + 'moderator': const Role( + name: 'moderator', allowedContent: ['read', 'write'], ), - 'user': const Policy( - roleName: 'user', + 'user': const Role( + name: 'user', allowedContent: ['read'], ), - 'guest': const Policy( - roleName: 'guest', + 'guest': const Role( + name: 'guest', allowedContent: [], ), }; @@ -305,8 +305,8 @@ void main() { test('should handle file system permissions scenario', () { final filePolicies = { - 'root': const Policy( - roleName: 'root', + 'root': const Role( + name: 'root', allowedContent: [ 'read', 'write', @@ -316,16 +316,16 @@ void main() { 'chown' ], ), - 'owner': const Policy( - roleName: 'owner', + 'owner': const Role( + name: 'owner', allowedContent: ['read', 'write', 'delete', 'chmod'], ), - 'group': const Policy( - roleName: 'group', + 'group': const Role( + name: 'group', allowedContent: ['read', 'write'], ), - 'other': const Policy( - roleName: 'other', + 'other': const Role( + name: 'other', allowedContent: ['read'], ), }; diff --git a/test/models/policy_test.dart b/test/models/role_test.dart similarity index 77% rename from test/models/policy_test.dart rename to test/models/role_test.dart index 80ffc62..60c8c32 100644 --- a/test/models/policy_test.dart +++ b/test/models/role_test.dart @@ -1,75 +1,75 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_policy_engine/src/models/policy.dart'; +import 'package:flutter_policy_engine/src/models/role.dart'; void main() { - group('Policy', () { + group('Role', () { group('Constructor', () { test('should create instance with required parameters', () { - const policy = Policy( - roleName: 'admin', + const role = Role( + name: 'admin', allowedContent: ['read', 'write'], ); - expect(policy.roleName, equals('admin')); - expect(policy.allowedContent, equals(['read', 'write'])); - expect(policy.metadata, equals({})); + expect(role.name, equals('admin')); + expect(role.allowedContent, equals(['read', 'write'])); + expect(role.metadata, equals({})); }); test('should create instance with all parameters', () { final metadata = {'level': 'high', 'department': 'IT'}; - final policy = Policy( - roleName: 'admin', + final policy = Role( + name: 'admin', allowedContent: const ['read', 'write', 'delete'], metadata: metadata, ); - expect(policy.roleName, equals('admin')); + expect(policy.name, equals('admin')); expect(policy.allowedContent, equals(['read', 'write', 'delete'])); expect(policy.metadata, equals(metadata)); }); test('should create immutable instance', () { - const policy = Policy( - roleName: 'admin', + const policy = Role( + name: 'admin', allowedContent: ['read'], ); - expect(policy, isA()); + expect(policy, isA()); // Verify it's const-constructible - const policyConst = Policy( - roleName: 'admin', + const policyConst = Role( + name: 'admin', allowedContent: ['read'], ); - expect(policyConst, isA()); + expect(policyConst, isA()); }); test('should handle empty allowed content', () { - const policy = Policy( - roleName: 'guest', + const policy = Role( + name: 'guest', allowedContent: [], ); - expect(policy.roleName, equals('guest')); + expect(policy.name, equals('guest')); expect(policy.allowedContent, isEmpty); }); test('should handle empty role name', () { - const policy = Policy( - roleName: '', + const policy = Role( + name: '', allowedContent: ['read'], ); - expect(policy.roleName, equals('')); + expect(policy.name, equals('')); expect(policy.allowedContent, equals(['read'])); }); }); group('isContentAllowed', () { - late Policy policy; + late Role policy; setUp(() { - policy = const Policy( - roleName: 'admin', + policy = const Role( + name: 'admin', allowedContent: ['read', 'write', 'delete'], ); }); @@ -97,8 +97,8 @@ void main() { }); test('should handle policy with empty allowed content', () { - const emptyPolicy = Policy( - roleName: 'guest', + const emptyPolicy = Role( + name: 'guest', allowedContent: [], ); @@ -108,8 +108,8 @@ void main() { }); test('should handle duplicate content in allowed list', () { - const duplicatePolicy = Policy( - roleName: 'duplicate_role', + const duplicatePolicy = Role( + name: 'duplicate_role', allowedContent: ['read', 'read', 'write'], ); @@ -119,8 +119,8 @@ void main() { }); test('should handle special characters in content', () { - const specialPolicy = Policy( - roleName: 'special_role', + const specialPolicy = Role( + name: 'special_role', allowedContent: ['read@domain', 'write-file', 'delete_user'], ); @@ -131,8 +131,8 @@ void main() { }); test('should handle whitespace in content', () { - const whitespacePolicy = Policy( - roleName: 'whitespace_role', + const whitespacePolicy = Role( + name: 'whitespace_role', allowedContent: ['read file', 'write document', 'delete record'], ); @@ -144,11 +144,11 @@ void main() { }); group('copyWith', () { - late Policy originalPolicy; + late Role originalPolicy; setUp(() { - originalPolicy = const Policy( - roleName: 'admin', + originalPolicy = const Role( + name: 'admin', allowedContent: ['read', 'write'], metadata: {'level': 'high'}, ); @@ -158,16 +158,16 @@ void main() { () { final copy = originalPolicy.copyWith(); - expect(copy.roleName, equals(originalPolicy.roleName)); + expect(copy.name, equals(originalPolicy.name)); expect(copy.allowedContent, equals(originalPolicy.allowedContent)); expect(copy.metadata, equals(originalPolicy.metadata)); expect(copy, isNot(same(originalPolicy))); }); test('should create copy with updated role name', () { - final copy = originalPolicy.copyWith(roleName: 'super_admin'); + final copy = originalPolicy.copyWith(name: 'super_admin'); - expect(copy.roleName, equals('super_admin')); + expect(copy.name, equals('super_admin')); expect(copy.allowedContent, equals(originalPolicy.allowedContent)); expect(copy.metadata, equals(originalPolicy.metadata)); }); @@ -176,7 +176,7 @@ void main() { final newContent = ['read', 'write', 'delete']; final copy = originalPolicy.copyWith(allowedContent: newContent); - expect(copy.roleName, equals(originalPolicy.roleName)); + expect(copy.name, equals(originalPolicy.name)); expect(copy.allowedContent, equals(newContent)); expect(copy.metadata, equals(originalPolicy.metadata)); }); @@ -185,31 +185,31 @@ void main() { final newMetadata = {'level': 'low', 'department': 'IT'}; final copy = originalPolicy.copyWith(metadata: newMetadata); - expect(copy.roleName, equals(originalPolicy.roleName)); + expect(copy.name, equals(originalPolicy.name)); expect(copy.allowedContent, equals(originalPolicy.allowedContent)); expect(copy.metadata, equals(newMetadata)); }); test('should create copy with multiple updated fields', () { final copy = originalPolicy.copyWith( - roleName: 'user', + name: 'user', allowedContent: ['read'], metadata: {'level': 'normal'}, ); - expect(copy.roleName, equals('user')); + expect(copy.name, equals('user')); expect(copy.allowedContent, equals(['read'])); expect(copy.metadata, equals({'level': 'normal'})); }); test('should handle empty values in copyWith', () { final copy = originalPolicy.copyWith( - roleName: '', + name: '', allowedContent: [], metadata: {}, ); - expect(copy.roleName, equals('')); + expect(copy.name, equals('')); expect(copy.allowedContent, isEmpty); expect(copy.metadata, isEmpty); }); @@ -217,8 +217,8 @@ void main() { group('Equality', () { test('should be equal to itself', () { - const policy = Policy( - roleName: 'admin', + const policy = Role( + name: 'admin', allowedContent: ['read', 'write'], ); @@ -227,13 +227,13 @@ void main() { }); test('should be equal to policy with same values', () { - const policy1 = Policy( - roleName: 'admin', + const policy1 = Role( + name: 'admin', allowedContent: ['read', 'write'], metadata: {'level': 'high'}, ); - const policy2 = Policy( - roleName: 'admin', + const policy2 = Role( + name: 'admin', allowedContent: ['read', 'write'], metadata: {'level': 'high'}, ); @@ -243,12 +243,12 @@ void main() { }); test('should not be equal to policy with different role name', () { - const policy1 = Policy( - roleName: 'admin', + const policy1 = Role( + name: 'admin', allowedContent: ['read', 'write'], ); - const policy2 = Policy( - roleName: 'user', + const policy2 = Role( + name: 'user', allowedContent: ['read', 'write'], ); @@ -257,12 +257,12 @@ void main() { }); test('should not be equal to policy with different allowed content', () { - const policy1 = Policy( - roleName: 'admin', + const policy1 = Role( + name: 'admin', allowedContent: ['read', 'write'], ); - const policy2 = Policy( - roleName: 'admin', + const policy2 = Role( + name: 'admin', allowedContent: ['read', 'delete'], ); @@ -271,12 +271,12 @@ void main() { }); test('should be equal regardless of content order', () { - const policy1 = Policy( - roleName: 'admin', + const policy1 = Role( + name: 'admin', allowedContent: ['read', 'write'], ); - const policy2 = Policy( - roleName: 'admin', + const policy2 = Role( + name: 'admin', allowedContent: ['write', 'read'], ); @@ -285,8 +285,8 @@ void main() { }); test('should not be equal to different object types', () { - const policy = Policy( - roleName: 'admin', + const policy = Role( + name: 'admin', allowedContent: ['read'], ); @@ -296,13 +296,13 @@ void main() { }); test('should not consider metadata in equality', () { - const policy1 = Policy( - roleName: 'admin', + const policy1 = Role( + name: 'admin', allowedContent: ['read', 'write'], metadata: {'level': 'high'}, ); - const policy2 = Policy( - roleName: 'admin', + const policy2 = Role( + name: 'admin', allowedContent: ['read', 'write'], metadata: {'level': 'low'}, ); @@ -314,8 +314,8 @@ void main() { group('toString', () { test('should return meaningful string representation', () { - const policy = Policy( - roleName: 'admin', + const policy = Role( + name: 'admin', allowedContent: ['read', 'write'], metadata: {'level': 'high'}, ); @@ -328,8 +328,8 @@ void main() { }); test('should handle empty values in string representation', () { - const policy = Policy( - roleName: '', + const policy = Role( + name: '', allowedContent: [], metadata: {}, ); @@ -343,8 +343,8 @@ void main() { group('JSON serialization', () { test('should serialize to JSON correctly', () { - const policy = Policy( - roleName: 'admin', + const policy = Role( + name: 'admin', allowedContent: ['read', 'write', 'delete'], metadata: {'level': 'high', 'department': 'IT'}, ); @@ -364,9 +364,9 @@ void main() { 'metadata': {'level': 'normal'}, }; - final policy = Policy.fromJson(json); + final policy = Role.fromJson(json); - expect(policy.roleName, equals('user')); + expect(policy.name, equals('user')); expect(policy.allowedContent, equals(['read'])); expect(policy.metadata, equals({'level': 'normal'})); }); @@ -378,9 +378,9 @@ void main() { 'metadata': {}, }; - final policy = Policy.fromJson(json); + final policy = Role.fromJson(json); - expect(policy.roleName, equals('')); + expect(policy.name, equals('')); expect(policy.allowedContent, isEmpty); expect(policy.metadata, isEmpty); }); @@ -391,22 +391,22 @@ void main() { 'allowedContent': ['read', 'write'], }; - final policy = Policy.fromJson(json); + final policy = Role.fromJson(json); - expect(policy.roleName, equals('admin')); + expect(policy.name, equals('admin')); expect(policy.allowedContent, equals(['read', 'write'])); expect(policy.metadata, equals({})); }); test('should round-trip through JSON correctly', () { - const originalPolicy = Policy( - roleName: 'admin', + const originalPolicy = Role( + name: 'admin', allowedContent: ['read', 'write', 'delete'], metadata: {'level': 'high', 'department': 'IT'}, ); final json = originalPolicy.toJson(); - final deserializedPolicy = Policy.fromJson(json); + final deserializedPolicy = Role.fromJson(json); expect(deserializedPolicy, equals(originalPolicy)); }); @@ -429,7 +429,7 @@ void main() { 'metadata': complexMetadata, }; - final policy = Policy.fromJson(json); + final policy = Role.fromJson(json); expect(policy.metadata, equals(complexMetadata)); expect( @@ -442,19 +442,19 @@ void main() { group('Edge cases', () { test('should handle very long role names', () { final longRoleName = 'a' * 1000; - final policy = Policy( - roleName: longRoleName, + final policy = Role( + name: longRoleName, allowedContent: const ['read'], ); - expect(policy.roleName, equals(longRoleName)); + expect(policy.name, equals(longRoleName)); expect(policy.isContentAllowed('read'), isTrue); }); test('should handle very long content items', () { final longContent = 'a' * 10000; - final policy = Policy( - roleName: 'admin', + final policy = Role( + name: 'admin', allowedContent: [longContent], ); @@ -464,8 +464,8 @@ void main() { test('should handle large number of allowed content items', () { final largeContentList = List.generate(1000, (i) => 'content_$i'); - final policy = Policy( - roleName: 'admin', + final policy = Role( + name: 'admin', allowedContent: largeContentList, ); @@ -476,8 +476,8 @@ void main() { }); test('should handle unicode characters', () { - const policy = Policy( - roleName: 'admin', + const policy = Role( + name: 'admin', allowedContent: ['cafรฉ', 'naรฏve', 'rรฉsumรฉ'], ); @@ -488,8 +488,8 @@ void main() { }); test('should handle numbers as content', () { - const policy = Policy( - roleName: 'admin', + const policy = Role( + name: 'admin', allowedContent: ['123', '456', '789'], ); @@ -508,7 +508,7 @@ void main() { 'metadata': null, }; - expect(() => Policy.fromJson(json), throwsA(isA())); + expect(() => Role.fromJson(json), throwsA(isA())); }); test('should handle missing required fields in JSON', () { @@ -517,7 +517,7 @@ void main() { // missing allowedContent }; - expect(() => Policy.fromJson(json), throwsA(isA())); + expect(() => Role.fromJson(json), throwsA(isA())); }); test('should handle wrong types in JSON', () { @@ -526,7 +526,7 @@ void main() { 'allowedContent': ['read'], }; - expect(() => Policy.fromJson(json), throwsA(isA())); + expect(() => Role.fromJson(json), throwsA(isA())); }); test('should handle non-string items in allowedContent', () { @@ -535,18 +535,18 @@ void main() { 'allowedContent': ['read', 123, 'write'], // contains non-string }; - expect(() => Policy.fromJson(json), throwsA(isA())); + expect(() => Role.fromJson(json), throwsA(isA())); }); }); group('hashCode', () { test('should generate consistent hash codes for equal policies', () { - const policy1 = Policy( - roleName: 'admin', + const policy1 = Role( + name: 'admin', allowedContent: ['read', 'write'], ); - const policy2 = Policy( - roleName: 'admin', + const policy2 = Role( + name: 'admin', allowedContent: ['write', 'read'], // different order ); @@ -554,12 +554,12 @@ void main() { }); test('should generate different hash codes for different policies', () { - const policy1 = Policy( - roleName: 'admin', + const policy1 = Role( + name: 'admin', allowedContent: ['read', 'write'], ); - const policy2 = Policy( - roleName: 'user', + const policy2 = Role( + name: 'user', allowedContent: ['read', 'write'], ); @@ -567,8 +567,8 @@ void main() { }); test('should handle empty allowedContent in hashCode', () { - const policy = Policy( - roleName: 'admin', + const policy = Role( + name: 'admin', allowedContent: [], ); @@ -578,8 +578,8 @@ void main() { test('should handle large allowedContent lists in hashCode', () { final largeContentList = List.generate(100, (i) => 'content_$i'); - final policy = Policy( - roleName: 'admin', + final policy = Role( + name: 'admin', allowedContent: largeContentList, );