aboutsummaryrefslogtreecommitdiff
path: root/okupy
diff options
context:
space:
mode:
authorMichał Górny <mgorny@gentoo.org>2013-08-22 14:16:55 +0200
committerMichał Górny <mgorny@gentoo.org>2013-08-25 22:45:10 +0200
commitc8c41f930089a519fb2c53a3efb6a37801719a3d (patch)
treeb06cfd41aad245a5c777f6673df28b8b0261a63f /okupy
parentReset RNG in @postfork. (diff)
downloadidentity.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.py26
-rw-r--r--okupy/accounts/views.py12
-rw-r--r--okupy/common/auth.py46
-rw-r--r--okupy/common/ssh.py18
-rw-r--r--okupy/settings/__init__.py1
-rw-r--r--okupy/templates/login.html7
-rw-r--r--okupy/tests/settings.py1
-rw-r--r--okupy/tests/unit/test_auth.py33
-rw-r--r--okupy/tests/vars.py6
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='