[ADD] password_security: Thanks to LasLabs, Odoo Community Association (OCA) for this module. As we are not using the module as it is, we did some required changes in order to improve this module to next level.

So, Now with the flectra password_security is part of core addons.
This commit is contained in:
Kaushal Prajapati 2017-10-24 17:43:16 +05:30 committed by Siddharth Bhalgami
parent ec66961de6
commit 78e64b594d
19 changed files with 1016 additions and 0 deletions

View File

@ -0,0 +1,63 @@
.. image:: https://img.shields.io/badge/license-LGPL--3-blue.svg
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
=================
Password Security
=================
This module allows admin to set company-level password security requirements
and enforces them on the user.
It contains features such as
* Password expiration days
* Password length requirement
* Password minimum number of lowercase letters
* Password minimum number of uppercase letters
* Password minimum number of numbers
* Password minimum number of special characters
Configuration
=============
# Navigate to company you would like to set requirements on
# Click the ``Password Policy`` page
# Set the policies to your liking.
Password complexity requirements will be enforced upon next password change for
any user in that company.
Settings & Defaults
-------------------
These are defined at the company level:
===================== ======= ===================================================
Name Default Description
===================== ======= ===================================================
password_expiration 60 Days until passwords expire
password_length 12 Minimum number of characters in password
password_lower 0 Minimum number of lowercase letter in password
password_upper 0 Minimum number of uppercase letters in password
password_numeric 0 Minimum number of number in password
password_special 0 Minimum number of unique special character in password
password_history 30 Disallow reuse of this many previous passwords
password_minimum 24 Amount of hours that must pass until another reset
===================== ======= ===================================================
Credits
=======
Images
------
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
Contributors
------------
* James Foster <jfoster@laslabs.com>
* Dave Lasley <dave@laslabs.com>

View File

@ -0,0 +1,5 @@
# Copyright 2015 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from . import controllers
from . import models

View File

@ -0,0 +1,25 @@
# Copyright 2015 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
{
'name': 'Password Security',
"summary": "Allow admin to set password security requirements.",
'version': '11.0.1.0.0',
'author': "LasLabs, Odoo Community Association (OCA), FlectraHQ",
'category': 'Base',
'depends': [
'auth_crypt',
'auth_signup',
],
"website": "https://laslabs.com",
"license": "LGPL-3",
"data": [
'views/res_company_view.xml',
'security/ir.model.access.csv',
'security/res_users_pass_history.xml',
],
"demo": [
'demo/res_users.xml',
],
'installable': True,
}

View File

@ -0,0 +1,4 @@
# Copyright 2015 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from . import main

View File

@ -0,0 +1,93 @@
# Copyright 2015 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import operator
from flectra import http
from flectra.http import request
from flectra.addons.auth_signup.controllers.main import AuthSignupHome
from flectra.addons.web.controllers.main import ensure_db, Session
from ..exceptions import PassError
class PasswordSecuritySession(Session):
@http.route()
def change_password(self, fields):
new_password = operator.itemgetter('new_password')(
dict(map(operator.itemgetter('name', 'value'), fields))
)
user_id = request.env.user
user_id._check_password(new_password)
return super(PasswordSecuritySession, self).change_password(fields)
class PasswordSecurityHome(AuthSignupHome):
def do_signup(self, qcontext):
password = qcontext.get('password')
user_id = request.env.user
user_id._check_password(password)
return super(PasswordSecurityHome, self).do_signup(qcontext)
@http.route()
def web_login(self, *args, **kw):
ensure_db()
response = super(PasswordSecurityHome, self).web_login(*args, **kw)
if not request.httprequest.method == 'POST':
return response
uid = request.session.authenticate(
request.session.db,
request.params['login'],
request.params['password']
)
if not uid:
return response
users_obj = request.env['res.users'].sudo()
user_id = users_obj.browse(request.uid)
if not user_id._password_has_expired():
return response
user_id.action_expire_password()
request.session.logout(keep_db=True)
redirect = user_id.partner_id.signup_url
return http.redirect_with_hash(redirect)
@http.route()
def web_auth_signup(self, *args, **kw):
try:
return super(PasswordSecurityHome, self).web_auth_signup(
*args, **kw
)
except PassError as e:
qcontext = self.get_auth_signup_qcontext()
qcontext['error'] = e.message
return request.render('auth_signup.signup', qcontext)
@http.route()
def web_auth_reset_password(self, *args, **kw):
""" It provides hook to disallow front-facing resets inside of min
Unfortuantely had to reimplement some core logic here because of
nested logic in parent
"""
qcontext = self.get_auth_signup_qcontext()
if (
request.httprequest.method == 'POST' and
qcontext.get('login') and
'error' not in qcontext and
'token' not in qcontext
):
login = qcontext.get('login')
user_ids = request.env.sudo().search(
[('login', '=', login)],
limit=1,
)
if not user_ids:
user_ids = request.env.sudo().search(
[('email', '=', login)],
limit=1,
)
user_ids._validate_pass_reset()
return super(PasswordSecurityHome, self).web_auth_reset_password(
*args, **kw
)

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2016 LasLabs Inc.
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<flectra>
<record id="base.user_root" model="res.users">
<field name="password_write_date"
eval="datetime.now()"
/>
</record>
</flectra>

