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