密码策略模块测试
This commit is contained in:
4
password_security/models/__init__.py
Normal file
4
password_security/models/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# Copyright 2015 LasLabs Inc.
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
|
||||
from . import res_company, res_config_settings, res_users, res_users_pass_history
|
||||
62
password_security/models/res_company.py
Normal file
62
password_security/models/res_company.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# Copyright 2016 LasLabs Inc.
|
||||
# Copyright 2017 Kaushal Prajapati <kbprajapati@live.com>.
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = "res.company"
|
||||
|
||||
password_expiration = fields.Integer(
|
||||
"Days",
|
||||
default=60,
|
||||
help="How many days until passwords expire",
|
||||
)
|
||||
password_length = fields.Integer(
|
||||
"Characters",
|
||||
default=12,
|
||||
help="Minimum number of characters",
|
||||
)
|
||||
password_lower = fields.Integer(
|
||||
"Lowercase",
|
||||
default=1,
|
||||
help="Require number of lowercase letters",
|
||||
)
|
||||
password_upper = fields.Integer(
|
||||
"Uppercase",
|
||||
default=1,
|
||||
help="Require number of uppercase letters",
|
||||
)
|
||||
password_numeric = fields.Integer(
|
||||
"Numeric",
|
||||
default=1,
|
||||
help="Require number of numeric digits",
|
||||
)
|
||||
password_special = fields.Integer(
|
||||
"Special",
|
||||
default=1,
|
||||
help="Require number of unique special characters",
|
||||
)
|
||||
password_estimate = fields.Integer(
|
||||
"Estimation",
|
||||
default=3,
|
||||
help="Required score for the strength estimation. Between 0 and 4",
|
||||
)
|
||||
password_history = fields.Integer(
|
||||
"History",
|
||||
default=30,
|
||||
help="Disallow reuse of this many previous passwords - use negative "
|
||||
"number for infinite, or 0 to disable",
|
||||
)
|
||||
password_minimum = fields.Integer(
|
||||
"Minimum Hours",
|
||||
default=24,
|
||||
help="Amount of hours until a user may change password again",
|
||||
)
|
||||
|
||||
@api.constrains("password_estimate")
|
||||
def _check_password_estimate(self):
|
||||
if 0 > self.password_estimate > 4:
|
||||
raise ValidationError(_("The estimation must be between 0 and 4."))
|
||||
31
password_security/models/res_config_settings.py
Normal file
31
password_security/models/res_config_settings.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# Copyright 2018 Modoolar <info@modoolar.com>
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = "res.config.settings"
|
||||
|
||||
password_expiration = fields.Integer(
|
||||
related="company_id.password_expiration", readonly=False
|
||||
)
|
||||
password_minimum = fields.Integer(
|
||||
related="company_id.password_minimum", readonly=False
|
||||
)
|
||||
password_history = fields.Integer(
|
||||
related="company_id.password_history", readonly=False
|
||||
)
|
||||
password_length = fields.Integer(
|
||||
related="company_id.password_length", readonly=False
|
||||
)
|
||||
password_lower = fields.Integer(related="company_id.password_lower", readonly=False)
|
||||
password_upper = fields.Integer(related="company_id.password_upper", readonly=False)
|
||||
password_numeric = fields.Integer(
|
||||
related="company_id.password_numeric", readonly=False
|
||||
)
|
||||
password_special = fields.Integer(
|
||||
related="company_id.password_special", readonly=False
|
||||
)
|
||||
password_estimate = fields.Integer(
|
||||
related="company_id.password_estimate", readonly=False
|
||||
)
|
||||
207
password_security/models/res_users.py
Normal file
207
password_security/models/res_users.py
Normal file
@@ -0,0 +1,207 @@
|
||||
# Copyright 2016 LasLabs Inc.
|
||||
# Copyright 2017 Kaushal Prajapati <kbprajapati@live.com>.
|
||||
# Copyright 2018 Modoolar <info@modoolar.com>.
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
try:
|
||||
import zxcvbn
|
||||
|
||||
zxcvbn.feedback._ = _
|
||||
except ImportError:
|
||||
_logger.debug(
|
||||
"Could not import zxcvbn. Please make sure this library is available"
|
||||
" in your environment."
|
||||
)
|
||||
|
||||
|
||||
def delta_now(**kwargs):
|
||||
dt = datetime.now() + timedelta(**kwargs)
|
||||
return fields.Datetime.to_string(dt)
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
_inherit = "res.users"
|
||||
|
||||
password_write_date = fields.Datetime(
|
||||
"Last password update", default=fields.Datetime.now, readonly=True
|
||||
)
|
||||
password_history_ids = fields.One2many(
|
||||
string="Password History",
|
||||
comodel_name="res.users.pass.history",
|
||||
inverse_name="user_id",
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
vals["password_write_date"] = fields.Datetime.now()
|
||||
return super(ResUsers, self).create(vals)
|
||||
|
||||
def write(self, vals):
|
||||
if vals.get("password"):
|
||||
self._check_password(vals["password"])
|
||||
vals["password_write_date"] = fields.Datetime.now()
|
||||
return super(ResUsers, self).write(vals)
|
||||
|
||||
@api.model
|
||||
def get_password_policy(self):
|
||||
data = super(ResUsers, self).get_password_policy()
|
||||
company_id = self.env.user.company_id
|
||||
data.update(
|
||||
{
|
||||
"password_lower": company_id.password_lower,
|
||||
"password_upper": company_id.password_upper,
|
||||
"password_numeric": company_id.password_numeric,
|
||||
"password_special": company_id.password_special,
|
||||
"password_length": company_id.password_length,
|
||||
"password_estimate": company_id.password_estimate,
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
def _check_password_policy(self, passwords):
|
||||
result = super(ResUsers, self)._check_password_policy(passwords)
|
||||
|
||||
for password in passwords:
|
||||
if not password:
|
||||
continue
|
||||
self._check_password(password)
|
||||
|
||||
return result
|
||||
|
||||
@api.model
|
||||
def get_estimation(self, password):
|
||||
return zxcvbn.zxcvbn(password)
|
||||
|
||||
def password_match_message(self):
|
||||
self.ensure_one()
|
||||
company_id = self.company_id
|
||||
message = []
|
||||
if company_id.password_lower:
|
||||
message.append(
|
||||
_("\n* Lowercase letter (at least %s characters)")
|
||||
% str(company_id.password_lower)
|
||||
)
|
||||
if company_id.password_upper:
|
||||
message.append(
|
||||
_("\n* Uppercase letter (at least %s characters)")
|
||||
% str(company_id.password_upper)
|
||||
)
|
||||
if company_id.password_numeric:
|
||||
message.append(
|
||||
_("\n* Numeric digit (at least %s characters)")
|
||||
% str(company_id.password_numeric)
|
||||
)
|
||||
if company_id.password_special:
|
||||
message.append(
|
||||
_("\n* Special character (at least %s characters)")
|
||||
% str(company_id.password_special)
|
||||
)
|
||||
if message:
|
||||
message = [_("Must contain the following:")] + message
|
||||
if company_id.password_length:
|
||||
message = [
|
||||
_("Password must be %d characters or more.")
|
||||
% company_id.password_length
|
||||
] + message
|
||||
return "\r".join(message)
|
||||
|
||||
def _check_password(self, password):
|
||||
self._check_password_rules(password)
|
||||
self._check_password_history(password)
|
||||
return True
|
||||
|
||||
def _check_password_rules(self, password):
|
||||
self.ensure_one()
|
||||
if not password:
|
||||
return True
|
||||
company_id = self.company_id
|
||||
password_regex = [
|
||||
"^",
|
||||
"(?=.*?[a-z]){" + str(company_id.password_lower) + ",}",
|
||||
"(?=.*?[A-Z]){" + str(company_id.password_upper) + ",}",
|
||||
"(?=.*?\\d){" + str(company_id.password_numeric) + ",}",
|
||||
r"(?=.*?[\W_]){" + str(company_id.password_special) + ",}",
|
||||
".{%d,}$" % int(company_id.password_length),
|
||||
]
|
||||
if not re.search("".join(password_regex), password):
|
||||
raise ValidationError(self.password_match_message())
|
||||
|
||||
estimation = self.get_estimation(password)
|
||||
if estimation["score"] < company_id.password_estimate:
|
||||
raise UserError(estimation["feedback"]["warning"])
|
||||
|
||||
return True
|
||||
|
||||
def _password_has_expired(self):
|
||||
self.ensure_one()
|
||||
if not self.password_write_date:
|
||||
return True
|
||||
|
||||
if not self.company_id.password_expiration:
|
||||
return False
|
||||
|
||||
days = (fields.Datetime.now() - self.password_write_date).days
|
||||
return days > self.company_id.password_expiration
|
||||
|
||||
def action_expire_password(self):
|
||||
expiration = delta_now(days=+1)
|
||||
for rec_id in self:
|
||||
rec_id.mapped("partner_id").signup_prepare(
|
||||
signup_type="reset", expiration=expiration
|
||||
)
|
||||
|
||||
def _validate_pass_reset(self):
|
||||
"""It provides validations before initiating a pass reset email
|
||||
:raises: UserError on invalidated pass reset attempt
|
||||
:return: True on allowed reset
|
||||
"""
|
||||
for rec_id in self:
|
||||
pass_min = rec_id.company_id.password_minimum
|
||||
if pass_min <= 0:
|
||||
pass
|
||||
write_date = rec_id.password_write_date
|
||||
delta = timedelta(hours=pass_min)
|
||||
if write_date + delta > datetime.now():
|
||||
raise UserError(
|
||||
_(
|
||||
"Passwords can only be reset every %d hour(s). "
|
||||
"Please contact an administrator for assistance."
|
||||
)
|
||||
% pass_min
|
||||
)
|
||||
return True
|
||||
|
||||
def _check_password_history(self, password):
|
||||
"""It validates proposed password against existing history
|
||||
:raises: UserError on reused password
|
||||
"""
|
||||
crypt = self._crypt_context()
|
||||
for rec_id in self:
|
||||
recent_passes = rec_id.company_id.password_history
|
||||
if recent_passes < 0:
|
||||
recent_passes = rec_id.password_history_ids
|
||||
else:
|
||||
recent_passes = rec_id.password_history_ids[0 : recent_passes - 1]
|
||||
if recent_passes.filtered(
|
||||
lambda r: crypt.verify(password, r.password_crypt)
|
||||
):
|
||||
raise UserError(
|
||||
_("Cannot use the most recent %d passwords")
|
||||
% rec_id.company_id.password_history
|
||||
)
|
||||
|
||||
def _set_encrypted_password(self, uid, pw):
|
||||
"""It saves password crypt history for history rules"""
|
||||
res = super(ResUsers, self)._set_encrypted_password(uid, pw)
|
||||
|
||||
self.write({"password_history_ids": [(0, 0, {"password_crypt": pw})]})
|
||||
return res
|
||||
25
password_security/models/res_users_pass_history.py
Normal file
25
password_security/models/res_users_pass_history.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Copyright 2016 LasLabs Inc.
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResUsersPassHistory(models.Model):
|
||||
_name = "res.users.pass.history"
|
||||
_description = "Res Users Password History"
|
||||
|
||||
_order = "user_id, date desc"
|
||||
|
||||
user_id = fields.Many2one(
|
||||
string="User",
|
||||
comodel_name="res.users",
|
||||
ondelete="cascade",
|
||||
index=True,
|
||||
)
|
||||
password_crypt = fields.Char(
|
||||
string="Encrypted Password",
|
||||
)
|
||||
date = fields.Datetime(
|
||||
default=lambda s: fields.Datetime.now(),
|
||||
index=True,
|
||||
)
|
||||
Reference in New Issue
Block a user