Compare commits

...

20 Commits

Author SHA1 Message Date
odain
cc56413ee7 5324-add debug logs 2023-09-05 15:03:59 +02:00
odain
b3820560f1 5324-make test work with iTop core profiles AND saas targets 2023-09-05 15:03:46 +02:00
odain
144ca490ed 5324-adapt test 2023-09-05 13:35:12 +02:00
odain
7443fdd525 5324-userfriendly messages 2023-09-05 13:11:44 +02:00
odain
ff351d6b4b 5324-when calling FireEvent throw first exception raised instead of last one 2023-09-05 13:10:59 +02:00
odain
6f6b385550 5324-fix reentrance in case of object creation 2023-09-05 13:10:13 +02:00
odain
f25e9045c9 5324-fix no profile name provided during ci errors 2023-09-04 10:41:12 +02:00
odain
03568f2fa5 5324-handle error message differently 2023-09-01 16:46:28 +02:00
odain
15900720c8 5324-handle forgotten usecases 2023-09-01 15:33:42 +02:00
odain
6d3f7f4976 5324-enhance CRUD to avoid collision/reentrance when using events on links 2023-09-01 15:33:11 +02:00
odain
6dc88c372b Merge branch 'support/3.1.0' into feature/5324-powerportaluser-repairprofiles 2023-09-01 15:15:16 +02:00
Molkobain
58f4d3b53c Remove "is_link" fixes as they have been done in Done in 12dbd0e 2023-06-29 10:05:41 +02:00
odain
427fc6f9f9 5324-Guillaume s feedback in PR 2023-06-28 21:31:11 +02:00
odain
07eadb3ea7 N°5324 -rename and move conf parameter to security.single-profile-completion + display warningmessage 2023-06-28 14:41:20 +02:00
odain
97f4818076 5324- Guillaume PR feedbacks 2023-06-26 15:01:57 +02:00
odain
ad46d47e21 N°5324 - repairing or warning profiles conf 2023-06-21 23:44:39 +02:00
odain
cd3f7d7ead N°5324 - disable repairment with backoffice and a customized portal 2023-06-21 21:42:40 +02:00
odain
c6b203fc4e N°5324 - make EVENT_DB_LINKS_CHANGED event work with URP_UserProfile by adding is-link property 2023-06-14 15:14:55 +02:00
odain
b8d04e40e4 N°5324 - repair profiles by default or if configured for customer with multiple profiles + move dedicated test in UserProfilesEventListenerTest.php 2023-06-14 15:13:29 +02:00
odain
d8c7888eac N°5324 - [ERGO] Avoid to have users with non-standalone power portal profile only 2023-06-13 15:39:34 +02:00
10 changed files with 1191 additions and 79 deletions

View File

@@ -1627,6 +1627,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)',

View File

