Changeset View
Changeset View
Standalone View
Standalone View
src/include/Kolab2FA/Storage/LDAP.php
- This file was added.
<?php | |||||
/** | |||||
* Storage backend to store 2-Factor-Authentication settings in LDAP | |||||
* | |||||
* @author Thomas Bruederli <bruederli@kolabsys.com> | |||||
* | |||||
* Copyright (C) 2015, Kolab Systems AG <contact@kolabsys.com> | |||||
* | |||||
* This program is free software: you can redistribute it and/or modify | |||||
* it under the terms of the GNU Affero General Public License as | |||||
* published by the Free Software Foundation, either version 3 of the | |||||
* License, or (at your option) any later version. | |||||
* | |||||
* This program is distributed in the hope that it will be useful, | |||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||||
* GNU Affero General Public License for more details. | |||||
* | |||||
* You should have received a copy of the GNU Affero General Public License | |||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||||
*/ | |||||
namespace Kolab2FA\Storage; | |||||
use \Net_LDAP3; | |||||
use \Kolab2FA\Log\Logger; | |||||
class LDAP extends Base | |||||
{ | |||||
public $userdn; | |||||
public $ready; | |||||
private $cache = array(); | |||||
private $ldapcache = array(); | |||||
private $conn; | |||||
private $error; | |||||
public function init(array $config) | |||||
{ | |||||
parent::init($config); | |||||
$this->conn = new Net_LDAP3($config); | |||||
$this->conn->config_set('log_hook', array($this, 'log')); | |||||
$this->conn->connect(); | |||||
$bind_pass = $this->config['bind_pass']; | |||||
$bind_user = $this->config['bind_user']; | |||||
$bind_dn = $this->config['bind_dn']; | |||||
$this->ready = $this->conn->bind($bind_dn, $bind_pass); | |||||
if (!$this->ready) { | |||||
throw new Exception("LDAP storage not ready: " . $this->error); | |||||
} | |||||
} | |||||
/** | |||||
* List/set methods activated for this user | |||||
*/ | |||||
public function enumerate($active = true) | |||||
{ | |||||
$filter = $this->parse_vars($this->config['filter'], '*'); | |||||
$base_dn = $this->parse_vars($this->config['base_dn'], '*'); | |||||
$scope = $this->config['scope'] ?: 'sub'; | |||||
$ids = array(); | |||||
if ($this->ready && ($result = $this->conn->search($base_dn, $filter, $scope, array($this->config['fieldmap']['id'], $this->config['fieldmap']['active'])))) { | |||||
foreach ($result as $dn => $entry) { | |||||
$rec = $this->field_mapping($dn, Net_LDAP3::normalize_entry($entry, true)); | |||||
if (!empty($rec['id']) && ($active === null || $active == $rec['active'])) { | |||||
$ids[] = $rec['id']; | |||||
} | |||||
} | |||||
} | |||||
// TODO: cache this in memory | |||||
return $ids; | |||||
} | |||||
/** | |||||
* Read data for the given key | |||||
*/ | |||||
public function read($key) | |||||
{ | |||||
if (!isset($this->cache[$key])) { | |||||
$this->cache[$key] = $this->get_ldap_record($this->username, $key); | |||||
} | |||||
return $this->cache[$key]; | |||||
} | |||||
/** | |||||
* Save data for the given key | |||||
*/ | |||||
public function write($key, $value) | |||||
{ | |||||
$success = false; | |||||
$ldap_attrs = array(); | |||||
if (is_array($value)) { | |||||
// add some default values | |||||
$value += (array)$this->config['defaults'] + array('active' => false, 'username' => $this->username, 'userdn' => $this->userdn); | |||||
foreach ($value as $k => $val) { | |||||
if ($attr = $this->config['fieldmap'][$k]) { | |||||
$ldap_attrs[$attr] = $this->value_mapping($k, $val, false); | |||||
} | |||||
} | |||||
} | |||||
else { | |||||
// invalid data structure | |||||
return false; | |||||
} | |||||
// update existing record | |||||
if ($rec = $this->get_ldap_record($this->username, $key)) { | |||||
$old_attrs = $rec['_raw']; | |||||
$new_attrs = array_merge($old_attrs, $ldap_attrs); | |||||
$result = $this->conn->modify_entry($rec['_dn'], $old_attrs, $new_attrs); | |||||
$success = !empty($result); | |||||
} | |||||
// insert new record | |||||
else if ($this->ready) { | |||||
$entry_dn = $this->get_entry_dn($this->username, $key); | |||||
// add object class attribute | |||||
$me = $this; | |||||
$ldap_attrs['objectclass'] = array_map(function($cls) use ($me, $key) { | |||||
return $me->parse_vars($cls, $key); | |||||
}, (array)$this->config['objectclass']); | |||||
$success = $this->conn->add_entry($entry_dn, $ldap_attrs); | |||||
} | |||||
if ($success) { | |||||
$this->cache[$key] = $value; | |||||
$this->ldapcache = array(); | |||||
// cleanup: remove disabled/inactive/temporary entries | |||||
if ($value['active']) { | |||||
foreach ($this->enumerate(false) as $id) { | |||||
if ($id != $key) { | |||||
$this->remove($id); | |||||
} | |||||
} | |||||
// set user roles according to active factors | |||||
$this->set_user_roles(); | |||||
} | |||||
} | |||||
return $success; | |||||
} | |||||
/** | |||||
* Remove the data stored for the given key | |||||
*/ | |||||
public function remove($key) | |||||
{ | |||||
if ($this->ready) { | |||||
$entry_dn = $this->get_entry_dn($this->username, $key); | |||||
$success = $this->conn->delete_entry($entry_dn); | |||||
// set user roles according to active factors | |||||
if ($success) { | |||||
$this->set_user_roles(); | |||||
} | |||||
return $success; | |||||
} | |||||
return false; | |||||
} | |||||
/** | |||||
* Set username to store data for | |||||
*/ | |||||
public function set_username($username) | |||||
{ | |||||
parent::set_username($username); | |||||
// reset cached values | |||||
$this->cache = array(); | |||||
$this->ldapcache = array(); | |||||
} | |||||
/** | |||||
* | |||||
*/ | |||||
protected function set_user_roles() | |||||
{ | |||||
if (!$this->ready || !$this->userdn || empty($this->config['user_roles'])) { | |||||
return false; | |||||
} | |||||
$auth_roles = array(); | |||||
foreach ($this->enumerate(true) as $id) { | |||||
foreach ($this->config['user_roles'] as $prefix => $role) { | |||||
if (strpos($id, $prefix) === 0) { | |||||
$auth_roles[] = $role; | |||||
} | |||||
} | |||||
} | |||||
$role_attr = $this->config['fieldmap']['roles'] ?: 'nsroledn'; | |||||
if ($user_attrs = $this->conn->get_entry($this->userdn, array($role_attr))) { | |||||
$internals = array_values($this->config['user_roles']); | |||||
$new_attrs = $old_attrs = Net_LDAP3::normalize_entry($user_attrs); | |||||
$new_attrs[$role_attr] = array_merge( | |||||
array_unique($auth_roles), | |||||
array_filter((array)$old_attrs[$role_attr], function($f) use ($internals) { return !in_array($f, $internals); }) | |||||
); | |||||
$result = $this->conn->modify_entry($this->userdn, $old_attrs, $new_attrs); | |||||
return !empty($result); | |||||
} | |||||
return false; | |||||
} | |||||
/** | |||||
* Fetches user data from LDAP addressbook | |||||
*/ | |||||
protected function get_ldap_record($user, $key) | |||||
{ | |||||
$entry_dn = $this->get_entry_dn($user, $key); | |||||
if (!isset($this->ldapcache[$entry_dn])) { | |||||
$this->ldapcache[$entry_dn] = array(); | |||||
if ($this->ready && ($entry = $this->conn->get_entry($entry_dn, array_values($this->config['fieldmap'])))) { | |||||
$this->ldapcache[$entry_dn] = $this->field_mapping($entry_dn, Net_LDAP3::normalize_entry($entry, true)); | |||||
} | |||||
} | |||||
return $this->ldapcache[$entry_dn]; | |||||
} | |||||
/** | |||||
* Compose a full DN for the given record identifier | |||||
*/ | |||||
protected function get_entry_dn($user, $key) | |||||
{ | |||||
$base_dn = $this->parse_vars($this->config['base_dn'], $key); | |||||
return sprintf('%s=%s,%s', $this->config['rdn'], Net_LDAP3::quote_string($key, true), $base_dn); | |||||
} | |||||
/** | |||||
* Maps LDAP attributes to defined fields | |||||
*/ | |||||
protected function field_mapping($dn, $entry) | |||||
{ | |||||
$entry['_dn'] = $dn; | |||||
$entry['_raw'] = $entry; | |||||
// fields mapping | |||||
foreach ($this->config['fieldmap'] as $field => $attr) { | |||||
$attr_lc = strtolower($attr); | |||||
if (isset($entry[$attr_lc])) { | |||||
$entry[$field] = $this->value_mapping($field, $entry[$attr_lc], true); | |||||
} | |||||
else if (isset($entry[$attr])) { | |||||
$entry[$field] = $this->value_mapping($field, $entry[$attr], true); | |||||
} | |||||
} | |||||
return $entry; | |||||
} | |||||
/** | |||||
* | |||||
*/ | |||||
protected function value_mapping($attr, $value, $reverse = false) | |||||
{ | |||||
if ($map = $this->config['valuemap'][$attr]) { | |||||
if ($reverse) { | |||||
$map = array_flip($map); | |||||
} | |||||
if (is_array($value)) { | |||||
$value = array_filter(array_map(function($val) use ($map) { | |||||
return $map[$val]; | |||||
}, $value)); | |||||
} | |||||
else { | |||||
$value = $map[$value]; | |||||
} | |||||
} | |||||
// convert (date) type | |||||
switch ($this->config['attrtypes'][$attr]) { | |||||
case 'datetime': | |||||
$ts = is_numeric($value) ? $value : strtotime($value); | |||||
if ($ts) { | |||||
$value = gmdate($reverse ? 'U' : 'YmdHi\Z', $ts); | |||||
} | |||||
break; | |||||
case 'integer': | |||||
$value = intval($value); | |||||
break; | |||||
} | |||||
return $value; | |||||
} | |||||
/** | |||||
* Prepares filter query for LDAP search | |||||
*/ | |||||
protected function parse_vars($str, $key) | |||||
{ | |||||
$user = $this->username; | |||||
if (strpos($user, '@') > 0) { | |||||
list($u, $d) = explode('@', $user); | |||||
} | |||||
else if ($this->userdn) { | |||||
$u = $this->userdn; | |||||
$d = trim(str_replace(',dc=', '.', substr($u, strpos($u, ',dc='))), '.'); | |||||
} | |||||
if ($this->userdn) { | |||||
$user = $this->userdn; | |||||
} | |||||
// build hierarchal domain string | |||||
$dc = $this->conn->domain_root_dn($d); | |||||
$class = $this->config['classmap'] ? $this->config['classmap']['*'] : '*'; | |||||
// map key to objectclass | |||||
if (is_array($this->config['classmap'])) { | |||||
foreach ($this->config['classmap'] as $k => $c) { | |||||
if (strpos($key, $k) === 0) { | |||||
$class = $c; | |||||
break; | |||||
} | |||||
} | |||||
} | |||||
$replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $user, '%u' => $u, '%c' => $class); | |||||
return strtr($str, $replaces); | |||||
} | |||||
} |