[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