From 17feec709c02612c285ebddaf712876b619aa5ed Mon Sep 17 00:00:00 2001
From: Clay Gerrard <clay.gerrard@gmail.com>
Date: Tue, 8 Apr 2014 21:14:13 -0700
Subject: [PATCH] Add some bash helpers for auth stuff

Change-Id: If61ac9a050e7a115f37dbf4e74b904ac5dfd2052
---
 swiftclient/shell.py     |  54 +++++++++++++++++++--
 tests/unit/test_shell.py | 102 +++++++++++++++++++++++++++++++++++++++
 tests/unit/utils.py      |   6 ++-
 3 files changed, 156 insertions(+), 6 deletions(-)

diff --git a/swiftclient/shell.py b/swiftclient/shell.py
index 430efd29..31147f13 100755
--- a/swiftclient/shell.py
+++ b/swiftclient/shell.py
@@ -31,14 +31,19 @@ from swiftclient.utils import config_true_value, generate_temp_url, prt_bytes
 from swiftclient.multithreading import OutputManager
 from swiftclient.exceptions import ClientException
 from swiftclient import __version__ as client_version
-from swiftclient.service import SwiftService, SwiftError, SwiftUploadObject
+from swiftclient.service import SwiftService, SwiftError, \
+    SwiftUploadObject, get_conn
 from swiftclient.command_helpers import print_account_stats, \
     print_container_stats, print_object_stats
 
+try:
+    from shlex import quote as sh_quote
+except ImportError:
+    from pipes import quote as sh_quote
 
 BASENAME = 'swift'
-commands = ('delete', 'download', 'list', 'post',
-            'stat', 'upload', 'capabilities', 'info', 'tempurl')
+commands = ('delete', 'download', 'list', 'post', 'stat', 'upload',
+            'capabilities', 'info', 'tempurl', 'auth')
 
 
 def immediate_exit(signum, frame):
@@ -905,6 +910,46 @@ def st_capabilities(parser, args, output_manager):
 
 st_info = st_capabilities
 
+st_auth_help = '''
+Display auth related authentication variables in shell friendly format.
+
+  Commands to run to export storage url and auth token into
+  OS_STORAGE_URL and OS_AUTH_TOKEN:
+
+      swift auth
+
+  Commands to append to a runcom file (e.g. ~/.bashrc, /etc/profile) for
+  automatic authentication:
+
+      swift auth -v -U test:tester -K testing \
+          -A http://localhost:8080/auth/v1.0
+
+'''.strip('\n')
+
+
+def st_auth(parser, args, thread_manager):
+    (options, args) = parse_args(parser, args)
+    _opts = vars(options)
+    if options.verbose > 1:
+        if options.auth_version in ('1', '1.0'):
+            print('export ST_AUTH=%s' % sh_quote(options.auth))
+            print('export ST_USER=%s' % sh_quote(options.user))
+            print('export ST_KEY=%s' % sh_quote(options.key))
+        else:
+            print('export OS_IDENTITY_API_VERSION=%s' % sh_quote(
+                options.auth_version))
+            print('export OS_AUTH_VERSION=%s' % sh_quote(options.auth_version))
+            print('export OS_AUTH_URL=%s' % sh_quote(options.auth))
+            for k, v in sorted(_opts.items()):
+                if v and k.startswith('os_') and \
+                        k not in ('os_auth_url', 'os_options'):
+                    print('export %s=%s' % (k.upper(), sh_quote(v)))
+    else:
+        conn = get_conn(_opts)
+        url, token = conn.get_auth()
+        print('export OS_STORAGE_URL=%s' % sh_quote(url))
+        print('export OS_AUTH_TOKEN=%s' % sh_quote(token))
+
 
 st_tempurl_options = '<method> <seconds> <path> <key>'
 
@@ -1073,7 +1118,8 @@ Positional arguments:
                          or object.
     upload               Uploads files or directories to the given container.
     capabilities         List cluster capabilities.
-    tempurl              Create a temporary URL
+    tempurl              Create a temporary URL.
+    auth                 Display auth related environment variables.
 
 Examples:
   %%prog download --help
diff --git a/tests/unit/test_shell.py b/tests/unit/test_shell.py
index 98cef855..56c96f86 100644
--- a/tests/unit/test_shell.py
+++ b/tests/unit/test_shell.py
@@ -19,8 +19,10 @@ import mock
 import os
 import tempfile
 import unittest
+import textwrap
 from testtools import ExpectedException
 
+
 import six
 
 import swiftclient
@@ -46,6 +48,11 @@ mocked_os_environ = {
     'ST_USER': 'test:tester',
     'ST_KEY': 'testing'
 }
+clean_os_environ = {}
+environ_prefixes = ('ST_', 'OS_')
+for key in os.environ:
+    if any(key.startswith(m) for m in environ_prefixes):
+        clean_os_environ[key] = ''
 
 clean_os_environ = {}
 environ_prefixes = ('ST_', 'OS_')
