flectra/addons/password_security/models/res_users.py

163 lines
5.9 KiB
Python

# Copyright 2015 LasLabs Inc.
# Copyright 2004-TODAY FlectraHQ.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import re
from datetime import datetime, timedelta
from flectra import api, fields, models, _
from ..exceptions import PassError
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',
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)
@api.multi
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.multi
def _check_password(self, password):
self._check_password_rules(password)
self._check_password_history(password)
return True
@api.multi
def _check_password_rules(self, password):
self.ensure_one()
if not password:
return True
company_id = self.company_id
message = []
if company_id.password_lower and sum(map(str.islower, password)) < \
company_id.password_lower:
message.append('\n ' + _('Lowercase letter (At least ' +
str(company_id.password_lower) +
' character)')
)
if company_id.password_upper and sum(map(str.isupper, password)) < \
company_id.password_upper:
message.append('\n ' + _('Uppercase letter (At least ' +
str(company_id.password_upper) +
' character)')
)
if company_id.password_numeric and sum(map(str.isdigit, password)) < \
company_id.password_numeric:
message.append('\n ' + _('Numeric digit (At least ' +
str(company_id.password_numeric) +
' numeric)')
)
if company_id.password_special and len(set('[~!@#$%^&*()_+{}":;\']+$'
).intersection(
password)) < company_id.password_numeric:
message.append('\n ' + _('Special character (At least ' +
str(company_id.password_special) +
' character of [ ~ ! @ # $ % ^ & * ( )_+ {'
' } " : ; \' ])')
)
if company_id.password_length and len(password) < \
company_id.password_length:
message = [_('Password must be %d characters or more.') %
company_id.password_length] + message
if len(message) > 0:
raise PassError('\r'.join(message))
else:
return True
@api.multi
def _password_has_expired(self):
self.ensure_one()
if not self.password_write_date:
return True
write_date = fields.Datetime.from_string(self.password_write_date)
today = fields.Datetime.from_string(fields.Datetime.now())
days = (today - write_date).days
return days > self.company_id.password_expiration
@api.multi
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
)
@api.multi
def _validate_pass_reset(self):
""" It provides validations before initiating a pass reset email
:raises: PassError 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 = fields.Datetime.from_string(
rec_id.password_write_date
)
delta = timedelta(hours=pass_min)
if write_date + delta > datetime.now():
raise PassError(
_('Passwords can only be reset every %d hour(s). '
'Please contact an administrator for assistance.') %
pass_min,
)
return True
@api.multi
def _check_password_history(self, password):
""" It validates proposed password against existing history
:raises: PassError 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 PassError(
_('Cannot use the most recent %d passwords') %
rec_id.company_id.password_history
)
@api.multi
def _set_encrypted_password(self, encrypted):
""" It saves password crypt history for history rules """
super(ResUsers, self)._set_encrypted_password(encrypted)
self.write({
'password_history_ids': [(0, 0, {
'password_crypt': encrypted,
})],
})