Skip to content

Commit

Permalink
Create forbidden abilities feature
Browse files Browse the repository at this point in the history
  • Loading branch information
JosephSilber committed Nov 29, 2016
1 parent fad2e2d commit 865227b
Show file tree
Hide file tree
Showing 14 changed files with 727 additions and 337 deletions.
40 changes: 32 additions & 8 deletions src/Bouncer.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
use Silber\Bouncer\Conductors\RemovesRole;
use Silber\Bouncer\Conductors\GivesAbility;
use Silber\Bouncer\Conductors\RemovesAbility;
use Silber\Bouncer\Conductors\ForbidsAbility;
use Silber\Bouncer\Conductors\UnforbidsAbility;

class Bouncer
{
Expand Down Expand Up @@ -79,25 +81,47 @@ public function seed()
}

/**
* Start a chain, to allow the given role a ability.
* Start a chain, to allow the given authority an ability.
*
* @param \Silber\Bouncer\Database\Role|string $role
* @param \Illuminate\Database\Eloquent\Model|string $authority
* @return \Silber\Bouncer\Conductors\GivesAbility
*/
public function allow($role)
public function allow($authority)
{
return new GivesAbility($role);
return new GivesAbility($authority);
}

/**
* Start a chain, to disallow the given role a ability.
* Start a chain, to disallow the given authority an ability.
*
* @param \Silber\Bouncer\Database\Role|string $role
* @param \Illuminate\Database\Eloquent\Model|string $authority
* @return \Silber\Bouncer\Conductors\RemovesAbility
*/
public function disallow($authority)
{
return new RemovesAbility($authority);
}

/**
* Start a chain, to forbid the given authority an ability.
*
* @param \Illuminate\Database\Eloquent\Model|string $authority
* @return \Silber\Bouncer\Conductors\GivesAbility
*/
public function forbid($authority)
{
return new ForbidsAbility($authority);
}

/**
* Start a chain, to unforbid the given authority an ability.
*
* @param \Illuminate\Database\Eloquent\Model|string $authority
* @return \Silber\Bouncer\Conductors\RemovesAbility
*/
public function disallow($role)
public function unforbid($authority)
{
return new RemovesAbility($role);
return new UnforbidsAbility($authority);
}

