Преглед на файлове

[Web] add manage identity provider

FreddleSpl0it преди 2 години
родител
ревизия
6e9980bf0f

+ 3 - 0
data/web/admin.php

@@ -88,6 +88,8 @@ $cors_settings['allowed_methods'] = explode(", ", $cors_settings['allowed_method
 $f2b_data = fail2ban('get');
 // identity provider
 $identity_provider_settings = identity_provider('get');
+// mbox templates
+$mbox_templates = mailbox('get', 'mailbox_templates');
 
 $template = 'admin.twig';
 $template_data = [
@@ -120,6 +122,7 @@ $template_data = [
   'cors_settings' => $cors_settings,
   'is_https' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
   'identity_provider_settings' => $identity_provider_settings,
+  'mbox_templates' => $mbox_templates,
   'lang_admin' => json_encode($lang['admin']),
   'lang_datatables' => json_encode($lang['datatables'])
 ];

+ 2 - 0
data/web/inc/footer.inc.php

@@ -65,6 +65,8 @@ $globalVariables = [
   'lang_acl' => json_encode($lang['acl']),
   'lang_tfa' => json_encode($lang['tfa']),
   'lang_fido2' => json_encode($lang['fido2']),
+  'lang_success' => json_encode($lang['success']),
+  'lang_danger' => json_encode($lang['danger']),
   'docker_timeout' => $DOCKER_TIMEOUT,
   'session_lifetime' => (int)$SESSION_LIFETIME,
   'csrf_token' => $_SESSION['CSRF']['TOKEN'],

+ 83 - 26
data/web/inc/functions.inc.php

@@ -2071,7 +2071,6 @@ function identity_provider($_action, $_data = null) {
 function identity_provider($_action, $_data = null, $hide_secret = false) {
   global $pdo;
 
-
   switch ($_action) {
     case 'get':
       $settings = array();
@@ -2079,12 +2078,19 @@ function identity_provider($_action, $_data = null, $hide_secret = false) {
       $stmt->execute();
       $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
       foreach($rows as $row){
-        $settings[$row["key"]] = $row["value"];
+        if ($row["key"] == 'roles'){
+          $settings['roles'] = json_decode($row["value"]);
+        } else if ($row["key"] == 'templates'){
+          $settings['templates'] = json_decode($row["value"]);
+        } else {
+          $settings[$row["key"]] = $row["value"];
+        }
       }
       if ($hide_secret){
-        $settings['client_secret'] = '***********************';
+        $settings['client_secret'] = '';
       }
       return $settings;
+    break;
     case 'edit':
       if ($_SESSION['mailcow_cc_role'] != "admin") {
         $_SESSION['return'][] = array(
@@ -2094,36 +2100,24 @@ function identity_provider($_action, $_data = null, $hide_secret = false) {
         );
         return false;
       }
-
+      $data_log = $_data;
+      $data_log['client_secret'] = '*';
+      $stmt = $pdo->prepare("INSERT INTO identity_provider (`key`, `value`) VALUES (:key, :value) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);");
+      
+      // add connection settings      
       $required_settings = array('server_url', 'authsource', 'realm', 'client_id', 'client_secret', 'redirect_url', 'version');
       foreach($required_settings as $setting){
         if (!$_data[$setting]){
+          $_SESSION['return'][] =  array(
+            'type' => 'danger',
+            'log' => array(__FUNCTION__, $_action, $data_log),
+            'msg' => 'required_data_missing'
+          );
           return false;
         }
       }
-      try {
-        $_SESSION['return'][] =  array(
-          'type' => 'success',
-          'log' => array(__FUNCTION__, $_action, $_data),
-          'msg' => '2'
-        );
-        $stmt = $pdo->prepare("INSERT INTO identity_provider (`key`, `value`) VALUES (:key, :value) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);");
-        $_SESSION['return'][] =  array(
-          'type' => 'success',
-          'log' => array(__FUNCTION__, $_action, $_data),
-          'msg' => '3'
-        );
-      } catch (Exception $e){
-        $_SESSION['return'][] =  array(
-          'type' => 'success',
-          'log' => array(__FUNCTION__, $_action, $_data, $e->getMessage()),
-          'msg' => 'post'
-        );
-        return;
-      }
-
       foreach($_data as $key => $value){
-        if (!in_array($key, $required_settings)){
+        if (!in_array($key, $required_settings) || $key == 'roles' || $key == 'templates'){
           continue;
         }
 
@@ -2131,8 +2125,71 @@ function identity_provider($_action, $_data = null, $hide_secret = false) {
         $stmt->bindParam(':value', $value);
         $stmt->execute();
       }
+
+      // add role mappings
+      if ($_data['roles'] && $_data['templates']){
+        if (!is_array($_data['roles'])){
+          $_data['roles'] = array($_data['roles']);
+        }
+        if (!is_array($_data['templates'])){
+          $_data['templates'] = array($_data['templates']);
+        }
+        $roles = array();
+        $templates = array();
+        foreach($_data['roles'] as $role){
+          if ($role){
+            array_push($roles, $role);
+          }
+        }
+        foreach($_data['templates'] as $template){
+          if ($template){
+            array_push($templates, $template);
+          }
+        }
+        if (count($roles) == count($templates)){
+          $roles = json_encode($roles);
+          $templates = json_encode($templates);
+
+          $stmt = $pdo->prepare("INSERT INTO identity_provider (`key`, `value`) VALUES ('roles', :value) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);");
+          $stmt->bindParam(':value', $roles);
+          $stmt->execute();
+          $stmt = $pdo->prepare("INSERT INTO identity_provider (`key`, `value`) VALUES ('templates', :value) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);");
+          $stmt->bindParam(':value', $templates);
+          $stmt->execute();
+        }
+      }
+
+      $_SESSION['return'][] =  array(
+        'type' => 'success',
+        'log' => array(__FUNCTION__, $_action, $data_log),
+        'msg' => array('object_modified', '')
+      );
       return true;
     break;
+    case 'test':  
+      $identity_provider_settings = identity_provider('get');
+      $url = "{$identity_provider_settings['server_url']}/realms/{$identity_provider_settings['realm']}/protocol/openid-connect/token";
+      $req = http_build_query(array(
+        'grant_type'    => 'password',
+        'client_id'     => $identity_provider_settings['client_id'],
+        'client_secret' => $identity_provider_settings['client_secret'],
+        'username'      => "test",
+        'password'      => "test",
+      ));
+      $curl = curl_init();
+      curl_setopt($curl, CURLOPT_URL, $url);
+      curl_setopt($curl, CURLOPT_POST, 1);
+      curl_setopt($curl, CURLOPT_POSTFIELDS, $req);
+      curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: application/x-www-form-urlencoded'));
+      curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
+      $res = json_decode(curl_exec($curl), true);
+      curl_close ($curl);
+
+      if ($res["error"] && $res["error"] === 'invalid_grant'){
+        return true;
+      }
+      return false;
+    break;
   }
 }
 

+ 1 - 1
data/web/inc/init_db.inc.php

@@ -571,7 +571,7 @@ function init_db_schema() {
       "identity_provider" => array(
         "cols" => array(
           "key" => "VARCHAR(255) NOT NULL",
-          "value" => "VARCHAR(255) NOT NULL",
+          "value" => "TEXT NOT NULL",
           "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
           "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP"
         ),

+ 11 - 0
data/web/js/build/013-mailcow.js

@@ -352,6 +352,17 @@ $(document).ready(function() {
       localStorage.setItem('theme', 'dark');
     }
   }
+
+  // Reveal Password Input
+  $(".reveal-password-input").on('click', '.toggle-password', function() {
+    $(this).parent().find('.toggle-password').children().toggleClass("bi-eye bi-eye-slash");
+    var input = $(this).parent().find('.password-field')
+    if (input.attr("type") === "password") {
+      input.attr("type", "text");
+    } else {
+      input.attr("type", "password");
+    }
+  });
 });
 
 

+ 34 - 0
data/web/js/site/admin.js

@@ -749,4 +749,38 @@ jQuery(function($){
   $('#add_f2b_regex_row').click(function() {
     add_table_row($('#f2b_regex_table'), "f2b_regex");
   });
+  // IAM test connection
+  $('#iam_test_connection').click(async function(e){
+    e.preventDefault();
+    var res = await fetch("/api/v1/get/status/identity-provider", { method:'GET', cache:'no-cache' });
+    res = await res.json();
+    console.log(res);
+    if (res.type === 'success'){
+      return mailcow_alert_box(lang_success.iam_test_connection, 'success');
+    }
+    return mailcow_alert_box(lang_danger.iam_test_connection, 'danger');
+  });
+  $('#iam_rolemap_add').click(async function(e){
+    e.preventDefault();
+
+    var parent = $(this).parent().parent();
+    $(parent).children().last().clone().appendTo(parent);
+    var newChild = $(parent).children().last();
+    $(newChild).find('input').val('');
+    $(newChild).find('.dropdown-toggle').remove();
+    $(newChild).find('.dropdown-menu').remove();
+    $(newChild).find('.bs-title-option').remove();
+    $(newChild).find('select').selectpicker('destroy');
+    $(newChild).find('select').selectpicker();
+
+    $('.iam_rolemap_del').off('click');
+    $('.iam_rolemap_del').click(async function(e){
+      e.preventDefault();
+      $(this).parent().remove();
+    });
+  });
+  $('.iam_rolemap_del').click(async function(e){
+    e.preventDefault();
+    $(this).parent().remove();
+  });
 });

+ 39 - 1
data/web/js/site/mailbox.js

@@ -162,6 +162,17 @@ $(document).ready(function() {
       }
     });
   });
+  // @selecting identity provider mbox_add_modal
+  $('#mbox_add_iam').on('change', function(){
+    // toggle password fields
+    if (this.value === 'mailcow'){
+      $('#mbox_add_pwds').removeClass('d-none');
+      $('#mbox_add_pwds').find('.form-control').prop('required', true);
+    } else {
+      $('#mbox_add_pwds').addClass('d-none');
+      $('#mbox_add_pwds').find('.form-control').prop('required', false);
+    }
+  });
   // Sieve data modal
   $('#sieveDataModal').on('show.bs.modal', function(e) {
     var sieveScript = $(e.relatedTarget).data('sieve-script');
@@ -269,6 +280,15 @@ $(document).ready(function() {
   }
   function setMailboxTemplateData(template){
     $("#addInputQuota").val(template.quota / 1048576);
+    $('#mbox_add_iam').selectpicker('val', template.authsource);
+    // toggle password fields
+    if (template.authsource === 'mailcow'){
+      $('#mbox_add_pwds').removeClass('d-none');
+      $('#mbox_add_pwds').find('.form-control').prop('required', true);
+    } else {
+      $('#mbox_add_pwds').addClass('d-none');
+      $('#mbox_add_pwds').find('.form-control').prop('required', false);
+    }
 
     if (template.quarantine_notification === "never"){
       $('#quarantine_notification_never').prop('checked', true);
@@ -1035,7 +1055,16 @@ jQuery(function($){
           title: lang.domain,
           data: 'domain',
           defaultContent: '',
-          className: 'none'
+          className: 'none',
+        },
+        {
+          title: lang.iam,
+          data: 'authsource',
+          defaultContent: '',
+          className: 'none',
+          render: function (data, type) {
+            return '<span class="badge bg-primary">' + data + '<i class="ms-2 bi bi-person-circle"></i></i></span>';
+          }
         },
         {
           title: lang.tls_enforce_in,
@@ -1263,6 +1292,15 @@ jQuery(function($){
           data: 'attributes.quota',
           defaultContent: '',
         },
+        {
+          title: lang.iam,
+          data: 'attributes.authsource',
+          defaultContent: '',
+          render: function (data, type) {
+            data = data ? '<span class="badge bg-primary">' + data + '<i class="ms-2 bi bi-person-circle"></i></i></span>' : '<i class="bi bi-x-lg"></i>';
+            return data;
+          }
+        },
         {
           title: lang.tls_enforce_in,
           data: 'attributes.tls_enforce_in',

+ 13 - 0
data/web/json_api.php

@@ -1702,6 +1702,19 @@ if (isset($_GET['query'])) {
                     'version' => $GLOBALS['MAILCOW_GIT_VERSION']
                   ));
                 break;
+                case "identity-provider":
+                  if (identity_provider('test')){
+                    echo json_encode(array(
+                      'type' => 'success',
+                      'msg' => 'connection successfull'
+                    ));
+                  } else {
+                    echo json_encode(array(
+                      'type' => 'error',
+                      'msg' => 'connection failed'
+                    ));
+                  }
+                break;
               }
             }
           break;

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

@@ -211,6 +211,17 @@
         "help_text": "Override help text below login mask (HTML allowed)",
         "host": "Host",
         "html": "HTML",
+        "iam": "Identity Provider",
+        "iam_client_id": "Client Id",
+        "iam_client_secret": "Client Secret",
+        "iam_description": "Here, you can configure the integration with an external Keycloak service. The Keycloak user's mailboxes will be automatically created upon their first login, provided that a role mapping has been set.",
+        "iam_realm": "Realm",
+        "iam_redirect_url": "Redirect Url",
+        "iam_rolemapping": "Role Mapping",
+        "iam_server_url": "Server Url",
+        "iam_sso": "SSO",
+        "iam_test_connection": "Test Connection",
+        "iam_version": "Version",
         "import": "Import",
         "import_private_key": "Import private key",
         "in_use_by": "In use by",
@@ -395,6 +406,8 @@
         "goto_empty": "An alias address must contain at least one valid goto address",
         "goto_invalid": "Goto address %s is invalid",
         "ham_learn_error": "Ham learn error: %s",
+        "iam_invalid_sso": "SSO login failed",
+        "iam_test_connection": "Connection failed",
         "imagick_exception": "Error: Imagick exception while reading image",
         "img_dimensions_exceeded": "Image exceeds the maximum image size",
         "img_invalid": "Cannot validate image file",
@@ -449,6 +462,7 @@
         "redis_error": "Redis error: %s",
         "relayhost_invalid": "Map entry %s is invalid",
         "release_send_failed": "Message could not be released: %s",
+        "required_data_missing": "Required data is missing",
         "reset_f2b_regex": "Regex filter could not be reset in time, please try again or wait a few more seconds and reload the website.",
         "resource_invalid": "Resource name %s is invalid",
         "rl_timeframe": "Rate limit time frame is incorrect",
@@ -825,6 +839,7 @@
         "goto_ham": "Learn as <b>ham</b>",
         "goto_spam": "Learn as <b>spam</b>",
         "hourly": "Hourly",
+        "iam": "Identity Provider",
         "in_use": "In use (%)",
         "inactive": "Inactive",
         "insert_preset": "Insert example preset \"%s\"",
@@ -1060,6 +1075,7 @@
         "forwarding_host_removed": "Forwarding host %s has been removed",
         "global_filter_written": "Filter was successfully written to file",
         "hash_deleted": "Hash deleted",
+        "iam_test_connection": "Connection successfull",
         "ip_check_opt_in_modified": "IP check was saved successfully",
         "item_deleted": "Item %s successfully deleted",
         "item_released": "Item %s released",

+ 2 - 2
data/web/templates/admin.twig

@@ -7,7 +7,7 @@
       <a class="nav-link dropdown-toggle active" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false">{{ lang.admin.access }}</a>
       <ul class="dropdown-menu">
         <li><button class="dropdown-item active" data-bs-target="#tab-config-admins" aria-selected="false" aria-controls="tab-config-admins" role="tab" data-bs-toggle="tab">{{ lang.admin.admins }}</button></li>
-        <li><button class="dropdown-item" data-bs-target="#tab-config-identity-providers" aria-selected="false" aria-controls="tab-config-identity-providers" role="tab" data-bs-toggle="tab">Identity Providers</button></li>
+        <li><button class="dropdown-item" data-bs-target="#tab-config-identity-provider" aria-selected="false" aria-controls="tab-config-identity-provider" role="tab" data-bs-toggle="tab">Identity Provider</button></li>
         <!-- <li><button class="dropdown-item" data-bs-target="#tab-config-ldap-admins" aria-controls="tab-config-ldap-admins" role="tab" data-bs-toggle="tab">{{ lang.admin.admins_ldap }}</button></li> -->
         <li><button class="dropdown-item" data-bs-target="#tab-config-oauth2" aria-selected="false" aria-controls="tab-config-oauth2" role="tab" data-bs-toggle="tab">{{ lang.admin.oauth2_apps }}</button></li>
         <li><button class="dropdown-item" data-bs-target="#tab-config-rspamd" aria-selected="false" aria-controls="tab-config-rspamd" role="tab" data-bs-toggle="tab">Rspamd UI</button></li>
@@ -41,7 +41,7 @@
     <div class="col-md-12">
       <div class="tab-content" style="padding-top:20px">
         {% include 'admin/tab-config-admins.twig' %}
-        {% include 'admin/tab-config-identity-providers.twig' %}
+        {% include 'admin/tab-config-identity-provider.twig' %}
         {# {% include 'admin/tab-ldap.twig' %} #}
         {% include 'admin/tab-config-oauth2.twig' %}
         {% include 'admin/tab-config-rspamd.twig' %}

+ 95 - 0
data/web/templates/admin/tab-config-identity-provider.twig

@@ -0,0 +1,95 @@
+<div role="tabpanel" class="tab-pane fade" id="tab-config-identity-provider" role="tabpanel" aria-labelledby="tab-config-identity-provider">
+  <div class="card mb-4">
+    <div class="card-header d-flex fs-5">
+      <button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-identity-provider" data-bs-toggle="collapse" aria-controls="collapse-tab-config-identity-provider">
+        {{ lang.admin.iam }}
+      </button>
+      <span class="d-none d-md-block">{{ lang.admin.iam }}</span>
+    </div>
+    <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 }}</p>
+      <form class="form-horizontal" autocapitalize="none" data-id="iam_sso" autocorrect="off" role="form" method="post">
+        <input type="hidden" name="authsource" value="keycloak">
+        <div class="row mb-2">
+          <label class="control-label col-sm-3 text-sm-end" for="iam_url">{{ lang.admin.iam_server_url }}:</label>
+          <div class="col-sm-4">
+            <input type="text" class="form-control" id="iam_server_url" name="server_url" value="{{ identity_provider_settings.server_url }}" required>
+          </div>
+        </div>
+        <div class="row mb-2">
+          <label class="control-label col-sm-3 text-sm-end" for="iam_realm">{{ lang.admin.iam_realm }}:</label>
+          <div class="col-sm-4">
+            <input type="text" class="form-control" id="iam_realm" name="realm" value="{{ identity_provider_settings.realm }}" required>
+          </div>
+        </div>
+        <div class="row mb-2">
+          <label class="control-label col-sm-3 text-sm-end" for="iam_client_id">{{ lang.admin.iam_client_id }}:</label>
+          <div class="col-sm-4">
+            <input type="text" class="form-control" id="iam_client_id" name="client_id" value="{{ identity_provider_settings.client_id }}" required>
+          </div>
+        </div>
+        <div class="row mb-2">
+          <label class="control-label col-sm-3 text-sm-end" for="iam_client_secret">{{ lang.admin.iam_client_secret }}:</label>
+          <div class="col-sm-4">
+            <div class="reveal-password-input input-group">
+              <input type="password" class="password-field form-control" id="iam_client_secret" name="client_secret" value="{{ identity_provider_settings.client_secret }}" required>
+              <button class="toggle-password btn btn-secondary" type="button"><i class="bi bi-eye"></i></button>
+            </div>
+          </div>
+        </div>
+        <div class="row mb-2">
+          <label class="control-label col-sm-3 text-sm-end" for="iam_redirect_url">{{ lang.admin.iam_redirect_url }}:</label>
+          <div class="col-sm-4">
+            <input type="text" class="form-control" id="iam_redirect_url" name="redirect_url" value="{{ identity_provider_settings.redirect_url }}" required>
+          </div>
+        </div>
+        <div class="row mb-4">
+          <label class="control-label col-sm-3 text-sm-end" for="iam_version">{{ lang.admin.iam_version }}:</label>
+          <div class="col-sm-4">
+            <input type="text" class="form-control" id="iam_version" name="version" value="{{ identity_provider_settings.version }}" required>
+          </div>
+        </div>
+        <div class="row mb-2">
+          <label class="control-label col-sm-3 text-sm-end" for="iam_version">{{ lang.admin.iam_rolemapping }}:</label>
+          <div class="col-4 d-flex mb-2">
+            <span class="w-100 me-2">Role</span>
+            <span class="w-100 ms-2">Template</span>
+            <button id="iam_rolemap_add" class="btn btn-sm d-block d-sm-inline btn-secondary ms-2"><i class="bi bi-plus-lg"></i></button>
+          </div>
+          {% for key, role in identity_provider_settings.roles %}
+          <div class="offset-sm-3 col-4 d-flex mb-2">
+            <input type="text" class="form-control me-2" name="roles" value="{{ identity_provider_settings.roles[key] }}">
+            <select data-live-search="true" name="templates" class="form-control" title="{{ lang.mailbox.template }}">
+            {% for mbox_template in mbox_templates %}
+              <option{% if mbox_template.template == identity_provider_settings.templates[key] %} selected{% endif %}>
+                {{ mbox_template.template }}
+              </option>
+            {% endfor %}
+            </select>
+            <button class="iam_rolemap_del btn btn-sm d-block d-sm-inline btn-secondary ms-2"><i class="bi bi-x-lg"></i></button>
+          </div>
+          {% endfor %}
+          <div class="offset-sm-3 col-4 d-flex mb-2">
+            <input type="text" class="form-control me-2" name="roles" value="">
+            <select data-live-search="true" name="templates" class="form-control" title="{{ lang.mailbox.template }}">
+            {% for mbox_template in mbox_templates %}
+              <option>
+                {{ mbox_template.template }}
+              </option>
+            {% endfor %}
+            </select>
+            <button class="iam_rolemap_del btn btn-sm d-block d-sm-inline btn-secondary ms-2"><i class="bi bi-x-lg"></i></button>
+          </div>
+        </div>
+        <div class="row mt-4 mb-2">
+          <div class="offset-sm-3 col-sm-9">
+            <div class="btn-group">   
+              <button id="iam_test_connection" class="btn btn-sm d-block d-sm-inline btn-secondary"><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="iam_sso" data-action="edit_selected" data-id="iam_sso" data-api-url='edit/identity_provider' data-api-attr='{}' href="#"><i class="bi bi-check-lg"></i> {{ lang.admin.save }}</button>
+            </div>
+          </div>
+        </div>
+      </form>
+    </div>
+  </div>
+</div>

+ 0 - 58
data/web/templates/admin/tab-config-identity-providers.twig

@@ -1,58 +0,0 @@
-<div role="tabpanel" class="tab-pane fade" id="tab-config-identity-providers" role="tabpanel" aria-labelledby="tab-config-identity-providers">
-  <div class="card mb-4">
-    <div class="card-header d-flex fs-5">
-      <button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-identity-providers" data-bs-toggle="collapse" aria-controls="collapse-tab-config-identity-providers">
-        {{ lang.admin.oauth2_apps }}
-      </button>
-      <span class="d-none d-md-block">{{ lang.admin.oauth2_apps }}</span>
-    </div>
-    <div id="collapse-tab-config-identity-providers" class="card-body collapse" data-bs-parent="#admin-content">
-      <form class="form-horizontal" autocapitalize="none" data-id="keycloak_sso" autocorrect="off" role="form" method="post">
-        <input type="hidden" name="authsource" value="keycloak">
-        <div class="row mb-2">
-          <label class="control-label col-sm-3 text-sm-end" for="keycloak_url">Server URL:</label>
-          <div class="col-sm-4">
-            <input type="text" class="form-control" id="keycloak_url" name="server_url" value="{{ identity_provider_settings.server_url }}" required>
-          </div>
-        </div>
-        <div class="row mb-2">
-          <label class="control-label col-sm-3 text-sm-end" for="keycloak_realm">Realm:</label>
-          <div class="col-sm-4">
-            <input type="text" class="form-control" id="keycloak_realm" name="realm" value="{{ identity_provider_settings.realm }}" required>
-          </div>
-        </div>
-        <div class="row mb-2">
-          <label class="control-label col-sm-3 text-sm-end" for="keycloak_client_id">Client Id:</label>
-          <div class="col-sm-4">
-            <input type="text" class="form-control" id="keycloak_client_id" name="client_id" value="{{ identity_provider_settings.client_id }}" required>
-          </div>
-        </div>
-        <div class="row mb-2">
-          <label class="control-label col-sm-3 text-sm-end" for="keycloak_client_secret">Client Secret:</label>
-          <div class="col-sm-4">
-            <input type="text" class="form-control" id="keycloak_client_secret" name="client_secret" value="{{ identity_provider_settings.client_secret }}" required>
-          </div>
-        </div>
-        <div class="row mb-2">
-          <label class="control-label col-sm-3 text-sm-end" for="keycloak_redirect_url">Redirect Url:</label>
-          <div class="col-sm-4">
-            <input type="text" class="form-control" id="keycloak_redirect_url" name="redirect_url" value="{{ identity_provider_settings.redirect_url }}" required>
-          </div>
-        </div>
-        <div class="row mb-2">
-          <label class="control-label col-sm-3 text-sm-end" for="keycloak_version">Keycloak Version:</label>
-          <div class="col-sm-4">
-            <input type="text" class="form-control" id="keycloak_version" name="version" value="{{ identity_provider_settings.version }}" required>
-          </div>
-        </div>
-        <div class="row mt-4 mb-2">
-          <div class="offset-sm-3 col-sm-9">
-            <div class="btn-group">
-              <button class="btn btn-sm d-block d-sm-inline btn-success" data-item="keycloak_sso" data-action="edit_selected" data-id="keycloak_sso" data-api-url='edit/identity_provider' data-api-attr='{}' href="#"><i class="bi bi-check-lg"></i> {{ lang.admin.save }}</button>
-            </div>
-          </div>
-        </div>
-      </form>
-    </div>
-  </div>
-</div>

+ 2 - 0
data/web/templates/base.twig

@@ -153,6 +153,8 @@
   var lang_acl = {{ lang_acl|raw }};
   var lang_tfa = {{ lang_tfa|raw }};
   var lang_fido2 = {{ lang_fido2|raw }};
+  var lang_success = {{ lang_success|raw }};
+  var lang_danger = {{ lang_danger|raw }};
   var docker_timeout = {{ docker_timeout|raw }} * 1000;
   var mailcow_cc_role = '{{ mailcow_cc_role }}';
   var last_login = '{{ last_login }}';

+ 9 - 0
data/web/templates/edit/mailbox-templates.twig

@@ -19,6 +19,15 @@
         </div>
       </div>
     </div>
+    <div class="row mb-2">
+      <label class="control-label col-sm-2" for="authsource">{{ lang.admin.iam }}</label>
+      <div class="col-sm-10">
+        <select class="full-width-select" data-live-search="true" id="mbox_template_iam" name="authsource" required>
+          <option {% if template.attributes.authsource == 'mailcow' %}selected{% endif %}>mailcow</option>
+          <option {% if template.attributes.authsource == 'keycloak' %}selected{% endif %}>keycloak</option>
+        </select>
+      </div>
+    </div>
     <div class="row mb-2">
       <label class="control-label col-sm-2">{{ lang.add.tags }}</label>
       <div class="col-sm-10">

+ 2 - 2
data/web/templates/edit/mailbox.twig

@@ -26,9 +26,9 @@
                   <input type="hidden" value="0" name="sogo_access">
                   <input type="hidden" value="0" name="protocol_access">      
                   <div class="row mb-2">
-                    <label class="control-label col-sm-2">{{ lang.edit.full_name }}</label>
+                    <label class="control-label col-sm-2">{{ lang.admin.iam }}</label>
                     <div class="col-sm-10">
-                      <span>{{ result.authsource }}</span>
+                      <h4><span class="badge bg-primary">{{ result.authsource }}<i class="ms-2 bi bi-person-circle"></i></i></span></h4>
                     </div>
                   </div>
                   <div class="row mb-2">

+ 16 - 3
data/web/templates/index.twig

@@ -26,6 +26,13 @@
         <div class="my-4 alert alert-info ">{{ lang.login.mobileconfig_info }}</div>
         {% endif %}
         <form method="post" autofill="off">
+          {% if invalid_keycloak_sso %}
+          <div class="d-flex mt-3 w-100">
+            <div class="alert alert-danger w-100" role="alert">
+              {{ lang.danger.iam_invalid_sso}}
+            </div>
+          </div>
+          {% endif %}
           <div class="d-flex mt-3">
             <label class="visually-hidden" for="login_user">{{ lang.login.username }}</label>
             <div class="input-group">
@@ -40,10 +47,16 @@
               <input name="pass_user" type="password" id="pass_user" class="form-control" placeholder="{{ lang.login.password }}" required="" autocomplete="current-password">
             </div>
           </div>
-          <div class="d-flex justify-content-between mt-4" style="position: relative">
-            <div class="d-grid gap-2 d-sm-block">
+          <div class="d-flex mt-4" style="position: relative">
+            <div class="btn-group">
               <button type="submit" class="btn btn-xs-lg btn-success" value="Login">{{ lang.login.login }}</button>
-              <button type="button" class="btn btn-xs-lg btn-success" id="fido2-login"><i class="bi bi-shield-fill-check"></i> {{ lang.login.fido2_webauthn }}</button>
+              <button type="button" class="btn btn-xs-lg btn-success dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
+              <ul class="dropdown-menu">
+                <li><a class="dropdown-item" href="#" id="fido2-login"><i class="bi bi-shield-fill-check"></i> {{ lang.login.fido2_webauthn }}</a></li>
+                {% if has_keycloak_sso %}
+                <li><a class="dropdown-item" href="/?keycloak_sso=1"><i class="bi bi-cloud-arrow-up-fill"></i> {{ lang.admin.iam_sso }}</a></li>
+                {% endif %}
+              </ul>
             </div>
             {% if not oauth2_request %}
             <div class="d-grid d-sm-block">

+ 34 - 23
data/web/templates/modals/mailbox.twig

@@ -12,6 +12,12 @@
           <input type="hidden" value="0" name="sogo_access">
           <input type="hidden" value="0" name="protocol_access">
 
+          <div class="row mb-2">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="name">{{ lang.add.full_name }}</label>
+            <div class="col-sm-10">
+              <input type="text" class="form-control" name="name">
+            </div>
+          </div>
           <div class="row mb-2">
             <label class="control-label col-sm-2 text-sm-end text-sm-end" for="local_part">{{ lang.add.mailbox_username }}</label>
             <div class="col-sm-10">
@@ -28,38 +34,34 @@
               </select>
             </div>
           </div>
-          <div class="row mb-2">
-            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="authsource">{{ lang.add.domain }}</label>
-            <div class="col-sm-10">
-              <select class="full-width-select" data-live-search="true" id="addAuthsource" name="authsource" required>
-                <option selected>mailcow</option>
-                <option>keycloak</option>
-              </select>
-            </div>
-          </div>
           <div class="row mb-4">
-            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="name">{{ lang.add.full_name }}</label>
+            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="description">{{ lang.mailbox.template }}</label>
             <div class="col-sm-10">
-              <input type="text" class="form-control" name="name">
+              <select data-live-search="true" id="mailbox_templates" class="form-control" title="{{ lang.mailbox.template }}">
+              </select>
             </div>
           </div>
           <div class="row mb-2">
-            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="password">{{ lang.add.password }} (<a href="#" class="generate_password">{{ lang.add.generate }}</a>)</label>
+            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="authsource">{{ lang.admin.iam }}</label>
             <div class="col-sm-10">
-              <input type="password" data-pwgen-field="true" data-hibp="true" class="form-control" name="password" placeholder="" autocomplete="new-password" required>
+              <select class="full-width-select" data-live-search="true" id="mbox_add_iam" name="authsource" required>
+                <option selected>mailcow</option>
+                <option>keycloak</option>
+              </select>
             </div>
           </div>
-          <div class="row mb-4">
-            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="password2">{{ lang.add.password_repeat }}</label>
-            <div class="col-sm-10">
-              <input type="password" data-pwgen-field="true" class="form-control" name="password2" placeholder="" autocomplete="new-password" required>
+          <div id="mbox_add_pwds">
+            <div class="row mb-2">
+              <label class="control-label col-sm-2 text-sm-end text-sm-end" for="password">{{ lang.add.password }} (<a href="#" class="generate_password">{{ lang.add.generate }}</a>)</label>
+              <div class="col-sm-10">
+                <input type="password" data-pwgen-field="true" data-hibp="true" class="form-control" name="password" placeholder="" autocomplete="new-password" required>
+              </div>
             </div>
-          </div>
-          <div class="row mb-2">
-            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="description">{{ lang.mailbox.template }}</label>
-            <div class="col-sm-10">
-              <select data-live-search="true" id="mailbox_templates" class="form-control" title="{{ lang.mailbox.template }}">
-              </select>
+            <div class="row mb-4">
+              <label class="control-label col-sm-2 text-sm-end text-sm-end" for="password2">{{ lang.add.password_repeat }}</label>
+              <div class="col-sm-10">
+                <input type="password" data-pwgen-field="true" class="form-control" name="password2" placeholder="" autocomplete="new-password" required>
+              </div>
             </div>
           </div>
           <div class="row mb-2">
@@ -234,6 +236,15 @@
               </div>
             </div>
           </div>
+          <div class="row mb-2">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="authsource">{{ lang.admin.iam }}</label>
+            <div class="col-sm-10">
+              <select class="full-width-select" data-live-search="true" id="mbox_template_iam" name="authsource">
+                <option selected>mailcow</option>
+                <option>keycloak</option>
+              </select>
+            </div>
+          </div>
           <div class="row mb-2">
             <label class="control-label col-sm-2 text-sm-end text-sm-end">{{ lang.add.tags }}</label>
             <div class="col-sm-10">