keycloak-sync.php 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. <?php
  2. require_once(__DIR__ . '/../web/inc/vars.inc.php');
  3. if (file_exists(__DIR__ . '/../web/inc/vars.local.inc.php')) {
  4. include_once(__DIR__ . '/../web/inc/vars.local.inc.php');
  5. }
  6. require_once __DIR__ . '/../web/inc/lib/vendor/autoload.php';
  7. // Init database
  8. //$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
  9. $dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
  10. $opt = [
  11. PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
  12. PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
  13. PDO::ATTR_EMULATE_PREPARES => false,
  14. ];
  15. try {
  16. $pdo = new PDO($dsn, $database_user, $database_pass, $opt);
  17. }
  18. catch (PDOException $e) {
  19. logMsg("err", $e->getMessage());
  20. session_destroy();
  21. exit;
  22. }
  23. // Init Redis
  24. $redis = new Redis();
  25. try {
  26. if (!empty(getenv('REDIS_SLAVEOF_IP'))) {
  27. $redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT'));
  28. }
  29. else {
  30. $redis->connect('redis-mailcow', 6379);
  31. }
  32. $redis->auth(getenv("REDISPASS"));
  33. }
  34. catch (Exception $e) {
  35. echo "Exiting: " . $e->getMessage();
  36. session_destroy();
  37. exit;
  38. }
  39. function logMsg($priority, $message, $task = "Keycloak Sync") {
  40. global $redis;
  41. $finalMsg = array(
  42. "time" => time(),
  43. "priority" => $priority,
  44. "task" => $task,
  45. "message" => $message
  46. );
  47. $redis->lPush('CRON_LOG', json_encode($finalMsg));
  48. }
  49. // Load core functions first
  50. require_once __DIR__ . '/../web/inc/functions.inc.php';
  51. require_once __DIR__ . '/../web/inc/functions.auth.inc.php';
  52. require_once __DIR__ . '/../web/inc/sessions.inc.php';
  53. require_once __DIR__ . '/../web/inc/functions.mailbox.inc.php';
  54. require_once __DIR__ . '/../web/inc/functions.ratelimit.inc.php';
  55. require_once __DIR__ . '/../web/inc/functions.acl.inc.php';
  56. $_SESSION['mailcow_cc_username'] = "admin";
  57. $_SESSION['mailcow_cc_role'] = "admin";
  58. $_SESSION['acl']['tls_policy'] = "1";
  59. $_SESSION['acl']['quarantine_notification'] = "1";
  60. $_SESSION['acl']['quarantine_category'] = "1";
  61. $_SESSION['acl']['ratelimit'] = "1";
  62. $_SESSION['acl']['sogo_access'] = "1";
  63. $_SESSION['acl']['protocol_access'] = "1";
  64. $_SESSION['acl']['mailbox_relayhost'] = "1";
  65. $_SESSION['acl']['unlimited_quota'] = "1";
  66. $iam_settings = identity_provider('get');
  67. if ($iam_settings['authsource'] != "keycloak" || (intval($iam_settings['periodic_sync']) != 1 && intval($iam_settings['import_users']) != 1)) {
  68. session_destroy();
  69. exit;
  70. }
  71. // Set pagination variables
  72. $start = 0;
  73. $max = 100;
  74. // lock sync if already running
  75. $lock_file = '/tmp/iam-sync.lock';
  76. if (file_exists($lock_file)) {
  77. $lock_file_parts = explode("\n", file_get_contents($lock_file));
  78. $pid = $lock_file_parts[0];
  79. if (count($lock_file_parts) > 1){
  80. $last_execution = $lock_file_parts[1];
  81. $elapsed_time = (time() - $last_execution) / 60;
  82. if ($elapsed_time < intval($iam_settings['sync_interval'])) {
  83. logMsg("warning", "Sync not ready (".number_format((float)$elapsed_time, 2, '.', '')."min / ".$iam_settings['sync_interval']."min)");
  84. session_destroy();
  85. exit;
  86. }
  87. }
  88. if (posix_kill($pid, 0)) {
  89. logMsg("warning", "Sync is already running");
  90. session_destroy();
  91. exit;
  92. } else {
  93. unlink($lock_file);
  94. }
  95. }
  96. $lock_file_handle = fopen($lock_file, 'w');
  97. fwrite($lock_file_handle, getmypid());
  98. fclose($lock_file_handle);
  99. // Init Keycloak Provider
  100. $iam_provider = identity_provider('init');
  101. // Loop until all users have been retrieved
  102. while (true) {
  103. // Get admin access token
  104. $admin_token = identity_provider("get-keycloak-admin-token");
  105. // Make the API request to retrieve the users
  106. $url = "{$iam_settings['server_url']}/admin/realms/{$iam_settings['realm']}/users?first=$start&max=$max";
  107. $ch = curl_init();
  108. curl_setopt($ch, CURLOPT_URL, $url);
  109. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  110. curl_setopt($ch, CURLOPT_HTTPHEADER, [
  111. "Content-Type: application/json",
  112. "Authorization: Bearer " . $admin_token
  113. ]);
  114. $response = curl_exec($ch);
  115. $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  116. curl_close($ch);
  117. if ($code != 200){
  118. logMsg("err", "Received HTTP {$code}");
  119. session_destroy();
  120. exit;
  121. }
  122. try {
  123. $response = json_decode($response, true);
  124. } catch (Exception $e) {
  125. logMsg("err", $e->getMessage());
  126. break;
  127. }
  128. if (!is_array($response)){
  129. logMsg("err", "Received malformed response from keycloak api");
  130. break;
  131. }
  132. if (count($response) == 0) {
  133. break;
  134. }
  135. // Process the batch of users
  136. foreach ($response as $user) {
  137. if (empty($user['email'])){
  138. logMsg("warning", "No email address in keycloak found for user " . $user['name']);
  139. continue;
  140. }
  141. // try get mailbox user
  142. $stmt = $pdo->prepare("SELECT
  143. mailbox.*,
  144. domain.active AS d_active
  145. FROM `mailbox`
  146. INNER JOIN domain on mailbox.domain = domain.domain
  147. WHERE `kind` NOT REGEXP 'location|thing|group'
  148. AND `username` = :user");
  149. $stmt->execute(array(':user' => $user['email']));
  150. $row = $stmt->fetch(PDO::FETCH_ASSOC);
  151. // check if matching attribute mapping exists
  152. $user_template = $user['attributes']['mailcow_template'][0];
  153. $mapper_key = array_search($user_template, $iam_settings['mappers']);
  154. $_SESSION['access_all_exception'] = '1';
  155. if (!$row && intval($iam_settings['import_users']) == 1){
  156. if ($mapper_key === false){
  157. if (!empty($iam_settings['default_template'])) {
  158. $mbox_template = $iam_settings['default_template'];
  159. logMsg("warning", "Using default template for user " . $user['email']);
  160. } else {
  161. logMsg("warning", "No matching attribute mapping found for user " . $user['email']);
  162. continue;
  163. }
  164. } else {
  165. $mbox_template = $iam_settings['templates'][$mapper_key];
  166. }
  167. // mailbox user does not exist, create...
  168. logMsg("info", "Creating user " . $user['email']);
  169. $create_res = mailbox('add', 'mailbox_from_template', array(
  170. 'domain' => explode('@', $user['email'])[1],
  171. 'local_part' => explode('@', $user['email'])[0],
  172. 'name' => $user['firstName'] . " " . $user['lastName'],
  173. 'authsource' => 'keycloak',
  174. 'template' => $mbox_template
  175. ));
  176. if (!$create_res){
  177. logMsg("err", "Could not create user " . $user['email']);
  178. continue;
  179. }
  180. } else if ($row && intval($iam_settings['periodic_sync']) == 1) {
  181. if ($mapper_key === false){
  182. logMsg("warning", "No matching attribute mapping found for user " . $user['email']);
  183. continue;
  184. }
  185. $mbox_template = $iam_settings['templates'][$mapper_key];
  186. // mailbox user does exist, sync attribtues...
  187. logMsg("info", "Syncing attributes for user " . $user['email']);
  188. mailbox('edit', 'mailbox_from_template', array(
  189. 'username' => $user['email'],
  190. 'name' => $user['firstName'] . " " . $user['lastName'],
  191. 'template' => $mbox_template
  192. ));
  193. } else {
  194. // skip mailbox user
  195. logMsg("info", "Skipping user " . $user['email']);
  196. }
  197. $_SESSION['access_all_exception'] = '0';
  198. sleep(0.025);
  199. }
  200. // Update the pagination variables for the next batch
  201. $start += $max;
  202. sleep(1);
  203. }
  204. logMsg("info", "DONE!");
  205. // add last execution time to lock file
  206. $lock_file_handle = fopen($lock_file, 'w');
  207. fwrite($lock_file_handle, getmypid() . "\n" . time());
  208. fclose($lock_file_handle);
  209. session_destroy();