Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding new fields PasswordField and IpAddressField #542

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
188 changes: 187 additions & 1 deletion mongoengine/fields.py
Expand Up @@ -4,6 +4,7 @@
import gridfs
import re
import uuid
import hashlib

from bson import Binary, DBRef, SON, ObjectId

Expand Down Expand Up @@ -33,7 +34,7 @@
'DecimalField', 'ComplexDateTimeField', 'URLField', 'DynamicField',
'GenericReferenceField', 'FileField', 'BinaryField',
'SortedListField', 'EmailField', 'GeoPointField', 'ImageField',
'SequenceField', 'UUIDField', 'GenericEmbeddedDocumentField']
'SequenceField', 'UUIDField', 'GenericEmbeddedDocumentField','PasswordField','IPAddressField']

RECURSIVE_REFERENCE_CONSTANT = 'self'

Expand Down Expand Up @@ -92,8 +93,193 @@ def prepare_query_value(self, op, value):
value = re.escape(value)
value = re.compile(regex % value, flags)
return value


class PasswordField(BaseField):
"""A password field - generate password using specific algorithm (md5,sha1,sha512 etc) and regex validator

Default regex validator: r[a-zA-Z0-9]+ <- Match any of the above

Example:

class User(Document):
username = StringField(required=True,unique=True)
password = PasswordField(algorithm="md5")
ip = IPAddressField()

# save user:
user = User(username=username,password="mongoengine789",ip="192.167.12.255")
user.save()

# search user
user = User.objects(username=username).first()
if user is None:
print "Not found!"
return
user_password = user.password
print str(upassword) -> {'hash': 'c2e920e469d14f240d4de02883489750a1a63e68', 'salt': 'QBX6FZD', 'algorithm': 'sha1'}
... check password ...

"""

ALGORITHM_MD5 = "md5"
ALGORITHM_SHA1 = "sha1"
ALGORITHM_SHA256 = "sha256"
ALGORITHM_SHA512 = "sha512"
ALGORITHM_CRYPT = "crypt"

CHARS = "azertyupqsdfghjkmwxcvbn1234567890AZERTYUPQSDFGHJKMWXCVBN"

DEFAULT_VALIDATOR = r'[a-zA-Z0-9]+'
DOLLAR = "$"


class ValidatorRegex:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I like this syntax / approach - couldnt we handle this on init ?

Also we should check the regex is valid / can be compiled

def __init__(self,pattern):
self.compiled_pattern = re.compile(pattern)

def __call__(self, password):
if re.match(self.compiled_pattern, password):
return True
return False

def __init__(self, max_length=None, algorithm=ALGORITHM_SHA1,validator=DEFAULT_VALIDATOR, min_length=None,**kwargs):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should validate the algorithm is available here and that the validator works.

self.max_length = max_length
self.min_length = min_length
self.algorithm = algorithm.lower()
self.salt = self.random_password()
self.validator = PasswordField.ValidatorRegex(validator)


super(PasswordField, self).__init__(**kwargs)

def random_password(self,nchars=7):
chars = PasswordField.CHARS
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might want to just use: string.printable for this - but does it have to pass the validation?

hash = ''
for char in xrange(nchars):
rand_char = random.randrange(0,len(chars))
hash += chars[rand_char]
return hash

def hexdigest(self,password):
if self.algorithm == PasswordField.ALGORITHM_CRYPT:
try:
import crypt
except ImportError:
self.error("crypt module not found in this system. Please use md5 or sha* algorithm")
return crypt.crypt(password, self.salt)

try:
import hashlib
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already imported at the top of the page

except ImportError:
self.error("hashlib module not found in this system.")

''' use sha1 algoritm '''
if self.algorithm == PasswordField.ALGORITHM_SHA1:
return hashlib.sha1(self.salt + password).hexdigest()
elif self.algorithm == PasswordField.ALGORITHM_MD5:
return hashlib.md5(self.salt + password).hexdigest()
elif self.algorithm == PasswordField.ALGORITHM_SHA256:
return hashlib.sha256(self.salt + password).hexdigest()
elif self.algorithm == PasswordField.ALGORITHM_SHA512:
return hashlib.sha512(self.salt + password).hexdigest()
raise ValueError('Unsupported hash type %s' % self.algorithm)

def set_password(self,password):
'''
Sets the user's password using format [encryption algorithm]$[salt]$[password]
Example: sha1$SgwcbaH$20f16a1fa9af6fa40d59f78fd2c247f426950e46

'''
password = self.hexdigest(password)
return '%s$%s$%s' % (self.algorithm,self.salt,password)

def to_mongo(self,value):
return self.set_password(value)

def to_python(self, value):
'''
Return password like sha1$DEnDMSj$ef5cd35779bba65528c900d248f3e939fb495c65
'''
return value

def to_dict(self,value):
(algorithm,salt,hash) = value.split(PasswordField.DOLLAR)
return {
"algorithm" : algorithm,
"salt" : salt,
"hash" : hash
}

def validate(self, value):
if value is None:
self.error('Password is empty!')

if self.max_length is not None and len(value) > self.max_length:
self.error('Password is too long')

if self.min_length is not None and len(value) < self.min_length:
self.error('Password value is too short')

if PasswordField.DOLLAR in str(value):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really needed? - especially as its hashed

self.error("Password invalid! Character '$' found in password ")

if not self.validator(value):
self.error('Password is invalid!')

class IPAddressField(BaseField):
"""
An IP address field

"""

