Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
apache.py1256 linesDownload Raw Back to passlib
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.

Showing the first 1,200 of 1256 lines. Download the file for the rest.

codekingpro/portable-devtools · Team Ai