diff options
author | shmel1k <shmel1k@ydb.tech> | 2023-11-26 18:16:14 +0300 |
---|---|---|
committer | shmel1k <shmel1k@ydb.tech> | 2023-11-26 18:43:30 +0300 |
commit | b8cf9e88f4c5c64d9406af533d8948deb050d695 (patch) | |
tree | 218eb61fb3c3b96ec08b4d8cdfef383104a87d63 /contrib/python/Twisted/py2/twisted/cred | |
parent | 523f645a83a0ec97a0332dbc3863bb354c92a328 (diff) | |
download | ydb-b8cf9e88f4c5c64d9406af533d8948deb050d695.tar.gz |
add kikimr_configure
Diffstat (limited to 'contrib/python/Twisted/py2/twisted/cred')
-rw-r--r-- | contrib/python/Twisted/py2/twisted/cred/__init__.py | 7 | ||||
-rw-r--r-- | contrib/python/Twisted/py2/twisted/cred/_digest.py | 132 | ||||
-rw-r--r-- | contrib/python/Twisted/py2/twisted/cred/checkers.py | 329 | ||||
-rw-r--r-- | contrib/python/Twisted/py2/twisted/cred/credentials.py | 510 | ||||
-rw-r--r-- | contrib/python/Twisted/py2/twisted/cred/error.py | 45 | ||||
-rw-r--r-- | contrib/python/Twisted/py2/twisted/cred/portal.py | 124 | ||||
-rw-r--r-- | contrib/python/Twisted/py2/twisted/cred/strcred.py | 272 |
7 files changed, 1419 insertions, 0 deletions
diff --git a/contrib/python/Twisted/py2/twisted/cred/__init__.py b/contrib/python/Twisted/py2/twisted/cred/__init__.py new file mode 100644 index 0000000000..2ee268c5e9 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/cred/__init__.py @@ -0,0 +1,7 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Twisted Cred: Support for verifying credentials, and providing services to user +based on those credentials. +""" diff --git a/contrib/python/Twisted/py2/twisted/cred/_digest.py b/contrib/python/Twisted/py2/twisted/cred/_digest.py new file mode 100644 index 0000000000..4ed1ce317a --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/cred/_digest.py @@ -0,0 +1,132 @@ +# -*- test-case-name: twisted.cred.test.test_digestauth -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Calculations for HTTP Digest authentication. + +@see: U{http://www.faqs.org/rfcs/rfc2617.html} +""" + +from __future__ import division, absolute_import + +from binascii import hexlify +from hashlib import md5, sha1 + + + +# The digest math + +algorithms = { + b'md5': md5, + + # md5-sess is more complicated than just another algorithm. It requires + # H(A1) state to be remembered from the first WWW-Authenticate challenge + # issued and re-used to process any Authorization header in response to + # that WWW-Authenticate challenge. It is *not* correct to simply + # recalculate H(A1) each time an Authorization header is received. Read + # RFC 2617, section 3.2.2.2 and do not try to make DigestCredentialFactory + # support this unless you completely understand it. -exarkun + b'md5-sess': md5, + + b'sha': sha1, +} + +# DigestCalcHA1 +def calcHA1(pszAlg, pszUserName, pszRealm, pszPassword, pszNonce, pszCNonce, + preHA1=None): + """ + Compute H(A1) from RFC 2617. + + @param pszAlg: The name of the algorithm to use to calculate the digest. + Currently supported are md5, md5-sess, and sha. + @param pszUserName: The username + @param pszRealm: The realm + @param pszPassword: The password + @param pszNonce: The nonce + @param pszCNonce: The cnonce + + @param preHA1: If available this is a str containing a previously + calculated H(A1) as a hex string. If this is given then the values for + pszUserName, pszRealm, and pszPassword must be L{None} and are ignored. + """ + + if (preHA1 and (pszUserName or pszRealm or pszPassword)): + raise TypeError(("preHA1 is incompatible with the pszUserName, " + "pszRealm, and pszPassword arguments")) + + if preHA1 is None: + # We need to calculate the HA1 from the username:realm:password + m = algorithms[pszAlg]() + m.update(pszUserName) + m.update(b":") + m.update(pszRealm) + m.update(b":") + m.update(pszPassword) + HA1 = hexlify(m.digest()) + else: + # We were given a username:realm:password + HA1 = preHA1 + + if pszAlg == b"md5-sess": + m = algorithms[pszAlg]() + m.update(HA1) + m.update(b":") + m.update(pszNonce) + m.update(b":") + m.update(pszCNonce) + HA1 = hexlify(m.digest()) + + return HA1 + + +def calcHA2(algo, pszMethod, pszDigestUri, pszQop, pszHEntity): + """ + Compute H(A2) from RFC 2617. + + @param pszAlg: The name of the algorithm to use to calculate the digest. + Currently supported are md5, md5-sess, and sha. + @param pszMethod: The request method. + @param pszDigestUri: The request URI. + @param pszQop: The Quality-of-Protection value. + @param pszHEntity: The hash of the entity body or L{None} if C{pszQop} is + not C{'auth-int'}. + @return: The hash of the A2 value for the calculation of the response + digest. + """ + m = algorithms[algo]() + m.update(pszMethod) + m.update(b":") + m.update(pszDigestUri) + if pszQop == b"auth-int": + m.update(b":") + m.update(pszHEntity) + return hexlify(m.digest()) + + +def calcResponse(HA1, HA2, algo, pszNonce, pszNonceCount, pszCNonce, pszQop): + """ + Compute the digest for the given parameters. + + @param HA1: The H(A1) value, as computed by L{calcHA1}. + @param HA2: The H(A2) value, as computed by L{calcHA2}. + @param pszNonce: The challenge nonce. + @param pszNonceCount: The (client) nonce count value for this response. + @param pszCNonce: The client nonce. + @param pszQop: The Quality-of-Protection value. + """ + m = algorithms[algo]() + m.update(HA1) + m.update(b":") + m.update(pszNonce) + m.update(b":") + if pszNonceCount and pszCNonce: + m.update(pszNonceCount) + m.update(b":") + m.update(pszCNonce) + m.update(b":") + m.update(pszQop) + m.update(b":") + m.update(HA2) + respHash = hexlify(m.digest()) + return respHash diff --git a/contrib/python/Twisted/py2/twisted/cred/checkers.py b/contrib/python/Twisted/py2/twisted/cred/checkers.py new file mode 100644 index 0000000000..7a29809cb6 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/cred/checkers.py @@ -0,0 +1,329 @@ +# -*- test-case-name: twisted.cred.test.test_cred -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Basic credential checkers + +@var ANONYMOUS: An empty tuple used to represent the anonymous avatar ID. +""" + +from __future__ import division, absolute_import + +import os + +from zope.interface import implementer, Interface, Attribute + +from twisted.logger import Logger +from twisted.internet import defer +from twisted.python import failure +from twisted.cred import error, credentials + + + +class ICredentialsChecker(Interface): + """ + An object that can check sub-interfaces of L{ICredentials}. + """ + + credentialInterfaces = Attribute(( + 'A list of sub-interfaces of L{ICredentials} which specifies which I ' + 'may check.' + )) + + + def requestAvatarId(credentials): + """ + Validate credentials and produce an avatar ID. + + @param credentials: something which implements one of the interfaces in + C{credentialInterfaces}. + + @return: a L{Deferred} which will fire with a L{bytes} that identifies + an avatar, an empty tuple to specify an authenticated anonymous user + (provided as L{twisted.cred.checkers.ANONYMOUS}) or fail with + L{UnauthorizedLogin}. Alternatively, return the result itself. + + @see: L{twisted.cred.credentials} + """ + + + +# A note on anonymity - We do not want None as the value for anonymous +# because it is too easy to accidentally return it. We do not want the +# empty string, because it is too easy to mistype a password file. For +# example, an .htpasswd file may contain the lines: ['hello:asdf', +# 'world:asdf', 'goodbye', ':world']. This misconfiguration will have an +# ill effect in any case, but accidentally granting anonymous access is a +# worse failure mode than simply granting access to an untypeable +# username. We do not want an instance of 'object', because that would +# create potential problems with persistence. + +ANONYMOUS = () + + + +@implementer(ICredentialsChecker) +class AllowAnonymousAccess: + """ + A credentials checker that unconditionally grants anonymous access. + + @cvar credentialInterfaces: Tuple containing L{IAnonymous}. + """ + credentialInterfaces = credentials.IAnonymous, + + def requestAvatarId(self, credentials): + """ + Succeed with the L{ANONYMOUS} avatar ID. + + @return: L{Deferred} that fires with L{twisted.cred.checkers.ANONYMOUS} + """ + return defer.succeed(ANONYMOUS) + + + +@implementer(ICredentialsChecker) +class InMemoryUsernamePasswordDatabaseDontUse(object): + """ + An extremely simple credentials checker. + + This is only of use in one-off test programs or examples which don't + want to focus too much on how credentials are verified. + + You really don't want to use this for anything else. It is, at best, a + toy. If you need a simple credentials checker for a real application, + see L{FilePasswordDB}. + + @cvar credentialInterfaces: Tuple of L{IUsernamePassword} and + L{IUsernameHashedPassword}. + + @ivar users: Mapping of usernames to passwords. + @type users: L{dict} mapping L{bytes} to L{bytes} + """ + credentialInterfaces = (credentials.IUsernamePassword, + credentials.IUsernameHashedPassword) + + def __init__(self, **users): + """ + Initialize the in-memory database. + + For example:: + + db = InMemoryUsernamePasswordDatabaseDontUse( + user1=b'sesame', + user2=b'hunter2', + ) + + @param users: Usernames and passwords to seed the database with. + Each username given as a keyword is encoded to L{bytes} as ASCII. + Passwords must be given as L{bytes}. + @type users: L{dict} of L{str} to L{bytes} + """ + self.users = {x.encode('ascii'): y for x, y in users.items()} + + + def addUser(self, username, password): + """ + Set a user's password. + + @param username: Name of the user. + @type username: L{bytes} + + @param password: Password to associate with the username. + @type password: L{bytes} + """ + self.users[username] = password + + + def _cbPasswordMatch(self, matched, username): + if matched: + return username + else: + return failure.Failure(error.UnauthorizedLogin()) + + + def requestAvatarId(self, credentials): + if credentials.username in self.users: + return defer.maybeDeferred( + credentials.checkPassword, + self.users[credentials.username]).addCallback( + self._cbPasswordMatch, credentials.username) + else: + return defer.fail(error.UnauthorizedLogin()) + + + +@implementer(ICredentialsChecker) +class FilePasswordDB: + """ + A file-based, text-based username/password database. + + Records in the datafile for this class are delimited by a particular + string. The username appears in a fixed field of the columns delimited + by this string, as does the password. Both fields are specifiable. If + the passwords are not stored plaintext, a hash function must be supplied + to convert plaintext passwords to the form stored on disk and this + CredentialsChecker will only be able to check L{IUsernamePassword} + credentials. If the passwords are stored plaintext, + L{IUsernameHashedPassword} credentials will be checkable as well. + """ + + cache = False + _credCache = None + _cacheTimestamp = 0 + _log = Logger() + + def __init__(self, filename, delim=b':', usernameField=0, passwordField=1, + caseSensitive=True, hash=None, cache=False): + """ + @type filename: L{str} + @param filename: The name of the file from which to read username and + password information. + + @type delim: L{bytes} + @param delim: The field delimiter used in the file. + + @type usernameField: L{int} + @param usernameField: The index of the username after splitting a + line on the delimiter. + + @type passwordField: L{int} + @param passwordField: The index of the password after splitting a + line on the delimiter. + + @type caseSensitive: L{bool} + @param caseSensitive: If true, consider the case of the username when + performing a lookup. Ignore it otherwise. + + @type hash: Three-argument callable or L{None} + @param hash: A function used to transform the plaintext password + received over the network to a format suitable for comparison + against the version stored on disk. The arguments to the callable + are the username, the network-supplied password, and the in-file + version of the password. If the return value compares equal to the + version stored on disk, the credentials are accepted. + + @type cache: L{bool} + @param cache: If true, maintain an in-memory cache of the + contents of the password file. On lookups, the mtime of the + file will be checked, and the file will only be re-parsed if + the mtime is newer than when the cache was generated. + """ + self.filename = filename + self.delim = delim + self.ufield = usernameField + self.pfield = passwordField + self.caseSensitive = caseSensitive + self.hash = hash + self.cache = cache + + if self.hash is None: + # The passwords are stored plaintext. We can support both + # plaintext and hashed passwords received over the network. + self.credentialInterfaces = ( + credentials.IUsernamePassword, + credentials.IUsernameHashedPassword + ) + else: + # The passwords are hashed on disk. We can support only + # plaintext passwords received over the network. + self.credentialInterfaces = ( + credentials.IUsernamePassword, + ) + + + def __getstate__(self): + d = dict(vars(self)) + for k in '_credCache', '_cacheTimestamp': + try: + del d[k] + except KeyError: + pass + return d + + + def _cbPasswordMatch(self, matched, username): + if matched: + return username + else: + return failure.Failure(error.UnauthorizedLogin()) + + + def _loadCredentials(self): + """ + Loads the credentials from the configured file. + + @return: An iterable of C{username, password} couples. + @rtype: C{iterable} + + @raise UnauthorizedLogin: when failing to read the credentials from the + file. + """ + try: + with open(self.filename, "rb") as f: + for line in f: + line = line.rstrip() + parts = line.split(self.delim) + + if self.ufield >= len(parts) or self.pfield >= len(parts): + continue + if self.caseSensitive: + yield parts[self.ufield], parts[self.pfield] + else: + yield parts[self.ufield].lower(), parts[self.pfield] + except IOError as e: + self._log.error("Unable to load credentials db: {e!r}", e=e) + raise error.UnauthorizedLogin() + + + def getUser(self, username): + """ + Look up the credentials for a username. + + @param username: The username to look up. + @type username: L{bytes} + + @returns: Two-tuple of the canonicalicalized username (i.e. lowercase + if the database is not case sensitive) and the associated password + value, both L{bytes}. + @rtype: L{tuple} + + @raises KeyError: When lookup of the username fails. + """ + if not self.caseSensitive: + username = username.lower() + + if self.cache: + if self._credCache is None or os.path.getmtime(self.filename) > self._cacheTimestamp: + self._cacheTimestamp = os.path.getmtime(self.filename) + self._credCache = dict(self._loadCredentials()) + return username, self._credCache[username] + else: + for u, p in self._loadCredentials(): + if u == username: + return u, p + raise KeyError(username) + + + def requestAvatarId(self, c): + try: + u, p = self.getUser(c.username) + except KeyError: + return defer.fail(error.UnauthorizedLogin()) + else: + up = credentials.IUsernamePassword(c, None) + if self.hash: + if up is not None: + h = self.hash(up.username, up.password, p) + if h == p: + return defer.succeed(u) + return defer.fail(error.UnauthorizedLogin()) + else: + return defer.maybeDeferred(c.checkPassword, p + ).addCallback(self._cbPasswordMatch, u) + + + +# For backwards compatibility +# Allow access as the old name. +OnDiskUsernamePasswordDatabase = FilePasswordDB diff --git a/contrib/python/Twisted/py2/twisted/cred/credentials.py b/contrib/python/Twisted/py2/twisted/cred/credentials.py new file mode 100644 index 0000000000..5469e51587 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/cred/credentials.py @@ -0,0 +1,510 @@ +# -*- test-case-name: twisted.cred.test.test_cred-*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +This module defines L{ICredentials}, an interface for objects that represent +authentication credentials to provide, and also includes a number of useful +implementations of that interface. +""" + +from __future__ import division, absolute_import + +from zope.interface import implementer, Interface + +import base64 +import hmac +import random +import re +import time + +from binascii import hexlify +from hashlib import md5 + +from twisted.python.randbytes import secureRandom +from twisted.python.compat import networkString, nativeString +from twisted.python.compat import intToBytes, unicode +from twisted.cred._digest import calcResponse, calcHA1, calcHA2 +from twisted.cred import error + + + +class ICredentials(Interface): + """ + I check credentials. + + Implementors I{must} specify the sub-interfaces of ICredentials + to which it conforms, using L{zope.interface.declarations.implementer}. + """ + + + +class IUsernameDigestHash(ICredentials): + """ + This credential is used when a CredentialChecker has access to the hash + of the username:realm:password as in an Apache .htdigest file. + """ + def checkHash(digestHash): + """ + @param digestHash: The hashed username:realm:password to check against. + + @return: C{True} if the credentials represented by this object match + the given hash, C{False} if they do not, or a L{Deferred} which + will be called back with one of these values. + """ + + + +class IUsernameHashedPassword(ICredentials): + """ + I encapsulate a username and a hashed password. + + This credential is used when a hashed password is received from the + party requesting authentication. CredentialCheckers which check this + kind of credential must store the passwords in plaintext (or as + password-equivalent hashes) form so that they can be hashed in a manner + appropriate for the particular credentials class. + + @type username: L{bytes} + @ivar username: The username associated with these credentials. + """ + + def checkPassword(password): + """ + Validate these credentials against the correct password. + + @type password: L{bytes} + @param password: The correct, plaintext password against which to + check. + + @rtype: C{bool} or L{Deferred} + @return: C{True} if the credentials represented by this object match the + given password, C{False} if they do not, or a L{Deferred} which will + be called back with one of these values. + """ + + + +class IUsernamePassword(ICredentials): + """ + I encapsulate a username and a plaintext password. + + This encapsulates the case where the password received over the network + has been hashed with the identity function (That is, not at all). The + CredentialsChecker may store the password in whatever format it desires, + it need only transform the stored password in a similar way before + performing the comparison. + + @type username: L{bytes} + @ivar username: The username associated with these credentials. + + @type password: L{bytes} + @ivar password: The password associated with these credentials. + """ + + def checkPassword(password): + """ + Validate these credentials against the correct password. + + @type password: L{bytes} + @param password: The correct, plaintext password against which to + check. + + @rtype: C{bool} or L{Deferred} + @return: C{True} if the credentials represented by this object match the + given password, C{False} if they do not, or a L{Deferred} which will + be called back with one of these values. + """ + + + +class IAnonymous(ICredentials): + """ + I am an explicitly anonymous request for access. + + @see: L{twisted.cred.checkers.AllowAnonymousAccess} + """ + + + +@implementer(IUsernameHashedPassword, IUsernameDigestHash) +class DigestedCredentials(object): + """ + Yet Another Simple HTTP Digest authentication scheme. + """ + + def __init__(self, username, method, realm, fields): + self.username = username + self.method = method + self.realm = realm + self.fields = fields + + + def checkPassword(self, password): + """ + Verify that the credentials represented by this object agree with the + given plaintext C{password} by hashing C{password} in the same way the + response hash represented by this object was generated and comparing + the results. + """ + response = self.fields.get('response') + uri = self.fields.get('uri') + nonce = self.fields.get('nonce') + cnonce = self.fields.get('cnonce') + nc = self.fields.get('nc') + algo = self.fields.get('algorithm', b'md5').lower() + qop = self.fields.get('qop', b'auth') + + expected = calcResponse( + calcHA1(algo, self.username, self.realm, password, nonce, cnonce), + calcHA2(algo, self.method, uri, qop, None), + algo, nonce, nc, cnonce, qop) + + return expected == response + + + def checkHash(self, digestHash): + """ + Verify that the credentials represented by this object agree with the + credentials represented by the I{H(A1)} given in C{digestHash}. + + @param digestHash: A precomputed H(A1) value based on the username, + realm, and password associate with this credentials object. + """ + response = self.fields.get('response') + uri = self.fields.get('uri') + nonce = self.fields.get('nonce') + cnonce = self.fields.get('cnonce') + nc = self.fields.get('nc') + algo = self.fields.get('algorithm', b'md5').lower() + qop = self.fields.get('qop', b'auth') + + expected = calcResponse( + calcHA1(algo, None, None, None, nonce, cnonce, preHA1=digestHash), + calcHA2(algo, self.method, uri, qop, None), + algo, nonce, nc, cnonce, qop) + + return expected == response + + + +class DigestCredentialFactory(object): + """ + Support for RFC2617 HTTP Digest Authentication + + @cvar CHALLENGE_LIFETIME_SECS: The number of seconds for which an + opaque should be valid. + + @type privateKey: L{bytes} + @ivar privateKey: A random string used for generating the secure opaque. + + @type algorithm: L{bytes} + @param algorithm: Case insensitive string specifying the hash algorithm to + use. Must be either C{'md5'} or C{'sha'}. C{'md5-sess'} is B{not} + supported. + + @type authenticationRealm: L{bytes} + @param authenticationRealm: case sensitive string that specifies the realm + portion of the challenge + """ + + _parseparts = re.compile( + b'([^= ]+)' # The key + b'=' # Conventional key/value separator (literal) + b'(?:' # Group together a couple options + b'"([^"]*)"' # A quoted string of length 0 or more + b'|' # The other option in the group is coming + b'([^,]+)' # An unquoted string of length 1 or more, up to a comma + b')' # That non-matching group ends + b',?') # There might be a comma at the end (none on last pair) + + CHALLENGE_LIFETIME_SECS = 15 * 60 # 15 minutes + + scheme = b"digest" + + def __init__(self, algorithm, authenticationRealm): + self.algorithm = algorithm + self.authenticationRealm = authenticationRealm + self.privateKey = secureRandom(12) + + + def getChallenge(self, address): + """ + Generate the challenge for use in the WWW-Authenticate header. + + @param address: The client address to which this challenge is being + sent. + + @return: The L{dict} that can be used to generate a WWW-Authenticate + header. + """ + c = self._generateNonce() + o = self._generateOpaque(c, address) + + return {'nonce': c, + 'opaque': o, + 'qop': b'auth', + 'algorithm': self.algorithm, + 'realm': self.authenticationRealm} + + + def _generateNonce(self): + """ + Create a random value suitable for use as the nonce parameter of a + WWW-Authenticate challenge. + + @rtype: L{bytes} + """ + return hexlify(secureRandom(12)) + + + def _getTime(self): + """ + Parameterize the time based seed used in C{_generateOpaque} + so we can deterministically unittest it's behavior. + """ + return time.time() + + + def _generateOpaque(self, nonce, clientip): + """ + Generate an opaque to be returned to the client. This is a unique + string that can be returned to us and verified. + """ + # Now, what we do is encode the nonce, client ip and a timestamp in the + # opaque value with a suitable digest. + now = intToBytes(int(self._getTime())) + + if not clientip: + clientip = b'' + elif isinstance(clientip, unicode): + clientip = clientip.encode('ascii') + + key = b",".join((nonce, clientip, now)) + digest = hexlify(md5(key + self.privateKey).digest()) + ekey = base64.b64encode(key) + return b"-".join((digest, ekey.replace(b'\n', b''))) + + + def _verifyOpaque(self, opaque, nonce, clientip): + """ + Given the opaque and nonce from the request, as well as the client IP + that made the request, verify that the opaque was generated by us. + And that it's not too old. + + @param opaque: The opaque value from the Digest response + @param nonce: The nonce value from the Digest response + @param clientip: The remote IP address of the client making the request + or L{None} if the request was submitted over a channel where this + does not make sense. + + @return: C{True} if the opaque was successfully verified. + + @raise error.LoginFailed: if C{opaque} could not be parsed or + contained the wrong values. + """ + # First split the digest from the key + opaqueParts = opaque.split(b'-') + if len(opaqueParts) != 2: + raise error.LoginFailed('Invalid response, invalid opaque value') + + if not clientip: + clientip = b'' + elif isinstance(clientip, unicode): + clientip = clientip.encode('ascii') + + # Verify the key + key = base64.b64decode(opaqueParts[1]) + keyParts = key.split(b',') + + if len(keyParts) != 3: + raise error.LoginFailed('Invalid response, invalid opaque value') + + if keyParts[0] != nonce: + raise error.LoginFailed( + 'Invalid response, incompatible opaque/nonce values') + + if keyParts[1] != clientip: + raise error.LoginFailed( + 'Invalid response, incompatible opaque/client values') + + try: + when = int(keyParts[2]) + except ValueError: + raise error.LoginFailed( + 'Invalid response, invalid opaque/time values') + + if (int(self._getTime()) - when > + DigestCredentialFactory.CHALLENGE_LIFETIME_SECS): + + raise error.LoginFailed( + 'Invalid response, incompatible opaque/nonce too old') + + # Verify the digest + digest = hexlify(md5(key + self.privateKey).digest()) + if digest != opaqueParts[0]: + raise error.LoginFailed('Invalid response, invalid opaque value') + + return True + + + def decode(self, response, method, host): + """ + Decode the given response and attempt to generate a + L{DigestedCredentials} from it. + + @type response: L{bytes} + @param response: A string of comma separated key=value pairs + + @type method: L{bytes} + @param method: The action requested to which this response is addressed + (GET, POST, INVITE, OPTIONS, etc). + + @type host: L{bytes} + @param host: The address the request was sent from. + + @raise error.LoginFailed: If the response does not contain a username, + a nonce, an opaque, or if the opaque is invalid. + + @return: L{DigestedCredentials} + """ + response = b' '.join(response.splitlines()) + parts = self._parseparts.findall(response) + auth = {} + for (key, bare, quoted) in parts: + value = (quoted or bare).strip() + auth[nativeString(key.strip())] = value + + username = auth.get('username') + if not username: + raise error.LoginFailed('Invalid response, no username given.') + + if 'opaque' not in auth: + raise error.LoginFailed('Invalid response, no opaque given.') + + if 'nonce' not in auth: + raise error.LoginFailed('Invalid response, no nonce given.') + + # Now verify the nonce/opaque values for this client + if self._verifyOpaque(auth.get('opaque'), auth.get('nonce'), host): + return DigestedCredentials(username, + method, + self.authenticationRealm, + auth) + + + +@implementer(IUsernameHashedPassword) +class CramMD5Credentials(object): + """ + An encapsulation of some CramMD5 hashed credentials. + + @ivar challenge: The challenge to be sent to the client. + @type challenge: L{bytes} + + @ivar response: The hashed response from the client. + @type response: L{bytes} + + @ivar username: The username from the response from the client. + @type username: L{bytes} or L{None} if not yet provided. + """ + username = None + challenge = b'' + response = b'' + + def __init__(self, host=None): + self.host = host + + + def getChallenge(self): + if self.challenge: + return self.challenge + # The data encoded in the first ready response contains an + # presumptively arbitrary string of random digits, a timestamp, and + # the fully-qualified primary host name of the server. The syntax of + # the unencoded form must correspond to that of an RFC 822 'msg-id' + # [RFC822] as described in [POP3]. + # -- RFC 2195 + r = random.randrange(0x7fffffff) + t = time.time() + self.challenge = networkString('<%d.%d@%s>' % ( + r, t, nativeString(self.host) if self.host else None)) + return self.challenge + + + def setResponse(self, response): + self.username, self.response = response.split(None, 1) + + + def moreChallenges(self): + return False + + + def checkPassword(self, password): + verify = hexlify(hmac.HMAC(password, self.challenge).digest()) + return verify == self.response + + + +@implementer(IUsernameHashedPassword) +class UsernameHashedPassword: + + def __init__(self, username, hashed): + self.username = username + self.hashed = hashed + + def checkPassword(self, password): + return self.hashed == password + + + +@implementer(IUsernamePassword) +class UsernamePassword: + + def __init__(self, username, password): + self.username = username + self.password = password + + def checkPassword(self, password): + return self.password == password + + + +@implementer(IAnonymous) +class Anonymous: + pass + + + +class ISSHPrivateKey(ICredentials): + """ + L{ISSHPrivateKey} credentials encapsulate an SSH public key to be checked + against a user's private key. + + @ivar username: The username associated with these credentials. + @type username: L{bytes} + + @ivar algName: The algorithm name for the blob. + @type algName: L{bytes} + + @ivar blob: The public key blob as sent by the client. + @type blob: L{bytes} + + @ivar sigData: The data the signature was made from. + @type sigData: L{bytes} + + @ivar signature: The signed data. This is checked to verify that the user + owns the private key. + @type signature: L{bytes} or L{None} + """ + + + +@implementer(ISSHPrivateKey) +class SSHPrivateKey: + def __init__(self, username, algName, blob, sigData, signature): + self.username = username + self.algName = algName + self.blob = blob + self.sigData = sigData + self.signature = signature diff --git a/contrib/python/Twisted/py2/twisted/cred/error.py b/contrib/python/Twisted/py2/twisted/cred/error.py new file mode 100644 index 0000000000..efd3ec3426 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/cred/error.py @@ -0,0 +1,45 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Cred errors. +""" + +from __future__ import division, absolute_import + + +class Unauthorized(Exception): + """Standard unauthorized error.""" + + + +class LoginFailed(Exception): + """ + The user's request to log in failed for some reason. + """ + + + +class UnauthorizedLogin(LoginFailed, Unauthorized): + """The user was not authorized to log in. + """ + + + +class UnhandledCredentials(LoginFailed): + """A type of credentials were passed in with no knowledge of how to check + them. This is a server configuration error - it means that a protocol was + connected to a Portal without a CredentialChecker that can check all of its + potential authentication strategies. + """ + + + +class LoginDenied(LoginFailed): + """ + The realm rejected this login for some reason. + + Examples of reasons this might be raised include an avatar logging in + too frequently, a quota having been fully used, or the overall server + load being too high. + """ diff --git a/contrib/python/Twisted/py2/twisted/cred/portal.py b/contrib/python/Twisted/py2/twisted/cred/portal.py new file mode 100644 index 0000000000..4c81d5f12c --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/cred/portal.py @@ -0,0 +1,124 @@ +# -*- test-case-name: twisted.cred.test.test_cred -*- + +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +The point of integration of application and authentication. +""" + +from __future__ import division, absolute_import + +from twisted.internet import defer +from twisted.internet.defer import maybeDeferred +from twisted.python import failure, reflect +from twisted.cred import error +from zope.interface import providedBy, Interface + + +class IRealm(Interface): + """ + The realm connects application-specific objects to the + authentication system. + """ + def requestAvatar(avatarId, mind, *interfaces): + """ + Return avatar which provides one of the given interfaces. + + @param avatarId: a string that identifies an avatar, as returned by + L{ICredentialsChecker.requestAvatarId<twisted.cred.checkers.ICredentialsChecker.requestAvatarId>} + (via a Deferred). Alternatively, it may be + C{twisted.cred.checkers.ANONYMOUS}. + @param mind: usually None. See the description of mind in + L{Portal.login}. + @param interfaces: the interface(s) the returned avatar should + implement, e.g. C{IMailAccount}. See the description of + L{Portal.login}. + + @returns: a deferred which will fire a tuple of (interface, + avatarAspect, logout), or the tuple itself. The interface will be + one of the interfaces passed in the 'interfaces' argument. The + 'avatarAspect' will implement that interface. The 'logout' object + is a callable which will detach the mind from the avatar. + """ + + +class Portal(object): + """ + A mediator between clients and a realm. + + A portal is associated with one Realm and zero or more credentials checkers. + When a login is attempted, the portal finds the appropriate credentials + checker for the credentials given, invokes it, and if the credentials are + valid, retrieves the appropriate avatar from the Realm. + + This class is not intended to be subclassed. Customization should be done + in the realm object and in the credentials checker objects. + """ + def __init__(self, realm, checkers=()): + """ + Create a Portal to a L{IRealm}. + """ + self.realm = realm + self.checkers = {} + for checker in checkers: + self.registerChecker(checker) + + + def listCredentialsInterfaces(self): + """ + Return list of credentials interfaces that can be used to login. + """ + return list(self.checkers.keys()) + + + def registerChecker(self, checker, *credentialInterfaces): + if not credentialInterfaces: + credentialInterfaces = checker.credentialInterfaces + for credentialInterface in credentialInterfaces: + self.checkers[credentialInterface] = checker + + + def login(self, credentials, mind, *interfaces): + """ + @param credentials: an implementor of + L{twisted.cred.credentials.ICredentials} + + @param mind: an object which implements a client-side interface for + your particular realm. In many cases, this may be None, so if the + word 'mind' confuses you, just ignore it. + + @param interfaces: list of interfaces for the perspective that the mind + wishes to attach to. Usually, this will be only one interface, for + example IMailAccount. For highly dynamic protocols, however, this + may be a list like (IMailAccount, IUserChooser, IServiceInfo). To + expand: if we are speaking to the system over IMAP, any information + that will be relayed to the user MUST be returned as an + IMailAccount implementor; IMAP clients would not be able to + understand anything else. Any information about unusual status + would have to be relayed as a single mail message in an + otherwise-empty mailbox. However, in a web-based mail system, or a + PB-based client, the ``mind'' object inside the web server + (implemented with a dynamic page-viewing mechanism such as a + Twisted Web Resource) or on the user's client program may be + intelligent enough to respond to several ``server''-side + interfaces. + + @return: A deferred which will fire a tuple of (interface, + avatarAspect, logout). The interface will be one of the interfaces + passed in the 'interfaces' argument. The 'avatarAspect' will + implement that interface. The 'logout' object is a callable which + will detach the mind from the avatar. It must be called when the + user has conceptually disconnected from the service. Although in + some cases this will not be in connectionLost (such as in a + web-based session), it will always be at the end of a user's + interactive session. + """ + for i in self.checkers: + if i.providedBy(credentials): + return maybeDeferred(self.checkers[i].requestAvatarId, credentials + ).addCallback(self.realm.requestAvatar, mind, *interfaces + ) + ifac = providedBy(credentials) + return defer.fail(failure.Failure(error.UnhandledCredentials( + "No checker for %s" % ', '.join(map(reflect.qual, ifac))))) diff --git a/contrib/python/Twisted/py2/twisted/cred/strcred.py b/contrib/python/Twisted/py2/twisted/cred/strcred.py new file mode 100644 index 0000000000..4c3e5bc95e --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/cred/strcred.py @@ -0,0 +1,272 @@ +# -*- test-case-name: twisted.cred.test.test_strcred -*- +# +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. +# + +""" +Support for resolving command-line strings that represent different +checkers available to cred. + +Examples: + - passwd:/etc/passwd + - memory:admin:asdf:user:lkj + - unix +""" + +from __future__ import absolute_import, division + +import sys + +from zope.interface import Interface, Attribute + +from twisted.plugin import getPlugins +from twisted.python import usage + + + +class ICheckerFactory(Interface): + """ + A factory for objects which provide + L{twisted.cred.checkers.ICredentialsChecker}. + + It's implemented by twistd plugins creating checkers. + """ + + authType = Attribute( + 'A tag that identifies the authentication method.') + + + authHelp = Attribute( + 'A detailed (potentially multi-line) description of precisely ' + 'what functionality this CheckerFactory provides.') + + + argStringFormat = Attribute( + 'A short (one-line) description of the argument string format.') + + + credentialInterfaces = Attribute( + 'A list of credentials interfaces that this factory will support.') + + + def generateChecker(argstring): + """ + Return an L{twisted.cred.checkers.ICredentialsChecker} provider using the supplied + argument string. + """ + + + +class StrcredException(Exception): + """ + Base exception class for strcred. + """ + + + +class InvalidAuthType(StrcredException): + """ + Raised when a user provides an invalid identifier for the + authentication plugin (known as the authType). + """ + + + +class InvalidAuthArgumentString(StrcredException): + """ + Raised by an authentication plugin when the argument string + provided is formatted incorrectly. + """ + + + +class UnsupportedInterfaces(StrcredException): + """ + Raised when an application is given a checker to use that does not + provide any of the application's supported credentials interfaces. + """ + + + +# This will be used to warn the users whenever they view help for an +# authType that is not supported by the application. +notSupportedWarning = ("WARNING: This authType is not supported by " + "this application.") + + + +def findCheckerFactories(): + """ + Find all objects that implement L{ICheckerFactory}. + """ + return getPlugins(ICheckerFactory) + + + +def findCheckerFactory(authType): + """ + Find the first checker factory that supports the given authType. + """ + for factory in findCheckerFactories(): + if factory.authType == authType: + return factory + raise InvalidAuthType(authType) + + + +def makeChecker(description): + """ + Returns an L{twisted.cred.checkers.ICredentialsChecker} based on the + contents of a descriptive string. Similar to + L{twisted.application.strports}. + """ + if ':' in description: + authType, argstring = description.split(':', 1) + else: + authType = description + argstring = '' + return findCheckerFactory(authType).generateChecker(argstring) + + + +class AuthOptionMixin: + """ + Defines helper methods that can be added on to any + L{usage.Options} subclass that needs authentication. + + This mixin implements three new options methods: + + The opt_auth method (--auth) will write two new values to the + 'self' dictionary: C{credInterfaces} (a dict of lists) and + C{credCheckers} (a list). + + The opt_help_auth method (--help-auth) will search for all + available checker plugins and list them for the user; it will exit + when finished. + + The opt_help_auth_type method (--help-auth-type) will display + detailed help for a particular checker plugin. + + @cvar supportedInterfaces: An iterable object that returns + credential interfaces which this application is able to support. + + @cvar authOutput: A writeable object to which this options class + will send all help-related output. Default: L{sys.stdout} + """ + + supportedInterfaces = None + authOutput = sys.stdout + + + def supportsInterface(self, interface): + """ + Returns whether a particular credentials interface is supported. + """ + return (self.supportedInterfaces is None + or interface in self.supportedInterfaces) + + + def supportsCheckerFactory(self, factory): + """ + Returns whether a checker factory will provide at least one of + the credentials interfaces that we care about. + """ + for interface in factory.credentialInterfaces: + if self.supportsInterface(interface): + return True + return False + + + def addChecker(self, checker): + """ + Supply a supplied credentials checker to the Options class. + """ + # First figure out which interfaces we're willing to support. + supported = [] + if self.supportedInterfaces is None: + supported = checker.credentialInterfaces + else: + for interface in checker.credentialInterfaces: + if self.supportsInterface(interface): + supported.append(interface) + if not supported: + raise UnsupportedInterfaces(checker.credentialInterfaces) + # If we get this far, then we know we can use this checker. + if 'credInterfaces' not in self: + self['credInterfaces'] = {} + if 'credCheckers' not in self: + self['credCheckers'] = [] + self['credCheckers'].append(checker) + for interface in supported: + self['credInterfaces'].setdefault(interface, []).append(checker) + + + def opt_auth(self, description): + """ + Specify an authentication method for the server. + """ + try: + self.addChecker(makeChecker(description)) + except UnsupportedInterfaces as e: + raise usage.UsageError( + 'Auth plugin not supported: %s' % e.args[0]) + except InvalidAuthType as e: + raise usage.UsageError( + 'Auth plugin not recognized: %s' % e.args[0]) + except Exception as e: + raise usage.UsageError('Unexpected error: %s' % e) + + + def _checkerFactoriesForOptHelpAuth(self): + """ + Return a list of which authTypes will be displayed by --help-auth. + This makes it a lot easier to test this module. + """ + for factory in findCheckerFactories(): + for interface in factory.credentialInterfaces: + if self.supportsInterface(interface): + yield factory + break + + + def opt_help_auth(self): + """ + Show all authentication methods available. + """ + self.authOutput.write("Usage: --auth AuthType[:ArgString]\n") + self.authOutput.write("For detailed help: --help-auth-type AuthType\n") + self.authOutput.write('\n') + # Figure out the right width for our columns + firstLength = 0 + for factory in self._checkerFactoriesForOptHelpAuth(): + if len(factory.authType) > firstLength: + firstLength = len(factory.authType) + formatString = ' %%-%is\t%%s\n' % firstLength + self.authOutput.write(formatString % ('AuthType', 'ArgString format')) + self.authOutput.write(formatString % ('========', '================')) + for factory in self._checkerFactoriesForOptHelpAuth(): + self.authOutput.write( + formatString % (factory.authType, factory.argStringFormat)) + self.authOutput.write('\n') + raise SystemExit(0) + + + def opt_help_auth_type(self, authType): + """ + Show help for a particular authentication type. + """ + try: + cf = findCheckerFactory(authType) + except InvalidAuthType: + raise usage.UsageError("Invalid auth type: %s" % authType) + self.authOutput.write("Usage: --auth %s[:ArgString]\n" % authType) + self.authOutput.write("ArgString format: %s\n" % cf.argStringFormat) + self.authOutput.write('\n') + for line in cf.authHelp.strip().splitlines(): + self.authOutput.write(' %s\n' % line.rstrip()) + self.authOutput.write('\n') + if not self.supportsCheckerFactory(cf): + self.authOutput.write(' %s\n' % notSupportedWarning) + self.authOutput.write('\n') + raise SystemExit(0) |