Compare commits

...

4 Commits

Author SHA1 Message Date
jf-cbd
389d95fec5 Add bypass for admin, even if the caller already do this check 2026-10-16 16:02:09 +02:00
jf-cbd
6536eeab1c Rollback unwanted change 2026-10-16 15:34:18 +02:00
jf-cbd
ec8ec767d2 Add UX 2026-10-16 15:31:31 +02:00
jf-cbd
7ef28d1aea Protect edition 2026-10-16 15:26:16 +02:00
4 changed files with 128 additions and 6 deletions

View File

@@ -3936,6 +3936,18 @@ HTML;
$iFlags = $iFlags & ~OPT_ATT_READONLY; // Mandatory fields cannot be read-only when creating an object
}
if (!$this->IsNew()
&& ($sAttCode === 'email')
&& MetaModel::IsParentClass('Contact', get_class($this))
&& UserRights::IsActionAllowedOnAttribute(
get_class($this),
$sAttCode,
UR_ACTION_MODIFY,
DBObjectSet::FromObject($this)
) != UR_ALLOWED_YES) {
$iFlags |= OPT_ATT_READONLY;
}
return $iFlags;
}

View File

@@ -438,9 +438,21 @@ class CoreServices implements iRestServiceProvider, iRestInputSanitizer
$oResult->message = "The current user does not have enough permissions for massively modifying data of class $sTargetClass";
} else {
$oObject = RestUtils::FindObjectFromKey($sClass, $key);
RestUtils::UpdateObjectFromFields($oObject, $aFields);
$oObject->DBUpdate();
$oResult->AddObject(0, 'updated', $oObject, $aShowFields, $bExtendedOutput);
if (MetaModel::IsParentClass('Contact', get_class($oObject))
&& property_exists($aFields, 'email')
&& UserRights::IsActionAllowedOnAttribute(
get_class($oObject),
'email',
UR_ACTION_MODIFY,
DBObjectSet::FromObject($oObject)
) != UR_ALLOWED_YES) {
$oResult->code = RestResult::UNAUTHORIZED;
$oResult->message = "The current user does not have enough permissions for modifying the email of the contact of class $sTargetClass";
} else {
RestUtils::UpdateObjectFromFields($oObject, $aFields);
$oObject->DBUpdate();
$oResult->AddObject(0, 'updated', $oObject, $aShowFields, $bExtendedOutput);
}
}
break;

View File

