Skip to content

Commit

Permalink
Optimize the non-cached clipboard to only fetch relevant abilities
Browse files Browse the repository at this point in the history
References #263
  • Loading branch information
JosephSilber committed Mar 29, 2018
1 parent 64edb05 commit abeac18
Show file tree
Hide file tree
Showing 29 changed files with 1,423 additions and 621 deletions.
166 changes: 166 additions & 0 deletions src/BaseClipboard.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<?php

namespace Silber\Bouncer;

use Silber\Bouncer\Database\Models;
use Silber\Bouncer\Database\Queries\Abilities;

use Illuminate\Support\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Contracts\Auth\Access\Gate;
use Illuminate\Auth\Access\HandlesAuthorization;

abstract class BaseClipboard implements Contracts\Clipboard
{
use HandlesAuthorization;

/**
* Register the clipboard at the given gate.
*
* @param \Illuminate\Contracts\Auth\Access\Gate $gate
* @return void
*/
public function registerAt(Gate $gate)
{
$gate->before(function ($authority, $ability, $arguments = [], $additional = null) {
list($model, $additional) = $this->parseGateArguments($arguments, $additional);

if ( ! is_null($additional)) {
return;
}

if ($id = $this->checkGetId($authority, $ability, $model)) {
return $this->allow('Bouncer granted permission via ability #'.$id);
}

// If the response from "checkGetId" is "false", then this ability
// has been explicity forbidden. We'll return false so the gate
// doesn't run any further checks. Otherwise we return null.
return $id;
});
}

/**
* Parse the arguments we got from the gate.
*
* @param mixed $arguments
* @param mixed $additional
* @return array
*/
protected function parseGateArguments($arguments, $additional)
{
// The way arguments are passed into the gate's before callback has changed in Laravel
// in the middle of the 5.2 release. Before, arguments were spread out. Now they're
// all supplied in a single array instead. We will normalize it into two values.
if ( ! is_null($additional)) {
return [$arguments, $additional];
}

if (is_array($arguments)) {
return [
isset($arguments[0]) ? $arguments[0] : null,
isset($arguments[1]) ? $arguments[1] : null,
];
}

return [$arguments, null];
}

/**
* Determine if the given authority has the given ability.
*
* @param \Illuminate\Database\Eloquent\Model $authority
* @param string $ability
* @param \Illuminate\Database\Eloquent\Model|string|null $model
* @return bool
*/
public function check(Model $authority, $ability, $model = null)
{
return (bool) $this->checkGetId($authority, $ability, $model);
}

/**
* Determine if the given authority has the given ability, and return the ability ID.
*
* @param \Illuminate\Database\Eloquent\Model $authority
* @param string $ability
* @param \Illuminate\Database\Eloquent\Model|string|null $model
* @return int|bool|null
*/
abstract protected function checkGetId(Model $authority, $ability, $model = null);

/**
* Check if an authority has the given roles.
*
* @param \Illuminate\Database\Eloquent\Model $authority
* @param array|string $roles
* @param string $boolean
* @return bool
*/
public function checkRole(Model $authority, $roles, $boolean = 'or')
{
$available = $this->getRoles($authority)
->intersect(Models::role()->getRoleNames($roles));

if ($boolean == 'or') {
return $available->count() > 0;
} elseif ($boolean === 'not') {
return $available->count() === 0;
}

return $available->count() == count((array) $roles);
}

/**
* Get the given authority's roles.
*
* @param \Illuminate\Database\Eloquent\Model $authority
* @return \Illuminate\Support\Collection
*/
public function getRoles(Model $authority)
{
$collection = $authority->roles()->get(['name'])->pluck('name');

// In Laravel 5.1, "pluck" returns an Eloquent collection,
// so we call "toBase" on it. In 5.2, "pluck" returns a
// base instance, so there is no "toBase" available.
if (method_exists($collection, 'toBase')) {
$collection = $collection->toBase();
}

return $collection;
}

/**
* Get a list of the authority's abilities.
*
* @param \Illuminate\Database\Eloquent\Model $authority
* @param bool $allowed
* @return \Illuminate\Database\Eloquent\Collection
*/
public function getAbilities(Model $authority, $allowed = true)
{
return Abilities::forAuthority($authority, $allowed)->get();
}

/**
* Get a list of the authority's forbidden abilities.
*
* @param \Illuminate\Database\Eloquent\Model $authority
* @return \Illuminate\Database\Eloquent\Collection
*/
public function getForbiddenAbilities(Model $authority)
{
return $this->getAbilities($authority, false);
}

/**
* Determine whether the authority owns the given model.
*
* @return bool
*/
public function isOwnedBy($authority, $model)
{
return $model instanceof Model && Models::isOwnedBy($authority, $model);
}
}
47 changes: 35 additions & 12 deletions src/Bouncer.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,22 +150,49 @@ public function is(Model $authority)
}

/**
* Use the given cache instance.
* Get the clipboard instance.
*
* @return \Silber\Bouncer\Contracts\Clipboard
*/
public function getClipboard()
{
return $this->clipboard;
}

