diff --git a/application/loginwebpage.class.inc.php b/application/loginwebpage.class.inc.php
index bec2565b5..17b2166a2 100644
--- a/application/loginwebpage.class.inc.php
+++ b/application/loginwebpage.class.inc.php
@@ -1044,6 +1044,33 @@ class LoginWebPage extends NiceWebPage
exit;
}
}
+
+ // Check allowed context for the selected user
+ // If the user has no context -> allow to connect
+ // If the entry point has no context -> allow only users with no context specified
+ // If the entry point has one or more contexts -> allow only users with one of the entry point context specified
+ $oUser = UserRights::GetUserObject();
+ $aContexts = $oUser->Get('allowed_contexts')->GetValues();
+ if (count($aContexts) > 0) {
+ if (ContextTag::Check($aContexts) === false) {
+ IssueLog::Error(sprintf(
+ 'User "%s" cannot connect: the current context (%s) does not match current allowed contexts: %s',
+ UserRights::GetUser(),
+ implode(',', \ContextTag::GetStack()),
+ implode(',', $aContexts)));
+ if ($iOnExit == self::EXIT_RETURN) {
+ return self::EXIT_CODE_NOTAUTHORIZED;
+ } else {
+ require_once(APPROOT.'/setup/setuppage.class.inc.php');
+ $oP = new ErrorPage(Dict::S('UI:PageTitle:FatalError'));
+ $oP->add('
'.Dict::S('UI:Login:Error:AccessRestricted')."
\n");
+ $oP->p("".Dict::S('UI:LogOffMenu').'');
+ $oP->output();
+ exit;
+ }
+ }
+ }
+
$iRet = call_user_func(array(self::$sHandlerClass, 'ChangeLocation'), $sRequestedPortalId, $iOnExit);
}
if ($iOnExit == self::EXIT_RETURN)
diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php
index a0e371e3f..5ffc43446 100644
--- a/core/attributedef.class.inc.php
+++ b/core/attributedef.class.inc.php
@@ -11084,7 +11084,7 @@ class AttributeEnumSet extends AttributeSet
return max(255, $iMaxSize);
}
- private function GetRawPossibleValues($aArgs = array(), $sContains = '')
+ protected function GetRawPossibleValues($aArgs = array(), $sContains = '')
{
/** @var ValueSetEnumPadded $oValSetDef */
$oValSetDef = $this->Get('possible_values');
@@ -11317,6 +11317,65 @@ class AttributeEnumSet extends AttributeSet
}
}
+/**
+ * @since 3.2.0 N°7423
+ */
+class AttributeContextTagSet extends AttributeEnumSet
+{
+ public static function ListExpectedParams()
+ {
+ // allowed_values and possible_values are replaced by context_type and excluded_contexts
+ return array_diff(
+ array_merge(parent::ListExpectedParams(), ['is_null_allowed', 'max_items', 'context_type', 'denied_contexts']),
+ ['allowed_values', 'possible_values']);
+ }
+
+ protected function GetRawPossibleValues($aArgs = array(), $sContains = ''): array
+ {
+ $sType = $this->Get('context_type');
+ $aExcludedContexts = $this->Get('denied_contexts');
+ $aContexts = [];
+ switch ($sType) {
+ case 'authentication':
+ $aContexts = ContextTag::GetTagsForConnection();
+ break;
+
+ case 'all':
+ $aContexts = ContextTag::GetTags();
+ break;
+ }
+
+ $aContexts = array_diff($aContexts, $aExcludedContexts);
+ $oValSetDef = new ValueSetEnumPadded($aContexts);
+
+ return $oValSetDef->GetValues([], $sContains);
+ }
+
+ public function GetPossibleValues($aArgs = array(), $sContains = '')
+ {
+ return $this->GetRawPossibleValues($aArgs, $sContains);
+ }
+
+ public function GetValueLabel($sValue)
+ {
+ if ($sValue instanceof ormSet) {
+ $sValue = implode(', ', $sValue->GetValues());
+ }
+
+ $aValues = $this->GetRawPossibleValues();
+ $sLabel = Dict::S('Enum:Undefined');
+ if (is_string($sValue) && isset($aValues[$sValue])) {
+ $sLabel = $aValues[$sValue];
+ }
+
+ return $sLabel;
+ }
+
+ public function GetValueDescription($sValue)
+ {
+ return '';
+ }
+}
class AttributeClassAttCodeSet extends AttributeSet
{
diff --git a/core/contexttag.class.inc.php b/core/contexttag.class.inc.php
index bd15600bb..8504c5e3f 100644
--- a/core/contexttag.class.inc.php
+++ b/core/contexttag.class.inc.php
@@ -58,12 +58,15 @@ class ContextTag
public const TAG_SETUP = 'Setup';
public const TAG_SYNCHRO = 'Synchro';
public const TAG_REST = 'REST/JSON';
-
+ /**
+ * @since 3.2.0 N°7423
+ */
+ public const TAG_GUI = 'GUI';
/**
* @since 3.1.0 N°6047
*/
public const TAG_IMPORT = 'Import';
- /**
+ /**
* @since 3.1.0 N°6047
*/
public const TAG_EXPORT = 'Export';
@@ -101,11 +104,18 @@ class ContextTag
/**
* Check if a given tag is present in the stack
- * @param string $sTag
+ * or check if one of the tags in the array is present
+ *
+ * @param array|string $sTag
+ *
* @return bool
*/
- public static function Check($sTag)
+ public static function Check(array|string $sTag): bool
{
+ if (is_array($sTag)) {
+ return (count(array_intersect($sTag, static::$aStack)) > 0);
+ }
+
return in_array($sTag, static::$aStack);
}
@@ -118,6 +128,25 @@ class ContextTag
return static::$aStack;
}
+ public static function GetTagsForConnection(): array
+ {
+ $aRawTags = array(
+ ContextTag::TAG_GUI,
+ ContextTag::TAG_REST,
+ ContextTag::TAG_SYNCHRO,
+ ContextTag::TAG_IMPORT,
+ ContextTag::TAG_EXPORT);
+
+ $aTags = array();
+
+ foreach ($aRawTags as $sRawTag)
+ {
+ $aTags[$sRawTag] = Dict::S("Core:Context={$sRawTag}");
+ }
+
+ return $aTags;
+ }
+
/**
* Get all the predefined context tags
* @return array
@@ -125,11 +154,14 @@ class ContextTag
public static function GetTags()
{
$aRawTags = array(
+ ContextTag::TAG_GUI,
ContextTag::TAG_REST,
ContextTag::TAG_SYNCHRO,
ContextTag::TAG_SETUP,
ContextTag::TAG_CONSOLE,
ContextTag::TAG_CRON,
+ ContextTag::TAG_IMPORT,
+ ContextTag::TAG_EXPORT,
ContextTag::TAG_PORTAL);
$aTags = array();
diff --git a/core/datamodel.core.xml b/core/datamodel.core.xml
index 7b953e1e5..64fbbdaf2 100644
--- a/core/datamodel.core.xml
+++ b/core/datamodel.core.xml
@@ -808,6 +808,29 @@
+
+
+
+
+
+
+
+
+ context_type
+ true
+ string
+
+
+ denied_contexts
+ false
+ collection
+ denied_context
+ id
+
+
+
+
+
@@ -830,11 +853,6 @@
true
string
-
- sql
- true
- string
-
class_attcode
true
diff --git a/core/userrights.class.inc.php b/core/userrights.class.inc.php
index 023cdf1bd..d182f7d52 100644
--- a/core/userrights.class.inc.php
+++ b/core/userrights.class.inc.php
@@ -252,6 +252,8 @@ abstract class User extends cmdbAbstractObject
MetaModel::Init_AddAttribute(new AttributeLinkedSetIndirect("profile_list",array("linked_class" => "URP_UserProfile", "ext_key_to_me" => "userid", "ext_key_to_remote" => "profileid", "allowed_values" => null, "count_min" => 1, "count_max" => 0, "depends_on" => array(), "display_style" => 'property', "with_php_constraint" => true, "with_php_computation" => true)));
MetaModel::Init_AddAttribute(new AttributeLinkedSetIndirect("allowed_org_list", array("linked_class" => "URP_UserOrg", "ext_key_to_me" => "userid", "ext_key_to_remote" => "allowed_org_id", "allowed_values" => null, "count_min" => 1, "count_max" => 0, "depends_on" => array(), 'with_php_constraint' => true)));
MetaModel::Init_AddAttribute(new AttributeCaseLog("log", array("sql" => 'log', "is_null_allowed" => true, "default_value" => '', "allowed_values" => null, "depends_on" => array(), "always_load_in_tables" => false)));
+ $aTags = ContextTag::GetTagsForConnection();
+ MetaModel::Init_AddAttribute(new AttributeEnumSet('allowed_contexts', array('allowed_values' => null, 'possible_values' => new ValueSetEnumPadded($aTags, true), 'sql' => 'allowed_contexts', 'depends_on' => array(), 'is_null_allowed' => true, 'max_items' => 12)));
// Display lists
MetaModel::Init_SetZListItems('details', array('contactid', 'org_id', 'email', 'login', 'language', 'status', 'profile_list', 'allowed_org_list', 'log')); // Unused as it's an abstract class !
diff --git a/core/valuesetdef.class.inc.php b/core/valuesetdef.class.inc.php
index d6600c0f9..b4f5c6a60 100644
--- a/core/valuesetdef.class.inc.php
+++ b/core/valuesetdef.class.inc.php
@@ -580,6 +580,7 @@ class ValueSetEnumPadded extends ValueSetEnum
$aPaddedValues[$sKey] = $sVal;
}
$this->m_values = $aPaddedValues;
+ $this->m_bIsLoaded = true;
}
}
diff --git a/dictionaries/en.dictionary.itop.core.php b/dictionaries/en.dictionary.itop.core.php
index 565680f59..494239871 100644
--- a/dictionaries/en.dictionary.itop.core.php
+++ b/dictionaries/en.dictionary.itop.core.php
@@ -209,12 +209,15 @@ Operators:
'Core:AttributeTag' => 'Tags',
'Core:AttributeTag+' => '',
- 'Core:Context=REST/JSON' => 'REST',
+ 'Core:Context=REST/JSON' => 'Webservice',
'Core:Context=Synchro' => 'Synchro',
'Core:Context=Setup' => 'Setup',
'Core:Context=GUI:Console' => 'Console',
- 'Core:Context=CRON' => 'cron',
+ 'Core:Context=CRON' => 'Background tasks',
'Core:Context=GUI:Portal' => 'Portal',
+ 'Core:Context=GUI' => 'GUI',
+ 'Core:Context=Import' => 'Import',
+ 'Core:Context=Export' => 'Export',
));
diff --git a/dictionaries/en.dictionary.itop.ui.php b/dictionaries/en.dictionary.itop.ui.php
index a4e9ec6c3..f914dce55 100644
--- a/dictionaries/en.dictionary.itop.ui.php
+++ b/dictionaries/en.dictionary.itop.ui.php
@@ -175,6 +175,8 @@ Dict::Add('EN US', 'English', 'English', array(
'Class:User/Attribute:status+' => 'Whether the user account is enabled or disabled.',
'Class:User/Attribute:status/Value:enabled' => 'Enabled',
'Class:User/Attribute:status/Value:disabled' => 'Disabled',
+ 'Class:User/Attribute:allowed_contexts' => 'Allowed authentication contexts',
+ 'Class:User/Attribute:allowed_contexts+' => 'List of authentication contexts that the user is allowed to access',
'Class:User/Error:LoginMustBeUnique' => 'Login must be unique - "%1$s" is already being used.',
'Class:User/Error:AtLeastOneProfileIsNeeded' => 'At least one profile must be assigned to this user.',
diff --git a/dictionaries/fr.dictionary.itop.core.php b/dictionaries/fr.dictionary.itop.core.php
index 11efcce46..566a38714 100644
--- a/dictionaries/fr.dictionary.itop.core.php
+++ b/dictionaries/fr.dictionary.itop.core.php
@@ -161,12 +161,15 @@ Opérateurs :
'Core:FriendlyName-Description' => 'Nom complet',
'Core:AttributeTag' => 'Taxon',
'Core:AttributeTag+' => '',
- 'Core:Context=REST/JSON' => 'REST',
+ 'Core:Context=REST/JSON' => 'Webservice',
'Core:Context=Synchro' => 'Synchro',
'Core:Context=Setup' => 'Setup',
'Core:Context=GUI:Console' => 'Console',
- 'Core:Context=CRON' => 'cron',
+ 'Core:Context=CRON' => 'Tâche de fond',
'Core:Context=GUI:Portal' => 'Portal',
+ 'Core:Context=GUI' => 'Interface graphique',
+ 'Core:Context=Import' => 'Import',
+ 'Core:Context=Export' => 'Export',
));
diff --git a/dictionaries/fr.dictionary.itop.ui.php b/dictionaries/fr.dictionary.itop.ui.php
index 9bdf41275..0c50b46c3 100644
--- a/dictionaries/fr.dictionary.itop.ui.php
+++ b/dictionaries/fr.dictionary.itop.ui.php
@@ -160,6 +160,8 @@ Dict::Add('FR FR', 'French', 'Français', array(
'Class:User/Attribute:status+' => 'Est-ce que ce compte utilisateur est actif, ou non?',
'Class:User/Attribute:status/Value:enabled' => 'Actif',
'Class:User/Attribute:status/Value:disabled' => 'Désactivé',
+ 'Class:User/Attribute:allowed_contexts' => 'Contextes de connexion autorisés',
+ 'Class:User/Attribute:allowed_contexts+' => 'Liste des contextes de connexion autorisés pour cet utilisateur',
'Class:User/Error:LoginMustBeUnique' => 'Le login doit être unique - "%1s" est déjà utilisé.',
'Class:User/Error:AtLeastOneProfileIsNeeded' => 'L\'utilisateur doit avoir au moins un profil.',
'Class:User/Error:ProfileNotAllowed' => 'Le profil "%1$s" ne peux pas être ajouté à son propre utilisateur, il interdit l\'accès à la console',
diff --git a/pages/UI.php b/pages/UI.php
index 953d958ee..049d4a359 100644
--- a/pages/UI.php
+++ b/pages/UI.php
@@ -314,6 +314,8 @@ try
$oKPI = new ExecutionKPI();
+ $oContextGUI = new ContextTag(ContextTag::TAG_GUI);
+
require_once(APPROOT.'/application/loginwebpage.class.inc.php');
$sLoginMessage = LoginWebPage::DoLogin(); // Check user rights and prompt if needed
$oAppContext = new ApplicationContext();
diff --git a/setup/compiler.class.inc.php b/setup/compiler.class.inc.php
index d1a6ac0a9..e71610781 100644
--- a/setup/compiler.class.inc.php
+++ b/setup/compiler.class.inc.php
@@ -2421,14 +2421,14 @@ EOF
/**
* @param string $sPropertyName
* @param array $aProperty
- * @param \DOMElement $oField
+ * @param DesignElement $oField
* @param array $aParameters
*
* @return array
* @throws \DOMFormatException
* @since 3.1.0 N°6040
*/
- protected function CompileDynamicProperty(string $sPropertyName, array $aProperty, DOMElement $oField, array $aParameters): array
+ protected function CompileDynamicProperty(string $sPropertyName, array $aProperty, DesignElement $oField, array $aParameters): array
{
$sPHPParam = $aProperty['php_param'] ?? $sPropertyName;
$bMandatory = $aProperty['mandatory'] ?? false;
@@ -2471,6 +2471,34 @@ EOF
$aParameters[$sPHPParam] = 'null';
}
break;
+ case 'collection':
+ $sCollectionElementName = $aProperty['collection_element_name'] ?? null;
+ $sCollectionType = $aProperty['collection_type'] ?? null;
+ if (is_null($sCollectionElementName) || is_null($sCollectionType)) {
+ throw new DOMFormatException("Missing 'collection_element_name' or 'collection_type' in collection definition in meta", null, null, $oField);
+ }
+ $oValues = $oField->GetOptionalElement($sPropertyName);
+ if ($oValues) {
+ $oValuesNodes = $oValues->getElementsByTagName($sCollectionElementName);
+ $aValues = [];
+ /** @var \MFElement $oValueNode */
+ foreach ($oValuesNodes as $oValueNode) {
+ switch ($sCollectionType) {
+ case 'id':
+ $aValues[] = $oValueNode->GetAttribute('id');
+ break;
+ case 'text':
+ $aValues[] = $oValueNode->textContent;
+ break;
+ }
+ }
+ $aParameters[$sPHPParam] = var_export($aValues, true);
+ } elseif ($bMandatory) {
+ $aParameters[$sPHPParam] = var_export([$sDefault], true);
+ } else {
+ $aParameters[$sPHPParam] = 'null';
+ }
+ break;
case 'null':
$aParameters[$sPHPParam] = 'null';
break;
@@ -3995,6 +4023,10 @@ PHP;
$aDefinition['default'] = $oNode->GetText();
}
}
+ if (($aDefinition['type'] ?? null) === 'collection') {
+ $aDefinition['collection_element_name'] = $oProperty->GetChildText('collection_element_name');
+ $aDefinition['collection_type'] = $oProperty->GetChildText('collection_type');
+ }
return $aDefinition;
}