diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a5ddcc7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+vendor/
+build/
+composer.lock
+test.db
+.vscode/
+coverage/
+clover.xml
+.phpunit.result.cache
+.runway-config.json
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..4b71739
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 FlightPHP, n0nag0n
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..67be549
--- /dev/null
+++ b/README.md
@@ -0,0 +1,112 @@
+# Flight Permissions Plugin
+[](https://packagist.org/packages/flightphp/permissions)
+[](https://packagist.org/packages/flightphp/permissions)
+[](https://packagist.org/packages/flightphp/permissions)
+[](https://packagist.org/packages/flightphp/permissions)
+
+Permissions are an important part to any application. Even in a RESTful API you'll need to check that the API key has permission to perform the action requested. In some cases it makes sense to handle authentication in a middleware, but in other cases, it's more helpful to have a standard set of permissions.
+
+This library follows a CRUD based permissions systems. See [basic example](#basic-example) for example on how this is accomplished.
+
+## Basic Example
+
+Let's assume you have a feature in your application that checks if a user is logged in. You can create a permissions object like this:
+
+```php
+// index.php
+require 'vendor/autoload.php';
+
+// some code
+
+// then you probably have something that tells you who the current role is of the person
+// likely you have something where you pull the current role
+// from a session variable which defines this
+// after someone logs in, otherwise they will have a 'guest' or 'public' role.
+$current_role = 'admin';
+
+// setup permissions
+$permission = new \flight\Permission($current_role);
+$permission->defineRule('loggedIn', function($current_role) {
+ return $current_role !== 'guest';
+});
+
+// You'll probably want to persist this object in Flight somewhere
+Flight::set('permission', $permission);
+```
+
+Then in a controller somewhere, you might have something like this.
+
+```php
+has('loggedIn')) {
+ // do something
+ } else {
+ // do something else
+ }
+ }
+}
+```
+
+You can also use this to track if they have permission to do something in your application.
+For instance, if your have a way that users can interact with posting on your software, you can
+check if they have permission to perform certain actions.
+
+```php
+$current_role = 'admin';
+
+// setup permissions
+$permission = new \flight\Permission($current_role);
+$permission->defineRule('post', function($current_role) {
+ if($current_role === 'admin') {
+ $permissions = ['create', 'read', 'update', 'delete'];
+ } else if($current_role === 'editor') {
+ $permissions = ['create', 'read', 'update'];
+ } else if($current_role === 'author') {
+ $permissions = ['create', 'read'];
+ } else if($current_role === 'contributor') {
+ $permissions = ['create'];
+ } else {
+ $permissions = [];
+ }
+ return $permissions;
+});
+Flight::set('permission', $permission);
+```
+
+Then in a controller somewhere...
+
+```php
+class PostController {
+ public function create() {
+ $permission = Flight::get('permission');
+ if ($permission->can('post.create')) {
+ // do something
+ } else {
+ // do something else
+ }
+ }
+}
+```
+
+See how much fun this is? Let's install it and get started!
+
+## Installation
+
+Simply install with Composer
+
+```php
+composer require flightphp/permissions
+```
+
+## Documentation
+
+Head over to the [documentation page](https://docs.flightphp.com/awesome-plugins/permissions) to learn more about usage and how cool this thing is! :)
+
+## License
+
+MIT
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..529555f
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,37 @@
+{
+ "name": "flightphp/permissions",
+ "type": "library",
+ "description": "Library for managing permissions in Flight Applications",
+ "keywords": ["permissions", "authentication","lite","simple"],
+ "homepage": "https://docs.flightphp.com",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "n0nag0n",
+ "email": "n0nag0n@sky-9.com",
+ "role": "Owner"
+ }
+ ],
+ "require": {
+ "php": ">=7.4"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.0",
+ "squizlabs/php_codesniffer": "^3.8",
+ "rregeer/phpunit-coverage-check": "^0.3.1",
+ "flightphp/core": "^3.10",
+ "wruczek/php-file-cache": "^0.0.5"
+ },
+ "autoload": {
+ "psr-4": {"flight\\": "src/"}
+ },
+ "autoload-dev": {
+ "psr-4": {"flight\\tests\\": "tests/"}
+ },
+ "scripts": {
+ "test": "phpunit",
+ "test-coverage": "XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html=coverage --coverage-clover=clover.xml && vendor/bin/coverage-check clover.xml 100",
+ "beautify": "phpcbf --standard=phpcs.xml",
+ "phpcs": "phpcs --standard=phpcs.xml"
+ }
+}
diff --git a/phpcs.xml b/phpcs.xml
new file mode 100644
index 0000000..592cabc
--- /dev/null
+++ b/phpcs.xml
@@ -0,0 +1,10 @@
+
+
+ Created with the PHP Coding Standard Generator. http://edorian.github.io/php-coding-standard-generator/
+
+
+
+
+ src/
+ tests/
+
\ No newline at end of file
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..98e037f
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,13 @@
+
+
+
+
+ src/
+
+
+
+
+ tests/
+
+
+
diff --git a/src/Permission.php b/src/Permission.php
new file mode 100644
index 0000000..53d1ed2
--- /dev/null
+++ b/src/Permission.php
@@ -0,0 +1,213 @@
+ */
+ protected $localClassCache = [];
+
+ /**
+ * Constructor
+ *
+ * @param string $currentRole
+ * @param Engine $f3
+ */
+ public function __construct(string $currentRole = '', Engine $app = null, $Cache = null)
+ {
+ $this->currentRole = $currentRole;
+ $this->app = $app === null ? Flight::app() : $app;
+ $this->Cache = $Cache;
+ }
+
+ /**
+ * Sets the current user role
+ *
+ * @param string $currentRole the current role of the logged in user
+ * @return void
+ */
+ public function setCurrentRole(string $currentRole)
+ {
+ $this->currentRole = $currentRole;
+ }
+
+ /**
+ * Gets the current user role
+ *
+ * @return string
+ */
+ public function getCurrentRole(): string
+ {
+ return $this->currentRole;
+ }
+
+ /**
+ * Gets the defined rules
+ *
+ * @return array
+ */
+ public function getRules(): array
+ {
+ return $this->rules;
+ }
+
+ /**
+ * Defines a new rule for a permission
+ *
+ * @param string $rule the generic name of the rule
+ * @param callable|string $callable either a callable, or the name of the Class->method to call that
+ * will return the allowed permissions.
+ * This will return an array of strings, each string being a permission
+ * e.g. return true; would allow access to this permission
+ * return [ 'create', 'read', 'update', 'delete' ] would allow all permissions
+ * return [ 'read' ] would only allow the read permission.
+ * You define what the callable or method returns for allowed permissions
+ * The callable or method will be passed the following parameters:
+ * - $f3: the \Base instance
+ * - $currentRole: the current role of the logged in user
+ * @param bool $overwrite if true, will overwrite any existing rule with the same name
+ * @return void
+ */
+ public function defineRule(string $rule, $callableOrClassString, bool $overwrite = false)
+ {
+ if ($overwrite === false && isset($this->rules[$rule]) === true) {
+ throw new \Exception('Rule already defined: ' . $rule);
+ }
+ $this->rules[$rule] = $callableOrClassString;
+ }
+
+ /**
+ * Defines rules based on the public methods of a class
+ *
+ * @param string $className the name of the class to define rules from
+ * @return void
+ */
+ public function defineRulesFromClassMethods(string $className, int $ttl = 0): void
+ {
+
+ $useCache = false;
+ if ($this->Cache !== null && $ttl > 0) {
+ $useCache = true;
+ $Cache = $this->Cache;
+ $cacheKey = 'flight_permissions_class_methods_' . $className;
+ if (is_a($Cache, PhpFileCache::class) === true) {
+ /** @var PhpFileCache $Cache */
+ $isCached = $Cache->isCached($cacheKey);
+ if ($isCached === true) {
+ $this->rules = $Cache->retrieve($cacheKey);
+ return;
+ }
+ }
+ }
+
+ $reflection = new \ReflectionClass($className);
+ $methods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);
+ $classRules = [];
+ foreach ($methods as $method) {
+ $methodName = $method->getName();
+ if (strpos($methodName, '__') === 0) {
+ continue;
+ }
+ $classRules[$methodName] = $className . '->' . $methodName;
+ }
+
+ if ($useCache === true) {
+ if (is_a($Cache, PhpFileCache::class) === true) {
+ /** @var PhpFileCache $Cache */
+ $Cache->store($cacheKey, $classRules, $ttl);
+ }
+ }
+
+ $this->rules = array_merge($this->rules, $classRules);
+ }
+
+ /**
+ * Checks if the current user has permission to perform the action
+ *
+ * @param string $permission the permission to check. This can be the rule you defined, or a permission.action
+ * e.g. 'video.create' or 'video' depending on how you setup the callback.
+ * @param mixed $additionalArgs any additional arguments to pass to the callback or method.
+ * @return bool
+ */
+ public function can(string $permission, ...$additionalArgs): bool
+ {
+ $allowed = false;
+ $action = '';
+ if (strpos($permission, '.') !== false) {
+ [ $permission, $action ] = explode('.', $permission);
+ }
+
+ $permissionsRaw = $this->rules[$permission] ?? null;
+ if ($permissionsRaw === null) {
+ throw new Exception('Permission not defined: ' . $permission);
+ }
+
+ $executedPermissions = null;
+
+ if (is_callable($permissionsRaw) === true) {
+ $executedPermissions = $permissionsRaw($this->currentRole, ...$additionalArgs);
+ } else {
+ if (is_string($permissionsRaw) === true) {
+ $permissionsRaw = explode('->', $permissionsRaw);
+ }
+ [ $className, $methodName ] = $permissionsRaw;
+ if (isset($this->localClassCache[$className]) === false) {
+ $class = new $className($this->app);
+ $this->localClassCache[$className] = $class;
+ } else {
+ $class = $this->localClassCache[$className];
+ }
+ $executedPermissions = $class->$methodName($this->currentRole, ...$additionalArgs);
+ }
+
+ if (is_array($executedPermissions) === true) {
+ $allowed = in_array($action, $executedPermissions, true) === true;
+ } elseif (is_bool($executedPermissions) === true) {
+ $allowed = $executedPermissions;
+ }
+
+ return $allowed;
+ }
+
+ /**
+ * Alias for can. Sometimes it's nice to say has instead of can
+ *
+ * @param string $permission Permission to check
+ * @param mixed $additionalArgs any additional arguments to pass to the callback or method.
+ * @return boolean
+ */
+ public function has(string $permission, ...$additionalArgs): bool
+ {
+ return $this->can($permission, ...$additionalArgs);
+ }
+
+ /**
+ * Checks if the current user has the given role
+ *
+ * @param string $role [description]
+ * @return boolean
+ */
+ public function is(string $role): bool
+ {
+ return $this->currentRole === $role;
+ }
+}
diff --git a/tests/FakePermissionsClass.php b/tests/FakePermissionsClass.php
new file mode 100644
index 0000000..9a7a099
--- /dev/null
+++ b/tests/FakePermissionsClass.php
@@ -0,0 +1,45 @@
+app = $app;
+ }
+
+ public function createOrder(): bool
+ {
+ return true;
+ }
+
+ public function order($currentRole, $id, $quantity): array
+ {
+ $permissions = [];
+ if ($this->createOrder()) {
+ $permissions[] = 'create';
+ }
+
+ if ($quantity > 10) {
+ $permissions[] = 'update';
+ }
+
+ if ($id > 0) {
+ $permissions[] = 'delete';
+ }
+
+ if ($currentRole === 'admin') {
+ $permissions[] = 'admin';
+ }
+
+ return $permissions;
+ }
+}
diff --git a/tests/PermissionTest.php b/tests/PermissionTest.php
new file mode 100644
index 0000000..b14579c
--- /dev/null
+++ b/tests/PermissionTest.php
@@ -0,0 +1,114 @@
+setCurrentRole('user');
+ $this->assertSame('user', $permission->getCurrentRole());
+ }
+
+ public function testDefineRule()
+ {
+ $permission = new Permission('admin');
+ $permission->defineRule('createOrder', 'Some_Permissions_Class->createOrder');
+ $this->assertSame([ 'createOrder' => 'Some_Permissions_Class->createOrder' ], $permission->getRules());
+ }
+
+ public function testDefineDuplicateRule()
+ {
+ $permission = new Permission('admin');
+ $permission->defineRule('createOrder', 'Some_Permissions_Class->createOrder');
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('Rule already defined: createOrder');
+ $permission->defineRule('createOrder', 'Some_Permissions_Class->createOrder');
+ }
+
+ public function testDefineRulesFromClassMethodsNoCacheSimpleBoolean()
+ {
+ $permission = new Permission('admin');
+ $permission->defineRulesFromClassMethods(FakePermissionsClass::class);
+ $this->assertTrue($permission->can('createOrder'));
+ }
+
+ public function testDefineRulesFromClassMethodsNoCacheArray()
+ {
+ $permission = new Permission('public');
+ $permission->defineRulesFromClassMethods(FakePermissionsClass::class);
+ $this->assertTrue($permission->can('order.create', 1, 1));
+ $this->assertTrue($permission->can('order.delete', 1, 1));
+ $this->assertFalse($permission->can('order.update', 1, 1));
+ $this->assertTrue($permission->can('order.update', 1, 11));
+ $this->assertFalse($permission->can('order.delete', 0, 11));
+ $this->assertFalse($permission->can('order.admin', 0, 1));
+ $permission->setCurrentRole('admin');
+ $this->assertTrue($permission->has('order.admin', 1, 11));
+ }
+
+ public function testDefineRulesFromClassMethodsWithCache()
+ {
+ $PhpFileCache = new PhpFileCache(__DIR__, 'my_test');
+ $permission = new Permission('public', null, $PhpFileCache);
+ $permission->defineRulesFromClassMethods(FakePermissionsClass::class, 60);
+ $this->assertTrue($permission->can('order.create', 1, 1));
+ $this->assertTrue($permission->can('order.delete', 1, 1));
+ $this->assertFalse($permission->can('order.update', 1, 1));
+ $this->assertTrue($permission->can('order.update', 1, 11));
+ $this->assertFalse($permission->can('order.delete', 0, 11));
+ $this->assertFalse($permission->can('order.admin', 0, 1));
+ $permission->setCurrentRole('admin');
+ $this->assertTrue($permission->has('order.admin', 1, 11));
+ }
+
+ public function testDefineRulesFromClassMethodsWithCacheTouchCache()
+ {
+ $PhpFileCache = new PhpFileCache(__DIR__, 'my_test');
+ $permission = new Permission('public', null, $PhpFileCache);
+ $permission->defineRulesFromClassMethods(FakePermissionsClass::class, 60);
+ // Make sure it works
+ $this->assertTrue($permission->can('order.create', 1, 1));
+
+ // Then load the classes again like from another request and
+ // make sure it pulls from the cache
+ $permission->defineRulesFromClassMethods(FakePermissionsClass::class, 60);
+ $this->assertFalse($permission->can('order.delete', 0, 11));
+ }
+
+ public function testCanNoPermission()
+ {
+ $permission = new Permission('admin');
+ $this->expectExceptionMessage('Permission not defined: smell');
+ $permission->can('smell');
+ }
+
+ public function testCanWithCallable()
+ {
+ $permission = new Permission('admin');
+ $permission->defineRule('canSmell', function ($currentRole) {
+ return $currentRole === 'admin';
+ });
+ $this->assertTrue($permission->can('canSmell'));
+ }
+
+ public function testIs()
+ {
+ $permission = new Permission('admin');
+ $this->assertTrue($permission->is('admin'));
+ $this->assertFalse($permission->is('user'));
+ }
+}