Selaa lähdekoodia

Merge pull request #6194 from mailcow/feat/nightly-enhancements

[Nightly] Enhancements
FreddleSpl0it 9 kuukautta sitten
vanhempi
sitoutus
b6174fae23

+ 27 - 5
data/conf/dovecot/auth/mailcowauth.php

@@ -12,7 +12,7 @@ $return = array("success" => false);
 if(!isset($post['username']) || !isset($post['password']) || !isset($post['real_rip'])){
   error_log("MAILCOWAUTH: Bad Request");
   http_response_code(400); // Bad Request
-  echo json_encode($return); 
+  echo json_encode($return);
   exit();
 }
 
@@ -22,6 +22,24 @@ if (file_exists('../../../web/inc/vars.local.inc.php')) {
 }
 require_once '../../../web/inc/lib/vendor/autoload.php';
 
+
+// Init Redis
+$redis = new Redis();
+try {
+  if (!empty(getenv('REDIS_SLAVEOF_IP'))) {
+    $redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT'));
+  }
+  else {
+    $redis->connect('redis-mailcow', 6379);
+  }
+}
+catch (Exception $e) {
+  error_log("MAILCOWAUTH: " . $e . PHP_EOL);
+  http_response_code(500); // Internal Server Error
+  echo json_encode($return);
+  exit;
+}
+
 // Init database
 $dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
 $opt = [
@@ -35,7 +53,7 @@ try {
 catch (PDOException $e) {
   error_log("MAILCOWAUTH: " . $e . PHP_EOL);
   http_response_code(500); // Internal Server Error
-  echo json_encode($return); 
+  echo json_encode($return);
   exit;
 }
 
@@ -44,6 +62,8 @@ require_once 'functions.inc.php';
 require_once 'functions.auth.inc.php';
 require_once 'sessions.inc.php';
 require_once 'functions.mailbox.inc.php';
+require_once 'functions.ratelimit.inc.php';
+require_once 'functions.acl.inc.php';
 
 
 $isSOGoRequest = $post['real_rip'] == getenv('IPV4_NETWORK') . '.248';
@@ -57,7 +77,6 @@ if ($isSOGoRequest) {
     error_log('MAILCOWAUTH: SOGo SSO auth for user ' . $post['username']);
     $result = true;
   }
-  
 }
 if ($result === false){
   $result = apppass_login($post['username'], $post['password'], $protocol, array(
@@ -67,7 +86,10 @@ if ($result === false){
   if ($result) error_log('MAILCOWAUTH: App auth for user ' . $post['username']);
 }
 if ($result === false){
-  $result = user_login($post['username'], $post['password'], $protocol, array('is_internal' => true));
+  // Init Identity Provider
+  $iam_provider = identity_provider('init');
+  $iam_settings = identity_provider('get');
+  $result = user_login($post['username'], $post['password'], array('is_internal' => true));
   if ($result) error_log('MAILCOWAUTH: User auth for user ' . $post['username']);
 }
 
@@ -80,6 +102,6 @@ if ($result) {
 }
 
 
-echo json_encode($return); 
+echo json_encode($return);
 session_destroy();
 exit;

+ 9 - 7
data/conf/phpfpm/crons/keycloak-sync.php

@@ -114,7 +114,7 @@ $iam_provider = identity_provider('init');
 while (true) {
   // Get admin access token
   $admin_token = identity_provider("get-keycloak-admin-token");
-  
+
   // Make the API request to retrieve the users
   $url = "{$iam_settings['server_url']}/admin/realms/{$iam_settings['realm']}/users?first=$start&max=$max";
   $ch = curl_init();
@@ -127,7 +127,7 @@ while (true) {
   $response = curl_exec($ch);
   $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
   curl_close($ch);
-  
+
   if ($code != 200){
     logMsg("err", "Recieved HTTP {$code}");
     session_destroy();
@@ -157,8 +157,8 @@ while (true) {
       logMsg("warning", "No attributes in keycloak found for user " . $user['email']);
       continue;
     }
-    if (!isset($user['attributes']['mailcow_template']) || 
-        !is_array($user['attributes']['mailcow_template']) || 
+    if (!isset($user['attributes']['mailcow_template']) ||
+        !is_array($user['attributes']['mailcow_template']) ||
         count($user['attributes']['mailcow_template']) == 0) {
       logMsg("warning", "No mailcow_template in keycloak found for user " . $user['email']);
       continue;
@@ -195,7 +195,8 @@ while (true) {
         'local_part' => explode('@', $user['email'])[0],
         'name' => $user['firstName'] . " " . $user['lastName'],
         'authsource' => 'keycloak',
-        'template' => $mbox_template
+        'template' => $mbox_template,
+        'hasAccess' => true
       ));
     } else if ($row && intval($iam_settings['periodic_sync']) == 1) {
       // mailbox user does exist, sync attribtues...
@@ -203,7 +204,8 @@ while (true) {
       mailbox('edit', 'mailbox_from_template', array(
         'username' => $user['email'],
         'name' => $user['firstName'] . " " . $user['lastName'],
-        'template' => $mbox_template
+        'template' => $mbox_template,
+        'hasAccess' => true
       ));
     } else {
       // skip mailbox user
@@ -212,7 +214,7 @@ while (true) {
 
     sleep(0.025);
   }
-  
+
   // Update the pagination variables for the next batch
   $start += $max;
   sleep(1);

+ 4 - 2
data/conf/phpfpm/crons/ldap-sync.php

@@ -159,7 +159,8 @@ foreach ($response as $user) {
       'local_part' => explode('@',  $user[$iam_settings['username_field']][0])[0],
       'name' => $user['displayname'][0],
       'authsource' => 'ldap',
-      'template' => $mbox_template
+      'template' => $mbox_template,
+      'hasAccess' => true
     ));
   } else if ($row && intval($iam_settings['periodic_sync']) == 1) {
     // mailbox user does exist, sync attribtues...
@@ -167,7 +168,8 @@ foreach ($response as $user) {
     mailbox('edit', 'mailbox_from_template', array(
       'username' =>  $user[$iam_settings['username_field']][0],
       'name' => $user['displayname'][0],
-      'template' => $mbox_template
+      'template' => $mbox_template,
+      'hasAccess' => true
     ));
   } else {
     // skip mailbox user

+ 2 - 2
data/conf/sogo/custom-sogo.js

@@ -2,7 +2,7 @@
 document.addEventListener('DOMContentLoaded', function () {
     var loginForm = document.forms.namedItem("loginForm");
     if (loginForm) {
-        window.location.href = '/';
+        window.location.href = '/user';
     }
 
     angularReady = false;
@@ -34,7 +34,7 @@ document.addEventListener('DOMContentLoaded', function () {
     function mcElementsExists() {
         if (document.getElementById("mc_backlink"))
             return true;
-        else 
+        else
             return false;
     }
     function addMCElements() {

+ 0 - 2
data/web/admin.php

@@ -86,8 +86,6 @@ $cors_settings['allowed_origins'] = str_replace(", ", "\n", $cors_settings['allo
 $cors_settings['allowed_methods'] = explode(", ", $cors_settings['allowed_methods']);
 
 $f2b_data = fail2ban('get');
-// identity provider
-$iam_settings = identity_provider('get');
 // mbox templates
 $mbox_templates = mailbox('get', 'mailbox_templates');
 

+ 1 - 0
data/web/autodiscover.php

@@ -55,6 +55,7 @@ $pdo = new PDO($dsn, $database_user, $database_pass, $opt);
 
 // Init Identity Provider
 $iam_provider = identity_provider('init');
+$iam_settings = identity_provider('get');
 
 $login_user = strtolower(trim($_SERVER['PHP_AUTH_USER']));
 $login_pass = trim(htmlspecialchars_decode($_SERVER['PHP_AUTH_PW']));

+ 0 - 1
data/web/edit.php

@@ -119,7 +119,6 @@ if (isset($_SESSION['mailcow_cc_role'])) {
         $quarantine_category = mailbox('get', 'quarantine_category', $mailbox);
         $get_tls_policy = mailbox('get', 'tls_policy', $mailbox);
         $rlyhosts = relayhost('get');
-        $iam_settings = identity_provider('get');
         $template = 'edit/mailbox.twig';
         $template_data = [
           'acl' => $_SESSION['acl'],

+ 5 - 5
data/web/inc/functions.acl.inc.php

@@ -1,5 +1,5 @@
 <?php
-function acl($_action, $_scope = null, $_data = null) {
+function acl($_action, $_scope = null, $_data = null, $_extra = null) {
   global $pdo;
   global $lang;
   $_data_log = $_data;
@@ -23,8 +23,8 @@ function acl($_action, $_scope = null, $_data = null) {
               $acl_post[$acl_val] = 1;
             }
             // Users cannot change their own ACL
-            if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)
-              || ($_SESSION['mailcow_cc_role'] != 'admin' && $_SESSION['mailcow_cc_role'] != 'domainadmin')) {
+            if (!$_extra['hasAccess'] && (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)
+              || ($_SESSION['mailcow_cc_role'] != 'admin' && $_SESSION['mailcow_cc_role'] != 'domainadmin'))) {
               $_SESSION['return'][] = array(
                 'type' => 'danger',
                 'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
@@ -34,7 +34,7 @@ function acl($_action, $_scope = null, $_data = null) {
             }
             // Read all available acl options by calling acl(get)
             // Set all available acl options we cannot find in the post data to 0, else 1
-            $is_now = acl('get', 'user', $username);
+            $is_now = acl('get', 'user', $username, $_extra);
             if (!empty($is_now)) {
               foreach ($is_now as $acl_now_name => $acl_now_val) {
                 $set_acls[$acl_now_name] = (isset($acl_post[$acl_now_name])) ? 1 : 0;
@@ -130,7 +130,7 @@ function acl($_action, $_scope = null, $_data = null) {
     case 'get':
       switch ($_scope) {
         case 'user':
-          if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
+          if (!$_extra['hasAccess'] && !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
             return false;
           }
           $stmt = $pdo->prepare("SELECT * FROM `user_acl` WHERE `username` = :username");

+ 55 - 29
data/web/inc/functions.auth.inc.php

@@ -162,6 +162,8 @@ function domainadmin_login($user, $pass){
 }
 function user_login($user, $pass, $extra = null){
   global $pdo;
+  global $iam_provider;
+  global $iam_settings;
 
   $is_internal = $extra['is_internal'];
 
@@ -186,12 +188,11 @@ function user_login($user, $pass, $extra = null){
 
   // user does not exist, try call idp login and create user if possible via rest flow
   if (!$row){
-    $iam_settings = identity_provider('get');
     if ($iam_settings['authsource'] == 'keycloak' && intval($iam_settings['mailpassword_flow']) == 1){
-      $result = keycloak_mbox_login_rest($user, $pass, $iam_settings, array('is_internal' => $is_internal, 'create' => true));
+      $result = keycloak_mbox_login_rest($user, $pass, array('is_internal' => $is_internal, 'create' => true));
       if ($result !== false) return $result;
     } else if ($iam_settings['authsource'] == 'ldap') {
-      $result = ldap_mbox_login($user, $pass, $iam_settings, array('is_internal' => $is_internal, 'create' => true));
+      $result = ldap_mbox_login($user, $pass, array('is_internal' => $is_internal, 'create' => true));
       if ($result !== false) return $result;
     }
   }
@@ -202,9 +203,8 @@ function user_login($user, $pass, $extra = null){
   switch ($row['authsource']) {
     case 'keycloak':
       // user authsource is keycloak, try using via rest flow
-      $iam_settings = identity_provider('get');
       if (intval($iam_settings['mailpassword_flow']) == 1){
-        $result = keycloak_mbox_login_rest($user, $pass, $iam_settings, array('is_internal' => $is_internal));
+        $result = keycloak_mbox_login_rest($user, $pass, array('is_internal' => $is_internal));
         if ($result !== false) {
           // check for tfa authenticators
           $authenticators = get_tfa($user);
@@ -243,8 +243,7 @@ function user_login($user, $pass, $extra = null){
     break;
     case 'ldap':
       // user authsource is ldap
-      $iam_settings = identity_provider('get');
-      $result = ldap_mbox_login($user, $pass, $iam_settings, array('is_internal' => $is_internal));
+      $result = ldap_mbox_login($user, $pass, array('is_internal' => $is_internal));
       if ($result !== false) {
         // check for tfa authenticators
         $authenticators = get_tfa($user);
@@ -397,8 +396,10 @@ function apppass_login($user, $pass, $app_passwd_data, $extra = null){
 // Keycloak REST Api Flow - auth user by mailcow_password attribute
 // This password will be used for direct UI, IMAP and SMTP Auth
 // To use direct user credentials, only Authorization Code Flow is valid
-function keycloak_mbox_login_rest($user, $pass, $iam_settings, $extra = null){
+function keycloak_mbox_login_rest($user, $pass, $extra = null){
   global $pdo;
+  global $iam_provider;
+  global $iam_settings;
 
   $is_internal = $extra['is_internal'];
   $create = $extra['create'];
@@ -448,36 +449,49 @@ function keycloak_mbox_login_rest($user, $pass, $iam_settings, $extra = null){
     return false;
   }
 
-  // get mapped template, if not set return false
-  // also return false if no mappers were defined
+  // get mapped template
   $user_template = $user_res['attributes']['mailcow_template'][0];
-  if ($create && (empty($iam_settings['mappers']) || !$user_template)){
-    return false;
-  } else if (!$create) {
-    // login success - dont create mailbox
+  $mapper_key = array_search($user_template, $iam_settings['mappers']);
+
+  if (!$create) {
+    // login success
+    if ($mapper_key !== false) {
+      // update user
+      mailbox('edit', 'mailbox_from_template', array(
+        'username' => $user,
+        'name' => $user_res['name'],
+        'template' => $iam_settings['templates'][$mapper_key],
+        'hasAccess' => true
+      ));
+    }
     return 'user';
   }
 
   // check if matching attribute exist
-  $mapper_key = array_search($user_template, $iam_settings['mappers']);
+  if (empty($iam_settings['mappers']) || !$user_template) return false;
   if ($mapper_key === false) return false;
 
   // create mailbox
   $create_res = mailbox('add', 'mailbox_from_template', array(
     'domain' => explode('@', $user)[1],
     'local_part' => explode('@', $user)[0],
-    'name' => $user_res['firstName'] . " " . $user_res['lastName'],
+    'name' => $user_res['name'],
     'authsource' => 'keycloak',
-    'template' => $iam_settings['templates'][$mapper_key]
+    'template' => $iam_settings['templates'][$mapper_key],
+    'hasAccess' => true
   ));
-  if (!$create_res) return false;
+  if (!$create_res){
+    clear_session();
+    return false;
+  }
 
   return 'user';
 }
-function ldap_mbox_login($user, $pass, $iam_settings, $extra = null){
+function ldap_mbox_login($user, $pass, $extra = null){
   global $pdo;
+  global $iam_provider;
+  global $iam_settings;
 
-  $iam_provider = identity_provider();
   $is_internal = $extra['is_internal'];
   $create = $extra['create'];
 
@@ -534,18 +548,26 @@ function ldap_mbox_login($user, $pass, $iam_settings, $extra = null){
     return false;
   }
 
-  // get mapped template, if not set return false
-  // also return false if no mappers were defined
+  // get mapped template
   $user_template = $user_res[$iam_settings['attribute_field']][0];
-  if ($create && (empty($iam_settings['mappers']) || !$user_template)){
-    return false;
-  } else if (!$create) {
-    // login success - dont create mailbox
+  $mapper_key = array_search($user_template, $iam_settings['mappers']);
+
+  if (!$create) {
+    // login success
+    if ($mapper_key !== false) {
+      // update user
+      mailbox('edit', 'mailbox_from_template', array(
+        'username' => $user,
+        'name' => $user_res['displayname'][0],
+        'template' => $iam_settings['templates'][$mapper_key],
+        'hasAccess' => true
+      ));
+    }
     return 'user';
   }
 
   // check if matching attribute exist
-  $mapper_key = array_search($user_template, $iam_settings['mappers']);
+  if (empty($iam_settings['mappers']) || !$user_template) return false;
   if ($mapper_key === false) return false;
 
   // create mailbox
@@ -554,9 +576,13 @@ function ldap_mbox_login($user, $pass, $iam_settings, $extra = null){
     'local_part' => explode('@', $user)[0],
     'name' => $user_res['displayname'][0],
     'authsource' => 'ldap',
-    'template' => $iam_settings['templates'][$mapper_key]
+    'template' => $iam_settings['templates'][$mapper_key],
+    'hasAccess' => true
   ));
-  if (!$create_res) return false;
+  if (!$create_res){
+    clear_session();
+    return false;
+  }
 
   return 'user';
 }

+ 86 - 70
data/web/inc/functions.inc.php

@@ -1072,6 +1072,8 @@ function set_tfa($_data) {
   global $pdo;
   global $yubi;
   global $tfa;
+  global $iam_settings;
+
   $_data_log = $_data;
   $access_denied = null;
   !isset($_data_log['confirm_password']) ?: $_data_log['confirm_password'] = '*';
@@ -1100,7 +1102,6 @@ function set_tfa($_data) {
     $row = $stmt->fetch(PDO::FETCH_ASSOC);
     if ($row) {
       if ($row['authsource'] == 'ldap'){
-        $iam_settings = identity_provider('get');
         if (!ldap_mbox_login($username, $_data["confirm_password"], $iam_settings)) $access_denied = true;
         else $access_denied = false;
       } else {
@@ -2129,20 +2130,13 @@ function uuid4() {
 function identity_provider($_action = null, $_data = null, $_extra = null) {
   global $pdo;
   global $iam_provider;
+  global $iam_settings;
 
   $data_log = $_data;
   if (isset($data_log['client_secret'])) $data_log['client_secret'] = '*';
   if (isset($data_log['access_token'])) $data_log['access_token'] = '*';
 
   switch ($_action) {
-    case NULL:
-      if ($iam_provider) {
-        return $iam_provider;
-      } else {
-        $iam_provider = identity_provider("init");
-        return $iam_provider;
-      }
-    break;
     case 'get':
       $settings = array();
       $stmt = $pdo->prepare("SELECT * FROM `identity_provider`;");
@@ -2228,6 +2222,7 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
         return false;
       }
 
+      $_data['ignore_ssl_error']  = isset($_data['ignore_ssl_error']) ? boolval($_data['ignore_ssl_error']) : false;
       switch ($_data['authsource']) {
         case "keycloak":
           $_data['server_url']        = (!empty($_data['server_url'])) ? rtrim($_data['server_url'], '/') : null;
@@ -2236,16 +2231,17 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
           $_data['import_users']      = isset($_data['import_users']) ? intval($_data['import_users']) : 0;
           $_data['sync_interval']     = (!empty($_data['sync_interval'])) ? intval($_data['sync_interval']) : 15;
           $_data['sync_interval']     = $_data['sync_interval'] < 1 ? 1 : $_data['sync_interval'];
-          $required_settings          = array('authsource', 'server_url', 'realm', 'client_id', 'client_secret', 'redirect_url', 'version', 'mailpassword_flow', 'periodic_sync', 'import_users', 'sync_interval');
+          $required_settings          = array('authsource', 'server_url', 'realm', 'client_id', 'client_secret', 'redirect_url', 'version', 'mailpassword_flow', 'periodic_sync', 'import_users', 'sync_interval', 'ignore_ssl_error');
         break;
         case "generic-oidc":
           $_data['authorize_url']     = (!empty($_data['authorize_url'])) ? $_data['authorize_url'] : null;
           $_data['token_url']         = (!empty($_data['token_url'])) ? $_data['token_url'] : null;
           $_data['userinfo_url']      = (!empty($_data['userinfo_url'])) ? $_data['userinfo_url'] : null;
           $_data['client_scopes']     = (!empty($_data['client_scopes'])) ? $_data['client_scopes'] : "openid profile email";
-          $required_settings          = array('authsource', 'authorize_url', 'token_url', 'client_id', 'client_secret', 'redirect_url', 'userinfo_url', 'client_scopes');
+          $required_settings          = array('authsource', 'authorize_url', 'token_url', 'client_id', 'client_secret', 'redirect_url', 'userinfo_url', 'client_scopes', 'ignore_ssl_error');
         break;
         case "ldap":
+          $_data['host']              = (!empty($_data['host'])) ? str_replace(" ", "", $_data['host']) : "";
           $_data['port']              = (!empty($_data['port'])) ? intval($_data['port']) : 389;
           $_data['username_field']    = (!empty($_data['username_field'])) ? strtolower($_data['username_field']) : "mail";
           $_data['attribute_field']   = (!empty($_data['attribute_field'])) ? strtolower($_data['attribute_field']) : "";
@@ -2254,7 +2250,6 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
           $_data['import_users']      = isset($_data['import_users']) ? intval($_data['import_users']) : 0;
           $_data['use_ssl']           = isset($_data['use_ssl']) ? boolval($_data['use_ssl']) : false;
           $_data['use_tls']           = isset($_data['use_tls']) && !$_data['use_ssl'] ? boolval($_data['use_tls']) : false;
-          $_data['ignore_ssl_error']  = isset($_data['ignore_ssl_error']) ? boolval($_data['ignore_ssl_error']) : false;
           $_data['sync_interval']     = (!empty($_data['sync_interval'])) ? intval($_data['sync_interval']) : 15;
           $_data['sync_interval']     = $_data['sync_interval'] < 1 ? 1 : $_data['sync_interval'];
           $required_settings          = array('authsource', 'host', 'port', 'basedn', 'username_field', 'filter', 'attribute_field', 'binddn', 'bindpass', 'periodic_sync', 'import_users', 'sync_interval', 'use_ssl', 'use_tls', 'ignore_ssl_error');
@@ -2362,7 +2357,7 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
             $options[LDAP_OPT_X_TLS_REQUIRE_CERT] = LDAP_OPT_X_TLS_NEVER;
           }
           $provider = new \LdapRecord\Connection([
-            'hosts'                     => [$_data['host']],
+            'hosts'                     => explode(",", $_data['host']),
             'port'                      => $_data['port'],
             'base_dn'                   => $_data['basedn'],
             'username'                  => $_data['binddn'],
@@ -2414,55 +2409,71 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
       return true;
     break;
     case "init":
-      $iam_settings = identity_provider('get');
+      $settings = identity_provider('get');
       $provider = null;
 
-      switch ($iam_settings['authsource']) {
+      switch ($settings['authsource']) {
         case "keycloak":
-          if ($iam_settings['server_url'] && $iam_settings['realm'] && $iam_settings['client_id'] &&
-            $iam_settings['client_secret'] && $iam_settings['redirect_url'] && $iam_settings['version']){
+          if ($settings['server_url'] && $settings['realm'] && $settings['client_id'] &&
+            $settings['client_secret'] && $settings['redirect_url'] && $settings['version']){
+            $guzzyClient = new GuzzleHttp\Client([
+              'defaults' => [
+                \GuzzleHttp\RequestOptions::CONNECT_TIMEOUT => 5,
+                \GuzzleHttp\RequestOptions::ALLOW_REDIRECTS => true],
+                \GuzzleHttp\RequestOptions::VERIFY => !$settings['ignore_ssl_error'],
+              ]
+            );
             $provider = new Stevenmaguire\OAuth2\Client\Provider\Keycloak([
-              'authServerUrl'         => $iam_settings['server_url'],
-              'realm'                 => $iam_settings['realm'],
-              'clientId'              => $iam_settings['client_id'],
-              'clientSecret'          => $iam_settings['client_secret'],
-              'redirectUri'           => $iam_settings['redirect_url'],
-              'version'               => $iam_settings['version'],
+              'authServerUrl'         => $settings['server_url'],
+              'realm'                 => $settings['realm'],
+              'clientId'              => $settings['client_id'],
+              'clientSecret'          => $settings['client_secret'],
+              'redirectUri'           => $settings['redirect_url'],
+              'version'               => $settings['version'],
               // 'encryptionAlgorithm'   => 'RS256',                             // optional
               // 'encryptionKeyPath'     => '../key.pem'                         // optional
               // 'encryptionKey'         => 'contents_of_key_or_certificate'     // optional
             ]);
+            $provider->setHttpClient($guzzyClient);
           }
         break;
         case "generic-oidc":
-          if ($iam_settings['client_id'] && $iam_settings['client_secret'] && $iam_settings['redirect_url'] &&
-            $iam_settings['authorize_url'] && $iam_settings['token_url'] && $iam_settings['userinfo_url']){
+          if ($settings['client_id'] && $settings['client_secret'] && $settings['redirect_url'] &&
+            $settings['authorize_url'] && $settings['token_url'] && $settings['userinfo_url']){
+            $guzzyClient = new GuzzleHttp\Client([
+              'defaults' => [
+                \GuzzleHttp\RequestOptions::CONNECT_TIMEOUT => 5,
+                \GuzzleHttp\RequestOptions::ALLOW_REDIRECTS => true],
+                \GuzzleHttp\RequestOptions::VERIFY => !$settings['ignore_ssl_error'],
+              ]
+            );
             $provider = new \League\OAuth2\Client\Provider\GenericProvider([
-              'clientId'                => $iam_settings['client_id'],
-              'clientSecret'            => $iam_settings['client_secret'],
-              'redirectUri'             => $iam_settings['redirect_url'],
-              'urlAuthorize'            => $iam_settings['authorize_url'],
-              'urlAccessToken'          => $iam_settings['token_url'],
-              'urlResourceOwnerDetails' => $iam_settings['userinfo_url'],
-              'scopes'                  => $iam_settings['client_scopes']
+              'clientId'                => $settings['client_id'],
+              'clientSecret'            => $settings['client_secret'],
+              'redirectUri'             => $settings['redirect_url'],
+              'urlAuthorize'            => $settings['authorize_url'],
+              'urlAccessToken'          => $settings['token_url'],
+              'urlResourceOwnerDetails' => $settings['userinfo_url'],
+              'scopes'                  => $settings['client_scopes']
             ]);
+            $provider->setHttpClient($guzzyClient);
           }
         break;
         case "ldap":
-          if ($iam_settings['host'] && $iam_settings['port'] && $iam_settings['basedn'] &&
-            $iam_settings['binddn'] && $iam_settings['bindpass']){
+          if ($settings['host'] && $settings['port'] && $settings['basedn'] &&
+            $settings['binddn'] && $settings['bindpass']){
             $options = array();
-            if ($iam_settings['ignore_ssl_error']) {
+            if ($settings['ignore_ssl_error']) {
               $options[LDAP_OPT_X_TLS_REQUIRE_CERT] = LDAP_OPT_X_TLS_NEVER;
             }
             $provider = new \LdapRecord\Connection([
-              'hosts'                     => [$iam_settings['host']],
-              'port'                      => $iam_settings['port'],
-              'base_dn'                   => $iam_settings['basedn'],
-              'username'                  => $iam_settings['binddn'],
-              'password'                  => $iam_settings['bindpass'],
-              'use_ssl'                   => $iam_settings['use_ssl'],
-              'use_tls'                   => $iam_settings['use_tls'],
+              'hosts'                     => explode(",", $settings['host']),
+              'port'                      => $settings['port'],
+              'base_dn'                   => $settings['basedn'],
+              'username'                  => $settings['binddn'],
+              'password'                  => $settings['bindpass'],
+              'use_ssl'                   => $settings['use_ssl'],
+              'use_tls'                   => $settings['use_tls'],
               'options'                   => $options
             ]);
             try {
@@ -2473,12 +2484,9 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
           }
         break;
       }
-
       return $provider;
     break;
     case "verify-sso":
-      $provider = $_data['iam_provider'];
-      $iam_settings = identity_provider('get');
       if ($iam_settings['authsource'] != 'keycloak' && $iam_settings['authsource'] != 'generic-oidc'){
         $_SESSION['return'][] =  array(
           'type' => 'danger',
@@ -2489,10 +2497,10 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
       }
 
       try {
-        $token = $provider->getAccessToken('authorization_code', ['code' => $_GET['code']]);
-        $_SESSION['iam_token'] = $token->getToken();
-        $_SESSION['iam_refresh_token'] = $token->getRefreshToken();
-        $info = $provider->getResourceOwner($token)->toArray();
+        $token = $iam_provider->getAccessToken('authorization_code', ['code' => $_GET['code']]);
+        $plain_token = $token->getToken();
+        $plain_refreshtoken = $token->getRefreshToken();
+        $info = $iam_provider->getResourceOwner($token)->toArray();
       } catch (Throwable $e) {
         $_SESSION['return'][] =  array(
           'type' => 'danger',
@@ -2504,6 +2512,10 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
       // check if email address is given
       if (empty($info['email'])) return false;
 
+      // get mapped template
+      $user_template = $info['mailcow_template'];
+      $mapper_key = array_search($user_template, $iam_settings['mappers']);
+
       // token valid, get mailbox
       $stmt = $pdo->prepare("SELECT * FROM `mailbox`
         INNER JOIN domain on mailbox.domain = domain.domain
@@ -2516,7 +2528,18 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
       $row = $stmt->fetch(PDO::FETCH_ASSOC);
       if ($row){
         // success
+        if ($mapper_key !== false) {
+          // update user
+          mailbox('edit', 'mailbox_from_template', array(
+            'username' => $info['email'],
+            'name' => $info['name'],
+            'template' => $iam_settings['templates'][$mapper_key],
+            'hasAccess' => true
+          ));
+        }
         set_user_loggedin_session($info['email']);
+        $_SESSION['iam_token'] = $plain_token;
+        $_SESSION['iam_refresh_token'] = $plain_refreshtoken;
         $_SESSION['return'][] =  array(
           'type' => 'success',
           'log' => array(__FUNCTION__, $_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role']),
@@ -2525,9 +2548,6 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
         return true;
       }
 
-      // get mapped template, if not set return false
-      // also return false if no mappers were defined
-      $user_template = $info['mailcow_template'];
       if (empty($iam_settings['mappers']) || empty($user_template)){
         clear_session();
         $_SESSION['return'][] =  array(
@@ -2537,9 +2557,6 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
         );
         return false;
       }
-
-      // check if matching attribute exist
-      $mapper_key = array_search($user_template, $iam_settings['mappers']);
       if ($mapper_key === false) {
         clear_session();
         $_SESSION['return'][] =  array(
@@ -2554,9 +2571,10 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
       $create_res = mailbox('add', 'mailbox_from_template', array(
         'domain' => explode('@', $info['email'])[1],
         'local_part' => explode('@', $info['email'])[0],
-        'name' => $info['firstName'] . " " . $info['lastName'],
+        'name' => $info['name'],
         'authsource' => $iam_settings['authsource'],
-        'template' => $iam_settings['templates'][$mapper_key]
+        'template' => $iam_settings['templates'][$mapper_key],
+        'hasAccess' => true
       ));
       if (!$create_res){
         clear_session();
@@ -2569,6 +2587,8 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
       }
 
       set_user_loggedin_session($info['email']);
+      $_SESSION['iam_token'] = $plain_token;
+      $_SESSION['iam_refresh_token'] = $plain_refreshtoken;
       $_SESSION['return'][] =  array(
         'type' => 'success',
         'log' => array(__FUNCTION__, $_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role']),
@@ -2577,13 +2597,11 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
       return true;
     break;
     case "refresh-token":
-      $provider = $_data['iam_provider'];
-
       try {
-        $token = $provider->getAccessToken('refresh_token', ['refresh_token' => $_SESSION['iam_refresh_token']]);
-        $_SESSION['iam_token'] = $token->getToken();
-        $_SESSION['iam_refresh_token'] = $token->getRefreshToken();
-        $info = $provider->getResourceOwner($token)->toArray();
+        $token = $iam_provider->getAccessToken('refresh_token', ['refresh_token' => $_SESSION['iam_refresh_token']]);
+        $plain_token = $token->getToken();
+        $plain_refreshtoken = $token->getRefreshToken();
+        $info = $iam_provider->getResourceOwner($token)->toArray();
       } catch (Throwable $e) {
         clear_session();
         $_SESSION['return'][] =  array(
@@ -2604,22 +2622,20 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
         return false;
       }
 
-      $_SESSION['mailcow_cc_username'] = $info['email'];
-      $_SESSION['mailcow_cc_role'] = "user";
+      set_user_loggedin_session($info['email']);
+      $_SESSION['iam_token'] = $plain_token;
+      $_SESSION['iam_refresh_token'] = $plain_refreshtoken;
       return true;
     break;
     case "get-redirect":
-      $iam_settings = identity_provider('get');
       if ($iam_settings['authsource'] != 'keycloak' && $iam_settings['authsource'] != 'generic-oidc')
         return false;
-      $provider = $_data['iam_provider'];
-      $authUrl = $provider->getAuthorizationUrl();
-      $_SESSION['oauth2state'] = $provider->getState();
+      $authUrl = $iam_provider->getAuthorizationUrl();
+      $_SESSION['oauth2state'] = $iam_provider->getState();
       return $authUrl;
     break;
     case "get-keycloak-admin-token":
       // get access_token for service account of mailcow client
-      $iam_settings = identity_provider('get');
       if ($iam_settings['authsource'] !== 'keycloak') return false;
       if (isset($iam_settings['access_token'])) {
         // check if access_token is valid

+ 46 - 38
data/web/inc/functions.mailbox.inc.php

@@ -1045,7 +1045,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             $password2 = '';
             $password_hashed = '';
           }
-          if (!$_extra['iam_create_login'] && ((!isset($_SESSION['acl']['unlimited_quota']) || $_SESSION['acl']['unlimited_quota'] != "1") && $quota_m === 0)) {
+          if (!$_extra['hasAccess'] && ((!isset($_SESSION['acl']['unlimited_quota']) || $_SESSION['acl']['unlimited_quota'] != "1") && $quota_m === 0)) {
             $_SESSION['return'][] = array(
               'type' => 'danger',
               'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -1075,6 +1075,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $quarantine_category = (isset($_data['quarantine_category'])) ? strval($_data['quarantine_category']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']);
           $quota_b    = ($quota_m * 1048576);
           $attribute_hash = (!empty($_data['attribute_hash'])) ? $_data['attribute_hash'] : '';
+          if (in_array($authsource, array('keycloak', 'generic-oidc', 'ldap'))){
+            $force_pw_update = 0;
+          }
           $mailbox_attrs = json_encode(
             array(
               'force_pw_update' => strval($force_pw_update),
@@ -1101,7 +1104,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             );
             return false;
           }
-          if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain) && !$_extra['iam_create_login']) {
+          if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain) && !$_extra['hasAccess']) {
             $_SESSION['return'][] = array(
               'type' => 'danger',
               'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -1364,6 +1367,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $attribute_hash = sha1(json_encode($mbox_template_data["attributes"]));
           $mbox_template_data = json_decode($mbox_template_data["attributes"], true);
           $mbox_template_data['domain'] = $_data['domain'];
+          $mbox_template_data['name'] = $_data['name'];
           $mbox_template_data['local_part'] = $_data['local_part'];
           $mbox_template_data['authsource'] = $_data['authsource'];
           $mbox_template_data['attribute_hash'] = $attribute_hash;
@@ -1381,7 +1385,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             }
           }
 
-          return mailbox('add', 'mailbox', $mailbox_attributes, array('iam_create_login' => true));
+          return mailbox('add', 'mailbox', $mailbox_attributes, array('hasAccess' => $_data['hasAccess']));
         break;
         case 'resource':
           $domain             = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
@@ -1749,7 +1753,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           else {
             $usernames = $_data['username'];
           }
-          if (!isset($_SESSION['acl']['tls_policy']) || $_SESSION['acl']['tls_policy'] != "1" ) {
+          if (!$_extra['hasAccess'] && (!isset($_SESSION['acl']['tls_policy']) || $_SESSION['acl']['tls_policy'] != "1")) {
             $_SESSION['return'][] = array(
               'type' => 'danger',
               'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -1758,7 +1762,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             return false;
           }
           foreach ($usernames as $username) {
-            if (!filter_var($username, FILTER_VALIDATE_EMAIL) || !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
+            if (!$_extra['hasAccess'] && (!filter_var($username, FILTER_VALIDATE_EMAIL) || !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username))) {
               $_SESSION['return'][] = array(
                 'type' => 'danger',
                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -1766,7 +1770,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               continue;
             }
-            $is_now = mailbox('get', 'tls_policy', $username);
+            $is_now = mailbox('get', 'tls_policy', $username, $_extra);
             if (!empty($is_now)) {
               $tls_enforce_in = (isset($_data['tls_enforce_in'])) ? intval($_data['tls_enforce_in']) : $is_now['tls_enforce_in'];
               $tls_enforce_out = (isset($_data['tls_enforce_out'])) ? intval($_data['tls_enforce_out']) : $is_now['tls_enforce_out'];
@@ -1803,7 +1807,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           else {
             $usernames = $_data['username'];
           }
-          if (!isset($_SESSION['acl']['quarantine_notification']) || $_SESSION['acl']['quarantine_notification'] != "1" ) {
+          if (!$_extra['hasAccess'] && (!isset($_SESSION['acl']['quarantine_notification']) || $_SESSION['acl']['quarantine_notification'] != "1")) {
             $_SESSION['return'][] = array(
               'type' => 'danger',
               'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -1812,7 +1816,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             return false;
           }
           foreach ($usernames as $username) {
-            if (!filter_var($username, FILTER_VALIDATE_EMAIL) || !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
+            if (!$_extra['hasAccess'] && (!filter_var($username, FILTER_VALIDATE_EMAIL) || !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username))) {
               $_SESSION['return'][] = array(
                 'type' => 'danger',
                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -1820,7 +1824,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               continue;
             }
-            $is_now = mailbox('get', 'quarantine_notification', $username);
+            $is_now = mailbox('get', 'quarantine_notification', $username, $_extra);
             if (!empty($is_now)) {
               $quarantine_notification = (isset($_data['quarantine_notification'])) ? $_data['quarantine_notification'] : $is_now['quarantine_notification'];
             }
@@ -1862,7 +1866,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           else {
             $usernames = $_data['username'];
           }
-          if (!isset($_SESSION['acl']['quarantine_category']) || $_SESSION['acl']['quarantine_category'] != "1" ) {
+          if (!$_extra['hasAccess'] && (!isset($_SESSION['acl']['quarantine_category']) || $_SESSION['acl']['quarantine_category'] != "1")) {
             $_SESSION['return'][] = array(
               'type' => 'danger',
               'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -1871,7 +1875,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             return false;
           }
           foreach ($usernames as $username) {
-            if (!filter_var($username, FILTER_VALIDATE_EMAIL) || !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
+            if (!$_extra['hasAccess'] && (!filter_var($username, FILTER_VALIDATE_EMAIL) || !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username))) {
               $_SESSION['return'][] = array(
                 'type' => 'danger',
                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -1879,7 +1883,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               continue;
             }
-            $is_now = mailbox('get', 'quarantine_category', $username);
+            $is_now = mailbox('get', 'quarantine_category', $username, $_extra);
             if (!empty($is_now)) {
               $quarantine_category = (isset($_data['quarantine_category'])) ? $_data['quarantine_category'] : $is_now['quarantine_category'];
             }
@@ -2923,7 +2927,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               continue;
             }
-            $is_now = mailbox('get', 'mailbox_details', $username);
+            $is_now = mailbox('get', 'mailbox_details', $username, $_extra);
             if (isset($_data['protocol_access'])) {
               $_data['protocol_access'] = (array)$_data['protocol_access'];
               $_data['imap_access'] = (in_array('imap', $_data['protocol_access'])) ? 1 : 0;
@@ -2934,12 +2938,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             if (!empty($is_now)) {
               $active               = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
               (int)$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($is_now['attributes']['force_pw_update']);
-              (int)$sogo_access     = (isset($_data['sogo_access']) && isset($_SESSION['acl']['sogo_access']) && $_SESSION['acl']['sogo_access'] == "1") ? intval($_data['sogo_access']) : intval($is_now['attributes']['sogo_access']);
-              (int)$imap_access     = (isset($_data['imap_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['imap_access']) : intval($is_now['attributes']['imap_access']);
-              (int)$pop3_access     = (isset($_data['pop3_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']);
-              (int)$smtp_access     = (isset($_data['smtp_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']);
-              (int)$sieve_access    = (isset($_data['sieve_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['sieve_access']) : intval($is_now['attributes']['sieve_access']);
-              (int)$relayhost       = (isset($_data['relayhost']) && isset($_SESSION['acl']['mailbox_relayhost']) && $_SESSION['acl']['mailbox_relayhost'] == "1") ? intval($_data['relayhost']) : intval($is_now['attributes']['relayhost']);
+              (int)$sogo_access     = ((isset($_data['sogo_access']) && isset($_SESSION['acl']['sogo_access']) && $_SESSION['acl']['sogo_access'] == "1") || $_extra['hasAccess']) ? intval($_data['sogo_access']) : intval($is_now['attributes']['sogo_access']);
+              (int)$imap_access     = ((isset($_data['imap_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") || $_extra['hasAccess']) ? intval($_data['imap_access']) : intval($is_now['attributes']['imap_access']);
+              (int)$pop3_access     = ((isset($_data['pop3_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") || $_extra['hasAccess']) ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']);
+              (int)$smtp_access     = ((isset($_data['smtp_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") || $_extra['hasAccess']) ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']);
+              (int)$sieve_access    = ((isset($_data['sieve_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") || $_extra['hasAccess']) ? intval($_data['sieve_access']) : intval($is_now['attributes']['sieve_access']);
+              (int)$relayhost       = ((isset($_data['relayhost']) && isset($_SESSION['acl']['mailbox_relayhost']) && $_SESSION['acl']['mailbox_relayhost'] == "1") || $_extra['hasAccess']) ? intval($_data['relayhost']) : intval($is_now['attributes']['relayhost']);
               (int)$quota_m         = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576);
               $name                 = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name'];
               $domain               = $is_now['domain'];
@@ -2952,6 +2956,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               if (in_array($_data['authsource'], array('mailcow', 'keycloak', 'generic-oidc', 'ldap'))){
                 $authsource = $_data['authsource'];
               }
+              if (in_array($authsource, array('keycloak', 'generic-oidc', 'ldap'))){
+                $force_pw_update = 0;
+              }
               $pw_recovery_email    = (isset($_data['pw_recovery_email']) && $authsource == 'mailcow') ? $_data['pw_recovery_email'] : $is_now['attributes']['recovery_email'];
             }
             else {
@@ -2963,7 +2970,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               continue;
             }
             // if already 0 == ok
-            if ((!isset($_SESSION['acl']['unlimited_quota']) || $_SESSION['acl']['unlimited_quota'] != "1") && ($quota_m == 0 && $is_now['quota'] != 0)) {
+            if (!$_extra['hasAccess'] && (!isset($_SESSION['acl']['unlimited_quota']) || $_SESSION['acl']['unlimited_quota'] != "1") && ($quota_m == 0 && $is_now['quota'] != 0)) {
               $_SESSION['return'][] = array(
                 'type' => 'danger',
                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -2971,7 +2978,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               return false;
             }
-            if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+            if (!$_extra['hasAccess'] && !hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
               $_SESSION['return'][] = array(
                 'type' => 'danger',
                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -2979,7 +2986,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               continue;
             }
-            $DomainData = mailbox('get', 'domain_details', $domain);
+            $DomainData = mailbox('get', 'domain_details', $domain, $_extra);
             if ($quota_m > ($is_now['max_new_quota'] / 1048576)) {
               $_SESSION['return'][] = array(
                 'type' => 'danger',
@@ -2998,7 +3005,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             }
             $extra_acls = array();
             if (isset($_data['extended_sender_acl'])) {
-              if (!isset($_SESSION['acl']['extend_sender_acl']) || $_SESSION['acl']['extend_sender_acl'] != "1" ) {
+              if (!$_extra['hasAccess'] && (!isset($_SESSION['acl']['extend_sender_acl']) || $_SESSION['acl']['extend_sender_acl'] != "1")) {
                 $_SESSION['return'][] = array(
                   'type' => 'danger',
                   'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -3493,7 +3500,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           }
 
           $attribute_hash = sha1(json_encode($mbox_template_data["attributes"]));
-          $is_now = mailbox('get', 'mailbox_details', $_data['username']);
+          $is_now = mailbox('get', 'mailbox_details', $_data['username'], array('hasAccess' => $_data['hasAccess']));
           $name = ltrim(rtrim($_data['name'], '>'), '<');
           if ($is_now['attributes']['attribute_hash'] == $attribute_hash && $is_now['name'] == $name)
             return true;
@@ -3529,19 +3536,20 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           }
 
           $mailbox_attributes['quota'] = intval($mailbox_attributes['quota'] / 1048576);
-          $result = mailbox('edit', 'mailbox', $mailbox_attributes);
+          $result = mailbox('edit', 'mailbox', $mailbox_attributes, array('hasAccess' => $_data['hasAccess']));
           if ($result === false) return $result;
-          $result = mailbox('edit', 'tls_policy', $tls_attributes);
+          $result = mailbox('edit', 'tls_policy', $tls_attributes, array('hasAccess' => $_data['hasAccess']));
           if ($result === false) return $result;
-          $result = mailbox('edit', 'quarantine_notification', $quarantine_attributes);
+          $result = mailbox('edit', 'quarantine_notification', $quarantine_attributes, array('hasAccess' => $_data['hasAccess']));
           if ($result === false) return $result;
-          $result = mailbox('edit', 'quarantine_category', $quarantine_attributes);
+          $result = mailbox('edit', 'quarantine_category', $quarantine_attributes, array('hasAccess' => $_data['hasAccess']));
           if ($result === false) return $result;
-          $result = ratelimit('edit', 'mailbox', $ratelimit_attributes);
+          $result = ratelimit('edit', 'mailbox', $ratelimit_attributes, array('hasAccess' => $_data['hasAccess']));
           if ($result === false) return $result;
-          $result = acl('edit', 'user', $acl_attributes);
+          $result = acl('edit', 'user', $acl_attributes, array('hasAccess' => $_data['hasAccess']));
           if ($result === false) return $result;
 
+          $_SESSION['return'] = array();
           return true;
         break;
         case 'mailbox_templates':
@@ -4077,7 +4085,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
         case 'tls_policy':
           $attrs = array();
           if (isset($_data) && filter_var($_data, FILTER_VALIDATE_EMAIL)) {
-            if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
+            if (!$_extra['hasAccess'] && !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
               return false;
             }
           }
@@ -4096,7 +4104,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
         case 'quarantine_notification':
           $attrs = array();
           if (isset($_data) && filter_var($_data, FILTER_VALIDATE_EMAIL)) {
-            if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
+            if (!$_extra['hasAccess'] && !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
               return false;
             }
           }
@@ -4112,7 +4120,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
         case 'quarantine_category':
           $attrs = array();
           if (isset($_data) && filter_var($_data, FILTER_VALIDATE_EMAIL)) {
-            if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
+            if (!$_extra['hasAccess'] && (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data))) {
               return false;
             }
           }
@@ -4627,7 +4635,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
         case 'domain_details':
           $domaindata = array();
           $_data = idn_to_ascii(strtolower(trim($_data)), 0, INTL_IDNA_VARIANT_UTS46);
-          if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
+          if (!$_extra['hasAccess'] && !hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
             return false;
           }
           $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` =  :domain");
@@ -4793,7 +4801,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           }
         break;
         case 'mailbox_details':
-          if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
+          if (!$_extra['hasAccess'] && !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
             return false;
           }
           $mailboxdata = array();
@@ -4891,7 +4899,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             else if ($SaslLogs['service'] == 'pop3') {
               $last_pop3_login = strtotime($SaslLogs['datetime']);
             }
-			else if ($SaslLogs['service'] == 'SSO') {
+			      else if ($SaslLogs['service'] == 'SSO') {
               $last_sso_login = strtotime($SaslLogs['datetime']);
             }
           }
@@ -4904,7 +4912,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           if (!isset($last_pop3_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
             $last_pop3_login = 0;
           }
-		  if (!isset($last_sso_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
+		      if (!isset($last_sso_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
             $last_sso_login = 0;
           }
           $mailboxdata['last_imap_login'] = $last_imap_login;
@@ -4956,7 +4964,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           return $mailboxdata;
         break;
         case 'mailbox_templates':
-          if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin" && !$_extra['iam_create_login']) {
+          if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin" && !$_extra['hasAccess']) {
             return false;
           }
           $_data = (isset($_data)) ? intval($_data) : null;

+ 5 - 5
data/web/inc/functions.ratelimit.inc.php

@@ -4,7 +4,7 @@ function ratelimit($_action, $_scope, $_data = null, $_extra = null) {
   $_data_log = $_data;
   switch ($_action) {
     case 'edit':
-      if ((!isset($_SESSION['acl']['ratelimit']) || $_SESSION['acl']['ratelimit'] != "1") && !$_extra['iam_create_login']) {
+      if ((!isset($_SESSION['acl']['ratelimit']) || $_SESSION['acl']['ratelimit'] != "1") && !$_extra['hasAccess']) {
         $_SESSION['return'][] = array(
           'type' => 'danger',
           'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -92,8 +92,8 @@ function ratelimit($_action, $_scope, $_data = null, $_extra = null) {
               );
               continue;
             }
-            if ((!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $object)
-                || ($_SESSION['mailcow_cc_role'] != 'admin' && $_SESSION['mailcow_cc_role'] != 'domainadmin')) && !$_extra['iam_create_login']) {
+            if (((!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $object)
+                || ($_SESSION['mailcow_cc_role'] != 'admin' && $_SESSION['mailcow_cc_role'] != 'domainadmin'))) && !$_extra['hasAccess']) {
               $_SESSION['return'][] = array(
                 'type' => 'danger',
                 'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
@@ -139,7 +139,7 @@ function ratelimit($_action, $_scope, $_data = null, $_extra = null) {
     case 'get':
       switch ($_scope) {
         case 'domain':
-          if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
+          if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data) && !$_extra['hasAccess']) {
             return false;
           }
           try {
@@ -164,7 +164,7 @@ function ratelimit($_action, $_scope, $_data = null, $_extra = null) {
           return false;
         break;
         case 'mailbox':
-          if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)
+          if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data && !$_extra['hasAccess'])
             || ($_SESSION['mailcow_cc_role'] != 'admin' && $_SESSION['mailcow_cc_role'] != 'domainadmin')) {
             return false;
           }

+ 2 - 1
data/web/inc/prerequisites.inc.php

@@ -180,6 +180,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php';
 
 // Init Identity Provider
 $iam_provider = identity_provider('init');
+$iam_settings = identity_provider('get');
 
 // IMAP lib
 // use Ddeboer\Imap\Server;
@@ -323,7 +324,7 @@ $UI_TEXTS = customize('get', 'ui_texts');
 if (file_exists('/web/css/themes/'.$UI_THEME.'-bootstrap.css'))
   $css_minifier->add('/web/css/themes/'.$UI_THEME.'-bootstrap.css');
 else
-  $css_minifier->add('/web/css/themes/lumen-bootstrap.css'); 
+  $css_minifier->add('/web/css/themes/lumen-bootstrap.css');
 // minify css build files
 foreach ($css_dir as $css_file) {
   $css_minifier->add('/web/css/build/' . $css_file);

+ 4 - 4
data/web/inc/triggers.inc.php

@@ -3,18 +3,18 @@
 if ($iam_provider){
   if (isset($_GET['iam_sso'])){
     // redirect for sso
-    $redirect_uri = identity_provider('get-redirect', array('iam_provider' => $iam_provider));
+    $redirect_uri = identity_provider('get-redirect');
     $redirect_uri = !empty($redirect_uri) ? $redirect_uri : '/';
     header('Location: ' . $redirect_uri);
     die();
   }
   if ($_SESSION['iam_token'] && $_SESSION['iam_refresh_token']) {
     // Session found, try to refresh
-    $isRefreshed = identity_provider('refresh-token', array('iam_provider' => $iam_provider));
+    $isRefreshed = identity_provider('refresh-token');
 
     if (!$isRefreshed){
       // Session could not be refreshed, redirect to provider
-      $redirect_uri = identity_provider('get-redirect', array('iam_provider' => $iam_provider));
+      $redirect_uri = identity_provider('get-redirect');
       $redirect_uri = !empty($redirect_uri) ? $redirect_uri : '/';
       header('Location: ' . $redirect_uri);
       die();
@@ -23,7 +23,7 @@ if ($iam_provider){
     // Check given state against previously stored one to mitigate CSRF attack
     // Recieved access token in $_GET['code']
     // extract info and verify user
-    identity_provider('verify-sso', array('iam_provider' => $iam_provider));
+    identity_provider('verify-sso');
   }
 }
 

+ 2 - 2
data/web/index.php

@@ -15,7 +15,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
   header('Location: /mailbox');
   exit();
 }
-elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {    
+elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
   $user_details = mailbox("get", "mailbox_details", $_SESSION['mailcow_cc_username']);
   $is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false;
   if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual) {
@@ -32,7 +32,7 @@ $_SESSION['index_query_string'] = $_SERVER['QUERY_STRING'];
 
 $has_iam_sso = false;
 if ($iam_provider){
-  $has_iam_sso = identity_provider("get-redirect", array('iam_provider' => $iam_provider)) ? true : false; 
+  $has_iam_sso = identity_provider("get-redirect") ? true : false;
 }
 
 

+ 7 - 3
data/web/json_api.php

@@ -1707,8 +1707,13 @@ if (isset($_GET['query'])) {
             if ($score)
               $score = array("score" => preg_replace("/\s+/", "", $score));
             process_get_return($score);
-          case "identity_provider":
-            process_get_return(identity_provider('get'));
+          break;
+          case "identity-provider":
+            if($_SESSION['mailcow_cc_role'] === 'admin') {
+              process_get_return($iam_settings);
+            } else {
+              process_get_return(null);
+            }
           break;
         break;
         // return no route found if no case is matched
@@ -2086,7 +2091,6 @@ if (isset($_GET['query'])) {
         break;
         case "cors":
           process_edit_return(cors('edit', $attr));
-        case "identity_provider":
         case "identity-provider":
           process_edit_return(identity_provider('edit', $attr));
         break;

+ 1 - 0
data/web/lang/lang.en-gb.json

@@ -225,6 +225,7 @@
         "iam_description": "Configure an external Provider for Authentication<br>User's mailboxes will be automatically created upon their first login, provided that an attribute mapping has been set.",
         "iam_extra_permission": "For the following settings to work, the mailcow client in Keycloak needs a <code>Service account</code> and the permission to <code>view-users</code>.",
         "iam_host": "Host",
+        "iam_host_info": "Enter one or more LDAP hosts, separated by commas.",
         "iam_import_users": "Import Users",
         "iam_mapping": "Attribute Mapping",
         "iam_bindpass": "Bind Password",

+ 132 - 41
data/web/templates/admin/tab-config-identity-provider.twig

@@ -9,7 +9,9 @@
     <div id="collapse-tab-config-identity-provider" class="card-body collapse" data-bs-parent="#admin-content">
       <p class="offset-sm-3 mb-4">{{ lang.admin.iam_description|raw }}</p>
       <div class="row mb-4">
-        <label class="control-label col-md-3 text-sm-end" for="iam_realm">{{ lang.admin.iam }}:</label>
+        <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+          <label class="control-label" for="iam_realm">{{ lang.admin.iam }}:</label>
+        </div>
         <div class="col-12 col-md-9 col-lg-4">
           <select
             data-style="btn btn-secondary"
@@ -26,25 +28,33 @@
         <form class="form-horizontal" autocapitalize="none" data-id="iam_keycloak" autocorrect="off" role="form" method="post">
           <input type="hidden" name="authsource" value="keycloak">
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end" for="iam_keycloak_url">{{ lang.admin.iam_server_url }}:</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+                <label class="control-label" for="iam_keycloak_url">{{ lang.admin.iam_server_url }}:</label>
+            </div>
             <div class="col-12 col-md-9 col-lg-4">
               <input type="text" class="form-control" id="iam_keycloak_url" name="server_url" value="{{ iam_settings.server_url }}" required>
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end" for="iam_keycloak_realm">{{ lang.admin.iam_realm }}:</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label" for="iam_keycloak_realm">{{ lang.admin.iam_realm }}:</label>
+            </div>
             <div class="col-12 col-md-9 col-lg-4">
               <input type="text" class="form-control" id="iam_keycloak_realm" name="realm" value="{{ iam_settings.realm }}" required>
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end" for="iam_keycloak_clientid">{{ lang.admin.iam_client_id }}:</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label" for="iam_keycloak_clientid">{{ lang.admin.iam_client_id }}:</label>
+            </div>
             <div class="col-12 col-md-9 col-lg-4">
               <input type="text" class="form-control" id="iam_keycloak_clientid" name="client_id" value="{{ iam_settings.client_id }}" required>
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end" for="iam_keycloak_clientsecret">{{ lang.admin.iam_client_secret }}:</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label" for="iam_keycloak_clientsecret">{{ lang.admin.iam_client_secret }}:</label>
+            </div>
             <div class="col-12 col-md-9 col-lg-4">
               <div class="reveal-password-input input-group">
                 <input type="password" class="password-field form-control" id="iam_keycloak_clientsecret" name="client_secret" value="{{ iam_settings.client_secret }}" required>
@@ -53,19 +63,25 @@
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end" for="iam_keycloak_redirecturl">{{ lang.admin.iam_redirect_url }}:</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label" for="iam_keycloak_redirecturl">{{ lang.admin.iam_redirect_url }}:</label>
+            </div>
             <div class="col-12 col-md-9 col-lg-4">
               <input type="text" class="form-control" id="iam_keycloak_redirecturl" name="redirect_url" value="{{ iam_settings.redirect_url }}" required>
             </div>
           </div>
           <div class="row mb-4">
-            <label class="control-label col-md-3 text-sm-end" for="iam_keycloak_version">{{ lang.admin.iam_version }}:</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label" for="iam_keycloak_version">{{ lang.admin.iam_version }}:</label>
+            </div>
             <div class="col-sm-4">
               <input type="text" class="form-control" id="iam_keycloak_version" name="version" value="{{ iam_settings.version }}" required>
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end">{{ lang.admin.iam_mapping }}:</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label">{{ lang.admin.iam_mapping }}:</label>
+            </div>
             <div class="col-12 col-md-9 col-lg-4">
               <div class="row px-2 align-items-center">
                 <span class="col-5 p-0 pe-2">Attribute</span>
@@ -121,13 +137,15 @@
             {% endif %}
           </div>
           <div class="row mb-2 mt-4">
-            <label class="control-label col-md-3 text-sm-end"></label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end"></div>
             <div class="col-12 col-md-9">
               <span>{{ lang.admin.iam_extra_permission|raw }}</span>
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end">{{ lang.admin.iam_rest_flow }}</label>
+            <div class="col-md-3 d-flex align-items-start justify-content-md-end">
+              <label class="control-label">{{ lang.admin.iam_rest_flow }}</label>
+            </div>
             <div class="col-12 col-md-9">
               <div class="form-check form-switch">
                 <input class="form-check-input" type="checkbox" role="switch" name="mailpassword_flow" value="1" {% if iam_settings.mailpassword_flow == 1 %}checked{% endif %}>
@@ -140,7 +158,19 @@
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end">{{ lang.admin.iam_periodic_full_sync }}</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label">{{ lang.admin.ignore_ssl_error }}</label>
+            </div>
+            <div class="col-12 col-md-9">
+              <div class="form-check form-switch">
+                <input class="form-check-input" type="checkbox" role="switch" name="ignore_ssl_error" value="1" {% if iam_settings.ignore_ssl_error == 1 %}checked{% endif %}>
+              </div>
+            </div>
+          </div>
+          <div class="row mb-2">
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label">{{ lang.admin.iam_periodic_full_sync }}</label>
+            </div>
             <div class="col-12 col-md-9">
               <div class="form-check form-switch">
                 <input class="form-check-input" type="checkbox" role="switch" name="periodic_sync"  value="1" {% if iam_settings.periodic_sync == 1 %}checked{% endif %}>
@@ -148,7 +178,9 @@
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end">{{ lang.admin.iam_import_users }}</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label">{{ lang.admin.iam_import_users }}</label>
+            </div>
             <div class="col-12 col-md-9">
               <div class="form-check form-switch">
                 <input class="form-check-input" type="checkbox" role="switch" name="import_users"  value="1" {% if iam_settings.import_users == 1 %}checked{% endif %}>
@@ -156,14 +188,16 @@
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end">{{ lang.admin.iam_sync_interval }}</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label">{{ lang.admin.iam_sync_interval }}</label>
+            </div>
             <div class="col-12 col-md-9 col-lg-4">
               <input class="form-control" type="number" min="1" name="sync_interval" style="width: 80px;"  {% if iam_settings.sync_interval %}value="{{ iam_settings.sync_interval }}"{% else %}value="15"{% endif %}>
             </div>
           </div>
           <div class="row mt-4 mb-2">
             <div class="offset-md-3 col-12 col-md-9 d-flex flex-wrap">
-              <div class="btn-group mb-2">   
+              <div class="btn-group mb-2">
                 <button class="btn btn-sm d-block d-sm-inline btn-secondary iam_test_connection iam_test_connection" data-id="iam_keycloak"><i class="bi bi-play"></i> {{ lang.admin.iam_test_connection }}</button>
                 <button class="btn btn-sm d-block d-sm-inline btn-success" data-item="identity-provider" data-action="edit_selected" data-id="iam_keycloak" data-api-url='edit/identity-provider' data-api-attr='{}'><i class="bi bi-check-lg"></i> {{ lang.admin.save }}</button>
               </div>
@@ -176,31 +210,41 @@
         <form class="form-horizontal" autocapitalize="none" data-id="iam_generic" autocorrect="off" role="form" method="post">
           <input type="hidden" name="authsource" value="generic-oidc">
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end" for="iam_authorize_url">{{ lang.admin.iam_authorize_url }}:</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label" for="iam_authorize_url">{{ lang.admin.iam_authorize_url }}:</label>
+            </div>
             <div class="col-12 col-md-9 col-lg-4">
               <input type="text" class="form-control" id="iam_authorize_url" name="authorize_url" value="{{ iam_settings.authorize_url }}" required>
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end" for="iam_token_url">{{ lang.admin.iam_token_url }}:</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label" for="iam_token_url">{{ lang.admin.iam_token_url }}:</label>
+            </div>
             <div class="col-12 col-md-9 col-lg-4">
               <input type="text" class="form-control" id="iam_token_url" name="token_url" value="{{ iam_settings.token_url }}" required>
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end" for="iam_userinfo_url">{{ lang.admin.iam_userinfo_url }}:</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label" for="iam_userinfo_url">{{ lang.admin.iam_userinfo_url }}:</label>
+            </div>
             <div class="col-12 col-md-9 col-lg-4">
               <input type="text" class="form-control" id="iam_userinfo_url" name="userinfo_url" value="{{ iam_settings.userinfo_url }}" required>
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end" for="iam_client_id">{{ lang.admin.iam_client_id }}:</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label" for="iam_client_id">{{ lang.admin.iam_client_id }}:</label>
+            </div>
             <div class="col-12 col-md-9 col-lg-4">
               <input type="text" class="form-control" id="iam_client_id" name="client_id" value="{{ iam_settings.client_id }}" required>
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end" for="iam_client_secret">{{ lang.admin.iam_client_secret }}:</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label" for="iam_client_secret">{{ lang.admin.iam_client_secret }}:</label>
+            </div>
             <div class="col-12 col-md-9 col-lg-4">
               <div class="reveal-password-input input-group">
                 <input type="password" class="password-field form-control" id="iam_client_secret" name="client_secret" value="{{ iam_settings.client_secret }}" required>
@@ -209,19 +253,25 @@
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end" for="iam_redirect_url">{{ lang.admin.iam_redirect_url }}:</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label" for="iam_redirect_url">{{ lang.admin.iam_redirect_url }}:</label>
+            </div>
             <div class="col-12 col-md-9 col-lg-4">
               <input type="text" class="form-control" id="iam_redirect_url" name="redirect_url" value="{{ iam_settings.redirect_url }}" required>
             </div>
           </div>
           <div class="row mb-4">
-            <label class="control-label col-md-3 text-sm-end" for="iam_client_scopes">{{ lang.admin.iam_client_scopes }}:</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label" for="iam_client_scopes">{{ lang.admin.iam_client_scopes }}:</label>
+            </div>
             <div class="col-12 col-md-9 col-lg-4">
               <input type="text" placeholder="openid profile email" class="form-control" id="iam_client_scopes" name="client_scopes" value="{{ iam_settings.client_scopes }}">
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end">{{ lang.admin.iam_mapping }}:</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label">{{ lang.admin.iam_mapping }}:</label>
+            </div>
             <div class="col-12 col-md-9 col-lg-4">
               <div class="row px-2 align-items-center">
                 <span class="col-5 p-0 pe-2">Attribute</span>
@@ -276,9 +326,19 @@
             </div>
             {% endif %}
           </div>
+          <div class="row mb-4">
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label">{{ lang.admin.ignore_ssl_error }}</label>
+            </div>
+            <div class="col-12 col-md-9">
+              <div class="form-check form-switch">
+                <input class="form-check-input" type="checkbox" role="switch" name="ignore_ssl_error" value="1" {% if iam_settings.ignore_ssl_error == 1 %}checked{% endif %}>
+              </div>
+            </div>
+          </div>
           <div class="row mt-4 mb-2">
             <div class="offset-md-3 col-12 col-md-9 d-flex flex-wrap">
-              <div class="btn-group mb-2">   
+              <div class="btn-group mb-2">
                 <button class="btn btn-sm d-block d-sm-inline btn-secondary iam_test_connection" data-id="iam_generic"><i class="bi bi-play"></i> {{ lang.admin.iam_test_connection }}</button>
                 <button class="btn btn-sm d-block d-sm-inline btn-success" data-item="identity-provider" data-action="edit_selected" data-id="iam_generic" data-api-url='edit/identity-provider' data-api-attr='{}'><i class="bi bi-check-lg"></i> {{ lang.admin.save }}</button>
               </div>
@@ -291,19 +351,26 @@
         <form class="form-horizontal" autocapitalize="none" data-id="iam_ldap" autocorrect="off" role="form" method="post">
           <input type="hidden" name="authsource" value="ldap">
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end" for="iam_ldap_host">{{ lang.admin.iam_host }}:</label>
-            <div class="col-12 col-md-9 col-lg-4">
-              <input type="text" class="form-control" id="iam_ldap_host" name="host" value="{{ iam_settings.host }}" required>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill m-2 ms-0" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom" title="{{ lang.admin.iam_host_info }}"></i>
+              <label class="control-label" for="iam_ldap_host">{{ lang.admin.iam_host }}:</label>
+            </div>
+            <div class="col-12 col-md-9 col-lg-4 d-flex">
+            <input type="text" class="form-control" id="iam_ldap_host" name="host" value="{{ iam_settings.host }}" required>
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end" for="iam_ldap_port">{{ lang.admin.iam_port }}:</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label" for="iam_ldap_port">{{ lang.admin.iam_port }}:</label>
+            </div>
             <div class="col-12 col-md-9 col-lg-4">
               <input type="number" class="form-control" id="iam_ldap_port" name="port" value="{{ iam_settings.port }}" required>
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end">{{ lang.admin.iam_use_ssl }}</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label">{{ lang.admin.iam_use_ssl }}</label>
+            </div>
             <div class="col-12 col-md-9">
               <div class="form-check form-switch">
                 <input class="form-check-input" type="checkbox" role="switch" name="use_ssl" value="1" {% if iam_settings.use_ssl == 1 %}checked{% endif %}>
@@ -311,7 +378,9 @@
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end">{{ lang.admin.iam_use_tls }}</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label">{{ lang.admin.iam_use_tls }}</label>
+            </div>
             <div class="col-12 col-md-9">
               <div class="form-check form-switch">
                 <input class="form-check-input" type="checkbox" role="switch" name="use_tls" value="1" {% if iam_settings.use_tls == 1 %}checked{% endif %}>
@@ -319,7 +388,9 @@
             </div>
           </div>
           <div class="row mb-4">
-            <label class="control-label col-md-3 text-sm-end">{{ lang.admin.ignore_ssl_error }}</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label">{{ lang.admin.ignore_ssl_error }}</label>
+            </div>
             <div class="col-12 col-md-9">
               <div class="form-check form-switch">
                 <input class="form-check-input" type="checkbox" role="switch" name="ignore_ssl_error" value="1" {% if iam_settings.ignore_ssl_error == 1 %}checked{% endif %}>
@@ -327,37 +398,49 @@
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end" for="iam_ldap_basedn">{{ lang.admin.iam_basedn }}:</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label" for="iam_ldap_basedn">{{ lang.admin.iam_basedn }}:</label>
+            </div>
             <div class="col-12 col-md-9 col-lg-4">
               <input type="text" class="form-control" id="iam_ldap_basedn" name="basedn" value="{{ iam_settings.basedn }}" required>
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end" for="iam_ldap_username_field">{{ lang.admin.iam_username_field }}:</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label" for="iam_ldap_username_field">{{ lang.admin.iam_username_field }}:</label>
+            </div>
             <div class="col-12 col-md-9 col-lg-4">
               <input type="text" class="form-control" placeholder="mail" id="iam_ldap_username_field" name="username_field" value="{{ iam_settings.username_field }}">
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end" for="iam_ldap_filter">{{ lang.admin.filter }}:</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label" for="iam_ldap_filter">{{ lang.admin.filter }}:</label>
+            </div>
             <div class="col-12 col-md-9 col-lg-4">
               <input type="text" class="form-control" placeholder="" id="iam_ldap_filter" name="filter" value="{{ iam_settings.filter }}">
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end" for="iam_ldap_attribute_field">{{ lang.admin.iam_attribute_field }}:</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label" for="iam_ldap_attribute_field">{{ lang.admin.iam_attribute_field }}:</label>
+            </div>
             <div class="col-12 col-md-9 col-lg-4">
               <input type="text" class="form-control" id="iam_ldap_attribute_field" name="attribute_field" value="{{ iam_settings.attribute_field }}" required>
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end" for="iam_ldap_binddn">{{ lang.admin.iam_binddn }}:</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label" for="iam_ldap_binddn">{{ lang.admin.iam_binddn }}:</label>
+            </div>
             <div class="col-12 col-md-9 col-lg-4">
               <input type="text" class="form-control" id="iam_ldap_binddn" name="binddn" value="{{ iam_settings.binddn }}" required>
             </div>
           </div>
           <div class="row mb-4">
-            <label class="control-label col-md-3 text-sm-end" for="iam_ldap_bindpass">{{ lang.admin.iam_bindpass }}:</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label" for="iam_ldap_bindpass">{{ lang.admin.iam_bindpass }}:</label>
+            </div>
             <div class="col-12 col-md-9 col-lg-4">
               <div class="reveal-password-input input-group">
                 <input type="password" class="password-field form-control" id="iam_ldap_bindpass" name="bindpass" value="{{ iam_settings.bindpass }}" required>
@@ -366,7 +449,9 @@
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end">{{ lang.admin.iam_mapping }}:</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label">{{ lang.admin.iam_mapping }}:</label>
+            </div>
             <div class="col-12 col-md-9 col-lg-4">
               <div class="row px-2 align-items-center">
                 <span class="col-5 p-0 pe-2">Attribute</span>
@@ -422,7 +507,9 @@
             {% endif %}
           </div>
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end">{{ lang.admin.iam_periodic_full_sync }}</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label">{{ lang.admin.iam_periodic_full_sync }}</label>
+            </div>
             <div class="col-12 col-md-9">
               <div class="form-check form-switch">
                 <input class="form-check-input" type="checkbox" role="switch" name="periodic_sync"  value="1" {% if iam_settings.periodic_sync == 1 %}checked{% endif %}>
@@ -430,7 +517,9 @@
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end">{{ lang.admin.iam_import_users }}</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label">{{ lang.admin.iam_import_users }}</label>
+            </div>
             <div class="col-12 col-md-9">
               <div class="form-check form-switch">
                 <input class="form-check-input" type="checkbox" role="switch" name="import_users"  value="1" {% if iam_settings.import_users == 1 %}checked{% endif %}>
@@ -438,14 +527,16 @@
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-md-3 text-sm-end">{{ lang.admin.iam_sync_interval }}</label>
+            <div class="col-md-3 d-flex align-items-center justify-content-md-end">
+              <label class="control-label">{{ lang.admin.iam_sync_interval }}</label>
+            </div>
             <div class="col-12 col-md-9 col-lg-4">
               <input class="form-control" type="number" min="1" name="sync_interval" style="width: 80px;"  {% if iam_settings.sync_interval %}value="{{ iam_settings.sync_interval }}"{% else %}value="15"{% endif %}>
             </div>
           </div>
           <div class="row mt-4 mb-2">
             <div class="offset-md-3 col-12 col-md-9 d-flex flex-wrap">
-              <div class="btn-group mb-2">   
+              <div class="btn-group mb-2">
                 <button class="btn btn-sm d-block d-sm-inline btn-secondary iam_test_connection iam_test_connection" data-id="iam_ldap"><i class="bi bi-play"></i> {{ lang.admin.iam_test_connection }}</button>
                 <button class="btn btn-sm d-block d-sm-inline btn-success" data-item="identity-provider" data-action="edit_selected" data-id="iam_ldap" data-api-url='edit/identity-provider' data-api-attr='{}'><i class="bi bi-check-lg"></i> {{ lang.admin.save }}</button>
               </div>

+ 7 - 7
data/web/templates/user/tab-user-auth.twig

@@ -43,7 +43,7 @@
               {{ mailboxdata.percent_in_use }}%
             </div>
           </div>
-          
+
           <div class="row">
             <div class="col-12 col-md-3 d-flex">
               <span class="mt-2 w-100 text-md-end">{{ lang.user.protocols }}:</span>
@@ -64,12 +64,12 @@
             </div>
             <div class="col-12 col-md-8">
               <div class="d-flex">
-                <i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill me-2" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom" title="{{ lang.user.apple_connection_profile_mailonly }}"></i> 
+                <i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill me-2" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom" title="{{ lang.user.apple_connection_profile_mailonly }}"></i>
                 <a href="/mobileconfig.php?only_email">{{ lang.user.email }} <small>[IMAP, SMTP]</small></a>
-              </div> 
+              </div>
               {% if not skip_sogo %}
               <div class="d-flex">
-                <i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill me-2" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom" title="{{ lang.user.apple_connection_profile_complete }}"></i>  
+                <i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill me-2" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom" title="{{ lang.user.apple_connection_profile_complete }}"></i>
                 <a href="/mobileconfig.php">{{ lang.user.email_and_dav }} <small>[IMAP, SMTP, Cal/CardDAV]</small></a>
               </div>
               {% endif %}
@@ -81,12 +81,12 @@
             </div>
             <div class="col-12 col-md-9">
               <div class="d-flex">
-                <i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill me-2" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom" title="{{ lang.user.apple_connection_profile_mailonly }} {{ lang.user.apple_connection_profile_with_app_password }}"></i> 
+                <i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill me-2" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom" title="{{ lang.user.apple_connection_profile_mailonly }} {{ lang.user.apple_connection_profile_with_app_password }}"></i>
                 <a href="/mobileconfig.php?only_email&amp;app_password">{{ lang.user.email }} <small>[IMAP, SMTP]</small></a>
               </div>
               {% if not skip_sogo %}
               <div class="d-flex">
-                <i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill me-2" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom" title="{{ lang.user.apple_connection_profile_complete }} {{ lang.user.apple_connection_profile_with_app_password }}"></i> 
+                <i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill me-2" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom" title="{{ lang.user.apple_connection_profile_complete }} {{ lang.user.apple_connection_profile_with_app_password }}"></i>
                 <a href="/mobileconfig.php?app_password">{{ lang.user.email_and_dav }} <small>[IMAP, SMTP, Cal/CardDAV]</small></a>
               </div>
               {% endif %}
@@ -99,10 +99,10 @@
             </div>
           </div>
 
+          {% if mailboxdata.authsource == "mailcow" %}
           <legend class="mt-4">{{ lang.user.authentication }}</legend>
           <hr>
           {# Password Change #}
-          {% if mailboxdata.authsource == "mailcow" %}
           <div class="row">
             <div class="col-12 col-md-3 d-flex"></div>
             <div class="col-12 col-md-9 d-flex flex-wrap">

+ 1 - 0
docker-compose.yml

@@ -127,6 +127,7 @@ services:
         - ./data/web/inc/sessions.inc.php:/mailcowauth/sessions.inc.php:z
         - ./data/web/inc/functions.mailbox.inc.php:/mailcowauth/functions.mailbox.inc.php:z
         - ./data/web/inc/functions.ratelimit.inc.php:/mailcowauth/functions.ratelimit.inc.php:z
+        - ./data/web/inc/functions.acl.inc.php:/mailcowauth/functions.acl.inc.php:z
         - rspamd-vol-1:/var/lib/rspamd
         - mysql-socket-vol-1:/var/run/mysqld/
         - ./data/conf/sogo/:/etc/sogo/:z