Open Media Library Platform

This commit is contained in:
j 2013-10-11 19:28:32 +02:00
commit 411ad5b16f
5849 changed files with 1778641 additions and 0 deletions

View file

@ -0,0 +1,13 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Twisted Cred
Support for verifying credentials, and providing services to users based on
those credentials.
(This package was previously known as the module twisted.internet.passport.)
"""

View file

@ -0,0 +1,129 @@
# -*- test-case-name: twisted.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 hashlib import md5, sha1
# The digest math
algorithms = {
'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
'md5-sess': md5,
'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 C{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(":")
m.update(pszRealm)
m.update(":")
m.update(pszPassword)
HA1 = m.digest()
else:
# We were given a username:realm:password
HA1 = preHA1.decode('hex')
if pszAlg == "md5-sess":
m = algorithms[pszAlg]()
m.update(HA1)
m.update(":")
m.update(pszNonce)
m.update(":")
m.update(pszCNonce)
HA1 = m.digest()
return HA1.encode('hex')
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 C{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(":")
m.update(pszDigestUri)
if pszQop == "auth-int":
m.update(":")
m.update(pszHEntity)
return m.digest().encode('hex')
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(":")
m.update(pszNonce)
m.update(":")
if pszNonceCount and pszCNonce:
m.update(pszNonceCount)
m.update(":")
m.update(pszCNonce)
m.update(":")
m.update(pszQop)
m.update(":")
m.update(HA2)
respHash = m.digest().encode('hex')
return respHash

View file

@ -0,0 +1,268 @@
# -*- test-case-name: twisted.test.test_newcred -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
import os
from zope.interface import implements, Interface, Attribute
from twisted.internet import defer
from twisted.python import failure, log
from twisted.cred import error, credentials
class ICredentialsChecker(Interface):
"""
An object that can check sub-interfaces of ICredentials.
"""
credentialInterfaces = Attribute(
'A list of sub-interfaces of ICredentials which specifies which I may check.')
def requestAvatarId(credentials):
"""
@param credentials: something which implements one of the interfaces in
self.credentialInterfaces.
@return: a Deferred which will fire a string which identifies an
avatar, an empty tuple to specify an authenticated anonymous user
(provided as checkers.ANONYMOUS) or fire a Failure(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 = ()
class AllowAnonymousAccess:
implements(ICredentialsChecker)
credentialInterfaces = credentials.IAnonymous,
def requestAvatarId(self, credentials):
return defer.succeed(ANONYMOUS)
class InMemoryUsernamePasswordDatabaseDontUse:
"""
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}.
"""
implements(ICredentialsChecker)
credentialInterfaces = (credentials.IUsernamePassword,
credentials.IUsernameHashedPassword)
def __init__(self, **users):
self.users = users
def addUser(self, username, password):
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, str(credentials.username))
else:
return defer.fail(error.UnauthorizedLogin())
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 IUsernamePassword
credentials. If the passwords are stored plaintext,
IUsernameHashedPassword credentials will be checkable as well.
"""
implements(ICredentialsChecker)
cache = False
_credCache = None
_cacheTimestamp = 0
def __init__(self, filename, delim=':', usernameField=0, passwordField=1,
caseSensitive=True, hash=None, cache=False):
"""
@type filename: C{str}
@param filename: The name of the file from which to read username and
password information.
@type delim: C{str}
@param delim: The field delimiter used in the file.
@type usernameField: C{int}
@param usernameField: The index of the username after splitting a
line on the delimiter.
@type passwordField: C{int}
@param passwordField: The index of the password after splitting a
line on the delimiter.
@type caseSensitive: C{bool}
@param caseSensitive: If true, consider the case of the username when
performing a lookup. Ignore it otherwise.
@type hash: Three-argument callable or C{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: C{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):
try:
f = file(self.filename)
except:
log.err()
raise error.UnauthorizedLogin()
else:
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]
def getUser(self, username):
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)
class PluggableAuthenticationModulesChecker:
implements(ICredentialsChecker)
credentialInterfaces = credentials.IPluggableAuthenticationModules,
service = 'Twisted'
def requestAvatarId(self, credentials):
try:
from twisted.cred import pamauth
except ImportError: # PyPAM is missing
return defer.fail(error.UnauthorizedLogin())
else:
d = pamauth.pamAuthenticate(self.service, credentials.username,
credentials.pamConversion)
d.addCallback(lambda x: credentials.username)
return d
# For backwards compatibility
# Allow access as the old name.
OnDiskUsernamePasswordDatabase = FilePasswordDB

View file

