diff --git a/CHANGELOG.md b/CHANGELOG.md index 7372a8a1..d222bc52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Enh: Changed exception thrown in PasswordRecoveryService from `RuntimeException` to `NotFoundException`. (eseperio) - New #553: created Da\User\AuthClient\Microsoft365 auth client (edegaudenzi) - Ehh: Added SecurityHelper to the Bootstrap classMap +- Fix #546: The profile/show page must not be visible by default, implement configurable policy (TonisOrmisson) - Fix #397: No more fatal Exceptions when connecting to already taken Social Network (edegaudenzi) - Ehh: Added option to pre-fill recovery email via url parameter (TonisOrmisson) diff --git a/docs/install/configuration-options.md b/docs/install/configuration-options.md index ed86a61e..f1ffd672 100755 --- a/docs/install/configuration-options.md +++ b/docs/install/configuration-options.md @@ -241,6 +241,15 @@ simple backends with static administrators that won't change throughout time. Configures the permission name for `administrators`. See [AuthHelper](../../src/User/Helper/AuthHelper.php). +#### profileVisibility (type: `integer`, default:`0` (ProfileController::PROFILE_VISIBILITY_OWNER)) + +Configures to whom users 'profile/show' (public profile) page is shown. Constant values are defined in +[ProfileController](../../src/User/Controller/ProfileController.php) as constants. The visibility levels are: +- `0` (ProfileController::PROFILE_VISIBILITY_OWNER): The users profile page is shown ONLY to user itself, the owner of the profile. +- `1` (ProfileController::PROFILE_VISIBILITY_ADMIN): The users profile is shown ONLY to user itself (owner) AND users defined by module as admins. +- `2` (ProfileController::PROFILE_VISIBILITY_USERS): Any users profile page is shown to any other non-guest user. +- `3` (ProfileController::PROFILE_VISIBILITY_PUBLIC): Any user profile views are globally public and visible to anyone (including guests). + #### prefix (type: `string`, default: `user`) Configures the URL prefix for the module. @@ -313,11 +322,6 @@ Set to `true` to restrict user assignments to roles only. If `true` registration and last login IPs are not logged into users table, instead a dummy 127.0.0.1 is used - -#### disableProfileViewsForRegularUsers (type: `boolean`, default: `false`) - -If `true` only admin users have access to view any other user's profile. By default any user can see any other users public profile page. - #### minPasswordRequirements (type: `array`, default: `['lower' => 1, 'digit' => 1, 'upper' => 1]`) Minimum requirements when a new password is automatically generated. diff --git a/src/User/Controller/ProfileController.php b/src/User/Controller/ProfileController.php index 2a9e66b9..da779b8b 100644 --- a/src/User/Controller/ProfileController.php +++ b/src/User/Controller/ProfileController.php @@ -25,6 +25,15 @@ class ProfileController extends Controller { use ModuleAwareTrait; + /** @var int will allow only profile owner */ + const PROFILE_VISIBILITY_OWNER = 0; + /** @var int will allow profile owner and admin users */ + const PROFILE_VISIBILITY_ADMIN = 1; + /** @var int will allow any logged-in users */ + const PROFILE_VISIBILITY_USERS = 2; + /** @var int will allow anyone, including guests */ + public const PROFILE_VISIBILITY_PUBLIC = 3; + protected $profileQuery; /** @@ -73,10 +82,32 @@ public function actionIndex() public function actionShow($id) { $user = Yii::$app->user; - /** @var User $identity */ + $id = (int) $id; + + /** @var ?User $identity */ $identity = $user->getIdentity(); - if($user->getId() != $id && $this->module->disableProfileViewsForRegularUsers && !$identity->getIsAdmin()) { - throw new ForbiddenHttpException(); + + switch($this->module->profileVisibility) { + case static::PROFILE_VISIBILITY_OWNER: + if($identity === null || $id !== $user->getId()) { + throw new ForbiddenHttpException(); + } + break; + case static::PROFILE_VISIBILITY_ADMIN: + if($id === $user->getId() || ($identity !== null && $identity->getIsAdmin())) { + break; + } + throw new ForbiddenHttpException(); + case static::PROFILE_VISIBILITY_USERS: + if((!$user->getIsGuest())) { + break; + } + throw new ForbiddenHttpException(); + case static::PROFILE_VISIBILITY_PUBLIC: + break; + default: + throw new ForbiddenHttpException(); + } $profile = $this->profileQuery->whereUserId($id)->one(); diff --git a/src/User/Module.php b/src/User/Module.php index 8749715a..0f70d5e0 100755 --- a/src/User/Module.php +++ b/src/User/Module.php @@ -12,6 +12,7 @@ namespace Da\User; use Da\User\Contracts\MailChangeStrategyInterface; +use Da\User\Controller\ProfileController; use Da\User\Filter\AccessRuleFilter; use Yii; use yii\base\Module as BaseModule; @@ -181,6 +182,12 @@ class Module extends BaseModule * @var string the administrator permission name */ public $administratorPermissionName; + /** + * @var int $profileVisibility Defines the level of user's profile page visibility. + * Defaults to ProfileController::PROFILE_VISIBILITY_OWNER meaning no-one except the user itself can view + * the profile. @see ProfileController constants for possible options + */ + public $profileVisibility = ProfileController::PROFILE_VISIBILITY_OWNER; /** * @var string the route prefix */ @@ -242,10 +249,6 @@ class Module extends BaseModule * @var boolean whether to disable IP logging into user table */ public $disableIpLogging = false; - /** - * @var boolean whether to disable viewing any user's profile for non-admin users - */ - public $disableProfileViewsForRegularUsers = false; /** * @var array Minimum requirements when a new password is automatically generated. * Array structure: `requirement => minimum number characters`. diff --git a/tests/_fixtures/data/profile.php b/tests/_fixtures/data/profile.php index 7b2a40fa..f1b7234a 100644 --- a/tests/_fixtures/data/profile.php +++ b/tests/_fixtures/data/profile.php @@ -7,4 +7,8 @@ 'user_id' => 1, 'name' => 'John Doe', ], + 'seconduser' => [ + 'user_id' => 9, + 'name' => 'John Doe 2', + ], ]; diff --git a/tests/_fixtures/data/user.php b/tests/_fixtures/data/user.php index 2f596612..01fba29f 100644 --- a/tests/_fixtures/data/user.php +++ b/tests/_fixtures/data/user.php @@ -87,4 +87,30 @@ 'confirmed_at' => $time, 'gdpr_consent' => false, ], + 'admin' => [ + 'id' => 8, + 'username' => 'admin', + 'email' => 'admin@example.com', + 'password_hash' => '$2y$13$qY.ImaYBppt66qez6B31QO92jc5DYVRzo5NxM1ivItkW74WsSG6Ui', + 'auth_key' => '39HU0m5lpjWtqstFVGFjj6lFb7UZDeRq', + 'auth_tf_key' => '', + 'auth_tf_enabled' => false, + 'created_at' => $time, + 'updated_at' => $time, + 'confirmed_at' => $time, + 'gdpr_consent' => false, + ], + 'seconduser' => [ + 'id' => 9, + 'username' => 'seconduser', + 'email' => 'seconduser@example.com', + 'password_hash' => '$2y$13$qY.ImaYBppt66qez6B31QO92jc5DYVRzo5NxM1ivItkW74WsSG6Ui', + 'auth_key' => '776960890cec5ac53525f0e910716f5a', + 'auth_tf_key' => '', + 'auth_tf_enabled' => false, + 'created_at' => $time, + 'updated_at' => $time, + 'confirmed_at' => $time, + 'gdpr_consent' => false, + ], ]; diff --git a/tests/functional/ProfileCept.php b/tests/functional/ProfileCept.php new file mode 100644 index 00000000..29330982 --- /dev/null +++ b/tests/functional/ProfileCept.php @@ -0,0 +1,110 @@ +haveFixtures([ + 'user' => UserFixture::class, + 'profile' => ProfileFixture::class +]); +$user = $I->grabFixture('user', 'user'); +$secondUser = $I->grabFixture('user', 'seconduser'); +$adminUser = $I->grabFixture('user', 'admin'); +$I->wantTo('Ensure that profile profile pages are shown only to when user has correct permissions and else forbidden'); + +Yii::$app->getModule('user')->profileVisibility = \Da\User\Controller\ProfileController::PROFILE_VISIBILITY_OWNER; +Yii::$app->getModule('user')->administrators = ['admin']; + +$I->amLoggedInAs($user); +$I->amGoingTo('try to open users own profile page'); +$I->amOnRoute('/user/profile/show', ['id' => $user->id]); +$I->expectTo('See the profile page'); +$I->dontSee('Forbidden'); +$I->see('Joined on'); + +$I->amGoingTo('Profile visibility::OWNER: try to open another users profile page'); +$I->amOnRoute('/user/profile/show', ['id' => $secondUser->id]); +$I->expectTo('See the profile page'); +$I->see('Forbidden'); +$I->dontSee('Joined on'); + +Yii::$app->user->logout(); +$I->amGoingTo('Profile visibility::OWNER: try to open users profile page as guest'); +$I->amOnRoute('/user/profile/show', ['id' => $user->id]); +$I->expectTo('See the profile page'); +$I->see('Forbidden'); +$I->dontSee('Joined on'); + + +Yii::$app->getModule('user')->profileVisibility = \Da\User\Controller\ProfileController::PROFILE_VISIBILITY_ADMIN; +$I->amLoggedInAs($user); +$I->amGoingTo('Profile visibility::PROFILE_VISIBILITY_ADMIN: try to open users own profile page'); +$I->amOnRoute('/user/profile/show', ['id' => $user->id]); +$I->expectTo('See the profile page'); +$I->dontSee('Forbidden'); +$I->see('Joined on'); + +$I->amGoingTo('Profile visibility::PROFILE_VISIBILITY_ADMIN: try to open another users profile page as regular user'); +$I->amOnRoute('/user/profile/show', ['id' => $secondUser->id]); +$I->expectTo('See the profile page'); +$I->see('Forbidden'); +$I->dontSee('Joined on'); + +$I->amLoggedInAs($adminUser); +$I->amGoingTo('Profile visibility::PROFILE_VISIBILITY_ADMIN: try to open another users profile page as admin'); +$I->amOnRoute('/user/profile/show', ['id' => $user->id]); +$I->expectTo('See the profile page'); +$I->dontSee('Forbidden'); +$I->see('Joined on'); + +Yii::$app->user->logout(); +$I->amGoingTo('Profile visibility::PROFILE_VISIBILITY_ADMIN: try to open users profile page as guest'); +$I->amOnRoute('/user/profile/show', ['id' => $user->id]); +$I->expectTo('See the profile page'); +$I->see('Forbidden'); +$I->dontSee('Joined on'); + + +Yii::$app->getModule('user')->profileVisibility = \Da\User\Controller\ProfileController::PROFILE_VISIBILITY_USERS; +$I->amLoggedInAs($user); +$I->amGoingTo('Profile visibility::PROFILE_VISIBILITY_USERS: try to open users own profile page'); +$I->amOnRoute('/user/profile/show', ['id' => $user->id]); +$I->expectTo('See the profile page'); +$I->dontSee('Forbidden'); +$I->see('Joined on'); + +$I->amGoingTo('Profile visibility::PROFILE_VISIBILITY_USERS: try to open another users profile page as regular user'); +$I->amOnRoute('/user/profile/show', ['id' => $secondUser->id]); +$I->expectTo('See the profile page'); +$I->dontSee('Forbidden'); +$I->see('Joined on'); + +$I->amLoggedInAs($adminUser); +$I->amGoingTo('Profile visibility::PROFILE_VISIBILITY_USERS: try to open another users profile page as admin'); +$I->amOnRoute('/user/profile/show', ['id' => $user->id]); +$I->expectTo('See the profile page'); +$I->dontSee('Forbidden'); +$I->see('Joined on'); + +Yii::$app->user->logout(); +$I->amGoingTo('Profile visibility::PROFILE_VISIBILITY_USERS: try to open users profile page as guest'); +$I->amOnRoute('/user/profile/show', ['id' => $user->id]); +$I->expectTo('See the profile page'); +$I->see('Forbidden'); +$I->dontSee('Joined on'); + +Yii::$app->getModule('user')->profileVisibility = \Da\User\Controller\ProfileController::PROFILE_VISIBILITY_PUBLIC; + +Yii::$app->user->logout(); +$I->amGoingTo('Profile visibility::PROFILE_VISIBILITY_PUBLIC: try to open users profile page as guest'); +$I->amOnRoute('/user/profile/show', ['id' => $user->id]); +$I->expectTo('See the profile page'); +$I->dontSee('Forbidden'); +$I->see('Joined on'); +