Add constraint on user password

issue6379
review28091002
This commit is contained in:
Cédric Krier
2017-04-03 17:05:23 +02:00
parent 5b86281166
commit f7a6da3328
8 changed files with 150 additions and 2 deletions
+1
View File
@@ -1,3 +1,4 @@
* Add constraint on user password
* Remove Property field
* Add MultiValueMixin and ValueMixin
* Use sql type in column creation
+1
View File
@@ -22,3 +22,4 @@ include trytond/res/view/*.xml
include trytond/res/locale/*.po
include trytond/tests/tryton.cfg
include trytond/tests/*.xml
include trytond/tests/forbidden.txt
+22
View File
@@ -228,6 +228,28 @@ attempts over a period of `timeout`.
Default: `5`
password
--------
length
~~~~~~
The minimal length required for the user password.
Default: `8`
forbidden
~~~~~~~~~
The path to a file containing one forbidden password per line.
entropy
~~~~~~~
The ratio of non repeated characters for the user password.
Default: `0.75`
report
------
+1 -1
View File
@@ -57,7 +57,7 @@ setup(name=name,
'trytond.ir.module': ['*.xml'],
'trytond.ir.ui': ['*.xml', '*.rng', '*.rnc'],
'trytond.res': ['tryton.cfg', '*.xml', 'view/*.xml', 'locale/*.po'],
'trytond.tests': ['tryton.cfg', '*.xml'],
'trytond.tests': ['tryton.cfg', '*.xml', 'forbidden.txt'],
},
scripts=['bin/trytond', 'bin/trytond-admin', 'bin/trytond-cron'],
classifiers=[
+3
View File
@@ -63,6 +63,9 @@ class TrytonConfigParser(ConfigParser.RawConfigParser):
self.set('session', 'authentications', 'password')
self.set('session', 'timeout', 600)
self.set('session', 'max_attempt', 5)
self.add_section('password')
self.set('password', 'length', 8)
self.set('password', 'entropy', 0.75)
self.add_section('report')
self.set('report', 'unoconv',
'pipe,name=trytond;urp;StarOffice.ComponentContext')
+47
View File
@@ -1,6 +1,7 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
"User"
from __future__ import division
import copy
import string
import random
@@ -9,6 +10,7 @@ import time
import datetime
import logging
import uuid
import mmap
from functools import wraps
from itertools import groupby, ifilter
from operator import attrgetter
@@ -116,6 +118,17 @@ class User(ModelSQL, ModelView):
'for logging purpose.\n'
'Instead you must inactivate them.'),
'wrong_password': 'Wrong password!',
'password_length': "The password is too short.",
'password_forbidden': "The password is forbidden.",
'password_name': (
"The password can not be the same as user name."),
'password_login': (
"The password can not be the same as user login."),
'password_email': (
"The password can not be the same as user email."),
'password_entropy': (
"The password contains too much times "
"the same characters."),
})
@classmethod
@@ -200,6 +213,10 @@ class User(ModelSQL, ModelView):
def set_password(cls, users, name, value):
if value == 'x' * 10:
return
if Transaction().user and value:
cls.validate_password(value, users)
to_write = []
for user in users:
to_write.extend([[user], {
@@ -207,6 +224,36 @@ class User(ModelSQL, ModelView):
}])
cls.write(*to_write)
@classmethod
def validate_password(cls, password, users):
length = config.getint('password', 'length', default=0)
if length > 0:
password_b = password
if isinstance(password, unicode):
password_b = password.encode('utf-8')
if len(password_b) < length:
cls.raise_user_error('password_length')
path = config.get('password', 'forbidden', default=None)
if path:
with open(path, 'r') as f:
forbidden = mmap.mmap(
f.fileno(), 0, access=mmap.ACCESS_READ)
if forbidden.find(password) >= 0:
cls.raise_user_error('password_forbidden')
entropy = config.getfloat('password', 'entropy', default=0)
if entropy:
if len(set(password)) / len(password) < entropy:
cls.raise_user_error('password_entropy')
for user in users:
# Use getattr to allow to use non User instances
for test, error in [
(getattr(user, 'name', ''), 'password_name'),
(getattr(user, 'login', ''), 'password_login'),
(getattr(user, 'email', ''), 'password_email'),
]:
if test and password.lower() == test.lower():
cls.raise_user_error(error)
@staticmethod
def get_sessions(users, name):
Session = Pool().get('ir.session')
+1
View File
@@ -0,0 +1 @@
password
+74 -1
View File
@@ -1,11 +1,12 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of this
# repository contains the full copyright notices and license terms.
import os
import unittest
from trytond.tests.test_tryton import activate_module, with_transaction
from trytond.pool import Pool
from trytond.res.user import bcrypt
from trytond.config import config
from trytond.error import UserError
class UserTestCase(unittest.TestCase):
@@ -20,6 +21,20 @@ class UserTestCase(unittest.TestCase):
config.set('session', 'authentications', 'password')
self.addCleanup(config.set, 'session', 'authentications', methods)
length = config.get('password', 'length')
config.set('password', 'length', 4)
self.addCleanup(config.set, 'password', 'length', length)
forbidden = config.get('password', 'forbidden', default='')
config.set(
'password', 'forbidden',
os.path.join(os.path.dirname(__file__), 'forbidden.txt'))
self.addCleanup(config.set, 'password', 'forbidden', forbidden)
entropy = config.get('password', 'entropy')
config.set('password', 'entropy', 0.9)
self.addCleanup(config.set, 'password', 'entropy', entropy)
def create_user(self, login, password, hash_method=None):
pool = Pool()
User = pool.get('res.user')
@@ -79,6 +94,64 @@ class UserTestCase(unittest.TestCase):
user = self.create_user('user', '12345')
self.assertIsNone(user.password_hash)
@with_transaction()
def test_validate_password_length(self):
"Test validate password length"
pool = Pool()
User = pool.get('res.user')
with self.assertRaises(UserError):
User.validate_password(u'123', [])
User.validate_password(u'1234', [])
@with_transaction()
def test_validate_password_forbidden(self):
"Test validate password forbidden"
pool = Pool()
User = pool.get('res.user')
with self.assertRaises(UserError):
User.validate_password(u'password', [])
@with_transaction()
def test_validate_password_entropy(self):
"Test validate password entropy"
pool = Pool()
User = pool.get('res.user')
with self.assertRaises(UserError):
User.validate_password('aaaaaa', [])
@with_transaction()
def test_validate_password_name(self):
"Test validate password name"
pool = Pool()
User = pool.get('res.user')
user = User(name='name')
with self.assertRaises(UserError):
User.validate_password('name', [user])
@with_transaction()
def test_validate_password_login(self):
"Test validate password login"
pool = Pool()
User = pool.get('res.user')
user = User(login='login')
with self.assertRaises(UserError):
User.validate_password('login', [user])
@with_transaction()
def test_validate_password_email(self):
"Test validate password email"
pool = Pool()
User = pool.get('res.user')
user = User(email='email')
with self.assertRaises(UserError):
User.validate_password('email', [user])
def suite():
return unittest.TestLoader().loadTestsFromTestCase(UserTestCase)