1
0
mirror of https://github.com/mailcow/mailcow-dockerized.git synced 2024-12-23 02:04:46 +02:00

[Web] add manage identity provider

This commit is contained in:
FreddleSpl0it 2023-03-14 14:10:46 +01:00 committed by DerLinkman
parent 67c9c5b8ed
commit 6e9980bf0f
No known key found for this signature in database
GPG Key ID: F109FD97469550A2
17 changed files with 364 additions and 118 deletions

View File

@ -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'])
];

View File

@ -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'],

View File

@ -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;
}
}

View File

@ -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"
),

View File

@ -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");
}
});
});

View File

@ -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();
});
});

View File

@ -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',

View File

@ -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;

View File

@ -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",

View File

@ -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' %}

View File

@ -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>

View File

@ -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>

View File

@ -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 }}';

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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="row mb-4">
<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 class="full-width-select" data-live-search="true" id="addAuthsource" name="authsource" required>
<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="authsource">{{ lang.admin.iam }}</label>
<div class="col-sm-10">
<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="name">{{ lang.add.full_name }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="name">
<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="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 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 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">