| 1 | # -*- test-case-name: twisted.test.test_newcred -*- |
|---|
| 2 | # Copyright (c) Twisted Matrix Laboratories. |
|---|
| 3 | # See LICENSE for details. |
|---|
| 4 | |
|---|
| 5 | import os |
|---|
| 6 | |
|---|
| 7 | from zope.interface import implements, Interface, Attribute |
|---|
| 8 | |
|---|
| 9 | from twisted.internet import defer |
|---|
| 10 | from twisted.python import failure, log |
|---|
| 11 | from twisted.cred import error, credentials |
|---|
| 12 | |
|---|
| 13 | |
|---|
| 14 | |
|---|
| 15 | class ICredentialsChecker(Interface): |
|---|
| 16 | """ |
|---|
| 17 | An object that can check sub-interfaces of ICredentials. |
|---|
| 18 | """ |
|---|
| 19 | |
|---|
| 20 | credentialInterfaces = Attribute( |
|---|
| 21 | 'A list of sub-interfaces of ICredentials which specifies which I may check.') |
|---|
| 22 | |
|---|
| 23 | |
|---|
| 24 | def requestAvatarId(credentials): |
|---|
| 25 | """ |
|---|
| 26 | @param credentials: something which implements one of the interfaces in |
|---|
| 27 | self.credentialInterfaces. |
|---|
| 28 | |
|---|
| 29 | @return: a Deferred which will fire a string which identifies an |
|---|
| 30 | avatar, an empty tuple to specify an authenticated anonymous user |
|---|
| 31 | (provided as checkers.ANONYMOUS) or fire a Failure(UnauthorizedLogin). |
|---|
| 32 | Alternatively, return the result itself. |
|---|
| 33 | |
|---|
| 34 | @see: L{twisted.cred.credentials} |
|---|
| 35 | """ |
|---|
| 36 | |
|---|
| 37 | |
|---|
| 38 | |
|---|
| 39 | # A note on anonymity - We do not want None as the value for anonymous |
|---|
| 40 | # because it is too easy to accidentally return it. We do not want the |
|---|
| 41 | # empty string, because it is too easy to mistype a password file. For |
|---|
| 42 | # example, an .htpasswd file may contain the lines: ['hello:asdf', |
|---|
| 43 | # 'world:asdf', 'goodbye', ':world']. This misconfiguration will have an |
|---|
| 44 | # ill effect in any case, but accidentally granting anonymous access is a |
|---|
| 45 | # worse failure mode than simply granting access to an untypeable |
|---|
| 46 | # username. We do not want an instance of 'object', because that would |
|---|
| 47 | # create potential problems with persistence. |
|---|
| 48 | |
|---|
| 49 | ANONYMOUS = () |
|---|
| 50 | |
|---|
| 51 | |
|---|
| 52 | class AllowAnonymousAccess: |
|---|
| 53 | implements(ICredentialsChecker) |
|---|
| 54 | credentialInterfaces = credentials.IAnonymous, |
|---|
| 55 | |
|---|
| 56 | def requestAvatarId(self, credentials): |
|---|
| 57 | return defer.succeed(ANONYMOUS) |
|---|
| 58 | |
|---|
| 59 | |
|---|
| 60 | class InMemoryUsernamePasswordDatabaseDontUse: |
|---|
| 61 | """ |
|---|
| 62 | An extremely simple credentials checker. |
|---|
| 63 | |
|---|
| 64 | This is only of use in one-off test programs or examples which don't |
|---|
| 65 | want to focus too much on how credentials are verified. |
|---|
| 66 | |
|---|
| 67 | You really don't want to use this for anything else. It is, at best, a |
|---|
| 68 | toy. If you need a simple credentials checker for a real application, |
|---|
| 69 | see L{FilePasswordDB}. |
|---|
| 70 | """ |
|---|
| 71 | |
|---|
| 72 | implements(ICredentialsChecker) |
|---|
| 73 | |
|---|
| 74 | credentialInterfaces = (credentials.IUsernamePassword, |
|---|
| 75 | credentials.IUsernameHashedPassword) |
|---|
| 76 | |
|---|
| 77 | def __init__(self, **users): |
|---|
| 78 | self.users = users |
|---|
| 79 | |
|---|
| 80 | def addUser(self, username, password): |
|---|
| 81 | self.users[username] = password |
|---|
| 82 | |
|---|
| 83 | def _cbPasswordMatch(self, matched, username): |
|---|
| 84 | if matched: |
|---|
| 85 | return username |
|---|
| 86 | else: |
|---|
| 87 | return failure.Failure(error.UnauthorizedLogin()) |
|---|
| 88 | |
|---|
| 89 | def requestAvatarId(self, credentials): |
|---|
| 90 | if credentials.username in self.users: |
|---|
| 91 | return defer.maybeDeferred( |
|---|
| 92 | credentials.checkPassword, |
|---|
| 93 | self.users[credentials.username]).addCallback( |
|---|
| 94 | self._cbPasswordMatch, str(credentials.username)) |
|---|
| 95 | else: |
|---|
| 96 | return defer.fail(error.UnauthorizedLogin()) |
|---|
| 97 | |
|---|
| 98 | |
|---|
| 99 | class FilePasswordDB: |
|---|
| 100 | """A file-based, text-based username/password database. |
|---|
| 101 | |
|---|
| 102 | Records in the datafile for this class are delimited by a particular |
|---|
| 103 | string. The username appears in a fixed field of the columns delimited |
|---|
| 104 | by this string, as does the password. Both fields are specifiable. If |
|---|
| 105 | the passwords are not stored plaintext, a hash function must be supplied |
|---|
| 106 | to convert plaintext passwords to the form stored on disk and this |
|---|
| 107 | CredentialsChecker will only be able to check IUsernamePassword |
|---|
| 108 | credentials. If the passwords are stored plaintext, |
|---|
| 109 | IUsernameHashedPassword credentials will be checkable as well. |
|---|
| 110 | """ |
|---|
| 111 | |
|---|
| 112 | implements(ICredentialsChecker) |
|---|
| 113 | |
|---|
| 114 | cache = False |
|---|
| 115 | _credCache = None |
|---|
| 116 | _cacheTimestamp = 0 |
|---|
| 117 | |
|---|
| 118 | def __init__(self, filename, delim=':', usernameField=0, passwordField=1, |
|---|
| 119 | caseSensitive=True, hash=None, cache=False): |
|---|
| 120 | """ |
|---|
| 121 | @type filename: C{str} |
|---|
| 122 | @param filename: The name of the file from which to read username and |
|---|
| 123 | password information. |
|---|
| 124 | |
|---|
| 125 | @type delim: C{str} |
|---|
| 126 | @param delim: The field delimiter used in the file. |
|---|
| 127 | |
|---|
| 128 | @type usernameField: C{int} |
|---|
| 129 | @param usernameField: The index of the username after splitting a |
|---|
| 130 | line on the delimiter. |
|---|
| 131 | |
|---|
| 132 | @type passwordField: C{int} |
|---|
| 133 | @param passwordField: The index of the password after splitting a |
|---|
| 134 | line on the delimiter. |
|---|
| 135 | |
|---|
| 136 | @type caseSensitive: C{bool} |
|---|
| 137 | @param caseSensitive: If true, consider the case of the username when |
|---|
| 138 | performing a lookup. Ignore it otherwise. |
|---|
| 139 | |
|---|
| 140 | @type hash: Three-argument callable or C{None} |
|---|
| 141 | @param hash: A function used to transform the plaintext password |
|---|
| 142 | received over the network to a format suitable for comparison |
|---|
| 143 | against the version stored on disk. The arguments to the callable |
|---|
| 144 | are the username, the network-supplied password, and the in-file |
|---|
| 145 | version of the password. If the return value compares equal to the |
|---|
| 146 | version stored on disk, the credentials are accepted. |
|---|
| 147 | |
|---|
| 148 | @type cache: C{bool} |
|---|
| 149 | @param cache: If true, maintain an in-memory cache of the |
|---|
| 150 | contents of the password file. On lookups, the mtime of the |
|---|
| 151 | file will be checked, and the file will only be re-parsed if |
|---|
| 152 | the mtime is newer than when the cache was generated. |
|---|
| 153 | """ |
|---|
| 154 | self.filename = filename |
|---|
| 155 | self.delim = delim |
|---|
| 156 | self.ufield = usernameField |
|---|
| 157 | self.pfield = passwordField |
|---|
| 158 | self.caseSensitive = caseSensitive |
|---|
| 159 | self.hash = hash |
|---|
| 160 | self.cache = cache |
|---|
| 161 | |
|---|
| 162 | if self.hash is None: |
|---|
| 163 | # The passwords are stored plaintext. We can support both |
|---|
| 164 | # plaintext and hashed passwords received over the network. |
|---|
| 165 | self.credentialInterfaces = ( |
|---|
| 166 | credentials.IUsernamePassword, |
|---|
| 167 | credentials.IUsernameHashedPassword |
|---|
| 168 | ) |
|---|
| 169 | else: |
|---|
| 170 | # The passwords are hashed on disk. We can support only |
|---|
| 171 | # plaintext passwords received over the network. |
|---|
| 172 | self.credentialInterfaces = ( |
|---|
| 173 | credentials.IUsernamePassword, |
|---|
| 174 | ) |
|---|
| 175 | |
|---|
| 176 | |
|---|
| 177 | def __getstate__(self): |
|---|
| 178 | d = dict(vars(self)) |
|---|
| 179 | for k in '_credCache', '_cacheTimestamp': |
|---|
| 180 | try: |
|---|
| 181 | del d[k] |
|---|
| 182 | except KeyError: |
|---|
| 183 | pass |
|---|
| 184 | return d |
|---|
| 185 | |
|---|
| 186 | |
|---|
| 187 | def _cbPasswordMatch(self, matched, username): |
|---|
| 188 | if matched: |
|---|
| 189 | return username |
|---|
| 190 | else: |
|---|
| 191 | return failure.Failure(error.UnauthorizedLogin()) |
|---|
| 192 | |
|---|
| 193 | |
|---|
| 194 | def _loadCredentials(self): |
|---|
| 195 | try: |
|---|
| 196 | f = file(self.filename) |
|---|
| 197 | except: |
|---|
| 198 | log.err() |
|---|
| 199 | raise error.UnauthorizedLogin() |
|---|
| 200 | else: |
|---|
| 201 | for line in f: |
|---|
| 202 | line = line.rstrip() |
|---|
| 203 | parts = line.split(self.delim) |
|---|
| 204 | |
|---|
| 205 | if self.ufield >= len(parts) or self.pfield >= len(parts): |
|---|
| 206 | continue |
|---|
| 207 | if self.caseSensitive: |
|---|
| 208 | yield parts[self.ufield], parts[self.pfield] |
|---|
| 209 | else: |
|---|
| 210 | yield parts[self.ufield].lower(), parts[self.pfield] |
|---|
| 211 | |
|---|
| 212 | |
|---|
| 213 | def getUser(self, username): |
|---|
| 214 | if not self.caseSensitive: |
|---|
| 215 | username = username.lower() |
|---|
| 216 | |
|---|
| 217 | if self.cache: |
|---|
| 218 | if self._credCache is None or os.path.getmtime(self.filename) > self._cacheTimestamp: |
|---|
| 219 | self._cacheTimestamp = os.path.getmtime(self.filename) |
|---|
| 220 | self._credCache = dict(self._loadCredentials()) |
|---|
| 221 | return username, self._credCache[username] |
|---|
| 222 | else: |
|---|
| 223 | for u, p in self._loadCredentials(): |
|---|
| 224 | if u == username: |
|---|
| 225 | return u, p |
|---|
| 226 | raise KeyError(username) |
|---|
| 227 | |
|---|
| 228 | |
|---|
| 229 | def requestAvatarId(self, c): |
|---|
| 230 | try: |
|---|
| 231 | u, p = self.getUser(c.username) |
|---|
| 232 | except KeyError: |
|---|
| 233 | return defer.fail(error.UnauthorizedLogin()) |
|---|
| 234 | else: |
|---|
| 235 | up = credentials.IUsernamePassword(c, None) |
|---|
| 236 | if self.hash: |
|---|
| 237 | if up is not None: |
|---|
| 238 | h = self.hash(up.username, up.password, p) |
|---|
| 239 | if h == p: |
|---|
| 240 | return defer.succeed(u) |
|---|
| 241 | return defer.fail(error.UnauthorizedLogin()) |
|---|
| 242 | else: |
|---|
| 243 | return defer.maybeDeferred(c.checkPassword, p |
|---|
| 244 | ).addCallback(self._cbPasswordMatch, u) |
|---|
| 245 | |
|---|
| 246 | |
|---|
| 247 | |
|---|
| 248 | class PluggableAuthenticationModulesChecker: |
|---|
| 249 | implements(ICredentialsChecker) |
|---|
| 250 | credentialInterfaces = credentials.IPluggableAuthenticationModules, |
|---|
| 251 | service = 'Twisted' |
|---|
| 252 | |
|---|
| 253 | def requestAvatarId(self, credentials): |
|---|
| 254 | try: |
|---|
| 255 | from twisted.cred import pamauth |
|---|
| 256 | except ImportError: # PyPAM is missing |
|---|
| 257 | return defer.fail(error.UnauthorizedLogin()) |
|---|
| 258 | else: |
|---|
| 259 | d = pamauth.pamAuthenticate(self.service, credentials.username, |
|---|
| 260 | credentials.pamConversion) |
|---|
| 261 | d.addCallback(lambda x: credentials.username) |
|---|
| 262 | return d |
|---|
| 263 | |
|---|
| 264 | |
|---|
| 265 | |
|---|
| 266 | # For backwards compatibility |
|---|
| 267 | # Allow access as the old name. |
|---|
| 268 | OnDiskUsernamePasswordDatabase = FilePasswordDB |
|---|