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 all 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
2 changes: 2 additions & 0 deletions docs/apireference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,5 @@ Fields
.. autoclass:: mongoengine.BinaryField
.. autoclass:: mongoengine.GeoPointField
.. autoclass:: mongoengine.SequenceField
.. autoclass:: mongoengine.PasswordField
.. autoclass:: mongoengine.IPAddressField
2 changes: 2 additions & 0 deletions docs/guide/defining-documents.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ are as follows:
* :class:`~mongoengine.BinaryField`
* :class:`~mongoengine.GeoPointField`
* :class:`~mongoengine.SequenceField`
* :class:`~mongoengine.PasswordField`
* :class:`~mongoengine.IPAddressField`

Field arguments
---------------
Expand Down
164 changes: 163 additions & 1 deletion mongoengine/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import gridfs
import re
import uuid
import hashlib
import string

from bson import Binary, DBRef, SON, ObjectId

Expand Down Expand Up @@ -33,7 +35,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 +94,168 @@ 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]{6,} <- Match any of the above: leters and digits min 6 chars

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"
DEFAULT_VALIDATOR = r'[A-Za-z0-9]' # letters and digits - min length 6 chars
DOLLAR = "$"

def __init__(self, max_length=None, algorithm=ALGORITHM_SHA1, validator=DEFAULT_VALIDATOR, min_length=None, **kwargs):
self.max_length = max_length
self.min_length = min_length
self.algorithm = algorithm.lower()
self.salt = self.random_password()
self.validator = re.compile(validator) if validator else None
super(PasswordField, self).__init__(kwargs)

def random_password(self, nchars=6):
chars = string.printable
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)

''' 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 self.validator is not None and self.validator.match(value) is None:
self.error('String value did not match validation regex')

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
83 changes: 83 additions & 0 deletions mongoengine/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# -*- 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):
result |= ord(x) ^ ord(y)
return result == 0
46 changes: 46 additions & 0 deletions tests/test_fields.py
Original file line number Diff line number Diff line change
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