// /** * Created by PhpStorm. * User: Eric * Date: 25/01/2018 * Time: 11:12 */ namespace Combodo\iTop\Test\UnitTest\Core; use Combodo\iTop\Test\UnitTest\ItopDataTestCase; use CoreCannotSaveObjectException; use CoreException; use DBObject; use DBObjectSearch; use DBObjectSet; use DeleteException; use MetaModel; use UserLocal; use UserRights; use utils; /** * @group itopRequestMgmt * @group userRights * @group defaultProfiles */ class UserRightsTest extends ItopDataTestCase { public function setUp(): void { parent::setUp(); utils::GetConfig()->SetModuleSetting('authent-local', 'password_validation.pattern', ''); } public static $aClasses = [ 'FunctionalCI' => ['class' => 'FunctionalCI', 'attcode' => 'name'], 'URP_UserProfile' => ['class' => 'URP_UserProfile', 'attcode' => 'reason'], 'UserLocal' => ['class' => 'UserLocal', 'attcode' => 'login'], 'UserRequest' => ['class' => 'UserRequest', 'attcode' => 'title'], 'ModuleInstallation' => ['class' => 'ModuleInstallation', 'attcode' => 'name'], ]; /** * @param string $sLoginPrefix * @param int $iProfileId initial profile * * @return \DBObject * @throws \CoreException * @throws \Exception */ protected function CreateUniqueUserAndLogin(string $sLoginPrefix, int $iProfileId): DBObject { static $iCount = 0; $sLogin = $sLoginPrefix.$iCount; $iCount++; $oUser = self::CreateUser($sLogin, $iProfileId); $_SESSION = []; UserRights::Login($sLogin); return $oUser; } protected function GivenUserWithProfiles(string $sLogin, array $aProfileIds): DBObject { $oProfiles = new \ormLinkSet(\UserLocal::class, 'profile_list', \DBObjectSet::FromScratch(\URP_UserProfile::class)); foreach ($aProfileIds as $iProfileId) { $oProfiles->AddItem(MetaModel::NewObject('URP_UserProfile', ['profileid' => $iProfileId, 'reason' => 'UNIT Tests'])); } $oUser = MetaModel::NewObject('UserLocal', array( 'login' => $sLogin, 'password' => 'Password1!', 'expiration' => UserLocal::EXPIRE_NEVER, 'profile_list' => $oProfiles, )); return $oUser; } public function testIsLoggedIn() { $this->assertFalse(UserRights::IsLoggedIn()); } /** * Test Login validation * * @dataProvider LoginProvider * * @param $sLogin * @param $bResult * * @throws \DictExceptionUnknownLanguage * @throws \OQLException */ public function testLogin($sLogin, $bResult) { $_SESSION = []; if ($sLogin == 'admin') { // Fixture data required in this case only try { self::CreateUser('admin', 1); } catch (CoreCannotSaveObjectException $e) { // The admin account could exist, depending on where and when the test suite is executed } } $this->assertEquals($bResult, UserRights::Login($sLogin)); $this->assertEquals($bResult, UserRights::IsLoggedIn()); } public function LoginProvider(): array { return [ ['admin', true], ['NotALoginForUnitTests', false], ['', false], ]; } /** Test IsActionAllowed when not logged => always true * * @dataProvider ActionAllowedNotLoggedProvider * * @param $aClassAction * * @throws \CoreException */ public function testIsActionAllowedNotLogged($aClassAction) { $bRes = UserRights::IsActionAllowed($aClassAction['class'], $aClassAction['action']) == UR_ALLOWED_YES; $this->assertEquals(true, $bRes); } public function ActionAllowedNotLoggedProvider(): array { $aClassActions = []; foreach (array_keys(self::$aClasses) as $sClass) { for ($i = 1; $i < 8; $i++) { $aClassAction = ['class' => $sClass, 'action' => $i]; $aClassActions[] = [$aClassAction]; } } return $aClassActions; } /** * @dataProvider ActionAllowedProvider * * @param int $iProfileId * @param array $aClassActionResult * * @throws \CoreException * @throws \DictExceptionUnknownLanguage * @throws \OQLException */ public function testIsActionAllowed(int $iProfileId, array $aClassActionResult) { $this->CreateUniqueUserAndLogin('test1', $iProfileId); $bRes = UserRights::IsActionAllowed($aClassActionResult['class'], $aClassActionResult['action']) == UR_ALLOWED_YES; $this->assertEquals($aClassActionResult['res'], $bRes); } /* * FunctionalCI => bizmodel searchable * UserRequest => bizmodel searchable requestmgmt * URP_UserProfile => addon/userrights * UserLocal => addon/authentication * ModuleInstallation => core view_in_gui * * Profiles: * 1 - Administrator * 2 - User Portal * 3 - Configuration manager * */ public function ActionAllowedProvider(): array { return [ /* Administrator (7 = UR_ACTION_CREATE) */ 'Administrator FunctionalCI write' => [1, ['class' => 'FunctionalCI', 'action' => 7, 'res' => true]], 'Administrator UserRequest write' => [1, ['class' => 'UserRequest', 'action' => 7, 'res' => true]], 'Administrator URP_UserProfile write' => [1, ['class' => 'URP_UserProfile', 'action' => 7, 'res' => true]], 'Administrator UserLocal write' => [1, ['class' => 'UserLocal', 'action' => 7, 'res' => true]], 'Administrator ModuleInstallation write' => [1, ['class' => 'ModuleInstallation', 'action' => 7, 'res' => true]], /* User Portal (7 = UR_ACTION_CREATE) */ 'User Portal FunctionalCI write' => [2, ['class' => 'FunctionalCI', 'action' => 7, 'res' => false]], 'User Portal UserRequest write' => [2, ['class' => 'UserRequest', 'action' => 7, 'res' => true]], 'User Portal URP_UserProfile write' => [2, ['class' => 'URP_UserProfile', 'action' => 7, 'res' => false]], 'User Portal UserLocal write' => [2, ['class' => 'UserLocal', 'action' => 7, 'res' => false]], 'User Portal ModuleInstallation write' => [2, ['class' => 'ModuleInstallation', 'action' => 7, 'res' => false]], /* Configuration manager (7 = UR_ACTION_CREATE) */ 'Configuration manager FunctionalCI write' => [3, ['class' => 'FunctionalCI', 'action' => 7, 'res' => true]], 'Configuration manager UserRequest write' => [3, ['class' => 'UserRequest', 'action' => 7, 'res' => false]], 'Configuration manager URP_UserProfile write' => [3, ['class' => 'URP_UserProfile', 'action' => 7, 'res' => false]], 'Configuration manager UserLocal write' => [3, ['class' => 'UserLocal', 'action' => 7, 'res' => false]], 'Configuration manager ModuleInstallation write' => [3, ['class' => 'ModuleInstallation', 'action' => 7, 'res' => false]], /* Administrator (1 = UR_ACTION_READ) */ 'Administrator FunctionalCI read' => [1, ['class' => 'FunctionalCI', 'action' => 1, 'res' => true]], 'Administrator UserRequest read' => [1, ['class' => 'UserRequest', 'action' => 1, 'res' => true]], 'Administrator URP_UserProfile read' => [1, ['class' => 'URP_UserProfile', 'action' => 1, 'res' => true]], 'Administrator UserLocal read' => [1, ['class' => 'UserLocal', 'action' => 1, 'res' => true]], 'Administrator ModuleInstallation read' => [1, ['class' => 'ModuleInstallation', 'action' => 1, 'res' => true]], /* User Portal (1 = UR_ACTION_READ) */ 'User Portal FunctionalCI read' => [2, ['class' => 'FunctionalCI', 'action' => 1, 'res' => true]], 'User Portal UserRequest read' => [2, ['class' => 'UserRequest', 'action' => 1, 'res' => true]], 'User Portal URP_UserProfile read' => [2, ['class' => 'URP_UserProfile', 'action' => 1, 'res' => false]], 'User Portal UserLocal read' => [2, ['class' => 'UserLocal', 'action' => 1, 'res' => false]], 'User Portal ModuleInstallation read' => [2, ['class' => 'ModuleInstallation', 'action' => 1, 'res' => true]], /* Configuration manager (1 = UR_ACTION_READ) */ 'Configuration manager FunctionalCI read' => [3, ['class' => 'FunctionalCI', 'action' => 1, 'res' => true]], 'Configuration manager UserRequest read' => [3, ['class' => 'UserRequest', 'action' => 1, 'res' => true]], 'Configuration manager URP_UserProfile read' => [3, ['class' => 'URP_UserProfile', 'action' => 1, 'res' => false]], 'Configuration manager UserLocal read' => [3, ['class' => 'UserLocal', 'action' => 1, 'res' => false]], 'Configuration manager ModuleInstallation read' => [3, ['class' => 'ModuleInstallation', 'action' => 1, 'res' => true]], ]; } /** Test IsActionAllowedOnAttribute * * @dataProvider ActionAllowedOnAttributeProvider * * @param int $iProfileId * @param array $aClassActionResult * * @throws \CoreException * @throws \DictExceptionUnknownLanguage * @throws \OQLException */ public function testIsActionAllowedOnAttribute(int $iProfileId, array $aClassActionResult) { $this->CreateUniqueUserAndLogin('test1', $iProfileId); $sClass = $aClassActionResult['class']; $bRes = UserRights::IsActionAllowedOnAttribute($sClass, self::$aClasses[$sClass]['attcode'], $aClassActionResult['action']) == UR_ALLOWED_YES; $this->assertEquals($aClassActionResult['res'], $bRes); } /* * FunctionalCI => bizmodel searchable * UserRequest => bizmodel searchable requestmgmt * URP_UserProfile => addon/userrights grant_by_profile * UserLocal => addon/authentication grant_by_profile * ModuleInstallation => core view_in_gui * */ public function ActionAllowedOnAttributeProvider(): array { return [ /* Administrator (2 = UR_ACTION_MODIFY) */ 'Administrator FunctionalCI' => [1, ['class' => 'FunctionalCI', 'action' => 2, 'res' => true]], 'Administrator UserRequest' => [1, ['class' => 'UserRequest', 'action' => 2, 'res' => true]], 'Administrator URP_UserProfile' => [1, ['class' => 'URP_UserProfile', 'action' => 2, 'res' => true]], 'Administrator UserLocal' => [1, ['class' => 'UserLocal', 'action' => 2, 'res' => true]], 'Administrator ModuleInstallation' => [1, ['class' => 'ModuleInstallation', 'action' => 2, 'res' => true]], /* User Portal (2 = UR_ACTION_MODIFY) */ 'User Portal FunctionalCI' => [2, ['class' => 'FunctionalCI', 'action' => 2, 'res' => false]], 'User Portal UserRequest' => [2, ['class' => 'UserRequest', 'action' => 2, 'res' => true]], 'User Portal URP_UserProfile' => [2, ['class' => 'URP_UserProfile', 'action' => 2, 'res' => false]], 'User Portal UserLocal' => [2, ['class' => 'UserLocal', 'action' => 2, 'res' => false]], 'User Portal ModuleInstallation' => [2, ['class' => 'ModuleInstallation', 'action' => 2, 'res' => true]], /* Configuration manager (2 = UR_ACTION_MODIFY) */ 'Configuration manager FunctionalCI' => [3, ['class' => 'FunctionalCI', 'action' => 2, 'res' => true]], 'Configuration manager UserRequest' => [3, ['class' => 'UserRequest', 'action' => 2, 'res' => false]], 'Configuration manager URP_UserProfile' => [3, ['class' => 'URP_UserProfile', 'action' => 2, 'res' => false]], 'Configuration manager UserLocal' => [3, ['class' => 'UserLocal', 'action' => 2, 'res' => false]], 'Configuration manager ModuleInstallation' => [3, ['class' => 'ModuleInstallation', 'action' => 2, 'res' => true]], ]; } /** * @dataProvider UserCannotLoseConsoleAccessProvider * * @throws \CoreException * @throws \DictExceptionUnknownLanguage * @throws \OQLException */ public function testUserCannotLoseConsoleAccess(int $iProfileId) { $oUser = $this->CreateUniqueUserAndLogin('test1', $iProfileId); $this->expectException(CoreCannotSaveObjectException::class); $this->expectExceptionMessage('Profile "Portal user" cannot be added it will deny the access to backoffice'); $this->AddProfileToUser($oUser, 2); } public function UserCannotLoseConsoleAccessProvider(): array { return [ 'Administrator' => [1], 'SuperUser' => [117], ]; } /** * @dataProvider UserCannotElevateTheirOwnRightsProvider * * @throws \CoreException * @throws \DictExceptionUnknownLanguage * @throws \OQLException */ public function testUserCannotElevateTheirOwnRights(int $iCurrentProfileId, int $iElevatedProfileId) { $oUser = $this->CreateUniqueUserAndLogin('test1', $iCurrentProfileId); $this->expectException(CoreCannotSaveObjectException::class); $this->AddProfileToUser($oUser, $iElevatedProfileId); } public function UserCannotElevateTheirOwnRightsProvider(): array { return [ 'Configuration manager to SuperUser' => ['current'=> 3, 'added' => 117], 'Configuration manager to Administrator' => ['current'=> 3, 'added' => 1], 'SuperUser to Administrator' => ['current'=> 117, 'added' => 1], ]; } /** * @dataProvider UserCannotDeleteOwnUserProvider * * @throws \CoreException * @throws \DictExceptionUnknownLanguage * @throws \OQLException */ public function testUserCannotDeleteOwnUser(int $iProfileId) { $oUser = $this->CreateUniqueUserAndLogin('test1', $iProfileId); $this->expectException(DeleteException::class); $oUser->DBDelete(); } public function UserCannotDeleteOwnUserProvider(): array { return [ 'Administrator' => [1], 'Configuration manager' => [3], 'SuperUser' => [117], ]; } /** * @dataProvider UserCannotRemoveOwnContactProvider * * @param int $iProfileId * * @throws \CoreException * @throws \DictExceptionUnknownLanguage * @throws \OQLException */ public function testUserCannotRemoveOwnContact(int $iProfileId) { $oUser = $this->CreateUniqueUserAndLogin('test1', $iProfileId); $oUser->Set('contactid', 0); $this->expectException(CoreCannotSaveObjectException::class); $oUser->DBWrite(); } public function UserCannotRemoveOwnContactProvider(): array { return [ 'Administrator' => [1], 'Configuration manager' => [3], 'SuperUser' => [117], ]; } public function testAdminCannotRemoveOwnAdminProfile() { $oUser = $this->CreateUniqueUserAndLogin('admin111', 1); // Administrator // Keep only the SuperUser profile (remove Administrator profile) $this->AddProfileToUser($oUser, 117); // SuperUser profile for the test $this->expectException(CoreCannotSaveObjectException::class); $this->expectExceptionMessage('You cannot remove your own Administrator profile. Ask another Administrator to do it for you'); $this->RemoveProfileFromUser($oUser, 1); // Remove admin profile } /** * @dataProvider UserCannotLoseUserEditionRightsProvider */ public function testUserCannotLoseUserEditionRights(int $iProfileId) { $oUser = $this->CreateUniqueUserAndLogin('configmgr111', $iProfileId); // SuperUser $this->AddProfileToUser($oUser, 3); $this->expectException(CoreCannotSaveObjectException::class); $this->expectExceptionMessage('You cannot remove your own rights to edit Users'); $this->RemoveProfileFromUser($oUser, $iProfileId); } public function UserCannotLoseUserEditionRightsProvider(): array { return [ 'Administrator' => [1], 'SuperUser' => [117], ]; } /** * @dataProvider PrivilegedUsersMustHaveBackofficeAccessProvider */ public function testPrivilegedUsersMustHaveBackofficeAccess(int $iProfileId) { $oUser = $this->GivenUserWithProfiles('test1', [$iProfileId, 2]); $this->expectException(CoreCannotSaveObjectException::class); $this->expectExceptionMessage('Profile "Portal user" cannot be given to privileged Users (Administrators, SuperUsers and REST Services Users)'); $oUser->DBInsert(); } public function PrivilegedUsersMustHaveBackofficeAccessProvider(): array { return [ 'killing another administrator' => [1], 'killing superuser ' => [117], 'killing Rest User' => [1024], ]; } public function testNonPrivilegedUsersCanBeDeniedFromBackoffice() { $oUser = $this->GivenUserWithProfiles('test1', [5, 2]); // No exception expected $oUser->DBInsert(); $this->expectNotToPerformAssertions(); } /** *@dataProvider NonAdminCanListOwnProfilesProvider */ public function testNonAdminCanListOwnProfiles($bHideAdministrators) { utils::GetConfig()->Set('security.hide_administrators', $bHideAdministrators); $oUser = $this->CreateUniqueUserAndLogin('test1', 2); // portal user // List the link between the User and the Profiles $oSearch = new DBObjectSearch('URP_UserProfile'); $oSearch->AddCondition('userid', $oUser->GetKey()); $oSet = new DBObjectSet($oSearch); $this->assertEquals(1, $oSet->Count()); // Get the Profiles as well $oSearch = DBObjectSearch::FromOQL('SELECT URP_Profiles JOIN URP_UserProfile ON URP_UserProfile.profileid = URP_Profiles.id WHERE URP_UserProfile.userid='.$oUser->GetKey()); $oSet = new DBObjectSet($oSearch); $this->assertEquals(1, $oSet->Count()); } public function NonAdminCanListOwnProfilesProvider(): array { return [ 'with Admins visible' => [false], 'with Admins hidden' => [true], ]; } /** * @dataProvider NonAdminCannotListAdminProfilesProvider */ public function testNonAdminCannotListAdminProfiles($bHideAdministrators, $iExpectedCount) { utils::GetConfig()->Set('security.hide_administrators', $bHideAdministrators); $oUserAdmin = $this->CreateUser('admin1', 1); $this->CreateUniqueUserAndLogin('test1', 2); // portal user $oSearch = new DBObjectSearch('URP_UserProfile'); $oSearch->AddCondition('userid', $oUserAdmin->GetKey()); $oSet = new DBObjectSet($oSearch); $this->assertEquals($iExpectedCount, $oSet->Count(), 'Visibility on Link between User and Administrator Profiles should be controlled by hide_administrators setting'); // Get the Profiles as well $oSearch = DBObjectSearch::FromOQL('SELECT URP_Profiles JOIN URP_UserProfile ON URP_UserProfile.profileid = URP_Profiles.id WHERE URP_UserProfile.userid='.$oUserAdmin->GetKey()); $oSet = new DBObjectSet($oSearch); $this->assertEquals($iExpectedCount, $oSet->Count(), 'Visibility on Administrator Profiles should be controlled by hide_administrators setting'); } public function NonAdminCannotListAdminProfilesProvider(): array { return [ 'with Admins visible' => ['hide_administrators' => false, 'visible_objects' => 1], 'with Admins hidden' => ['hide_administrators' => true, 'visible_objects' => 0], ]; } public function testFindUser_ExistingInternalUser() { $sLogin = 'AnInternalUser'.uniqid(); $iKey = $this->GivenObjectInDB(\UserLocal::class, ['login' => $sLogin]); $this->assertDBQueryCount( 1, fn () => $this->FindUserAndAssertItHasBeenFound($sLogin, $iKey), 'A query should be performed the first time FindUser is called' ); $this->assertDBQueryCount( 0, fn () => $this->FindUserAndAssertItHasBeenFound($sLogin, $iKey), 'The cache should prevent additional queries on subsequent calls' ); } public function testFindUser_ExistingExternalUser() { $sLogin = 'AnExternalUser'.uniqid(); $iKey = $this->GivenObjectInDB(\UserExternal::class, ['login' => $sLogin]); $this->assertDBQueryCount( 2, fn () => $this->FindUserAndAssertItHasBeenFound($sLogin, $iKey), 'Some queries should be performed the first time FindUser is called' ); $this->assertDBQueryCount( 0, fn () => $this->FindUserAndAssertItHasBeenFound($sLogin, $iKey), 'The cache should prevent additional queries on subsequent calls' ); } public function testFindUser_UnknownLogin() { $sLogin = 'NobodyLogin'; $this->assertDBQueryCount( 2, fn () => $this->FindUserAndAssertItWasNotFound($sLogin), 'Some queries should be performed the first time FindUser is called' ); $this->assertDBQueryCount( 0, fn () => $this->FindUserAndAssertItWasNotFound($sLogin), 'The cache should prevent additional queries on subsequent calls' ); } public function FindUserAndAssertItHasBeenFound($sLogin, $iExpectedKey) { $oUser = $this->InvokeNonPublicStaticMethod(UserRights::class, "FindUser", [$sLogin]); static::assertIsDBObject(\User::class, $iExpectedKey, $oUser, 'FindUser should return the User object corresponding to the login'); } public function FindUserAndAssertItWasNotFound($sLogin) { $oUser = $this->InvokeNonPublicStaticMethod(UserRights::class, "FindUser", [$sLogin]); static::assertNull($oUser, 'FindUser should return null when the login is unknown'); } }