1
# Copyright (c) 2007-2009 Twisted Matrix Laboratories.
2
# See LICENSE for details.
5
Tests for L{twisted.cred.strcred}.
11
from twisted import plugin
12
from twisted.trial import unittest
13
from twisted.cred import credentials, checkers, error, strcred
14
from twisted.plugins import cred_file, cred_anonymous
15
from twisted.python import usage
16
from twisted.python.filepath import FilePath
17
from twisted.python.fakepwd import UserDatabase
36
def getInvalidAuthType():
38
Helper method to produce an auth type that doesn't exist.
40
invalidAuthType = 'ThisPluginDoesNotExist'
41
while (invalidAuthType in
42
[factory.authType for factory in strcred.findCheckerFactories()]):
43
invalidAuthType += '_'
44
return invalidAuthType
48
class TestPublicAPI(unittest.TestCase):
50
def test_emptyDescription(self):
52
Test that the description string cannot be empty.
54
iat = getInvalidAuthType()
55
self.assertRaises(strcred.InvalidAuthType, strcred.makeChecker, iat)
56
self.assertRaises(strcred.InvalidAuthType, strcred.findCheckerFactory, iat)
59
def test_invalidAuthType(self):
61
Test that an unrecognized auth type raises an exception.
63
iat = getInvalidAuthType()
64
self.assertRaises(strcred.InvalidAuthType, strcred.makeChecker, iat)
65
self.assertRaises(strcred.InvalidAuthType, strcred.findCheckerFactory, iat)
69
class TestStrcredFunctions(unittest.TestCase):
71
def test_findCheckerFactories(self):
73
Test that findCheckerFactories returns all available plugins.
75
availablePlugins = list(strcred.findCheckerFactories())
76
for plg in plugin.getPlugins(strcred.ICheckerFactory):
77
self.assertIn(plg, availablePlugins)
80
def test_findCheckerFactory(self):
82
Test that findCheckerFactory returns the first plugin
83
available for a given authentication type.
85
self.assertIdentical(strcred.findCheckerFactory('file'),
86
cred_file.theFileCheckerFactory)
90
class TestMemoryChecker(unittest.TestCase):
93
self.admin = credentials.UsernamePassword('admin', 'asdf')
94
self.alice = credentials.UsernamePassword('alice', 'foo')
95
self.badPass = credentials.UsernamePassword('alice', 'foobar')
96
self.badUser = credentials.UsernamePassword('x', 'yz')
97
self.checker = strcred.makeChecker('memory:admin:asdf:alice:foo')
100
def test_isChecker(self):
102
Verifies that strcred.makeChecker('memory') returns an object
103
that implements the L{ICredentialsChecker} interface.
105
self.assertTrue(checkers.ICredentialsChecker.providedBy(self.checker))
106
self.assertIn(credentials.IUsernamePassword,
107
self.checker.credentialInterfaces)
110
def test_badFormatArgString(self):
112
Test that an argument string which does not contain user:pass
113
pairs (i.e., an odd number of ':' characters) raises an exception.
115
self.assertRaises(strcred.InvalidAuthArgumentString,
116
strcred.makeChecker, 'memory:a:b:c')
119
def test_memoryCheckerSucceeds(self):
121
Test that the checker works with valid credentials.
123
def _gotAvatar(username):
124
self.assertEquals(username, self.admin.username)
126
.requestAvatarId(self.admin)
127
.addCallback(_gotAvatar))
130
def test_memoryCheckerFailsUsername(self):
132
Test that the checker fails with an invalid username.
134
return self.assertFailure(self.checker.requestAvatarId(self.badUser),
135
error.UnauthorizedLogin)
138
def test_memoryCheckerFailsPassword(self):
140
Test that the checker fails with an invalid password.
142
return self.assertFailure(self.checker.requestAvatarId(self.badPass),
143
error.UnauthorizedLogin)
147
class TestAnonymousChecker(unittest.TestCase):
149
def test_isChecker(self):
151
Verifies that strcred.makeChecker('anonymous') returns an object
152
that implements the L{ICredentialsChecker} interface.
154
checker = strcred.makeChecker('anonymous')
155
self.assertTrue(checkers.ICredentialsChecker.providedBy(checker))
156
self.assertIn(credentials.IAnonymous, checker.credentialInterfaces)
159
def testAnonymousAccessSucceeds(self):
161
Test that we can log in anonymously using this checker.
163
checker = strcred.makeChecker('anonymous')
164
request = checker.requestAvatarId(credentials.Anonymous())
165
def _gotAvatar(avatar):
166
self.assertIdentical(checkers.ANONYMOUS, avatar)
167
return request.addCallback(_gotAvatar)
171
class TestUnixChecker(unittest.TestCase):
178
def _spwd(self, username):
179
return (username, crypt.crypt(self.users[username], 'F/'),
180
0, 0, 99999, 7, -1, -1, -1)
184
self.admin = credentials.UsernamePassword('admin', 'asdf')
185
self.alice = credentials.UsernamePassword('alice', 'foo')
186
self.badPass = credentials.UsernamePassword('alice', 'foobar')
187
self.badUser = credentials.UsernamePassword('x', 'yz')
188
self.checker = strcred.makeChecker('unix')
190
# Hack around the pwd and spwd modules, since we can't really
191
# go about reading your /etc/passwd or /etc/shadow files
193
database = UserDatabase()
194
for username, password in self.users.items():
196
username, crypt.crypt(password, 'F/'),
197
1000, 1000, username, '/home/' + username, '/bin/sh')
198
self.patch(pwd, 'getpwnam', database.getpwnam)
200
self._spwd_getspnam = spwd.getspnam
201
spwd.getspnam = self._spwd
206
spwd.getspnam = self._spwd_getspnam
209
def test_isChecker(self):
211
Verifies that strcred.makeChecker('unix') returns an object
212
that implements the L{ICredentialsChecker} interface.
214
self.assertTrue(checkers.ICredentialsChecker.providedBy(self.checker))
215
self.assertIn(credentials.IUsernamePassword,
216
self.checker.credentialInterfaces)
219
def test_unixCheckerSucceeds(self):
221
Test that the checker works with valid credentials.
223
def _gotAvatar(username):
224
self.assertEquals(username, self.admin.username)
226
.requestAvatarId(self.admin)
227
.addCallback(_gotAvatar))
230
def test_unixCheckerFailsUsername(self):
232
Test that the checker fails with an invalid username.
234
return self.assertFailure(self.checker.requestAvatarId(self.badUser),
235
error.UnauthorizedLogin)
238
def test_unixCheckerFailsPassword(self):
240
Test that the checker fails with an invalid password.
242
return self.assertFailure(self.checker.requestAvatarId(self.badPass),
243
error.UnauthorizedLogin)
246
if None in (pwd, spwd, crypt):
247
for method in (test_unixCheckerSucceeds,
248
test_unixCheckerFailsUsername,
249
test_unixCheckerFailsPassword):
250
method.skip = 'pwd and spwd are both unavailable'
254
class TestFileDBChecker(unittest.TestCase):
256
Test for the --auth=file:... file checker.
260
self.admin = credentials.UsernamePassword('admin', 'asdf')
261
self.alice = credentials.UsernamePassword('alice', 'foo')
262
self.badPass = credentials.UsernamePassword('alice', 'foobar')
263
self.badUser = credentials.UsernamePassword('x', 'yz')
264
self.filename = self.mktemp()
265
FilePath(self.filename).setContent('admin:asdf\nalice:foo\n')
266
self.checker = strcred.makeChecker('file:' + self.filename)
269
def _fakeFilename(self):
270
filename = '/DoesNotExist'
271
while os.path.exists(filename):
276
def test_isChecker(self):
278
Verifies that strcred.makeChecker('memory') returns an object
279
that implements the L{ICredentialsChecker} interface.
281
self.assertTrue(checkers.ICredentialsChecker.providedBy(self.checker))
282
self.assertIn(credentials.IUsernamePassword,
283
self.checker.credentialInterfaces)
286
def test_fileCheckerSucceeds(self):
288
Test that the checker works with valid credentials.
290
def _gotAvatar(username):
291
self.assertEquals(username, self.admin.username)
293
.requestAvatarId(self.admin)
294
.addCallback(_gotAvatar))
297
def test_fileCheckerFailsUsername(self):
299
Test that the checker fails with an invalid username.
301
return self.assertFailure(self.checker.requestAvatarId(self.badUser),
302
error.UnauthorizedLogin)
305
def test_fileCheckerFailsPassword(self):
307
Test that the checker fails with an invalid password.
309
return self.assertFailure(self.checker.requestAvatarId(self.badPass),
310
error.UnauthorizedLogin)
313
def test_failsWithEmptyFilename(self):
315
Test that an empty filename raises an error.
317
self.assertRaises(ValueError, strcred.makeChecker, 'file')
318
self.assertRaises(ValueError, strcred.makeChecker, 'file:')
321
def test_warnWithBadFilename(self):
323
When the file auth plugin is given a file that doesn't exist, it
324
should produce a warning.
326
oldOutput = cred_file.theFileCheckerFactory.errorOutput
327
newOutput = StringIO.StringIO()
328
cred_file.theFileCheckerFactory.errorOutput = newOutput
329
checker = strcred.makeChecker('file:' + self._fakeFilename())
330
cred_file.theFileCheckerFactory.errorOutput = oldOutput
331
self.assertIn(cred_file.invalidFileWarning, newOutput.getvalue())
335
class DummyOptions(usage.Options, strcred.AuthOptionMixin):
337
Simple options for testing L{strcred.AuthOptionMixin}.
342
class TestCheckerOptions(unittest.TestCase):
344
def test_createsList(self):
346
Test that the --auth command line creates a list in the
347
Options instance and appends values to it.
349
options = DummyOptions()
350
options.parseOptions(['--auth', 'memory'])
351
self.assertEqual(len(options['credCheckers']), 1)
352
options = DummyOptions()
353
options.parseOptions(['--auth', 'memory', '--auth', 'memory'])
354
self.assertEqual(len(options['credCheckers']), 2)
357
def test_invalidAuthError(self):
359
Test that the --auth command line raises an exception when it
360
gets a parameter it doesn't understand.
362
options = DummyOptions()
363
# If someone adds a 'ThisPluginDoesNotExist' then this unit
364
# test should still run.
365
invalidParameter = getInvalidAuthType()
368
options.parseOptions, ['--auth', invalidParameter])
371
options.parseOptions, ['--help-auth-type', invalidParameter])
374
def test_createsDictionary(self):
376
Test that the --auth command line creates a dictionary
377
mapping supported interfaces to the list of credentials
378
checkers that support it.
380
options = DummyOptions()
381
options.parseOptions(['--auth', 'memory', '--auth', 'anonymous'])
382
chd = options['credInterfaces']
383
self.assertEquals(len(chd[credentials.IAnonymous]), 1)
384
self.assertEquals(len(chd[credentials.IUsernamePassword]), 1)
385
chdAnonymous = chd[credentials.IAnonymous][0]
386
chdUserPass = chd[credentials.IUsernamePassword][0]
387
self.assertTrue(checkers.ICredentialsChecker.providedBy(chdAnonymous))
388
self.assertTrue(checkers.ICredentialsChecker.providedBy(chdUserPass))
389
self.assertIn(credentials.IAnonymous,
390
chdAnonymous.credentialInterfaces)
391
self.assertIn(credentials.IUsernamePassword,
392
chdUserPass.credentialInterfaces)
395
def test_credInterfacesProvidesLists(self):
397
Test that when two --auth arguments are passed along which
398
support the same interface, a list with both is created.
400
options = DummyOptions()
401
options.parseOptions(['--auth', 'memory', '--auth', 'unix'])
403
options['credCheckers'],
404
options['credInterfaces'][credentials.IUsernamePassword])
407
def test_listDoesNotDisplayDuplicates(self):
409
Test that the list for --help-auth does not duplicate items.
412
options = DummyOptions()
413
for cf in options._checkerFactoriesForOptHelpAuth():
414
self.assertNotIn(cf.authType, authTypes)
415
authTypes.append(cf.authType)
418
def test_displaysListCorrectly(self):
420
Test that the --help-auth argument correctly displays all
421
available authentication plugins, then exits.
423
newStdout = StringIO.StringIO()
424
options = DummyOptions()
425
options.authOutput = newStdout
426
self.assertRaises(SystemExit, options.parseOptions, ['--help-auth'])
427
for checkerFactory in strcred.findCheckerFactories():
428
self.assertIn(checkerFactory.authType, newStdout.getvalue())
431
def test_displaysHelpCorrectly(self):
433
Test that the --help-auth-for argument will correctly display
434
the help file for a particular authentication plugin.
436
newStdout = StringIO.StringIO()
437
options = DummyOptions()
438
options.authOutput = newStdout
440
SystemExit, options.parseOptions, ['--help-auth-type', 'file'])
441
for line in cred_file.theFileCheckerFactory.authHelp:
443
self.assertIn(line.strip(), newStdout.getvalue())
446
def test_unexpectedException(self):
448
When the checker specified by --auth raises an unexpected error, it
449
should be caught and re-raised within a L{usage.UsageError}.
451
options = DummyOptions()
452
err = self.assertRaises(usage.UsageError, options.parseOptions,
454
self.assertEquals(str(err),
455
"Unexpected error: 'file' requires a filename")
459
class OptionsForUsernamePassword(usage.Options, strcred.AuthOptionMixin):
460
supportedInterfaces = (credentials.IUsernamePassword,)
464
class OptionsForUsernameHashedPassword(usage.Options, strcred.AuthOptionMixin):
465
supportedInterfaces = (credentials.IUsernameHashedPassword,)
469
class OptionsSupportsAllInterfaces(usage.Options, strcred.AuthOptionMixin):
470
supportedInterfaces = None
474
class OptionsSupportsNoInterfaces(usage.Options, strcred.AuthOptionMixin):
475
supportedInterfaces = []
479
class TestLimitingInterfaces(unittest.TestCase):
481
Tests functionality that allows an application to limit the
482
credential interfaces it can support. For the purposes of this
483
test, we use IUsernameHashedPassword, although this will never
484
really be used by the command line.
486
(I have, to date, not thought of a half-decent way for a user to
487
specify a hash algorithm via the command-line. Nor do I think it's
490
I should note that, at first, this test is counter-intuitive,
491
because we're using the checker with a pre-defined hash function
492
as the 'bad' checker. See the documentation for
493
L{twisted.cred.checkers.FilePasswordDB.hash} for more details.
497
self.filename = self.mktemp()
498
file(self.filename, 'w').write('admin:asdf\nalice:foo\n')
499
self.goodChecker = checkers.FilePasswordDB(self.filename)
500
self.badChecker = checkers.FilePasswordDB(self.filename, hash=self._hash)
501
self.anonChecker = checkers.AllowAnonymousAccess()
504
def _hash(self, networkUsername, networkPassword, storedPassword):
506
A dumb hash that doesn't really do anything.
508
return networkPassword
511
def test_supportsInterface(self):
513
Test that the supportsInterface method behaves appropriately.
515
options = OptionsForUsernamePassword()
517
options.supportsInterface(credentials.IUsernamePassword))
519
options.supportsInterface(credentials.IAnonymous))
521
strcred.UnsupportedInterfaces, options.addChecker, self.anonChecker)
524
def test_supportsAllInterfaces(self):
526
Test that the supportsInterface method behaves appropriately
527
when the supportedInterfaces attribute is None.
529
options = OptionsSupportsAllInterfaces()
531
options.supportsInterface(credentials.IUsernamePassword))
533
options.supportsInterface(credentials.IAnonymous))
536
def test_supportsCheckerFactory(self):
538
Test that the supportsCheckerFactory method behaves appropriately.
540
options = OptionsForUsernamePassword()
541
fileCF = cred_file.theFileCheckerFactory
542
anonCF = cred_anonymous.theAnonymousCheckerFactory
543
self.assertTrue(options.supportsCheckerFactory(fileCF))
544
self.assertFalse(options.supportsCheckerFactory(anonCF))
547
def test_canAddSupportedChecker(self):
549
Test that when addChecker is called with a checker that
550
implements at least one of the interfaces our application
551
supports, it is successful.
553
options = OptionsForUsernamePassword()
554
options.addChecker(self.goodChecker)
555
iface = options.supportedInterfaces[0]
556
# Test that we did get IUsernamePassword
557
self.assertIdentical(options['credInterfaces'][iface][0], self.goodChecker)
558
self.assertIdentical(options['credCheckers'][0], self.goodChecker)
559
# Test that we didn't get IUsernameHashedPassword
560
self.assertEquals(len(options['credInterfaces'][iface]), 1)
561
self.assertEquals(len(options['credCheckers']), 1)
564
def test_failOnAddingUnsupportedChecker(self):
566
Test that when addChecker is called with a checker that does
567
not implement any supported interfaces, it fails.
569
options = OptionsForUsernameHashedPassword()
570
self.assertRaises(strcred.UnsupportedInterfaces,
571
options.addChecker, self.badChecker)
574
def test_unsupportedInterfaceError(self):
576
Test that the --auth command line raises an exception when it
577
gets a checker we don't support.
579
options = OptionsSupportsNoInterfaces()
580
authType = cred_anonymous.theAnonymousCheckerFactory.authType
583
options.parseOptions, ['--auth', authType])
586
def test_helpAuthLimitsOutput(self):
588
Test that --help-auth will only list checkers that purport to
589
supply at least one of the credential interfaces our
592
options = OptionsForUsernamePassword()
593
for factory in options._checkerFactoriesForOptHelpAuth():
595
for interface in factory.credentialInterfaces:
596
if options.supportsInterface(interface):
599
raise strcred.UnsupportedInterfaces()
602
def test_helpAuthTypeLimitsOutput(self):
604
Test that --help-auth-type will display a warning if you get
605
help for an authType that does not supply at least one of the
606
credential interfaces our application can use.
608
options = OptionsForUsernamePassword()
609
# Find an interface that we can use for our test
610
invalidFactory = None
611
for factory in strcred.findCheckerFactories():
612
if not options.supportsCheckerFactory(factory):
613
invalidFactory = factory
615
self.assertNotIdentical(invalidFactory, None)
616
# Capture output and make sure the warning is there
617
newStdout = StringIO.StringIO()
618
options.authOutput = newStdout
619
self.assertRaises(SystemExit, options.parseOptions,
620
['--help-auth-type', 'anonymous'])
621
self.assertIn(strcred.notSupportedWarning, newStdout.getvalue())