View File

@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
# Copyright 2015 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from flectra.exceptions import Warning as UserError
class PassError(UserError):
""" Example: When you try to create an insecure password."""
def __init__(self, msg):
self.message = msg
super(PassError, self).__init__(msg)

View File

@ -0,0 +1,6 @@
# Copyright 2015 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from . import res_users
from . import res_company
from . import res_users_pass_history

View File

@ -0,0 +1,47 @@
# Copyright 2015 LasLabs Inc.
# Copyright 2004-TODAY FlectraHQ.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from flectra import models, fields
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',
help='Require lowercase letters',
)
password_upper = fields.Integer(
'Uppercase',
help='Require uppercase letters',
)
password_numeric = fields.Integer(
'Numeric',
help='Require numeric digits',
)
password_special = fields.Integer(
'Special',
help='Require unique special characters',
)
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',
)

View File

@ -0,0 +1,162 @@
# 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,
})],
})

View File

@ -0,0 +1,25 @@
# Copyright 2016 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from flectra 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,
)

View File

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_res_users_pass_history,access_res_users_pass_history,model_res_users_pass_history,base.group_user,1,0,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_res_users_pass_history access_res_users_pass_history model_res_users_pass_history base.group_user 1 0 1 0

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2016 LasLabs Inc.
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<flectra>
<record id="res_users_pass_history_rule" model="ir.rule">
<field name="name">Res Users Pass History Access</field>
<field name="model_id"
ref="password_security.model_res_users_pass_history"/>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="domain_force">[
('user_id', '=', user.id)
]</field>
</record>
</flectra>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright 2015 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from . import test_res_users
from . import test_password_security_home
from . import test_password_security_session

View File

