Переглянути джерело

[Web] add generic-oidc provider

FreddleSpl0it 2 роки тому
батько
коміт
61559f3a66

+ 87 - 54
data/web/inc/functions.inc.php

@@ -2069,8 +2069,13 @@ function uuid4() {
 }
 function identity_provider($_action, $_data = null) {
 function identity_provider($_action, $_data = null, $hide_secret = false) {
+function identity_provider($_action, $_data = null, $_extra = null) {
   global $pdo;
 
+  $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 'get':
       $settings = array();
@@ -2078,16 +2083,15 @@ function identity_provider($_action, $_data = null, $hide_secret = false) {
       $stmt->execute();
       $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
       foreach($rows as $row){
-        if ($row["key"] == 'mappers'){
-          $settings['mappers'] = json_decode($row["value"]);
-        } else if ($row["key"] == 'templates'){
-          $settings['templates'] = json_decode($row["value"]);
+        if ($row["key"] == 'mappers' || $row["key"] == 'templates'){
+          $settings[$row["key"]] = json_decode($row["value"]);
         } else {
           $settings[$row["key"]] = $row["value"];
         }
       }
-      if ($hide_secret){
+      if ($_extra['hide_sensitive']){
         $settings['client_secret'] = '';
+        $settings['access_token'] = '';
       }
       return $settings;
     break;
@@ -2100,54 +2104,60 @@ 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`);");
-      
-      $_data['login_flow'] = (isset($_data['login_flow']) && $_data['login_flow'] == 'ropc') ? 'ropc' : 'rest';
+      if (!isset($_data['authsource'])){
+        $_SESSION['return'][] =  array(
+          'type' => 'danger',
+          'log' => array(__FUNCTION__, $_action, $data_log),
+          'msg' => array('required_data_missing', $setting)
+        );
+        return false;
+      }
+      $_data['authsource'] = strtolower($_data['authsource']);
+      if ($_data['authsource'] != "keycloak" && $_data['authsource'] != "generic-oidc"){
+        $_SESSION['return'][] =  array(
+          'type' => 'danger',
+          'log' => array(__FUNCTION__, $_action, $data_log),
+          'msg' => array('invalid_authsource', $setting)
+        );
+        return false;
+      }
 
-      // add connection settings      
-      $required_settings = array('server_url', 'authsource', 'realm', 'client_id', 'client_secret', 'redirect_url', 'version', 'login_flow');
+      if ($_data['authsource'] == "keycloak") {
+        $_data['mailpassword_flow'] = isset($_data['mailpassword_flow']) ? intval($_data['mailpassword_flow']) : 0;
+        $_data['periodic_sync'] = isset($_data['periodic_sync']) ? intval($_data['periodic_sync']) : 0;
+        $_data['import_users'] = isset($_data['import_users']) ? intval($_data['import_users']) : 0;
+        $required_settings = array('authsource', 'server_url', 'realm', 'client_id', 'client_secret', 'redirect_url', 'version', 'mailpassword_flow', 'periodic_sync', 'import_users');
+      } else if ($_data['authsource'] == "generic-oidc") {
+        $required_settings = array('authsource', 'authorize_url', 'token_url', 'client_id', 'client_secret', 'redirect_url', 'userinfo_url');
+      }
+      
+      $pdo->beginTransaction();
+      $stmt = $pdo->prepare("INSERT INTO identity_provider (`key`, `value`) VALUES (:key, :value) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);");
+      // add connection settings
       foreach($required_settings as $setting){
-        if (!$_data[$setting]){
+        if (!isset($_data[$setting])){
           $_SESSION['return'][] =  array(
             'type' => 'danger',
             'log' => array(__FUNCTION__, $_action, $data_log),
-            'msg' => 'required_data_missing'
+            'msg' => array('required_data_missing', $setting)
           );
+          $pdo->rollback();
           return false;
         }
-      }
-      foreach($_data as $key => $value){
-        if (!in_array($key, $required_settings) || $key == 'mappers' || $key == 'templates'){
-          continue;
-        }
 
-        $stmt->bindParam(':key', $key);
-        $stmt->bindParam(':value', $value);
+        $stmt->bindParam(':key', $setting);
+        $stmt->bindParam(':value', $_data[$setting]);
         $stmt->execute();
       }
+      $pdo->commit();
 
       // add mappers
       if ($_data['mappers'] && $_data['templates']){
-        if (!is_array($_data['mappers'])){
-          $_data['mappers'] = array($_data['mappers']);
-        }
-        if (!is_array($_data['templates'])){
-          $_data['templates'] = array($_data['templates']);
-        }
-        $mappers = array();
-        $templates = array();
-        foreach($_data['mappers'] as $mapper){
-          if ($mapper){
-            array_push($mappers, $mapper);
-          }
-        }
-        foreach($_data['templates'] as $template){
-          if ($template){
-            array_push($templates, $template);
-          }
-        }
+        $_data['mappers'] = (!is_array($_data['mappers'])) ? array($_data['mappers']) : $_data['mappers'];
+        $_data['templates'] = (!is_array($_data['templates'])) ? array($_data['templates']) : $_data['templates'];
+
+        $mappers = array_filter($_data['mappers']);
+        $templates = array_filter($_data['templates']);
         if (count($mappers) == count($templates)){
           $mappers = json_encode($mappers);
           $templates = json_encode($templates);
@@ -2161,6 +2171,9 @@ function identity_provider($_action, $_data = null, $hide_secret = false) {
         }
       }
 
+      // delete old access_token
+      $stmt = $pdo->query("INSERT INTO identity_provider (`key`, `value`) VALUES ('access_token', '') ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);");
+
       $_SESSION['return'][] =  array(
         'type' => 'success',
         'log' => array(__FUNCTION__, $_action, $data_log),
@@ -2178,7 +2191,11 @@ function identity_provider($_action, $_data = null, $hide_secret = false) {
         return false;
       }
 
-      $url = "{$_data['server_url']}/realms/{$_data['realm']}/protocol/openid-connect/token";
+      if ($_data['authsource'] == 'keycloak') {
+        $url = "{$_data['server_url']}/realms/{$_data['realm']}/protocol/openid-connect/token";
+      } else {
+        $url = $_data['token_url'];
+      }
       $req = http_build_query(array(
         'grant_type'    => 'client_credentials',
         'client_id'     => $_data['client_id'],
@@ -2215,21 +2232,37 @@ function identity_provider($_action, $_data = null, $hide_secret = false) {
       return true;
     break;
     case "init":
-      $identity_provider_settings = identity_provider('get');
+      $iam_settings = identity_provider('get');
       $provider = null;
-      if ($identity_provider_settings['server_url'] && $identity_provider_settings['realm'] && $identity_provider_settings['client_id'] &&
-          $identity_provider_settings['client_secret'] && $identity_provider_settings['redirect_url'] && $identity_provider_settings['version']){
-        $provider = new Stevenmaguire\OAuth2\Client\Provider\Keycloak([
-          'authServerUrl'         => $identity_provider_settings['server_url'],
-          'realm'                 => $identity_provider_settings['realm'],
-          'clientId'              => $identity_provider_settings['client_id'],
-          'clientSecret'          => $identity_provider_settings['client_secret'],
-          'redirectUri'           => $identity_provider_settings['redirect_url'],
-          'version'               => $identity_provider_settings['version'],                            
-          // 'encryptionAlgorithm'   => 'RS256',                             // optional
-          // 'encryptionKeyPath'     => '../key.pem'                         // optional
-          // 'encryptionKey'         => 'contents_of_key_or_certificate'     // optional
-        ]);
+      if ($iam_settings['authsource'] == 'keycloak'){
+        if ($iam_settings['server_url'] && $iam_settings['realm'] && $iam_settings['client_id'] &&
+            $iam_settings['client_secret'] && $iam_settings['redirect_url'] && $iam_settings['version']){
+          $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'],                            
+            // 'encryptionAlgorithm'   => 'RS256',                             // optional
+            // 'encryptionKeyPath'     => '../key.pem'                         // optional
+            // 'encryptionKey'         => 'contents_of_key_or_certificate'     // optional
+          ]);
+        }
+      }
+      else if ($iam_settings['authsource'] == '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']){
+          $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'                  => 'openid profile email'
+          ]);
+        }
       }
       return $provider;
     break;

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

@@ -362,7 +362,7 @@ function init_db_schema() {
           "custom_attributes" => "JSON NOT NULL DEFAULT ('{}')",
           "kind" => "VARCHAR(100) NOT NULL DEFAULT ''",
           "multiple_bookings" => "INT NOT NULL DEFAULT -1",
-          "authsource" => "ENUM('mailcow', 'keycloak') DEFAULT 'mailcow'",
+          "authsource" => "ENUM('mailcow', 'keycloak', 'generic-oidc') DEFAULT 'mailcow'",
           "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
           "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
           "active" => "TINYINT(1) NOT NULL DEFAULT '1'"

+ 210 - 92
data/web/templates/admin/tab-config-identity-provider.twig

@@ -8,110 +8,228 @@
     </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 class="row mb-4">
+        <label class="control-label col-sm-3 text-sm-end" for="iam_realm">{{ lang.admin.iam }}:</label>
+        <div class="col-sm-4">
+          <select
+            data-style="btn btn-secondary"
+            data-id="iam_provider"
+            title="{{ lang.admin.iam_provider }}"
+            name="iam_provider" id="iam_provider" class="full-width-select form-control" required>
+              <option value="keycloak" {% if not iam_settings.authsource or iam_settings.authsource == 'keycloak' %}selected{% endif %}>Keycloak</option>
+              <option value="generic-oidc" {% if iam_settings.authsource == 'generic-oidc' %}selected{% endif %}>Generic-OIDC</option>
+          </select>
         </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 id="keycloak_settings" class="{% if iam_settings.authsource and iam_settings.authsource != 'keycloak' %}d-none{% endif %}">
+        <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-sm-3 text-sm-end" for="iam_keycloak_url">{{ lang.admin.iam_server_url }}:</label>
+            <div class="col-sm-4">
+              <input type="text" class="form-control" id="iam_keycloak_url" name="server_url" value="{{ iam_settings.server_url }}" required>
+            </div>
           </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 class="row mb-2">
+            <label class="control-label col-sm-3 text-sm-end" for="iam_keycloak_realm">{{ lang.admin.iam_realm }}:</label>
+            <div class="col-sm-4">
+              <input type="text" class="form-control" id="iam_keycloak_realm" name="realm" value="{{ iam_settings.realm }}" required>
+            </div>
           </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 class="row mb-2">
+            <label class="control-label col-sm-3 text-sm-end" for="iam_keycloak_clientid">{{ lang.admin.iam_client_id }}:</label>
+            <div class="col-sm-4">
+              <input type="text" class="form-control" id="iam_keycloak_clientid" name="client_id" value="{{ iam_settings.client_id }}" required>
             </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 class="row mb-2">
+            <label class="control-label col-sm-3 text-sm-end" for="iam_keycloak_clientsecret">{{ 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_keycloak_clientsecret" name="client_secret" value="{{ iam_settings.client_secret }}" required>
+                <button class="toggle-password btn btn-secondary" type="button"><i class="bi bi-eye"></i></button>
+              </div>
+            </div>
           </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 class="row mb-2">
+            <label class="control-label col-sm-3 text-sm-end" for="iam_keycloak_redirecturl">{{ lang.admin.iam_redirect_url }}:</label>
+            <div class="col-sm-4">
+              <input type="text" class="form-control" id="iam_keycloak_redirecturl" name="redirect_url" value="{{ iam_settings.redirect_url }}" required>
+            </div>
           </div>
-        </div>
-        <div class="row mb-4">
-          <label class="control-label col-sm-3 text-sm-end" for="iam_version">{{ lang.admin.iam_mapping }}:</label>
-          <div class="col-4 d-flex mb-2">
-            <span class="w-100 me-2">Attribute</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.mappers %}
-          <div class="offset-sm-3 col-4 d-flex mb-2">
-            <input type="text" class="form-control me-2" name="mappers" value="{{ identity_provider_settings.mappers[key] }}" required>
-            <select data-live-search="true" name="templates" class="form-control" title="{{ lang.mailbox.template }}" required>
-            {% 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 %}
-          {% if not identity_provider_settings.mappers %}
-          <div class="offset-sm-3 col-4 d-flex mb-2">
-            <input type="text" class="form-control me-2" name="mappers" value="" required>
-            <select data-live-search="true" name="templates" class="form-control" title="{{ lang.mailbox.template }}" required>
-            {% for mbox_template in mbox_templates %}
-              <option>
-                {{ mbox_template.template }}
-              </option>
+          <div class="row mb-4">
+            <label class="control-label col-sm-3 text-sm-end" for="iam_keycloak_version">{{ lang.admin.iam_version }}:</label>
+            <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-4">
+            <label class="control-label col-sm-3 text-sm-end">{{ lang.admin.iam_mapping }}:</label>
+            <div class="col-4 d-flex mb-2">
+              <span class="w-100 me-2">Attribute</span>
+              <span class="w-100 ms-2">Template</span>
+              <button class="btn btn-sm d-block d-sm-inline btn-secondary ms-2 iam_rolemap_add"><i class="bi bi-plus-lg"></i></button>
+            </div>
+            {% for key, role in iam_settings.mappers %}
+            <div class="offset-sm-3 col-4 d-flex mb-2">
+              <input type="text" class="form-control me-2" name="mappers" value="{{ iam_settings.mappers[key] }}" required>
+              <select data-live-search="true" name="templates" class="form-control" title="{{ lang.mailbox.template }}" required>
+              {% for mbox_template in mbox_templates %}
+                <option{% if mbox_template.template == iam_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 %}
-            </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>
+            {% if not iam_settings.mappers %}
+            <div class="offset-sm-3 col-4 d-flex mb-2">
+              <input type="text" class="form-control me-2" name="mappers" value="" required>
+              <select data-live-search="true" name="templates" class="form-control" title="{{ lang.mailbox.template }}" required>
+              {% 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>
+            {% endif %}
           </div>
-          {% endif %}
-        </div>
-        <div class="row mb-2">
-          <label class="control-label col-sm-3 text-sm-end">{{ lang.admin.iam_auth_flow }}</label>
-          <div class="col-sm-9">
-            <div class="btn-group">
-              <input type="radio" class="btn-check" name="login_flow" id="iam_login_flow_rest" autocomplete="off" value="rest" {% if  identity_provider_settings.login_flow != 'ropc' %}checked{% endif %}>
-              <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="iam_login_flow_rest">{{ lang.admin.iam_rest_flow }}</label>
-
-              <input type="radio" class="btn-check" name="login_flow" id="iam_login_flow_ropc" autocomplete="off" value="ropc" {% if  identity_provider_settings.login_flow == 'ropc' %}checked{% endif %}>
-              <label class="btn btn-sm btn-xs-quart d-block d-sm-inline btn-secondary" for="iam_login_flow_ropc">{{ lang.admin.iam_ropc_flow }}</label>
-            </div>
-            <p class="text-muted">
-              <small>
-                {{ lang.admin.iam_auth_flow_info|raw }}<br>
-                {{ lang.admin.iam_auth_flow_rest_info|raw }}<br>
-                {{ lang.admin.iam_auth_flow_ropc_info|raw }}
-              </small>
-            </p>
+          <div class="row mb-2 mt-4">
+            <label class="control-label col-sm-3 text-sm-end"></label>
+            <div class="col-sm-9">
+              <span>{{ lang.admin.iam_extra_permission|raw }}</span>
+            </div>
           </div>
-        </div>
-        <div class="row mt-4 mb-2">
-          <div class="offset-sm-3 col-sm-9 d-flex">
-            <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='{}'><i class="bi bi-check-lg"></i> {{ lang.admin.save }}</button>
+          <div class="row mb-2">
+            <label class="control-label col-sm-3 text-sm-end">{{ lang.admin.iam_rest_flow }}</label>
+            <div class="col-sm-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 %}>
+              </div>
+              <p class="text-muted">
+                <small>
+                  {{ lang.admin.iam_auth_flow_info|raw }}
+                </small>
+              </p>
             </div>
-            <button class="btn btn-sm d-block d-sm-inline btn-danger ms-auto" data-item="identity-provider" data-action="delete_selected" data-id="iam_sso" data-api-url='delete/identity-provider'><i class="bi bi-trash"></i> {{ lang.mailbox.remove }}</button>
           </div>
-        </div>
-      </form>
+          <div class="row mb-2">
+            <label class="control-label col-sm-3 text-sm-end">Periodic Full Sync</label>
+            <div class="col-sm-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 %}>
+              </div>
+            </div>
+          </div>
+          <div class="row mb-2">
+            <label class="control-label col-sm-3 text-sm-end">Import Users</label>
+            <div class="col-sm-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 %}>
+              </div>
+            </div>
+          </div>
+          <div class="row mt-4 mb-2">
+            <div class="offset-sm-3 col-sm-9 d-flex">
+              <div class="btn-group">   
+                <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>
+              <button class="btn btn-sm d-block d-sm-inline btn-danger ms-auto" data-item="identity-provider" data-action="delete_selected" data-id="iam_keycloak" data-api-url='delete/identity-provider'><i class="bi bi-trash"></i> {{ lang.mailbox.remove }}</button>
+            </div>
+          </div>
+        </form>
+      </div>
+      <div id="generic_oidc_settings" class="{% if not iam_settings.authsource or iam_settings.authsource != 'generic-oidc' %}d-none{% endif %}">
+        <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-sm-3 text-sm-end" for="iam_authorize_url">{{ lang.admin.iam_authorize_url }}:</label>
+            <div class="col-sm-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-sm-3 text-sm-end" for="iam_token_url">{{ lang.admin.iam_token_url }}:</label>
+            <div class="col-sm-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-sm-3 text-sm-end" for="iam_userinfo_url">{{ lang.admin.iam_userinfo_url }}:</label>
+            <div class="col-sm-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-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="{{ iam_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="{{ iam_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-4">
+            <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="{{ iam_settings.redirect_url }}" required>
+            </div>
+          </div>
+          <div class="row mb-4">
+            <label class="control-label col-sm-3 text-sm-end">{{ lang.admin.iam_mapping }}:</label>
+            <div class="col-4 d-flex mb-2">
+              <span class="w-100 me-2">Attribute</span>
+              <span class="w-100 ms-2">Template</span>
+              <button class="btn btn-sm d-block d-sm-inline btn-secondary ms-2 iam_rolemap_add"><i class="bi bi-plus-lg"></i></button>
+            </div>
+            {% for key, role in iam_settings.mappers %}
+            <div class="offset-sm-3 col-4 d-flex mb-2">
+              <input type="text" class="form-control me-2" name="mappers" value="{{ iam_settings.mappers[key] }}" required>
+              <select data-live-search="true" name="templates" class="form-control" title="{{ lang.mailbox.template }}" required>
+              {% for mbox_template in mbox_templates %}
+                <option{% if mbox_template.template == iam_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 %}
+            {% if not iam_settings.mappers %}
+            <div class="offset-sm-3 col-4 d-flex mb-2">
+              <input type="text" class="form-control me-2" name="mappers" value="" required>
+              <select data-live-search="true" name="templates" class="form-control" title="{{ lang.mailbox.template }}" required>
+              {% 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>
+            {% endif %}
+          </div>
+          <div class="row mt-4 mb-2">
+            <div class="offset-sm-3 col-sm-9 d-flex">
+              <div class="btn-group">   
+                <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>
+              <button class="btn btn-sm d-block d-sm-inline btn-danger ms-auto" data-item="identity-provider" data-action="delete_selected" data-id="iam_generic" data-api-url='delete/identity-provider'><i class="bi bi-trash"></i> {{ lang.mailbox.remove }}</button>
+            </div>
+          </div>
+        </form>
+      </div>
     </div>
   </div>
 </div>