From 58f4d3b53ca6e064e466d95b5615e44980ff35cb Mon Sep 17 00:00:00 2001 From: Molkobain Date: Thu, 29 Jun 2023 10:05:41 +0200 Subject: [PATCH 1/3] Remove "is_link" fixes as they have been done in Done in 12dbd0e --- addons/userrights/userrightsprofile.class.inc.php | 1 - addons/userrights/userrightsprofile.db.class.inc.php | 1 - addons/userrights/userrightsprojection.class.inc.php | 1 - core/datamodel.core.xml | 1 - 4 files changed, 4 deletions(-) diff --git a/addons/userrights/userrightsprofile.class.inc.php b/addons/userrights/userrightsprofile.class.inc.php index e1406e1a4..610cb9183 100644 --- a/addons/userrights/userrightsprofile.class.inc.php +++ b/addons/userrights/userrightsprofile.class.inc.php @@ -220,7 +220,6 @@ 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 1ba202c0c..8b02989f4 100644 --- a/addons/userrights/userrightsprofile.db.class.inc.php +++ b/addons/userrights/userrightsprofile.db.class.inc.php @@ -326,7 +326,6 @@ 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 5b70bd6ac..7a9642218 100644 --- a/addons/userrights/userrightsprojection.class.inc.php +++ b/addons/userrights/userrightsprojection.class.inc.php @@ -269,7 +269,6 @@ 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/datamodel.core.xml b/core/datamodel.core.xml index 26a606a8c..0a98b4da6 100644 --- a/core/datamodel.core.xml +++ b/core/datamodel.core.xml @@ -57,7 +57,6 @@ cmdbAbstractObject - 1 addon/userrights,grant_by_profile From 6d3f7f49761cf5c8cdf288b8fceb9b46dde2f0a3 Mon Sep 17 00:00:00 2001 From: odain Date: Fri, 1 Sep 2023 15:33:11 +0200 Subject: [PATCH 2/3] 5324-enhance CRUD to avoid collision/reentrance when using events on links --- core/dbobject.class.php | 139 ++++++++++++++++++++------------------- core/metamodel.class.php | 24 +++++-- 2 files changed, 91 insertions(+), 72 deletions(-) diff --git a/core/dbobject.class.php b/core/dbobject.class.php index 5b1e3dd18..854390796 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -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 .= "$sClass::$iPKey ($sFriendlyname)
\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:
\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) {
@@ -3215,9 +3220,6 @@ abstract class DBObject implements iDisplay
 				$this->m_aOrigValues[$sAttCode] = $value;
 			}
 
-			// Prevent DBUpdate at this point (reentrance protection)
-			MetaModel::StartReentranceProtection($this);
-
 			try {
 				$this->PostInsertActions();
 			}
@@ -3293,7 +3295,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 +3384,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 +3499,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 +3914,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 +4337,7 @@ abstract class DBObject implements iDisplay
      *
      * @api
      *
-	 */	 	
+	 */
 	public function Reset($sAttCode)
 	{
 		$this->Set($sAttCode, $this->GetDefaultValue($sAttCode));
@@ -4344,7 +4349,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 +4679,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 +4697,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 +4891,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 +4918,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 +4988,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 +5137,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 +5319,7 @@ abstract class DBObject implements iDisplay
 				$aSynchroClasses[] = $sTarget;
 			}
 		}
-		
+
 		foreach($aSynchroClasses as $sClass)
 		{
 			if ($this instanceof $sClass)
diff --git a/core/metamodel.class.php b/core/metamodel.class.php
index 7e3b8cdc8..eef5783aa 100644
--- a/core/metamodel.class.php
+++ b/core/metamodel.class.php
@@ -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
 	 *

From 15900720c8e19d8b5b93d819474c3b027adc00bb Mon Sep 17 00:00:00 2001
From: odain 
Date: Fri, 1 Sep 2023 15:33:42 +0200
Subject: [PATCH 3/3] 5324-handle forgotten usecases

---
 .../src/UserProfilesEventListener.php         | 291 +++++++++++++-----
 .../UserProfilesEventListenerTest.php         | 210 +++++++------
 2 files changed, 324 insertions(+), 177 deletions(-)

diff --git a/datamodels/2.x/itop-profiles-itil/src/UserProfilesEventListener.php b/datamodels/2.x/itop-profiles-itil/src/UserProfilesEventListener.php
index 1da291fdc..c4ab5088b 100644
--- a/datamodels/2.x/itop-profiles-itil/src/UserProfilesEventListener.php
+++ b/datamodels/2.x/itop-profiles-itil/src/UserProfilesEventListener.php
@@ -41,19 +41,27 @@ class UserProfilesEventListener implements iEventServiceSetup
 			return;
 		}
 