@ -0,0 +1,281 @@
# -*- coding: utf-8 -*-
# Copyright 2016 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import mock
from contextlib import contextmanager
from flectra.tests.common import TransactionCase
from flectra.http import Response
from ..controllers import main
IMPORT = 'flectra.addons.password_security.controllers.main'
class EndTestException(Exception):
""" It allows for isolation of resources by raise """
class MockResponse(object):
def __new__(cls):
return mock.Mock(spec=Response)
class MockPassError(main.PassError):
def __init__(self):
super(MockPassError, self).__init__('Message')
class TestPasswordSecurityHome(TransactionCase):
def setUp(self):
super(TestPasswordSecurityHome, self).setUp()
self.PasswordSecurityHome = main.PasswordSecurityHome
self.password_security_home = self.PasswordSecurityHome()
self.passwd = 'I am a password!'
self.qcontext = {
'password': self.passwd,
}
@contextmanager
def mock_assets(self):
""" It mocks and returns assets used by this controller """
methods = ['do_signup', 'web_login', 'web_auth_signup',
'web_auth_reset_password',
]
with mock.patch.multiple(
main.AuthSignupHome, **{m: mock.DEFAULT for m in methods}
) as _super:
mocks = {}
for method in methods:
mocks[method] = _super[method]
mocks[method].return_value = MockResponse()
with mock.patch('%s.request' % IMPORT) as request:
with mock.patch('%s.ensure_db' % IMPORT) as ensure:
with mock.patch('%s.http' % IMPORT) as http:
http.redirect_with_hash.return_value = \
MockResponse()
mocks.update({
'request': request,
'ensure_db': ensure,
'http': http,
})
yield mocks
def test_do_signup_check(self):
""" It should check password on user """
with self.mock_assets() as assets:
check_password = assets['request'].env.user._check_password
check_password.side_effect = EndTestException
with self.assertRaises(EndTestException):
self.password_security_home.do_signup(self.qcontext)
check_password.assert_called_once_with(
self.passwd,
)
def test_do_signup_return(self):
""" It should return result of super """
with self.mock_assets() as assets:
res = self.password_security_home.do_signup(self.qcontext)
self.assertEqual(assets['do_signup'](), res)
def test_web_login_ensure_db(self):
""" It should verify available db """
with self.mock_assets() as assets:
assets['ensure_db'].side_effect = EndTestException
with self.assertRaises(EndTestException):
self.password_security_home.web_login()
def test_web_login_super(self):
""" It should call superclass w/ proper args """
expect_list = [1, 2, 3]
expect_dict = {'test1': 'good1', 'test2': 'good2'}
with self.mock_assets() as assets:
assets['web_login'].side_effect = EndTestException
with self.assertRaises(EndTestException):
self.password_security_home.web_login(
*expect_list, **expect_dict
)
assets['web_login'].assert_called_once_with(
*expect_list, **expect_dict
)
def test_web_login_no_post(self):
""" It should return immediate result of super when not POST """
with self.mock_assets() as assets:
assets['request'].httprequest.method = 'GET'
assets['request'].session.authenticate.side_effect = \
EndTestException
res = self.password_security_home.web_login()
self.assertEqual(
assets['web_login'](), res,
)
def test_web_login_authenticate(self):
""" It should attempt authentication to obtain uid """
with self.mock_assets() as assets:
assets['request'].httprequest.method = 'POST'
authenticate = assets['request'].session.authenticate
request = assets['request']
authenticate.side_effect = EndTestException
with self.assertRaises(EndTestException):
self.password_security_home.web_login()
authenticate.assert_called_once_with(
request.session.db,
request.params['login'],
request.params['password'],
)
def test_web_login_authenticate_fail(self):
""" It should return super result if failed auth """
with self.mock_assets() as assets:
authenticate = assets['request'].session.authenticate
request = assets['request']
request.httprequest.method = 'POST'
request.env['res.users'].sudo.side_effect = EndTestException
authenticate.return_value = False
res = self.password_security_home.web_login()
self.assertEqual(
assets['web_login'](), res,
)
def test_web_login_get_user(self):
""" It should get the proper user as sudo """
with self.mock_assets() as assets:
request = assets['request']
request.httprequest.method = 'POST'
sudo = request.env['res.users'].sudo()
sudo.browse.side_effect = EndTestException
with self.assertRaises(EndTestException):
self.password_security_home.web_login()
sudo.browse.assert_called_once_with(
request.uid
)
def test_web_login_valid_pass(self):
""" It should return parent result if pass isn't expired """
with self.mock_assets() as assets:
request = assets['request']
request.httprequest.method = 'POST'
user = request.env['res.users'].sudo().browse()
user.action_expire_password.side_effect = EndTestException
user._password_has_expired.return_value = False
res = self.password_security_home.web_login()
self.assertEqual(
assets['web_login'](), res,
)
def test_web_login_expire_pass(self):
""" It should expire password if necessary """
with self.mock_assets() as assets:
request = assets['request']
request.httprequest.method = 'POST'
user = request.env['res.users'].sudo().browse()
user.action_expire_password.side_effect = EndTestException
user._password_has_expired.return_value = True
with self.assertRaises(EndTestException):
self.password_security_home.web_login()
def test_web_login_log_out_if_expired(self):
"""It should log out user if password expired"""
with self.mock_assets() as assets:
request = assets['request']
request.httprequest.method = 'POST'
user = request.env['res.users'].sudo().browse()
user._password_has_expired.return_value = True
self.password_security_home.web_login()
logout_mock = request.session.logout
logout_mock.assert_called_once_with(keep_db=True)
def test_web_login_redirect(self):
""" It should redirect w/ hash to reset after expiration """
with self.mock_assets() as assets:
request = assets['request']
request.httprequest.method = 'POST'
user = request.env['res.users'].sudo().browse()
user._password_has_expired.return_value = True
res = self.password_security_home.web_login()
self.assertEqual(
assets['http'].redirect_with_hash(), res,
)
def test_web_auth_signup_valid(self):
""" It should return super if no errors """
with self.mock_assets() as assets:
res = self.password_security_home.web_auth_signup()
self.assertEqual(
assets['web_auth_signup'](), res,
)
def test_web_auth_signup_invalid_qcontext(self):
""" It should catch PassError and get signup qcontext """
with self.mock_assets() as assets:
with mock.patch.object(
main.AuthSignupHome, 'get_auth_signup_qcontext',
) as qcontext:
assets['web_auth_signup'].side_effect = MockPassError
qcontext.side_effect = EndTestException
with self.assertRaises(EndTestException):
self.password_security_home.web_auth_signup()
def test_web_auth_signup_invalid_render(self):
""" It should render & return signup form on invalid """
with self.mock_assets() as assets:
with mock.patch.object(
main.AuthSignupHome, 'get_auth_signup_qcontext', spec=dict
) as qcontext:
assets['web_auth_signup'].side_effect = MockPassError
res = self.password_security_home.web_auth_signup()
assets['request'].render.assert_called_once_with(
'auth_signup.signup', qcontext(),
)
self.assertEqual(
assets['request'].render(), res,
)
def test_web_auth_reset_password_fail_login(self):
""" It should raise from failed _validate_pass_reset by login """
with self.mock_assets() as assets:
with mock.patch.object(
main.AuthSignupHome, 'get_auth_signup_qcontext', spec=dict
) as qcontext:
qcontext['login'] = 'login'
search = assets['request'].env.sudo().search
assets['request'].httprequest.method = 'POST'
user = mock.MagicMock()
user._validate_pass_reset.side_effect = MockPassError
search.return_value = user
with self.assertRaises(MockPassError):
self.password_security_home.web_auth_reset_password()
def test_web_auth_reset_password_fail_email(self):
""" It should raise from failed _validate_pass_reset by email """
with self.mock_assets() as assets:
with mock.patch.object(
main.AuthSignupHome, 'get_auth_signup_qcontext', spec=dict
) as qcontext:
qcontext['login'] = 'login'
search = assets['request'].env.sudo().search
assets['request'].httprequest.method = 'POST'
user = mock.MagicMock()
user._validate_pass_reset.side_effect = MockPassError
search.side_effect = [[], user]
with self.assertRaises(MockPassError):
self.password_security_home.web_auth_reset_password()
def test_web_auth_reset_password_success(self):
""" It should return parent response on no validate errors """
with self.mock_assets() as assets:
with mock.patch.object(
main.AuthSignupHome, 'get_auth_signup_qcontext', spec=dict
) as qcontext:
qcontext['login'] = 'login'
assets['request'].httprequest.method = 'POST'
res = self.password_security_home.web_auth_reset_password()
self.assertEqual(
assets['web_auth_reset_password'](), res,
)

