1
0
mirror of https://github.com/flarum/core.git synced 2025-10-27 05:31:29 +01:00

Policy Extender and Tests (#2461)

Policy application has also been refactored, so that policies return one of `allow`, `deny`, `forceAllow`, `forceDeny`. The result of a set of policies is no longer the first non-null result, but rather the highest priority result (forceDeny > forceAllow > deny > allow, so if a single forceDeny is present, that beats out all other returned results). This removes order in which extensions boot as a factor.
This commit is contained in:
Alexander Skvortsov
2020-12-08 19:10:06 -05:00
committed by GitHub
parent 8901073d12
commit d1dfa758e4
15 changed files with 597 additions and 125 deletions

View File

@@ -0,0 +1,64 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\User\Access;
use Flarum\User\User;
abstract class AbstractPolicy
{
public const GLOBAL = 'GLOBAL';
public const ALLOW = 'ALLOW';
public const DENY = 'DENY';
public const FORCE_ALLOW = 'FORCE_ALLOW';
public const FORCE_DENY = 'FORCE_DENY';
protected function allow()
{
return static::ALLOW;
}
protected function deny()
{
return static::DENY;
}
protected function forceAllow()
{
return static::FORCE_ALLOW;
}
protected function forceDeny()
{
return static::FORCE_DENY;
}
/**
* @param User $user
* @param string $ability
* @param $instance
* @return bool|void
*/
public function checkAbility(User $actor, string $ability, $instance)
{ // If a specific method for this ability is defined,
// call that and return any non-null results
if (method_exists($this, $ability)) {
$result = call_user_func_array([$this, $ability], [$actor, $instance]);
if (! is_null($result)) {
return $result;
}
}
// If a "total access" method is defined, try that.
if (method_exists($this, 'can')) {
return call_user_func_array([$this, 'can'], [$actor, $ability, $instance]);
}
}
}

131
src/User/Access/Gate.php Normal file
View File

@@ -0,0 +1,131 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\User\Access;
use Flarum\Database\AbstractModel;
use Flarum\Event\GetPermission;
use Flarum\User\User;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\Arr;
class Gate
{
protected const EVALUATION_CRITERIA_PRIORITY = [
AbstractPolicy::FORCE_DENY => false,
AbstractPolicy::FORCE_ALLOW => true,
AbstractPolicy::DENY => false,
AbstractPolicy::ALLOW => true,
];
/**
* @var Container
*/
protected $container;
/**
* @var Dispatcher
*/
protected $events;
/**
* @var array
*/
protected $policyClasses;
/**
* @var array
*/
protected $policies;
/**
* @param Dispatcher $events
*/
public function __construct(Container $container, Dispatcher $events, array $policyClasses)
{
$this->container = $container;
$this->events = $events;
$this->policyClasses = $policyClasses;
}
/**
* Determine if the given ability should be granted for the current user.
*
* @param User $actor
* @param string $ability
* @param string|AbstractModel $model
* @return bool
*/
public function allows(User $actor, string $ability, $model): bool
{
$results = [];
$appliedPolicies = [];
if ($model) {
$modelClasses = is_string($model) ? [$model] : array_merge(class_parents(($model)), [get_class($model)]);
foreach ($modelClasses as $class) {
$appliedPolicies = array_merge($appliedPolicies, $this->getPolicies($class));
}
} else {
$appliedPolicies = $this->getPolicies(AbstractPolicy::GLOBAL);
}
foreach ($appliedPolicies as $policy) {
$results[] = $policy->checkAbility($actor, $ability, $model);
}
foreach (static::EVALUATION_CRITERIA_PRIORITY as $criteria => $decision) {
if (in_array($criteria, $results, true)) {
return $decision;
}
}
// START OLD DEPRECATED SYSTEM
// Fire an event so that core and extension modelPolicies can hook into
// this permission query and explicitly grant or deny the
// permission.
$allowed = $this->events->until(
new GetPermission($actor, $ability, $model)
);
if (! is_null($allowed)) {
return $allowed;
}
// END OLD DEPRECATED SYSTEM
// If no policy covered this permission query, we will only grant
// the permission if the actor's groups have it. Otherwise, we will
// not allow the user to perform this action.
if ($actor->isAdmin() || ($actor->hasPermission($ability))) {
return true;
}
return false;
}
/**
* Get all policies for a given model and ability.
*/
protected function getPolicies(string $model)
{
$compiledPolicies = Arr::get($this->policies, $model);
if (is_null($compiledPolicies)) {
$policyClasses = Arr::get($this->policyClasses, $model, []);
$compiledPolicies = array_map(function ($policyClass) {
return $this->container->make($policyClass);
}, $policyClasses);
Arr::set($this->policies, $model, $compiledPolicies);
}
return $compiledPolicies;
}
}

View File

@@ -7,15 +7,12 @@
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\User;
namespace Flarum\User\Access;
use Flarum\User\User;
class UserPolicy extends AbstractPolicy
{
/**
* {@inheritdoc}
*/
protected $model = User::class;
/**
* @param User $actor
* @param string $ability
@@ -24,7 +21,7 @@ class UserPolicy extends AbstractPolicy
public function can(User $actor, $ability)
{
if ($actor->hasPermission('user.'.$ability)) {
return true;
return $this->allow();
}
}
}

View File

@@ -1,60 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\User;
use Flarum\Event\GetPermission;
use Illuminate\Contracts\Events\Dispatcher;
class Gate
{
/**
* @var Dispatcher
*/
protected $events;
/**
* @param Dispatcher $events
*/
public function __construct(Dispatcher $events)
{
$this->events = $events;
}
/**
* Determine if the given ability should be granted for the current user.
*
* @param User $actor
* @param string $ability
* @param array|mixed $arguments
* @return bool
*/
public function allows($actor, $ability, $arguments)
{
// Fire an event so that core and extension policies can hook into
// this permission query and explicitly grant or deny the
// permission.
$allowed = $this->events->until(
new GetPermission($actor, $ability, $arguments)
);
if (! is_null($allowed)) {
return $allowed;
}
// If no policy covered this permission query, we will only grant
// the permission if the actor's groups have it. Otherwise, we will
// not allow the user to perform this action.
if ($actor->isAdmin() || ($actor->hasPermission($ability))) {
return true;
}
return false;
}
}

View File

@@ -624,7 +624,7 @@ class User extends AbstractModel
* @param mixed $arguments
* @throws PermissionDeniedException
*/
public function assertCan($ability, $arguments = [])
public function assertCan($ability, $arguments = null)
{
$this->assertPermission(
$this->can($ability, $arguments)
@@ -762,7 +762,7 @@ class User extends AbstractModel
* @param array|mixed $arguments
* @return bool
*/
public function can($ability, $arguments = [])
public function can($ability, $arguments = null)
{
return static::$gate->allows($this, $ability, $arguments);
}
@@ -772,7 +772,7 @@ class User extends AbstractModel
* @param array|mixed $arguments
* @return bool
*/
public function cannot($ability, $arguments = [])
public function cannot($ability, $arguments = null)
{
return ! $this->can($ability, $arguments);
}

View File

@@ -9,8 +9,14 @@
namespace Flarum\User;
use Flarum\Discussion\Access\DiscussionPolicy;
use Flarum\Discussion\Discussion;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Foundation\ContainerUtil;
use Flarum\Group\Access\GroupPolicy;
use Flarum\Group\Group;
use Flarum\Post\Access\PostPolicy;
use Flarum\Post\Post;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\Access\ScopeUserVisibility;
use Flarum\User\DisplayName\DriverInterface;
@@ -36,6 +42,16 @@ class UserServiceProvider extends AbstractServiceProvider
$this->app->singleton('flarum.user.group_processors', function () {
return [];
});
$this->app->singleton('flarum.policies', function () {
return [
Access\AbstractPolicy::GLOBAL => [],
Discussion::class => [DiscussionPolicy::class],
Group::class => [GroupPolicy::class],
Post::class => [PostPolicy::class],
User::class => [Access\UserPolicy::class],
];
});
}
protected function registerDisplayNameDrivers()
@@ -81,18 +97,17 @@ class UserServiceProvider extends AbstractServiceProvider
User::addGroupProcessor(ContainerUtil::wrapCallback($callback, $this->app));
}
User::setHasher($this->app->make('hash'));
User::setGate($this->app->make(Gate::class));
User::setDisplayNameDriver($this->app->make('flarum.user.display_name.driver'));
$events = $this->app->make('events');
User::setHasher($this->app->make('hash'));
User::setGate($this->app->makeWith(Access\Gate::class, ['policyClasses' => $this->app->make('flarum.policies')]));
User::setDisplayNameDriver($this->app->make('flarum.user.display_name.driver'));
$events->listen(Saving::class, SelfDemotionGuard::class);
$events->listen(Registered::class, AccountActivationMailer::class);
$events->listen(EmailChangeRequested::class, EmailConfirmationMailer::class);
$events->subscribe(UserMetadataUpdater::class);
$events->subscribe(UserPolicy::class);
User::registerPreference('discloseOnline', 'boolval', true);
User::registerPreference('indexProfile', 'boolval', true);