@@ -57,7 +57,7 @@ require_once('mutex.class.inc.php');
/**
* A persistent object, as defined by the metamodel
* A persistent object, as defined by the metamodel
*
* @package iTopORM
* @api
@@ -299,9 +299,9 @@ abstract class DBObject implements iDisplay
/**
* Whether the object is already persisted in DB or not.
*
*
* @api
*
*
* @return bool
*/
public function IsNew()
@@ -311,9 +311,9 @@ abstract class DBObject implements iDisplay
/**
* Returns an Id for memory objects
*
*
* @internal
*
*
* @param string $sClass
*
* @return int
@@ -350,7 +350,7 @@ abstract class DBObject implements iDisplay
$sRet .= "<b title=\"$sRootClass\">$sClass</b>::$iPKey ($sFriendlyname)<br/>\n";
return $sRet;
}
/**
* Alias of DBObject::Reload()
*
@@ -373,7 +373,7 @@ abstract class DBObject implements iDisplay
*
* @internal
* @see m_bFullyLoaded
*
*
* @return bool
* @throws CoreException
*/
@@ -496,7 +496,7 @@ abstract class DBObject implements iDisplay
{
$aAttList = $aAttToLoad[$sClassAlias];
}
foreach($aAttList as $sAttCode=>$oAttDef)
{
// Skip links (could not be loaded by the mean of this query)
@@ -556,7 +556,7 @@ abstract class DBObject implements iDisplay
$bFullyLoaded = false;
}
}
// Load extended data
if ($aExtendedDataSpec != null)
{
@@ -580,7 +580,7 @@ abstract class DBObject implements iDisplay
*
* @internal
* @see Set()
*
*
* @param string $sAttCode
* @param mixed $value
*/
@@ -785,11 +785,11 @@ abstract class DBObject implements iDisplay
/**
* Get the label of an attribute.
*
*
* Shortcut to the field's AttributeDefinition->GetLabel()
*
* @api
*
*
* @param string $sAttCode
*
* @return string
@@ -862,7 +862,7 @@ abstract class DBObject implements iDisplay
*
* @internal
* @see Get
*
*
* @param string $sAttCode
*
* @return int|mixed|null
@@ -967,7 +967,7 @@ abstract class DBObject implements iDisplay
* Returns the default value of the $sAttCode.
*
* Returns the default value of the given attribute.
*
*
* @internal
*
* @param string $sAttCode
@@ -988,12 +988,12 @@ abstract class DBObject implements iDisplay
* @internal
*
* @return array|null
*/
*/
public function GetExtendedData()
{
return $this->m_aExtendedData;
}
/**
* Set the HighlightCode
*
@@ -1015,7 +1015,7 @@ abstract class DBObject implements iDisplay
{
$fCurrentRank = $aHighlightScale[$this->m_sHighlightCode]['rank'];
}
if (array_key_exists($sCode, $aHighlightScale))
{
$fRank = $aHighlightScale[$sCode]['rank'];
@@ -1025,13 +1025,13 @@ abstract class DBObject implements iDisplay
}
}
}
/**
* Get the current HighlightCode
*
*
* @internal
* @used-by DBObject::ComputeHighlightCode()
*
*
* @return string|null The Hightlight code (null if none set, meaning rank = 0)
*/
protected function GetHighlightCode()
@@ -1080,7 +1080,7 @@ abstract class DBObject implements iDisplay
* corresponding to the external key and getting the value from it
*
* UNUSED ?
*
*
* @internal
* @todo: check if this is dead code.
*
@@ -1149,7 +1149,7 @@ abstract class DBObject implements iDisplay
/**
* @api
*
*
* @param string $sAttCode
* @param bool $bLocalize
*
@@ -1195,11 +1195,11 @@ abstract class DBObject implements iDisplay
/**
* Get the value as it must be in the edit areas (forms)
*
*
* Makes a raw text representation of the value.
*
* @internal
*
*
* @param string $sAttCode
*
* @return int|mixed|string
@@ -1229,7 +1229,7 @@ abstract class DBObject implements iDisplay
else
{
$sEditValue = 0;
}
}
}
else
{
@@ -1245,14 +1245,14 @@ abstract class DBObject implements iDisplay
/**
* Get $sAttCode formatted as XML
*
*
* The returned value is a text that is suitable for insertion into an XML node.
* Depending on the type of attribute, the returned text is either:
* * A literal, with XML entities already escaped,
* * XML
*
* @api
*
*
* @param string $sAttCode
* @param bool $bLocalize
*
@@ -1290,10 +1290,10 @@ abstract class DBObject implements iDisplay
}
/**
*
*
* @see GetAsHTML()
* @see GetOriginal()
*
*
* @param string $sAttCode
* @param bool $bLocalize
*
@@ -1468,7 +1468,7 @@ abstract class DBObject implements iDisplay
/**
* @internal
*
*
* @param string $sClass
*
* @return mixed
@@ -1523,7 +1523,7 @@ abstract class DBObject implements iDisplay
* Get the id
*
* @api
*
*
* @return string|null
*/
public function GetKey()
@@ -1534,7 +1534,7 @@ abstract class DBObject implements iDisplay
/**
* Primary key Setter
* Usable only for not yet persisted DBObjects
*
*
* @internal
*
* @param int $iNewKey the desired identifier
@@ -1547,7 +1547,7 @@ abstract class DBObject implements iDisplay
{
throw new CoreException("An object id must be an integer value ($iNewKey)");
}
if ($this->m_bIsInDB && !empty($this->m_iKey) && ($this->m_iKey != $iNewKey))
{
throw new CoreException("Changing the key ({$this->m_iKey} to $iNewKey) on an object (class {".get_class($this).") wich already exists in the Database");
@@ -1557,7 +1557,7 @@ abstract class DBObject implements iDisplay
/**
* Get the icon representing this object
*
*
* @api
*
* @param boolean $bImgTag If true the result is a full IMG tag (or an empty string if no icon is defined)
@@ -1647,7 +1647,7 @@ abstract class DBObject implements iDisplay
*
* Returns the label as defined in the dictionary for the language of the current user
*
* @api
* @api
*
* @return string (empty for default name scheme)
*/
@@ -1714,7 +1714,7 @@ abstract class DBObject implements iDisplay
/**
* Helper to get the state
*
*
* @api
*
* @return mixed|string '' if no state attribute, object representing its value otherwise
@@ -1736,9 +1736,9 @@ abstract class DBObject implements iDisplay
/**
* Get the label (raw text) of the current state
* helper for MetaModel::GetStateLabel()
*
*
* @api
*
*
* @return mixed|string
*
* @throws ArchivedObjectException
@@ -1785,7 +1785,7 @@ abstract class DBObject implements iDisplay
* Define attributes read-only from the end-user perspective
*
* @return array|null List of attcodes
*/
*/
public static function GetReadOnlyAttributes()
{
return null;
@@ -1794,14 +1794,14 @@ abstract class DBObject implements iDisplay
/**
* Get predefined objects
*
*
* The predefined objects will be synchronized with the DB at each install/upgrade
* As soon as a class has predefined objects, then nobody can create nor delete objects
*
* @internal
*
* @return array An array of id => array of attcode => php value(so-called "real value": integer, string, ormDocument, DBObjectSet, etc.)
*/
*/
public static function GetPredefinedObjects()
{
return null;
@@ -1930,7 +1930,7 @@ abstract class DBObject implements iDisplay
* Note: Attributes (and flags) from the target state and the transition are combined.
*
* @internal
*
*
* @param string $sStimulus
* @param string $sOriginState Default is current state
*
@@ -2137,7 +2137,7 @@ abstract class DBObject implements iDisplay
/**
* @internal
*
*
* @throws \CoreException
* @throws \OQLException
*
@@ -2509,7 +2509,7 @@ abstract class DBObject implements iDisplay
*
* an array of displayable error is added in {@see DBObject::$m_aDeleteIssues}
*
* @internal
* @internal
*
* @param \DeletionPlan $oDeletionPlan
*
@@ -2626,7 +2626,7 @@ abstract class DBObject implements iDisplay
{
// The value is a scalar, the comparison must be 100% strict
if($this->m_aOrigValues[$sAtt] !== $proposedValue)
{
{
//echo "$sAtt:<pre>\n";
//var_dump($this->m_aOrigValues[$sAtt]);
//var_dump($proposedValue);
@@ -2748,7 +2748,7 @@ abstract class DBObject implements iDisplay
/**
* Used only by insert, Meant to be overloaded
*
*
* @overwritable-hook You can extend this method in order to provide your own logic.
*/
protected function OnObjectKeyReady()
@@ -2856,7 +2856,7 @@ abstract class DBObject implements iDisplay
// fields in first array, values in the second
$aFieldsToWrite = array();
$aValuesToWrite = array();
if (!empty($this->m_iKey) && ($this->m_iKey >= 0))
{
// Add it to the list of fields to write
@@ -2865,7 +2865,7 @@ abstract class DBObject implements iDisplay
}
$aHierarchicalKeys = array();
foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef) {
// Skip this attribute if not defined in this table
if ((!MetaModel::IsAttributeOrigin($sTableClass, $sAttCode) && !$oAttDef->CopyOnAllTables())
@@ -2875,7 +2875,7 @@ abstract class DBObject implements iDisplay
$aAttColumns = $oAttDef->GetSQLValues($this->m_aCurrValues[$sAttCode]);
foreach($aAttColumns as $sColumn => $sValue)
{
$aFieldsToWrite[] = "`$sColumn`";
$aFieldsToWrite[] = "`$sColumn`";
$aValuesToWrite[] = CMDBSource::Quote($sValue);
}
if ($oAttDef->IsHierarchicalKey())
@@ -2899,7 +2899,7 @@ abstract class DBObject implements iDisplay
self::$m_aBulkInsertCols[$sClass][$sTable] = implode(', ', $aFieldsToWrite);
}
self::$m_aBulkInsertItems[$sClass][$sTable][] = '('.implode (', ', $aValuesToWrite).')';
$iNewKey = 999999; // TODO - compute next id....
}
else
@@ -2984,7 +2984,7 @@ abstract class DBObject implements iDisplay
// fields in first array, values in the second
$aFieldsToWrite = array();
$aValuesToWrite = array();
if (!empty($this->m_iKey) && ($this->m_iKey >= 0))
{
// Add it to the list of fields to write
@@ -3019,7 +3019,7 @@ abstract class DBObject implements iDisplay
$aAttColumns = $oAttDef->GetSQLValues($value);
foreach($aAttColumns as $sColumn => $sValue)
{
$aFieldsToWrite[] = "`$sColumn`";
$aFieldsToWrite[] = "`$sColumn`";
$aValuesToWrite[] = CMDBSource::Quote($sValue);
}
if ($oAttDef->IsHierarchicalKey())
@@ -3157,6 +3157,10 @@ abstract class DBObject implements iDisplay
// First query built upon on the root class, because the ID must be created first
$this->m_iKey = $this->DBInsertSingleTable($sRootClass);
//since N°5324: issue with test and db links events
$this->SetReadOnly('No modification allowed during transaction');
MetaModel::StartReentranceProtection($this);
// Then do the leaf class, if different from the root class
if ($sClass != $sRootClass) {
$this->DBInsertSingleTable($sClass);
@@ -3206,6 +3210,7 @@ abstract class DBObject implements iDisplay
}
}
$this->SetReadWrite();
$this->m_bIsInDB = true;
$this->m_bDirty = false;
foreach ($this->m_aCurrValues as $sAttCode => $value) {
@@ -3216,7 +3221,7 @@ abstract class DBObject implements iDisplay
}
// Prevent DBUpdate at this point (reentrance protection)
MetaModel::StartReentranceProtection($this);
//MetaModel::StartReentranceProtection($this);
try {
$this->PostInsertActions();
@@ -3293,7 +3298,7 @@ abstract class DBObject implements iDisplay
$this->RecordObjCreation();
return $ret;
}
/**
* This function is automatically called after cloning an object with the "clone" PHP language construct
* The purpose of this method is to reset the appropriate attributes of the object in
@@ -3382,6 +3387,7 @@ abstract class DBObject implements iDisplay
}
}
$this->SetReadOnly('No modification allowed during transaction');
$iTransactionRetry = 1;
$bIsTransactionEnabled = MetaModel::GetConfig()->Get('db_core_transactions_enabled');
if ($bIsTransactionEnabled) {
@@ -3496,6 +3502,8 @@ abstract class DBObject implements iDisplay
// following lines are resetting changes (so after this {@see DBObject::ListChanges()} won't return changes anymore)
// new values are already in the object (call {@see DBObject::Get()} to get them)
// call {@see DBObject::ListPreviousValuesForUpdatedAttributes()} to get changed fields and previous values
$this->SetReadWrite();
$this->m_bDirty = false;
$this->m_aTouchedAtt = array();
$this->m_aModifiedAtt = array();
@@ -3909,7 +3917,7 @@ abstract class DBObject implements iDisplay
* First, checks if the object can be deleted regarding database integrity.
* If the answer is yes, it performs any required cleanup (delete other objects or reset external keys) in addition to the object
* deletion.
*
*
* @api
*
* @param \DeletionPlan $oDeletionPlan Do not use: aims at dealing with recursion
@@ -4332,7 +4340,7 @@ abstract class DBObject implements iDisplay
*
* @api
*
*/
*/
public function Reset($sAttCode)
{
$this->Set($sAttCode, $this->GetDefaultValue($sAttCode));
@@ -4344,7 +4352,7 @@ abstract class DBObject implements iDisplay
* Suitable for use as a lifecycle action
*
* @api
*/
*/
public function Copy($sDestAttCode, $sSourceAttCode)
{
$oTypeValueToCopy = MetaModel::GetAttributeDef(get_class($this), $sSourceAttCode);
@@ -4674,7 +4682,7 @@ abstract class DBObject implements iDisplay
{
throw new CoreException("Unknown attribute '$sExtKeyAttCode' for the class ".get_class($this));
}
$oKeyAttDef = MetaModel::GetAttributeDef(get_class($this), $sExtKeyAttCode);
if (!$oKeyAttDef instanceof AttributeExternalKey)
{
@@ -4692,14 +4700,14 @@ abstract class DBObject implements iDisplay
$ret = $oRemoteObj->GetForTemplate($sRemoteAttCode);
}
}
else
else
{
switch($sPlaceholderAttCode)
{
case 'id':
$ret = $this->GetKey();
break;
case 'name()':
$ret = $this->GetName();
break;
@@ -4886,7 +4894,7 @@ abstract class DBObject implements iDisplay
if ($oOwner)
{
$sLinkSetOwnerClass = get_class($oOwner);
$oMyChangeOp = MetaModel::NewObject($sChangeOpClass);
$oMyChangeOp->Set("objclass", $sLinkSetOwnerClass);
$oMyChangeOp->Set("objkey", $iLinkSetOwnerId);
@@ -4913,7 +4921,7 @@ abstract class DBObject implements iDisplay
{
/** @var \AttributeLinkedSet $oLinkSet */
if (($oLinkSet->GetTrackingLevel() & LINKSET_TRACKING_LIST) == 0) continue;
$iLinkSetOwnerId = $this->Get($sExtKeyAttCode);
$oMyChangeOp = $this->PrepareChangeOpLinkSet($iLinkSetOwnerId, $oLinkSet, 'CMDBChangeOpSetAttributeLinksAddRemove');
if ($oMyChangeOp)
@@ -4983,7 +4991,7 @@ abstract class DBObject implements iDisplay
// Keep track of link changes
//
if (($oLinkSet->GetTrackingLevel() & LINKSET_TRACKING_DETAILS) == 0) continue;
$iLinkSetOwnerId = $this->Get($sExtKeyAttCode);
$oMyChangeOp = $this->PrepareChangeOpLinkSet($iLinkSetOwnerId, $oLinkSet, 'CMDBChangeOpSetAttributeLinksTune');
if ($oMyChangeOp)
@@ -5132,7 +5140,7 @@ abstract class DBObject implements iDisplay
$this->FireEventCheckToDelete($oDeletionPlan);
$this->DoCheckToDelete($oDeletionPlan);
$oDeletionPlan->SetDeletionIssues($this, $this->m_aDeleteIssues, $this->m_bSecurityIssue);
$aDependentObjects = $this->GetReferencingObjects(true /* allow all data */);
// Getting and setting time limit are not symmetric:
@@ -5314,7 +5322,7 @@ abstract class DBObject implements iDisplay
$aSynchroClasses[] = $sTarget;
}
}
foreach($aSynchroClasses as $sClass)
{
if ($this instanceof $sClass)

View File

@@ -1241,7 +1241,7 @@ abstract class MetaModel
}
$sTable = self::DBGetTable($sClass);
// Could be completed later with all the classes that are using a given table
// Could be completed later with all the classes that are using a given table
if (!array_key_exists($sTable, $aTables)) {
$aTables[$sTable] = array();
}
@@ -3522,7 +3522,7 @@ abstract class MetaModel
}
// Set the "host class" as soon as possible, since HierarchicalKeys use it for their 'target class' as well
// and this needs to be know early (for Init_IsKnowClass 19 lines below)
// and this needs to be know early (for Init_IsKnowClass 19 lines below)
$oAtt->SetHostClass($sTargetClass);
// Some attributes could refer to a class
@@ -3564,7 +3564,7 @@ abstract class MetaModel
self::$m_aAttribDefs[$sTargetClass][$oAtt->GetCode()] = $oAtt;
self::$m_aAttribOrigins[$sTargetClass][$oAtt->GetCode()] = $sTargetClass;
// Note: it looks redundant to put targetclass there, but a mix occurs when inheritance is used
// Note: it looks redundant to put targetclass there, but a mix occurs when inheritance is used
}
/**
@@ -3764,7 +3764,7 @@ abstract class MetaModel
self::$m_aStimuli[$sTargetClass][$oStimulus->GetCode()] = $oStimulus;
// I wanted to simplify the syntax of the declaration of objects in the biz model
// Therefore, the reference to the host class is set there
// Therefore, the reference to the host class is set there
$oStimulus->SetHostClass($sTargetClass);
}
@@ -6470,7 +6470,7 @@ abstract class MetaModel
$aCache['m_aExtensionClassNames'] = self::$m_aExtensionClassNames;
$aCache['m_Category2Class'] = self::$m_Category2Class;
$aCache['m_aRootClasses'] = self::$m_aRootClasses; // array of "classname" => "rootclass"
$aCache['m_aParentClasses'] = self::$m_aParentClasses; // array of ("classname" => array of "parentclass")
$aCache['m_aParentClasses'] = self::$m_aParentClasses; // array of ("classname" => array of "parentclass")
$aCache['m_aChildClasses'] = self::$m_aChildClasses; // array of ("classname" => array of "childclass")
$aCache['m_aClassParams'] = self::$m_aClassParams; // array of ("classname" => array of class information)
$aCache['m_aAttribDefs'] = self::$m_aAttribDefs; // array of ("classname" => array of attributes)
@@ -7567,6 +7567,20 @@ abstract class MetaModel
return false;
}
/**
* @since 3.1.0 N°5324: to ease reentrance checks when using events on links (to avoid reentering if main link object ongoing operation)
*/
public static function GetReentranceObjectByChildClass(string $sParentClass, $sKey)
{
foreach (self::EnumChildClasses($sParentClass, ENUM_CHILD_CLASSES_ALL, false) as $sChildClass){
if (self::GetReentranceObject($sChildClass, $sKey)){
return true;
}
}
return false;
}
/**
* @param \DBObject $oObject
*

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.1">
<itop_design version="3.1">
<classes/>
<user_rights>
<groups>
@@ -544,5 +544,6 @@
<groups/>
</profile>
</profiles>
<dictionaries/>
</user_rights>
</itop_design>

View File

@@ -0,0 +1,13 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) Combodo SARL 2022
* @license http://opensource.org/licenses/AGPL-3.0
*/
Dict::Add('EN US', 'English', 'English', array(
'Class:User/NonStandaloneProfileWarning' => 'Profile %1$s cannot be standalone. You should add other profiles to user %2$s otherwise you may encounter access issue with this user.',
'Class:User/NonStandaloneProfileWarning-ReparationMessage' => 'Profile %1$s cannot be standalone. User %2$s has been completed with another profile: %3$s.',
));

View File

@@ -0,0 +1,13 @@
<?php
/**
* Localized data
*
* @copyright Copyright (C) Combodo SARL 2022
* @license http://opensource.org/licenses/AGPL-3.0
*/
Dict::Add('FR FR', 'French', 'Français', array(
'Class:User/NonStandaloneProfileWarning' => 'Le profil %1$s ne peut être seul. Sans le rajout d\'autres profiles, l\'utilisateur %2$s peut rencontrer des problèmes dans iTop.',
'Class:User/NonStandaloneProfileWarning-ReparationMessage' => 'Le profil %1$s ne peut être seul. Le user %2$s a été complété par le profil %3$s.',
));

View File

@@ -36,6 +36,7 @@ SetupWebPage::AddModule(
// Components
//
'datamodel' => array(
'src/UserProfilesEventListener.php'
),
'webservice' => array(
//'webservices.itop-profiles-itil.php',

View File

@@ -0,0 +1,394 @@
<?php
/*
* @copyright Copyright (C) 2010-2023 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\ItilProfiles;
use Combodo\iTop\Service\Events\EventData;
use Combodo\iTop\Service\Events\EventService;
use Combodo\iTop\Service\Events\iEventServiceSetup;
use Exception;
use IssueLog;
use LogChannels;
define('POWER_USER_PORTAL_PROFILE_NAME', 'Portal power user');
/**
* Class UserProfilesEventListener
*
* @package Combodo\iTop\Core\EventListener
* @since 3.1 N°5324 - Avoid to have users with non-standalone power portal profile only
*
*/
class UserProfilesEventListener implements iEventServiceSetup
{
const USERPROFILE_REPAIR_ITOP_PARAM_NAME = 'security.single_profile_completion';
private $bIsRepairmentEnabled = false;
//map: non standalone profile name => repairing profile id
private $aNonStandaloneProfilesMap = [];
/**
* @inheritDoc
*/
public function RegisterEventsAndListeners()
{
$this->Init();
if (false === $this->bIsRepairmentEnabled){
IssueLog::Debug('UserProfilesEventListener bIsRepairmentEnabled disabled', LogChannels::DM_CRUD);
return;
}
$aEventSource = [\User::class, \UserExternal::class, \UserInternal::class];
EventService::RegisterListener(
EVENT_DB_BEFORE_WRITE,
[$this, 'OnUserEdition'],
$aEventSource
);
EventService::RegisterListener(
EVENT_DB_BEFORE_WRITE,
[ $this, 'OnUserProfileEdition' ],
[ \URP_UserProfile::class ],
[],
null
);
EventService::RegisterListener(
EVENT_DB_CHECK_TO_DELETE,
[ $this, 'OnUserProfileLinkDeletion' ],
[ \URP_UserProfile::class ],
[],
null
);
}
public function IsRepairmentEnabled() : bool
{
return $this->bIsRepairmentEnabled;
}
public function OnUserEdition(EventData $oEventData): void {
/** @var \User $oObject */
$oUser = $oEventData->Get('object');
try {
$this->ValidateThenRepairOrWarn($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(),
]);
if ($e instanceof \CoreCannotSaveObjectException){
throw $e;
}
}
}
public function OnUserProfileEdition(EventData $oEventData): void {
$oURP_UserProfile = $oEventData->Get('object');
try {
$iUserId = $oURP_UserProfile->Get('userid');
$oUser = \MetaModel::GetReentranceObjectByChildClass(\User::class, $iUserId);
if (false !== $oUser){
IssueLog::Debug('OnUserProfileEdition user already being edited', LogChannels::DM_CRUD);
//user edition: handled by other event
return;
}
$oUser = \MetaModel::GetObject(\User::class, $iUserId);
$aChanges = $oURP_UserProfile->ListChanges();
if (array_key_exists('userid', $aChanges)) {
IssueLog::Debug('OnUserProfileEdition userid changed', LogChannels::DM_CRUD);
$iUserId = $oURP_UserProfile->GetOriginal('userid');
$oPreviousUser = \MetaModel::GetObject(\User::class, $iUserId);
$oProfileLinkSet = $oPreviousUser->Get('profile_list');
$oProfileLinkSet->Rewind();
$iCount = 0;
$sSingleProfileName = null;
while ($oCurrentURP_UserProfile = $oProfileLinkSet->Fetch()) {
$sNewUserId = $oCurrentURP_UserProfile->Get('userid');
$sOriginalUserId = $oCurrentURP_UserProfile->GetOriginal('userid');
if ($sNewUserId !== $sOriginalUserId) {
$sRemovedProfileId = $oCurrentURP_UserProfile->GetOriginal('profileid');
IssueLog::Debug('OnUserProfileEdition profile moved does not count', LogChannels::DM_CRUD, [
'URP_UserProfile' => $oURP_UserProfile->GetKey(),
'sRemovedProfileId' => $sRemovedProfileId,
'sNewUserId' => $sNewUserId,
'sOriginalUserId' => $sOriginalUserId,
]);
continue;
}
$iCount++;
if ($iCount > 1){
IssueLog::Debug('OnUserProfileEdition more than one user', LogChannels::DM_CRUD);
//more than one profile: no repairment needed
return;
}
$sSingleProfileName = $oCurrentURP_UserProfile->Get('profile');
}
$this->RepairProfileChangesOrWarn($oPreviousUser, $sSingleProfileName, $oURP_UserProfile, $sRemovedProfileId);
} else if (array_key_exists('profileid', $aChanges)){
IssueLog::Debug('OnUserProfileEdition profileid changed', LogChannels::DM_CRUD);
$oCurrentUserProfileSet = $oUser->Get('profile_list');
if ($oCurrentUserProfileSet->Count() === 1){
$oProfile = $oCurrentUserProfileSet->Fetch();
$this->RepairProfileChangesOrWarn($oUser, $oProfile->Get('profile'), $oURP_UserProfile, $oProfile->GetOriginal("profileid"));
}
}
} catch (Exception $e) {
IssueLog::Error('OnUserProfileEdition Exception', LogChannels::DM_CRUD, [
'user_id' => $iUserId,
'lnk_id' => $oURP_UserProfile->GetKey(),
'exception_message' => $e->getMessage(),
'exception_stacktrace' => $e->getTraceAsString(),
]);
if ($e instanceof \CoreCannotSaveObjectException){
throw $e;
}
}
}
public function OnUserProfileLinkDeletion(EventData $oEventData): void {
$oURP_UserProfile = $oEventData->Get('object');
try {
$iUserId = $oURP_UserProfile->Get('userid');
$oUser = \MetaModel::GetReentranceObjectByChildClass(\User::class, $iUserId);
if (false !== $oUser){
IssueLog::Debug('OnUserProfileLinkDeletion user being deleted already', LogChannels::DM_CRUD);
//user edition: handled by other event
return;
}
$oUser = \MetaModel::GetObject(\User::class, $iUserId);
/** @var \DeletionPlan $oDeletionPlan */
$oDeletionPlan = $oEventData->Get('deletion_plan');
$aDeletedURP_UserProfiles = [];
if (! is_null($oDeletionPlan)){
$aListDeletes = $oDeletionPlan->ListDeletes();
if (array_key_exists(\URP_UserProfile::class, $aListDeletes)) {
foreach ($aListDeletes[\URP_UserProfile::class] as $iId => $aDeletes) {
$aDeletedURP_UserProfiles []= $iId;
}
}
}
$oProfileLinkSet = $oUser->Get('profile_list');
$oProfileLinkSet->Rewind();
$sSingleProfileName = null;
$iCount = 0;
while ($oCurrentURP_UserProfile = $oProfileLinkSet->Fetch()) {
if (in_array($oCurrentURP_UserProfile->GetKey(), $aDeletedURP_UserProfiles)) {
continue;
}
$iCount++;
if ($iCount > 1){
IssueLog::Debug('OnUserProfileLinkDeletion more than one profile', LogChannels::DM_CRUD);
//more than one profile: no repairment needed
return;
}
$sSingleProfileName = $oCurrentURP_UserProfile->Get('profile');
}
$this->RepairProfileChangesOrWarn($oUser, $sSingleProfileName, $oURP_UserProfile, $oURP_UserProfile->Get('profileid'), true);
} catch (Exception $e) {
IssueLog::Error('OnUserProfileLinkDeletion Exception', LogChannels::DM_CRUD, [
'user_id' => $iUserId,
'profile_id' => $oURP_UserProfile->Get('profileid'),
'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){
IssueLog::Debug('Init repairment disabled as there are more than 2 portals (extended customer should decide on their own)', LogChannels::DM_CRUD);
$this->bIsRepairmentEnabled = false;
return;
}
$aPortalNames = array_keys($aPortalDispatcherData);
sort($aPortalNames);
if ($aPortalNames !== ['backoffice', 'itop-portal']){
IssueLog::Debug('Init repairment disabled there is a custom portal', LogChannels::DM_CRUD, [$aPortalNames]);
$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),LogChannels::DM_CRUD, [self::USERPROFILE_REPAIR_ITOP_PARAM_NAME => $aNonStandaloneProfiles]);
$this->bIsRepairmentEnabled = false;
return;
}
if (empty($aNonStandaloneProfiles)){
//Feature specifically disabled in itop configuration
IssueLog::Debug('Init repairment disabled by conf on purpose', LogChannels::DM_CRUD);
$this->bIsRepairmentEnabled = false;
return;
}
$this->FetchRepairingProfileIds($aNonStandaloneProfiles);
}
public function FetchRepairingProfileIds(array $aNonStandaloneProfiles) : void {
$aProfiles = [];
try {
$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);
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] = [ 'name' => $sRepairProfileName, 'id' => $aProfiles[$sRepairProfileName]];
}
$this->bIsRepairmentEnabled = true;
} catch (\Exception $e) {
IssueLog::Error('Exception when searching user portal profile', LogChannels::DM_CRUD, [
'exception_message' => $e->getMessage(),
'exception_stacktrace' => $e->getTraceAsString(),
'aProfiles' => $aProfiles,
'aNonStandaloneProfiles' => $aNonStandaloneProfiles,
]);
$this->bIsRepairmentEnabled = false;
}
}
public function ValidateThenRepairOrWarn(\User $oUser) : void
{
$oCurrentUserProfileSet = $oUser->Get('profile_list');
if ($oCurrentUserProfileSet->Count() === 1){
IssueLog::Debug('ValidateThenRepairOrWarn one profile found', LogChannels::DM_CRUD);
$oProfile = $oCurrentUserProfileSet->Fetch();
$this->RepairUserChangesOrWarn($oUser, $oProfile->Get('profile'));
}
}
public function RepairUserChangesOrWarn(\User $oUser, string $sSingleProfileName) : void {
IssueLog::Debug('RepairUserChangesOrWarn', LogChannels::DM_CRUD,
[
'aNonStandaloneProfilesMap' => $this->aNonStandaloneProfilesMap,
'sSingleProfileName' => $sSingleProfileName
]
);
if (array_key_exists($sSingleProfileName, $this->aNonStandaloneProfilesMap)) {
$aRepairingProfileInfo = $this->aNonStandaloneProfilesMap[$sSingleProfileName];
if (is_null($aRepairingProfileInfo)){
$sMessage = \Dict::Format("Class:User/NonStandaloneProfileWarning", $sSingleProfileName, $oUser->Get('friendlyname'));
throw new \CoreCannotSaveObjectException(array('issues' => [$sMessage], 'class' => get_class($oUser), 'id' => $oUser->GetKey()));
} else {
//Completing profiles profiles by adding repairing one : by default portal user to a power portal user
$oUserProfile = new \URP_UserProfile();
$oUserProfile->Set('profileid', $aRepairingProfileInfo['id']);
$oCurrentUserProfileSet = $oUser->Get('profile_list');
$oCurrentUserProfileSet->AddItem($oUserProfile);
$oUser->Set('profile_list', $oCurrentUserProfileSet);
$sMessage = \Dict::Format("Class:User/NonStandaloneProfileWarning-ReparationMessage", $sSingleProfileName, $oUser->Get('friendlyname'), $aRepairingProfileInfo['name']);
$oUser::SetSessionMessage(get_class($oUser), $oUser->GetKey(), 1, $sMessage, 'WARNING', 1);
}
}
}
public function RepairProfileChangesOrWarn(\User $oUser, ?string $sSingleProfileName, \URP_UserProfile $oURP_UserProfile, string $sRemovedProfileId, $bIsRemoval=false) : void {
IssueLog::Debug('RepairUserChangesOrWarn', LogChannels::DM_CRUD,
[
'aNonStandaloneProfilesMap' => $this->aNonStandaloneProfilesMap,
'sSingleProfileName' => $sSingleProfileName
]
);
if (is_null($sSingleProfileName)){
return;
}
if (array_key_exists($sSingleProfileName, $this->aNonStandaloneProfilesMap)) {
$aRepairingProfileInfo = $this->aNonStandaloneProfilesMap[$sSingleProfileName];
if (is_null($aRepairingProfileInfo)
|| ($aRepairingProfileInfo['id'] === $sRemovedProfileId) //cannot repair by readding same remove profile as it will raise uniqueness rule
){
$sMessage = \Dict::Format("Class:User/NonStandaloneProfileWarning", $sSingleProfileName, $oUser->Get('friendlyname'));
if ($bIsRemoval){
$oURP_UserProfile->AddDeleteIssue($sMessage);
} else {
throw new \CoreCannotSaveObjectException(array('issues' => [$sMessage], 'class' => get_class($oURP_UserProfile), 'id' => $oURP_UserProfile->GetKey()));
}
} else {
//Completing profiles profiles by adding repairing one : by default portal user to a power portal user
$oUserProfile = new \URP_UserProfile();
$oUserProfile->Set('profileid', $aRepairingProfileInfo['id']);
$oCurrentUserProfileSet = $oUser->Get('profile_list');
$oCurrentUserProfileSet->AddItem($oUserProfile);
$oUser->Set('profile_list', $oCurrentUserProfileSet);
$oUser->DBWrite();
$sMessage = \Dict::Format("Class:User/NonStandaloneProfileWarning-ReparationMessage", $sSingleProfileName, $oUser->Get('friendlyname'), $aRepairingProfileInfo['name']);
$oURP_UserProfile::SetSessionMessage(get_class($oURP_UserProfile), $oURP_UserProfile->GetKey(), 1, $sMessage, 'WARNING', 1);
}
}
}
}

View File

@@ -134,8 +134,8 @@ final class EventService
return;
}
$oLastException = null;
$sLastExceptionMessage = null;
$oFirstException = null;
$sFirstExceptionMessage = null;
$bEventFired = false;
foreach (self::GetListeners($sEvent, $eventSource) as $aEventCallback) {
if (!self::MatchContext($aEventCallback['context'])) {
@@ -153,9 +153,12 @@ final class EventService
throw $e;
}
catch (Exception $e) {
$sLastExceptionMessage = "Event '$sLogEventName' for '$sName' id {$aEventCallback['id']} failed with non-blocking error: ".$e->getMessage();
EventServiceLog::Error($sLastExceptionMessage);
$oLastException = $e;
$sExceptionMessage = "Event '$sLogEventName' for '$sName' id {$aEventCallback['id']} failed with non-blocking error: ".$e->getMessage();
EventServiceLog::Error($sExceptionMessage);
if (is_null($oFirstException)){
$oFirstException = $e;
$sFirstExceptionMessage = $sExceptionMessage;
}
}
}
if ($bEventFired) {
@@ -163,9 +166,9 @@ final class EventService
}
$oKPI->ComputeStats('FireEvent', $sEvent);
if (!is_null($oLastException)) {
EventServiceLog::Error("Throwing the last exception caught: $sLastExceptionMessage");
throw $oLastException;
if (!is_null($oFirstException)) {
EventServiceLog::Error("Throwing the last exception caught: $sFirstExceptionMessage");
throw $oFirstException;
}
}

