diff --git a/addons/userrights/userrightsprofile.class.inc.php b/addons/userrights/userrightsprofile.class.inc.php
index 6370e5fe3c..b35793ac2c 100644
--- a/addons/userrights/userrightsprofile.class.inc.php
+++ b/addons/userrights/userrightsprofile.class.inc.php
@@ -220,6 +220,7 @@ class URP_UserProfile extends UserRightsBaseClassGUI
{
$aParams = array
(
+ "is_link" => true, //since 3.1 N°5324
"category" => "addon/userrights,grant_by_profile,filter",
"key_type" => "autoincrement",
"name_attcode" => array("userlogin", "profile"),
diff --git a/addons/userrights/userrightsprofile.db.class.inc.php b/addons/userrights/userrightsprofile.db.class.inc.php
index 12a8874c94..714fa6ca02 100644
--- a/addons/userrights/userrightsprofile.db.class.inc.php
+++ b/addons/userrights/userrightsprofile.db.class.inc.php
@@ -326,6 +326,7 @@ class URP_UserProfile extends UserRightsBaseClassGUI
{
$aParams = array
(
+ "is_link" => true, //since 3.1 N°5324
"category" => "addon/userrights",
"key_type" => "autoincrement",
"name_attcode" => array("userlogin", "profile"),
diff --git a/addons/userrights/userrightsprojection.class.inc.php b/addons/userrights/userrightsprojection.class.inc.php
index 50413fabc5..cc26b1467d 100644
--- a/addons/userrights/userrightsprojection.class.inc.php
+++ b/addons/userrights/userrightsprojection.class.inc.php
@@ -269,6 +269,7 @@ class URP_UserProfile extends UserRightsBaseClass
{
$aParams = array
(
+ "is_link" => true, //since 3.1 N°5324
"category" => "addon/userrights",
"key_type" => "autoincrement",
"name_attcode" => array("userlogin", "profile"),
diff --git a/core/config.class.inc.php b/core/config.class.inc.php
index 626b3e819d..e4fa8847b8 100644
--- a/core/config.class.inc.php
+++ b/core/config.class.inc.php
@@ -1635,6 +1635,14 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
],
+ 'security.single_profile_completion' => [
+ 'type' => 'array',
+ 'description' => 'Non standalone profiles can be completed by other profiles via this configuration. default configuration is equivalent to [\'Portal power user\' => \'Portal user\'] configuration. unless you have specific portal customization.',
+ 'default' => null,
+ 'value' => false,
+ 'source_of_value' => '',
+ 'show_in_conf_sample' => false,
+ ],
'behind_reverse_proxy' => [
'type' => 'bool',
'description' => 'If true, then proxies custom header (X-Forwarded-*) are taken into account. Use only if the webserver is not publicly accessible (reachable only by the reverse proxy)',
diff --git a/core/datamodel.core.xml b/core/datamodel.core.xml
index d15ca731b3..6703f758ce 100644
--- a/core/datamodel.core.xml
+++ b/core/datamodel.core.xml
@@ -57,6 +57,7 @@
cmdbAbstractObject
+ 1
addon/userrights,grant_by_profile
1
diff --git a/datamodels/2.x/itop-profiles-itil/datamodel.itop-profiles-itil.xml b/datamodels/2.x/itop-profiles-itil/datamodel.itop-profiles-itil.xml
index d4e83b713f..c0e45d451c 100755
--- a/datamodels/2.x/itop-profiles-itil/datamodel.itop-profiles-itil.xml
+++ b/datamodels/2.x/itop-profiles-itil/datamodel.itop-profiles-itil.xml
@@ -1,5 +1,5 @@
-
+
@@ -544,5 +544,19 @@
+
+
+
+ User profile %1$s cannot be standalone. You should add
+ other profiles otherwise you may encounter access issue with this user.
+
+
+
+
+ Le profil %1$s ne peut être seul. Sans le rajout d'autres
+ profiles, l'utilisateur peut rencontrer des problèmes dans iTop.
+
+
+
diff --git a/datamodels/2.x/itop-profiles-itil/module.itop-profiles-itil.php b/datamodels/2.x/itop-profiles-itil/module.itop-profiles-itil.php
index 9f91ca4bc8..8f58ee2c41 100755
--- a/datamodels/2.x/itop-profiles-itil/module.itop-profiles-itil.php
+++ b/datamodels/2.x/itop-profiles-itil/module.itop-profiles-itil.php
@@ -36,6 +36,7 @@ SetupWebPage::AddModule(
// Components
//
'datamodel' => array(
+ 'src/UserProfilesEventListener.php'
),
'webservice' => array(
//'webservices.itop-profiles-itil.php',
diff --git a/datamodels/2.x/itop-profiles-itil/src/UserProfilesEventListener.php b/datamodels/2.x/itop-profiles-itil/src/UserProfilesEventListener.php
new file mode 100644
index 0000000000..1da291fdc9
--- /dev/null
+++ b/datamodels/2.x/itop-profiles-itil/src/UserProfilesEventListener.php
@@ -0,0 +1,207 @@
+ repairing profile id
+ private $aNonStandaloneProfilesMap = [];
+
+ /**
+ * @inheritDoc
+ */
+ public function RegisterEventsAndListeners()
+ {
+ $this->Init();
+
+ if (false === $this->bIsRepairmentEnabled){
+ return;
+ }
+
+ $callback = [$this, 'OnUserProfileLinkChange'];
+ $aEventSource = [\User::class, \UserExternal::class, \UserInternal::class];
+
+ EventService::RegisterListener(
+ EVENT_DB_BEFORE_WRITE,
+ $callback,
+ $aEventSource
+ );
+
+ EventService::RegisterListener(
+ EVENT_DB_LINKS_CHANGED,
+ $callback,
+ $aEventSource
+ );
+ }
+
+ public function IsRepairmentEnabled() : bool
+ {
+ return $this->bIsRepairmentEnabled;
+ }
+
+ public function OnUserProfileLinkChange(EventData $oEventData): void {
+ /** @var \User $oObject */
+ $oUser = $oEventData->Get('object');
+
+ try {
+ $this->RepairProfiles($oUser);
+ } catch (Exception $e) {
+ IssueLog::Error('Exception occurred on RepairProfiles', LogChannels::DM_CRUD, [
+ 'user_class' => get_class($oUser),
+ 'user_id' => $oUser->GetKey(),
+ 'exception_message' => $e->getMessage(),
+ 'exception_stacktrace' => $e->getTraceAsString(),
+ ]);
+ }
+ }
+
+ /**
+ * @param $aPortalDispatcherData: passed only for testing purpose
+ *
+ * @return void
+ * @throws \ConfigException
+ * @throws \CoreException
+ */
+ public function Init($aPortalDispatcherData=null) : void {
+ if (is_null($aPortalDispatcherData)){
+ $aPortalDispatcherData = \PortalDispatcherData::GetData();
+ }
+
+ $aNonStandaloneProfiles = \utils::GetConfig()->Get(self::USERPROFILE_REPAIR_ITOP_PARAM_NAME, null);
+
+ //When there are several customized portals on an itop, choosing a specific profile means choosing which portal user will access
+ //In that case, itop administrator has to specify it via itop configuration. we dont use default profiles repairment otherwise
+ if (is_null($aNonStandaloneProfiles)){
+ if (count($aPortalDispatcherData) > 2){
+ $this->bIsRepairmentEnabled = false;
+ return;
+ }
+
+ $aPortalNames = array_keys($aPortalDispatcherData);
+ sort($aPortalNames);
+ if ($aPortalNames !== ['backoffice', 'itop-portal']){
+ $this->bIsRepairmentEnabled = false;
+ return;
+ }
+ }
+
+ if (is_null($aNonStandaloneProfiles)){
+ //default configuration in the case there are no customized portals
+ $aNonStandaloneProfiles = [ POWER_USER_PORTAL_PROFILE_NAME => PORTAL_PROFILE_NAME ];
+ }
+
+ if (! is_array($aNonStandaloneProfiles)){
+ \IssueLog::Error(sprintf("%s is badly configured. it should be an array.", self::USERPROFILE_REPAIR_ITOP_PARAM_NAME), null, [self::USERPROFILE_REPAIR_ITOP_PARAM_NAME => $aNonStandaloneProfiles]);
+ $this->bIsRepairmentEnabled = false;
+ return;
+ }
+
+ if (empty($aNonStandaloneProfiles)){
+ //Feature specifically disabled in itop configuration
+ $this->bIsRepairmentEnabled = false;
+ return;
+ }
+
+ try {
+ $this->FetchRepairingProfileIds($aNonStandaloneProfiles);
+ } catch (\Exception $e) {
+ IssueLog::Error('Exception when searching user portal profile', LogChannels::DM_CRUD, [
+ 'exception_message' => $e->getMessage(),
+ 'exception_stacktrace' => $e->getTraceAsString(),
+ ]);
+ $this->bIsRepairmentEnabled = false;
+ return;
+ }
+
+ $this->bIsRepairmentEnabled = true;
+ }
+
+ public function FetchRepairingProfileIds(array $aNonStandaloneProfiles) : void {
+ $aProfilesToSearch = array_unique(array_values($aNonStandaloneProfiles));
+ if(($iIndex = array_search(null, $aProfilesToSearch)) !== false) {
+ unset($aProfilesToSearch[$iIndex]);
+ }
+
+ if (1 === count($aProfilesToSearch)){
+ $sInCondition = sprintf('"%s"', array_pop($aProfilesToSearch));
+ } else {
+ $sInCondition = sprintf('"%s"', implode('","', $aProfilesToSearch));
+ }
+
+ $sOql = "SELECT URP_Profiles WHERE name IN ($sInCondition)";
+ $oSearch = \DBSearch::FromOQL($sOql);
+ $oSearch->AllowAllData();
+ $oSet = new \DBObjectSet($oSearch);
+ $aProfiles = [];
+ while(($oProfile = $oSet->Fetch()) != null) {
+ $sProfileName = $oProfile->Get('name');
+ $aProfiles[$sProfileName] = $oProfile->GetKey();
+ }
+
+ $this->aNonStandaloneProfilesMap = [];
+ foreach ($aNonStandaloneProfiles as $sNonStandaloneProfileName => $sRepairProfileName) {
+ if (is_null($sRepairProfileName)) {
+ $this->aNonStandaloneProfilesMap[$sNonStandaloneProfileName] = null;
+ continue;
+ }
+
+ if (!array_key_exists($sRepairProfileName, $aProfiles)) {
+ throw new \Exception(sprintf("%s is badly configured. profile $sRepairProfileName does not exist.", self::USERPROFILE_REPAIR_ITOP_PARAM_NAME));
+ }
+
+ $this->aNonStandaloneProfilesMap[$sNonStandaloneProfileName] = $aProfiles[$sRepairProfileName];
+ }
+ }
+
+ public function RepairProfiles(?\User $oUser) : void
+ {
+ if (!is_null($oUser))
+ {
+ $oCurrentUserProfileSet = $oUser->Get('profile_list');
+ if ($oCurrentUserProfileSet->Count() === 1){
+ $oProfile = $oCurrentUserProfileSet->Fetch();
+ $sSingleProfileName = $oProfile->Get('profile');
+
+ if (array_key_exists($sSingleProfileName, $this->aNonStandaloneProfilesMap)) {
+ $sRepairingProfileId = $this->aNonStandaloneProfilesMap[$sSingleProfileName];
+ if (is_null($sRepairingProfileId)){
+ //Notify current user via session messages that there will be an issue
+ //Without preventing from commiting
+ $sMessage = \Dict::Format("Class:User/NonStandaloneProfileWarning", $sSingleProfileName);
+ $oUser::SetSessionMessage(get_class($oUser), $oUser->GetKey(), 1, $sMessage, 'WARNING', 1);
+ } else {
+ //Completing profiles profiles by adding repairing one : by default portal user to a power portal user
+ $oUserProfile = new \URP_UserProfile();
+ $oUserProfile->Set('profileid', $sRepairingProfileId);
+ $oCurrentUserProfileSet->AddItem($oUserProfile);
+ $oUser->Set('profile_list', $oCurrentUserProfileSet);
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/tests/php-unit-tests/unitary-tests/datamodels/2.x/itop-profiles-itil/UserProfilesEventListenerTest.php b/tests/php-unit-tests/unitary-tests/datamodels/2.x/itop-profiles-itil/UserProfilesEventListenerTest.php
new file mode 100644
index 0000000000..a1e1e7e6bc
--- /dev/null
+++ b/tests/php-unit-tests/unitary-tests/datamodels/2.x/itop-profiles-itil/UserProfilesEventListenerTest.php
@@ -0,0 +1,614 @@
+
+//
+
+/**
+ * Created by PhpStorm.
+ * User: Eric
+ * Date: 25/01/2018
+ * Time: 11:12
+ */
+
+namespace Combodo\iTop\Test\UnitTest\Module\iTopProfilesItil;
+
+use Combodo\iTop\Application\Helper\Session;
+use Combodo\iTop\Application\UI\Base\Layout\NavigationMenu\NavigationMenuFactory;
+use Combodo\iTop\ItilProfiles\UserProfilesEventListener;
+use Combodo\iTop\Service\Events\EventService;
+use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
+use DBObjectSet;
+use URP_UserProfile;
+use UserRights;
+
+/**
+ * @since 3.1.0 N°5324
+ * @group itopRequestMgmt
+ * @group userRights
+ * @group defaultProfiles
+ *
+ * @runTestsInSeparateProcesses
+ * @preserveGlobalState disabled
+ * @backupGlobals disabled
+ */
+class UserProfilesEventListenerTest extends ItopDataTestCase
+{
+ public function setUp(): void {
+ parent::setUp();
+
+ //reset conf to have nominal behaviour
+ \MetaModel::GetConfig()->Set(UserProfilesEventListener::USERPROFILE_REPAIR_ITOP_PARAM_NAME, null);
+ }
+
+ public function PortaPowerUserProvider(){
+ return [
+ 'Portal power user only => user should be repaired by adding User portal profile' => [
+ 'aAssociatedProfilesBeforeUserCreation' => [
+ 'Portal power user'
+ ],
+ 'aExpectedAssociatedProfilesAfterUserCreation'=> [
+ 'Portal power user',
+ 'Portal user',
+ ]
+ ],
+ 'Portal power user + Support Agent => profiles untouched' => [
+ 'aAssociatedProfilesBeforeUserCreation' => [
+ 'Portal power user',
+ 'Support Agent',
+ ],
+ 'aExpectedAssociatedProfilesAfterUserCreation'=> [
+ 'Portal power user',
+ 'Support Agent',
+ ]
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider PortaPowerUserProvider
+ */
+ public function testUserLocalCreation($aAssociatedProfilesBeforeUserCreation,
+ $aExpectedAssociatedProfilesAfterUserCreation)
+ {
+ $oUser = new \UserLocal();
+ $sLogin = 'testUserLocalCreationWithPortalPowerUserProfile-'.uniqid();
+ $oUser->Set('login', $sLogin);
+ $oUser->Set('password', 'ABCD1234@gabuzomeu');
+ $oUser->Set('language', 'EN US');
+ $this->commonUserCreation($oUser, $aAssociatedProfilesBeforeUserCreation, $aExpectedAssociatedProfilesAfterUserCreation);
+ }
+
+ /**
+ * @dataProvider PortaPowerUserProvider
+ */
+ public function testUserLocalUpdate($aAssociatedProfilesBeforeUserCreation,
+ $aExpectedAssociatedProfilesAfterUserCreation)
+ {
+ $oUser = new \UserLocal();
+ $sLogin = 'testUserLocalUpdateWithPortalPowerUserProfile-'.uniqid();
+ $oUser->Set('login', $sLogin);
+ $oUser->Set('password', 'ABCD1234@gabuzomeu');
+ $oUser->Set('language', 'EN US');
+ $this->commonUserUpdate($oUser, $aAssociatedProfilesBeforeUserCreation, $aExpectedAssociatedProfilesAfterUserCreation);
+ }
+
+ /**
+ * @dataProvider PortaPowerUserProvider
+ */
+ public function testUserLDAPCreation($aAssociatedProfilesBeforeUserCreation,
+ $aExpectedAssociatedProfilesAfterUserCreation)
+ {
+ $oUser = new \UserLDAP();
+ $sLogin = 'testUserLDAPCreationWithPortalPowerUserProfile-'.uniqid();
+ $oUser->Set('login', $sLogin);
+ $this->commonUserCreation($oUser, $aAssociatedProfilesBeforeUserCreation, $aExpectedAssociatedProfilesAfterUserCreation);
+ }
+
+ /**
+ * @dataProvider PortaPowerUserProvider
+ */
+ public function testUserLDAPUpdate($aAssociatedProfilesBeforeUserCreation,
+ $aExpectedAssociatedProfilesAfterUserCreation)
+ {
+ $oUser = new \UserLDAP();
+ $sLogin = 'testUserLDAPUpdateWithPortalPowerUserProfile-'.uniqid();
+ $oUser->Set('login', $sLogin);
+ $this->commonUserUpdate($oUser, $aAssociatedProfilesBeforeUserCreation, $aExpectedAssociatedProfilesAfterUserCreation);
+ }
+
+ /**
+ * @dataProvider PortaPowerUserProvider
+ */
+ public function testUserExternalCreation($aAssociatedProfilesBeforeUserCreation,
+ $aExpectedAssociatedProfilesAfterUserCreation)
+ {
+ $oUser = new \UserExternal();
+ $sLogin = 'testUserLDAPCreationWithPortalPowerUserProfile-'.uniqid();
+ $oUser->Set('login', $sLogin);
+ $this->commonUserCreation($oUser, $aAssociatedProfilesBeforeUserCreation, $aExpectedAssociatedProfilesAfterUserCreation);
+ }
+
+ /**
+ * @dataProvider PortaPowerUserProvider
+ */
+ public function testUserExternalUpdate($aAssociatedProfilesBeforeUserCreation,
+ $aExpectedAssociatedProfilesAfterUserCreation)
+ {
+ $oUser = new \UserExternal();
+ $sLogin = 'testUserLDAPUpdateWithPortalPowerUserProfile-'.uniqid();
+ $oUser->Set('login', $sLogin);
+ $this->commonUserUpdate($oUser, $aAssociatedProfilesBeforeUserCreation, $aExpectedAssociatedProfilesAfterUserCreation);
+ }
+
+ public function CreateUserForProfileTesting(\User $oUserToCreate, array $aAssociatedProfilesBeforeUserCreation, $bDbInsert=true) : array
+ {
+ $aProfiles = [];
+ $oSearch = \DBSearch::FromOQL("SELECT URP_Profiles");
+ $oProfileSet = new DBObjectSet($oSearch);
+ while (($oProfile = $oProfileSet->Fetch()) != null){
+ $aProfiles[$oProfile->Get('name')] = $oProfile;
+ }
+
+ $this->CreateTestOrganization();
+ $oContact = $this->CreatePerson("1");
+ $iContactid = $oContact->GetKey();
+
+ $oUserToCreate->Set('contactid', $iContactid);
+ $sUserClass = get_class($oUserToCreate);
+
+ $oUserProfileList = $oUserToCreate->Get('profile_list');
+ foreach ($aAssociatedProfilesBeforeUserCreation as $sProfileName){
+ $oUserProfile = new URP_UserProfile();
+ $oProfile = $aProfiles[$sProfileName];
+ $oUserProfile->Set('profileid', $oProfile->GetKey());
+ $oUserProfile->Set('reason', 'UNIT Tests');
+ $oUserProfileList->AddItem($oUserProfile);
+ }
+
+ $oUserToCreate->Set('profile_list', $oUserProfileList);
+ if ($bDbInsert){
+ $sId = $oUserToCreate->DBInsert();
+ } else {
+ $sId = -1;
+ }
+
+ return [ $sId, $aProfiles];
+ }
+
+ public function commonUserCreation($oUserToCreate, $aAssociatedProfilesBeforeUserCreation,
+ $aExpectedAssociatedProfilesAfterUserCreation, $bTestUserItopAccess=true)
+ {
+ $sUserClass = get_class($oUserToCreate);
+ list ($sId, $aProfiles) = $this->CreateUserForProfileTesting($oUserToCreate, $aAssociatedProfilesBeforeUserCreation);
+
+ $this->CheckProfilesAreOk($sUserClass, $sId, $aExpectedAssociatedProfilesAfterUserCreation, $bTestUserItopAccess);
+ }
+
+ public function CheckProfilesAreOk($sUserClass, $sId, $aExpectedAssociatedProfilesAfterUserCreation, $bTestUserItopAccess=true){
+ $oUser = \MetaModel::GetObject($sUserClass, $sId);
+ $oUserProfileList = $oUser->Get('profile_list');
+ $aProfilesAfterCreation=[];
+ while (($oProfile = $oUserProfileList->Fetch()) != null){
+ $aProfilesAfterCreation[] = $oProfile->Get('profile');
+ }
+
+ foreach ($aExpectedAssociatedProfilesAfterUserCreation as $sExpectedProfileName){
+ $this->assertTrue(in_array($sExpectedProfileName, $aProfilesAfterCreation),
+ "profile \'$sExpectedProfileName\' should be asociated to user after creation. " . var_export($aProfilesAfterCreation, true) );
+ }
+
+ if (! $bTestUserItopAccess){
+ return;
+ }
+
+ $_SESSION = [];
+
+ //$this->expectException(\Exception::class);
+ UserRights::Login($oUser->Get('login'));
+
+ if (! UserRights::IsPortalUser()) {
+ //calling this API triggers Fatal Error on below OQL used by \User->GetContactObject() for a user with only 'portal power user' profile
+ /**
+ * Error: No result for the single row query: 'SELECT DISTINCT `Contact`.`id` AS `Contactid`, `Contact`.`name` AS `Contactname`, `Contact`.`status` AS `Contactstatus`, `Contact`.`org_id` AS `Contactorg_id`, `Organization_org_id`.`name` AS `Contactorg_name`, `Contact`.`email` AS `Contactemail`, `Contact`.`phone` AS `Contactphone`, `Contact`.`notify` AS `Contactnotify`, `Contact`.`function` AS `Contactfunction`, `Contact`.`finalclass` AS `Contactfinalclass`, IF((`Contact`.`finalclass` IN ('Team', 'Contact')), CAST(CONCAT(COALESCE(`Contact`.`name`, '')) AS CHAR), CAST(CONCAT(COALESCE(`Contact_poly_Person`.`first_name`, ''), COALESCE(' ', ''), COALESCE(`Contact`.`name`, '')) AS CHAR)) AS `Contactfriendlyname`, COALESCE((`Contact`.`status` = 'inactive'), 0) AS `Contactobsolescence_flag`, `Contact`.`obsolescence_date` AS `Contactobsolescence_date`, CAST(CONCAT(COALESCE(`Organization_org_id`.`name`, '')) AS CHAR) AS `Contactorg_id_friendlyname`, COALESCE((`Organization_org_id`.`status` = 'inactive'), 0) AS `Contactorg_id_obsolescence_flag` FROM `contact` AS `Contact` INNER JOIN `organization` AS `Organization_org_id` ON `Contact`.`org_id` = `Organization_org_id`.`id` LEFT JOIN `person` AS `Contact_poly_Person` ON `Contact`.`id` = `Contact_poly_Person`.`id` WHERE ((`Contact`.`id` = 40) AND 0) '.
+ */
+ NavigationMenuFactory::MakeStandard();
+ }
+
+ $this->assertTrue(true, 'after fix N°5324 no exception raised');
+ // logout
+ $_SESSION = [];
+ }
+
+ public function commonUserUpdate($oUserToCreate, $aAssociatedProfilesBeforeUserCreation,
+ $aExpectedAssociatedProfilesAfterUserCreation)
+ {
+ $sUserClass = get_class($oUserToCreate);
+ list ($sId, $aProfiles) = $this->CreateUserForProfileTesting($oUserToCreate, ["Administrator"]);
+
+ $oUserToUpdate = \MetaModel::GetObject($sUserClass, $sId);
+ $oProfileList = $oUserToUpdate->Get('profile_list');
+ while($oObj = $oProfileList->Fetch()){
+ $oProfileList->RemoveItem($oObj->GetKey());
+ }
+
+ foreach ($aAssociatedProfilesBeforeUserCreation as $sProfileName){
+ $oAdminUrpProfile = new URP_UserProfile();
+ $oProfile = $aProfiles[$sProfileName];
+ $oAdminUrpProfile->Set('profileid', $oProfile->GetKey());
+ $oAdminUrpProfile->Set('reason', 'UNIT Tests');
+ $oProfileList->AddItem($oAdminUrpProfile);
+ }
+
+ $oUserToUpdate->Set('profile_list', $oProfileList);
+ $oUserToUpdate->DBWrite();
+
+ $this->CheckProfilesAreOk($sUserClass, $sId, $aExpectedAssociatedProfilesAfterUserCreation);
+ }
+
+ public function testUpdateUserExternalProfilesViaLinks(){
+ $aInitialProfiles = [ "Administrator", "Portal power user"];
+
+ $oUser = new \UserExternal();
+ $sLogin = 'testUserLDAPUpdateWithPortalPowerUserProfile-'.uniqid();
+ $oUser->Set('login', $sLogin);
+
+ $sUserClass = get_class($oUser);
+ list ($sId, $aProfiles) = $this->CreateUserForProfileTesting($oUser, $aInitialProfiles);
+
+ $aURPUserProfileByUser = $this->GetURPUserProfileByUser($sId);
+ $sProfileNameToRemove = "Administrator";
+ if (array_key_exists($sProfileNameToRemove, $aURPUserProfileByUser)){
+ $oURPUserProfile = $aURPUserProfileByUser[$sProfileNameToRemove];
+ $oURPUserProfile->DBDelete();
+ }
+
+ $aExpectedProfilesAfterUpdate = ["Portal power user", "Portal user"];
+ $this->CheckProfilesAreOk($sUserClass, $sId, $aExpectedProfilesAfterUpdate);
+ }
+
+ public function BulkUpdateUserExternalProfilesViaLinksProvider(){
+ return [
+ 'user profiles REPAIR 1' => [
+ "aInitialProfiles" => [ "Administrator"],
+ "aOperation" => [
+ '-Administrator',
+ '+Portal power user',
+ ],
+ "aExpectedProfilesAfterUpdate" => ["Portal power user", "Portal user"],
+ ],
+ 'user profiles REPAIR 2' => [
+ "aInitialProfiles" => [ "Administrator"],
+ "aOperation" => [
+ '+Portal power user',
+ '-Administrator',
+ ],
+ "aExpectedProfilesAfterUpdate" => ["Portal power user", "Portal user"],
+ ],
+ 'user profiles REPAIR 3' => [
+ "aInitialProfiles" => [ "Administrator", "Portal power user"],
+ "aOperation" => [
+ '-Administrator',
+ ],
+ "aExpectedProfilesAfterUpdate" => ["Portal power user", "Portal user"],
+ ],
+ 'NOTHING DONE with 1 profile' => [
+ "aInitialProfiles" => [ "Administrator", "Portal power user"],
+ "aOperation" => [
+ '-Portal power user',
+ ],
+ "aExpectedProfilesAfterUpdate" => ["Administrator"],
+ ],
+ 'NOTHING DONE with 2 profiles including power...' => [
+ "aInitialProfiles" => [ "Administrator"],
+ "aOperation" => [
+ '+Portal power user',
+ ],
+ "aExpectedProfilesAfterUpdate" => ["Administrator", "Portal power user"],
+ ],
+ 'NOTHING DONE with 2 profiles including power again ...' => [
+ "aInitialProfiles" => [ "Portal user"],
+ "aOperation" => [
+ '+Portal power user',
+ ],
+ "aExpectedProfilesAfterUpdate" => ["Portal user", "Portal power user"],
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider BulkUpdateUserExternalProfilesViaLinksProvider
+ */
+ public function testBulkUpdateUserExternalProfilesViaLinks($aInitialProfiles, $aOperation, $aExpectedProfilesAfterUpdate){
+ $oUser = new \UserExternal();
+ $sLogin = 'testUserLDAPUpdateWithPortalPowerUserProfile-'.uniqid();
+ $oUser->Set('login', $sLogin);
+
+ $sUserClass = get_class($oUser);
+ list ($sId, $aProfiles) = $this->CreateUserForProfileTesting($oUser, $aInitialProfiles);
+
+ \cmdbAbstractObject::SetEventDBLinksChangedBlocked(true);
+
+ $aURPUserProfileByUser = $this->GetURPUserProfileByUser($sId);
+ foreach ($aOperation as $sOperation){
+ $sOp = substr($sOperation,0, 1);
+ $sProfileName = substr($sOperation,1);
+
+ if ($sOp === "-"){
+ if (array_key_exists($sProfileName, $aURPUserProfileByUser)){
+ $oURPUserProfile = $aURPUserProfileByUser[$sProfileName];
+ $oURPUserProfile->DBDelete();
+ }
+ } else {
+ $oAdminUrpProfile = new URP_UserProfile();
+ $oProfile = $aProfiles[$sProfileName];
+ $oAdminUrpProfile->Set('profileid', $oProfile->GetKey());
+ $oAdminUrpProfile->Set('userid', $sId);
+ $oAdminUrpProfile->DBInsert();
+ }
+ }
+
+ \cmdbAbstractObject::SetEventDBLinksChangedBlocked(false);
+ \cmdbAbstractObject::FireEventDbLinksChangedForAllObjects();
+
+ $this->CheckProfilesAreOk($sUserClass, $sId, $aExpectedProfilesAfterUpdate);
+ }
+
+ private function GetURPUserProfileByUser($iUserId) : array {
+ $aRes = [];
+ $oSearch = \DBSearch::FromOQL("SELECT URP_UserProfile WHERE userid=$iUserId");
+ $oSet = new DBObjectSet($oSearch);
+ while (($oURPUserProfile = $oSet->Fetch()) != null){
+ $aRes[$oURPUserProfile->Get('profile')] = $oURPUserProfile;
+ }
+
+ return $aRes;
+ }
+
+ public function testUserProfilesEventListenerInit_nominal(){
+ $oUserProfilesEventListener = new UserProfilesEventListener();
+ $oUserProfilesEventListener->Init();
+
+ $this->assertTrue($oUserProfilesEventListener->IsRepairmentEnabled());
+ }
+
+ public function testUserProfilesEventListenerInit_badlyconfigured(){
+ \MetaModel::GetConfig()->Set(UserProfilesEventListener::USERPROFILE_REPAIR_ITOP_PARAM_NAME, "a string instead of an array");
+
+ $oUserProfilesEventListener = new UserProfilesEventListener();
+ $oUserProfilesEventListener->Init();
+
+ $this->assertFalse($oUserProfilesEventListener->IsRepairmentEnabled());
+ }
+
+ public function testUserProfilesEventListenerInit_specifically_disabled(){
+ \MetaModel::GetConfig()->Set(UserProfilesEventListener::USERPROFILE_REPAIR_ITOP_PARAM_NAME, []);
+ $oUserProfilesEventListener = new UserProfilesEventListener();
+ $oUserProfilesEventListener->Init();
+
+ $this->assertFalse($oUserProfilesEventListener->IsRepairmentEnabled());
+ }
+
+ public function CustomizedPortalsProvider(){
+ return [
+ 'console + customized portal' => [
+ 'aPortalDispatcherData' => [
+ 'customer-portal',
+ 'backoffice'
+ ]],
+ 'console + itop portal + customized portal' => [
+ 'aPortalDispatcherData' => [
+ 'itop-portal',
+ 'customer-portal',
+ 'backoffice'
+ ]
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider CustomizedPortalsProvider
+ */
+ public function testUserProfilesEventListenerInit_furtherportals_norepairmentconfigured($aPortalDispatcherData){
+ $oUserProfilesEventListener = new UserProfilesEventListener();
+ $oUserProfilesEventListener->Init($aPortalDispatcherData);
+
+ $this->assertFalse($oUserProfilesEventListener->IsRepairmentEnabled());
+ }
+
+ public function testUserProfilesEventListenerInit_furtherportals_repairmentconfigured(){
+ $aPortalDispatcherData = [
+ 'itop-portal',
+ 'customer-portal',
+ 'backoffice'
+ ];
+
+ \MetaModel::GetConfig()->Set(UserProfilesEventListener::USERPROFILE_REPAIR_ITOP_PARAM_NAME, ['Portal power user' => 'Portal user']);
+
+ $oUserProfilesEventListener = new UserProfilesEventListener();
+ $oUserProfilesEventListener->Init($aPortalDispatcherData);
+
+ $this->assertTrue($oUserProfilesEventListener->IsRepairmentEnabled());
+ }
+
+ public function testUserProfilesEventListenerInit_with_unknownprofile(){
+ $aPortalDispatcherData = [
+ 'itop-portal',
+ 'customer-portal',
+ 'backoffice'
+ ];
+
+ \MetaModel::GetConfig()->Set(UserProfilesEventListener::USERPROFILE_REPAIR_ITOP_PARAM_NAME, ['Portal power user' => 'Dummy Profile']);
+
+ $oUserProfilesEventListener = new UserProfilesEventListener();
+ $oUserProfilesEventListener->Init($aPortalDispatcherData);
+
+ $this->assertFalse($oUserProfilesEventListener->IsRepairmentEnabled());
+ }
+
+ public function testInit_ConfWithOneWarningProfile() {
+ \MetaModel::GetConfig()->Set(UserProfilesEventListener::USERPROFILE_REPAIR_ITOP_PARAM_NAME,
+ ['Change Supervisor' => 'Administrator', 'Portal power user' => null]
+ );
+ $oUserProfilesEventListener = new UserProfilesEventListener();
+ $oUserProfilesEventListener->Init();
+ $this->assertTrue($oUserProfilesEventListener->IsRepairmentEnabled());
+ }
+
+ public function testInit_ConfWithFurtherWarningProfiles() {
+ \MetaModel::GetConfig()->Set(UserProfilesEventListener::USERPROFILE_REPAIR_ITOP_PARAM_NAME,
+ ['Change Supervisor' => null, 'Portal power user' => null]
+ );
+ $oUserProfilesEventListener = new UserProfilesEventListener();
+ $oUserProfilesEventListener->Init();
+ $this->assertTrue($oUserProfilesEventListener->IsRepairmentEnabled());
+ }
+
+ public function testInit_ConfWithFurtherWarningProfilesAndOneRepairment() {
+ \MetaModel::GetConfig()->Set(UserProfilesEventListener::USERPROFILE_REPAIR_ITOP_PARAM_NAME,
+ ['Portal power user' => null, 'Change Supervisor' => null, 'Administrator' => "REST Services User"]
+ );
+ $oUserProfilesEventListener = new UserProfilesEventListener();
+ $oUserProfilesEventListener->Init();
+ $this->assertTrue($oUserProfilesEventListener->IsRepairmentEnabled());
+ }
+
+ public function testRepairProfiles_WithAnotherFallbackProfile()
+ {
+ $oUser = new \UserLocal();
+ $sLogin = 'testUserLocalCreationWithPortalPowerUserProfile-'.uniqid();
+ $oUser->Set('login', $sLogin);
+ $oUser->Set('password', 'ABCD1234@gabuzomeu');
+ $oUser->Set('language', 'EN US');
+
+ \MetaModel::GetConfig()->Set(UserProfilesEventListener::USERPROFILE_REPAIR_ITOP_PARAM_NAME,
+ ['Portal power user' => 'Change Supervisor']
+ );
+ $oUserProfilesEventListener = new UserProfilesEventListener();
+ $oUserProfilesEventListener->Init();
+ $this->assertTrue($oUserProfilesEventListener->IsRepairmentEnabled());
+
+ $this->CreateUserForProfileTesting($oUser, ['Portal power user'], false);
+ $oUserProfilesEventListener->RepairProfiles($oUser);
+
+ $oUserProfileList = $oUser->Get('profile_list');
+ $aProfilesAfterCreation=[];
+ while (($oProfile = $oUserProfileList->Fetch()) != null){
+ $aProfilesAfterCreation[] = $oProfile->Get('profile');
+ }
+
+ $this->assertContains('Change Supervisor', $aProfilesAfterCreation, var_export($aProfilesAfterCreation, true));
+ $this->assertContains('Portal power user', $aProfilesAfterCreation, var_export($aProfilesAfterCreation, true));
+ }
+
+ public function testRepairProfiles_MultiRepairmentConf()
+ {
+ \MetaModel::GetConfig()->Set(UserProfilesEventListener::USERPROFILE_REPAIR_ITOP_PARAM_NAME,
+ [
+ 'Administrator' => 'REST Services User',
+ 'Portal power user' => 'Change Supervisor'
+ ]
+ );
+ $oUserProfilesEventListener = new UserProfilesEventListener();
+ $oUserProfilesEventListener->Init();
+ $this->assertTrue($oUserProfilesEventListener->IsRepairmentEnabled());
+
+ $oUser = new \UserLocal();
+ $sLogin = 'testUserLocalCreationWithPortalPowerUserProfile-'.uniqid();
+ $oUser->Set('login', $sLogin);
+ $oUser->Set('password', 'ABCD1234@gabuzomeu');
+ $oUser->Set('language', 'EN US');
+ $this->CreateUserForProfileTesting($oUser, ['Portal power user'], false);
+ $oUserProfilesEventListener->RepairProfiles($oUser);
+
+ $oUserProfileList = $oUser->Get('profile_list');
+ $aProfilesAfterCreation=[];
+ while (($oProfile = $oUserProfileList->Fetch()) != null){
+ $aProfilesAfterCreation[] = $oProfile->Get('profile');
+ }
+
+ $this->assertContains('Change Supervisor', $aProfilesAfterCreation, var_export($aProfilesAfterCreation, true));
+ $this->assertContains('Portal power user', $aProfilesAfterCreation, var_export($aProfilesAfterCreation, true));
+
+ $oUser2 = new \UserLocal();
+ $sLogin = 'testUserLocalCreationWithPortalPowerUserProfile-'.uniqid();
+ $oUser2->Set('login', $sLogin);
+ $oUser2->Set('password', 'ABCD1234@gabuzomeu');
+ $oUser2->Set('language', 'EN US');
+
+ $this->CreateUserForProfileTesting($oUser2, ['Administrator'], false);
+ $oUserProfilesEventListener->RepairProfiles($oUser2);
+
+ $oUserProfileList = $oUser2->Get('profile_list');
+ $aProfilesAfterCreation=[];
+ while (($oProfile = $oUserProfileList->Fetch()) != null){
+ $aProfilesAfterCreation[] = $oProfile->Get('profile');
+ }
+
+ $this->assertContains('Administrator', $aProfilesAfterCreation, var_export($aProfilesAfterCreation, true));
+ $this->assertContains('REST Services User', $aProfilesAfterCreation, var_export($aProfilesAfterCreation, true));
+ }
+
+ public function testUserCreationWithWarningMessageConf()
+ {
+ $_SESSION = [];
+ $oAdminUser = new \UserLocal();
+ $sLogin = 'testUserCreationWithWarningMessageConf-Admin'.uniqid();
+ $oAdminUser->Set('login', $sLogin);
+ $oAdminUser->Set('password', 'ABCD1234@gabuzomeu');
+ $oAdminUser->Set('language', 'EN US');
+ $aAssociatedProfilesBeforeUserCreation = ['Administrator'];
+ $this->commonUserCreation($oAdminUser, $aAssociatedProfilesBeforeUserCreation, $aAssociatedProfilesBeforeUserCreation, false);
+ UserRights::Login($oAdminUser->Get('login'));
+
+
+
+ $aAssociatedProfilesBeforeUserCreation = [
+ 'Portal power user'
+ ];
+
+ $oUser = new \UserLocal();
+ $sLogin = 'testUserCreationWithWarningMessageConf-'.uniqid();
+ $oUser->Set('login', $sLogin);
+ $oUser->Set('password', 'ABCD1234@gabuzomeu');
+ $oUser->Set('language', 'EN US');
+
+ \MetaModel::GetConfig()->Set(UserProfilesEventListener::USERPROFILE_REPAIR_ITOP_PARAM_NAME,
+ ['Portal power user' => null ]
+ );
+
+ $this->SetNonPublicStaticProperty(EventService::class, "aEventListeners", []);
+ $oUserProfilesEventListener = new UserProfilesEventListener();
+ $oUserProfilesEventListener->RegisterEventsAndListeners();
+
+ $this->commonUserCreation($oUser, $aAssociatedProfilesBeforeUserCreation, $aAssociatedProfilesBeforeUserCreation, false);
+ $aObjMessages = Session::Get('obj_messages');
+ $this->assertNotEmpty($aObjMessages);
+ $sKey = sprintf("%s::%s", get_class($oUser), $oUser->GetKey());
+ $this->assertTrue(array_key_exists($sKey, $aObjMessages));
+
+ $aExpectedMessages = [
+ [
+ 'rank' => 1,
+ 'severity' => 'WARNING',
+ 'message' => \Dict::Format("Class:User/NonStandaloneProfileWarning", 'Portal power user')
+ ]
+ ];
+ $this->assertEquals($aExpectedMessages, array_values($aObjMessages[$sKey]), var_export($aObjMessages[$sKey], true));
+
+ $_SESSION = [];
+ }
+}