├── LICENSE.txt ├── composer.json ├── middleware └── ScopeBouncer.php ├── migrations ├── create_bouncer_tables.php └── sql │ └── MySQL.sql └── src ├── BaseClipboard.php ├── Bouncer.php ├── BouncerFacade.php ├── BouncerServiceProvider.php ├── CachedClipboard.php ├── Clipboard.php ├── Conductors ├── AssignsRoles.php ├── ChecksRoles.php ├── Concerns │ ├── AssociatesAbilities.php │ ├── ConductsAbilities.php │ ├── DisassociatesAbilities.php │ └── FindsAndCreatesAbilities.php ├── ForbidsAbilities.php ├── GivesAbilities.php ├── Lazy │ ├── ConductsAbilities.php │ └── HandlesOwnership.php ├── RemovesAbilities.php ├── RemovesRoles.php ├── SyncsRolesAndAbilities.php └── UnforbidsAbilities.php ├── Console └── CleanCommand.php ├── Constraints ├── Builder.php ├── ColumnConstraint.php ├── Constrainer.php ├── Constraint.php ├── Group.php └── ValueConstraint.php ├── Contracts ├── CachedClipboard.php ├── Clipboard.php └── Scope.php ├── Database ├── Ability.php ├── Concerns │ ├── Authorizable.php │ ├── HasAbilities.php │ ├── HasRoles.php │ ├── IsAbility.php │ └── IsRole.php ├── HasRolesAndAbilities.php ├── Models.php ├── Queries │ ├── Abilities.php │ ├── AbilitiesForModel.php │ └── Roles.php ├── Role.php ├── Scope │ ├── Scope.php │ └── TenantScope.php └── Titles │ ├── AbilityTitle.php │ ├── RoleTitle.php │ └── Title.php ├── Factory.php ├── Guard.php └── Helpers.php /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "silber/bouncer", 3 | "description": "Eloquent roles and abilities.", 4 | "keywords": [ 5 | "abilities", 6 | "acl", 7 | "capabilities", 8 | "eloquent", 9 | "laravel", 10 | "permissions", 11 | "roles" 12 | ], 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Joseph Silber", 17 | "email": "contact@josephsilber.com" 18 | } 19 | ], 20 | "autoload": { 21 | "psr-4": { 22 | "Silber\\Bouncer\\": "src/" 23 | } 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "Silber\\Bouncer\\Tests\\": "tests/", 28 | "Workbench\\App\\": "workbench/app/", 29 | "Workbench\\Database\\Factories\\": "workbench/database/factories/", 30 | "Workbench\\Database\\Seeders\\": "workbench/database/seeders/" 31 | } 32 | }, 33 | "require": { 34 | "php": "^8.2", 35 | "illuminate/auth": "^11.0|^12.0", 36 | "illuminate/cache": "^11.0|^12.0", 37 | "illuminate/container": "^11.0|^12.0", 38 | "illuminate/contracts": "^11.0|^12.0", 39 | "illuminate/database": "^11.0|^12.0" 40 | }, 41 | "require-dev": { 42 | "illuminate/console": "^11.0|^12.0", 43 | "illuminate/events": "^11.0|^12.0", 44 | "phpunit/phpunit": "^11.0", 45 | "orchestra/testbench": "^9.0|^10.0", 46 | "pestphp/pest": "^3.7", 47 | "laravel/pint": "^1.14" 48 | }, 49 | "suggest": { 50 | "illuminate/console": "Allows running the bouncer:clean artisan command", 51 | "illuminate/events": "Required for multi-tenancy support" 52 | }, 53 | "scripts": { 54 | "test": "phpunit", 55 | "post-autoload-dump": [ 56 | "@clear", 57 | "@prepare" 58 | ], 59 | "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", 60 | "prepare": "@php vendor/bin/testbench package:discover --ansi", 61 | "build": "@php vendor/bin/testbench workbench:build --ansi", 62 | "serve": [ 63 | "Composer\\Config::disableProcessTimeout", 64 | "@build", 65 | "@php vendor/bin/testbench serve" 66 | ], 67 | "lint": [ 68 | "@php vendor/bin/phpstan analyse" 69 | ] 70 | }, 71 | "minimum-stability": "dev", 72 | "prefer-stable": true, 73 | "extra": { 74 | "laravel": { 75 | "providers": [ 76 | "Silber\\Bouncer\\BouncerServiceProvider" 77 | ], 78 | "aliases": { 79 | "Bouncer": "Silber\\Bouncer\\BouncerFacade" 80 | } 81 | } 82 | }, 83 | "config": { 84 | "allow-plugins": { 85 | "pestphp/pest-plugin": true 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /middleware/ScopeBouncer.php: -------------------------------------------------------------------------------- 1 | bouncer = $bouncer; 23 | } 24 | 25 | /** 26 | * Set the proper Bouncer scope for the incoming request. 27 | * 28 | * @param \Illuminate\Http\Request $request 29 | * @return mixed 30 | */ 31 | public function handle($request, Closure $next) 32 | { 33 | // Here you may use whatever mechanism you use in your app 34 | // to determine the current tenant. To demonstrate, the 35 | // $tenantId is set here from the user's account_id. 36 | $tenantId = $request->user()->account_id; 37 | 38 | $this->bouncer->scope()->to($tenantId); 39 | 40 | return $next($request); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /migrations/create_bouncer_tables.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 19 | $table->string('name'); 20 | $table->string('title')->nullable(); 21 | $table->bigInteger('entity_id')->unsigned()->nullable(); 22 | $table->string('entity_type')->nullable(); 23 | $table->boolean('only_owned')->default(false); 24 | $table->json('options')->nullable(); 25 | $table->integer('scope')->nullable()->index(); 26 | $table->timestamps(); 27 | }); 28 | 29 | Schema::create(Models::table('roles'), function (Blueprint $table) { 30 | $table->bigIncrements('id'); 31 | $table->string('name'); 32 | $table->string('title')->nullable(); 33 | $table->integer('scope')->nullable()->index(); 34 | $table->timestamps(); 35 | 36 | $table->unique( 37 | ['name', 'scope'], 38 | 'roles_name_unique' 39 | ); 40 | }); 41 | 42 | Schema::create(Models::table('assigned_roles'), function (Blueprint $table) { 43 | $table->bigIncrements('id'); 44 | $table->bigInteger('role_id')->unsigned()->index(); 45 | $table->bigInteger('entity_id')->unsigned(); 46 | $table->string('entity_type'); 47 | $table->bigInteger('restricted_to_id')->unsigned()->nullable(); 48 | $table->string('restricted_to_type')->nullable(); 49 | $table->integer('scope')->nullable()->index(); 50 | 51 | $table->index( 52 | ['entity_id', 'entity_type', 'scope'], 53 | 'assigned_roles_entity_index' 54 | ); 55 | 56 | $table->foreign('role_id') 57 | ->references('id')->on(Models::table('roles')) 58 | ->onUpdate('cascade')->onDelete('cascade'); 59 | }); 60 | 61 | Schema::create(Models::table('permissions'), function (Blueprint $table) { 62 | $table->bigIncrements('id'); 63 | $table->bigInteger('ability_id')->unsigned()->index(); 64 | $table->bigInteger('entity_id')->unsigned()->nullable(); 65 | $table->string('entity_type')->nullable(); 66 | $table->boolean('forbidden')->default(false); 67 | $table->integer('scope')->nullable()->index(); 68 | 69 | $table->index( 70 | ['entity_id', 'entity_type', 'scope'], 71 | 'permissions_entity_index' 72 | ); 73 | 74 | $table->foreign('ability_id') 75 | ->references('id')->on(Models::table('abilities')) 76 | ->onUpdate('cascade')->onDelete('cascade'); 77 | }); 78 | } 79 | 80 | /** 81 | * Reverse the migrations. 82 | * 83 | * @return void 84 | */ 85 | public function down() 86 | { 87 | Schema::drop(Models::table('permissions')); 88 | Schema::drop(Models::table('assigned_roles')); 89 | Schema::drop(Models::table('roles')); 90 | Schema::drop(Models::table('abilities')); 91 | } 92 | }; 93 | -------------------------------------------------------------------------------- /migrations/sql/MySQL.sql: -------------------------------------------------------------------------------- 1 | create table `abilities` ( 2 | `id` int unsigned not null auto_increment primary key, 3 | `name` varchar(255) not null, 4 | `title` varchar(255) null, 5 | `entity_id` int unsigned null, 6 | `entity_type` varchar(255) null, 7 | `only_owned` tinyint(1) not null default '0', 8 | `options` json null, 9 | `scope` int null, 10 | `created_at` timestamp null, 11 | `updated_at` timestamp null 12 | ) default character set utf8mb4 collate utf8mb4_unicode_ci; 13 | 14 | alter table `abilities` 15 | add index `abilities_scope_index`(`scope`); 16 | 17 | create table `roles` ( 18 | `id` int unsigned not null auto_increment primary key, 19 | `name` varchar(255) not null, 20 | `title` varchar(255) null, 21 | `level` int unsigned null, 22 | `scope` int null, 23 | `created_at` timestamp null, 24 | `updated_at` timestamp null 25 | ) default character set utf8mb4 collate utf8mb4_unicode_ci; 26 | 27 | alter table `roles` 28 | add unique `roles_name_unique`(`name`, `scope`); 29 | 30 | alter table `roles` 31 | add index `roles_scope_index`(`scope`); 32 | 33 | create table `assigned_roles` ( 34 | `id` int unsigned not null auto_increment primary key, 35 | `role_id` int unsigned not null, 36 | `entity_id` int unsigned not null, 37 | `entity_type` varchar(255) not null, 38 | `restricted_to_id` int unsigned null, 39 | `restricted_to_type` varchar(255) null, 40 | `scope` int null 41 | ) default character set utf8mb4 collate utf8mb4_unicode_ci; 42 | 43 | alter table `assigned_roles` 44 | add index `assigned_roles_entity_index`(`entity_id`, `entity_type`, `scope`); 45 | 46 | alter table `assigned_roles` 47 | add constraint `assigned_roles_role_id_foreign` 48 | foreign key (`role_id`) 49 | references `roles` (`id`) 50 | on delete cascade 51 | on update cascade; 52 | 53 | alter table `assigned_roles` 54 | add index `assigned_roles_role_id_index`(`role_id`); 55 | 56 | alter table `assigned_roles` 57 | add index `assigned_roles_scope_index`(`scope`); 58 | 59 | create table `permissions` ( 60 | `id` int unsigned not null auto_increment primary key, 61 | `ability_id` int unsigned not null, 62 | `entity_id` int unsigned null, 63 | `entity_type` varchar(255) null, 64 | `forbidden` tinyint(1) not null default '0', 65 | `scope` int null 66 | ) default character set utf8mb4 collate utf8mb4_unicode_ci; 67 | 68 | alter table `permissions` 69 | add index `permissions_entity_index`(`entity_id`, `entity_type`, `scope`); 70 | 71 | alter table `permissions` 72 | add constraint `permissions_ability_id_foreign` 73 | foreign key (`ability_id`) 74 | references `abilities` (`id`) 75 | on delete cascade 76 | on update cascade; 77 | 78 | alter table `permissions` 79 | add index `permissions_ability_id_index`(`ability_id`); 80 | 81 | alter table `permissions` 82 | add index `permissions_scope_index`(`scope`); 83 | -------------------------------------------------------------------------------- /src/BaseClipboard.php: -------------------------------------------------------------------------------- 1 | checkGetId($authority, $ability, $model); 22 | } 23 | 24 | /** 25 | * Check if an authority has the given roles. 26 | * 27 | * @param array|string $roles 28 | * @param string $boolean 29 | * @return bool 30 | */ 31 | public function checkRole(Model $authority, $roles, $boolean = 'or') 32 | { 33 | $count = $this->countMatchingRoles($authority, $roles); 34 | 35 | if ($boolean == 'or') { 36 | return $count > 0; 37 | } elseif ($boolean === 'not') { 38 | return $count === 0; 39 | } 40 | 41 | return $count == count((array) $roles); 42 | } 43 | 44 | /** 45 | * Count the authority's roles matching the given roles. 46 | * 47 | * @param \Illuminate\Database\Eloquent\Model $authority 48 | * @param array|string $roles 49 | * @return int 50 | */ 51 | protected function countMatchingRoles($authority, $roles) 52 | { 53 | $lookups = $this->getRolesLookup($authority); 54 | 55 | return count(array_filter($roles, function ($role) use ($lookups) { 56 | switch (true) { 57 | case is_string($role): 58 | return $lookups['names']->has($role); 59 | case is_numeric($role): 60 | return $lookups['ids']->has($role); 61 | case $role instanceof Model: 62 | return $lookups['ids']->has($role->getKey()); 63 | } 64 | 65 | throw new InvalidArgumentException('Invalid model identifier'); 66 | })); 67 | } 68 | 69 | /** 70 | * Get the given authority's roles' IDs and names. 71 | * 72 | * @return array 73 | */ 74 | public function getRolesLookup(Model $authority) 75 | { 76 | $roles = $authority->roles()->get([ 77 | 'name', Models::role()->getQualifiedKeyName(), 78 | ])->pluck('name', Models::role()->getKeyName()); 79 | 80 | return ['ids' => $roles, 'names' => $roles->flip()]; 81 | } 82 | 83 | /** 84 | * Get the given authority's roles' names. 85 | * 86 | * @return \Illuminate\Support\Collection 87 | */ 88 | public function getRoles(Model $authority) 89 | { 90 | return $this->getRolesLookup($authority)['names']->keys(); 91 | } 92 | 93 | /** 94 | * Get a list of the authority's abilities. 95 | * 96 | * @param bool $allowed 97 | * @return \Illuminate\Database\Eloquent\Collection 98 | */ 99 | public function getAbilities(Model $authority, $allowed = true) 100 | { 101 | return Abilities::forAuthority($authority, $allowed)->get(); 102 | } 103 | 104 | /** 105 | * Get a list of the authority's forbidden abilities. 106 | * 107 | * @return \Illuminate\Database\Eloquent\Collection 108 | */ 109 | public function getForbiddenAbilities(Model $authority) 110 | { 111 | return $this->getAbilities($authority, false); 112 | } 113 | 114 | /** 115 | * Determine whether the authority owns the given model. 116 | * 117 | * @return bool 118 | */ 119 | public function isOwnedBy($authority, $model) 120 | { 121 | return $model instanceof Model && Models::isOwnedBy($authority, $model); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Bouncer.php: -------------------------------------------------------------------------------- 1 | guard = $guard; 36 | } 37 | 38 | /** 39 | * Create a new Bouncer instance. 40 | * 41 | * @param mixed $user 42 | * @return static 43 | */ 44 | public static function create($user = null) 45 | { 46 | return static::make($user)->create(); 47 | } 48 | 49 | /** 50 | * Create a bouncer factory instance. 51 | * 52 | * @param mixed $user 53 | * @return \Silber\Bouncer\Factory 54 | */ 55 | public static function make($user = null) 56 | { 57 | return new Factory($user); 58 | } 59 | 60 | /** 61 | * Start a chain, to allow the given authority an ability. 62 | * 63 | * @param \Illuminate\Database\Eloquent\Model|string $authority 64 | * @return \Silber\Bouncer\Conductors\GivesAbilities 65 | */ 66 | public function allow($authority) 67 | { 68 | return new Conductors\GivesAbilities($authority); 69 | } 70 | 71 | /** 72 | * Start a chain, to allow everyone an ability. 73 | * 74 | * @return \Silber\Bouncer\Conductors\GivesAbilities 75 | */ 76 | public function allowEveryone() 77 | { 78 | return new Conductors\GivesAbilities(); 79 | } 80 | 81 | /** 82 | * Start a chain, to disallow the given authority an ability. 83 | * 84 | * @param \Illuminate\Database\Eloquent\Model|string $authority 85 | * @return \Silber\Bouncer\Conductors\RemovesAbilities 86 | */ 87 | public function disallow($authority) 88 | { 89 | return new Conductors\RemovesAbilities($authority); 90 | } 91 | 92 | /** 93 | * Start a chain, to disallow everyone the an ability. 94 | * 95 | * @return \Silber\Bouncer\Conductors\RemovesAbilities 96 | */ 97 | public function disallowEveryone() 98 | { 99 | return new Conductors\RemovesAbilities(); 100 | } 101 | 102 | /** 103 | * Start a chain, to forbid the given authority an ability. 104 | * 105 | * @param \Illuminate\Database\Eloquent\Model|string $authority 106 | * @return \Silber\Bouncer\Conductors\GivesAbilities 107 | */ 108 | public function forbid($authority) 109 | { 110 | return new Conductors\ForbidsAbilities($authority); 111 | } 112 | 113 | /** 114 | * Start a chain, to forbid everyone an ability. 115 | * 116 | * @return \Silber\Bouncer\Conductors\GivesAbilities 117 | */ 118 | public function forbidEveryone() 119 | { 120 | return new Conductors\ForbidsAbilities(); 121 | } 122 | 123 | /** 124 | * Start a chain, to unforbid the given authority an ability. 125 | * 126 | * @param \Illuminate\Database\Eloquent\Model|string $authority 127 | * @return \Silber\Bouncer\Conductors\RemovesAbilities 128 | */ 129 | public function unforbid($authority) 130 | { 131 | return new Conductors\UnforbidsAbilities($authority); 132 | } 133 | 134 | /** 135 | * Start a chain, to unforbid an ability from everyone. 136 | * 137 | * @return \Silber\Bouncer\Conductors\RemovesAbilities 138 | */ 139 | public function unforbidEveryone() 140 | { 141 | return new Conductors\UnforbidsAbilities(); 142 | } 143 | 144 | /** 145 | * Start a chain, to assign the given role to a model. 146 | * 147 | * @param \Silber\Bouncer\Database\Role|\Illuminate\Support\Collection|string $roles 148 | * @return \Silber\Bouncer\Conductors\AssignsRoles 149 | */ 150 | public function assign($roles) 151 | { 152 | return new Conductors\AssignsRoles($roles); 153 | } 154 | 155 | /** 156 | * Start a chain, to retract the given role from a model. 157 | * 158 | * @param \Illuminate\Support\Collection|\Silber\Bouncer\Database\Role|string $roles 159 | * @return \Silber\Bouncer\Conductors\RemovesRoles 160 | */ 161 | public function retract($roles) 162 | { 163 | return new Conductors\RemovesRoles($roles); 164 | } 165 | 166 | /** 167 | * Start a chain, to sync roles/abilities for the given authority. 168 | * 169 | * @param \Illuminate\Database\Eloquent\Model|string $authority 170 | * @return \Silber\Bouncer\Conductors\SyncsRolesAndAbilities 171 | */ 172 | public function sync($authority) 173 | { 174 | return new Conductors\SyncsRolesAndAbilities($authority); 175 | } 176 | 177 | /** 178 | * Start a chain, to check if the given authority has a certain role. 179 | * 180 | * @return \Silber\Bouncer\Conductors\ChecksRoles 181 | */ 182 | public function is(Model $authority) 183 | { 184 | return new Conductors\ChecksRoles($authority, $this->getClipboard()); 185 | } 186 | 187 | /** 188 | * Get the clipboard instance. 189 | * 190 | * @return \Silber\Bouncer\Contracts\Clipboard 191 | */ 192 | public function getClipboard() 193 | { 194 | return $this->guard->getClipboard(); 195 | } 196 | 197 | /** 198 | * Set the clipboard instance used by bouncer. 199 | * 200 | * Will also register the given clipboard with the container. 201 | * 202 | * @param \Silber\Bouncer\Contracts\Clipboard 203 | * @return $this 204 | */ 205 | public function setClipboard(Contracts\Clipboard $clipboard) 206 | { 207 | $this->guard->setClipboard($clipboard); 208 | 209 | return $this->registerClipboardAtContainer(); 210 | } 211 | 212 | /** 213 | * Register the guard's clipboard at the container. 214 | * 215 | * @return $this 216 | */ 217 | public function registerClipboardAtContainer() 218 | { 219 | $clipboard = $this->guard->getClipboard(); 220 | 221 | Container::getInstance()->instance(Contracts\Clipboard::class, $clipboard); 222 | 223 | return $this; 224 | } 225 | 226 | /** 227 | * Use a cached clipboard with the given cache instance. 228 | * 229 | * @return $this 230 | */ 231 | public function cache(?Store $cache = null) 232 | { 233 | $cache = $cache ?: $this->resolve(CacheRepository::class)->getStore(); 234 | 235 | if ($this->usesCachedClipboard()) { 236 | $this->guard->getClipboard()->setCache($cache); 237 | 238 | return $this; 239 | } 240 | 241 | return $this->setClipboard(new CachedClipboard($cache)); 242 | } 243 | 244 | /** 245 | * Fully disable all query caching. 246 | * 247 | * @return $this 248 | */ 249 | public function dontCache() 250 | { 251 | return $this->setClipboard(new Clipboard); 252 | } 253 | 254 | /** 255 | * Clear the cache. 256 | * 257 | * @return $this 258 | */ 259 | public function refresh(?Model $authority = null) 260 | { 261 | if ($this->usesCachedClipboard()) { 262 | $this->getClipboard()->refresh($authority); 263 | } 264 | 265 | return $this; 266 | } 267 | 268 | /** 269 | * Clear the cache for the given authority. 270 | * 271 | * @return $this 272 | */ 273 | public function refreshFor(Model $authority) 274 | { 275 | if ($this->usesCachedClipboard()) { 276 | $this->getClipboard()->refreshFor($authority); 277 | } 278 | 279 | return $this; 280 | } 281 | 282 | /** 283 | * Set the access gate instance. 284 | * 285 | * @return $this 286 | */ 287 | public function setGate(Gate $gate) 288 | { 289 | $this->gate = $gate; 290 | 291 | return $this; 292 | } 293 | 294 | /** 295 | * Get the gate instance. 296 | * 297 | * @return \Illuminate\Contracts\Auth\Access\Gate|null 298 | */ 299 | public function getGate() 300 | { 301 | return $this->gate; 302 | } 303 | 304 | /** 305 | * Get the gate instance. Throw if not set. 306 | * 307 | * @return \Illuminate\Contracts\Auth\Access\Gate 308 | * 309 | * @throws \RuntimeException 310 | */ 311 | public function gate() 312 | { 313 | if (is_null($this->gate)) { 314 | throw new RuntimeException('The gate instance has not been set.'); 315 | } 316 | 317 | return $this->gate; 318 | } 319 | 320 | /** 321 | * Determine whether the clipboard used is a cached clipboard. 322 | * 323 | * @return bool 324 | */ 325 | public function usesCachedClipboard() 326 | { 327 | return $this->guard->usesCachedClipboard(); 328 | } 329 | 330 | /** 331 | * Define a new ability using a callback. 332 | * 333 | * @param string $ability 334 | * @param callable|string $callback 335 | * @return $this 336 | * 337 | * @throws \InvalidArgumentException 338 | */ 339 | public function define($ability, $callback) 340 | { 341 | return $this->gate()->define($ability, $callback); 342 | } 343 | 344 | /** 345 | * Determine if the given ability should be granted for the current user. 346 | * 347 | * @param string $ability 348 | * @param array|mixed $arguments 349 | * @return \Illuminate\Auth\Access\Response 350 | * 351 | * @throws \Illuminate\Auth\Access\AuthorizationException 352 | */ 353 | public function authorize($ability, $arguments = []) 354 | { 355 | return $this->gate()->authorize($ability, $arguments); 356 | } 357 | 358 | /** 359 | * Determine if the given ability is allowed. 360 | * 361 | * @param string $ability 362 | * @param array|mixed $arguments 363 | * @return bool 364 | */ 365 | public function can($ability, $arguments = []) 366 | { 367 | return $this->gate()->allows($ability, $arguments); 368 | } 369 | 370 | /** 371 | * Determine if any of the given abilities are allowed. 372 | * 373 | * @param array $abilities 374 | * @param array|mixed $arguments 375 | * @return bool 376 | */ 377 | public function canAny($abilities, $arguments = []) 378 | { 379 | return $this->gate()->any($abilities, $arguments); 380 | } 381 | 382 | /** 383 | * Determine if the given ability is denied. 384 | * 385 | * @param string $ability 386 | * @param array|mixed $arguments 387 | * @return bool 388 | */ 389 | public function cannot($ability, $arguments = []) 390 | { 391 | return $this->gate()->denies($ability, $arguments); 392 | } 393 | 394 | /** 395 | * Determine if the given ability is allowed. 396 | * 397 | * Alias for the "can" method. 398 | * 399 | * @deprecated 400 | * 401 | * @param string $ability 402 | * @param array|mixed $arguments 403 | * @return bool 404 | */ 405 | public function allows($ability, $arguments = []) 406 | { 407 | return $this->can($ability, $arguments); 408 | } 409 | 410 | /** 411 | * Determine if the given ability is denied. 412 | * 413 | * Alias for the "cannot" method. 414 | * 415 | * @deprecated 416 | * 417 | * @param string $ability 418 | * @param array|mixed $arguments 419 | * @return bool 420 | */ 421 | public function denies($ability, $arguments = []) 422 | { 423 | return $this->cannot($ability, $arguments); 424 | } 425 | 426 | /** 427 | * Get an instance of the role model. 428 | * 429 | * @return \Silber\Bouncer\Database\Role 430 | */ 431 | public function role(array $attributes = []) 432 | { 433 | return Models::role($attributes); 434 | } 435 | 436 | /** 437 | * Get an instance of the ability model. 438 | * 439 | * @return \Silber\Bouncer\Database\Ability 440 | */ 441 | public function ability(array $attributes = []) 442 | { 443 | return Models::ability($attributes); 444 | } 445 | 446 | /** 447 | * Set Bouncer to run its checks before the policies. 448 | * 449 | * @param bool $boolean 450 | * @return $this 451 | */ 452 | public function runBeforePolicies($boolean = true) 453 | { 454 | $this->guard->slot($boolean ? 'before' : 'after'); 455 | 456 | return $this; 457 | } 458 | 459 | /** 460 | * Register an attribute/callback to determine if a model is owned by a given authority. 461 | * 462 | * @param string|\Closure $model 463 | * @param string|\Closure|null $attribute 464 | * @return $this 465 | */ 466 | public function ownedVia($model, $attribute = null) 467 | { 468 | Models::ownedVia($model, $attribute); 469 | 470 | return $this; 471 | } 472 | 473 | /** 474 | * Set the model to be used for abilities. 475 | * 476 | * @param string $model 477 | * @return $this 478 | */ 479 | public function useAbilityModel($model) 480 | { 481 | Models::setAbilitiesModel($model); 482 | 483 | return $this; 484 | } 485 | 486 | /** 487 | * Set the model to be used for roles. 488 | * 489 | * @param string $model 490 | * @return $this 491 | */ 492 | public function useRoleModel($model) 493 | { 494 | Models::setRolesModel($model); 495 | 496 | return $this; 497 | } 498 | 499 | /** 500 | * Set the model to be used for users. 501 | * 502 | * @param string $model 503 | * @return $this 504 | */ 505 | public function useUserModel($model) 506 | { 507 | Models::setUsersModel($model); 508 | 509 | return $this; 510 | } 511 | 512 | /** 513 | * Set custom table names. 514 | * 515 | * @return $this 516 | */ 517 | public function tables(array $map) 518 | { 519 | Models::setTables($map); 520 | 521 | return $this; 522 | } 523 | 524 | /** 525 | * Get the model scoping instance. 526 | * 527 | * @return mixed 528 | */ 529 | public function scope(?Scope $scope = null) 530 | { 531 | return Models::scope($scope); 532 | } 533 | 534 | /** 535 | * Resolve the given type from the container. 536 | * 537 | * @param string $abstract 538 | * @return mixed 539 | */ 540 | protected function resolve($abstract, array $parameters = []) 541 | { 542 | return Container::getInstance()->make($abstract, $parameters); 543 | } 544 | } 545 | -------------------------------------------------------------------------------- /src/BouncerFacade.php: -------------------------------------------------------------------------------- 1 | registerBouncer(); 23 | $this->registerCommands(); 24 | } 25 | 26 | /** 27 | * Bootstrap any application services. 28 | * 29 | * @return void 30 | */ 31 | public function boot() 32 | { 33 | $this->registerMorphs(); 34 | $this->setUserModel(); 35 | 36 | $this->registerAtGate(); 37 | 38 | if ($this->app->runningInConsole()) { 39 | $this->publishMiddleware(); 40 | $this->publishMigrations(); 41 | } 42 | } 43 | 44 | /** 45 | * Register Bouncer as a singleton. 46 | * 47 | * @return void 48 | */ 49 | protected function registerBouncer() 50 | { 51 | $this->app->singleton(Bouncer::class, function ($app) { 52 | return Bouncer::make() 53 | ->withClipboard(new CachedClipboard(new ArrayStore)) 54 | ->withGate($app->make(Gate::class)) 55 | ->create(); 56 | }); 57 | } 58 | 59 | /** 60 | * Register Bouncer's commands with artisan. 61 | * 62 | * @return void 63 | */ 64 | protected function registerCommands() 65 | { 66 | $this->commands(CleanCommand::class); 67 | } 68 | 69 | /** 70 | * Register Bouncer's models in the relation morph map. 71 | * 72 | * @return void 73 | */ 74 | protected function registerMorphs() 75 | { 76 | Models::updateMorphMap(); 77 | } 78 | 79 | /** 80 | * Publish the package's middleware. 81 | * 82 | * @return void 83 | */ 84 | protected function publishMiddleware() 85 | { 86 | $stub = __DIR__.'/../middleware/ScopeBouncer.php'; 87 | 88 | $target = app_path('Http/Middleware/ScopeBouncer.php'); 89 | 90 | $this->publishes([$stub => $target], 'bouncer.middleware'); 91 | } 92 | 93 | /** 94 | * Publish the package's migrations. 95 | * 96 | * @return void 97 | */ 98 | protected function publishMigrations() 99 | { 100 | if (class_exists('CreateBouncerTables')) { 101 | return; 102 | } 103 | 104 | $timestamp = date('Y_m_d_His', time()); 105 | 106 | $stub = __DIR__.'/../migrations/create_bouncer_tables.php'; 107 | 108 | $target = $this->app->databasePath().'/migrations/'.$timestamp.'_create_bouncer_tables.php'; 109 | 110 | $this->publishes([$stub => $target], 'bouncer.migrations'); 111 | } 112 | 113 | /** 114 | * Set the classname of the user model to be used by Bouncer. 115 | * 116 | * @return void 117 | */ 118 | protected function setUserModel() 119 | { 120 | if ($model = $this->getUserModel()) { 121 | Models::setUsersModel($model); 122 | } 123 | } 124 | 125 | /** 126 | * Get the user model from the application's auth config. 127 | * 128 | * @return string|null 129 | */ 130 | protected function getUserModel() 131 | { 132 | $config = $this->app->make('config'); 133 | 134 | if (is_null($guard = $config->get('auth.defaults.guard'))) { 135 | return null; 136 | } 137 | 138 | if (is_null($provider = $config->get("auth.guards.{$guard}.provider"))) { 139 | return null; 140 | } 141 | 142 | $model = $config->get("auth.providers.{$provider}.model"); 143 | 144 | // The standard auth config that ships with Laravel references the 145 | // Eloquent User model in the above config path. However, users 146 | // are free to reference anything there - so we check first. 147 | if (is_subclass_of($model, EloquentModel::class)) { 148 | return $model; 149 | } 150 | } 151 | 152 | /** 153 | * Register the bouncer's clipboard at the gate. 154 | * 155 | * @return void 156 | */ 157 | protected function registerAtGate() 158 | { 159 | // When creating a Bouncer instance thru the Factory class, it'll 160 | // auto-register at the gate. We already registered Bouncer in 161 | // the container using the Factory, so now we'll resolve it. 162 | $this->app->make(Bouncer::class); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/CachedClipboard.php: -------------------------------------------------------------------------------- 1 | setCache($cache); 34 | } 35 | 36 | /** 37 | * Set the cache instance. 38 | * 39 | * @return $this 40 | */ 41 | public function setCache(Store $cache) 42 | { 43 | if (method_exists($cache, 'tags')) { 44 | $cache = $cache->tags($this->tag()); 45 | } 46 | 47 | $this->cache = $cache; 48 | 49 | return $this; 50 | } 51 | 52 | /** 53 | * Get the cache instance. 54 | * 55 | * @return \Illuminate\Contracts\Cache\Store 56 | */ 57 | public function getCache() 58 | { 59 | return $this->cache; 60 | } 61 | 62 | /** 63 | * Determine if the given authority has the given ability, and return the ability ID. 64 | * 65 | * @param string $ability 66 | * @param \Illuminate\Database\Eloquent\Model|string|null $model 67 | * @return int|bool|null 68 | */ 69 | public function checkGetId(Model $authority, $ability, $model = null) 70 | { 71 | $applicable = $this->compileAbilityIdentifiers($ability, $model); 72 | 73 | // We will first check if any of the applicable abilities have been forbidden. 74 | // If so, we'll return false right away, so as to not pass the check. Then, 75 | // we'll check if any of them have been allowed & return the matched ID. 76 | $forbiddenId = $this->findMatchingAbility( 77 | $this->getForbiddenAbilities($authority), $applicable, $model, $authority 78 | ); 79 | 80 | if ($forbiddenId) { 81 | return false; 82 | } 83 | 84 | return $this->findMatchingAbility( 85 | $this->getAbilities($authority), $applicable, $model, $authority 86 | ); 87 | } 88 | 89 | /** 90 | * Determine if any of the abilities can be matched against the provided applicable ones. 91 | * 92 | * @param \Illuminate\Support\Collection $abilities 93 | * @param \Illuminate\Support\Collection $applicable 94 | * @param \Illuminate\Database\Eloquent\Model $model 95 | * @param \Illuminate\Database\Eloquent\Model $authority 96 | * @return int|null 97 | */ 98 | protected function findMatchingAbility($abilities, $applicable, $model, $authority) 99 | { 100 | $abilities = $abilities->toBase()->pluck('identifier', 'id'); 101 | 102 | if ($id = $this->getMatchedAbilityId($abilities, $applicable)) { 103 | return $id; 104 | } 105 | 106 | if ($this->isOwnedBy($authority, $model)) { 107 | return $this->getMatchedAbilityId( 108 | $abilities, 109 | $applicable->map(function ($identifier) { 110 | return $identifier.'-owned'; 111 | }) 112 | ); 113 | } 114 | } 115 | 116 | /** 117 | * Get the ID of the ability that matches one of the applicable abilities. 118 | * 119 | * @param \Illuminate\Support\Collection $abilityMap 120 | * @param \Illuminate\Support\Collection $applicable 121 | * @return int|null 122 | */ 123 | protected function getMatchedAbilityId($abilityMap, $applicable) 124 | { 125 | foreach ($abilityMap as $id => $identifier) { 126 | if ($applicable->contains($identifier)) { 127 | return $id; 128 | } 129 | } 130 | } 131 | 132 | /** 133 | * Compile a list of ability identifiers that match the provided parameters. 134 | * 135 | * @param string $ability 136 | * @param \Illuminate\Database\Eloquent\Model|string|null $model 137 | * @return \Illuminate\Support\Collection 138 | */ 139 | protected function compileAbilityIdentifiers($ability, $model) 140 | { 141 | $identifiers = new BaseCollection( 142 | is_null($model) 143 | ? [$ability, '*-*', '*'] 144 | : $this->compileModelAbilityIdentifiers($ability, $model) 145 | ); 146 | 147 | return $identifiers->map(function ($identifier) { 148 | return strtolower($identifier); 149 | }); 150 | } 151 | 152 | /** 153 | * Compile a list of ability identifiers that match the given model. 154 | * 155 | * @param string $ability 156 | * @param \Illuminate\Database\Eloquent\Model|string $model 157 | * @return array 158 | */ 159 | protected function compileModelAbilityIdentifiers($ability, $model) 160 | { 161 | if ($model === '*') { 162 | return ["{$ability}-*", '*-*']; 163 | } 164 | 165 | $model = $model instanceof Model ? $model : new $model; 166 | 167 | $type = $model->getMorphClass(); 168 | 169 | $abilities = [ 170 | "{$ability}-{$type}", 171 | "{$ability}-*", 172 | "*-{$type}", 173 | '*-*', 174 | ]; 175 | 176 | if ($model->exists) { 177 | $abilities[] = "{$ability}-{$type}-{$model->getKey()}"; 178 | $abilities[] = "*-{$type}-{$model->getKey()}"; 179 | } 180 | 181 | return $abilities; 182 | } 183 | 184 | /** 185 | * Get the given authority's abilities. 186 | * 187 | * @param bool $allowed 188 | * @return \Illuminate\Database\Eloquent\Collection 189 | */ 190 | public function getAbilities(Model $authority, $allowed = true) 191 | { 192 | $key = $this->getCacheKey($authority, 'abilities', $allowed); 193 | 194 | if (is_array($abilities = $this->cache->get($key))) { 195 | return $this->deserializeAbilities($abilities); 196 | } 197 | 198 | $abilities = $this->getFreshAbilities($authority, $allowed); 199 | 200 | $this->cache->forever($key, $this->serializeAbilities($abilities)); 201 | 202 | return $abilities; 203 | } 204 | 205 | /** 206 | * Get a fresh copy of the given authority's abilities. 207 | * 208 | * @param bool $allowed 209 | * @return \Illuminate\Database\Eloquent\Collection 210 | */ 211 | public function getFreshAbilities(Model $authority, $allowed) 212 | { 213 | return parent::getAbilities($authority, $allowed); 214 | } 215 | 216 | /** 217 | * Get the given authority's roles' IDs and names. 218 | * 219 | * @return array 220 | */ 221 | public function getRolesLookup(Model $authority) 222 | { 223 | $key = $this->getCacheKey($authority, 'roles'); 224 | 225 | return $this->sear($key, function () use ($authority) { 226 | return parent::getRolesLookup($authority); 227 | }); 228 | } 229 | 230 | /** 231 | * Get an item from the cache, or store the default value forever. 232 | * 233 | * @param string $key 234 | * @return mixed 235 | */ 236 | protected function sear($key, callable $callback) 237 | { 238 | if (is_null($value = $this->cache->get($key))) { 239 | $this->cache->forever($key, $value = $callback()); 240 | } 241 | 242 | return $value; 243 | } 244 | 245 | /** 246 | * Clear the cache. 247 | * 248 | * @param null|\Illuminate\Database\Eloquent\Model $authority 249 | * @return $this 250 | */ 251 | public function refresh($authority = null) 252 | { 253 | if (! is_null($authority)) { 254 | return $this->refreshFor($authority); 255 | } 256 | 257 | if ($this->cache instanceof TaggedCache) { 258 | $this->cache->flush(); 259 | } else { 260 | $this->refreshAllIteratively(); 261 | } 262 | 263 | return $this; 264 | } 265 | 266 | /** 267 | * Clear the cache for the given authority. 268 | * 269 | * @return $this 270 | */ 271 | public function refreshFor(Model $authority) 272 | { 273 | $this->cache->forget($this->getCacheKey($authority, 'abilities', true)); 274 | $this->cache->forget($this->getCacheKey($authority, 'abilities', false)); 275 | $this->cache->forget($this->getCacheKey($authority, 'roles')); 276 | 277 | return $this; 278 | } 279 | 280 | /** 281 | * Refresh the cache for all roles and users, iteratively. 282 | * 283 | * @return void 284 | */ 285 | protected function refreshAllIteratively() 286 | { 287 | foreach (Models::user()->all() as $user) { 288 | $this->refreshFor($user); 289 | } 290 | 291 | foreach (Models::role()->all() as $role) { 292 | $this->refreshFor($role); 293 | } 294 | } 295 | 296 | /** 297 | * Get the cache key for the given model's cache type. 298 | * 299 | * @param string $type 300 | * @param bool $allowed 301 | * @return string 302 | */ 303 | protected function getCacheKey(Model $model, $type, $allowed = true) 304 | { 305 | return implode('-', [ 306 | $this->tag(), 307 | $type, 308 | $model->getMorphClass(), 309 | $model->getKey(), 310 | $allowed ? 'a' : 'f', 311 | ]); 312 | } 313 | 314 | /** 315 | * Get the cache tag. 316 | * 317 | * @return string 318 | */ 319 | protected function tag() 320 | { 321 | return Models::scope()->appendToCacheKey($this->tag); 322 | } 323 | 324 | /** 325 | * Deserialize an array of abilities into a collection of models. 326 | * 327 | * @return \Illuminate\Database\Eloquent\Collection 328 | */ 329 | protected function deserializeAbilities(array $abilities) 330 | { 331 | return Models::ability()->hydrate($abilities); 332 | } 333 | 334 | /** 335 | * Serialize a collection of ability models into a plain array. 336 | * 337 | * @return array 338 | */ 339 | protected function serializeAbilities(Collection $abilities) 340 | { 341 | return $abilities->map(function ($ability) { 342 | return $ability->getAttributes(); 343 | })->all(); 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /src/Clipboard.php: -------------------------------------------------------------------------------- 1 | isForbidden($authority, $ability, $model)) { 20 | return false; 21 | } 22 | 23 | $ability = $this->getAllowingAbility($authority, $ability, $model); 24 | 25 | return $ability ? $ability->getKey() : null; 26 | } 27 | 28 | /** 29 | * Determine whether the given ability request is explicitely forbidden. 30 | * 31 | * @param string $ability 32 | * @param \Illuminate\Database\Eloquent\Model|string|null $model 33 | * @return bool 34 | */ 35 | protected function isForbidden(Model $authority, $ability, $model = null) 36 | { 37 | return $this->getHasAbilityQuery( 38 | $authority, $ability, $model, $allowed = false 39 | )->exists(); 40 | } 41 | 42 | /** 43 | * Get the ability model that allows the given ability request. 44 | * 45 | * Returns null if the ability is not allowed. 46 | * 47 | * @param string $ability 48 | * @param \Illuminate\Database\Eloquent\Model|string|null $model 49 | * @return \Illuminate\Database\Eloquent\Model|null 50 | */ 51 | protected function getAllowingAbility(Model $authority, $ability, $model = null) 52 | { 53 | return $this->getHasAbilityQuery( 54 | $authority, $ability, $model, $allowed = true 55 | )->first(); 56 | } 57 | 58 | /** 59 | * Get the query for where the given authority has the given ability. 60 | * 61 | * @param \Illuminate\Database\Eloquent\Model $authority 62 | * @param string $ability 63 | * @param \Illuminate\Database\Eloquent\Model|string|null $model 64 | * @param bool $allowed 65 | * @return \Illuminate\Database\Eloquent\Builder 66 | */ 67 | protected function getHasAbilityQuery($authority, $ability, $model, $allowed) 68 | { 69 | $query = Abilities::forAuthority($authority, $allowed); 70 | 71 | if (! $this->isOwnedBy($authority, $model)) { 72 | $query->where('only_owned', false); 73 | } 74 | 75 | if (is_null($model)) { 76 | return $this->constrainToSimpleAbility($query, $ability); 77 | } 78 | 79 | return $query->byName($ability)->forModel($model); 80 | } 81 | 82 | /** 83 | * Constrain the query to the given non-model ability. 84 | * 85 | * @param \Illuminate\Database\Eloquent\Builder $query 86 | * @param string $ability 87 | * @return \Illuminate\Database\Eloquent\Builder 88 | */ 89 | protected function constrainToSimpleAbility($query, $ability) 90 | { 91 | return $query->where(function ($query) use ($ability) { 92 | $query->where('name', $ability)->whereNull('entity_type'); 93 | 94 | $query->orWhere(function ($query) { 95 | $query->where('name', '*')->where(function ($query) { 96 | $query->whereNull('entity_type')->orWhere('entity_type', '*'); 97 | }); 98 | }); 99 | }); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Conductors/AssignsRoles.php: -------------------------------------------------------------------------------- 1 | roles = Helpers::toArray($roles); 26 | } 27 | 28 | /** 29 | * Assign the roles to the given authority. 30 | * 31 | * @param \Illuminate\Database\Eloquent\Model|array|int $authority 32 | * @return bool 33 | */ 34 | public function to($authority) 35 | { 36 | $authorities = is_array($authority) ? $authority : [$authority]; 37 | 38 | $roles = Models::role()->findOrCreateRoles($this->roles); 39 | 40 | foreach (Helpers::mapAuthorityByClass($authorities) as $class => $ids) { 41 | $this->assignRoles($roles, $class, new Collection($ids)); 42 | } 43 | 44 | return true; 45 | } 46 | 47 | /** 48 | * Assign the given roles to the given authorities. 49 | * 50 | * @param string $authorityClass 51 | * @return void 52 | */ 53 | protected function assignRoles(Collection $roles, $authorityClass, Collection $authorityIds) 54 | { 55 | $roleIds = $roles->map(function ($model) { 56 | return $model->getKey(); 57 | }); 58 | 59 | $morphType = (new $authorityClass)->getMorphClass(); 60 | 61 | $records = $this->buildAttachRecords($roleIds, $morphType, $authorityIds); 62 | 63 | $existing = $this->getExistingAttachRecords($roleIds, $morphType, $authorityIds); 64 | 65 | $this->createMissingAssignRecords($records, $existing); 66 | } 67 | 68 | /** 69 | * Get the pivot table records for the roles already assigned. 70 | * 71 | * @param \Illuminate\Support\Collection $roleIds 72 | * @param string $morphType 73 | * @param \Illuminate\Support\Collection $authorityIds 74 | * @return \Illuminate\Support\Collection 75 | */ 76 | protected function getExistingAttachRecords($roleIds, $morphType, $authorityIds) 77 | { 78 | $query = $this->newPivotTableQuery() 79 | ->whereIn('role_id', $roleIds->all()) 80 | ->whereIn('entity_id', $authorityIds->all()) 81 | ->where('entity_type', $morphType); 82 | 83 | Models::scope()->applyToRelationQuery($query, $query->from); 84 | 85 | return new Collection($query->get()); 86 | } 87 | 88 | /** 89 | * Build the raw attach records for the assigned roles pivot table. 90 | * 91 | * @param \Illuminate\Support\Collection $roleIds 92 | * @param string $morphType 93 | * @param \Illuminate\Support\Collection $authorityIds 94 | * @return \Illuminate\Support\Collection 95 | */ 96 | protected function buildAttachRecords($roleIds, $morphType, $authorityIds) 97 | { 98 | return $roleIds 99 | ->crossJoin($authorityIds) 100 | ->mapSpread(function ($roleId, $authorityId) use ($morphType) { 101 | return Models::scope()->getAttachAttributes() + [ 102 | 'role_id' => $roleId, 103 | 'entity_id' => $authorityId, 104 | 'entity_type' => $morphType, 105 | ]; 106 | }); 107 | } 108 | 109 | /** 110 | * Save the non-existing attach records in the DB. 111 | * 112 | * @return void 113 | */ 114 | protected function createMissingAssignRecords(Collection $records, Collection $existing) 115 | { 116 | $existing = $existing->keyBy(function ($record) { 117 | return $this->getAttachRecordHash((array) $record); 118 | }); 119 | 120 | $records = $records->reject(function ($record) use ($existing) { 121 | return $existing->has($this->getAttachRecordHash($record)); 122 | }); 123 | 124 | $this->newPivotTableQuery()->insert($records->all()); 125 | } 126 | 127 | /** 128 | * Get a string identifying the given attach record. 129 | * 130 | * @return string 131 | */ 132 | protected function getAttachRecordHash(array $record) 133 | { 134 | return $record['role_id'].$record['entity_id'].$record['entity_type']; 135 | } 136 | 137 | /** 138 | * Get a query builder instance for the assigned roles pivot table. 139 | * 140 | * @return \Illuminate\Database\Query\Builder 141 | */ 142 | protected function newPivotTableQuery() 143 | { 144 | return Models::query('assigned_roles'); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Conductors/ChecksRoles.php: -------------------------------------------------------------------------------- 1 | authority = $authority; 30 | $this->clipboard = $clipboard; 31 | } 32 | 33 | /** 34 | * Check if the authority has any of the given roles. 35 | * 36 | * @param string ...$roles 37 | * @return bool 38 | */ 39 | public function a(...$roles) 40 | { 41 | return $this->clipboard->checkRole($this->authority, $roles, 'or'); 42 | } 43 | 44 | /** 45 | * Check if the authority doesn't have any of the given roles. 46 | * 47 | * @param string ...$roles 48 | * @return bool 49 | */ 50 | public function notA(...$roles) 51 | { 52 | return $this->clipboard->checkRole($this->authority, $roles, 'not'); 53 | } 54 | 55 | /** 56 | * Alias to the "a" method. 57 | * 58 | * @param string ...$roles 59 | * @return bool 60 | */ 61 | public function an(...$roles) 62 | { 63 | return $this->clipboard->checkRole($this->authority, $roles, 'or'); 64 | } 65 | 66 | /** 67 | * Alias to the "notA" method. 68 | * 69 | * @param string ...$roles 70 | * @return bool 71 | */ 72 | public function notAn(...$roles) 73 | { 74 | return $this->clipboard->checkRole($this->authority, $roles, 'not'); 75 | } 76 | 77 | /** 78 | * Check if the authority has all of the given roles. 79 | * 80 | * @param string ...$roles 81 | * @return bool 82 | */ 83 | public function all(...$roles) 84 | { 85 | return $this->clipboard->checkRole($this->authority, $roles, 'and'); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Conductors/Concerns/AssociatesAbilities.php: -------------------------------------------------------------------------------- 1 | shouldConductLazy(...func_get_args())) { 23 | return $this->conductLazy($abilities); 24 | } 25 | 26 | $ids = $this->getAbilityIds($abilities, $model, $attributes); 27 | 28 | $this->associateAbilities($ids, $this->getAuthority()); 29 | } 30 | 31 | /** 32 | * Get the authority, creating a role authority if necessary. 33 | * 34 | * @return \Illuminate\Database\Eloquent\Model|null 35 | */ 36 | protected function getAuthority() 37 | { 38 | if (is_null($this->authority)) { 39 | return null; 40 | } 41 | 42 | if ($this->authority instanceof Model) { 43 | return $this->authority; 44 | } 45 | 46 | return Models::role()->firstOrCreate(['name' => $this->authority]); 47 | } 48 | 49 | /** 50 | * Get the IDs of the associated abilities. 51 | * 52 | * @param \Illuminate\Database\Eloquent\Model|null $authority 53 | * @return array 54 | */ 55 | protected function getAssociatedAbilityIds($authority, array $abilityIds) 56 | { 57 | if (is_null($authority)) { 58 | return $this->getAbilityIdsAssociatedWithEveryone($abilityIds); 59 | } 60 | 61 | $relation = $authority->abilities(); 62 | 63 | $table = Models::table('abilities'); 64 | 65 | $relation->whereIn("{$table}.id", $abilityIds) 66 | ->wherePivot('forbidden', '=', $this->forbidding); 67 | 68 | Models::scope()->applyToRelation($relation); 69 | 70 | return $relation->get(["{$table}.id"])->pluck('id')->all(); 71 | } 72 | 73 | /** 74 | * Get the IDs of the abilities associated with everyone. 75 | * 76 | * @return array 77 | */ 78 | protected function getAbilityIdsAssociatedWithEveryone(array $abilityIds) 79 | { 80 | $query = Models::query('permissions') 81 | ->whereNull('entity_id') 82 | ->whereIn('ability_id', $abilityIds) 83 | ->where('forbidden', '=', $this->forbidding); 84 | 85 | Models::scope()->applyToRelationQuery($query, $query->from); 86 | 87 | return Arr::pluck($query->get(['ability_id']), 'ability_id'); 88 | } 89 | 90 | /** 91 | * Associate the given ability IDs on the permissions table. 92 | * 93 | * @return void 94 | */ 95 | protected function associateAbilities(array $ids, ?Model $authority = null) 96 | { 97 | $ids = array_diff($ids, $this->getAssociatedAbilityIds($authority, $ids, false)); 98 | 99 | if (is_null($authority)) { 100 | $this->associateAbilitiesToEveryone($ids); 101 | } else { 102 | $this->associateAbilitiesToAuthority($ids, $authority); 103 | } 104 | } 105 | 106 | /** 107 | * Associate these abilities with the given authority. 108 | * 109 | * @return void 110 | */ 111 | protected function associateAbilitiesToAuthority(array $ids, Model $authority) 112 | { 113 | $attributes = Models::scope()->getAttachAttributes(get_class($authority)); 114 | 115 | $authority 116 | ->abilities() 117 | ->attach($ids, ['forbidden' => $this->forbidding] + $attributes); 118 | } 119 | 120 | /** 121 | * Associate these abilities with everyone. 122 | * 123 | * @return void 124 | */ 125 | protected function associateAbilitiesToEveryone(array $ids) 126 | { 127 | $attributes = ['forbidden' => $this->forbidding]; 128 | 129 | $attributes += Models::scope()->getAttachAttributes(); 130 | 131 | $records = array_map(function ($id) use ($attributes) { 132 | return ['ability_id' => $id] + $attributes; 133 | }, $ids); 134 | 135 | Models::query('permissions')->insert($records); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Conductors/Concerns/ConductsAbilities.php: -------------------------------------------------------------------------------- 1 | to('*', '*', $attributes); 19 | } 20 | 21 | /** 22 | * Allow/disallow all abilities on the given model. 23 | * 24 | * @param string|array|\Illuminate\Database\Eloquent\Model $models 25 | * @return void 26 | */ 27 | public function toManage($models, array $attributes = []) 28 | { 29 | if (is_array($models)) { 30 | foreach ($models as $model) { 31 | $this->to('*', $model, $attributes); 32 | } 33 | } else { 34 | $this->to('*', $models, $attributes); 35 | } 36 | } 37 | 38 | /** 39 | * Allow/disallow owning the given model. 40 | * 41 | * @param string|object $model 42 | * @return \Silber\Bouncer\Conductors\Lazy\HandlesOwnership 43 | */ 44 | public function toOwn($model, array $attributes = []) 45 | { 46 | return new Lazy\HandlesOwnership($this, $model, $attributes); 47 | } 48 | 49 | /** 50 | * Allow/disallow owning all models. 51 | * 52 | * @return \Silber\Bouncer\Conductors\Lazy\HandlesOwnership 53 | */ 54 | public function toOwnEverything(array $attributes = []) 55 | { 56 | return $this->toOwn('*', $attributes); 57 | } 58 | 59 | /** 60 | * Determines whether a call to "to" with the given parameters should be conducted lazily. 61 | * 62 | * @param mixed $abilities 63 | * @param mixed $model 64 | * @return bool 65 | */ 66 | protected function shouldConductLazy($abilities) 67 | { 68 | // We'll only create a lazy conductor if we got a single 69 | // param, and that single param is either a string or 70 | // a numerically-indexed array (of simple strings). 71 | if (func_num_args() > 1) { 72 | return false; 73 | } 74 | 75 | if (is_string($abilities)) { 76 | return true; 77 | } 78 | 79 | if (! is_array($abilities) || ! Helpers::isIndexedArray($abilities)) { 80 | return false; 81 | } 82 | 83 | return (new Collection($abilities))->every('is_string'); 84 | } 85 | 86 | /** 87 | * Create a lazy abilities conductor. 88 | * 89 | * @param string|string[] $ablities 90 | * @return \Silber\Bouncer\Conductors\Lazy\ConductsAbilities 91 | */ 92 | protected function conductLazy($abilities) 93 | { 94 | return new Lazy\ConductsAbilities($this, $abilities); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Conductors/Concerns/DisassociatesAbilities.php: -------------------------------------------------------------------------------- 1 | shouldConductLazy(...func_get_args())) { 23 | return $this->conductLazy($abilities); 24 | } 25 | 26 | if ($ids = $this->getAbilityIds($abilities, $entity, $attributes)) { 27 | $this->disassociateAbilities($this->getAuthority(), $ids); 28 | } 29 | 30 | return true; 31 | } 32 | 33 | /** 34 | * Detach the given IDs from the authority. 35 | * 36 | * @param \Illuminate\Database\Eloquent\Model|null $authority 37 | * @return void 38 | */ 39 | protected function disassociateAbilities($authority, array $ids) 40 | { 41 | if (is_null($authority)) { 42 | $this->disassociateEveryone($ids); 43 | } else { 44 | $this->disassociateAuthority($authority, $ids); 45 | } 46 | } 47 | 48 | /** 49 | * Disassociate the authority from the abilities with the given IDs. 50 | * 51 | * @return void 52 | */ 53 | protected function disassociateAuthority(Model $authority, array $ids) 54 | { 55 | $this->getAbilitiesPivotQuery($authority, $ids) 56 | ->where($this->constraints()) 57 | ->delete(); 58 | } 59 | 60 | /** 61 | * Get the base abilities pivot query. 62 | * 63 | * @param array $ids 64 | * @return \Illuminate\Database\Query\Builder 65 | */ 66 | protected function getAbilitiesPivotQuery(Model $model, $ids) 67 | { 68 | $relation = $model->abilities(); 69 | 70 | $query = $relation 71 | ->newPivotStatement() 72 | ->where($relation->getQualifiedForeignPivotKeyName(), $model->getKey()) 73 | ->where('entity_type', $model->getMorphClass()) 74 | ->whereIn($relation->getQualifiedRelatedPivotKeyName(), $ids); 75 | 76 | return Models::scope()->applyToRelationQuery( 77 | $query, $relation->getTable() 78 | ); 79 | } 80 | 81 | /** 82 | * Disassociate everyone from the abilities with the given IDs. 83 | * 84 | * @return void 85 | */ 86 | protected function disassociateEveryone(array $ids) 87 | { 88 | $query = Models::query('permissions') 89 | ->whereNull('entity_id') 90 | ->where($this->constraints()) 91 | ->whereIn('ability_id', $ids); 92 | 93 | Models::scope()->applyToRelationQuery($query, $query->from); 94 | 95 | $query->delete(); 96 | } 97 | 98 | /** 99 | * Get the authority from which to disassociate the abilities. 100 | * 101 | * @return \Illuminate\Database\Eloquent\Model|null 102 | */ 103 | protected function getAuthority() 104 | { 105 | if (is_null($this->authority)) { 106 | return null; 107 | } 108 | 109 | if ($this->authority instanceof Model) { 110 | return $this->authority; 111 | } 112 | 113 | return Models::role()->where('name', $this->authority)->firstOrFail(); 114 | } 115 | 116 | /** 117 | * Get the additional constraints for the detaching query. 118 | * 119 | * @return array 120 | */ 121 | protected function constraints() 122 | { 123 | return property_exists($this, 'constraints') ? $this->constraints : []; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Conductors/Concerns/FindsAndCreatesAbilities.php: -------------------------------------------------------------------------------- 1 | getKey()]; 25 | } 26 | 27 | if (! is_null($model)) { 28 | return $this->getModelAbilityKeys($abilities, $model, $attributes); 29 | } 30 | 31 | if (Helpers::isAssociativeArray($abilities)) { 32 | return $this->getAbilityIdsFromMap($abilities, $attributes); 33 | } 34 | 35 | if (! is_array($abilities) && ! $abilities instanceof Collection) { 36 | $abilities = [$abilities]; 37 | } 38 | 39 | return $this->getAbilityIdsFromArray($abilities, $attributes); 40 | } 41 | 42 | /** 43 | * Get the ability IDs for the given map. 44 | * 45 | * The map should use the ['ability-name' => Entity::class] format. 46 | * 47 | * @return array 48 | */ 49 | protected function getAbilityIdsFromMap(array $map, array $attributes) 50 | { 51 | [$map, $list] = Helpers::partition($map, function ($value, $key) { 52 | return ! is_int($key); 53 | }); 54 | 55 | return $map->map(function ($entity, $ability) use ($attributes) { 56 | return $this->getAbilityIds($ability, $entity, $attributes); 57 | })->collapse()->merge($this->getAbilityIdsFromArray($list, $attributes))->all(); 58 | } 59 | 60 | /** 61 | * Get the ability IDs from the provided array, creating the ones that don't exist. 62 | * 63 | * @param iterable $abilities 64 | * @return array 65 | */ 66 | protected function getAbilityIdsFromArray($abilities, array $attributes) 67 | { 68 | $groups = Helpers::groupModelsAndIdentifiersByType($abilities); 69 | 70 | $keyName = Models::ability()->getKeyName(); 71 | 72 | $groups['strings'] = $this->abilitiesByName($groups['strings'], $attributes) 73 | ->pluck($keyName)->all(); 74 | 75 | $groups['models'] = Arr::pluck($groups['models'], $keyName); 76 | 77 | return Arr::collapse($groups); 78 | } 79 | 80 | /** 81 | * Get the abilities for the given model ability descriptors. 82 | * 83 | * @param array|string $abilities 84 | * @param \Illuminate\Database\Eloquent\Model|string|array $model 85 | * @return array 86 | */ 87 | protected function getModelAbilityKeys($abilities, $model, array $attributes) 88 | { 89 | $abilities = Collection::make(is_array($abilities) ? $abilities : [$abilities]); 90 | 91 | $models = Collection::make(is_array($model) ? $model : [$model]); 92 | 93 | return $abilities->map(function ($ability) use ($models, $attributes) { 94 | return $models->map(function ($model) use ($ability, $attributes) { 95 | return $this->getModelAbility($ability, $model, $attributes)->getKey(); 96 | }); 97 | })->collapse()->all(); 98 | } 99 | 100 | /** 101 | * Get an ability for the given entity. 102 | * 103 | * @param string $ability 104 | * @param \Illuminate\Database\Eloquent\Model|string $entity 105 | * @return \Silber\Bouncer\Database\Ability 106 | */ 107 | protected function getModelAbility($ability, $entity, array $attributes) 108 | { 109 | $entity = $this->getEntityInstance($entity); 110 | 111 | $existing = $this->findAbility($ability, $entity, $attributes); 112 | 113 | return $existing ?: $this->createAbility($ability, $entity, $attributes); 114 | } 115 | 116 | /** 117 | * Find the ability for the given entity. 118 | * 119 | * @param string $ability 120 | * @param \Illuminate\Database\Eloquent\Model|string $entity 121 | * @param array $attributes 122 | * @return \Silber\Bouncer\Database\Ability|null 123 | */ 124 | protected function findAbility($ability, $entity, $attributes) 125 | { 126 | $onlyOwned = isset($attributes['only_owned']) ? $attributes['only_owned'] : false; 127 | 128 | $query = Models::ability() 129 | ->where('name', $ability) 130 | ->forModel($entity, true) 131 | ->where('only_owned', $onlyOwned); 132 | 133 | return Models::scope()->applyToModelQuery($query)->first(); 134 | } 135 | 136 | /** 137 | * Create an ability for the given entity. 138 | * 139 | * @param string $ability 140 | * @param \Illuminate\Database\Eloquent\Model|string $entity 141 | * @param array $attributes 142 | * @return \Silber\Bouncer\Database\Ability 143 | */ 144 | protected function createAbility($ability, $entity, $attributes) 145 | { 146 | return Models::ability()->createForModel($entity, $attributes + [ 147 | 'name' => $ability, 148 | ]); 149 | } 150 | 151 | /** 152 | * Get an instance of the given model. 153 | * 154 | * @param \Illuminate\Database\Eloquent\Model|string $model 155 | * @return \Illuminate\Database\Eloquent\Model|string 156 | */ 157 | protected function getEntityInstance($model) 158 | { 159 | if ($model === '*') { 160 | return '*'; 161 | } 162 | 163 | if (! $model instanceof Model) { 164 | return new $model; 165 | } 166 | 167 | // Creating an ability for a non-existent model gives the authority that 168 | // ability on all instances of that model. If the developer passed in 169 | // a model instance that does not exist, it is probably a mistake. 170 | if (! $model->exists) { 171 | throw new InvalidArgumentException( 172 | 'The model does not exist. To edit access to all models, use the class name instead' 173 | ); 174 | } 175 | 176 | return $model; 177 | } 178 | 179 | /** 180 | * Get or create abilities by their name. 181 | * 182 | * @param array|string $abilities 183 | * @param array $attributes 184 | * @return \Illuminate\Support\Collection 185 | */ 186 | protected function abilitiesByName($abilities, $attributes = []) 187 | { 188 | $abilities = array_unique(is_array($abilities) ? $abilities : [$abilities]); 189 | 190 | if (empty($abilities)) { 191 | return new Collection; 192 | } 193 | 194 | $existing = Models::ability()->simpleAbility()->whereIn('name', $abilities)->get(); 195 | 196 | return $existing->merge($this->createMissingAbilities( 197 | $existing, $abilities, $attributes 198 | )); 199 | } 200 | 201 | /** 202 | * Create the non-existant abilities by name. 203 | * 204 | * @param \Illuminate\Database\Eloquent\Collection $existing 205 | * @param string[] $abilities 206 | * @param array $attributes 207 | * @return array 208 | */ 209 | protected function createMissingAbilities($existing, array $abilities, $attributes = []) 210 | { 211 | $missing = array_diff($abilities, $existing->pluck('name')->all()); 212 | 213 | return array_map(function ($ability) use ($attributes) { 214 | return Models::ability()->create($attributes + ['name' => $ability]); 215 | }, $missing); 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/Conductors/ForbidsAbilities.php: -------------------------------------------------------------------------------- 1 | authority = $authority; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Conductors/GivesAbilities.php: -------------------------------------------------------------------------------- 1 | authority = $authority; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Conductors/Lazy/ConductsAbilities.php: -------------------------------------------------------------------------------- 1 | conductor = $conductor; 45 | $this->abilities = $abilities; 46 | } 47 | 48 | /** 49 | * Sets that the abilities should be applied towards everything. 50 | * 51 | * @return void 52 | */ 53 | public function everything(array $attributes = []) 54 | { 55 | $this->everything = true; 56 | 57 | $this->attributes = $attributes; 58 | } 59 | 60 | /** 61 | * Destructor. 62 | */ 63 | public function __destruct() 64 | { 65 | $this->conductor->to( 66 | $this->abilities, 67 | $this->everything ? '*' : null, 68 | $this->attributes 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Conductors/Lazy/HandlesOwnership.php: -------------------------------------------------------------------------------- 1 | conductor = $conductor; 44 | $this->model = $model; 45 | $this->attributes = $attributes; 46 | } 47 | 48 | /** 49 | * Limit ownership to the given ability. 50 | * 51 | * @param string|string[] $ability 52 | * @return void 53 | */ 54 | public function to($ability, array $attributes = []) 55 | { 56 | $this->ability = $ability; 57 | 58 | $this->attributes = array_merge($this->attributes, $attributes); 59 | } 60 | 61 | /** 62 | * Destructor. 63 | */ 64 | public function __destruct() 65 | { 66 | $this->conductor->to( 67 | $this->ability, 68 | $this->model, 69 | $this->attributes + ['only_owned' => true] 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Conductors/RemovesAbilities.php: -------------------------------------------------------------------------------- 1 | false]; 22 | 23 | /** 24 | * Constructor. 25 | * 26 | * @param \Illuminate\Database\Eloquent\Model|string|null $authority 27 | */ 28 | public function __construct($authority = null) 29 | { 30 | $this->authority = $authority; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Conductors/RemovesRoles.php: -------------------------------------------------------------------------------- 1 | roles = Helpers::toArray($roles); 27 | } 28 | 29 | /** 30 | * Remove the role from the given authority. 31 | * 32 | * @param \Illuminate\Database\Eloquent\Model|array|int $authority 33 | * @return void 34 | */ 35 | public function from($authority) 36 | { 37 | if (! ($roleIds = $this->getRoleIds())) { 38 | return; 39 | } 40 | 41 | $authorities = is_array($authority) ? $authority : [$authority]; 42 | 43 | foreach (Helpers::mapAuthorityByClass($authorities) as $class => $keys) { 44 | $this->retractRoles($roleIds, $class, $keys); 45 | } 46 | } 47 | 48 | /** 49 | * Get the IDs of anyexisting roles provided. 50 | * 51 | * @return array 52 | */ 53 | protected function getRoleIds() 54 | { 55 | [$models, $names] = Helpers::partition($this->roles, function ($role) { 56 | return $role instanceof Model; 57 | }); 58 | 59 | $ids = $models->map(function ($model) { 60 | return $model->getKey(); 61 | }); 62 | 63 | if ($names->count()) { 64 | $ids = $ids->merge($this->getRoleIdsFromNames($names->all())); 65 | } 66 | 67 | return $ids->all(); 68 | } 69 | 70 | /** 71 | * Get the IDs of the roles with the given names. 72 | * 73 | * @param string[] $names 74 | * @return \Illuminate\Database\Eloquent\Collection 75 | */ 76 | protected function getRoleIdsFromNames(array $names) 77 | { 78 | $key = Models::role()->getKeyName(); 79 | 80 | return Models::role() 81 | ->whereIn('name', $names) 82 | ->get([$key]) 83 | ->pluck($key); 84 | } 85 | 86 | /** 87 | * Retract the given roles from the given authorities. 88 | * 89 | * @param array $roleIds 90 | * @param string $authorityClass 91 | * @param array $authorityIds 92 | * @return void 93 | */ 94 | protected function retractRoles($roleIds, $authorityClass, $authorityIds) 95 | { 96 | $query = $this->newPivotTableQuery(); 97 | 98 | $morphType = (new $authorityClass)->getMorphClass(); 99 | 100 | foreach ($roleIds as $roleId) { 101 | foreach ($authorityIds as $authorityId) { 102 | $query->orWhere($this->getDetachQueryConstraint( 103 | $roleId, $authorityId, $morphType 104 | )); 105 | } 106 | } 107 | 108 | $query->delete(); 109 | } 110 | 111 | /** 112 | * Get a constraint for the detach query for the given parameters. 113 | * 114 | * @param mixed $roleId 115 | * @param mixed $authorityId 116 | * @param string $morphType 117 | * @return \Closure 118 | */ 119 | protected function getDetachQueryConstraint($roleId, $authorityId, $morphType) 120 | { 121 | return function ($query) use ($roleId, $authorityId, $morphType) { 122 | $query->where(Models::scope()->getAttachAttributes() + [ 123 | 'role_id' => $roleId, 124 | 'entity_id' => $authorityId, 125 | 'entity_type' => $morphType, 126 | ]); 127 | }; 128 | } 129 | 130 | /** 131 | * Get a query builder instance for the assigned roles pivot table. 132 | * 133 | * @return \Illuminate\Database\Query\Builder 134 | */ 135 | protected function newPivotTableQuery() 136 | { 137 | return Models::query('assigned_roles'); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Conductors/SyncsRolesAndAbilities.php: -------------------------------------------------------------------------------- 1 | authority = $authority; 28 | } 29 | 30 | /** 31 | * Sync the provided roles to the authority. 32 | * 33 | * @param iterable $roles 34 | * @return void 35 | */ 36 | public function roles($roles) 37 | { 38 | $this->sync( 39 | Models::role()->getRoleKeys($roles), 40 | $this->authority->roles() 41 | ); 42 | } 43 | 44 | /** 45 | * Sync the provided abilities to the authority. 46 | * 47 | * @param iterable $abilities 48 | * @return void 49 | */ 50 | public function abilities($abilities) 51 | { 52 | $this->syncAbilities($abilities); 53 | } 54 | 55 | /** 56 | * Sync the provided forbidden abilities to the authority. 57 | * 58 | * @param iterable $abilities 59 | * @return void 60 | */ 61 | public function forbiddenAbilities($abilities) 62 | { 63 | $this->syncAbilities($abilities, ['forbidden' => true]); 64 | } 65 | 66 | /** 67 | * Sync the given abilities for the authority. 68 | * 69 | * @param iterable $abilities 70 | * @param array $options 71 | * @return void 72 | */ 73 | protected function syncAbilities($abilities, $options = ['forbidden' => false]) 74 | { 75 | $abilityKeys = $this->getAbilityIds($abilities); 76 | $authority = $this->getAuthority(); 77 | $relation = $authority->abilities(); 78 | 79 | $this->newPivotQuery($relation) 80 | ->where('entity_type', $authority->getMorphClass()) 81 | ->whereNotIn($relation->getRelatedPivotKeyName(), $abilityKeys) 82 | ->where('forbidden', $options['forbidden']) 83 | ->delete(); 84 | 85 | if ($options['forbidden']) { 86 | (new ForbidsAbilities($this->authority))->to($abilityKeys); 87 | } else { 88 | (new GivesAbilities($this->authority))->to($abilityKeys); 89 | } 90 | } 91 | 92 | /** 93 | * Get (and cache) the authority for whom to sync roles/abilities. 94 | * 95 | * @return \Illuminate\Database\Eloquent\Model 96 | */ 97 | protected function getAuthority() 98 | { 99 | if (is_string($this->authority)) { 100 | $this->authority = Models::role()->firstOrCreate([ 101 | 'name' => $this->authority, 102 | ]); 103 | } 104 | 105 | return $this->authority; 106 | } 107 | 108 | /** 109 | * Get the fully qualified column name for the abilities table's primary key. 110 | * 111 | * @return string 112 | */ 113 | protected function getAbilitiesQualifiedKeyName() 114 | { 115 | $model = Models::ability(); 116 | 117 | return $model->getTable().'.'.$model->getKeyName(); 118 | } 119 | 120 | /** 121 | * Sync the given IDs on the pivot relation. 122 | * 123 | * This is a heavily-modified version of Eloquent's built-in 124 | * BelongsToMany@sync - which we sadly cannot use because 125 | * our scope sets a "closure where" on the pivot table. 126 | * 127 | * @return void 128 | */ 129 | protected function sync(array $ids, BelongsToMany $relation) 130 | { 131 | $current = $this->pluck( 132 | $this->newPivotQuery($relation), 133 | $relation->getRelatedPivotKeyName() 134 | ); 135 | 136 | $this->detach(array_diff($current, $ids), $relation); 137 | 138 | $relation->attach( 139 | array_diff($ids, $current), 140 | Models::scope()->getAttachAttributes($this->authority) 141 | ); 142 | } 143 | 144 | /** 145 | * Detach the records with the given IDs from the relationship. 146 | * 147 | * @return void 148 | */ 149 | public function detach(array $ids, BelongsToMany $relation) 150 | { 151 | if (empty($ids)) { 152 | return; 153 | } 154 | 155 | $this->newPivotQuery($relation) 156 | ->whereIn($relation->getRelatedPivotKeyName(), $ids) 157 | ->delete(); 158 | } 159 | 160 | /** 161 | * Get a scoped query for the pivot table. 162 | * 163 | * @return \Illuminate\Database\Query\Builder 164 | */ 165 | protected function newPivotQuery(BelongsToMany $relation) 166 | { 167 | $query = $relation 168 | ->newPivotStatement() 169 | ->where('entity_type', $this->getAuthority()->getMorphClass()) 170 | ->where( 171 | $relation->getForeignPivotKeyName(), 172 | $relation->getParent()->getKey() 173 | ); 174 | 175 | return Models::scope()->applyToRelationQuery( 176 | $query, $relation->getTable() 177 | ); 178 | } 179 | 180 | /** 181 | * Pluck the values of the given column using the provided query. 182 | * 183 | * @param mixed $query 184 | * @param string $column 185 | * @return string[] 186 | */ 187 | protected function pluck($query, $column) 188 | { 189 | return Arr::pluck($query->get([$column]), last(explode('.', $column))); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/Conductors/UnforbidsAbilities.php: -------------------------------------------------------------------------------- 1 | true]; 22 | 23 | /** 24 | * Constructor. 25 | * 26 | * @param \Illuminate\Database\Eloquent\Model|string|null $authority 27 | */ 28 | public function __construct($authority = null) 29 | { 30 | $this->authority = $authority; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Console/CleanCommand.php: -------------------------------------------------------------------------------- 1 | getComputedOptions(); 36 | 37 | if ($unassigned) { 38 | $this->deleteUnassignedAbilities(); 39 | } 40 | 41 | if ($orphaned) { 42 | $this->deleteOrphanedAbilities(); 43 | } 44 | } 45 | 46 | /** 47 | * Get the options to use, computed by omission. 48 | * 49 | * @return array 50 | */ 51 | protected function getComputedOptions() 52 | { 53 | $unassigned = $this->option('unassigned'); 54 | $orphaned = $this->option('orphaned'); 55 | 56 | if (! $unassigned && ! $orphaned) { 57 | $unassigned = $orphaned = true; 58 | } 59 | 60 | return [$unassigned, $orphaned]; 61 | } 62 | 63 | /** 64 | * Delete abilities not assigned to anyone. 65 | * 66 | * @return void 67 | */ 68 | protected function deleteUnassignedAbilities() 69 | { 70 | $query = $this->getUnassignedAbilitiesQuery(); 71 | 72 | if (($count = $query->count()) > 0) { 73 | $query->delete(); 74 | 75 | $this->info("Deleted {$count} unassigned ".Str::plural('ability', $count).'.'); 76 | } else { 77 | $this->info('No unassigned abilities.'); 78 | } 79 | } 80 | 81 | /** 82 | * Get the base query for all unassigned abilities. 83 | * 84 | * @return \Illuminate\Database\Eloquent\Query 85 | */ 86 | protected function getUnassignedAbilitiesQuery() 87 | { 88 | $model = Models::ability(); 89 | 90 | return $model->whereNotIn($model->getKeyName(), function ($query) { 91 | $query->from(Models::table('permissions'))->select('ability_id'); 92 | }); 93 | } 94 | 95 | /** 96 | * Delete model abilities whose models have been deleted. 97 | * 98 | * @return void 99 | */ 100 | protected function deleteOrphanedAbilities() 101 | { 102 | $query = $this->getBaseOrphanedQuery()->where(function ($query) { 103 | foreach ($this->getEntityTypes() as $entityType) { 104 | $query->orWhere(function ($query) use ($entityType) { 105 | $this->scopeQueryToWhereModelIsMissing($query, $entityType); 106 | }); 107 | } 108 | }); 109 | 110 | if (($count = $query->count()) > 0) { 111 | $query->delete(); 112 | 113 | $this->info("Deleted {$count} orphaned ".Str::plural('ability', $count).'.'); 114 | } else { 115 | $this->info('No orphaned abilities.'); 116 | } 117 | } 118 | 119 | /** 120 | * Scope the given query to where the ability's model is missing. 121 | * 122 | * @param \Illuminate\Database\Query\Builder $query 123 | * @param string $entityType 124 | * @return void 125 | */ 126 | protected function scopeQueryToWhereModelIsMissing($query, $entityType) 127 | { 128 | $model = $this->makeModel($entityType); 129 | $abilities = $this->abilitiesTable(); 130 | 131 | $query->where("{$abilities}.entity_type", $entityType); 132 | 133 | $query->whereNotIn("{$abilities}.entity_id", function ($query) use ($model) { 134 | $table = $model->getTable(); 135 | 136 | $query->from($table)->select($table.'.'.$model->getKeyName()); 137 | }); 138 | } 139 | 140 | /** 141 | * Get the entity types of all model abilities. 142 | * 143 | * @return iterable 144 | */ 145 | protected function getEntityTypes() 146 | { 147 | return $this 148 | ->getBaseOrphanedQuery() 149 | ->distinct() 150 | ->pluck('entity_type'); 151 | } 152 | 153 | /** 154 | * Get the base query for abilities with missing models. 155 | * 156 | * @return \Illuminate\Database\Eloquent\Builder 157 | */ 158 | protected function getBaseOrphanedQuery() 159 | { 160 | $table = $this->abilitiesTable(); 161 | 162 | return Models::ability() 163 | ->whereNotNull("{$table}.entity_id") 164 | ->where("{$table}.entity_type", '!=', '*'); 165 | } 166 | 167 | /** 168 | * Get the name of the abilities table. 169 | * 170 | * @return string 171 | */ 172 | protected function abilitiesTable() 173 | { 174 | return Models::ability()->getTable(); 175 | } 176 | 177 | /** 178 | * Get an instance of the model for the given entity type. 179 | * 180 | * @param string $entityType 181 | * @return string 182 | */ 183 | protected function makeModel($entityType) 184 | { 185 | $class = Relation::getMorphedModel($entityType) ?? $entityType; 186 | 187 | return new $class; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/Constraints/Builder.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | protected $constraints; 16 | 17 | /** 18 | * Constructor. 19 | */ 20 | public function __construct() 21 | { 22 | $this->constraints = new Collection; 23 | } 24 | 25 | /** 26 | * Create a new builder instance. 27 | * 28 | * @return static 29 | */ 30 | public static function make() 31 | { 32 | return new static; 33 | } 34 | 35 | /** 36 | * Add a "where" constraint. 37 | * 38 | * @param string|\Closure $column 39 | * @param mixed $operator 40 | * @param mixed $value 41 | * @return $this 42 | */ 43 | public function where($column, $operator = null, $value = null) 44 | { 45 | if ($column instanceof Closure) { 46 | return $this->whereNested('and', $column); 47 | } 48 | 49 | return $this->addConstraint(Constraint::where(...func_get_args())); 50 | } 51 | 52 | /** 53 | * Add an "or where" constraint. 54 | * 55 | * @param string|\Closure $column 56 | * @param mixed $operator 57 | * @param mixed $value 58 | * @return $this 59 | */ 60 | public function orWhere($column, $operator = null, $value = null) 61 | { 62 | if ($column instanceof Closure) { 63 | return $this->whereNested('or', $column); 64 | } 65 | 66 | return $this->addConstraint(Constraint::orWhere(...func_get_args())); 67 | } 68 | 69 | /** 70 | * Add a "where column" constraint. 71 | * 72 | * @param string $a 73 | * @param mixed $operator 74 | * @param mixed $b 75 | * @return $this 76 | */ 77 | public function whereColumn($a, $operator, $b = null) 78 | { 79 | return $this->addConstraint(Constraint::whereColumn(...func_get_args())); 80 | } 81 | 82 | /** 83 | * Add an "or where column" constraint. 84 | * 85 | * @param string $a 86 | * @param mixed $operator 87 | * @param mixed $b 88 | * @return $this 89 | */ 90 | public function orWhereColumn($a, $operator, $b = null) 91 | { 92 | return $this->addConstraint(Constraint::orWhereColumn(...func_get_args())); 93 | } 94 | 95 | /** 96 | * Build the compiled list of constraints. 97 | * 98 | * @return \Silber\Bouncer\Constraints\Constrainer 99 | */ 100 | public function build() 101 | { 102 | if ($this->constraints->count() == 1) { 103 | return $this->constraints->first(); 104 | } 105 | 106 | return new Group($this->constraints); 107 | } 108 | 109 | /** 110 | * Add a nested "where" clause. 111 | * 112 | * @param string $logicalOperator 'and'|'or' 113 | * @return $this 114 | */ 115 | protected function whereNested($logicalOperator, Closure $callback) 116 | { 117 | $callback($builder = new static); 118 | 119 | $constraint = $builder->build()->logicalOperator($logicalOperator); 120 | 121 | return $this->addConstraint($constraint); 122 | } 123 | 124 | /** 125 | * Add a constraint to the list of constraints. 126 | * 127 | * @param \Silber\Bouncer\Constraints\Constrainer $constraint 128 | * @return $this 129 | */ 130 | protected function addConstraint($constraint) 131 | { 132 | $this->constraints[] = $constraint; 133 | 134 | return $this; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Constraints/ColumnConstraint.php: -------------------------------------------------------------------------------- 1 | a = $a; 33 | $this->operator = $operator; 34 | $this->b = $b; 35 | } 36 | 37 | /** 38 | * Determine whether the given entity/authority passes the constraint. 39 | * 40 | * @return bool 41 | */ 42 | public function check(Model $entity, ?Model $authority = null) 43 | { 44 | if (is_null($authority)) { 45 | return false; 46 | } 47 | 48 | return $this->compare($entity->{$this->a}, $authority->{$this->b}); 49 | } 50 | 51 | /** 52 | * Create a new instance from the raw data. 53 | * 54 | * @return static 55 | */ 56 | public static function fromData(array $data) 57 | { 58 | $constraint = new static( 59 | $data['a'], 60 | $data['operator'], 61 | $data['b'] 62 | ); 63 | 64 | return $constraint->logicalOperator($data['logicalOperator']); 65 | } 66 | 67 | /** 68 | * Get the JSON-able data of this object. 69 | * 70 | * @return array 71 | */ 72 | public function data() 73 | { 74 | return [ 75 | 'class' => static::class, 76 | 'params' => [ 77 | 'a' => $this->a, 78 | 'operator' => $this->operator, 79 | 'b' => $this->b, 80 | 'logicalOperator' => $this->logicalOperator, 81 | ], 82 | ]; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Constraints/Constrainer.php: -------------------------------------------------------------------------------- 1 | logicalOperator('or'); 61 | } 62 | 63 | /** 64 | * Create a new constraint for where a column matches the given column on the authority. 65 | * 66 | * @param string $a 67 | * @param mixed $operator 68 | * @param mixed $b 69 | * @return \Silber\Bouncer\Constraints\ColumnConstraint 70 | */ 71 | public static function whereColumn($a, $operator, $b = null) 72 | { 73 | [$operator, $b] = static::prepareOperatorAndValue( 74 | $operator, $b, func_num_args() === 2 75 | ); 76 | 77 | return new ColumnConstraint($a, $operator, $b); 78 | } 79 | 80 | /** 81 | * Create a new constraint for where a column matches the given column on the authority, 82 | * with the logical operator set to "or". 83 | * 84 | * @param string $a 85 | * @param mixed $operator 86 | * @param mixed $b 87 | * @return \Silber\Bouncer\Constraints\ColumnConstraint 88 | */ 89 | public static function orWhereColumn($a, $operator, $b = null) 90 | { 91 | return static::whereColumn(...func_get_args())->logicalOperator('or'); 92 | } 93 | 94 | /** 95 | * Set the logical operator to use when checked after a previous constraint. 96 | * 97 | * @param string|null $operator 98 | * @return $this|string 99 | */ 100 | public function logicalOperator($operator = null) 101 | { 102 | if (is_null($operator)) { 103 | return $this->logicalOperator; 104 | } 105 | 106 | Helpers::ensureValidLogicalOperator($operator); 107 | 108 | $this->logicalOperator = $operator; 109 | 110 | return $this; 111 | } 112 | 113 | /** 114 | * Checks whether the logical operator is an "and" operator. 115 | * 116 | * @param string $operator 117 | */ 118 | public function isAnd() 119 | { 120 | return $this->logicalOperator == 'and'; 121 | } 122 | 123 | /** 124 | * Checks whether the logical operator is an "and" operator. 125 | * 126 | * @param string $operator 127 | */ 128 | public function isOr() 129 | { 130 | return $this->logicalOperator == 'or'; 131 | } 132 | 133 | /** 134 | * Determine whether the given constrainer is equal to this object. 135 | * 136 | * @return bool 137 | */ 138 | public function equals(Constrainer $constrainer) 139 | { 140 | if (! $constrainer instanceof static) { 141 | return false; 142 | } 143 | 144 | return $this->data()['params'] == $constrainer->data()['params']; 145 | } 146 | 147 | /** 148 | * Prepare the value and operator. 149 | * 150 | * @param string $operator 151 | * @param string $value 152 | * @param bool $usesDefault 153 | * @return array 154 | * 155 | * @throws \InvalidArgumentException 156 | */ 157 | protected static function prepareOperatorAndValue($operator, $value, $usesDefault) 158 | { 159 | if ($usesDefault) { 160 | return ['=', $operator]; 161 | } 162 | 163 | if (! in_array($operator, ['=', '==', '!=', '<', '>', '<=', '>='])) { 164 | throw new InvalidArgumentException("{$operator} is not a valid operator"); 165 | } 166 | 167 | return [$operator, $value]; 168 | } 169 | 170 | /** 171 | * Compare the two values by the constraint's operator. 172 | * 173 | * @param mixed $a 174 | * @param mixed $b 175 | * @return bool 176 | */ 177 | protected function compare($a, $b) 178 | { 179 | switch ($this->operator) { 180 | case '=': 181 | case '==': return $a == $b; 182 | case '!=': return $a != $b; 183 | case '<': return $a < $b; 184 | case '>': return $a > $b; 185 | case '<=': return $a <= $b; 186 | case '>=': return $a >= $b; 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/Constraints/Group.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | protected $constraints; 17 | 18 | /** 19 | * The logical operator to use when checked after a previous constrainer. 20 | * 21 | * @var string 22 | */ 23 | protected $logicalOperator = 'and'; 24 | 25 | /** 26 | * Constructor. 27 | * 28 | * @param iterable<\Silber\Bouncer\Constraints\Constrainer> $constraints 29 | */ 30 | public function __construct($constraints = []) 31 | { 32 | $this->constraints = new Collection($constraints); 33 | } 34 | 35 | /** 36 | * Create a new "and" group. 37 | * 38 | * @return static 39 | */ 40 | public static function withAnd() 41 | { 42 | return new static; 43 | } 44 | 45 | /** 46 | * Create a new "and" group. 47 | * 48 | * @return static 49 | */ 50 | public static function withOr() 51 | { 52 | return (new static)->logicalOperator('or'); 53 | } 54 | 55 | /** 56 | * Create a new instance from the raw data. 57 | * 58 | * @return static 59 | */ 60 | public static function fromData(array $data) 61 | { 62 | $group = new static(array_map(function ($data) { 63 | return $data['class']::fromData($data['params']); 64 | }, $data['constraints'])); 65 | 66 | return $group->logicalOperator($data['logicalOperator']); 67 | } 68 | 69 | /** 70 | * Add the given constraint to the list of constraints. 71 | */ 72 | public function add(Constrainer $constraint) 73 | { 74 | $this->constraints->push($constraint); 75 | 76 | return $this; 77 | } 78 | 79 | /** 80 | * Determine whether the given entity/authority passes this constraint. 81 | * 82 | * @return bool 83 | */ 84 | public function check(Model $entity, ?Model $authority = null) 85 | { 86 | if ($this->constraints->isEmpty()) { 87 | return true; 88 | } 89 | 90 | return $this->constraints->reduce(function ($result, $constraint) use ($entity, $authority) { 91 | $passes = $constraint->check($entity, $authority); 92 | 93 | if (is_null($result)) { 94 | return $passes; 95 | } 96 | 97 | return $constraint->isOr() ? ($result || $passes) : ($result && $passes); 98 | }); 99 | } 100 | 101 | /** 102 | * Set the logical operator to use when checked after a previous constrainer. 103 | * 104 | * @param string|null $operator 105 | * @return $this|string 106 | */ 107 | public function logicalOperator($operator = null) 108 | { 109 | if (is_null($operator)) { 110 | return $this->logicalOperator; 111 | } 112 | 113 | Helpers::ensureValidLogicalOperator($operator); 114 | 115 | $this->logicalOperator = $operator; 116 | 117 | return $this; 118 | } 119 | 120 | /** 121 | * Checks whether the logical operator is an "and" operator. 122 | * 123 | * @param string $operator 124 | */ 125 | public function isAnd() 126 | { 127 | return $this->logicalOperator == 'and'; 128 | } 129 | 130 | /** 131 | * Checks whether the logical operator is an "and" operator. 132 | * 133 | * @param string $operator 134 | */ 135 | public function isOr() 136 | { 137 | return $this->logicalOperator == 'or'; 138 | } 139 | 140 | /** 141 | * Determine whether the given constrainer is equal to this object. 142 | * 143 | * @return bool 144 | */ 145 | public function equals(Constrainer $constrainer) 146 | { 147 | if (! $constrainer instanceof static) { 148 | return false; 149 | } 150 | 151 | if ($this->constraints->count() != $constrainer->constraints->count()) { 152 | return false; 153 | } 154 | 155 | foreach ($this->constraints as $index => $constraint) { 156 | if (! $constrainer->constraints[$index]->equals($constraint)) { 157 | return false; 158 | } 159 | } 160 | 161 | return true; 162 | } 163 | 164 | /** 165 | * Get the JSON-able data of this object. 166 | * 167 | * @return array 168 | */ 169 | public function data() 170 | { 171 | return [ 172 | 'class' => static::class, 173 | 'params' => [ 174 | 'logicalOperator' => $this->logicalOperator, 175 | 'constraints' => $this->constraints->map(function ($constraint) { 176 | return $constraint->data(); 177 | })->all(), 178 | ], 179 | ]; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/Constraints/ValueConstraint.php: -------------------------------------------------------------------------------- 1 | column = $column; 33 | $this->operator = $operator; 34 | $this->value = $value; 35 | } 36 | 37 | /** 38 | * Determine whether the given entity/authority passed this constraint. 39 | * 40 | * @return bool 41 | */ 42 | public function check(Model $entity, ?Model $authority = null) 43 | { 44 | return $this->compare($entity->{$this->column}, $this->value); 45 | } 46 | 47 | /** 48 | * Create a new instance from the raw data. 49 | * 50 | * @return static 51 | */ 52 | public static function fromData(array $data) 53 | { 54 | $constraint = new static( 55 | $data['column'], 56 | $data['operator'], 57 | $data['value'] 58 | ); 59 | 60 | return $constraint->logicalOperator($data['logicalOperator']); 61 | } 62 | 63 | /** 64 | * Get the JSON-able data of this object. 65 | * 66 | * @return array 67 | */ 68 | public function data() 69 | { 70 | return [ 71 | 'class' => static::class, 72 | 'params' => [ 73 | 'column' => $this->column, 74 | 'operator' => $this->operator, 75 | 'value' => $this->value, 76 | 'logicalOperator' => $this->logicalOperator, 77 | ], 78 | ]; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Contracts/CachedClipboard.php: -------------------------------------------------------------------------------- 1 | 'int', 25 | 'entity_id' => 'int', 26 | 'only_owned' => 'boolean', 27 | ]; 28 | 29 | /** 30 | * Constructor. 31 | */ 32 | public function __construct(array $attributes = []) 33 | { 34 | $this->table = Models::table('abilities'); 35 | 36 | parent::__construct($attributes); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Database/Concerns/Authorizable.php: -------------------------------------------------------------------------------- 1 | make(Clipboard::class) 21 | ->check($this, $ability, $model); 22 | } 23 | 24 | /** 25 | * Determine if the authority does not have a given ability. 26 | * 27 | * @param string $ability 28 | * @param \Illuminate\Database\Eloquent\Model|null $model 29 | * @return bool 30 | */ 31 | public function cant($ability, $model = null) 32 | { 33 | return ! $this->can($ability, $model); 34 | } 35 | 36 | /** 37 | * Determine if the authority does not have a given ability. 38 | * 39 | * @param string $ability 40 | * @param \Illuminate\Database\Eloquent\Model|null $model 41 | * @return bool 42 | */ 43 | public function cannot($ability, $model = null) 44 | { 45 | return $this->cant($ability, $model); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Database/Concerns/HasAbilities.php: -------------------------------------------------------------------------------- 1 | abilities()->detach(); 28 | } 29 | }); 30 | } 31 | 32 | /** 33 | * The abilities relationship. 34 | */ 35 | public function abilities(): MorphToMany 36 | { 37 | $relation = $this->morphToMany( 38 | Models::classname(Ability::class), 39 | 'entity', 40 | Models::table('permissions') 41 | )->withPivot('forbidden', 'scope'); 42 | 43 | return Models::scope()->applyToRelation($relation); 44 | } 45 | 46 | /** 47 | * Get all of the model's allowed abilities. 48 | * 49 | * @return \Illuminate\Database\Eloquent\Collection 50 | */ 51 | public function getAbilities() 52 | { 53 | return Container::getInstance() 54 | ->make(Clipboard::class) 55 | ->getAbilities($this); 56 | } 57 | 58 | /** 59 | * Get all of the model's allowed abilities. 60 | * 61 | * @return \Illuminate\Database\Eloquent\Collection 62 | */ 63 | public function getForbiddenAbilities() 64 | { 65 | return Container::getInstance() 66 | ->make(Clipboard::class) 67 | ->getAbilities($this, false); 68 | } 69 | 70 | /** 71 | * Give an ability to the model. 72 | * 73 | * @param mixed $ability 74 | * @param mixed|null $model 75 | * @return \Silber\Bouncer\Conductors\GivesAbilities|$this 76 | */ 77 | public function allow($ability = null, $model = null) 78 | { 79 | if (is_null($ability)) { 80 | return new GivesAbilities($this); 81 | } 82 | 83 | (new GivesAbilities($this))->to($ability, $model); 84 | 85 | return $this; 86 | } 87 | 88 | /** 89 | * Remove an ability from the model. 90 | * 91 | * @param mixed $ability 92 | * @param mixed|null $model 93 | * @return \Silber\Bouncer\Conductors\RemovesAbilities|$this 94 | */ 95 | public function disallow($ability = null, $model = null) 96 | { 97 | if (is_null($ability)) { 98 | return new RemovesAbilities($this); 99 | } 100 | 101 | (new RemovesAbilities($this))->to($ability, $model); 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * Forbid an ability to the model. 108 | * 109 | * @param mixed $ability 110 | * @param mixed|null $model 111 | * @return \Silber\Bouncer\Conductors\ForbidsAbilities|$this 112 | */ 113 | public function forbid($ability = null, $model = null) 114 | { 115 | if (is_null($ability)) { 116 | return new ForbidsAbilities($this); 117 | } 118 | 119 | (new ForbidsAbilities($this))->to($ability, $model); 120 | 121 | return $this; 122 | } 123 | 124 | /** 125 | * Remove ability forbiddal from the model. 126 | * 127 | * @param mixed $ability 128 | * @param mixed|null $model 129 | * @return \Silber\Bouncer\Conductors\UnforbidsAbilities|$this 130 | */ 131 | public function unforbid($ability = null, $model = null) 132 | { 133 | if (is_null($ability)) { 134 | return new UnforbidsAbilities($this); 135 | } 136 | 137 | (new UnforbidsAbilities($this))->to($ability, $model); 138 | 139 | return $this; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Database/Concerns/HasRoles.php: -------------------------------------------------------------------------------- 1 | roles()->detach(); 27 | } 28 | }); 29 | } 30 | 31 | /** 32 | * The roles relationship. 33 | */ 34 | public function roles(): MorphToMany 35 | { 36 | $relation = $this->morphToMany( 37 | Models::classname(Role::class), 38 | 'entity', 39 | Models::table('assigned_roles') 40 | )->withPivot('scope'); 41 | 42 | return Models::scope()->applyToRelation($relation); 43 | } 44 | 45 | /** 46 | * Get all of the model's assigned roles. 47 | * 48 | * @return \Illuminate\Support\Collection 49 | */ 50 | public function getRoles() 51 | { 52 | return Container::getInstance() 53 | ->make(Clipboard::class) 54 | ->getRoles($this); 55 | } 56 | 57 | /** 58 | * Assign the given roles to the model. 59 | * 60 | * @param \Illuminate\Database\Eloquent\Model|string|array $roles 61 | * @return $this 62 | */ 63 | public function assign($roles) 64 | { 65 | (new AssignsRoles($roles))->to($this); 66 | 67 | return $this; 68 | } 69 | 70 | /** 71 | * Retract the given roles from the model. 72 | * 73 | * @param \Illuminate\Database\Eloquent\Model|string|array $roles 74 | * @return $this 75 | */ 76 | public function retract($roles) 77 | { 78 | (new RemovesRoles($roles))->from($this); 79 | 80 | return $this; 81 | } 82 | 83 | /** 84 | * Check if the model has any of the given roles. 85 | * 86 | * @param string ...$roles 87 | * @return bool 88 | */ 89 | public function isAn(...$roles) 90 | { 91 | return Container::getInstance() 92 | ->make(Clipboard::class) 93 | ->checkRole($this, $roles, 'or'); 94 | } 95 | 96 | /** 97 | * Check if the model has any of the given roles. 98 | * 99 | * Alias for the "isAn" method. 100 | * 101 | * @param string ...$roles 102 | * @return bool 103 | */ 104 | public function isA(...$roles) 105 | { 106 | return $this->isAn(...$roles); 107 | } 108 | 109 | /** 110 | * Check if the model has none of the given roles. 111 | * 112 | * @param string ...$roles 113 | * @return bool 114 | */ 115 | public function isNotAn(...$roles) 116 | { 117 | return Container::getInstance() 118 | ->make(Clipboard::class) 119 | ->checkRole($this, $roles, 'not'); 120 | } 121 | 122 | /** 123 | * Check if the model has none of the given roles. 124 | * 125 | * Alias for the "isNotAn" method. 126 | * 127 | * @param string ...$roles 128 | * @return bool 129 | */ 130 | public function isNotA(...$roles) 131 | { 132 | return $this->isNotAn(...$roles); 133 | } 134 | 135 | /** 136 | * Check if the model has all of the given roles. 137 | * 138 | * @param string ...$roles 139 | * @return bool 140 | */ 141 | public function isAll(...$roles) 142 | { 143 | return Container::getInstance() 144 | ->make(Clipboard::class) 145 | ->checkRole($this, $roles, 'and'); 146 | } 147 | 148 | /** 149 | * Constrain the given query by the provided role. 150 | * 151 | * @param \Illuminate\Database\Eloquent\Builder $query 152 | * @param string $role 153 | * @return void 154 | */ 155 | public function scopeWhereIs($query, $role) 156 | { 157 | (new RolesQuery)->constrainWhereIs(...func_get_args()); 158 | } 159 | 160 | /** 161 | * Constrain the given query by all provided roles. 162 | * 163 | * @param \Illuminate\Database\Eloquent\Builder $query 164 | * @param string $role 165 | * @return void 166 | */ 167 | public function scopeWhereIsAll($query, $role) 168 | { 169 | (new RolesQuery)->constrainWhereIsAll(...func_get_args()); 170 | } 171 | 172 | /** 173 | * Constrain the given query by the provided role. 174 | * 175 | * @param \Illuminate\Database\Eloquent\Builder $query 176 | * @param string $role 177 | * @return void 178 | */ 179 | public function scopeWhereIsNot($query, $role) 180 | { 181 | (new RolesQuery)->constrainWhereIsNot(...func_get_args()); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/Database/Concerns/IsAbility.php: -------------------------------------------------------------------------------- 1 | applyToModel($ability); 28 | 29 | if (is_null($ability->title)) { 30 | $ability->title = AbilityTitle::from($ability)->toString(); 31 | } 32 | }); 33 | } 34 | 35 | /** 36 | * Get the options attribute. 37 | * 38 | * @return array 39 | */ 40 | public function getOptionsAttribute() 41 | { 42 | if (empty($this->attributes['options'])) { 43 | return []; 44 | } 45 | 46 | return json_decode($this->attributes['options'], true); 47 | } 48 | 49 | /** 50 | * Set the "options" attribute. 51 | * 52 | * @return void 53 | */ 54 | public function setOptionsAttribute(array $options) 55 | { 56 | $this->attributes['options'] = json_encode($options); 57 | } 58 | 59 | /** 60 | * CHecks if the ability has constraints. 61 | * 62 | * @return bool 63 | */ 64 | public function hasConstraints() 65 | { 66 | return ! empty($this->options['constraints']); 67 | } 68 | 69 | /** 70 | * Get the ability's constraints. 71 | * 72 | * @return \Silber\Bouncer\Constraints\Constrainer 73 | */ 74 | public function getConstraints() 75 | { 76 | if (empty($this->options['constraints'])) { 77 | return new Group(); 78 | } 79 | 80 | $data = $this->options['constraints']; 81 | 82 | return $data['class']::fromData($data['params']); 83 | } 84 | 85 | /** 86 | * Set the ability's constraints. 87 | * 88 | * @return $this 89 | */ 90 | public function setConstraints(Constrainer $constrainer) 91 | { 92 | $this->options = array_merge($this->options, [ 93 | 'constraints' => $constrainer->data(), 94 | ]); 95 | 96 | return $this; 97 | } 98 | 99 | /** 100 | * Create a new ability for a specific model. 101 | * 102 | * @param \Illuminate\Database\Eloquent\Model|string $model 103 | * @param string|array $attributes 104 | * @return static 105 | */ 106 | public static function createForModel($model, $attributes) 107 | { 108 | $model = static::makeForModel($model, $attributes); 109 | 110 | $model->save(); 111 | 112 | return $model; 113 | } 114 | 115 | /** 116 | * Make a new ability for a specific model. 117 | * 118 | * @param \Illuminate\Database\Eloquent\Model|string $model 119 | * @param string|array $attributes 120 | * @return static 121 | */ 122 | public static function makeForModel($model, $attributes) 123 | { 124 | if (is_string($attributes)) { 125 | $attributes = ['name' => $attributes]; 126 | } 127 | 128 | if ($model === '*') { 129 | return (new static)->forceFill($attributes + [ 130 | 'entity_type' => '*', 131 | ]); 132 | } 133 | 134 | if (is_string($model)) { 135 | $model = new $model; 136 | } 137 | 138 | return (new static)->forceFill($attributes + [ 139 | 'entity_type' => $model->getMorphClass(), 140 | 'entity_id' => $model->exists ? $model->getKey() : null, 141 | ]); 142 | } 143 | 144 | /** 145 | * The roles relationship. 146 | */ 147 | public function roles(): MorphToMany 148 | { 149 | $relation = $this->morphedByMany( 150 | Models::classname(Role::class), 151 | 'entity', 152 | Models::table('permissions') 153 | )->withPivot('forbidden', 'scope'); 154 | 155 | return Models::scope()->applyToRelation($relation); 156 | } 157 | 158 | /** 159 | * The users relationship. 160 | */ 161 | public function users(): MorphToMany 162 | { 163 | $relation = $this->morphedByMany( 164 | Models::classname(User::class), 165 | 'entity', 166 | Models::table('permissions') 167 | )->withPivot('forbidden', 'scope'); 168 | 169 | return Models::scope()->applyToRelation($relation); 170 | } 171 | 172 | /** 173 | * Get the identifier for this ability. 174 | * 175 | * @return string 176 | */ 177 | final public function getIdentifierAttribute() 178 | { 179 | $slug = $this->attributes['name']; 180 | 181 | if ($this->attributes['entity_type'] !== null) { 182 | $slug .= '-'.$this->attributes['entity_type']; 183 | } 184 | 185 | if ($this->attributes['entity_id'] !== null) { 186 | $slug .= '-'.$this->attributes['entity_id']; 187 | } 188 | 189 | if ($this->attributes['only_owned']) { 190 | $slug .= '-owned'; 191 | } 192 | 193 | return strtolower($slug); 194 | } 195 | 196 | /** 197 | * Get the ability's "slug" attribute. 198 | * 199 | * @return string 200 | */ 201 | public function getSlugAttribute() 202 | { 203 | return $this->getIdentifierAttribute(); 204 | } 205 | 206 | /** 207 | * Constrain a query to having the given name. 208 | * 209 | * @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder $query 210 | * @return string|array $name 211 | * @return bool $strict 212 | * @return void 213 | */ 214 | public function scopeByName($query, $name, $strict = false) 215 | { 216 | $names = (array) $name; 217 | 218 | if (! $strict && $name !== '*') { 219 | $names[] = '*'; 220 | } 221 | 222 | $query->whereIn("{$this->table}.name", $names); 223 | } 224 | 225 | /** 226 | * Constrain a query to simple abilities. 227 | * 228 | * @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder $query 229 | * @return void 230 | */ 231 | public function scopeSimpleAbility($query) 232 | { 233 | $query->whereNull("{$this->table}.entity_type"); 234 | } 235 | 236 | /** 237 | * Constrain a query to an ability for a specific model. 238 | * 239 | * @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder $query 240 | * @param \Illuminate\Database\Eloquent\Model|string $model 241 | * @param bool $strict 242 | * @return void 243 | */ 244 | public function scopeForModel($query, $model, $strict = false) 245 | { 246 | (new AbilitiesForModel)->constrain($query, $model, $strict); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/Database/Concerns/IsRole.php: -------------------------------------------------------------------------------- 1 | applyToModel($role); 31 | 32 | if (is_null($role->title)) { 33 | $role->title = RoleTitle::from($role)->toString(); 34 | } 35 | }); 36 | 37 | static::deleted(function ($role) { 38 | $role->abilities()->detach(); 39 | }); 40 | } 41 | 42 | /** 43 | * The users relationship. 44 | * 45 | * @return \Illuminate\Database\Eloquent\Relations\MorphedToMany 46 | */ 47 | public function users(): MorphToMany 48 | { 49 | $relation = $this->morphedByMany( 50 | Models::classname(User::class), 51 | 'entity', 52 | Models::table('assigned_roles') 53 | )->withPivot('scope'); 54 | 55 | return Models::scope()->applyToRelation($relation); 56 | } 57 | 58 | /** 59 | * Assign the role to the given model(s). 60 | * 61 | * @param string|\Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection $model 62 | * @return $this 63 | */ 64 | public function assignTo($model, ?array $keys = null) 65 | { 66 | [$model, $keys] = Helpers::extractModelAndKeys($model, $keys); 67 | 68 | $query = $this->newBaseQueryBuilder()->from(Models::table('assigned_roles')); 69 | 70 | $query->insert($this->createAssignRecords($model, $keys)); 71 | 72 | return $this; 73 | } 74 | 75 | /** 76 | * Find the given roles, creating the names that don't exist yet. 77 | * 78 | * @param iterable $roles 79 | * @return \Illuminate\Database\Eloquent\Collection 80 | */ 81 | public function findOrCreateRoles($roles) 82 | { 83 | $roles = Helpers::groupModelsAndIdentifiersByType($roles); 84 | 85 | $roles['integers'] = $this->find($roles['integers']); 86 | 87 | $roles['strings'] = $this->findOrCreateRolesByName($roles['strings']); 88 | 89 | return $this->newCollection(Arr::collapse($roles)); 90 | } 91 | 92 | /** 93 | * Find roles by name, creating the ones that don't exist. 94 | * 95 | * @param iterable $names 96 | * @return \Illuminate\Database\Eloquent\Collection 97 | */ 98 | protected function findOrCreateRolesByName($names) 99 | { 100 | if (empty($names)) { 101 | return []; 102 | } 103 | 104 | $existing = static::whereIn('name', $names)->get()->keyBy('name'); 105 | 106 | return (new Collection($names)) 107 | ->diff($existing->pluck('name')) 108 | ->map(function ($name) { 109 | return static::create(compact('name')); 110 | }) 111 | ->merge($existing); 112 | } 113 | 114 | /** 115 | * Get the IDs of the given roles. 116 | * 117 | * @param iterable $roles 118 | * @return array 119 | */ 120 | public function getRoleKeys($roles) 121 | { 122 | $roles = Helpers::groupModelsAndIdentifiersByType($roles); 123 | 124 | $roles['strings'] = $this->getKeysByName($roles['strings']); 125 | 126 | $roles['models'] = Arr::pluck($roles['models'], $this->getKeyName()); 127 | 128 | return Arr::collapse($roles); 129 | } 130 | 131 | /** 132 | * Get the names of the given roles. 133 | * 134 | * @param iterable $roles 135 | * @return array 136 | */ 137 | public function getRoleNames($roles) 138 | { 139 | $roles = Helpers::groupModelsAndIdentifiersByType($roles); 140 | 141 | $roles['integers'] = $this->getNamesByKey($roles['integers']); 142 | 143 | $roles['models'] = Arr::pluck($roles['models'], 'name'); 144 | 145 | return Arr::collapse($roles); 146 | } 147 | 148 | /** 149 | * Get the keys of the roles with the given names. 150 | * 151 | * @param iterable $names 152 | * @return array 153 | */ 154 | public function getKeysByName($names) 155 | { 156 | if (empty($names)) { 157 | return []; 158 | } 159 | 160 | return $this->whereIn('name', $names) 161 | ->select($this->getKeyName())->get() 162 | ->pluck($this->getKeyName())->all(); 163 | } 164 | 165 | /** 166 | * Get the names of the roles with the given IDs. 167 | * 168 | * @param iterable $keys 169 | * @return array 170 | */ 171 | public function getNamesByKey($keys) 172 | { 173 | if (empty($keys)) { 174 | return []; 175 | } 176 | 177 | return $this->whereIn($this->getKeyName(), $keys) 178 | ->select('name')->get() 179 | ->pluck('name')->all(); 180 | } 181 | 182 | /** 183 | * Retract the role from the given model(s). 184 | * 185 | * @param string|\Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection $model 186 | * @return $this 187 | */ 188 | public function retractFrom($model, ?array $keys = null) 189 | { 190 | [$model, $keys] = Helpers::extractModelAndKeys($model, $keys); 191 | 192 | $query = $this->newBaseQueryBuilder() 193 | ->from(Models::table('assigned_roles')) 194 | ->where('role_id', $this->getKey()) 195 | ->where('entity_type', $model->getMorphClass()) 196 | ->whereIn('entity_id', $keys); 197 | 198 | Models::scope()->applyToRelationQuery($query, $query->from); 199 | 200 | $query->delete(); 201 | 202 | return $this; 203 | } 204 | 205 | /** 206 | * Create the pivot table records for assigning the role to given models. 207 | * 208 | * @return array 209 | */ 210 | protected function createAssignRecords(Model $model, array $keys) 211 | { 212 | $type = $model->getMorphClass(); 213 | 214 | return array_map(function ($key) use ($type) { 215 | return Models::scope()->getAttachAttributes() + [ 216 | 'role_id' => $this->getKey(), 217 | 'entity_type' => $type, 218 | 'entity_id' => $key, 219 | ]; 220 | }, $keys); 221 | } 222 | 223 | /** 224 | * Constrain the given query to roles that were assigned to the given authorities. 225 | * 226 | * @param \Illuminate\Database\Eloquent\Builder $query 227 | * @param string|\Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection $model 228 | * @return void 229 | */ 230 | public function scopeWhereAssignedTo($query, $model, ?array $keys = null) 231 | { 232 | (new RolesQuery)->constrainWhereAssignedTo($query, $model, $keys); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/Database/HasRolesAndAbilities.php: -------------------------------------------------------------------------------- 1 | getTable(); 80 | } 81 | 82 | /** 83 | * Set custom table names. 84 | * 85 | * @return void 86 | */ 87 | public static function setTables(array $map) 88 | { 89 | static::$tables = array_merge(static::$tables, $map); 90 | 91 | static::updateMorphMap(); 92 | } 93 | 94 | /** 95 | * Get a custom table name mapping for the given table. 96 | * 97 | * @param string $table 98 | * @return string 99 | */ 100 | public static function table($table) 101 | { 102 | if (isset(static::$tables[$table])) { 103 | return static::$tables[$table]; 104 | } 105 | 106 | return $table; 107 | } 108 | 109 | /** 110 | * Get or set the model scoping instance. 111 | * 112 | * @return mixed 113 | */ 114 | public static function scope(?ScopeContract $scope = null) 115 | { 116 | if (! is_null($scope)) { 117 | return static::$scope = $scope; 118 | } 119 | 120 | if (is_null(static::$scope)) { 121 | static::$scope = new Scope; 122 | } 123 | 124 | return static::$scope; 125 | } 126 | 127 | /** 128 | * Get the classname mapping for the given model. 129 | * 130 | * @param string $model 131 | * @return string 132 | */ 133 | public static function classname($model) 134 | { 135 | if (isset(static::$models[$model])) { 136 | return static::$models[$model]; 137 | } 138 | 139 | return $model; 140 | } 141 | 142 | /** 143 | * Update Eloquent's morph map with the Bouncer models and tables. 144 | * 145 | * @param array|null $classNames 146 | * @return void 147 | */ 148 | public static function updateMorphMap($classNames = null) 149 | { 150 | if (is_null($classNames)) { 151 | $classNames = [ 152 | static::classname(Role::class), 153 | static::classname(Ability::class), 154 | ]; 155 | } 156 | 157 | Relation::morphMap($classNames); 158 | } 159 | 160 | /** 161 | * Register an attribute/callback to determine if a model is owned by a given authority. 162 | * 163 | * @param string|\Closure $model 164 | * @param string|\Closure|null $attribute 165 | * @return void 166 | */ 167 | public static function ownedVia($model, $attribute = null) 168 | { 169 | if (is_null($attribute)) { 170 | static::$ownership['*'] = $model; 171 | } 172 | 173 | static::$ownership[$model] = $attribute; 174 | } 175 | 176 | /** 177 | * Determines whether the given model is owned by the given authority. 178 | * 179 | * @return bool 180 | */ 181 | public static function isOwnedBy(Model $authority, Model $model) 182 | { 183 | $type = get_class($model); 184 | 185 | if (isset(static::$ownership[$type])) { 186 | $attribute = static::$ownership[$type]; 187 | } elseif (isset(static::$ownership['*'])) { 188 | $attribute = static::$ownership['*']; 189 | } else { 190 | $attribute = strtolower(static::basename($authority)).'_id'; 191 | } 192 | 193 | return static::isOwnedVia($attribute, $authority, $model); 194 | } 195 | 196 | /** 197 | * Determines ownership via the given attribute. 198 | * 199 | * @param string|\Closure $attribute 200 | * @return bool 201 | */ 202 | protected static function isOwnedVia($attribute, Model $authority, Model $model) 203 | { 204 | if ($attribute instanceof Closure) { 205 | return $attribute($model, $authority); 206 | } 207 | 208 | return $authority->getKey() == $model->{$attribute}; 209 | } 210 | 211 | /** 212 | * Get an instance of the ability model. 213 | * 214 | * @return \Silber\Bouncer\Database\Ability 215 | */ 216 | public static function ability(array $attributes = []) 217 | { 218 | return static::make(Ability::class, $attributes); 219 | } 220 | 221 | /** 222 | * Get an instance of the role model. 223 | * 224 | * @return \Silber\Bouncer\Database\Role 225 | */ 226 | public static function role(array $attributes = []) 227 | { 228 | return static::make(Role::class, $attributes); 229 | } 230 | 231 | /** 232 | * Get an instance of the user model. 233 | * 234 | * @return \Illuminate\Database\Eloquent\Model 235 | */ 236 | public static function user(array $attributes = []) 237 | { 238 | return static::make(User::class, $attributes); 239 | } 240 | 241 | /** 242 | * Get a new query builder instance. 243 | * 244 | * @param string $table 245 | * @return \Illuminate\Database\Query\Builder 246 | */ 247 | public static function query($table) 248 | { 249 | $query = new Builder( 250 | $connection = static::user()->getConnection(), 251 | $connection->getQueryGrammar(), 252 | $connection->getPostProcessor() 253 | ); 254 | 255 | return $query->from(static::table($table)); 256 | } 257 | 258 | /** 259 | * Reset all settings to their original state. 260 | * 261 | * @return void 262 | */ 263 | public static function reset() 264 | { 265 | static::$models = static::$tables = static::$ownership = []; 266 | } 267 | 268 | /** 269 | * Get an instance of the given model. 270 | * 271 | * @param string $model 272 | * @return \Illuminate\Database\Eloquent\Model 273 | */ 274 | protected static function make($model, array $attributes = []) 275 | { 276 | $model = static::classname($model); 277 | 278 | return new $model($attributes); 279 | } 280 | 281 | /** 282 | * Get the basename of the given class. 283 | * 284 | * @param string|object $class 285 | * @return string 286 | */ 287 | protected static function basename($class) 288 | { 289 | if (! is_string($class)) { 290 | $class = get_class($class); 291 | } 292 | 293 | $segments = explode('\\', $class); 294 | 295 | return end($segments); 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /src/Database/Queries/Abilities.php: -------------------------------------------------------------------------------- 1 | where(function ($query) use ($authority, $allowed) { 19 | $query->whereExists(static::getRoleConstraint($authority, $allowed)); 20 | $query->orWhereExists(static::getAuthorityConstraint($authority, $allowed)); 21 | $query->orWhereExists(static::getEveryoneConstraint($allowed)); 22 | }); 23 | } 24 | 25 | /** 26 | * Get a query for the authority's forbidden abilities. 27 | * 28 | * @return \Illuminate\Database\Eloquent\Builder 29 | */ 30 | public static function forbiddenForAuthority(Model $authority) 31 | { 32 | return static::forAuthority($authority, false); 33 | } 34 | 35 | /** 36 | * Get a constraint for abilities that have been granted to the given authority through a role. 37 | * 38 | * @param bool $allowed 39 | * @return \Closure 40 | */ 41 | protected static function getRoleConstraint(Model $authority, $allowed) 42 | { 43 | return function ($query) use ($authority, $allowed) { 44 | $permissions = Models::table('permissions'); 45 | $abilities = Models::table('abilities'); 46 | $roles = Models::table('roles'); 47 | 48 | $query->from($roles) 49 | ->join($permissions, $roles.'.id', '=', $permissions.'.entity_id') 50 | ->whereColumn("{$permissions}.ability_id", "{$abilities}.id") 51 | ->where($permissions.'.forbidden', ! $allowed) 52 | ->where($permissions.'.entity_type', Models::role()->getMorphClass()); 53 | 54 | Models::scope()->applyToModelQuery($query, $roles); 55 | Models::scope()->applyToRelationQuery($query, $permissions); 56 | 57 | $query->where(function ($query) use ($authority) { 58 | $query->whereExists(static::getAuthorityRoleConstraint($authority)); 59 | }); 60 | }; 61 | } 62 | 63 | /** 64 | * Get a constraint for roles that are assigned to the given authority. 65 | * 66 | * @return \Closure 67 | */ 68 | protected static function getAuthorityRoleConstraint(Model $authority) 69 | { 70 | return function ($query) use ($authority) { 71 | $pivot = Models::table('assigned_roles'); 72 | $roles = Models::table('roles'); 73 | $table = $authority->getTable(); 74 | 75 | $query->from($table) 76 | ->join($pivot, "{$table}.{$authority->getKeyName()}", '=', $pivot.'.entity_id') 77 | ->whereColumn("{$pivot}.role_id", "{$roles}.id") 78 | ->where($pivot.'.entity_type', $authority->getMorphClass()) 79 | ->where("{$table}.{$authority->getKeyName()}", $authority->getKey()); 80 | 81 | Models::scope()->applyToModelQuery($query, $roles); 82 | Models::scope()->applyToRelationQuery($query, $pivot); 83 | }; 84 | } 85 | 86 | /** 87 | * Get a constraint for abilities that have been granted to the given authority. 88 | * 89 | * @param bool $allowed 90 | * @return \Closure 91 | */ 92 | protected static function getAuthorityConstraint(Model $authority, $allowed) 93 | { 94 | return function ($query) use ($authority, $allowed) { 95 | $permissions = Models::table('permissions'); 96 | $abilities = Models::table('abilities'); 97 | $table = $authority->getTable(); 98 | 99 | $query->from($table) 100 | ->join($permissions, "{$table}.{$authority->getKeyName()}", '=', $permissions.'.entity_id') 101 | ->whereColumn("{$permissions}.ability_id", "{$abilities}.id") 102 | ->where("{$permissions}.forbidden", ! $allowed) 103 | ->where("{$permissions}.entity_type", $authority->getMorphClass()) 104 | ->where("{$table}.{$authority->getKeyName()}", $authority->getKey()); 105 | 106 | Models::scope()->applyToModelQuery($query, $abilities); 107 | Models::scope()->applyToRelationQuery($query, $permissions); 108 | }; 109 | } 110 | 111 | /** 112 | * Get a constraint for abilities that have been granted to everyone. 113 | * 114 | * @param bool $allowed 115 | * @return \Closure 116 | */ 117 | protected static function getEveryoneConstraint($allowed) 118 | { 119 | return function ($query) use ($allowed) { 120 | $permissions = Models::table('permissions'); 121 | $abilities = Models::table('abilities'); 122 | 123 | $query->from($permissions) 124 | ->whereColumn("{$permissions}.ability_id", "{$abilities}.id") 125 | ->where("{$permissions}.forbidden", ! $allowed) 126 | ->whereNull('entity_id'); 127 | 128 | Models::scope()->applyToRelationQuery($query, $permissions); 129 | }; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Database/Queries/AbilitiesForModel.php: -------------------------------------------------------------------------------- 1 | table = Models::table('abilities'); 23 | } 24 | 25 | /** 26 | * Constrain a query to an ability for a specific model or wildcard. 27 | * 28 | * @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder $query 29 | * @param \Illuminate\Database\Eloquent\Model|string $model 30 | * @param bool $strict 31 | * @return void 32 | */ 33 | public function constrain($query, $model, $strict = false) 34 | { 35 | if ($model === '*') { 36 | return $this->constrainByWildcard($query); 37 | } 38 | 39 | $model = is_string($model) ? new $model : $model; 40 | 41 | $this->constrainByModel($query, $model, $strict); 42 | } 43 | 44 | /** 45 | * Constrain a query to a model wiildcard. 46 | * 47 | * @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder $query 48 | * @return void 49 | */ 50 | protected function constrainByWildcard($query) 51 | { 52 | $query->where("{$this->table}.entity_type", '*'); 53 | } 54 | 55 | /** 56 | * Constrain a query to an ability for a specific model. 57 | * 58 | * @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder $query 59 | * @param bool $strict 60 | * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder 61 | */ 62 | protected function constrainByModel($query, Model $model, $strict) 63 | { 64 | if ($strict) { 65 | return $query->where( 66 | $this->modelAbilityConstraint($model, $strict) 67 | ); 68 | } 69 | 70 | return $query->where(function ($query) use ($model, $strict) { 71 | $query->where("{$this->table}.entity_type", '*') 72 | ->orWhere($this->modelAbilityConstraint($model, $strict)); 73 | }); 74 | } 75 | 76 | /** 77 | * Get the constraint for regular model abilities. 78 | * 79 | * @param bool $strict 80 | * @return \Closure 81 | */ 82 | protected function modelAbilityConstraint(Model $model, $strict) 83 | { 84 | return function ($query) use ($model, $strict) { 85 | $query->where("{$this->table}.entity_type", $model->getMorphClass()); 86 | 87 | $query->where($this->abilitySubqueryConstraint($model, $strict)); 88 | }; 89 | } 90 | 91 | /** 92 | * Get the constraint for the ability subquery. 93 | * 94 | * @param bool $strict 95 | * @return \Closure 96 | */ 97 | protected function abilitySubqueryConstraint(Model $model, $strict) 98 | { 99 | return function ($query) use ($model, $strict) { 100 | // If the model does not exist, we want to search for blanket abilities 101 | // that cover all instances of this model. If it does exist, we only 102 | // want to find blanket abilities if we're not using strict mode. 103 | if (! $model->exists || ! $strict) { 104 | $query->whereNull("{$this->table}.entity_id"); 105 | } 106 | 107 | if ($model->exists) { 108 | $query->orWhere("{$this->table}.entity_id", $model->getKey()); 109 | } 110 | }; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Database/Queries/Roles.php: -------------------------------------------------------------------------------- 1 | whereHas('roles', function ($query) use ($roles) { 20 | $query->whereIn('name', $roles); 21 | }); 22 | } 23 | 24 | /** 25 | * Constrain the given query by all provided roles. 26 | * 27 | * @param \Illuminate\Database\Eloquent\Builder $query 28 | * @param string ...$roles 29 | * @return \Illuminate\Database\Eloquent\Builder 30 | */ 31 | public function constrainWhereIsAll($query, ...$roles) 32 | { 33 | return $query->whereHas('roles', function ($query) use ($roles) { 34 | $query->whereIn('name', $roles); 35 | }, '=', count($roles)); 36 | } 37 | 38 | /** 39 | * Constrain the given query by the provided role. 40 | * 41 | * @param \Illuminate\Database\Eloquent\Builder $query 42 | * @param string ...$roles 43 | * @return \Illuminate\Database\Eloquent\Builder 44 | */ 45 | public function constrainWhereIsNot($query, ...$roles) 46 | { 47 | return $query->whereDoesntHave('roles', function ($query) use ($roles) { 48 | $query->whereIn('name', $roles); 49 | }); 50 | } 51 | 52 | /** 53 | * Constrain the given roles query to those that were assigned to the given authorities. 54 | * 55 | * @param \Illuminate\Database\Eloquent\Builder $query 56 | * @param string|\Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection $model 57 | * @return void 58 | */ 59 | public function constrainWhereAssignedTo($query, $model, ?array $keys = null) 60 | { 61 | [$model, $keys] = Helpers::extractModelAndKeys($model, $keys); 62 | 63 | $query->whereExists(function ($query) use ($model, $keys) { 64 | $table = $model->getTable(); 65 | $key = "{$table}.{$model->getKeyName()}"; 66 | $pivot = Models::table('assigned_roles'); 67 | $roles = Models::table('roles'); 68 | 69 | $query->from($table) 70 | ->join($pivot, $key, '=', $pivot.'.entity_id') 71 | ->whereColumn("{$pivot}.role_id", "{$roles}.id") 72 | ->where("{$pivot}.entity_type", $model->getMorphClass()) 73 | ->whereIn($key, $keys); 74 | 75 | Models::scope()->applyToModelQuery($query, $roles); 76 | Models::scope()->applyToRelationQuery($query, $pivot); 77 | }); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Database/Role.php: -------------------------------------------------------------------------------- 1 | 'int', 25 | ]; 26 | 27 | /** 28 | * Constructor. 29 | */ 30 | public function __construct(array $attributes = []) 31 | { 32 | $this->table = Models::table('roles'); 33 | 34 | parent::__construct($attributes); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Database/Scope/Scope.php: -------------------------------------------------------------------------------- 1 | scope = $id; 45 | 46 | return $this; 47 | } 48 | 49 | /** 50 | * Only scope relationships. Models should stay global. 51 | * 52 | * @param bool $boolean 53 | * @return $this 54 | */ 55 | public function onlyRelations($boolean = true) 56 | { 57 | $this->onlyScopeRelations = $boolean; 58 | 59 | return $this; 60 | } 61 | 62 | /** 63 | * Don't scope abilities granted to roles. 64 | * 65 | * The role <=> ability associations will be global. 66 | * 67 | * @return $this 68 | */ 69 | public function dontScopeRoleAbilities() 70 | { 71 | $this->scopeRoleAbilities = false; 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * Append the tenant ID to the given cache key. 78 | * 79 | * @param string $key 80 | * @return string 81 | */ 82 | public function appendToCacheKey($key) 83 | { 84 | return is_null($this->scope) ? $key : $key.'-'.$this->scope; 85 | } 86 | 87 | /** 88 | * Scope the given model to the current tenant. 89 | * 90 | * @return \Illuminate\Database\Eloquent\Model 91 | */ 92 | public function applyToModel(Model $model) 93 | { 94 | if (! $this->onlyScopeRelations && ! is_null($this->scope)) { 95 | $model->scope = $this->scope; 96 | } 97 | 98 | return $model; 99 | } 100 | 101 | /** 102 | * Scope the given model query to the current tenant. 103 | * 104 | * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $query 105 | * @param string|null $table 106 | * @return \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder 107 | */ 108 | public function applyToModelQuery($query, $table = null) 109 | { 110 | if ($this->onlyScopeRelations) { 111 | return $query; 112 | } 113 | 114 | if (is_null($table)) { 115 | $table = $query->getModel()->getTable(); 116 | } 117 | 118 | return $this->applyToQuery($query, $table); 119 | } 120 | 121 | /** 122 | * Scope the given relationship query to the current tenant. 123 | * 124 | * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $query 125 | * @param string $table 126 | * @return \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder 127 | */ 128 | public function applyToRelationQuery($query, $table) 129 | { 130 | return $this->applyToQuery($query, $table); 131 | } 132 | 133 | /** 134 | * Scope the given relation to the current tenant. 135 | * 136 | * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany 137 | */ 138 | public function applyToRelation(BelongsToMany $relation) 139 | { 140 | $this->applyToRelationQuery( 141 | $relation->getQuery(), 142 | $relation->getTable() 143 | ); 144 | 145 | return $relation; 146 | } 147 | 148 | /** 149 | * Apply the current scope to the given query. 150 | * 151 | * This internal method does not check whether 152 | * the given query needs to be scoped. That 153 | * is fully the caller's responsibility. 154 | * 155 | * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $query 156 | * @param string $table 157 | * @return \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder 158 | */ 159 | protected function applyToQuery($query, $table) 160 | { 161 | return $query->where(function ($query) use ($table) { 162 | $query->whereNull("{$table}.scope"); 163 | 164 | if (! is_null($this->scope)) { 165 | $query->orWhere("{$table}.scope", $this->scope); 166 | } 167 | }); 168 | } 169 | 170 | /** 171 | * Get the current scope value. 172 | * 173 | * @return mixed 174 | */ 175 | public function get() 176 | { 177 | return $this->scope; 178 | } 179 | 180 | /** 181 | * Get the additional attributes for pivot table records. 182 | * 183 | * @param string|null $authority 184 | * @return array 185 | */ 186 | public function getAttachAttributes($authority = null) 187 | { 188 | if (is_null($this->scope)) { 189 | return []; 190 | } 191 | 192 | if (! $this->scopeRoleAbilities && $this->isRoleClass($authority)) { 193 | return []; 194 | } 195 | 196 | return ['scope' => $this->scope]; 197 | } 198 | 199 | /** 200 | * Run the given callback with the given temporary scope. 201 | * 202 | * @param mixed $scope 203 | * @return mixed 204 | */ 205 | public function onceTo($scope, callable $callback) 206 | { 207 | $mainScope = $this->scope; 208 | 209 | $this->scope = $scope; 210 | 211 | $result = $callback(); 212 | 213 | $this->scope = $mainScope; 214 | 215 | return $result; 216 | } 217 | 218 | /** 219 | * Remove the scope completely. 220 | * 221 | * @return $this 222 | */ 223 | public function remove() 224 | { 225 | $this->scope = null; 226 | } 227 | 228 | /** 229 | * Run the given callback without the scope applied. 230 | * 231 | * @return mixed 232 | */ 233 | public function removeOnce(callable $callback) 234 | { 235 | return $this->onceTo(null, $callback); 236 | } 237 | 238 | /** 239 | * Determine whether the given class name is the role model. 240 | * 241 | * @param string|null $className 242 | * @return bool 243 | */ 244 | protected function isRoleClass($className) 245 | { 246 | return Models::classname(Role::class) === $className; 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/Database/Scope/TenantScope.php: -------------------------------------------------------------------------------- 1 | applyToModelQuery($query, $model->getTable()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Database/Titles/AbilityTitle.php: -------------------------------------------------------------------------------- 1 | isWildcardAbility($ability)) { 16 | $this->title = $this->getWildcardAbilityTitle($ability); 17 | } elseif ($this->isRestrictedWildcardAbility($ability)) { 18 | $this->title = 'All simple abilities'; 19 | } elseif ($this->isSimpleAbility($ability)) { 20 | $this->title = $this->humanize($ability->name); 21 | } elseif ($this->isRestrictedOwnershipAbility($ability)) { 22 | $this->title = $this->humanize($ability->name.' everything owned'); 23 | } elseif ($this->isGeneralManagementAbility($ability)) { 24 | $this->title = $this->getBlanketModelAbilityTitle($ability); 25 | } elseif ($this->isBlanketModelAbility($ability)) { 26 | $this->title = $this->getBlanketModelAbilityTitle($ability, $ability->name); 27 | } elseif ($this->isSpecificModelAbility($ability)) { 28 | $this->title = $this->getSpecificModelAbilityTitle($ability); 29 | } elseif ($this->isGlobalActionAbility($ability)) { 30 | $this->title = $this->humanize($ability->name.' everything'); 31 | } 32 | } 33 | 34 | /** 35 | * Determines if the given ability allows all abilities. 36 | * 37 | * @return bool 38 | */ 39 | protected function isWildcardAbility(Model $ability) 40 | { 41 | return $ability->name === '*' && $ability->entity_type === '*'; 42 | } 43 | 44 | /** 45 | * Determines if the given ability allows all simple abilities. 46 | * 47 | * @return bool 48 | */ 49 | protected function isRestrictedWildcardAbility(Model $ability) 50 | { 51 | return $ability->name === '*' && is_null($ability->entity_type); 52 | } 53 | 54 | /** 55 | * Determines if the given ability is a simple (non model) ability. 56 | * 57 | * @return bool 58 | */ 59 | protected function isSimpleAbility(Model $ability) 60 | { 61 | return is_null($ability->entity_type); 62 | } 63 | 64 | /** 65 | * Determines whether the given ability is a global 66 | * ownership ability restricted to a specific action. 67 | * 68 | * @return bool 69 | */ 70 | protected function isRestrictedOwnershipAbility(Model $ability) 71 | { 72 | return $ability->only_owned && $ability->name !== '*' && $ability->entity_type === '*'; 73 | } 74 | 75 | /** 76 | * Determines whether the given ability is for managing all models of a given type. 77 | * 78 | * @return bool 79 | */ 80 | protected function isGeneralManagementAbility(Model $ability) 81 | { 82 | return $ability->name === '*' 83 | && $ability->entity_type !== '*' 84 | && ! is_null($ability->entity_type) 85 | && is_null($ability->entity_id); 86 | } 87 | 88 | /** 89 | * Determines whether the given ability is for an action on all models of a given type. 90 | * 91 | * @return bool 92 | */ 93 | protected function isBlanketModelAbility(Model $ability) 94 | { 95 | return $ability->name !== '*' 96 | && $ability->entity_type !== '*' 97 | && ! is_null($ability->entity_type) 98 | && is_null($ability->entity_id); 99 | } 100 | 101 | /** 102 | * Determines whether the given ability is for an action on a specific model. 103 | * 104 | * @return bool 105 | */ 106 | protected function isSpecificModelAbility(Model $ability) 107 | { 108 | return $ability->entity_type !== '*' 109 | && ! is_null($ability->entity_type) 110 | && ! is_null($ability->entity_id); 111 | } 112 | 113 | /** 114 | * Determines whether the given ability allows an action on all models. 115 | * 116 | * @return bool 117 | */ 118 | protected function isGlobalActionAbility(Model $ability) 119 | { 120 | return $ability->name !== '*' 121 | && $ability->entity_type === '*' 122 | && is_null($ability->entity_id); 123 | } 124 | 125 | /** 126 | * Get the title for the given wildcard ability. 127 | * 128 | * @return string 129 | */ 130 | protected function getWildcardAbilityTitle(Model $ability) 131 | { 132 | if ($ability->only_owned) { 133 | return 'Manage everything owned'; 134 | } 135 | 136 | return 'All abilities'; 137 | } 138 | 139 | /** 140 | * Get the title for the given blanket model ability. 141 | * 142 | * @param string $name 143 | * @return string 144 | */ 145 | protected function getBlanketModelAbilityTitle(Model $ability, $name = 'manage') 146 | { 147 | return $this->humanize($name.' '.$this->getPluralName($ability->entity_type)); 148 | } 149 | 150 | /** 151 | * Get the title for the given model ability. 152 | * 153 | * @return string 154 | */ 155 | protected function getSpecificModelAbilityTitle(Model $ability) 156 | { 157 | $name = $ability->name === '*' ? 'manage' : $ability->name; 158 | 159 | return $this->humanize( 160 | $name.' '.$this->basename($ability->entity_type).' #'.$ability->entity_id 161 | ); 162 | } 163 | 164 | /** 165 | * Get the human-readable plural form of the given class name. 166 | * 167 | * @param string $class 168 | * @return string 169 | */ 170 | protected function getPluralName($class) 171 | { 172 | return $this->pluralize($this->basename($class)); 173 | } 174 | 175 | /** 176 | * Get the class "basename" of the given class. 177 | * 178 | * @param string $class 179 | * @return string 180 | */ 181 | protected function basename($class) 182 | { 183 | return basename(str_replace('\\', '/', $class)); 184 | } 185 | 186 | /** 187 | * Pluralize the given value. 188 | * 189 | * @param string $value 190 | * @return string 191 | */ 192 | protected function pluralize($value) 193 | { 194 | return Str::plural($value, 2); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/Database/Titles/RoleTitle.php: -------------------------------------------------------------------------------- 1 | title = $this->humanize($role->name); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Database/Titles/Title.php: -------------------------------------------------------------------------------- 1 | title; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Factory.php: -------------------------------------------------------------------------------- 1 | user = $user; 63 | } 64 | 65 | /** 66 | * Create an instance of Bouncer. 67 | * 68 | * @return \Silber\Bouncer\Bouncer 69 | */ 70 | public function create() 71 | { 72 | $gate = $this->getGate(); 73 | $guard = $this->getGuard(); 74 | 75 | $bouncer = (new Bouncer($guard))->setGate($gate); 76 | 77 | if ($this->registerAtGate) { 78 | $guard->registerAt($gate); 79 | } 80 | 81 | if ($this->registerAtContainer) { 82 | $bouncer->registerClipboardAtContainer(); 83 | } 84 | 85 | return $bouncer; 86 | } 87 | 88 | /** 89 | * Set the cache instance to use for the clipboard. 90 | * 91 | * @return $this 92 | */ 93 | public function withCache(Store $cache) 94 | { 95 | $this->cache = $cache; 96 | 97 | return $this; 98 | } 99 | 100 | /** 101 | * Set the instance of the clipboard to use. 102 | * 103 | * @return $this 104 | */ 105 | public function withClipboard(Contracts\Clipboard $clipboard) 106 | { 107 | $this->clipboard = $clipboard; 108 | 109 | return $this; 110 | } 111 | 112 | /** 113 | * Set the gate instance to use. 114 | * 115 | * @return $this 116 | */ 117 | public function withGate(GateContract $gate) 118 | { 119 | $this->gate = $gate; 120 | 121 | return $this; 122 | } 123 | 124 | /** 125 | * Set the user model to use for the gate. 126 | * 127 | * @param mixed $user 128 | * @return $this 129 | */ 130 | public function withUser($user) 131 | { 132 | $this->user = $user; 133 | 134 | return $this; 135 | } 136 | 137 | /** 138 | * Set whether the factory registers the clipboard instance with the container. 139 | * 140 | * @param bool $bool 141 | * @return $this 142 | */ 143 | public function registerClipboardAtContainer($bool = true) 144 | { 145 | $this->registerAtContainer = $bool; 146 | 147 | return $this; 148 | } 149 | 150 | /** 151 | * Set whether the factory registers the guard instance with the gate. 152 | * 153 | * @param bool $bool 154 | * @return $this 155 | */ 156 | public function registerAtGate($bool = true) 157 | { 158 | $this->registerAtGate = $bool; 159 | 160 | return $this; 161 | } 162 | 163 | /** 164 | * Get an instance of the clipboard. 165 | * 166 | * @return \Silber\Bouncer\Guard 167 | */ 168 | protected function getGuard() 169 | { 170 | return new Guard($this->getClipboard()); 171 | } 172 | 173 | /** 174 | * Get an instance of the clipboard. 175 | * 176 | * @return \Silber\Bouncer\Contracts\Clipboard 177 | */ 178 | protected function getClipboard() 179 | { 180 | return $this->clipboard ?: new CachedClipboard($this->getCacheStore()); 181 | } 182 | 183 | /** 184 | * Get an instance of the cache store. 185 | * 186 | * @return \Illuminate\Contracts\Cache\Store 187 | */ 188 | protected function getCacheStore() 189 | { 190 | return $this->cache ?: new ArrayStore; 191 | } 192 | 193 | /** 194 | * Get an instance of the gate. 195 | * 196 | * @return \Illuminate\Contracts\Auth\Access\Gate 197 | */ 198 | protected function getGate() 199 | { 200 | if ($this->gate) { 201 | return $this->gate; 202 | } 203 | 204 | return new Gate(Container::getInstance(), function () { 205 | return $this->user; 206 | }); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/Guard.php: -------------------------------------------------------------------------------- 1 | clipboard = $clipboard; 36 | } 37 | 38 | /** 39 | * Get the clipboard instance. 40 | * 41 | * @return \Silber\Bouncer\Contracts\Clipboard 42 | */ 43 | public function getClipboard() 44 | { 45 | return $this->clipboard; 46 | } 47 | 48 | /** 49 | * Set the clipboard instance. 50 | * 51 | * @return $this 52 | */ 53 | public function setClipboard(Contracts\Clipboard $clipboard) 54 | { 55 | $this->clipboard = $clipboard; 56 | 57 | return $this; 58 | } 59 | 60 | /** 61 | * Determine whether the clipboard used is a cached clipboard. 62 | * 63 | * @return bool 64 | */ 65 | public function usesCachedClipboard() 66 | { 67 | return $this->clipboard instanceof Contracts\CachedClipboard; 68 | } 69 | 70 | /** 71 | * Set or get which slot to run the clipboard's checks. 72 | * 73 | * @param string|null $slot 74 | * @return $this|string 75 | */ 76 | public function slot($slot = null) 77 | { 78 | if (is_null($slot)) { 79 | return $this->slot; 80 | } 81 | 82 | if (! in_array($slot, ['before', 'after'])) { 83 | throw new InvalidArgumentException( 84 | "{$slot} is an invalid gate slot" 85 | ); 86 | } 87 | 88 | $this->slot = $slot; 89 | 90 | return $this; 91 | } 92 | 93 | /** 94 | * Register the clipboard at the given gate. 95 | * 96 | * @return $this 97 | */ 98 | public function registerAt(Gate $gate) 99 | { 100 | $gate->before(function () { 101 | return $this->runBeforeCallback(...func_get_args()); 102 | }); 103 | 104 | $gate->after(function () { 105 | return $this->runAfterCallback(...func_get_args()); 106 | }); 107 | 108 | return $this; 109 | } 110 | 111 | /** 112 | * Run the gate's "before" callback. 113 | * 114 | * @param \Illuminate\Database\Eloquent\Model $authority 115 | * @param string $ability 116 | * @param mixed $arguments 117 | * @param mixed $additional 118 | * @return bool|null 119 | */ 120 | protected function runBeforeCallback($authority, $ability, $arguments = []) 121 | { 122 | if ($this->slot != 'before') { 123 | return; 124 | } 125 | 126 | if (count($arguments) > 2) { 127 | return; 128 | } 129 | 130 | $model = isset($arguments[0]) ? $arguments[0] : null; 131 | 132 | return $this->checkAtClipboard($authority, $ability, $model); 133 | } 134 | 135 | /** 136 | * Run the gate's "before" callback. 137 | * 138 | * @param \Illuminate\Database\Eloquent\Model $authority 139 | * @param string $ability 140 | * @param mixed $result 141 | * @param array $arguments 142 | * @return bool|null 143 | */ 144 | protected function runAfterCallback($authority, $ability, $result, $arguments = []) 145 | { 146 | if (! is_null($result)) { 147 | return $result; 148 | } 149 | 150 | if ($this->slot != 'after') { 151 | return; 152 | } 153 | 154 | if (count($arguments) > 2) { 155 | return; 156 | } 157 | 158 | $model = isset($arguments[0]) ? $arguments[0] : null; 159 | 160 | return $this->checkAtClipboard($authority, $ability, $model); 161 | } 162 | 163 | /** 164 | * Run an auth check at the clipboard. 165 | * 166 | * @param string $ability 167 | * @param \Illuminate\Database\Eloquent\Model|string|null $model 168 | * @return mixed 169 | */ 170 | protected function checkAtClipboard(Model $authority, $ability, $model) 171 | { 172 | if ($id = $this->clipboard->checkGetId($authority, $ability, $model)) { 173 | return $this->allow('Bouncer granted permission via ability #'.$id); 174 | } 175 | 176 | // If the response from "checkGetId" is "false", then this ability 177 | // has been explicity forbidden. We'll return false so the gate 178 | // doesn't run any further checks. Otherwise we return null. 179 | return $id; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/Helpers.php: -------------------------------------------------------------------------------- 1 | getKey()]]; 48 | } 49 | 50 | if ($model instanceof Collection) { 51 | $keys = $model->map(function ($model) { 52 | return $model->getKey(); 53 | }); 54 | 55 | return [$model->first(), $keys]; 56 | } 57 | } 58 | 59 | /** 60 | * Fill the given array with the given value for any missing keys. 61 | * 62 | * @param iterable $array 63 | * @param mixed $value 64 | * @param iterable $keys 65 | * @return iterable 66 | */ 67 | public static function fillMissingKeys($array, $value, $keys) 68 | { 69 | foreach ($keys as $key) { 70 | if (! array_key_exists($key, $array)) { 71 | $array[$key] = $value; 72 | } 73 | } 74 | 75 | return $array; 76 | } 77 | 78 | /** 79 | * Group models and their identifiers by type (models, strings & integers). 80 | * 81 | * @param iterable $models 82 | * @return array 83 | */ 84 | public static function groupModelsAndIdentifiersByType($models) 85 | { 86 | $groups = (new Collection($models))->groupBy(function ($model) { 87 | if (is_numeric($model)) { 88 | return 'integers'; 89 | } elseif (is_string($model)) { 90 | return 'strings'; 91 | } elseif ($model instanceof Model) { 92 | return 'models'; 93 | } 94 | 95 | throw new InvalidArgumentException('Invalid model identifier'); 96 | })->map(function ($items) { 97 | return $items->all(); 98 | })->all(); 99 | 100 | return static::fillMissingKeys($groups, [], ['integers', 'strings', 'models']); 101 | } 102 | 103 | /** 104 | * Determines if an array is associative. 105 | * 106 | * An array is "associative" if it doesn't have sequential numerical keys beginning with zero. 107 | * 108 | * @param mixed $array 109 | * @return bool 110 | */ 111 | public static function isAssociativeArray($array) 112 | { 113 | if (! is_array($array)) { 114 | return false; 115 | } 116 | 117 | $keys = array_keys($array); 118 | 119 | return array_keys($keys) !== $keys; 120 | } 121 | 122 | /** 123 | * Determines if an array is numerically indexed. 124 | * 125 | * @param mixed $array 126 | * @return bool 127 | */ 128 | public static function isIndexedArray($array) 129 | { 130 | if (! is_array($array)) { 131 | return false; 132 | } 133 | 134 | foreach ($array as $key => $value) { 135 | if (! is_numeric($key)) { 136 | return false; 137 | } 138 | } 139 | 140 | return true; 141 | } 142 | 143 | /** 144 | * Determines whether the given model is set to soft delete. 145 | * 146 | * @return bool 147 | */ 148 | public static function isSoftDeleting(Model $model) 149 | { 150 | // Soft deleting models is controlled by adding the SoftDeletes trait 151 | // to the model. Instead of recursively looking for that trait, we 152 | // will check for the existence of the `isForceDeleting` method. 153 | if (! method_exists($model, 'isForceDeleting')) { 154 | return false; 155 | } 156 | 157 | return ! $model->isForceDeleting(); 158 | } 159 | 160 | /** 161 | * Convert the given value to an array. 162 | * 163 | * @param mixed $value 164 | * @return array 165 | */ 166 | public static function toArray($value) 167 | { 168 | if (is_array($value)) { 169 | return $value; 170 | } 171 | 172 | if ($value instanceof Collection) { 173 | return $value->all(); 174 | } 175 | 176 | return [$value]; 177 | } 178 | 179 | /** 180 | * Map a list of authorities by their class name. 181 | * 182 | * @return array 183 | */ 184 | public static function mapAuthorityByClass(array $authorities) 185 | { 186 | $map = []; 187 | 188 | foreach ($authorities as $authority) { 189 | if ($authority instanceof Model) { 190 | $map[get_class($authority)][] = $authority->getKey(); 191 | } else { 192 | $map[Models::classname(User::class)][] = $authority; 193 | } 194 | } 195 | 196 | return $map; 197 | } 198 | 199 | /** 200 | * Partition the given collection into two collection using the given callback. 201 | * 202 | * @param iterable $items 203 | * @return \Illuminate\Support\Collection 204 | */ 205 | public static function partition($items, callable $callback) 206 | { 207 | $partitions = [new Collection, new Collection]; 208 | 209 | foreach ($items as $key => $item) { 210 | $partitions[(int) ! $callback($item, $key)][$key] = $item; 211 | } 212 | 213 | return new Collection($partitions); 214 | } 215 | } 216 | --------------------------------------------------------------------------------