<?php
namespace Illuminate\Auth\Access;
use Closure;
use Exception;
use Illuminate\Auth\Access\Events\GateEvaluated;
use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use InvalidArgumentException;
use ReflectionClass;
use ReflectionFunction;
class Gate implements GateContract
{
use HandlesAuthorization;
/**
* The container instance.
*
* @var \Illuminate\Contracts\Container\Container
*/
protected $container;
/**
* The user resolver callable.
*
* @var callable
*/
protected $userResolver;
/**
* All of the defined abilities.
*
* @var array
*/
protected $abilities = [];
/**
* All of the defined policies.
*
* @var array
*/
protected $policies = [];
/**
* All of the registered before callbacks.
*
* @var array
*/
protected $beforeCallbacks = [];
/**
* All of the registered after callbacks.
*
* @var array
*/
protected $afterCallbacks = [];
/**
* All of the defined abilities using class@method notation.
*
* @var array
*/
protected $stringCallbacks = [];
/**
* The callback to be used to guess policy names.
*
* @var callable|null
*/
protected $guessPolicyNamesUsingCallback;
/**
* Create a new gate instance.
*
* @param \Illuminate\Contracts\Container\Container $container
* @param callable $userResolver
* @param array $abilities
* @param array $policies
* @param array $beforeCallbacks
* @param array $afterCallbacks
* @param callable|null $guessPolicyNamesUsingCallback
* @return void
*/
public function __construct(Container $container, callable $userResolver, array $abilities = [],
array $policies = [], array $beforeCallbacks = [], array $afterCallbacks = [],
callable $guessPolicyNamesUsingCallback = null)
{
$this->policies = $policies;
$this->container = $container;
$this->abilities = $abilities;
$this->userResolver = $userResolver;
$this->afterCallbacks = $afterCallbacks;
$this->beforeCallbacks = $beforeCallbacks;
$this->guessPolicyNamesUsingCallback = $guessPolicyNamesUsingCallback;
}
/**
* Determine if a given ability has been defined.
*
* @param string|array $ability
* @return bool
*/
public function has($ability)
{
$abilities = is_array($ability) ? $ability : func_get_args();
foreach ($abilities as $ability) {
if (! isset($this->abilities[$ability])) {
return false;
}
}
return true;
}
/**
* Perform an on-demand authorization check. Throw an authorization exception if the condition or callback is false.
*
* @param \Illuminate\Auth\Access\Response|\Closure|bool $condition
* @param string|null $message
* @param string|null $code
* @return \Illuminate\Auth\Access\Response
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function allowIf($condition, $message = null, $code = null)
{
return $this->authorizeOnDemand($condition, $message, $code, true);
}
/**
* Perform an on-demand authorization check. Throw an authorization exception if the condition or callback is true.
*
* @param \Illuminate\Auth\Access\Response|\Closure|bool $condition
* @param string|null $message
* @param string|null $code
* @return \Illuminate\Auth\Access\Response
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function denyIf($condition, $message = null, $code = null)
{
return $this->authorizeOnDemand($condition, $message, $code, false);
}
/**
* Authorize a given condition or callback.
*
* @param \Illuminate\Auth\Access\Response|\Closure|bool $condition
* @param string|null $message
* @param string|null $code
* @param bool $allowWhenResponseIs
* @return \Illuminate\Auth\Access\Response
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
protected function authorizeOnDemand($condition, $message, $code, $allowWhenResponseIs)
{
$user = $this->resolveUser();
if ($condition instanceof Closure) {
$response = $this->canBeCalledWithUser($user, $condition)
? $condition($user)
: new Response(false, $message, $code);
} else {
$response = $condition;
}
return with($response instanceof Response ? $response : new Response(
(bool) $response === $allowWhenResponseIs, $message, $code
))->authorize();
}
/**
* Define a new ability.
*
* @param string $ability
* @param callable|array|string $callback
* @return $this
*
* @throws \InvalidArgumentException
*/
public function define($ability, $callback)
{
if (is_array($callback) && isset($callback[0]) && is_string($callback[0])) {
$callback = $callback[0].'@'.$callback[1];
}
if (is_callable($callback)) {
$this->abilities[$ability] = $callback;
} elseif (is_string($callback)) {
$this->stringCallbacks[$ability] = $callback;
$this->abilities[$ability] = $this->buildAbilityCallback($ability, $callback);
} else {
throw new InvalidArgumentException("Callback must be a callable, callback array, or a 'Class@method' string.");
}
return $this;
}
/**
* Define abilities for a resource.
*
* @param string $name
* @param string $class
* @param array|null $abilities
* @return $this
*/
public function resource($name, $class, array $abilities = null)
{
$abilities = $abilities ?: [
'viewAny' => 'viewAny',
'view' => 'view',
'create' => 'create',
'update' => 'update',
'delete' => 'delete',
];
foreach ($abilities as $ability => $method) {
$this->define($name.'.'.$ability, $class.'@'.$method);
}
return $this;
}
/**
* Create the ability callback for a callback string.
*
* @param string $ability
* @param string $callback
* @return \Closure
*/
protected function buildAbilityCallback($ability, $callback)
{
return function () use ($ability, $callback) {
if (str_contains($callback, '@')) {
[$class, $method] = Str::parseCallback($callback);
} else {
$class = $callback;
}
$policy = $this->resolvePolicy($class);
$arguments = func_get_args();
$user = array_shift($arguments);
$result = $this->callPolicyBefore(
$policy, $user, $ability, $arguments
);
if (! is_null($result)) {
return $result;
}
return isset($method)
? $policy->{$method}(...func_get_args())
: $policy(...func_get_args());
};
}
/**
* Define a policy class for a given class type.
*
* @param string $class
* @param string $policy
* @return $this
*/
public function policy($class, $policy)
{
$this->policies[$class] = $policy;
return $this;
}
/**
* Register a callback to run before all Gate checks.
*
* @param callable $callback
* @return $this
*/
public function before(callable $callback)
{
$this->beforeCallbacks[] = $callback;
return $this;
}
/**
* Register a callback to run after all Gate checks.
*
* @param callable $callback
* @return $this
*/
public function after(callable $callback)
{
$this->afterCallbacks[] = $callback;
return $this;
}
/**
* Determine if the given ability should be granted for the current user.
*
* @param string $ability
* @param array|mixed $arguments
* @return bool
*/
public function allows($ability, $arguments = [])
{
return $this->check($ability, $arguments);
}
/**
* Determine if the given ability should be denied for the current user.
*
* @param string $ability
* @param array|mixed $arguments
* @return bool
*/
public function denies($ability, $arguments = [])
{
return ! $this->allows($ability, $arguments);
}
/**
* Determine if all of the given abilities should be granted for the current user.
*
* @param iterable|string $abilities
* @param array|mixed $arguments
* @return bool
*/
public function check($abilities, $arguments = [])
{
return collect($abilities)->every(
fn ($ability) => $this->inspect($ability, $arguments)->allowed()
);
}
/**
* Determine if any one of the given abilities should be granted for the current user.
*
* @param iterable|string $abilities
* @param array|mixed $arguments
* @return bool
*/
public function any($abilities, $arguments = [])
{
return collect($abilities)->contains(fn ($ability) => $this->check($ability, $arguments));
}
/**
* Determine if all of the given abilities should be denied for the current user.
*
* @param iterable|string $abilities
* @param array|mixed $arguments
* @return bool
*/
public function none($abilities, $arguments = [])
{
return ! $this->any($abilities, $arguments);
}
/**
* 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->inspect($ability, $arguments)->authorize();
}
/**
* Inspect the user for the given ability.
*
* @param string $ability
* @param array|mixed $arguments
* @return \Illuminate\Auth\Access\Response
*/
public function inspect($ability, $arguments = [])
{
try {
$result = $this->raw($ability, $arguments);
if ($result instanceof Response) {
return $result;
}
return $result ? Response::allow() : Response::deny();
} catch (AuthorizationException $e) {
return $e->toResponse();
}
}
/**
* Get the raw result from the authorization callback.
*
* @param string $ability
* @param array|mixed $arguments
* @return mixed
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function raw($ability, $arguments = [])
{
$arguments = Arr::wrap($arguments);
$user = $this->resolveUser();
// First we will call the "before" callbacks for the Gate. If any of these give
// back a non-null response, we will immediately return that result in order
// to let the developers override all checks for some authorization cases.
$result = $this->callBeforeCallbacks(
$user, $ability, $arguments
);
if (is_null($result)) {
$result = $this->callAuthCallback($user, $ability, $arguments);
}
// After calling the authorization callback, we will call the "after" callbacks
// that are registered with the Gate, which allows a developer to do logging
// if that is required for this application. Then we'll return the result.
return tap($this->callAfterCallbacks(
$user, $ability, $arguments, $result
), function ($result) use ($user, $ability, $arguments) {
$this->dispatchGateEvaluatedEvent($user, $ability, $arguments, $result);
});
}
/**
* Determine whether the callback/method can be called with the given user.
*
* @param \Illuminate\Contracts\Auth\Authenticatable|null $user
* @param \Closure|string|array $class
* @param string|null $method
* @return bool
*/
protected function canBeCalledWithUser($user, $class, $method = null)
{
if (! is_null($user)) {
return true;
}
if (! is_null($method)) {
return $this->methodAllowsGuests($class, $method);
}
if (is_array($class)) {
$className = is_string($class[0]) ? $class[0] : get_class($class[0]);
return $this->methodAllowsGuests($className, $class[1]);
}
return $this->callbackAllowsGuests($class);
}
/**
* Determine if the given class method allows guests.
*
* @param string $class
* @param string $method
* @return bool
*/
protected function methodAllowsGuests($class, $method)
{
try {
$reflection = new ReflectionClass($class);
$method = $reflection->getMethod($method);
} catch (Exception $e) {
return false;
}
if ($method) {
$parameters = $method->getParameters();
return isset($parameters[0]) && $this->parameterAllowsGuests($parameters[0]);
}
return false;
}
/**
* Determine if the callback allows guests.
*
* @param callable $callback
* @return bool
*
* @throws \ReflectionException
*/
protected function callbackAllowsGuests($callback)
{
$parameters = (new ReflectionFunction($callback))->getParameters();
return isset($parameters[0]) && $this->parameterAllowsGuests($parameters[0]);
}
/**
* Determine if the given parameter allows guests.
*
* @param \ReflectionParameter $parameter
* @return bool
*/
protected function parameterAllowsGuests($parameter)
{
return ($parameter->hasType() && $parameter->allowsNull()) ||
($parameter->isDefaultValueAvailable() && is_null($parameter->getDefaultValue()));
}
/**
* Resolve and call the appropriate authorization callback.
*
* @param \Illuminate\Contracts\Auth\Authenticatable|null $user
* @param string $ability
* @param array $arguments
* @return bool
*/
protected function callAuthCallback($user, $ability, array $arguments)
{
$callback = $this->resolveAuthCallback($user, $ability, $arguments);
return $callback($user, ...$arguments);
}
/**
* Call all of the before callbacks and return if a result is given.
*
* @param \Illuminate\Contracts\Auth\Authenticatable|null $user
* @param string $ability
* @param array $arguments
* @return bool|null
*/
protected function callBeforeCallbacks($user, $ability, array $arguments)
{
foreach ($this->beforeCallbacks as $before) {
if (! $this->canBeCalledWithUser($user, $before)) {
continue;
}
if (! is_null($result = $before($user, $ability, $arguments))) {
return $result;
}
}
}
/**
* Call all of the after callbacks with check result.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param string $ability
* @param array $arguments
* @param bool $result
* @return bool|null
*/
protected function callAfterCallbacks($user, $ability, array $arguments, $result)
{
foreach ($this->afterCallbacks as $after) {
if (! $this->canBeCalledWithUser($user, $after)) {
continue;
}
$afterResult = $after($user, $ability, $result, $arguments);
$result ??= $afterResult;
}
return $result;
}
/**
* Dispatch a gate evaluation event.
*
* @param \Illuminate\Contracts\Auth\Authenticatable|null $user
* @param string $ability
* @param array $arguments
* @param bool|null $result
* @return void
*/
protected function dispatchGateEvaluatedEvent($user, $ability, array $arguments, $result)
{
if ($this->container->bound(Dispatcher::class)) {
$this->container->make(Dispatcher::class)->dispatch(
new GateEvaluated($user, $ability, $result, $arguments)
);
}
}
/**
* Resolve the callable for the given ability and arguments.
*
* @param \Illuminate\Contracts\Auth\Authenticatable|null $user
* @param string $ability
* @param array $arguments
* @return callable
*/
protected function resolveAuthCallback($user, $ability, array $arguments)
{
if (isset($arguments[0]) &&
! is_null($policy = $this->getPolicyFor($arguments[0])) &&
$callback = $this->resolvePolicyCallback($user, $ability, $arguments, $policy)) {
return $callback;
}
if (isset($this->stringCallbacks[$ability])) {
[$class, $method] = Str::parseCallback($this->stringCallbacks[$ability]);
if ($this->canBeCalledWithUser($user, $class, $method ?: '__invoke')) {
return $this->abilities[$ability];
}
}
if (isset($this->abilities[$ability]) &&
$this->canBeCalledWithUser($user, $this->abilities[$ability])) {
return $this->abilities[$ability];
}
return function () {
//
};
}
/**
* Get a policy instance for a given class.
*
* @param object|string $class
* @return mixed
*/
public function getPolicyFor($class)
{
if (is_object($class)) {
$class = get_class($class);
}
if (! is_string($class)) {
return;
}
if (isset($this->policies[$class])) {
return $this->resolvePolicy($this->policies[$class]);
}
foreach ($this->guessPolicyName($class) as $guessedPolicy) {
if (class_exists($guessedPolicy)) {
return $this->resolvePolicy($guessedPolicy);
}
}
foreach ($this->policies as $expected => $policy) {
if (is_subclass_of($class, $expected)) {
return $this->resolvePolicy($policy);
}
}
}
/**
* Guess the policy name for the given class.
*
* @param string $class
* @return array
*/
protected function guessPolicyName($class)
{
if ($this->guessPolicyNamesUsingCallback) {
return Arr::wrap(call_user_func($this->guessPolicyNamesUsingCallback, $class));
}
$classDirname = str_replace('/', '\\', dirname(str_replace('\\', '/', $class)));
$classDirnameSegments = explode('\\', $classDirname);
return Arr::wrap(Collection::times(count($classDirnameSegments), function ($index) use ($class, $classDirnameSegments) {
$classDirname = implode('\\', array_slice($classDirnameSegments, 0, $index));
return $classDirname.'\\Policies\\'.class_basename($class).'Policy';
})->reverse()->values()->first(function ($class) {
return class_exists($class);
}) ?: [$classDirname.'\\Policies\\'.class_basename($class).'Policy']);
}
/**
* Specify a callback to be used to guess policy names.
*
* @param callable $callback
* @return $this
*/
public function guessPolicyNamesUsing(callable $callback)
{
$this->guessPolicyNamesUsingCallback = $callback;
return $this;
}
/**
* Build a policy class instance of the given type.
*
* @param object|string $class
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function resolvePolicy($class)
{
return $this->container->make($class);
}
/**
* Resolve the callback for a policy check.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param string $ability
* @param array $arguments
* @param mixed $policy
* @return bool|callable
*/
protected function resolvePolicyCallback($user, $ability, array $arguments, $policy)
{
if (! is_callable([$policy, $this->formatAbilityToMethod($ability)])) {
return false;
}
return function () use ($user, $ability, $arguments, $policy) {
// This callback will be responsible for calling the policy's before method and
// running this policy method if necessary. This is used to when objects are
// mapped to policy objects in the user's configurations or on this class.
$result = $this->callPolicyBefore(
$policy, $user, $ability, $arguments
);
// When we receive a non-null result from this before method, we will return it
// as the "final" results. This will allow developers to override the checks
// in this policy to return the result for all rules defined in the class.
if (! is_null($result)) {
return $result;
}
$method = $this->formatAbilityToMethod($ability);
return $this->callPolicyMethod($policy, $method, $user, $arguments);
};
}
/**
* Call the "before" method on the given policy, if applicable.
*
* @param mixed $policy
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param string $ability
* @param array $arguments
* @return mixed
*/
protected function callPolicyBefore($policy, $user, $ability, $arguments)
{
if (! method_exists($policy, 'before')) {
return;
}
if ($this->canBeCalledWithUser($user, $policy, 'before')) {
return $policy->before($user, $ability, ...$arguments);
}
}
/**
* Call the appropriate method on the given policy.
*
* @param mixed $policy
* @param string $method
* @param \Illuminate\Contracts\Auth\Authenticatable|null $user
* @param array $arguments
* @return mixed
*/
protected function callPolicyMethod($policy, $method, $user, array $arguments)
{
// If this first argument is a string, that means they are passing a class name
// to the policy. We will remove the first argument from this argument array
// because this policy already knows what type of models it can authorize.
if (isset($arguments[0]) && is_string($arguments[0])) {
array_shift($arguments);
}
if (! is_callable([$policy, $method])) {
return;
}
if ($this->canBeCalledWithUser($user, $policy, $method)) {
return $policy->{$method}($user, ...$arguments);
}
}
/**
* Format the policy ability into a method name.
*
* @param string $ability
* @return string
*/
protected function formatAbilityToMethod($ability)
{
return str_contains($ability, '-') ? Str::camel($ability) : $ability;
}
/**
* Get a gate instance for the given user.
*
* @param \Illuminate\Contracts\Auth\Authenticatable|mixed $user
* @return static
*/
public function forUser($user)
{
$callback = fn () => $user;
return new static(
$this->container, $callback, $this->abilities,
$this->policies, $this->beforeCallbacks, $this->afterCallbacks,
$this->guessPolicyNamesUsingCallback
);
}
/**
* Resolve the user from the user resolver.
*
* @return mixed
*/
protected function resolveUser()
{
return call_user_func($this->userResolver);
}
/**
* Get all of the defined abilities.
*
* @return array
*/
public function abilities()
{
return $this->abilities;
}
/**
* Get all of the defined policies.
*
* @return array
*/
public function policies()
{
return $this->policies;
}
/**
* Set the container instance used by the gate.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return $this
*/
public function setContainer(Container $container)
{
$this->container = $container;
return $this;
}
}