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; }