1<?php 2/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ 3 4namespace Icinga\Forms\Config\UserBackend; 5 6use Exception; 7use Icinga\Data\ResourceFactory; 8use Icinga\Protocol\Ldap\LdapCapabilities; 9use Icinga\Protocol\Ldap\LdapConnection; 10use Icinga\Protocol\Ldap\LdapException; 11use Icinga\Web\Form; 12 13/** 14 * Form class for adding/modifying LDAP user backends 15 */ 16class LdapBackendForm extends Form 17{ 18 /** 19 * The ldap resource names the user can choose from 20 * 21 * @var array 22 */ 23 protected $resources; 24 25 /** 26 * Default values for the form elements 27 * 28 * @var string[] 29 */ 30 protected $suggestions = array(); 31 32 /** 33 * Cache for {@link getLdapCapabilities()} 34 * 35 * @var LdapCapabilities 36 */ 37 protected $ldapCapabilities; 38 39 /** 40 * Initialize this form 41 */ 42 public function init() 43 { 44 $this->setName('form_config_authbackend_ldap'); 45 } 46 47 /** 48 * Set the resource names the user can choose from 49 * 50 * @param array $resources The resources to choose from 51 * 52 * @return $this 53 */ 54 public function setResources(array $resources) 55 { 56 $this->resources = $resources; 57 return $this; 58 } 59 60 /** 61 * Create and add elements to this form 62 * 63 * @param array $formData 64 */ 65 public function createElements(array $formData) 66 { 67 $isAd = isset($formData['type']) ? $formData['type'] === 'msldap' : false; 68 69 $this->addElement( 70 'text', 71 'name', 72 array( 73 'required' => true, 74 'label' => $this->translate('Backend Name'), 75 'description' => $this->translate( 76 'The name of this authentication provider that is used to differentiate it from others.' 77 ), 78 'value' => $this->getSuggestion('name') 79 ) 80 ); 81 $this->addElement( 82 'select', 83 'resource', 84 array( 85 'required' => true, 86 'label' => $this->translate('LDAP Connection'), 87 'description' => $this->translate( 88 'The LDAP connection to use for authenticating with this provider.' 89 ), 90 'multiOptions' => !empty($this->resources) 91 ? array_combine($this->resources, $this->resources) 92 : array(), 93 'value' => $this->getSuggestion('resource') 94 ) 95 ); 96 97 if (! $isAd && !empty($this->resources)) { 98 $this->addElement( 99 'button', 100 'discovery_btn', 101 array( 102 'class' => 'control-button', 103 'type' => 'submit', 104 'value' => 'discovery_btn', 105 'label' => $this->translate('Discover', 'A button to discover LDAP capabilities'), 106 'title' => $this->translate( 107 'Push to fill in the chosen connection\'s default settings.' 108 ), 109 'decorators' => array( 110 array('ViewHelper', array('separator' => '')), 111 array('Spinner'), 112 array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls')) 113 ), 114 'formnovalidate' => 'formnovalidate' 115 ) 116 ); 117 } 118 119 if ($isAd) { 120 // ActiveDirectory defaults 121 $userClass = 'user'; 122 $filter = '!(objectClass=computer)'; 123 $userNameAttribute = 'sAMAccountName'; 124 } else { 125 // OpenLDAP defaults 126 $userClass = 'inetOrgPerson'; 127 $filter = null; 128 $userNameAttribute = 'uid'; 129 } 130 131 $this->addElement( 132 'text', 133 'user_class', 134 array( 135 'preserveDefault' => true, 136 'required' => ! $isAd, 137 'ignore' => $isAd, 138 'disabled' => $isAd ?: null, 139 'label' => $this->translate('LDAP User Object Class'), 140 'description' => $this->translate('The object class used for storing users on the LDAP server.'), 141 'value' => $this->getSuggestion('user_class', $userClass) 142 ) 143 ); 144 $this->addElement( 145 'text', 146 'filter', 147 array( 148 'preserveDefault' => true, 149 'allowEmpty' => true, 150 'value' => $this->getSuggestion('filter', $filter), 151 'label' => $this->translate('LDAP Filter'), 152 'description' => $this->translate( 153 'An additional filter to use when looking up users using the specified connection. ' 154 . 'Leave empty to not to use any additional filter rules.' 155 ), 156 'requirement' => $this->translate( 157 'The filter needs to be expressed as standard LDAP expression.' 158 . ' (e.g. &(foo=bar)(bar=foo) or foo=bar)' 159 ), 160 'validators' => array( 161 array( 162 'Callback', 163 false, 164 array( 165 'callback' => function ($v) { 166 // This is not meant to be a full syntax check. It will just 167 // ensure that we can safely strip unnecessary parentheses. 168 $v = trim($v); 169 return ! $v || $v[0] !== '(' || ( 170 strpos($v, ')(') !== false ? substr($v, -2) === '))' : substr($v, -1) === ')' 171 ); 172 }, 173 'messages' => array( 174 'callbackValue' => $this->translate('The filter is invalid. Please check your syntax.') 175 ) 176 ) 177 ) 178 ) 179 ) 180 ); 181 $this->addElement( 182 'text', 183 'user_name_attribute', 184 array( 185 'preserveDefault' => true, 186 'required' => ! $isAd, 187 'ignore' => $isAd, 188 'disabled' => $isAd ?: null, 189 'label' => $this->translate('LDAP User Name Attribute'), 190 'description' => $this->translate( 191 'The attribute name used for storing the user name on the LDAP server.' 192 ), 193 'value' => $this->getSuggestion('user_name_attribute', $userNameAttribute) 194 ) 195 ); 196 $this->addElement( 197 'hidden', 198 'backend', 199 array( 200 'disabled' => true, 201 'value' => $this->getSuggestion('backend', $isAd ? 'msldap' : 'ldap') 202 ) 203 ); 204 $this->addElement( 205 'text', 206 'base_dn', 207 array( 208 'preserveDefault' => true, 209 'required' => false, 210 'label' => $this->translate('LDAP Base DN'), 211 'description' => $this->translate( 212 'The path where users can be found on the LDAP server. Leave ' . 213 'empty to select all users available using the specified connection.' 214 ), 215 'value' => $this->getSuggestion('base_dn') 216 ) 217 ); 218 219 $this->addElement( 220 'text', 221 'domain', 222 array( 223 'label' => $this->translate('Domain'), 224 'description' => $this->translate( 225 'The domain the LDAP server is responsible for upon authentication.' 226 . ' Note that if you specify a domain here,' 227 . ' the LDAP backend only authenticates users who specify a domain upon login.' 228 . ' If the domain of the user matches the domain configured here, this backend is responsible for' 229 . ' authenticating the user based on the username without the domain part.' 230 . ' If your LDAP backend holds usernames with a domain part or if it is not necessary in your setup' 231 . ' to authenticate users based on their domains, leave this field empty.' 232 ), 233 'preserveDefault' => true, 234 'value' => $this->getSuggestion('domain') 235 ) 236 ); 237 238 $this->addElement( 239 'button', 240 'btn_discover_domain', 241 array( 242 'class' => 'control-button', 243 'type' => 'submit', 244 'value' => 'discovery_btn', 245 'label' => $this->translate('Discover the domain'), 246 'title' => $this->translate( 247 'Push to disover and fill in the domain of the LDAP server.' 248 ), 249 'decorators' => array( 250 array('ViewHelper', array('separator' => '')), 251 array('Spinner'), 252 array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls')) 253 ), 254 'formnovalidate' => 'formnovalidate' 255 ) 256 ); 257 } 258 259 public function isValidPartial(array $formData) 260 { 261 $isAd = isset($formData['type']) && $formData['type'] === 'msldap'; 262 $baseDn = null; 263 $hasAdOid = false; 264 $discoverySuccessful = false; 265 266 if (! $isAd && ! empty($this->resources) && isset($formData['discovery_btn']) 267 && $formData['discovery_btn'] === 'discovery_btn') { 268 $discoverySuccessful = true; 269 try { 270 $capabilities = $this->getLdapCapabilities($formData); 271 $baseDn = $capabilities->getDefaultNamingContext(); 272 $hasAdOid = $capabilities->isActiveDirectory(); 273 } catch (Exception $e) { 274 $this->warning(sprintf( 275 $this->translate('Failed to discover the chosen LDAP connection: %s'), 276 $e->getMessage() 277 )); 278 $discoverySuccessful = false; 279 } 280 } 281 282 if ($discoverySuccessful) { 283 if ($isAd || $hasAdOid) { 284 // ActiveDirectory defaults 285 $userClass = 'user'; 286 $filter = '!(objectClass=computer)'; 287 $userNameAttribute = 'sAMAccountName'; 288 } else { 289 // OpenLDAP defaults 290 $userClass = 'inetOrgPerson'; 291 $filter = null; 292 $userNameAttribute = 'uid'; 293 } 294 295 $formData['user_class'] = $userClass; 296 297 if (! isset($formData['filter']) || $formData['filter'] === '') { 298 $formData['filter'] = $filter; 299 } 300 301 $formData['user_name_attribute'] = $userNameAttribute; 302 303 if ($baseDn !== null && (! isset($formData['base_dn']) || $formData['base_dn'] === '')) { 304 $formData['base_dn'] = $baseDn; 305 } 306 } 307 308 if (isset($formData['btn_discover_domain']) && $formData['btn_discover_domain'] === 'discovery_btn') { 309 try { 310 $formData['domain'] = $this->discoverDomain($formData); 311 } catch (LdapException $e) { 312 $this->error($e->getMessage()); 313 } 314 } 315 316 return parent::isValidPartial($formData); 317 } 318 319 /** 320 * Get the LDAP capabilities of either the resource specified by the user or the default one 321 * 322 * @param string[] $formData 323 * 324 * @return LdapCapabilities 325 */ 326 protected function getLdapCapabilities(array $formData) 327 { 328 if ($this->ldapCapabilities === null) { 329 $this->ldapCapabilities = ResourceFactory::create( 330 isset($formData['resource']) ? $formData['resource'] : reset($this->resources) 331 )->bind()->getCapabilities(); 332 } 333 334 return $this->ldapCapabilities; 335 } 336 337 /** 338 * Discover the domain the LDAP server is responsible for 339 * 340 * @param string[] $formData 341 * 342 * @return string 343 */ 344 protected function discoverDomain(array $formData) 345 { 346 $cap = $this->getLdapCapabilities($formData); 347 348 if ($cap->isActiveDirectory()) { 349 $netBiosName = $cap->getNetBiosName(); 350 if ($netBiosName !== null) { 351 return $netBiosName; 352 } 353 } 354 355 return $this->defaultNamingContextToFQDN($cap); 356 } 357 358 /** 359 * Get the default naming context as FQDN 360 * 361 * @param LdapCapabilities $cap 362 * 363 * @return string|null 364 */ 365 protected function defaultNamingContextToFQDN(LdapCapabilities $cap) 366 { 367 $defaultNamingContext = $cap->getDefaultNamingContext(); 368 if ($defaultNamingContext !== null) { 369 $validationMatches = array(); 370 if (preg_match('/\bdc=[^,]+(?:,dc=[^,]+)*$/', strtolower($defaultNamingContext), $validationMatches)) { 371 $splitMatches = array(); 372 preg_match_all('/dc=([^,]+)/', $validationMatches[0], $splitMatches); 373 return implode('.', $splitMatches[1]); 374 } 375 } 376 } 377 378 /** 379 * Get the default values for the form elements 380 * 381 * @return string[] 382 */ 383 public function getSuggestions() 384 { 385 return $this->suggestions; 386 } 387 388 /** 389 * Get the default value for the given form element or the given default 390 * 391 * @param string $element 392 * @param string $default 393 * 394 * @return string 395 */ 396 public function getSuggestion($element, $default = null) 397 { 398 return isset($this->suggestions[$element]) ? $this->suggestions[$element] : $default; 399 } 400 401 /** 402 * Set the default values for the form elements 403 * 404 * @param string[] $suggestions 405 * 406 * @return $this 407 */ 408 public function setSuggestions(array $suggestions) 409 { 410 $this->suggestions = $suggestions; 411 412 return $this; 413 } 414} 415