View File

@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# Copyright 2016 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import mock
from contextlib import contextmanager
from flectra.tests.common import TransactionCase
from ..controllers import main
IMPORT = 'flectra.addons.password_security.controllers.main'
class EndTestException(Exception):
""" It allows for isolation of resources by raise """
class TestPasswordSecuritySession(TransactionCase):
def setUp(self):
super(TestPasswordSecuritySession, self).setUp()
self.PasswordSecuritySession = main.PasswordSecuritySession
self.password_security_session = self.PasswordSecuritySession()
self.passwd = 'I am a password!'
self.fields = [
{'name': 'new_password', 'value': self.passwd},
]
@contextmanager
def mock_assets(self):
""" It mocks and returns assets used by this controller """
with mock.patch('%s.request' % IMPORT) as request:
yield {
'request': request,
}
def test_change_password_check(self):
""" It should check password on request user """
with self.mock_assets() as assets:
check_password = assets['request'].env.user._check_password
check_password.side_effect = EndTestException
with self.assertRaises(EndTestException):
self.password_security_session.change_password(self.fields)
check_password.assert_called_once_with(
self.passwd,
)
def test_change_password_return(self):
""" It should return result of super """
with self.mock_assets():
with mock.patch.object(main.Session, 'change_password') as chg:
res = self.password_security_session.change_password(
self.fields
)
self.assertEqual(chg(), res)

View File

