Compare commits

...

5 Commits
3.2.1 ... tag

Author SHA1 Message Date
jf-cbd
5232694c04 N°8281 - Unable to click search button in multiple request template 2025-03-21 11:20:37 +01:00
jf-cbd
874a5fd2ce Dump autoloader 2025-03-12 11:36:06 +01:00
jf-cbd
063bb9680e N°8231 - Better variable fallback 2025-03-06 16:09:51 +01:00
jf-cbd
8f8ac46f55 N°8215 - When PHP warning are enabled, Global Request doesn't work 2025-03-06 11:59:08 +01:00
denis.flaven@combodo.com
07b904ee1b N°8231 - making rest api logs more readable 2025-03-06 11:59:08 +01:00
10 changed files with 597 additions and 66 deletions

View File

@@ -1316,6 +1316,11 @@ interface iRestServiceProvider
public function ExecOperation($sVersion, $sVerb, $aParams);
}
interface iRestInputSanitizer
{
public function SanitizeJsonInput(string $sJsonInput): string;
}
/**
* Minimal REST response structure. Derive this structure to add response data and error codes.
*
@@ -1405,6 +1410,14 @@ class RestResult
* @api
*/
public $message;
/**
* Sanitize the content of this result to hide sensitive information
*/
public function SanitizeContent()
{
// The default implementation does nothing
}
}
/**

View File

@@ -138,7 +138,7 @@ abstract class AttributeDefinition
protected $aCSSClasses;
public function GetType()
public function GetType()
{
return Dict::S('Core:'.get_class($this));
}
@@ -3775,7 +3775,7 @@ class AttributeFinalClass extends AttributeString
*/
class AttributePassword extends AttributeString implements iAttributeNoGroupBy
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
/**
* Useless constructor, but if not present PHP 7.4.0/7.4.1 is crashing :( (N°2329)
@@ -3851,7 +3851,7 @@ class AttributePassword extends AttributeString implements iAttributeNoGroupBy
*/
class AttributeEncryptedString extends AttributeString implements iAttributeNoGroupBy
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
static $sKey = null; // Encryption key used for all encrypted fields
static $sLibrary = null; // Encryption library used for all encrypted fields
@@ -9243,7 +9243,7 @@ class AttributeSubItem extends AttributeDefinition
*/
class AttributeOneWayPassword extends AttributeDefinition implements iAttributeNoGroupBy
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
/**
* Useless constructor, but if not present PHP 7.4.0/7.4.1 is crashing :( (N°2329)

View File

@@ -34,7 +34,9 @@
*/
class ObjectResult
{
public $code;
use SanitizeTrait;
public $code;
public $message;
public $class;
public $key;
@@ -122,6 +124,19 @@ class ObjectResult
{
$this->fields[$sAttCode] = $this->MakeResultValue($oObject, $sAttCode, $bExtendedOutput);
}
public function SanitizeContent()
{
foreach($this->fields as $sFieldAttCode => $fieldValue)
{
try {
$oAttDef = MetaModel::GetAttributeDef($this->class, $sFieldAttCode);
} catch (Exception $e) { // for special cases like ID
continue;
}
$this->SanitizeFieldIfSensitive($this->fields, $sFieldAttCode, $fieldValue, $oAttDef);
}
}
}
@@ -181,6 +196,16 @@ class RestResultWithObjects extends RestResult
$sObjKey = get_class($oObject).'::'.$oObject->GetKey();
$this->objects[$sObjKey] = $oObjRes;
}
public function SanitizeContent()
{
parent::SanitizeContent();
foreach($this->objects as $sObjKey => $oObjRes)
{
$oObjRes->SanitizeContent();
}
}
}
class RestResultWithRelations extends RestResultWithObjects
@@ -247,9 +272,10 @@ class RestDelete
*
* @package Core
*/
class CoreServices implements iRestServiceProvider
class CoreServices implements iRestServiceProvider, iRestInputSanitizer
{
/**
use SanitizeTrait;
/**
* Enumerate services delivered by this class
*
* @param string $sVersion The version (e.g. 1.0) supported by the services
@@ -663,6 +689,33 @@ class CoreServices implements iRestServiceProvider
}
return $oResult;
}
public function SanitizeJsonInput(string $sJsonInput): string
{
$sSanitizedJsonInput = $sJsonInput;
$aJsonData = json_decode($sSanitizedJsonInput, true);
$sOperation = $aJsonData['operation'];
switch ($sOperation) {
case 'core/check_credentials':
if (isset($aJsonData['password'])) {
$aJsonData['password'] = '*****';
}
break;
case 'core/update':
case 'core/create':
default :
$sClass = $aJsonData['class'];
if (isset($aJsonData['fields'])) {
foreach ($aJsonData['fields'] as $sFieldAttCode => $fieldValue) {
$oAttDef = MetaModel::GetAttributeDef($sClass, $sFieldAttCode);
$this->SanitizeFieldIfSensitive($aJsonData['fields'], $sFieldAttCode, $fieldValue, $oAttDef);
}
}
break;
}
return json_encode($aJsonData, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
/**
* Helper for object deletion
@@ -802,3 +855,50 @@ class CoreServices implements iRestServiceProvider
return $iLimit * max(0, $iPage - 1);
}
}
trait SanitizeTrait
{
/**
* Sanitize a field if it is sensitive.
*
* @param array $fields The fields array
* @param string $sFieldAttCode The attribute code
* @param mixed $oAttDef The attribute definition
* @throws Exception
*/
private function SanitizeFieldIfSensitive(array &$fields, string $sFieldAttCode, $fieldValue, $oAttDef): void
{
// for simple attribute
if ($oAttDef instanceof iAttributeNoGroupBy) // iAttributeNoGroupBy is equivalent to sensitive attribute
{
$fields[$sFieldAttCode] = '*****';
return;
}
// for 1-n / n-n relation
if ($oAttDef instanceof AttributeLinkedSet) {
foreach ($fieldValue as $i => $aLnkValues) {
foreach ($aLnkValues as $sLnkAttCode => $sLnkValue) {
$oLnkAttDef = MetaModel::GetAttributeDef($oAttDef->GetLinkedClass(), $sLnkAttCode);
if ($oLnkAttDef instanceof iAttributeNoGroupBy) { // 1-n relation
$fields[$sFieldAttCode][$i][$sLnkAttCode] = '*****';
}
elseif ($oAttDef instanceof AttributeLinkedSetIndirect && $oLnkAttDef instanceof AttributeExternalField) { // for n-n relation
$oExtKeyAttDef = MetaModel::GetAttributeDef($oLnkAttDef->GetTargetClass(), $oLnkAttDef->GetExtAttCode());
if ($oExtKeyAttDef instanceof iAttributeNoGroupBy) {
$fields[$sFieldAttCode][$i][$sLnkAttCode] = '*****';
}
}
}
}
return;
}
// for external attribute
if ($oAttDef instanceof AttributeExternalField) {
$oExtKeyAttDef = MetaModel::GetAttributeDef($oAttDef->GetTargetClass(), $oAttDef->GetExtAttCode());
if ($oExtKeyAttDef instanceof iAttributeNoGroupBy) {
$fields[$sFieldAttCode] = '*****';
}
}
}
}

