File manager - Edit - /home/autoph/public_html/projects/Rating-AutoHub/public/css/bouncer.tar
Back
composer.json 0000644 00000003401 15025033125 0007260 0 ustar 00 { "name": "silber/bouncer", "description": "Eloquent roles and abilities.", "keywords": [ "abilities", "acl", "capabilities", "eloquent", "laravel", "permissions", "roles" ], "license": "MIT", "authors": [ { "name": "Joseph Silber", "email": "contact@josephsilber.com" } ], "autoload": { "psr-4": { "Silber\\Bouncer\\": "src/" } }, "autoload-dev": { "psr-4": { "Silber\\Bouncer\\Tests\\": "tests/" }, "files": [ "tests/helpers.php" ] }, "require": { "php": "^7.2|^8.0", "illuminate/auth": "^6.0|^7.0|^8.0|^9.0|^10.0", "illuminate/cache": "^6.0|^7.0|^8.0|^9.0|^10.0", "illuminate/container": "^6.0|^7.0|^8.0|^9.0|^10.0", "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0", "illuminate/database": "^6.0|^7.0|^8.0|^9.0|^10.0" }, "require-dev": { "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0", "illuminate/events": "^6.0|^7.0|^8.0|^9.0|^10.0", "larapack/dd": "^1.1", "mockery/mockery": "^1.3.3", "phpunit/phpunit": "^8.0|^9.0" }, "suggest": { "illuminate/console": "Allows running the bouncer:clean artisan command", "illuminate/events": "Required for multi-tenancy support" }, "scripts": { "test": "phpunit" }, "minimum-stability": "dev", "prefer-stable": true, "extra": { "laravel": { "providers": [ "Silber\\Bouncer\\BouncerServiceProvider" ], "aliases": { "Bouncer": "Silber\\Bouncer\\BouncerFacade" } } } } src/CachedClipboard.php 0000644 00000023120 15025033125 0011025 0 ustar 00 <?php namespace Silber\Bouncer; use Silber\Bouncer\Database\Models; 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 BaseClipboard implements Contracts\CachedClipboard { /** * The tag used for caching. * * @var string */ protected $tag = 'silber-bouncer'; /** * The cache store. * * @var \Illuminate\Contracts\Cache\Store */ protected $cache; /** * Constructor. * * @param \Illuminate\Contracts\Cache\Store $cache */ public function __construct(Store $cache) { $this->setCache($cache); } /** * Set the cache instance. * * @param \Illuminate\Contracts\Cache\Store $cache * @return $this */ public function setCache(Store $cache) { if (method_exists($cache, 'tags')) { $cache = $cache->tags($this->tag()); } $this->cache = $cache; return $this; } /** * Get the cache instance. * * @return \Illuminate\Contracts\Cache\Store */ 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 */ public 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. * * @param \Illuminate\Database\Eloquent\Model $authority * @param bool $allowed * @return \Illuminate\Database\Eloquent\Collection */ public function getAbilities(Model $authority, $allowed = true) { $key = $this->getCacheKey($authority, 'abilities', $allowed); if (is_array($abilities = $this->cache->get($key))) { return $this->deserializeAbilities($abilities); } $abilities = $this->getFreshAbilities($authority, $allowed); $this->cache->forever($key, $this->serializeAbilities($abilities)); return $abilities; } /** * 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, $allowed) { return parent::getAbilities($authority, $allowed); } /** * Get the given authority's roles' IDs and names. * * @param \Illuminate\Database\Eloquent\Model $authority * @return array */ public function getRolesLookup(Model $authority) { $key = $this->getCacheKey($authority, 'roles'); return $this->sear($key, function () use ($authority) { return parent::getRolesLookup($authority); }); } /** * Get an item from the cache, or store the default value forever. * * @param string $key * @param callable $callback * @return mixed */ protected function sear($key, callable $callback) { if (is_null($value = $this->cache->get($key))) { $this->cache->forever($key, $value = $callback()); } return $value; } /** * Clear the cache. * * @param null|\Illuminate\Database\Eloquent\Model $authority * @return $this */ public function refresh($authority = null) { if ( ! is_null($authority)) { return $this->refreshFor($authority); } if ($this->cache instanceof TaggedCache) { $this->cache->flush(); } else { $this->refreshAllIteratively(); } return $this; } /** * Clear the cache for the given authority. * * @param \Illuminate\Database\Eloquent\Model $authority * @return $this */ public function refreshFor(Model $authority) { $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; } /** * Refresh the cache for all roles and users, iteratively. * * @return void */ protected function refreshAllIteratively() { foreach (Models::user()->all() as $user) { $this->refreshFor($user); } foreach (Models::role()->all() as $role) { $this->refreshFor($role); } } /** * Get the cache key for the given model's cache type. * * @param \Illuminate\Database\Eloquent\Model $model * @param string $type * @param bool $allowed * @return string */ protected function getCacheKey(Model $model, $type, $allowed = true) { return implode('-', [ $this->tag(), $type, $model->getMorphClass(), $model->getKey(), $allowed ? 'a' : 'f', ]); } /** * Get the cache tag. * * @return string */ protected function tag() { return Models::scope()->appendToCacheKey($this->tag); } /** * Deserialize an array of abilities into a collection of models. * * @param array $abilities * @return \Illuminate\Database\Eloquent\Collection */ protected function deserializeAbilities(array $abilities) { return Models::ability()->hydrate($abilities); } /** * Serialize a collection of ability models into a plain array. * * @param \Illuminate\Database\Eloquent\Collection $abilities * @return array */ protected function serializeAbilities(Collection $abilities) { return $abilities->map(function ($ability) { return $ability->getAttributes(); })->all(); } } src/Database/Titles/Title.php 0000644 00000002700 15025033125 0012070 0 ustar 00 <?php namespace Silber\Bouncer\Database\Titles; use Illuminate\Support\Str; use Illuminate\Database\Eloquent\Model; abstract class Title { /** * The human-readable title. * * @var string */ protected $title = ''; /** * Create a new title instance for the given model. * * @param \Illuminate\Database\Eloquent\Model $model * @return static */ public static function from(Model $model) { return new static($model); } /** * Convert the given string into a human-readable format. * * @param string $value * @return string */ protected function humanize($value) { // Older versions of Laravel's inflector strip out spaces // in the original string, so we'll first swap out all // spaces with underscores, then convert them back. $value = str_replace(' ', '_', $value); // First we'll convert the string to snake case. Then we'll // convert all dashes and underscores to spaces. Finally, // we'll add a space before a pound (Laravel doesn't). $value = Str::snake($value); $value = preg_replace('~(?:-|_)+~', ' ', $value); $value = preg_replace('~([^ ])(?:#)+~', '$1 #', $value); return ucfirst($value); } /** * Get the title as a string. * * @return string */ public function toString() { return $this->title; } } src/Database/Titles/RoleTitle.php 0000644 00000000353 15025033125 0012714 0 ustar 00 <?php namespace Silber\Bouncer\Database\Titles; use Illuminate\Database\Eloquent\model; class RoleTitle extends Title { public function __construct(Model $role) { $this->title = $this->humanize($role->name); } } src/Database/Titles/AbilityTitle.php 0000644 00000014155 15025033125 0013415 0 ustar 00 <?php namespace Silber\Bouncer\Database\Titles; use Illuminate\Support\Str; use Illuminate\Database\Eloquent\model; class AbilityTitle extends Title { /** * Constructor. * * @param \Illuminate\Database\Eloquent\model $ability */ public function __construct(Model $ability) { if ($this->isWildcardAbility($ability)) { $this->title = $this->getWildcardAbilityTitle($ability); } else if ($this->isRestrictedWildcardAbility($ability)) { $this->title = 'All simple abilities'; } else if ($this->isSimpleAbility($ability)) { $this->title = $this->humanize($ability->name); } else if ($this->isRestrictedOwnershipAbility($ability)) { $this->title = $this->humanize($ability->name.' everything owned'); } else if ($this->isGeneralManagementAbility($ability)) { $this->title = $this->getBlanketModelAbilityTitle($ability); } else if ($this->isBlanketModelAbility($ability)) { $this->title = $this->getBlanketModelAbilityTitle($ability, $ability->name); } else if ($this->isSpecificModelAbility($ability)) { $this->title = $this->getSpecificModelAbilityTitle($ability); } else if ($this->isGlobalActionAbility($ability)) { $this->title = $this->humanize($ability->name.' everything'); } } /** * Determines if the given ability allows all abilities. * * @param \Illuminate\Database\Eloquent\model $ability * @return bool */ protected function isWildcardAbility(Model $ability) { return $ability->name === '*' && $ability->entity_type === '*'; } /** * Determines if the given ability allows all simple abilities. * * @param \Illuminate\Database\Eloquent\model $ability * @return bool */ protected function isRestrictedWildcardAbility(Model $ability) { return $ability->name === '*' && is_null($ability->entity_type); } /** * Determines if the given ability is a simple (non model) ability. * * @param \Illuminate\Database\Eloquent\model $ability * @return bool */ protected function isSimpleAbility(Model $ability) { return is_null($ability->entity_type); } /** * Determines whether the given ability is a global * ownership ability restricted to a specific action. * * @param \Illuminate\Database\Eloquent\model $ability * @return bool */ protected function isRestrictedOwnershipAbility(Model $ability) { return $ability->only_owned && $ability->name !== '*' && $ability->entity_type === '*'; } /** * Determines whether the given ability is for managing all models of a given type. * * @param \Illuminate\Database\Eloquent\model $ability * @return bool */ protected function isGeneralManagementAbility(Model $ability) { return $ability->name === '*' && $ability->entity_type !== '*' && ! is_null($ability->entity_type) && is_null($ability->entity_id); } /** * Determines whether the given ability is for an action on all models of a given type. * * @param \Illuminate\Database\Eloquent\model $ability * @return bool */ protected function isBlanketModelAbility(Model $ability) { return $ability->name !== '*' && $ability->entity_type !== '*' && ! is_null($ability->entity_type) && is_null($ability->entity_id); } /** * Determines whether the given ability is for an action on a specific model. * * @param \Illuminate\Database\Eloquent\model $ability * @return bool */ protected function isSpecificModelAbility(Model $ability) { return $ability->entity_type !== '*' && ! is_null($ability->entity_type) && ! is_null($ability->entity_id); } /** * Determines whether the given ability allows an action on all models. * * @param \Illuminate\Database\Eloquent\model $ability * @return bool */ protected function isGlobalActionAbility(Model $ability) { return $ability->name !== '*' && $ability->entity_type === '*' && is_null($ability->entity_id); } /** * Get the title for the given wildcard ability. * * @param \Illuminate\Database\Eloquent\model $ability * @return string */ protected function getWildcardAbilityTitle(Model $ability) { if ($ability->only_owned) { return 'Manage everything owned'; } return 'All abilities'; } /** * Get the title for the given blanket model ability. * * @param \Illuminate\Database\Eloquent\model $ability * @param string $name * @return string */ protected function getBlanketModelAbilityTitle(Model $ability, $name = 'manage') { return $this->humanize($name.' '.$this->getPluralName($ability->entity_type)); } /** * Get the title for the given model ability. * * @param \Illuminate\Database\Eloquent\model $ability * @return string */ protected function getSpecificModelAbilityTitle(Model $ability) { $name = $ability->name === '*' ? 'manage' : $ability->name; return $this->humanize( $name.' '.$this->basename($ability->entity_type).' #'.$ability->entity_id ); } /** * Get the human-readable plural form of the given class name. * * @param string $class * @return string */ protected function getPluralName($class) { return $this->pluralize($this->basename($class)); } /** * Get the class "basename" of the given class. * * @param string $class * @return string */ protected function basename($class) { return basename(str_replace('\\', '/', $class)); } /** * Pluralize the given value. * * @param string $value * @return string */ protected function pluralize($value) { return Str::plural($value, 2); } } src/Database/Queries/Roles.php 0000644 00000005115 15025033125 0012247 0 ustar 00 <?php namespace Silber\Bouncer\Database\Queries; use Silber\Bouncer\Helpers; use Silber\Bouncer\Database\Models; class Roles { /** * Constrain the given query by the provided role. * * @param \Illuminate\Database\Eloquent\Builder $query * @param string ...$roles * @return \Illuminate\Database\Eloquent\Builder */ public function constrainWhereIs($query, ...$roles) { return $query->whereHas('roles', function ($query) use ($roles) { $query->whereIn('name', $roles); }); } /** * Constrain the given query by all provided roles. * * @param \Illuminate\Database\Eloquent\Builder $query * @param string ...$roles * @return \Illuminate\Database\Eloquent\Builder */ public function constrainWhereIsAll($query, ...$roles) { return $query->whereHas('roles', function ($query) use ($roles) { $query->whereIn('name', $roles); }, '=', count($roles)); } /** * Constrain the given query by the provided role. * * @param \Illuminate\Database\Eloquent\Builder $query * @param string ...$roles * @return \Illuminate\Database\Eloquent\Builder */ public function constrainWhereIsNot($query, ...$roles) { return $query->whereDoesntHave('roles', function ($query) use ($roles) { $query->whereIn('name', $roles); }); } /** * Constrain the given roles query to those that were assigned to the given authorities. * * @param \Illuminate\Database\Eloquent\Builder $query * @param string|\Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection $model * @param array $keys * @return void */ public function constrainWhereAssignedTo($query, $model, array $keys = null) { list($model, $keys) = Helpers::extractModelAndKeys($model, $keys); $query->whereExists(function ($query) use ($model, $keys) { $table = $model->getTable(); $key = "{$table}.{$model->getKeyName()}"; $pivot = Models::table('assigned_roles'); $roles = Models::table('roles'); $query->from($table) ->join($pivot, $key, '=', $pivot.'.entity_id') ->whereColumn("{$pivot}.role_id", "{$roles}.id") ->where("{$pivot}.entity_type", $model->getMorphClass()) ->whereIn($key, $keys); Models::scope()->applyToModelQuery($query, $roles); Models::scope()->applyToRelationQuery($query, $pivot); }); } } src/Database/Queries/Abilities.php 0000644 00000012212 15025033125 0013064 0 ustar 00 <?php namespace Silber\Bouncer\Database\Queries; use Silber\Bouncer\Database\Models; use Illuminate\Database\Query\Builder; use Illuminate\Database\Eloquent\Model; class Abilities { /** * Get a query for the authority's abilities. * * @param \Illuminate\Database\Eloquent\Model $authority * @param bool $allowed * @return \Illuminate\Database\Eloquent\Builder */ public static function forAuthority(Model $authority, $allowed = true) { return Models::ability()->where(function ($query) use ($authority, $allowed) { $query->whereExists(static::getRoleConstraint($authority, $allowed)); $query->orWhereExists(static::getAuthorityConstraint($authority, $allowed)); $query->orWhereExists(static::getEveryoneConstraint($allowed)); }); } /** * Get a query for the authority's forbidden abilities. * * @param \Illuminate\Database\Eloquent\Model $authority * @return \Illuminate\Database\Eloquent\Builder */ public static function forbiddenForAuthority(Model $authority) { return static::forAuthority($authority, false); } /** * Get a constraint for abilities that have been granted to the given authority through a role. * * @param \Illuminate\Database\Eloquent\Model $authority * @param bool $allowed * @return \Closure */ protected static function getRoleConstraint(Model $authority, $allowed) { return function ($query) use ($authority, $allowed) { $permissions = Models::table('permissions'); $abilities = Models::table('abilities'); $roles = Models::table('roles'); $query->from($roles) ->join($permissions, $roles.'.id', '=', $permissions.'.entity_id') ->whereColumn("{$permissions}.ability_id", "{$abilities}.id") ->where($permissions.".forbidden", ! $allowed) ->where($permissions.".entity_type", Models::role()->getMorphClass()); Models::scope()->applyToModelQuery($query, $roles); Models::scope()->applyToRelationQuery($query, $permissions); $query->where(function ($query) use ($roles, $authority, $allowed) { $query->whereExists(static::getAuthorityRoleConstraint($authority)); }); }; } /** * Get a constraint for roles that are assigned to the given authority. * * @param \Illuminate\Database\Eloquent\Model $authority * @return \Closure */ protected static function getAuthorityRoleConstraint(Model $authority) { return function ($query) use ($authority) { $pivot = Models::table('assigned_roles'); $roles = Models::table('roles'); $table = $authority->getTable(); $query->from($table) ->join($pivot, "{$table}.{$authority->getKeyName()}", '=', $pivot.'.entity_id') ->whereColumn("{$pivot}.role_id", "{$roles}.id") ->where($pivot.'.entity_type', $authority->getMorphClass()) ->where("{$table}.{$authority->getKeyName()}", $authority->getKey()); Models::scope()->applyToModelQuery($query, $roles); Models::scope()->applyToRelationQuery($query, $pivot); }; } /** * Get a constraint for abilities that have been granted to the given authority. * * @param \Illuminate\Database\Eloquent\Model $authority * @param bool $allowed * @return \Closure */ protected static function getAuthorityConstraint(Model $authority, $allowed) { return function ($query) use ($authority, $allowed) { $permissions = Models::table('permissions'); $abilities = Models::table('abilities'); $table = $authority->getTable(); $query->from($table) ->join($permissions, "{$table}.{$authority->getKeyName()}", '=', $permissions.'.entity_id') ->whereColumn("{$permissions}.ability_id", "{$abilities}.id") ->where("{$permissions}.forbidden", ! $allowed) ->where("{$permissions}.entity_type", $authority->getMorphClass()) ->where("{$table}.{$authority->getKeyName()}", $authority->getKey()); Models::scope()->applyToModelQuery($query, $abilities); Models::scope()->applyToRelationQuery($query, $permissions); }; } /** * Get a constraint for abilities that have been granted to everyone. * * @param bool $allowed * @return \Closure */ protected static function getEveryoneConstraint($allowed) { return function ($query) use ($allowed) { $permissions = Models::table('permissions'); $abilities = Models::table('abilities'); $query->from($permissions) ->whereColumn("{$permissions}.ability_id", "{$abilities}.id") ->where("{$permissions}.forbidden", ! $allowed) ->whereNull('entity_id'); Models::scope()->applyToRelationQuery($query, $permissions); }; } } src/Database/Queries/AbilitiesForModel.php 0000644 00000006573 15025033125 0014531 0 ustar 00 <?php namespace Silber\Bouncer\Database\Queries; use Silber\Bouncer\Database\Models; use Illuminate\Database\Eloquent\Model; class AbilitiesForModel { /** * The name of the abilities table. * * @var string */ protected $table; /** * Constructor. * */ public function __construct() { $this->table = Models::table('abilities'); } /** * Constrain a query to an ability for a specific model or wildcard. * * @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder $query * @param \Illuminate\Database\Eloquent\Model|string $model * @param bool $strict * @return void */ public function constrain($query, $model, $strict = false) { if ($model === '*') { return $this->constrainByWildcard($query); } $model = is_string($model) ? new $model : $model; $this->constrainByModel($query, $model, $strict); } /** * Constrain a query to a model wiildcard. * * @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder $query * @return void */ protected function constrainByWildcard($query) { $query->where("{$this->table}.entity_type", '*'); } /** * Constrain a query to an ability for a specific model. * * @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder $query * @param \Illuminate\Database\Eloquent\Model $model * @param bool $strict * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder */ protected function constrainByModel($query, Model $model, $strict) { if ($strict) { return $query->where( $this->modelAbilityConstraint($model, $strict) ); } return $query->where(function ($query) use ($model, $strict) { $query->where("{$this->table}.entity_type", '*') ->orWhere($this->modelAbilityConstraint($model, $strict)); }); } /** * Get the constraint for regular model abilities. * * @param \Illuminate\Database\Eloquent\Model $model * @param bool $strict * @return \Closure */ protected function modelAbilityConstraint(Model $model, $strict) { return function ($query) use ($model, $strict) { $query->where("{$this->table}.entity_type", $model->getMorphClass()); $query->where($this->abilitySubqueryConstraint($model, $strict)); }; } /** * Get the constraint for the ability subquery. * * @param \Illuminate\Database\Eloquent\Model $model * @param bool $strict * @return \Closure */ protected function abilitySubqueryConstraint(Model $model, $strict) { return function ($query) use ($model, $strict) { // If the model does not exist, we want to search for blanket abilities // that cover all instances of this model. If it does exist, we only // want to find blanket abilities if we're not using strict mode. if ( ! $model->exists || ! $strict) { $query->whereNull("{$this->table}.entity_id"); } if ($model->exists) { $query->orWhere("{$this->table}.entity_id", $model->getKey()); } }; } } src/Database/Ability.php 0000644 00000001343 15025033125 0011142 0 ustar 00 <?php namespace Silber\Bouncer\Database; use Illuminate\Database\Eloquent\Model; class Ability extends Model { use Concerns\IsAbility; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = ['name', 'title']; /** * The attributes that should be cast to native types. * * @var array */ protected $casts = [ 'id' => 'int', 'entity_id' => 'int', 'only_owned' => 'boolean', ]; /** * Constructor. * * @param array $attributes */ public function __construct(array $attributes = []) { $this->table = Models::table('abilities'); parent::__construct($attributes); } } src/Database/Role.php 0000644 00000001230 15025033125 0010441 0 ustar 00 <?php namespace Silber\Bouncer\Database; use Illuminate\Database\Eloquent\Model; class Role extends Model { use Concerns\IsRole; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = ['name', 'title']; /** * The attributes that should be cast to native types. * * @var array */ protected $casts = [ 'id' => 'int', ]; /** * Constructor. * * @param array $attributes */ public function __construct(array $attributes = []) { $this->table = Models::table('roles'); parent::__construct($attributes); } } src/Database/Models.php 0000644 00000016132 15025033125 0010772 0 ustar 00 <?php namespace Silber\Bouncer\Database; use Closure; use App\User; use Illuminate\Database\Query\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Relation; use Silber\Bouncer\Database\Scope\Scope; use Silber\Bouncer\Contracts\Scope as ScopeContract; class Models { /** * Map of bouncer's models. * * @var array */ protected static $models = []; /** * Map of ownership for models. * * @var array */ protected static $ownership = []; /** * Map of bouncer's tables. * * @var array */ protected static $tables = []; /** * The model scoping instance. * * @var \Silber\Bouncer\Database\Scope\Scope */ protected static $scope; /** * Set the model to be used for abilities. * * @param string $model * @return void */ public static function setAbilitiesModel($model) { static::$models[Ability::class] = $model; static::updateMorphMap([$model]); } /** * Set the model to be used for roles. * * @param string $model * @return void */ public static function setRolesModel($model) { static::$models[Role::class] = $model; static::updateMorphMap([$model]); } /** * Set the model to be used for users. * * @param string $model * @return void */ public static function setUsersModel($model) { static::$models[User::class] = $model; static::$tables['users'] = static::user()->getTable(); } /** * Set custom table names. * * @param array $map * @return void */ public static function setTables(array $map) { static::$tables = array_merge(static::$tables, $map); static::updateMorphMap(); } /** * Get a custom table name mapping for the given table. * * @param string $table * @return string */ public static function table($table) { if (isset(static::$tables[$table])) { return static::$tables[$table]; } return $table; } /** * Get or set the model scoping instance. * * @param \Silber\Bouncer\Contracts\Scope|null $scope * @return mixed */ public static function scope(ScopeContract $scope = null) { if (! is_null($scope)) { return static::$scope = $scope; } if (is_null(static::$scope)) { static::$scope = new Scope; } return static::$scope; } /** * Get the classname mapping for the given model. * * @param string $model * @return string */ public static function classname($model) { if (isset(static::$models[$model])) { return static::$models[$model]; } return $model; } /** * Update Eloquent's morph map with the Bouncer models and tables. * * @param array|null $classNames * @return void */ public static function updateMorphMap($classNames = null) { if (is_null($classNames)) { $classNames = [ static::classname(Role::class), static::classname(Ability::class), ]; } Relation::morphMap($classNames); } /** * Register an attribute/callback to determine if a model is owned by a given authority. * * @param string|\Closure $model * @param string|\Closure|null $attribute * @return void */ public static function ownedVia($model, $attribute = null) { if (is_null($attribute)) { static::$ownership['*'] = $model; } static::$ownership[$model] = $attribute; } /** * Determines whether the given model is owned by the given authority. * * @param \Illuminate\Database\Eloquent\Model $authority * @param \Illuminate\Database\Eloquent\Model $model * @return bool */ public static function isOwnedBy(Model $authority, Model $model) { $type = get_class($model); if (isset(static::$ownership[$type])) { $attribute = static::$ownership[$type]; } elseif (isset(static::$ownership['*'])) { $attribute = static::$ownership['*']; } else { $attribute = strtolower(static::basename($authority)).'_id'; } return static::isOwnedVia($attribute, $authority, $model); } /** * Determines ownership via the given attribute. * * @param string|\Closure $attribute * @param \Illuminate\Database\Eloquent\Model $authority * @param \Illuminate\Database\Eloquent\Model $model * @return bool */ protected static function isOwnedVia($attribute, Model $authority, Model $model) { if ($attribute instanceof Closure) { return $attribute($model, $authority); } return $authority->getKey() == $model->{$attribute}; } /** * Get an instance of the ability model. * * @param array $attributes * @return \Silber\Bouncer\Database\Ability */ public static function ability(array $attributes = []) { return static::make(Ability::class, $attributes); } /** * Get an instance of the role model. * * @param array $attributes * @return \Silber\Bouncer\Database\Role */ public static function role(array $attributes = []) { return static::make(Role::class, $attributes); } /** * Get an instance of the user model. * * @param array $attributes * @return \Illuminate\Database\Eloquent\Model */ public static function user(array $attributes = []) { return static::make(User::class, $attributes); } /** * Get a new query builder instance. * * @param string $table * @return \Illuminate\Database\Query\Builder */ public static function query($table) { $query = new Builder( $connection = static::user()->getConnection(), $connection->getQueryGrammar(), $connection->getPostProcessor() ); return $query->from(static::table($table)); } /** * Reset all settings to their original state. * * @return void */ public static function reset() { static::$models = static::$tables = static::$ownership = []; } /** * Get an instance of the given model. * * @param string $model * @param array $attributes * @return \Illuminate\Database\Eloquent\Model */ protected static function make($model, array $attributes = []) { $model = static::classname($model); return new $model($attributes); } /** * Get the basename of the given class. * * @param string|object $class * @return string */ protected static function basename($class) { if ( ! is_string($class)) { $class = get_class($class); } $segments = explode('\\', $class); return end($segments); } } src/Database/Concerns/HasRoles.php 0000644 00000010441 15025033125 0013036 0 ustar 00 <?php namespace Silber\Bouncer\Database\Concerns; use Illuminate\Container\Container; use Silber\Bouncer\Helpers; use Silber\Bouncer\Database\Role; use Silber\Bouncer\Database\Models; use Silber\Bouncer\Contracts\Clipboard; use Silber\Bouncer\Conductors\AssignsRoles; use Silber\Bouncer\Conductors\RemovesRoles; use Silber\Bouncer\Database\Queries\Roles as RolesQuery; use Illuminate\Database\Eloquent\Relations\MorphToMany; trait HasRoles { /** * Boot the HasRoles trait. * * @return void */ public static function bootHasRoles() { static::deleted(function ($model) { if (! Helpers::isSoftDeleting($model)) { $model->roles()->detach(); } }); } /** * The roles relationship. * * @return \Illuminate\Database\Eloquent\Relations\MorphToMany */ public function roles(): MorphToMany { $relation = $this->morphToMany( Models::classname(Role::class), 'entity', Models::table('assigned_roles') )->withPivot('scope'); return Models::scope()->applyToRelation($relation); } /** * Get all of the model's assigned roles. * * @return \Illuminate\Support\Collection */ public function getRoles() { return Container::getInstance() ->make(Clipboard::class) ->getRoles($this); } /** * Assign the given roles to the model. * * @param \Illuminate\Database\Eloquent\Model|string|array $roles * @return $this */ public function assign($roles) { (new AssignsRoles($roles))->to($this); return $this; } /** * Retract the given roles from the model. * * @param \Illuminate\Database\Eloquent\Model|string|array $roles * @return $this */ public function retract($roles) { (new RemovesRoles($roles))->from($this); return $this; } /** * Check if the model has any of the given roles. * * @param string ...$roles * @return bool */ public function isAn(...$roles) { return Container::getInstance() ->make(Clipboard::class) ->checkRole($this, $roles, 'or'); } /** * Check if the model has any of the given roles. * * Alias for the "isAn" method. * * @param string ...$roles * @return bool */ public function isA(...$roles) { return $this->isAn(...$roles); } /** * Check if the model has none of the given roles. * * @param string ...$roles * @return bool */ public function isNotAn(...$roles) { return Container::getInstance() ->make(Clipboard::class) ->checkRole($this, $roles, 'not'); } /** * Check if the model has none of the given roles. * * Alias for the "isNotAn" method. * * @param string ...$roles * @return bool */ public function isNotA(...$roles) { return $this->isNotAn(...$roles); } /** * Check if the model has all of the given roles. * * @param string ...$roles * @return bool */ public function isAll(...$roles) { return Container::getInstance() ->make(Clipboard::class) ->checkRole($this, $roles, 'and'); } /** * Constrain the given query by the provided role. * * @param \Illuminate\Database\Eloquent\Builder $query * @param string $role * @return void */ public function scopeWhereIs($query, $role) { (new RolesQuery)->constrainWhereIs(...func_get_args()); } /** * Constrain the given query by all provided roles. * * @param \Illuminate\Database\Eloquent\Builder $query * @param string $role * @return void */ public function scopeWhereIsAll($query, $role) { (new RolesQuery)->constrainWhereIsAll(...func_get_args()); } /** * Constrain the given query by the provided role. * * @param \Illuminate\Database\Eloquent\Builder $query * @param string $role * @return void */ public function scopeWhereIsNot($query, $role) { (new RolesQuery)->constrainWhereIsNot(...func_get_args()); } } src/Database/Concerns/IsRole.php 0000644 00000014746 15025033125 0012527 0 ustar 00 <?php namespace Silber\Bouncer\Database\Concerns; use Silber\Bouncer\Helpers; use Silber\Bouncer\Database\Models; use Silber\Bouncer\Database\Titles\RoleTitle; use Silber\Bouncer\Database\Scope\TenantScope; use Silber\Bouncer\Database\Queries\Roles as RolesQuery; use App\User; use Illuminate\Support\Arr; use InvalidArgumentException; use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphToMany; trait IsRole { use HasAbilities, Authorizable; /** * Boot the is role trait. * * @return void */ public static function bootIsRole() { TenantScope::register(static::class); static::creating(function ($role) { Models::scope()->applyToModel($role); if (is_null($role->title)) { $role->title = RoleTitle::from($role)->toString(); } }); static::deleted(function ($role) { $role->abilities()->detach(); }); } /** * The users relationship. * * @return \Illuminate\Database\Eloquent\Relations\MorphedToMany */ public function users(): MorphToMany { $relation = $this->morphedByMany( Models::classname(User::class), 'entity', Models::table('assigned_roles') )->withPivot('scope'); return Models::scope()->applyToRelation($relation); } /** * Assign the role to the given model(s). * * @param string|\Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection $model * @param array|null $keys * @return $this */ public function assignTo($model, array $keys = null) { list($model, $keys) = Helpers::extractModelAndKeys($model, $keys); $query = $this->newBaseQueryBuilder()->from(Models::table('assigned_roles')); $query->insert($this->createAssignRecords($model, $keys)); return $this; } /** * Find the given roles, creating the names that don't exist yet. * * @param iterable $roles * @return \Illuminate\Database\Eloquent\Collection */ public function findOrCreateRoles($roles) { $roles = Helpers::groupModelsAndIdentifiersByType($roles); $roles['integers'] = $this->find($roles['integers']); $roles['strings'] = $this->findOrCreateRolesByName($roles['strings']); return $this->newCollection(Arr::collapse($roles)); } /** * Find roles by name, creating the ones that don't exist. * * @param iterable $names * @return \Illuminate\Database\Eloquent\Collection */ protected function findOrCreateRolesByName($names) { if (empty($names)) { return []; } $existing = static::whereIn('name', $names)->get()->keyBy('name'); return (new Collection($names)) ->diff($existing->pluck('name')) ->map(function ($name) { return static::create(compact('name')); }) ->merge($existing); } /** * Get the IDs of the given roles. * * @param iterable $roles * @return array */ public function getRoleKeys($roles) { $roles = Helpers::groupModelsAndIdentifiersByType($roles); $roles['strings'] = $this->getKeysByName($roles['strings']); $roles['models'] = Arr::pluck($roles['models'], $this->getKeyName()); return Arr::collapse($roles); } /** * Get the names of the given roles. * * @param iterable $roles * @return array */ public function getRoleNames($roles) { $roles = Helpers::groupModelsAndIdentifiersByType($roles); $roles['integers'] = $this->getNamesByKey($roles['integers']); $roles['models'] = Arr::pluck($roles['models'], 'name'); return Arr::collapse($roles); } /** * Get the keys of the roles with the given names. * * @param iterable $names * @return array */ public function getKeysByName($names) { if (empty($names)) { return []; } return $this->whereIn('name', $names) ->select($this->getKeyName())->get() ->pluck($this->getKeyName())->all(); } /** * Get the names of the roles with the given IDs. * * @param iterable $keys * @return array */ public function getNamesByKey($keys) { if (empty($keys)) { return []; } return $this->whereIn($this->getKeyName(), $keys) ->select('name')->get() ->pluck('name')->all(); } /** * Retract the role from the given model(s). * * @param string|\Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection $model * @param array|null $keys * @return $this */ public function retractFrom($model, array $keys = null) { list($model, $keys) = Helpers::extractModelAndKeys($model, $keys); $query = $this->newBaseQueryBuilder() ->from(Models::table('assigned_roles')) ->where('role_id', $this->getKey()) ->where('entity_type', $model->getMorphClass()) ->whereIn('entity_id', $keys); Models::scope()->applyToRelationQuery($query, $query->from); $query->delete(); return $this; } /** * Create the pivot table records for assigning the role to given models. * * @param \Illuminate\Database\Eloquent\Model $model * @param array $keys * @return array */ protected function createAssignRecords(Model $model, array $keys) { $type = $model->getMorphClass(); return array_map(function ($key) use ($type) { return Models::scope()->getAttachAttributes() + [ 'role_id' => $this->getKey(), 'entity_type' => $type, 'entity_id' => $key, ]; }, $keys); } /** * Constrain the given query to roles that were assigned to the given authorities. * * @param \Illuminate\Database\Eloquent\Builder $query * @param string|\Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection $model * @param array $keys * @return void */ public function scopeWhereAssignedTo($query, $model, array $keys = null) { (new RolesQuery)->constrainWhereAssignedTo($query, $model, $keys); } } src/Database/Concerns/IsAbility.php 0000644 00000014342 15025033125 0013213 0 ustar 00 <?php namespace Silber\Bouncer\Database\Concerns; use App\User; use Silber\Bouncer\Database\Role; use Silber\Bouncer\Database\Models; use Silber\Bouncer\Constraints\Group; use Silber\Bouncer\Constraints\constrainer; use Silber\Bouncer\Database\Titles\AbilityTitle; use Silber\Bouncer\Database\Scope\TenantScope; use Silber\Bouncer\Database\Queries\AbilitiesForModel; use Illuminate\Database\Eloquent\Relations\MorphToMany; trait IsAbility { /** * Boot the is ability trait. * * @return void */ public static function bootIsAbility() { TenantScope::register(static::class); static::creating(function ($ability) { Models::scope()->applyToModel($ability); if (is_null($ability->title)) { $ability->title = AbilityTitle::from($ability)->toString(); } }); } /** * Get the options attribute. * * @return array */ public function getOptionsAttribute() { if (empty($this->attributes['options'])) { return []; } return json_decode($this->attributes['options'], true); } /** * Set the "options" attribute. * * @param array $options * @return void */ public function setOptionsAttribute(array $options) { $this->attributes['options'] = json_encode($options); } /** * CHecks if the ability has constraints. * * @return bool */ public function hasConstraints() { return ! empty($this->options['constraints']); } /** * Get the ability's constraints. * * @return \Silber\Bouncer\Constraints\Constrainer */ public function getConstraints() { if (empty($this->options['constraints'])) { return new Group(); } $data = $this->options['constraints']; return $data['class']::fromData($data['params']); } /** * Set the ability's constraints. * * @param \Silber\Bouncer\Constraints\Constrainer $constrainer * @return $this */ public function setConstraints(Constrainer $constrainer) { $this->options = array_merge($this->options, [ 'constraints' => $constrainer->data(), ]); return $this; } /** * Create a new ability for a specific model. * * @param \Illuminate\Database\Eloquent\Model|string $model * @param string|array $attributes * @return static */ public static function createForModel($model, $attributes) { $model = static::makeForModel($model, $attributes); $model->save(); return $model; } /** * Make a new ability for a specific model. * * @param \Illuminate\Database\Eloquent\Model|string $model * @param string|array $attributes * @return static */ public static function makeForModel($model, $attributes) { if (is_string($attributes)) { $attributes = ['name' => $attributes]; } if ($model === '*') { return (new static)->forceFill($attributes + [ 'entity_type' => '*', ]); } if (is_string($model)) { $model = new $model; } return (new static)->forceFill($attributes + [ 'entity_type' => $model->getMorphClass(), 'entity_id' => $model->exists ? $model->getKey() : null, ]); } /** * The roles relationship. * * @return \Illuminate\Database\Eloquent\Relations\MorphToMany */ public function roles(): MorphToMany { $relation = $this->morphedByMany( Models::classname(Role::class), 'entity', Models::table('permissions') )->withPivot('forbidden', 'scope'); return Models::scope()->applyToRelation($relation); } /** * The users relationship. * * @return \Illuminate\Database\Eloquent\Relations\MorphToMany */ public function users(): MorphToMany { $relation = $this->morphedByMany( Models::classname(User::class), 'entity', Models::table('permissions') )->withPivot('forbidden', 'scope'); return Models::scope()->applyToRelation($relation); } /** * Get the identifier for this ability. * * @return string */ final public function getIdentifierAttribute() { $slug = $this->attributes['name']; if ($this->attributes['entity_type'] !== null) { $slug .= '-'.$this->attributes['entity_type']; } if ($this->attributes['entity_id'] !== null) { $slug .= '-'.$this->attributes['entity_id']; } if ($this->attributes['only_owned']) { $slug .= '-owned'; } return strtolower($slug); } /** * Get the ability's "slug" attribute. * * @return string */ public function getSlugAttribute() { return $this->getIdentifierAttribute(); } /** * Constrain a query to having the given name. * * @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder $query * @return string|array $name * @return bool $strict * @return void */ public function scopeByName($query, $name, $strict = false) { $names = (array) $name; if (! $strict && $name !== '*') { $names[] = '*'; } $query->whereIn("{$this->table}.name", $names); } /** * Constrain a query to simple abilities. * * @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder $query * @return void */ public function scopeSimpleAbility($query) { $query->whereNull("{$this->table}.entity_type"); } /** * Constrain a query to an ability for a specific model. * * @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder $query * @param \Illuminate\Database\Eloquent\Model|string $model * @param bool $strict * @return void */ public function scopeForModel($query, $model, $strict = false) { (new AbilitiesForModel)->constrain($query, $model, $strict); } } src/Database/Concerns/Authorizable.php 0000644 00000002207 15025033125 0013750 0 ustar 00 <?php namespace Silber\Bouncer\Database\Concerns; use Illuminate\Container\Container; use Silber\Bouncer\Contracts\Clipboard; trait Authorizable { /** * Determine if the authority has a given ability. * * @param string $ability * @param \Illuminate\Database\Eloquent\Model|null $model * @return bool */ public function can($ability, $model = null) { return Container::getInstance() ->make(Clipboard::class) ->check($this, $ability, $model); } /** * Determine if the authority does not have a given ability. * * @param string $ability * @param \Illuminate\Database\Eloquent\Model|null $model * @return bool */ public function cant($ability, $model = null) { return ! $this->can($ability, $model); } /** * Determine if the authority does not have a given ability. * * @param string $ability * @param \Illuminate\Database\Eloquent\Model|null $model * @return bool */ public function cannot($ability, $model = null) { return $this->cant($ability, $model); } } src/Database/Concerns/HasAbilities.php 0000644 00000007034 15025033125 0013663 0 ustar 00 <?php namespace Silber\Bouncer\Database\Concerns; use Illuminate\Container\Container; use Silber\Bouncer\Helpers; use Silber\Bouncer\Database\Models; use Silber\Bouncer\Database\Ability; use Silber\Bouncer\Contracts\Clipboard; use Silber\Bouncer\Conductors\GivesAbilities; use Silber\Bouncer\Conductors\ForbidsAbilities; use Silber\Bouncer\Conductors\RemovesAbilities; use Silber\Bouncer\Conductors\UnforbidsAbilities; use Illuminate\Database\Eloquent\Relations\MorphToMany; trait HasAbilities { /** * Boot the HasAbilities trait. * * @return void */ public static function bootHasAbilities() { static::deleted(function ($model) { if (! Helpers::isSoftDeleting($model)) { $model->abilities()->detach(); } }); } /** * The abilities relationship. * * @return \Illuminate\Database\Eloquent\Relations\MorphToMany */ public function abilities(): MorphToMany { $relation = $this->morphToMany( Models::classname(Ability::class), 'entity', Models::table('permissions') )->withPivot('forbidden', 'scope'); return Models::scope()->applyToRelation($relation); } /** * Get all of the model's allowed abilities. * * @return \Illuminate\Database\Eloquent\Collection */ public function getAbilities() { return Container::getInstance() ->make(Clipboard::class) ->getAbilities($this); } /** * Get all of the model's allowed abilities. * * @return \Illuminate\Database\Eloquent\Collection */ public function getForbiddenAbilities() { return Container::getInstance() ->make(Clipboard::class) ->getAbilities($this, false); } /** * Give an ability to the model. * * @param mixed $ability * @param mixed|null $model * @return \Silber\Bouncer\Conductors\GivesAbilities|$this */ public function allow($ability = null, $model = null) { if (is_null($ability)) { return new GivesAbilities($this); } (new GivesAbilities($this))->to($ability, $model); return $this; } /** * Remove an ability from the model. * * @param mixed $ability * @param mixed|null $model * @return \Silber\Bouncer\Conductors\RemovesAbilities|$this */ public function disallow($ability = null, $model = null) { if (is_null($ability)) { return new RemovesAbilities($this); } (new RemovesAbilities($this))->to($ability, $model); return $this; } /** * Forbid an ability to the model. * * @param mixed $ability * @param mixed|null $model * @return \Silber\Bouncer\Conductors\ForbidsAbilities|$this */ public function forbid($ability = null, $model = null) { if (is_null($ability)) { return new ForbidsAbilities($this); } (new ForbidsAbilities($this))->to($ability, $model); return $this; } /** * Remove ability forbiddal from the model. * * @param mixed $ability * @param mixed|null $model * @return \Silber\Bouncer\Conductors\UnforbidsAbilities|$this */ public function unforbid($ability = null, $model = null) { if (is_null($ability)) { return new UnforbidsAbilities($this); } (new UnforbidsAbilities($this))->to($ability, $model); return $this; } } src/Database/Scope/TenantScope.php 0000644 00000001577 15025033125 0013052 0 ustar 00 <?php namespace Silber\Bouncer\Database\Scope; use Illuminate\Support\Collection; use Silber\Bouncer\Database\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Scope as EloquentScope; class TenantScope implements EloquentScope { /** * Register the correct global scope class. * * @param string $model * @return void */ public static function register($model) { $model::addGlobalScope(new TenantScope); } /** * Apply the scope to a given Eloquent query builder. * * @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Database\Eloquent\Model $model * @return void */ public function apply(Builder $query, Model $model) { Models::scope()->applyToModelQuery($query, $model->getTable()); } } src/Database/Scope/Scope.php 0000644 00000013705 15025033125 0011674 0 ustar 00 <?php namespace Silber\Bouncer\Database\Scope; use Silber\Bouncer\Database\Role; use Silber\Bouncer\Database\Models; use Silber\Bouncer\Contracts\Scope as ScopeContract; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Scope implements ScopeContract { /** * The tenant ID by which to scope all queries. * * @var mixed */ protected $scope = null; /** * Determines whether the scope is only applied to relationships. * * Set to true if you don't want the scopes on the role/ability models. * * @var mixed */ protected $onlyScopeRelations = false; /** * Determines whether roles' abilities should be scoped. * * @var mixed */ protected $scopeRoleAbilities = true; /** * Scope queries to the given tenant ID. * * @param mixed $id * @return $this */ public function to($id) { $this->scope = $id; return $this; } /** * Only scope relationships. Models should stay global. * * @param bool $boolean * @return $this */ public function onlyRelations($boolean = true) { $this->onlyScopeRelations = $boolean; return $this; } /** * Don't scope abilities granted to roles. * * The role <=> ability associations will be global. * * @return $this */ public function dontScopeRoleAbilities() { $this->scopeRoleAbilities = false; return $this; } /** * Append the tenant ID to the given cache key. * * @param string $key * @return string */ public function appendToCacheKey($key) { return is_null($this->scope) ? $key : $key.'-'.$this->scope; } /** * Scope the given model to the current tenant. * * @param \Illuminate\Database\Eloquent\Model $model * @return \Illuminate\Database\Eloquent\Model */ public function applyToModel(Model $model) { if (! $this->onlyScopeRelations && ! is_null($this->scope)) { $model->scope = $this->scope; } return $model; } /** * Scope the given model query to the current tenant. * * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $query * @param string|null $table * @return \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder */ public function applyToModelQuery($query, $table = null) { if ($this->onlyScopeRelations) { return $query; } if (is_null($table)) { $table = $query->getModel()->getTable(); } return $this->applyToQuery($query, $table); } /** * Scope the given relationship query to the current tenant. * * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $query * @param string $table * @return \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder */ public function applyToRelationQuery($query, $table) { return $this->applyToQuery($query, $table); } /** * Scope the given relation to the current tenant. * * @param \Illuminate\Database\Eloquent\Relations\BelongsToMany $relation * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ public function applyToRelation(BelongsToMany $relation) { $this->applyToRelationQuery( $relation->getQuery(), $relation->getTable() ); return $relation; } /** * Apply the current scope to the given query. * * This internal method does not check whether * the given query needs to be scoped. That * is fully the caller's responsibility. * * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $query * @param string $table * @return \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder */ protected function applyToQuery($query, $table) { return $query->where(function ($query) use ($table) { $query->whereNull("{$table}.scope"); if (! is_null($this->scope)) { $query->orWhere("{$table}.scope", $this->scope); } }); } /** * Get the current scope value. * * @return mixed */ public function get() { return $this->scope; } /** * Get the additional attributes for pivot table records. * * @param string|null $authority * @return array */ public function getAttachAttributes($authority = null) { if (is_null($this->scope)) { return []; } if (! $this->scopeRoleAbilities && $this->isRoleClass($authority)) { return []; } return ['scope' => $this->scope]; } /** * Run the given callback with the given temporary scope. * * @param mixed $scope * @param callable $callback * @return mixed */ public function onceTo($scope, callable $callback) { $mainScope = $this->scope; $this->scope = $scope; $result = $callback(); $this->scope = $mainScope; return $result; } /** * Remove the scope completely. * * @return $this */ public function remove() { $this->scope = null; } /** * Run the given callback without the scope applied. * * @param callable $callback * @return mixed */ public function removeOnce(callable $callback) { return $this->onceTo(null, $callback); } /** * Determine whether the given class name is the role model. * * @param string|null $className * @return bool */ protected function isRoleClass($className) { return Models::classname(Role::class) === $className; } } src/Database/HasRolesAndAbilities.php 0000644 00000000315 15025033125 0013534 0 ustar 00 <?php namespace Silber\Bouncer\Database; use Silber\Bouncer\Database\Concerns\HasRoles; use Silber\Bouncer\Database\Concerns\HasAbilities; trait HasRolesAndAbilities { use HasRoles, HasAbilities; } src/Conductors/RemovesRoles.php 0000644 00000006754 15025033125 0012624 0 ustar 00 <?php namespace Silber\Bouncer\Conductors; use Silber\Bouncer\Helpers; use Silber\Bouncer\Database\Role; use Silber\Bouncer\Database\Models; use Illuminate\Database\Eloquent\Model; class RemovesRoles { /** * The roles to be removed. * * @var array */ protected $roles; /** * Constructor. * * @param \Illuminate\Support\Collection|\Silber\Bouncer\Database\Role|string $roles */ public function __construct($roles) { $this->roles = Helpers::toArray($roles); } /** * Remove the role from the given authority. * * @param \Illuminate\Database\Eloquent\Model|array|int $authority * @return void */ public function from($authority) { if (! ($roleIds = $this->getRoleIds())) { return; } $authorities = is_array($authority) ? $authority : [$authority]; foreach (Helpers::mapAuthorityByClass($authorities) as $class => $keys) { $this->retractRoles($roleIds, $class, $keys); } } /** * Get the IDs of anyexisting roles provided. * * @return array */ protected function getRoleIds() { list($models, $names) = Helpers::partition($this->roles, function ($role) { return $role instanceof Model; }); $ids = $models->map(function ($model) { return $model->getKey(); }); if ($names->count()) { $ids = $ids->merge($this->getRoleIdsFromNames($names->all())); } return $ids->all(); } /** * Get the IDs of the roles with the given names. * * @param string[] $names * @return \Illuminate\Database\Eloquent\Collection */ protected function getRoleIdsFromNames(array $names) { $key = Models::role()->getKeyName(); return Models::role() ->whereIn('name', $names) ->get([$key]) ->pluck($key); } /** * Retract the given roles from the given authorities. * * @param array $roleIds * @param string $authorityClass * @param array $authorityIds * @return void */ protected function retractRoles($roleIds, $authorityClass, $authorityIds) { $query = $this->newPivotTableQuery(); $morphType = (new $authorityClass)->getMorphClass(); foreach ($roleIds as $roleId) { foreach ($authorityIds as $authorityId) { $query->orWhere($this->getDetachQueryConstraint( $roleId, $authorityId, $morphType )); } } $query->delete(); } /** * Get a constraint for the detach query for the given parameters. * * @param mixed $roleId * @param mixed $authorityId * @param string $morphType * @return \Closure */ protected function getDetachQueryConstraint($roleId, $authorityId, $morphType) { return function ($query) use ($roleId, $authorityId, $morphType) { $query->where(Models::scope()->getAttachAttributes() + [ 'role_id' => $roleId, 'entity_id' => $authorityId, 'entity_type' => $morphType, ]); }; } /** * Get a query builder instance for the assigned roles pivot table. * * @return \Illuminate\Database\Query\Builder */ protected function newPivotTableQuery() { return Models::query('assigned_roles'); } } src/Conductors/UnforbidsAbilities.php 0000644 00000001222 15025033125 0013741 0 ustar 00 <?php namespace Silber\Bouncer\Conductors; class UnforbidsAbilities { use Concerns\DisassociatesAbilities; /** * The authority from which to remove the forbiddal. * * @var \Illuminate\Database\Eloquent\Model|string|null */ protected $authority; /** * The constraints to use for the detach abilities query. * * @var array */ protected $constraints = ['forbidden' => true]; /** * Constructor. * * @param \Illuminate\Database\Eloquent\Model|string|null $authority */ public function __construct($authority = null) { $this->authority = $authority; } } src/Conductors/ForbidsAbilities.php 0000644 00000001320 15025033125 0013375 0 ustar 00 <?php namespace Silber\Bouncer\Conductors; use Silber\Bouncer\Database\Models; use Illuminate\Database\Eloquent\Model; class ForbidsAbilities { use Concerns\AssociatesAbilities; /** * The authority to be forbidden from the abilities. * * @var \Illuminate\Database\Eloquent\Model|string|null */ protected $authority; /** * Whether the associated abilities should be forbidden abilities. * * @var bool */ protected $forbidding = true; /** * Constructor. * * @param \Illuminate\Database\Eloquent\Model|string|null $authority */ public function __construct($authority = null) { $this->authority = $authority; } } src/Conductors/ChecksRoles.php 0000644 00000003744 15025033125 0012400 0 ustar 00 <?php namespace Silber\Bouncer\Conductors; use Illuminate\Database\Eloquent\Model; use Silber\Bouncer\Contracts\Clipboard; class ChecksRoles { /** * The authority against which to check for roles. * * @var \Illuminate\Database\Eloquent\Model */ protected $authority; /** * The bouncer clipboard instance. * * @var \Silber\Bouncer\Contracts\Clipboard */ protected $clipboard; /** * Constructor. * * @param \Illuminate\Database\Eloquent\Model $authority * @param \Silber\Bouncer\Contracts\Clipboard $clipboard */ public function __construct(Model $authority, Clipboard $clipboard) { $this->authority = $authority; $this->clipboard = $clipboard; } /** * Check if the authority has any of the given roles. * * @param string ...$roles * @return bool */ public function a(...$roles) { return $this->clipboard->checkRole($this->authority, $roles, 'or'); } /** * Check if the authority doesn't have any of the given roles. * * @param string ...$roles * @return bool */ public function notA(...$roles) { return $this->clipboard->checkRole($this->authority, $roles, 'not'); } /** * Alias to the "a" method. * * @param string ...$roles * @return bool */ public function an(...$roles) { return $this->clipboard->checkRole($this->authority, $roles, 'or'); } /** * Alias to the "notA" method. * * @param string ...$roles * @return bool */ public function notAn(...$roles) { return $this->clipboard->checkRole($this->authority, $roles, 'not'); } /** * Check if the authority has all of the given roles. * * @param string ...$roles * @return bool */ public function all(...$roles) { return $this->clipboard->checkRole($this->authority, $roles, 'and'); } } src/Conductors/RemovesAbilities.php 0000644 00000001215 15025033125 0013430 0 ustar 00 <?php namespace Silber\Bouncer\Conductors; class RemovesAbilities { use Concerns\DisassociatesAbilities; /** * The authority from which to remove abilities. * * @var \Illuminate\Database\Eloquent\Model|string|null */ protected $authority; /** * The constraints to use for the detach abilities query. * * @var array */ protected $constraints = ['forbidden' => false]; /** * Constructor. * * @param \Illuminate\Database\Eloquent\Model|string|null $authority */ public function __construct($authority = null) { $this->authority = $authority; } } src/Conductors/Lazy/ConductsAbilities.php 0000644 00000002736 15025033125 0014522 0 ustar 00 <?php namespace Silber\Bouncer\Conductors\Lazy; class ConductsAbilities { /** * The conductor handling the permission. * * @var \Silber\Bouncer\Conductors\Concerns\ConductsAbilities */ protected $conductor; /** * The abilities to which ownership is restricted. * * @var string|string[] */ protected $abilities; /** * Determines whether the given abilities should be granted on all models. * * @var bool */ protected $everything = false; /** * The extra attributes for the abilities. * * @var array */ protected $attributes = []; /** * Constructor. * * @param \Silber\Bouncer\Conductors\Concerns\ConductsAbilities $conductor * @param mixed $model * @param array $attributes */ public function __construct($conductor, $abilities) { $this->conductor = $conductor; $this->abilities = $abilities; } /** * Sets that the abilities should be applied towards everything. * * @param array $attributes * @return void */ public function everything(array $attributes = []) { $this->everything = true; $this->attributes = $attributes; } /** * Destructor. * */ public function __destruct() { $this->conductor->to( $this->abilities, $this->everything ? '*' : null, $this->attributes ); } } src/Conductors/Lazy/HandlesOwnership.php 0000644 00000003033 15025033125 0014356 0 ustar 00 <?php namespace Silber\Bouncer\Conductors\Lazy; class HandlesOwnership { /** * The conductor handling the permission. * * @var \Silber\Bouncer\Conductors\Concerns\ConductsAbilities */ protected $conductor; /** * The model to be owned. * * @var string|object */ protected $model; /** * The extra attributes for the ability. * * @var array */ protected $attributes; /** * The abilities to which ownership is restricted. * * @var string|string[] */ protected $ability = '*'; /** * Constructor. * * @param \Silber\Bouncer\Conductors\Concerns\ConductsAbilities $conductor * @param string|object $model * @param array $attributes */ public function __construct($conductor, $model, array $attributes = []) { $this->conductor = $conductor; $this->model = $model; $this->attributes = $attributes; } /** * Limit ownership to the given ability. * * @param string|string[] $ability * @param array $attributes * @return void */ public function to($ability, array $attributes = []) { $this->ability = $ability; $this->attributes = array_merge($this->attributes, $attributes); } /** * Destructor. * */ public function __destruct() { $this->conductor->to( $this->ability, $this->model, $this->attributes + ['only_owned' => true] ); } } src/Conductors/GivesAbilities.php 0000644 00000001302 15025033125 0013062 0 ustar 00 <?php namespace Silber\Bouncer\Conductors; use Silber\Bouncer\Database\Models; use Illuminate\Database\Eloquent\Model; class GivesAbilities { use Concerns\AssociatesAbilities; /** * The authority to be given abilities. * * @var \Illuminate\Database\Eloquent\Model|string|null */ protected $authority; /** * Whether the associated abilities should be forbidden abilities. * * @var bool */ protected $forbidding = false; /** * Constructor. * * @param \Illuminate\Database\Eloquent\Model|string|null $authority */ public function __construct($authority = null) { $this->authority = $authority; } } src/Conductors/SyncsRolesAndAbilities.php 0000644 00000012347 15025033125 0014547 0 ustar 00 <?php namespace Silber\Bouncer\Conductors; use Silber\Bouncer\Helpers; use Silber\Bouncer\Database\Models; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; class SyncsRolesAndAbilities { use Concerns\FindsAndCreatesAbilities; /** * The authority for whom to sync roles/abilities. * * @var \Illuminate\Database\Eloquent\Model|string */ protected $authority; /** * Constructor. * * @param \Illuminate\Database\Eloquent\Model|string $authority */ public function __construct($authority) { $this->authority = $authority; } /** * Sync the provided roles to the authority. * * @param iterable $roles * @return void */ public function roles($roles) { $this->sync( Models::role()->getRoleKeys($roles), $this->authority->roles() ); } /** * Sync the provided abilities to the authority. * * @param iterable $abilities * @return void */ public function abilities($abilities) { $this->syncAbilities($abilities); } /** * Sync the provided forbidden abilities to the authority. * * @param iterable $abilities * @return void */ public function forbiddenAbilities($abilities) { $this->syncAbilities($abilities, ['forbidden' => true]); } /** * Sync the given abilities for the authority. * * @param iterable $abilities * @param array $options * @return void */ protected function syncAbilities($abilities, $options = ['forbidden' => false]) { $abilityKeys = $this->getAbilityIds($abilities); $authority = $this->getAuthority(); $relation = $authority->abilities(); $this->newPivotQuery($relation) ->where('entity_type', $authority->getMorphClass()) ->whereNotIn($relation->getRelatedPivotKeyName(), $abilityKeys) ->where('forbidden', $options['forbidden']) ->delete(); if ($options['forbidden']) { (new ForbidsAbilities($this->authority))->to($abilityKeys); } else { (new GivesAbilities($this->authority))->to($abilityKeys); } } /** * Get (and cache) the authority for whom to sync roles/abilities. * * @return \Illuminate\Database\Eloquent\Model */ protected function getAuthority() { if (is_string($this->authority)) { $this->authority = Models::role()->firstOrCreate([ 'name' => $this->authority ]); } return $this->authority; } /** * Get the fully qualified column name for the abilities table's primary key. * * @return string */ protected function getAbilitiesQualifiedKeyName() { $model = Models::ability(); return $model->getTable().'.'.$model->getKeyName(); } /** * Sync the given IDs on the pivot relation. * * This is a heavily-modified version of Eloquent's built-in * BelongsToMany@sync - which we sadly cannot use because * our scope sets a "closure where" on the pivot table. * * @param array $ids * @param \Illuminate\Database\Eloquent\Relations\BelongsToMany $relation * @return void */ protected function sync(array $ids, BelongsToMany $relation) { $current = $this->pluck( $this->newPivotQuery($relation), $relation->getRelatedPivotKeyName() ); $this->detach(array_diff($current, $ids), $relation); $relation->attach( array_diff($ids, $current), Models::scope()->getAttachAttributes($this->authority) ); } /** * Detach the records with the given IDs from the relationship. * * @param array $ids * @param \Illuminate\Database\Eloquent\Relations\BelongsToMany $relation * @return void */ public function detach(array $ids, BelongsToMany $relation) { if (empty($ids)) { return; } $this->newPivotQuery($relation) ->whereIn($relation->getRelatedPivotKeyName(), $ids) ->delete(); } /** * Get a scoped query for the pivot table. * * @param \Illuminate\Database\Eloquent\Relations\BelongsToMany $relation * @return \Illuminate\Database\Query\Builder */ protected function newPivotQuery(BelongsToMany $relation) { $query = $relation ->newPivotStatement() ->where('entity_type', $this->getAuthority()->getMorphClass()) ->where( $relation->getForeignPivotKeyName(), $relation->getParent()->getKey() ); return Models::scope()->applyToRelationQuery( $query, $relation->getTable() ); } /** * Pluck the values of the given column using the provided query. * * @param mixed $query * @param string $column * @return string[] */ protected function pluck($query, $column) { return Arr::pluck($query->get([$column]), last(explode('.', $column))); } } src/Conductors/Concerns/ConductsAbilities.php 0000644 00000005103 15025033125 0015344 0 ustar 00 <?php namespace Silber\Bouncer\Conductors\Concerns; use Silber\Bouncer\Helpers; use Illuminate\Support\Collection; use Silber\Bouncer\Conductors\Lazy; trait ConductsAbilities { /** * Allow/disallow all abilities on everything. * * @param array $attributes * @return mixed */ public function everything(array $attributes = []) { return $this->to('*', '*', $attributes); } /** * Allow/disallow all abilities on the given model. * * @param string|array|\Illuminate\Database\Eloquent\Model $models * @param array $attributes * @return void */ public function toManage($models, array $attributes = []) { if (is_array($models)) { foreach ($models as $model) { $this->to('*', $model, $attributes); } } else { $this->to('*', $models, $attributes); } } /** * Allow/disallow owning the given model. * * @param string|object $model * @param array $attributes * @return \Silber\Bouncer\Conductors\Lazy\HandlesOwnership */ public function toOwn($model, array $attributes = []) { return new Lazy\HandlesOwnership($this, $model, $attributes); } /** * Allow/disallow owning all models. * * @param array $attributes * @return \Silber\Bouncer\Conductors\Lazy\HandlesOwnership */ public function toOwnEverything(array $attributes = []) { return $this->toOwn('*', $attributes); } /** * Determines whether a call to "to" with the given parameters should be conducted lazily. * * @param mixed $abilities * @param mixed $model * @return bool */ protected function shouldConductLazy($abilities) { // We'll only create a lazy conductor if we got a single // param, and that single param is either a string or // a numerically-indexed array (of simple strings). if (func_num_args() > 1) { return false; } if (is_string($abilities)) { return true; } if (! is_array($abilities) || ! Helpers::isIndexedArray($abilities)) { return false; } return (new Collection($abilities))->every('is_string'); } /** * Create a lazy abilities conductor. * * @param string|string[] $ablities * @return \Silber\Bouncer\Conductors\Lazy\ConductsAbilities */ protected function conductLazy($abilities) { return new Lazy\ConductsAbilities($this, $abilities); } } src/Conductors/Concerns/FindsAndCreatesAbilities.php 0000644 00000015774 15025033125 0016576 0 ustar 00 <?php namespace Silber\Bouncer\Conductors\Concerns; use Silber\Bouncer\Helpers; use Silber\Bouncer\Database\Models; use Illuminate\Support\Arr; use InvalidArgumentException; use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Model; trait FindsAndCreatesAbilities { /** * Get the IDs of the provided abilities. * * @param \Illuminate\Database\Eloquent\model|array|int $abilities * @param \Illuminate\Database\Eloquent\Model|string|array|null $model * @param array $attributes * @return array */ protected function getAbilityIds($abilities, $model = null, array $attributes = []) { if ($abilities instanceof Model) { return [$abilities->getKey()]; } if ( ! is_null($model)) { return $this->getModelAbilityKeys($abilities, $model, $attributes); } if (Helpers::isAssociativeArray($abilities)) { return $this->getAbilityIdsFromMap($abilities, $attributes); } if (! is_array($abilities) && ! $abilities instanceof Collection) { $abilities = [$abilities]; } return $this->getAbilityIdsFromArray($abilities, $attributes); } /** * Get the ability IDs for the given map. * * The map should use the ['ability-name' => Entity::class] format. * * @param array $map * @param array $attributes * @return array */ protected function getAbilityIdsFromMap(array $map, array $attributes) { list($map, $list) = Helpers::partition($map, function ($value, $key) { return ! is_int($key); }); return $map->map(function ($entity, $ability) use ($attributes) { return $this->getAbilityIds($ability, $entity, $attributes); })->collapse()->merge($this->getAbilityIdsFromArray($list, $attributes))->all(); } /** * Get the ability IDs from the provided array, creating the ones that don't exist. * * @param iterable $abilities * @param array $attributes * @return array */ protected function getAbilityIdsFromArray($abilities, array $attributes) { $groups = Helpers::groupModelsAndIdentifiersByType($abilities); $keyName = Models::ability()->getKeyName(); $groups['strings'] = $this->abilitiesByName($groups['strings'], $attributes) ->pluck($keyName)->all(); $groups['models'] = Arr::pluck($groups['models'], $keyName); return Arr::collapse($groups); } /** * Get the abilities for the given model ability descriptors. * * @param array|string $abilities * @param \Illuminate\Database\Eloquent\Model|string|array $model * @param array $attributes * @return array */ protected function getModelAbilityKeys($abilities, $model, array $attributes) { $abilities = Collection::make(is_array($abilities) ? $abilities : [$abilities]); $models = Collection::make(is_array($model) ? $model : [$model]); return $abilities->map(function ($ability) use ($models, $attributes) { return $models->map(function ($model) use ($ability, $attributes) { return $this->getModelAbility($ability, $model, $attributes)->getKey(); }); })->collapse()->all(); } /** * Get an ability for the given entity. * * @param string $ability * @param \Illuminate\Database\Eloquent\Model|string $entity * @param array $attributes * @return \Silber\Bouncer\Database\Ability */ protected function getModelAbility($ability, $entity, array $attributes) { $entity = $this->getEntityInstance($entity); $existing = $this->findAbility($ability, $entity, $attributes); return $existing ?: $this->createAbility($ability, $entity, $attributes); } /** * Find the ability for the given entity. * * @param string $ability * @param \Illuminate\Database\Eloquent\Model|string $entity * @param array $attributes * @return \Silber\Bouncer\Database\Ability|null */ protected function findAbility($ability, $entity, $attributes) { $onlyOwned = isset($attributes['only_owned']) ? $attributes['only_owned'] : false; $query = Models::ability() ->where('name', $ability) ->forModel($entity, true) ->where('only_owned', $onlyOwned); return Models::scope()->applyToModelQuery($query)->first(); } /** * Create an ability for the given entity. * * @param string $ability * @param \Illuminate\Database\Eloquent\Model|string $entity * @param array $attributes * @return \Silber\Bouncer\Database\Ability */ protected function createAbility($ability, $entity, $attributes) { return Models::ability()->createForModel($entity, $attributes + [ 'name' => $ability, ]); } /** * Get an instance of the given model. * * @param \Illuminate\Database\Eloquent\Model|string $model * @return \Illuminate\Database\Eloquent\Model|string */ protected function getEntityInstance($model) { if ($model === '*') { return '*'; } if ( ! $model instanceof Model) { return new $model; } // Creating an ability for a non-existent model gives the authority that // ability on all instances of that model. If the developer passed in // a model instance that does not exist, it is probably a mistake. if ( ! $model->exists) { throw new InvalidArgumentException( 'The model does not exist. To edit access to all models, use the class name instead' ); } return $model; } /** * Get or create abilities by their name. * * @param array|string $abilities * @param array $attributes * @return \Illuminate\Support\Collection */ protected function abilitiesByName($abilities, $attributes = []) { $abilities = array_unique(is_array($abilities) ? $abilities : [$abilities]); if (empty($abilities)) { return new Collection; } $existing = Models::ability()->simpleAbility()->whereIn('name', $abilities)->get(); return $existing->merge($this->createMissingAbilities( $existing, $abilities, $attributes )); } /** * Create the non-existant abilities by name. * * @param \Illuminate\Database\Eloquent\Collection $existing * @param string[] $abilities * @param array $attributes * @return array */ protected function createMissingAbilities($existing, array $abilities, $attributes = []) { $missing = array_diff($abilities, $existing->pluck('name')->all()); return array_map(function ($ability) use ($attributes) { return Models::ability()->create($attributes + ['name' => $ability]); }, $missing); } } src/Conductors/Concerns/DisassociatesAbilities.php 0000644 00000007305 15025033125 0016366 0 ustar 00 <?php namespace Silber\Bouncer\Conductors\Concerns; use Silber\Bouncer\Database\Models; use Silber\Bouncer\Database\Ability; use InvalidArgumentException; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Relations\MorphToMany; trait DisassociatesAbilities { use ConductsAbilities, FindsAndCreatesAbilities; /** * Remove the given ability from the model. * * @param mixed $abilities * @param \Illuminate\Database\Eloquent\Model|string|null $entity * @param array $attributes * @return bool|\Silber\Bouncer\Conductors\Lazy\ConductsAbilities */ public function to($abilities, $entity = null, array $attributes = []) { if ($this->shouldConductLazy(...func_get_args())) { return $this->conductLazy($abilities); } if ($ids = $this->getAbilityIds($abilities, $entity, $attributes)) { $this->disassociateAbilities($this->getAuthority(), $ids); } return true; } /** * Detach the given IDs from the authority. * * @param \Illuminate\Database\Eloquent\Model|null $authority * @param array $ids * @return void */ protected function disassociateAbilities($authority, array $ids) { if (is_null($authority)) { $this->disassociateEveryone($ids); } else { $this->disassociateAuthority($authority, $ids); } } /** * Disassociate the authority from the abilities with the given IDs. * * @param \Illuminate\Database\Eloquent\Model $authority * @param array $ids * @return void */ protected function disassociateAuthority(Model $authority, array $ids) { $this->getAbilitiesPivotQuery($authority, $ids) ->where($this->constraints()) ->delete(); } /** * Get the base abilities pivot query. * * @param \Illuminate\Database\Eloquent\Model $model * @param array $ids * @return \Illuminate\Database\Query\Builder */ protected function getAbilitiesPivotQuery(Model $model, $ids) { $relation = $model->abilities(); $query = $relation ->newPivotStatement() ->where($relation->getQualifiedForeignPivotKeyName(), $model->getKey()) ->where('entity_type', $model->getMorphClass()) ->whereIn($relation->getQualifiedRelatedPivotKeyName(), $ids); return Models::scope()->applyToRelationQuery( $query, $relation->getTable() ); } /** * Disassociate everyone from the abilities with the given IDs. * * @param array $ids * @return void */ protected function disassociateEveryone(array $ids) { $query = Models::query('permissions') ->whereNull('entity_id') ->where($this->constraints()) ->whereIn('ability_id', $ids); Models::scope()->applyToRelationQuery($query, $query->from); $query->delete(); } /** * Get the authority from which to disassociate the abilities. * * @return \Illuminate\Database\Eloquent\Model|null */ protected function getAuthority() { if (is_null($this->authority)) { return null; } if ($this->authority instanceof Model) { return $this->authority; } return Models::role()->where('name', $this->authority)->firstOrFail(); } /** * Get the additional constraints for the detaching query. * * @return array */ protected function constraints() { return property_exists($this, 'constraints') ? $this->constraints : []; } } src/Conductors/Concerns/AssociatesAbilities.php 0000644 00000010220 15025033125 0015654 0 ustar 00 <?php namespace Silber\Bouncer\Conductors\Concerns; use Illuminate\Support\Arr; use Silber\Bouncer\Database\Models; use Illuminate\Database\Eloquent\Model; trait AssociatesAbilities { use ConductsAbilities, FindsAndCreatesAbilities; /** * Associate the abilities with the authority. * * @param \Illuminate\Database\Eloquent\model|array|int $abilities * @param \Illuminate\Database\Eloquent\Model|string|null $model * @param array $attributes * @return \Silber\Bouncer\Conductors\Lazy\ConductsAbilities|null */ public function to($abilities, $model = null, array $attributes = []) { if ($this->shouldConductLazy(...func_get_args())) { return $this->conductLazy($abilities); } $ids = $this->getAbilityIds($abilities, $model, $attributes); $this->associateAbilities($ids, $this->getAuthority()); } /** * Get the authority, creating a role authority if necessary. * * @return \Illuminate\Database\Eloquent\Model|null */ protected function getAuthority() { if (is_null($this->authority)) { return null; } if ($this->authority instanceof Model) { return $this->authority; } return Models::role()->firstOrCreate(['name' => $this->authority]); } /** * Get the IDs of the associated abilities. * * @param \Illuminate\Database\Eloquent\Model|null $authority * @param array $abilityIds * @return array */ protected function getAssociatedAbilityIds($authority, array $abilityIds) { if (is_null($authority)) { return $this->getAbilityIdsAssociatedWithEveryone($abilityIds); } $relation = $authority->abilities(); $table = Models::table('abilities'); $relation->whereIn("{$table}.id", $abilityIds) ->wherePivot('forbidden', '=', $this->forbidding); Models::scope()->applyToRelation($relation); return $relation->get(["{$table}.id"])->pluck('id')->all(); } /** * Get the IDs of the abilities associated with everyone. * * @param array $abilityIds * @return array */ protected function getAbilityIdsAssociatedWithEveryone(array $abilityIds) { $query = Models::query('permissions') ->whereNull('entity_id') ->whereIn('ability_id', $abilityIds) ->where('forbidden', '=', $this->forbidding); Models::scope()->applyToRelationQuery($query, $query->from); return Arr::pluck($query->get(['ability_id']), 'ability_id'); } /** * Associate the given ability IDs on the permissions table. * * @param array $ids * @param \Illuminate\Database\Eloquent\Model|null $authority * @return void */ protected function associateAbilities(array $ids, Model $authority = null) { $ids = array_diff($ids, $this->getAssociatedAbilityIds($authority, $ids, false)); if (is_null($authority)) { $this->associateAbilitiesToEveryone($ids); } else { $this->associateAbilitiesToAuthority($ids, $authority); } } /** * Associate these abilities with the given authority. * * @param array $ids * @param \Illuminate\Database\Eloquent\Model $authority * @return void */ protected function associateAbilitiesToAuthority(array $ids, Model $authority) { $attributes = Models::scope()->getAttachAttributes(get_class($authority)); $authority ->abilities() ->attach($ids, ['forbidden' => $this->forbidding] + $attributes); } /** * Associate these abilities with everyone. * * @param array $ids * @return void */ protected function associateAbilitiesToEveryone(array $ids) { $attributes = ['forbidden' => $this->forbidding]; $attributes += Models::scope()->getAttachAttributes(); $records = array_map(function ($id) use ($attributes) { return ['ability_id' => $id] + $attributes; }, $ids); Models::query('permissions')->insert($records); } } src/Conductors/AssignsRoles.php 0000644 00000010633 15025033125 0012602 0 ustar 00 <?php namespace Silber\Bouncer\Conductors; use Silber\Bouncer\Helpers; use Illuminate\Support\Collection; use Silber\Bouncer\Database\Models; use Illuminate\Database\Eloquent\Model; class AssignsRoles { /** * The roles to be assigned. * * @var array */ protected $roles; /** * Constructor. * * @param \Illuminate\Support\Collection|\Silber\Bouncer\Database\Role|string $roles */ public function __construct($roles) { $this->roles = Helpers::toArray($roles); } /** * Assign the roles to the given authority. * * @param \Illuminate\Database\Eloquent\Model|array|int $authority * @return bool */ public function to($authority) { $authorities = is_array($authority) ? $authority : [$authority]; $roles = Models::role()->findOrCreateRoles($this->roles); foreach (Helpers::mapAuthorityByClass($authorities) as $class => $ids) { $this->assignRoles($roles, $class, new Collection($ids)); } return true; } /** * Assign the given roles to the given authorities. * * @param \Illuminate\Support\Collection $roles * @param string $authorityClass * @param \Illuminate\Support\Collection $authorityIds * @return void */ protected function assignRoles(Collection $roles, $authorityClass, Collection $authorityIds) { $roleIds = $roles->map(function ($model) { return $model->getKey(); }); $morphType = (new $authorityClass)->getMorphClass(); $records = $this->buildAttachRecords($roleIds, $morphType, $authorityIds); $existing = $this->getExistingAttachRecords($roleIds, $morphType, $authorityIds); $this->createMissingAssignRecords($records, $existing); } /** * Get the pivot table records for the roles already assigned. * * @param \Illuminate\Support\Collection $roleIds * @param string $morphType * @param \Illuminate\Support\Collection $authorityIds * @return \Illuminate\Support\Collection */ protected function getExistingAttachRecords($roleIds, $morphType, $authorityIds) { $query = $this->newPivotTableQuery() ->whereIn('role_id', $roleIds->all()) ->whereIn('entity_id', $authorityIds->all()) ->where('entity_type', $morphType); Models::scope()->applyToRelationQuery($query, $query->from); return new Collection($query->get()); } /** * Build the raw attach records for the assigned roles pivot table. * * @param \Illuminate\Support\Collection $roleIds * @param string $morphType * @param \Illuminate\Support\Collection $authorityIds * @return \Illuminate\Support\Collection */ protected function buildAttachRecords($roleIds, $morphType, $authorityIds) { return $roleIds ->crossJoin($authorityIds) ->mapSpread(function ($roleId, $authorityId) use ($morphType) { return Models::scope()->getAttachAttributes() + [ 'role_id' => $roleId, 'entity_id' => $authorityId, 'entity_type' => $morphType, ]; }); } /** * Save the non-existing attach records in the DB. * * @param \Illuminate\Support\Collection $records * @param \Illuminate\Support\Collection $existing * @return void */ protected function createMissingAssignRecords(Collection $records, Collection $existing) { $existing = $existing->keyBy(function ($record) { return $this->getAttachRecordHash((array) $record); }); $records = $records->reject(function ($record) use ($existing) { return $existing->has($this->getAttachRecordHash($record)); }); $this->newPivotTableQuery()->insert($records->all()); } /** * Get a string identifying the given attach record. * * @param array $record * @return string */ protected function getAttachRecordHash(array $record) { return $record['role_id'].$record['entity_id'].$record['entity_type']; } /** * Get a query builder instance for the assigned roles pivot table. * * @return \Illuminate\Database\Query\Builder */ protected function newPivotTableQuery() { return Models::query('assigned_roles'); } } src/BaseClipboard.php 0000644 00000007436 15025033125 0010544 0 ustar 00 <?php namespace Silber\Bouncer; use Silber\Bouncer\Database\Models; use Silber\Bouncer\Database\Queries\Abilities; use InvalidArgumentException; use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Contracts\Auth\Access\Gate; abstract class BaseClipboard implements Contracts\Clipboard { /** * 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); } /** * 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') { $count = $this->countMatchingRoles($authority, $roles); if ($boolean == 'or') { return $count > 0; } elseif ($boolean === 'not') { return $count === 0; } return $count == count((array) $roles); } /** * Count the authority's roles matching the given roles. * * @param \Illuminate\Database\Eloquent\Model $authority * @param array|string $roles * @return int */ protected function countMatchingRoles($authority, $roles) { $lookups = $this->getRolesLookup($authority); return count(array_filter($roles, function ($role) use ($lookups) { switch (true) { case is_string($role): return $lookups['names']->has($role); case is_numeric($role): return $lookups['ids']->has($role); case $role instanceof Model: return $lookups['ids']->has($role->getKey()); } throw new InvalidArgumentException('Invalid model identifier'); })); } /** * Get the given authority's roles' IDs and names. * * @param \Illuminate\Database\Eloquent\Model $authority * @return array */ public function getRolesLookup(Model $authority) { $roles = $authority->roles()->get([ 'name', Models::role()->getQualifiedKeyName() ])->pluck('name', Models::role()->getKeyName()); return ['ids' => $roles, 'names' => $roles->flip()]; } /** * Get the given authority's roles' names. * * @param \Illuminate\Database\Eloquent\Model $authority * @return \Illuminate\Support\Collection */ public function getRoles(Model $authority) { return $this->getRolesLookup($authority)['names']->keys(); } /** * 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); } } src/BouncerFacade.php 0000644 00000006670 15025033125 0010532 0 ustar 00 <?php namespace Silber\Bouncer; use Illuminate\Support\Facades\Facade; /** * @method static \Silber\Bouncer\Conductors\GivesAbilities allow(\Illuminate\Database\Eloquent\Model|string $authority) * @method static \Silber\Bouncer\Conductors\GivesAbilities allowEveryone() * @method static \Silber\Bouncer\Conductors\RemovesAbilities disallow(\Illuminate\Database\Eloquent\Model|string $authority) * @method static \Silber\Bouncer\Conductors\RemovesAbilities disallowEveryone() * @method static \Silber\Bouncer\Conductors\GivesAbilities forbid(\Illuminate\Database\Eloquent\Model|string $authority) * @method static \Silber\Bouncer\Conductors\GivesAbilities forbidEveryone() * @method static \Silber\Bouncer\Conductors\RemovesAbilities unforbid(\Illuminate\Database\Eloquent\Model|string $authority) * @method static \Silber\Bouncer\Conductors\RemovesAbilities unforbidEveryone() * @method static \Silber\Bouncer\Conductors\AssignsRoles assign(\Silber\Bouncer\Database\Role|\Illuminate\Support\Collection|string $roles) * @method static \Silber\Bouncer\Conductors\RemovesRoles retract(\Illuminate\Support\Collection|\Silber\Bouncer\Database\Role|string $roles) * @method static \Silber\Bouncer\Conductors\SyncsRolesAndAbilities sync(\Illuminate\Database\Eloquent\Model|string $authority) * @method static \Silber\Bouncer\Conductors\ChecksRoles is(\Illuminate\Database\Eloquent\Model $authority) * @method static \Silber\Bouncer\Contracts\Clipboard getClipboard() * @method static self setClipboard(\Silber\Bouncer\Contracts\Clipboard $clipboard) * @method static self registerClipboardAtContainer() * @method static self cache(null|\Illuminate\Contracts\Cache\Store $cache) * @method static self dontCache() * @method static self refresh(null|\Illuminate\Database\Eloquent\Model $authority) * @method static self refreshFor(\Illuminate\Database\Eloquent\Model $authority) * @method static self setGate(\Illuminate\Contracts\Auth\Access\Gate $gate) * @method static \Illuminate\Contracts\Auth\Access\Gate|null getGate() * @method static \Illuminate\Contracts\Auth\Access\Gate gate() * @method static bool usesCachedClipboard() * @method static self define(string $ability, callable|string $callback) * @method static \Illuminate\Auth\Access\Response authorize(string $ability, array|mixed $arguments) * @method static bool can(string $ability, array|mixed $arguments) * @method static bool canAny(array $abilities, array|mixed $arguments) * @method static bool cannot(string $ability, array|mixed $arguments) * @method static bool allows(string $ability, array|mixed $arguments) * @method static bool denies(string $ability, array|mixed $arguments) * @method static \Silber\Bouncer\Database\Role role(array $attributes) * @method static \Silber\Bouncer\Database\Ability ability(array $attributes) * @method static self runBeforePolicies(bool $boolean) * @method static self ownedVia(string|\Closure $model, string|\Closure|null $attribute) * @method static self useAbilityModel(string $model) * @method static self useRoleModel(string $model) * @method static self useUserModel(string $model) * @method static self tables(array $map) * @method static mixed scope(null|\Silber\Bouncer\Contracts\Scope $scope) * * @see \Silber\Bouncer\Bouncer */ class BouncerFacade extends Facade { /** * Get the registered name of the component. * * @return string */ protected static function getFacadeAccessor() { return Bouncer::class; } } src/Clipboard.php 0000644 00000006455 15025033125 0007751 0 ustar 00 <?php namespace Silber\Bouncer; use Illuminate\Database\Eloquent\Model; use Silber\Bouncer\Database\Queries\Abilities; class Clipboard extends BaseClipboard { /** * 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 */ public function checkGetId(Model $authority, $ability, $model = null) { if ($this->isForbidden($authority, $ability, $model)) { return false; } $ability = $this->getAllowingAbility($authority, $ability, $model); return $ability ? $ability->getKey() : null; } /** * Determine whether the given ability request is explicitely forbidden. * * @param \Illuminate\Database\Eloquent\Model $authority * @param string $ability * @param \Illuminate\Database\Eloquent\Model|string|null $model * @return bool */ protected function isForbidden(Model $authority, $ability, $model = null) { return $this->getHasAbilityQuery( $authority, $ability, $model, $allowed = false )->exists(); } /** * Get the ability model that allows the given ability request. * * Returns null if the ability is not allowed. * * @param \Illuminate\Database\Eloquent\Model $authority * @param string $ability * @param \Illuminate\Database\Eloquent\Model|string|null $model * @return \Illuminate\Database\Eloquent\Model|null */ protected function getAllowingAbility(Model $authority, $ability, $model = null) { return $this->getHasAbilityQuery( $authority, $ability, $model, $allowed = true )->first(); } /** * Get the query for where the given authority has the given ability. * * @param \Illuminate\Database\Eloquent\Model $authority * @param string $ability * @param \Illuminate\Database\Eloquent\Model|string|null $model * @param bool $allowed * @return \Illuminate\Database\Eloquent\Builder */ protected function getHasAbilityQuery($authority, $ability, $model, $allowed) { $query = Abilities::forAuthority($authority, $allowed); if (! $this->isOwnedBy($authority, $model)) { $query->where('only_owned', false); } if (is_null($model)) { return $this->constrainToSimpleAbility($query, $ability); } return $query->byName($ability)->forModel($model); } /** * Constrain the query to the given non-model ability. * * @param \Illuminate\Database\Eloquent\Builder $query * @param string $ability * @return \Illuminate\Database\Eloquent\Builder */ protected function constrainToSimpleAbility($query, $ability) { return $query->where(function ($query) use ($ability) { $query->where('name', $ability)->whereNull('entity_type'); $query->orWhere(function ($query) use ($ability) { $query->where('name', '*')->where(function ($query) { $query->whereNull('entity_type')->orWhere('entity_type', '*'); }); }); }); } } src/Console/CleanCommand.php 0000644 00000011504 15025033125 0011764 0 ustar 00 <?php namespace Silber\Bouncer\Console; use Illuminate\Support\Str; use Illuminate\Console\Command; use Illuminate\Support\Collection; use Silber\Bouncer\Database\Models; use Illuminate\Database\Eloquent\Relations\Relation; class CleanCommand extends Command { /** * The name and signature of the console command. * * @var string */ protected $signature = 'bouncer:clean {--u|unassigned : Whether to delete abilities not assigned to anyone} {--o|orphaned : Whether to delete abilities for missing models}'; /** * The console command description. * * @var string */ protected $description = 'Delete abilities that are no longer in use'; /** * Execute the console command. * * @return void */ public function handle() { [$unassigned, $orphaned] = $this->getComputedOptions(); if ($unassigned) { $this->deleteUnassignedAbilities(); } if ($orphaned) { $this->deleteOrphanedAbilities(); } } /** * Get the options to use, computed by omission. * * @return array */ protected function getComputedOptions() { $unassigned = $this->option('unassigned'); $orphaned = $this->option('orphaned'); if (! $unassigned && ! $orphaned) { $unassigned = $orphaned = true; } return [$unassigned, $orphaned]; } /** * Delete abilities not assigned to anyone. * * @return void */ protected function deleteUnassignedAbilities() { $query = $this->getUnassignedAbilitiesQuery(); if (($count = $query->count()) > 0) { $query->delete(); $this->info("Deleted {$count} unassigned ".Str::plural('ability', $count).'.'); } else { $this->info('No unassigned abilities.'); } } /** * Get the base query for all unassigned abilities. * * @return \Illuminate\Database\Eloquent\Query */ protected function getUnassignedAbilitiesQuery() { $model = Models::ability(); return $model->whereNotIn($model->getKeyName(), function ($query) { $query->from(Models::table('permissions'))->select('ability_id'); }); } /** * Delete model abilities whose models have been deleted. * * @return void */ protected function deleteOrphanedAbilities() { $query = $this->getBaseOrphanedQuery()->where(function ($query) { foreach ($this->getEntityTypes() as $entityType) { $query->orWhere(function ($query) use ($entityType) { $this->scopeQueryToWhereModelIsMissing($query, $entityType); }); } }); if (($count = $query->count()) > 0) { $query->delete(); $this->info("Deleted {$count} orphaned ".Str::plural('ability', $count).'.'); } else { $this->info('No orphaned abilities.'); } } /** * Scope the given query to where the ability's model is missing. * * @param \Illuminate\Database\Query\Builder $query * @param string $entityType * @return void */ protected function scopeQueryToWhereModelIsMissing($query, $entityType) { $model = $this->makeModel($entityType); $abilities = $this->abilitiesTable(); $query->where("{$abilities}.entity_type", $entityType); $query->whereNotIn("{$abilities}.entity_id", function ($query) use ($model) { $table = $model->getTable(); $query->from($table)->select($table.'.'.$model->getKeyName()); }); } /** * Get the entity types of all model abilities. * * @return iterable */ protected function getEntityTypes() { return $this ->getBaseOrphanedQuery() ->distinct() ->pluck('entity_type'); } /** * Get the base query for abilities with missing models. * * @return \Illuminate\Database\Eloquent\Builder */ protected function getBaseOrphanedQuery() { $table = $this->abilitiesTable(); return Models::ability() ->whereNotNull("{$table}.entity_id") ->where("{$table}.entity_type", '!=', '*'); } /** * Get the name of the abilities table. * * @return string */ protected function abilitiesTable() { return Models::ability()->getTable(); } /** * Get an instance of the model for the given entity type. * * @param string $entityType * @return string */ protected function makeModel($entityType) { $class = Relation::getMorphedModel($entityType) ?? $entityType; return new $class; } } src/Contracts/CachedClipboard.php 0000644 00000002226 15025033125 0012771 0 ustar 00 <?php namespace Silber\Bouncer\Contracts; use Illuminate\Contracts\Cache\Store; use Illuminate\Database\Eloquent\Model; interface CachedClipboard extends Clipboard { /** * Set the cache instance. * * @param \Illuminate\Contracts\Cache\Store $cache * @return $this */ public function setCache(Store $cache); /** * Get the cache instance. * * @return \Illuminate\Contracts\Cache\Store */ public function getCache(); /** * 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, $allowed); /** * Clear the cache. * * @param null|\Illuminate\Database\Eloquent\Model $authority * @return $this */ public function refresh($authority = null); /** * Clear the cache for the given authority. * * @param \Illuminate\Database\Eloquent\Model $authority * @return $this */ public function refreshFor(Model $authority); } src/Contracts/Clipboard.php 0000644 00000003636 15025033125 0011707 0 ustar 00 <?php namespace Silber\Bouncer\Contracts; use Illuminate\Database\Eloquent\Model; use Illuminate\Contracts\Auth\Access\Gate; interface Clipboard { /** * 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); /** * 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 */ public 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'); /** * Get the given authority's roles. * * @param \Illuminate\Database\Eloquent\Model $authority * @return \Illuminate\Support\Collection */ 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); /** * 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); } src/Contracts/Scope.php 0000644 00000004566 15025033125 0011064 0 ustar 00 <?php namespace Silber\Bouncer\Contracts; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; interface Scope { /** * Append the tenant ID to the given cache key. * * @param string $key * @return string */ public function appendToCacheKey($key); /** * Scope the given model to the current tenant. * * @param \Illuminate\Database\Eloquent\Model $model * @return \Illuminate\Database\Eloquent\Model */ public function applyToModel(Model $model); /** * Scope the given model query to the current tenant. * * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $query * @param string|null $table * @return \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder */ public function applyToModelQuery($query, $table = null); /** * Scope the given relationship query to the current tenant. * * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $query * @param string $table * @return \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder */ public function applyToRelationQuery($query, $table); /** * Scope the given relation to the current tenant. * * @param \Illuminate\Database\Eloquent\Relations\BelongsToMany $relation * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ public function applyToRelation(BelongsToMany $relation); /** * Get the current scope value. * * @return mixed */ public function get(); /** * Get the additional attributes for pivot table records. * * @param string|null $authority * @return array */ public function getAttachAttributes($authority = null); /** * Run the given callback with the given temporary scope. * * @param mixed $scope * @param callable $callback * @return mixed */ public function onceTo($scope, callable $callback); /** * Remove the scope completely. * * @return $this */ public function remove(); /** * Run the given callback without the scope applied. * * @param callable $callback * @return mixed */ public function removeOnce(callable $callback); } src/Helpers.php 0000644 00000013012 15025033125 0007437 0 ustar 00 <?php namespace Silber\Bouncer; use Silber\Bouncer\Database\Models; use App\User; use InvalidArgumentException; use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Model; class Helpers { /** * Ensure that the given logical operator is 'and'|'or'. * * @param string $operator * @return void * * @throws \InvalidArgumentException */ public static function ensureValidLogicalOperator($operator) { if (! in_array($operator, ['and', 'or'])) { throw new InvalidArgumentException( "{$logicalOperator} is an invalid logical operator" ); } } /** * Extract the model instance and model keys from the given parameters. * * @param \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection|string $model * @param array|null $keys * @return array */ public static function extractModelAndKeys($model, array $keys = null) { if (! is_null($keys)) { if (is_string($model)) { $model = new $model; } return [$model, $keys]; } if ($model instanceof Model) { return [$model, [$model->getKey()]]; } if ($model instanceof Collection) { $keys = $model->map(function ($model) { return $model->getKey(); }); return [$model->first(), $keys]; } } /** * Fill the given array with the given value for any missing keys. * * @param iterable $array * @param mixed $value * @param iterable $keys * @return iterable */ public static function fillMissingKeys($array, $value, $keys) { foreach ($keys as $key) { if (! array_key_exists($key, $array)) { $array[$key] = $value; } } return $array; } /** * Group models and their identifiers by type (models, strings & integers). * * @param iterable $models * @return array */ public static function groupModelsAndIdentifiersByType($models) { $groups = (new Collection($models))->groupBy(function ($model) { if (is_numeric($model)) { return 'integers'; } else if (is_string($model)) { return 'strings'; } else if ($model instanceof Model) { return 'models'; } throw new InvalidArgumentException('Invalid model identifier'); })->map(function ($items) { return $items->all(); })->all(); return static::fillMissingKeys($groups, [], ['integers', 'strings', 'models']); } /** * Determines if an array is associative. * * An array is "associative" if it doesn't have sequential numerical keys beginning with zero. * * @param mixed $array * @return bool */ public static function isAssociativeArray($array) { if (! is_array($array)) { return false; } $keys = array_keys($array); return array_keys($keys) !== $keys; } /** * Determines if an array is numerically indexed. * * @param mixed $array * @return bool */ public static function isIndexedArray($array) { if (! is_array($array)) { return false; } foreach ($array as $key => $value) { if (! is_numeric($key)) { return false; } } return true; } /** * Determines whether the given model is set to soft delete. * * @param \Illuminate\Database\Eloquent\Model $model * @return bool */ public static function isSoftDeleting(Model $model) { // Soft deleting models is controlled by adding the SoftDeletes trait // to the model. Instead of recursively looking for that trait, we // will check for the existence of the `isForceDeleting` method. if (! method_exists($model, 'isForceDeleting')) { return false; } return ! $model->isForceDeleting(); } /** * Convert the given value to an array. * * @param mixed $value * @return array */ public static function toArray($value) { if (is_array($value)) { return $value; } if ($value instanceof Collection) { return $value->all(); } return [$value]; } /** * Map a list of authorities by their class name. * * @param array $authorities * @return array */ public static function mapAuthorityByClass(array $authorities) { $map = []; foreach ($authorities as $authority) { if ($authority instanceof Model) { $map[get_class($authority)][] = $authority->getKey(); } else { $map[Models::classname(User::class)][] = $authority; } } return $map; } /** * Partition the given collection into two collection using the given callback. * * @param iterable $items * @param callable $callback * @return \Illuminate\Support\Collection */ public static function partition($items, callable $callback) { $partitions = [new Collection, new Collection]; foreach ($items as $key => $item) { $partitions[(int) ! $callback($item, $key)][$key] = $item; } return new Collection($partitions); } } src/Guard.php 0000644 00000010644 15025033125 0007107 0 ustar 00 <?php namespace Silber\Bouncer; use InvalidArgumentException; use Illuminate\Database\Eloquent\Model; use Illuminate\Contracts\Auth\Access\Gate; use Illuminate\Auth\Access\HandlesAuthorization; class Guard { use HandlesAuthorization; /** * The guard's clipboard instance. * * @var \Silber\Bouncer\Contracts\Clipboard */ protected $clipboard; /** * Determines where to run the clipboard's checks. * * Can be set to "before" or "after". * * @var string */ protected $slot = 'after'; /** * Create a new guard instance. * * @param \Silber\Bouncer\Contracts\Clipboard $clipboard */ public function __construct(Contracts\Clipboard $clipboard) { $this->clipboard = $clipboard; } /** * Get the clipboard instance. * * @return \Silber\Bouncer\Contracts\Clipboard */ public function getClipboard() { return $this->clipboard; } /** * Set the clipboard instance. * * @param \Silber\Bouncer\Contracts\Clipboard $clipboard * @return $this */ public function setClipboard(Contracts\Clipboard $clipboard) { $this->clipboard = $clipboard; return $this; } /** * Determine whether the clipboard used is a cached clipboard. * * @return bool */ public function usesCachedClipboard() { return $this->clipboard instanceof Contracts\CachedClipboard; } /** * Set or get which slot to run the clipboard's checks. * * @param string|null $slot * @return $this|string */ public function slot($slot = null) { if (is_null($slot)) { return $this->slot; } if (! in_array($slot, ['before', 'after'])) { throw new InvalidArgumentException( "{$slot} is an invalid gate slot" ); } $this->slot = $slot; return $this; } /** * Register the clipboard at the given gate. * * @param \Illuminate\Contracts\Auth\Access\Gate $gate * @return $this */ public function registerAt(Gate $gate) { $gate->before(function () { return $this->runBeforeCallback(...func_get_args()); }); $gate->after(function () { return $this->runAfterCallback(...func_get_args()); }); return $this; } /** * Run the gate's "before" callback. * * @param \Illuminate\Database\Eloquent\Model $authority * @param string $ability * @param mixed $arguments * @param mixed $additional * @return bool|null */ protected function runBeforeCallback($authority, $ability, $arguments = []) { if ($this->slot != 'before') { return; } if (count($arguments) > 2) { return; } $model = isset($arguments[0]) ? $arguments[0] : null; return $this->checkAtClipboard($authority, $ability, $model); } /** * Run the gate's "before" callback. * * @param \Illuminate\Database\Eloquent\Model $authority * @param string $ability * @param mixed $result * @param array $arguments * @return bool|null */ protected function runAfterCallback($authority, $ability, $result, $arguments = []) { if (! is_null($result)) { return $result; } if ($this->slot != 'after') { return; } if (count($arguments) > 2) { return; } $model = isset($arguments[0]) ? $arguments[0] : null; return $this->checkAtClipboard($authority, $ability, $model); } /** * Run an auth check at the clipboard. * * @param \Illuminate\Database\Eloquent\Model $authority * @param string $ability * @param \Illuminate\Database\Eloquent\Model|string|null $model * @return mixed */ protected function checkAtClipboard(Model $authority, $ability, $model) { if ($id = $this->clipboard->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; } } src/Bouncer.php 0000644 00000031521 15025033125 0007437 0 ustar 00 <?php namespace Silber\Bouncer; use RuntimeException; use Illuminate\Cache\NullStore; use Illuminate\Container\Container; use Illuminate\Contracts\Cache\Store; use Illuminate\Database\Eloquent\Model; use Illuminate\Contracts\Auth\Access\Gate; use Illuminate\Contracts\Cache\Repository as CacheRepository; use Silber\Bouncer\Contracts\Scope; use Silber\Bouncer\Database\Models; class Bouncer { /** * The bouncer guard instance. * * @var \Silber\Bouncer\Guard */ protected $guard; /** * The access gate instance. * * @var \Illuminate\Contracts\Auth\Access\Gate|null */ protected $gate; /** * Constructor. * * @param \Silber\Bouncer\Guard $guard */ public function __construct(Guard $guard) { $this->guard = $guard; } /** * Create a new Bouncer instance. * * @param mixed $user * @return static */ public static function create($user = null) { return static::make($user)->create(); } /** * Create a bouncer factory instance. * * @param mixed $user * @return \Silber\Bouncer\Factory */ public static function make($user = null) { return new Factory($user); } /** * Start a chain, to allow the given authority an ability. * * @param \Illuminate\Database\Eloquent\Model|string $authority * @return \Silber\Bouncer\Conductors\GivesAbilities */ public function allow($authority) { return new Conductors\GivesAbilities($authority); } /** * Start a chain, to allow everyone an ability. * * @return \Silber\Bouncer\Conductors\GivesAbilities */ public function allowEveryone() { return new Conductors\GivesAbilities(); } /** * Start a chain, to disallow the given authority an ability. * * @param \Illuminate\Database\Eloquent\Model|string $authority * @return \Silber\Bouncer\Conductors\RemovesAbilities */ public function disallow($authority) { return new Conductors\RemovesAbilities($authority); } /** * Start a chain, to disallow everyone the an ability. * * @return \Silber\Bouncer\Conductors\RemovesAbilities */ public function disallowEveryone() { return new Conductors\RemovesAbilities(); } /** * Start a chain, to forbid the given authority an ability. * * @param \Illuminate\Database\Eloquent\Model|string $authority * @return \Silber\Bouncer\Conductors\GivesAbilities */ public function forbid($authority) { return new Conductors\ForbidsAbilities($authority); } /** * Start a chain, to forbid everyone an ability. * * @return \Silber\Bouncer\Conductors\GivesAbilities */ public function forbidEveryone() { return new Conductors\ForbidsAbilities(); } /** * Start a chain, to unforbid the given authority an ability. * * @param \Illuminate\Database\Eloquent\Model|string $authority * @return \Silber\Bouncer\Conductors\RemovesAbilities */ public function unforbid($authority) { return new Conductors\UnforbidsAbilities($authority); } /** * Start a chain, to unforbid an ability from everyone. * * @return \Silber\Bouncer\Conductors\RemovesAbilities */ public function unforbidEveryone() { return new Conductors\UnforbidsAbilities(); } /** * Start a chain, to assign the given role to a model. * * @param \Silber\Bouncer\Database\Role|\Illuminate\Support\Collection|string $roles * @return \Silber\Bouncer\Conductors\AssignsRoles */ public function assign($roles) { return new Conductors\AssignsRoles($roles); } /** * Start a chain, to retract the given role from a model. * * @param \Illuminate\Support\Collection|\Silber\Bouncer\Database\Role|string $roles * @return \Silber\Bouncer\Conductors\RemovesRoles */ public function retract($roles) { return new Conductors\RemovesRoles($roles); } /** * Start a chain, to sync roles/abilities for the given authority. * * @param \Illuminate\Database\Eloquent\Model|string $authority * @return \Silber\Bouncer\Conductors\SyncsRolesAndAbilities */ public function sync($authority) { return new Conductors\SyncsRolesAndAbilities($authority); } /** * Start a chain, to check if the given authority has a certain role. * * @param \Illuminate\Database\Eloquent\Model $authority * @return \Silber\Bouncer\Conductors\ChecksRoles */ public function is(Model $authority) { return new Conductors\ChecksRoles($authority, $this->getClipboard()); } /** * Get the clipboard instance. * * @return \Silber\Bouncer\Contracts\Clipboard */ public function getClipboard() { return $this->guard->getClipboard(); } /** * 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) { $this->guard->setClipboard($clipboard); return $this->registerClipboardAtContainer(); } /** * Register the guard's clipboard at the container. * * @return $this */ public function registerClipboardAtContainer() { $clipboard = $this->guard->getClipboard(); Container::getInstance()->instance(Contracts\Clipboard::class, $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) { $cache = $cache ?: $this->resolve(CacheRepository::class)->getStore(); if ($this->usesCachedClipboard()) { $this->guard->getClipboard()->setCache($cache); return $this; } return $this->setClipboard(new CachedClipboard($cache)); } /** * Fully disable all query caching. * * @return $this */ public function dontCache() { return $this->setClipboard(new Clipboard); } /** * Clear the cache. * * @param null|\Illuminate\Database\Eloquent\Model $authority * @return $this */ public function refresh(Model $authority = null) { if ($this->usesCachedClipboard()) { $this->getClipboard()->refresh($authority); } return $this; } /** * Clear the cache for the given authority. * * @param \Illuminate\Database\Eloquent\Model $authority * @return $this */ public function refreshFor(Model $authority) { if ($this->usesCachedClipboard()) { $this->getClipboard()->refreshFor($authority); } return $this; } /** * Set the access gate instance. * * @param \Illuminate\Contracts\Auth\Access\Gate $gate * @return $this */ public function setGate(Gate $gate) { $this->gate = $gate; return $this; } /** * Get the gate instance. * * @return \Illuminate\Contracts\Auth\Access\Gate|null */ public function getGate() { return $this->gate; } /** * Get the gate instance. Throw if not set. * * @return \Illuminate\Contracts\Auth\Access\Gate * * @throws \RuntimeException */ public function gate() { if (is_null($this->gate)) { throw new RuntimeException('The gate instance has not been set.'); } return $this->gate; } /** * Determine whether the clipboard used is a cached clipboard. * * @return bool */ public function usesCachedClipboard() { return $this->guard->usesCachedClipboard(); } /** * Define a new ability using a callback. * * @param string $ability * @param callable|string $callback * @return $this * * @throws \InvalidArgumentException */ public function define($ability, $callback) { return $this->gate()->define($ability, $callback); } /** * Determine if the given ability should be granted for the current user. * * @param string $ability * @param array|mixed $arguments * @return \Illuminate\Auth\Access\Response * * @throws \Illuminate\Auth\Access\AuthorizationException */ public function authorize($ability, $arguments = []) { return $this->gate()->authorize($ability, $arguments); } /** * Determine if the given ability is allowed. * * @param string $ability * @param array|mixed $arguments * @return bool */ public function can($ability, $arguments = []) { return $this->gate()->allows($ability, $arguments); } /** * Determine if any of the given abilities are allowed. * * @param array $abilities * @param array|mixed $arguments * @return bool */ public function canAny($abilities, $arguments = []) { return $this->gate()->any($abilities, $arguments); } /** * Determine if the given ability is denied. * * @param string $ability * @param array|mixed $arguments * @return bool */ public function cannot($ability, $arguments = []) { return $this->gate()->denies($ability, $arguments); } /** * Determine if the given ability is allowed. * * Alias for the "can" method. * * @deprecated * @param string $ability * @param array|mixed $arguments * @return bool */ public function allows($ability, $arguments = []) { return $this->can($ability, $arguments); } /** * Determine if the given ability is denied. * * Alias for the "cannot" method. * * @deprecated * @param string $ability * @param array|mixed $arguments * @return bool */ public function denies($ability, $arguments = []) { return $this->cannot($ability, $arguments); } /** * Get an instance of the role model. * * @param array $attributes * @return \Silber\Bouncer\Database\Role */ public function role(array $attributes = []) { return Models::role($attributes); } /** * Get an instance of the ability model. * * @param array $attributes * @return \Silber\Bouncer\Database\Ability */ public function ability(array $attributes = []) { return Models::ability($attributes); } /** * Set Bouncer to run its checks before the policies. * * @param bool $boolean * @return $this */ public function runBeforePolicies($boolean = true) { $this->guard->slot($boolean ? 'before' : 'after'); return $this; } /** * Register an attribute/callback to determine if a model is owned by a given authority. * * @param string|\Closure $model * @param string|\Closure|null $attribute * @return $this */ public function ownedVia($model, $attribute = null) { Models::ownedVia($model, $attribute); return $this; } /** * Set the model to be used for abilities. * * @param string $model * @return $this */ public function useAbilityModel($model) { Models::setAbilitiesModel($model); return $this; } /** * Set the model to be used for roles. * * @param string $model * @return $this */ public function useRoleModel($model) { Models::setRolesModel($model); return $this; } /** * Set the model to be used for users. * * @param string $model * @return $this */ public function useUserModel($model) { Models::setUsersModel($model); return $this; } /** * Set custom table names. * * @param array $map * @return $this */ public function tables(array $map) { Models::setTables($map); return $this; } /** * Get the model scoping instance. * * @param \Silber\Bouncer\Contracts\Scope|null $scope * @return mixed */ public function scope(Scope $scope = null) { return Models::scope($scope); } /** * Resolve the given type from the container. * * @param string $abstract * @param array $parameters * @return mixed */ protected function resolve($abstract, array $parameters = []) { return Container::getInstance()->make($abstract, $parameters); } } src/Constraints/Group.php 0000644 00000010655 15025033125 0011452 0 ustar 00 <?php namespace Silber\Bouncer\Constraints; use Silber\Bouncer\Helpers; use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Model; class Group implements Constrainer { /** * The list of constraints. * * @var \Illuminate\Support\Collection<\Silber\Bouncer\Constraints\Constrainer> */ protected $constraints; /** * The logical operator to use when checked after a previous constrainer. * * @var string */ protected $logicalOperator = 'and'; /** * Constructor. * * @param iterable<\Silber\Bouncer\Constraints\Constrainer> $constraints */ public function __construct($constraints = []) { $this->constraints = new Collection($constraints); } /** * Create a new "and" group. * * @return static */ public static function withAnd() { return new static; } /** * Create a new "and" group. * * @return static */ public static function withOr() { return (new static)->logicalOperator('or'); } /** * Create a new instance from the raw data. * * @param array $data * @return static */ public static function fromData(array $data) { $group = new static(array_map(function ($data) { return $data['class']::fromData($data['params']); }, $data['constraints'])); return $group->logicalOperator($data['logicalOperator']); } /** * Add the given constraint to the list of constraints. * * @param \Silber\Bouncer\Constraints\Constrainer $constraint */ public function add(Constrainer $constraint) { $this->constraints->push($constraint); return $this; } /** * Determine whether the given entity/authority passes this constraint. * * @param \Illuminate\Database\Eloquent\Model $entity * @param \Illuminate\Database\Eloquent\Model|null $authority * @return bool */ public function check(Model $entity, Model $authority = null) { if ($this->constraints->isEmpty()) { return true; } return $this->constraints->reduce(function ($result, $constraint) use ($entity, $authority) { $passes = $constraint->check($entity, $authority); if (is_null($result)) { return $passes; } return $constraint->isOr() ? ($result || $passes) : ($result && $passes); }); } /** * Set the logical operator to use when checked after a previous constrainer. * * @param string|null $operator * @return $this|string */ public function logicalOperator($operator = null) { if (is_null($operator)) { return $this->logicalOperator; } Helpers::ensureValidLogicalOperator($operator); $this->logicalOperator = $operator; return $this; } /** * Checks whether the logical operator is an "and" operator. * * @param string $operator */ public function isAnd() { return $this->logicalOperator == 'and'; } /** * Checks whether the logical operator is an "and" operator. * * @param string $operator */ public function isOr() { return $this->logicalOperator == 'or'; } /** * Determine whether the given constrainer is equal to this object. * * @param \Silber\Bouncer\Constraints\Constrainer $constrainer * @return bool */ public function equals(Constrainer $constrainer) { if (! $constrainer instanceof static) { return false; } if ($this->constraints->count() != $constrainer->constraints->count()) { return false; } foreach ($this->constraints as $index => $constraint) { if (! $constrainer->constraints[$index]->equals($constraint)) { return false; } } return true; } /** * Get the JSON-able data of this object. * * @return array */ public function data() { return [ 'class' => static::class, 'params' => [ 'logicalOperator' => $this->logicalOperator, 'constraints' => $this->constraints->map(function ($constraint) { return $constraint->data(); })->all(), ], ]; } } src/Constraints/ValueConstraint.php 0000644 00000003517 15025033125 0013476 0 ustar 00 <?php namespace Silber\Bouncer\Constraints; use Illuminate\Database\Eloquent\Model; class ValueConstraint extends Constraint { /** * The column on the entity against which to compare. * * @var string */ protected $column; /** * The value to compare to. * * @var mixed */ protected $value; /** * Constructor. * * @param string $column * @param string $operator * @param mixed $value */ public function __construct($column, $operator, $value) { $this->column = $column; $this->operator = $operator; $this->value = $value; } /** * Determine whether the given entity/authority passed this constraint. * * @param \Illuminate\Database\Eloquent\Model $entity * @param \Illuminate\Database\Eloquent\Model|null $authority * @return bool */ public function check(Model $entity, Model $authority = null) { return $this->compare($entity->{$this->column}, $this->value); } /** * Create a new instance from the raw data. * * @param array $data * @return static */ public static function fromData(array $data) { $constraint = new static( $data['column'], $data['operator'], $data['value'] ); return $constraint->logicalOperator($data['logicalOperator']); } /** * Get the JSON-able data of this object. * * @return array */ public function data() { return [ 'class' => static::class, 'params' => [ 'column' => $this->column, 'operator' => $this->operator, 'value' => $this->value, 'logicalOperator' => $this->logicalOperator, ], ]; } } src/Constraints/ColumnConstraint.php 0000644 00000003562 15025033125 0013657 0 ustar 00 <?php namespace Silber\Bouncer\Constraints; use Illuminate\Database\Eloquent\Model; class ColumnConstraint extends Constraint { /** * The column on the entity against which to compare. * * @var string */ protected $a; /** * The column on the authority against which to compare. * * @var mixed */ protected $b; /** * Constructor. * * @param string $a * @param string $operator * @param mixed $b */ public function __construct($a, $operator, $b) { $this->a = $a; $this->operator = $operator; $this->b = $b; } /** * Determine whether the given entity/authority passes the constraint. * * @param \Illuminate\Database\Eloquent\Model $entity * @param \Illuminate\Database\Eloquent\Model|null $authority * @return bool */ public function check(Model $entity, Model $authority = null) { if (is_null($authority)) { return false; } return $this->compare($entity->{$this->a}, $authority->{$this->b}); } /** * Create a new instance from the raw data. * * @param array $data * @return static */ public static function fromData(array $data) { $constraint = new static( $data['a'], $data['operator'], $data['b'] ); return $constraint->logicalOperator($data['logicalOperator']); } /** * Get the JSON-able data of this object. * * @return array */ public function data() { return [ 'class' => static::class, 'params' => [ 'a' => $this->a, 'operator' => $this->operator, 'b' => $this->b, 'logicalOperator' => $this->logicalOperator, ], ]; } } src/Constraints/Builder.php 0000644 00000006122 15025033125 0011736 0 ustar 00 <?php namespace Silber\Bouncer\Constraints; use Closure; use Illuminate\Support\Collection; class Builder { /** * The list of constraints. * * @var \Illuminate\Support\Collection<\Silber\Bouncer\Constraints\Constraint|static> */ protected $constraints; /** * Constructor. * */ public function __construct() { $this->constraints = new Collection; } /** * Create a new builder instance. * * @return static */ public static function make() { return new static; } /** * Add a "where" constraint. * * @param string|\Closure $column * @param mixed $operator * @param mixed $value * @return $this */ public function where($column, $operator = null, $value = null) { if ($column instanceof Closure) { return $this->whereNested('and', $column); } return $this->addConstraint(Constraint::where(...func_get_args())); } /** * Add an "or where" constraint. * * @param string|\Closure $column * @param mixed $operator * @param mixed $value * @return $this */ public function orWhere($column, $operator = null, $value = null) { if ($column instanceof Closure) { return $this->whereNested('or', $column); } return $this->addConstraint(Constraint::orWhere(...func_get_args())); } /** * Add a "where column" constraint. * * @param string $a * @param mixed $operator * @param mixed $b * @return $this */ public function whereColumn($a, $operator, $b = null) { return $this->addConstraint(Constraint::whereColumn(...func_get_args())); } /** * Add an "or where column" constraint. * * @param string $a * @param mixed $operator * @param mixed $b * @return $this */ public function orWhereColumn($a, $operator, $b = null) { return $this->addConstraint(Constraint::orWhereColumn(...func_get_args())); } /** * Build the compiled list of constraints. * * @return \Silber\Bouncer\Constraints\Constrainer */ public function build() { if ($this->constraints->count() == 1) { return $this->constraints->first(); } return new Group($this->constraints); } /** * Add a nested "where" clause. * * @param string $logicalOperator 'and'|'or' * @param \Closure $callback * @return $this */ protected function whereNested($logicalOperator, Closure $callback) { $callback($builder = new static); $constraint = $builder->build()->logicalOperator($logicalOperator); return $this->addConstraint($constraint); } /** * Add a constraint to the list of constraints. * * @param \Silber\Bouncer\Constraints\Constrainer $constraint * @return $this */ protected function addConstraint($constraint) { $this->constraints[] = $constraint; return $this; } } src/Constraints/Constraint.php 0000644 00000012042 15025033125 0012472 0 ustar 00 <?php namespace Silber\Bouncer\Constraints; use Silber\Bouncer\Helpers; use InvalidArgumentException; use Illuminate\Database\Eloquent\Model; abstract class Constraint implements Constrainer { /** * The operator to use for the comparison. * * @var string */ protected $operator; /** * The logical operator to use when checked after a previous constaint. * * @var string */ protected $logicalOperator = 'and'; /** * Determine whether the given entity/authority passes this constraint. * * @param \Illuminate\Database\Eloquent\Model $entity * @param \Illuminate\Database\Eloquent\Model|null $authority * @return bool */ abstract public function check(Model $entity, Model $authority = null); /** * Create a new constraint for where a column matches the given value. * * @param string $column * @param mixed $operator * @param mixed $value * @return \Silber\Bouncer\Constraints\ValueConstraint */ public static function where($column, $operator, $value = null) { list($operator, $value) = static::prepareOperatorAndValue( $operator, $value, func_num_args() === 2 ); return new ValueConstraint($column, $operator, $value); } /** * Create a new constraint for where a column matches the given value, * with the logical operator set to "or". * * @param string $column * @param mixed $operator * @param mixed $value * @return \Silber\Bouncer\Constraints\ValueConstraint */ public static function orWhere($column, $operator, $value = null) { return static::where(...func_get_args())->logicalOperator('or'); } /** * Create a new constraint for where a column matches the given column on the authority. * * @param string $a * @param mixed $operator * @param mixed $b * @return \Silber\Bouncer\Constraints\ColumnConstraint */ public static function whereColumn($a, $operator, $b = null) { list($operator, $b) = static::prepareOperatorAndValue( $operator, $b, func_num_args() === 2 ); return new ColumnConstraint($a, $operator, $b); } /** * Create a new constraint for where a column matches the given column on the authority, * with the logical operator set to "or". * * @param string $a * @param mixed $operator * @param mixed $b * @return \Silber\Bouncer\Constraints\ColumnConstraint */ public static function orWhereColumn($a, $operator, $b = null) { return static::whereColumn(...func_get_args())->logicalOperator('or'); } /** * Set the logical operator to use when checked after a previous constraint. * * @param string|null $operator * @return $this|string */ public function logicalOperator($operator = null) { if (is_null($operator)) { return $this->logicalOperator; } Helpers::ensureValidLogicalOperator($operator); $this->logicalOperator = $operator; return $this; } /** * Checks whether the logical operator is an "and" operator. * * @param string $operator */ public function isAnd() { return $this->logicalOperator == 'and'; } /** * Checks whether the logical operator is an "and" operator. * * @param string $operator */ public function isOr() { return $this->logicalOperator == 'or'; } /** * Determine whether the given constrainer is equal to this object. * * @param \Silber\Bouncer\Constraints\Constrainer $constrainer * @return bool */ public function equals(Constrainer $constrainer) { if (! $constrainer instanceof static) { return false; } return $this->data()['params'] == $constrainer->data()['params']; } /** * Prepare the value and operator. * * @param string $operator * @param string $value * @param bool $usesDefault * @return array * * @throws \InvalidArgumentException */ protected static function prepareOperatorAndValue($operator, $value, $usesDefault) { if ($usesDefault) { return ['=', $operator]; } if (! in_array($operator, ['=', '==', '!=', '<', '>', '<=', '>='])) { throw new InvalidArgumentException("{$operator} is not a valid operator"); } return [$operator, $value]; } /** * Compare the two values by the constraint's operator. * * @param mixed $a * @param mixed $b * @return bool */ protected function compare($a, $b) { switch ($this->operator) { case '=': case '==': return $a == $b; case '!=': return $a != $b; case '<': return $a < $b; case '>': return $a > $b; case '<=': return $a <= $b; case '>=': return $a >= $b; } } } src/Constraints/Constrainer.php 0000644 00000002713 15025033125 0012641 0 ustar 00 <?php namespace Silber\Bouncer\Constraints; use Illuminate\Database\Eloquent\Model; interface Constrainer { /** * Create a new instance from the raw data. * * @param array $data * @return static */ public static function fromData(array $data); /** * Get the JSON-able data of this object. * * @return array */ public function data(); /** * Determine whether the given entity/authority passes this constraint. * * @param \Illuminate\Database\Eloquent\Model $entity * @param \Illuminate\Database\Eloquent\Model|null $authority * @return bool */ public function check(Model $entity, Model $authority = null); /** * Set the logical operator to use when checked after a previous constrainer. * * @param string|null $operator * @return $this|string */ public function logicalOperator($operator = null); /** * Checks whether the logical operator is an "and" operator. * * @param string $operator */ public function isAnd(); /** * Checks whether the logical operator is an "and" operator. * * @param string $operator */ public function isOr(); /** * Determine whether the given constrainer is equal to this object. * * @param \Silber\Bouncer\Constraints\Constrainer $constrainer * @return bool */ public function equals(Constrainer $constrainer); } src/BouncerServiceProvider.php 0000644 00000007763 15025033125 0012506 0 ustar 00 <?php namespace Silber\Bouncer; use Illuminate\Support\Arr; use Silber\Bouncer\Database\Models; use Silber\Bouncer\Console\CleanCommand; use Illuminate\Cache\ArrayStore; use Illuminate\Support\ServiceProvider; use Illuminate\Contracts\Auth\Access\Gate; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Eloquent\Model as EloquentModel; class BouncerServiceProvider extends ServiceProvider { /** * Register any application services. * * @return void */ public function register() { $this->registerBouncer(); $this->registerCommands(); } /** * Bootstrap any application services. * * @return void */ public function boot() { $this->registerMorphs(); $this->setUserModel(); $this->registerAtGate(); if ($this->app->runningInConsole()) { $this->publishMiddleware(); $this->publishMigrations(); } } /** * Register Bouncer as a singleton. * * @return void */ protected function registerBouncer() { $this->app->singleton(Bouncer::class, function ($app) { return Bouncer::make() ->withClipboard(new CachedClipboard(new ArrayStore)) ->withGate($app->make(Gate::class)) ->create(); }); } /** * Register Bouncer's commands with artisan. * * @return void */ protected function registerCommands() { $this->commands(CleanCommand::class); } /** * Register Bouncer's models in the relation morph map. * * @return void */ protected function registerMorphs() { Models::updateMorphMap(); } /** * Publish the package's middleware. * * @return void */ protected function publishMiddleware() { $stub = __DIR__.'/../middleware/ScopeBouncer.php'; $target = app_path('Http/Middleware/ScopeBouncer.php'); $this->publishes([$stub => $target], 'bouncer.middleware'); } /** * Publish the package's migrations. * * @return void */ protected function publishMigrations() { if (class_exists('CreateBouncerTables')) { return; } $timestamp = date('Y_m_d_His', time()); $stub = __DIR__.'/../migrations/create_bouncer_tables.php'; $target = $this->app->databasePath().'/migrations/'.$timestamp.'_create_bouncer_tables.php'; $this->publishes([$stub => $target], 'bouncer.migrations'); } /** * Set the classname of the user model to be used by Bouncer. * * @return void */ protected function setUserModel() { if ($model = $this->getUserModel()) { Models::setUsersModel($model); } } /** * Get the user model from the application's auth config. * * @return string|null */ protected function getUserModel() { $config = $this->app->make('config'); if (is_null($guard = $config->get('auth.defaults.guard'))) { return null; } if (is_null($provider = $config->get("auth.guards.{$guard}.provider"))) { return null; } $model = $config->get("auth.providers.{$provider}.model"); // The standard auth config that ships with Laravel references the // Eloquent User model in the above config path. However, users // are free to reference anything there - so we check first. if (is_subclass_of($model, EloquentModel::class)) { return $model; } } /** * Register the bouncer's clipboard at the gate. * * @return void */ protected function registerAtGate() { // When creating a Bouncer instance thru the Factory class, it'll // auto-register at the gate. We already registered Bouncer in // the container using the Factory, so now we'll resolve it. $this->app->make(Bouncer::class); } } src/Factory.php 0000644 00000010412 15025033125 0007445 0 ustar 00 <?php namespace Silber\Bouncer; use Illuminate\Auth\Access\Gate; use Illuminate\Cache\ArrayStore; use Illuminate\Container\Container; use Illuminate\Contracts\Cache\Store; use Illuminate\Contracts\Auth\Access\Gate as GateContract; class Factory { /** * The cache instance to use for the clipboard. * * @var \Illuminate\Contracts\Cache\Store */ protected $cache; /** * The clipboard instance to use. * * @var \Silber\Bouncer\Contracts\Clipboard */ protected $clipboard; /** * The gate instance to use. * * @var \Illuminate\Contracts\Auth\Access\Gate */ protected $gate; /** * The user model to use for the gate. * * @var mixed */ protected $user; /** * Determines whether the clipboard instance will be registered at the container. * * @var bool */ protected $registerAtContainer = true; /** * Determines whether the guard instance will be registered at the gate. * * @var bool */ protected $registerAtGate = true; /** * Create a new Factory instance. * * @param mixed $user */ public function __construct($user = null) { $this->user = $user; } /** * Create an instance of Bouncer. * * @return \Silber\Bouncer\Bouncer */ public function create() { $gate = $this->getGate(); $guard = $this->getGuard(); $bouncer = (new Bouncer($guard))->setGate($gate); if ($this->registerAtGate) { $guard->registerAt($gate); } if ($this->registerAtContainer) { $bouncer->registerClipboardAtContainer(); } return $bouncer; } /** * Set the cache instance to use for the clipboard. * * @param \Illuminate\Contracts\Cache\Store $cache * @return $this */ public function withCache(Store $cache) { $this->cache = $cache; return $this; } /** * Set the instance of the clipboard to use. * * @param \Silber\Bouncer\Contracts\Clipboard $clipboard * @return $this */ public function withClipboard(Contracts\Clipboard $clipboard) { $this->clipboard = $clipboard; return $this; } /** * Set the gate instance to use. * * @param \Illuminate\Contracts\Auth\Access\Gate $gate * @return $this */ public function withGate(GateContract $gate) { $this->gate = $gate; return $this; } /** * Set the user model to use for the gate. * * @param mixed $user * @return $this */ public function withUser($user) { $this->user = $user; return $this; } /** * Set whether the factory registers the clipboard instance with the container. * * @param bool $bool * @return $this */ public function registerClipboardAtContainer($bool = true) { $this->registerAtContainer = $bool; return $this; } /** * Set whether the factory registers the guard instance with the gate. * * @param bool $bool * @return $this */ public function registerAtGate($bool = true) { $this->registerAtGate = $bool; return $this; } /** * Get an instance of the clipboard. * * @return \Silber\Bouncer\Guard */ protected function getGuard() { return new Guard($this->getClipboard()); } /** * Get an instance of the clipboard. * * @return \Silber\Bouncer\Contracts\Clipboard */ protected function getClipboard() { return $this->clipboard ?: new CachedClipboard($this->getCacheStore()); } /** * Get an instance of the cache store. * * @return \Illuminate\Contracts\Cache\Store */ protected function getCacheStore() { return $this->cache ?: new ArrayStore; } /** * Get an instance of the gate. * * @return \Illuminate\Contracts\Auth\Access\Gate */ protected function getGate() { if ($this->gate) { return $this->gate; } return new Gate(Container::getInstance(), function () { return $this->user; }); } } LICENSE.txt 0000644 00000002065 15025033125 0006366 0 ustar 00 The MIT License (MIT) Copyright (c) <Joseph Silber> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. migrations/create_bouncer_tables.php 0000644 00000006160 15025033125 0013742 0 ustar 00 <?php use Silber\Bouncer\Database\Models; use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateBouncerTables extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create(Models::table('abilities'), function (Blueprint $table) { $table->bigIncrements('id'); $table->string('name'); $table->string('title')->nullable(); $table->bigInteger('entity_id')->unsigned()->nullable(); $table->string('entity_type')->nullable(); $table->boolean('only_owned')->default(false); $table->json('options')->nullable(); $table->integer('scope')->nullable()->index(); $table->timestamps(); }); Schema::create(Models::table('roles'), function (Blueprint $table) { $table->bigIncrements('id'); $table->string('name'); $table->string('title')->nullable(); $table->integer('scope')->nullable()->index(); $table->timestamps(); $table->unique( ['name', 'scope'], 'roles_name_unique' ); }); Schema::create(Models::table('assigned_roles'), function (Blueprint $table) { $table->bigIncrements('id'); $table->bigInteger('role_id')->unsigned()->index(); $table->bigInteger('entity_id')->unsigned(); $table->string('entity_type'); $table->bigInteger('restricted_to_id')->unsigned()->nullable(); $table->string('restricted_to_type')->nullable(); $table->integer('scope')->nullable()->index(); $table->index( ['entity_id', 'entity_type', 'scope'], 'assigned_roles_entity_index' ); $table->foreign('role_id') ->references('id')->on(Models::table('roles')) ->onUpdate('cascade')->onDelete('cascade'); }); Schema::create(Models::table('permissions'), function (Blueprint $table) { $table->bigIncrements('id'); $table->bigInteger('ability_id')->unsigned()->index(); $table->bigInteger('entity_id')->unsigned()->nullable(); $table->string('entity_type')->nullable(); $table->boolean('forbidden')->default(false); $table->integer('scope')->nullable()->index(); $table->index( ['entity_id', 'entity_type', 'scope'], 'permissions_entity_index' ); $table->foreign('ability_id') ->references('id')->on(Models::table('abilities')) ->onUpdate('cascade')->onDelete('cascade'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::drop(Models::table('permissions')); Schema::drop(Models::table('assigned_roles')); Schema::drop(Models::table('roles')); Schema::drop(Models::table('abilities')); } } migrations/sql/MySQL.sql 0000644 00000005130 15025033125 0011200 0 ustar 00 create table `abilities` ( `id` int unsigned not null auto_increment primary key, `name` varchar(255) not null, `title` varchar(255) null, `entity_id` int unsigned null, `entity_type` varchar(255) null, `only_owned` tinyint(1) not null default '0', `options` json null, `scope` int null, `created_at` timestamp null, `updated_at` timestamp null ) default character set utf8mb4 collate utf8mb4_unicode_ci; alter table `abilities` add index `abilities_scope_index`(`scope`); create table `roles` ( `id` int unsigned not null auto_increment primary key, `name` varchar(255) not null, `title` varchar(255) null, `level` int unsigned null, `scope` int null, `created_at` timestamp null, `updated_at` timestamp null ) default character set utf8mb4 collate utf8mb4_unicode_ci; alter table `roles` add unique `roles_name_unique`(`name`, `scope`); alter table `roles` add index `roles_scope_index`(`scope`); create table `assigned_roles` ( `id` int unsigned not null auto_increment primary key, `role_id` int unsigned not null, `entity_id` int unsigned not null, `entity_type` varchar(255) not null, `restricted_to_id` int unsigned null, `restricted_to_type` varchar(255) null, `scope` int null ) default character set utf8mb4 collate utf8mb4_unicode_ci; alter table `assigned_roles` add index `assigned_roles_entity_index`(`entity_id`, `entity_type`, `scope`); alter table `assigned_roles` add constraint `assigned_roles_role_id_foreign` foreign key (`role_id`) references `roles` (`id`) on delete cascade on update cascade; alter table `assigned_roles` add index `assigned_roles_role_id_index`(`role_id`); alter table `assigned_roles` add index `assigned_roles_scope_index`(`scope`); create table `permissions` ( `id` int unsigned not null auto_increment primary key, `ability_id` int unsigned not null, `entity_id` int unsigned null, `entity_type` varchar(255) null, `forbidden` tinyint(1) not null default '0', `scope` int null ) default character set utf8mb4 collate utf8mb4_unicode_ci; alter table `permissions` add index `permissions_entity_index`(`entity_id`, `entity_type`, `scope`); alter table `permissions` add constraint `permissions_ability_id_foreign` foreign key (`ability_id`) references `abilities` (`id`) on delete cascade on update cascade; alter table `permissions` add index `permissions_ability_id_index`(`ability_id`); alter table `permissions` add index `permissions_scope_index`(`scope`); middleware/ScopeBouncer.php 0000644 00000001726 15025033125 0011763 0 ustar 00 <?php namespace App\Http\Middleware; use Silber\Bouncer\Bouncer; use Closure; class ScopeBouncer { /** * The Bouncer instance. * * @var \Silber\Bouncer\Bouncer */ protected $bouncer; /** * Constructor. * * @param \Silber\Bouncer\Bouncer $bouncer */ public function __construct(Bouncer $bouncer) { $this->bouncer = $bouncer; } /** * Set the proper Bouncer scope for the incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { // Here you may use whatever mechanism you use in your app // to determine the current tenant. To demonstrate, the // $tenantId is set here from the user's account_id. $tenantId = $request->user()->account_id; $this->bouncer->scope()->to($tenantId); return $next($request); } }
| ver. 1.4 |
.
| PHP 8.1.32 | Generation time: 0 |
proxy
|
phpinfo
|
Settings