-
Notifications
You must be signed in to change notification settings - Fork 20
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
base: master
Are you sure you want to change the base?
Changes from 4 commits
449f4f1
de6a91e
9cf22f6
c550c9c
411da8c
e8c1284
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ | |
import gridfs | ||
import re | ||
import uuid | ||
import hashlib | ||
|
||
from bson import Binary, DBRef, SON, ObjectId | ||
|
||
|
@@ -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' | ||
|
||
|
@@ -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: | ||
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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. erroneous print |
||
result |= ord(x) ^ ord(y) | ||
return result == 0 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
""" | ||
|
There was a problem hiding this comment.
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