@@ -1635,10 +1635,18 @@ class UserRights
return UR_ALLOWED_YES;
}
if (is_null($oUser)) {
$oUser = self::$m_oUser;
}
if (($sAttCode === 'email')
&& in_array($iActionCode, [UR_ACTION_MODIFY, UR_ACTION_BULK_MODIFY], true)
&& MetaModel::IsParentClass('Contact', $sClass)
&& !self::IsContactEmailEditAllowed($oInstanceSet, $oUser)) {
return UR_ALLOWED_NO;
}
if (MetaModel::HasCategory($sClass, 'bizmodel') || MetaModel::HasCategory($sClass, 'grant_by_profile')) {
if (is_null($oUser)) {
$oUser = self::$m_oUser;
}
return self::$m_oAddOn->IsActionAllowedOnAttribute($oUser, $sClass, $sAttCode, $iActionCode, $oInstanceSet);
}
@@ -1652,6 +1660,30 @@ class UserRights
}
private static function IsContactEmailEditAllowed($oInstanceSet, $oUser): bool
{
if (self::HasProfile('SuperUser', $oUser) && self::IsAdministrator($oUser)) {
return true;
}
$oCurrentContact = is_null($oUser) ? null : $oUser->GetContactObject();
if (is_null($oCurrentContact)) {
return false;
}
$oTargetContact = null;
if ($oInstanceSet instanceof DBObject) {
$oTargetContact = $oInstanceSet;
} elseif (($oInstanceSet instanceof DBObjectSet) && ($oInstanceSet->Count() === 1)) {
$aTargetObjects = $oInstanceSet->ToArray(false);
$oTargetContact = $aTargetObjects[0] ?? null;
}
return ($oTargetContact instanceof DBObject)
&& MetaModel::IsParentClass('Contact', get_class($oTargetContact))
&& ($oCurrentContact->GetKey() == $oTargetContact->GetKey());
}
/**
* @param \User|null $oUser
*

View File

@@ -34,6 +34,7 @@ use DBObject;
use DBObjectSearch;
use DBObjectSet;
use DeleteException;
use MetaModel;
use URP_UserProfile;
use UserRights;
use utils;
@@ -278,6 +279,71 @@ class UserRightsTest extends ItopDataTestCase
];
}
public function testLinkedUserCanUpdateOwnContactEmailViaDbUpdate(): void
{
$this->CreateTestOrganization();
$oContact = $this->CreatePerson(201);
$this->CreateAndLoginContactEmailTestUser('contact-email-linked-', 3, $oContact);
$oContact->Set('email', 'linked-user@example.com');
$oContact->DBUpdate();
$this->assertSame('linked-user@example.com', MetaModel::GetObject('Contact', $oContact->GetKey(), false, true)->Get('email'));
}
public function testAdministratorCanUpdateAnotherContactEmailViaDbUpdate(): void
{
$this->CreateTestOrganization();
$oTargetContact = $this->CreatePerson(202);
$oAdministratorContact = $this->CreatePerson(203);
$this->CreateAndLoginContactEmailTestUser('contact-email-admin-', 1, $oAdministratorContact);
$oTargetContact->Set('email', 'administrator@example.com');
$oTargetContact->DBUpdate();
$this->assertSame('administrator@example.com', MetaModel::GetObject('Contact', $oTargetContact->GetKey(), false, true)->Get('email'));
}
public function testSuperUserCanUpdateAnotherContactEmailViaDbUpdate(): void
{
$this->CreateTestOrganization();
$oTargetContact = $this->CreatePerson(204);
$oSuperUserContact = $this->CreatePerson(205);
$oSuperUserProfile = MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", ['name' => 'SuperUser'], true);
$this->assertNotNull($oSuperUserProfile, 'The SuperUser profile must be available for this test');
$this->CreateAndLoginContactEmailTestUser('contact-email-superuser-', $oSuperUserProfile->GetKey(), $oSuperUserContact);
$oTargetContact->Set('email', 'superuser@example.com');
$oTargetContact->DBUpdate();
$this->assertSame('superuser@example.com', MetaModel::GetObject('Contact', $oTargetContact->GetKey(), false, true)->Get('email'));
}
public function testConfigurationManagerCannotUpdateAnotherContactEmailViaDbUpdate(): void
{
$this->CreateTestOrganization();
$oTargetContact = $this->CreatePerson(206);
$oConfigurationManagerContact = $this->CreatePerson(207);
$this->CreateAndLoginContactEmailTestUser('contact-email-config-manager-', 3, $oConfigurationManagerContact);
$oTargetContact->Set('email', 'forbidden@example.com');
try {
$oTargetContact->DBUpdate();
$this->fail('The object write check must reject an email change to another contact');
} catch (CoreCannotSaveObjectException $e) {
$this->assertNotEmpty($e->getIssues());
}
$this->assertSame('', MetaModel::GetObject('Contact', $oTargetContact->GetKey(), false, true)->Get('email'));
}
private function CreateAndLoginContactEmailTestUser(string $sLoginPrefix, int $iProfileId, DBObject $oContact): void
{
$_SESSION = [];
$oUser = $this->CreateUser(uniqid($sLoginPrefix), $iProfileId, 'A1b!Passw0rd', $oContact->GetKey());
UserRights::Login($oUser->Get('login'));
}
/**
* @dataProvider ProfileDenyingConsoleProvider
* @doesNotPerformAssertions