|
- """passlib.apache - apache password support"""
- # XXX: relocate this to passlib.ext.apache?
- #=============================================================================
- # imports
- #=============================================================================
- from __future__ import with_statement
- # core
- import logging; log = logging.getLogger(__name__)
- import os
- from warnings import warn
- # site
- # pkg
- from passlib import exc, registry
- from passlib.context import CryptContext
- from passlib.exc import ExpectedStringError
- from passlib.hash import htdigest
- from passlib.utils import render_bytes, to_bytes, is_ascii_codec
- from passlib.utils.decor import deprecated_method
- from passlib.utils.compat import join_bytes, unicode, BytesIO, PY3
- # local
- __all__ = [
- 'HtpasswdFile',
- 'HtdigestFile',
- ]
-
- #=============================================================================
- # constants & support
- #=============================================================================
- _UNSET = object()
-
- _BCOLON = b":"
- _BHASH = b"#"
-
- # byte values that aren't allowed in fields.
- _INVALID_FIELD_CHARS = b":\n\r\t\x00"
-
- #: _CommonFile._source token types
- _SKIPPED = "skipped"
- _RECORD = "record"
-
- #=============================================================================
- # common helpers
- #=============================================================================
- class _CommonFile(object):
- """common framework for HtpasswdFile & HtdigestFile"""
- #===================================================================
- # instance attrs
- #===================================================================
-
- # charset encoding used by file (defaults to utf-8)
- encoding = None
-
- # whether users() and other public methods should return unicode or bytes?
- # (defaults to False under PY2, True under PY3)
- return_unicode = None
-
- # if bound to local file, these will be set.
- _path = None # local file path
- _mtime = None # mtime when last loaded, or 0
-
- # if true, automatically save to local file after changes are made.
- autosave = False
-
- # dict mapping key -> value for all records in database.
- # (e.g. user => hash for Htpasswd)
- _records = None
-
- #: list of tokens for recreating original file contents when saving. if present,
- #: will be sequence of (_SKIPPED, b"whitespace/comments") and (_RECORD, <record key>) tuples.
- _source = None
-
- #===================================================================
- # alt constuctors
- #===================================================================
- @classmethod
- def from_string(cls, data, **kwds):
- """create new object from raw string.
-
- :type data: unicode or bytes
- :arg data:
- database to load, as single string.
-
- :param \\*\\*kwds:
- all other keywords are the same as in the class constructor
- """
- if 'path' in kwds:
- raise TypeError("'path' not accepted by from_string()")
- self = cls(**kwds)
- self.load_string(data)
- return self
-
- @classmethod
- def from_path(cls, path, **kwds):
- """create new object from file, without binding object to file.
-
- :type path: str
- :arg path:
- local filepath to load from
-
- :param \\*\\*kwds:
- all other keywords are the same as in the class constructor
- """
- self = cls(**kwds)
- self.load(path)
- return self
-
- #===================================================================
- # init
- #===================================================================
- def __init__(self, path=None, new=False, autoload=True, autosave=False,
- encoding="utf-8", return_unicode=PY3,
- ):
- # set encoding
- if not encoding:
- warn("``encoding=None`` is deprecated as of Passlib 1.6, "
- "and will cause a ValueError in Passlib 1.8, "
- "use ``return_unicode=False`` instead.",
- DeprecationWarning, stacklevel=2)
- encoding = "utf-8"
- return_unicode = False
- elif not is_ascii_codec(encoding):
- # htpasswd/htdigest files assumes 1-byte chars, and use ":" separator,
- # so only ascii-compatible encodings are allowed.
- raise ValueError("encoding must be 7-bit ascii compatible")
- self.encoding = encoding
-
- # set other attrs
- self.return_unicode = return_unicode
- self.autosave = autosave
- self._path = path
- self._mtime = 0
-
- # init db
- if not autoload:
- warn("``autoload=False`` is deprecated as of Passlib 1.6, "
- "and will be removed in Passlib 1.8, use ``new=True`` instead",
- DeprecationWarning, stacklevel=2)
- new = True
- if path and not new:
- self.load()
- else:
- self._records = {}
- self._source = []
-
- def __repr__(self):
- tail = ''
- if self.autosave:
- tail += ' autosave=True'
- if self._path:
- tail += ' path=%r' % self._path
- if self.encoding != "utf-8":
- tail += ' encoding=%r' % self.encoding
- return "<%s 0x%0x%s>" % (self.__class__.__name__, id(self), tail)
-
- # NOTE: ``path`` is a property so that ``_mtime`` is wiped when it's set.
-
- @property
- def path(self):
- return self._path
-
- @path.setter
- def path(self, value):
- if value != self._path:
- self._mtime = 0
- self._path = value
-
- @property
- def mtime(self):
- """modify time when last loaded (if bound to a local file)"""
- return self._mtime
-
- #===================================================================
- # loading
- #===================================================================
- def load_if_changed(self):
- """Reload from ``self.path`` only if file has changed since last load"""
- if not self._path:
- raise RuntimeError("%r is not bound to a local file" % self)
- if self._mtime and self._mtime == os.path.getmtime(self._path):
- return False
- self.load()
- return True
-
- def load(self, path=None, force=True):
- """Load state from local file.
- If no path is specified, attempts to load from ``self.path``.
-
- :type path: str
- :arg path: local file to load from
-
- :type force: bool
- :param force:
- if ``force=False``, only load from ``self.path`` if file
- has changed since last load.
-
- .. deprecated:: 1.6
- This keyword will be removed in Passlib 1.8;
- Applications should use :meth:`load_if_changed` instead.
- """
- if path is not None:
- with open(path, "rb") as fh:
- self._mtime = 0
- self._load_lines(fh)
- elif not force:
- warn("%(name)s.load(force=False) is deprecated as of Passlib 1.6,"
- "and will be removed in Passlib 1.8; "
- "use %(name)s.load_if_changed() instead." %
- dict(name=self.__class__.__name__),
- DeprecationWarning, stacklevel=2)
- return self.load_if_changed()
- elif self._path:
- with open(self._path, "rb") as fh:
- self._mtime = os.path.getmtime(self._path)
- self._load_lines(fh)
- else:
- raise RuntimeError("%s().path is not set, an explicit path is required" %
- self.__class__.__name__)
- return True
-
- def load_string(self, data):
- """Load state from unicode or bytes string, replacing current state"""
- data = to_bytes(data, self.encoding, "data")
- self._mtime = 0
- self._load_lines(BytesIO(data))
-
- def _load_lines(self, lines):
- """load from sequence of lists"""
- parse = self._parse_record
- records = {}
- source = []
- skipped = b''
- for idx, line in enumerate(lines):
- # NOTE: per htpasswd source (https://github.com/apache/httpd/blob/trunk/support/htpasswd.c),
- # lines with only whitespace, or with "#" as first non-whitespace char,
- # are left alone / ignored.
- tmp = line.lstrip()
- if not tmp or tmp.startswith(_BHASH):
- skipped += line
- continue
-
- # parse valid line
- key, value = parse(line, idx+1)
-
- # NOTE: if multiple entries for a key, we use the first one,
- # which seems to match htpasswd source
- if key in records:
- log.warning("username occurs multiple times in source file: %r" % key)
- skipped += line
- continue
-
- # flush buffer of skipped whitespace lines
- if skipped:
- source.append((_SKIPPED, skipped))
- skipped = b''
-
- # store new user line
- records[key] = value
- source.append((_RECORD, key))
-
- # don't bother preserving trailing whitespace, but do preserve trailing comments
- if skipped.rstrip():
- source.append((_SKIPPED, skipped))
-
- # NOTE: not replacing ._records until parsing succeeds, so loading is atomic.
- self._records = records
- self._source = source
-
- def _parse_record(self, record, lineno): # pragma: no cover - abstract method
- """parse line of file into (key, value) pair"""
- raise NotImplementedError("should be implemented in subclass")
-
- def _set_record(self, key, value):
- """
- helper for setting record which takes care of inserting source line if needed;
-
- :returns:
- bool if key already present
- """
- records = self._records
- existing = (key in records)
- records[key] = value
- if not existing:
- self._source.append((_RECORD, key))
- return existing
-
- #===================================================================
- # saving
- #===================================================================
- def _autosave(self):
- """subclass helper to call save() after any changes"""
- if self.autosave and self._path:
- self.save()
-
- def save(self, path=None):
- """Save current state to file.
- If no path is specified, attempts to save to ``self.path``.
- """
- if path is not None:
- with open(path, "wb") as fh:
- fh.writelines(self._iter_lines())
- elif self._path:
- self.save(self._path)
- self._mtime = os.path.getmtime(self._path)
- else:
- raise RuntimeError("%s().path is not set, cannot autosave" %
- self.__class__.__name__)
-
- def to_string(self):
- """Export current state as a string of bytes"""
- return join_bytes(self._iter_lines())
-
- # def clean(self):
- # """
- # discard any comments or whitespace that were being preserved from the source file,
- # and re-sort keys in alphabetical order
- # """
- # self._source = [(_RECORD, key) for key in sorted(self._records)]
- # self._autosave()
-
- def _iter_lines(self):
- """iterator yielding lines of database"""
- # NOTE: this relies on <records> being an OrderedDict so that it outputs
- # records in a deterministic order.
- records = self._records
- if __debug__:
- pending = set(records)
- for action, content in self._source:
- if action == _SKIPPED:
- # 'content' is whitespace/comments to write
- yield content
- else:
- assert action == _RECORD
- # 'content' is record key
- if content not in records:
- # record was deleted
- # NOTE: doing it lazily like this so deleting & re-adding user
- # preserves their original location in the file.
- continue
- yield self._render_record(content, records[content])
- if __debug__:
- pending.remove(content)
- if __debug__:
- # sanity check that we actually wrote all the records
- # (otherwise _source & _records are somehow out of sync)
- assert not pending, "failed to write all records: missing=%r" % (pending,)
-
- def _render_record(self, key, value): # pragma: no cover - abstract method
- """given key/value pair, encode as line of file"""
- raise NotImplementedError("should be implemented in subclass")
-
- #===================================================================
- # field encoding
- #===================================================================
- def _encode_user(self, user):
- """user-specific wrapper for _encode_field()"""
- return self._encode_field(user, "user")
-
- def _encode_realm(self, realm): # pragma: no cover - abstract method
- """realm-specific wrapper for _encode_field()"""
- return self._encode_field(realm, "realm")
-
- def _encode_field(self, value, param="field"):
- """convert field to internal representation.
-
- internal representation is always bytes. byte strings are left as-is,
- unicode strings encoding using file's default encoding (or ``utf-8``
- if no encoding has been specified).
-
- :raises UnicodeEncodeError:
- if unicode value cannot be encoded using default encoding.
-
- :raises ValueError:
- if resulting byte string contains a forbidden character,
- or is too long (>255 bytes).
-
- :returns:
- encoded identifer as bytes
- """
- if isinstance(value, unicode):
- value = value.encode(self.encoding)
- elif not isinstance(value, bytes):
- raise ExpectedStringError(value, param)
- if len(value) > 255:
- raise ValueError("%s must be at most 255 characters: %r" %
- (param, value))
- if any(c in _INVALID_FIELD_CHARS for c in value):
- raise ValueError("%s contains invalid characters: %r" %
- (param, value,))
- return value
-
- def _decode_field(self, value):
- """decode field from internal representation to format
- returns by users() method, etc.
-
- :raises UnicodeDecodeError:
- if unicode value cannot be decoded using default encoding.
- (usually indicates wrong encoding set for file).
-
- :returns:
- field as unicode or bytes, as appropriate.
- """
- assert isinstance(value, bytes), "expected value to be bytes"
- if self.return_unicode:
- return value.decode(self.encoding)
- else:
- return value
-
- # FIXME: htpasswd doc says passwords limited to 255 chars under Windows & MPE,
- # and that longer ones are truncated. this may be side-effect of those
- # platforms supporting the 'plaintext' scheme. these classes don't currently
- # check for this.
-
- #===================================================================
- # eoc
- #===================================================================
-
- #=============================================================================
- # htpasswd context
- #
- # This section sets up a CryptContexts to mimic what schemes Apache
- # (and the htpasswd tool) should support on the current system.
- #
- # Apache has long-time supported some basic builtin schemes (listed below),
- # as well as the host's crypt() method -- though it's limited to being able
- # to *verify* any scheme using that method, but can only generate "des_crypt" hashes.
- #
- # Apache 2.4 added builtin bcrypt support (even for platforms w/o native support).
- # c.f. http://httpd.apache.org/docs/2.4/programs/htpasswd.html vs the 2.2 docs.
- #=============================================================================
-
- #: set of default schemes that (if chosen) should be using bcrypt,
- #: but can't due to lack of bcrypt.
- _warn_no_bcrypt = set()
-
- def _init_default_schemes():
-
- #: pick strongest one for host
- host_best = None
- for name in ["bcrypt", "sha256_crypt"]:
- if registry.has_os_crypt_support(name):
- host_best = name
- break
-
- # check if we have a bcrypt backend -- otherwise issue warning
- # XXX: would like to not spam this unless the user *requests* apache 24
- bcrypt = "bcrypt" if registry.has_backend("bcrypt") else None
- _warn_no_bcrypt.clear()
- if not bcrypt:
- _warn_no_bcrypt.update(["portable_apache_24", "host_apache_24",
- "linux_apache_24", "portable", "host"])
-
- defaults = dict(
- # strongest hash builtin to specific apache version
- portable_apache_24=bcrypt or "apr_md5_crypt",
- portable_apache_22="apr_md5_crypt",
-
- # strongest hash across current host & specific apache version
- host_apache_24=bcrypt or host_best or "apr_md5_crypt",
- host_apache_22=host_best or "apr_md5_crypt",
-
- # strongest hash on a linux host
- linux_apache_24=bcrypt or "sha256_crypt",
- linux_apache_22="sha256_crypt",
- )
-
- # set latest-apache version aliases
- # XXX: could check for apache install, and pick correct host 22/24 default?
- # could reuse _detect_htpasswd() helper in UTs
- defaults.update(
- portable=defaults['portable_apache_24'],
- host=defaults['host_apache_24'],
- )
- return defaults
-
- #: dict mapping default alias -> appropriate scheme
- htpasswd_defaults = _init_default_schemes()
-
- def _init_htpasswd_context():
-
- # start with schemes built into apache
- schemes = [
- # builtin support added in apache 2.4
- # (https://bz.apache.org/bugzilla/show_bug.cgi?id=49288)
- "bcrypt",
-
- # support not "builtin" to apache, instead it requires support through host's crypt().
- # adding them here to allow editing htpasswd under windows and then deploying under unix.
- "sha256_crypt",
- "sha512_crypt",
- "des_crypt",
-
- # apache default as of 2.2.18, and still default in 2.4
- "apr_md5_crypt",
-
- # NOTE: apache says ONLY intended for transitioning htpasswd <-> ldap
- "ldap_sha1",
-
- # NOTE: apache says ONLY supported on Windows, Netware, TPF
- "plaintext"
- ]
-
- # apache can verify anything supported by the native crypt(),
- # though htpasswd tool can only generate a limited set of hashes.
- # (this list may overlap w/ builtin apache schemes)
- schemes.extend(registry.get_supported_os_crypt_schemes())
-
- # hack to remove dups and sort into preferred order
- preferred = schemes[:3] + ["apr_md5_crypt"] + schemes
- schemes = sorted(set(schemes), key=preferred.index)
-
- # create context object
- return CryptContext(
- schemes=schemes,
-
- # NOTE: default will change to "portable" in passlib 2.0
- default=htpasswd_defaults['portable_apache_22'],
-
- # NOTE: bcrypt "2y" is required, "2b" isn't recognized by libapr (issue 95)
- bcrypt__ident="2y",
- )
-
- #: CryptContext configured to match htpasswd
- htpasswd_context = _init_htpasswd_context()
-
- #=============================================================================
- # htpasswd editing
- #=============================================================================
-
- class HtpasswdFile(_CommonFile):
- """class for reading & writing Htpasswd files.
-
- The class constructor accepts the following arguments:
-
- :type path: filepath
- :param path:
-
- Specifies path to htpasswd file, use to implicitly load from and save to.
-
- This class has two modes of operation:
-
- 1. It can be "bound" to a local file by passing a ``path`` to the class
- constructor. In this case it will load the contents of the file when
- created, and the :meth:`load` and :meth:`save` methods will automatically
- load from and save to that file if they are called without arguments.
-
- 2. Alternately, it can exist as an independant object, in which case
- :meth:`load` and :meth:`save` will require an explicit path to be
- provided whenever they are called. As well, ``autosave`` behavior
- will not be available.
-
- This feature is new in Passlib 1.6, and is the default if no
- ``path`` value is provided to the constructor.
-
- This is also exposed as a readonly instance attribute.
-
- :type new: bool
- :param new:
-
- Normally, if *path* is specified, :class:`HtpasswdFile` will
- immediately load the contents of the file. However, when creating
- a new htpasswd file, applications can set ``new=True`` so that
- the existing file (if any) will not be loaded.
-
- .. versionadded:: 1.6
- This feature was previously enabled by setting ``autoload=False``.
- That alias has been deprecated, and will be removed in Passlib 1.8
-
- :type autosave: bool
- :param autosave:
-
- Normally, any changes made to an :class:`HtpasswdFile` instance
- will not be saved until :meth:`save` is explicitly called. However,
- if ``autosave=True`` is specified, any changes made will be
- saved to disk immediately (assuming *path* has been set).
-
- This is also exposed as a writeable instance attribute.
-
- :type encoding: str
- :param encoding:
-
- Optionally specify character encoding used to read/write file
- and hash passwords. Defaults to ``utf-8``, though ``latin-1``
- is the only other commonly encountered encoding.
-
- This is also exposed as a readonly instance attribute.
-
- :type default_scheme: str
- :param default_scheme:
- Optionally specify default scheme to use when encoding new passwords.
-
- This can be any of the schemes with builtin Apache support,
- OR natively supported by the host OS's :func:`crypt.crypt` function.
-
- * Builtin schemes include ``"bcrypt"`` (apache 2.4+), ``"apr_md5_crypt"`,
- and ``"des_crypt"``.
-
- * Schemes commonly supported by Unix hosts
- include ``"bcrypt"``, ``"sha256_crypt"``, and ``"des_crypt"``.
-
- In order to not have to sort out what you should use,
- passlib offers a number of aliases, that will resolve
- to the most appropriate scheme based on your needs:
-
- * ``"portable"``, ``"portable_apache_24"`` -- pick scheme that's portable across hosts
- running apache >= 2.4. **This will be the default as of Passlib 2.0**.
-
- * ``"portable_apache_22"`` -- pick scheme that's portable across hosts
- running apache >= 2.4. **This is the default up to Passlib 1.9**.
-
- * ``"host"``, ``"host_apache_24"`` -- pick strongest scheme supported by
- apache >= 2.4 and/or host OS.
-
- * ``"host_apache_22"`` -- pick strongest scheme supported by
- apache >= 2.2 and/or host OS.
-
- .. versionadded:: 1.6
- This keyword was previously named ``default``. That alias
- has been deprecated, and will be removed in Passlib 1.8.
-
- .. versionchanged:: 1.6.3
-
- Added support for ``"bcrypt"``, ``"sha256_crypt"``, and ``"portable"`` alias.
-
- .. versionchanged:: 1.7
-
- Added apache 2.4 semantics, and additional aliases.
-
- :type context: :class:`~passlib.context.CryptContext`
- :param context:
- :class:`!CryptContext` instance used to create
- and verify the hashes found in the htpasswd file.
- The default value is a pre-built context which supports all
- of the hashes officially allowed in an htpasswd file.
-
- This is also exposed as a readonly instance attribute.
-
- .. warning::
-
- This option may be used to add support for non-standard hash
- formats to an htpasswd file. However, the resulting file
- will probably not be usable by another application,
- and particularly not by Apache.
-
- :param autoload:
- Set to ``False`` to prevent the constructor from automatically
- loaded the file from disk.
-
- .. deprecated:: 1.6
- This has been replaced by the *new* keyword.
- Instead of setting ``autoload=False``, you should use
- ``new=True``. Support for this keyword will be removed
- in Passlib 1.8.
-
- :param default:
- Change the default algorithm used to hash new passwords.
-
- .. deprecated:: 1.6
- This has been renamed to *default_scheme* for clarity.
- Support for this alias will be removed in Passlib 1.8.
-
- Loading & Saving
- ================
- .. automethod:: load
- .. automethod:: load_if_changed
- .. automethod:: load_string
- .. automethod:: save
- .. automethod:: to_string
-
- Inspection
- ================
- .. automethod:: users
- .. automethod:: check_password
- .. automethod:: get_hash
-
- Modification
- ================
- .. automethod:: set_password
- .. automethod:: delete
-
- Alternate Constructors
- ======================
- .. automethod:: from_string
-
- Attributes
- ==========
- .. attribute:: path
-
- Path to local file that will be used as the default
- for all :meth:`load` and :meth:`save` operations.
- May be written to, initialized by the *path* constructor keyword.
-
- .. attribute:: autosave
-
- Writeable flag indicating whether changes will be automatically
- written to *path*.
-
- Errors
- ======
- :raises ValueError:
- All of the methods in this class will raise a :exc:`ValueError` if
- any user name contains a forbidden character (one of ``:\\r\\n\\t\\x00``),
- or is longer than 255 characters.
- """
- #===================================================================
- # instance attrs
- #===================================================================
-
- # NOTE: _records map stores <user> for the key, and <hash> for the value,
- # both in bytes which use self.encoding
-
- #===================================================================
- # init & serialization
- #===================================================================
- def __init__(self, path=None, default_scheme=None, context=htpasswd_context,
- **kwds):
- if 'default' in kwds:
- warn("``default`` is deprecated as of Passlib 1.6, "
- "and will be removed in Passlib 1.8, it has been renamed "
- "to ``default_scheem``.",
- DeprecationWarning, stacklevel=2)
- default_scheme = kwds.pop("default")
- if default_scheme:
- if default_scheme in _warn_no_bcrypt:
- warn("HtpasswdFile: no bcrypt backends available, "
- "using fallback for default scheme %r" % default_scheme,
- exc.PasslibSecurityWarning)
- default_scheme = htpasswd_defaults.get(default_scheme, default_scheme)
- context = context.copy(default=default_scheme)
- self.context = context
- super(HtpasswdFile, self).__init__(path, **kwds)
-
- def _parse_record(self, record, lineno):
- # NOTE: should return (user, hash) tuple
- result = record.rstrip().split(_BCOLON)
- if len(result) != 2:
- raise ValueError("malformed htpasswd file (error reading line %d)"
- % lineno)
- return result
-
- def _render_record(self, user, hash):
- return render_bytes("%s:%s\n", user, hash)
-
- #===================================================================
- # public methods
- #===================================================================
-
- def users(self):
- """
- Return list of all users in database
- """
- return [self._decode_field(user) for user in self._records]
-
- ##def has_user(self, user):
- ## "check whether entry is present for user"
- ## return self._encode_user(user) in self._records
-
- ##def rename(self, old, new):
- ## """rename user account"""
- ## old = self._encode_user(old)
- ## new = self._encode_user(new)
- ## hash = self._records.pop(old)
- ## self._records[new] = hash
- ## self._autosave()
-
- def set_password(self, user, password):
- """Set password for user; adds user if needed.
-
- :returns:
- * ``True`` if existing user was updated.
- * ``False`` if user account was added.
-
- .. versionchanged:: 1.6
- This method was previously called ``update``, it was renamed
- to prevent ambiguity with the dictionary method.
- The old alias is deprecated, and will be removed in Passlib 1.8.
- """
- hash = self.context.hash(password)
- return self.set_hash(user, hash)
-
- @deprecated_method(deprecated="1.6", removed="1.8",
- replacement="set_password")
- def update(self, user, password):
- """set password for user"""
- return self.set_password(user, password)
-
- def get_hash(self, user):
- """Return hash stored for user, or ``None`` if user not found.
-
- .. versionchanged:: 1.6
- This method was previously named ``find``, it was renamed
- for clarity. The old name is deprecated, and will be removed
- in Passlib 1.8.
- """
- try:
- return self._records[self._encode_user(user)]
- except KeyError:
- return None
-
- def set_hash(self, user, hash):
- """
- semi-private helper which allows writing a hash directly;
- adds user if needed.
-
- .. warning::
- does not (currently) do any validation of the hash string
-
- .. versionadded:: 1.7
- """
- # assert self.context.identify(hash), "unrecognized hash format"
- if PY3 and isinstance(hash, str):
- hash = hash.encode(self.encoding)
- user = self._encode_user(user)
- existing = self._set_record(user, hash)
- self._autosave()
- return existing
-
- @deprecated_method(deprecated="1.6", removed="1.8",
- replacement="get_hash")
- def find(self, user):
- """return hash for user"""
- return self.get_hash(user)
-
- # XXX: rename to something more explicit, like delete_user()?
- def delete(self, user):
- """Delete user's entry.
-
- :returns:
- * ``True`` if user deleted.
- * ``False`` if user not found.
- """
- try:
- del self._records[self._encode_user(user)]
- except KeyError:
- return False
- self._autosave()
- return True
-
- def check_password(self, user, password):
- """
- Verify password for specified user.
- If algorithm marked as deprecated by CryptContext, will automatically be re-hashed.
-
- :returns:
- * ``None`` if user not found.
- * ``False`` if user found, but password does not match.
- * ``True`` if user found and password matches.
-
- .. versionchanged:: 1.6
- This method was previously called ``verify``, it was renamed
- to prevent ambiguity with the :class:`!CryptContext` method.
- The old alias is deprecated, and will be removed in Passlib 1.8.
- """
- user = self._encode_user(user)
- hash = self._records.get(user)
- if hash is None:
- return None
- if isinstance(password, unicode):
- # NOTE: encoding password to match file, making the assumption
- # that server will use same encoding to hash the password.
- password = password.encode(self.encoding)
- ok, new_hash = self.context.verify_and_update(password, hash)
- if ok and new_hash is not None:
- # rehash user's password if old hash was deprecated
- assert user in self._records # otherwise would have to use ._set_record()
- self._records[user] = new_hash
- self._autosave()
- return ok
-
- @deprecated_method(deprecated="1.6", removed="1.8",
- replacement="check_password")
- def verify(self, user, password):
- """verify password for user"""
- return self.check_password(user, password)
-
- #===================================================================
- # eoc
- #===================================================================
-
- #=============================================================================
- # htdigest editing
- #=============================================================================
- class HtdigestFile(_CommonFile):
- """class for reading & writing Htdigest files.
-
- The class constructor accepts the following arguments:
-
- :type path: filepath
- :param path:
-
- Specifies path to htdigest file, use to implicitly load from and save to.
-
- This class has two modes of operation:
-
- 1. It can be "bound" to a local file by passing a ``path`` to the class
- constructor. In this case it will load the contents of the file when
- created, and the :meth:`load` and :meth:`save` methods will automatically
- load from and save to that file if they are called without arguments.
-
- 2. Alternately, it can exist as an independant object, in which case
- :meth:`load` and :meth:`save` will require an explicit path to be
- provided whenever they are called. As well, ``autosave`` behavior
- will not be available.
-
- This feature is new in Passlib 1.6, and is the default if no
- ``path`` value is provided to the constructor.
-
- This is also exposed as a readonly instance attribute.
-
- :type default_realm: str
- :param default_realm:
-
- If ``default_realm`` is set, all the :class:`HtdigestFile`
- methods that require a realm will use this value if one is not
- provided explicitly. If unset, they will raise an error stating
- that an explicit realm is required.
-
- This is also exposed as a writeable instance attribute.
-
- .. versionadded:: 1.6
-
- :type new: bool
- :param new:
-
- Normally, if *path* is specified, :class:`HtdigestFile` will
- immediately load the contents of the file. However, when creating
- a new htpasswd file, applications can set ``new=True`` so that
- the existing file (if any) will not be loaded.
-
- .. versionadded:: 1.6
- This feature was previously enabled by setting ``autoload=False``.
- That alias has been deprecated, and will be removed in Passlib 1.8
-
- :type autosave: bool
- :param autosave:
-
- Normally, any changes made to an :class:`HtdigestFile` instance
- will not be saved until :meth:`save` is explicitly called. However,
- if ``autosave=True`` is specified, any changes made will be
- saved to disk immediately (assuming *path* has been set).
-
- This is also exposed as a writeable instance attribute.
-
- :type encoding: str
- :param encoding:
-
- Optionally specify character encoding used to read/write file
- and hash passwords. Defaults to ``utf-8``, though ``latin-1``
- is the only other commonly encountered encoding.
-
- This is also exposed as a readonly instance attribute.
-
- :param autoload:
- Set to ``False`` to prevent the constructor from automatically
- loaded the file from disk.
-
- .. deprecated:: 1.6
- This has been replaced by the *new* keyword.
- Instead of setting ``autoload=False``, you should use
- ``new=True``. Support for this keyword will be removed
- in Passlib 1.8.
-
- Loading & Saving
- ================
- .. automethod:: load
- .. automethod:: load_if_changed
- .. automethod:: load_string
- .. automethod:: save
- .. automethod:: to_string
-
- Inspection
- ==========
- .. automethod:: realms
- .. automethod:: users
- .. automethod:: check_password(user[, realm], password)
- .. automethod:: get_hash
-
- Modification
- ============
- .. automethod:: set_password(user[, realm], password)
- .. automethod:: delete
- .. automethod:: delete_realm
-
- Alternate Constructors
- ======================
- .. automethod:: from_string
-
- Attributes
- ==========
- .. attribute:: default_realm
-
- The default realm that will be used if one is not provided
- to methods that require it. By default this is ``None``,
- in which case an explicit realm must be provided for every
- method call. Can be written to.
-
- .. attribute:: path
-
- Path to local file that will be used as the default
- for all :meth:`load` and :meth:`save` operations.
- May be written to, initialized by the *path* constructor keyword.
-
- .. attribute:: autosave
-
- Writeable flag indicating whether changes will be automatically
- written to *path*.
-
- Errors
- ======
- :raises ValueError:
- All of the methods in this class will raise a :exc:`ValueError` if
- any user name or realm contains a forbidden character (one of ``:\\r\\n\\t\\x00``),
- or is longer than 255 characters.
- """
- #===================================================================
- # instance attrs
- #===================================================================
-
- # NOTE: _records map stores (<user>,<realm>) for the key,
- # and <hash> as the value, all as <self.encoding> bytes.
-
- # NOTE: unlike htpasswd, this class doesn't use a CryptContext,
- # as only one hash format is supported: htdigest.
-
- # optionally specify default realm that will be used if none
- # is provided to a method call. otherwise realm is always required.
- default_realm = None
-
- #===================================================================
- # init & serialization
- #===================================================================
- def __init__(self, path=None, default_realm=None, **kwds):
- self.default_realm = default_realm
- super(HtdigestFile, self).__init__(path, **kwds)
-
- def _parse_record(self, record, lineno):
- result = record.rstrip().split(_BCOLON)
- if len(result) != 3:
- raise ValueError("malformed htdigest file (error reading line %d)"
- % lineno)
- user, realm, hash = result
- return (user, realm), hash
-
- def _render_record(self, key, hash):
- user, realm = key
- return render_bytes("%s:%s:%s\n", user, realm, hash)
-
- def _require_realm(self, realm):
- if realm is None:
- realm = self.default_realm
- if realm is None:
- raise TypeError("you must specify a realm explicitly, "
- "or set the default_realm attribute")
- return realm
-
- def _encode_realm(self, realm):
- realm = self._require_realm(realm)
- return self._encode_field(realm, "realm")
-
- def _encode_key(self, user, realm):
- return self._encode_user(user), self._encode_realm(realm)
-
- #===================================================================
- # public methods
- #===================================================================
-
- def realms(self):
- """Return list of all realms in database"""
- realms = set(key[1] for key in self._records)
- return [self._decode_field(realm) for realm in realms]
-
- def users(self, realm=None):
- """Return list of all users in specified realm.
-
- * uses ``self.default_realm`` if no realm explicitly provided.
- * returns empty list if realm not found.
- """
- realm = self._encode_realm(realm)
- return [self._decode_field(key[0]) for key in self._records
- if key[1] == realm]
-
- ##def has_user(self, user, realm=None):
- ## "check if user+realm combination exists"
- ## return self._encode_key(user,realm) in self._records
-
- ##def rename_realm(self, old, new):
- ## """rename all accounts in realm"""
- ## old = self._encode_realm(old)
- ## new = self._encode_realm(new)
- ## keys = [key for key in self._records if key[1] == old]
- ## for key in keys:
- ## hash = self._records.pop(key)
- ## self._set_record((key[0], new), hash)
- ## self._autosave()
- ## return len(keys)
-
- ##def rename(self, old, new, realm=None):
- ## """rename user account"""
- ## old = self._encode_user(old)
- ## new = self._encode_user(new)
- ## realm = self._encode_realm(realm)
- ## hash = self._records.pop((old,realm))
- ## self._set_record((new, realm), hash)
- ## self._autosave()
-
- def set_password(self, user, realm=None, password=_UNSET):
- """Set password for user; adds user & realm if needed.
-
- If ``self.default_realm`` has been set, this may be called
- with the syntax ``set_password(user, password)``,
- otherwise it must be called with all three arguments:
- ``set_password(user, realm, password)``.
-
- :returns:
- * ``True`` if existing user was updated
- * ``False`` if user account added.
- """
- if password is _UNSET:
- # called w/ two args - (user, password), use default realm
- realm, password = None, realm
- realm = self._require_realm(realm)
- hash = htdigest.hash(password, user, realm, encoding=self.encoding)
- return self.set_hash(user, realm, hash)
-
- @deprecated_method(deprecated="1.6", removed="1.8",
- replacement="set_password")
- def update(self, user, realm, password):
- """set password for user"""
- return self.set_password(user, realm, password)
-
- def get_hash(self, user, realm=None):
- """Return :class:`~passlib.hash.htdigest` hash stored for user.
-
- * uses ``self.default_realm`` if no realm explicitly provided.
- * returns ``None`` if user or realm not found.
-
- .. versionchanged:: 1.6
- This method was previously named ``find``, it was renamed
- for clarity. The old name is deprecated, and will be removed
- in Passlib 1.8.
- """
- key = self._encode_key(user, realm)
- hash = self._records.get(key)
- if hash is None:
- return None
- if PY3:
- hash = hash.decode(self.encoding)
- return hash
-
- def set_hash(self, user, realm=None, hash=_UNSET):
- """
- semi-private helper which allows writing a hash directly;
- adds user & realm if needed.
-
- If ``self.default_realm`` has been set, this may be called
- with the syntax ``set_hash(user, hash)``,
- otherwise it must be called with all three arguments:
- ``set_hash(user, realm, hash)``.
-
- .. warning::
- does not (currently) do any validation of the hash string
-
- .. versionadded:: 1.7
- """
- if hash is _UNSET:
- # called w/ two args - (user, hash), use default realm
- realm, hash = None, realm
- # assert htdigest.identify(hash), "unrecognized hash format"
- if PY3 and isinstance(hash, str):
- hash = hash.encode(self.encoding)
- key = self._encode_key(user, realm)
- existing = self._set_record(key, hash)
- self._autosave()
- return existing
-
- @deprecated_method(deprecated="1.6", removed="1.8",
- replacement="get_hash")
- def find(self, user, realm):
- """return hash for user"""
- return self.get_hash(user, realm)
-
- # XXX: rename to something more explicit, like delete_user()?
- def delete(self, user, realm=None):
- """Delete user's entry for specified realm.
-
- if realm is not specified, uses ``self.default_realm``.
-
- :returns:
- * ``True`` if user deleted,
- * ``False`` if user not found in realm.
- """
- key = self._encode_key(user, realm)
- try:
- del self._records[key]
- except KeyError:
- return False
- self._autosave()
- return True
-
- def delete_realm(self, realm):
- """Delete all users for specified realm.
-
- if realm is not specified, uses ``self.default_realm``.
-
- :returns: number of users deleted (0 if realm not found)
- """
- realm = self._encode_realm(realm)
- records = self._records
- keys = [key for key in records if key[1] == realm]
- for key in keys:
- del records[key]
- self._autosave()
- return len(keys)
-
- def check_password(self, user, realm=None, password=_UNSET):
- """Verify password for specified user + realm.
-
- If ``self.default_realm`` has been set, this may be called
- with the syntax ``check_password(user, password)``,
- otherwise it must be called with all three arguments:
- ``check_password(user, realm, password)``.
-
- :returns:
- * ``None`` if user or realm not found.
- * ``False`` if user found, but password does not match.
- * ``True`` if user found and password matches.
-
- .. versionchanged:: 1.6
- This method was previously called ``verify``, it was renamed
- to prevent ambiguity with the :class:`!CryptContext` method.
- The old alias is deprecated, and will be removed in Passlib 1.8.
- """
- if password is _UNSET:
- # called w/ two args - (user, password), use default realm
- realm, password = None, realm
- user = self._encode_user(user)
- realm = self._encode_realm(realm)
- hash = self._records.get((user,realm))
- if hash is None:
- return None
- return htdigest.verify(password, hash, user, realm,
- encoding=self.encoding)
-
- @deprecated_method(deprecated="1.6", removed="1.8",
- replacement="check_password")
- def verify(self, user, realm, password):
- """verify password for user"""
- return self.check_password(user, realm, password)
-
- #===================================================================
- # eoc
- #===================================================================
-
- #=============================================================================
- # eof
- #=============================================================================
|