1<?php
2// Copyright (C) 2010-2017 Combodo SARL
3//
4//   This file is part of iTop.
5//
6//   iTop is free software; you can redistribute it and/or modify
7//   it under the terms of the GNU Affero General Public License as published by
8//   the Free Software Foundation, either version 3 of the License, or
9//   (at your option) any later version.
10//
11//   iTop is distributed in the hope that it will be useful,
12//   but WITHOUT ANY WARRANTY; without even the implied warranty of
13//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14//   GNU Affero General Public License for more details.
15//
16//   You should have received a copy of the GNU Affero General Public License
17//   along with iTop. If not, see <http://www.gnu.org/licenses/>
18
19
20/**
21 * User rights management API
22 *
23 * @copyright   Copyright (C) 2010-2017 Combodo SARL
24 * @license     http://opensource.org/licenses/AGPL-3.0
25 */
26
27
28class UserRightException extends CoreException
29{
30}
31
32
33define('UR_ALLOWED_NO', 0);
34define('UR_ALLOWED_YES', 1);
35define('UR_ALLOWED_DEPENDS', 2);
36
37define('UR_ACTION_READ', 1); // View an object
38define('UR_ACTION_MODIFY', 2); // Create/modify an object/attribute
39define('UR_ACTION_DELETE', 3); // Delete an object
40
41define('UR_ACTION_BULK_READ', 4); // Export multiple objects
42define('UR_ACTION_BULK_MODIFY', 5); // Create/modify multiple objects
43define('UR_ACTION_BULK_DELETE', 6); // Delete multiple objects
44
45define('UR_ACTION_CREATE', 7); // Instantiate an object
46
47define('UR_ACTION_APPLICATION_DEFINED', 10000); // Application specific actions (CSV import, View schema...)
48
49/**
50 * User management module API
51 *
52 * @package     iTopORM
53 */
54abstract class UserRightsAddOnAPI
55{
56	abstract public function CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage = 'EN US'); // could be used during initial installation
57
58	abstract public function Init(); // loads data (possible optimizations)
59
60	// Used to build select queries showing only objects visible for the given user
61	abstract public function GetSelectFilter($sLogin, $sClass, $aSettings = array()); // returns a filter object
62
63	abstract public function IsActionAllowed($oUser, $sClass, $iActionCode, /*dbObjectSet*/ $oInstanceSet = null);
64	abstract public function IsStimulusAllowed($oUser, $sClass, $sStimulusCode, /*dbObjectSet*/ $oInstanceSet = null);
65	abstract public function IsActionAllowedOnAttribute($oUser, $sClass, $sAttCode, $iActionCode, /*dbObjectSet*/ $oInstanceSet = null);
66	abstract public function IsAdministrator($oUser);
67	abstract public function IsPortalUser($oUser);
68	abstract public function FlushPrivileges();
69
70
71	/**
72	 * Default behavior for addons that do not support profiles
73	 *
74	 * @param $oUser User
75	 * @return array
76	 */
77	public function ListProfiles($oUser)
78	{
79		return array();
80	}
81
82	/**
83	 *	...
84	 */
85	public function MakeSelectFilter($sClass, $aAllowedOrgs, $aSettings = array(), $sAttCode = null)
86	{
87		if ($sAttCode == null)
88		{
89			$sAttCode = $this->GetOwnerOrganizationAttCode($sClass);
90		}
91		if (empty($sAttCode))
92		{
93			return $oFilter  = new DBObjectSearch($sClass);
94		}
95
96		$oExpression = new FieldExpression($sAttCode, $sClass);
97		$oFilter  = new DBObjectSearch($sClass);
98		$oListExpr = ListExpression::FromScalars($aAllowedOrgs);
99
100		$oCondition = new BinaryExpression($oExpression, 'IN', $oListExpr);
101		$oFilter->AddConditionExpression($oCondition);
102
103		if ($this->HasSharing())
104		{
105			if (($sAttCode == 'id') && isset($aSettings['bSearchMode']) && $aSettings['bSearchMode'])
106			{
107				// Querying organizations (or derived)
108				// and the expected list of organizations will be used as a search criteria
109				// Therefore the query can also return organization having objects shared with the allowed organizations
110				//
111				// 1) build the list of organizations sharing something with the allowed organizations
112				// Organization <== sharing_org_id == SharedObject having org_id IN {user orgs}
113				$oShareSearch = new DBObjectSearch('SharedObject');
114				$oOrgField = new FieldExpression('org_id', 'SharedObject');
115				$oShareSearch->AddConditionExpression(new BinaryExpression($oOrgField, 'IN', $oListExpr));
116
117				$oSearchSharers = new DBObjectSearch('Organization');
118				$oSearchSharers->AllowAllData();
119				$oSearchSharers->AddCondition_ReferencedBy($oShareSearch, 'sharing_org_id');
120				$aSharers = array();
121				foreach($oSearchSharers->ToDataArray(array('id')) as $aRow)
122				{
123					$aSharers[] = $aRow['id'];
124				}
125				// 2) Enlarge the overall results: ... OR id IN(id1, id2, id3)
126				if (count($aSharers) > 0)
127				{
128					$oSharersList = ListExpression::FromScalars($aSharers);
129					$oFilter->MergeConditionExpression(new BinaryExpression($oExpression, 'IN', $oSharersList));
130				}
131			}
132
133			$aShareProperties = SharedObject::GetSharedClassProperties($sClass);
134			if ($aShareProperties)
135			{
136				$sShareClass = $aShareProperties['share_class'];
137				$sShareAttCode = $aShareProperties['attcode'];
138
139				$oSearchShares = new DBObjectSearch($sShareClass);
140				$oSearchShares->AllowAllData();
141
142				$sHierarchicalKeyCode = MetaModel::IsHierarchicalClass('Organization');
143				$oOrgField = new FieldExpression('org_id', $sShareClass);
144				$oSearchShares->AddConditionExpression(new BinaryExpression($oOrgField, 'IN', $oListExpr));
145				$aShared = array();
146				foreach($oSearchShares->ToDataArray(array($sShareAttCode)) as $aRow)
147				{
148					$aShared[] = $aRow[$sShareAttCode];
149				}
150				if (count($aShared) > 0)
151				{
152					$oObjId = new FieldExpression('id', $sClass);
153					$oSharedIdList = ListExpression::FromScalars($aShared);
154					$oFilter->MergeConditionExpression(new BinaryExpression($oObjId, 'IN', $oSharedIdList));
155				}
156			}
157		} // if HasSharing
158
159		return $oFilter;
160	}
161}
162
163
164require_once(APPROOT.'/application/cmdbabstract.class.inc.php');
165abstract class User extends cmdbAbstractObject
166{
167	public static function Init()
168	{
169		$aParams = array
170		(
171			"category" => "core,grant_by_profile",
172			"key_type" => "autoincrement",
173			"name_attcode" => "login",
174			"state_attcode" => "",
175			"reconc_keys" => array(),
176			"db_table" => "priv_user",
177			"db_key_field" => "id",
178			"db_finalclass_field" => "",
179			"display_template" => "",
180		);
181		MetaModel::Init_Params($aParams);
182		//MetaModel::Init_InheritAttributes();
183
184		MetaModel::Init_AddAttribute(new AttributeExternalKey("contactid", array("targetclass"=>"Person", "allowed_values"=>null, "sql"=>"contactid", "is_null_allowed"=>true, "on_target_delete"=>DEL_MANUAL, "depends_on"=>array())));
185		MetaModel::Init_AddAttribute(new AttributeExternalField("last_name", array("allowed_values"=>null, "extkey_attcode"=> 'contactid', "target_attcode"=>"name")));
186		MetaModel::Init_AddAttribute(new AttributeExternalField("first_name", array("allowed_values"=>null, "extkey_attcode"=> 'contactid', "target_attcode"=>"first_name")));
187		MetaModel::Init_AddAttribute(new AttributeExternalField("email", array("allowed_values"=>null, "extkey_attcode"=> 'contactid', "target_attcode"=>"email")));
188		MetaModel::Init_AddAttribute(new AttributeExternalField("org_id", array("allowed_values"=>null, "extkey_attcode"=> 'contactid', "target_attcode"=>"org_id")));
189
190		MetaModel::Init_AddAttribute(new AttributeString("login", array("allowed_values"=>null, "sql"=>"login", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
191
192		MetaModel::Init_AddAttribute(new AttributeApplicationLanguage("language", array("sql"=>"language", "default_value"=>"EN US", "is_null_allowed"=>false, "depends_on"=>array())));
193		MetaModel::Init_AddAttribute(new AttributeEnum("status", array("allowed_values" => new ValueSetEnum('enabled,disabled'), "sql"=>"status", "default_value"=>"enabled", "is_null_allowed"=>false, "depends_on"=>array())));
194
195		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())));
196		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())));
197
198		// Display lists
199		MetaModel::Init_SetZListItems('details', array('contactid', 'org_id', 'email', 'login', 'language', 'status', 'profile_list', 'allowed_org_list')); // Unused as it's an abstract class !
200		MetaModel::Init_SetZListItems('list', array('finalclass', 'first_name', 'last_name', 'status', 'org_id')); // Attributes to be displayed for a list
201		// Search criteria
202		MetaModel::Init_SetZListItems('standard_search', array('login', 'contactid', 'email', 'language', 'status', 'org_id')); // Criteria of the std search form
203		MetaModel::Init_SetZListItems('default_search', array('login', 'contactid', 'org_id')); // Default criteria of the search banner
204	}
205
206	abstract public function CheckCredentials($sPassword);
207	abstract public function TrustWebServerContext();
208	abstract public function CanChangePassword();
209	abstract public function ChangePassword($sOldPassword, $sNewPassword);
210
211	/*
212	* Compute a name in best effort mode
213	*/
214	public function GetFriendlyName()
215	{
216		if (!MetaModel::IsValidAttCode(get_class($this), 'contactid'))
217		{
218			return $this->Get('login');
219		}
220		if ($this->Get('contactid') != 0)
221		{
222			$sFirstName = $this->Get('first_name');
223			$sLastName = $this->Get('last_name');
224			$sEmail = $this->Get('email');
225			if (strlen($sFirstName) > 0)
226			{
227				return "$sFirstName $sLastName";
228			}
229			elseif (strlen($sEmail) > 0)
230			{
231				return "$sLastName <$sEmail>";
232			}
233			else
234			{
235				return $sLastName;
236			}
237		}
238		return $this->Get('login');
239	}
240
241	protected $oContactObject;
242
243	/**
244	 * Fetch and memorize the associated contact (if any)
245	 */
246	public function GetContactObject()
247	{
248		if (is_null($this->oContactObject))
249		{
250			if (MetaModel::IsValidAttCode(get_class($this), 'contactid') && ($this->Get('contactid') != 0))
251			{
252				$this->oContactObject = MetaModel::GetObject('Contact', $this->Get('contactid'));
253			}
254		}
255		return $this->oContactObject;
256	}
257
258	/**
259	 * Overload the standard behavior.
260	 *
261	 * @throws \CoreException
262	 */
263	public function DoCheckToWrite()
264	{
265		parent::DoCheckToWrite();
266
267		// Note: This MUST be factorized later: declare unique keys (set of columns) in the data model
268		$aChanges = $this->ListChanges();
269		if (array_key_exists('login', $aChanges))
270		{
271			if (strcasecmp($this->Get('login'), $this->GetOriginal('login')) !== 0)
272			{
273				$sNewLogin = $aChanges['login'];
274				$oSearch = DBObjectSearch::FromOQL_AllData("SELECT User WHERE login = :newlogin");
275				if (!$this->IsNew())
276				{
277					$oSearch->AddCondition('id', $this->GetKey(), '!=');
278				}
279				$oSet = new DBObjectSet($oSearch, array(), array('newlogin' => $sNewLogin));
280				if ($oSet->Count() > 0)
281				{
282					$this->m_aCheckIssues[] = Dict::Format('Class:User/Error:LoginMustBeUnique', $sNewLogin);
283				}
284			}
285		}
286		// Check that this user has at least one profile assigned when profiles have changed
287		if (array_key_exists('profile_list', $aChanges))
288		{
289			$oSet = $this->Get('profile_list');
290			if ($oSet->Count() == 0)
291			{
292				$this->m_aCheckIssues[] = Dict::S('Class:User/Error:AtLeastOneProfileIsNeeded');
293			}
294		}
295		// Only administrators can manage administrators
296		if (UserRights::IsAdministrator($this) && !UserRights::IsAdministrator())
297		{
298			$this->m_aCheckIssues[] = Dict::S('UI:Login:Error:AccessRestricted');
299		}
300
301		if (!UserRights::IsAdministrator())
302		{
303			$oUser = UserRights::GetUserObject();
304			$oAddon = UserRights::GetModuleInstance();
305			if (!is_null($oUser) && method_exists($oAddon, 'GetUserOrgs'))
306			{
307				if ((empty($this->GetOriginal('contactid')) && !($this->IsNew())) || empty($this->Get('contactid')))
308				{
309					$this->m_aCheckIssues[] = Dict::S('Class:User/Error:PersonIsMandatory');
310				}
311				else
312				{
313					$aOrgs = $oAddon->GetUserOrgs($oUser, '');
314					if (count($aOrgs) > 0)
315					{
316						// Check that the modified User belongs to one of our organization
317						if (!in_array($this->GetOriginal('org_id'), $aOrgs) && !in_array($this->Get('org_id'), $aOrgs))
318						{
319							$this->m_aCheckIssues[] = Dict::S('Class:User/Error:UserOrganizationNotAllowed');
320						}
321						// Check users with restricted organizations when allowed organizations have changed
322						if ($this->IsNew() || array_key_exists('allowed_org_list', $aChanges))
323						{
324							$oSet = $this->get('allowed_org_list');
325							if ($oSet->Count() == 0)
326							{
327								$this->m_aCheckIssues[] = Dict::S('Class:User/Error:AtLeastOneOrganizationIsNeeded');
328							}
329							else
330							{
331								$aModifiedLinks = $oSet->ListModifiedLinks();
332								foreach ($aModifiedLinks as $oLink)
333								{
334									if (!in_array($oLink->Get('allowed_org_id'), $aOrgs))
335									{
336										$this->m_aCheckIssues[] = Dict::S('Class:User/Error:OrganizationNotAllowed');
337									}
338								}
339							}
340						}
341					}
342				}
343			}
344		}
345	}
346
347	function GetGrantAsHtml($sClass, $iAction)
348	{
349		if (UserRights::IsActionAllowed($sClass, $iAction, null, $this))
350		{
351			return '<span style="background-color: #ddffdd;">'.Dict::S('UI:UserManagement:ActionAllowed:Yes').'</span>';
352		}
353		else
354		{
355			return '<span style="background-color: #ffdddd;">'.Dict::S('UI:UserManagement:ActionAllowed:No').'</span>';
356		}
357	}
358
359	function DoShowGrantSumary($oPage, $sClassCategory)
360	{
361		if (UserRights::IsAdministrator($this))
362		{
363			// Looks dirty, but ok that's THE ONE
364			$oPage->p(Dict::S('UI:UserManagement:AdminProfile+'));
365			return;
366		}
367
368		$oKPI = new ExecutionKPI();
369
370		$aDisplayData = array();
371		foreach (MetaModel::GetClasses($sClassCategory) as $sClass)
372		{
373			$aClassStimuli = MetaModel::EnumStimuli($sClass);
374			if (count($aClassStimuli) > 0)
375			{
376				$aStimuli = array();
377				foreach ($aClassStimuli as $sStimulusCode => $oStimulus)
378				{
379					if (UserRights::IsStimulusAllowed($sClass, $sStimulusCode, null, $this))
380					{
381						$aStimuli[] = '<span title="'.$sStimulusCode.': '.htmlentities($oStimulus->GetDescription(), ENT_QUOTES, 'UTF-8').'">'.htmlentities($oStimulus->GetLabel(), ENT_QUOTES, 'UTF-8').'</span>';
382					}
383				}
384				$sStimuli = implode(', ', $aStimuli);
385			}
386			else
387			{
388				$sStimuli = '<em title="'.Dict::S('UI:UserManagement:NoLifeCycleApplicable+').'">'.Dict::S('UI:UserManagement:NoLifeCycleApplicable').'</em>';
389			}
390
391			$aDisplayData[] = array(
392				'class' => MetaModel::GetName($sClass),
393				'read' => $this->GetGrantAsHtml($sClass, UR_ACTION_READ),
394				'bulkread' => $this->GetGrantAsHtml($sClass, UR_ACTION_BULK_READ),
395				'write' => $this->GetGrantAsHtml($sClass, UR_ACTION_MODIFY),
396				'bulkwrite' => $this->GetGrantAsHtml($sClass, UR_ACTION_BULK_MODIFY),
397				'delete' => $this->GetGrantAsHtml($sClass, UR_ACTION_DELETE),
398				'bulkdelete' => $this->GetGrantAsHtml($sClass, UR_ACTION_BULK_DELETE),
399				'stimuli' => $sStimuli,
400			);
401		}
402
403		$oKPI->ComputeAndReport('Computation of user rights');
404
405		$aDisplayConfig = array();
406		$aDisplayConfig['class'] = array('label' => Dict::S('UI:UserManagement:Class'), 'description' => Dict::S('UI:UserManagement:Class+'));
407		$aDisplayConfig['read'] = array('label' => Dict::S('UI:UserManagement:Action:Read'), 'description' => Dict::S('UI:UserManagement:Action:Read+'));
408		$aDisplayConfig['bulkread'] = array('label' => Dict::S('UI:UserManagement:Action:BulkRead'), 'description' => Dict::S('UI:UserManagement:Action:BulkRead+'));
409		$aDisplayConfig['write'] = array('label' => Dict::S('UI:UserManagement:Action:Modify'), 'description' => Dict::S('UI:UserManagement:Action:Modify+'));
410		$aDisplayConfig['bulkwrite'] = array('label' => Dict::S('UI:UserManagement:Action:BulkModify'), 'description' => Dict::S('UI:UserManagement:Action:BulkModify+'));
411		$aDisplayConfig['delete'] = array('label' => Dict::S('UI:UserManagement:Action:Delete'), 'description' => Dict::S('UI:UserManagement:Action:Delete+'));
412		$aDisplayConfig['bulkdelete'] = array('label' => Dict::S('UI:UserManagement:Action:BulkDelete'), 'description' => Dict::S('UI:UserManagement:Action:BulkDelete+'));
413		$aDisplayConfig['stimuli'] = array('label' => Dict::S('UI:UserManagement:Action:Stimuli'), 'description' => Dict::S('UI:UserManagement:Action:Stimuli+'));
414		$oPage->table($aDisplayConfig, $aDisplayData);
415	}
416
417	function DisplayBareRelations(WebPage $oPage, $bEditMode = false)
418	{
419		parent::DisplayBareRelations($oPage, $bEditMode);
420		if (!$bEditMode)
421		{
422			$oPage->SetCurrentTab(Dict::S('UI:UserManagement:GrantMatrix'));
423			$this->DoShowGrantSumary($oPage, 'bizmodel,grant_by_profile');
424
425			// debug
426			if (false)
427			{
428				$oPage->SetCurrentTab('More on user rigths (dev only)');
429				$oPage->add("<h3>User rights</h3>\n");
430				$this->DoShowGrantSumary($oPage, 'addon/userrights');
431				$oPage->add("<h3>Change log</h3>\n");
432				$this->DoShowGrantSumary($oPage, 'core/cmdb');
433				$oPage->add("<h3>Application</h3>\n");
434				$this->DoShowGrantSumary($oPage, 'application');
435				$oPage->add("<h3>GUI</h3>\n");
436				$this->DoShowGrantSumary($oPage, 'gui');
437
438			}
439		}
440	}
441
442  	public function CheckToDelete(&$oDeletionPlan)
443  	{
444  		if (MetaModel::GetConfig()->Get('demo_mode'))
445		{
446			// Users deletion is NOT allowed in demo mode
447			$oDeletionPlan->AddToDelete($this, null);
448			$oDeletionPlan->SetDeletionIssues($this, array('deletion not allowed in demo mode.'), true);
449			$oDeletionPlan->ComputeResults();
450			return false;
451		}
452		return parent::CheckToDelete($oDeletionPlan);
453  	}
454
455	protected function DBDeleteSingleObject()
456	{
457		if (MetaModel::GetConfig()->Get('demo_mode'))
458		{
459			// Users deletion is NOT allowed in demo mode
460			return;
461		}
462		parent::DBDeleteSingleObject();
463	}
464}
465
466/**
467 * Abstract class for all types of "internal" authentication i.e. users
468 * for which the application is supplied a login and a password opposed
469 * to "external" users for whom the authentication is performed outside
470 * of the application (by the web server for example).
471 * Note that "internal" users do not necessary correspond to a local authentication
472 * they may be authenticated by a remote system, like in authent-ldap.
473 */
474abstract class UserInternal extends User
475{
476	// Nothing special, just a base class to categorize this type of authenticated users
477	public static function Init()
478	{
479		$aParams = array
480		(
481			"category" => "core,grant_by_profile",
482			"key_type" => "autoincrement",
483			"name_attcode" => "login",
484			"state_attcode" => "",
485			"reconc_keys" => array('login'),
486			"db_table" => "priv_internaluser",
487			"db_key_field" => "id",
488			"db_finalclass_field" => "",
489		);
490		MetaModel::Init_Params($aParams);
491		MetaModel::Init_InheritAttributes();
492
493		// When set, this token allows for password reset
494		MetaModel::Init_AddAttribute(new AttributeOneWayPassword("reset_pwd_token", array("allowed_values"=>null, "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
495
496		// Display lists
497		MetaModel::Init_SetZListItems('details', array('contactid', 'org_id', 'email', 'login', 'status', 'language', 'profile_list', 'allowed_org_list')); // Attributes to be displayed for the complete details
498		MetaModel::Init_SetZListItems('list', array('finalclass', 'first_name', 'last_name', 'status', 'org_id')); // Attributes to be displayed for a list
499		// Search criteria
500		MetaModel::Init_SetZListItems('standard_search', array('login', 'contactid', 'status', 'org_id')); // Criteria of the std search form
501	}
502
503	/**
504	 * Use with care!
505	 */
506	public function SetPassword($sNewPassword)
507	{
508	}
509
510	/**
511	 * The email recipient is the person who is allowed to regain control when the password gets lost
512	 * Throws an exception if the feature cannot be available
513	 */
514	public function GetResetPasswordEmail()
515	{
516		if (!MetaModel::IsValidAttCode(get_class($this), 'contactid'))
517		{
518			throw new Exception(Dict::S('UI:ResetPwd-Error-NoContact'));
519		}
520		$iContactId = $this->Get('contactid');
521		if ($iContactId == 0)
522		{
523			throw new Exception(Dict::S('UI:ResetPwd-Error-NoContact'));
524		}
525		$oContact = MetaModel::GetObject('Contact', $iContactId);
526		// Determine the email attribute (the first one will be our choice)
527		foreach (MetaModel::ListAttributeDefs(get_class($oContact)) as $sAttCode => $oAttDef)
528		{
529			if ($oAttDef instanceof AttributeEmailAddress)
530			{
531				$sEmailAttCode = $sAttCode;
532				// we've got one, exit the loop
533				break;
534			}
535		}
536		if (!isset($sEmailAttCode))
537		{
538			throw new Exception(Dict::S('UI:ResetPwd-Error-NoEmailAtt'));
539		}
540		$sRes = trim($oContact->Get($sEmailAttCode));
541		return $sRes;
542	}
543}
544
545/**
546 * Self register extension
547 *
548 * @package     iTopORM
549 */
550interface iSelfRegister
551{
552	/**
553	 * Called when no user is found in iTop for the corresponding 'name'. This method
554	 * can create/synchronize the User in iTop with an external source (such as AD/LDAP) on the fly
555	 * @param string $sName The typed-in user name
556	 * @param string $sPassword The typed-in password
557	 * @param string $sLoginMode The login method used (cas|form|basic|url)
558	 * @param string $sAuthentication The authentication method used (any|internal|external)
559	 * @return bool true if the user is a valid one, false otherwise
560	 */
561	public static function CheckCredentialsAndCreateUser($sName, $sPassword, $sLoginMode, $sAuthentication);
562
563	/**
564	 * Called after the user has been authenticated and found in iTop. This method can
565	 * Update the user's definition on the fly (profiles...) to keep it in sync with an external source
566	 * @param User $oUser The user to update/synchronize
567	 * @param string $sLoginMode The login mode used (cas|form|basic|url)
568	 * @param string $sAuthentication The authentication method used
569	 * @return void
570	 */
571	public static function UpdateUser(User $oUser, $sLoginMode, $sAuthentication);
572}
573
574/**
575 * User management core API
576 *
577 * @package     iTopORM
578 */
579class UserRights
580{
581	/** @var UserRightsAddOnAPI $m_oAddOn */
582	protected static $m_oAddOn;
583	protected static $m_oUser;
584	protected static $m_oRealUser;
585	protected static $m_sSelfRegisterAddOn = null;
586
587	public static function SelectModule($sModuleName)
588	{
589		if (!class_exists($sModuleName))
590		{
591			throw new CoreException("Could not select this module, '$sModuleName' in not a valid class name");
592			return;
593		}
594		if (!is_subclass_of($sModuleName, 'UserRightsAddOnAPI'))
595		{
596			throw new CoreException("Could not select this module, the class '$sModuleName' is not derived from UserRightsAddOnAPI");
597			return;
598		}
599		self::$m_oAddOn = new $sModuleName;
600		self::$m_oAddOn->Init();
601		self::$m_oUser = null;
602		self::$m_oRealUser = null;
603	}
604
605	public static function SelectSelfRegister($sModuleName)
606	{
607		if (!class_exists($sModuleName))
608		{
609			throw new CoreException("Could not select the class, '$sModuleName' for self register, is not a valid class name");
610		}
611		self::$m_sSelfRegisterAddOn = $sModuleName;
612	}
613
614	public static function GetModuleInstance()
615	{
616		return self::$m_oAddOn;
617	}
618
619	// Installation: create the very first user
620	public static function CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage = 'EN US')
621	{
622		$bRes = self::$m_oAddOn->CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage);
623		self::FlushPrivileges(true /* reset admin cache */);
624		return $bRes;
625	}
626
627	public static function IsLoggedIn()
628	{
629		if (self::$m_oUser == null)
630		{
631			return false;
632		}
633		else
634		{
635			return true;
636		}
637	}
638
639	public static function Login($sName, $sAuthentication = 'any')
640	{
641		$oUser = self::FindUser($sName, $sAuthentication);
642		if (is_null($oUser))
643		{
644			return false;
645		}
646		self::$m_oUser = $oUser;
647
648		if (isset($_SESSION['impersonate_user']))
649		{
650			self::$m_oRealUser = self::$m_oUser;
651			self::$m_oUser = self::FindUser($_SESSION['impersonate_user']);
652		}
653
654		Dict::SetUserLanguage(self::GetUserLanguage());
655		return true;
656	}
657
658	public static function CheckCredentials($sName, $sPassword, $sLoginMode = 'form', $sAuthentication = 'any')
659	{
660		$oUser = self::FindUser($sName, $sAuthentication);
661		if (is_null($oUser))
662		{
663			// Check if the user does not exist at all or if it is just disabled
664			if (self::FindUser($sName, $sAuthentication, true) == null)
665			{
666				// User does not exist at all
667				return self::CheckCredentialsAndCreateUser($sName, $sPassword, $sLoginMode, $sAuthentication);
668			}
669			else
670			{
671				// User is actually disabled
672				return  false;
673			}
674		}
675
676		if (!$oUser->CheckCredentials($sPassword))
677		{
678			return false;
679		}
680		self::UpdateUser($oUser, $sLoginMode, $sAuthentication);
681		return true;
682	}
683
684	public static function CheckCredentialsAndCreateUser($sName, $sPassword, $sLoginMode, $sAuthentication)
685	{
686		if (self::$m_sSelfRegisterAddOn != null)
687		{
688			return call_user_func(array(self::$m_sSelfRegisterAddOn, 'CheckCredentialsAndCreateUser'), $sName, $sPassword, $sLoginMode, $sAuthentication);
689		}
690	}
691
692	public static function UpdateUser($oUser, $sLoginMode, $sAuthentication)
693	{
694		if (self::$m_sSelfRegisterAddOn != null)
695		{
696			call_user_func(array(self::$m_sSelfRegisterAddOn, 'UpdateUser'), $oUser, $sLoginMode, $sAuthentication);
697		}
698	}
699
700	public static function TrustWebServerContext()
701	{
702		if (!is_null(self::$m_oUser))
703		{
704 			return self::$m_oUser->TrustWebServerContext();
705		}
706		else
707		{
708			return false;
709		}
710	}
711
712	/**
713	 * Tells whether or not the archive mode is allowed to the current user
714	 * @return boolean
715	 */
716	static function CanBrowseArchive()
717	{
718		if (is_null(self::$m_oUser))
719		{
720			$bRet = false;
721		}
722		elseif (isset($_SESSION['archive_allowed']))
723		{
724			$bRet = $_SESSION['archive_allowed'];
725		}
726		else
727		{
728			// As of now, anybody can switch to the archive mode as soon as there is an archivable class
729			$bRet = (count(MetaModel::EnumArchivableClasses()) > 0);
730			$_SESSION['archive_allowed'] = $bRet;
731		}
732		return $bRet;
733	}
734
735	public static function CanChangePassword()
736	{
737		if (MetaModel::DBIsReadOnly())
738		{
739			return false;
740		}
741
742		if (!is_null(self::$m_oUser))
743		{
744 			return self::$m_oUser->CanChangePassword();
745		}
746		else
747		{
748			return false;
749		}
750	}
751
752	public static function ChangePassword($sOldPassword, $sNewPassword, $sName = '')
753	{
754		if (empty($sName))
755		{
756			$oUser = self::$m_oUser;
757		}
758		else
759		{
760			// find the id out of the login string
761			$oUser = self::FindUser($sName);
762		}
763		if (is_null($oUser))
764		{
765			return false;
766		}
767		else
768		{
769			$oUser->AllowWrite(true);
770			return $oUser->ChangePassword($sOldPassword, $sNewPassword);
771		}
772	}
773
774	/**
775	 * @param string $sName Login identifier of the user to impersonate
776	 * @return bool True if an impersonation occurred
777	 */
778	public static function Impersonate($sName)
779	{
780		if (!self::CheckLogin()) return false;
781
782		$bRet = false;
783		$oUser = self::FindUser($sName);
784		if ($oUser)
785		{
786			$bRet = true;
787			if (is_null(self::$m_oRealUser))
788			{
789				// First impersonation
790				self::$m_oRealUser = self::$m_oUser;
791			}
792			if (self::$m_oRealUser && (self::$m_oRealUser->GetKey() == $oUser->GetKey()))
793			{
794				// Equivalent to "Deimpersonate"
795				self::Deimpersonate();
796			}
797			else
798			{
799				// Do impersonate!
800				self::$m_oUser = $oUser;
801				Dict::SetUserLanguage(self::GetUserLanguage());
802				$_SESSION['impersonate_user'] = $sName;
803				self::_ResetSessionCache();
804			}
805		}
806		return $bRet;
807	}
808
809	public static function Deimpersonate()
810	{
811		if (!is_null(self::$m_oRealUser))
812		{
813			self::$m_oUser = self::$m_oRealUser;
814			Dict::SetUserLanguage(self::GetUserLanguage());
815			unset($_SESSION['impersonate_user']);
816			self::_ResetSessionCache();
817		}
818	}
819
820	public static function GetUser()
821	{
822		if (is_null(self::$m_oUser))
823		{
824			return '';
825		}
826		else
827		{
828			return self::$m_oUser->Get('login');
829		}
830	}
831
832	public static function GetUserObject()
833	{
834		if (is_null(self::$m_oUser))
835		{
836			return null;
837		}
838		else
839		{
840			return self::$m_oUser;
841		}
842	}
843
844	public static function GetUserLanguage()
845	{
846		if (is_null(self::$m_oUser))
847		{
848			return 'EN US';
849
850		}
851		else
852		{
853			return self::$m_oUser->Get('language');
854		}
855	}
856
857	public static function GetUserId($sName = '')
858	{
859		if (empty($sName))
860		{
861			// return current user id
862			if (is_null(self::$m_oUser))
863			{
864				return null;
865			}
866			return self::$m_oUser->GetKey();
867		}
868		else
869		{
870			// find the id out of the login string
871			$oUser = self::$m_oAddOn->FindUser($sName);
872			if (is_null($oUser))
873			{
874				return null;
875			}
876			return $oUser->GetKey();
877		}
878	}
879
880	public static function GetContactId($sName = '')
881	{
882		if (empty($sName))
883		{
884			$oUser = self::$m_oUser;
885		}
886		else
887		{
888			$oUser = FindUser($sName);
889		}
890		if (is_null($oUser))
891		{
892			return '';
893		}
894		if (!MetaModel::IsValidAttCode(get_class($oUser), 'contactid'))
895		{
896			return '';
897		}
898		return $oUser->Get('contactid');
899	}
900
901	public static function GetContactObject()
902	{
903		if (is_null(self::$m_oUser))
904		{
905			return null;
906		}
907		else
908		{
909			return self::$m_oUser->GetContactObject();
910		}
911	}
912
913	// Render the user name in best effort mode
914	public static function GetUserFriendlyName($sName = '')
915	{
916		if (empty($sName))
917		{
918			$oUser = self::$m_oUser;
919		}
920		else
921		{
922			$oUser = FindUser($sName);
923		}
924		if (is_null($oUser))
925		{
926			return '';
927		}
928		return $oUser->GetFriendlyName();
929	}
930
931	public static function IsImpersonated()
932	{
933		if (is_null(self::$m_oRealUser))
934		{
935			return false;
936		}
937		return true;
938	}
939
940	public static function GetRealUser()
941	{
942		if (is_null(self::$m_oRealUser))
943		{
944			return '';
945		}
946		return self::$m_oRealUser->Get('login');
947	}
948
949	public static function GetRealUserObject()
950	{
951		return self::$m_oRealUser;
952	}
953
954	public static function GetRealUserId()
955	{
956		if (is_null(self::$m_oRealUser))
957		{
958			return '';
959		}
960		return self::$m_oRealUser->GetKey();
961	}
962
963	public static function GetRealUserFriendlyName()
964	{
965		if (is_null(self::$m_oRealUser))
966		{
967			return '';
968		}
969		return self::$m_oRealUser->GetFriendlyName();
970	}
971
972	protected static function CheckLogin()
973	{
974		if (!self::IsLoggedIn())
975		{
976			//throw new UserRightException('No user logged in', array());
977			return false;
978		}
979		return true;
980	}
981
982	/**
983	 * Add additional filter for organization silos to all the requests.
984	 *
985	 * @param $sClass
986	 * @param array $aSettings
987	 *
988	 * @return bool|\Expression
989	 */
990	public static function GetSelectFilter($sClass, $aSettings = array())
991	{
992		// When initializing, we need to let everything pass trough
993		if (!self::CheckLogin()) {return true;}
994
995		if (self::IsAdministrator()) {return true;}
996
997		try
998		{
999			// Check Bug 1436 for details
1000			if (MetaModel::HasCategory($sClass, 'bizmodel'))
1001			{
1002				return self::$m_oAddOn->GetSelectFilter(self::$m_oUser, $sClass, $aSettings);
1003			}
1004			else
1005			{
1006				return true;
1007			}
1008		} catch (Exception $e)
1009		{
1010			return false;
1011		}
1012	}
1013
1014	/**
1015	 * @param string $sClass
1016	 * @param int $iActionCode
1017	 * @param DBObjectSet $oInstanceSet
1018	 * @param User $oUser
1019	 * @return int (UR_ALLOWED_YES|UR_ALLOWED_NO|UR_ALLOWED_DEPENDS)
1020	 */
1021	public static function IsActionAllowed($sClass, $iActionCode, /*dbObjectSet*/$oInstanceSet = null, $oUser = null)
1022	{
1023		// When initializing, we need to let everything pass trough
1024		if (!self::CheckLogin()) return UR_ALLOWED_YES;
1025
1026		if (MetaModel::DBIsReadOnly())
1027		{
1028			if ($iActionCode == UR_ACTION_CREATE) return UR_ALLOWED_NO;
1029			if ($iActionCode == UR_ACTION_MODIFY) return UR_ALLOWED_NO;
1030			if ($iActionCode == UR_ACTION_BULK_MODIFY) return UR_ALLOWED_NO;
1031			if ($iActionCode == UR_ACTION_DELETE) return UR_ALLOWED_NO;
1032			if ($iActionCode == UR_ACTION_BULK_DELETE) return UR_ALLOWED_NO;
1033		}
1034
1035		$aPredefinedObjects = call_user_func(array($sClass, 'GetPredefinedObjects'));
1036		if ($aPredefinedObjects != null)
1037		{
1038			// As opposed to the read-only DB, modifying an object is allowed
1039			// (the constant columns will be marked as read-only)
1040			//
1041			if ($iActionCode == UR_ACTION_CREATE) return UR_ALLOWED_NO;
1042			if ($iActionCode == UR_ACTION_DELETE) return UR_ALLOWED_NO;
1043			if ($iActionCode == UR_ACTION_BULK_DELETE) return UR_ALLOWED_NO;
1044		}
1045
1046		if (self::IsAdministrator($oUser)) return UR_ALLOWED_YES;
1047
1048		if (MetaModel::HasCategory($sClass, 'bizmodel') || MetaModel::HasCategory($sClass, 'grant_by_profile'))
1049		{
1050			if (is_null($oUser))
1051			{
1052				$oUser = self::$m_oUser;
1053			}
1054			if ($iActionCode == UR_ACTION_CREATE)
1055			{
1056				// The addons currently DO NOT handle the case "CREATE"
1057				// Therefore it is considered to be equivalent to "MODIFY"
1058				$iActionCode = UR_ACTION_MODIFY;
1059			}
1060			return self::$m_oAddOn->IsActionAllowed($oUser, $sClass, $iActionCode, $oInstanceSet);
1061		}
1062		elseif(($iActionCode == UR_ACTION_READ) && MetaModel::HasCategory($sClass, 'view_in_gui'))
1063		{
1064			return UR_ALLOWED_YES;
1065		}
1066		else
1067		{
1068			// Other classes could be edited/listed by the administrators
1069			return UR_ALLOWED_NO;
1070		}
1071	}
1072
1073	public static function IsStimulusAllowed($sClass, $sStimulusCode, /*dbObjectSet*/ $oInstanceSet = null, $oUser = null)
1074	{
1075		// When initializing, we need to let everything pass trough
1076		if (!self::CheckLogin()) return true;
1077
1078		if (MetaModel::DBIsReadOnly())
1079		{
1080			return false;
1081		}
1082
1083		if (self::IsAdministrator($oUser)) return true;
1084
1085		if (MetaModel::HasCategory($sClass, 'bizmodel'))
1086		{
1087			if (is_null($oUser))
1088			{
1089				$oUser = self::$m_oUser;
1090			}
1091			return self::$m_oAddOn->IsStimulusAllowed($oUser, $sClass, $sStimulusCode, $oInstanceSet);
1092		}
1093		else
1094		{
1095			// Other classes could be edited/listed by the administrators
1096			return false;
1097		}
1098	}
1099
1100	/**
1101	 * @param string $sClass
1102	 * @param string $sAttCode
1103	 * @param int $iActionCode
1104	 * @param DBObjectSet $oInstanceSet
1105	 * @param User $oUser
1106	 * @return int (UR_ALLOWED_YES|UR_ALLOWED_NO)
1107	 */
1108	public static function IsActionAllowedOnAttribute($sClass, $sAttCode, $iActionCode, /*dbObjectSet*/$oInstanceSet = null, $oUser = null)
1109	{
1110		// When initializing, we need to let everything pass trough
1111		if (!self::CheckLogin()) return UR_ALLOWED_YES;
1112
1113		if (MetaModel::DBIsReadOnly())
1114		{
1115			if ($iActionCode == UR_ACTION_MODIFY) return UR_ALLOWED_NO;
1116			if ($iActionCode == UR_ACTION_DELETE) return UR_ALLOWED_NO;
1117			if ($iActionCode == UR_ACTION_BULK_MODIFY) return falUR_ALLOWED_NOse;
1118			if ($iActionCode == UR_ACTION_BULK_DELETE) return UR_ALLOWED_NO;
1119		}
1120
1121		if (self::IsAdministrator($oUser)) return UR_ALLOWED_YES;
1122
1123		if (MetaModel::HasCategory($sClass, 'bizmodel') || MetaModel::HasCategory($sClass, 'grant_by_profile'))
1124		{
1125			if (is_null($oUser))
1126			{
1127				$oUser = self::$m_oUser;
1128			}
1129			return self::$m_oAddOn->IsActionAllowedOnAttribute($oUser, $sClass, $sAttCode, $iActionCode, $oInstanceSet);
1130		}
1131
1132		// this module is forbidden for non admins
1133		if (MetaModel::HasCategory($sClass, 'addon/userrights')) return UR_ALLOWED_NO;
1134
1135		// the rest is allowed
1136		return UR_ALLOWED_YES;
1137
1138
1139	}
1140
1141	protected static $m_aAdmins = array();
1142	public static function IsAdministrator($oUser = null)
1143	{
1144		if (!self::CheckLogin()) return false;
1145
1146		if (is_null($oUser))
1147		{
1148			$oUser = self::$m_oUser;
1149		}
1150		$iUser = $oUser->GetKey();
1151		if (!isset(self::$m_aAdmins[$iUser]))
1152		{
1153			self::$m_aAdmins[$iUser] = self::$m_oAddOn->IsAdministrator($oUser);
1154		}
1155		return self::$m_aAdmins[$iUser];
1156	}
1157
1158	protected static $m_aPortalUsers = array();
1159	public static function IsPortalUser($oUser = null)
1160	{
1161		if (!self::CheckLogin()) return false;
1162
1163		if (is_null($oUser))
1164		{
1165			$oUser = self::$m_oUser;
1166		}
1167		$iUser = $oUser->GetKey();
1168		if (!isset(self::$m_aPortalUsers[$iUser]))
1169		{
1170			self::$m_aPortalUsers[$iUser] = self::$m_oAddOn->IsPortalUser($oUser);
1171		}
1172		return self::$m_aPortalUsers[$iUser];
1173	}
1174
1175	public static function GetAllowedPortals()
1176    {
1177        $aAllowedPortals = array();
1178        $aPortalsConf = PortalDispatcherData::GetData();
1179        $aDispatchers = array();
1180        foreach ($aPortalsConf as $sPortalId => $aConf)
1181        {
1182            $sHandlerClass = $aConf['handler'];
1183            $aDispatchers[$sPortalId] = new $sHandlerClass($sPortalId);
1184        }
1185
1186        foreach ($aDispatchers as $sPortalId => $oDispatcher)
1187        {
1188            if ($oDispatcher->IsUserAllowed())
1189            {
1190                $aAllowedPortals[] = array(
1191                    'id' => $sPortalId,
1192                    'label' => $oDispatcher->GetLabel(),
1193                    'url' => $oDispatcher->GetUrl(),
1194                );
1195            }
1196        }
1197        return $aAllowedPortals;
1198    }
1199
1200    public static function ListProfiles($oUser = null)
1201	{
1202		if (is_null($oUser))
1203		{
1204			$oUser = self::$m_oUser;
1205		}
1206		if ($oUser === null)
1207		{
1208			// Not logged in: no profile at all
1209			$aProfiles = array();
1210		}
1211		elseif ((self::$m_oUser !== null) && ($oUser->GetKey() == self::$m_oUser->GetKey()))
1212		{
1213			// Data about the current user can be found into the session data
1214			if (array_key_exists('profile_list', $_SESSION))
1215			{
1216				$aProfiles = $_SESSION['profile_list'];
1217			}
1218		}
1219
1220		if (!isset($aProfiles))
1221		{
1222			$aProfiles = self::$m_oAddOn->ListProfiles($oUser);
1223		}
1224		return $aProfiles;
1225	}
1226
1227	/**
1228	 * @param string $sProfileName Profile name to search for
1229	 * @param User|null $oUser
1230	 *
1231	 * @return bool
1232	 */
1233	public static function HasProfile($sProfileName, $oUser = null)
1234	{
1235		$bRet = in_array($sProfileName, self::ListProfiles($oUser));
1236		return $bRet;
1237	}
1238
1239	/**
1240	 * Reset cached data
1241	 * @param Bool Reset admin cache as well
1242	 * @return void
1243	 */
1244	public static function FlushPrivileges($bResetAdminCache = false)
1245	{
1246		if ($bResetAdminCache)
1247		{
1248			self::$m_aAdmins = array();
1249			self::$m_aPortalUsers = array();
1250		}
1251		if (!isset($_SESSION) && !utils::IsModeCLI())
1252		{
1253			session_name('itop-'.md5(APPROOT));
1254			session_start();
1255		}
1256		self::_ResetSessionCache();
1257		if (self::$m_oAddOn)
1258		{
1259			self::$m_oAddOn->FlushPrivileges();
1260		}
1261	}
1262
1263	static $m_aCacheUsers;
1264
1265	/**
1266	 * Find a user based on its login and its type of authentication
1267	 *
1268	 * @param string $sLogin Login/identifier of the user
1269	 * @param string $sAuthentication Type of authentication used: internal|external|any
1270	 * @param bool $bAllowDisabledUsers Whether or not to retrieve disabled users (status != enabled)
1271	 *
1272	 * @return User The found user or null
1273	 * @throws \OQLException
1274	 */
1275	protected static function FindUser($sLogin, $sAuthentication = 'any', $bAllowDisabledUsers = false)
1276	{
1277		if ($sAuthentication == 'any')
1278		{
1279			$oUser = self::FindUser($sLogin, 'internal');
1280			if ($oUser == null)
1281			{
1282				$oUser = self::FindUser($sLogin, 'external');
1283			}
1284		}
1285		else
1286		{
1287			if (!isset(self::$m_aCacheUsers))
1288			{
1289				self::$m_aCacheUsers = array('internal' => array(), 'external' => array());
1290			}
1291
1292			if (!isset(self::$m_aCacheUsers[$sAuthentication][$sLogin]))
1293			{
1294				switch($sAuthentication)
1295				{
1296					case 'external':
1297					$sBaseClass = 'UserExternal';
1298					break;
1299
1300					case 'internal':
1301					$sBaseClass = 'UserInternal';
1302					break;
1303
1304					default:
1305					echo "<p>sAuthentication = $sAuthentication</p>\n";
1306					assert(false); // should never happen
1307				}
1308				$oSearch = DBObjectSearch::FromOQL("SELECT $sBaseClass WHERE login = :login");
1309				if (!$bAllowDisabledUsers)
1310				{
1311					$oSearch->AddCondition('status', 'enabled');
1312				}
1313				$oSet = new DBObjectSet($oSearch, array(), array('login' => $sLogin));
1314				$oUser = $oSet->fetch();
1315				self::$m_aCacheUsers[$sAuthentication][$sLogin] = $oUser;
1316			}
1317			$oUser = self::$m_aCacheUsers[$sAuthentication][$sLogin];
1318		}
1319		return $oUser;
1320	}
1321
1322	public static function MakeSelectFilter($sClass, $aAllowedOrgs, $aSettings = array(), $sAttCode = null)
1323	{
1324		return self::$m_oAddOn->MakeSelectFilter($sClass, $aAllowedOrgs, $aSettings, $sAttCode);
1325	}
1326
1327	public static function _InitSessionCache()
1328	{
1329		// Cache data about the current user into the session
1330		if (isset($_SESSION))
1331		{
1332			$_SESSION['profile_list'] = self::ListProfiles();
1333		}
1334
1335		$oConfig = MetaModel::GetConfig();
1336		$bSessionIdRegeneration = $oConfig->Get('regenerate_session_id_enabled');
1337		if ($bSessionIdRegeneration)
1338		{
1339			// Protection against session fixation/injection: generate a new session id.
1340
1341			// Alas a PHP bug (technically a bug in the memcache session handler, https://bugs.php.net/bug.php?id=71187)
1342			// causes session_regenerate_id to fail with a catchable fatal error in PHP 7.0 if the session handler is memcache(d).
1343			// The bug has been fixed in PHP 7.2, but in case session_regenerate_id()
1344			// fails we just silently ignore the error and keep the same session id...
1345			$old_error_handler = set_error_handler(array(__CLASS__, 'VoidErrorHandler'));
1346			session_regenerate_id();
1347			if ($old_error_handler !== null)
1348			{
1349				set_error_handler($old_error_handler);
1350			}
1351		}
1352	}
1353
1354	public static function _ResetSessionCache()
1355	{
1356		if (isset($_SESSION['profile_list']))
1357		{
1358			unset($_SESSION['profile_list']);
1359		}
1360		if (isset($_SESSION['archive_allowed']))
1361		{
1362			unset($_SESSION['archive_allowed']);
1363		}
1364	}
1365
1366	/**
1367	 * Fake error handler to silently discard fatal errors
1368	 * @param int $iErrNo
1369	 * @param string $sErrStr
1370	 * @param string $sErrFile
1371	 * @param int $iErrLine
1372	 * @return boolean
1373	 */
1374	public static function VoidErrorHandler($iErrno, $sErrStr, $sErrFile, $iErrLine)
1375	{
1376		return true; // Ignore the error
1377	}
1378}
1379
1380/**
1381 * Helper class to get the number/list of items for which a given action is allowed/possible
1382 */
1383class ActionChecker
1384{
1385	var $oFilter;
1386	var $iActionCode;
1387	var $iAllowedCount = null;
1388	var $aAllowedIDs = null;
1389
1390	public function __construct(DBSearch $oFilter, $iActionCode)
1391	{
1392		$this->oFilter = $oFilter;
1393		$this->iActionCode = $iActionCode;
1394		$this->iAllowedCount = null;
1395		$this->aAllowedIDs = null;
1396	}
1397
1398	/**
1399	 * returns the number of objects for which the action is allowed
1400	 * @return integer The number of "allowed" objects 0..N
1401	 */
1402	public function GetAllowedCount()
1403	{
1404		if ($this->iAllowedCount == null) $this->CheckObjects();
1405		return $this->iAllowedCount;
1406	}
1407
1408	/**
1409	 * If IsAllowed returned UR_ALLOWED_DEPENDS, this methods returns
1410	 * an array of ObjKey => Status (true|false)
1411	 * @return array
1412	 */
1413	public function GetAllowedIDs()
1414	{
1415		if ($this->aAllowedIDs == null) $this->IsAllowed();
1416		return $this->aAllowedIDs;
1417	}
1418
1419	/**
1420	 * Check if the speficied stimulus is allowed for the set of objects
1421	 * @return UR_ALLOWED_YES, UR_ALLOWED_NO or UR_ALLOWED_DEPENDS
1422	 */
1423	public function IsAllowed()
1424	{
1425		$sClass = $this->oFilter->GetClass();
1426		$oSet = new DBObjectSet($this->oFilter);
1427		$iActionAllowed = UserRights::IsActionAllowed($sClass, $this->iActionCode, $oSet);
1428		if ($iActionAllowed == UR_ALLOWED_DEPENDS)
1429		{
1430			// Check for each object if the action is allowed or not
1431			$this->aAllowedIDs = array();
1432			$oSet->Rewind();
1433			$this->iAllowedCount = 0;
1434			while($oObj = $oSet->Fetch())
1435			{
1436				$oObjSet = DBObjectSet::FromArray($sClass, array($oObj));
1437				if (UserRights::IsActionAllowed($sClass, $this->iActionCode, $oObjSet) == UR_ALLOWED_NO)
1438				{
1439					$this->aAllowedIDs[$oObj->GetKey()] = false;
1440				}
1441				else
1442				{
1443					// Assume UR_ALLOWED_YES, since there is just one object !
1444					$this->aAllowedIDs[$oObj->GetKey()] = true;
1445					$this->iAllowedCount++;
1446				}
1447			}
1448		}
1449		else if ($iActionAllowed == UR_ALLOWED_YES)
1450		{
1451			$this->iAllowedCount = $oSet->Count();
1452			$this->aAllowedIDs = array(); // Optimization: not filled when Ok for all objects
1453		}
1454		else // UR_ALLOWED_NO
1455		{
1456			$this->iAllowedCount = 0;
1457			$this->aAllowedIDs = array();
1458		}
1459		return $iActionAllowed;
1460	}
1461}
1462
1463/**
1464 * Helper class to get the number/list of items for which a given stimulus can be applied (allowed & possible)
1465 */
1466class StimulusChecker extends ActionChecker
1467{
1468	var $sState = null;
1469
1470	public function __construct(DBSearch $oFilter, $sState, $iStimulusCode)
1471	{
1472		parent::__construct($oFilter, $iStimulusCode);
1473		$this->sState = $sState;
1474	}
1475
1476	/**
1477	 * Check if the speficied stimulus is allowed for the set of objects
1478	 * @return UR_ALLOWED_YES, UR_ALLOWED_NO or UR_ALLOWED_DEPENDS
1479	 */
1480	public function IsAllowed()
1481	{
1482		$sClass = $this->oFilter->GetClass();
1483		if (MetaModel::IsAbstract($sClass)) return UR_ALLOWED_NO; // Safeguard, not implemented if the base class of the set is abstract !
1484
1485		$oSet = new DBObjectSet($this->oFilter);
1486		$iActionAllowed = UserRights::IsStimulusAllowed($sClass,  $this->iActionCode, $oSet);
1487		if ($iActionAllowed == UR_ALLOWED_NO)
1488		{
1489			$this->iAllowedCount = 0;
1490			$this->aAllowedIDs = array();
1491		}
1492		else // Even if UR_ALLOWED_YES, we need to check if each object is in the appropriate state
1493		{
1494			// Hmmm, may not be needed right now because we limit the "multiple" action to object in
1495			// the same state... may be useful later on if we want to extend this behavior...
1496
1497			// Check for each object if the action is allowed or not
1498			$this->aAllowedIDs = array();
1499			$oSet->Rewind();
1500			$iAllowedCount = 0;
1501			$iActionAllowed = UR_ALLOWED_DEPENDS;
1502			while($oObj = $oSet->Fetch())
1503			{
1504				$aTransitions = $oObj->EnumTransitions();
1505				if (array_key_exists($this->iActionCode, $aTransitions))
1506				{
1507					// Temporary optimization possible: since the current implementation
1508					// of IsActionAllowed does not perform a 'per instance' check, we could
1509					// skip this second validation phase and assume it would return UR_ALLOWED_YES
1510					$oObjSet = DBObjectSet::FromArray($sClass, array($oObj));
1511					if (!UserRights::IsStimulusAllowed($sClass, $this->iActionCode, $oObjSet))
1512					{
1513						$this->aAllowedIDs[$oObj->GetKey()] = false;
1514					}
1515					else
1516					{
1517						// Assume UR_ALLOWED_YES, since there is just one object !
1518						$this->aAllowedIDs[$oObj->GetKey()] = true;
1519						$this->iState = $oObj->GetState();
1520						$this->iAllowedCount++;
1521					}
1522				}
1523				else
1524				{
1525					$this->aAllowedIDs[$oObj->GetKey()] = false;
1526				}
1527			}
1528		}
1529
1530		if ($this->iAllowedCount == $oSet->Count())
1531		{
1532			$iActionAllowed = UR_ALLOWED_YES;
1533		}
1534		if ($this->iAllowedCount == 0)
1535		{
1536			$iActionAllowed = UR_ALLOWED_NO;
1537		}
1538
1539		return $iActionAllowed;
1540	}
1541
1542	public function GetState()
1543	{
1544		return $this->iState;
1545	}
1546}
1547
1548/**
1549 * Self-register extension to allow the automatic creation & update of CAS users
1550 *
1551 * @package iTopORM
1552 *
1553 */
1554class CAS_SelfRegister implements iSelfRegister
1555{
1556	/**
1557	 * Called when no user is found in iTop for the corresponding 'name'. This method
1558	 * can create/synchronize the User in iTop with an external source (such as AD/LDAP) on the fly
1559	 * @param string $sName The CAS authenticated user name
1560	 * @param string $sPassword Ignored
1561	 * @param string $sLoginMode The login mode used (cas|form|basic|url)
1562	 * @param string $sAuthentication The authentication method used
1563	 * @return bool true if the user is a valid one, false otherwise
1564	 */
1565	public static function CheckCredentialsAndCreateUser($sName, $sPassword, $sLoginMode, $sAuthentication)
1566	{
1567		$bOk = true;
1568		if ($sLoginMode != 'cas') return false; // Must be authenticated via CAS
1569
1570		$sCASMemberships = MetaModel::GetConfig()->Get('cas_memberof');
1571		$bFound =  false;
1572		if (!empty($sCASMemberships))
1573		{
1574			if (phpCAS::hasAttribute('memberOf'))
1575			{
1576				// A list of groups is specified, the user must a be member of (at least) one of them to pass
1577				$aCASMemberships = array();
1578				$aTmp = explode(';', $sCASMemberships);
1579				setlocale(LC_ALL, "en_US.utf8"); // !!! WARNING: this is needed to have  the iconv //TRANSLIT working fine below !!!
1580				foreach($aTmp as $sGroupName)
1581				{
1582					$aCASMemberships[] = trim(iconv('UTF-8', 'ASCII//TRANSLIT', $sGroupName)); // Just in case remove accents and spaces...
1583				}
1584
1585				$aMemberOf = phpCAS::getAttribute('memberOf');
1586				if (!is_array($aMemberOf)) $aMemberOf = array($aMemberOf); // Just one entry, turn it into an array
1587				$aFilteredGroupNames = array();
1588				foreach($aMemberOf as $sGroupName)
1589				{
1590					phpCAS::log("Info: user if a member of the group: ".$sGroupName);
1591					$sGroupName = trim(iconv('UTF-8', 'ASCII//TRANSLIT', $sGroupName)); // Remove accents and spaces as well
1592					$aFilteredGroupNames[] = $sGroupName;
1593					$bIsMember = false;
1594					foreach($aCASMemberships as $sCASPattern)
1595					{
1596						if (self::IsPattern($sCASPattern))
1597						{
1598							if (preg_match($sCASPattern, $sGroupName))
1599							{
1600								$bIsMember = true;
1601								break;
1602							}
1603						}
1604						else if ($sCASPattern == $sGroupName)
1605						{
1606							$bIsMember = true;
1607							break;
1608						}
1609					}
1610					if ($bIsMember)
1611					{
1612						$bCASUserSynchro = MetaModel::GetConfig()->Get('cas_user_synchro');
1613						if ($bCASUserSynchro)
1614						{
1615							// If needed create a new user for this email/profile
1616							phpCAS::log('Info: cas_user_synchro is ON');
1617							$bOk = self::CreateCASUser(phpCAS::getUser(), $aMemberOf);
1618							if($bOk)
1619							{
1620								$bFound = true;
1621							}
1622							else
1623							{
1624								phpCAS::log("User ".phpCAS::getUser()." cannot be created in iTop. Logging off...");
1625							}
1626						}
1627						else
1628						{
1629							phpCAS::log('Info: cas_user_synchro is OFF');
1630							$bFound = true;
1631						}
1632						break;
1633					}
1634				}
1635				if($bOk && !$bFound)
1636				{
1637					phpCAS::log("User ".phpCAS::getUser().", none of his/her groups (".implode('; ', $aFilteredGroupNames).") match any of the required groups: ".implode('; ', $aCASMemberships));
1638				}
1639			}
1640			else
1641			{
1642				// Too bad, the user is not part of any of the group => not allowed
1643				phpCAS::log("No 'memberOf' attribute found for user ".phpCAS::getUser().". Are you using the SAML protocol (S1) ?");
1644			}
1645		}
1646		else
1647		{
1648			// No membership: no way to create the user that should exist prior to authentication
1649			phpCAS::log("User ".phpCAS::getUser().": missing user account in iTop (or iTop badly configured, Cf setting cas_memberof)");
1650			$bFound = false;
1651		}
1652
1653		if (!$bFound)
1654		{
1655			// The user is not part of the allowed groups, => log out
1656			$sUrl = utils::GetAbsoluteUrlAppRoot().'pages/UI.php';
1657			$sCASLogoutUrl = MetaModel::GetConfig()->Get('cas_logout_redirect_service');
1658			if (empty($sCASLogoutUrl))
1659			{
1660				$sCASLogoutUrl = $sUrl;
1661			}
1662			phpCAS::logoutWithRedirectService($sCASLogoutUrl); // Redirects to the CAS logout page
1663			// Will never return !
1664		}
1665		return $bFound;
1666	}
1667
1668	/**
1669	 * Called after the user has been authenticated and found in iTop. This method can
1670	 * Update the user's definition (profiles...) on the fly to keep it in sync with an external source
1671	 * @param User $oUser The user to update/synchronize
1672	 * @param string $sLoginMode The login mode used (cas|form|basic|url)
1673	 * @param string $sAuthentication The authentication method used
1674	 * @return void
1675	 */
1676	public static function UpdateUser(User $oUser, $sLoginMode, $sAuthentication)
1677	{
1678		$bCASUpdateProfiles = MetaModel::GetConfig()->Get('cas_update_profiles');
1679		if (($sLoginMode == 'cas') && $bCASUpdateProfiles && (phpCAS::hasAttribute('memberOf')))
1680		{
1681			$aMemberOf = phpCAS::getAttribute('memberOf');
1682			if (!is_array($aMemberOf)) $aMemberOf = array($aMemberOf); // Just one entry, turn it into an array
1683
1684			return self::SetProfilesFromCAS($oUser, $aMemberOf);
1685		}
1686		// No groups defined in CAS or not CAS at all: do nothing...
1687		return true;
1688	}
1689
1690	/**
1691	 * Helper method to create a CAS based user
1692	 * @param string $sEmail
1693	 * @param array $aGroups
1694	 * @return bool true on success, false otherwise
1695	 */
1696	protected static function CreateCASUser($sEmail, $aGroups)
1697	{
1698		if (!MetaModel::IsValidClass('URP_Profiles'))
1699		{
1700			phpCAS::log("URP_Profiles is not a valid class. Automatic creation of Users is not supported in this context, sorry.");
1701			return false;
1702		}
1703
1704		$oUser = MetaModel::GetObjectByName('UserExternal', $sEmail, false);
1705		if ($oUser == null)
1706		{
1707			// Create the user, link it to a contact
1708			phpCAS::log("Info: the user '$sEmail' does not exist. A new UserExternal will be created.");
1709			$oSearch = new DBObjectSearch('Person');
1710			$oSearch->AddCondition('email', $sEmail);
1711			$oSet = new DBObjectSet($oSearch);
1712			$iContactId = 0;
1713			switch($oSet->Count())
1714			{
1715				case 0:
1716				phpCAS::log("Error: found no contact with the email: '$sEmail'. Cannot create the user in iTop.");
1717				return false;
1718
1719				case 1:
1720				$oContact = $oSet->Fetch();
1721				$iContactId = $oContact->GetKey();
1722				phpCAS::log("Info: Found 1 contact '".$oContact->GetName()."' (id=$iContactId) corresponding to the email '$sEmail'.");
1723				break;
1724
1725				default:
1726				phpCAS::log("Error: ".$oSet->Count()." contacts have the same email: '$sEmail'. Cannot create a user for this email.");
1727				return false;
1728			}
1729
1730			$oUser = new UserExternal();
1731			$oUser->Set('login', $sEmail);
1732			$oUser->Set('contactid', $iContactId);
1733			$oUser->Set('language', MetaModel::GetConfig()->GetDefaultLanguage());
1734		}
1735		else
1736		{
1737			phpCAS::log("Info: the user '$sEmail' already exists (id=".$oUser->GetKey().").");
1738		}
1739
1740		// Now synchronize the profiles
1741		if (!self::SetProfilesFromCAS($oUser, $aGroups))
1742		{
1743			return false;
1744		}
1745		else
1746		{
1747			if ($oUser->IsNew() || $oUser->IsModified())
1748			{
1749				$oMyChange = MetaModel::NewObject("CMDBChange");
1750				$oMyChange->Set("date", time());
1751				$oMyChange->Set("userinfo", 'CAS/LDAP Synchro');
1752				$oMyChange->DBInsert();
1753				if ($oUser->IsNew())
1754				{
1755					$oUser->DBInsertTracked($oMyChange);
1756				}
1757				else
1758				{
1759					$oUser->DBUpdateTracked($oMyChange);
1760				}
1761			}
1762
1763			return true;
1764		}
1765	}
1766
1767	protected static function SetProfilesFromCAS($oUser, $aGroups)
1768	{
1769		if (!MetaModel::IsValidClass('URP_Profiles'))
1770		{
1771			phpCAS::log("URP_Profiles is not a valid class. Automatic creation of Users is not supported in this context, sorry.");
1772			return false;
1773		}
1774
1775		// read all the existing profiles
1776		$oProfilesSearch = new DBObjectSearch('URP_Profiles');
1777		$oProfilesSet = new DBObjectSet($oProfilesSearch);
1778		$aAllProfiles = array();
1779		while($oProfile = $oProfilesSet->Fetch())
1780		{
1781			$aAllProfiles[strtolower($oProfile->GetName())] = $oProfile->GetKey();
1782		}
1783
1784		// Translate the CAS/LDAP group names into iTop profile names
1785		$aProfiles = array();
1786		$sPattern = MetaModel::GetConfig()->Get('cas_profile_pattern');
1787		foreach($aGroups as $sGroupName)
1788		{
1789			if (preg_match($sPattern, $sGroupName, $aMatches))
1790			{
1791				if (array_key_exists(strtolower($aMatches[1]), $aAllProfiles))
1792				{
1793					$aProfiles[] = $aAllProfiles[strtolower($aMatches[1])];
1794					phpCAS::log("Info: Adding the profile '{$aMatches[1]}' from CAS.");
1795				}
1796				else
1797				{
1798					phpCAS::log("Warning: {$aMatches[1]} is not a valid iTop profile (extracted from group name: '$sGroupName'). Ignored.");
1799				}
1800			}
1801			else
1802			{
1803				phpCAS::log("Info: The CAS group '$sGroupName' does not seem to match an iTop pattern. Ignored.");
1804			}
1805		}
1806		if (count($aProfiles) == 0)
1807		{
1808			phpCAS::log("Info: The user '".$oUser->GetName()."' has no profiles retrieved from CAS. Default profile(s) will be used.");
1809
1810			// Second attempt: check if there is/are valid default profile(s)
1811			$sCASDefaultProfiles = MetaModel::GetConfig()->Get('cas_default_profiles');
1812			$aCASDefaultProfiles = explode(';', $sCASDefaultProfiles);
1813			foreach($aCASDefaultProfiles as $sDefaultProfileName)
1814			{
1815				if (array_key_exists(strtolower($sDefaultProfileName), $aAllProfiles))
1816				{
1817					$aProfiles[] = $aAllProfiles[strtolower($sDefaultProfileName)];
1818					phpCAS::log("Info: Adding the default profile '".$aAllProfiles[strtolower($sDefaultProfileName)]."' from CAS.");
1819				}
1820				else
1821				{
1822					phpCAS::log("Warning: the default profile {$sDefaultProfileName} is not a valid iTop profile. Ignored.");
1823				}
1824			}
1825
1826			if (count($aProfiles) == 0)
1827			{
1828				phpCAS::log("Error: The user '".$oUser->GetName()."' has no profiles in iTop, and therefore cannot be created.");
1829				return false;
1830			}
1831		}
1832
1833		// Now synchronize the profiles
1834		$oProfilesSet = DBObjectSet::FromScratch('URP_UserProfile');
1835		foreach($aProfiles as $iProfileId)
1836		{
1837			$oLink = new URP_UserProfile();
1838			$oLink->Set('profileid', $iProfileId);
1839			$oLink->Set('reason', 'CAS/LDAP Synchro');
1840			$oProfilesSet->AddObject($oLink);
1841		}
1842		$oUser->Set('profile_list', $oProfilesSet);
1843		phpCAS::log("Info: the user '".$oUser->GetName()."' (id=".$oUser->GetKey().") now has the following profiles: '".implode("', '", $aProfiles)."'.");
1844		if ($oUser->IsModified())
1845		{
1846			$oMyChange = MetaModel::NewObject("CMDBChange");
1847			$oMyChange->Set("date", time());
1848			$oMyChange->Set("userinfo", 'CAS/LDAP Synchro');
1849			$oMyChange->DBInsert();
1850			if ($oUser->IsNew())
1851			{
1852				$oUser->DBInsertTracked($oMyChange);
1853			}
1854			else
1855			{
1856				$oUser->DBUpdateTracked($oMyChange);
1857			}
1858		}
1859
1860		return true;
1861	}
1862	/**
1863	 * Helper function to check if the supplied string is a litteral string or a regular expression pattern
1864	 * @param string $sCASPattern
1865	 * @return bool True if it's a regular expression pattern, false otherwise
1866	 */
1867	protected static function IsPattern($sCASPattern)
1868	{
1869		if ((substr($sCASPattern, 0, 1) == '/') && (substr($sCASPattern, -1) == '/'))
1870		{
1871			// the string is enclosed by slashes, let's assume it's a pattern
1872			return true;
1873		}
1874		else
1875		{
1876			return false;
1877		}
1878	}
1879}
1880
1881// By default enable the 'CAS_SelfRegister' defined above
1882UserRights::SelectSelfRegister('CAS_SelfRegister');