OCA/server-tools

View on GitHub
password_security/models/res_users.py

Summary

Maintainability
A
1 hr
Test Coverage
# -*- coding: utf-8 -*-
# Copyright 2015 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).

import re

from datetime import datetime, timedelta

from openerp 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',
        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)

    @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 password_match_message(self):
        self.ensure_one()
        company_id = self.company_id
        message = []
        if company_id.password_lower:
            message.append('* ' + _('Lowercase letter'))
        if company_id.password_upper:
            message.append('* ' + _('Uppercase letter'))
        if company_id.password_numeric:
            message.append('* ' + _('Numeric digit'))
        if company_id.password_special:
            message.append('* ' + _('Special character'))
        if len(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)

    @api.multi
    def check_password(self, password):
        self.ensure_one()
        if not password:
            return True
        company_id = self.company_id
        password_regex = ['^']
        if company_id.password_lower:
            password_regex.append('(?=.*?[a-z])')
        if company_id.password_upper:
            password_regex.append('(?=.*?[A-Z])')
        if company_id.password_numeric:
            password_regex.append(r'(?=.*?\d)')
        if company_id.password_special:
            password_regex.append(r'(?=.*?[\W_])')
        password_regex.append('.{%d,}$' % company_id.password_length)
        if not re.search(''.join(password_regex), password):
            raise PassError(_(self.password_match_message()))
        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 _set_password(self, password):
        """ It validates proposed password against existing history
        :raises: PassError on reused password
        """
        crypt = self._crypt_context()[0]
        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 len(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
                )
        super(ResUsers, self)._set_password(password)

    @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,
            })],
        })