@ -0,0 +1,493 @@
# -*- test-case-name: twisted.test.test_newcred-*-
# 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 zope.interface import implements, Interface
import hmac, time, random, re
from hashlib import md5
from twisted.python.randbytes import secureRandom
from twisted.cred._digest import calcResponse, calcHA1, calcHA2
from twisted.cred import error
class ICredentials(Interface):
"""
I check credentials.
Implementors _must_ specify which sub-interfaces of ICredentials
to which it conforms, using zope.interface.implements().
"""
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: C{str}
@ivar username: The username associated with these credentials.
"""
def checkPassword(password):
"""
Validate these credentials against the correct password.
@type password: C{str}
@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: C{str}
@ivar username: The username associated with these credentials.
@type password: C{str}
@ivar password: The password associated with these credentials.
"""
def checkPassword(password):
"""
Validate these credentials against the correct password.
@type password: C{str}
@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.
"""
class DigestedCredentials(object):
"""
Yet Another Simple HTTP Digest authentication scheme.
"""
implements(IUsernameHashedPassword, IUsernameDigestHash)
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', 'md5').lower()
qop = self.fields.get('qop', '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', 'md5').lower()
qop = self.fields.get('qop', '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: C{str}
@ivar privateKey: A random string used for generating the secure opaque.
@type algorithm: C{str}
@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: C{str}
@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 = "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 C{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': '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: C{str}
"""
return secureRandom(12).encode('hex')
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 = str(int(self._getTime()))
if clientip is None:
clientip = ''
key = "%s,%s,%s" % (nonce, clientip, now)
digest = md5(key + self.privateKey).hexdigest()
ekey = key.encode('base64')
return "%s-%s" % (digest, ekey.replace('\n', ''))
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 C{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('-')
if len(opaqueParts) != 2:
raise error.LoginFailed('Invalid response, invalid opaque value')
if clientip is None:
clientip = ''
# Verify the key
key = opaqueParts[1].decode('base64')
keyParts = key.split(',')
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 = md5(key + self.privateKey).hexdigest()
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: C{str}
@param response: A string of comma seperated key=value pairs
@type method: C{str}
@param method: The action requested to which this response is addressed
(GET, POST, INVITE, OPTIONS, etc).
@type host: C{str}
@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 = ' '.join(response.splitlines())
parts = self._parseparts.findall(response)
auth = {}
for (key, bare, quoted) in parts:
value = (quoted or bare).strip()
auth[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)
class CramMD5Credentials:
implements(IUsernameHashedPassword)
challenge = ''
response = ''
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 = '<%d.%d@%s>' % (r, t, self.host)
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 = hmac.HMAC(password, self.challenge).hexdigest()
return verify == self.response
class UsernameHashedPassword:
implements(IUsernameHashedPassword)
def __init__(self, username, hashed):
self.username = username
self.hashed = hashed
def checkPassword(self, password):
return self.hashed == password
class UsernamePassword:
implements(IUsernamePassword)
def __init__(self, username, password):
self.username = username
self.password = password
def checkPassword(self, password):
return self.password == password
class Anonymous:
implements(IAnonymous)
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: C{str}
@ivar algName: The algorithm name for the blob.
@type algName: C{str}
@ivar blob: The public key blob as sent by the client.
@type blob: C{str}
@ivar sigData: The data the signature was made from.
@type sigData: C{str}
@ivar signature: The signed data. This is checked to verify that the user
owns the private key.
@type signature: C{str} or C{NoneType}
"""
class SSHPrivateKey:
implements(ISSHPrivateKey)
def __init__(self, username, algName, blob, sigData, signature):
self.username = username
self.algName = algName
self.blob = blob
self.sigData = sigData
self.signature = signature
class IPluggableAuthenticationModules(ICredentials):
"""I encapsulate the authentication of a user via PAM (Pluggable
Authentication Modules. I use PyPAM (available from
http://www.tummy.com/Software/PyPam/index.html).
@ivar username: The username for the user being logged in.
@ivar pamConversion: A function that is called with a list of tuples
(message, messageType). See the PAM documentation
for the meaning of messageType. The function
returns a Deferred which will fire with a list
of (response, 0), one for each message. The 0 is
currently unused, but is required by the PAM library.
"""
class PluggableAuthenticationModules:
implements(IPluggableAuthenticationModules)
def __init__(self, username, pamConversion):
self.username = username
self.pamConversion = pamConversion

View file

@ -0,0 +1,41 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""Cred errors."""
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.
"""

View file

@ -0,0 +1,79 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Support for asynchronously authenticating using PAM.
"""
import PAM
import getpass, threading, os
from twisted.internet import threads, defer
def pamAuthenticateThread(service, user, conv):
def _conv(items):
from twisted.internet import reactor
try:
d = conv(items)
except:
import traceback
traceback.print_exc()
return
ev = threading.Event()
def cb(r):
ev.r = (1, r)
ev.set()
def eb(e):
ev.r = (0, e)
ev.set()
reactor.callFromThread(d.addCallbacks, cb, eb)
ev.wait()
done = ev.r
if done[0]:
return done[1]
else:
raise done[1].type, done[1].value
return callIntoPAM(service, user, _conv)
def callIntoPAM(service, user, conv):
"""A testing hook.
"""
pam = PAM.pam()
pam.start(service)
pam.set_item(PAM.PAM_USER, user)
pam.set_item(PAM.PAM_CONV, conv)
gid = os.getegid()
uid = os.geteuid()
os.setegid(0)
os.seteuid(0)
try:
pam.authenticate() # these will raise
pam.acct_mgmt()
return 1
finally:
os.setegid(gid)
os.seteuid(uid)
def defConv(items):
resp = []
for i in range(len(items)):
message, kind = items[i]
if kind == 1: # password
p = getpass.getpass(message)
resp.append((p, 0))
elif kind == 2: # text
p = raw_input(message)
resp.append((p, 0))
elif kind in (3,4):
print message
resp.append(("", 0))
else:
return defer.fail('foo')
d = defer.succeed(resp)
return d
def pamAuthenticate(service, user, conv):
return threads.deferToThread(pamAuthenticateThread, service, user, conv)

View file

@ -0,0 +1,121 @@
# -*- test-case-name: twisted.test.test_newcred -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
The point of integration of application and authentication.
"""
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:
"""
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 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)))))

View file

@ -0,0 +1,270 @@
# -*- test-case-name: twisted.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
"""
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{ICredentialChecker} 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, e:
raise usage.UsageError(
'Auth plugin not supported: %s' % e.args[0])
except InvalidAuthType, e:
raise usage.UsageError(
'Auth plugin not recognized: %s' % e.args[0])
except Exception, 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)