/**
Expand Down
20 changes: 12 additions & 8 deletions src/CachedClipboard.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,18 @@ public function getCache()
* Get the given authority's abilities.
*
* @param \Illuminate\Database\Eloquent\Model $authority
* @param bool $allowed
* @return \Illuminate\Database\Eloquent\Collection
*/
public function getAbilities(Model $authority)
public function getAbilities(Model $authority, $allowed = true)
{
$key = $this->getCacheKey($authority, 'abilities');
$key = $this->getCacheKey($authority, 'abilities', $allowed);

if (is_array($abilities = $this->cache->get($key))) {
return $this->deserializeAbilities($abilities);
}

$abilities = $this->getFreshAbilities($authority);
$abilities = $this->getFreshAbilities($authority, $allowed);

$this->cache->forever($key, $this->serializeAbilities($abilities));

Expand All @@ -87,11 +88,12 @@ public function getAbilities(Model $authority)
* Get a fresh copy of the given authority's abilities.
*
* @param \Illuminate\Database\Eloquent\Model $authority
* @param bool $allowed
* @return \Illuminate\Database\Eloquent\Collection
*/
public function getFreshAbilities(Model $authority)
public function getFreshAbilities(Model $authority, $allowed)
{
return parent::getAbilities($authority);
return parent::getAbilities($authority, $allowed);
}

/**
Expand Down Expand Up @@ -154,8 +156,8 @@ public function refresh($authority = null)
*/
public function refreshFor(Model $authority)
{
$this->cache->forget($this->getCacheKey($authority, 'abilities'));

$this->cache->forget($this->getCacheKey($authority, 'abilities', true));
$this->cache->forget($this->getCacheKey($authority, 'abilities', false));
$this->cache->forget($this->getCacheKey($authority, 'roles'));

return $this;
Expand All @@ -182,15 +184,17 @@ protected function refreshAllIteratively()
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $type
* @param bool $allowed
* @return string
*/
protected function getCacheKey(Model $model, $type)
protected function getCacheKey(Model $model, $type, $allowed = true)
{
return implode('-', [
$this->tag,
$type,
$model->getMorphClass(),
$model->getKey(),
$allowed ? 'a' : 'f',
]);
}

Expand Down
66 changes: 56 additions & 10 deletions src/Clipboard.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,27 +106,61 @@ public function check(Model $authority, $ability, $model = null)
*/
protected function checkGetId(Model $authority, $ability, $model = null)
{
$abilities = $this->getAbilities($authority)->toBase()->pluck('identifier', 'id');
$applicable = $this->compileAbilityIdentifiers($ability, $model);

$allowed = $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 ($id = $this->getMatchedAbilityId($abilities, $allowed)) {
if ($forbiddenId) {
return false;
}

$allowedId = $this->findMatchingAbility(
$this->getAbilities($authority), $applicable, $model, $authority
);

return $allowedId ?: false;
}

/**
* 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 ($model instanceof Model && Models::isOwnedBy($authority, $model)) {
$id = $this->getMatchedAbilityId($abilities, $allowed->map(function ($identifier) {
return $this->getMatchedAbilityId($abilities, $applicable->map(function ($identifier) {
return $identifier.'-owned';
}));
}

return $id ?: false;
}

protected function getMatchedAbilityId(Collection $abilityMap, Collection $allowed)
/**
* 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(Collection $abilityMap, Collection $applicable)
{
foreach ($abilityMap as $id => $identifier) {
if ($allowed->contains($identifier)) {
if ($applicable->contains($identifier)) {
return $id;
}
}
Expand Down Expand Up @@ -227,10 +261,22 @@ public function getRoles(Model $authority)
* 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 (new AbilitiesQuery)->getForAuthority($authority, $allowed);
}

/**
* Get a list of the authority's forbidden abilities.
*
* @param \Illuminate\Database\Eloquent\Model $authority
* @return \Illuminate\Database\Eloquent\Collection
*/
public function getAbilities(Model $authority)
public function getForbiddenAbilities(Model $authority)
{
return (new AbilitiesQuery)->getForAuthority($authority);
return $this->getAbilities($authority, false);
}
}
60 changes: 60 additions & 0 deletions src/Conductors/ForbidsAbility.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

namespace Silber\Bouncer\Conductors;

use Illuminate\Database\Eloquent\Model;
use Silber\Bouncer\Conductors\Traits\ConductsAbilities;
use Silber\Bouncer\Conductors\Traits\AssociatesAbilities;

class ForbidsAbility
{
use AssociatesAbilities, ConductsAbilities;

/**
* The authority to be forbidden from the abilities.
*
* @var \Illuminate\Database\Eloquent\Model|string
*/
protected $authority;

/**
* Constructor.
*
* @param \Illuminate\Database\Eloquent\Model|string $authority
*/
public function __construct($authority)
{
$this->authority = $authority;
}

/**
* Forbid the abilities to the authority.
*
* @param mixed $abilities
* @param \Illuminate\Database\Eloquent\Model|string|null $model
* @param bool $onlyOwned
* @return bool
*/
public function to($abilities, $model = null, $onlyOwned = false)
{
$ids = $this->getAbilityIds($abilities, $model, $onlyOwned);

$this->forbidAbilities($ids, $this->getAuthority());

return true;
}

/**
* Associate the given abilitiy IDs as forbidden abilities.
*
* @param array $ids
* @param \Illuminate\Database\Eloquent\Model $authority
* @return void
*/
protected function forbidAbilities(array $ids, Model $authority)
{
$ids = array_diff($ids, $this->getAssociatedAbilityIds($authority, $ids, true));

$authority->abilities()->attach($ids, ['forbidden' => true]);
}
}

0 comments on commit 865227b

Please sign in to comment.