← Back to team overview

cloud-init-dev team mailing list archive

[Merge] ~amzn-cmiller/cloud-init:ssh_authorizedkeys into cloud-init:master

 

Chad has proposed merging ~amzn-cmiller/cloud-init:ssh_authorizedkeys into cloud-init:master.

Commit message:
Mimic list-behavior of sshd AuthorizedKeyFile setting

Quoth sshd_config(5), "Multiple files may be listed, separated by whitespace". 

Requested reviews:
  cloud-init commiters (cloud-init-dev)

For more details, see:
https://code.launchpad.net/~amzn-cmiller/cloud-init/+git/cloud-init/+merge/365057

Use the first location specified as where to write provided keys.
-- 
Your team cloud-init commiters is requested to review the proposed merge of ~amzn-cmiller/cloud-init:ssh_authorizedkeys into cloud-init:master.
diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py
index 3f99b58..d03b3ee 100644
--- a/cloudinit/ssh_util.py
+++ b/cloudinit/ssh_util.py
@@ -9,6 +9,8 @@
 import os
 import pwd
 
+from shlex import split as shell_split
+
 from cloudinit import log as logging
 from cloudinit import util
 
@@ -223,6 +225,11 @@ def extract_authorized_keys(username):
             # authenticated and %u is replaced by the username of that user.
             ssh_cfg = parse_ssh_config_map(DEF_SSHD_CFG)
             auth_key_fn = ssh_cfg.get("authorizedkeysfile", '').strip()
+            # AuthorizedKeysFile can have multiple entries separated with
+            # whitespace, this will set auth_key_fn to the first entry, we
+            # will manage keys in the first file listed, but not break
+            # configuration for more than one file.
+            auth_key_fn = shell_split(auth_key_fn)[0]
             if not auth_key_fn:
                 auth_key_fn = "%h/.ssh/authorized_keys"
             auth_key_fn = auth_key_fn.replace("%h", pw_ent.pw_dir)
diff --git a/tests/unittests/test_sshutil.py b/tests/unittests/test_sshutil.py
index 73ae897..daf160c 100644
--- a/tests/unittests/test_sshutil.py
+++ b/tests/unittests/test_sshutil.py
@@ -1,11 +1,15 @@
 # This file is part of cloud-init. See LICENSE file for license information.
 
-from mock import patch
+from cloudinit.tests.helpers import mock
+
+from collections import namedtuple
 
 from cloudinit import ssh_util
 from cloudinit.tests import helpers as test_helpers
 from cloudinit import util
 
+from mock import patch
+
 
 VALID_CONTENT = {
     'dsa': (
@@ -326,4 +330,51 @@ class TestUpdateSshConfig(test_helpers.CiTestCase):
         m_write_file.assert_not_called()
 
 
+def _dummy_users_ssh_info(username):
+    # Faking a passwd entry for mocking
+    kwargs = {"pw_name": username, "pw_passwd": "***",
+              "pw_uid": 101, "pw_gid": 101, "pw_gecos": username,
+              "pw_dir": "/home/{}".format(username), "pw_shell": "/bin/bash"}
+    Passwd = namedtuple("Passwd", kwargs.keys())
+    return '/home/{}/.ssh'.format(username), Passwd(**kwargs)
+
+
+class TestExtractAuthorizedKeys(test_helpers.TestCase):
+
+    @mock.patch("cloudinit.ssh_util.users_ssh_info")
+    @mock.patch("cloudinit.ssh_util.parse_ssh_config_map")
+    def test_one_entry(self, m_parse_ssh_config_map, m_users_ssh_info):
+        one_entry = '%h/.ssh/authorized_keys'
+        m_users_ssh_info.side_effect = _dummy_users_ssh_info
+        m_parse_ssh_config_map.return_value = {
+            "authorizedkeysfile": one_entry
+        }
+
+        ret = ssh_util.extract_authorized_keys('testuser')
+        self.assertEqual('/home/testuser/.ssh/authorized_keys', ret[0])
+
+    @mock.patch("cloudinit.ssh_util.users_ssh_info")
+    @mock.patch("cloudinit.ssh_util.parse_ssh_config_map")
+    def test_two_entries(self, m_parse_ssh_config_map, m_users_ssh_info):
+        both_entries = '%h/.ssh/authorized_keys /etc/ssh/authorized_keys/%u'
+        m_users_ssh_info.side_effect = _dummy_users_ssh_info
+        m_parse_ssh_config_map.return_value = {
+            "authorizedkeysfile": both_entries
+        }
+
+        ret = ssh_util.extract_authorized_keys('testuser')
+        self.assertEqual('/home/testuser/.ssh/authorized_keys', ret[0])
+
+    @mock.patch("cloudinit.ssh_util.users_ssh_info")
+    @mock.patch("cloudinit.ssh_util.parse_ssh_config_map")
+    def test_escaped_space(self, m_parse_ssh_config_map, m_users_ssh_info):
+        both_entries = '/with\\ spaces/ak second third'
+        m_users_ssh_info.side_effect = _dummy_users_ssh_info
+        m_parse_ssh_config_map.return_value = {
+            "authorizedkeysfile": both_entries
+        }
+
+        ret = ssh_util.extract_authorized_keys('testuser')
+        self.assertEqual('/with spaces/ak', ret[0])
+
 # vi: ts=4 expandtab