Add constraint on user password
issue6379 review28091002
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
* Add constraint on user password
|
||||
* Remove Property field
|
||||
* Add MultiValueMixin and ValueMixin
|
||||
* Use sql type in column creation
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
------
|
||||
|
||||
|
||||
@@ -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=[
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
password
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user