mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-12 23:14:18 +01:00
Merge remote-tracking branch 'origin/support/3.0' into support/3.1
# Conflicts: # core/dbobject.class.php # datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php # lib/composer/autoload_files.php # lib/composer/autoload_real.php # lib/composer/autoload_static.php # pages/UI.php # tests/php-unit-tests/unitary-tests/core/DBObjectTest.php
This commit is contained in:
@@ -933,8 +933,9 @@ class UserRightsProfile extends UserRightsAddOnAPI
|
||||
}
|
||||
|
||||
/**
|
||||
* Find out which attribute is corresponding the the dimension 'owner org'
|
||||
* returns null if no such attribute has been found (no filtering should occur)
|
||||
* @param string $sClass
|
||||
* @return string|null Find out which attribute is corresponding the dimension 'owner org'
|
||||
* returns null if no such attribute has been found (no filtering should occur)
|
||||
*/
|
||||
public static function GetOwnerOrganizationAttCode($sClass)
|
||||
{
|
||||
|
||||
@@ -580,10 +580,10 @@ class UserRightsProfile extends UserRightsAddOnAPI
|
||||
/**
|
||||
* Read and cache organizations allowed to the given user
|
||||
*
|
||||
* @param $oUser
|
||||
* @param $sClass (not used here but can be used in overloads)
|
||||
* @param User $oUser
|
||||
* @param string $sClass (not used here but can be used in overloads)
|
||||
*
|
||||
* @return array
|
||||
* @return array keys of the User allowed org
|
||||
* @throws \CoreException
|
||||
* @throws \Exception
|
||||
*/
|
||||
|
||||
@@ -2015,6 +2015,8 @@ class RestUtils
|
||||
*
|
||||
* @return DBObject The object found
|
||||
* @throws Exception If the input structure is not valid or it could not find exactly one object
|
||||
*
|
||||
* @see DBObject::CheckChangedExtKeysValues() generic method to check that we can access the linked object isn't used in that use case because values can be literal, OQL, friendlyname
|
||||
*/
|
||||
public static function FindObjectFromKey($sClass, $key, $bAllowNullValue = false)
|
||||
{
|
||||
|
||||
@@ -5365,6 +5365,11 @@ EOF
|
||||
'errors' => '<p>'.($bResult ? '' : implode('</p><p>', $aErrorsToDisplay)).'</p>',
|
||||
);
|
||||
if ($bResult && (!$bPreview)) {
|
||||
// doing the check will load multiple times same objects :/
|
||||
// but it shouldn't cost too much on execution time
|
||||
// user can mitigate by selecting less extkeys/lnk to set and/or less objects to update 🤷♂️
|
||||
$oObj->CheckChangedExtKeysValues();
|
||||
|
||||
$oObj->DBUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
36
application/exceptions/InvalidExternalKeyValueException.php
Normal file
36
application/exceptions/InvalidExternalKeyValueException.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @since 2.7.10 3.0.4 3.1.1 3.2.0 N°6458 object creation
|
||||
*/
|
||||
class InvalidExternalKeyValueException extends CoreUnexpectedValue
|
||||
{
|
||||
private const ENUM_PARAMS_OBJECT = 'current_object';
|
||||
private const ENUM_PARAMS_ATTCODE = 'attcode';
|
||||
private const ENUM_PARAMS_ATTVALUE = 'attvalue';
|
||||
private const ENUM_PARAMS_USER = 'current_user';
|
||||
|
||||
public function __construct($oObject, $sAttCode, $aContextData = null, $oPrevious = null)
|
||||
{
|
||||
$aContextData[self::ENUM_PARAMS_OBJECT] = get_class($oObject) . '::' . $oObject->GetKey();
|
||||
$aContextData[self::ENUM_PARAMS_ATTCODE] = $sAttCode;
|
||||
$aContextData[self::ENUM_PARAMS_ATTVALUE] = $oObject->Get($sAttCode);
|
||||
|
||||
$oCurrentUser = UserRights::GetUserObject();
|
||||
if (false === is_null($oCurrentUser)) {
|
||||
$aContextData[self::ENUM_PARAMS_USER] = get_class($oCurrentUser) . '::' . $oCurrentUser->GetKey();
|
||||
}
|
||||
|
||||
parent::__construct('Attribute pointing to an object that is either non existing or not readable by the current user', $aContextData, '', $oPrevious);
|
||||
}
|
||||
|
||||
public function GetAttCode(): string
|
||||
{
|
||||
return $this->getContextData()[self::ENUM_PARAMS_ATTCODE];
|
||||
}
|
||||
|
||||
public function GetAttValue(): string
|
||||
{
|
||||
return $this->getContextData()[self::ENUM_PARAMS_ATTVALUE];
|
||||
}
|
||||
}
|
||||
@@ -3029,6 +3029,7 @@ HTML;
|
||||
*
|
||||
* @return bool if string null or empty
|
||||
* @since 3.0.2 N°5302
|
||||
* @since 2.7.10 N°6458 add method in the 2.7 branch
|
||||
*/
|
||||
public static function IsNullOrEmptyString(?string $sString): bool
|
||||
{
|
||||
@@ -3044,6 +3045,7 @@ HTML;
|
||||
*
|
||||
* @return bool if string is not null and not empty
|
||||
* @since 3.0.2 N°5302
|
||||
* @since 2.7.10 N°6458 add method in the 2.7 branch
|
||||
*/
|
||||
public static function IsNotNullOrEmptyString(?string $sString): bool
|
||||
{
|
||||
|
||||
@@ -2512,6 +2512,83 @@ abstract class DBObject implements iDisplay
|
||||
return array($this->m_bCheckStatus, $this->m_aCheckIssues, $this->m_bSecurityIssue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for extkey attributes values. This will throw exception on non-existing as well as non-accessible objects (silo, scopes).
|
||||
* That's why the test is done for all users including Administrators
|
||||
*
|
||||
* Note that due to perf issues, this isn't called directly by the ORM, but has to be called by consumers when possible.
|
||||
*
|
||||
* @param callable(string, string):bool|null $oIsObjectLoadableCallback Override to check if object is accessible.
|
||||
* Parameters are object class and key
|
||||
* Return value should be false if cannot access object, true otherwise
|
||||
* @return void
|
||||
*
|
||||
* @throws ArchivedObjectException
|
||||
* @throws CoreException if cannot get object attdef list
|
||||
* @throws CoreUnexpectedValue
|
||||
* @throws InvalidExternalKeyValueException
|
||||
* @throws MySQLException
|
||||
* @throws SecurityException if one extkey is pointing to an invalid value
|
||||
*
|
||||
* @link https://github.com/Combodo/iTop/security/advisories/GHSA-245j-66p9-pwmh
|
||||
* @since 2.7.10 3.0.4 3.1.1 3.2.0 N°6458
|
||||
*
|
||||
* @see \RestUtils::FindObjectFromKey for the same check in the REST endpoint
|
||||
*/
|
||||
final public function CheckChangedExtKeysValues(callable $oIsObjectLoadableCallback = null)
|
||||
{
|
||||
if (is_null($oIsObjectLoadableCallback)) {
|
||||
$oIsObjectLoadableCallback = function ($sClass, $sId) {
|
||||
$oRemoteObject = MetaModel::GetObject($sClass, $sId, false);
|
||||
if (is_null($oRemoteObject)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
$aChanges = $this->ListChanges();
|
||||
$aAttCodesChanged = array_keys($aChanges);
|
||||
foreach ($aAttCodesChanged as $sAttDefCode) {
|
||||
$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttDefCode);
|
||||
|
||||
if ($oAttDef instanceof AttributeLinkedSetIndirect) {
|
||||
/** @var ormLinkSet $oOrmSet */
|
||||
$oOrmSet = $this->Get($sAttDefCode);
|
||||
while ($oLnk = $oOrmSet->Fetch()) {
|
||||
$oLnk->CheckChangedExtKeysValues($oIsObjectLoadableCallback);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @noinspection PhpConditionCheckedByNextConditionInspection */
|
||||
/** @noinspection NotOptimalIfConditionsInspection */
|
||||
if (($oAttDef instanceof AttributeHierarchicalKey) || ($oAttDef instanceof AttributeExternalKey)) {
|
||||
$sRemoteObjectClass = $oAttDef->GetTargetClass();
|
||||
$sRemoteObjectKey = $this->Get($sAttDefCode);
|
||||
} else if ($oAttDef instanceof AttributeObjectKey) {
|
||||
$sRemoteObjectClassAttCode = $oAttDef->Get('class_attcode');
|
||||
$sRemoteObjectClass = $this->Get($sRemoteObjectClassAttCode);
|
||||
$sRemoteObjectKey = $this->Get($sAttDefCode);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @noinspection NotOptimalIfConditionsInspection */
|
||||
/** @noinspection TypeUnsafeComparisonInspection */
|
||||
if (utils::IsNullOrEmptyString($sRemoteObjectClass)
|
||||
|| utils::IsNullOrEmptyString($sRemoteObjectKey)
|
||||
|| ($sRemoteObjectKey == 0) // non-strict comparison as we might have bad surprises
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (false === $oIsObjectLoadableCallback($sRemoteObjectClass, $sRemoteObjectKey)) {
|
||||
throw new InvalidExternalKeyValueException($this, $sAttDefCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if it is allowed to delete the existing object from the database
|
||||
*
|
||||
@@ -2655,11 +2732,11 @@ abstract class DBObject implements iDisplay
|
||||
* @api
|
||||
* @api-advanced
|
||||
*
|
||||
* @see \DBObject::ListPreviousValuesForUpdatedAttributes() to get previous values anywhere in the CRUD stack
|
||||
* @see https://www.itophub.io/wiki/page?id=latest%3Acustomization%3Asequence_crud iTop CRUD stack documentation
|
||||
* @return array attname => currentvalue List the attributes that have been changed using {@see DBObject::Set()}.
|
||||
* @return array attcode => currentvalue List the attributes that have been changed using {@see DBObject::Set()}.
|
||||
* Reset during {@see DBObject::DBUpdate()}
|
||||
* @throws Exception
|
||||
* @see \DBObject::ListPreviousValuesForUpdatedAttributes() to get previous values anywhere in the CRUD stack
|
||||
* @see https://www.itophub.io/wiki/page?id=latest%3Acustomization%3Asequence_crud iTop CRUD stack documentation
|
||||
* @uses m_aCurrValues
|
||||
*/
|
||||
public function ListChanges()
|
||||
@@ -3098,6 +3175,8 @@ abstract class DBObject implements iDisplay
|
||||
* @throws \CoreWarning
|
||||
* @throws \MySQLException
|
||||
* @throws \OQLException
|
||||
*
|
||||
* @see DBWrite
|
||||
*/
|
||||
public function DBInsertNoReload()
|
||||
{
|
||||
@@ -3326,13 +3405,13 @@ abstract class DBObject implements iDisplay
|
||||
* Update an object in DB
|
||||
*
|
||||
* @api
|
||||
* @see DBObject::DBWrite()
|
||||
*
|
||||
* @return int object key
|
||||
*
|
||||
* @throws \CoreException
|
||||
* @throws \CoreCannotSaveObjectException if CheckToWrite() returns issues
|
||||
* @throws \Exception
|
||||
*
|
||||
* @see DBObject::DBWrite()
|
||||
*/
|
||||
public function DBUpdate()
|
||||
{
|
||||
@@ -3784,13 +3863,18 @@ abstract class DBObject implements iDisplay
|
||||
|
||||
/**
|
||||
* Make the current changes persistent - clever wrapper for Insert or Update
|
||||
*
|
||||
* @api
|
||||
*
|
||||
* @api
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
* @throws \CoreCannotSaveObjectException
|
||||
* @throws \CoreException
|
||||
*
|
||||
* @throws ArchivedObjectException
|
||||
* @throws CoreCannotSaveObjectException
|
||||
* @throws CoreException
|
||||
* @throws CoreUnexpectedValue
|
||||
* @throws CoreWarning
|
||||
* @throws MySQLException
|
||||
* @throws OQLException
|
||||
*/
|
||||
public function DBWrite()
|
||||
{
|
||||
|
||||
@@ -1445,8 +1445,10 @@ abstract class MetaModel
|
||||
*
|
||||
* @return AttributeDefinition[]
|
||||
* @throws \CoreException
|
||||
*
|
||||
* @see GetAttributesList for attcode list
|
||||
*/
|
||||
final static public function ListAttributeDefs($sClass)
|
||||
final public static function ListAttributeDefs($sClass)
|
||||
{
|
||||
self::_check_subclass($sClass);
|
||||
return self::$m_aAttribDefs[$sClass];
|
||||
@@ -1459,8 +1461,10 @@ abstract class MetaModel
|
||||
* @param string[] $aDesiredAttTypes Array of AttributeDefinition classes to filter the list on
|
||||
* @param string|null $sListCode If provided, attributes will be limited to those in this zlist
|
||||
*
|
||||
* @return array
|
||||
* @return string[] list of attcodes
|
||||
* @throws \CoreException
|
||||
*
|
||||
* @see ListAttributeDefs to get AttributeDefinition array instead
|
||||
*/
|
||||
final public static function GetAttributesList(string $sClass, array $aDesiredAttTypes = [], ?string $sListCode = null)
|
||||
{
|
||||
|
||||
@@ -1121,9 +1121,7 @@ class UserRights
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current user login or an empty string if nobody connected.
|
||||
*
|
||||
* @return string
|
||||
* @return string connected {@see User} login field value, otherwise empty string
|
||||
*/
|
||||
public static function GetUser()
|
||||
{
|
||||
@@ -1571,9 +1569,9 @@ class UserRights
|
||||
|
||||
/**
|
||||
* @param string $sClass
|
||||
* @param int $iActionCode
|
||||
* @param \DBObjectSet $oInstanceSet
|
||||
* @param \User $oUser
|
||||
* @param int $iActionCode see UR_ACTION_* constants
|
||||
* @param DBObjectSet $oInstanceSet
|
||||
* @param User $oUser
|
||||
*
|
||||
* @return int (UR_ALLOWED_YES|UR_ALLOWED_NO|UR_ALLOWED_DEPENDS)
|
||||
* @throws \CoreException
|
||||
|
||||
@@ -32,6 +32,7 @@ use Combodo\iTop\Form\Form;
|
||||
use Combodo\iTop\Form\FormManager;
|
||||
use Combodo\iTop\Portal\Helper\ApplicationHelper;
|
||||
use Combodo\iTop\Portal\Helper\ObjectFormHandlerHelper;
|
||||
use Combodo\iTop\Portal\Helper\SecurityHelper;
|
||||
use CoreCannotSaveObjectException;
|
||||
use DBObject;
|
||||
use DBObjectSearch;
|
||||
@@ -43,6 +44,7 @@ use DOMXPath;
|
||||
use Exception;
|
||||
use ExceptionLog;
|
||||
use InlineImage;
|
||||
use InvalidExternalKeyValueException;
|
||||
use IssueLog;
|
||||
use LogChannels;
|
||||
use MetaModel;
|
||||
@@ -50,6 +52,7 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use UserRights;
|
||||
use utils;
|
||||
use const UR_ACTION_READ;
|
||||
|
||||
/**
|
||||
* Description of ObjectFormManager
|
||||
@@ -1133,6 +1136,9 @@ class ObjectFormManager extends FormManager
|
||||
$bWasModified = $this->oObject->IsModified();
|
||||
$bActivateTriggers = (!$bIsNew && $bWasModified);
|
||||
|
||||
/** @var SecurityHelper $oSecurityHelper */
|
||||
$oSecurityHelper = $this->oContainer->get('security_helper');
|
||||
|
||||
// Forcing allowed writing on the object if necessary. This is used in some particular cases.
|
||||
$bAllowWrite = $this->oFormHandlerHelper->getSecurityHelper()->IsActionAllowed($bIsNew ? UR_ACTION_CREATE : UR_ACTION_MODIFY, $sObjectClass, $this->oObject->GetKey());
|
||||
if ($bAllowWrite) {
|
||||
@@ -1142,12 +1148,15 @@ class ObjectFormManager extends FormManager
|
||||
// Writing object to DB
|
||||
try
|
||||
{
|
||||
$this->oObject->CheckChangedExtKeysValues(function ($sClass, $sId) use ($oSecurityHelper): bool {
|
||||
return $oSecurityHelper->IsActionAllowed(UR_ACTION_READ, $sClass, $sId);
|
||||
});
|
||||
$this->oObject->DBWrite();
|
||||
}
|
||||
catch (CoreCannotSaveObjectException $e) {
|
||||
} catch (CoreCannotSaveObjectException $e) {
|
||||
throw new Exception($e->getHtmlMessage());
|
||||
}
|
||||
catch (Exception $e) {
|
||||
} catch (InvalidExternalKeyValueException $e) {
|
||||
throw new Exception($e->getIssue());
|
||||
} catch (Exception $e) {
|
||||
$aContext = [
|
||||
'origin' => __CLASS__.'::'.__METHOD__,
|
||||
'obj_class' => get_class($this->oObject),
|
||||
|
||||
@@ -700,6 +700,7 @@ return array(
|
||||
'IntervalOqlExpression' => $baseDir . '/core/oql/oqlquery.class.inc.php',
|
||||
'Introspection' => $baseDir . '/core/introspection.class.inc.php',
|
||||
'InvalidConfigParamException' => $baseDir . '/application/exceptions/InvalidConfigParamException.php',
|
||||
'InvalidExternalKeyValueException' => $baseDir . '/application/exceptions/InvalidExternalKeyValueException.php',
|
||||
'InvalidPasswordAttributeOneWayPassword' => $baseDir . '/application/exceptions/InvalidPasswordAttributeOneWayPassword.php',
|
||||
'IssueLog' => $baseDir . '/core/log.class.inc.php',
|
||||
'ItopCounter' => $baseDir . '/core/counter.class.inc.php',
|
||||
|
||||
@@ -1064,6 +1064,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
|
||||
'IntervalOqlExpression' => __DIR__ . '/../..' . '/core/oql/oqlquery.class.inc.php',
|
||||
'Introspection' => __DIR__ . '/../..' . '/core/introspection.class.inc.php',
|
||||
'InvalidConfigParamException' => __DIR__ . '/../..' . '/application/exceptions/InvalidConfigParamException.php',
|
||||
'InvalidExternalKeyValueException' => __DIR__ . '/../..' . '/application/exceptions/InvalidExternalKeyValueException.php',
|
||||
'InvalidPasswordAttributeOneWayPassword' => __DIR__ . '/../..' . '/application/exceptions/InvalidPasswordAttributeOneWayPassword.php',
|
||||
'IssueLog' => __DIR__ . '/../..' . '/core/log.class.inc.php',
|
||||
'ItopCounter' => __DIR__ . '/../..' . '/core/counter.class.inc.php',
|
||||
|
||||
@@ -18,8 +18,8 @@ use Combodo\iTop\Application\UI\Base\Component\QuickCreate\QuickCreateHelper;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\Object\ObjectSummary;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\PageContent\PageContentFactory;
|
||||
use Combodo\iTop\Controller\AbstractController;
|
||||
use Combodo\iTop\Service\Router\Router;
|
||||
use Combodo\iTop\Service\Base\ObjectRepository;
|
||||
use Combodo\iTop\Service\Router\Router;
|
||||
use CoreCannotSaveObjectException;
|
||||
use DeleteException;
|
||||
use Dict;
|
||||
@@ -428,6 +428,8 @@ JS;
|
||||
'transaction_id' => $sTransactionId,
|
||||
],
|
||||
]);
|
||||
|
||||
$oObj->CheckChangedExtKeysValues();
|
||||
$oObj->DBInsertNoReload();
|
||||
|
||||
|
||||
@@ -632,6 +634,8 @@ JS;
|
||||
throw new CoreCannotSaveObjectException(array('id' => $oObj->GetKey(), 'class' => $sClass, 'issues' => $aErrors));
|
||||
}
|
||||
|
||||
$oObj->CheckChangedExtKeysValues();
|
||||
|
||||
// Transactions are now handled in DBUpdate
|
||||
$oObj->SetContextSection('temporary_objects', [
|
||||
'finalize' => [
|
||||
|
||||
@@ -59,6 +59,16 @@ Fix that in the XML configuration in the PHP section
|
||||
<ini name="memory_limit" value="512M"/>
|
||||
```
|
||||
|
||||
|
||||
### Measure the time spent in a test
|
||||
|
||||
Simply cut'n paste the following line at several places within the test function:
|
||||
|
||||
```php
|
||||
if (isset($fStarted)) {echo 'L'.__LINE__.': '.round(microtime(true) - $fStarted, 3)."\n";} $fStarted = microtime(true);
|
||||
```
|
||||
|
||||
|
||||
### Understand tests interactions
|
||||
|
||||
With PHPStorm, select two tests, right click to get the context menu, then `run`.
|
||||
@@ -119,4 +129,3 @@ This won't work because the comment MUST start with `/**` (two stars) to be cons
|
||||
|
||||
Therefore, if the tests are isolated, then `setupBeforeClass` will be called as often as `setUp`.
|
||||
|
||||
This has been proven with [`runClassInSeparateProcessTest.php`](experiments/runClassInSeparateProcessTest.php)
|
||||
@@ -749,7 +749,7 @@ abstract class ItopDataTestCase extends ItopTestCase
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function AddCIToTicket($oCI, $oTicket, $sImpactCode)
|
||||
protected function AddCIToTicket($oCI, $oTicket, $sImpactCode = 'manual')
|
||||
{
|
||||
$oNewLink = new lnkFunctionalCIToTicket();
|
||||
$oNewLink->Set('functionalci_id', $oCI->GetKey());
|
||||
|
||||
@@ -17,22 +17,23 @@
|
||||
// along with iTop. If not, see <http://www.gnu.org/licenses/>
|
||||
//
|
||||
|
||||
/**
|
||||
* Created by PhpStorm.
|
||||
* User: Eric
|
||||
* Date: 02/10/2017
|
||||
* Time: 13:58
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Core;
|
||||
|
||||
use Attachment;
|
||||
use Combodo\iTop\Service\Events\EventData;
|
||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
use CoreException;
|
||||
use DBObject;
|
||||
use InvalidExternalKeyValueException;
|
||||
use lnkContactToFunctionalCI;
|
||||
use lnkPersonToTeam;
|
||||
use MetaModel;
|
||||
use Organization;
|
||||
use Person;
|
||||
use Team;
|
||||
use User;
|
||||
use UserRights;
|
||||
use utils;
|
||||
|
||||
|
||||
/**
|
||||
@@ -41,6 +42,7 @@ use MetaModel;
|
||||
class DBObjectTest extends ItopDataTestCase
|
||||
{
|
||||
const CREATE_TEST_ORG = true;
|
||||
const INVALID_OBJECT_KEY = 123456789;
|
||||
|
||||
// Counts
|
||||
public $aReloadCount = [];
|
||||
@@ -433,6 +435,267 @@ class DBObjectTest extends ItopDataTestCase
|
||||
}
|
||||
}
|
||||
|
||||
private function GetAlwaysTrueCallback(): callable
|
||||
{
|
||||
return static function () {
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
private function GetAlwaysFalseCallback(): callable
|
||||
{
|
||||
return static function () {
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers DBObject::CheckChangedExtKeysValues()
|
||||
* @runInSeparateProcess MetaModel::GetObject returning wrong values :(
|
||||
*/
|
||||
public function testCheckExtKeysSiloOnAttributeExternalKey()
|
||||
{
|
||||
//--- Preparing data...
|
||||
$oAlwaysTrueCallback = $this->GetAlwaysTrueCallback();
|
||||
$oAlwaysFalseCallback = $this->GetAlwaysFalseCallback();
|
||||
|
||||
/** @var Organization $oDemoOrg */
|
||||
$oDemoOrg = MetaModel::GetObjectByName(Organization::class, 'Demo');
|
||||
/** @var Organization $oMyCompanyOrg */
|
||||
$oMyCompanyOrg = MetaModel::GetObjectByName(Organization::class, 'My Company/Department');
|
||||
|
||||
/** @var Person $oPersonOfDemoOrg */
|
||||
$oPersonOfDemoOrg = MetaModel::GetObjectByName(Person::class, 'Agatha Christie');
|
||||
/** @var Person $oPersonOfMyCompanyOrg */
|
||||
$oPersonOfMyCompanyOrg = MetaModel::GetObjectByName(Person::class, 'My first name My last name');
|
||||
|
||||
$sConfigurationManagerProfileId = 3; // Access to Person objects
|
||||
$oUserWithAllowedOrgs = $this->CreateDemoOrgUser($oDemoOrg, $sConfigurationManagerProfileId);
|
||||
|
||||
$oAdminUser = MetaModel::GetObjectByName(User::class, 'admin', false);
|
||||
if (is_null($oAdminUser)) {
|
||||
$oAdminUser = $this->CreateUser('admin', 1);
|
||||
}
|
||||
|
||||
/** @var Person $oPersonObject */
|
||||
$oPersonObject = $this->CreatePerson(0, $oMyCompanyOrg->GetKey());
|
||||
|
||||
//--- Now we can do some tests !
|
||||
UserRights::Login($oUserWithAllowedOrgs->Get('login'));
|
||||
$this->ResetMetaModelQueyCacheGetObject();
|
||||
|
||||
try {
|
||||
$oPersonObject->CheckChangedExtKeysValues();
|
||||
} catch (InvalidExternalKeyValueException $eCannotSave) {
|
||||
$this->fail('Should skip external keys already written in Database');
|
||||
}
|
||||
|
||||
$oPersonObject->Set('manager_id', $oPersonOfDemoOrg->GetKey());
|
||||
try {
|
||||
$oPersonObject->CheckChangedExtKeysValues();
|
||||
} catch (InvalidExternalKeyValueException $eCannotSave) {
|
||||
$this->fail('Should allow objects in the same org as the current user');
|
||||
}
|
||||
|
||||
try {
|
||||
$oPersonObject->CheckChangedExtKeysValues($oAlwaysFalseCallback);
|
||||
$this->fail('Should consider the callback returning "false"');
|
||||
} catch (InvalidExternalKeyValueException $eCannotSave) {
|
||||
// Ok, the exception was expected
|
||||
}
|
||||
|
||||
$oPersonObject->Set('manager_id', $oPersonOfMyCompanyOrg->GetKey());
|
||||
try {
|
||||
$oPersonObject->CheckChangedExtKeysValues();
|
||||
$this->fail('Should not allow objects not being in the allowed orgs of the current user');
|
||||
} catch (InvalidExternalKeyValueException $eCannotSave) {
|
||||
$this->assertEquals('manager_id', $eCannotSave->GetAttCode(), 'Should report the wrong external key attcode');
|
||||
$this->assertEquals($oMyCompanyOrg->GetKey(), $eCannotSave->GetAttValue(), 'Should report the unauthorized external key value');
|
||||
}
|
||||
|
||||
try {
|
||||
$oPersonObject->CheckChangedExtKeysValues($oAlwaysTrueCallback);
|
||||
} catch (InvalidExternalKeyValueException $eCannotSave) {
|
||||
$this->fail('Should consider the callback returning "true"');
|
||||
}
|
||||
|
||||
UserRights::Logoff();
|
||||
$this->ResetMetaModelQueyCacheGetObject();
|
||||
|
||||
UserRights::Login($oAdminUser->Get('login'));
|
||||
$oPersonObject->CheckChangedExtKeysValues();
|
||||
$this->assertTrue(true, 'Admin user can create objects in any org');
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers DBObject::CheckChangedExtKeysValues()
|
||||
* @runInSeparateProcess MetaModel::GetObject returning wrong values :(
|
||||
*/
|
||||
public function testCheckExtKeysOnAttributeLinkedSetIndirect()
|
||||
{
|
||||
//--- Preparing data...
|
||||
/** @var Organization $oDemoOrg */
|
||||
$oDemoOrg = MetaModel::GetObjectByName(Organization::class, 'Demo');
|
||||
/** @var Person $oPersonOnItDepartmentOrg */
|
||||
$oPersonOnItDepartmentOrg = MetaModel::GetObjectByName(Person::class, 'Anna Gavalda');
|
||||
/** @var Person $oPersonOnDemoOrg */
|
||||
$oPersonOnDemoOrg = MetaModel::GetObjectByName(Person::class, 'Claude Monet');
|
||||
|
||||
$sConfigManagerProfileId = 3; // access to Team and Contact objects
|
||||
$oUserWithAllowedOrgs = $this->CreateDemoOrgUser($oDemoOrg, $sConfigManagerProfileId);
|
||||
|
||||
//--- Now we can do some tests !
|
||||
UserRights::Login($oUserWithAllowedOrgs->Get('login'));
|
||||
$this->ResetMetaModelQueyCacheGetObject();
|
||||
|
||||
$oTeam = MetaModel::NewObject(Team::class, [
|
||||
'name' => 'The A Team',
|
||||
'org_id' => $oDemoOrg->GetKey()
|
||||
]);
|
||||
|
||||
// Part 1 - Test with an invalid id (non-existing object)
|
||||
//
|
||||
$oPersonLinks = \DBObjectSet::FromScratch(lnkPersonToTeam::class);
|
||||
$oPersonLinks->AddObject(MetaModel::NewObject(lnkPersonToTeam::class, [
|
||||
'person_id' => self::INVALID_OBJECT_KEY,
|
||||
]));
|
||||
$oTeam->Set('persons_list', $oPersonLinks);
|
||||
|
||||
try {
|
||||
$oTeam->CheckChangedExtKeysValues();
|
||||
$this->fail('An unknown object should be detected as invalid');
|
||||
} catch (InvalidExternalKeyValueException $e) {
|
||||
// we are getting the exception on the lnk class
|
||||
// In consequence attcode is `lnkPersonToTeam.person_id` instead of `Team.persons_list`
|
||||
$this->assertEquals('person_id', $e->GetAttCode(), 'The reported attcode should be the external key on the link');
|
||||
$this->assertEquals(self::INVALID_OBJECT_KEY, $e->GetAttValue(), 'The reported value should be the external key on the link');
|
||||
}
|
||||
|
||||
try {
|
||||
$oTeam->CheckChangedExtKeysValues($this->GetAlwaysTrueCallback());
|
||||
} catch (InvalidExternalKeyValueException $e) {
|
||||
$this->fail('Should have no error when callback returns true');
|
||||
}
|
||||
|
||||
// Part 2 - Test with an allowed object
|
||||
//
|
||||
$oPersonLinks = \DBObjectSet::FromScratch(lnkPersonToTeam::class);
|
||||
$oPersonLinks->AddObject(MetaModel::NewObject(lnkPersonToTeam::class, [
|
||||
'person_id' => $oPersonOnDemoOrg->GetKey(),
|
||||
]));
|
||||
$oTeam->Set('persons_list', $oPersonLinks);
|
||||
|
||||
try {
|
||||
$oTeam->CheckChangedExtKeysValues();
|
||||
} catch (InvalidExternalKeyValueException $e) {
|
||||
$this->fail('An authorized object should be detected as valid');
|
||||
}
|
||||
|
||||
try {
|
||||
$oTeam->CheckChangedExtKeysValues($this->GetAlwaysFalseCallback());
|
||||
$this->fail('Should cascade the callback result when it is "false"');
|
||||
} catch (InvalidExternalKeyValueException $e) {
|
||||
// Ok, the exception was expected
|
||||
}
|
||||
|
||||
// Part 3 - Test with a not allowed object
|
||||
//
|
||||
$oPersonLinks = \DBObjectSet::FromScratch(lnkPersonToTeam::class);
|
||||
$oPersonLinks->AddObject(MetaModel::NewObject(lnkPersonToTeam::class, [
|
||||
'person_id' => $oPersonOnItDepartmentOrg->GetKey(),
|
||||
]));
|
||||
$oTeam->Set('persons_list', $oPersonLinks);
|
||||
|
||||
try {
|
||||
$oTeam->CheckChangedExtKeysValues();
|
||||
$this->fail('An unauthorized object should be detected as invalid');
|
||||
}
|
||||
catch (InvalidExternalKeyValueException $e) {
|
||||
// Ok, the exception was expected
|
||||
}
|
||||
|
||||
try {
|
||||
$oTeam->CheckChangedExtKeysValues($this->GetAlwaysTrueCallback());
|
||||
} catch (InvalidExternalKeyValueException $e) {
|
||||
$this->fail('Should cascade the callback result when it is "true"');
|
||||
}
|
||||
|
||||
$oTeam->DBInsert(); // persisting invalid value and resets the object changed values
|
||||
try {
|
||||
$oTeam->CheckChangedExtKeysValues();
|
||||
}
|
||||
catch (InvalidExternalKeyValueException $e) {
|
||||
$this->fail('An unauthorized value should be ignored when it is not being modified');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers DBObject::CheckChangedExtKeysValues()
|
||||
* @runInSeparateProcess MetaModel::GetObject returning wrong values :(
|
||||
*/
|
||||
public function testCheckExtKeysSiloOnAttributeObjectKey()
|
||||
{
|
||||
//--- Preparing data...
|
||||
/** @var Organization $oDemoOrg */
|
||||
$oDemoOrg = MetaModel::GetObjectByName(Organization::class, 'Demo');
|
||||
/** @var Person $oPersonOnItDepartmentOrg */
|
||||
$oPersonOnItDepartmentOrg = MetaModel::GetObjectByName(Person::class, 'Anna Gavalda');
|
||||
/** @var Person $oPersonOnDemoOrg */
|
||||
$oPersonOnDemoOrg = MetaModel::GetObjectByName(Person::class, 'Claude Monet');
|
||||
|
||||
$sConfigManagerProfileId = 3; // access to Team and Contact objects
|
||||
$oUserWithAllowedOrgs = $this->CreateDemoOrgUser($oDemoOrg, $sConfigManagerProfileId);
|
||||
|
||||
//--- Now we can do some tests !
|
||||
UserRights::Login($oUserWithAllowedOrgs->Get('login'));
|
||||
$this->ResetMetaModelQueyCacheGetObject();
|
||||
|
||||
$oAttachment = MetaModel::NewObject(Attachment::class, [
|
||||
'item_class' => Person::class,
|
||||
'item_id' => $oPersonOnDemoOrg->GetKey(),
|
||||
]);
|
||||
try {
|
||||
$oAttachment->CheckChangedExtKeysValues();
|
||||
} catch (InvalidExternalKeyValueException $e) {
|
||||
$this->fail('Should be allowed to create an attachment pointing to a ticket in the allowed org list');
|
||||
}
|
||||
|
||||
$oAttachment = MetaModel::NewObject(Attachment::class, [
|
||||
'item_class' => Person::class,
|
||||
'item_id' => $oPersonOnItDepartmentOrg->GetKey(),
|
||||
]);
|
||||
$this->ResetMetaModelQueyCacheGetObject();
|
||||
try {
|
||||
$oAttachment->CheckChangedExtKeysValues();
|
||||
$this->fail('There should be an error on attachment pointing to a non allowed org object');
|
||||
} catch (InvalidExternalKeyValueException $e) {
|
||||
$this->assertEquals('item_id', $e->GetAttCode(), 'Should report the object key attribute');
|
||||
$this->assertEquals($oPersonOnItDepartmentOrg->GetKey(), $e->GetAttValue(), 'Should report the object key value');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to reset the metamodel cache
|
||||
* We might need to create something generic and add it to {@see UserRights::Logoff()} ?
|
||||
*/
|
||||
private function ResetMetaModelQueyCacheGetObject() {
|
||||
$this->SetNonPublicStaticProperty(MetaModel::class, 'aQueryCacheGetObject', []);
|
||||
}
|
||||
|
||||
private function CreateDemoOrgUser(Organization $oDemoOrg, string $sProfileId): User
|
||||
{
|
||||
utils::GetConfig()->SetModuleSetting('authent-local', 'password_validation.pattern', '');
|
||||
$oUserWithAllowedOrgs = $this->CreateContactlessUser('demo_test_' . __CLASS__, $sProfileId);
|
||||
/** @var \URP_UserOrg $oUserOrg */
|
||||
$oUserOrg = \MetaModel::NewObject('URP_UserOrg', ['allowed_org_id' => $oDemoOrg->GetKey(),]);
|
||||
$oAllowedOrgList = $oUserWithAllowedOrgs->Get('allowed_org_list');
|
||||
$oAllowedOrgList->AddItem($oUserOrg);
|
||||
$oUserWithAllowedOrgs->Set('allowed_org_list', $oAllowedOrgList);
|
||||
$oUserWithAllowedOrgs->DBWrite();
|
||||
|
||||
return $oUserWithAllowedOrgs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test attribute integer incrementation.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user