View File

@@ -1107,15 +1107,17 @@ class ObjectFormManager extends FormManager
// First we need to update the Object with its new values in order to enable the dependents fields to update
$sObjectClass = get_class($this->oObject);
foreach ($aCurrentValues as $sAttCode => $value) {
if (!array_key_exists($sAttCode, $this->aFieldsAtts)) {
continue;
}
$iAttributeFlags = $this->aFieldsAtts[$sAttCode];
if ($iAttributeFlags & OPT_ATT_HIDDEN) {
continue;
}
if ($iAttributeFlags & OPT_ATT_READONLY) {
continue;
if (count($this->aFieldsAtts) !== 0) {
if (!array_key_exists($sAttCode, $this->aFieldsAtts)) {
continue;
}
$iAttributeFlags = $this->aFieldsAtts[$sAttCode];
if ($iAttributeFlags & OPT_ATT_HIDDEN) {
continue;
}
if ($iAttributeFlags & OPT_ATT_READONLY) {
continue;
}
}
if (MetaModel::IsValidAttCode($sObjectClass, $sAttCode)) {
@@ -1230,55 +1232,56 @@ class ObjectFormManager extends FormManager
$this->aFieldsAtts = array();
$this->aExtraData = array();
$aFieldsDMOnlyAttCodes = array();
switch ($this->aFormProperties['type']) {
case 'custom_list':
case 'static':
foreach ($this->aFormProperties['fields'] as $sAttCode => $aOptions) {
// When in a transition and no flags are specified for the field, we will retrieve its flags from DM later
if ($this->IsTransitionForm() && empty($aOptions)) {
$aFieldsDMOnlyAttCodes[] = $sAttCode;
continue;
}
if (array_key_exists('type', $this->aFormProperties)) {
switch ($this->aFormProperties['type']) {
case 'custom_list':
case 'static':
foreach ($this->aFormProperties['fields'] as $sAttCode => $aOptions) {
// When in a transition and no flags are specified for the field, we will retrieve its flags from DM later
if ($this->IsTransitionForm() && empty($aOptions)) {
$aFieldsDMOnlyAttCodes[] = $sAttCode;
continue;
}
// Otherwise we proceed as usual
$iFieldFlags = OPT_ATT_NORMAL;
// Checking if field should be slave
if (isset($aOptions['slave']) && ($aOptions['slave'] === true)) {
$iFieldFlags = $iFieldFlags | OPT_ATT_SLAVE;
// Otherwise we proceed as usual
$iFieldFlags = OPT_ATT_NORMAL;
// Checking if field should be slave
if (isset($aOptions['slave']) && ($aOptions['slave'] === true)) {
$iFieldFlags = $iFieldFlags | OPT_ATT_SLAVE;
}
// Checking if field should be must_change
if (isset($aOptions['must_change']) && ($aOptions['must_change'] === true)) {
$iFieldFlags = $iFieldFlags | OPT_ATT_MUSTCHANGE;
}
// Checking if field should be must prompt
if (isset($aOptions['must_prompt']) && ($aOptions['must_prompt'] === true)) {
$iFieldFlags = $iFieldFlags | OPT_ATT_MUSTPROMPT;
}
// Checking if field should be hidden
if (isset($aOptions['hidden']) && ($aOptions['hidden'] === true)) {
$iFieldFlags = $iFieldFlags | OPT_ATT_HIDDEN;
}
// Checking if field should be readonly
if (isset($aOptions['read_only']) && ($aOptions['read_only'] === true)) {
$iFieldFlags = $iFieldFlags | OPT_ATT_READONLY;
}
// Checking if field should be mandatory
if (isset($aOptions['mandatory']) && ($aOptions['mandatory'] === true)) {
$iFieldFlags = $iFieldFlags | OPT_ATT_MANDATORY;
}
// Finally, adding the attribute and its flags
$this->aFieldsAtts[$sAttCode] = $iFieldFlags;
}
// Checking if field should be must_change
if (isset($aOptions['must_change']) && ($aOptions['must_change'] === true)) {
$iFieldFlags = $iFieldFlags | OPT_ATT_MUSTCHANGE;
}
// Checking if field should be must prompt
if (isset($aOptions['must_prompt']) && ($aOptions['must_prompt'] === true)) {
$iFieldFlags = $iFieldFlags | OPT_ATT_MUSTPROMPT;
}
// Checking if field should be hidden
if (isset($aOptions['hidden']) && ($aOptions['hidden'] === true)) {
$iFieldFlags = $iFieldFlags | OPT_ATT_HIDDEN;
}
// Checking if field should be readonly
if (isset($aOptions['read_only']) && ($aOptions['read_only'] === true)) {
$iFieldFlags = $iFieldFlags | OPT_ATT_READONLY;
}
// Checking if field should be mandatory
if (isset($aOptions['mandatory']) && ($aOptions['mandatory'] === true)) {
$iFieldFlags = $iFieldFlags | OPT_ATT_MANDATORY;
}
// Finally, adding the attribute and its flags
$this->aFieldsAtts[$sAttCode] = $iFieldFlags;
}
break;
break;
case 'zlist':
foreach (MetaModel::FlattenZList(MetaModel::GetZListItems($sObjectClass, $this->aFormProperties['fields'])) as $sAttCode) {
$this->aFieldsAtts[$sAttCode] = OPT_ATT_NORMAL;
}
break;
case 'zlist':
foreach (MetaModel::FlattenZList(MetaModel::GetZListItems($sObjectClass, $this->aFormProperties['fields'])) as $sAttCode) {
$this->aFieldsAtts[$sAttCode] = OPT_ATT_NORMAL;
}
break;
}
}
if ($this->aFormProperties['layout'] !== null) {
if (isset($this->aFormProperties['layout'])) {
$oXPath = new DOMXPath($this->oHtmlDocument);
/** @var \DOMElement $oFieldNode */
foreach ($oXPath->query('//div[contains(@class, "form_field")][@data-field-id]') as $oFieldNode) {
@@ -1337,7 +1340,7 @@ class ObjectFormManager extends FormManager
// Also, retrieving mandatory attributes from metamodel to be able to complete the form with them if necessary
//
// Note: When in a transition, we don't do this for fields that should be set from DM
if ($this->aFormProperties['type'] !== 'static') {
if (array_key_exists('type', $this->aFormProperties) && $this->aFormProperties['type'] !== 'static') {
if ($this->IsTransitionForm()) {
$aDatamodelAttCodes = $this->oObject->GetTransitionAttributes($this->aFormProperties['stimulus_code']);
}
@@ -1448,7 +1451,7 @@ class ObjectFormManager extends FormManager
// - The layout
$this->oHtmlDocument = new DOMDocument();
if ($this->aFormProperties['layout'] !== null) {
if (isset($this->aFormProperties['layout'])) {
// Checking if we need to render the template from twig to html in order to parse the fields
if ($this->aFormProperties['layout']['type'] === 'twig') {
if ($this->oContainer !== null) {

View File

@@ -1218,6 +1218,7 @@ return array(
'SQLQuery' => $baseDir . '/core/sqlquery.class.inc.php',
'SQLUnionQuery' => $baseDir . '/core/sqlunionquery.class.inc.php',
'SVGDOMSanitizer' => $baseDir . '/core/htmlsanitizer.class.inc.php',
'SanitizeTrait' => $baseDir . '/core/restservices.class.inc.php',
'ScalarExpression' => $baseDir . '/core/oql/expression.class.inc.php',
'ScalarOqlExpression' => $baseDir . '/core/oql/oqlquery.class.inc.php',
'ScssPhp\\ScssPhp\\Base\\Range' => $vendorDir . '/scssphp/scssphp/src/Base/Range.php',
@@ -2732,6 +2733,7 @@ return array(
'iPreferencesExtension' => $baseDir . '/application/applicationextension.inc.php',
'iProcess' => $baseDir . '/core/backgroundprocess.inc.php',
'iQueryModifier' => $baseDir . '/core/querymodifier.class.inc.php',
'iRestInputSanitizer' => $baseDir . '/application/applicationextension.inc.php',
'iRestServiceProvider' => $baseDir . '/application/applicationextension.inc.php',
'iScheduledProcess' => $baseDir . '/core/backgroundprocess.inc.php',
'iSelfRegister' => $baseDir . '/core/userrights.class.inc.php',

View File

@@ -1586,6 +1586,7 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b
'SQLQuery' => __DIR__ . '/../..' . '/core/sqlquery.class.inc.php',
'SQLUnionQuery' => __DIR__ . '/../..' . '/core/sqlunionquery.class.inc.php',
'SVGDOMSanitizer' => __DIR__ . '/../..' . '/core/htmlsanitizer.class.inc.php',
'SanitizeTrait' => __DIR__ . '/../..' . '/core/restservices.class.inc.php',
'ScalarExpression' => __DIR__ . '/../..' . '/core/oql/expression.class.inc.php',
'ScalarOqlExpression' => __DIR__ . '/../..' . '/core/oql/oqlquery.class.inc.php',
'ScssPhp\\ScssPhp\\Base\\Range' => __DIR__ . '/..' . '/scssphp/scssphp/src/Base/Range.php',
@@ -3100,6 +3101,7 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b
'iPreferencesExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
'iProcess' => __DIR__ . '/../..' . '/core/backgroundprocess.inc.php',
'iQueryModifier' => __DIR__ . '/../..' . '/core/querymodifier.class.inc.php',
'iRestInputSanitizer' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
'iRestServiceProvider' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
'iScheduledProcess' => __DIR__ . '/../..' . '/core/backgroundprocess.inc.php',
'iSelfRegister' => __DIR__ . '/../..' . '/core/userrights.class.inc.php',

View File

@@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.7">
<classes>
<class id="TestServer" _delta="define">
<parent>cmdbAbstractObject</parent>
<properties>
<category>bizmodel</category>
<abstract>false</abstract>
<key_type>autoincrement</key_type>
<db_table>test_server</db_table>
<db_key_field>id</db_key_field>
</properties>
<presentation/>
<methods/>
<fields>
<field id="contact_list" xsi:type="AttributeLinkedSetIndirect">
<linked_class>lnkContactTestToServer</linked_class>
<ext_key_to_me>test_server_id</ext_key_to_me>
<ext_key_to_remote>contact_test_id</ext_key_to_remote>
<is_null_allowed>true</is_null_allowed>
</field>
<field id="password_list" xsi:type="AttributeLinkedSet">
<linked_class>PasswordTest</linked_class>
<ext_key_to_me>server_test_id</ext_key_to_me>
<is_null_allowed>true</is_null_allowed>
</field>
<field id="name" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
</field>
</fields>
</class>
<class id="ContactTest" _delta="define">
<parent>cmdbAbstractObject</parent>
<properties>
<category>bizmodel</category>
<abstract>false</abstract>
<key_type>autoincrement</key_type>
<db_table>contact_test</db_table>
<db_key_field>id</db_key_field>
</properties>
<presentation/>
<methods/>
<fields>
<field id="password" xsi:type="AttributeEncryptedString">
<sql>password</sql>
</field>
<field id="server_test_list" xsi:type="AttributeLinkedSetIndirect">
<linked_class>lnkContactTestToServer</linked_class>
<ext_key_to_me>contact_test_id</ext_key_to_me>
<ext_key_to_remote>test_server_id</ext_key_to_remote>
<is_null_allowed>true</is_null_allowed>
</field>
</fields>
</class>
<class id="lnkContactTestToServer" _delta="define">
<parent>cmdbAbstractObject</parent>
<properties>
<category>bizmodel</category>
<abstract>false</abstract>
<key_type>autoincrement</key_type>
<db_table>lnk_contact_server_test</db_table>
<db_key_field>id</db_key_field>
</properties>
<presentation/>
<methods/>
<fields>
<field id="contact_test_password" xsi:type="AttributeExternalField" _delta="define">
<extkey_attcode>contact_test_id</extkey_attcode>
<target_attcode>password</target_attcode>
</field>
<field id="test_server_id" xsi:type="AttributeExternalKey" _delta="define">
<target_class>TestServer</target_class>
<on_target_delete>DEL_MANUAL</on_target_delete>
<sql>test_server</sql>
<is_null_allowed>false</is_null_allowed>
</field>
<field id="contact_test_id" xsi:type="AttributeExternalKey" _delta="define">
<target_class>ContactTest</target_class>
<on_target_delete>DEL_MANUAL</on_target_delete>
<sql>contact_test</sql>
<is_null_allowed>false</is_null_allowed>
</field>
</fields>
</class>
<class id="PasswordTest" _delta="define">
<parent>cmdbAbstractObject</parent>
<properties>
<category>bizmodel</category>
<abstract>false</abstract>
<key_type>autoincrement</key_type>
<db_table>password_test</db_table>
<db_key_field>id</db_key_field>
</properties>
<presentation/>
<methods/>
<fields>
<field id="server_test_id" xsi:type="AttributeExternalKey" _delta="define">
<target_class>TestServer</target_class>
<sql>server_test_id</sql>
<on_target_delete>DEL_MANUAL</on_target_delete>
</field>
<field id="password" xsi:type="AttributeEncryptedString" _delta="define">
<sql>password</sql>
</field>
</fields>
</class>
</classes>
</itop_design>

View File

@@ -0,0 +1,164 @@
<?php
declare(strict_types=1);
namespace Combodo\iTop\Test\UnitTest\Core;
use ArchivedObjectException;
use AttributeEncryptedString;
use Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase;
use CoreException;
use CoreUnexpectedValue;
use Exception;
use MetaModel;
use ormLinkSet;
use PasswordTest;
use RestResultWithObjects;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class RestServicesSanitizeOutputTest extends ItopCustomDatamodelTestCase
{
private const SIMPLE_PASSWORD = '123456';
/**
* @throws Exception
*/
protected function setUp(): void
{
parent::setUp();
// Workaround to cope with inconsistent settings in itop-config files from the CI
AttributeEncryptedString::$sKey = '6eb9d9afa3ee0fbcebe622a33bf57aaeafb7c37998fd24c403c2522c2d60117f';
}
/**
* @return void
* @throws CoreException
*/
public function testSanitizeAttributeOnRequestedObject()
{
$oContactTest = MetaModel::NewObject('ContactTest', [
'password' => self::SIMPLE_PASSWORD]
);
$oRestResultWithObject = new RestResultWithObjects();
$oRestResultWithObject->AddObject(0, 'ok', $oContactTest, ['ContactTest' => ['password']]);
$oRestResultWithObject->SanitizeContent();
static::assertEquals(
'{"objects":{"ContactTest::-1":{"code":0,"message":"ok","class":"ContactTest","key":-1,"fields":{"password":"*****"}}},"code":0,"message":null}',
json_encode($oRestResultWithObject));
}
/**
* @return void
* @throws Exception
*/
public function testSanitizeAttributeExternalFieldOnLink()
{
$oContactTest = $this->createObject('ContactTest', [
'password' => self::SIMPLE_PASSWORD]
);
$oTestServer = $this->createObject('TestServer', [
'name' => 'test_server',
]);
// create lnkContactTestToServer
$oLnkContactTestToServer = $this->createObject('lnkContactTestToServer', [
'contact_test_id' => $oContactTest->GetKey(),
'test_server_id' => $oTestServer->GetKey()
]);
$oRestResultWithObject = new RestResultWithObjects();
$oRestResultWithObject->AddObject(0, 'ok', $oLnkContactTestToServer,
['lnkContactTestToServer' => ['contact_test_password']]);
$oRestResultWithObject->SanitizeContent();
static::assertContains(
'*****',
json_encode($oRestResultWithObject));
static::assertNotContains(
self::SIMPLE_PASSWORD,
json_encode($oRestResultWithObject));
}
/**
* @throws Exception
*/
public function testSanitizeAttributeOnObjectRelatedThroughNNRelation()
{
$oContactTest = $this->createObject('ContactTest', [
'password' => self::SIMPLE_PASSWORD]);
$oTestServer = $this->createObject('TestServer', [
'name' => 'test_server',
]);
// create lnkContactTestToServer
$this->createObject('lnkContactTestToServer', [
'contact_test_id' => $oContactTest->GetKey(),
'test_server_id' => $oTestServer->GetKey()
]);
$oRestResultWithObject = new RestResultWithObjects();
$oRestResultWithObject->AddObject(0, 'ok', $oTestServer,
['TestServer' => ['contact_list']]);
$oRestResultWithObject->SanitizeContent();
static::assertContains(
'*****',
json_encode($oRestResultWithObject));
static::assertNotContains(
self::SIMPLE_PASSWORD,
json_encode($oRestResultWithObject));
}
/**
* @throws CoreException
* @throws CoreUnexpectedValue
* @throws ArchivedObjectException
* @throws Exception
*/
public function testSanitizeOnObjectRelatedThrough1NRelation()
{
$oTestServer = $this->createObject('TestServer', [
'name' => 'my_server',
]);
$oPassword = new PasswordTest();
$oPassword->Set('password', self::SIMPLE_PASSWORD);
$oPassword->Set('server_test_id', $oTestServer->GetKey());
/** @var ormLinkSet $oContactList */
$oContactList = $oTestServer->Get('password_list');
$oContactList->AddItem($oPassword);
$oTestServer->Set('password_list', $oContactList);
$oRestResultWithObject = new RestResultWithObjects();
$oRestResultWithObject->AddObject(0, 'ok', $oTestServer, ['TestServer' => ['id', 'password_list']]);
$oRestResultWithObject->SanitizeContent();
static::assertContains(
'*****',
json_encode($oRestResultWithObject));
static::assertNotContains(
self::SIMPLE_PASSWORD,
json_encode($oRestResultWithObject));
}
/**
* @return string Abs path to the XML delta to use for the tests of that class
*/
public function GetDatamodelDeltaAbsPath(): string
{
return __DIR__ . '/Delta/delta_test_sanitize_output.xml';
}
}

View File

@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace Combodo\iTop\Test\UnitTest\Core;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use CoreException;
use CoreServices;
use CoreUnexpectedValue;
use RestResultWithObjects;
use UserLocal;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class RestServicesTest extends ItopDataTestCase
{
/**
* @return void
* @dataProvider providerTestSanitizeJsonInput
*/
public function testSanitizeJsonInput($sJsonData, $sExpectedJsonDataSanitized)
{
$oRS = new CoreServices();
$sOutputJson = $oRS->SanitizeJsonInput($sJsonData);
static::assertEquals($sExpectedJsonDataSanitized, $sOutputJson);
}
/**
* @return array[]
*/
public function providerTestSanitizeJsonInput(): array
{
return [
'core/check_credentials' => [
'{"operation": "core/check_credentials", "user": "admin", "password": "admin"}',
'{
"operation": "core/check_credentials",
"user": "admin",
"password": "*****"
}'
],
'core/update' => [
'{"operation": "core/update", "comment": "Update user", "class": "UserLocal", "key": {"id":1}, "output_fields": "first_name, password", "fields": {"password" : "123456"}}',
'{
"operation": "core/update",
"comment": "Update user",
"class": "UserLocal",
"key": {
"id": 1
},
"output_fields": "first_name, password",
"fields": {
"password": "*****"
}
}'
],
'core/create' => [
'{"operation": "core/create", "comment": "Create user", "class": "UserLocal", "fields": {"first_name": "John", "last_name": "Doe", "email": "jd@example/com", "password" : "123456"}}',
'{
"operation": "core/create",
"comment": "Create user",
"class": "UserLocal",
"fields": {
"first_name": "John",
"last_name": "Doe",
"email": "jd@example/com",
"password": "*****"
}
}'
],
];
}
/**
* @param $sOperation
* @param $aJsonData
* @param $sExpectedJsonDataSanitized
* @return void
* @throws CoreException
* @throws CoreUnexpectedValue
* @dataProvider providerTestSanitizeJsonOutput
*/
public function testSanitizeJsonOutput($sOperation, $aJsonData, $sExpectedJsonDataSanitized)
{
$oUser = new UserLocal();
$oUser->Set('password', '123456');
$oRestResultWithObject = new RestResultWithObjects();
$oRestResultWithObject->AddObject(0, 'ok', $oUser, ['UserLocal' => ['login', 'password']]);
$oRestResultWithObject->SanitizeContent();
static::assertEquals($sExpectedJsonDataSanitized, json_encode($oRestResultWithObject));
}
/**
* @return array[]
*/
public function providerTestSanitizeJsonOutput(): array
{
return [
'core/update' => [
'core/update',
['comment' => 'Update user', 'class' => 'UserLocal', 'key' => ['login' => 'my_example'], 'output_fields' => 'password', 'fields' => ['password' => 'opkB!req57']],
'{"objects":{"UserLocal::-1":{"code":0,"message":"ok","class":"UserLocal","key":-1,"fields":{"login":"","password":"*****"}}},"code":0,"message":null}'
],
'core/create' => [
'core/create',
['comment' => 'Create user', 'class' => 'UserLocal', 'fields' => ['password' => 'Azertyuiiop*12', 'login' => 'toto', 'profile_list' => [1]]],
'{"objects":{"UserLocal::-1":{"code":0,"message":"ok","class":"UserLocal","key":-1,"fields":{"login":"","password":"*****"}}},"code":0,"message":null}'
],
'core/get' => [
'core/get',
['comment' => 'Get user', 'class' => 'UserLocal', 'key' => ['login' => 'my_example'], 'output_fields' => 'first_name, password'],
'{"objects":{"UserLocal::-1":{"code":0,"message":"ok","class":"UserLocal","key":-1,"fields":{"login":"","password":"*****"}}},"code":0,"message":null}'
],
'core/check_credentials' => [
'core/check_credentials',
['user' => 'admin', 'password' => 'admin'],
'{"objects":{"UserLocal::-1":{"code":0,"message":"ok","class":"UserLocal","key":-1,"fields":{"login":"","password":"*****"}}},"code":0,"message":null}'
],
];
}
}

View File

@@ -209,7 +209,11 @@ try
/** @var iRestServiceProvider $oRS */
$oRS = $aOpToRestService[$sOperation]['service_provider'];
$sProvider = get_class($oRS);
if ($oRS instanceof iRestInputSanitizer) {
$sSanitizedJsonInput = $oRS->SanitizeJsonInput($sJsonString);
}
CMDBObject::SetTrackOrigin('webservice-rest');
$oResult = $oRS->ExecOperation($sVersion, $sOperation, $aJsonData);
}
@@ -234,6 +238,7 @@ catch(Exception $e)
//
$sResponse = json_encode($oResult);
if ($sResponse === false)
{
$oJsonIssue = new RestResult();
@@ -267,7 +272,7 @@ if (MetaModel::GetConfig()->Get('log_rest_service'))
$oLog->SetTrim('userinfo', UserRights::GetUser());
$oLog->Set('version', $sVersion);
$oLog->Set('operation', $sOperation);
$oLog->SetTrim('json_input', $sJsonString);
$oLog->SetTrim('json_input', $sSanitizedJsonInput ?? $sJsonString);
$oLog->Set('provider', $sProvider);
$sMessage = $oResult->message;
@@ -277,7 +282,8 @@ if (MetaModel::GetConfig()->Get('log_rest_service'))
}
$oLog->SetTrim('message', $sMessage);
$oLog->Set('code', $oResult->code);
$oLog->SetTrim('json_output', $sResponse);
$oResult->SanitizeContent();
$oLog->SetTrim('json_output', json_encode($oResult, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
$oLog->DBInsertNoReload();
$oKPI->ComputeAndReport('Log inserted');