View File

@@ -0,0 +1,657 @@
<?php
// Copyright (c) 2010-2023 Combodo SARL
//
// This file is part of iTop.
//
// iTop is free software; you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// iTop is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
//
/**
* 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',
],
'bCheckSessionMessage' => true
],
'Portal power user + Configuration Manager => profiles untouched' => [
'aAssociatedProfilesBeforeUserCreation' => [
'Portal power user',
'Configuration Manager',
],
'aExpectedAssociatedProfilesAfterUserCreation'=> [
'Portal power user',
'Configuration Manager',
]
],
];
}
/**
* @dataProvider PortaPowerUserProvider
*/
public function testUserLocalCreation($aAssociatedProfilesBeforeUserCreation,
$aExpectedAssociatedProfilesAfterUserCreation, $bCheckSessionMessage=false)
{
/*if ($bCheckSessionMessage){
$sLogin = "Admin-" . uniqid();
$oConnectedUser = $this->CreateContactlessUser($sLogin, 1, "Iuytrez9876543ç_è-(");
$_SESSION = [];
\UserRights::Login($oConnectedUser->Get('login'));
}*/
$oUser = new \UserLocal();
$sLogin = 'testUserLocalCreationWithPortalPowerUserProfile-'.uniqid();
$oUser->Set('login', $sLogin);
$oUser->Set('password', 'ABCD1234@gabuzomeu');
$oUser->Set('language', 'EN US');
$this->commonUserCreationTest($oUser, $aAssociatedProfilesBeforeUserCreation, $aExpectedAssociatedProfilesAfterUserCreation);
/*if ($bCheckSessionMessage){
$aObjMessages = Session::Get('obj_messages');
$this->assertNotEmpty($aObjMessages);
$sKey = sprintf("%s::%s", get_class($oUser), $oUser->GetKey());
$this->assertTrue(array_key_exists($sKey, $aObjMessages));
$sMsg = <<<TXT
User profile Portal power user cannot be standalone. User has been completed with profile Portal power user.
TXT;
$aExpectedMessages = [
[
'rank' => 1,
'severity' => 'WARNING',
'message' => $sMsg
]
];
$this->assertEquals($aExpectedMessages, array_values($aObjMessages[$sKey]), var_export($aObjMessages[$sKey], true));
}*/
}
/**
* @dataProvider PortaPowerUserProvider
*/
public function testUserLocalUpdate($aAssociatedProfilesBeforeUserCreation,
$aExpectedAssociatedProfilesAfterUserCreation, $bCheckSessionMessage=false)
{
$oUser = new \UserLocal();
$sLogin = 'testUserLocalUpdateWithPortalPowerUserProfile-'.uniqid();
$oUser->Set('login', $sLogin);
$oUser->Set('password', 'ABCD1234@gabuzomeu');
$oUser->Set('language', 'EN US');
$this->commonUserUpdateTest($oUser, $aAssociatedProfilesBeforeUserCreation, $aExpectedAssociatedProfilesAfterUserCreation);
}
/**
* @dataProvider PortaPowerUserProvider
*/
public function testUserLDAPCreation($aAssociatedProfilesBeforeUserCreation,
$aExpectedAssociatedProfilesAfterUserCreation, $bCheckSessionMessage=false)
{
$oUser = new \UserLDAP();
$sLogin = 'testUserLDAPCreationWithPortalPowerUserProfile-'.uniqid();
$oUser->Set('login', $sLogin);
$this->commonUserCreationTest($oUser, $aAssociatedProfilesBeforeUserCreation, $aExpectedAssociatedProfilesAfterUserCreation);
}
/**
* @dataProvider PortaPowerUserProvider
*/
public function testUserLDAPUpdate($aAssociatedProfilesBeforeUserCreation,
$aExpectedAssociatedProfilesAfterUserCreation)
{
$oUser = new \UserLDAP();
$sLogin = 'testUserLDAPUpdateWithPortalPowerUserProfile-'.uniqid();
$oUser->Set('login', $sLogin);
$this->commonUserUpdateTest($oUser, $aAssociatedProfilesBeforeUserCreation, $aExpectedAssociatedProfilesAfterUserCreation);
}
/**
* @dataProvider PortaPowerUserProvider
*/
public function testUserExternalCreation($aAssociatedProfilesBeforeUserCreation,
$aExpectedAssociatedProfilesAfterUserCreation, $bCheckSessionMessage=false)
{
$oUser = new \UserExternal();
$sLogin = 'testUserLDAPCreationWithPortalPowerUserProfile-'.uniqid();
$oUser->Set('login', $sLogin);
$this->commonUserCreationTest($oUser, $aAssociatedProfilesBeforeUserCreation, $aExpectedAssociatedProfilesAfterUserCreation);
}
/**
* @dataProvider PortaPowerUserProvider
*/
public function testUserExternalUpdate($aAssociatedProfilesBeforeUserCreation,
$aExpectedAssociatedProfilesAfterUserCreation, $bCheckSessionMessage=false)
{
$oUser = new \UserExternal();
$sLogin = 'testUserLDAPUpdateWithPortalPowerUserProfile-'.uniqid();
$oUser->Set('login', $sLogin);
$this->commonUserUpdateTest($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 commonUserCreationTest($oUserToCreate, $aAssociatedProfilesBeforeUserCreation,
$aExpectedAssociatedProfilesAfterUserCreation, $bTestUserItopAccess=true)
{
$sUserClass = get_class($oUserToCreate);
list ($sId, $aProfiles) = $this->CreateUserForProfileTesting($oUserToCreate, $aAssociatedProfilesBeforeUserCreation);
$this->CheckProfilesAreOkAndThenConnectToITop($sUserClass, $sId, $aExpectedAssociatedProfilesAfterUserCreation, $bTestUserItopAccess);
}
public function CheckProfilesAreOkAndThenConnectToITop($sUserClass, $sId, $aExpectedAssociatedProfilesAfterUserCreation, $bTestItopConnection=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 (! $bTestItopConnection){
return;
}
$_SESSION = [];
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 commonUserUpdateTest($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->CheckProfilesAreOkAndThenConnectToITop($sUserClass, $sId, $aExpectedAssociatedProfilesAfterUserCreation);
}
/**
* @dataProvider ProfilesLinksProvider
*/
public function testProfilesLinksDBDelete(string $sProfileNameToRemove, $bRaiseException=false){
$aInitialProfiles = [ $sProfileNameToRemove, "Portal power user"];
$oUser = new \UserExternal();
$sLogin = 'testUserLDAPUpdateWithPortalPowerUserProfile-'.uniqid();
$oUser->Set('login', $sLogin);
$sUserClass = get_class($oUser);
list ($sId, $aProfiles) = $this->CreateUserForProfileTesting($oUser, $aInitialProfiles);
if ($bRaiseException){
$this->expectException(\DeleteException::class);
$sMessage = <<<TXT
Profile Portal power user cannot be standalone. You should add other profiles to user $sLogin otherwise you may encounter access issue with this user.
TXT;
$this->expectExceptionMessage($sMessage);
}
$aURPUserProfileByUser = $this->GetURPUserProfileByUser($sId);
if (array_key_exists($sProfileNameToRemove, $aURPUserProfileByUser)){
$oURPUserProfile = $aURPUserProfileByUser[$sProfileNameToRemove];
$oURPUserProfile->DBDelete();
}
if (! $bRaiseException) {
$aExpectedProfilesAfterUpdate = ["Portal power user", "Portal user"];
$this->CheckProfilesAreOkAndThenConnectToITop($sUserClass, $sId, $aExpectedProfilesAfterUpdate);
}
}
/**
* @dataProvider ProfilesLinksProvider
*/
public function testProfilesLinksEdit_ChangeProfileId(string $sInitialProfile, $bRaiseException=false){
$oUser = new \UserExternal();
$sLogin = 'testUserLDAPUpdateWithPortalPowerUserProfile-'.uniqid();
$oUser->Set('login', $sLogin);
$sUserClass = get_class($oUser);
list ($sId, $aProfiles) = $this->CreateUserForProfileTesting($oUser, [$sInitialProfile]);
$oURP_Profile = \MetaModel::GetObjectByColumn("URP_Profiles", "name", "Portal power user");
$aURPUserProfileByUser = $this->GetURPUserProfileByUser($sId);
if ($bRaiseException){
$this->expectException(\CoreCannotSaveObjectException::class);
}
if (array_key_exists($sInitialProfile, $aURPUserProfileByUser)){
$oURPUserProfile = $aURPUserProfileByUser[$sInitialProfile];
$oURPUserProfile->Set('profileid', $oURP_Profile->GetKey());
$oURPUserProfile->DBWrite();
}
if (!$bRaiseException) {
$aExpectedProfilesAfterUpdate = ["Portal power user", "Portal user"];
$this->CheckProfilesAreOkAndThenConnectToITop($sUserClass, $sId, $aExpectedProfilesAfterUpdate);
//check warning
/*$aObjMessages = Session::Get('obj_messages');
$this->assertNotEmpty($aObjMessages);
$sKey = sprintf("%s::%s", get_class($oURPUserProfile), $oURPUserProfile->GetKey());
$this->assertTrue(array_key_exists($sKey, $aObjMessages));
$sMsg = <<<TXT
User profile Portal power user cannot be standalone. User has been completed with profile Portal power user.
TXT;
$aExpectedMessages = [
[
'rank' => 1,
'severity' => 'WARNING',
'message' => $sMsg
]
];
$this->assertEquals($aExpectedMessages, array_values($aObjMessages[$sKey]), var_export($aObjMessages[$sKey], true));*/
}
}
public function ProfilesLinksProvider() {
return [
"Administrator" => [ "sProfileNameToMove" => "Administrator" ],
"Portal user" => [ "sProfileNameToMove" => "Portal user", "bRaiseException" => true ],
];
}
/**
* @dataProvider ProfilesLinksProvider
*/
public function testProfilesLinksEdit_ChangeUserId($sProfileNameToMove, $bRaiseException=false){
$aInitialProfiles = [ $sProfileNameToMove, "Portal power user"];
$oUser = new \UserExternal();
$sLogin1 = 'testUserLDAPUpdateWithPortalPowerUserProfile-'.uniqid();
$oUser->Set('login', $sLogin1);
$sUserClass = get_class($oUser);
list ($sId, $aProfiles) = $this->CreateUserForProfileTesting($oUser, $aInitialProfiles);
$oUser = new \UserExternal();
$sLogin2 = 'testUserLDAPUpdateWithPortalPowerUserProfile-'.uniqid();
$oUser->Set('login', $sLogin2);
list ($sAnotherUserId, $aProfiles) = $this->CreateUserForProfileTesting($oUser, ["Configuration Manager"]);
if ($bRaiseException){
$this->expectException(\CoreCannotSaveObjectException::class);
$sMessage = <<<TXT
Profile Portal power user cannot be standalone. You should add other profiles to user $sLogin1 otherwise you may encounter access issue with this user.
TXT;
$this->expectExceptionMessage($sMessage);
}
$aURPUserProfileByUser = $this->GetURPUserProfileByUser($sId);
if (array_key_exists($sProfileNameToMove, $aURPUserProfileByUser)){
$oURPUserProfile = $aURPUserProfileByUser[$sProfileNameToMove];
$oURPUserProfile->Set('userid', $sAnotherUserId);
$oURPUserProfile->DBWrite();
}
if (! $bRaiseException) {
$aExpectedProfilesAfterUpdate = [$sProfileNameToMove, "Configuration Manager"];
$this->CheckProfilesAreOkAndThenConnectToITop($sUserClass, $sAnotherUserId, $aExpectedProfilesAfterUpdate);
$aExpectedProfilesAfterUpdate = ["Portal power user", "Portal user"];
$this->CheckProfilesAreOkAndThenConnectToITop($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,
['Configuration Manager' => '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,
['Configuration Manager' => 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, 'Configuration Manager' => null, 'Administrator' => "Configuration Manager"]
);
$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' => 'Configuration Manager']
);
$oUserProfilesEventListener = new UserProfilesEventListener();
$oUserProfilesEventListener->Init();
$this->assertTrue($oUserProfilesEventListener->IsRepairmentEnabled());
$this->CreateUserForProfileTesting($oUser, ['Portal power user'], false);
$oUserProfilesEventListener->ValidateThenRepairOrWarn($oUser);
$oUserProfileList = $oUser->Get('profile_list');
$aProfilesAfterCreation=[];
while (($oProfile = $oUserProfileList->Fetch()) != null){
$aProfilesAfterCreation[] = $oProfile->Get('profile');
}
$this->assertContains('Configuration Manager', $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' => 'Portal user',
'Portal power user' => 'Configuration Manager'
]
);
$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->ValidateThenRepairOrWarn($oUser);
$oUserProfileList = $oUser->Get('profile_list');
$aProfilesAfterCreation=[];
while (($oProfile = $oUserProfileList->Fetch()) != null){
$aProfilesAfterCreation[] = $oProfile->Get('profile');
}
$this->assertContains('Configuration Manager', $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->ValidateThenRepairOrWarn($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('Portal 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->commonUserCreationTest($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->expectException(\CoreCannotSaveObjectException::class);
$sMessage = <<<TXT
Profile Portal power user cannot be standalone. You should add other profiles to user $sLogin otherwise you may encounter access issue with this user.
TXT;
$this->expectExceptionMessage($sMessage);
$this->commonUserCreationTest($oUser, $aAssociatedProfilesBeforeUserCreation, $aAssociatedProfilesBeforeUserCreation, false);
$_SESSION = [];
}
}