@ -0,0 +1,148 @@
# -*- coding: utf-8 -*-
# Copyright 2015 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import time
from flectra.tests.common import TransactionCase
from ..exceptions import PassError
class TestResUsers(TransactionCase):
def setUp(self):
super(TestResUsers, self).setUp()
self.login = 'foslabs@example.com'
self.partner_vals = {
'name': 'Partner',
'is_company': False,
'email': self.login,
}
self.password = 'asdQWE123$%^'
self.main_comp = self.env.ref('base.main_company')
self.vals = {
'name': 'User',
'login': self.login,
'password': self.password,
'company_id': self.main_comp.id
}
self.model_obj = self.env['res.users']
def _new_record(self):
partner_id = self.env['res.partner'].create(self.partner_vals)
self.vals['partner_id'] = partner_id.id
return self.model_obj.create(self.vals)
def test_password_write_date_is_saved_on_create(self):
rec_id = self._new_record()
self.assertTrue(
rec_id.password_write_date,
'Password write date was not saved to db.',
)
def test_password_write_date_is_updated_on_write(self):
rec_id = self._new_record()
old_write_date = rec_id.password_write_date
time.sleep(2)
rec_id.write({'password': 'asdQWE123$%^2'})
rec_id.refresh()
new_write_date = rec_id.password_write_date
self.assertNotEqual(
old_write_date, new_write_date,
'Password write date was not updated on write.',
)
def test_does_not_update_write_date_if_password_unchanged(self):
rec_id = self._new_record()
old_write_date = rec_id.password_write_date
time.sleep(2)
rec_id.write({'name': 'Luser'})
rec_id.refresh()
new_write_date = rec_id.password_write_date
self.assertEqual(
old_write_date, new_write_date,
'Password not changed but write date updated anyway.',
)
def test_check_password_returns_true_for_valid_password(self):
rec_id = self._new_record()
self.assertTrue(
rec_id._check_password('asdQWE123$%^3'),
'Password is valid but check failed.',
)
def test_check_password_raises_error_for_invalid_password(self):
rec_id = self._new_record()
with self.assertRaises(PassError):
rec_id._check_password('password')
def test_save_password_crypt(self):
rec_id = self._new_record()
self.assertEqual(
1, len(rec_id.password_history_ids),
)
def test_check_password_crypt(self):
""" It should raise PassError if previously used """
rec_id = self._new_record()
with self.assertRaises(PassError):
rec_id.write({'password': self.password})
def test_password_is_expired_if_record_has_no_write_date(self):
rec_id = self._new_record()
rec_id.write({'password_write_date': None})
rec_id.refresh()
self.assertTrue(
rec_id._password_has_expired(),
'Record has no password write date but check failed.',
)
def test_an_old_password_is_expired(self):
rec_id = self._new_record()
old_write_date = '1970-01-01 00:00:00'
rec_id.write({'password_write_date': old_write_date})
rec_id.refresh()
self.assertTrue(
rec_id._password_has_expired(),
'Password is out of date but check failed.',
)
def test_a_new_password_is_not_expired(self):
rec_id = self._new_record()
self.assertFalse(
rec_id._password_has_expired(),
'Password was just created but has already expired.',
)
def test_expire_password_generates_token(self):
rec_id = self._new_record()
rec_id.sudo().action_expire_password()
rec_id.refresh()
token = rec_id.partner_id.signup_token
self.assertTrue(
token,
'A token was not generated.',
)
def test_validate_pass_reset_error(self):
""" It should throw PassError on reset inside min threshold """
rec_id = self._new_record()
with self.assertRaises(PassError):
rec_id._validate_pass_reset()
def test_validate_pass_reset_allow(self):
""" It should allow reset pass when outside threshold """
rec_id = self._new_record()
rec_id.password_write_date = '2016-01-01'
self.assertEqual(
True, rec_id._validate_pass_reset(),
)
def test_validate_pass_reset_zero(self):
""" It should allow reset pass when <= 0 """
rec_id = self._new_record()
rec_id.company_id.password_minimum = 0
self.assertEqual(
True, rec_id._validate_pass_reset(),
)

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2015 LasLabs Inc.
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<flectra>
<record id="view_company_form" model="ir.ui.view">
<field name="name">res.company.form</field>
<field name="model">res.company</field>
<field name="inherit_id" ref="base.view_company_form" />
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="Password Policy">
<group>
<group string="Timings">
<field name="password_expiration" />
<field name="password_minimum" />
</group>
<group string="Extra">
<field name="password_length" />
<field name="password_history" />
</group>
</group>
<group name="chars_grp" string="Required Characters">
<group>
<field name="password_lower" />
<field name="password_upper" />
</group>
<group>
<field name="password_numeric" />
<field name="password_special" />
</group>
</group>
</page>
</xpath>
</field>
</record>
</flectra>