-		$callback = [$this, 'OnUserProfileLinkChange'];
 		$aEventSource = [\User::class, \UserExternal::class, \UserInternal::class];
-
 		EventService::RegisterListener(
 			EVENT_DB_BEFORE_WRITE,
-			$callback,
+			[$this, 'OnUserEdition'],
 			$aEventSource
 		);
 
 		EventService::RegisterListener(
-			EVENT_DB_LINKS_CHANGED,
-			$callback,
-			$aEventSource
+			EVENT_DB_BEFORE_WRITE,
+			[ $this, 'OnUserProfileEdition' ],
+			[ \URP_UserProfile::class ],
+			[],
+			null
+		);
+
+		EventService::RegisterListener(
+			EVENT_DB_CHECK_TO_DELETE,
+			[ $this, 'OnUserProfileLinkDeletion' ],
+			[ \URP_UserProfile::class ],
+			[],
+			null
 		);
 	}
 
@@ -62,12 +70,13 @@ class UserProfilesEventListener implements iEventServiceSetup
 		return $this->bIsRepairmentEnabled;
 	}
 
-	public function OnUserProfileLinkChange(EventData $oEventData): void {
+
+	public function OnUserEdition(EventData $oEventData): void {
 		/** @var \User $oObject */
 		$oUser = $oEventData->Get('object');
 
 		try {
-			$this->RepairProfiles($oUser);
+			$this->ValidateThenRepairOrWarn($oUser);
 		} catch (Exception $e) {
 			IssueLog::Error('Exception occurred on RepairProfiles', LogChannels::DM_CRUD, [
 				'user_class' => get_class($oUser),
@@ -75,9 +84,119 @@ class UserProfilesEventListener implements iEventServiceSetup
 				'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){
+				//user edition: handled by other event
+				return;
+			}
+
+			$oUser = \MetaModel::GetObject(\User::class, $iUserId);
+			$aChanges = $oURP_UserProfile->ListChanges();
+			if (array_key_exists('userid', $aChanges)) {
+				$iUserId = $oURP_UserProfile->GetOriginal('userid');
+				$oPreviousUser = \MetaModel::GetObject(\User::class, $iUserId);
+
+				$oProfileLinkSet = $oPreviousUser->Get('profile_list');
+				$oProfileLinkSet->Rewind();
+				$iCount = 0;
+				while ($oCurrentURP_UserProfile = $oProfileLinkSet->Fetch()) {
+					if ($oCurrentURP_UserProfile->Get('userid') !== $oCurrentURP_UserProfile->GetOriginal('userid')) {
+						$sRemovedProfileId = $oCurrentURP_UserProfile->GetOriginal('profileid');
+						continue;
+					}
+
+					$iCount++;
+					if ($iCount  > 1){
+						//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)){
+				$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){
+				//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();
+			$iCount = 0;
+			while ($oCurrentURP_UserProfile = $oProfileLinkSet->Fetch()) {
+				if (in_array($oCurrentURP_UserProfile->GetKey(), $aDeletedURP_UserProfiles)) {
+					continue;
+				}
+				$iCount++;
+				if ($iCount  > 1){
+					//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
 	 *
@@ -125,83 +244,113 @@ class UserProfilesEventListener implements iEventServiceSetup
 			return;
 		}
 
+
+		$this->FetchRepairingProfileIds($aNonStandaloneProfiles);
+	}
+
+	public function FetchRepairingProfileIds(array $aNonStandaloneProfiles) : void {
+		$aProfiles = [];
 		try {
-			$this->FetchRepairingProfileIds($aNonStandaloneProfiles);
+			$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] = $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;
-			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
+	public function ValidateThenRepairOrWarn(\User $oUser) : void
 	{
-		if (!is_null($oUser))
-		{
-			$oCurrentUserProfileSet = $oUser->Get('profile_list');
-			if ($oCurrentUserProfileSet->Count() === 1){
-				$oProfile = $oCurrentUserProfileSet->Fetch();
-				$sSingleProfileName = $oProfile->Get('profile');
+		$oCurrentUserProfileSet = $oUser->Get('profile_list');
+		if ($oCurrentUserProfileSet->Count() === 1){
+			$oProfile = $oCurrentUserProfileSet->Fetch();
 
-				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);
-					}
-				}
+			$this->RepairUserChangesOrWarn($oUser, $oProfile->Get('profile'));
+		}
+	}
+
+	public function RepairUserChangesOrWarn(\User $oUser, string $sSingleProfileName) : void {
+		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);
+				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', $sRepairingProfileId);
+				$oCurrentUserProfileSet = $oUser->Get('profile_list');
+				$oCurrentUserProfileSet->AddItem($oUserProfile);
+				$oUser->Set('profile_list', $oCurrentUserProfileSet);
 			}
 		}
 	}
 
+	public function RepairProfileChangesOrWarn(\User $oUser, string $sSingleProfileName, \URP_UserProfile $oURP_UserProfile, string $sRemovedProfileId, $bIsRemoval=false) : void {
+		if (array_key_exists($sSingleProfileName, $this->aNonStandaloneProfilesMap)) {
+			$sRepairingProfileId = $this->aNonStandaloneProfilesMap[$sSingleProfileName];
+			if (is_null($sRepairingProfileId)
+				|| ($sRepairingProfileId === $sRemovedProfileId) //cannot repair by readding same remove profile as it will raise uniqueness rule
+			){
+				//Notify current user via session messages that there will be an issue
+				//Without preventing from commiting
+				$sMessage = \Dict::Format("Class:User/NonStandaloneProfileWarning", $sSingleProfileName);
+				//$oURP_UserProfile::SetSessionMessage(get_class($oURP_UserProfile), $oURP_UserProfile->GetKey(), 1, $sMessage, 'WARNING', 1);
+				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', $sRepairingProfileId);
+				$oCurrentUserProfileSet = $oUser->Get('profile_list');
+				$oCurrentUserProfileSet->AddItem($oUserProfile);
+				$oUser->Set('profile_list', $oCurrentUserProfileSet);
+				$oUser->DBWrite();
+			}
+		}
+	}
 }
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
index a1e1e7e6b..d791c64ca 100644
--- 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
@@ -65,14 +65,14 @@ class UserProfilesEventListenerTest extends ItopDataTestCase
 					'Portal user',
 				]
 			],
-			'Portal power user + Support Agent => profiles untouched' => [
+			'Portal power user + Configuration Manager => profiles untouched' => [
 				'aAssociatedProfilesBeforeUserCreation' => [
 					'Portal power user',
-					'Support Agent',
+					'Configuration Manager',
 				],
 				'aExpectedAssociatedProfilesAfterUserCreation'=> [
 					'Portal power user',
-					'Support Agent',
+					'Configuration Manager',
 				]
 			],
 		];
@@ -89,7 +89,7 @@ class UserProfilesEventListenerTest extends ItopDataTestCase
 		$oUser->Set('login', $sLogin);
 		$oUser->Set('password', 'ABCD1234@gabuzomeu');
 		$oUser->Set('language', 'EN US');
-		$this->commonUserCreation($oUser, $aAssociatedProfilesBeforeUserCreation, $aExpectedAssociatedProfilesAfterUserCreation);
+		$this->commonUserCreationTest($oUser, $aAssociatedProfilesBeforeUserCreation, $aExpectedAssociatedProfilesAfterUserCreation);
 	}
 
 	/**
@@ -103,7 +103,7 @@ class UserProfilesEventListenerTest extends ItopDataTestCase
 		$oUser->Set('login', $sLogin);
 		$oUser->Set('password', 'ABCD1234@gabuzomeu');
 		$oUser->Set('language', 'EN US');
-		$this->commonUserUpdate($oUser, $aAssociatedProfilesBeforeUserCreation, $aExpectedAssociatedProfilesAfterUserCreation);
+		$this->commonUserUpdateTest($oUser, $aAssociatedProfilesBeforeUserCreation, $aExpectedAssociatedProfilesAfterUserCreation);
 	}
 
 	/**
@@ -115,7 +115,7 @@ class UserProfilesEventListenerTest extends ItopDataTestCase
 		$oUser = new \UserLDAP();
 		$sLogin = 'testUserLDAPCreationWithPortalPowerUserProfile-'.uniqid();
 		$oUser->Set('login', $sLogin);
-		$this->commonUserCreation($oUser, $aAssociatedProfilesBeforeUserCreation, $aExpectedAssociatedProfilesAfterUserCreation);
+		$this->commonUserCreationTest($oUser, $aAssociatedProfilesBeforeUserCreation, $aExpectedAssociatedProfilesAfterUserCreation);
 	}
 
 	/**
@@ -127,7 +127,7 @@ class UserProfilesEventListenerTest extends ItopDataTestCase
 		$oUser = new \UserLDAP();
 		$sLogin = 'testUserLDAPUpdateWithPortalPowerUserProfile-'.uniqid();
 		$oUser->Set('login', $sLogin);
-		$this->commonUserUpdate($oUser, $aAssociatedProfilesBeforeUserCreation, $aExpectedAssociatedProfilesAfterUserCreation);
+		$this->commonUserUpdateTest($oUser, $aAssociatedProfilesBeforeUserCreation, $aExpectedAssociatedProfilesAfterUserCreation);
 	}
 
 	/**
@@ -139,7 +139,7 @@ class UserProfilesEventListenerTest extends ItopDataTestCase
 		$oUser = new \UserExternal();
 		$sLogin = 'testUserLDAPCreationWithPortalPowerUserProfile-'.uniqid();
 		$oUser->Set('login', $sLogin);
-		$this->commonUserCreation($oUser, $aAssociatedProfilesBeforeUserCreation, $aExpectedAssociatedProfilesAfterUserCreation);
+		$this->commonUserCreationTest($oUser, $aAssociatedProfilesBeforeUserCreation, $aExpectedAssociatedProfilesAfterUserCreation);
 	}
 
 	/**
@@ -151,7 +151,7 @@ class UserProfilesEventListenerTest extends ItopDataTestCase
 		$oUser = new \UserExternal();
 		$sLogin = 'testUserLDAPUpdateWithPortalPowerUserProfile-'.uniqid();
 		$oUser->Set('login', $sLogin);
-		$this->commonUserUpdate($oUser, $aAssociatedProfilesBeforeUserCreation, $aExpectedAssociatedProfilesAfterUserCreation);
+		$this->commonUserUpdateTest($oUser, $aAssociatedProfilesBeforeUserCreation, $aExpectedAssociatedProfilesAfterUserCreation);
 	}
 
 	public function CreateUserForProfileTesting(\User $oUserToCreate, array $aAssociatedProfilesBeforeUserCreation, $bDbInsert=true) : array
@@ -189,16 +189,16 @@ class UserProfilesEventListenerTest extends ItopDataTestCase
 		return [ $sId, $aProfiles];
 	}
 
-	public function commonUserCreation($oUserToCreate, $aAssociatedProfilesBeforeUserCreation,
+	public function commonUserCreationTest($oUserToCreate, $aAssociatedProfilesBeforeUserCreation,
 		$aExpectedAssociatedProfilesAfterUserCreation, $bTestUserItopAccess=true)
 	{
 		$sUserClass = get_class($oUserToCreate);
 		list ($sId, $aProfiles)  = $this->CreateUserForProfileTesting($oUserToCreate, $aAssociatedProfilesBeforeUserCreation);
 
-		$this->CheckProfilesAreOk($sUserClass, $sId, $aExpectedAssociatedProfilesAfterUserCreation, $bTestUserItopAccess);
+		$this->CheckProfilesAreOkAndThenConnectToITop($sUserClass, $sId, $aExpectedAssociatedProfilesAfterUserCreation, $bTestUserItopAccess);
 	}
 
-	public function CheckProfilesAreOk($sUserClass, $sId, $aExpectedAssociatedProfilesAfterUserCreation, $bTestUserItopAccess=true){
+	public function CheckProfilesAreOkAndThenConnectToITop($sUserClass, $sId, $aExpectedAssociatedProfilesAfterUserCreation, $bTestItopConnection=true){
 		$oUser = \MetaModel::GetObject($sUserClass, $sId);
 		$oUserProfileList = $oUser->Get('profile_list');
 		$aProfilesAfterCreation=[];
@@ -211,7 +211,7 @@ class UserProfilesEventListenerTest extends ItopDataTestCase
 				"profile \'$sExpectedProfileName\' should be asociated to user after creation. " .  var_export($aProfilesAfterCreation, true) );
 		}
 
-		if (! $bTestUserItopAccess){
+		if (! $bTestItopConnection){
 			return;
 		}
 
@@ -233,7 +233,7 @@ class UserProfilesEventListenerTest extends ItopDataTestCase
 		$_SESSION = [];
 	}
 
-	public function commonUserUpdate($oUserToCreate, $aAssociatedProfilesBeforeUserCreation,
+	public function commonUserUpdateTest($oUserToCreate, $aAssociatedProfilesBeforeUserCreation,
 		$aExpectedAssociatedProfilesAfterUserCreation)
 	{
 		$sUserClass = get_class($oUserToCreate);
@@ -256,11 +256,14 @@ class UserProfilesEventListenerTest extends ItopDataTestCase
 		$oUserToUpdate->Set('profile_list', $oProfileList);
 		$oUserToUpdate->DBWrite();
 
-		$this->CheckProfilesAreOk($sUserClass, $sId, $aExpectedAssociatedProfilesAfterUserCreation);
+		$this->CheckProfilesAreOkAndThenConnectToITop($sUserClass, $sId, $aExpectedAssociatedProfilesAfterUserCreation);
 	}
 
-	public function testUpdateUserExternalProfilesViaLinks(){
-		$aInitialProfiles = [ "Administrator", "Portal power user"];
+	/**
+	 * @dataProvider ProfilesLinksProvider
+	 */
+	public function testProfilesLinksDBDelete(string $sProfileNameToRemove, $bRaiseException=false){
+		$aInitialProfiles = [ $sProfileNameToRemove, "Portal power user"];
 
 		$oUser = new \UserExternal();
 		$sLogin = 'testUserLDAPUpdateWithPortalPowerUserProfile-'.uniqid();
@@ -269,70 +272,66 @@ class UserProfilesEventListenerTest extends ItopDataTestCase
 		$sUserClass = get_class($oUser);
 		list ($sId, $aProfiles)  = $this->CreateUserForProfileTesting($oUser, $aInitialProfiles);
 
+		if ($bRaiseException){
+			$this->expectException(\DeleteException::class);
+		}
+
 		$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);
