2019-07-22 11:18:28 +02:00
< ? php
/**
* EGroupware support for Univention UDM REST Api
*
* @ link https :// www . egroupware . org
* @ author Ralf Becker < rb @ egroupware . org >
*
* @ link https :// www . univention . com / blog - en / 2019 / 07 / udm - rest - api - beta - version - released /
*
* @ license http :// opensource . org / licenses / gpl - license . php GPL - GNU General Public License
* @ package api
* @ subpackage accounts
*/
namespace EGroupware\Api\Accounts\Univention ;
use EGroupware\Api ;
/**
* Univention UDM REST Api
*
* @ todo Use just UDM instead of still calling ldap / parent
*/
class Udm
{
/**
* Config to use
*
* @ var array $config
*/
protected $config ;
/**
* Hostname of master , derived from ldap_host
*
* @ var string
*/
protected $host ;
/**
* Username , derived from ldap_root_dn
*
* @ var string
*/
protected $user ;
/**
* Udm url prefix , prepend to relative path like 'users/user'
*/
const PREFIX = '/univention/udm/' ;
/**
* Log webservice - calls to error_log
*/
const DEBUG = true ;
/**
* Constructor
*
* @ param array $config = null config to use , default $GLOBALS [ 'egw_info' ][ 'server' ]
* @ throws Api\Exception\WrongParameter for missing LDAP config
*/
public function __construct ( array $config = null )
{
$this -> config = isset ( $config ) ? $config : $GLOBALS [ 'egw_info' ][ 'server' ];
$this -> host = parse_url ( $this -> config [ 'ldap_host' ], PHP_URL_HOST );
if ( empty ( $this -> host ))
{
throw new Api\Exception\WrongParameter ( " Univention needs 'ldap_host' configured! " );
}
$matches = null ;
if ( ! preg_match ( '/^(cn|uid)=([^,]+),/i' , $this -> config [ 'ldap_root_dn' ], $matches ))
{
throw new Api\Exception\WrongParameter ( " Univention needs 'ldap_rood_dn' configured! " );
}
$this -> user = $matches [ 2 ];
}
/**
* Call UDM REST Api
*
* @ param string $_path path to call , if relative PREFIX is prepended eg . 'users/user'
* @ param string $_method = 'GET'
* @ param array $_payload = [] payload to send
* @ param array & $headers = [] on return response headers
* @ param string $if_match = null etag for If - Match header
* @ param boolean $return_dn = false return DN of Location header
2019-07-31 12:10:04 +02:00
* @ param int $retry = 1 > 0 retry on connection - error only
2019-07-22 11:18:28 +02:00
* @ return array | string decoded JSON or DN for $return_DN === true
* @ throws UdmCantConnect for connection errors or JSON decoding errors
* @ throws UdmError for returned JSON error object
* @ throws UdmMissingLocation for missing Location header with DN ( $return_dn === true )
*/
2019-07-31 12:10:04 +02:00
protected function call ( $_path , $_method = 'GET' , array $_payload = [], & $headers = [], $if_match = null , $return_dn = false , $retry = 1 )
2019-07-22 11:18:28 +02:00
{
$curl = curl_init ();
// fix error: Request argument "policies" is not a "dict" (PHP encodes empty arrays as array, not object)
2019-07-30 18:17:17 +02:00
if ( array_key_exists ( 'policies' , $_payload ) && empty ( $_payload [ 'policies' ]))
2019-07-22 11:18:28 +02:00
{
$_payload [ 'policies' ] = new \stdClass (); // force "policies": {}
2019-07-30 18:17:17 +02:00
}
2019-07-22 11:18:28 +02:00
$headers = [];
$curlOpts = [
CURLOPT_URL => 'https://' . $this -> host . ( $_path [ 0 ] !== '/' ? self :: PREFIX : '' ) . $_path ,
CURLOPT_USERPWD => $this -> user . ':' . $this -> config [ 'ldap_root_pw' ],
//CURLOPT_SSL_VERIFYHOST => 2, // 0: to disable certificate check
CURLOPT_HTTPHEADER => [
'Accept: application/json' ,
],
CURLOPT_CUSTOMREQUEST => $_method ,
CURLOPT_RETURNTRANSFER => 1 ,
//CURLOPT_FOLLOWLOCATION => 1,
2019-07-31 12:48:59 +02:00
CURLOPT_TIMEOUT => 30 , // setting a timeout of 30 seconds, as recommended by Univention
2019-07-22 11:18:28 +02:00
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 ;
},
];
if ( isset ( $if_match ))
{
$curlOpts [ CURLOPT_HTTPHEADER ][] = 'If-Match: ' . $if_match ;
}
switch ( $_method )
{
case 'PUT' :
case 'POST' :
$curlOpts [ CURLOPT_HTTPHEADER ][] = 'Content-Type: application/json' ;
$curlOpts [ CURLOPT_POSTFIELDS ] = json_encode ( $_payload , JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
break ;
case 'GET' :
default :
if ( $_payload )
{
$curlOpts [ CURLOPT_URL ] .= '?' . http_build_query ( $_payload );
}
break ;
}
curl_setopt_array ( $curl , $curlOpts );
$response = curl_exec ( $curl );
2019-07-31 12:48:59 +02:00
$path = urldecode ( $_path ); // for nicer error-messages
2019-07-22 11:18:28 +02:00
if ( ! $response || ! ( $json = json_decode ( $response , true )) && json_last_error ())
{
2019-07-31 12:48:59 +02:00
$info = curl_getinfo ( $curl );
curl_close ( $curl );
2019-07-31 12:10:04 +02:00
if ( $retry > 0 )
{
2019-07-31 12:48:59 +02:00
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 ));
usleep ( 100000 );
2019-07-31 12:10:04 +02:00
return $this -> call ( $_path , $_method , $_payload , $headers , $if_match , $return_dn , -- $retry );
}
2019-07-31 12:48:59 +02:00
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 , " . json_encode ( $_payload , JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) . " ) " );
2019-07-22 11:18:28 +02:00
throw new UdmCantConnect ( " Error contacting Univention UDM REST Api ( $_path ) " . ( $response ? ': ' . json_last_error () : '' ));
}
curl_close ( $curl );
if ( ! empty ( $json [ 'error' ]))
{
2019-07-31 12:48:59 +02:00
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 ) . " ) " );
2019-07-31 12:10:04 +02:00
throw new UdmError ( " UDM REST Api ( " . urldecode ( $_path ) . " ): " . ( empty ( $json [ 'error' ][ 'message' ]) ? $response : $json [ 'error' ][ 'message' ]), $json [ 'error' ][ 'code' ]);
}
if ( self :: DEBUG )
{
2019-07-31 12:48:59 +02:00
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 ) . " ) " );
2019-07-22 11:18:28 +02:00
}
if ( $return_dn )
{
$matches = null ;
if ( ! isset ( $headers [ 'location' ]) || ! preg_match ( '|/([^/]+)$|' , $headers [ 'location' ], $matches ))
{
throw new UdmMissingLocation ( " UDM REST Api ( $_path ) did not return Location header! " );
}
return urldecode ( $matches [ 1 ]);
}
return $json ;
}
/**
* Create a user
*
* @ param array $data
* @ throws Exception on error - message
* @ return string with DN of new user
*/
public function createUser ( array $data )
{
// set default values
$payload = $this -> user2udm ( $data , $this -> call ( 'users/user/add' )[ 'entry' ]);
$payload [ 'superordinate' ] = null ;
$payload [ 'position' ] = $this -> config [ 'ldap_context' ];
$headers = [];
return $this -> call ( 'users/user/' , 'POST' , $payload , $headers , null , true );
}
/**
* Update a user
*
* @ param string $dn dn of user to update
* @ param array $data
* @ return string with dn
* @ throws Exception on error - message
*/
public function updateUser ( $dn , array $data )
{
// set existing values
$get_headers = [];
2019-07-30 19:44:22 +02:00
$payload = $this -> user2udm ( $data , $this -> call ( 'users/user/' . urlencode ( $dn ), 'GET' , [], $get_headers ));
2019-07-22 11:18:28 +02:00
$headers = [];
2019-07-31 12:10:04 +02:00
return $this -> call ( 'users/user/' . urlencode ( $dn ), 'PUT' , $payload , $headers , $get_headers [ 'etag' ], true );
2019-07-22 11:18:28 +02:00
}
/**
* Copy EGroupware user - values to UDM ones
*
* @ param array $data
* @ param array $payload
* @ return array with updated payload
*/
protected function user2udm ( array $data , array $payload )
{
foreach ([
'account_lid' => 'username' ,
'account_passwd' => 'password' ,
'account_lastname' => 'lastname' ,
'account_firstname' => 'firstname' ,
'account_id' => [ 'uidNumber' , 'sambaRID' ],
'account_email' => 'mailPrimaryAddress' ,
] as $egw => $names )
{
if ( ! empty ( $data [ $egw ]))
{
foreach (( array ) $names as $name )
{
if ( ! array_key_exists ( $name , $payload [ 'properties' ]))
{
throw new \Exception ( " No ' $name ' in properties: " . json_encode ( $payload [ 'properties' ]));
}
$payload [ 'properties' ][ $name ] = $data [ $egw ];
}
}
}
if ( ! empty ( $data [ 'account_email' ]))
{
// we need to set mailHomeServer, so mailbox gets created for Dovecot
// get_default() does not work for Adminstrator, try acc_id=1 instead
// if everything fails try ldap host / master ...
try {
if ( ! ( $account = Api\Mail\Account :: get_default ( false , false , false )))
{
$account = Api\Mail\Account :: read ( 1 );
}
$hostname = $account -> acc_imap_host ;
}
catch ( \Exception $e ) {
unset ( $e );
}
if ( empty ( $hostname )) $hostname = $this -> host ;
$payload [ 'properties' ][ 'mailHomeServer' ] = $hostname ;
}
return $payload ;
}
/**
* Create a group
*
* @ param array $data
* @ throws Exception on error - message
* @ return string with DN of new user
*/
public function createGroup ( array $data )
{
// set default values
$payload = $this -> group2udm ( $data , $this -> call ( 'groups/group/add' )[ 'entry' ]);
$payload [ 'superordinate' ] = null ;
$payload [ 'position' ] = empty ( $this -> config [ 'ldap_group_context' ]) ? $this -> config [ 'ldap_context' ] : $this -> config [ 'ldap_group_context' ];
$headers = [];
return $this -> call ( 'groups/group/' , 'POST' , $payload , $headers , null , true );
}
/**
* Update a group
*
* @ param string $dn dn of group to update
* @ param array $data
* @ throws Exception on error - message
* @ return string with DN of new user
*/
public function updateGroup ( $dn , array $data )
{
// set existing values
$get_headers = [];
2019-07-30 19:44:22 +02:00
$payload = $this -> user2udm ( $data , $this -> call ( 'groups/group/' . urlencode ( $dn ), 'GET' , [], $get_headers ));
2019-07-22 11:18:28 +02:00
$headers = [];
2019-07-31 12:10:04 +02:00
return $this -> call ( 'groups/group/' . urlencode ( $dn ), 'PUT' , $payload , $headers , $get_headers [ 'etag' ], true );
2019-07-22 11:18:28 +02:00
}
/**
* Copy EGroupware group values to UDM ones
*
* @ param array $data
* @ param array $payload
* @ return array with updated payload
*/
protected function group2udm ( array $data , array $payload )
{
foreach ([
'account_lid' => 'name' ,
'account_id' => [ 'gidNumber' , 'sambaRID' ],
] as $egw => $names )
{
if ( ! empty ( $data [ $egw ]))
{
foreach (( array ) $names as $name )
{
if ( ! array_key_exists ( $name , $payload [ 'properties' ]))
{
throw new \Exception ( " No ' $name ' in properties: " . json_encode ( $payload [ 'properties' ]));
}
$payload [ 'properties' ][ $name ] = $data [ $egw ];
}
}
}
return $payload ;
}
}