summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEli Collins <elic@assurancetechnologies.com>2011-03-15 03:26:35 -0400
committerEli Collins <elic@assurancetechnologies.com>2011-03-15 03:26:35 -0400
commitefaf1f6223ca4d8bc3f13330e9c0093826770dc3 (patch)
tree82f880bab196e3502ac8d71eef2d3650604ce912
parent2d2f1d0f53e79945288db8d05baf6a4cf07fdd66 (diff)
downloadpasslib-efaf1f6223ca4d8bc3f13330e9c0093826770dc3.tar.gz
apache & misc work
================== * added prelim helpers for htpasswd & htdigest * bugfix to ldap hashes * added CryptContext.replace() back * NOTE: all above need UTs and docs
-rw-r--r--passlib/apache.py192
-rw-r--r--passlib/base.py3
-rw-r--r--passlib/drivers/ldap.py8
3 files changed, 188 insertions, 15 deletions
diff --git a/passlib/apache.py b/passlib/apache.py
index 9888210..3187612 100644
--- a/passlib/apache.py
+++ b/passlib/apache.py
@@ -1,32 +1,202 @@
-"""
-apache support
+"""passlib.apache - apache password support
+
+.. todo::
+
+ support htpasswd context
+
+ needs ldap_sha1 support
+ detect when crypt should be used, and what ones.
+
+.. todo::
+ support htdigest context
-http://httpd.apache.org/docs/2.2/misc/password_encryptions.html
-http://httpd.apache.org/docs/2.0/programs/htpasswd.html
-NOTE: digest format is md5(user ":" realm ":" passwd).hexdigest()
- file is "user:realm:hash"
+.. todo::
+
+ support reading / writing htpasswd & htdigest files using this module.
+
+ references -
+ http://httpd.apache.org/docs/2.2/misc/password_encryptions.html
+ http://httpd.apache.org/docs/2.0/programs/htpasswd.html
+
+ NOTE: htdigest format is md5(user ":" realm ":" passwd).hexdigest()
+ file format is "user:realm:hash"
"""
#=========================================================
#imports
#=========================================================
from __future__ import with_statement
#core
+from hashlib import md5
import logging; log = logging.getLogger(__name__)
+import os
#site
#libs
-from passlib.hash import postgres_md5
from passlib.base import CryptContext
#pkg
#local
__all__ = [
- 'postgres_md5',
- 'postgres_context',
]
#=========================================================
-#db contexts
+#common helpers
+#=========================================================
+class _CommonFile(object):
+ "helper for HtpasswdFile / HtdigestFile"
+
+ def __init__(self, path, create=False):
+ self.path = path
+ if create:
+ self._entry_order = []
+ self._entry_map = {}
+ self.mtime = 0
+ else:
+ self.load()
+
+ def reload(self):
+ "load only if file has changed; throw error if file not found"
+ if self.mtime and self.mtime == os.path.getmtime(self.path):
+ return False
+ self.load()
+ return True
+
+ def load(self):
+ "load entries from file; throw error if file not found or malformed"
+ pl = self._parse_line
+ with file(self.path, "rU") as fh:
+ self.mtime = os.path.getmtime(self.path)
+ entry_order = self._entry_order = []
+ entry_map = self._entry_map = {}
+ for line in fh:
+ key, value = pl(line)
+ if key in entry_map:
+ #XXX: should we use data from first entry, or last entry?
+ # going w/ first entry for now.
+ continue
+ entry_order.append(key)
+ entry_map[key] = value
+ return True
+
+ #subclass: _parse_line(line) -> (key, hash)
+
+ def save(self):
+ "save entries to file"
+ rl = self._render_line
+ entry_order = self._entry_order
+ entry_map = self._entry_map
+ assert len(entry_order) == len(entry_map), "internal error in entry list"
+ with file(self.path, "wb") as fh:
+ fh.writelines(rl(key, entry_map[key]) for key in entry_order)
+ self.mtime = os.path.getmtime(self.path)
+
+ #subclass: _render_line(entry) -> line
+
+ def _update_key(self, key, value):
+ entry_map = self._entry_map
+ if key in entry_map:
+ entry_map[key] = value
+ return True
+ else:
+ self._entry_order.append(key)
+ entry_map[key] = value
+ return False
+
+ def _delete_key(self, key):
+ entry_map = self._entry_map
+ if key in entry_map:
+ del entry_map[key]
+ self._entry_order.remove(key)
+ return True
+ else:
+ return False
+
+#=========================================================
+#htpasswd editing
+#=========================================================
+#FIXME: apr_md5_crypt technically the default only for windows, netware and tpf.
+#TODO: find out if htpasswd's "crypt" mode is crypt *call* or des_crypt implementation.
+htpasswd_context = CryptContext(["apr_md5_crypt", "des_crypt", "ldap_sha1", "plaintext" ])
+
+class HtpasswdFile(_CommonFile):
+ "class for reading & writing Htpasswd files"
+
+ def __init__(self, path, default=None, **kwds):
+ self.context = htpasswd_context
+ if default:
+ self.context = self.context.replace(default=default)
+ super(HtpasswdFile, self).__init__(path, **kwds)
+
+ def _parse_line(self, line):
+ #should be user, hash
+ return line.rstrip().split(":")
+
+ def _render_line(self, user, hash):
+ return "%s:%s\n" % (user, hash)
+
+ def update(self, user, password):
+ "update entry for user; added user if needed"
+ hash = self.context.encrypt(password)
+ return self._update_key(user, hash)
+
+ def delete(self, user):
+ "delete any entries for specified user"
+ return self._delete_key(user)
+
+ def verify(self, user, password):
+ "verify password for specified user"
+ hash = self._entry_map.get(user)
+ if hash is None:
+ return None
+ else:
+ return self.context.verify(password, hash)
+
+#=========================================================
+#htdigest editing
#=========================================================
-postgres_context = CryptContext([postgres_md5])
+
+class HtdigestFile(_CommonFile):
+ "class for reading & writing Htdigest files"
+
+ def _parse_line(self, line):
+ user, realm, hash = line.rstrip().split(":")
+ return (user, realm), hash
+
+ def _render_line(self, key, hash):
+ return "%s:%s:%s\n" % (key[0], key[1], hash)
+
+ def update(self, user, realm, password):
+ "update entry for user+realm; added entry if needed"
+ key = (user,realm)
+ hash = md5("%s:%s:%s" % (user,realm,password)).hexdigest()
+ return self._update_key(key, hash)
+
+ def delete(self, user, realm):
+ "delete any entries for specified user+realm"
+ key = (user,realm)
+ return self._delete_key(key)
+
+ def delete_realm(self, realm):
+ "delete all entries for specified realm"
+ entry_order = self._entry_order
+ entry_map = self._entry_map
+ keys = [
+ key for key in entry_map
+ if key[1] == realm
+ ]
+ if keys:
+ for key in keys:
+ del entry_map[key]
+ entry_order.remove(key)
+ return True
+ else:
+ return False
+
+ def verify(self, user, realm, password):
+ "verify password for specified user+realm"
+ key = (user, realm)
+ hash = self._entry_map.get(key)
+ if hash is None:
+ return None
+ return hash == md5("%s:%s:%s" % (user,realm,password)).hexdigest()
#=========================================================
# eof
diff --git a/passlib/base.py b/passlib/base.py
index 76935bb..29fb45a 100644
--- a/passlib/base.py
+++ b/passlib/base.py
@@ -787,6 +787,9 @@ class CryptContext(object):
names = [ handler.name for handler in self.policy.iter_handlers() ]
return "<CryptContext %0xd schemes=%r>" % (id(self), names)
+ def replace(self, **kwds):
+ return CryptContext(policy=self.policy.replace(**kwds))
+
#===================================================================
#policy adaptation
#===================================================================
diff --git a/passlib/drivers/ldap.py b/passlib/drivers/ldap.py
index f242567..886474a 100644
--- a/passlib/drivers/ldap.py
+++ b/passlib/drivers/ldap.py
@@ -90,7 +90,7 @@ class ldap_md5(_Base64DigestHelper):
setting_kwds = ()
_ident = "{MD5}"
- _hash = hashlib.md5
+ _hash = md5
_pat = re.compile(r"^\{MD5\}(?P<chk>[+/a-zA-Z0-9]{22}==)$")
class ldap_sha1(_Base64DigestHelper):
@@ -98,20 +98,20 @@ class ldap_sha1(_Base64DigestHelper):
setting_kwds = ()
_ident = "{SHA}"
- _hash = hashlib.sha1
+ _hash = sha1
_pat = re.compile(r"^\{SHA\}(?P<chk>[+/a-zA-Z0-9]{27}=)$")
class ldap_salted_md5(_SaltedBase64DigestHelper):
name = "ldap_salted_md5"
_ident = "{SMD5}"
- _hash = hashlib.md5
+ _hash = md5
_pat = re.compile(r"^\{SMD5\}(?P<tmp>[+/a-zA-Z0-9]{27}=)$")
_default_chk = '\x00' * 16
class ldap_salted_sha1(_SaltedBase64DigestHelper):
name = "ldap_salted_sha1"
_ident = "{SSHA}"
- _hash = hashlib.sha1
+ _hash = sha1
_pat = re.compile(r"^\{SSHA\}(?P<tmp>[+/a-zA-Z0-9]{32})$")
_default_chk = '\x00' * 20