From 5dcf1e842f7bfb0f72fed6b3793f8c6a4bba7db7 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Mon, 30 Sep 2019 12:37:29 +0200 Subject: [PATCH] fix for stable Univention 4.4-2 REST API --- api/src/Accounts/Ldap.php | 11 ++- api/src/Accounts/Univention.php | 27 ++++++- api/src/Accounts/Univention/Udm.php | 115 ++++++++++++++++------------ api/src/Auth/Ldap.php | 2 +- 4 files changed, 102 insertions(+), 53 deletions(-) diff --git a/api/src/Accounts/Ldap.php b/api/src/Accounts/Ldap.php index 5845de725f..ddd4303fc3 100644 --- a/api/src/Accounts/Ldap.php +++ b/api/src/Accounts/Ldap.php @@ -557,10 +557,19 @@ class Ldap { $to_write['gidnumber'] = abs($data['account_id']); $to_write['cn'] = $data['account_lid']; - if (!empty($data['account_description']) || $old) + // do not overwrite exitsting description, if non is given + if (isset($data['account_description'])) { $to_write['description'] = !empty($data['account_description']) ? $data['account_description'] : array(); } + // to kope with various dependencies / requirements of objectclasses, simply write everything again + foreach($old as $name => $value) + { + if (!isset($to_write[$name]) && !in_array($name, ['dn', 'objectclass'])) + { + $to_write[$name] = $value; + } + } return $to_write; } diff --git a/api/src/Accounts/Univention.php b/api/src/Accounts/Univention.php index 8de389bf8f..8ca424b484 100644 --- a/api/src/Accounts/Univention.php +++ b/api/src/Accounts/Univention.php @@ -34,6 +34,27 @@ use EGroupware\Api; */ class Univention extends Ldap { + /** + * Name of mail attribute + */ + const MAIL_ATTR = 'mailprimaryaddress'; + + /** + * Constructor + * + * @param Api\Accounts $frontend reference to the frontend class, to be able to call it's methods if needed + */ + function __construct(Api\Accounts $frontend) + { + parent::__construct($frontend); + + // remove not supported groupOfNames to skip parent::save() first tries with it, before trying without + if (($groupOfNameKey = array_search('groupofnames', $this->requiredObjectClasses['group']))) + { + unset($this->requiredObjectClasses['group'][$groupOfNameKey]); + } + } + /** * Saves / adds the data of one account * @@ -82,9 +103,9 @@ class Univention extends Ldap $data['account_dn'] = $udm->createUser($data); $data['account_id'] = $this->name2id($data['account_lid'], 'account_lid', 'u'); } - // create new groups with given account_id via directory-manager too, to be able to set the RID - elseif($data['account_type'] === 'g' && !empty($data['account_id']) && - $data['account_id'] >= Ads::MIN_ACCOUNT_ID && !$this->id2name($data['account_id'])) + // create new groups incl. Samba objectclass and SID + elseif($data['account_type'] === 'g' && (empty($data['account_id']) || + $data['account_id'] >= Ads::MIN_ACCOUNT_ID && !$this->id2name($data['account_id']))) { $data['account_dn'] = $udm->createGroup($data); $data['account_id'] = $this->name2id($data['account_lid'], 'account_lid', 'g'); diff --git a/api/src/Accounts/Univention/Udm.php b/api/src/Accounts/Univention/Udm.php index 5e735423fb..0b9cd5dc7d 100644 --- a/api/src/Accounts/Univention/Udm.php +++ b/api/src/Accounts/Univention/Udm.php @@ -52,7 +52,7 @@ class Udm /** * Log webservice-calls to error_log */ - const DEBUG = true; + const DEBUG = false; /** * Constructor @@ -96,13 +96,16 @@ class Udm { $curl = curl_init(); - // fix error: Request argument "policies" is not a "dict" (PHP encodes empty arrays as array, not object) + // fix error like: Request argument "policies" is not a "dict" (PHP encodes empty arrays as array, not object) if (array_key_exists('policies', $_payload) && empty($_payload['policies'])) { $_payload['policies'] = new \stdClass(); // force "policies": {} } + if (is_array($_payload['properties']) && array_key_exists('umcProperty', $_payload['properties']) && empty($_payload['properties']['umcProperty'])) + { + $_payload['properties']['umcProperty'] = new \stdClass(); // force "umcProperty": {} + } - $headers = []; $curlOpts = [ CURLOPT_URL => 'https://'.$this->host.($_path[0] !== '/' ? self::PREFIX : '').$_path, CURLOPT_USERPWD => $this->user.':'.$this->config['ldap_root_pw'], @@ -115,29 +118,7 @@ class Udm //CURLOPT_FOLLOWLOCATION => 1, CURLOPT_TIMEOUT => 30, // setting a timeout of 30 seconds, as recommended by Univention CURLOPT_VERBOSE => 1, - CURLOPT_HEADERFUNCTION => - function($curl, $header) use (&$headers) - { - $len = strlen($header); - $header = explode(':', $header, 2); - if (count($header) < 2) - { - $headers[] = $header[0]; // http status - return $len; - } - $name = strtolower(trim($header[0])); - if (!array_key_exists($name, $headers)) - { - $headers[$name] = trim($header[1]); - } - else - { - $headers[$name] = [$headers[$name]]; - $headers[$name][] = trim($header[1]); - } - unset($curl); // not used, but required by function signature - return $len; - }, + CURLOPT_HEADER => 1, ]; if (isset($if_match)) { @@ -162,27 +143,32 @@ class Udm curl_setopt_array($curl, $curlOpts); $response = curl_exec($curl); + $header_size = curl_getinfo($curl, CURLINFO_HEADER_SIZE); + $headers = self::getHeaders(substr($response, 0, $header_size)); + $body = substr($response, $header_size); + $path = urldecode($_path); // for nicer error-messages - if (!$response || !($json = json_decode($response, true)) && json_last_error()) + if ($response === false || $body !== '' && !($json = json_decode($body, true)) && json_last_error()) { $info = curl_getinfo($curl); curl_close($curl); if ($retry > 0) { - error_log(__METHOD__."($path, $_method, ...) failed, retrying in 100ms, returned $response, headers=".json_encode($headers, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE).", curl_getinfo()=".json_encode($info, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE)); + error_log(__METHOD__."($path, $_method, ...) failed, retrying in 100ms, returned $body, headers=".json_encode($headers, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE).", curl_getinfo()=".json_encode($info, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE)); usleep(100000); return $this->call($_path, $_method, $_payload, $headers, $if_match, $return_dn, --$retry); } - error_log(__METHOD__."($path, $_method, ...) returned $response, headers=".json_encode($headers, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE).", curl_getinfo()=".json_encode($info, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE)); + error_log(__METHOD__."($path, $_method, ...) returned $body, headers=".json_encode($headers, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE).", curl_getinfo()=".json_encode($info, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE)); error_log(__METHOD__."($path, $_method, ".json_encode($_payload, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE).")"); - throw new UdmCantConnect("Error contacting Univention UDM REST Api ($_path)".($response ? ': '.json_last_error() : '')); + throw new UdmCantConnect("Error contacting Univention UDM REST Api ($path)".($response !== false ? ': '.json_last_error() : '')); } curl_close($curl); - if (!empty($json['error'])) + // error in json or non 20x http status + if (!empty($json['error']) || !preg_match('|^HTTP/[0-9.]+ 20|', $headers[0])) { error_log(__METHOD__."($path, $_method, ...) returned $response, headers=".json_encode($headers, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE)); error_log(__METHOD__."($path, $_method, ".json_encode($_payload, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE).")"); - throw new UdmError("UDM REST Api (".urldecode($_path)."): ".(empty($json['error']['message']) ? $response : $json['error']['message']), $json['error']['code']); + throw new UdmError("UDM REST Api ($path): ".(empty($json['error']['message']) ? $headers[0] : $json['error']['message']), $json['error']['code']); } if (self::DEBUG) { @@ -195,13 +181,54 @@ class Udm $matches = null; if (!isset($headers['location']) || !preg_match('|/([^/]+)$|', $headers['location'], $matches)) { - throw new UdmMissingLocation("UDM REST Api ($_path) did not return Location header!"); + throw new UdmMissingLocation("UDM REST Api ($path) did not return Location header!"); } return urldecode($matches[1]); } return $json; } + /** + * Convert header string in array of headers + * + * A "HTTP/1.1 100 Continue" is NOT returned! + * + * @param string $head + * @return array with name => value pairs, 0: http-status, value can be an array for multiple headers with same name + */ + protected static function getHeaders($head) + { + $headers = []; + foreach(explode("\r\n", $head) as $header) + { + if (empty($header)) continue; + + $parts = explode(':', $header, 2); + if (count($parts) < 2) + { + $headers[0] = $header; // http-status + } + else + { + $name = strtolower($parts[0]); + if (!isset($headers[$name])) + { + $headers[$name] = trim($parts[1]); + } + else + { + if (!is_array($headers[$name])) + { + $headers[$name] = [$headers[$name]]; + } + $headers[$name][] = trim($parts[1]); + } + } + } + if (self::DEBUG) error_log(__METHOD__."(\$head) returning ".json_encode($headers)); + return $headers; + } + /** * Create a user * @@ -212,7 +239,7 @@ class Udm public function createUser(array $data) { // set default values - $payload = $this->user2udm($data, $this->call('users/user/add')['entry']); + $payload = $this->user2udm($data, $this->call('users/user/add')); $payload['superordinate'] = null; $payload['position'] = $this->config['ldap_context']; @@ -236,16 +263,7 @@ class Udm $payload = $this->user2udm($data, $this->call('users/user/'.urlencode($dn), 'GET', [], $get_headers)); $headers = []; - $ret = $this->call('users/user/'.urlencode($dn), 'PUT', $payload, $headers, $get_headers['etag'], true); - - // you can not set the password and force a password change for next login in the same call - // the forced password change will be lost --> call again without password to force the change on next login - if (!empty($data['account_passwd']) && !empty($data['mustchangepassword'])) - { - unset($data['account_passwd']); - $ret = $this->updateUser($ret, $data); - } - return $ret; + return $this->call('users/user/'.urlencode($dn), 'PUT', $payload, $headers, $get_headers['etag'], true); } /** @@ -315,7 +333,7 @@ class Udm public function createGroup(array $data) { // set default values - $payload = $this->group2udm($data, $this->call('groups/group/add')['entry']); + $payload = $this->group2udm($data, $this->call('groups/group/add')); $payload['superordinate'] = null; $payload['position'] = empty($this->config['ldap_group_context']) ? $this->config['ldap_context'] : $this->config['ldap_group_context']; @@ -336,7 +354,7 @@ class Udm { // set existing values $get_headers = []; - $payload = $this->user2udm($data, $this->call('groups/group/'.urlencode($dn), 'GET', [], $get_headers)); + $payload = $this->group2udm($data, $this->call('groups/group/'.urlencode($dn), 'GET', [], $get_headers)); $headers = []; return $this->call('groups/group/'.urlencode($dn), 'PUT', $payload, $headers, $get_headers['etag'], true); @@ -353,7 +371,7 @@ class Udm { foreach([ 'account_lid' => 'name', - 'account_id' => ['gidNumber', 'sambaRID'], + 'account_id' => 'gidNumber', ] as $egw => $names) { if (!empty($data[$egw])) @@ -364,7 +382,8 @@ class Udm { throw new \Exception ("No '$name' in properties: ".json_encode($payload['properties'])); } - $payload['properties'][$name] = $data[$egw]; + // our account_id is negative for groups! + $payload['properties'][$name] = $egw === 'account_id' ? abs($data[$egw]) : $data[$egw]; } } } diff --git a/api/src/Auth/Ldap.php b/api/src/Auth/Ldap.php index ec72577aea..0c75ccffcf 100644 --- a/api/src/Auth/Ldap.php +++ b/api/src/Auth/Ldap.php @@ -199,7 +199,7 @@ class Ldap implements Backend isset($allValues[0]['sambapwdlastset']) && (string)$allValues[0]['sambapwdlastset'][0] === '0' || isset($allValues[0]['krb5passwordend']) && Api\DateTime::user2server($allValues[0]['krb5passwordend'][0]) < time()) { - error_log(__METHOD__."('$_username') shadowlastchange={$allValues[0]['shadowlastchange']}, sambapwdlastset={$allValues[0]['sambapwdlastset'][0]}, krb5passwordend={$allValues[0]['krb5passwordend'][0]} --> return 0"); + if ($this->debug) error_log(__METHOD__."('$_username') shadowlastchange={$allValues[0]['shadowlastchange'][0]}, sambapwdlastset={$allValues[0]['sambapwdlastset'][0]}, krb5passwordend={$allValues[0]['krb5passwordend'][0]} --> return 0"); return 0; } if (!isset($allValues[0]['shadowlastchange']))