/**
* Set the clipboard instance used by bouncer.
*
* Will also register the given clipboard with the container.
*
* @param \Silber\Bouncer\Contracts\Clipboard
* @return $this
*/
public function setClipboard(Contracts\Clipboard $clipboard)
{
Container::getInstance()->instance(Contracts\Clipboard::class, $clipboard);

$this->clipboard = $clipboard;

return $this;
}

/**
* Use a cached clipboard with the given cache instance.
*
* @param \Illuminate\Contracts\Cache\Store $cache
* @return $this
*/
public function cache(Store $cache = null)
{
if (! $this->usesCachedClipboard()) {
throw new RuntimeException('To use caching, you must use an instance of CachedClipboard.');
}

$cache = $cache ?: $this->resolve(CacheRepository::class)->getStore();

$this->clipboard->setCache($cache);
if ($this->usesCachedClipboard()) {
$this->clipboard->setCache($cache);

return $this;
}

return $this;
return $this->setClipboard(new CachedClipboard($cache));
}

/**
Expand All @@ -175,11 +202,7 @@ public function cache(Store $cache = null)
*/
public function dontCache()
{
if ($this->usesCachedClipboard()) {
$this->clipboard->setCache(new NullStore);
}

return $this;
return $this->setClipboard(new Clipboard);
}

/**
Expand Down
127 changes: 125 additions & 2 deletions src/CachedClipboard.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
namespace Silber\Bouncer;

use Silber\Bouncer\Database\Models;
use Silber\Bouncer\Contracts\CachedClipboard as CachedClipboardContract;

use Illuminate\Cache\TaggedCache;
use Illuminate\Contracts\Cache\Store;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Collection as BaseCollection;

class CachedClipboard extends Clipboard implements CachedClipboardContract
class CachedClipboard extends BaseClipboard implements Contracts\CachedClipboard
{
/**
* The tag used for caching.
Expand Down Expand Up @@ -63,6 +63,129 @@ public function getCache()
return $this->cache;
}

/**
* Determine if the given authority has the given ability, and return the ability ID.
*
* @param \Illuminate\Database\Eloquent\Model $authority
* @param string $ability
* @param \Illuminate\Database\Eloquent\Model|string|null $model
* @return int|bool|null
*/
protected function checkGetId(Model $authority, $ability, $model = null)
{
$applicable = $this->compileAbilityIdentifiers($ability, $model);

// We will first check if any of the applicable abilities have been forbidden.
// If so, we'll return false right away, so as to not pass the check. Then,
// we'll check if any of them have been allowed & return the matched ID.
$forbiddenId = $this->findMatchingAbility(
$this->getForbiddenAbilities($authority), $applicable, $model, $authority
);

if ($forbiddenId) {
return false;
}

return $this->findMatchingAbility(
$this->getAbilities($authority), $applicable, $model, $authority
);
}

/**
* Determine if any of the abilities can be matched against the provided applicable ones.
*
* @param \Illuminate\Support\Collection $abilities
* @param \Illuminate\Support\Collection $applicable
* @param \Illuminate\Database\Eloquent\Model $model
* @param \Illuminate\Database\Eloquent\Model $authority
* @return int|null
*/
protected function findMatchingAbility($abilities, $applicable, $model, $authority)
{
$abilities = $abilities->toBase()->pluck('identifier', 'id');

if ($id = $this->getMatchedAbilityId($abilities, $applicable)) {
return $id;
}

if ($this->isOwnedBy($authority, $model)) {
return $this->getMatchedAbilityId(
$abilities,
$applicable->map(function ($identifier) {
return $identifier.'-owned';
})
);
}
}

/**
* Get the ID of the ability that matches one of the applicable abilities.
*
* @param \Illuminate\Support\Collection $abilityMap
* @param \Illuminate\Support\Collection $applicable
* @return int|null
*/
protected function getMatchedAbilityId($abilityMap, $applicable)
{
foreach ($abilityMap as $id => $identifier) {
if ($applicable->contains($identifier)) {
return $id;
}
}
}

/**
* Compile a list of ability identifiers that match the provided parameters.
*
* @param string $ability
* @param \Illuminate\Database\Eloquent\Model|string|null $model
* @return \Illuminate\Support\Collection
*/
protected function compileAbilityIdentifiers($ability, $model)
{
$identifiers = new BaseCollection(
is_null($model)
? [$ability, '*-*', '*']
: $this->compileModelAbilityIdentifiers($ability, $model)
);

return $identifiers->map(function ($identifier) {
return strtolower($identifier);
});
}

/**
* Compile a list of ability identifiers that match the given model.
*
* @param string $ability
* @param \Illuminate\Database\Eloquent\Model|string $model
* @return array
*/
protected function compileModelAbilityIdentifiers($ability, $model)
{
if ($model === '*') {
return ["{$ability}-*", "*-*"];
}

$model = $model instanceof Model ? $model : new $model;

$type = $model->getMorphClass();

$abilities = [
"{$ability}-{$type}",
"{$ability}-*",
"*-{$type}",
"*-*",
];

if ($model->exists) {
$abilities[] = "{$ability}-{$type}-{$model->getKey()}";
$abilities[] = "*-{$type}-{$model->getKey()}";
}

return $abilities;
}

/**
* Get the given authority's abilities.
*
Expand Down
Loading

0 comments on commit abeac18

Please sign in to comment.