diff --git a/framework/core/src/Event/PrepareUserGroups.php b/framework/core/src/Event/PrepareUserGroups.php index 32e13e0db..10757544c 100644 --- a/framework/core/src/Event/PrepareUserGroups.php +++ b/framework/core/src/Event/PrepareUserGroups.php @@ -12,6 +12,7 @@ namespace Flarum\Event; use Flarum\User\User; /** + * @deprecated beta 13, remove in beta 14. Use the User extender instead. * The `PrepareUserGroups` event. */ class PrepareUserGroups diff --git a/framework/core/src/Extend/User.php b/framework/core/src/Extend/User.php index 63dd92da5..130f19249 100644 --- a/framework/core/src/Extend/User.php +++ b/framework/core/src/Extend/User.php @@ -15,9 +15,10 @@ use Illuminate\Contracts\Container\Container; class User implements ExtenderInterface { private $displayNameDrivers = []; + private $groupProcessors = []; /** - * Add a mail driver. + * Add a display name driver. * * @param string $identifier Identifier for display name driver. E.g. 'username' for UserNameDriver * @param string $driver ::class attribute of driver class, which must implement Flarum\User\DisplayName\DriverInterface @@ -29,10 +30,35 @@ class User implements ExtenderInterface return $this; } + /** + * Dynamically process a user's list of groups when calculating permissions. + * This can be used to give a user permissions for groups they aren't actually in, based on context. + * It will not change the group badges displayed for the user. + * + * @param callable $callable + * + * The callable can be a closure or invokable class, and should accept: + * - \Flarum\User\User $user: the user in question. + * - array $groupIds: an array of ids for the groups the user belongs to. + * + * The callable should return: + * - array $groupIds: an array of ids for the groups the user belongs to. + */ + public function permissionGroups(callable $callable) + { + $this->groupProcessors[] = $callable; + + return $this; + } + public function extend(Container $container, Extension $extension = null) { $container->extend('flarum.user.display_name.supported_drivers', function ($existingDrivers) { return array_merge($existingDrivers, $this->displayNameDrivers); }); + + $container->extend('flarum.user.group_processors', function ($existingRelations) { + return array_merge($existingRelations, $this->groupProcessors); + }); } } diff --git a/framework/core/src/User/User.php b/framework/core/src/User/User.php index 48b02b64a..d3832abb5 100644 --- a/framework/core/src/User/User.php +++ b/framework/core/src/User/User.php @@ -83,6 +83,12 @@ class User extends AbstractModel */ protected $session; + /** + * An array of callables, through each of which the user's list of groups is passed + * before being returned. + */ + protected static $groupProcessors = []; + /** * An array of registered user preferences. Each preference is defined with * a key, and its value is an array containing the following keys:. @@ -660,8 +666,13 @@ class User extends AbstractModel $groupIds = array_merge($groupIds, [Group::MEMBER_ID], $this->groups->pluck('id')->all()); } + // Deprecated, remove in beta 14. event(new PrepareUserGroups($this, $groupIds)); + foreach (static::$groupProcessors as $processor) { + $groupIds = $processor($this, $groupIds); + } + return Permission::whereIn('group_id', $groupIds); } @@ -751,6 +762,17 @@ class User extends AbstractModel static::$preferences[$key] = compact('transformer', 'default'); } + /** + * Register a callback that processes a user's list of groups. + * + * @param callable $callback + * @return array $groupIds + */ + public static function addGroupProcessor($callback) + { + static::$groupProcessors[] = $callback; + } + /** * Get the key for a preference which flags whether or not the user will * receive a notification for $type via $method. diff --git a/framework/core/src/User/UserServiceProvider.php b/framework/core/src/User/UserServiceProvider.php index c83fe6ad1..5cb6d8bde 100644 --- a/framework/core/src/User/UserServiceProvider.php +++ b/framework/core/src/User/UserServiceProvider.php @@ -31,6 +31,10 @@ class UserServiceProvider extends AbstractServiceProvider { $this->registerAvatarsFilesystem(); $this->registerDisplayNameDrivers(); + + $this->app->singleton('flarum.user.group_processors', function () { + return []; + }); } protected function registerDisplayNameDrivers() @@ -72,6 +76,14 @@ class UserServiceProvider extends AbstractServiceProvider */ public function boot() { + foreach ($this->app->make('flarum.user.group_processors') as $callback) { + if (is_string($callback)) { + $callback = $this->app->make($callback); + } + + User::addGroupProcessor($callback); + } + User::setHasher($this->app->make('hash')); User::setGate($this->app->make(Gate::class)); User::setDisplayNameDriver($this->app->make('flarum.user.display_name.driver')); diff --git a/framework/core/tests/integration/extenders/UserTest.php b/framework/core/tests/integration/extenders/UserTest.php index 78449b373..1a897c719 100644 --- a/framework/core/tests/integration/extenders/UserTest.php +++ b/framework/core/tests/integration/extenders/UserTest.php @@ -24,7 +24,9 @@ class UserTest extends TestCase $this->prepareDatabase([ 'users' => [ $this->adminUser(), - ], 'settings' => [ + $this->normalUser(), + ], + 'settings' => [ ['key' => 'display_name_driver', 'value' => 'custom'], ], ]); @@ -58,6 +60,34 @@ class UserTest extends TestCase $this->assertEquals('admin@machine.local$$$suffix', $user->displayName); } + + /** + * @test + */ + public function user_has_permissions_for_expected_groups_if_no_processors_added() + { + $this->prepDb(); + $user = User::find(2); + + $this->assertContains('viewUserList', $user->getPermissions()); + } + + /** + * @test + */ + public function processor_can_restrict_user_groups() + { + $this->extend((new Extend\User)->permissionGroups(function (User $user, array $groupIds) { + return array_filter($groupIds, function ($id) { + return $id != 3; + }); + })); + + $this->prepDb(); + $user = User::find(2); + + $this->assertNotContains('viewUserList', $user->getPermissions()); + } } class CustomDisplayNameDriver implements DriverInterface