IPV6_REGEXP = re.compile(r"""
^
\s* # Leading whitespace
(?!.*::.*::) # Only a single whildcard allowed
(?:(?!:)|:(?=:)) # Colon iff it would be part of a wildcard
(?: # Repeat 6 times:
[0-9a-f]{0,4} # A group of at most four hexadecimal digits
(?:(?<=::)|(?<!::):) # Colon unless preceeded by wildcard
){6} #
(?: # Either
[0-9a-f]{0,4} # Another group
(?:(?<=::)|(?<!::):) # Colon unless preceeded by wildcard
[0-9a-f]{0,4} # Last group
(?: (?<=::) # Colon iff preceeded by exacly one colon
| (?<!:) #
| (?<=:) (?<!::) : #
) # OR
| # A v4 address with NO leading zeros
(?:25[0-4]|2[0-4]\d|1\d\d|[1-9]?\d)
(?: \.
(?:25[0-4]|2[0-4]\d|1\d\d|[1-9]?\d)
){3}
)
\s* # Trailing whitespace
$
""", re.VERBOSE | re.IGNORECASE | re.DOTALL)

def validate_ipv4_address(self,address):
addr_parts = address.split(".")
if len(addr_parts) != 4:
return False
for part in addr_parts:
try:
if not 0 <= int(part) <= 255:
return False
except ValueError:
return False
return True

def validate_ipv6_address(self,address):
return IPAddressField.IPV6_REGEXP.match(address) is not None

def validate(self,value):
addr_is_valid = self.validate_ipv4_address(value) or self.validate_ipv6_address(value)
if not addr_is_valid:
self.error('Invalid IP Address: %s' % value)

class URLField(StringField):
"""A field that validates input as an URL.

Expand Down
84 changes: 84 additions & 0 deletions mongoengine/utils.py
@@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-

import hashlib
from fields import PasswordField

class PasswordHandler:
"""
Check encrypted password

Example:

bpass = PasswordHandler()
is_valid = bpass.verify("sha1$e7eHHdB$7033e721c7a9ed84f7d473d99da92b00a7ca81f","sha1$e7eHHdB$7033e721c7a9ed84f7d473d99da92b00a7ca81fb")
--> return is_valid=False

"""

def hexdigest(self,password):
if self.algorithm == PasswordField.ALGORITHM_CRYPT:
try:
import crypt
except ImportError:
self.error("crypt module not found in this system. Please use md5 or sha* algorithm")
return crypt.crypt(password, self.salt)

try:
import hashlib
except ImportError:
self.error("hashlib module not found in this system.")

''' use sha1 algoritm '''
if self.algorithm == PasswordField.ALGORITHM_SHA1:
return hashlib.sha1(self.salt + password).hexdigest()
elif self.algorithm == PasswordField.ALGORITHM_MD5:
return hashlib.md5(self.salt + password).hexdigest()
elif self.algorithm == PasswordField.ALGORITHM_SHA256:
return hashlib.sha256(self.salt + password).hexdigest()
elif self.algorithm == PasswordField.ALGORITHM_SHA512:
return hashlib.sha512(self.salt + password).hexdigest()
raise ValueError('Unsupported hash type %s' % self.algorithm)

def encode(self, password):
"""
Creates an encoded password

The result is normally formatted as "algorithm$salt$hash"

"""
password = self.hexdigest(password)
return '%s$%s$%s' % (self.algorithm,self.salt,password)

def decode(self,encoded_password):
"""
Decode password

The result is normally formatted as "algorithm$salt$hash" like in the example:
ha1$SgwcbaH$20f16a1fa9af6fa40d59f78fd2c247f426950e46

"""
(self.algorithm,self.salt,self.hash) = encoded_password.split(PasswordField.DOLLAR)
if self.algorithm is None:
raise Exception("Algorithm not found in encrypted password")
if self.salt is None:
raise Exception("Password salt not found in encrypted password")

def verify(self, password, encoded_password):
"""
Checks if the given password is correct
"""
self.decode(encoded_password)
input_encoded_password = self.encode(password)
return self._compare(encoded_password, input_encoded_password)

def _compare(self,value1,value2):
"""
Returns True if the two strings are equal, False otherwise.
"""
if len(value1) != len(value2):
return False
result = 0
for x, y in zip(value1, value2):
print x,y
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

erroneous print

result |= ord(x) ^ ord(y)
return result == 0
46 changes: 46 additions & 0 deletions tests/test_fields.py
Expand Up @@ -170,6 +170,52 @@ class Person(Document):
person.name = 'Shorter name'
person.validate()

def test_ip_validation(self):
"""Ensure that IPAddressField is valid.
"""

class Host(Document):
ip = IPAddressField()

host = Host()
host.ip = "192.168.190.10"
host.validate()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Convert to assertions

host.save()


host.ip = "::1"
host.validate()
host.save()

host.ip = "fe80::1177:db4a:18e:bda8%20"
self.assertRaises(ValidationError, host.validate)

def test_password_validation(self):
"""Ensure that PasswordField is valid.
"""

class Users(Document):
username = StringField()
password = PasswordField(algorithm="sha1",validator='[a-z]+')

import random
user = Users()
user.username=str(random.random())
user.password="abracadabra"

user.validate()
user.save()

class Users(Document):
username = StringField()
password = PasswordField(algorithm="md5")

user = Users()
user.username=str(random.random())
user.password="pymongoengine2012"
user.validate()
user.save()

def test_url_validation(self):
"""Ensure that URLFields validate urls properly.
"""
Expand Down