codekingpro/portable-devtools
115k
1"""passlib.apache - apache password support"""2# XXX: relocate this to passlib.ext.apache?3#=============================================================================4# imports5#=============================================================================6from __future__ import with_statement7# core8import logging; log = logging.getLogger(__name__)9import os10from warnings import warn11# site12# pkg13from passlib import exc, registry14from passlib.context import CryptContext15from passlib.exc import ExpectedStringError16from passlib.hash import htdigest17from passlib.utils import render_bytes, to_bytes, is_ascii_codec18from passlib.utils.decor import deprecated_method19from passlib.utils.compat import join_bytes, unicode, BytesIO, PY320# local21__all__ = [22 'HtpasswdFile',23 'HtdigestFile',24]25 26#=============================================================================27# constants & support28#=============================================================================29_UNSET = object()30 31_BCOLON = b":"32_BHASH = b"#"33 34# byte values that aren't allowed in fields.35_INVALID_FIELD_CHARS = b":\n\r\t\x00"36 37#: _CommonFile._source token types38_SKIPPED = "skipped"39_RECORD = "record"40 41#=============================================================================42# common helpers43#=============================================================================44class _CommonFile(object):45 """common framework for HtpasswdFile & HtdigestFile"""46 #===================================================================47 # instance attrs48 #===================================================================49 50 # charset encoding used by file (defaults to utf-8)51 encoding = None52 53 # whether users() and other public methods should return unicode or bytes?54 # (defaults to False under PY2, True under PY3)55 return_unicode = None56 57 # if bound to local file, these will be set.58 _path = None # local file path59 _mtime = None # mtime when last loaded, or 060 61 # if true, automatically save to local file after changes are made.62 autosave = False63 64 # dict mapping key -> value for all records in database.65 # (e.g. user => hash for Htpasswd)66 _records = None67 68 #: list of tokens for recreating original file contents when saving. if present,69 #: will be sequence of (_SKIPPED, b"whitespace/comments") and (_RECORD, <record key>) tuples.70 _source = None71 72 #===================================================================73 # alt constuctors74 #===================================================================75 @classmethod76 def from_string(cls, data, **kwds):77 """create new object from raw string.78 79 :type data: unicode or bytes80 :arg data:81 database to load, as single string.82 83 :param \\*\\*kwds:84 all other keywords are the same as in the class constructor85 """86 if 'path' in kwds:87 raise TypeError("'path' not accepted by from_string()")88 self = cls(**kwds)89 self.load_string(data)90 return self91 92 @classmethod93 def from_path(cls, path, **kwds):94 """create new object from file, without binding object to file.95 96 :type path: str97 :arg path:98 local filepath to load from99 100 :param \\*\\*kwds:101 all other keywords are the same as in the class constructor102 """103 self = cls(**kwds)104 self.load(path)105 return self106 107 #===================================================================108 # init109 #===================================================================110 def __init__(self, path=None, new=False, autoload=True, autosave=False,111 encoding="utf-8", return_unicode=PY3,112 ):113 # set encoding114 if not encoding:115 warn("``encoding=None`` is deprecated as of Passlib 1.6, "116 "and will cause a ValueError in Passlib 1.8, "117 "use ``return_unicode=False`` instead.",118 DeprecationWarning, stacklevel=2)119 encoding = "utf-8"120 return_unicode = False121 elif not is_ascii_codec(encoding):122 # htpasswd/htdigest files assumes 1-byte chars, and use ":" separator,123 # so only ascii-compatible encodings are allowed.124 raise ValueError("encoding must be 7-bit ascii compatible")125 self.encoding = encoding126 127 # set other attrs128 self.return_unicode = return_unicode129 self.autosave = autosave130 self._path = path131 self._mtime = 0132 133 # init db134 if not autoload:135 warn("``autoload=False`` is deprecated as of Passlib 1.6, "136 "and will be removed in Passlib 1.8, use ``new=True`` instead",137 DeprecationWarning, stacklevel=2)138 new = True139 if path and not new:140 self.load()141 else:142 self._records = {}143 self._source = []144 145 def __repr__(self):146 tail = ''147 if self.autosave:148 tail += ' autosave=True'149 if self._path:150 tail += ' path=%r' % self._path151 if self.encoding != "utf-8":152 tail += ' encoding=%r' % self.encoding153 return "<%s 0x%0x%s>" % (self.__class__.__name__, id(self), tail)154 155 # NOTE: ``path`` is a property so that ``_mtime`` is wiped when it's set.156 157 @property158 def path(self):159 return self._path160 161 @path.setter162 def path(self, value):163 if value != self._path:164 self._mtime = 0165 self._path = value166 167 @property168 def mtime(self):169 """modify time when last loaded (if bound to a local file)"""170 return self._mtime171 172 #===================================================================173 # loading174 #===================================================================175 def load_if_changed(self):176 """Reload from ``self.path`` only if file has changed since last load"""177 if not self._path:178 raise RuntimeError("%r is not bound to a local file" % self)179 if self._mtime and self._mtime == os.path.getmtime(self._path):180 return False181 self.load()182 return True183 184 def load(self, path=None, force=True):185 """Load state from local file.186 If no path is specified, attempts to load from ``self.path``.187 188 :type path: str189 :arg path: local file to load from190 191 :type force: bool192 :param force:193 if ``force=False``, only load from ``self.path`` if file194 has changed since last load.195 196 .. deprecated:: 1.6197 This keyword will be removed in Passlib 1.8;198 Applications should use :meth:`load_if_changed` instead.199 """200 if path is not None:201 with open(path, "rb") as fh:202 self._mtime = 0203 self._load_lines(fh)204 elif not force:205 warn("%(name)s.load(force=False) is deprecated as of Passlib 1.6,"206 "and will be removed in Passlib 1.8; "207 "use %(name)s.load_if_changed() instead." %208 dict(name=self.__class__.__name__),209 DeprecationWarning, stacklevel=2)210 return self.load_if_changed()211 elif self._path:212 with open(self._path, "rb") as fh:213 self._mtime = os.path.getmtime(self._path)214 self._load_lines(fh)215 else:216 raise RuntimeError("%s().path is not set, an explicit path is required" %217 self.__class__.__name__)218 return True219 220 def load_string(self, data):221 """Load state from unicode or bytes string, replacing current state"""222 data = to_bytes(data, self.encoding, "data")223 self._mtime = 0224 self._load_lines(BytesIO(data))225 226 def _load_lines(self, lines):227 """load from sequence of lists"""228 parse = self._parse_record229 records = {}230 source = []231 skipped = b''232 for idx, line in enumerate(lines):233 # NOTE: per htpasswd source (https://github.com/apache/httpd/blob/trunk/support/htpasswd.c),234 # lines with only whitespace, or with "#" as first non-whitespace char,235 # are left alone / ignored.236 tmp = line.lstrip()237 if not tmp or tmp.startswith(_BHASH):238 skipped += line239 continue240 241 # parse valid line242 key, value = parse(line, idx+1)243 244 # NOTE: if multiple entries for a key, we use the first one,245 # which seems to match htpasswd source246 if key in records:247 log.warning("username occurs multiple times in source file: %r" % key)248 skipped += line249 continue250 251 # flush buffer of skipped whitespace lines252 if skipped:253 source.append((_SKIPPED, skipped))254 skipped = b''255 256 # store new user line257 records[key] = value258 source.append((_RECORD, key))259 260 # don't bother preserving trailing whitespace, but do preserve trailing comments261 if skipped.rstrip():262 source.append((_SKIPPED, skipped))263 264 # NOTE: not replacing ._records until parsing succeeds, so loading is atomic.265 self._records = records266 self._source = source267 268 def _parse_record(self, record, lineno): # pragma: no cover - abstract method269 """parse line of file into (key, value) pair"""270 raise NotImplementedError("should be implemented in subclass")271 272 def _set_record(self, key, value):273 """274 helper for setting record which takes care of inserting source line if needed;275 276 :returns:277 bool if key already present278 """279 records = self._records280 existing = (key in records)281 records[key] = value282 if not existing:283 self._source.append((_RECORD, key))284 return existing285 286 #===================================================================287 # saving288 #===================================================================289 def _autosave(self):290 """subclass helper to call save() after any changes"""291 if self.autosave and self._path:292 self.save()293 294 def save(self, path=None):295 """Save current state to file.296 If no path is specified, attempts to save to ``self.path``.297 """298 if path is not None:299 with open(path, "wb") as fh:300 fh.writelines(self._iter_lines())301 elif self._path:302 self.save(self._path)303 self._mtime = os.path.getmtime(self._path)304 else:305 raise RuntimeError("%s().path is not set, cannot autosave" %306 self.__class__.__name__)307 308 def to_string(self):309 """Export current state as a string of bytes"""310 return join_bytes(self._iter_lines())311 312 # def clean(self):313 # """314 # discard any comments or whitespace that were being preserved from the source file,315 # and re-sort keys in alphabetical order316 # """317 # self._source = [(_RECORD, key) for key in sorted(self._records)]318 # self._autosave()319 320 def _iter_lines(self):321 """iterator yielding lines of database"""322 # NOTE: this relies on <records> being an OrderedDict so that it outputs323 # records in a deterministic order.324 records = self._records325 if __debug__:326 pending = set(records)327 for action, content in self._source:328 if action == _SKIPPED:329 # 'content' is whitespace/comments to write330 yield content331 else:332 assert action == _RECORD333 # 'content' is record key334 if content not in records:335 # record was deleted336 # NOTE: doing it lazily like this so deleting & re-adding user337 # preserves their original location in the file.338 continue339 yield self._render_record(content, records[content])340 if __debug__:341 pending.remove(content)342 if __debug__:343 # sanity check that we actually wrote all the records344 # (otherwise _source & _records are somehow out of sync)345 assert not pending, "failed to write all records: missing=%r" % (pending,)346 347 def _render_record(self, key, value): # pragma: no cover - abstract method348 """given key/value pair, encode as line of file"""349 raise NotImplementedError("should be implemented in subclass")350 351 #===================================================================352 # field encoding353 #===================================================================354 def _encode_user(self, user):355 """user-specific wrapper for _encode_field()"""356 return self._encode_field(user, "user")357 358 def _encode_realm(self, realm): # pragma: no cover - abstract method359 """realm-specific wrapper for _encode_field()"""360 return self._encode_field(realm, "realm")361 362 def _encode_field(self, value, param="field"):363 """convert field to internal representation.364 365 internal representation is always bytes. byte strings are left as-is,366 unicode strings encoding using file's default encoding (or ``utf-8``367 if no encoding has been specified).368 369 :raises UnicodeEncodeError:370 if unicode value cannot be encoded using default encoding.371 372 :raises ValueError:373 if resulting byte string contains a forbidden character,374 or is too long (>255 bytes).375 376 :returns:377 encoded identifer as bytes378 """379 if isinstance(value, unicode):380 value = value.encode(self.encoding)381 elif not isinstance(value, bytes):382 raise ExpectedStringError(value, param)383 if len(value) > 255:384 raise ValueError("%s must be at most 255 characters: %r" %385 (param, value))386 if any(c in _INVALID_FIELD_CHARS for c in value):387 raise ValueError("%s contains invalid characters: %r" %388 (param, value,))389 return value390 391 def _decode_field(self, value):392 """decode field from internal representation to format393 returns by users() method, etc.394 395 :raises UnicodeDecodeError:396 if unicode value cannot be decoded using default encoding.397 (usually indicates wrong encoding set for file).398 399 :returns:400 field as unicode or bytes, as appropriate.401 """402 assert isinstance(value, bytes), "expected value to be bytes"403 if self.return_unicode:404 return value.decode(self.encoding)405 else:406 return value407 408 # FIXME: htpasswd doc says passwords limited to 255 chars under Windows & MPE,409 # and that longer ones are truncated. this may be side-effect of those410 # platforms supporting the 'plaintext' scheme. these classes don't currently411 # check for this.412 413 #===================================================================414 # eoc415 #===================================================================416 417#=============================================================================418# htpasswd context419#420# This section sets up a CryptContexts to mimic what schemes Apache421# (and the htpasswd tool) should support on the current system.422#423# Apache has long-time supported some basic builtin schemes (listed below),424# as well as the host's crypt() method -- though it's limited to being able425# to *verify* any scheme using that method, but can only generate "des_crypt" hashes.426#427# Apache 2.4 added builtin bcrypt support (even for platforms w/o native support).428# c.f. http://httpd.apache.org/docs/2.4/programs/htpasswd.html vs the 2.2 docs.429#=============================================================================430 431#: set of default schemes that (if chosen) should be using bcrypt,432#: but can't due to lack of bcrypt.433_warn_no_bcrypt = set()434 435def _init_default_schemes():436 437 #: pick strongest one for host438 host_best = None439 for name in ["bcrypt", "sha256_crypt"]:440 if registry.has_os_crypt_support(name):441 host_best = name442 break443 444 # check if we have a bcrypt backend -- otherwise issue warning445 # XXX: would like to not spam this unless the user *requests* apache 24446 bcrypt = "bcrypt" if registry.has_backend("bcrypt") else None447 _warn_no_bcrypt.clear()448 if not bcrypt:449 _warn_no_bcrypt.update(["portable_apache_24", "host_apache_24",450 "linux_apache_24", "portable", "host"])451 452 defaults = dict(453 # strongest hash builtin to specific apache version454 portable_apache_24=bcrypt or "apr_md5_crypt",455 portable_apache_22="apr_md5_crypt",456 457 # strongest hash across current host & specific apache version458 host_apache_24=bcrypt or host_best or "apr_md5_crypt",459 host_apache_22=host_best or "apr_md5_crypt",460 461 # strongest hash on a linux host462 linux_apache_24=bcrypt or "sha256_crypt",463 linux_apache_22="sha256_crypt",464 )465 466 # set latest-apache version aliases467 # XXX: could check for apache install, and pick correct host 22/24 default?468 # could reuse _detect_htpasswd() helper in UTs469 defaults.update(470 portable=defaults['portable_apache_24'],471 host=defaults['host_apache_24'],472 )473 return defaults474 475#: dict mapping default alias -> appropriate scheme476htpasswd_defaults = _init_default_schemes()477 478def _init_htpasswd_context():479 480 # start with schemes built into apache481 schemes = [482 # builtin support added in apache 2.4483 # (https://bz.apache.org/bugzilla/show_bug.cgi?id=49288)484 "bcrypt",485 486 # support not "builtin" to apache, instead it requires support through host's crypt().487 # adding them here to allow editing htpasswd under windows and then deploying under unix.488 "sha256_crypt",489 "sha512_crypt",490 "des_crypt",491 492 # apache default as of 2.2.18, and still default in 2.4493 "apr_md5_crypt",494 495 # NOTE: apache says ONLY intended for transitioning htpasswd <-> ldap496 "ldap_sha1",497 498 # NOTE: apache says ONLY supported on Windows, Netware, TPF499 "plaintext"500 ]501 502 # apache can verify anything supported by the native crypt(),503 # though htpasswd tool can only generate a limited set of hashes.504 # (this list may overlap w/ builtin apache schemes)505 schemes.extend(registry.get_supported_os_crypt_schemes())506 507 # hack to remove dups and sort into preferred order508 preferred = schemes[:3] + ["apr_md5_crypt"] + schemes509 schemes = sorted(set(schemes), key=preferred.index)510 511 # create context object512 return CryptContext(513 schemes=schemes,514 515 # NOTE: default will change to "portable" in passlib 2.0516 default=htpasswd_defaults['portable_apache_22'],517 518 # NOTE: bcrypt "2y" is required, "2b" isn't recognized by libapr (issue 95)519 bcrypt__ident="2y",520 )521 522#: CryptContext configured to match htpasswd523htpasswd_context = _init_htpasswd_context()524 525#=============================================================================526# htpasswd editing527#=============================================================================528 529class HtpasswdFile(_CommonFile):530 """class for reading & writing Htpasswd files.531 532 The class constructor accepts the following arguments:533 534 :type path: filepath535 :param path:536 537 Specifies path to htpasswd file, use to implicitly load from and save to.538 539 This class has two modes of operation:540 541 1. It can be "bound" to a local file by passing a ``path`` to the class542 constructor. In this case it will load the contents of the file when543 created, and the :meth:`load` and :meth:`save` methods will automatically544 load from and save to that file if they are called without arguments.545 546 2. Alternately, it can exist as an independant object, in which case547 :meth:`load` and :meth:`save` will require an explicit path to be548 provided whenever they are called. As well, ``autosave`` behavior549 will not be available.550 551 This feature is new in Passlib 1.6, and is the default if no552 ``path`` value is provided to the constructor.553 554 This is also exposed as a readonly instance attribute.555 556 :type new: bool557 :param new:558 559 Normally, if *path* is specified, :class:`HtpasswdFile` will560 immediately load the contents of the file. However, when creating561 a new htpasswd file, applications can set ``new=True`` so that562 the existing file (if any) will not be loaded.563 564 .. versionadded:: 1.6565 This feature was previously enabled by setting ``autoload=False``.566 That alias has been deprecated, and will be removed in Passlib 1.8567 568 :type autosave: bool569 :param autosave:570 571 Normally, any changes made to an :class:`HtpasswdFile` instance572 will not be saved until :meth:`save` is explicitly called. However,573 if ``autosave=True`` is specified, any changes made will be574 saved to disk immediately (assuming *path* has been set).575 576 This is also exposed as a writeable instance attribute.577 578 :type encoding: str579 :param encoding:580 581 Optionally specify character encoding used to read/write file582 and hash passwords. Defaults to ``utf-8``, though ``latin-1``583 is the only other commonly encountered encoding.584 585 This is also exposed as a readonly instance attribute.586 587 :type default_scheme: str588 :param default_scheme:589 Optionally specify default scheme to use when encoding new passwords.590 591 This can be any of the schemes with builtin Apache support,592 OR natively supported by the host OS's :func:`crypt.crypt` function.593 594 * Builtin schemes include ``"bcrypt"`` (apache 2.4+), ``"apr_md5_crypt"`,595 and ``"des_crypt"``.596 597 * Schemes commonly supported by Unix hosts598 include ``"bcrypt"``, ``"sha256_crypt"``, and ``"des_crypt"``.599 600 In order to not have to sort out what you should use,601 passlib offers a number of aliases, that will resolve602 to the most appropriate scheme based on your needs:603 604 * ``"portable"``, ``"portable_apache_24"`` -- pick scheme that's portable across hosts605 running apache >= 2.4. **This will be the default as of Passlib 2.0**.606 607 * ``"portable_apache_22"`` -- pick scheme that's portable across hosts608 running apache >= 2.4. **This is the default up to Passlib 1.9**.609 610 * ``"host"``, ``"host_apache_24"`` -- pick strongest scheme supported by611 apache >= 2.4 and/or host OS.612 613 * ``"host_apache_22"`` -- pick strongest scheme supported by614 apache >= 2.2 and/or host OS.615 616 .. versionadded:: 1.6617 This keyword was previously named ``default``. That alias618 has been deprecated, and will be removed in Passlib 1.8.619 620 .. versionchanged:: 1.6.3621 622 Added support for ``"bcrypt"``, ``"sha256_crypt"``, and ``"portable"`` alias.623 624 .. versionchanged:: 1.7625 626 Added apache 2.4 semantics, and additional aliases.627 628 :type context: :class:`~passlib.context.CryptContext`629 :param context:630 :class:`!CryptContext` instance used to create631 and verify the hashes found in the htpasswd file.632 The default value is a pre-built context which supports all633 of the hashes officially allowed in an htpasswd file.634 635 This is also exposed as a readonly instance attribute.636 637 .. warning::638 639 This option may be used to add support for non-standard hash640 formats to an htpasswd file. However, the resulting file641 will probably not be usable by another application,642 and particularly not by Apache.643 644 :param autoload:645 Set to ``False`` to prevent the constructor from automatically646 loaded the file from disk.647 648 .. deprecated:: 1.6649 This has been replaced by the *new* keyword.650 Instead of setting ``autoload=False``, you should use651 ``new=True``. Support for this keyword will be removed652 in Passlib 1.8.653 654 :param default:655 Change the default algorithm used to hash new passwords.656 657 .. deprecated:: 1.6658 This has been renamed to *default_scheme* for clarity.659 Support for this alias will be removed in Passlib 1.8.660 661 Loading & Saving662 ================663 .. automethod:: load664 .. automethod:: load_if_changed665 .. automethod:: load_string666 .. automethod:: save667 .. automethod:: to_string668 669 Inspection670 ================671 .. automethod:: users672 .. automethod:: check_password673 .. automethod:: get_hash674 675 Modification676 ================677 .. automethod:: set_password678 .. automethod:: delete679 680 Alternate Constructors681 ======================682 .. automethod:: from_string683 684 Attributes685 ==========686 .. attribute:: path687 688 Path to local file that will be used as the default689 for all :meth:`load` and :meth:`save` operations.690 May be written to, initialized by the *path* constructor keyword.691 692 .. attribute:: autosave693 694 Writeable flag indicating whether changes will be automatically695 written to *path*.696 697 Errors698 ======699 :raises ValueError:700 All of the methods in this class will raise a :exc:`ValueError` if701 any user name contains a forbidden character (one of ``:\\r\\n\\t\\x00``),702 or is longer than 255 characters.703 """704 #===================================================================705 # instance attrs706 #===================================================================707 708 # NOTE: _records map stores <user> for the key, and <hash> for the value,709 # both in bytes which use self.encoding710 711 #===================================================================712 # init & serialization713 #===================================================================714 def __init__(self, path=None, default_scheme=None, context=htpasswd_context,715 **kwds):716 if 'default' in kwds:717 warn("``default`` is deprecated as of Passlib 1.6, "718 "and will be removed in Passlib 1.8, it has been renamed "719 "to ``default_scheem``.",720 DeprecationWarning, stacklevel=2)721 default_scheme = kwds.pop("default")722 if default_scheme:723 if default_scheme in _warn_no_bcrypt:724 warn("HtpasswdFile: no bcrypt backends available, "725 "using fallback for default scheme %r" % default_scheme,726 exc.PasslibSecurityWarning)727 default_scheme = htpasswd_defaults.get(default_scheme, default_scheme)728 context = context.copy(default=default_scheme)729 self.context = context730 super(HtpasswdFile, self).__init__(path, **kwds)731 732 def _parse_record(self, record, lineno):733 # NOTE: should return (user, hash) tuple734 result = record.rstrip().split(_BCOLON)735 if len(result) != 2:736 raise ValueError("malformed htpasswd file (error reading line %d)"737 % lineno)738 return result739 740 def _render_record(self, user, hash):741 return render_bytes("%s:%s\n", user, hash)742 743 #===================================================================744 # public methods745 #===================================================================746 747 def users(self):748 """749 Return list of all users in database750 """751 return [self._decode_field(user) for user in self._records]752 753 ##def has_user(self, user):754 ## "check whether entry is present for user"755 ## return self._encode_user(user) in self._records756 757 ##def rename(self, old, new):758 ## """rename user account"""759 ## old = self._encode_user(old)760 ## new = self._encode_user(new)761 ## hash = self._records.pop(old)762 ## self._records[new] = hash763 ## self._autosave()764 765 def set_password(self, user, password):766 """Set password for user; adds user if needed.767 768 :returns:769 * ``True`` if existing user was updated.770 * ``False`` if user account was added.771 772 .. versionchanged:: 1.6773 This method was previously called ``update``, it was renamed774 to prevent ambiguity with the dictionary method.775 The old alias is deprecated, and will be removed in Passlib 1.8.776 """777 hash = self.context.hash(password)778 return self.set_hash(user, hash)779 780 @deprecated_method(deprecated="1.6", removed="1.8",781 replacement="set_password")782 def update(self, user, password):783 """set password for user"""784 return self.set_password(user, password)785 786 def get_hash(self, user):787 """Return hash stored for user, or ``None`` if user not found.788 789 .. versionchanged:: 1.6790 This method was previously named ``find``, it was renamed791 for clarity. The old name is deprecated, and will be removed792 in Passlib 1.8.793 """794 try:795 return self._records[self._encode_user(user)]796 except KeyError:797 return None798 799 def set_hash(self, user, hash):800 """801 semi-private helper which allows writing a hash directly;802 adds user if needed.803 804 .. warning::805 does not (currently) do any validation of the hash string806 807 .. versionadded:: 1.7808 """809 # assert self.context.identify(hash), "unrecognized hash format"810 if PY3 and isinstance(hash, str):811 hash = hash.encode(self.encoding)812 user = self._encode_user(user)813 existing = self._set_record(user, hash)814 self._autosave()815 return existing816 817 @deprecated_method(deprecated="1.6", removed="1.8",818 replacement="get_hash")819 def find(self, user):820 """return hash for user"""821 return self.get_hash(user)822 823 # XXX: rename to something more explicit, like delete_user()?824 def delete(self, user):825 """Delete user's entry.826 827 :returns:828 * ``True`` if user deleted.829 * ``False`` if user not found.830 """831 try:832 del self._records[self._encode_user(user)]833 except KeyError:834 return False835 self._autosave()836 return True837 838 def check_password(self, user, password):839 """840 Verify password for specified user.841 If algorithm marked as deprecated by CryptContext, will automatically be re-hashed.842 843 :returns:844 * ``None`` if user not found.845 * ``False`` if user found, but password does not match.846 * ``True`` if user found and password matches.847 848 .. versionchanged:: 1.6849 This method was previously called ``verify``, it was renamed850 to prevent ambiguity with the :class:`!CryptContext` method.851 The old alias is deprecated, and will be removed in Passlib 1.8.852 """853 user = self._encode_user(user)854 hash = self._records.get(user)855 if hash is None:856 return None857 if isinstance(password, unicode):858 # NOTE: encoding password to match file, making the assumption859 # that server will use same encoding to hash the password.860 password = password.encode(self.encoding)861 ok, new_hash = self.context.verify_and_update(password, hash)862 if ok and new_hash is not None:863 # rehash user's password if old hash was deprecated864 assert user in self._records # otherwise would have to use ._set_record()865 self._records[user] = new_hash866 self._autosave()867 return ok868 869 @deprecated_method(deprecated="1.6", removed="1.8",870 replacement="check_password")871 def verify(self, user, password):872 """verify password for user"""873 return self.check_password(user, password)874 875 #===================================================================876 # eoc877 #===================================================================878 879#=============================================================================880# htdigest editing881#=============================================================================882class HtdigestFile(_CommonFile):883 """class for reading & writing Htdigest files.884 885 The class constructor accepts the following arguments:886 887 :type path: filepath888 :param path:889 890 Specifies path to htdigest file, use to implicitly load from and save to.891 892 This class has two modes of operation:893 894 1. It can be "bound" to a local file by passing a ``path`` to the class895 constructor. In this case it will load the contents of the file when896 created, and the :meth:`load` and :meth:`save` methods will automatically897 load from and save to that file if they are called without arguments.898 899 2. Alternately, it can exist as an independant object, in which case900 :meth:`load` and :meth:`save` will require an explicit path to be901 provided whenever they are called. As well, ``autosave`` behavior902 will not be available.903 904 This feature is new in Passlib 1.6, and is the default if no905 ``path`` value is provided to the constructor.906 907 This is also exposed as a readonly instance attribute.908 909 :type default_realm: str910 :param default_realm:911 912 If ``default_realm`` is set, all the :class:`HtdigestFile`913 methods that require a realm will use this value if one is not914 provided explicitly. If unset, they will raise an error stating915 that an explicit realm is required.916 917 This is also exposed as a writeable instance attribute.918 919 .. versionadded:: 1.6920 921 :type new: bool922 :param new:923 924 Normally, if *path* is specified, :class:`HtdigestFile` will925 immediately load the contents of the file. However, when creating926 a new htpasswd file, applications can set ``new=True`` so that927 the existing file (if any) will not be loaded.928 929 .. versionadded:: 1.6930 This feature was previously enabled by setting ``autoload=False``.931 That alias has been deprecated, and will be removed in Passlib 1.8932 933 :type autosave: bool934 :param autosave:935 936 Normally, any changes made to an :class:`HtdigestFile` instance937 will not be saved until :meth:`save` is explicitly called. However,938 if ``autosave=True`` is specified, any changes made will be939 saved to disk immediately (assuming *path* has been set).940 941 This is also exposed as a writeable instance attribute.942 943 :type encoding: str944 :param encoding:945 946 Optionally specify character encoding used to read/write file947 and hash passwords. Defaults to ``utf-8``, though ``latin-1``948 is the only other commonly encountered encoding.949 950 This is also exposed as a readonly instance attribute.951 952 :param autoload:953 Set to ``False`` to prevent the constructor from automatically954 loaded the file from disk.955 956 .. deprecated:: 1.6957 This has been replaced by the *new* keyword.958 Instead of setting ``autoload=False``, you should use959 ``new=True``. Support for this keyword will be removed960 in Passlib 1.8.961 962 Loading & Saving963 ================964 .. automethod:: load965 .. automethod:: load_if_changed966 .. automethod:: load_string967 .. automethod:: save968 .. automethod:: to_string969 970 Inspection971 ==========972 .. automethod:: realms973 .. automethod:: users974 .. automethod:: check_password(user[, realm], password)975 .. automethod:: get_hash976 977 Modification978 ============979 .. automethod:: set_password(user[, realm], password)980 .. automethod:: delete981 .. automethod:: delete_realm982 983 Alternate Constructors984 ======================985 .. automethod:: from_string986 987 Attributes988 ==========989 .. attribute:: default_realm990 991 The default realm that will be used if one is not provided992 to methods that require it. By default this is ``None``,993 in which case an explicit realm must be provided for every994 method call. Can be written to.995 996 .. attribute:: path997 998 Path to local file that will be used as the default999 for all :meth:`load` and :meth:`save` operations.1000 May be written to, initialized by the *path* constructor keyword.1001 1002 .. attribute:: autosave1003 1004 Writeable flag indicating whether changes will be automatically1005 written to *path*.1006 1007 Errors1008 ======1009 :raises ValueError:1010 All of the methods in this class will raise a :exc:`ValueError` if1011 any user name or realm contains a forbidden character (one of ``:\\r\\n\\t\\x00``),1012 or is longer than 255 characters.1013 """1014 #===================================================================1015 # instance attrs1016 #===================================================================1017 1018 # NOTE: _records map stores (<user>,<realm>) for the key,1019 # and <hash> as the value, all as <self.encoding> bytes.1020 1021 # NOTE: unlike htpasswd, this class doesn't use a CryptContext,1022 # as only one hash format is supported: htdigest.1023 1024 # optionally specify default realm that will be used if none1025 # is provided to a method call. otherwise realm is always required.1026 default_realm = None1027 1028 #===================================================================1029 # init & serialization1030 #===================================================================1031 def __init__(self, path=None, default_realm=None, **kwds):1032 self.default_realm = default_realm1033 super(HtdigestFile, self).__init__(path, **kwds)1034 1035 def _parse_record(self, record, lineno):1036 result = record.rstrip().split(_BCOLON)1037 if len(result) != 3:1038 raise ValueError("malformed htdigest file (error reading line %d)"1039 % lineno)1040 user, realm, hash = result1041 return (user, realm), hash1042 1043 def _render_record(self, key, hash):1044 user, realm = key1045 return render_bytes("%s:%s:%s\n", user, realm, hash)1046 1047 def _require_realm(self, realm):1048 if realm is None:1049 realm = self.default_realm1050 if realm is None:1051 raise TypeError("you must specify a realm explicitly, "1052 "or set the default_realm attribute")1053 return realm1054 1055 def _encode_realm(self, realm):1056 realm = self._require_realm(realm)1057 return self._encode_field(realm, "realm")1058 1059 def _encode_key(self, user, realm):1060 return self._encode_user(user), self._encode_realm(realm)1061 1062 #===================================================================1063 # public methods1064 #===================================================================1065 1066 def realms(self):1067 """Return list of all realms in database"""1068 realms = set(key[1] for key in self._records)1069 return [self._decode_field(realm) for realm in realms]1070 1071 def users(self, realm=None):1072 """Return list of all users in specified realm.1073 1074 * uses ``self.default_realm`` if no realm explicitly provided.1075 * returns empty list if realm not found.1076 """1077 realm = self._encode_realm(realm)1078 return [self._decode_field(key[0]) for key in self._records1079 if key[1] == realm]1080 1081 ##def has_user(self, user, realm=None):1082 ## "check if user+realm combination exists"1083 ## return self._encode_key(user,realm) in self._records1084 1085 ##def rename_realm(self, old, new):1086 ## """rename all accounts in realm"""1087 ## old = self._encode_realm(old)1088 ## new = self._encode_realm(new)1089 ## keys = [key for key in self._records if key[1] == old]1090 ## for key in keys:1091 ## hash = self._records.pop(key)1092 ## self._set_record((key[0], new), hash)1093 ## self._autosave()1094 ## return len(keys)1095 1096 ##def rename(self, old, new, realm=None):1097 ## """rename user account"""1098 ## old = self._encode_user(old)1099 ## new = self._encode_user(new)1100 ## realm = self._encode_realm(realm)1101 ## hash = self._records.pop((old,realm))1102 ## self._set_record((new, realm), hash)1103 ## self._autosave()1104 1105 def set_password(self, user, realm=None, password=_UNSET):1106 """Set password for user; adds user & realm if needed.1107 1108 If ``self.default_realm`` has been set, this may be called1109 with the syntax ``set_password(user, password)``,1110 otherwise it must be called with all three arguments:1111 ``set_password(user, realm, password)``.1112 1113 :returns:1114 * ``True`` if existing user was updated1115 * ``False`` if user account added.1116 """1117 if password is _UNSET:1118 # called w/ two args - (user, password), use default realm1119 realm, password = None, realm1120 realm = self._require_realm(realm)1121 hash = htdigest.hash(password, user, realm, encoding=self.encoding)1122 return self.set_hash(user, realm, hash)1123 1124 @deprecated_method(deprecated="1.6", removed="1.8",1125 replacement="set_password")1126 def update(self, user, realm, password):1127 """set password for user"""1128 return self.set_password(user, realm, password)1129 1130 def get_hash(self, user, realm=None):1131 """Return :class:`~passlib.hash.htdigest` hash stored for user.1132 1133 * uses ``self.default_realm`` if no realm explicitly provided.1134 * returns ``None`` if user or realm not found.1135 1136 .. versionchanged:: 1.61137 This method was previously named ``find``, it was renamed1138 for clarity. The old name is deprecated, and will be removed1139 in Passlib 1.8.1140 """1141 key = self._encode_key(user, realm)1142 hash = self._records.get(key)1143 if hash is None:1144 return None1145 if PY3:1146 hash = hash.decode(self.encoding)1147 return hash1148 1149 def set_hash(self, user, realm=None, hash=_UNSET):1150 """1151 semi-private helper which allows writing a hash directly;1152 adds user & realm if needed.1153 1154 If ``self.default_realm`` has been set, this may be called1155 with the syntax ``set_hash(user, hash)``,1156 otherwise it must be called with all three arguments:1157 ``set_hash(user, realm, hash)``.1158 1159 .. warning::1160 does not (currently) do any validation of the hash string1161 1162 .. versionadded:: 1.71163 """1164 if hash is _UNSET:1165 # called w/ two args - (user, hash), use default realm1166 realm, hash = None, realm1167 # assert htdigest.identify(hash), "unrecognized hash format"1168 if PY3 and isinstance(hash, str):1169 hash = hash.encode(self.encoding)1170 key = self._encode_key(user, realm)1171 existing = self._set_record(key, hash)1172 self._autosave()1173 return existing1174 1175 @deprecated_method(deprecated="1.6", removed="1.8",1176 replacement="get_hash")1177 def find(self, user, realm):1178 """return hash for user"""1179 return self.get_hash(user, realm)1180 1181 # XXX: rename to something more explicit, like delete_user()?1182 def delete(self, user, realm=None):1183 """Delete user's entry for specified realm.1184 1185 if realm is not specified, uses ``self.default_realm``.1186 1187 :returns:1188 * ``True`` if user deleted,1189 * ``False`` if user not found in realm.1190 """1191 key = self._encode_key(user, realm)1192 try:1193 del self._records[key]1194 except KeyError:1195 return False1196 self._autosave()1197 return True1198 1199 def delete_realm(self, realm):1200 """Delete all users for specified realm.