[ARVADOS] created: 4fc613797f88dbb33c234ba7cd13965b1236bfee
git at public.curoverse.com
git at public.curoverse.com
Fri Aug 7 22:50:29 EDT 2015
at 4fc613797f88dbb33c234ba7cd13965b1236bfee (commit)
commit 4fc613797f88dbb33c234ba7cd13965b1236bfee
Author: Tom Clegg <tom at curoverse.com>
Date: Fri Aug 7 22:50:24 2015 -0400
6934: Add arvados_pam package.
diff --git a/sdk/pam/.gitignore b/sdk/pam/.gitignore
new file mode 120000
index 0000000..1399fd4
--- /dev/null
+++ b/sdk/pam/.gitignore
@@ -0,0 +1 @@
+../python/.gitignore
\ No newline at end of file
diff --git a/sdk/pam/MANIFEST.in b/sdk/pam/MANIFEST.in
new file mode 100644
index 0000000..9561fb1
--- /dev/null
+++ b/sdk/pam/MANIFEST.in
@@ -0,0 +1 @@
+include README.rst
diff --git a/sdk/pam/README.rst b/sdk/pam/README.rst
new file mode 100644
index 0000000..fdf1f8e
--- /dev/null
+++ b/sdk/pam/README.rst
@@ -0,0 +1,21 @@
+==================
+Arvados PAM Module
+==================
+
+Overview
+--------
+
+Accept Arvados API tokens to authenticate to shell accounts.
+
+.. _Arvados: https://arvados.org
+
+Installation
+------------
+
+See http://doc.arvados.org
+
+Testing and Development
+-----------------------
+
+https://arvados.org/projects/arvados/wiki/Hacking
+describes how to set up a development environment and run tests.
diff --git a/sdk/pam/arvados_pam.py b/sdk/pam/arvados_pam.py
deleted file mode 100644
index b38e54f..0000000
--- a/sdk/pam/arvados_pam.py
+++ /dev/null
@@ -1,100 +0,0 @@
-import syslog
-import sys
-sys.argv=['']
-import arvados
-import os
-
-def auth_log(msg):
- """Send errors to default auth log"""
- syslog.openlog(facility=syslog.LOG_AUTH)
- #syslog.openlog()
- syslog.syslog("libpam python Logged: " + msg)
- syslog.closelog()
-
-
-def check_arvados_token(requested_username, token):
- auth_log("%s %s" % (requested_username, token))
-
- try:
- f=file('/etc/default/arvados_pam')
- config=dict([l.split('=') for l in f.readlines() if not l.startswith('#') or l.strip()==""])
- arvados_api_host=config['ARVADOS_API_HOST'].strip()
- hostname=config['HOSTNAME'].strip()
- except Exception as e:
- auth_log("problem getting default values %s" % e)
- return False
-
- try:
- arv = arvados.api('v1',host=arvados_api_host, token=token, cache=None)
- except Exception as e:
- auth_log(str(e))
- return False
-
- try:
- matches = arv.virtual_machines().list(filters=[['hostname','=',hostname]]).execute()['items']
- except Exception as e:
- auth_log(str(e))
- return False
-
-
- if len(matches) != 1:
- auth_log("libpam_arvados could not determine vm uuid for '%s'" % hostname)
- return False
-
- this_vm_uuid = matches[0]['uuid']
- auth_log("this_vm_uuid: %s" % this_vm_uuid)
- client_user_uuid = arv.users().current().execute()['uuid']
-
- filters = [
- ['link_class','=','permission'],
- ['name','=','can_login'],
- ['head_uuid','=',this_vm_uuid],
- ['tail_uuid','=',client_user_uuid]]
-
- for l in arv.links().list(filters=filters).execute()['items']:
- if requested_username == l['properties']['username']:
- return True
- return False
-
-
-def pam_sm_authenticate(pamh, flags, argv):
- try:
- user = pamh.get_user()
- except pamh.exception, e:
- return e.pam_result
-
- if not user:
- return pamh.PAM_USER_UNKNOWN
-
- try:
- resp = pamh.conversation(pamh.Message(pamh.PAM_PROMPT_ECHO_OFF, ''))
- except pamh.exception, e:
- return e.pam_result
-
- try:
- check = check_arvados_token(user, resp.resp)
- except Exception as e:
- auth_log(str(e))
- return False
-
- if not check:
- auth_log("Auth failed Remote Host: %s (%s:%s)" % (pamh.rhost, user, resp.resp))
- return pamh.PAM_AUTH_ERR
-
- auth_log("Success! Remote Host: %s (%s:%s)" % (pamh.rhost, user, resp.resp))
- return pamh.PAM_SUCCESS
-
-def pam_sm_setcred(pamh, flags, argv):
- return pamh.PAM_SUCCESS
-
-def pam_sm_acct_mgmt(pamh, flags, argv):
- return pamh.PAM_SUCCESS
-
-def pam_sm_open_session(pamh, flags, argv):
- return pamh.PAM_SUCCESS
-
-def pam_sm_close_session(pamh, flags, argv):
- return pamh.PAM_SUCCESS
-
-def pam_sm_chauthtok(pamh, flags, argv):
- return pamh.PAM_SUCCESS
diff --git a/sdk/pam/arvados_pam/__init__.py b/sdk/pam/arvados_pam/__init__.py
new file mode 100644
index 0000000..4db6e58
--- /dev/null
+++ b/sdk/pam/arvados_pam/__init__.py
@@ -0,0 +1,134 @@
+import sys
+sys.argv=['']
+
+import arvados
+import os
+import syslog
+
+def auth_log(msg):
+ """Send errors to default auth log"""
+ syslog.openlog(facility=syslog.LOG_AUTH)
+ syslog.syslog('arvados_pam: ' + msg)
+ syslog.closelog()
+
+def config_file():
+ return file('/etc/default/arvados_pam')
+
+def config():
+ txt = config_file().read()
+ c = dict()
+ for x in txt.splitlines(False):
+ if not x.strip().startswith('#'):
+ kv = x.split('=', 2)
+ c[kv[0].strip()] = kv[1].strip()
+ return c
+
+class AuthEvent(object):
+ def __init__(self, client_host, api_host, shell_host, username, token):
+ self.client_host = client_host
+ self.api_host = api_host
+ self.shell_hostname = shell_host
+ self.username = username
+ self.token = token
+ self.vm = None
+ self.user = None
+
+ def can_login(self):
+ ok = False
+ try:
+ self.arv = arvados.api('v1', host=self.api_host, token=self.token, cache=None)
+ self._lookup_vm()
+ if self._check_login_permission():
+ self.result = 'Authenticated'
+ ok = True
+ else:
+ self.result = 'Denied'
+ except Exception as e:
+ self.result = 'Error: ' + repr(e)
+ auth_log(self.message())
+ return ok
+
+ def _lookup_vm(self):
+ """Load the VM record for this host into self.vm. Raise if not possible."""
+
+ vms = self.arv.virtual_machines().list(filters=[['hostname','=',self.shell_hostname]]).execute()
+ if vms['items_available'] > 1:
+ raise Exception("ambiguous VM hostname matched %d records" % vms['items_available'])
+ if vms['items_available'] == 0:
+ raise Exception("VM hostname not found")
+ self.vm = vms['items'][0]
+ if self.vm['hostname'] != self.shell_hostname:
+ raise Exception("API returned record with wrong hostname")
+
+ def _check_login_permission(self):
+ """Check permission to log in. Return True if permission is granted."""
+ self._lookup_vm()
+ self.user = self.arv.users().current().execute()
+ filters = [
+ ['link_class','=','permission'],
+ ['name','=','can_login'],
+ ['head_uuid','=',self.vm['uuid']],
+ ['tail_uuid','=',self.user['uuid']]]
+ for l in self.arv.links().list(filters=filters, limit=10000).execute()['items']:
+ if (l['properties']['username'] == self.username and
+ l['tail_uuid'] == self.user['uuid'] and
+ l['head_uuid'] == self.vm['uuid'] and
+ l['link_class'] == 'permission' and
+ l['name'] == 'can_login'):
+ return True
+ return False
+
+ def message(self):
+ if len(self.token) > 40:
+ log_token = self.token[0:15]
+ else:
+ log_token = '<invalid>'
+ log_label = [self.client_host, self.api_host, self.shell_hostname, self.username, log_token]
+ if self.vm:
+ log_label += [self.vm.get('uuid')]
+ if self.user:
+ log_label += [self.user.get('uuid'), self.user.get('full_name')]
+ return str(log_label) + ': ' + self.result
+
+
+def pam_sm_authenticate(pamh, flags, argv):
+ try:
+ user = pamh.get_user()
+ except pamh.exception as e:
+ return e.pam_result
+
+ if not user:
+ return pamh.PAM_USER_UNKNOWN
+
+ try:
+ resp = pamh.conversation(pamh.Message(pamh.PAM_PROMPT_ECHO_OFF, ''))
+ except pamh.exception as e:
+ return e.pam_result
+
+ try:
+ config = config()
+ api_host = config['ARVADOS_API_HOST'].strip()
+ shell_host = config['HOSTNAME'].strip()
+ except Exception as e:
+ auth_log("loading config: " + repr(e))
+ return False
+
+ if AuthEvent(pamh.rhost, api_host, shell_host, user, resp.resp).can_login():
+ return pamh.PAM_SUCCESS
+ else:
+ return pamh.PAM_AUTH_ERR
+
+def pam_sm_setcred(pamh, flags, argv):
+ return pamh.PAM_SUCCESS
+
+def pam_sm_acct_mgmt(pamh, flags, argv):
+ return pamh.PAM_SUCCESS
+
+def pam_sm_open_session(pamh, flags, argv):
+ return pamh.PAM_SUCCESS
+
+def pam_sm_close_session(pamh, flags, argv):
+ return pamh.PAM_SUCCESS
+
+def pam_sm_chauthtok(pamh, flags, argv):
+ return pamh.PAM_SUCCESS
diff --git a/sdk/pam/gittaggers.py b/sdk/pam/gittaggers.py
new file mode 120000
index 0000000..d59c02c
--- /dev/null
+++ b/sdk/pam/gittaggers.py
@@ -0,0 +1 @@
+../python/gittaggers.py
\ No newline at end of file
diff --git a/sdk/pam/setup.py b/sdk/pam/setup.py
new file mode 100644
index 0000000..ed47388
--- /dev/null
+++ b/sdk/pam/setup.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+
+import os
+import sys
+import setuptools.command.egg_info as egg_info_cmd
+
+from setuptools import setup, find_packages
+
+SETUP_DIR = os.path.dirname(__file__) or '.'
+README = os.path.join(SETUP_DIR, 'README.rst')
+
+try:
+ import gittaggers
+ tagger = gittaggers.EggInfoFromGit
+except ImportError:
+ tagger = egg_info_cmd.egg_info
+
+setup(name='arvados-pam',
+ version='0.1',
+ description='Arvados PAM module',
+ long_description=open(README).read(),
+ author='Arvados',
+ author_email='info at arvados.org',
+ url='https://arvados.org',
+ download_url='https://github.com/curoverse/arvados.git',
+ license='Apache 2.0',
+ packages=[
+ 'arvados_pam',
+ ],
+ scripts=[
+ ],
+ install_requires=[
+ 'arvados-python-client>=0.1.20150801000000',
+ ],
+ test_suite='tests',
+ tests_require=['mock>=1.0', 'PyYAML'],
+ zip_safe=False,
+ cmdclass={'egg_info': tagger},
+ )
diff --git a/sdk/pam/tests/__init__.py b/sdk/pam/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sdk/pam/tests/test_pam.py b/sdk/pam/tests/test_pam.py
new file mode 100644
index 0000000..f59a4fd
--- /dev/null
+++ b/sdk/pam/tests/test_pam.py
@@ -0,0 +1,164 @@
+#!/usr/bin/env python
+
+import arvados
+import arvados_pam
+import mock
+import StringIO
+import unittest
+
+class ConfigTest(unittest.TestCase):
+ def test_ok_config(self):
+ self.assertConfig(
+ "#comment\nARVADOS_API_HOST=xyzzy.example\nHOSTNAME=foo.shell\n#HOSTNAME=bogus\n",
+ 'xyzzy.example',
+ 'foo.shell')
+
+ def test_config_missing_apihost(self):
+ with self.assertRaises(KeyError):
+ self.assertConfig('HOSTNAME=foo', '', 'foo')
+
+ def test_config_missing_shellhost(self):
+ with self.assertRaises(KeyError):
+ self.assertConfig('ARVADOS_API_HOST=foo', 'foo', '')
+
+ def test_config_empty_shellhost(self):
+ self.assertConfig("ARVADOS_API_HOST=foo\nHOSTNAME=\n", 'foo', '')
+
+ def test_config_strip_whitespace(self):
+ self.assertConfig(" ARVADOS_API_HOST = foo \n\tHOSTNAME\t=\tbar\t\n", 'foo', 'bar')
+
+ @mock.patch('arvados_pam.config_file')
+ def assertConfig(self, txt, apihost, shellhost, config_file):
+ configfake = StringIO.StringIO(txt)
+ config_file.side_effect = [configfake]
+ c = arvados_pam.config()
+ self.assertEqual(apihost, c['ARVADOS_API_HOST'])
+ self.assertEqual(shellhost, c['HOSTNAME'])
+
+class AuthTest(unittest.TestCase):
+
+ default_request = {
+ 'api_host': 'zzzzz.api_host.example',
+ 'shell_host': 'testvm2.shell',
+ 'token': '3kg6k6lzmp9kj5cpkcoxie963cmvjahbt2fod9zru30k1jqdmi',
+ 'username': 'active',
+ }
+
+ default_response = {
+ 'links': lambda: {
+ 'items': [{
+ 'uuid': 'zzzzz-o0j2j-rah2ya1ohx9xaev',
+ 'tail_uuid': 'zzzzz-tpzed-xurymjxw79nv3jz',
+ 'head_uuid': 'zzzzz-2x53u-382brsig8rp3065',
+ 'link_class': 'permission',
+ 'name': 'can_login',
+ 'properties': {
+ 'username': 'active',
+ },
+ }],
+ },
+ 'users': lambda: {
+ 'uuid': 'zzzzz-tpzed-xurymjxw79nv3jz',
+ 'full_name': 'Active User',
+ },
+ 'virtual_machines': lambda: {
+ 'items': [{
+ 'uuid': 'zzzzz-2x53u-382brsig8rp3065',
+ 'hostname': 'testvm2.shell',
+ }],
+ 'items_available': 1,
+ },
+ }
+
+ def attempt(self):
+ return arvados_pam.AuthEvent('::1', **self.request).can_login()
+
+ def test_success(self):
+ self.assertTrue(self.attempt())
+ self.api_client.virtual_machines().list.assert_called_with(
+ filters=[['hostname','=',self.request['shell_host']]])
+ self.api.assert_called_with(
+ 'v1', host=self.request['api_host'], token=self.request['token'], cache=None)
+
+ def test_fail_vm_lookup(self):
+ self.response['virtual_machines'] = self._raise
+ self.assertFalse(self.attempt())
+
+ def test_vm_hostname_not_found(self):
+ self.response['virtual_machines'] = lambda: {
+ 'items': [],
+ 'items_available': 0,
+ }
+ self.assertFalse(self.attempt())
+
+ def test_vm_hostname_ambiguous(self):
+ self.response['virtual_machines'] = lambda: {
+ 'items': [
+ {
+ 'uuid': 'zzzzz-2x53u-382brsig8rp3065',
+ 'hostname': 'testvm2.shell',
+ },
+ {
+ 'uuid': 'zzzzz-2x53u-382brsig8rp3065',
+ 'hostname': 'testvm2.shell',
+ },
+ ],
+ 'items_available': 2,
+ }
+ self.assertFalse(self.attempt())
+
+ def test_server_ignores_vm_filters(self):
+ self.response['virtual_machines'] = lambda: {
+ 'items': [
+ {
+ 'uuid': 'zzzzz-2x53u-382brsig8rp3065',
+ 'hostname': 'testvm22.shell', # <-----
+ },
+ ],
+ 'items_available': 1,
+ }
+ self.assertFalse(self.attempt())
+
+ def test_fail_user_lookup(self):
+ self.response['users'] = self._raise
+ self.assertFalse(self.attempt())
+
+ def test_fail_permission_check(self):
+ self.response['links'] = self._raise
+ self.assertFalse(self.attempt())
+
+ def test_no_login_permission(self):
+ self.response['links'] = lambda: {
+ 'items': [],
+ }
+ self.assertFalse(self.attempt())
+
+ def test_server_ignores_permission_filters(self):
+ self.response['links'] = lambda: {
+ 'items': [{
+ 'uuid': 'zzzzz-o0j2j-rah2ya1ohx9xaev',
+ 'tail_uuid': 'zzzzz-tpzed-xurymjxw79nv3jz',
+ 'head_uuid': 'zzzzz-2x53u-382brsig8rp3065',
+ 'link_class': 'permission',
+ 'name': 'CANT_login', # <-----
+ 'properties': {
+ 'username': 'active',
+ },
+ }],
+ }
+ self.assertFalse(self.attempt())
+
+ def setUp(self):
+ self.request = self.default_request.copy()
+ self.response = self.default_response.copy()
+ self.api_client = mock.MagicMock(name='api_client')
+ self.api_client.users().current().execute.side_effect = lambda: self.response['users']()
+ self.api_client.virtual_machines().list().execute.side_effect = lambda: self.response['virtual_machines']()
+ self.api_client.links().list().execute.side_effect = lambda: self.response['links']()
+ patcher = mock.patch('arvados.api')
+ self.api = patcher.start()
+ self.addCleanup(patcher.stop)
+ self.api.side_effect = [self.api_client]
+
+ def _raise(self, exception=Exception("Test-induced failure"), *args, **kwargs):
+ raise exception
-----------------------------------------------------------------------
hooks/post-receive
--
More information about the arvados-commits
mailing list