[Web] Feature: Add password policy

master
andryyy 2021-04-09 13:46:17 +02:00
parent 616226be8a
commit 5ea649b292
No known key found for this signature in database
GPG Key ID: 8EC34FF2794E25EF
12 changed files with 1042 additions and 921 deletions

View File

@ -23,6 +23,7 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
<li role="presentation"><a href="#tab-config-quarantine" aria-controls="tab-config-quarantine" role="tab" data-toggle="tab"><?=$lang['admin']['quarantine'];?></a></li>
<li role="presentation"><a href="#tab-config-quota" aria-controls="tab-config-quota" role="tab" data-toggle="tab"><?=$lang['admin']['quota_notifications'];?></a></li>
<li role="presentation"><a href="#tab-config-rsettings" aria-controls="tab-config-rsettings" role="tab" data-toggle="tab"><?=$lang['admin']['rspamd_settings_map'];?></a></li>
<li role="presentation"><a href="#tab-config-password-policy" aria-controls="tab-config-password-policy" role="tab" data-toggle="tab"><?=$lang['admin']['password_policy'];?></a></li>
<li role="presentation"><a href="#tab-config-customize" aria-controls="tab-config-customize" role="tab" data-toggle="tab"><?=$lang['admin']['customize'];?></a></li>
</ul>
</li>
@ -77,7 +78,7 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
foreach ($tfa_data['additional'] as $key_info) {
?>
<form style="display:inline;" method="post">
<input type="hidden" name="unset_tfa_key" value="<?=$key_info['id'];?>" />
<input type="hidden" name="unset_tfa_key" value="<?=$key_info['id'];?>">
<div style="padding:4px;margin:4px" class="label label-keys label-<?=($_SESSION['tfa_id'] == $key_info['id']) ? 'success' : 'default'; ?>">
<?=$key_info['key_id'];?>
<a href="#" style="font-weight:bold;color:white" onClick="$(this).closest('form').submit()">[<?=$lang['admin']['remove'];?>]</a>
@ -128,7 +129,7 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
</td>
<td style="min-width:240px;text-align: right">
<form style="display:inline;" method="post">
<input type="hidden" name="unset_fido2_key" value="<?=$key_info['cid'];?>" />
<input type="hidden" name="unset_fido2_key" value="<?=$key_info['cid'];?>">
<div class="btn-group">
<a href="#" class="btn btn-xs btn-default" data-cid="<?=$key_info['cid'];?>" data-subject="<?=base64_encode($key_info['subject']);?>" data-toggle="modal" data-target="#fido2ChangeFn"><span class="glyphicon glyphicon-pencil"></span> <?=$lang['fido2']['rename'];?></a>
<a href="#" onClick='return confirm("<?=$lang['admin']['ays'];?>")?$(this).closest("form").submit():"";' class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> <?=$lang['admin']['remove'];?></a>
@ -381,7 +382,7 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
</form>
</div>
<div class="col-sm-3">
<img class="img-responsive" src="/img/rspamd_logo.png" alt="Rspamd UI" />
<img class="img-responsive" src="/img/rspamd_logo.png" alt="Rspamd UI">
</div>
</div>
</div>
@ -507,7 +508,7 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
($GLOBALS['SHOW_DKIM_PRIV_KEYS'] === true) ?: $dkim['privkey'] = base64_encode('Please set $SHOW_DKIM_PRIV_KEYS to true to show DKIM private keys.');
?>
<div class="row collapse in dkim_key_valid">
<div class="col-md-1"><input type="checkbox" data-id="dkim" name="multi_select" value="<?=$domain;?>" /></div>
<div class="col-md-1"><input type="checkbox" data-id="dkim" name="multi_select" value="<?=$domain;?>"></div>
<div class="col-md-3">
<p><?=$lang['admin']['domain'];?>: <strong><?=htmlspecialchars($domain);?></strong>
<p class="dkim-label"><span class="label label-success"><?=$lang['admin']['dkim_key_valid'];?></span></p>
@ -526,7 +527,7 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
else {
?>
<div class="row collapse in dkim_key_missing">
<div class="col-md-1"><input class="dkim_missing" type="checkbox" data-id="dkim" name="multi_select" value="<?=$domain;?>" disabled /></div>
<div class="col-md-1"><input class="dkim_missing" type="checkbox" data-id="dkim" name="multi_select" value="<?=$domain;?>" disabled></div>
<div class="col-md-3">
<p><?=$lang['admin']['domain'];?>: <strong><?=htmlspecialchars($domain);?></strong><br><span class="label label-danger"><?=$lang['admin']['dkim_key_missing'];?></span></p>
</div>
@ -541,7 +542,7 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
($GLOBALS['SHOW_DKIM_PRIV_KEYS'] === true) ?: $dkim['privkey'] = base64_encode('Please set $SHOW_DKIM_PRIV_KEYS to true to show DKIM private keys.');
?>
<div class="row collapse in dkim_key_valid">
<div class="col-md-1"><input type="checkbox" data-id="dkim" name="multi_select" value="<?=$alias_domain;?>" /></div>
<div class="col-md-1"><input type="checkbox" data-id="dkim" name="multi_select" value="<?=$alias_domain;?>"></div>
<div class="col-md-2 col-md-offset-1">
<p><small> Alias-Domain: <strong><?=htmlspecialchars($alias_domain);?></strong></small>
<p class="dkim-label"><span class="label label-success"><?=$lang['admin']['dkim_key_valid'];?></span></p>
@ -560,7 +561,7 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
else {
?>
<div class="row collapse in dkim_key_missing">
<div class="col-md-1"><input class="dkim_missing" type="checkbox" data-id="dkim" name="multi_select" value="<?=$alias_domain;?>" disabled /></div>
<div class="col-md-1"><input class="dkim_missing" type="checkbox" data-id="dkim" name="multi_select" value="<?=$alias_domain;?>" disabled></div>
<div class="col-md-2 col-md-offset-1">
<p><small> Alias-Domain: <strong><?=htmlspecialchars($alias_domain);?></strong><br></small><span class="label label-danger"><?=$lang['admin']['dkim_key_missing'];?></span></p>
</div>
@ -577,7 +578,7 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
($GLOBALS['SHOW_DKIM_PRIV_KEYS'] === true) ?: $dkim['privkey'] = base64_encode('Please set $SHOW_DKIM_PRIV_KEYS to true to show DKIM private keys.');
?>
<div class="row collapse in dkim_key_unused">
<div class="col-md-1"><input type="checkbox" data-id="dkim" name="multi_select" value="<?=$blind;?>" /></div>
<div class="col-md-1"><input type="checkbox" data-id="dkim" name="multi_select" value="<?=$blind;?>"></div>
<div class="col-md-3">
<p><?=$lang['admin']['domain'];?>: <strong><?=htmlspecialchars($blind);?></strong>
<p class="dkim-label"><span class="label label-warning"><?=$lang['admin']['dkim_key_unused'];?></span></p>
@ -1212,6 +1213,49 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
</div>
</div>
<div role="tabpanel" class="tab-pane" id="tab-config-password-policy">
<div class="panel panel-default">
<div class="panel-heading"><?=$lang['admin']['password_policy'];?></div>
<div class="panel-body">
<?php $password_complexity = password_complexity('get'); ?>
<form class="form-horizontal" data-id="passwordpolicy" role="form" method="post">
<?php
foreach ($password_complexity as $name => $value) {
if ($name == 'length') {
?>
<div class="form-group">
<label class="control-label col-sm-3" for="<?=$name;?>"><?=$lang['admin']['password_length'];?>:</label>
<div class="col-sm-2">
<input type="number" class="form-control" min="3" max="64" name="<?=$name;?>" id="<?=$name;?>" value="<?=$value;?>" required>
</div>
</div>
<?php
} else {
?>
<input type="hidden" name="<?=$name;?>" value="0">
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<label>
<input type="checkbox" name="<?=$name;?>" id="<?=$name;?>" value="1" <?=($value == 1) ? 'checked' : null;?>> <?=$lang['admin']["password_policy_$name"];?>
</label>
</div>
</div>
<?php
}
}
?>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<div class="btn-group">
<button class="btn btn-sm btn-success" data-item="passwordpolicy" data-action="edit_selected" data-id="passwordpolicy" data-api-url='edit/passwordpolicy' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="tab-sys-mails">
<div class="panel panel-default">
<div class="panel-heading"><?=$lang['admin']['sys_mails'];?></div>

View File

@ -48,24 +48,16 @@ function admin($_action, $_data = null) {
return false;
}
}
if (!empty($password) && !empty($password2)) {
if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'password_complexity'
);
if (password_check($password_new, $password_new2) !== true) {
return false;
}
if ($password != $password2) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'password_mismatch'
);
return false;
// support pre hashed passwords
if (preg_match('/^{(ARGON2I|ARGON2ID|BLF-CRYPT|CLEAR|CLEARTEXT|CRYPT|DES-CRYPT|LDAP-MD5|MD5|MD5-CRYPT|PBKDF2|PLAIN|PLAIN-MD4|PLAIN-MD5|PLAIN-TRUNC|PLAIN-TRUNC|SHA|SHA1|SHA256|SHA256-CRYPT|SHA512|SHA512-CRYPT|SMD5|SSHA|SSHA256|SSHA512)}/i', $password)) {
$password_hashed = $password_new;
}
else {
$password_hashed = hash_password($password_new);
}
$password_hashed = hash_password($password);
$stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`)
VALUES (:username, :password_hashed, '1', :active)");
$stmt->execute(array(
@ -73,15 +65,6 @@ function admin($_action, $_data = null) {
':password_hashed' => $password_hashed,
':active' => $active
));
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'password_empty'
);
return false;
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data_log),
@ -145,23 +128,16 @@ function admin($_action, $_data = null) {
}
}
if (!empty($password) && !empty($password2)) {
if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'password_complexity'
);
continue;
if (password_check($password, $password2) !== true) {
return false;
}
if ($password != $password2) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'password_mismatch'
);
continue;
// support pre hashed passwords
if (preg_match('/^{(ARGON2I|ARGON2ID|BLF-CRYPT|CLEAR|CLEARTEXT|CRYPT|DES-CRYPT|LDAP-MD5|MD5|MD5-CRYPT|PBKDF2|PLAIN|PLAIN-MD4|PLAIN-MD5|PLAIN-TRUNC|PLAIN-TRUNC|SHA|SHA1|SHA256|SHA256-CRYPT|SHA512|SHA512-CRYPT|SMD5|SSHA|SSHA256|SSHA512)}/i', $password)) {
$password_hashed = $password;
}
else {
$password_hashed = hash_password($password);
}
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
$stmt->execute(array(
':password_hashed' => $password_hashed,

View File

@ -21,7 +21,7 @@ function dkim($_action, $_data = null, $privkey = false) {
if (!is_valid_domain_name($domain) || !is_numeric($key_length)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data, $privkey),
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('dkim_domain_or_sel_invalid', $domain)
);
continue;
@ -29,7 +29,7 @@ function dkim($_action, $_data = null, $privkey = false) {
if ($redis->hGet('DKIM_PUB_KEYS', $domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data, $privkey),
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('dkim_domain_or_sel_invalid', $domain)
);
continue;
@ -37,7 +37,7 @@ function dkim($_action, $_data = null, $privkey = false) {
if (!ctype_alnum(str_replace(['-', '_'], '', $dkim_selector))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data, $privkey),
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('dkim_domain_or_sel_invalid', $domain)
);
continue;
@ -62,7 +62,7 @@ function dkim($_action, $_data = null, $privkey = false) {
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data, $privkey),
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('redis_error', $e)
);
continue;
@ -76,7 +76,7 @@ function dkim($_action, $_data = null, $privkey = false) {
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data, $privkey),
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('redis_error', $e)
);
continue;
@ -84,14 +84,14 @@ function dkim($_action, $_data = null, $privkey = false) {
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data, $privkey),
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('dkim_added', $domain)
);
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data, $privkey),
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('dkim_domain_or_sel_invalid', $domain)
);
continue;
@ -102,7 +102,7 @@ function dkim($_action, $_data = null, $privkey = false) {
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data, $privkey),
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => 'access_denied'
);
return false;
@ -112,7 +112,7 @@ function dkim($_action, $_data = null, $privkey = false) {
if (empty($from_domain_dkim)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data, $privkey),
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('dkim_domain_or_sel_invalid', $from_domain)
);
continue;
@ -128,14 +128,14 @@ function dkim($_action, $_data = null, $privkey = false) {
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data, $privkey),
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('redis_error', $e)
);
continue;
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data, $privkey),
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('dkim_duplicated', $from_domain, $to_domain)
);
}
@ -144,7 +144,7 @@ function dkim($_action, $_data = null, $privkey = false) {
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data, $privkey),
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => 'access_denied'
);
return false;
@ -156,7 +156,7 @@ function dkim($_action, $_data = null, $privkey = false) {
if ($ssl_error = openssl_error_string()) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data, $privkey),
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('private_key_error', $ssl_error)
);
return false;
@ -173,7 +173,7 @@ function dkim($_action, $_data = null, $privkey = false) {
if (!is_valid_domain_name($domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data, $privkey),
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('dkim_domain_or_sel_invalid', $domain)
);
return false;
@ -182,7 +182,7 @@ function dkim($_action, $_data = null, $privkey = false) {
if ($overwrite_existing == 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data, $privkey),
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('dkim_domain_or_sel_exists', $domain)
);
return false;
@ -191,7 +191,7 @@ function dkim($_action, $_data = null, $privkey = false) {
if (!ctype_alnum($dkim_selector)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data, $privkey),
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('dkim_domain_or_sel_invalid', $domain)
);
return false;
@ -205,7 +205,7 @@ function dkim($_action, $_data = null, $privkey = false) {
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data, $privkey),
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('redis_error', $e)
);
return false;
@ -218,14 +218,14 @@ function dkim($_action, $_data = null, $privkey = false) {
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data, $privkey),
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('redis_error', $e)
);
return false;
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data, $privkey),
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('dkim_added', $domain)
);
return true;
@ -270,7 +270,7 @@ function dkim($_action, $_data = null, $privkey = false) {
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data, $privkey),
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => 'access_denied'
);
return false;
@ -286,7 +286,7 @@ function dkim($_action, $_data = null, $privkey = false) {
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data, $privkey),
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => 'access_denied'
);
return false;
@ -295,7 +295,7 @@ function dkim($_action, $_data = null, $privkey = false) {
if (!is_valid_domain_name($domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data, $privkey),
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('dkim_domain_or_sel_invalid', $domain)
);
continue;
@ -309,14 +309,14 @@ function dkim($_action, $_data = null, $privkey = false) {
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data, $privkey),
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('redis_error', $e)
);
continue;
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data, $privkey),
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('dkim_removed', htmlspecialchars($domain))
);
}

View File

@ -65,24 +65,16 @@ function domain_admin($_action, $_data = null) {
return false;
}
}
if (!empty($password) && !empty($password2)) {
if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'password_complexity'
);
return false;
if (password_check($password, $password2) !== true) {
continue;
}
if ($password != $password2) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'password_mismatch'
);
return false;
// support pre hashed passwords
if (preg_match('/^{(ARGON2I|ARGON2ID|BLF-CRYPT|CLEAR|CLEARTEXT|CRYPT|DES-CRYPT|LDAP-MD5|MD5|MD5-CRYPT|PBKDF2|PLAIN|PLAIN-MD4|PLAIN-MD5|PLAIN-TRUNC|PLAIN-TRUNC|SHA|SHA1|SHA256|SHA256-CRYPT|SHA512|SHA512-CRYPT|SMD5|SSHA|SSHA256|SSHA512)}/i', $password)) {
$password_hashed = $password;
}
else {
$password_hashed = hash_password($password);
}
$valid_domains = 0;
foreach ($domains as $domain) {
if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) {
@ -112,15 +104,6 @@ function domain_admin($_action, $_data = null) {
':active' => $active
));
}
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'password_empty'
);
return false;
}
$stmt = $pdo->prepare("INSERT INTO `da_acl` (`username`) VALUES (:username)");
$stmt->execute(array(
':username' => $username
@ -219,23 +202,16 @@ function domain_admin($_action, $_data = null) {
}
}
if (!empty($password) && !empty($password2)) {
if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'password_complexity'
);
continue;
if (password_check($password, $password2) !== true) {
return false;
}
if ($password != $password2) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'password_mismatch'
);
continue;
// support pre hashed passwords
if (preg_match('/^{(ARGON2I|ARGON2ID|BLF-CRYPT|CLEAR|CLEARTEXT|CRYPT|DES-CRYPT|LDAP-MD5|MD5|MD5-CRYPT|PBKDF2|PLAIN|PLAIN-MD4|PLAIN-MD5|PLAIN-TRUNC|PLAIN-TRUNC|SHA|SHA1|SHA256|SHA256-CRYPT|SHA512|SHA512-CRYPT|SMD5|SSHA|SSHA256|SSHA512)}/i', $password)) {
$password_hashed = $password;
}
else {
$password_hashed = hash_password($password);
}
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
$stmt->execute(array(
':password_hashed' => $password_hashed,
@ -296,21 +272,7 @@ function domain_admin($_action, $_data = null) {
);
return false;
}
if (!empty($password_new2) && !empty($password_new)) {
if ($password_new2 != $password_new) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'password_mismatch'
);
return false;
}
if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password_new)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'password_complexity'
);
if (password_check($password_new, $password_new2) !== true) {
return false;
}
$password_hashed = hash_password($password_new);
@ -319,7 +281,6 @@ function domain_admin($_action, $_data = null) {
':password_hashed' => $password_hashed,
':username' => $username
));
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data_log),

View File

@ -114,6 +114,132 @@ function hash_password($password) {
}
return $pw_hash;
}
function password_complexity($_action, $_data = null) {
global $redis;
global $lang;
switch ($_action) {
case 'edit':
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => 'access_denied'
);
return false;
}
$is_now = password_complexity('get');
if (!empty($is_now)) {
$length = (isset($_data['length']) && intval($_data['length']) >= 3) ? intval($_data['length']) : $is_now['length'];
$chars = (isset($_data['chars'])) ? intval($_data['chars']) : $is_now['chars'];
$lowerupper = (isset($_data['lowerupper'])) ? intval($_data['lowerupper']) : $is_now['lowerupper'];
$special_chars = (isset($_data['special_chars'])) ? intval($_data['special_chars']) : $is_now['special_chars'];
$numbers = (isset($_data['numbers'])) ? intval($_data['numbers']) : $is_now['numbers'];
}
try {
$redis->hMSet('PASSWD_POLICY', [
'length' => $length,
'chars' => $chars,
'special_chars' => $special_chars,
'lowerupper' => $lowerupper,
'numbers' => $numbers
]);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('redis_error', $e)
);
return false;
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => print_r($_data, true)
);
break;
case 'get':
try {
$length = $redis->hGet('PASSWD_POLICY', 'length');
$chars = $redis->hGet('PASSWD_POLICY', 'chars');
$special_chars = $redis->hGet('PASSWD_POLICY', 'special_chars');
$lowerupper = $redis->hGet('PASSWD_POLICY', 'lowerupper');
$numbers = $redis->hGet('PASSWD_POLICY', 'numbers');
return array(
'length' => $length,
'chars' => $chars,
'special_chars' => $special_chars,
'lowerupper' => $lowerupper,
'numbers' => $numbers
);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('redis_error', $e)
);
return false;
}
return false;
break;
case 'html':
$policies = password_complexity('get');
foreach ($policies as $name => $value) {
if ($value != 0) {
$policy_text[] = sprintf($lang['admin']["password_policy_$name"], $value);
}
}
return '<p class="help-block small">- ' . implode('<br>- ', $policy_text) . '</p>';
break;
}
}
function password_check($password1, $password2) {
$password_complexity = password_complexity('get');
if (empty($password1) || empty($password2)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type),
'msg' => 'password_complexity'
);
return false;
}
if ($password1 != $password2) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type),
'msg' => 'password_mismatch'
);
return false;
}
$given_password['length'] = strlen($password1);
$given_password['special_chars'] = preg_match('/[^a-zA-Z\d]/', $password1);
$given_password['chars'] = preg_match('/[a-zA-Z]/',$password1);
$given_password['numbers'] = preg_match('/\d/', $password1);
$lower = strlen(preg_replace("/[^a-z]/", '', $password1));
$upper = strlen(preg_replace("/[^A-Z]/", '', $password1));
$given_password['lowerupper'] = ($lower > 0 && $upper > 0) ? true : false;
if (
($given_password['length'] < $password_complexity['length']) ||
($password_complexity['special_chars'] == 1 && (intval($given_password['special_chars']) != $password_complexity['special_chars'])) ||
($password_complexity['chars'] == 1 && (intval($given_password['chars']) != $password_complexity['chars'])) ||
($password_complexity['numbers'] == 1 && (intval($given_password['numbers']) != $password_complexity['numbers'])) ||
($password_complexity['lowerupper'] == 1 && (intval($given_password['lowerupper']) != $password_complexity['lowerupper']))
) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type),
'msg' => 'password_complexity'
);
return false;
}
return true;
}
function last_login($user) {
global $pdo;
$stmt = $pdo->prepare('SELECT `remote`, `time` FROM `logs`

View File

@ -1045,32 +1045,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
return false;
}
if (!empty($password) && !empty($password2)) {
if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'password_complexity'
);
if (password_check($password, $password2) !== true) {
return false;
}
if ($password != $password2) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'password_mismatch'
);
return false;
}
$password_hashed = hash_password($password);
// support pre hashed passwords
if (preg_match('/^{(ARGON2I|ARGON2ID|BLF-CRYPT|CLEAR|CLEARTEXT|CRYPT|DES-CRYPT|LDAP-MD5|MD5|MD5-CRYPT|PBKDF2|PLAIN|PLAIN-MD4|PLAIN-MD5|PLAIN-TRUNC|PLAIN-TRUNC|SHA|SHA1|SHA256|SHA256-CRYPT|SHA512|SHA512-CRYPT|SMD5|SSHA|SSHA256|SSHA512)}/i', $password)) {
$password_hashed = $password;
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'password_empty'
);
return false;
$password_hashed = hash_password($password);
}
if ($MailboxData['count'] >= $DomainData['mailboxes']) {
$_SESSION['return'][] = array(
@ -2599,21 +2582,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
));
}
}
if (!empty($password) && !empty($password2)) {
if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'password_complexity'
);
continue;
}
if ($password != $password2) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'password_mismatch'
);
if (password_check($password, $password2) !== true) {
continue;
}
// support pre hashed passwords
@ -2630,7 +2599,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':password_hashed' => $password_hashed,
':username' => $username
));
}
// We could either set alias = 1 if alias = 2 or tune the Postfix alias table (that's what we did, TODO: to it the other way)
$stmt = $pdo->prepare("UPDATE `alias` SET
`active` = :active

View File

@ -90,14 +90,6 @@ $AVAILABLE_LANGUAGES = array('ca', 'cs', 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu
// WARNING: Only lumen is loaded locally. Enabling any other theme, will download external sources.
$DEFAULT_THEME = 'lumen';
// Password complexity as regular expression
// Min. 6 characters
$PASSWD_REGEP = '.{6,}';
// Min. 6 characters, which must include at least one uppercase letter, one lowercase letter and one number
// $PASSWD_REGEP = '^(?=.*[A-Z])(?=.*[0-9])(?=.*[a-z]).{6,}$';
// Min. 6 characters, which must include at least one letter and one number
// $PASSWD_REGEP = '^(?=.*[0-9])(?=.*[A-Za-z]).{6,}$';
// Show DKIM private keys - false by default
$SHOW_DKIM_PRIV_KEYS = false;

View File

@ -95,7 +95,14 @@ $(document).ready(function() {
// selectpicker
$('select').selectpicker();
// haveibeenpwned?
// haveibeenpwned and passwd policy
$.ajax({
url: '/api/v1/get/passwordpolicy/html',
type: 'GET',
success: function(res) {
$(".hibp-out").after(res);
}
});
$('[data-hibp]').after('<p class="small haveibeenpwned">↪ Check against haveibeenpwned.com</p><span class="hibp-out"></span>');
$('[data-hibp]').on('input', function() {
out_field = $(this).next('.haveibeenpwned').next('.hibp-out').text('').attr('class', 'hibp-out');

File diff suppressed because it is too large Load Diff

View File

@ -446,6 +446,20 @@ if (isset($_GET['query'])) {
}
break;
case "passwordpolicy":
switch ($object) {
case "html":
$password_complexity_rules = password_complexity('html');
if ($password_complexity_rules !== false) {
process_get_return($password_complexity_rules);
}
else {
echo '{}';
}
break;
}
break;
case "app-passwd":
switch ($object) {
case "all":
@ -1561,6 +1575,9 @@ if (isset($_GET['query'])) {
case "app_links":
process_edit_return(customize('edit', 'app_links', $attr));
break;
case "passwordpolicy":
process_edit_return(password_complexity('edit', $attr));
break;
case "relayhost":
process_edit_return(relayhost('edit', array_merge(array('id' => $items), $attr)));
break;

View File

@ -234,6 +234,13 @@
"oauth2_revoke_tokens": "Alle Client Tokens entfernen",
"optional": "Optional",
"password": "Passwort",
"password_length": "Passwortlänge",
"password_policy": "Passwortrichtlinie",
"password_policy_chars": "Muss ein alphabetisches Zeichen enthalten",
"password_policy_length": "Mindestlänge des Passwortes ist %d Zeichen",
"password_policy_numbers": "Muss eine Ziffer enthalten",
"password_policy_lowerupper": "Muss Großbuchstaben und Kleinbuchstaben enthalten",
"password_policy_special_chars": "Muss Sonderzeichen enthalten",
"password_repeat": "Passwort wiederholen",
"priority": "Gewichtung",
"private_key": "Private Key",
@ -907,6 +914,7 @@
"object_modified": "Änderungen an Objekt %s wurden gespeichert",
"pushover_settings_edited": "Pushover Konfiguration gespeichert, bitte den Zugang im Anschluss verifizieren.",
"qlearn_spam": "Nachricht ID %s wurde als Spam gelernt und gelöscht",
"password_policy_saved": "Passwortrichtlinie wurde erfolgreich gespeichert",
"queue_command_success": "Queue-Aufgabe erfolgreich ausgeführt",
"recipient_map_entry_deleted": "Empfängerumschreibung mit der ID %s wurde gelöscht",
"recipient_map_entry_saved": "Empfängerumschreibung für Objekt \"%s\" wurde gespeichert",
@ -1027,7 +1035,6 @@
"messages": "Nachrichten",
"never": "Niemals",
"new_password": "Neues Passwort",
"new_password_description": "Mindestanforderung: 6 Zeichen lang, Buchstaben und Zahlen.",
"new_password_repeat": "Neues Passwort (Wiederholung)",
"no_active_filter": "Kein aktiver Filter vorhanden",
"no_last_login": "Keine letzte UI-Anmeldung gespeichert",

View File

@ -232,6 +232,13 @@
"oauth2_revoke_tokens": "Revoke all client tokens",
"optional": "optional",
"password": "Password",
"password_length": "Password length",
"password_policy": "Password policy",
"password_policy_chars": "Must contain at least one alphabetic character",
"password_policy_length": "Minimum password length is %d",
"password_policy_numbers": "Must contain at least one number",
"password_policy_lowerupper": "Must contain lowercase and uppercase characters",
"password_policy_special_chars": "Must contain special chars",
"password_repeat": "Confirmation password (repeat)",
"priority": "Priority",
"private_key": "Private key",
@ -906,6 +913,7 @@
"pushover_settings_edited": "Pushover settings successfully set, please verify credentials.",
"qlearn_spam": "Message ID %s was learned as spam and deleted",
"queue_command_success": "Queue command completed successfully",
"password_policy_saved": "Password policy was saved successfully",
"recipient_map_entry_deleted": "Recipient map ID %s has been deleted",
"recipient_map_entry_saved": "Recipient map entry \"%s\" has been saved",
"relayhost_added": "Map entry %s has been added",
@ -1025,7 +1033,6 @@
"messages": "messages",
"never": "Never",
"new_password": "New password",
"new_password_description": "Requirement: 6 characters long, letters and numbers.",
"new_password_repeat": "Confirmation password (repeat)",
"no_active_filter": "No active filter available",
"no_last_login": "No last UI login information",