Skip to content

Commit 57d2e60

Browse files
author
Ben Weaver
committed
Initial implementation; SimpleAuth and PLAIN
0 parents  commit 57d2e60

File tree

6 files changed

+178
-0
lines changed

6 files changed

+178
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.py?

sasl/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from __future__ import absolute_import
2+
from .mechanism import *
3+
from .auth import *
4+
from . import plain

sasl/auth.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from __future__ import absolute_import
2+
import abc
3+
4+
__all__ = ('Authenticator', 'SimpleAuth')
5+
6+
class Authenticator(object):
7+
"""A basic authentication interface used by SASL mechanisms."""
8+
9+
__metaclass__ = abc.ABCMeta
10+
11+
@abc.abstractmethod
12+
def authentication_id(self):
13+
"""Identify the entity being authenticated (e.g. username)."""
14+
15+
@abc.abstractmethod
16+
def password(self):
17+
"""The password associated with the authentication_id."""
18+
19+
@abc.abstractmethod
20+
def verify_password(self, authzid, authcid, passwd):
21+
return False
22+
23+
def authorization_id(self):
24+
"""Identify the effective entity if authentication
25+
succeeds."""
26+
return u''
27+
28+
class SimpleAuth(Authenticator):
29+
"""Authenticate from a Mapping."""
30+
31+
def __init__(self, entities, authcid, passwd, authzid=None):
32+
self.entities = entities
33+
self.authcid = authcid
34+
self.passwd = passwd
35+
self.authzid = authzid
36+
37+
def authentication_id(self):
38+
value = self.authcid()
39+
if not value:
40+
raise RuntimeError('Undefined authentication entity.')
41+
return value
42+
43+
def password(self):
44+
return self.passwd()
45+
46+
def authorization_id(self):
47+
return self.authzid and self.authzid()
48+
49+
def verify_password(self, authzid, authcid, passwd):
50+
try:
51+
return self.entities[authcid] == passwd
52+
except KeyError:
53+
return False
54+

sasl/mechanism.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""mechanism.py -- SASL mechanism registry
2+
3+
<http://tools.ietf.org/html/rfc2222>
4+
<http://www.iana.org/assignments/sasl-mechanisms>
5+
"""
6+
from __future__ import absolute_import
7+
import abc
8+
9+
__all__ = ('define', 'Mechanism')
10+
11+
MECHANISMS = {}
12+
13+
def define(name=None):
14+
"""A class decorator that registers a SASL mechanism."""
15+
16+
def decorator(cls):
17+
return register(name or cls.__name__, cls)
18+
19+
return decorator
20+
21+
def register(name, cls):
22+
"""Register a SASL mechanism."""
23+
24+
MECHANISMS[name.upper()] = cls
25+
return cls
26+
27+
class MechanismType(abc.ABCMeta):
28+
"""This metaclass registers a SASL mechanism when it's defined."""
29+
30+
def __new__(mcls, name, bases, attr):
31+
cls = abc.ABCMeta.__new__(mcls, name, bases, attr)
32+
return register(name, cls)
33+
34+
class Mechanism(object):
35+
"""The SASL mechanism interface."""
36+
37+
__metaclass__ = MechanismType
38+
__slots__ = ()
39+
40+
@abc.abstractmethod
41+
def challenge(self):
42+
"""Issue a challenge."""
43+
44+
@abc.abstractmethod
45+
def respond(self, challenge):
46+
"""Respond to a challenge."""
47+
48+
@abc.abstractmethod
49+
def verify_challenge(self, response):
50+
"""Verify a challenge. Return True if the response was
51+
verified. Return False if the challenge failed."""

sasl/plain.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""plain -- simple, unencrypted user/password authentication
2+
3+
<http://www.ietf.org/rfc/rfc4616.txt>
4+
"""
5+
from __future__ import absolute_import
6+
from . import mechanism as mech
7+
8+
__all__ = ('Plain', )
9+
10+
class Plain(mech.Mechanism):
11+
"""The plain mechanism simply submits the optional authorization
12+
id, the authentication id, and password separated by null
13+
bytes."""
14+
15+
NULL = u'0x00'
16+
17+
def __init__(self, auth):
18+
self.auth = auth
19+
20+
def challenge(self):
21+
return ''
22+
23+
def respond(self, data):
24+
assert data == ''
25+
26+
auth = self.auth
27+
zid = auth.authorization_id()
28+
cid = auth.authentication_id()
29+
30+
return self.NULL.join((
31+
u'' if (not zid or zid == cid) else zid,
32+
(cid or u''),
33+
(auth.password() or u'')
34+
)).encode('utf-8')
35+
36+
def verify_challenge(self, response):
37+
try:
38+
(zid, cid, passwd) = response.decode('utf-8').split(self.NULL)
39+
except ValueError:
40+
return False
41+
42+
return self.auth.verify_password(zid, cid, passwd)

sasl/tests.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from __future__ import absolute_import
2+
import unittest
3+
from md import fluid
4+
from . import *
5+
6+
USER = fluid.cell()
7+
PASS = fluid.cell()
8+
9+
class TestPlain(unittest.TestCase):
10+
11+
def setUp(self):
12+
users = { 'foo@bar.com': 'baz' }
13+
self.auth = SimpleAuth(users, lambda: USER.value, lambda: PASS.value)
14+
self.mech = plain.Plain(self.auth)
15+
16+
def test_success(self):
17+
ch = self.mech.challenge()
18+
with fluid.let((USER, 'foo@bar.com'), (PASS, 'baz')):
19+
re = self.mech.respond(ch)
20+
self.assert_(self.mech.verify_challenge(re))
21+
22+
def test_failure(self):
23+
ch = self.mech.challenge()
24+
with fluid.let((USER, 'foo@bar.com'), (PASS, '')):
25+
re = self.mech.respond(ch)
26+
self.assertFalse(self.mech.verify_challenge(re))

0 commit comments

Comments
 (0)