AdServer.class.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. <?php
  2. /**
  3. * Define a Serveur active directory
  4. * @author Jean CARRUESCO
  5. * @category Plugin
  6. * @license MIT
  7. */
  8. class AdServer extends Entity{
  9. public $id;
  10. public $ip; //IP (Texte)
  11. public $port = 389; //Port (Nombre Entier)
  12. public $sslPort = 636; //Port SSL (Nombre Entier)
  13. public $protocolVersion = 3; //Version de protocole ldap (Texte)
  14. public $domain; //Domaine (Texte)
  15. public $userRoot; //Racine des comptes utilisateurs (Texte Long)
  16. public $groupRoot; //Racine des groupes utilisateurs (Texte Long)
  17. public $readonlyLogin; //Identifiant du compte lecture seule (Texte)
  18. public $readonlyPassword; //Mot de passe du compte lecture seule (Mot de passe)
  19. public $adminLogin; //Identifiant compte administrateur (Texte)
  20. public $adminPassword; //Mot de passe compte administrateur (Mot de passe)
  21. public $mapping; //Plan de liaisons des champs (Texte Long)
  22. public $authenticationMode = 'samaccountname'; //Authentication mode (Texte)
  23. private $datasource; //Ressource AD
  24. protected $TABLE_NAME = 'activedirectory_ad_server';
  25. public $entityLabel = 'Serveur active directory';
  26. public $fields = array(
  27. 'id' => array('type'=>'key', 'label' => 'Identifiant'),
  28. 'ip' => array('type'=>'text','label' => 'IP'),
  29. 'port' => array('type'=>'integer','label' => 'Port'),
  30. 'sslPort' => array('type'=>'integer','label' => 'Port SSL'),
  31. 'protocolVersion' => array('type'=>'text','label' => 'Version de protocole ldap'),
  32. 'domain' => array('type'=>'text','label' => 'Domaine'),
  33. 'userRoot' => array('type'=>'textarea','label' => 'Racine des comptes utilisateurs'),
  34. 'groupRoot' => array('type'=>'textarea','label' => 'Racine des groupes utilisateurs'),
  35. 'readonlyLogin' => array('type'=>'text','label' => 'Identifiant du compte lecture seule'),
  36. 'readonlyPassword' => array('type'=>'password','label' => 'Mot de passe du compte lecture seule'),
  37. 'adminLogin' => array('type'=>'text','label' => 'Identifiant compte administrateur'),
  38. 'adminPassword' => array('type'=>'password','label' => 'Mot de passe compte administrateur'),
  39. 'mapping' => array('type'=>'textarea','label' => 'Plan de liaisons des champs'),
  40. 'authenticationMode' => array('type'=>'text','label' => 'Authentication mode'),
  41. );
  42. //retourne le mapping champ ad -> champ ustilisateur de base
  43. function default_mapping(){
  44. $mapping = array(
  45. 'sn'=>array('label'=>'Nom','field'=>'name','source'=>'active_directory'),
  46. 'givenname'=>array('label'=>'Prénom','field'=>'firstname','source'=>'active_directory'),
  47. 'mail'=>array('label'=>'E-mail','field'=>'mail','source'=>'active_directory'),
  48. 'telephonenumber'=>array('label'=>'Téléphone fixe','field'=>'phone','source'=>'active_directory'),
  49. 'mobile'=>array('label'=>'Téléphone mobile','field'=>'mobile','source'=>'active_directory'),
  50. 'title'=>array('label'=>'Fonction','field'=>'function','source'=>'active_directory'),
  51. //prend le samaccountname ou le userprincipalname en fct de la conf
  52. 'samaccountname'=>array('label'=>'samaccountname','field'=>function(&$user,$infos){
  53. $user->login = str_replace($this->domain,'',mb_strtolower($infos[$this->authenticationMode][0]));
  54. },'source'=>'active_directory'),
  55. 'origin'=>array('label'=>'samaccountname','field'=>'origin','value'=>'active_directory','source'=>'hardcoded'),
  56. 'department'=>array('label'=>'Nom','field'=>'service','source'=>'active_directory'),
  57. 'thumbnailphoto'=>array('label'=>'Photo 1','field'=>function(&$user,$infos){
  58. $user->meta['ldap_avatar'] = base64_encode($infos['thumbnailphoto'][0]);
  59. },'source'=>'active_directory'),
  60. 'jpegphoto'=>array('label'=>'Photo 2','field'=>function(&$user,$infos){
  61. $user->meta['ldap_avatar'] = base64_encode($infos['jpegphoto'][0]);
  62. },'source'=>'active_directory'),
  63. 'accountexpires'=>array('label'=>'Expiration','field'=>function(&$user,$infos){
  64. $seconds = (float)($infos['accountexpires'][0] / 10000000);
  65. //Convertion d'un timestamp AD en timestamp UNIX
  66. $timestamp = round($seconds - (((1970-1601) * 365.242190) * 86400));
  67. $user->meta['expiration'] = $timestamp;
  68. },'source'=>'active_directory'),
  69. //on traite memberof de manière particuliere donc pas de field
  70. 'memberof'=>array('label'=>'Membre de','field'=>'','source'=>'active_directory'),
  71. 'manager'=>array('label'=>'Manager','field'=>'manager','source'=>'active_directory'),
  72. 'userprincipalname'=>array('label'=>'userprincipalname','field'=>'','source'=>'active_directory'),
  73. 'whencreated'=>array('label'=>'Date création','field'=>function(&$user,$infos){
  74. if(!isset($infos['whencreated'][0]) || strlen($infos['whencreated'][0])<12 ) return;
  75. $created = substr($infos['whencreated'][0],0,8).' '.substr($infos['whencreated'][0],8,2).':'.substr($infos['whencreated'][0],10,2);
  76. $user->created = strtotime($created);
  77. },'source'=>'active_directory')
  78. );
  79. return $mapping;
  80. }
  81. //retourn les mapping champ ad->champ user par défaut + customs
  82. function getMapping(){
  83. $mapping = array();
  84. $jsonmapping = json_decode($this->mapping,true) ;
  85. if(is_array($jsonmapping)){
  86. foreach(json_decode($this->mapping,true) as $json){
  87. if(!isset($json['slug'])) continue;
  88. $json['source'] = 'active_directory';
  89. $mapping[$json['slug']]= $json;
  90. }
  91. }
  92. $mapping = array_merge(self::default_mapping(),$mapping);
  93. return $mapping;
  94. }
  95. //Gestion des modes d'authentification possibles
  96. function authenticationModes($key=null){
  97. $items = array(
  98. 'samaccountname' => array('label'=>'SamAccountName','filter'=>function($server,$login){
  99. return "(&(samaccountname=".$login."))(objectClass=user)(objectCategory=person))";
  100. }),
  101. 'userprincipalname' => array('label'=>'UserPrincipalName','filter'=>function($server,$login){
  102. return "(&(userprincipalname=".$login.$server->domain.")(objectClass=user)(objectCategory=person))";
  103. }),
  104. );
  105. if(!isset($key)) return $items;
  106. return isset($items[$key]) ? $items[$key] : array('label'=>'Non définis','filter'=>function($server,$login){});
  107. }
  108. //connexion ad
  109. function login($options = array()){
  110. $options = array_merge(array(
  111. 'mode' => 'readonly',
  112. 'ssl' => false
  113. ),$options);
  114. putenv('LDAPTLS_REQCERT=never');
  115. $port = $options['ssl'] ? $this->sslPort : $this->port ;
  116. $ip = $this->ip;
  117. $ip = 'ldap'.($options['ssl']? 's' :'').'://'.$ip;
  118. $ip .= ':'.$port;
  119. if($ip==null || $port==null || $this->userRoot==null) throw new Exception('Paramètres de connexion manquants',400);
  120. $this->datasource = ldap_connect($ip);
  121. if(!$this->datasource) throw new Exception('Connexion échouée', 404);
  122. ldap_set_option($this->datasource,LDAP_OPT_PROTOCOL_VERSION,$this->protocolVersion);
  123. ldap_set_option($this->datasource, LDAP_OPT_REFERRALS, 0);
  124. switch($options['mode']){
  125. case 'admin':
  126. $login = $this->adminLogin;
  127. $password = decrypt($this->adminPassword);
  128. break;
  129. case 'readonly':
  130. $login = $this->readonlyLogin;
  131. $password = decrypt($this->readonlyPassword);
  132. break;
  133. case 'login':
  134. $login = $options['login'];
  135. $password = $options['password'];
  136. break;
  137. }
  138. try{
  139. if(@ldap_bind($this->datasource,$login,$password) == false)
  140. throw new Exception(ldap_error($this->datasource), 403);
  141. }catch(Exception $e){
  142. if(ldap_errno($this->datasource)==49){
  143. throw new Exception('Identifiant ou mot de passe incorrect', 403);
  144. }else{
  145. throw new Exception('Connexion échouée :'.ldap_error($this->datasource), 404);
  146. }
  147. }
  148. }
  149. /**
  150. * Recherche des valeurs dans la base de données en fonction du filter
  151. * @param <String> Racine contexte de la recherche
  152. * @param <String> Filtre contenant les éléments a rechercher
  153. * @param <Array> Liste des attributs à récupérer
  154. * @return <Array> tableau contenant les objets correspondants a la recherche
  155. */
  156. public function search($dns, $filter=null, $attributes=array(),$limit=0){
  157. $searches = ldap_search( array_fill(0, count($dns), $this->datasource) , $dns,$filter,$attributes,0,$limit);
  158. $infos = array();
  159. foreach($searches as $search){
  160. if($search === false) continue;
  161. $infos = array_merge($infos ,ldap_get_entries($this->datasource, $search));
  162. }
  163. return $infos;
  164. }
  165. //Déconnexion ad
  166. function logout(){
  167. if($this->datasource!=null) @ldap_close($this->datasource);
  168. }
  169. //Récuperation des utilisateurs
  170. function users($filters = array(),$options = array()){
  171. require_once(__DIR__.SLASH.'AdFirmRank.class.php');
  172. $mapping = $this->getMapping();
  173. $options = array_merge(array(
  174. 'limit' => 0, // limit de users (0= pas de limite)
  175. 'select' => array_keys($mapping), //Colonnes a cibler
  176. 'group' => true, // charger les groupes ad
  177. 'rank' => true, // charger les rank et les firms
  178. 'manager' => false, // charger les managers en tant qu'objet user
  179. 'activeOnly' => true //n'afficher que les comptes non expirés
  180. ),$options);
  181. $groups = $options['group'] ? $this->groups() : array();
  182. $ranks = array();
  183. if($options['rank'] ){
  184. foreach(AdFirmRank::loadAll( array(), null, null, array('*'), 1) as $firmRank){
  185. if(!isset($ranks[$firmRank->group])) $ranks[$firmRank->group] = array();
  186. $ranks[$firmRank->group][] = $firmRank;
  187. }
  188. }
  189. $users = array();
  190. $default_filters = array();
  191. $filterChain = '';
  192. $default_filters['objectClass'] = 'user';
  193. $default_filters['objectCategory'] = 'person';
  194. $filters = array_merge($default_filters,$filters);
  195. if(count($filters)>1) $filterChain ='(&';
  196. foreach($filters as $key=>$value)
  197. $filterChain .= '('.$key.'='.$value.')';
  198. if(count($filters)>1) $filterChain .= ')';
  199. $entries = $this->search(explode(PHP_EOL,$this->userRoot),$filterChain,$options['select'],$options['limit']);
  200. if(count($entries)==0 || $entries['count']==0) return $users;
  201. if(!isset($entries[0])) return $users;
  202. $usersDn = array();
  203. for($i=0; $i<$entries['count']; $i++){
  204. $entry = $entries[$i];
  205. //Vérifie que le compte n'est pas expiré (nb : 0 et 9223372036854775807 sont les deux valeurs possibles pour un n'expire jamais (allez comprendre la logique microsoft...))
  206. if($options['activeOnly'] && isset($entry['accountexpires'][0]) && $entry['accountexpires'][0]!=0 && $entry['accountexpires'][0]!=9223372036854775807){
  207. //Convertion en seconds
  208. $seconds = (float)($entry['accountexpires'][0] / 10000000);
  209. //Convertion d'un timestamp AD en timestamp UNIX
  210. $timestamp = round($seconds - (((1970-1601) * 365.242190) * 86400));
  211. if($timestamp <= time()) continue;
  212. }
  213. $user = self::user_object($entry,$mapping,$options,$groups,$ranks);
  214. //on stocke l'id de l'ad ayant provisionné ce user dans les méta pour réutilisations ulterieure (ex save du mdp)
  215. $user->meta['activedirectory_server'] = $this->id;
  216. $user->meta = json_encode($user->meta);
  217. if($options['manager']) $usersDn[$entry['dn']] = $user;
  218. $users[$user->login] = $user;
  219. }
  220. //Chargement des managers
  221. if($options['manager']){
  222. foreach($users as $key=>$user){
  223. if(empty($user->manager)) continue;
  224. if(isset($usersDn[$user->manager])){
  225. $user->manager = $usersDn[$user->manager];
  226. }else{
  227. $manager = $this->users(array(
  228. 'dn' => $user->manager
  229. ),array(
  230. 'limit' => 1, // limit de users (0= pas de limite)
  231. 'group' => true, // charger les groupes ad
  232. 'rank' => true, // charger les rank et les firms
  233. 'rights' => false, // charger les rank et les firms
  234. 'manager' => true, // charger les managers en tant qu'objet user
  235. 'activeOnly' => true //n'afficher que les comptes non expirés
  236. ));
  237. $user->manager = array_shift($manager);
  238. }
  239. $users[$key] = $user;
  240. }
  241. }
  242. return $users;
  243. }
  244. //Remplis et return un objet user en fonction d'une entrée ad
  245. static function user_object($entry,$mapping,$options = array(),$groups=array(),$ranks=array()){
  246. $user = new User();
  247. //récuperation des attributs mappés (défaut et custom)
  248. foreach($mapping as $ldap=>$attribute){
  249. $field = $attribute['field'];
  250. if(empty($field )) continue;
  251. //si le champ doit contenir une valeur en dur (et non ldap ex : origin : active_directory)
  252. if($attribute['source']=='hardcoded' && !empty($attribute['value'])){
  253. $user->$field = $attribute['value'];
  254. continue;
  255. }
  256. //si aucune correspondance trouvée dans l'entrée ldap on passe au suivant
  257. if(empty($entry[$ldap]) || empty($entry[$ldap][0])) continue;
  258. //si le field définit est une méthode anonyme
  259. if(!is_string($field)){
  260. $field($user,$entry);
  261. //si le field définit est un simple attribut
  262. }else{
  263. if(property_exists($user,$field)){
  264. $user->$field = $entry[$ldap][0];
  265. }else{
  266. $user->meta[$field] = $entry[$ldap][0];
  267. }
  268. }
  269. }
  270. //Récuperation des groupes direct et indirects
  271. $user->groups = array();
  272. if(isset($entry['memberof'])){
  273. for($i=0; $i<count($entry['memberof'])-1; ++$i){
  274. if(empty($groups[$entry['memberof'][$i]])) continue;
  275. self::group_recursive_fill($user,$groups,$entry['memberof'][$i],$ranks);
  276. }
  277. }
  278. return $user;
  279. }
  280. //ajout les groupes a l'utilisateur de manière récursive (un groupe peut être parent d'un autre)
  281. static function group_recursive_fill(&$user,$groups,$dn,$ranks){
  282. if(!isset($groups[$dn])) return;
  283. $groupItem = $groups[$dn];
  284. $user->groups[] = $groupItem['label'];
  285. if(isset($ranks[$groupItem['label']])){
  286. foreach($ranks[$groupItem['label']] as $firmrank){
  287. $user->ranks[$firmrank->firm][$firmrank->rank] = $firmrank->join('rank');
  288. $user->firms[$firmrank->firm] = $firmrank->join('firm');
  289. }
  290. }
  291. foreach($groupItem['memberof'] as $parentDn){
  292. self::group_recursive_fill($user,$groups,$parentDn,$ranks);
  293. }
  294. }
  295. function userChange($login,$changes){
  296. switch($this->authenticationMode){
  297. case 'samaccountname':
  298. $filter = "(".$this->authenticationMode."=".$login.")";
  299. break;
  300. case 'userprincipalname':
  301. default:
  302. $filter= "(".$this->authenticationMode."=".$login.$this->domain.")";
  303. break;
  304. }
  305. $filter = '(&'.$filter.'(objectClass=user)(objectCategory=person))';
  306. $entries= $this->search(array($this->userRoot),$filter);
  307. $cn = (isset($entries[0])) ? $entries[0]['dn'] : false;
  308. if($cn == false) throw new Exception('Impossible de trouver l\'utilisateur sur ce serveur');
  309. putenv('LDAPTLS_REQCERT=never');
  310. foreach($changes as $adfield=>$value){
  311. if($adfield == 'password') continue;
  312. if($value != ''){
  313. $attributes[$adfield][0] = $value;
  314. return ldap_modify($this->datasource,$cn,$attributes);
  315. }else{
  316. //evite le crash si l'attribut n'existe pas lors d'une supression
  317. $attributes[$adfield] ='0';
  318. ldap_modify($this->datasource,$cn,$attributes);
  319. $attributes[$adfield] = array();
  320. return ldap_mod_del($this->datasource,$cn,$attributes);
  321. }
  322. }
  323. if(isset($changes['password'])){
  324. if (!ldap_mod_replace($this->datasource, $cn , self::encrypt_password($changes['password']))) throw new Exception("Impossible de modifier le mot de passe : ".ldap_error($this->datasource));
  325. }
  326. }
  327. public static function encrypt_password($newPassword){
  328. $newPassword = "\"" . $newPassword . "\"";
  329. $len = strlen( $newPassword );
  330. $newPassw = "";
  331. for ( $i = 0; $i < $len; $i++ )
  332. $newPassw .= $newPassword[$i]."\000";
  333. return array("unicodePwd" => $newPassw);
  334. }
  335. //Récuperation des groupes ad
  336. function groups($filters = array()){
  337. $groups = array();
  338. $default_filters = array();
  339. $filterChain = '';
  340. $default_filters['objectClass'] = 'group';
  341. $filters = array_merge($default_filters,$filters);
  342. if(count($filters)>1) $filterChain ='(&';
  343. foreach($filters as $key=>$value)
  344. $filterChain .= '('.$key.'='.$value.')';
  345. if(count($filters)>1) $filterChain .= ')';
  346. $entries = $this->search(explode(PHP_EOL,$this->groupRoot),$filterChain,array('name','memberOf'));
  347. for($i=0; $i<$entries['count']; $i++){
  348. $info = $entries[$i];
  349. $group = array(
  350. 'label' => $info['name'][0],
  351. 'dn' => $info['dn'],
  352. 'memberof' => array()
  353. );
  354. //on récupere les groupes parents
  355. if(isset($info['memberof'])){
  356. for($u=0; $u<count($info['memberof'])-1; ++$u){
  357. //on ne prend que les groupes parent qui font partie de la racine groupe
  358. if(strrpos($info['memberof'][$u],$this->groupRoot)===false) continue;
  359. $group['memberof'][] = $info['memberof'][$u];
  360. }
  361. }
  362. $groups[$group['dn']] = $group;
  363. }
  364. uasort($groups,function($a,$b){
  365. return $a['label'] > $b['label'];
  366. });
  367. return $groups;
  368. }
  369. }
  370. ?>