+		if (! $bRaiseException) {
+			$aExpectedProfilesAfterUpdate = ["Portal power user", "Portal user"];
+			$this->CheckProfilesAreOkAndThenConnectToITop($sUserClass, $sId, $aExpectedProfilesAfterUpdate);
+		}
 	}
 
-	public function BulkUpdateUserExternalProfilesViaLinksProvider(){
+	/**
+	 * @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);
+		}
+	}
+
+	public function ProfilesLinksProvider() {
 		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"],
-			],
+			"Administrator" => [ "sProfileNameToMove" => "Administrator" ],
+			"Portal user" => [ "sProfileNameToMove" => "Portal user", "bRaiseException" => true ],
 		];
 	}
 
 	/**
-	 * @dataProvider BulkUpdateUserExternalProfilesViaLinksProvider
+	 * @dataProvider ProfilesLinksProvider
 	 */
-	public function testBulkUpdateUserExternalProfilesViaLinks($aInitialProfiles, $aOperation, $aExpectedProfilesAfterUpdate){
+	public function testProfilesLinksEdit_ChangeUserId($sProfileNameToMove, $bRaiseException=false){
+		$aInitialProfiles = [ $sProfileNameToMove, "Portal power user"];
+
 		$oUser = new \UserExternal();
 		$sLogin = 'testUserLDAPUpdateWithPortalPowerUserProfile-'.uniqid();
 		$oUser->Set('login', $sLogin);
@@ -340,31 +339,29 @@ class UserProfilesEventListenerTest extends ItopDataTestCase
 		$sUserClass = get_class($oUser);
 		list ($sId, $aProfiles)  = $this->CreateUserForProfileTesting($oUser, $aInitialProfiles);
 
-		\cmdbAbstractObject::SetEventDBLinksChangedBlocked(true);
+		$oUser = new \UserExternal();
+		$sLogin = 'testUserLDAPUpdateWithPortalPowerUserProfile-'.uniqid();
+		$oUser->Set('login', $sLogin);
+		list ($sAnotherUserId, $aProfiles) = $this->CreateUserForProfileTesting($oUser, ["Configuration Manager"]);
 
-		$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();
-			}
+		if ($bRaiseException){
+			$this->expectException(\CoreCannotSaveObjectException::class);
 		}
 
-		\cmdbAbstractObject::SetEventDBLinksChangedBlocked(false);
-		\cmdbAbstractObject::FireEventDbLinksChangedForAllObjects();
+		$aURPUserProfileByUser = $this->GetURPUserProfileByUser($sId);
+		if (array_key_exists($sProfileNameToMove, $aURPUserProfileByUser)){
+			$oURPUserProfile = $aURPUserProfileByUser[$sProfileNameToMove];
+			$oURPUserProfile->Set('userid', $sAnotherUserId);
+			$oURPUserProfile->DBWrite();
+		}
 
-		$this->CheckProfilesAreOk($sUserClass, $sId, $aExpectedProfilesAfterUpdate);
+		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 {
@@ -461,7 +458,7 @@ class UserProfilesEventListenerTest extends ItopDataTestCase
 
 	public function testInit_ConfWithOneWarningProfile() {
 		\MetaModel::GetConfig()->Set(UserProfilesEventListener::USERPROFILE_REPAIR_ITOP_PARAM_NAME,
-			['Change Supervisor' => 'Administrator', 'Portal power user' => null]
+			['Ticket Manager' => 'Administrator', 'Portal power user' => null]
 		);
 		$oUserProfilesEventListener = new UserProfilesEventListener();
 		$oUserProfilesEventListener->Init();
@@ -470,7 +467,7 @@ class UserProfilesEventListenerTest extends ItopDataTestCase
 
 	public function testInit_ConfWithFurtherWarningProfiles() {
 		\MetaModel::GetConfig()->Set(UserProfilesEventListener::USERPROFILE_REPAIR_ITOP_PARAM_NAME,
-			['Change Supervisor' => null, 'Portal power user' => null]
+			['Ticket Manager' => null, 'Portal power user' => null]
 		);
 		$oUserProfilesEventListener = new UserProfilesEventListener();
 		$oUserProfilesEventListener->Init();
@@ -479,7 +476,7 @@ class UserProfilesEventListenerTest extends ItopDataTestCase
 
 	public function testInit_ConfWithFurtherWarningProfilesAndOneRepairment() {
 		\MetaModel::GetConfig()->Set(UserProfilesEventListener::USERPROFILE_REPAIR_ITOP_PARAM_NAME,
-			['Portal power user' => null, 'Change Supervisor' => null, 'Administrator' => "REST Services User"]
+			['Portal power user' => null, 'Ticket Manager' => null, 'Administrator' => "Configuration Manager"]
 		);
 		$oUserProfilesEventListener = new UserProfilesEventListener();
 		$oUserProfilesEventListener->Init();
@@ -495,14 +492,14 @@ class UserProfilesEventListenerTest extends ItopDataTestCase
 		$oUser->Set('language', 'EN US');
 
 		\MetaModel::GetConfig()->Set(UserProfilesEventListener::USERPROFILE_REPAIR_ITOP_PARAM_NAME,
-			['Portal power user' => 'Change Supervisor']
+			['Portal power user' => 'Ticket Manager']
 		);
 		$oUserProfilesEventListener = new UserProfilesEventListener();
 		$oUserProfilesEventListener->Init();
 		$this->assertTrue($oUserProfilesEventListener->IsRepairmentEnabled());
 
 		$this->CreateUserForProfileTesting($oUser, ['Portal power user'], false);
-		$oUserProfilesEventListener->RepairProfiles($oUser);
+		$oUserProfilesEventListener->ValidateThenRepairOrWarn($oUser);
 
 		$oUserProfileList = $oUser->Get('profile_list');
 		$aProfilesAfterCreation=[];
@@ -510,7 +507,7 @@ class UserProfilesEventListenerTest extends ItopDataTestCase
 			$aProfilesAfterCreation[] = $oProfile->Get('profile');
 		}
 
-		$this->assertContains('Change Supervisor', $aProfilesAfterCreation, var_export($aProfilesAfterCreation, true));
+		$this->assertContains('Ticket Manager', $aProfilesAfterCreation, var_export($aProfilesAfterCreation, true));
 		$this->assertContains('Portal power user', $aProfilesAfterCreation, var_export($aProfilesAfterCreation, true));
 	}
 
@@ -518,8 +515,8 @@ class UserProfilesEventListenerTest extends ItopDataTestCase
 	{
 		\MetaModel::GetConfig()->Set(UserProfilesEventListener::USERPROFILE_REPAIR_ITOP_PARAM_NAME,
 			[
-				'Administrator' => 'REST Services User',
-				'Portal power user' => 'Change Supervisor'
+				'Administrator' => 'Configuration Manager',
+				'Portal power user' => 'Ticket Manager'
 			]
 		);
 		$oUserProfilesEventListener = new UserProfilesEventListener();
@@ -532,7 +529,7 @@ class UserProfilesEventListenerTest extends ItopDataTestCase
 		$oUser->Set('password', 'ABCD1234@gabuzomeu');
 		$oUser->Set('language', 'EN US');
 		$this->CreateUserForProfileTesting($oUser, ['Portal power user'], false);
-		$oUserProfilesEventListener->RepairProfiles($oUser);
+		$oUserProfilesEventListener->ValidateThenRepairOrWarn($oUser);
 
 		$oUserProfileList = $oUser->Get('profile_list');
 		$aProfilesAfterCreation=[];
@@ -540,7 +537,7 @@ class UserProfilesEventListenerTest extends ItopDataTestCase
 			$aProfilesAfterCreation[] = $oProfile->Get('profile');
 		}
 
-		$this->assertContains('Change Supervisor', $aProfilesAfterCreation, var_export($aProfilesAfterCreation, true));
+		$this->assertContains('Ticket Manager', $aProfilesAfterCreation, var_export($aProfilesAfterCreation, true));
 		$this->assertContains('Portal power user', $aProfilesAfterCreation, var_export($aProfilesAfterCreation, true));
 
 		$oUser2 = new \UserLocal();
@@ -550,7 +547,7 @@ class UserProfilesEventListenerTest extends ItopDataTestCase
 		$oUser2->Set('language', 'EN US');
 
 		$this->CreateUserForProfileTesting($oUser2, ['Administrator'], false);
-		$oUserProfilesEventListener->RepairProfiles($oUser2);
+		$oUserProfilesEventListener->ValidateThenRepairOrWarn($oUser2);
 
 		$oUserProfileList = $oUser2->Get('profile_list');
 		$aProfilesAfterCreation=[];
@@ -559,7 +556,7 @@ class UserProfilesEventListenerTest extends ItopDataTestCase
 		}
 
 		$this->assertContains('Administrator', $aProfilesAfterCreation, var_export($aProfilesAfterCreation, true));
-		$this->assertContains('REST Services User', $aProfilesAfterCreation, var_export($aProfilesAfterCreation, true));
+		$this->assertContains('Configuration Manager', $aProfilesAfterCreation, var_export($aProfilesAfterCreation, true));
 	}
 
 	public function testUserCreationWithWarningMessageConf()
@@ -571,11 +568,9 @@ class UserProfilesEventListenerTest extends ItopDataTestCase
 		$oAdminUser->Set('password', 'ABCD1234@gabuzomeu');
 		$oAdminUser->Set('language', 'EN US');
 		$aAssociatedProfilesBeforeUserCreation = ['Administrator'];
-		$this->commonUserCreation($oAdminUser, $aAssociatedProfilesBeforeUserCreation, $aAssociatedProfilesBeforeUserCreation, false);
+		$this->commonUserCreationTest($oAdminUser, $aAssociatedProfilesBeforeUserCreation, $aAssociatedProfilesBeforeUserCreation, false);
 		UserRights::Login($oAdminUser->Get('login'));
 
-
-
 		$aAssociatedProfilesBeforeUserCreation = [
 			'Portal power user'
 		];
@@ -594,8 +589,11 @@ class UserProfilesEventListenerTest extends ItopDataTestCase
 		$oUserProfilesEventListener = new UserProfilesEventListener();
 		$oUserProfilesEventListener->RegisterEventsAndListeners();
 
-		$this->commonUserCreation($oUser, $aAssociatedProfilesBeforeUserCreation, $aAssociatedProfilesBeforeUserCreation, false);
-		$aObjMessages = Session::Get('obj_messages');
+		$this->expectException(\CoreCannotSaveObjectException::class);
+
+		$this->commonUserCreationTest($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));
@@ -608,7 +606,7 @@ class UserProfilesEventListenerTest extends ItopDataTestCase
 			]
 		];
 		$this->assertEquals($aExpectedMessages, array_values($aObjMessages[$sKey]), var_export($aObjMessages[$sKey], true));
-
+*/
 		$_SESSION = [];
 	}
 }