├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── composer.json ├── composer.lock ├── phpunit.xml ├── provides.json ├── src ├── Efficiently │ └── AuthorityController │ │ ├── Authority.php │ │ ├── AuthorityControllerServiceProvider.php │ │ ├── ControllerAdditions.php │ │ ├── ControllerResource.php │ │ ├── Exceptions │ │ └── AccessDenied.php │ │ ├── Facades │ │ ├── Authority.php │ │ └── Params.php │ │ ├── Parameters.php │ │ ├── Rule.php │ │ └── helpers.php ├── config │ └── config.php ├── lang │ ├── en │ │ └── messages.php │ └── fr │ │ └── messages.php ├── migrations │ ├── 2015_02_23_095033_create_roles_table.php │ ├── 2015_02_23_095107_create_permissions_table.php │ └── 2015_02_23_095152_create_role_user_table.php └── translations │ ├── en │ └── messages.php │ └── fr │ └── messages.php └── tests ├── AcAuthorityTest.php ├── AcControllerAdditionsTest.php ├── AcControllerResourceTest.php ├── AcExceptionsHandler.php ├── AcHelpersTest.php ├── AcParametersTest.php ├── AcProjectsControllerTest.php ├── AcTasksControllerTest.php ├── AcTestCase.php ├── fixtures ├── AcProduct.php ├── controllers │ ├── AcBaseController.php │ ├── AcCategoriesController.php │ ├── AcProjectsController.php │ └── AcTasksController.php ├── models │ ├── AcCategory.php │ ├── AcProject.php │ ├── AcRole.php │ ├── AcTask.php │ └── AcUser.php └── views │ ├── ac_projects │ ├── create.blade.php │ ├── edit.blade.php │ ├── index.blade.php │ └── show.blade.php │ └── ac_tasks │ ├── create.blade.php │ ├── edit.blade.php │ ├── index.blade.php │ └── show.blade.php └── helpers └── AuthorityControllerHelpers.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.6 5 | - 7.0 6 | - hhvm 7 | 8 | matrix: 9 | allow_failures: 10 | - php: hhvm 11 | 12 | sudo: false 13 | 14 | before_install: 15 | - travis_retry composer self-update 16 | 17 | install: 18 | - travis_retry composer install --no-interaction --prefer-dist 19 | 20 | script: vendor/bin/phpunit tests 21 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Tortue Torche 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AuthorityController [![Build Status](https://travis-ci.org/efficiently/authority-controller.png?branch=master)](http://travis-ci.org/efficiently/authority-controller) 2 | =================== 3 | 4 | AuthorityController is an PHP authorization library for [Laravel 5.3](http://laravel.com) which restricts what resources a given user is allowed to access. 5 | 6 | All permissions are defined in a single location: 7 | 8 | config/authority-controller.php 9 | 10 | and not duplicated across controllers, routes, views, and database queries. 11 | 12 | For [**Laravel 5.2**](http://laravel.com/docs/5.2) supports see [AuthorityController 2.2 branch](https://github.com/efficiently/authority-controller/tree/2.2) 13 | 14 | For [Laravel 5.0 or 5.1](http://laravel.com/docs/5.1) supports see [AuthorityController 2.1 branch](https://github.com/efficiently/authority-controller/tree/2.1) 15 | 16 | For [Laravel 4.1 or 4.2](http://laravel.com/docs/4.2) supports see [AuthorityController 1.2 branch](https://github.com/efficiently/authority-controller/tree/1.2) 17 | 18 | #### Demo application 19 | 20 | You can see in action this package with this Laravel 5.3 [**demo application**](https://github.com/efficiently/laravel_authority-controller_app#readme). 21 | 22 | #### Origins and Inspirations 23 | 24 | It's an extension of the [`authority-laravel`](https://github.com/authority-php/authority-laravel) package. 25 | 26 | And a port of the best [Ruby](https://ruby-lang.org) authorization library: [CanCan](https://github.com/ryanb/cancan). 27 | 28 | [Authority](https://github.com/authority-php/authority) ports some features of CanCan and this package ports [_almost_](https://github.com/efficiently/authority-controller/blob/master/README.md#missing-features) all the other features. 29 | 30 | Installation 31 | --------------------------- 32 | 33 | #### With [Composer](https://getcomposer.org/) 34 | 35 | 1. Add `authority-controller` package to your `composer.json` file to require AuthorityController: 36 | 37 | ```bash 38 | composer require efficiently/authority-controller:dev-master 39 | ``` 40 | 41 | 2. Add the service provider to `config/app.php`: 42 | 43 | ```php 44 | Efficiently\AuthorityController\AuthorityControllerServiceProvider::class, 45 | ``` 46 | 47 | 3. Add the aliases (facades) to your Laravel app config file: 48 | 49 | ```php 50 | 'Params' => Efficiently\AuthorityController\Facades\Params::class, 51 | 'Authority' => Efficiently\AuthorityController\Facades\Authority::class, 52 | ``` 53 | 54 | 4. This will allow you to access the Authority class through the static interface you are used to with Laravel components. 55 | 56 | ```php 57 | Authority::can('update', SomeModel::class); 58 | ``` 59 | 60 | Configuration 61 | ------------- 62 | ##### Create Roles and Permissions Tables 63 | 64 | We have provided a basic table structure to get you started in creating your roles and permissions. 65 | 66 | Publish them to your migrations directory or copy them directly. 67 | 68 | ```bash 69 | php artisan vendor:publish --provider="Efficiently\AuthorityController\AuthorityControllerServiceProvider" --tag="migrations" 70 | ``` 71 | 72 | Run the migrations 73 | 74 | ```bash 75 | php artisan migrate 76 | ``` 77 | 78 | This will create the following tables 79 | 80 | - roles 81 | - role_user 82 | - permissions 83 | 84 | To utilize these tables, you can add the following methods to your `User` model. You will also need to create Role and Permission Model stubs (replacing `App\Authority\` with you own namespace).. 85 | 86 | ```php 87 | //app/User.php 88 | public function roles() 89 | { 90 | return $this->belongsToMany(Authority\Role::class)->withTimestamps(); 91 | } 92 | 93 | public function permissions() 94 | { 95 | return $this->hasMany(Authority\Permission::class); 96 | } 97 | 98 | public function hasRole($key) 99 | { 100 | $hasRole = false; 101 | foreach ($this->roles as $role) { 102 | if ($role->name === $key) { 103 | $hasRole = true; 104 | break; 105 | } 106 | } 107 | 108 | return $hasRole; 109 | } 110 | ``` 111 | 112 | ```php 113 | //app/Authority/Role.php 114 | user()` return the current authenticated user. Now, by default Laravel 5 handles [this](https://laravel.com/docs/5.3/authentication#retrieving-the-authenticated-user). 177 | 178 | ##### Defining Authority rules 179 | 180 | User permissions are defined in an AuthorityController configuration file. 181 | 182 | You can publish the AuthorityController default configuration file with the command below: 183 | 184 | ```bash 185 | php artisan vendor:publish --provider="Efficiently\AuthorityController\AuthorityControllerServiceProvider" --tag="config" 186 | ``` 187 | 188 | This will place a copy of the configuration file at `config/authority-controller.php`. The config file includes an `initialize` function, which is a great place to setup your rules and aliases. 189 | 190 | ```php 191 | //config/authority-controller.php 192 | $serializer->serialize(function ($authority) { 197 | $user = auth()->guest() ? new App\User : $authority->getCurrentUser(); 198 | 199 | // Action aliases. For example: 200 | $authority->addAlias('moderate', ['read', 'update', 'delete']); 201 | 202 | // Define abilities for the passed in user here. For example: 203 | if ($user->hasRole('admin')) { 204 | $authority->allow('manage', 'all'); 205 | } else { 206 | $authority->allow('read', 'all'); 207 | } 208 | }) 209 | ]; 210 | ``` 211 | 212 | See [Defining Authority rules](https://github.com/efficiently/authority-controller/wiki/Defining-Authority-rules) for details. 213 | 214 | ##### Check Authority rules & Authorization 215 | 216 | The current user's permissions can then be checked using the `Authority::can()` and `Authority::cannot()` methods in the view and controller. 217 | 218 | ``` 219 | @if (Authority::can('update', $article)) 220 | {{ link_to_route("articles.edit", "Edit", $article->id) }} 221 | @endif 222 | ``` 223 | 224 | See [Checking Authority rules](https://github.com/efficiently/authority-controller/wiki/Checking-Authority-rules) for more information 225 | 226 | The `authorize()` method in the controller will throw an exception if the user is not able to perform the given action. 227 | 228 | ```php 229 | public function show($id) 230 | { 231 | $this->article = App\Article::find($id); 232 | $this->authorize('read', $this->article); 233 | } 234 | ``` 235 | 236 | Setting this for every action can be tedious, therefore the `loadAndAuthorizeResource()` method is provided to automatically authorize all actions in a RESTful style resource controller. It will use a before filter to load the resource into an instance variable and authorize it for every action. 237 | 238 | ```php 239 | loadAndAuthorizeResource(); 249 | } 250 | 251 | public function show($id) 252 | { 253 | // $this->article is already loaded and authorized 254 | } 255 | } 256 | ``` 257 | 258 | See [Authorizing Controller Actions](https://github.com/efficiently/authority-controller/wiki/authorizing-controller-actions) for more information. 259 | 260 | ##### Exception Handling 261 | 262 | The `Efficiently\AuthorityController\Exceptions\AccessDenied` exception is thrown when calling `authorize()` in the controller and the user is not able to perform the given action. A message can optionally be provided. 263 | 264 | ```php 265 | Authority::authorize('read', 'App\Product', 'Unable to read this product.'); 266 | ``` 267 | 268 | You can catch the exception and modify its behavior in the `render()` method of the `app/Exceptions/Handler.php` file. For example here we set the error message to a flash and redirect to the home page. 269 | 270 | ```php 271 | //app/Exceptions/Handler.php 272 | 273 | /** 274 | * Render an exception into an HTTP response. 275 | * 276 | * @param \Illuminate\Http\Request $request 277 | * @param \Exception $e 278 | * @return \Illuminate\Http\Response 279 | */ 280 | public function render($request, Exception $e) 281 | { 282 | //code... 283 | if ($e instanceof \Efficiently\AuthorityController\Exceptions\AccessDenied) { 284 | $msg = $e->getMessage(); 285 | \Log::error('Access denied! '.$msg); 286 | 287 | return redirect('/home')->with('flash_alert', $msg); 288 | } 289 | 290 | return parent::render($request, $e); 291 | } 292 | 293 | //code... 294 | ``` 295 | 296 | See [Exception Handling](https://github.com/efficiently/authority-controller/wiki/Exception-Handling) for more information. 297 | 298 | Documentations 299 | -------------- 300 | ##### Wiki Docs 301 | 302 | * [Defining Authority rules](https://github.com/efficiently/authority-controller/wiki/Defining-Authority-rules) 303 | * [Checking Authority rules](https://github.com/efficiently/authority-controller/wiki/Checking-Authority-rules) 304 | * [Authorizing Controller Actions](https://github.com/efficiently/authority-controller/wiki/Authorizing-Controller-Actions) 305 | * [Exception Handling](https://github.com/efficiently/authority-controller/wiki/Exception-Handling) 306 | * [See more](https://github.com/efficiently/authority-controller/wiki) 307 | 308 | ##### Authority Docs 309 | 310 | Authority [introduction](https://github.com/authority-php/authority/blob/2.2.2/readme.md#introduction). 311 | 312 | Authority-Laravel [general usage](https://github.com/authority-php/authority-laravel/blob/2.4.3/README.md#general-usage). 313 | 314 | ##### CanCan Wiki Docs 315 | 316 | Because AuthorityController is a CanCan port, you can also read the Wiki docs of CanCan [here](https://github.com/ryanb/cancan/wiki). 317 | 318 | Controller additions 319 | -------------------- 320 | Your controllers have now a `$params` property: 321 | 322 | ```php 323 | params['id'] == $id;//-> true 334 | $this->params['product'];//-> ["name" => "Best movie"] 335 | $this->params['controller'];//-> 'products' 336 | $this->params['action'];//-> 'update' 337 | //code... 338 | } 339 | 340 | //code... 341 | } 342 | ``` 343 | 344 | Changelog 345 | --------- 346 | #### 2.3.0-dev 347 | * Laravel 5.3 support! 348 | 349 | #### 2.2.0 350 | * Laravel 5.2 support! 351 | 352 | #### 2.1.1 353 | * Update installation instructions for Laravel >= 5.1.11 354 | 355 | #### 2.1.0 356 | * Laravel 5.1 support! 357 | 358 | #### 2.0.1 359 | * Replace the deprecated package [`illuminate/html`](https://github.com/illuminate/html) package by the [`laravelcollective/html`](https://github.com/LaravelCollective/html) package 360 | * Autoloading migrations class is useless, see issue [#30](https://github.com/efficiently/authority-controller/issues/30) (reported by @Fnatte) 361 | * Autoloading class from `tests` directory are now only available in Composer's dev mode to avoid conflicts 362 | 363 | #### 2.0.0 364 | * Laravel 5.0 support! 365 | * Use your Laravel Aliases to resolve your models namespace name. 366 | * Or auto guessing them, e.g. `User` => `App\User` 367 | * Add a new config option `controllerClass` which is by default `Illuminate\Routing\Controller` 368 | * Support Route Model Binding in the Parameters class. 369 | See: http://laravel.com/docs/5.0/routing#route-model-binding and issue [#21](https://github.com/efficiently/authority-controller/issues/21) 370 | * Use [authority-laravel](https://github.com/authority-php/authority-laravel) package instead of [authority-l4](https://github.com/machuga/authority-l4). 371 | * Upgrade Notes (if you used previously this package with Laravel 4): 372 | * Move your `authory-controller` config file from `app/config/packages/efficiently/authority-controller/config.php` to `config/authority-controller.php` 373 | * Publish the `authory-controller` migrations files (see the section [Create Roles and Permissions Tables](https://github.com/efficiently/authority-controller/blob/2.0/README.md#create-roles-and-permissions-tables) of this README) 374 | 375 | #### 1.2.4 376 | * Add `BaseController::flushAuthorityEvents()` static method. 377 | Useful for functional tests with Codeception (see issue [#14](https://github.com/efficiently/authority-controller/issues/14) and [this Wiki page](https://github.com/efficiently/authority-controller/wiki/Testing-Authority-rules#functional-tests-with-codeception) for more explanations). 378 | * Fix User::hasRoles() method to avoid duplicate roles. 379 | 380 | #### 1.2.3 381 | * Follow [PSR-2](http://www.php-fig.org) coding style 382 | 383 | #### 1.2.2 384 | * Run tests with Laravel 4.2 385 | 386 | #### 1.2.1 387 | * Fix `composer.json` file. 388 | 389 | #### 1.2.0 390 | * Security fix: conditional callback was never evaluated when an actual instance object was present. 391 | * Non backwards compatible: Deny rules override prior rules and Allow rules don't override prior rules but instead are logically or'ed (fix [#5](https://github.com/efficiently/authority-controller/issues/5)). 392 | Match more CanCan default behavior unlike `authority-php\authority` package. 393 | Read the Wiki doc for more information: [Authority-Precedence](https://github.com/efficiently/authority-controller/wiki/Authority-Precedence). 394 | * Support PHP 5.4, 5.5, 5.6 and HipHop Virtual Machine (hhvm). 395 | * Update [`Parameters`](https://github.com/efficiently/authority-controller/blob/18c2ad7788385da4e0309708772ea40cc8be0f53/src/Efficiently/AuthorityController/Parameters.php#L46) class to allow custom routes with `id` and `parent_id` routes's parameters (fix [#6](https://github.com/efficiently/authority-controller/issues/6)). 396 | 397 | #### 1.1.3 398 | * Upgrade Authority-L4 package to fix Laravel 4.1 support. 399 | 400 | #### 1.1.2 401 | * Tweak the mock system who simulates Eloquent's constructor method. 402 | 403 | #### 1.1.1 404 | * Less intrusive parameters injection in the controllers 405 | * Check if the current resolved controller responds to paramsBeforeFilter method. Otherwise the application crash. 406 | * Use the Controller alias of the current Laravel application instead of a hardcoded class name. 407 | 408 | #### 1.1.0 409 | * First beta release for Laravel **4.1** compatibility. 410 | * Non backwards compatible with Laravel **4.0**. 411 | 412 | #### 1.0.0 413 | * First stable release, only compatible with Laravel **4.0**. 414 | * For Laravel **4.1** supports, see [AuthorityController 1.1 branch](https://github.com/efficiently/authority-controller/tree/1.1). 415 | * Fix AccessDenied class, the exception message didn't fallback to the default message if it was empty. 416 | 417 | #### 0.10.0 418 | * Non backwards compatible: `Params::get('controller')` behaviour is now like Rails. It returns controller name in snake_case and in plural. 419 | 420 | #### 0.9.0 421 | * First beta release 422 | 423 | Missing features 424 | ---------------- 425 | 1. In `ControllerResource` class, the [`#load_collection`](https://github.com/ryanb/cancan/blob/1.6.10/lib/cancan/controller_resource.rb#L80) method, who uses in the `User` model [`#accessible_by`](https://github.com/ryanb/cancan/blob/1.6.10/lib/cancan/model_additions.rb#L22) method. Looks complicated. 426 | Instead, use specific query scopes with `collectionScope` option to filtering your data in your collection (e.g. `index`) controller actions. 427 | Because you'll allowing/denying access by roles or check user's authorizations on each record of the collection. 428 | 2. In `Ability` class, the [`#attributes_for`](https://github.com/ryanb/cancan/blob/1.6.10/lib/cancan/ability.rb#L221) method. 429 | Looks useless with `Authority` because rules conditions are only possible by `Closure` not by associative array. And CanCan handles `#attribute_for` only for `Hash` (associative array) conditions. 430 | 3. `#skip_*` methods in `ControllerAdditions`. 431 | 4. For `allow()` and `deny()` methods of `Authority`, the third argument isn't an optional hash (associative array) of conditions but an anonymous function (Closure): 432 | 433 | ```php 434 | $authority->allow('update', 'App\Product', function ($self, $product) { 435 | return $product->available === true; 436 | }); 437 | ``` 438 | 439 | Good to know 440 | ------------ 441 | #### Compatibility 442 | It's **only** compatible with **PHP >= 5.6** and **Laravel >= 5.3** framework. 443 | 444 | #### Differences between CanCan and AuthorityController 445 | See Wiki page [Differences between CanCan and AuthorityController](https://github.com/efficiently/authority-controller/wiki/Differences-between-CanCan-and-AuthorityController) 446 | 447 | #### Philosophy 448 | It's following the D.R.W.J.P.I. principle: 449 | 450 | > Don't Reinvent the Wheel, Just Port It ! 451 | > -- (c) 2013 A.D. 452 | 453 | Questions or Problems? 454 | ---------------------- 455 | If you have any issues with AuthorityController, please add an [issue on GitHub](https://github.com/efficiently/authority-controller/issues) or fork the project and send a pull request. 456 | 457 | To get the tests running you should install PHPUnit and run `phpunit tests`. 458 | 459 | 460 | Special Thanks 461 | -------------- 462 | AuthorityController was _heavily_ inspired by [CanCan](https://github.com/ryanb/cancan) and uses [Authority-Laravel](https://github.com/authority-php/authority-laravel). 463 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "efficiently/authority-controller", 3 | "description": "AuthorityController is an PHP authorization library for Laravel 5 which restricts what resources a given user is allowed to access.", 4 | "keywords": [ 5 | "cancan", 6 | "can", 7 | "authority", 8 | "acl", 9 | "role", 10 | "permission", 11 | "ability", 12 | "allow", 13 | "deny", 14 | "rbac", 15 | "authorization", 16 | "authentication", 17 | "security", 18 | "laravel", 19 | "laravel 5", 20 | "laravel 5.1", 21 | "laravel 5.2", 22 | "laravel 5.3", 23 | "rails" 24 | ], 25 | "license": "MIT", 26 | "authors": [{ 27 | "name": "TortueTorche", 28 | "email": "tortuetorche@spam.me" 29 | }], 30 | "require": { 31 | "php": ">=5.6.4", 32 | "authority-php/authority-laravel": "~2.4.3" 33 | }, 34 | "require-dev": { 35 | "phpunit/phpunit": "~4.5", 36 | "mockery/mockery": "0.9.*", 37 | "orchestra/testbench": "~3.3.0", 38 | "laravelcollective/html": "5.3.*" 39 | }, 40 | "autoload": { 41 | "files": [ 42 | "src/Efficiently/AuthorityController/helpers.php" 43 | ], 44 | "psr-0": { 45 | "Efficiently\\AuthorityController": "src/" 46 | } 47 | }, 48 | "autoload-dev": { 49 | "classmap": [ 50 | "tests/AcExceptionsHandler.php", "tests/AcTestCase.php", "tests/helpers", "tests/fixtures/", 51 | "tests/fixtures/models", "tests/fixtures/controllers" 52 | ] 53 | }, 54 | "extra": { 55 | "branch-alias": { 56 | "dev-1.0": "1.0.x-dev", 57 | "dev-1.1": "1.1.x-dev", 58 | "dev-1.2": "1.2.x-dev", 59 | "dev-2.0": "2.0.x-dev", 60 | "dev-2.1": "2.1.x-dev", 61 | "dev-2.2": "2.2.x-dev", 62 | "dev-master": "2.3.x-dev" 63 | } 64 | }, 65 | "config": { 66 | "preferred-install": "dist" 67 | }, 68 | "prefer-stable": true, 69 | "minimum-stability": "dev" 70 | } 71 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /provides.json: -------------------------------------------------------------------------------- 1 | { 2 | "providers": [ 3 | "Efficiently\AuthorityController\AuthorityControllerServiceProvider" 4 | ], 5 | "aliases": [ 6 | { 7 | "alias": "Params", 8 | "facade": "Efficiently\AuthorityController\Facades\Params" 9 | }, { 10 | "alias": "Authority", 11 | "facade": "Efficiently\AuthorityController\Facades\Authority" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/Efficiently/AuthorityController/Authority.php: -------------------------------------------------------------------------------- 1 | initDefaultAliases(); 20 | parent::__construct($currentUser, $dispatcher); 21 | } 22 | 23 | // Removes previously aliased actions including the defaults. 24 | public function clearAliasedActions() 25 | { 26 | $this->aliasedActions = []; 27 | } 28 | 29 | /** 30 | * Determine if current user can access the given action and resource 31 | * 32 | * @return boolean 33 | */ 34 | public function can($action, $resource, $resourceValue = null) 35 | { 36 | if (is_object($resource)) { 37 | $resourceValue = $resource; 38 | $resource = get_classname($resourceValue); 39 | } elseif (is_array($resource)) { 40 | // Nested resources can be passed through an associative array, this way conditions which are 41 | // dependent upon the association will work when using a class. 42 | $resourceValue = head(array_values($resource)); 43 | $resource = head(array_keys($resource)); 44 | } 45 | 46 | // The conditional callback (Closure) is only evaluated when an actual instance object is present. 47 | // It is not evaluated when checking permissions on the class name (such as in the 'index' action). 48 | $skipConditions = false; 49 | if (is_string($resource) && ! is_object($resourceValue) && $this->hasCondition($action, $resource)) { 50 | $skipConditions = true; 51 | } 52 | 53 | $self = $this; 54 | $rules = $this->getRulesFor($action, $resource); 55 | 56 | if (! $rules->isEmpty()) { 57 | $allowed = array_reduce($rules->all(), function ($result, $rule) use ($self, $resourceValue, $skipConditions) { 58 | if ($skipConditions) { 59 | return $rule->getBehavior(); // Short circuit 60 | } else { 61 | if ($rule->isRestriction()) { 62 | // 'deny' rules override prior rules. 63 | $result = $result && $rule->isAllowed($self, $resourceValue); 64 | } else { 65 | // 'allow' rules do not override prior rules but instead are logically or'ed. 66 | // Unlike Authority default behavior. 67 | $result = $result || $rule->isAllowed($self, $resourceValue); 68 | } 69 | } 70 | 71 | return $result; 72 | }, false); 73 | } else { 74 | $allowed = false; 75 | } 76 | 77 | return $allowed; 78 | } 79 | 80 | public function authorize($action, $resource, $args = null) 81 | { 82 | $args = is_array($args) ? $args : array_slice(func_get_args(), 2); 83 | 84 | $message = null; 85 | $options = array_extract_options($args); 86 | if (is_array($options) && array_key_exists('message', $options)) { 87 | $message = $options['message']; 88 | } elseif (is_array($args) && array_key_exists(0, $args)) { 89 | list($message) = $args; 90 | unset($args[0]); 91 | } 92 | 93 | if ($this->cannot($action, $resource, $args)) { 94 | $resourceClass = $resource; 95 | if (is_object($resourceClass)) { 96 | $resourceClass = get_classname($resourceClass); 97 | } elseif (is_array($resourceClass)) { 98 | $resourceClass = head(array_values($resourceClass)); 99 | if (is_object($resourceClass)) { 100 | $resourceClass = get_classname($resourceClass); 101 | } 102 | } 103 | $message = $message ?: $this->getUnauthorizedMessage($action, $resourceClass); 104 | throw new Exceptions\AccessDenied($message, $action, $resourceClass); 105 | } 106 | 107 | return $resource; 108 | } 109 | 110 | /** 111 | * Define rule(s) for a given action(s) and resource(s) 112 | * 113 | * @param boolean $allow True if privilege, false if restriction 114 | * @param string|array $actions Action(s) for the rule(s) 115 | * @param mixed $resources Resource(s) for the rule(s) 116 | * @param Closure|null $condition Optional condition for the rule 117 | * @return array 118 | */ 119 | public function addRule($allow, $actions, $resources, $condition = null) 120 | { 121 | $actions = (array) $actions; 122 | $resources = (array) $resources; 123 | $rules = []; 124 | foreach ($actions as $action) { 125 | foreach ($resources as $resource) { 126 | $rule = new Rule($allow, $action, $resource, $condition); 127 | $this->rules->add($rules[] = $rule); 128 | } 129 | } 130 | 131 | return $rules; 132 | } 133 | 134 | // alias of addRule() 135 | public function addRules($allow, $actions, $resources, $condition = null) 136 | { 137 | return $this->addRule($allow, $actions, $resources, $condition); 138 | } 139 | 140 | /** 141 | * Define new alias for an action 142 | * 143 | * $this->$authority->addAlias('read', ['index', 'show']); 144 | * $this->$authority->addAlias('create', 'new'); 145 | * $this->$authority->addAlias('update', 'edit'); 146 | * 147 | * This way one can use $params['action'] in the controller to determine the permission. 148 | * 149 | * @param string $name Name of action 150 | * @param string|array $actions Action(s) that $name aliases 151 | * @return RuleAlias 152 | */ 153 | public function addAlias($name, $actions) 154 | { 155 | $actions = (array) $actions; 156 | $this->addAliasAction($name, $actions); 157 | parent::addAlias($name, $this->getExpandActions($actions)); 158 | } 159 | 160 | /** 161 | * Returns all rules relevant to the given action and resource 162 | * 163 | * @return RuleRepository 164 | */ 165 | public function getRulesFor($action, $resource) 166 | { 167 | $aliases = array_merge((array) $action, $this->getAliasesForAction($action)); 168 | return $this->rules->getRelevantRules($aliases, $resource); 169 | } 170 | 171 | /** 172 | * @param string|array $action Name of action(s) 173 | * @param string|object $resource Resource for the rule 174 | * @return boolean 175 | */ 176 | public function hasCondition($action, $resource) 177 | { 178 | $relevantConditions = $this->getRelevantConditions($action, $resource); 179 | 180 | return ! empty($relevantConditions); 181 | } 182 | 183 | /** 184 | * @param string|array $action Name of action(s) 185 | * @param string|object $resource Resource for the rule 186 | * @return array 187 | */ 188 | public function getRelevantConditions($action, $resource) 189 | { 190 | $rules = $this->getRulesFor($action, $resource)->getIterator()->getArrayCopy(); 191 | 192 | $relevantConditions = array_filter($rules, function ($rule) { 193 | return $rule->onlyCondition(); 194 | }); 195 | 196 | return $relevantConditions; 197 | } 198 | 199 | /** 200 | * Returns an associative array of aliases. 201 | * The key is the target and the value is an array of actions aliasing the key. 202 | * 203 | * @return array 204 | */ 205 | public function getAliases() 206 | { 207 | if (! $this->aliases) { 208 | $this->initDefaultAliases(); 209 | } 210 | return parent::getAliases(); 211 | } 212 | 213 | protected function addAliasAction($target, $actions) 214 | { 215 | $this->validateTarget($target); 216 | if (! array_key_exists($target, $this->getAliasedActions())) { 217 | $this->aliasedActions[$target] = []; 218 | } 219 | $this->aliasedActions[$target] = array_unique(array_merge($this->getAliasedActions()[$target], $actions)); 220 | } 221 | 222 | // User shouldn't specify targets with names of real actions or it will cause Seg fault 223 | protected function validateTarget($target) 224 | { 225 | if (in_array($target, array_flatten(array_values($this->getAliasedActions())))) { 226 | throw new \Exception("You can't specify target ($target) as alias because it is real action name", 1); 227 | } 228 | } 229 | 230 | // Returns an associative array of aliased actions. 231 | // The key is the target and the value is an array of actions aliasing the key. 232 | protected function getAliasedActions() 233 | { 234 | if (! $this->aliasedActions) { 235 | $this->aliasedActions = $this->getDefaultAliasActions(); 236 | } 237 | return $this->aliasedActions; 238 | } 239 | 240 | 241 | public function getUnauthorizedMessage($action, $subject) 242 | { 243 | $keys = $this->getUnauthorizedMessageKeys($action, $subject); 244 | $variables = ['action' => $action]; 245 | $variables['subject'] = class_exists($subject) ? $subject : snake_case($subject, ' '); 246 | $transKey = null; 247 | foreach ($keys as $key) { 248 | if (\Lang::has('messages.unauthorized.'.$key)) { 249 | $transKey = "messages.unauthorized.".$key; 250 | break; 251 | } 252 | } 253 | $message = ac_trans($transKey, $variables); 254 | return $message ?: null; 255 | } 256 | 257 | protected function getUnauthorizedMessageKeys($action, $subject) 258 | { 259 | $subject = snake_case(class_exists($subject) ? $subject : $subject); 260 | return array_flatten(array_map(function ($trySubject) use ($action) { 261 | return array_map(function ($tryAction) use ($trySubject, $action) { 262 | return "$tryAction.$trySubject"; 263 | }, array_flatten([$action, $this->getAliasesForAction($action), 'manage'])); 264 | }, [$subject, 'all'])); 265 | } 266 | 267 | // Accepts an array of actions and returns an array of actions which match. 268 | // This should be called before "matches" and other checking methods since they 269 | // rely on the actions to be expanded. 270 | public function getExpandActions($actions) 271 | { 272 | $actions = (array) $actions; 273 | return array_flatten(array_map(function ($action) use ($actions) { 274 | return array_key_exists($action, $this->getAliasedActions()) ? [$action, $this->getExpandActions($this->getAliasedActions()[$action])] : $action; 275 | }, $actions)); 276 | } 277 | 278 | // Given an action, it will try to find all of the actions which are aliased to it. 279 | // This does the opposite kind of lookup as 'getExpandActions()'. 280 | public function getAliasesForAction($action) 281 | { 282 | $action = (array) $action; 283 | $results = []; 284 | foreach ($this->getAliasedActions() as $aliasedAction => $actions) { 285 | if (array_intersect($action, $actions)) { 286 | $results = array_merge($results, parent::getAliasesForAction($aliasedAction)); 287 | } 288 | } 289 | 290 | return array_unique($results); 291 | } 292 | 293 | protected function getDefaultAliasActions() 294 | { 295 | return [ 296 | 'read' => ['index', 'show'], 297 | 'create' => ['new', 'store'], 298 | 'update' => ['edit'], 299 | 'delete' => ['destroy'], 300 | //'manage' => ['any actions'], 301 | ]; 302 | } 303 | 304 | protected function initDefaultAliases() 305 | { 306 | foreach ($this->getDefaultAliasActions() as $name => $actions) { 307 | $this->addAlias($name, $actions); 308 | } 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /src/Efficiently/AuthorityController/AuthorityControllerServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 27 | __DIR__ . '/../../config/config.php' => config_path('authority-controller.php') 28 | ], 'config'); 29 | 30 | // Publish migrations 31 | $this->publishes([ 32 | __DIR__ . '/../../migrations/' => base_path('database/migrations') 33 | ], 'migrations'); 34 | 35 | // Load translations 36 | $this->loadTranslationsFrom(__DIR__ . '/../../lang', 'authority-controller'); 37 | 38 | // Publish translations 39 | $this->publishes([ 40 | __DIR__ . '/../../translations' => base_path('resources/lang') 41 | ], 'translations'); 42 | } 43 | 44 | /** 45 | * Register the service provider. 46 | * 47 | * @return void 48 | */ 49 | public function register() 50 | { 51 | $this->app->singleton('parameters', function ($app) { 52 | return new Parameters; 53 | }); 54 | 55 | // Find the default Controller class of the current Laravel application 56 | $controllerClass = $this->app['config']->get( 57 | 'authority-controller.controllerClass', 58 | 'Illuminate\Routing\Controller' 59 | ); 60 | 61 | $this->app->resolving(function ($object) use ($controllerClass) { 62 | // Check if the current $object class is a Controller class and if it responds to paramsBeforeFilter method 63 | if (is_a($object, $controllerClass) && respond_to($object, 'paramsBeforeFilter')) { 64 | // Fill $params properties of the current controller 65 | $this->app['parameters']->fillController($object); 66 | } 67 | }); 68 | 69 | $this->app->singleton('authority', function ($app) { 70 | $user = $app['auth']->user(); 71 | $authority = new Authority($user); 72 | 73 | $fn = $app['config']->get('authority-controller.initialize'); 74 | 75 | $serializer = new Serializer; 76 | if (is_string($fn)) { 77 | $fn = $serializer->unserialize($fn); 78 | } 79 | 80 | if ($fn) { 81 | $fn($authority); 82 | } 83 | 84 | return $authority; 85 | }); 86 | 87 | $this->app->bind('Efficiently\AuthorityController\ControllerResource', function ($app, $parameters) { 88 | list($controller, $resourceName, $resourceOptions) = $parameters; 89 | return new ControllerResource($controller, $resourceName, $resourceOptions); 90 | }); 91 | 92 | } 93 | 94 | /** 95 | * Get the services provided by the provider. 96 | * 97 | * @return array 98 | */ 99 | public function provides() 100 | { 101 | return ['authority']; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Efficiently/AuthorityController/ControllerAdditions.php: -------------------------------------------------------------------------------- 1 | $listener) { 47 | $remove = false; // flag 48 | if ($controllerName === "*") { // All Controllers 49 | if (starts_with($eventName, "router.filter: controller.")) { 50 | $remove = true; 51 | } 52 | } elseif (preg_match("/^router\.filter: controller\.[^.]+?\.$controllerName/", $eventName)) { 53 | $remove = true; 54 | } 55 | if ($remove) { 56 | $events->forget($eventName); 57 | } 58 | } 59 | 60 | } 61 | 62 | /** 63 | * Register a "before" filter on the controller. 64 | * 65 | * @param \Closure|string $filter 66 | * @param array $options 67 | * @return void 68 | */ 69 | public function beforeFilter($filter, array $options = []) 70 | { 71 | $this->beforeFilters[] = $this->parseFilter($filter, $options); 72 | } 73 | 74 | /** 75 | * Register an "after" filter on the controller. 76 | * 77 | * @param \Closure|string $filter 78 | * @param array $options 79 | * @return void 80 | */ 81 | public function afterFilter($filter, array $options = []) 82 | { 83 | $this->afterFilters[] = $this->parseFilter($filter, $options); 84 | } 85 | 86 | /** 87 | * Parse the given filter and options. 88 | * 89 | * @param string $filter 90 | * @param array $options 91 | * @return array 92 | */ 93 | protected function parseFilter($filter, array $options) 94 | { 95 | // AuthorityController doesn't have filters with parameters 96 | $parameters = []; 97 | $original = $filter; 98 | 99 | return compact('original', 'filter', 'parameters', 'options'); 100 | } 101 | 102 | /** 103 | * Remove the given before filter. 104 | * 105 | * @param string $filter 106 | * @return void 107 | */ 108 | public function forgetBeforeFilter($filter) 109 | { 110 | $this->beforeFilters = $this->removeFilter($filter, $this->getBeforeFilters()); 111 | } 112 | 113 | /** 114 | * Remove the given after filter. 115 | * 116 | * @param string $filter 117 | * @return void 118 | */ 119 | public function forgetAfterFilter($filter) 120 | { 121 | $this->afterFilters = $this->removeFilter($filter, $this->getAfterFilters()); 122 | } 123 | 124 | /** 125 | * Remove the given controller filter from the provided filter array. 126 | * 127 | * @param string $removing 128 | * @param array $current 129 | * @return array 130 | */ 131 | protected function removeFilter($removing, $current) 132 | { 133 | return array_filter($current, function ($filter) use ($removing) { 134 | return $filter['original'] != $removing; 135 | }); 136 | } 137 | 138 | /** 139 | * Get the registered "before" filters. 140 | * 141 | * @return array 142 | */ 143 | public function getBeforeFilters() 144 | { 145 | return $this->beforeFilters; 146 | } 147 | 148 | /** 149 | * Get the registered "after" filters. 150 | * 151 | * @return array 152 | */ 153 | public function getAfterFilters() 154 | { 155 | return $this->afterFilters; 156 | } 157 | 158 | /** 159 | * Execute an action on the controller. 160 | * 161 | * @param string $method 162 | * @param array $parameters 163 | * @return \Symfony\Component\HttpFoundation\Response 164 | */ 165 | public function callAction($method, $parameters) 166 | { 167 | $route = app('router')->current(); 168 | $request = app('request'); 169 | $this->assignAfter($route, $request, $method); 170 | $response = $this->before($route, $request, $method); 171 | 172 | if (is_null($response)) { 173 | $response = call_user_func_array([$this, $method], $parameters); 174 | } 175 | 176 | return $response; 177 | } 178 | 179 | /** 180 | * Call the "before" filters for the controller. 181 | * 182 | * @param \Illuminate\Routing\Route $route 183 | * @param \Illuminate\Http\Request $request 184 | * @param string $method 185 | * @return mixed 186 | */ 187 | protected function before($route, $request, $method) 188 | { 189 | foreach ($this->getBeforeFilters() as $filter) { 190 | if ($this->filterApplies($filter, $request, $method)) { 191 | // Here we will just check if the filter applies. If it does we will call the filter 192 | // and return the responses if it isn't null. If it is null, we will keep hitting 193 | // them until we get a response or are finished iterating through this filters. 194 | $response = $this->callFilter($filter, $route, $request); 195 | 196 | if (!is_null($response)) { 197 | return $response; 198 | } 199 | } 200 | } 201 | } 202 | 203 | /** 204 | * Apply the applicable after filters to the route. 205 | * 206 | * @param \Illuminate\Routing\Route $route 207 | * @param \Illuminate\Http\Request $request 208 | * @param string $method 209 | * @return mixed 210 | */ 211 | protected function assignAfter($route, $request, $method) 212 | { 213 | foreach ($this->getAfterFilters() as $filter) { 214 | // If the filter applies, we will add it to the route, since it has already been 215 | // registered with the router by the controller, and will just let the normal 216 | // router take care of calling these filters so we do not duplicate logics. 217 | if ($this->filterApplies($filter, $request, $method)) { 218 | $route->after($this->getAssignableAfter($filter)); 219 | } 220 | } 221 | } 222 | 223 | /** 224 | * Get the assignable after filter for the route. 225 | * 226 | * @param string $filter 227 | * @return string 228 | */ 229 | protected function getAssignableAfter($filter) 230 | { 231 | return $filter['original']; 232 | } 233 | 234 | /** 235 | * Determine if the given filter applies to the request. 236 | * 237 | * @param array $filter 238 | * @param \Illuminate\Http\Request $request 239 | * @param string $method 240 | * @return bool 241 | */ 242 | protected function filterApplies($filter, $request, $method) 243 | { 244 | if ($this->filterFailsMethod($filter, $request, $method)) { 245 | return false; 246 | } 247 | 248 | return true; 249 | } 250 | 251 | /** 252 | * Determine if the filter fails the method constraints. 253 | * 254 | * @param array $filter 255 | * @param \Illuminate\Http\Request $request 256 | * @param string $method 257 | * @return bool 258 | */ 259 | protected function filterFailsMethod($filter, $request, $method) 260 | { 261 | return $this->methodExcludedByOptions($method, $filter['options']); 262 | } 263 | 264 | /** 265 | * Call the given controller filter method. 266 | * 267 | * @param array $filter 268 | * @param \Illuminate\Routing\Route $route 269 | * @param \Illuminate\Http\Request $request 270 | * @return mixed 271 | */ 272 | protected function callFilter($filter, $route, $request) 273 | { 274 | return $this->callRouteFilter( 275 | $filter['filter'], $filter['parameters'], $route, $request 276 | ); 277 | } 278 | 279 | /** 280 | * Determine if the given options exclude a particular method. 281 | * 282 | * @param string $method 283 | * @param array $options 284 | * @return bool 285 | */ 286 | protected function methodExcludedByOptions($method, array $options) 287 | { 288 | return (isset($options['only']) && ! in_array($method, (array) $options['only'])) || 289 | (! empty($options['except']) && in_array($method, (array) $options['except'])); 290 | } 291 | 292 | /** 293 | * Call the given route filter. 294 | * 295 | * @param string $filter 296 | * @param array $parameters 297 | * @param \Illuminate\Routing\Route $route 298 | * @param \Illuminate\Http\Request $request 299 | * @param \Illuminate\Http\Response|null $response 300 | * @return mixed 301 | */ 302 | protected function callRouteFilter($filter, $parameters, $route, $request, $response = null) 303 | { 304 | $data = array_merge([$route, $request, $response], $parameters); 305 | 306 | return Event::until('router.filter: '.$filter, $this->cleanFilterParameters($data)); 307 | } 308 | 309 | /** 310 | * Clean the parameters being passed to a filter callback. 311 | * 312 | * @param array $parameters 313 | * @return array 314 | */ 315 | protected function cleanFilterParameters(array $parameters) 316 | { 317 | return array_filter($parameters, function ($p) { 318 | return !is_null($p) && $p !== ''; 319 | }); 320 | } 321 | 322 | public function paramsBeforeFilter($filter, array $options = []) 323 | { 324 | $this->prependBeforeFilter($filter, $options); 325 | } 326 | 327 | /** 328 | * Register a new "before" filter before any "before" filters on the controller. 329 | * 330 | * @param string $filter 331 | * @param array $options 332 | * @return void 333 | */ 334 | public function prependBeforeFilter($filter, array $options = []) 335 | { 336 | array_unshift($this->beforeFilters, $this->parseFilter($filter, $options)); 337 | } 338 | 339 | /** 340 | * Register a new "after" filter before any "after" filters on the controller. 341 | * 342 | * @param string $filter 343 | * @param array $options 344 | * @return void 345 | */ 346 | public function prependAfterFilter($filter, array $options = []) 347 | { 348 | array_unshift($this->afterFilters, $this->parseFilter($filter, $options)); 349 | } 350 | 351 | /** 352 | * Sets up a before filter which loads and authorizes the current resource. This performs both 353 | * loadResource() and authorizeResource() and accepts the same arguments. See those methods for details. 354 | * 355 | * class BooksController extends Controller 356 | * { 357 | * public function __construct() 358 | * { 359 | * $this->loadAndAuthorizeResource(); 360 | * } 361 | * } 362 | * 363 | */ 364 | public function loadAndAuthorizeResource($args = null) 365 | { 366 | $args = is_array($args) ? $args : func_get_args(); 367 | ControllerResource::addBeforeFilter($this, __METHOD__, $args); 368 | } 369 | 370 | /** 371 | * Sets up a before filter which loads the model resource into an instance variable. 372 | * For example, given an ArticlesController it will load the current article into the @article 373 | * instance variable. It does this by either calling Article->find($this->params['id']); or 374 | * new Article($this->params['article']); depending upon the action. The index action will 375 | * automatically set $this->articles to Article::get(); or Article::$options['collectionScope']()->get(); 376 | * 377 | * If a conditional callback is used in the Authority, the 'create' and 'store' actions will set 378 | * the initial attributes based on these conditions. This way these actions will satisfy 379 | * the authority restrictions. 380 | * 381 | * Call this method directly on the controller class. 382 | * 383 | * class BooksController extends Controller 384 | * { 385 | * public function __construct() 386 | * { 387 | * $this->loadAndAuthorizeResource(); 388 | * } 389 | * } 390 | * 391 | * A resource is not loaded if the instance variable is already set. This makes it easy to override 392 | * the behavior through a beforeFilter() on certain actions. 393 | * 394 | * class BooksController extends Controller 395 | * { 396 | * public function __construct() 397 | * { 398 | * $this->beforeFilter('findBookByPermalink', ['only' => 'show']); 399 | * $this->loadAndAuthorizeResource(); 400 | * } 401 | * 402 | * protected function findBookByPermalink() 403 | * { 404 | * $this->book = Book::where('permalink', $this->params['id'])->firstOrFail(); 405 | * } 406 | * } 407 | * 408 | * If a name is provided which does not match the controller it assumes it is a parent resource. Child 409 | * resources can then be loaded through it. 410 | * 411 | * class BooksController extends Controller 412 | * { 413 | * public function __construct() 414 | * { 415 | * $this->loadResource('author'); 416 | * $this->loadResource('book', ['through' => 'author']); 417 | * } 418 | * } 419 | * 420 | * Here the author resource will be loaded before each action using $this->params['author_id']. The book resource 421 | * will then be loaded through the $this->author instance variable. 422 | * 423 | * That first argument is optional and will default to the singular name of the controller. 424 | * A hash of options (see below) can also be passed to this method to further customize it. 425 | * 426 | * See loadAndAuthorizeResource() to automatically authorize the resource too. 427 | * 428 | * Options: 429 | * ['only'] 430 | * Only applies before filter to given actions. 431 | * 432 | * ['except'] 433 | * Does not apply before filter to given actions. 434 | * 435 | * ['through'] 436 | * Load this resource through another one. This should match the name of the parent instance variable or method. 437 | * 438 | * ['throughAssociation'] 439 | * The name of the association to fetch the child records through the parent resource. 440 | * This is normally not needed because it defaults to the pluralized resource name. 441 | * 442 | * ['shallow'] 443 | * Pass true to allow this resource to be loaded directly when parent is null. 444 | * Defaults to false. 445 | * 446 | * ['singleton'] 447 | * Pass true if this is a singleton resource through a hasOne association. 448 | * 449 | * ['parent'] 450 | * True or false depending on if the resource is considered a parent resource. 451 | * This defaults to true if a resource 452 | * name is given which does not match the controller. 453 | * 454 | * ['class'] 455 | * The class to use for the model (string). 456 | * 457 | * ['instanceName'] 458 | * The name of the instance variable to load the resource into. 459 | * 460 | * ['findBy'] 461 | * Find using a different attribute other than id. For example. 462 | * 463 | * $this->loadResource(['findBy' => 'permalink']); 464 | * // will use where('permalink', $this->params['id'])->firstOrFail() 465 | * 466 | * ['idParam'] 467 | * Find using a param key other than 'id'. For example: 468 | * 469 | * $this->loadResource(['idParam' => 'url']); // will use find($this->params['url']) 470 | * 471 | * ['collection'] 472 | * Specify which actions are resource collection actions in addition to index. This 473 | * is usually not necessary because it will try to guess depending on if the id param is present. 474 | * 475 | * $this->loadResource(['collection' => ['sort', 'list']]); 476 | * 477 | * ['create'] 478 | * Specify which actions are new resource actions in addition to new, create 479 | * and store. 480 | * Pass an action name into here if you would like to build a new resource instead of 481 | * fetch one. 482 | * 483 | * $this->loadResource(['create' => 'build']); 484 | * 485 | * ['collectionScope'] 486 | * The name of the query scope to fetch the collection records of collection actions (E.g. index action). 487 | * 488 | * $this->loadResource(['collectionScope' => 'scopePopular']); // will use Article::popular()->get(); to fetch records of collection actions 489 | * 490 | * You can pass parameters with an array. For example: 491 | * 492 | * $this->loadResource(['collectionScope' => ['scopeOfType', 'published']]); // will use Article::ofType('published')->get(); 493 | * 494 | * By default, collection actions (index action) returns all the collection record with: 495 | * 496 | * Article::get(); // which is equivalent to Article::get(); 497 | * 498 | * ['prepend'] 499 | * Passing true will use prependBeforeFilter() instead of a normal beforeFilter(). 500 | * 501 | */ 502 | public function loadResource($args = null) 503 | { 504 | $args = is_array($args) ? $args : func_get_args(); 505 | ControllerResource::addBeforeFilter($this, __METHOD__, $args); 506 | } 507 | 508 | /** 509 | * Sets up a before filter which authorizes the resource using the instance variable. 510 | * For example, if you have an ArticlesController it will check the $this->article instance variable 511 | * and ensure the user can perform the current action on it. Under the hood it is doing 512 | * something like the following. 513 | * 514 | * $this->authorize($this->params['action'], $this->article ?: 'Article') 515 | * 516 | * Call this method directly on the controller class. 517 | * 518 | * class BooksController extends Controller 519 | * { 520 | * public function __construct() 521 | * { 522 | * $this->authorizeResource(); 523 | * } 524 | * } 525 | * 526 | * If you pass in the name of a resource which does not match the controller it will assume 527 | * it is a parent resource. 528 | * 529 | * class BooksController extends Controller 530 | * { 531 | * public function __construct() 532 | * { 533 | * $this->authorizeResource('author'); 534 | * $this->authorizeResource('book'); 535 | * } 536 | * } 537 | * 538 | * Here it will authorize 'show', $this->author on every action before authorizing the book. 539 | * 540 | * That first argument is optional and will default to the singular name of the controller. 541 | * A hash of options (see below) can also be passed to this method to further customize it. 542 | * 543 | * See loadAndAuthorizeResource() to automatically load the resource too. 544 | * 545 | * Options: 546 | * ['only'] 547 | * Only applies before filter to given actions. 548 | * 549 | * ['except'] 550 | * Does not apply before filter to given actions. 551 | * 552 | * ['singleton'] 553 | * Pass true if this is a singleton resource through a hasOne association. 554 | * 555 | * ['parent'] 556 | * True or false depending on if the resource is considered a parent resource. This defaults to true if a resource 557 | * name is given which does not match the controller. 558 | * 559 | * ['class'] 560 | * The class to use for the model (string). This passed in when the instance variable is not set. 561 | * Pass false if there is no associated class for this resource and it will use a symbol of the resource name. 562 | * 563 | * ['instance_name'] 564 | * The name of the instance variable for this resource. 565 | * 566 | * ['through'] 567 | * Authorize conditions on this parent resource when instance isn't available. 568 | * 569 | * ['prepend'] 570 | * Passing true will use prependBeforeFilter() instead of a normal beforeFilter(). 571 | * 572 | */ 573 | public function authorizeResource($args = null) 574 | { 575 | $args = is_array($args) ? $args : func_get_args(); 576 | ControllerResource::addBeforeFilter($this, __METHOD__, $args); 577 | } 578 | 579 | // TODO: Add checkAuthorization() ? 580 | // More infos at: https://github.com/ryanb/cancan/blob/master/lib/cancan/controller_additions.rb#L256 581 | 582 | /** 583 | * Throws a Efficiently\AuthorityController\Exceptions\AccessDenied exception if the currentAuthority cannot 584 | * perform the given action. This is usually called in a controller action or 585 | * before filter to perform the authorization. 586 | * 587 | * public function show($id) 588 | * { 589 | * $this->article = Article::find($id); // Tips: instead of $id, you can use $this->params['id'] 590 | * $this->authorize('read', $this->article); 591 | * 592 | * // But you still need to return the view 593 | * // return view('articles.show', compact_property($this, 'article')); 594 | * } 595 | * 596 | * A 'message' option can be passed to specify a different message. 597 | * 598 | * $this->authorize('read', $this->article, ['message' => "Not authorized to read ".$this->article->name]); 599 | * 600 | * You can also use I18n to customize the message. Action aliases defined in Authority work here. 601 | * 602 | * return [ 603 | * 'unauthorized' => [ 604 | * 'manage' => [ 605 | * 'all' => "Not authorized to :action :subject.", 606 | * 'user' => "Not allowed to manage other user accounts.", 607 | * ], 608 | * 'update' => [ 609 | * 'project' => "Not allowed to update this project." 610 | * ], 611 | * ], 612 | * ]; 613 | * 614 | * You can catch the exception and modify its behavior in the report() method of the app/Exceptions/Handler.php file. 615 | * For example here we set the error message to a flash and redirect to the home page. 616 | * 617 | * public function report(Exception $e) 618 | * { 619 | * if ($e instanceof \Efficiently\AuthorityController\Exceptions\AccessDenied) { 620 | * $msg = $e->getMessage(); 621 | * \Log::error('Access denied! '.$msg); 622 | * 623 | * return redirect()->route('home')->with('flash_alert', $msg); 624 | * } 625 | * 626 | * return parent::report($e); 627 | * } 628 | * 629 | * //code... 630 | * 631 | * See the Efficiently\AuthorityController\Exceptions\AccessDenied exception for more details on working with the exception. 632 | * 633 | * See the loadAndAuthorizeResource() method to automatically add the authorize() behavior 634 | * to the default RESTful actions. 635 | * 636 | */ 637 | public function authorize($args = null) 638 | { 639 | $args = is_array($args) ? $args : func_get_args(); 640 | $this->_authorized = true; 641 | return call_user_func_array([$this->getCurrentAuthority(), 'authorize'], $args); 642 | } 643 | 644 | public function setCurrentAuthority($authority) 645 | { 646 | $this->currentAuthority = $authority; 647 | } 648 | 649 | // alias of setCurrentAuthority() to match CanCan API 650 | public function setCurrentAbility($ability) 651 | { 652 | $this->setCurrentAuthority($ability); 653 | } 654 | 655 | /** 656 | * Creates and returns the current user's authority and caches it. If you 657 | * want to override how the Authority is defined then this is the place. 658 | * Just define the method in the controller to change behavior. 659 | * 660 | * public function getCurrentAuthority() 661 | * { 662 | * // instead of app('authority'); 663 | * $this->currentAuthority = $this->currentAuthority ?: app('UserAuthority', [$this->getCurrentAccount()]); 664 | * 665 | * return $this->currentAuthority; 666 | * } 667 | * 668 | * Notice it is important to cache the authority object so it is not 669 | * recreated every time. 670 | * 671 | */ 672 | public function getCurrentAuthority() 673 | { 674 | if (is_null($this->currentAuthority)) { 675 | $this->currentAuthority = app('authority'); 676 | } 677 | 678 | return $this->currentAuthority; 679 | } 680 | 681 | // alias of getCurrentAuthority() to match CanCan API 682 | public function getCurrentAbility() 683 | { 684 | return $this->getCurrentAuthority(); 685 | } 686 | 687 | public function getCurrentUser() 688 | { 689 | if (is_null($this->currentUser)) { 690 | $this->currentUser = $this->getCurrentAuthority()->getCurrentUser(); 691 | } 692 | 693 | return $this->currentUser; 694 | } 695 | 696 | /** 697 | * Use in the controller or view to check the user's permission for a given action 698 | * and object. 699 | * 700 | * $this->can('destroy', $this->project); 701 | * 702 | * You can also pass the class instead of an instance (if you don't have one handy). 703 | * 704 | * @if (Authority::can('create', 'Project')) 705 | * {{ link_to_route('projects.create', "New Project") }} 706 | * @endif 707 | * 708 | * If it's a nested resource, you can pass the parent instance in an associative array. This way it will 709 | * check conditions which reach through that association. 710 | * 711 | * @if (Authority::can('create', ['Project' => $category])) 712 | * {{ link_to_route('categories.projects.create', "New Project") }} 713 | * @endif 714 | * 715 | * This simply calls "can()" on the $this->currentAuthority. See Authority::can(). 716 | * 717 | */ 718 | public function can($args = null) 719 | { 720 | $args = is_array($args) ? $args : func_get_args(); 721 | return call_user_func_array([$this->getCurrentAuthority(), 'can'], $args); 722 | } 723 | 724 | /** 725 | * Convenience method which works the same as "can()" but returns the opposite value. 726 | * 727 | * $this->cannot('destroy', $this->project); 728 | * 729 | */ 730 | public function cannot($args = null) 731 | { 732 | $args = is_array($args) ? $args : func_get_args(); 733 | return call_user_func_array([$this->getCurrentAuthority(), 'cannot'], $args); 734 | } 735 | 736 | // setParams() should be forbidden for security reasons ? 737 | // public function setParams($params = []) 738 | // { 739 | // $this->params = $params; 740 | // } 741 | 742 | public function getParams() 743 | { 744 | return (array) $this->params; 745 | } 746 | } 747 | -------------------------------------------------------------------------------- /src/Efficiently/AuthorityController/ControllerResource.php: -------------------------------------------------------------------------------- 1 | $method(); 49 | }); 50 | 51 | call_user_func_array([$controller, $beforeFilterMethod], [$filterName, array_only($options, ['only', 'except'])]); 52 | } 53 | } 54 | 55 | public function __construct($controller, $name = null, $options = []) 56 | { 57 | $args = array_slice(func_get_args(), 1); 58 | $name = array_key_exists(0, $args) && is_string($args[0]) ? array_shift($args) : null; 59 | 60 | $lastArg = last($args); 61 | if (is_array($lastArg)) { 62 | $args = array_merge($args, array_extract_options($lastArg)); 63 | } 64 | $options = $options ?: array_extract_options($args); 65 | 66 | $this->controller = $controller; 67 | $this->params = $controller->getParams(); 68 | $this->name = $name; 69 | $this->options = $options; 70 | } 71 | 72 | public function loadAndAuthorizeResource() 73 | { 74 | $this->loadResource(); 75 | $this->authorizeResource(); 76 | } 77 | 78 | public function loadResource() 79 | { 80 | if ($this->loadedInstance()) { 81 | if (! $this->getResourceInstance()) { 82 | $this->setResourceInstance($this->loadResourceInstance()); 83 | } 84 | } elseif ($this->loadedCollection()) { 85 | // Load resources of collection actions (E.g. 'index') here. Even if we don't support $instance->accessibleBy() (see: https://github.com/ryanb/cancan/blob/f2f40c7aac4a00a88651641129eaad71916c1c82/lib/cancan/model_additions.rb#L22) 86 | if (! $this->getCollectionInstance()) { 87 | $this->setCollectionInstance($this->loadCollection()); 88 | } 89 | } 90 | } 91 | 92 | public function authorizeResource() 93 | { 94 | $resource = $this->getResourceInstance() ?: $this->getResourceClassWithParent(); 95 | $this->controller->authorize($this->getAuthorizationAction(), $resource); 96 | } 97 | 98 | public function isParent() 99 | { 100 | return array_key_exists('parent', $this->options) ? $this->options['parent'] : ($this->name && $this->name !== $this->getNameFromController()); 101 | } 102 | 103 | protected function loadResourceInstance() 104 | { 105 | if (! $this->isParent() && in_array($this->params['action'], $this->getCreateActions())) { 106 | return $this->buildResource(); 107 | } elseif ($this->getIdParam() || array_key_exists('singleton', $this->options)) { 108 | return $this->findResource(); 109 | } 110 | } 111 | 112 | protected function loadedInstance() 113 | { 114 | return $this->isParent() || $this->isMemberAction(); 115 | } 116 | 117 | protected function loadedCollection() 118 | { 119 | return ! $this->getCurrentAuthority()->hasCondition($this->getAuthorizationAction(), $this->getResourceClass()); 120 | } 121 | 122 | protected function loadCollection() 123 | { 124 | $resourceModel = app($this->getResourceBase()); 125 | $collectionScope = $this->getCollectionScopeWithParams(); 126 | $collection = $resourceModel; 127 | if ($collectionScope) { 128 | list($collectionScope, $collectionScopeParams) = $collectionScope; 129 | $collectionScope = camel_case(str_replace('scope', '', $collectionScope)); 130 | $collection = call_user_func_array([$collection, $collectionScope], $collectionScopeParams); 131 | } 132 | 133 | return $collection->get(); 134 | } 135 | 136 | protected function buildResource() 137 | { 138 | $resourceBase = $this->getResourceBase(); 139 | $resourceParams = $this->getResourceParams(); 140 | 141 | $resource = app($resourceBase, is_array($resourceParams) ? [$resourceParams] : []); 142 | 143 | return $this->setAttributes($resource); 144 | } 145 | 146 | protected function setAttributes($resource) 147 | { 148 | if (array_key_exists('singleton', $this->options) && $this->getParentResource()) { 149 | $resource->{camel_case($this->getParentName())}()->associate($this->getParentResource()); 150 | } 151 | // TODO: ?Implements initial attributes feature? 152 | // See: https://github.com/ryanb/cancan/blob/1.6.10/lib/cancan/controller_resource.rb#L91 153 | 154 | return $resource; 155 | } 156 | 157 | protected function findResource() 158 | { 159 | $resource = null; 160 | if (array_key_exists('singleton', $this->options) && respond_to($this->getParentResource(), $this->getName())) { 161 | $resource = call_user_func([$this->getParentResource(), $this->getName()]); 162 | } else { 163 | $resourceModel = app($this->getResourceBase()); 164 | if (array_key_exists('findBy', $this->options)) { 165 | if (respond_to($resourceModel, "findBy".studly_case($this->options['findBy']))) { 166 | $resource = call_user_func_array([$resourceModel, "findBy".studly_case($this->options['findBy']) ], [$this->getIdParam()]); 167 | } elseif (respond_to($resourceModel, camel_case($this->options['findBy']))) { 168 | $resource = call_user_func_array([$resourceModel, camel_case($this->options['findBy']) ], [$this->getIdParam()]); 169 | } else { 170 | $resource = $resourceModel->where($this->getResourcePrimaryKey(), $this->getIdParam())->firstOrFail(); 171 | } 172 | } else { 173 | $resource = $resourceModel->where($this->getResourcePrimaryKey(), $this->getIdParam())->firstOrFail(); 174 | } 175 | } 176 | 177 | if (! is_null($resource)) { 178 | return $resource; 179 | } 180 | throw new \Illuminate\Database\Eloquent\ModelNotFoundException; 181 | } 182 | 183 | protected function getAuthorizationAction() 184 | { 185 | return $this->isParent() ? "show" : $this->params['action']; 186 | } 187 | 188 | protected function getResourcePrimaryKey() 189 | { 190 | return (! array_key_exists('idParam', $this->options) && $this->isParent() && $this->getIdKey() === $this->getName()."_id") ? "id" : $this->getIdKey(); 191 | } 192 | 193 | protected function getIdKey() 194 | { 195 | if (array_key_exists('idParam', $this->options)) { 196 | return $this->options['idParam']; 197 | } else { 198 | return $this->isParent() ? $this->getName()."_id" : "id"; 199 | } 200 | } 201 | 202 | protected function getIdParam() 203 | { 204 | return array_key_exists($this->getIdKey(), $this->params) ? print_r($this->params[$this->getIdKey()], true) : ""; 205 | } 206 | 207 | protected function isMemberAction() 208 | { 209 | return in_array($this->params['action'], $this->getCreateActions()) || array_key_exists('singleton', $this->options) || ($this->getIdParam() && ! in_array($this->params['action'], $this->getCollectionActions())); 210 | } 211 | 212 | // Returns the class name used for this resource. This can be overriden by the 'class' option. 213 | // If false is passed in it will use the resource name as a lowercase string in which case it should 214 | // only be used for authorization, not loading since there's no class to load through. 215 | protected function getResourceClass() 216 | { 217 | if (array_key_exists('class', $this->options)) { 218 | if ($this->options['class'] === false) { 219 | return $this->getName(); 220 | } elseif (is_string($this->options['class'])) { 221 | return studly_case($this->options['class']); 222 | } 223 | } 224 | 225 | return studly_case($this->getNamespacedName()); 226 | } 227 | 228 | protected function getResourceClassWithParent() 229 | { 230 | // NOTICE: Against CanCan, we reverse the key and value, because in PHP an array key can't be an Object. 231 | return $this->getParentResource() ? [$this->getResourceClass() => $this->getParentResource()] : $this->getResourceClass(); 232 | } 233 | 234 | protected function setResourceInstance($instance) 235 | { 236 | $instanceName = $this->getInstanceName(); 237 | if (property_exists($this->controller, $instanceName)) { 238 | set_property($this->controller, $instanceName, $instance); 239 | } else { 240 | $this->controller->$instanceName = $instance; 241 | } 242 | } 243 | 244 | protected function getResourceInstance() 245 | { 246 | if ($this->loadedInstance()) { 247 | $instanceName = $this->getInstanceName(); 248 | if (property_exists($this->controller, $instanceName)) { 249 | return get_property($this->controller, $instanceName); 250 | } 251 | } 252 | } 253 | 254 | protected function setCollectionInstance($instance) 255 | { 256 | $instanceName = str_plural($this->getInstanceName()); 257 | if (property_exists($this->controller, $instanceName)) { 258 | set_property($this->controller, $instanceName, $instance); 259 | } else { 260 | $this->controller->$instanceName = $instance; 261 | } 262 | } 263 | 264 | protected function getCollectionInstance() 265 | { 266 | if ($this->loadedInstance()) { 267 | $instanceName = str_plural($this->getInstanceName()); 268 | if (property_exists($this->controller, $instanceName)) { 269 | return get_property($this->controller, $instanceName); 270 | } 271 | } 272 | } 273 | 274 | /** 275 | * The object that methods (such as "find", "new" or "build") are called on. 276 | * If the 'through' option is passed it will go through an association on that instance. 277 | * If the 'shallow' option is passed it will use the getResourceClass() method if there's no parent 278 | * If the 'singleton' option is passed it won't use the association because it needs to be handled later. 279 | */ 280 | protected function getResourceBase() 281 | { 282 | if (array_key_exists('through', $this->options)) { 283 | if ($this->getParentResource()) { 284 | if (array_key_exists('singleton', $this->options)) { 285 | return $this->getResourceClass(); 286 | } elseif (array_key_exists('throughAssociation', $this->options)) { 287 | $associationName = $this->options['throughAssociation']; 288 | return get_classname($this->getParentResource()->$associationName()->getModel()); 289 | } else { 290 | $associationName = str_plural(camel_case($this->getName())); 291 | return get_classname($this->getParentResource()->$associationName()->getModel()); 292 | } 293 | } elseif (array_key_exists('shallow', $this->options)) { 294 | return $this->getResourceClass(); 295 | } else { 296 | // Maybe this should be a record not found error instead? 297 | throw new Exceptions\AccessDenied(null, $this->getAuthorizationAction(), $this->getResourceClass()); 298 | } 299 | } else { 300 | return $this->getResourceClass(); 301 | } 302 | } 303 | 304 | protected function getParentName() 305 | { 306 | if (array_key_exists('through', $this->options)) { 307 | return array_first(array_flatten((array) $this->options['through']), function ($value) { 308 | return $this->fetchParent($value); 309 | }); 310 | } 311 | } 312 | 313 | // The object to load this resource through. 314 | protected function getParentResource() 315 | { 316 | if ($this->getParentName()) { 317 | return $this->fetchParent($this->getParentName()); 318 | } 319 | } 320 | 321 | protected function fetchParent($name) 322 | { 323 | $name = camel_case($name); 324 | if (property_exists($this->controller, $name)) { 325 | return get_property($this->controller, $name); 326 | } elseif (respond_to($this->controller, "get".studly_case($name))) { 327 | $name ="get".studly_case($name); 328 | return $this->controller->$name(); 329 | } elseif (respond_to($this->controller, $name)) { 330 | return $this->controller->$name(); 331 | } 332 | } 333 | 334 | protected function getResourceParams() 335 | { 336 | if (array_key_exists('class', $this->options)) { 337 | $paramsKey = $this->extractKey($this->options['class']); 338 | if (array_key_exists($paramsKey, $this->params)) { 339 | return $this->params[$paramsKey]; 340 | } 341 | } 342 | 343 | return $this->getResourceParamsByNamespacedName(); 344 | } 345 | 346 | protected function getResourceParamsByNamespacedName() 347 | { 348 | $paramsKey = $this->extractKey($this->getNamespacedName()); 349 | 350 | return array_key_exists($paramsKey, $this->params) ? $this->params[$paramsKey] : []; 351 | } 352 | 353 | protected function getNamespace($controllerName = null) 354 | { 355 | $controllerName = $controllerName ?: $this->params['controller']; 356 | 357 | return array_slice(preg_split("/\\\\|\//", $controllerName), 0, -1); 358 | } 359 | 360 | protected function getNamespacedName() 361 | { 362 | $namespaceName = null; 363 | $namespace = $this->getNamespace(); 364 | if (! empty($namespace)) { 365 | $namespaceName = studly_case(str_singular(implode("\\", array_flatten([$namespace, studly_case($this->getName())])))); 366 | } 367 | 368 | if (class_exists($namespaceName)) { 369 | return $namespaceName; 370 | } 371 | 372 | $className = studly_case($this->getName()); 373 | if (class_exists($className)) { 374 | // Support Laravel Alias 375 | $aliasLoader = \Illuminate\Foundation\AliasLoader::getInstance(); 376 | $aliasName = array_get($aliasLoader->getAliases(), $className); 377 | return class_exists($aliasName) ? $aliasName : $className; 378 | } 379 | 380 | $controllerNamespaces = $this->getNamespace(get_classname($this->controller)); 381 | // Detect the Root Namespace, based on the current controller namespace 382 | // And test if the resource class exists with it 383 | // Borrowed from: https://github.com/laravel/framework/blob/v5.0.13/src/Illuminate/Routing/UrlGenerator.php#L526 384 | if (! empty($controllerNamespaces) && ! (strpos($className, '\\') === 0)) { 385 | $rootNamespace = head($controllerNamespaces); 386 | $guessName = $rootNamespace.'\\'.$className; 387 | if (class_exists($guessName)) { 388 | return $guessName; 389 | } 390 | } 391 | 392 | return $this->getName(); 393 | } 394 | 395 | protected function getCurrentAuthority() 396 | { 397 | return $this->controller->getCurrentAuthority(); 398 | } 399 | 400 | // Alias of getCurrentAuthority() to match CanCan API 401 | protected function getCurrentAbility() 402 | { 403 | return $this->getCurrentAuthority(); 404 | } 405 | 406 | protected function getName() 407 | { 408 | return $this->name ? $this->name : $this->getNameFromController(); 409 | } 410 | 411 | protected function getNameFromController() 412 | { 413 | return static::getNameByController($this->params['controller']); 414 | } 415 | 416 | protected function getInstanceName() 417 | { 418 | if (array_key_exists('instanceName', $this->options)) { 419 | return $this->options['instanceName']; 420 | } else { 421 | return camel_case($this->getName()); 422 | } 423 | } 424 | 425 | protected function getCollectionActions() 426 | { 427 | $optionsCollection = array_key_exists('collection', $this->options) ? $this->options['collection'] : []; 428 | return array_unique(array_flatten(array_merge(['index'], (array) $optionsCollection))); 429 | } 430 | 431 | // NOTICE: Against Rails, 'new' action is named 'create' in Laravel. 432 | // And the Rails 'create' action is named 'store' in Laravel. 433 | protected function getCreateActions() 434 | { 435 | // We keep the 'new' option to match CanCan API 436 | $optionNew = array_key_exists('new', $this->options) ? $this->options['new'] : []; 437 | $optionCreate = array_key_exists('create', $this->options) ? $this->options['create'] : []; 438 | $options = array_merge((array) $optionNew, (array) $optionCreate); 439 | return array_unique(array_flatten(array_merge(['new', 'create', 'store'], $options))); 440 | } 441 | 442 | // Alias of getCreateActions() to match CanCan API 443 | protected function getNewActions() 444 | { 445 | return $this->getCreateActions(); 446 | } 447 | 448 | protected function extractKey($value) 449 | { 450 | return str_replace('/', '', snake_case(preg_replace('/\\\\/', '', $value))); 451 | } 452 | 453 | protected function getCollectionScope() 454 | { 455 | return array_key_exists('collectionScope', $this->options) ? $this->options['collectionScope'] : null; 456 | } 457 | 458 | public function getCollectionScopeWithParams() 459 | { 460 | $collectionScope = $this->getCollectionScope(); 461 | if ($collectionScope) { 462 | $collectionScopeParams = []; 463 | if (is_array($collectionScope)) { 464 | $collectionScopeParams = array_splice($collectionScope, 1); 465 | $collectionScope = array_shift($collectionScope); 466 | } 467 | return [$collectionScope, $collectionScopeParams]; 468 | } else { 469 | return $collectionScope; 470 | } 471 | } 472 | } 473 | -------------------------------------------------------------------------------- /src/Efficiently/AuthorityController/Exceptions/AccessDenied.php: -------------------------------------------------------------------------------- 1 | getMessage(); //-> "Not authorized!" 13 | * $exception->action; //-> 'read' 14 | * $exception->subject; //-> 'Product' 15 | */ 16 | class AccessDenied extends \Exception 17 | { 18 | public $action; 19 | public $subject; 20 | public $defaultMessage; 21 | 22 | public function __construct($message = null, $action = null, $subject = null, $code = 0, \Exception $previous = null) 23 | { 24 | $this->action = $action; 25 | $this->subject = $subject; 26 | $this->defaultMessage = ac_trans("messages.unauthorized.default"); 27 | $this->message = $message ?: $this->defaultMessage; 28 | 29 | parent::__construct($this->message, $code, $previous); 30 | } 31 | 32 | public function __toString() 33 | { 34 | return $this->message ?: $this->defaultMessage; 35 | } 36 | 37 | public function setDefaultMessage($value = null) 38 | { 39 | $this->defaultMessage = $value; 40 | $this->message = $this->message ?: $this->defaultMessage; 41 | } 42 | 43 | public function getDefaultMessage() 44 | { 45 | return $this->defaultMessage; 46 | } 47 | 48 | public function getAction() 49 | { 50 | return $this->action; 51 | } 52 | 53 | public function getSubject() 54 | { 55 | return $this->subject; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Efficiently/AuthorityController/Facades/Authority.php: -------------------------------------------------------------------------------- 1 | current(); 27 | $resourceParams = []; 28 | list($resourceParams['controller'], $resourceParams['action']) = explode('@', $router->currentRouteAction()); 29 | $resourceParams['controller'] = $this->normalizeControllerName($resourceParams['controller']); 30 | 31 | $resourceId = str_singular($resourceParams['controller']); 32 | if (request()->has($resourceId)) { 33 | $params = request()->all(); 34 | } else { 35 | $specialInputKeys = $this->specialInputKeys(); 36 | $params = [$resourceId => request()->except($specialInputKeys)] + request()->only($specialInputKeys); 37 | } 38 | $routeParams = $currentRoute->parametersWithoutNulls(); 39 | 40 | // In Laravel, unlike Rails, by default 'id' parameter of a 'Product' resource is 'products' 41 | // And 'shop_id' parameter of a 'Shop' parent resource is 'shops' 42 | // So we need to reaffect correct parameter name before any controller's actions or filters. 43 | $routeParamsParsed = []; 44 | $keysToRemove = []; 45 | $lastRouteParamKey = last(array_keys($routeParams)); 46 | if ($lastRouteParamKey === 'id' || $resourceId === str_singular($lastRouteParamKey)) { 47 | $id = last($routeParams); 48 | if (is_a($id, 'Illuminate\Database\Eloquent\Model')) { 49 | $id = $id->getKey(); 50 | } 51 | if (is_string($id) || is_numeric($id)) { 52 | array_pop($routeParams); 53 | $routeParamsParsed['id'] = $id; 54 | } 55 | } 56 | 57 | foreach ($routeParams as $parentIdKey => $parentIdValue) { 58 | if (is_a($parentIdValue, 'Illuminate\Database\Eloquent\Model')) { 59 | $parentIdValue = $parentIdValue->getKey(); 60 | } 61 | if (is_string($parentIdValue) || is_numeric($parentIdValue)) { 62 | if (! ends_with($parentIdKey, '_id')) { 63 | $parentIdKey = str_singular($parentIdKey).'_id'; 64 | } 65 | $routeParamsParsed[$parentIdKey] = $parentIdValue; 66 | $keysToRemove[] = $parentIdKey; 67 | } 68 | } 69 | $routeParams = array_except($routeParams, $keysToRemove); 70 | 71 | /** 72 | * You can escape or purify these parameters. For example: 73 | * 74 | * class ProductsController extends Controller 75 | * { 76 | * public function __construct() 77 | * { 78 | * $self = $this; 79 | * $this->beforeFilter(function () use($self) { 80 | * if (array_get($self->params, 'product')) { 81 | * $productParams = $this->yourPurifyOrEscapeMethod('product'); 82 | * $self->params['product'] = $productParams; 83 | * } 84 | * }); 85 | * } 86 | * } 87 | * 88 | */ 89 | $this->params = array_filter(array_merge($params, $routeParams, $routeParamsParsed, $resourceParams)); 90 | 91 | if (property_exists($controller, 'params')) { 92 | set_property($controller, 'params', $this->params); 93 | } else { 94 | $controller->params = $this->params; 95 | } 96 | }); 97 | 98 | $controller->paramsBeforeFilter($paramsFilterName); 99 | } 100 | } 101 | 102 | /** 103 | * Get an item from the parameters. 104 | * 105 | * @param string $key 106 | * @param mixed $default 107 | * @return mixed 108 | */ 109 | public function get($key, $default = null) 110 | { 111 | return array_get($this->params, $key, $default); 112 | } 113 | 114 | /** 115 | * Determine if the request contains a given parameter item. 116 | * 117 | * @param string|array $key 118 | * @return bool 119 | */ 120 | public function has($key) 121 | { 122 | return !!array_get($this->params, $key); 123 | } 124 | 125 | /** 126 | * Get all of the parameters for the request. 127 | * 128 | * @return array 129 | */ 130 | public function all() 131 | { 132 | return $this->params; 133 | } 134 | 135 | /** 136 | * Get a subset of the items from the parameters. 137 | * 138 | * @param array $keys 139 | * @return array 140 | */ 141 | public function only($keys = null) 142 | { 143 | $keys = is_array($keys) ? $keys : func_get_args(); 144 | return array_only($this->params, $keys); 145 | } 146 | 147 | /** 148 | * Get all of the input except for a specified array of items. 149 | * 150 | * @param array $keys 151 | * @return array 152 | */ 153 | public function except($keys = null) 154 | { 155 | $keys = is_array($keys) ? $keys : func_get_args(); 156 | return array_except($this->params, $keys); 157 | } 158 | 159 | /** 160 | * Adds an item to the parameters. 161 | * 162 | * @param string $key Key to add value to. 163 | * @param mixed $value New data. 164 | * 165 | * @return mixed 166 | */ 167 | public function add($key, $value) 168 | { 169 | return array_set($this->params, $key, $value); 170 | } 171 | 172 | /** 173 | * Returns all inputs keys who starts with an underscore character (_). 174 | * For exmaple '_method' and '_token' inputs 175 | * 176 | * @param array $inputKeys 177 | * @return array 178 | */ 179 | protected function specialInputKeys($inputKeys = []) 180 | { 181 | $inputKeys = $inputKeys ?: array_keys(request()->all()); 182 | return array_filter($inputKeys, function ($value) { 183 | return is_string($value) ? starts_with($value, '_') : false; 184 | }); 185 | } 186 | 187 | /** 188 | * @param string $controller 189 | * @return string 190 | */ 191 | protected function normalizeControllerName($controller) 192 | { 193 | $name = preg_replace("/^(.+)Controller$/", "$1", $controller); 194 | return str_plural(snake_case(class_basename($name))); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/Efficiently/AuthorityController/Rule.php: -------------------------------------------------------------------------------- 1 | action === 'manage' || in_array($this->action, $action); 35 | } 36 | 37 | /** 38 | * @return boolean 39 | */ 40 | public function onlyCondition() 41 | { 42 | return ! $this->isConditionsEmpty(); 43 | } 44 | 45 | /** 46 | * @return boolean 47 | */ 48 | public function isConditionsEmpty() 49 | { 50 | $conditions = $this->conditions; 51 | return empty($conditions); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Efficiently/AuthorityController/helpers.php: -------------------------------------------------------------------------------- 1 | $value) { 11 | if (is_string($key)) { 12 | $options[$key] = $value; 13 | unset($array[$key]); 14 | } 15 | } 16 | } 17 | 18 | return $options; 19 | } 20 | } 21 | 22 | if (! function_exists('get_classname')) { 23 | /** 24 | * Like get_class() function but compatible with Mockery and $object parameter is required 25 | * 26 | * @param object|\Mockery\MockInterface $object 27 | * @return string 28 | */ 29 | function get_classname($object) 30 | { 31 | $classname = get_class($object); 32 | if ($object instanceof \Mockery\MockInterface) { 33 | $classname = preg_replace('/_/', '\\', preg_replace('/^Mockery_[0-9]+_+(.+)$/', '$1', $classname)); 34 | } 35 | 36 | return $classname; 37 | } 38 | } 39 | 40 | if (! function_exists('respond_to')) { 41 | /** 42 | * Like method_exists function but compatible with Mockery 43 | * 44 | * @param mixed $object 45 | * @param string $methodName 46 | * @return boolean 47 | */ 48 | function respond_to($object, $methodName) 49 | { 50 | if (method_exists($object, $methodName)) { 51 | return true; 52 | } elseif (is_a($object, '\Mockery\MockInterface') && ($expectationDirector = array_get($object->mockery_getExpectations(), $methodName))) { 53 | foreach ((array) $expectationDirector->getExpectations() as $expectation) { 54 | if ($expectation->isEligible()) { 55 | return true; 56 | } 57 | } 58 | } elseif (is_string($object) && class_exists($object) && is_a(($instance=app($object)), '\Mockery\MockInterface')) { 59 | // Check if a mocked static method exists or not. You need to do: 60 | // 61 | // $category = Mockery::mock('alias:Category', ['getProducts'=>'products']); 62 | // App::instance('Category', $category); 63 | // respond_to('Category', 'getProducts');//-> true 64 | return respond_to($instance, $methodName); 65 | } 66 | 67 | return false; 68 | } 69 | } 70 | 71 | if (! function_exists('compact_property')) { 72 | function compact_property($instance, $properties) 73 | { 74 | $properties = array_slice(func_get_args(), 1); 75 | $compactArray = []; 76 | foreach ($properties as $property) { 77 | if (property_exists($instance, $property)) { 78 | $$property = get_property($instance, $property); 79 | 80 | $compactArray = array_merge($compactArray, compact($property)); 81 | } 82 | } 83 | 84 | return $compactArray; 85 | } 86 | } 87 | 88 | if (! function_exists('ac_trans')) { 89 | function ac_trans($id, $parameters = [], $domain = 'messages', $locale = null) 90 | { 91 | $namespace = null; 92 | // TODO: DRY conditions 93 | if (! Lang::has($id)) { 94 | $namespace = 'authority-controller::'; 95 | $id = $namespace.$id; 96 | 97 | if (! Lang::has($id)) { 98 | $defaultId = 'messages.unauthorized.default'; 99 | $id = $namespace.$defaultId; 100 | 101 | if (! Lang::has($id)) { 102 | $id = $defaultId; 103 | if (Lang::has($id, 'en')) { 104 | return trans($id, $parameters, $domain, 'en'); 105 | } 106 | return trans($namespace.$id, $parameters, $domain, 'en'); 107 | } 108 | } 109 | } 110 | 111 | return trans($id, $parameters, $domain, $locale); 112 | } 113 | } 114 | 115 | if (! function_exists('ac_trans_choice')) { 116 | function ac_trans_choice($id, $number, array $parameters = [], $domain = 'messages', $locale = null) 117 | { 118 | $namespace = null; 119 | // TODO: DRY conditions 120 | if (! Lang::has($id)) { 121 | $namespace = 'authority-controller::'; 122 | $id = $namespace.$id; 123 | 124 | if (! Lang::has($id)) { 125 | $defaultId = 'messages.unauthorized.default'; 126 | $id = $namespace.$defaultId; 127 | 128 | if (! Lang::has($id)) { 129 | $id = $defaultId; 130 | if (Lang::has($id, 'en')) { 131 | return trans_choice($id, $number, $parameters, $domain, 'en'); 132 | } 133 | return trans_choice($namespace.$id, $number, $parameters, $domain, 'en'); 134 | } 135 | } 136 | } 137 | 138 | return trans_choice($id, $number, $parameters, $domain, $locale); 139 | } 140 | } 141 | 142 | if (! function_exists('set_property')) { 143 | function set_property($object, $propertyName, $value) 144 | { 145 | if (property_exists($object, $propertyName)) { 146 | $reflection = new \ReflectionProperty($object, $propertyName); 147 | $reflection->setAccessible(true); 148 | $reflection->setValue($object, $value); 149 | } else { 150 | $object->$propertyName = $value; 151 | } 152 | } 153 | } 154 | 155 | if (! function_exists('get_property')) { 156 | function get_property($object, $propertyName) 157 | { 158 | if (property_exists($object, $propertyName)) { 159 | $reflection = new \ReflectionProperty($object, $propertyName); 160 | $reflection->setAccessible(true); 161 | return $reflection->getValue($object); 162 | } 163 | return null; 164 | } 165 | } 166 | 167 | if (! function_exists('invoke_method')) { 168 | function invoke_method($object, $methodName, $values = []) 169 | { 170 | $values = (array) $values; 171 | if (method_exists($object, $methodName)) { 172 | $reflection = new \ReflectionMethod($object, $methodName); 173 | $reflection->setAccessible(true); 174 | return $reflection->invokeArgs($object, $values); 175 | } 176 | return call_user_func_array([$object, $methodName], $values); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/config/config.php: -------------------------------------------------------------------------------- 1 | $serializer->serialize(function ($authority) { 8 | 9 | // Action aliases. For example: 10 | // 11 | // $authority->addAlias('moderate', ['read', 'update', 'delete']); 12 | // 13 | // See the wiki of AuthorityController for details: 14 | // https://github.com/efficiently/authority-controller/wiki/Action-aliases 15 | // 16 | // Define abilities for the passed in user here. For example: 17 | // $user = auth()->guest() ? new App\User : $authority->getCurrentUser(); 18 | // if ($user->hasRole('admin')) { 19 | // $authority->allow('manage', 'all'); 20 | // } else { 21 | // $authority->allow('read', 'all'); 22 | // } 23 | 24 | // The first argument to `allow` is the action you are giving the user 25 | // permission to do. 26 | // If you pass 'manage' it will apply to every action. Other common actions 27 | // here are 'read', 'create', 'update' and 'destroy'. 28 | // 29 | // The second argument is the resource the user can perform the action on. 30 | // If you pass 'all' it will apply to every resource. Otherwise pass a Eloquent 31 | // class name of the resource. 32 | // 33 | // The third argument is an optional anonymous function (Closure) to further filter the 34 | // objects. 35 | // For example, here the user can only update available products. 36 | // $authority->allow('update', App\Product::class, function ($self, $product) { 37 | // return $product->available === true; 38 | // }); 39 | 40 | // See the wiki of AuthorityController for details: 41 | // https://github.com/efficiently/authority-controller/wiki/Defining-Authority-rules 42 | // 43 | // Loop through each of the users permissions, and create rules: 44 | // foreach ($user->permissions as $perm) { 45 | // if ($perm->type == 'allow') { 46 | // $authority->allow($perm->action, $perm->resource); 47 | // } else { 48 | // $authority->deny($perm->action, $perm->resource); 49 | // } 50 | // } 51 | }) 52 | 53 | ]; 54 | -------------------------------------------------------------------------------- /src/lang/en/messages.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'default' => 'You are not authorized to access this page.', 6 | ], 7 | ]; 8 | -------------------------------------------------------------------------------- /src/lang/fr/messages.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'default' => "Vous n'êtes pas autorisé à accéder à cette page.", 6 | ], 7 | ]; 8 | -------------------------------------------------------------------------------- /src/migrations/2015_02_23_095033_create_roles_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 17 | $table->string('name'); 18 | $table->timestamps(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::drop('roles'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/migrations/2015_02_23_095107_create_permissions_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 17 | $table->string('type'); 18 | $table->string('action'); 19 | $table->string('resource'); 20 | $table->integer('user_id')->unsigned(); 21 | $table->timestamps(); 22 | 23 | $table->index('user_id'); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::drop('permissions'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/migrations/2015_02_23_095152_create_role_user_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 17 | $table->integer('role_id')->unsigned(); 18 | $table->integer('user_id')->unsigned(); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::drop('role_user'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/translations/en/messages.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'manage' => [ 6 | 'all' => 'You are not authorized to access this page.', 7 | // 'all' => "You do not have access to :action :subject!", 8 | ], 9 | ], 10 | ]; 11 | -------------------------------------------------------------------------------- /src/translations/fr/messages.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'manage' => [ 6 | 'all' => "Vous n'êtes pas autorisé à accéder à cette page.", 7 | // 'all' => "Vous n'avez pas accès à :action pour un(e) :subject!", 8 | ], 9 | ], 10 | ]; 11 | -------------------------------------------------------------------------------- /tests/AcAuthorityTest.php: -------------------------------------------------------------------------------- 1 | user = new stdClass; 12 | $this->user->id = 1; 13 | $this->user->email = "testuser@localhost"; 14 | $this->user->name = "TestUser"; 15 | 16 | $this->authority = app('authority'); 17 | $this->authority->setCurrentUser($this->user); 18 | } 19 | 20 | public function tearDown() 21 | { 22 | m::close(); 23 | } 24 | 25 | public function testCanStoreCurrentUser() 26 | { 27 | $this->assertSame($this->user, $this->authority->getCurrentUser()); 28 | 29 | $user = new stdClass; 30 | $this->authority->setCurrentUser($user); 31 | $this->assertSame($user, $this->authority->getCurrentUser()); 32 | } 33 | 34 | public function testCanEvaluateRulesOnObject() 35 | { 36 | $rulesCount = $this->authority->getRules()->count(); 37 | $this->authority->allow('destroy', 'Project', function ($self, $project) { 38 | return $self->user()->id === $project->user_id; 39 | }); 40 | $this->assertGreaterThan($rulesCount, $this->authority->getRules()->count()); 41 | 42 | $project = m::mock('Project'); 43 | $project->user_id = 1; 44 | $this->assertCan('destroy', $project); 45 | 46 | $this->assertCannot('destroy', new stdClass); 47 | } 48 | 49 | // A user cannot do anything without rules 50 | public function testCannotDoAnythingWithoutRules() 51 | { 52 | $project = $this->mock('Project'); 53 | 54 | $this->assertCount(0, $this->authority->getRules()); 55 | 56 | $this->assertCannot('read', 'all'); 57 | $this->assertCannot('read', $project); 58 | $this->assertCannot('read', 'Project'); 59 | 60 | $this->assertCannot('create', 'all'); 61 | $this->assertCannot('create', $project); 62 | $this->assertCannot('create', 'Project'); 63 | 64 | $this->assertCannot('update', 'all'); 65 | $this->assertCannot('update', $project); 66 | $this->assertCannot('update', 'Project'); 67 | 68 | $this->assertCannot('delete', 'all'); 69 | $this->assertCannot('delete', $project); 70 | $this->assertCannot('delete', 'Project'); 71 | 72 | $this->assertCannot('manage', 'all'); 73 | $this->assertCannot('manage', $project); 74 | $this->assertCannot('manage', 'Project'); 75 | } 76 | 77 | // Adding deny rule overrides prior rules 78 | public function testDenyRuleOverridesPriorRules() 79 | { 80 | $project = $this->mock('Project'); 81 | 82 | $this->authority->allow('manage', 'Project'); 83 | $this->authority->deny('destroy', 'Project'); 84 | 85 | $this->assertCannot('destroy', $project); 86 | $this->assertCan('read', $project); 87 | $this->assertCan('update', $project); 88 | } 89 | 90 | // Adding allow rules do not override prior rules, but instead are logically or'ed 91 | public function testAllowRulesDoNotOverridePriorRules() 92 | { 93 | $user = $this->mock("User"); 94 | $userAttributes = [ 95 | "id" => 1, "email" => "admin@localhost", "name" => "Administrator", 96 | "created_at" => "2013-12-17 10:17:21", "updated_at" => "2013-12-17 10:17:21" 97 | ]; 98 | $this->fillMock($user, $userAttributes); 99 | 100 | $this->authority = app('authority'); 101 | $this->authority->setCurrentUser($this->user); 102 | 103 | $this->authority->allow('read', 'User', function ($self, $user) { 104 | return $user->id != 1;// Should return false 105 | }); 106 | 107 | $this->authority->allow('read', 'User', function ($self, $user) { 108 | return $user->email == "admin@localhost";// Should return true 109 | }); 110 | 111 | $this->authority->allow('read', 'User', function ($self, $user) { 112 | return $user->name != "Administrator";// Should return false 113 | }); 114 | 115 | // $user can view 'index' action even if there is only one 'allow' rules which is true 116 | $this->assertCan('index', $user); 117 | } 118 | 119 | public function testRulesPrecedence() 120 | { 121 | $user = $this->mock("User"); 122 | $userAttributes = [ 123 | "id" => 1, "email" => "admin@localhost", "name" => "Administrator", 124 | "created_at" => "2013-12-17 10:17:21", "updated_at" => "2013-12-17 10:17:21" 125 | ]; 126 | $this->fillMock($user, $userAttributes); 127 | 128 | $this->authority = app('authority'); 129 | $this->authority->setCurrentUser($this->user); 130 | 131 | $this->authority->allow('read', 'User', function ($self, $user) { 132 | return $user->id != 1;// Should return false 133 | }); 134 | 135 | $this->authority->allow('read', 'User', function ($self, $user) { 136 | return $user->email != "admin@localhost";// Should return false 137 | }); 138 | 139 | $this->authority->allow('read', 'User', function ($self, $user) { 140 | return $user->name != "Administrator";// Should return false 141 | }); 142 | 143 | $this->authority->allow('update', 'User'); 144 | 145 | $this->assertCan('update', 'User'); 146 | $this->assertCan('update', $user); 147 | 148 | $this->assertCan('index', 'User'); 149 | 150 | // $user cannot view 'index' action if there is only 'allow' rules with conditions 151 | $this->assertCannot('index', $user); 152 | 153 | // $user can view 'index' action if there is above one 'allow' rule without conditions 154 | $this->authority->allow('index', 'User'); 155 | $this->assertCan('index', $user); 156 | 157 | // $user cannot view the 'index' action if there above one 'deny' rules with conditions 158 | $this->authority->deny('read', 'User', function ($self, $user) { 159 | return $user->name == "Administrator";// Should return true 160 | }); 161 | $this->assertCannot('index', $user); 162 | 163 | // Deny rule is overrided by allow rule 164 | $this->authority->allow('index', 'User'); 165 | $this->assertCan('index', $user); 166 | 167 | // $user cannot view the 'index' action if there above one 'deny' rules without conditions 168 | $this->authority->deny('index', 'User'); 169 | $this->assertCannot('index', $user); 170 | } 171 | 172 | public function testDefaultAliasActions() 173 | { 174 | $this->assertEquals(['read'], $this->authority->getAliasesForAction('index')); 175 | $this->assertEquals(['read'], $this->authority->getAliasesForAction('show')); 176 | 177 | $this->assertEquals(['create'], $this->authority->getAliasesForAction('new')); 178 | $this->assertEquals(['create'], $this->authority->getAliasesForAction('store')); 179 | 180 | $this->assertEquals(['update'], $this->authority->getAliasesForAction('edit')); 181 | 182 | $this->assertEquals(['delete'], $this->authority->getAliasesForAction('destroy')); 183 | 184 | $this->assertEquals(['read', 'index', 'show'], $this->authority->getExpandActions('read')); 185 | $this->assertEquals(['create', 'new', 'store'], $this->authority->getExpandActions('create')); 186 | $this->assertEquals(['update', 'edit'], $this->authority->getExpandActions('update')); 187 | $this->assertEquals(['delete', 'destroy'], $this->authority->getExpandActions('delete')); 188 | } 189 | 190 | // Helpers 191 | 192 | protected function assertCan($action, $resource, $resourceValue = null, $authority = null) 193 | { 194 | $authority = $authority ?: $this->authority; 195 | $this->assertTrue($authority->can($action, $resource, $resourceValue)); 196 | } 197 | 198 | protected function assertCannot($action, $resource, $resourceValue = null, $authority = null) 199 | { 200 | $authority = $authority ?: $this->authority; 201 | $this->assertTrue($this->authority->cannot($action, $resource, $resourceValue)); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /tests/AcControllerAdditionsTest.php: -------------------------------------------------------------------------------- 1 | controllerClass = $this->mock('AcControllerAdditionsClass'); 23 | 24 | $this->controller = $this->controllerClass->makePartial(); 25 | $this->controller->shouldReceive('getParams')->andReturn([]); 26 | $this->controller->shouldReceive('getCurrentUser')->andReturn("currentUser"); 27 | } 28 | 29 | // Authorize should assign $_authorized property and pass args to current authority 30 | public function testAuthorizeShouldAssignAuthorizedInstanceVariableAndPassArgsToCurrentAuthority() 31 | { 32 | $this->controller->shouldReceive('getCurrentAuthority->authorize')->with('foo', 'bar')->once(); 33 | 34 | $this->controller->authorize('foo', 'bar'); 35 | $this->assertTrue($this->getProperty($this->controller, '_authorized')); 36 | } 37 | 38 | // Should have a getCurrentAuthority() method which generates an authority for the current user 39 | public function testShouldHaveACurrentAuthorityMethodWhichGeneratesAnAuthorityForTheCurrentUser() 40 | { 41 | $this->assertInstanceOf("Efficiently\AuthorityController\Authority", $this->controller->getCurrentAuthority()); 42 | } 43 | 44 | // Should provide a can() and cannot() methods which go through the current authority 45 | public function testShouldProvideACanAndCannotMethodsWhichGoThroughTheCurrentAuthority() 46 | { 47 | $this->assertInstanceOf("Efficiently\AuthorityController\Authority", $this->controller->getCurrentAuthority()); 48 | $this->assertFalse($this->controller->can('foo', 'bar')); 49 | $this->assertTrue($this->controller->cannot('foo', 'bar')); 50 | } 51 | 52 | // loadAndAuthorizeResource() should setup a before filter which passes call to ControllerResource 53 | public function testLoadAndAuthorizeResourceShouldSetupABeforeFilterWhichPassesCallToControllerResource() 54 | { 55 | $controller = $this->controller; 56 | $controllerResourceClass = 'Efficiently\AuthorityController\ControllerResource'; 57 | App::offsetUnset($controllerResourceClass); 58 | App::bind($controllerResourceClass, function ($app, $parameters) use ($controllerResourceClass, $controller) { 59 | $this->assertEquals($parameters, [$controller, null, ['foo' => 'bar']]); 60 | $controllerResource = m::mock($controllerResourceClass, $parameters); 61 | $controllerResource->shouldReceive('loadAndAuthorizeResource')->once(); 62 | return $controllerResource; 63 | }); 64 | 65 | $controller->shouldReceive('beforeFilter')->with(m::type('string'), [])->once() 66 | ->andReturnUsing(function ($filterName, $options) use ($controller) { 67 | $this->assertTrue(Event::hasListeners($this->filterPrefix.$filterName)); 68 | return Event::fire($this->filterPrefix.$filterName); 69 | }); 70 | 71 | $controller->loadAndAuthorizeResource(['foo' => 'bar']); 72 | } 73 | 74 | // loadAndAuthorizeResource() should properly pass first argument as the resource name 75 | public function testloadAndAuthorizeResourceShouldProperlyPassFirstArgumentAsTheResourceName() 76 | { 77 | $controller = $this->controller; 78 | $controllerResourceClass = 'Efficiently\AuthorityController\ControllerResource'; 79 | App::offsetUnset($controllerResourceClass); 80 | App::bind($controllerResourceClass, function ($app, $parameters) use ($controllerResourceClass, $controller) { 81 | $this->assertEquals($parameters, [$controller, 'project', ['foo' => 'bar']]); 82 | $controllerResource = m::mock($controllerResourceClass, $parameters); 83 | $controllerResource->shouldReceive('loadAndAuthorizeResource')->once(); 84 | return $controllerResource; 85 | }); 86 | 87 | $controller->shouldReceive('beforeFilter')->with(m::type('string'), [])->once() 88 | ->andReturnUsing(function ($filterName, $options) use ($controller) { 89 | $this->assertTrue(Event::hasListeners($this->filterPrefix.$filterName)); 90 | return Event::fire($this->filterPrefix.$filterName); 91 | }); 92 | 93 | $controller->loadAndAuthorizeResource('project', ['foo' => 'bar']); 94 | } 95 | 96 | // loadAndAuthorizeResource() with 'prepend' should prepend the before filter 97 | public function testLoadAndAuthorizeResourceWithPrependShouldPrependTheBeforeFilter() 98 | { 99 | $this->controller->shouldReceive('prependBeforeFilter')->once(); 100 | 101 | $this->controller->loadAndAuthorizeResource(['foo' => 'bar', 'prepend' => true]); 102 | } 103 | 104 | // authorizeResource() should setup a before filter which passes call to ControllerResource 105 | public function testAuthorizeResourceShouldSetupABeforeFilterWhichPassesCallToControllerResource() 106 | { 107 | $controller = $this->controller; 108 | $controllerResourceClass = 'Efficiently\AuthorityController\ControllerResource'; 109 | App::offsetUnset($controllerResourceClass); 110 | App::bind($controllerResourceClass, function ($app, $parameters) use ($controllerResourceClass, $controller) { 111 | $this->assertEquals($parameters, [$controller, null, ['foo' => 'bar']]); 112 | $controllerResource = m::mock($controllerResourceClass, $parameters); 113 | $controllerResource->shouldReceive('authorizeResource')->once(); 114 | return $controllerResource; 115 | }); 116 | 117 | $controller->shouldReceive('beforeFilter')->with(m::type('string'), ['except' => 'show'])->once() 118 | ->andReturnUsing(function ($filterName, $options) use ($controller) { 119 | $this->assertTrue(Event::hasListeners($this->filterPrefix.$filterName)); 120 | return Event::fire($this->filterPrefix.$filterName); 121 | }); 122 | 123 | $controller->authorizeResource(['foo' => 'bar', 'except' => 'show']); 124 | } 125 | 126 | // loadResource() should setup a before filter which passes call to ControllerResource 127 | public function testLoadResourceShouldSetupABeforeFilterWhichPassesCallToControllerResource() 128 | { 129 | $controller = $this->controller; 130 | $controllerResourceClass = 'Efficiently\AuthorityController\ControllerResource'; 131 | App::offsetUnset($controllerResourceClass); 132 | App::bind($controllerResourceClass, function ($app, $parameters) use ($controllerResourceClass, $controller) { 133 | $this->assertEquals($parameters, [$controller, null, ['foo' => 'bar']]); 134 | $controllerResource = m::mock($controllerResourceClass, $parameters); 135 | $controllerResource->shouldReceive('loadResource')->once(); 136 | return $controllerResource; 137 | }); 138 | 139 | $controller->shouldReceive('beforeFilter')->with(m::type('string'), ['only' => ['show', 'index']])->once() 140 | ->andReturnUsing(function ($filterName, $options) use ($controller) { 141 | $this->assertTrue(Event::hasListeners($this->filterPrefix.$filterName)); 142 | return Event::fire($this->filterPrefix.$filterName); 143 | }); 144 | 145 | $controller->loadResource(['foo' => 'bar', 'only' => ['show', 'index']]); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /tests/AcExceptionsHandler.php: -------------------------------------------------------------------------------- 1 | 'bar', 'value']; 11 | $options = array_extract_options($args); 12 | $expect = ['foo' => 'bar']; 13 | $this->assertEquals($options, $expect); 14 | } 15 | 16 | public function testCompactProperty() 17 | { 18 | $expectName = "Best movie"; 19 | $myProduct = new AcProduct; 20 | $myProduct->setName($expectName); 21 | $compactedArray = compact_property($myProduct, 'name'); 22 | 23 | $this->assertArrayHasKey('name', $compactedArray); 24 | $this->assertEquals($expectName, $compactedArray['name']); 25 | } 26 | 27 | public function testCompactProperties() 28 | { 29 | $expectName = "Best movie"; 30 | $expectPrice = "15"; 31 | $myProduct = new AcProduct; 32 | $myProduct->setName($expectName); 33 | $myProduct->setPrice($expectPrice); 34 | $compactedArray = compact_property($myProduct, 'name', 'price'); 35 | 36 | $this->assertArrayHasKey('name', $compactedArray); 37 | $this->assertArrayHasKey('price', $compactedArray); 38 | $this->assertEquals($expectName, $compactedArray['name']); 39 | $this->assertEquals($expectPrice, $compactedArray['price']); 40 | } 41 | 42 | public function testGetClassname() 43 | { 44 | $mock = m::mock('Project'); 45 | $this->assertNotEquals('Project', get_class($mock)); 46 | $this->assertEquals('Project', get_classname($mock)); 47 | 48 | $mockNamespace = m::mock('Sub\Task'); 49 | $this->assertNotEquals('Sub\Task', get_class($mockNamespace)); 50 | $this->assertEquals('Sub\Task', get_classname($mockNamespace)); 51 | } 52 | 53 | public function testAcTrans() 54 | { 55 | $defaultErrorMessage = ac_trans("messages.unauthorized.default"); 56 | $this->assertEquals('You are not authorized to access this page.', $defaultErrorMessage); 57 | 58 | App::setLocale('fr'); 59 | $defaultErrorMessageFr = ac_trans("messages.unauthorized.default"); 60 | 61 | $this->assertNotEquals('You are not authorized to access this page.', $defaultErrorMessageFr); 62 | $this->assertEquals("Vous n'êtes pas autorisé à accéder à cette page.", $defaultErrorMessageFr); 63 | } 64 | 65 | public function testRespondTo() 66 | { 67 | $mock = m::mock('Project'); 68 | $this->assertFalse(method_exists($mock, 'toto')); 69 | $this->assertFalse(respond_to($mock, 'toto')); 70 | 71 | $mock->shouldReceive('toto'); 72 | $this->assertFalse(method_exists($mock, 'toto')); 73 | $this->assertTrue(respond_to($mock, 'toto')); 74 | } 75 | 76 | public function testGetProperty() 77 | { 78 | $category = new AcCategory; 79 | $privatePropertyName = 'privateProperty'; 80 | $this->assertNotEquals('public property', get_property($category, $privatePropertyName)); 81 | $this->assertEquals('private property', get_property($category, $privatePropertyName)); 82 | 83 | } 84 | 85 | public function testSetProperty() 86 | { 87 | $category = new AcCategory; 88 | $privatePropertyName = 'privateProperty'; 89 | set_property($category, $privatePropertyName, 'updated private property'); 90 | $this->assertNotEquals('private property', get_property($category, $privatePropertyName)); 91 | $this->assertEquals('updated private property', get_property($category, $privatePropertyName)); 92 | } 93 | 94 | public function testInvokeMethod() 95 | { 96 | $category = new AcCategory; 97 | 98 | $this->assertTrue(is_callable([$category, 'publicMethod'])); 99 | 100 | $privateMethodName = 'privateMethod'; 101 | $this->assertFalse(is_callable([$category, $privateMethodName])); 102 | $this->assertEquals('private method', invoke_method($category, $privateMethodName)); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/AcParametersTest.php: -------------------------------------------------------------------------------- 1 | app = $this->app = app(); 32 | $this->app['router'] = $this->router = $this->mockRouter(); 33 | 34 | $this->controllerName = "ProjectsController"; 35 | $this->router->resource('projects', $this->controllerName); 36 | 37 | $this->parameters = new \Efficiently\AuthorityController\Parameters; 38 | $this->app->instance('Params', $this->parameters); 39 | } 40 | 41 | public function testAddParameter() 42 | { 43 | Params::add('key', 'value'); 44 | $this->assertEquals(Params::get('key'), 'value'); 45 | } 46 | 47 | public function testAddParameterWithDotKeys() 48 | { 49 | Params::add('key.subkey', 'value'); 50 | $this->assertEquals(Params::get('key.subkey'), 'value'); 51 | } 52 | 53 | public function testOnlyParameters() 54 | { 55 | Params::add('key1', 'value1'); 56 | Params::add('key2', 'value2'); 57 | $this->assertEquals(Params::only('key1'), ['key1' => 'value1']); 58 | } 59 | 60 | public function testExceptParameters() 61 | { 62 | Params::add('key1', 'value1'); 63 | Params::add('key2', 'value2'); 64 | $this->assertEquals(Params::except('key2'), ['key1' => 'value1']); 65 | } 66 | 67 | public function testExtractResourceFromInput() 68 | { 69 | $input = ['project' => ['name' => 'foo']]; 70 | $parameters = $this->parameters; 71 | $controller = $this->mockController(); 72 | 73 | $this->call('POST', '/projects', $input);// store action 74 | 75 | $this->assertArrayHasKey('project', $this->getProperty($parameters, 'params')); 76 | $this->assertEquals($this->getProperty($parameters, 'params')['project'], $input['project']); 77 | 78 | $this->assertArrayHasKey('project', $this->getProperty($controller, 'params')); 79 | $this->assertEquals($this->getProperty($controller, 'params')['project'], $input['project']); 80 | } 81 | 82 | public function testResolveResourceFromInput() 83 | { 84 | $input = ['name' => 'foo']; 85 | $parameters = $this->parameters; 86 | $controller = $this->mockController(); 87 | 88 | $this->call('POST', '/projects', $input);// store action 89 | 90 | $this->assertArrayHasKey('project', $this->getProperty($parameters, 'params')); 91 | $this->assertEquals($this->getProperty($parameters, 'params')['project'], $input); 92 | 93 | $this->assertArrayHasKey('project', $this->getProperty($controller, 'params')); 94 | $this->assertEquals($this->getProperty($controller, 'params')['project'], $input); 95 | } 96 | 97 | public function testExtractResourceFromInputWithSingularControllerAndRoute() 98 | { 99 | $input = ['project' => ['name' => 'foo']]; 100 | $parameters = $this->parameters; 101 | 102 | $controllerName = "ProjectController"; 103 | Route::resource('project', $controllerName); 104 | $controller = $this->mockController($controllerName); 105 | 106 | $this->call('POST', '/project', $input);// store action 107 | 108 | $this->assertArrayHasKey('project', $this->getProperty($parameters, 'params')); 109 | $this->assertEquals($this->getProperty($parameters, 'params')['project'], $input['project']); 110 | 111 | $this->assertArrayHasKey('project', $this->getProperty($controller, 'params')); 112 | $this->assertEquals($this->getProperty($controller, 'params')['project'], $input['project']); 113 | } 114 | 115 | public function testResolveResourceFromInputWithSingularControllerAndRoute() 116 | { 117 | $input = ['name' => 'foo']; 118 | $parameters = $this->parameters; 119 | 120 | $controllerName = "ProjectController"; 121 | Route::resource('project', $controllerName); 122 | $controller = $this->mockController($controllerName); 123 | 124 | $this->call('POST', '/project', $input);// store action 125 | 126 | $this->assertArrayHasKey('project', $this->getProperty($parameters, 'params')); 127 | $this->assertEquals($this->getProperty($parameters, 'params')['project'], $input); 128 | 129 | $this->assertArrayHasKey('project', $this->getProperty($controller, 'params')); 130 | $this->assertEquals($this->getProperty($controller, 'params')['project'], $input); 131 | } 132 | 133 | public function testResolveActionAndControllerNamesFromRequest() 134 | { 135 | $input = ['project' => ['name' => 'foo']]; 136 | $parameters = $this->parameters; 137 | $controller = $this->mockController(); 138 | 139 | $this->call('POST', '/projects', $input);// store action 140 | 141 | $this->assertArrayHasKey('action', $this->getProperty($parameters, 'params')); 142 | $this->assertEquals($this->getProperty($parameters, 'params')['action'], 'store'); 143 | 144 | $this->assertArrayHasKey('controller', $this->getProperty($parameters, 'params')); 145 | $this->assertEquals($this->getProperty($parameters, 'params')['controller'], 'projects'); 146 | 147 | 148 | $this->assertArrayHasKey('action', $this->getProperty($controller, 'params')); 149 | $this->assertEquals($this->getProperty($controller, 'params')['action'], 'store'); 150 | 151 | $this->assertArrayHasKey('controller', $this->getProperty($controller, 'params')); 152 | $this->assertEquals($this->getProperty($controller, 'params')['controller'], 'projects'); 153 | } 154 | 155 | public function testResolveResourceIdFromRequest() 156 | { 157 | $parameters = $this->parameters; 158 | $controller = $this->mockController(); 159 | 160 | $this->call('GET', '/projects/5');// show action 161 | 162 | $this->assertArrayHasKey('id', $this->getProperty($parameters, 'params')); 163 | $this->assertEquals($this->getProperty($parameters, 'params')['id'], '5'); 164 | 165 | $this->assertArrayHasKey('id', $this->getProperty($controller, 'params')); 166 | $this->assertEquals($this->getProperty($controller, 'params')['id'], '5'); 167 | } 168 | 169 | public function testResolveResourceAndParentResourceIdsFromRequest() 170 | { 171 | $parameters = $this->parameters; 172 | 173 | $controllerName = "TasksController"; 174 | Route::resource('projects.tasks', $controllerName); 175 | $controller = $this->mockController($controllerName); 176 | 177 | $this->call('GET', '/projects/5/tasks/2');// show action of task resource 178 | 179 | $this->assertArrayHasKey('project_id', $this->getProperty($parameters, 'params')); 180 | $this->assertEquals($this->getProperty($parameters, 'params')['project_id'], '5'); 181 | 182 | $this->assertArrayHasKey('project_id', $this->getProperty($controller, 'params')); 183 | $this->assertEquals($this->getProperty($controller, 'params')['project_id'], '5'); 184 | 185 | 186 | $this->assertArrayHasKey('id', $this->getProperty($parameters, 'params')); 187 | $this->assertEquals($this->getProperty($parameters, 'params')['id'], '2'); 188 | 189 | $this->assertArrayHasKey('id', $this->getProperty($controller, 'params')); 190 | $this->assertEquals($this->getProperty($controller, 'params')['id'], '2'); 191 | } 192 | 193 | public function testResolveResourceIdFromRequestWithSingularController() 194 | { 195 | $parameters = $this->parameters; 196 | 197 | $controllerName = "ProjectController"; 198 | Route::resource('projects', $controllerName); 199 | $controller = $this->mockController($controllerName); 200 | 201 | $this->call('GET', '/projects/6');// show action 202 | 203 | $this->assertArrayHasKey('id', $this->getProperty($parameters, 'params')); 204 | $this->assertEquals($this->getProperty($parameters, 'params')['id'], '6'); 205 | 206 | $this->assertArrayHasKey('id', $this->getProperty($controller, 'params')); 207 | $this->assertEquals($this->getProperty($controller, 'params')['id'], '6'); 208 | } 209 | 210 | public function testResolveResourceIdFromRequestWithSingularRoute() 211 | { 212 | $parameters = $this->parameters; 213 | 214 | Route::resource('project', $this->controllerName); 215 | $controller = $this->mockController(); 216 | 217 | $this->call('GET', '/projects/7');// show action 218 | 219 | $this->assertArrayHasKey('id', $this->getProperty($parameters, 'params')); 220 | $this->assertEquals($this->getProperty($parameters, 'params')['id'], '7'); 221 | 222 | $this->assertArrayHasKey('id', $this->getProperty($controller, 'params')); 223 | $this->assertEquals($this->getProperty($controller, 'params')['id'], '7'); 224 | } 225 | 226 | public function testResolveResourceIdFromRequestWithSingularControllerAndRoute() 227 | { 228 | $parameters = $this->parameters; 229 | 230 | $controllerName = "ProjectController"; 231 | Route::resource('project', $controllerName); 232 | $controller = $this->mockController($controllerName); 233 | 234 | $this->call('GET', '/project/8');// show action 235 | 236 | $this->assertArrayHasKey('id', $this->getProperty($parameters, 'params')); 237 | $this->assertEquals($this->getProperty($parameters, 'params')['id'], '8'); 238 | 239 | $this->assertArrayHasKey('id', $this->getProperty($controller, 'params')); 240 | $this->assertEquals($this->getProperty($controller, 'params')['id'], '8'); 241 | } 242 | 243 | public function testResolveResourceAndParentResourceIdsFromRequestWithSingularControllerAndRoute() 244 | { 245 | $parameters = $this->parameters; 246 | 247 | $controllerName = "TaskController"; 248 | Route::resource('project.task', $controllerName); 249 | $controller = $this->mockController($controllerName); 250 | 251 | $this->call('GET', '/project/5/task/2');// show action of task resource 252 | 253 | $this->assertArrayHasKey('project_id', $this->getProperty($parameters, 'params')); 254 | $this->assertEquals($this->getProperty($parameters, 'params')['project_id'], '5'); 255 | 256 | $this->assertArrayHasKey('project_id', $this->getProperty($controller, 'params')); 257 | $this->assertEquals($this->getProperty($controller, 'params')['project_id'], '5'); 258 | 259 | 260 | $this->assertArrayHasKey('id', $this->getProperty($parameters, 'params')); 261 | $this->assertEquals($this->getProperty($parameters, 'params')['id'], '2'); 262 | 263 | $this->assertArrayHasKey('id', $this->getProperty($controller, 'params')); 264 | $this->assertEquals($this->getProperty($controller, 'params')['id'], '2'); 265 | } 266 | 267 | public function testExtractResourceIdFromRequestWithImplicitAction() 268 | { 269 | $parameters = $this->parameters; 270 | 271 | $controllerName = "UsersController"; 272 | Route::get('users/{id}', ['as'=>'users.show', 'uses' =>'UsersController@show']); // show action 273 | $controller = $this->mockController($controllerName); 274 | 275 | $this->call('GET', '/users/6');// show action 276 | 277 | $this->assertArrayHasKey('id', $this->getProperty($parameters, 'params')); 278 | $this->assertEquals($this->getProperty($parameters, 'params')['id'], '6'); 279 | 280 | $this->assertArrayHasKey('id', $this->getProperty($controller, 'params')); 281 | $this->assertEquals($this->getProperty($controller, 'params')['id'], '6'); 282 | } 283 | 284 | public function testExtractResourceIdFromRequestWithEditAction() 285 | { 286 | $parameters = $this->parameters; 287 | 288 | $controllerName = "UsersController"; 289 | Route::get('users/{id}/edit', ['as'=>'users.edit', 'uses' =>'UsersController@edit']); // edit action 290 | $controller = $this->mockController($controllerName); 291 | 292 | $this->call('GET', '/users/6/edit');// edit action 293 | 294 | $this->assertArrayHasKey('id', $this->getProperty($parameters, 'params')); 295 | $this->assertEquals($this->getProperty($parameters, 'params')['id'], '6'); 296 | 297 | $this->assertArrayHasKey('id', $this->getProperty($controller, 'params')); 298 | $this->assertEquals($this->getProperty($controller, 'params')['id'], '6'); 299 | } 300 | 301 | public function testExtractResourceAndParentResourceIdsFromRequest() 302 | { 303 | $parameters = $this->parameters; 304 | 305 | Route::get('users/{id}', ['as'=>'users.show', 'uses' =>'UsersController@show']); // user show action 306 | 307 | $controllerName = "PostsController"; 308 | Route::get('users/{user_id}/posts/{id}', ['as'=>'users.posts.show', 'uses' =>'PostsController@show']); // post show action 309 | $controller = $this->mockController($controllerName); 310 | 311 | $this->call('GET', '/users/7/posts/3');// show action of post resource 312 | 313 | $this->assertArrayHasKey('user_id', $this->getProperty($parameters, 'params')); 314 | $this->assertEquals($this->getProperty($parameters, 'params')['user_id'], '7'); 315 | 316 | $this->assertArrayHasKey('user_id', $this->getProperty($controller, 'params')); 317 | $this->assertEquals($this->getProperty($controller, 'params')['user_id'], '7'); 318 | 319 | 320 | $this->assertArrayHasKey('id', $this->getProperty($parameters, 'params')); 321 | $this->assertEquals($this->getProperty($parameters, 'params')['id'], '3'); 322 | 323 | $this->assertArrayHasKey('id', $this->getProperty($controller, 'params')); 324 | $this->assertEquals($this->getProperty($controller, 'params')['id'], '3'); 325 | } 326 | 327 | public function testExtractResourceAndNestedResourceIdsFromRequest() 328 | { 329 | $parameters = $this->parameters; 330 | 331 | Route::get('users/{id}', ['as'=>'users.show', 'uses' =>'UsersController@show']); // user show action 332 | 333 | Route::get('users/{user_id}/posts/{id}', ['as'=>'users.posts.show', 'uses' =>'PostsController@show']); // post show action 334 | 335 | $controllerName = "CommentsController"; 336 | // comment show action 337 | Route::get('users/{user_id}/posts/{post_id}/comments/{id}', ['as'=>'users.posts.comments.show', 'uses' =>'CommentsController@show']); 338 | $controller = $this->mockController($controllerName); 339 | 340 | $this->call('GET', '/users/7/posts/3/comments/8');// show action of comment resource 341 | 342 | $this->assertArrayHasKey('user_id', $this->getProperty($parameters, 'params')); 343 | $this->assertEquals($this->getProperty($parameters, 'params')['user_id'], '7'); 344 | 345 | $this->assertArrayHasKey('user_id', $this->getProperty($controller, 'params')); 346 | $this->assertEquals($this->getProperty($controller, 'params')['user_id'], '7'); 347 | 348 | $this->assertArrayHasKey('post_id', $this->getProperty($parameters, 'params')); 349 | $this->assertEquals($this->getProperty($parameters, 'params')['post_id'], '3'); 350 | 351 | $this->assertArrayHasKey('post_id', $this->getProperty($controller, 'params')); 352 | $this->assertEquals($this->getProperty($controller, 'params')['post_id'], '3'); 353 | 354 | $this->assertArrayHasKey('id', $this->getProperty($parameters, 'params')); 355 | $this->assertEquals($this->getProperty($parameters, 'params')['id'], '8'); 356 | 357 | $this->assertArrayHasKey('id', $this->getProperty($controller, 'params')); 358 | $this->assertEquals($this->getProperty($controller, 'params')['id'], '8'); 359 | } 360 | 361 | 362 | protected function mockController($controllerName = null) 363 | { 364 | $controllerName = $controllerName ?: $this->controllerName; 365 | 366 | $this->router = $this->app['router']; 367 | 368 | $events = $this->getProperty($this->router, 'events'); 369 | $this->setProperty($this->router, 'events', $events); 370 | 371 | $this->app['router'] = $this->router; 372 | 373 | $this->mock($controllerName); 374 | $controllerInstance = $this->app->make($controllerName); 375 | $controllerInstance->shouldReceive('paramsBeforeFilter')->with(m::type('string'))->once(); 376 | $controllerInstance->shouldReceive('callAction')->with(m::type('string'), m::type('array'))->andReturnUsing(function ($method, $parameters) use ($controllerInstance) { 377 | $this->app->make('Params')->fillController($controllerInstance); 378 | 379 | $filterName = "router.filter: controller.parameters.".get_classname($controllerInstance); 380 | $this->assertTrue(Event::hasListeners($filterName)); 381 | Event::fire($filterName); 382 | 383 | return new \Symfony\Component\HttpFoundation\Response; 384 | }); 385 | 386 | $this->mock('\Efficiently\AuthorityController\ControllerResource'); 387 | $this->controllerResource = $this->app->make('\Efficiently\AuthorityController\ControllerResource'); 388 | $this->controllerResource->shouldReceive('getNameByController')->with('ProjectsController')->andReturn('project'); 389 | 390 | return $controllerInstance; 391 | } 392 | 393 | protected function mockRouter($app = null) 394 | { 395 | $app = $app ?: $this->app; 396 | $routerFacade = new \Illuminate\Support\Facades\Route; 397 | $this->invokeMethod($routerFacade, 'createFreshMockInstance', ['router']); 398 | $router = $routerFacade::getFacadeRoot()->makePartial(); 399 | 400 | $router->shouldReceive('substituteBindings')->with(m::type('\Illuminate\Routing\Route'))->andReturnUsing(function ($route) use ($router) { 401 | foreach ($route->parameters() as $key => $value) { 402 | if (isset($router->binders[$key])) { 403 | $route->setParameter($key, $router->performBinding($key, $value, $route)); 404 | } 405 | } 406 | $routePartial = m::mock($route); 407 | $routePartial->shouldReceive('signatureParameters')->andReturn([]); 408 | $router->substituteImplicitBindings($routePartial); 409 | 410 | return $routePartial; 411 | }); 412 | 413 | $this->setProperty($router, 'events', $app['events']); 414 | $this->setProperty($router, 'routes', new \Illuminate\Routing\RouteCollection); 415 | $this->setProperty($router, 'container', $app); 416 | 417 | return $router; 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /tests/AcProjectsControllerTest.php: -------------------------------------------------------------------------------- 1 | controllerName = "AcProjectsController"; 18 | $this->modelName = "AcProject"; 19 | $this->modelAttributes = ['id' => 5, 'name' => 'Test AuthorityController package', 'priority' => 1]; 20 | $this->resourceName = "ac_projects"; // str_plural(snake_case($this->modelName)); 21 | 22 | Route::resource($this->resourceName, $this->controllerName); 23 | 24 | $this->userAttributes = ['id' => 1, 'username' => 'tortue', 'firstname' => 'Tortue', 25 | 'lastname' => 'Torche', 'email' => 'tortue.torche@spam.me', 'password' => Hash::make('tortuetorche'), 26 | 'displayname' => 'Tortue Torche']; 27 | 28 | $this->user = $this->getUserWithRole('admin'); 29 | $this->authority = $this->getAuthority($this->user); 30 | 31 | $this->authority->allow('manage', $this->modelName); 32 | } 33 | 34 | public function testIndexActionAllows() 35 | { 36 | $actionName = 'index'; 37 | 38 | $model = $this->buildModel(); 39 | 40 | $this->assertCan($actionName, $this->modelName); 41 | 42 | $this->action('GET', $this->controllerName."@".$actionName); 43 | $this->assertViewHas('acProjects', $model->get()); 44 | } 45 | 46 | /** 47 | * @expectedException Efficiently\AuthorityController\Exceptions\AccessDenied 48 | */ 49 | public function testIndexActionDenies() 50 | { 51 | $actionName = 'index'; 52 | 53 | $this->authority->deny($actionName, $this->modelName); 54 | 55 | $model = $this->buildModel(); 56 | 57 | $this->assertCannot($actionName, $this->modelName); 58 | 59 | $this->action('GET', $this->controllerName."@".$actionName); 60 | } 61 | 62 | public function testCreateActionAllows() 63 | { 64 | $actionName = 'create'; 65 | 66 | $modelAttributes = array_except($this->modelAttributes, 'id'); 67 | $model = $this->buildModel($modelAttributes); 68 | 69 | $this->assertCan($actionName, $this->modelName); 70 | 71 | $this->action('GET', $this->controllerName."@".$actionName); 72 | $this->assertResponseOk(); 73 | } 74 | 75 | /** 76 | * @expectedException Efficiently\AuthorityController\Exceptions\AccessDenied 77 | */ 78 | public function testCreateActionDenies() 79 | { 80 | $actionName = 'create'; 81 | 82 | $this->authority->deny($actionName, $this->modelName); 83 | 84 | $modelAttributes = array_except($this->modelAttributes, 'id'); 85 | $model = $this->buildModel($modelAttributes); 86 | 87 | $this->assertCannot($actionName, $this->modelName); 88 | 89 | $this->action('GET', $this->controllerName."@".$actionName); 90 | } 91 | 92 | public function testStoreActionAllows() 93 | { 94 | $actionName = 'store'; 95 | 96 | $modelAttributes = array_except($this->modelAttributes, 'id'); 97 | $model = $this->buildModel($modelAttributes); 98 | 99 | $this->assertCan($actionName, $this->modelName); 100 | 101 | $this->action('POST', $this->controllerName."@".$actionName, [], $modelAttributes); 102 | $this->assertRedirectedToRoute($this->resourceName.'.index'); 103 | } 104 | 105 | /** 106 | * @expectedException Efficiently\AuthorityController\Exceptions\AccessDenied 107 | */ 108 | public function testStoreActionDenies() 109 | { 110 | $actionName = 'store'; 111 | 112 | $this->authority->deny($actionName, $this->modelName); 113 | 114 | $modelAttributes = array_except($this->modelAttributes, 'id'); 115 | $model = $this->buildModel($modelAttributes); 116 | 117 | $this->assertCannot($actionName, $this->modelName); 118 | 119 | $this->action('POST', $this->controllerName."@".$actionName, [], $modelAttributes); 120 | } 121 | 122 | public function testShowActionAllows() 123 | { 124 | $actionName = 'show'; 125 | 126 | $model = $this->buildModel(); 127 | 128 | $this->assertCan($actionName, $this->modelName); 129 | 130 | $response = $this->action('GET', $this->controllerName."@".$actionName, [$model->id]); 131 | $this->assertViewHas('acProject'); 132 | $view = $response->original; 133 | $this->assertEquals($view->acProject->id, 5); 134 | } 135 | 136 | /** 137 | * @expectedException Efficiently\AuthorityController\Exceptions\AccessDenied 138 | */ 139 | public function testShowActionDenies() 140 | { 141 | $actionName = 'show'; 142 | 143 | $this->authority->deny($actionName, $this->modelName); 144 | 145 | $model = $this->buildModel(); 146 | 147 | $this->assertCannot($actionName, $this->modelName); 148 | 149 | $this->action('GET', $this->controllerName."@".$actionName, [$model->id]); 150 | } 151 | 152 | public function testEditActionAllows() 153 | { 154 | $actionName = 'edit'; 155 | 156 | $model = $this->buildModel(); 157 | 158 | $this->assertCan($actionName, $this->modelName); 159 | 160 | $response = $this->action('GET', $this->controllerName."@".$actionName, [$model->id]); 161 | $this->assertViewHas('acProject'); 162 | $view = $response->original; 163 | $this->assertEquals($view->acProject->id, 5); 164 | } 165 | 166 | /** 167 | * @expectedException Efficiently\AuthorityController\Exceptions\AccessDenied 168 | */ 169 | public function testEditActionDenies() 170 | { 171 | $actionName = 'edit'; 172 | 173 | $this->authority->deny($actionName, $this->modelName); 174 | 175 | $model = $this->buildModel(); 176 | 177 | $this->assertCannot($actionName, $this->modelName); 178 | 179 | $this->action('GET', $this->controllerName."@".$actionName, [$model->id]); 180 | } 181 | 182 | public function testUpdateActionAllows() 183 | { 184 | $actionName = 'update'; 185 | 186 | $modelAttributes = $this->modelAttributes; 187 | $model = $this->buildModel($modelAttributes); 188 | 189 | $this->assertCan($actionName, $this->modelName); 190 | 191 | $this->action('PATCH', $this->controllerName."@".$actionName, [$model->id], $modelAttributes); 192 | $this->assertRedirectedToRoute($this->resourceName.'.show', 5); 193 | } 194 | 195 | /** 196 | * @expectedException Efficiently\AuthorityController\Exceptions\AccessDenied 197 | */ 198 | public function testUpdateActionDenies() 199 | { 200 | $actionName = 'update'; 201 | 202 | $this->authority->deny($actionName, $this->modelName); 203 | 204 | $modelAttributes = $this->modelAttributes; 205 | $model = $this->buildModel($modelAttributes); 206 | 207 | $this->assertCannot($actionName, $this->modelName); 208 | 209 | $this->action('PATCH', $this->controllerName."@".$actionName, [$model->id], $modelAttributes); 210 | } 211 | 212 | public function testDestroyActionAllows() 213 | { 214 | $actionName = 'destroy'; 215 | 216 | $model = $this->buildModel(); 217 | 218 | $this->assertCan($actionName, $this->modelName); 219 | 220 | $this->action('DELETE', $this->controllerName."@".$actionName, [$model->id]); 221 | $this->assertRedirectedToRoute($this->resourceName.'.index'); 222 | } 223 | 224 | /** 225 | * @expectedException Efficiently\AuthorityController\Exceptions\AccessDenied 226 | */ 227 | public function testDestroyActionDenies() 228 | { 229 | $actionName = 'destroy'; 230 | 231 | $this->authority->deny($actionName, $this->modelName); 232 | 233 | $model = $this->buildModel(); 234 | 235 | $this->assertCannot($actionName, $this->modelName); 236 | 237 | $this->action('DELETE', $this->controllerName."@".$actionName, [$model->id]); 238 | } 239 | 240 | protected function buildModel($modelAttributes = null) 241 | { 242 | $modelAttributes = $modelAttributes ?: $this->modelAttributes; 243 | $mock = $this->mock($this->modelName); 244 | $model = $this->fillMock($mock, $modelAttributes); 245 | 246 | $mock->shouldReceive('where->firstOrFail')->/*once()->*/andReturn($model); 247 | $mock->shouldReceive('save')->/*once()->*/andReturn(true); 248 | $models = collect(); 249 | $models->push($model); 250 | $mock->shouldReceive('get')->andReturn($models); 251 | $mock->shouldReceive('all')->andReturn($models->all()); 252 | 253 | return $model; 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /tests/AcTasksControllerTest.php: -------------------------------------------------------------------------------- 1 | parentControllerName = "AcProjectsController"; 23 | $this->parentModelName = "AcProject"; 24 | $this->parentModelAttributes = ['id' => 3, 'name' => 'Test AuthorityController package', 'priority' => 1]; 25 | $this->parentResourceName = "ac_projects"; 26 | 27 | $this->controllerName = "AcTasksController"; 28 | $this->modelName = "AcTask"; 29 | $this->modelAttributes = ['id' => 5, 'name' => 'Write more tests!', 'ac_project_id' => 3]; 30 | $this->resourceName = "ac_projects.ac_tasks"; 31 | 32 | Route::resource($this->parentResourceName, $this->parentControllerName); 33 | Route::resource($this->resourceName, $this->controllerName); 34 | 35 | $this->userAttributes = ['id' => 1, 'username' => 'tortue', 'firstname' => 'Tortue', 36 | 'lastname' => 'Torche', 'email' => 'tortue.torche@spam.me', 'password' => Hash::make('tortuetorche'), 37 | 'displayname' => 'Tortue Torche']; 38 | 39 | $this->user = $this->getUserWithRole('admin'); 40 | $this->authority = $this->getAuthority($this->user); 41 | 42 | $this->authority->allow('manage', $this->parentModelName); 43 | $this->authority->allow('manage', $this->modelName); 44 | 45 | } 46 | 47 | public function testIndexActionAllows() 48 | { 49 | $actionName = 'index'; 50 | 51 | $parentModel = $this->buildParentModel(); 52 | 53 | $model = $this->buildModel(); 54 | 55 | $this->assertCan($actionName, $this->modelName); 56 | $this->action('GET', $this->controllerName."@".$actionName, [$parentModel->id]); 57 | 58 | $this->assertViewHas('acTasks', $model->get()); 59 | } 60 | 61 | /** 62 | * @expectedException Efficiently\AuthorityController\Exceptions\AccessDenied 63 | */ 64 | public function testIndexActionDenies() 65 | { 66 | $actionName = 'index'; 67 | 68 | $this->authority->deny($actionName, $this->modelName); 69 | 70 | $parentModel = $this->buildParentModel(); 71 | 72 | $model = $this->buildModel(); 73 | 74 | $this->assertCannot($actionName, $this->modelName); 75 | $this->action('GET', $this->controllerName."@".$actionName, [$parentModel->id]); 76 | } 77 | 78 | public function testCreateActionAllows() 79 | { 80 | $actionName = 'create'; 81 | 82 | $parentModel = $this->buildParentModel(); 83 | 84 | $modelAttributes = array_except($this->modelAttributes, 'id'); 85 | $model = $this->buildModel($modelAttributes); 86 | 87 | $this->assertCan($actionName, $this->modelName); 88 | 89 | $this->action('GET', $this->controllerName."@".$actionName, [$parentModel->id]); 90 | 91 | $this->assertResponseOk(); 92 | } 93 | 94 | /** 95 | * @expectedException Efficiently\AuthorityController\Exceptions\AccessDenied 96 | */ 97 | public function testCreateActionDenies() 98 | { 99 | $actionName = 'create'; 100 | 101 | $this->authority->deny($actionName, $this->modelName); 102 | 103 | $parentModel = $this->buildParentModel(); 104 | 105 | $modelAttributes = array_except($this->modelAttributes, 'id'); 106 | $model = $this->buildModel($modelAttributes); 107 | 108 | $this->assertCannot($actionName, $this->modelName); 109 | 110 | $this->action('GET', $this->controllerName."@".$actionName, [$parentModel->id]); 111 | } 112 | 113 | public function testStoreActionAllows() 114 | { 115 | $actionName = 'store'; 116 | 117 | $parentModel = $this->buildParentModel(); 118 | 119 | $modelAttributes = array_except($this->modelAttributes, 'id'); 120 | $model = $this->buildModel($modelAttributes); 121 | 122 | $this->assertCan($actionName, $this->modelName); 123 | 124 | $this->action('POST', $this->controllerName."@".$actionName, [$parentModel->id], $modelAttributes); 125 | $this->assertRedirectedToRoute($this->resourceName.'.index', $parentModel->id); 126 | } 127 | 128 | /** 129 | * @expectedException Efficiently\AuthorityController\Exceptions\AccessDenied 130 | */ 131 | public function testStoreActionDenies() 132 | { 133 | $actionName = 'store'; 134 | 135 | $this->authority->deny($actionName, $this->modelName); 136 | 137 | $parentModel = $this->buildParentModel(); 138 | 139 | $modelAttributes = array_except($this->modelAttributes, 'id'); 140 | $model = $this->buildModel($modelAttributes); 141 | 142 | $this->assertCannot($actionName, $this->modelName); 143 | 144 | $this->action('POST', $this->controllerName."@".$actionName, [$parentModel->id], $modelAttributes); 145 | } 146 | 147 | public function testShowActionAllows() 148 | { 149 | $actionName = 'show'; 150 | 151 | $parentModel = $this->buildParentModel(); 152 | 153 | $model = $this->buildModel(); 154 | 155 | $this->assertCan($actionName, $this->modelName); 156 | 157 | $response = $this->action('GET', $this->controllerName."@".$actionName, [$parentModel->id, $model->id]); 158 | $this->assertViewHas('acTask'); 159 | $view = $response->original; 160 | $this->assertEquals($view->acTask->ac_project_id, 3); 161 | $this->assertEquals($view->acTask->id, 5); 162 | } 163 | 164 | /** 165 | * @expectedException Efficiently\AuthorityController\Exceptions\AccessDenied 166 | */ 167 | public function testShowActionDenies() 168 | { 169 | $actionName = 'show'; 170 | 171 | $this->authority->deny($actionName, $this->modelName); 172 | 173 | $parentModel = $this->buildParentModel(); 174 | 175 | $model = $this->buildModel(); 176 | 177 | $this->assertCannot($actionName, $this->modelName); 178 | 179 | $this->action('GET', $this->controllerName."@".$actionName, [$parentModel->id, $model->id]); 180 | } 181 | 182 | public function testEditActionAllows() 183 | { 184 | $actionName = 'edit'; 185 | 186 | $parentModel = $this->buildParentModel(); 187 | 188 | $model = $this->buildModel(); 189 | 190 | $this->assertCan($actionName, $this->modelName); 191 | 192 | $response = $this->action('GET', $this->controllerName."@".$actionName, [$parentModel->id, $model->id]); 193 | $this->assertViewHas('acTask'); 194 | $view = $response->original; 195 | $this->assertEquals($view->acTask->ac_project_id, 3); 196 | $this->assertEquals($view->acTask->id, 5); 197 | } 198 | 199 | /** 200 | * @expectedException Efficiently\AuthorityController\Exceptions\AccessDenied 201 | */ 202 | public function testEditActionDenies() 203 | { 204 | $actionName = 'edit'; 205 | 206 | $this->authority->deny($actionName, $this->modelName); 207 | 208 | $parentModel = $this->buildParentModel(); 209 | 210 | $model = $this->buildModel(); 211 | 212 | $this->assertCannot($actionName, $this->modelName); 213 | 214 | $this->action('GET', $this->controllerName."@".$actionName, [$parentModel->id, $model->id]); 215 | } 216 | 217 | public function testUpdateActionAllows() 218 | { 219 | $actionName = 'update'; 220 | 221 | $parentModel = $this->buildParentModel(); 222 | 223 | $modelAttributes = $this->modelAttributes; 224 | $model = $this->buildModel($modelAttributes); 225 | 226 | $this->assertCan($actionName, $this->modelName); 227 | 228 | $this->action('PATCH', $this->controllerName."@".$actionName, [$parentModel->id, $model->id], $modelAttributes); 229 | $this->assertRedirectedToRoute($this->resourceName.'.show', [3, 5]); 230 | } 231 | 232 | /** 233 | * @expectedException Efficiently\AuthorityController\Exceptions\AccessDenied 234 | */ 235 | public function testUpdateActionDenies() 236 | { 237 | $actionName = 'update'; 238 | 239 | $this->authority->deny($actionName, $this->modelName); 240 | 241 | $parentModel = $this->buildParentModel(); 242 | 243 | $modelAttributes = $this->modelAttributes; 244 | $model = $this->buildModel($modelAttributes); 245 | 246 | $this->assertCannot($actionName, $this->modelName); 247 | 248 | $this->action('PATCH', $this->controllerName."@".$actionName, [$parentModel->id, $model->id], $modelAttributes); 249 | } 250 | 251 | public function testDestroyActionAllows() 252 | { 253 | $actionName = 'destroy'; 254 | 255 | $parentModel = $this->buildParentModel(); 256 | 257 | $model = $this->buildModel(); 258 | 259 | $this->assertCan($actionName, $this->modelName); 260 | 261 | $this->action('DELETE', $this->controllerName."@".$actionName, [$parentModel->id, $model->id]); 262 | $this->assertRedirectedToRoute($this->resourceName.'.index', $parentModel->id); 263 | } 264 | 265 | /** 266 | * @expectedException Efficiently\AuthorityController\Exceptions\AccessDenied 267 | */ 268 | public function testDestroyActionDenies() 269 | { 270 | $actionName = 'destroy'; 271 | 272 | $this->authority->deny($actionName, $this->modelName); 273 | 274 | $parentModel = $this->buildParentModel(); 275 | 276 | $model = $this->buildModel(); 277 | 278 | $this->assertCannot($actionName, $this->modelName); 279 | 280 | $this->action('DELETE', $this->controllerName."@".$actionName, [$parentModel->id, $model->id]); 281 | } 282 | 283 | protected function buildParentModel($parentModelAttributes = null) 284 | { 285 | $parentModelAttributes = $parentModelAttributes ?: $this->parentModelAttributes; 286 | $parentMock = $this->mock($this->parentModelName); 287 | $parentModel = $this->fillMock($parentMock, $parentModelAttributes); 288 | 289 | $parentMock->shouldReceive('where->firstOrFail')->once()->andReturn($parentModel); 290 | $associationName = str_plural(camel_case($this->modelName)); 291 | $parentMock->shouldReceive($associationName.'->getModel')->/*once()->*/andReturn(app($this->modelName)); 292 | 293 | return $parentModel; 294 | } 295 | 296 | protected function buildModel($modelAttributes = null) 297 | { 298 | $modelAttributes = $modelAttributes ?: $this->modelAttributes; 299 | $mock = $this->mock($this->modelName); 300 | $model = $this->fillMock($mock, $modelAttributes); 301 | 302 | $mock->shouldReceive('where->firstOrFail')->/*once()->*/andReturn($model); 303 | $mock->shouldReceive('save')->/*once()->*/andReturn(true); 304 | $models = collect(); 305 | $models->push($model); 306 | $mock->shouldReceive('get')->andReturn($models); 307 | $mock->shouldReceive('all')->andReturn($models->all()); 308 | 309 | return $model; 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /tests/AcTestCase.php: -------------------------------------------------------------------------------- 1 | fillMock($mock, $attributes); 22 | } 23 | 24 | return $mock; 25 | }); 26 | 27 | return $mock; 28 | } 29 | 30 | protected function fillMock($mock, $attributes = []) 31 | { 32 | $instance = $mock->makePartial(); 33 | foreach ($attributes as $key => $value) { 34 | $instance->$key = $value; 35 | } 36 | 37 | return $instance; 38 | } 39 | 40 | protected function getPackageProviders($app) 41 | { 42 | return [ 43 | Collective\Html\HtmlServiceProvider::class, 44 | Efficiently\AuthorityController\AuthorityControllerServiceProvider::class, 45 | ]; 46 | } 47 | 48 | protected function getPackageAliases($app) 49 | { 50 | return [ 51 | 'Input' => Illuminate\Support\Facades\Input::class, 52 | 'Form' => Collective\Html\FormFacade::class, 53 | 'HTML' => Collective\Html\HtmlFacade::class, 54 | 'Authority' => Efficiently\AuthorityController\Facades\Authority::class, 55 | 'Params' => Efficiently\AuthorityController\Facades\Params::class, 56 | ]; 57 | } 58 | 59 | /** 60 | * Resolve application HTTP exception handler. 61 | * 62 | * @param \Illuminate\Foundation\Application $app 63 | * 64 | * @return void 65 | */ 66 | protected function resolveApplicationExceptionHandler($app) 67 | { 68 | $app->singleton('Illuminate\Contracts\Debug\ExceptionHandler', 'AcExceptionsHandler'); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/fixtures/AcProduct.php: -------------------------------------------------------------------------------- 1 | name = $value; 12 | } 13 | 14 | public function getName() 15 | { 16 | return $this->name; 17 | } 18 | 19 | public function isNamed() 20 | { 21 | return !! $this->name; 22 | } 23 | 24 | public function setPrice($value) 25 | { 26 | $this->price = $value; 27 | } 28 | 29 | public function getPrice() 30 | { 31 | return $this->price; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/fixtures/controllers/AcBaseController.php: -------------------------------------------------------------------------------- 1 | layout)) { 20 | $this->layout = view($this->layout); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/fixtures/controllers/AcCategoriesController.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/efficiently/authority-controller/ea9d1fc8817222202f9b0657ea294cb59eb9018d/tests/fixtures/controllers/AcCategoriesController.php -------------------------------------------------------------------------------- /tests/fixtures/controllers/AcProjectsController.php: -------------------------------------------------------------------------------- 1 | acProjectModel = $acProjectModel ?: new AcProject; 30 | $this->loadAndAuthorizeResource(); 31 | } 32 | 33 | /** 34 | * Display a listing of the resource. 35 | * 36 | * @return Response 37 | */ 38 | public function index() 39 | { 40 | // $this->acProjects = $this->acProjectModel->get(); 41 | 42 | return view('ac_projects.index', compact_property($this, 'acProjects')); 43 | } 44 | 45 | /** 46 | * Show the form for creating a new resource. 47 | * 48 | * @return Response 49 | */ 50 | public function create() 51 | { 52 | return view('ac_projects.create'); 53 | } 54 | 55 | /** 56 | * Store a newly created resource in storage. 57 | * 58 | * @return Response 59 | */ 60 | public function store() 61 | { 62 | // $this->acProject = app('AcProject'); 63 | 64 | $this->acProject->fill(request()->except('_method', '_token')); 65 | try { 66 | $this->acProject->save(); 67 | return redirect()->route('ac_projects.index'); 68 | } catch (Exception $e) { 69 | return redirect()->route('ac_projects.create') 70 | ->withErrors($this->acProject->errors()) 71 | ->with('message', 'There were validation errors.'); 72 | } 73 | } 74 | 75 | /** 76 | * Display the specified resource. 77 | * 78 | * @param int $id 79 | * @return Response 80 | */ 81 | public function show($id) 82 | { 83 | // $this->acProject = $this->acProjectModel->findOrFail($id); 84 | 85 | return view('ac_projects.show', compact_property($this, 'acProject')); 86 | } 87 | 88 | /** 89 | * Show the form for editing the specified resource. 90 | * 91 | * @param int $id 92 | * @return Response 93 | */ 94 | public function edit($id) 95 | { 96 | // $this->acProject = $this->acProjectModel->find($id); 97 | 98 | if (is_null($this->acProject)) { 99 | return redirect()->route('ac_projects.index'); 100 | } 101 | 102 | return view('ac_projects.edit', compact_property($this, 'acProject')); 103 | } 104 | 105 | /** 106 | * Update the specified resource in storage. 107 | * 108 | * @param int $id 109 | * @return Response 110 | */ 111 | public function update($id) 112 | { 113 | // $this->acProject = $this->acProjectModel->find($id); 114 | 115 | $this->acProject->fill(request()->except('_method', '_token')); 116 | try { 117 | $this->acProject->save(); 118 | return redirect()->route('ac_projects.show', $id); 119 | } catch (Exception $e) { 120 | return redirect()->route('ac_projects.edit', $id) 121 | ->withErrors($this->acProject->errors()) 122 | ->with('message', 'There were validation errors.'); 123 | } 124 | } 125 | 126 | /** 127 | * Remove the specified resource from storage. 128 | * 129 | * @param int $id 130 | * @return Response 131 | */ 132 | public function destroy($id) 133 | { 134 | // $this->acProjectModel->find($id)->delete(); 135 | 136 | $this->acProject->delete(); 137 | 138 | return redirect()->route('ac_projects.index'); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /tests/fixtures/controllers/AcTasksController.php: -------------------------------------------------------------------------------- 1 | acProjectModel = $acProjectModel; 44 | $this->acTaskModel = $acTaskModel; 45 | $this->loadAndAuthorizeResource('ac_project'); 46 | $this->loadAndAuthorizeResource('ac_task', ['through' => 'ac_project']); 47 | } 48 | 49 | /** 50 | * Display a listing of the resource. 51 | * 52 | * @param int $acProjectId 53 | * @return Response 54 | */ 55 | public function index($acProjectId) 56 | { 57 | // $this->acTasks = $this->acTaskModel->get(); 58 | 59 | return view('ac_tasks.index', compact_property($this, 'acProject', 'acTasks')); 60 | } 61 | 62 | /** 63 | * Show the form for creating a new resource. 64 | * 65 | * @param int $acProjectId 66 | * @return Response 67 | */ 68 | public function create($acProjectId) 69 | { 70 | return view('ac_tasks.create', compact_property($this, 'acProject')); 71 | } 72 | 73 | /** 74 | * Store a newly created resource in storage. 75 | * 76 | * @param int $acProjectId 77 | * @return Response 78 | */ 79 | public function store($acProjectId) 80 | { 81 | // $this->acTask = app('AcTask'); 82 | 83 | $this->acTask->fill(request()->except('_method', '_token')); 84 | $this->acTask->ac_project_id = $acProjectId; 85 | try { 86 | $this->acTask->save(); 87 | return redirect()->route('ac_projects.ac_tasks.index', $acProjectId); 88 | } catch (Exception $e) { 89 | return redirect()->route('ac_projects.ac_tasks.create', $acProjectId) 90 | ->withErrors($this->acTask->errors()) 91 | ->with('message', 'There were validation errors.'); 92 | } 93 | } 94 | 95 | /** 96 | * Display the specified resource. 97 | * 98 | * @param int $acProjectId 99 | * @param int $id 100 | * @return Response 101 | */ 102 | public function show($acProjectId, $id) 103 | { 104 | // $this->acTask = $this->acTaskModel->findOrFail($id); 105 | 106 | return view('ac_tasks.show', compact_property($this, 'acTask')); 107 | } 108 | 109 | /** 110 | * Show the form for editing the specified resource. 111 | * 112 | * @param int $acProjectId 113 | * @param int $id 114 | * @return Response 115 | */ 116 | public function edit($acProjectId, $id) 117 | { 118 | // $this->acTask = $this->acTaskModel->find($id); 119 | 120 | if (is_null($this->acTask)) { 121 | return redirect()->route('ac_projects.ac_tasks.index', $acProjectId); 122 | } 123 | 124 | return view('ac_tasks.edit', compact_property($this, 'acTask')); 125 | } 126 | 127 | /** 128 | * Update the specified resource in storage. 129 | * 130 | * @param int $acProjectId 131 | * @param int $id 132 | * @return Response 133 | */ 134 | public function update($acProjectId, $id) 135 | { 136 | // $this->acTask = $this->acTaskModel->find($id); 137 | 138 | $this->acTask->fill(request()->except('_method', '_token')); 139 | try { 140 | $this->acTask->save(); 141 | return redirect()->route('ac_projects.ac_tasks.show', [$acProjectId, $id]); 142 | } catch (Exception $e) { 143 | return redirect()->route('ac_projects.ac_tasks.edit', $id) 144 | ->withErrors($this->acTask->errors()) 145 | ->with('message', 'There were validation errors.'); 146 | } 147 | } 148 | 149 | /** 150 | * Remove the specified resource from storage. 151 | * 152 | * @param int $acProjectId 153 | * @param int $id 154 | * @return Response 155 | */ 156 | public function destroy($acProjectId, $id) 157 | { 158 | // $this->acTaskModel->find($id)->delete(); 159 | 160 | $this->acTask->delete(); 161 | 162 | return redirect()->route('ac_projects.ac_tasks.index', $acProjectId); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /tests/fixtures/models/AcCategory.php: -------------------------------------------------------------------------------- 1 | 'required', 9 | 'priority' => 'required',//|unique:projects', 10 | ]; 11 | 12 | protected $fillable = ['name', 'priority']; 13 | 14 | public function acUser() 15 | { 16 | return $this->belongsTo(AcUser::class); 17 | } 18 | 19 | public function acTasks() 20 | { 21 | return $this->hasMany(AcTask::class); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/fixtures/models/AcRole.php: -------------------------------------------------------------------------------- 1 | belongsTo(AcProject::class); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/fixtures/models/AcUser.php: -------------------------------------------------------------------------------- 1 | 'required|between:3,16|unique:users', 22 | 'email' => 'required|email|unique:users', 23 | 'password' => 'required|min:4|confirmed', 24 | 'password_confirmation' => 'required|min:4', 25 | ]; 26 | 27 | /** 28 | * The attributes excluded from the model's JSON form. 29 | * 30 | * @var array 31 | */ 32 | protected $hidden = ['password']; 33 | 34 | protected $fillable = ['username', 'firstname', 'lastname', 'email', 'displayname']; 35 | 36 | public function roles() 37 | { 38 | return $this->belongsToMany(AcRole::class)->withTimestamps(); 39 | } 40 | 41 | public function permissions() 42 | { 43 | return $this->hasMany(AcPermission::class); 44 | } 45 | 46 | public function hasRole($key) 47 | { 48 | foreach ($this->roles as $role) { 49 | if ($role->name === $key) { 50 | return true; 51 | } 52 | } 53 | return false; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/fixtures/views/ac_projects/create.blade.php: -------------------------------------------------------------------------------- 1 |

Create Project

2 | 3 | {!! Form::open(['route' => 'ac_projects.store']) !!} 4 | {!! Form::text('name') !!} 5 | 6 | {!! Form::text('priority') !!} 7 | 8 | {!! Form::submit('Submit') !!} 9 | {!! Form::close() !!} 10 | -------------------------------------------------------------------------------- /tests/fixtures/views/ac_projects/edit.blade.php: -------------------------------------------------------------------------------- 1 |

Edit Project

2 | 3 | {!! Form::model($acProject, ['route' => ['ac_projects.update', $acProject->id], 'method' => 'PUT']) !!} 4 | 5 | {!! Form::text('name') !!} 6 | 7 | {!! Form::text('priority') !!} 8 | 9 | {!! Form::submit('Update') !!} 10 | {!! link_to_route('ac_projects.show', 'Cancel', $acProject->id) !!} 11 | {!! Form::close() !!} 12 | -------------------------------------------------------------------------------- /tests/fixtures/views/ac_projects/index.blade.php: -------------------------------------------------------------------------------- 1 |

All Projects

2 | 3 |

{!! link_to_route('ac_projects.create', 'Add new project') !!}

4 | 5 | @if ($acProjects->count()) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | @foreach ($acProjects as $acProject) 16 | 17 | 18 | 19 | 20 | 25 | 26 | @endforeach 27 | 28 |
NamePriotity
{{ $acProject->name }}{{ $acProject->priority }}{!! link_to_route('ac_projects.edit', 'Edit', $acProject->id) !!} 21 | {!! Form::open(array('method' => 'DELETE', 'route' => ['ac_projects.destroy', $acProject->id])) !!} 22 | {!! Form::submit('Delete') !!} 23 | {!! Form::close() !!} 24 |
29 | @else 30 | There are no projects 31 | @endif 32 | -------------------------------------------------------------------------------- /tests/fixtures/views/ac_projects/show.blade.php: -------------------------------------------------------------------------------- 1 |

Show Project

2 | 3 |

{!! link_to_route('ac_projects.index', 'Return to all demandes') !!}

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 23 | 24 | 25 |
NamePriority
{{ $acProject->name }}{{ $acProject->priority }}{!! link_to_route('ac_projects.edit', 'Edit', $acProject->id) !!} 19 | {!! Form::open(array('method' => 'DELETE', 'route' => ['ac_projects.destroy', $acProject->id])) !!} 20 | {!! Form::submit('Delete') !!} 21 | {!! Form::close() !!} 22 |
26 | -------------------------------------------------------------------------------- /tests/fixtures/views/ac_tasks/create.blade.php: -------------------------------------------------------------------------------- 1 |

Create Task

2 | 3 | {!! Form::open( ['route' => ['ac_projects.ac_tasks.store', $acProject->id]] ) !!} 4 | {!! Form::text('name') !!} 5 | 6 | {!! Form::submit('Submit') !!} 7 | {!! Form::close() !!} 8 | -------------------------------------------------------------------------------- /tests/fixtures/views/ac_tasks/edit.blade.php: -------------------------------------------------------------------------------- 1 |

Edit Task

2 | {!! Form::model($acTask, ['route' => ['ac_projects.ac_tasks.update', $acTask->ac_project_id, $acTask->id], 'method' => 'PUT']) !!} 3 | {!! Form::text('name') !!} 4 | 5 | {!! Form::submit('Update') !!} 6 | {!! link_to_route('ac_projects.ac_tasks.show', 'Cancel', [$acTask->ac_project_id, $acTask->id]) !!} 7 | {!! Form::close() !!} 8 | -------------------------------------------------------------------------------- /tests/fixtures/views/ac_tasks/index.blade.php: -------------------------------------------------------------------------------- 1 |

All Tasks

2 | 3 |

{!! link_to_route('ac_projects.ac_tasks.create', 'Add new task', $acProject->id) !!}

4 | 5 | @if ($acTasks->count()) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | @foreach ($acTasks as $acTask) 15 | 16 | 17 | 18 | 23 | 24 | @endforeach 25 | 26 |
Name
{{ $acTask->name }}{!! link_to_route('ac_projects.ac_tasks.edit', 'Edit', [$acTask->ac_project_id, $acTask->id]) !!} 19 | {!! Form::open(['method' => 'DELETE', 'route' => ['ac_projects.ac_tasks.destroy', $acTask->ac_project_id, $acTask->id]]) !!} 20 | {!! Form::submit('Delete') !!} 21 | {!! Form::close() !!} 22 |
27 | @else 28 | There are no tasks 29 | @endif 30 | -------------------------------------------------------------------------------- /tests/fixtures/views/ac_tasks/show.blade.php: -------------------------------------------------------------------------------- 1 |

Show Task

2 | 3 |

{!! link_to_route('ac_projects.ac_tasks.index', 'Return to all issues', $acTask->ac_project_id) !!}

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 22 | 23 |
Name
{{ $acTask->name }}{!! link_to_route('ac_projects.ac_tasks.edit', 'Edit', [$acTask->ac_project_id, $acTask->id]) !!} 17 | {!! Form::open(['method' => 'DELETE', 'route' => ['ac_projects.ac_tasks.destroy', $acTask->ac_project_id, $acTask->id]]) !!} 18 | {!! Form::submit('Delete') !!} 19 | {!! Form::close() !!} 20 |
24 | -------------------------------------------------------------------------------- /tests/helpers/AuthorityControllerHelpers.php: -------------------------------------------------------------------------------- 1 | makePartial(); 19 | $userAttributes = $this->userAttributes; 20 | $userAttributes['roles'] = $roles; 21 | foreach ($userAttributes as $key => $value) { 22 | $user->$key = $value; 23 | } 24 | 25 | $user->shouldReceive('save')->andReturn(true); 26 | $this->be($user); // Equivalent to Auth::login($user); 27 | return $user; 28 | } 29 | 30 | /** 31 | * return an Authentificate User with role(s) 32 | * @param string|array $rolesNames Role(s) to add to the user 33 | * @return UserInterface 34 | */ 35 | protected function getUserWithRole($rolesNames = null) 36 | { 37 | $rolesNames = (array) $rolesNames; 38 | $roles = new \Illuminate\Database\Eloquent\Collection(); 39 | foreach ($rolesNames as $index => $roleName) { 40 | $role = m::mock('Eloquent', 'AcRole')->makePartial(); 41 | $roleAttributes = ['id' => $index+1, 'name' => $roleName]; 42 | foreach ($roleAttributes as $key => $value) { 43 | $role->$key = $value; 44 | } 45 | 46 | $roles->add($role); 47 | } 48 | 49 | return $this->getUser($roles); 50 | } 51 | 52 | protected function getAuthority($user) 53 | { 54 | $authority = new Efficiently\AuthorityController\Authority($user); 55 | $fn = config('authority-controller.initialize', null); 56 | 57 | if ($fn) { 58 | $fn($authority); 59 | } 60 | App::instance('authority', $authority); 61 | 62 | return $authority; 63 | } 64 | 65 | public function assertCan($action, $resource, $resourceValue = null) 66 | { 67 | $this->assertTrue($this->authority->can($action, $resource, $resourceValue)); 68 | } 69 | 70 | public function assertCannot($action, $resource, $resourceValue = null) 71 | { 72 | $this->assertTrue($this->authority->cannot($action, $resource, $resourceValue)); 73 | } 74 | 75 | public function setProperty($object, $propertyName, $value) 76 | { 77 | set_property($object, $propertyName, $value); 78 | } 79 | 80 | public function getProperty($object, $propertyName) 81 | { 82 | return get_property($object, $propertyName); 83 | } 84 | 85 | public function invokeMethod($object, $methodName, $values = []) 86 | { 87 | return invoke_method($object, $methodName, $values); 88 | } 89 | } 90 | --------------------------------------------------------------------------------