diff options
author | Michał Górny <mgorny@gentoo.org> | 2013-08-22 14:16:55 +0200 |
---|---|---|
committer | Michał Górny <mgorny@gentoo.org> | 2013-08-25 22:45:10 +0200 |
commit | c8c41f930089a519fb2c53a3efb6a37801719a3d (patch) | |
tree | b06cfd41aad245a5c777f6673df28b8b0261a63f /okupy | |
parent | Reset RNG in @postfork. (diff) | |
download | identity.gentoo.org-c8c41f930089a519fb2c53a3efb6a37801719a3d.tar.gz identity.gentoo.org-c8c41f930089a519fb2c53a3efb6a37801719a3d.tar.bz2 identity.gentoo.org-c8c41f930089a519fb2c53a3efb6a37801719a3d.zip |
Support authentication using SSH.
Diffstat (limited to 'okupy')
-rw-r--r-- | okupy/accounts/ssh.py | 26 | ||||
-rw-r--r-- | okupy/accounts/views.py | 12 | ||||
-rw-r--r-- | okupy/common/auth.py | 46 | ||||
-rw-r--r-- | okupy/common/ssh.py | 18 | ||||
-rw-r--r-- | okupy/settings/__init__.py | 1 | ||||
-rw-r--r-- | okupy/templates/login.html | 7 | ||||
-rw-r--r-- | okupy/tests/settings.py | 1 | ||||
-rw-r--r-- | okupy/tests/unit/test_auth.py | 33 | ||||
-rw-r--r-- | okupy/tests/vars.py | 6 |
9 files changed, 146 insertions, 4 deletions
diff --git a/okupy/accounts/ssh.py b/okupy/accounts/ssh.py index 83d1f10..37ec5c1 100644 --- a/okupy/accounts/ssh.py +++ b/okupy/accounts/ssh.py @@ -1,8 +1,34 @@ # vim:fileencoding=utf8:et:ts=4:sts=4:sw=4:ft=python +from django.contrib.auth import authenticate, login + +from ..common.test_helpers import set_request +from ..crypto.ciphers import sessionrefcipher +from ..otp import init_otp + + ssh_handlers = {} def ssh_handler(f): ssh_handlers[f.__name__] = f return f + + +@ssh_handler +def auth(session_id, key): + try: + session = sessionrefcipher.decrypt(session_id) + except ValueError: + return None + + request = set_request('/') + + user = authenticate(ssh_key=key) + if user and user.is_active: + login(request, user) + init_otp(request) + session.update(request.session) + session.save() + return 'Authenticated.' + return None diff --git a/okupy/accounts/views.py b/okupy/accounts/views.py index 103b267..96fc5d3 100644 --- a/okupy/accounts/views.py +++ b/okupy/accounts/views.py @@ -173,6 +173,7 @@ def login(request): if is_otp or strong_auth_req: ssl_auth_form = None ssl_auth_uri = None + ssh_auth_command = None else: encrypted_id = sessionrefcipher.encrypt(request.session) @@ -189,12 +190,23 @@ def login(request): ssl_auth_path = reverse(ssl_auth) ssl_auth_uri = urljoin('https://' + ssl_auth_host, ssl_auth_path) + if settings.SSH_BIND[1] == 22: + ssh_port_opt = '' + else: + ssh_port_opt = '-p %d ' % settings.SSH_BIND[1] + + ssh_auth_command = 'ssh %sauth+%s@%s' % ( + ssh_port_opt, + encrypted_id, + request.get_host().split(':')[0]) + return render(request, 'login.html', { 'login_form': login_form, 'openid_request': oreq, 'next': next, 'ssl_auth_uri': ssl_auth_uri, 'ssl_auth_form': ssl_auth_form, + 'ssh_auth_command': ssh_auth_command, 'is_otp': is_otp, }) diff --git a/okupy/common/auth.py b/okupy/common/auth.py index d7a7f95..9d4b205 100644 --- a/okupy/common/auth.py +++ b/okupy/common/auth.py @@ -8,6 +8,10 @@ from ..accounts.models import LDAPUser from OpenSSL.crypto import load_certificate, FILETYPE_PEM +import paramiko + +import base64 + class SSLCertAuthBackend(ModelBackend): """ @@ -47,3 +51,45 @@ class SSLCertAuthBackend(ModelBackend): user = UserModel.objects.get(**attr_dict) return user return None + + +class SSHKeyAuthBackend(ModelBackend): + """ + Authentication backend that uses SSH keys stored in LDAP. + """ + + def authenticate(self, ssh_key=None): + for u in LDAPUser.objects.all(): + for k in u.ssh_key: + spl = k.split() + if len(spl) < 2: + continue + + form, user_key = spl[:2] + if form == 'ssh-rsa': + key_class = paramiko.RSAKey + elif form == 'ssh-dss': + key_class = paramiko.DSSKey + else: + # key format not supported + continue + + try: + user_key = key_class(data=base64.b64decode(user_key)) + except (TypeError, paramiko.SSHException): + continue + + # paramiko reconstructs the key, so simple match should be fine + if ssh_key == user_key: + UserModel = get_user_model() + attr_dict = { + UserModel.USERNAME_FIELD: u.username + } + + user = UserModel(**attr_dict) + try: + user.save() + except IntegrityError: + user = UserModel.objects.get(**attr_dict) + return user + return None diff --git a/okupy/common/ssh.py b/okupy/common/ssh.py index 8854690..830f840 100644 --- a/okupy/common/ssh.py +++ b/okupy/common/ssh.py @@ -18,10 +18,18 @@ LISTEN_BACKLOG = 20 class SSHServer(paramiko.ServerInterface): + def __init__(self): + paramiko.ServerInterface.__init__(self) + self._message = None + def get_allowed_auths(self, username): return 'publickey' def check_auth_publickey(self, username, key): + # for some reason, this is called twice... + if self._message: + return paramiko.AUTH_SUCCESSFUL + spl = username.split('+') cmd = spl[0] args = spl[1:] @@ -33,7 +41,9 @@ class SSHServer(paramiko.ServerInterface): except (KeyError, TypeError) as e: pass else: - if h(*args, key=key): + ret = h(*args, key=key) + if ret is not None: + self._message = ret return paramiko.AUTH_SUCCESSFUL return paramiko.AUTH_FAILED @@ -46,15 +56,17 @@ class SSHServer(paramiko.ServerInterface): return False def check_channel_exec_request(self, channel, command): - channel.send('Not yet implemented, sorry.\r\n') + channel.send('%s\r\n' % self._message) channel.shutdown(2) channel.close() + self._message = None return True def check_channel_shell_request(self, channel): - channel.send('Not yet implemented, sorry.\r\n') + channel.send('%s\r\n' % self._message) channel.shutdown(2) channel.close() + self._message = None return True def check_channel_pty_request(self, channel, term, width, height, diff --git a/okupy/settings/__init__.py b/okupy/settings/__init__.py index e88939b..bdada0a 100644 --- a/okupy/settings/__init__.py +++ b/okupy/settings/__init__.py @@ -28,6 +28,7 @@ MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage' AUTHENTICATION_BACKENDS = ( 'django_auth_ldap.backend.LDAPBackend', 'okupy.common.auth.SSLCertAuthBackend', + 'okupy.common.auth.SSHKeyAuthBackend', ) MIDDLEWARE_CLASSES = ( diff --git a/okupy/templates/login.html b/okupy/templates/login.html index f444028..aa9e169 100644 --- a/okupy/templates/login.html +++ b/okupy/templates/login.html @@ -37,6 +37,13 @@ </form> </p> {% endif %} + {% if ssh_auth_command %} + <p> + Login via SSH: + <code>{{ ssh_auth_command }}</code> + and <a href="{{ next }}">continue</a> + </p> + {% endif %} {% if not is_otp %} <a href="/recover">Forgot your password?</a> {% endif %} diff --git a/okupy/tests/settings.py b/okupy/tests/settings.py index b626ca5..74dfa84 100644 --- a/okupy/tests/settings.py +++ b/okupy/tests/settings.py @@ -28,6 +28,7 @@ MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage' AUTHENTICATION_BACKENDS = ( 'django_auth_ldap.backend.LDAPBackend', 'okupy.common.auth.SSLCertAuthBackend', + 'okupy.common.auth.SSHKeyAuthBackend', ) MIDDLEWARE_CLASSES = ( diff --git a/okupy/tests/unit/test_auth.py b/okupy/tests/unit/test_auth.py index 27e6618..97c7ddb 100644 --- a/okupy/tests/unit/test_auth.py +++ b/okupy/tests/unit/test_auth.py @@ -8,6 +8,15 @@ from django.contrib.auth import authenticate from .. import vars from ...common.test_helpers import OkupyTestCase, set_request +import base64 + +import paramiko + + +def get_ssh_key(person, number=0): + keystr = person['sshPublicKey'][number] + return base64.b64decode(keystr.split()[1]) + class AuthUnitTests(OkupyTestCase): @classmethod @@ -60,3 +69,27 @@ class AuthUnitTests(OkupyTestCase): u = authenticate(request=request) self.assertIs(u, None) + + def test_valid_rsa_ssh_key_authenticates_alice(self): + alice = vars.DIRECTORY['uid=alice,ou=people,o=test'] + key = paramiko.RSAKey(data=get_ssh_key(alice)) + u = authenticate(ssh_key=key) + self.assertEqual(u.username, alice['uid'][0]) + + def test_valid_dss_ssh_key_authenticates_bob(self): + bob = vars.DIRECTORY['uid=bob,ou=people,o=test'] + key = paramiko.DSSKey(data=get_ssh_key(bob, 1)) + u = authenticate(ssh_key=key) + self.assertEqual(u.username, bob['uid'][0]) + + def test_valid_rsa_key_with_comment_authenticates_bob(self): + bob = vars.DIRECTORY['uid=bob,ou=people,o=test'] + key = paramiko.RSAKey(data=get_ssh_key(bob)) + u = authenticate(ssh_key=key) + self.assertEqual(u.username, bob['uid'][0]) + + def test_unknown_ssh_key_returns_none(self): + key = paramiko.RSAKey( + data=base64.b64decode(vars.TEST_SSH_KEY_FOR_NO_USER)) + u = authenticate(ssh_key=key) + self.assertIs(u, None) diff --git a/okupy/tests/vars.py b/okupy/tests/vars.py index 44270a6..c2e9152 100644 --- a/okupy/tests/vars.py +++ b/okupy/tests/vars.py @@ -32,6 +32,7 @@ DIRECTORY = { "gentooRoles": ["kde, qt, cluster"], "gentooLocation": ["City1, Country1"], "gentooACL": ["user.group", "developer.group"], + "sshPublicKey": ["ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCbtxfr9vRO4xkDuUnsu02rL7BtBiABADkWdugnMxRAV6nKokitytgLGDhjY6iB8C87K8mCxz/ksMO+uct/lUEHMf1M2P1rPEStrJoXQuTXQbtVl7iF5cySbXhtd7Nu7DcXe1cIynVkbFosB2mznr8Db3633DnEslppUGvHdjHYoCAWsjv5juHESkBy62HhYgc1ZoGFj6ilrJhOdHs2ji2YBHJXPG2sB3uQleY5/KfAeSwESBH7D36VqRXf22Ya0nExnVh3h9jtzZmwIll35VHH/G9NmTmW/8lpl7BGV7fx10tByfvSLrQg2ZniiY3SfXdbraVm/FEuJ9+X81jpNQDd", "invalid-key-too-short", "ssh-rsa $$$INVALID%", "invalid-key-type AAAA=="], }, "uid=bob,ou=people,o=test": { "uid": ["bob"], @@ -45,7 +46,8 @@ DIRECTORY = { "mail": ["bob@test.com"], "gentoRoles": ["nothing"], "gentooLocation": ["City2, Country2"], - "gentooACL": ["user.group", "foundation.group"] + "gentooACL": ["user.group", "foundation.group"], + "sshPublicKey": ["ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUSOgwQ6uljefD9BiwhiGzRGn+sg7D3AKcqU8PWrB+p74n9GBIccc/iSuG458iid08FvUqHjY0RLwMQADND7NOGaEEW0NXbyblA6xZhZu6BgnFC4LZBHy5eok+sWIZddAgT8qAYXMW8GYzUZSPchtOFbMkyzaQlWYkjx1Z0usOdnl/QRPuabFTQjWtJ+lw8hrPydl1ZYP+FIUZy9NU/SxC2qgufmh3+nTzfnfQgupfQc6I9lXNR98vm/t5saVsuQReIIc4sR3mOmT5AnH6uCyjRBKnxq8ndcInfGagwpcx80o6+/V0QNIdr5NP1jRiXDbc/BT8NP/X4mWIpJNEIujj bob@example.com", "ssh-dss AAAAB3NzaC1kc3MAAACBAOpXehglYVU5efZoBGrRKHcsQvlS4jDAFGgsqNRQwM4F7anFIhaEYxs8REEhKNOUXEalFCUegtBxgKjvNRH+MBMJ5o6BAsDuTobwhFS7imcj5JO7QA6kfyNokNkULbqCOfmS9xmFozj2bk0zpKcvW54Zf91dHHT+NsmAXrcIw1onAAAAFQDLARFN4O0wquVKl/XGItngEeQGdwAAAIEAtTP8JkR9XZHkqb0s/uRA+2Wh9uOipc1+IgJn+UX15or2/zuudcG5loaVpDepuLuzhjrn/BZwj1GAncv/AFo4YraATU77HxNEXstHwkf5K8FaJ2f/6bVs7i/P9NS9rXys+HdOiPmAbvv9Hm69jw/Xbwnz752O7gvSNJPWjrC2460AAACBAItwlTJ2aUD7BSgjgaqGOrjUamnIMOi833RCc2XN9F9aY2z8DNr3O7KN5qzTUuLU4ltQbBO9Ct5CZmx785COTkJMXjoYVC7ObfKc8T0xB1FZzf7bIaqcC0dDmfrCzmcQdOTIJvKNlniRBG1XAQ7lf7YvX0We+C14oVU2FhyueoEe", "invalid-key-too-short", "ssh-rsa $$$INVALID%", "invalid-key-type AAAA=="], }, "uid=jack,ou=people,o=test": { "uid": ["jack"], @@ -147,3 +149,5 @@ gmp1MA0GCSqGSIb3DQEBBQUAA2EAH+Qaz/Dmd5QqU1pVgPUz2loWQhy+cX6bgubJ vj3k/SSqj6qjnxryY6QSKWOTRbKhwmRHrrsFRuR2rCZWYZUJ6ohCDYrwVKvs7i2R VNG3Q7+oqLajmyDfZmHkENQ0rCdc -----END CERTIFICATE-----''' + +TEST_SSH_KEY_FOR_NO_USER = 'AAAAB3NzaC1yc2EAAAADAQABAAAAYQCXMUpwxMi/01Th94+pP9r3bPGOEejSic7eH1VXHnqHPRFh9rOenSbhWLXwCUcM+0ZMoLmkJ3gMz3IKq2HTJfEwBcW/v/cm5b2lT6biO0u9Q5br4KosNhrvJBZ0f6trkCk=' |