Always allow invitation by link (#7322)

* Always allow invitation by link

* Remove unused method `LinkRegistrationService->isEnabled()`

* Introduce different targets for invitation links (Administration vs People)

* Changes for invitation by link

* Update CHANGELOG.md for invitation by link
This commit is contained in:
Yuriy Bakhtin 2024-12-03 16:45:34 +01:00 committed by GitHub
parent 864a18fc6c
commit 642d327d0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 140 additions and 37 deletions

View File

@ -17,6 +17,7 @@ HumHub Changelog
- Fix #7278: Don't remove html tags by JS from search post record because it is done by PHP
- Fix #7296: Fix email validation of invite new users
- Fix #7319: Display correct profile field value in user subtitle
- Fix #7322: Always allow invitation by link from Administration. Implement separate invitation by link from People.
1.16.2 (September 5, 2024)
--------------------------

View File

@ -26,6 +26,7 @@ use humhub\modules\user\models\Invite;
use humhub\modules\user\models\Profile;
use humhub\modules\user\models\ProfileField;
use humhub\modules\user\models\User;
use humhub\modules\user\services\LinkRegistrationService;
use Yii;
use yii\base\Exception;
use yii\db\Query;
@ -268,7 +269,7 @@ class UserController extends Controller
return $this->redirect(['edit', 'id' => $registration->getUser()->id]);
}
$invite = new InviteForm();
$invite = new InviteForm(['target' => LinkRegistrationService::TARGET_ADMIN]);
return $this->render('add', [
'hForm' => $registration,

View File

@ -1,6 +1,7 @@
<?php
use humhub\compat\HForm;
use humhub\modules\user\services\LinkRegistrationService;
use humhub\widgets\Button;
use humhub\widgets\ModalButton;
use humhub\modules\ui\form\widgets\ActiveForm;
@ -20,7 +21,9 @@ use humhub\modules\ui\form\widgets\ActiveForm;
<?php if ($canInviteByEmail || $canInviteByLink): ?>
<?= ModalButton::success(Yii::t('AdminModule.user', 'Invite new people'))
->load(['/user/invite'])->icon('invite')->sm() ?>
->load(['/user/invite', 'target' => LinkRegistrationService::TARGET_ADMIN])
->icon('invite')
->sm() ?>
<?php endif; ?>
</div>

View File

@ -50,6 +50,10 @@ class InviteController extends Controller
{
$model = new InviteForm();
if ($target = Yii::$app->request->get('target')) {
$model->target = $target;
}
$canInviteByEmail = $model->canInviteByEmail();
$canInviteByLink = $model->canInviteByLink();
if (!$canInviteByEmail && !$canInviteByLink) {
@ -105,6 +109,10 @@ class InviteController extends Controller
{
$model = new InviteForm();
if ($target = Yii::$app->request->get('target')) {
$model->target = $target;
}
if (!Yii::$app->user->can([ManageUsers::class, ManageGroups::class])) {
$this->forbidden();
}

View File

@ -24,6 +24,8 @@ use yii\authclient\BaseClient;
use yii\authclient\ClientInterface;
use yii\base\Exception;
use yii\db\StaleObjectException;
use yii\web\BadRequestHttpException;
use yii\web\ForbiddenHttpException;
use yii\web\HttpException;
/**
@ -138,11 +140,11 @@ class RegistrationController extends Controller
$linkRegistrationService = new LinkRegistrationService($token, Space::findOne(['id' => (int)$spaceId]));
if (!$linkRegistrationService->isEnabled()) {
throw new HttpException(404);
throw new ForbiddenHttpException('Registration is disabled!');
}
if ($token === null || !$linkRegistrationService->isValid()) {
throw new HttpException(400, 'Invalid token provided!');
throw new BadRequestHttpException('Invalid token provided!');
}
$linkRegistrationService->storeInSession();

View File

@ -29,6 +29,11 @@ use yii\validators\StringValidator;
*/
class Invite extends Model
{
/**
* @var string Target where this form is used
*/
public string $target = LinkRegistrationService::TARGET_PEOPLE;
/**
* @var string user's username or email address
*/
@ -81,7 +86,7 @@ class Invite extends Model
*
* @return array the emails
*/
public function getEmails()
public function getEmails(): array
{
$emails = [];
foreach (explode(',', $this->emails) as $email) {
@ -98,7 +103,7 @@ class Invite extends Model
* @throws InvalidConfigException
* @throws Throwable
*/
public function canInviteByEmail()
public function canInviteByEmail(): bool
{
/** @var Module $module */
$module = Yii::$app->getModule('user');
@ -115,14 +120,21 @@ class Invite extends Model
* @throws Throwable
* @throws InvalidConfigException
*/
public function canInviteByLink()
public function canInviteByLink(): bool
{
/** @var Module $module */
$module = Yii::$app->getModule('user');
return (!Yii::$app->user->isGuest && $module->settings->get('auth.internalUsersCanInviteByLink'))
|| Yii::$app->user->isAdmin()
|| Yii::$app->user->can([ManageUsers::class, ManageGroups::class]);
if (!Yii::$app->user->isGuest && $module->settings->get('auth.internalUsersCanInviteByLink')) {
return true;
}
if ($this->target === LinkRegistrationService::TARGET_ADMIN) {
// Admins always can invite by link
return Yii::$app->user->isAdmin() || Yii::$app->user->can([ManageUsers::class, ManageGroups::class]);
}
return false;
}
/**
@ -130,9 +142,10 @@ class Invite extends Model
* @return string
* @throws Exception
*/
public function getInviteLink($forceResetToken = false)
public function getInviteLink(bool $forceResetToken = false): string
{
$linkRegistrationService = new LinkRegistrationService();
$linkRegistrationService->target = $this->target;
$token = $linkRegistrationService->getStoredToken();
if ($forceResetToken || !$token) {
$token = $linkRegistrationService->setNewToken();

View File

@ -8,6 +8,7 @@
namespace humhub\modules\user\services;
use humhub\components\SettingsManager;
use humhub\modules\space\models\Space;
use humhub\modules\user\models\Invite;
use humhub\modules\user\models\User;
@ -26,9 +27,15 @@ final class LinkRegistrationService
{
public const SETTING_VAR_ENABLED = 'auth.internalUsersCanInviteByLink';
public const SETTING_VAR_SPACE_TOKEN = 'inviteToken';
public const SETTING_VAR_TOKEN = 'registration.inviteToken';
public const SETTING_VAR_ADMIN_TOKEN = 'registration.inviteToken';
public const SETTING_VAR_PEOPLE_TOKEN = 'people.inviteToken';
public const TARGET_ADMIN = 'admin';
public const TARGET_PEOPLE = 'people';
private ?Space $space;
private ?string $token;
public ?string $target = null;
public static function createFromRequest(): LinkRegistrationService
{
@ -43,24 +50,37 @@ final class LinkRegistrationService
return new LinkRegistrationService($token, Space::findOne(['id' => $spaceId]));
}
public function __construct(?string $token = null, ?Space $space = null)
{
$this->token = $token;
$this->space = $space;
$this->initTarget();
}
private function initTarget(): void
{
if ($this->token && $this->target === null) {
if ($this->token === $this->getSettings()->get(self::SETTING_VAR_ADMIN_TOKEN)) {
$this->target = self::TARGET_ADMIN;
} elseif ($this->token === $this->getSettings()->get(self::SETTING_VAR_PEOPLE_TOKEN)) {
$this->target = self::TARGET_PEOPLE;
}
}
}
public function isValid(): bool
{
return ($this->isEnabled() && $this->getStoredToken() === $this->token);
return $this->isEnabled() && $this->getStoredToken() === $this->token;
}
public function isEnabled(): bool
{
/** @var Module $module */
$module = Yii::$app->getModule('user');
if ($this->target === self::TARGET_ADMIN) {
// The link registration with token from Administration is always enabled
return true;
}
return (!empty($module->settings->get(self::SETTING_VAR_ENABLED)));
return (bool) $this->getSettings()->get(self::SETTING_VAR_ENABLED, false);
}
public function getStoredToken(): ?string
@ -69,10 +89,15 @@ final class LinkRegistrationService
return $this->space->settings->get(self::SETTING_VAR_SPACE_TOKEN);
}
/** @var Module $module */
$module = Yii::$app->getModule('user');
if ($this->target === self::TARGET_ADMIN) {
return $this->getSettings()->get(self::SETTING_VAR_ADMIN_TOKEN);
}
return $module->settings->get(self::SETTING_VAR_TOKEN);
if ($this->target === self::TARGET_PEOPLE) {
return $this->getSettings()->get(self::SETTING_VAR_PEOPLE_TOKEN);
}
return null;
}
public function setNewToken(): string
@ -81,10 +106,10 @@ final class LinkRegistrationService
if ($this->space) {
$this->space->settings->set(self::SETTING_VAR_SPACE_TOKEN, $newToken);
} else {
/** @var Module $module */
$module = Yii::$app->getModule('user');
$module->settings->set(self::SETTING_VAR_TOKEN, $newToken);
$settingName = $this->target === 'admin'
? self::SETTING_VAR_ADMIN_TOKEN
: self::SETTING_VAR_PEOPLE_TOKEN;
$this->getSettings()->set($settingName, $newToken);
}
return $newToken;
@ -147,4 +172,11 @@ final class LinkRegistrationService
{
return $this->space;
}
private function getSettings(): SettingsManager
{
/* @var Module $module */
$module = Yii::$app->getModule('user');
return $module->settings;
}
}

View File

@ -2,6 +2,7 @@
use humhub\modules\space\models\forms\InviteForm;
use humhub\modules\space\models\Space;
use humhub\modules\user\models\forms\Invite as UserInviteForm;
use humhub\modules\user\services\LinkRegistrationService;
use user\FunctionalTester;
@ -9,16 +10,25 @@ class LinkInviteCest
{
public function testDisabledLinkInvite(FunctionalTester $I)
{
$I->wantTo('ensure that invite by link is correctly disabled');
$I->wantTo('ensure that invitation links are correctly disabled');
Yii::$app->getModule('user')->settings->set('auth.internalUsersCanInviteByLink', 0);
$inviteForm = new InviteForm();
$inviteForm->space = Space::findOne(['name' => 'Space 2']);
$inviteUrl = $inviteForm->getInviteLink();
$I->amOnPage($inviteForm->getInviteLink());
$I->seeResponseCodeIs(403);
$I->amOnPage($inviteUrl);
$I->seeResponseCodeIs(404);
$inviteForm = new UserInviteForm();
$inviteForm->target = LinkRegistrationService::TARGET_ADMIN;
$I->amOnPage($inviteForm->getInviteLink());
// The invitation by link is never disabled because admins or user managers always can send it
$I->seeResponseCodeIs(200);
$inviteForm = new UserInviteForm();
$inviteForm->target = LinkRegistrationService::TARGET_PEOPLE;
$I->amOnPage($inviteForm->getInviteLink());
$I->seeResponseCodeIs(403);
}
public function testInvalidToken(FunctionalTester $I)
@ -31,18 +41,56 @@ class LinkInviteCest
$I->seeResponseCodeIs(400);
}
public function testValidTokenDifferentTarget(FunctionalTester $I)
{
$I->wantTo('ensure that invitation links are different between targets');
Yii::$app->getModule('user')->settings->set('auth.internalUsersCanInviteByLink', 1);
$adminInviteForm = new UserInviteForm();
$adminInviteForm->target = LinkRegistrationService::TARGET_ADMIN;
$firstAdminInviteLink = $adminInviteForm->getInviteLink();
$peopleInviteForm = new UserInviteForm();
$peopleInviteForm->target = LinkRegistrationService::TARGET_PEOPLE;
$firstPeopleInviteLink = $peopleInviteForm->getInviteLink();
$I->amOnPage($firstAdminInviteLink);
$I->seeResponseCodeIs(200);
$I->amOnPage($firstPeopleInviteLink);
$I->seeResponseCodeIs(200);
// Reset only the link with admin target
$secondAdminInviteLink = $adminInviteForm->getInviteLink(true);
$I->amOnPage($firstAdminInviteLink);
$I->seeResponseCodeIs(400); // Invalid token
$I->amOnPage($secondAdminInviteLink);
$I->seeResponseCodeIs(200); // The second admin token is valid now
$I->amOnPage($firstPeopleInviteLink);
$I->seeResponseCodeIs(200); // The first people token must be still valid
// Reset the link with people target
$secondPeopleInviteLink = $peopleInviteForm->getInviteLink(true);
$I->amOnPage($secondAdminInviteLink);
$I->seeResponseCodeIs(200); // The second admin token should be valid
$I->amOnPage($firstPeopleInviteLink);
$I->seeResponseCodeIs(400); // The first people token is invalid after reset
$I->amOnPage($secondPeopleInviteLink);
$I->seeResponseCodeIs(200); // The second people token is valid now
}
public function testValidTokenDifferentSpaceId(FunctionalTester $I)
{
$I->wantTo('ensure that invite by link is with valid token and different space ID');
Yii::$app->getModule('user')->settings->set('auth.internalUsersCanInviteByLink', 1);
// Generate Token
$space = Space::findOne(['name' => 'Space 2']);
$inviteForm = new InviteForm();
$inviteForm->space = $space;
$inviteUrl = $inviteForm->getInviteLink();
$inviteForm->getInviteLink();
$linkRegistrationService = new LinkRegistrationService(null, $space);
$I->amOnRoute('/user/registration/by-link', ['token' => $linkRegistrationService->getStoredToken(), 'spaceId' => $space->id]);
@ -51,14 +99,11 @@ class LinkInviteCest
$I->amOnRoute('/user/registration/by-link', ['token' => $linkRegistrationService->getStoredToken(), 'spaceId' => 1]);
$I->seeResponseCodeIs(400);
Yii::$app->getModule('user')->settings->set('auth.internalUsersCanInviteByLink', 0);
$I->amOnRoute('/user/registration/by-link', ['token' => 'abc', 'spaceId' => 1]);
$I->seeResponseCodeIs(404);
$I->seeResponseCodeIs(403);
}
public function testSpaceInvite(FunctionalTester $I)
{
$I->wantTo('ensure that invited users become member of the space');
@ -105,6 +150,4 @@ class LinkInviteCest
$I->see('User is not member of invited Space!');
}
}
}

View File

@ -72,7 +72,7 @@ use yii\bootstrap\ActiveForm;
data-action-confirm-header="<?= Yii::t('SpaceModule.base', 'Create new link') ?>" ,
data-action-confirm="<?= Yii::t('SpaceModule.base', 'Please note that any links you have previously created will become invalid as soon as you create a new one. Would you like to proceed?') ?>"
data-action-click="ui.modal.load"
data-action-click-url="<?= Url::to(['/user/invite/reset-invite-link']) ?>">
data-action-click-url="<?= Url::to(['/user/invite/reset-invite-link', 'target' => $model->target]) ?>">
<small><?= Yii::t('SpaceModule.base', 'Create new link'); ?></small>
</a>
<?php endif; ?>