From 6e9980bf0f8b16420cc884e12889b074c7b8bc79 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Tue, 14 Mar 2023 14:10:46 +0100 Subject: [PATCH] [Web] add manage identity provider --- 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 + .../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 | 61 ++++++---- 17 files changed, 364 insertions(+), 118 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 --git a/data/web/admin.php b/data/web/admin.php index 278ab61b8..1ffb76fb4 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 61d81dffa..8c50c9c15 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 793e3e6b1..0a1edcff3 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 5cfb1cd62..3de3dec9b 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 f3ecbb904..f09098310 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 dd6dcce4b..708cb0be0 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 cc316b713..6fbcc3e3f 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 '' + data + ''; + } }, { 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 ? '' + data + '' : ''; + 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 769775cee..52ca8b715 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 851b98aa1..436a9b370 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 ham", "goto_spam": "Learn as spam", "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 53fdeadc1..792c0bca0 100644 --- a/data/web/templates/admin.twig +++ b/data/web/templates/admin.twig @@ -7,7 +7,7 @@