Repository: ansible/tower-cli
Pull request #481 (2018-02-28 14:33:52)

remove support for old-style Tower Auth Tokens in 3.3
related: https://github.com/ansible/tower-cli/issues/478

coveralls: 
[![Coverage Status](https://coveralls.io/builds/15739321/badge)](https://coveralls.io/builds/15739321)

Coverage increased (+0.03%) to 89.916% when pulling **df630b480a1f48fdd8d3795ea2a695fd8a8ca875 on ryanpetrello:remove-use-token-support** into **614494458f36701f389fb77d7728c902c49761af on ansible:master**.

ryanpetrello: @rooftopcellist yep, this PR is just a precursor to actual OAuth2.0 support.
AlanCoding: wait, tests!!
ryanpetrello: https://travis-ci.org/ansible/tower-cli/jobs/347369674

O_o this definitely passes for me locally...

Patch:
diff --git a/docs/source/api_ref/conf.rst b/docs/source/api_ref/conf.rst
index 4bb900f..1bd5adb 100644
--- a/docs/source/api_ref/conf.rst
+++ b/docs/source/api_ref/conf.rst
@@ -29,7 +29,7 @@ available Tower CLI settings:
 +------------------+-------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+
 |``certificate``   | String/''                                             | Path to a custom certificate file that will be used throughout the command. Ignored if `--insecure` flag if set in command or `verify_ssl` is set to false |
 +------------------+-------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+
-|``use_token``     | Boolean/'false'                                       | Whether to use token-based authentication.                                                                                                                 |
+|``use_token``     | Boolean/'false'                                       | Whether to use token-based authentication.  No longer supported in Tower 3.3 and above                                                                     |
 +------------------+-------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------+
 
 **Note:** Some settings are marked as 'CLI use only', this means although users are free to set values to those
diff --git a/docs/source/cli_ref/index.rst b/docs/source/cli_ref/index.rst
index ab0f667..19797c6 100644
--- a/docs/source/cli_ref/index.rst
+++ b/docs/source/cli_ref/index.rst
@@ -166,8 +166,6 @@ and ``tower-cli {resource} {action} --help`` shows details of the usage of this
                          False]
 
     Global Options:
-      --use-token                     Turn on Tower's token-based authentication.
-                                      Set config use_token to make this permanent.
       --certificate TEXT              Path to a custom certificate file that will
                                       be used throughout the command. Overwritten
                                       by --insecure flag if set.
@@ -192,6 +190,8 @@ and ``tower-cli {resource} {action} --help`` shows details of the usage of this
                                       "http://" is explicitly provided. This will
                                       take precedence over a host provided to
                                       `tower config`, if any.
+      --use-token                     Turn on Tower's token-based authentication.
+                                      No longer supported in Tower 3.3 and above
 
     Other Options:
       --help  Show this message and exit.
diff --git a/tower_cli/api.py b/tower_cli/api.py
index dfe4eab..28292ca 100644
--- a/tower_cli/api.py
+++ b/tower_cli/api.py
@@ -25,7 +25,7 @@ from requests.exceptions import ConnectionError, SSLError
 from requests.sessions import Session
 from requests.models import Response
 from requests.packages import urllib3
-from requests.auth import AuthBase
+from requests.auth import AuthBase, HTTPBasicAuth
 
 from tower_cli import exceptions as exc
 from tower_cli.conf import settings
@@ -36,12 +36,13 @@ from tower_cli.constants import CUR_API_VERSION
 TOWER_DATETIME_FMT = r'%Y-%m-%dT%H:%M:%S.%fZ'
 
 
-class TowerTokenAuth(AuthBase):
+class BasicTowerAuth(AuthBase):
 
     def __init__(self, username, password, cli_client):
         self.username = username
         self.password = password
         self.cli_client = cli_client
+        self.use_legacy_token = settings.use_token
 
     def _acquire_token(self):
         return self.cli_client._make_request(
@@ -69,7 +70,20 @@ class TowerTokenAuth(AuthBase):
             return 'Token ' + token_json['token']
 
     def __call__(self, r):
-        r.headers['Authorization'] = self._get_auth_token()
+        if self.use_legacy_token:
+            resp = self.cli_client._make_request(
+                'OPTIONS', self.cli_client.prefix + 'authtoken/', [], {}
+            )
+            if resp.ok:
+                r.headers['Authorization'] = self._get_auth_token()
+            else:
+                warnings.warn(
+                    'use_token is not supported in this version of Tower '
+                    '(the Auth Token API has been replaced with OAuth2.0 support)',
+                )
+                HTTPBasicAuth(self.username, self.password)(r)
+        else:
+            HTTPBasicAuth(self.username, self.password)(r)
         return r
 
 
@@ -161,12 +175,11 @@ class Client(Session):
         # from settings if it's provided.
         kwargs.setdefault(
             'auth',
-            TowerTokenAuth(
+            BasicTowerAuth(
                 settings.username,
                 settings.password,
                 self
-            ) if settings.use_token else (settings.username,
-                                          settings.password)
+            )
         )
 
         # POST and PUT requests will send JSON by default; make this
diff --git a/tower_cli/conf.py b/tower_cli/conf.py
index 3d6ddfc..22e8a5a 100644
--- a/tower_cli/conf.py
+++ b/tower_cli/conf.py
@@ -446,8 +446,8 @@ def with_global_options(method):
     method = click.option(
         '--use-token',
         default=None,
-        help='Turn on Tower\'s token-based authentication. Set config'
-             ' use_token to make this permanent.',
+        help='Turn on Tower\'s token-based authentication. No longer supported '
+             'in Tower 3.3 and above.',
         is_flag=True,
         required=False, callback=_apply_runtime_setting,
         is_eager=True


Test patch:
diff --git a/tests/test_api.py b/tests/test_api.py
index a962041..5d4b8aa 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -19,7 +19,7 @@ from datetime import datetime as dt, timedelta
 import requests
 from requests.sessions import Session
 
-from tower_cli.api import APIResponse, client, TowerTokenAuth,\
+from tower_cli.api import APIResponse, client, BasicTowerAuth,\
         TOWER_DATETIME_FMT
 from tower_cli import exceptions as exc
 from tower_cli.conf import settings
@@ -180,9 +180,9 @@ class ClientTests(unittest.TestCase):
                 with settings.runtime_values(verify_ssl=False):
                     client.get('/ping/')
                     g.assert_called_once_with(
-                        # The pont is to assure verify=False below
+                        # The point is to assure verify=False below
                         'GET', mock.ANY, allow_redirects=True,
-                        auth=(mock.ANY, mock.ANY), verify=False
+                        auth=mock.ANY, verify=False
                     )
 
     def test_http_contradiction_error(self):
@@ -205,14 +205,15 @@ class ClientTests(unittest.TestCase):
                     self.assertTrue(secho.called)
 
 
-class TowerTokenAuthTests(unittest.TestCase):
+class TowerAuthTokenTests(unittest.TestCase):
     def setUp(self):
 
         class Req(object):
             def __init__(self):
                 self.headers = {}
 
-        self.auth = TowerTokenAuth('alice', 'pass', client)
+        with settings.runtime_values(use_token=True):
+            self.auth = BasicTowerAuth('alice', 'pass', client)
         self.req = Req()
         self.expires = dt.utcnow()
 
@@ -221,8 +222,10 @@ class TowerTokenAuthTests(unittest.TestCase):
         expires = self.expires.strftime(TOWER_DATETIME_FMT)
         with mock.patch('six.moves.builtins.open', new_callable=mock.mock_open()):
             with mock.patch('tower_cli.api.json.load', return_value={'token': 'foobar', 'expires': expires}):
-                self.auth(self.req)
-                self.assertEqual(self.req.headers['Authorization'], 'Token foobar')
+                with client.test_mode as t:
+                    t.register('/authtoken/', json.dumps({}), status_code=200, method='OPTIONS')
+                    self.auth(self.req)
+                    self.assertEqual(self.req.headers['Authorization'], 'Token foobar')
 
     def test_reading_invalid_token(self):
         self.expires += timedelta(hours=1)
@@ -230,6 +233,7 @@ class TowerTokenAuthTests(unittest.TestCase):
         with mock.patch('six.moves.builtins.open', new_callable=mock.mock_open()):
             with mock.patch('tower_cli.api.json.load', return_value="invalid"):
                 with client.test_mode as t:
+                    t.register('/authtoken/', json.dumps({}), status_code=200, method='OPTIONS')
                     t.register('/authtoken/', json.dumps({'token': 'barfoo', 'expires': expires}), status_code=200,
                                method='POST')
                     self.auth(self.req)
@@ -241,6 +245,7 @@ class TowerTokenAuthTests(unittest.TestCase):
         with mock.patch('six.moves.builtins.open', new_callable=mock.mock_open()):
             with mock.patch('tower_cli.api.json.load', return_value={'token': 'foobar', 'expires': expires}):
                 with client.test_mode as t:
+                    t.register('/authtoken/', json.dumps({}), status_code=200, method='OPTIONS')
                     t.register('/authtoken/', json.dumps({'token': 'barfoo', 'expires': expires}), status_code=200,
                                method='POST')
                     self.auth(self.req)
@@ -253,5 +258,16 @@ class TowerTokenAuthTests(unittest.TestCase):
             with mock.patch('tower_cli.api.json.load', return_value={'token': 'foobar', 'expires': expires}):
                 with client.test_mode as t:
                     with self.assertRaises(exc.AuthError):
+                        t.register('/authtoken/', json.dumps({}), status_code=200, method='OPTIONS')
                         t.register('/authtoken/', json.dumps({'invalid': 'invalid'}), status_code=200, method='POST')
                         self.auth(self.req)
+
+    def test_auth_token_unsupported(self):
+        # If the user specifies `use_token=True`, but `/authtoken/` doesn't
+        # exist (in Tower 3.3 and beyond), just fall back to basic auth
+        with client.test_mode as t:
+            with settings.runtime_values(use_token=True):
+                t.register('/authtoken/', json.dumps({}), status_code=404, method='OPTIONS')
+                auth = BasicTowerAuth('alice', 'pass', client)
+                auth(self.req)
+                assert self.req.headers == {'Authorization': 'Basic YWxpY2U6cGFzcw=='}