aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichał Górny <mgorny@gentoo.org>2013-08-25 13:51:13 -0700
committerMichał Górny <mgorny@gentoo.org>2013-08-25 13:51:13 -0700
commita9d6a4b1baad577691a1f0b8eb7a8531b9de5086 (patch)
tree4e26d4dc3e76dd7ab5b72ba08f43358e5479298b
parentMerge pull request #79 from tampakrap/tests_v3 (diff)
parentAdd tests for SSH handlers. (diff)
downloadidentity.gentoo.org-a9d6a4b1baad577691a1f0b8eb7a8531b9de5086.tar.gz
identity.gentoo.org-a9d6a4b1baad577691a1f0b8eb7a8531b9de5086.tar.bz2
identity.gentoo.org-a9d6a4b1baad577691a1f0b8eb7a8531b9de5086.zip
Merge pull request #76 from mgorny/ssh-auth
SSH authentication support
-rw-r--r--okupy/accounts/ssh.py30
-rw-r--r--okupy/accounts/views.py12
-rw-r--r--okupy/common/auth.py46
-rw-r--r--okupy/common/ssh.py112
-rw-r--r--okupy/settings/__init__.py5
-rw-r--r--okupy/settings/local.py.sample9
-rw-r--r--okupy/templates/login.html7
-rw-r--r--okupy/tests/settings.py19
-rw-r--r--okupy/tests/unit/test_auth.py51
-rw-r--r--okupy/tests/unit/test_ssh.py189
-rw-r--r--okupy/tests/vars.py6
-rw-r--r--okupy/wsgi.py14
-rw-r--r--requirements.txt1
-rwxr-xr-xsetup.py1
14 files changed, 498 insertions, 4 deletions
diff --git a/okupy/accounts/ssh.py b/okupy/accounts/ssh.py
new file mode 100644
index 0000000..4e5028e
--- /dev/null
+++ b/okupy/accounts/ssh.py
@@ -0,0 +1,30 @@
+# vim:fileencoding=utf8:et:ts=4:sts=4:sw=4:ft=python
+
+from django.contrib.auth import authenticate, login
+
+from ..common.ssh import ssh_handler
+from ..common.test_helpers import set_request
+from ..crypto.ciphers import sessionrefcipher
+from ..otp import init_otp
+
+
+ssh_handlers = {}
+
+
+@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
new file mode 100644
index 0000000..1c4a49a
--- /dev/null
+++ b/okupy/common/ssh.py
@@ -0,0 +1,112 @@
+# vim:fileencoding=utf8:et:ts=4:sts=4:sw=4:ft=python
+
+from django.conf import settings
+
+import paramiko
+
+from io import BytesIO
+
+import asyncore
+import inspect
+import socket
+import threading
+
+
+LISTEN_BACKLOG = 20
+
+
+def ssh_handler(f):
+ if not hasattr(settings, 'SSH_HANDLERS'):
+ settings.SSH_HANDLERS = {}
+ settings.SSH_HANDLERS[f.__name__] = f
+ return f
+
+
+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... therefore, we need
+ # to cache the result since token will be revoked on first use
+ if self._message:
+ return paramiko.AUTH_SUCCESSFUL
+
+ spl = username.split('+')
+ cmd = spl[0]
+ args = spl[1:]
+
+ try:
+ h = settings.SSH_HANDLERS[cmd]
+ # this is an easy way of checking if we have correct args
+ inspect.getcallargs(h, *args, key=key)
+ except (KeyError, TypeError) as e:
+ pass
+ else:
+ ret = h(*args, key=key)
+ if ret is not None:
+ self._message = ret
+ return paramiko.AUTH_SUCCESSFUL
+ return paramiko.AUTH_FAILED
+
+ def check_channel_request(self, kind, chanid):
+ if kind == 'session':
+ return paramiko.OPEN_SUCCEEDED
+ return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
+
+ def check_channel_subsystem_request(self, channel, name):
+ return False
+
+ def check_channel_exec_request(self, channel, command):
+ 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('%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,
+ pixelwidth, pixelheight, modes):
+ return True
+
+
+class SSHDispatcher(asyncore.dispatcher):
+ def __init__(self, server_key):
+ asyncore.dispatcher.__init__(self)
+ self._server_key = server_key
+
+ self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.set_reuse_addr()
+ self.bind(settings.SSH_BIND)
+ self.listen(LISTEN_BACKLOG)
+
+ def handle_accepted(self, conn, addr):
+ t = paramiko.Transport(conn)
+ t.add_server_key(self._server_key)
+ # we need a dummy Event to make it non-blocking
+ # but we don't really need to play with it
+ t.start_server(event=threading.Event(), server=SSHServer())
+
+ # python<3.2 compat
+ def handle_accept(self):
+ ret = self.accept()
+ if ret is not None:
+ self.handle_accepted(*ret)
+
+
+def ssh_main():
+ server_key = paramiko.RSAKey(file_obj=BytesIO(settings.SSH_SERVER_KEY))
+
+ disp = SSHDispatcher(server_key)
+ asyncore.loop()
+ raise SystemError('SSH server loop exited')
diff --git a/okupy/settings/__init__.py b/okupy/settings/__init__.py
index 767bb22..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 = (
@@ -143,6 +144,10 @@ LOGGING = {
'handlers': ['console' if DEBUG else 'syslog'],
'level': 'DEBUG' if DEBUG else 'INFO',
},
+ 'paramiko': {
+ 'handlers': ['console' if DEBUG else 'syslog'],
+ 'level': 'DEBUG' if DEBUG else 'INFO',
+ }
}
}
diff --git a/okupy/settings/local.py.sample b/okupy/settings/local.py.sample
index 245d48a..172b78f 100644
--- a/okupy/settings/local.py.sample
+++ b/okupy/settings/local.py.sample
@@ -79,3 +79,12 @@ AUTH_LDAP_USER_OBJECTCLASS = ['top', 'person', 'organizationalPerson',
'inetOrgPerson', 'posixAccount', 'shadowAccount']
# additional objectClasses that are used by developers
AUTH_LDAP_DEV_OBJECTCLASS = ['developerAccount']
+
+# replace with your preferred port
+SSH_BIND = ('0.0.0.0', 8022)
+# paste your *server* private key here
+SSH_SERVER_KEY = '''
+-----BEGIN RSA PRIVATE KEY-----
+...
+-----END RSA PRIVATE KEY-----
+'''
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 316f165..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 = (
@@ -290,3 +291,21 @@ DATABASES['ldap'] = {
DATABASE_ROUTERS = ['ldapdb.router.Router']
TEST_RUNNER = 'discover_runner.DiscoverRunner'
+
+# replace with your preferred port
+SSH_BIND = ('0.0.0.0', 8022)
+# paste your *server* private key here
+SSH_SERVER_KEY = '''
+-----BEGIN RSA PRIVATE KEY-----
+MIIBywIBAAJhANlKTt3/R+xVCgQbUNfqVWt28iHXJ+OCtqqVHaiLp9syBEGNiowK
+KQZ3swwuaa5WSPFwrsmd1/+REm2HdnQITSF/HSAPtOpbv4xaJ45iX1S4JhOK88c+
+UuBrg+lOeclIHQIDAQABAmEAm9fr0NTzJNGpKWDeDr4HHdhluVezSD3L/XSNnQDt
+Fw08eDeoEuCGpBjd1fLD4UIIJHBzc+8ybLZCTgE8JebokHGJxkk8tRt7NgVwnUws
+arTKHqb4nOCu6s0re9IkSL0RAjEA9SV9PWk6J+smQQvVWIh6K+PnLrSV9C3LSisK
+U8Zfrffly03j5sanjV5ZKx7bRJsXAjEA4ukYNYz3GPqQaYzDP/OObRZwQ2Iema2B
+fCbBiokfh4w5Hio+UvbgDD+cBYCjcqbrAjAwjYBEjXbLOTOWZnWW11D7KGQ9R977
+QaalxeiBtyR0HEkS/xZIOsgso6cddzsOV3kCMQCpxK4JOsuRE77CSb+3dDkmYvhh
+YeL1JbxQMArz5H4DgyUk7YQtvGmKoHjSIRmo6TsCMH+7CSP/hnzR1GXLwpeQ+dgp
+IwszrV7pBZ5anBHnmPqwqBWF/I/0IIrtouUHzs9XHg==
+-----END RSA PRIVATE KEY-----
+'''
diff --git a/okupy/tests/unit/test_auth.py b/okupy/tests/unit/test_auth.py
index 27e6618..7c445f6 100644
--- a/okupy/tests/unit/test_auth.py
+++ b/okupy/tests/unit/test_auth.py
@@ -4,12 +4,17 @@ from mockldap import MockLdap
from django.conf import settings
from django.contrib.auth import authenticate
+from django.test import TestCase
from .. import vars
-from ...common.test_helpers import OkupyTestCase, set_request
+from ...common.test_helpers import ldap_users, set_request
+import base64
-class AuthUnitTests(OkupyTestCase):
+import paramiko
+
+
+class AuthSSLUnitTests(TestCase):
@classmethod
def setUpClass(cls):
cls.mockldap = MockLdap(vars.DIRECTORY)
@@ -60,3 +65,45 @@ class AuthUnitTests(OkupyTestCase):
u = authenticate(request=request)
self.assertIs(u, None)
+
+
+class AuthSSHUnitTests(TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls.mockldap = MockLdap(vars.DIRECTORY)
+
+ def setUp(self):
+ self.mockldap.start()
+ self.ldapobject = self.mockldap[settings.AUTH_LDAP_SERVER_URI]
+
+ def tearDown(self):
+ self.mockldap.stop()
+
+ @staticmethod
+ def get_ssh_key(person, number=0):
+ keystr = person['sshPublicKey'][number]
+ return base64.b64decode(keystr.split()[1])
+
+ def test_valid_rsa_ssh_key_authenticates_alice(self):
+ dn, alice = ldap_users('alice')
+ key = paramiko.RSAKey(data=self.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):
+ dn, bob = ldap_users('bob')
+ key = paramiko.DSSKey(data=self.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):
+ dn, bob = ldap_users('bob')
+ key = paramiko.RSAKey(data=self.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/unit/test_ssh.py b/okupy/tests/unit/test_ssh.py
new file mode 100644
index 0000000..915be6a
--- /dev/null
+++ b/okupy/tests/unit/test_ssh.py
@@ -0,0 +1,189 @@
+# vim:fileencoding=utf8:et:ts=4:sts=4:sw=4:ft=python
+
+from django.conf import settings
+from django.test import TestCase
+from django.test.utils import override_settings
+
+import base64
+import socket
+
+import paramiko
+
+from ..vars import TEST_SSH_KEY_FOR_NO_USER
+from ...common.ssh import ssh_handler, SSHServer
+from ...common.exceptions import OkupyError
+
+
+@override_settings(SSH_HANDLERS={})
+class SSHUnitTests(TestCase):
+ def setUp(self):
+ self._key = paramiko.RSAKey(
+ data=base64.b64decode(TEST_SSH_KEY_FOR_NO_USER))
+ self._server = SSHServer()
+
+ def test_ssh_handler_decorator_works(self):
+ @ssh_handler
+ def test(key):
+ pass
+
+ self.assertEqual(settings.SSH_HANDLERS.get('test'), test)
+
+ def test_noarg_handler_works(self):
+ @ssh_handler
+ def noarg(key):
+ return 'yay'
+
+ self.assertEqual(
+ self._server.check_auth_publickey('noarg', self._key),
+ paramiko.AUTH_SUCCESSFUL)
+
+ def test_failure_is_propagated_properly(self):
+ @ssh_handler
+ def failing(key):
+ return None
+
+ self.assertEqual(
+ self._server.check_auth_publickey('failing', self._key),
+ paramiko.AUTH_FAILED)
+
+ def test_argument_splitting_works(self):
+ @ssh_handler
+ def twoarg(a, b, key):
+ if a == '1' and b == '2':
+ return 'yay'
+ else:
+ return None
+
+ self.assertEqual(
+ self._server.check_auth_publickey('twoarg+1+2', self._key),
+ paramiko.AUTH_SUCCESSFUL)
+
+ def test_default_arguments_work(self):
+ @ssh_handler
+ def oneortwoarg(a, b='3', key=None):
+ if not key:
+ raise ValueError('key must not be None')
+ if a == '1' and b == '3':
+ return 'yay'
+ else:
+ return None
+
+ self.assertEqual(
+ self._server.check_auth_publickey('oneortwoarg+1', self._key),
+ paramiko.AUTH_SUCCESSFUL)
+
+ def test_wrong_command_returns_failure(self):
+ @ssh_handler
+ def somehandler(key):
+ return 'er?'
+
+ self.assertEqual(
+ self._server.check_auth_publickey('otherhandler', self._key),
+ paramiko.AUTH_FAILED)
+
+ def test_missing_arguments_return_failure(self):
+ @ssh_handler
+ def onearg(arg, key):
+ return 'er?'
+
+ self.assertEqual(
+ self._server.check_auth_publickey('onearg', self._key),
+ paramiko.AUTH_FAILED)
+
+ def test_too_many_arguments_return_failure(self):
+ @ssh_handler
+ def onearg(arg, key):
+ return 'er?'
+
+ self.assertEqual(
+ self._server.check_auth_publickey('onearg+1+2', self._key),
+ paramiko.AUTH_FAILED)
+
+ def test_typeerror_is_propagated_properly(self):
+ @ssh_handler
+ def onearg(key):
+ raise TypeError
+
+ self.assertRaises(TypeError,
+ self._server.check_auth_publickey, 'onearg', self._key)
+
+ def test_result_caching_works(self):
+ class Cache(object):
+ def __init__(self):
+ self.first = True
+
+ def __call__(self, key):
+ if self.first:
+ self.first = False
+ return 'yay'
+ else:
+ return None
+
+ cache = Cache()
+ @ssh_handler
+ def cached(key):
+ return cache(key)
+
+ if (self._server.check_auth_publickey('cached', self._key)
+ != paramiko.AUTH_SUCCESSFUL):
+ raise OkupyError('Test prerequisite failed')
+ self.assertEqual(
+ self._server.check_auth_publickey('cached', self._key),
+ paramiko.AUTH_SUCCESSFUL)
+
+ def test_message_is_printed_to_exec_request(self):
+ @ssh_handler
+ def noarg(key):
+ return 'test-message'
+
+ if (self._server.check_auth_publickey('noarg', self._key)
+ != paramiko.AUTH_SUCCESSFUL):
+ raise OkupyError('Test prerequisite failed')
+
+ s1, s2 = socket.socketpair()
+ self.assertTrue(self._server.check_channel_exec_request(s1, ':'))
+ self.assertEqual(s2.makefile().read().rstrip(), 'test-message')
+
+ def test_message_is_printed_to_shell_request(self):
+ @ssh_handler
+ def noarg(key):
+ return 'test-message'
+
+ if (self._server.check_auth_publickey('noarg', self._key)
+ != paramiko.AUTH_SUCCESSFUL):
+ raise OkupyError('Test prerequisite failed')
+
+ s1, s2 = socket.socketpair()
+ self.assertTrue(self._server.check_channel_shell_request(s1))
+ self.assertEqual(s2.makefile().read().rstrip(), 'test-message')
+
+ def test_cache_is_invalidated_after_channel_request(self):
+ class Cache(object):
+ def __init__(self):
+ self.first = True
+
+ def __call__(self, key):
+ if self.first:
+ self.first = False
+ return 'test-message'
+ else:
+ return None
+
+ cache = Cache()
+ @ssh_handler
+ def cached(key):
+ return cache(key)
+
+ if (self._server.check_auth_publickey('cached', self._key)
+ != paramiko.AUTH_SUCCESSFUL):
+ raise OkupyError('Test prerequisite failed')
+
+ s1, s2 = socket.socketpair()
+ if not self._server.check_channel_shell_request(s1):
+ raise OkupyError('Test prerequisite failed')
+ if s2.makefile().read().rstrip() != 'test-message':
+ raise OkupyError('Test prerequisite failed')
+
+ self.assertEqual(
+ self._server.check_auth_publickey('cached', self._key),
+ paramiko.AUTH_FAILED)
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='
diff --git a/okupy/wsgi.py b/okupy/wsgi.py
index 306104d..e2c657e 100644
--- a/okupy/wsgi.py
+++ b/okupy/wsgi.py
@@ -41,9 +41,21 @@ except ImportError:
# we're probably running from django's built-in server
pass
else:
- from uwsgidecorators import timer
+ from uwsgidecorators import postfork, thread, timer
from django.utils import autoreload
+ # autodiscover SSH handlers
+ import okupy.accounts.ssh
+ from okupy.common.ssh import ssh_main
+
+ import Crypto.Random
+
+ postfork(thread(ssh_main))
+
+ @postfork
+ def reset_rng():
+ Crypto.Random.atfork()
+
@timer(5)
def change_code_gracefull_reload(sig):
if autoreload.code_changed():
diff --git a/requirements.txt b/requirements.txt
index 11c4030..710459a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,6 +6,7 @@ django-otp>=0.1.7
git+https://github.com/tampakrap/django-ldapdb@okupy#egg=django-ldapdb
mock>=1.0.1
hg+https://bitbucket.org/psagers/mockldap#egg=mockldap
+paramiko>=1.10.1
passlib>=1.6.1
pycrypto>=2.6
pyopenssl>=0.13
diff --git a/setup.py b/setup.py
index 6a9bd3e..e9838e5 100755
--- a/setup.py
+++ b/setup.py
@@ -38,6 +38,7 @@ setup(
'django-compressor>=1.3',
'django-ldapdb',
'django-otp>=0.1.7',
+ 'paramiko>=1.10.1',
'passlib>=1.6.1',
'pycrypto>=2.6',
'pyopenssl>=0.13',