public
nobgit
read
NobMail
Based on mailcow: dockerized
Languages
Repository composition by tracked source files.
PHP
49%
JavaScript
35%
HTML
9%
CSS
4%
Shell
2%
Python
1%
Lua
0%
Perl
0%
Ruby
0%
SCSS
0%
Create file
Wiki Documentation
Clone
https://nobgit.com/orgs/nobgit/nobmail.git
ssh://[email protected]:2222/orgs/nobgit/nobmail.git
Commit
[Web] add manage identity provider
6e9980bf
data/web/admin.php | 3 +
data/web/inc/footer.inc.php | 2 +
data/web/inc/functions.inc.php | 109 ++++++++++++++++-----
data/web/inc/init_db.inc.php | 2 +-
data/web/js/build/013-mailcow.js | 11 +++
data/web/js/site/admin.js | 34 +++++++
data/web/js/site/mailbox.js | 40 +++++++-
data/web/json_api.php | 13 +++
data/web/lang/lang.en-gb.json | 16 +++
data/web/templates/admin.twig | 4 +-
.../admin/tab-config-identity-provider.twig | 95 ++++++++++++++++++
.../admin/tab-config-identity-providers.twig | 58 -----------
data/web/templates/base.twig | 2 +
data/web/templates/edit/mailbox-templates.twig | 9 ++
data/web/templates/edit/mailbox.twig | 4 +-
data/web/templates/index.twig | 19 +++-
data/web/templates/modals/mailbox.twig | 57 ++++++-----
17 files changed, 362 insertions(+), 116 deletions(-)
create mode 100644 data/web/templates/admin/tab-config-identity-provider.twig
delete mode 100644 data/web/templates/admin/tab-config-identity-providers.twig
Diff
diff --git a/data/web/admin.php b/data/web/admin.php
index 278ab61b..1ffb76fb 100644
--- a/data/web/admin.php
+++ b/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'])
];
diff --git a/data/web/inc/footer.inc.php b/data/web/inc/footer.inc.php
index 61d81dff..8c50c9c1 100644
--- a/data/web/inc/footer.inc.php
+++ b/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'],
diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php
index 793e3e6b..0a1edcff 100644
--- a/data/web/inc/functions.inc.php
+++ b/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;
}
}
diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php
index 5cfb1cd6..3de3dec9 100644
--- a/data/web/inc/init_db.inc.php
+++ b/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"
),
diff --git a/data/web/js/build/013-mailcow.js b/data/web/js/build/013-mailcow.js
index f3ecbb90..f0909831 100644
--- a/data/web/js/build/013-mailcow.js
+++ b/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");
+ }
+ });
});
diff --git a/data/web/js/site/admin.js b/data/web/js/site/admin.js
index dd6dcce4..708cb0be 100644
--- a/data/web/js/site/admin.js
+++ b/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();
+ });
});
diff --git a/data/web/js/site/mailbox.js b/data/web/js/site/mailbox.js
index cc316b71..6fbcc3e3 100644
--- a/data/web/js/site/mailbox.js
+++ b/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',
diff --git a/data/web/json_api.php b/data/web/json_api.php
index 769775ce..52ca8b71 100644
--- a/data/web/json_api.php
+++ b/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;
diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json
index 851b98aa..436a9b37 100644
--- a/data/web/lang/lang.en-gb.json
+++ b/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",
diff --git a/data/web/templates/admin.twig b/data/web/templates/admin.twig
index 53fdeadc..792c0bca 100644
--- a/data/web/templates/admin.twig
+++ b/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' %}
diff --git a/data/web/templates/admin/tab-config-identity-provider.twig b/data/web/templates/admin/tab-config-identity-provider.twig
new file mode 100644
index 00000000..600b336a
--- /dev/null
+++ b/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>
diff --git a/data/web/templates/admin/tab-config-identity-providers.twig b/data/web/templates/admin/tab-config-identity-providers.twig
deleted file mode 100644
index c81978db..00000000
--- a/data/web/templates/admin/tab-config-identity-providers.twig
+++ /dev/null
@@ -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>
diff --git a/data/web/templates/base.twig b/data/web/templates/base.twig
index 1c027138..29b3cc23 100644
--- a/data/web/templates/base.twig
+++ b/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 }}';
diff --git a/data/web/templates/edit/mailbox-templates.twig b/data/web/templates/edit/mailbox-templates.twig
index f606bd45..c83a15dd 100644
--- a/data/web/templates/edit/mailbox-templates.twig
+++ b/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">
diff --git a/data/web/templates/edit/mailbox.twig b/data/web/templates/edit/mailbox.twig
index 52b4096c..d2275cdd 100644
--- a/data/web/templates/edit/mailbox.twig
+++ b/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">
diff --git a/data/web/templates/index.twig b/data/web/templates/index.twig
index 420bd531..35e53b82 100644
--- a/data/web/templates/index.twig
+++ b/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">
diff --git a/data/web/templates/modals/mailbox.twig b/data/web/templates/modals/mailbox.twig
index 5d6ea535..e0b4f02b 100644
--- a/data/web/templates/modals/mailbox.twig
+++ b/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">