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');