* * Copyright (C) 2015, Kolab Systems AG * * 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 . */ namespace Kolab2FA\Storage; use \Net_LDAP3; use \Kolab2FA\Log\Logger; class LDAP extends Base { public $userdn; 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); } }