@@ -1529,6 +1536,101 @@ class TestAuth(MockHttpTest):
             }),
         ])
 
+    def test_auth(self):
+        headers = {
+            'x-auth-token': 'AUTH_tk5b6b12',
+            'x-storage-url': 'https://swift.storage.example.com/v1/AUTH_test',
+        }
+        mock_resp = self.fake_http_connection(200, headers=headers)
+        with mock.patch('swiftclient.client.http_connection', new=mock_resp):
+            stdout = six.StringIO()
+            with mock.patch('sys.stdout', new=stdout):
+                argv = [
+                    '',
+                    'auth',
+                    '--auth', 'https://swift.storage.example.com/auth/v1.0',
+                    '--user', 'test:tester', '--key', 'testing',
+                ]
+                swiftclient.shell.main(argv)
+
+        expected = """
+        export OS_STORAGE_URL=https://swift.storage.example.com/v1/AUTH_test
+        export OS_AUTH_TOKEN=AUTH_tk5b6b12
+        """
+        self.assertEquals(textwrap.dedent(expected).lstrip(),
+                          stdout.getvalue())
+
+    def test_auth_verbose(self):
+        with mock.patch('swiftclient.client.http_connection') as mock_conn:
+            stdout = six.StringIO()
+            with mock.patch('sys.stdout', new=stdout):
+                argv = [
+                    '',
+                    'auth',
+                    '--auth', 'https://swift.storage.example.com/auth/v1.0',
+                    '--user', 'test:tester', '--key', 'te$tin&',
+                    '--verbose',
+                ]
+                swiftclient.shell.main(argv)
+
+        expected = """
+        export ST_AUTH=https://swift.storage.example.com/auth/v1.0
+        export ST_USER=test:tester
+        export ST_KEY='te$tin&'
+        """
+        self.assertEquals(textwrap.dedent(expected).lstrip(),
+                          stdout.getvalue())
+        self.assertEqual([], mock_conn.mock_calls)
+
+    def test_auth_v2(self):
+        os_options = {'tenant_name': 'demo'}
+        with mock.patch('swiftclient.client.get_auth_keystone',
+                        new=fake_get_auth_keystone(os_options)):
+            stdout = six.StringIO()
+            with mock.patch('sys.stdout', new=stdout):
+                argv = [
+                    '',
+                    'auth', '-V2',
+                    '--auth', 'https://keystone.example.com/v2.0/',
+                    '--os-tenant-name', 'demo',
+                    '--os-username', 'demo', '--os-password', 'admin',
+                ]
+                swiftclient.shell.main(argv)
+
+        expected = """
+        export OS_STORAGE_URL=http://url/
+        export OS_AUTH_TOKEN=token
+        """
+        self.assertEquals(textwrap.dedent(expected).lstrip(),
+                          stdout.getvalue())
+
+    def test_auth_verbose_v2(self):
+        with mock.patch('swiftclient.client.get_auth_keystone') \
+                as mock_keystone:
+            stdout = six.StringIO()
+            with mock.patch('sys.stdout', new=stdout):
+                argv = [
+                    '',
+                    'auth', '-V2',
+                    '--auth', 'https://keystone.example.com/v2.0/',
+                    '--os-tenant-name', 'demo',
+                    '--os-username', 'demo', '--os-password', '$eKr3t',
+                    '--verbose',
+                ]
+                swiftclient.shell.main(argv)
+
+        expected = """
+        export OS_IDENTITY_API_VERSION=2.0
+        export OS_AUTH_VERSION=2.0
+        export OS_AUTH_URL=https://keystone.example.com/v2.0/
+        export OS_PASSWORD='$eKr3t'
+        export OS_TENANT_NAME=demo
+        export OS_USERNAME=demo
+        """
+        self.assertEquals(textwrap.dedent(expected).lstrip(),
+                          stdout.getvalue())
+        self.assertEqual([], mock_keystone.mock_calls)
+
 
 class TestCrossAccountObjectAccess(TestBase, MockHttpTest):
     """
diff --git a/tests/unit/utils.py b/tests/unit/utils.py
index 955296ef..0a45437f 100644
--- a/tests/unit/utils.py
+++ b/tests/unit/utils.py
@@ -38,8 +38,10 @@ def fake_get_auth_keystone(expected_os_options=None, exc=None,
         if exc:
             raise exc('test')
         # TODO: some way to require auth_url, user and key?
-        if expected_os_options and actual_os_options != expected_os_options:
-            return "", None
+        if expected_os_options:
+            for key, value in actual_os_options.items():
+                if value and value != expected_os_options.get(key):
+                    return "", None
         if 'required_kwargs' in kwargs:
             for k, v in kwargs['required_kwargs'].items():
                 if v != actual_kwargs.get(k):