├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── resources └── js │ └── Gate.js ├── src ├── Console │ ├── Commands │ │ └── JsPolicyMakeCommand.php │ └── stubs │ │ └── Policy.stub ├── PolicyServiceProvider.php └── UsesModelName.php └── tests ├── CreatesApplication.php ├── Feature └── BladeTest.php ├── Models ├── Comment.php └── User.php ├── TestCase.php ├── Unit └── ModelNameTest.php ├── factories ├── CommentFactory.php └── UserFactory.php ├── migrations └── 2017_01_01_000000_create_comments_table.php └── views ├── current-user.blade.php └── custom-key.blade.php /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /vendor 3 | /.vscode 4 | composer.phar 5 | composer.lock 6 | Thumbs.db 7 | .phpunit.result.cache 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.4.1 (2021-03-02) 4 | ### Added 5 | - PHP 8 support 6 | 7 | ## v0.4.0 (2020-09-16) 8 | ### Added 9 | - Laravel 8 compatiblity 10 | 11 | ### Changed 12 | - Update test suite 13 | 14 | ## v0.3.0 (2020-03-09) 15 | ### Changed 16 | - Migrate tests, set minimum Laravel versions 17 | 18 | ## v0.2.0 (2020-01-15) 19 | ### Changed 20 | - Refactoring classes 21 | - Fix styling 22 | - Upgrade testbech version 23 | - Update license date 24 | 25 | ## v0.1.2 (2019-06-05) 26 | ### Added 27 | - publish and generate JS in the corrent folder if Laravel is lower than 5.7 28 | 29 | ## v0.1.1 (2019-06-05) 30 | ### Added 31 | - `viewAny()` policy 32 | 33 | ## v0.1.0 (2019-06-05) 34 | - Initial release 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Cone Development 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Policy 2 | 3 | Using Laravel's authorization on the front-end. 4 | 5 | A nice tool for SPAs and front-end heavy applications. 6 | 7 | If you want to see behind the package, we suggest to read this post: 8 | [Implementing Laravel’s Authorization on the Front-End](https://pineco.de/implementing-laravels-authorization-front-end/). 9 | 10 | ### Table of contents 11 | 12 | 1. [Getting Started](#getting-started) 13 | 2. [Publishing and setting up the JavaScript library](#publishing-and-setting-up-the-javascript-library) 14 | - [Setting up the Gate.js](#setting-up-the-gatejs) 15 | - [Initializing a gate instance](#initializing-a-gate-instance) 16 | - [Passing the user to the gate instance](#passing-the-user-to-the-gate-instance) 17 | - [Using it as a Vue service](#using-it-as-a-vue-service) 18 | - [The @currentUser blade directive](#the-currentuser-blade-directive) 19 | 3. [Using the policies and the Gate.js](#using-the-policies-and-the-gatejs) 20 | - [The available methods](#the-available-methods) 21 | - [Adding the UsesModelName trait to the models](#adding-the-usesmodelname-trait-to-the-models) 22 | - [Generating policies with artisan](#generating-policies-with-artisan) 23 | - [Writing the policy rules](#writing-the-policy-rules) 24 | 4. [Example](#example) 25 | 5. [Contribute](#contribute) 26 | 27 | ## Getting started 28 | 29 | You can install the package with composer, running the `composer require thepinecode/policy` command. 30 | 31 | Since the package supports auto-discovery, Laravel will register the service provider automatically behind the scenes. 32 | 33 | In some cases you may disable auto-discovery for this package. 34 | You can add the provider class to the `dont-discover` array to disable it. 35 | Then you need to register it manually again. 36 | 37 | ## Publishing and setting up the JavaScript library 38 | 39 | By default the package provides a `Gate.js` file, that will handle the policies. 40 | Use the `php artisan vendor:publish` command and choose the `Pine\Policy\PolicyServiceProvider` provider. 41 | After publishing you can find your fresh copy in the `resources/js/policies` folder if you are using Laravel 5.7+. 42 | If your application is lower than 5.7, the JS will be published in the `resources/assets/js/policies`. 43 | 44 | ### Setting up the Gate.js 45 | 46 | Then you can import the `Gate` class and assign it to the `window` object. 47 | 48 | ```js 49 | import Gate from './policies/Gate'; 50 | window.Gate = Gate; 51 | ``` 52 | 53 | ### Initializing a gate instance 54 | 55 | From this point you can initialize the translation service anywhere from your application. 56 | 57 | ```js 58 | let gate = new Gate; 59 | ``` 60 | 61 | ### Passing the user to the gate instance 62 | 63 | The `Gate` object requires a passed user to work properly. This can be a `string` or an `object`. 64 | By default, it looks for the `window['user']` object, however you may customize the key or the object itself. 65 | 66 | ```js 67 | let gate = new Gate; // window['user'] 68 | 69 | let gate = new Gate('admin'); // window['admin'] 70 | 71 | let gate = new Gate({ ... }); // uses the custom object 72 | ``` 73 | 74 | > Note, you can pass any object as a *user*. 75 | > If you pass a team or a group object, it works as well. Since you define the logic behind the `Gate`, 76 | > you can pass anything you wish. 77 | 78 | ### Using it as a Vue service 79 | 80 | If you want to use it from Vue templates directly you can extend Vue with this easily. 81 | 82 | ```js 83 | Vue.prototype.$Gate = new Gate; 84 | ``` 85 | ```html 86 | 89 | ``` 90 | 91 | ```js 92 | computed: { 93 | hasPermission: { 94 | return this.$Gate.allow('view', this.model); 95 | } 96 | } 97 | ``` 98 | 99 | ### The @currentUser blade directive 100 | 101 | To make it quicker, the package comes with a `@currentUser` blade directive. 102 | This does nothing more, but to print the currently authenticated user as `JSON` and assign it to the `window` object. 103 | 104 | ```html 105 | @currentUser 106 | 107 | 108 | 109 | ``` 110 | 111 | You may override the default key for the user. You can do that by passing a string to the blade directive. 112 | 113 | ```html 114 | @currentUser ('admin') 115 | 116 | 117 | 118 | ``` 119 | 120 | > If there is no authenticated user, the value will be `null`. 121 | 122 | ## Using the policies and the Gate.js 123 | 124 | ### The available methods 125 | 126 | #### allow() 127 | 128 | The `allow()` accepts two parameters. The first is the action to perform, the second is the **model object** or the **model name**, like in Laravel. 129 | 130 | > Note: **model name** should be a lower case version of the actual model name in Laravel: for example `Comment` becomes `comment`. 131 | 132 | ```js 133 | gate.allow('view', model); 134 | 135 | gate.allow('create', 'comment'); 136 | ``` 137 | 138 | #### deny() 139 | 140 | The `deny()` has the same signature like `allow()` but it will negate its return value. 141 | 142 | ```js 143 | gate.deny('view', model); 144 | 145 | gate.deny('create', 'comment'); 146 | ``` 147 | 148 | #### before() 149 | 150 | Like in Laravel, in the `before()` method you can provide a custom logic to check for special conditions. 151 | If the condition passes, the rest of the policy rules in the `allow()` or `deny()` won't run at all. 152 | However if the condition fails, the policy rules will get place. 153 | To use the `before()` method, you may extend the gate object and define your custom logic. 154 | 155 | ```js 156 | Gate.prototype.before = function () { 157 | return this.user.is_admin; 158 | } 159 | ``` 160 | 161 | > Please note, to use the `this` object correctly, 162 | > **use the traditional function signature instead of the arrow (() => {}) functions**. 163 | 164 | ### Adding the `UsesModelName` trait to the models 165 | 166 | Since, the policies use real JSON shaped eloquent models, the models have to use the `Pine\Policy\UsesModelName` 167 | trait that generates the proper model name. This model name attribute is used for pairing the proper policy with 168 | the model by the `Gate.js`. 169 | 170 | ```php 171 | use Pine\Policy\UsesModelName; 172 | use Illuminate\Database\Eloquent\Model; 173 | 174 | class Comment extends Model 175 | { 176 | use UsesModelName; 177 | 178 | protected $appends = ['model_name']; 179 | } 180 | ``` 181 | 182 | > Please note, to be able to use this attribute on the front-end, the attribute has to be appended to the JSON form. 183 | > You can read more about appending values to JSON in the 184 | > [docs](https://laravel.com/docs/master/eloquent-serialization#appending-values-to-json). 185 | 186 | ### Generating policies with artisan 187 | 188 | The package comes with an artisan command by default, that helps you to generate your JavaScript policies easily. 189 | To make a policy, run the `php artisan make:js-policy Model` command, where the `Model` is the model's name. 190 | 191 | ```sh 192 | php artisan make:js-policy Comment 193 | ``` 194 | 195 | This command will create the `CommentPolicy.js` file next to the `Gate.js` in the `resources/js/policies` directory. 196 | If you are using lower than Laravel 5.7, the policies will be generated in the `resources/assets/js/policies` directory. 197 | 198 | > Note, the command will append the `Policy` automatically in the file name. 199 | > It means you may pass only the model name when running the command. 200 | 201 | After you generated the policy files, use `npm` to compile all the JavaScript, including policies. 202 | 203 | *** 204 | 205 | #### Important! 206 | 207 | **The policies are registered automatically**. It means, no need for importing them manually. 208 | The gate instance will **automatically** populate the policies. 209 | Every policy will be used where it matches with the model's `model_name` attribute. 210 | 211 | Based on 212 | [Laravel's default app.js](https://github.com/laravel/laravel/blob/master/resources/js/app.js#L19-L20) 213 | the Gate instance 214 | [registers the policies automatically](https://github.com/thepinecode/policy/blob/master/resources/js/Gate.js#L14-L18) 215 | when calling `npm run dev`, `npm run prod` and so on. 216 | 217 | *** 218 | 219 | ### Writing the policy rules 220 | 221 | Policies – like in Laravel – have the following methods by default: 222 | `viewAny`, `view`, `create`, `update`, `restore`, `delete` and `forceDelete`. 223 | Of course, you can use custom methods as well, policies are fully customizables. 224 | 225 | ```js 226 | ... 227 | 228 | view(user, model) 229 | { 230 | return user.id == model.user_id; 231 | } 232 | 233 | create(user) 234 | { 235 | return user.is_admin; 236 | } 237 | 238 | approve(user, model) 239 | { 240 | return user.is_editor && user.id == model.user_id; 241 | } 242 | 243 | ... 244 | ``` 245 | 246 | ## Example 247 | 248 | ```js 249 | // app.js 250 | Vue.prototype.$Gate = new Gate; 251 | 252 | Vue.component('posts', { 253 | mounted() { 254 | axios.get('/api/posts') 255 | .then(response => this.posts = response.data); 256 | }, 257 | data() { 258 | return { 259 | posts: [], 260 | }; 261 | }, 262 | template: ` 263 | 264 | 265 | ` 266 | }); 267 | 268 | let app = new Vue({ 269 | // 270 | }) 271 | ``` 272 | 273 | ```html 274 | 275 | 276 | 277 | @currentUser 278 | 279 | 280 | ``` 281 | 282 | 283 | ## Contribute 284 | 285 | If you found a bug or you have an idea connecting the package, feel free to open an issue. 286 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "conedevelopment/policy", 3 | "description": "Using Laravel's authorization on the front-end.", 4 | "type": "project", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Cone Development", 9 | "email": "hello@conedevelopment.com" 10 | } 11 | ], 12 | "autoload": { 13 | "psr-4": { 14 | "Pine\\Policy\\": "src/" 15 | } 16 | }, 17 | "autoload-dev": { 18 | "psr-4": { 19 | "Pine\\Policy\\Tests\\": "tests/", 20 | "Pine\\Policy\\Tests\\Factories\\": "tests/factories/" 21 | } 22 | }, 23 | "require": { 24 | "php" : "^7.2.5 | ^8.0", 25 | "laravel/framework": "^6.0 || ^7.0 || ^8.24" 26 | }, 27 | "require-dev": { 28 | "laravel/laravel": "^8.0", 29 | "fzaninotto/faker": "^1.9.1", 30 | "mockery/mockery": "^1.3.1", 31 | "phpunit/phpunit": "^9.0" 32 | }, 33 | "scripts": { 34 | "test": "vendor/bin/phpunit" 35 | }, 36 | "config": { 37 | "sort-packages": true 38 | }, 39 | "extra": { 40 | "laravel": { 41 | "providers": [ 42 | "Pine\\Policy\\PolicyServiceProvider" 43 | ] 44 | }, 45 | "branch-alias": { 46 | "dev-master": "1.0-dev" 47 | } 48 | }, 49 | "minimum-stability" : "dev", 50 | "prefer-stable" : true 51 | } 52 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./src 6 | 7 | 8 | 9 | 10 | ./tests 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /resources/js/Gate.js: -------------------------------------------------------------------------------- 1 | export default class Gate 2 | { 3 | /** 4 | * Initialize a new gate instance. 5 | * 6 | * @param {string|object} user 7 | * @return {void} 8 | */ 9 | constructor(user = 'user') 10 | { 11 | this.policies = {}; 12 | this.user = typeof user === 'object' ? user : (window[user] || null); 13 | 14 | const files = require.context('./', true, /Policy\.js$/); 15 | files.keys().map(key => { 16 | let name = key.split('/').pop().replace('Policy.js', '').toLowerCase(); 17 | this.policies[name] = new (files(key).default); 18 | }); 19 | } 20 | 21 | /** 22 | * Check if the user has a general perssion. 23 | * 24 | * @return {bool|void} 25 | */ 26 | before() 27 | { 28 | // 29 | } 30 | 31 | /** 32 | * Determine wheter the user can perform the action on the model. 33 | * 34 | * @param {string} action 35 | * @param {object|string} model 36 | * @return {bool} 37 | */ 38 | allow(action, model) 39 | { 40 | if (this.before()) { 41 | return true; 42 | } 43 | 44 | let type = typeof model === 'object' ? model.model_name : model; 45 | 46 | if (this.user && this.policies.hasOwnProperty(type) && typeof this.policies[type][action] === 'function') { 47 | return this.policies[type][action](this.user, typeof model === 'object' ? model : null); 48 | } 49 | 50 | return false; 51 | } 52 | 53 | /** 54 | * Determine wheter the user can't perform the action on the model. 55 | * 56 | * @param {string} action 57 | * @param {object} model 58 | * @return {bool} 59 | */ 60 | deny(action, model) 61 | { 62 | return ! this.allow(action, model); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Console/Commands/JsPolicyMakeCommand.php: -------------------------------------------------------------------------------- 1 | laravel['path']}/../resources/js/policies/{$name}Policy.js"; 52 | 53 | if (version_compare(app()->version(), '5.7.0', '<')) { 54 | $path = "{$this->laravel['path']}/../resources/assets/js/policies/{$name}Policy.js"; 55 | } 56 | 57 | return file_exists($path); 58 | } 59 | 60 | /** 61 | * Replace the namespace for the given stub. 62 | * 63 | * @param string $stub 64 | * @param string $name 65 | * @return $this 66 | */ 67 | protected function replaceNamespace(&$stub, $name) 68 | { 69 | $name = class_basename(str_replace('\\', '/', $name)); 70 | 71 | $model = Str::lower($name); 72 | 73 | $stub = str_replace( 74 | ['{Class}', '{model}'], 75 | [$name, $model === 'user' ? 'model' : $model], 76 | $stub 77 | ); 78 | 79 | return $this; 80 | } 81 | 82 | /** 83 | * Get the destination class path. 84 | * 85 | * @param string $name 86 | * @return string 87 | */ 88 | protected function getPath($name) 89 | { 90 | $name = class_basename(str_replace('\\', '/', $name)); 91 | 92 | if (version_compare(app()->version(), '5.7.0', '<')) { 93 | return "{$this->laravel['path']}/../resources/assets/js/policies/{$name}Policy.js"; 94 | } 95 | 96 | return "{$this->laravel['path']}/../resources/js/policies/{$name}Policy.js"; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Console/stubs/Policy.stub: -------------------------------------------------------------------------------- 1 | export default class {Class}Policy 2 | { 3 | /** 4 | * Determine whether the user can view any models. 5 | * 6 | * @param {object} user 7 | * @return mixed 8 | */ 9 | viewAny(user) 10 | { 11 | // 12 | } 13 | 14 | /** 15 | * Determine whether the user can view the model. 16 | * 17 | * @param {object} user 18 | * @param {object} {model} 19 | * @return {mixed} 20 | */ 21 | view(user, {model}) 22 | { 23 | // 24 | } 25 | 26 | /** 27 | * Determine whether the user can create models. 28 | * 29 | * @param {object} user 30 | * @return {mixed} 31 | */ 32 | create(user) 33 | { 34 | // 35 | } 36 | 37 | /** 38 | * Determine whether the user can update the model. 39 | * 40 | * @param {object} user 41 | * @param {object} {model} 42 | * @return {mixed} 43 | */ 44 | update(user, {model}) 45 | { 46 | // 47 | } 48 | 49 | /** 50 | * Determine whether the user can restore the model. 51 | * 52 | * @param {object} user 53 | * @param {object} {model} 54 | * @return {mixed} 55 | */ 56 | restore(user, {model}) 57 | { 58 | // 59 | } 60 | 61 | /** 62 | * Determine whether the user can delete the model. 63 | * 64 | * @param {object} user 65 | * @param {object} {model} 66 | * @return {mixed} 67 | */ 68 | delete(user, {model}) 69 | { 70 | // 71 | } 72 | 73 | /** 74 | * Determine whether the user can permanently delete the model. 75 | * 76 | * @param {object} user 77 | * @param {object} {model} 78 | * @return {mixed} 79 | */ 80 | forceDelete(user, {model}) 81 | { 82 | // 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/PolicyServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 20 | __DIR__.'/../resources/js' => resource_path( 21 | version_compare($this->app->version(), '5.7.0', '<') 22 | ? 'assets/js/policies' : 'js/policies' 23 | ), 24 | ]); 25 | 26 | // Register the commands 27 | if ($this->app->runningInConsole()) { 28 | $this->commands([JsPolicyMakeCommand::class]); 29 | } 30 | 31 | // Register the @currentUser blade directive 32 | Blade::directive('currentUser', function ($key) { 33 | return sprintf( 34 | '', 35 | $key ?: "'user'" 36 | ); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/UsesModelName.php: -------------------------------------------------------------------------------- 1 | booting(function () use ($app) { 22 | $app->register(PolicyServiceProvider::class); 23 | $app->make('migrator')->path(__DIR__.'/migrations'); 24 | }); 25 | 26 | $app->make(Kernel::class)->bootstrap(); 27 | 28 | return $app; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Feature/BladeTest.php: -------------------------------------------------------------------------------- 1 | actingAs($this->user) 13 | ->get('/policy/current-user') 14 | ->assertSee("window['user'] = ".$this->user->toJson(), false); 15 | } 16 | /** @test */ 17 | public function current_user_can_have_custom_key() 18 | { 19 | $this->actingAs($this->user) 20 | ->get('/policy/custom-key') 21 | ->assertSee("window['admin'] = ".$this->user->toJson(), false); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Models/Comment.php: -------------------------------------------------------------------------------- 1 | user = UserFactory::new()->create(); 22 | 23 | // $this->app->afterResolving('migrator', function ($migrator) { 24 | // $migrator->path(__DIR__.'/migrations'); 25 | // }); 26 | 27 | View::addNamespace('policy', __DIR__.'/views'); 28 | 29 | Route::get('/policy/{view}', function ($view) { 30 | return view("policy::{$view}"); 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Unit/ModelNameTest.php: -------------------------------------------------------------------------------- 1 | comment = CommentFactory::new()->create(); 17 | } 18 | 19 | /** @test */ 20 | public function a_model_can_use_model_name() 21 | { 22 | $this->assertEquals('comment', $this->comment->model_name); 23 | 24 | $this->assertEquals('model_name', array_search('comment', $this->comment->toArray())); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/factories/CommentFactory.php: -------------------------------------------------------------------------------- 1 | mt_rand(1, 10), 26 | 'body' => $this->faker->sentences(2, true), 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->name, 28 | 'email' => $this->faker->unique()->safeEmail, 29 | 'email_verified_at' => Carbon::now(), 30 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 31 | 'remember_token' => Str::random(10), 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/migrations/2017_01_01_000000_create_comments_table.php: -------------------------------------------------------------------------------- 1 | id('id'); 18 | $table->foreignId('user_id'); 19 | $table->text('body'); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down() 30 | { 31 | Schema::dropIfExists('comments'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/views/current-user.blade.php: -------------------------------------------------------------------------------- 1 | @currentUser 2 | -------------------------------------------------------------------------------- /tests/views/custom-key.blade.php: -------------------------------------------------------------------------------- 1 | @currentUser ('admin') 2 | --------------------------------------------------------------------------------