Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Removal of OAuth2 stuff from clients #15647

Merged
merged 2 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion awx_collection/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,28 @@ Installing the `tar.gz` involves no special instructions.
## Running

Non-deprecated modules in this collection have no Python requirements, but
may require the AWX CLI
may require the official [AWX CLI](https://pypi.org/project/awxkit/)
in the future. The `DOCUMENTATION` for each module will report this.

You can specify authentication by host, username, and password.

These can be specified via (from highest to lowest precedence):

- direct module parameters
- environment variables (most useful when running against localhost)
- a config file path specified by the `tower_config_file` parameter
- a config file at `~/.tower_cli.cfg`
- a config file at `/etc/tower/tower_cli.cfg`

Config file syntax looks like this:

```
[general]
host = https://localhost:8043
verify_ssl = true
username = foo
password = bar
```

## Release and Upgrade Notes

Expand All @@ -46,6 +63,7 @@ Notable releases of the `awx.awx` collection:
- 11.0.0 has no non-deprecated modules that depend on the deprecated `tower-cli` [PyPI](https://pypi.org/project/ansible-tower-cli/).
- 19.2.1 large renaming purged "tower" names (like options and module names), adding redirects for old names
- 21.11.0 "tower" modules deprecated and symlinks removed.
- 25.0.0 "token" and "application" modules have been removed as oauth is no longer supported, use basic auth instead
- X.X.X added support of named URLs to all modules. Anywhere that previously accepted name or id can also support named URLs
- 0.0.1-devel is the version you should see if installing from source, which is intended for development and expected to be unstable.

Expand Down
10 changes: 5 additions & 5 deletions awx_collection/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ rootdir: /home/student1/awx, configfile: pytest.ini
plugins: cov-2.10.1, django-3.10.0, pythonpath-0.7.3, mock-1.11.1, timeout-1.4.2, forked-1.3.0, xdist-1.34.0
collected 116 items

awx_collection/test/awx/test_application.py::test_create_application PASSED [ 0%]
awx_collection/test/awx/test_ad_hoc_wait.py::test_ad_hoc_wait_successful PASSED [ 0%]
awx_collection/test/awx/test_completeness.py::test_completeness PASSED [ 1%]

...
Expand All @@ -124,18 +124,18 @@ FAILED awx_collection/test/awx/test_module_utils.py::test_type_warning - SystemE
make: *** [Makefile:382: test_collection] Error 1
```

In addition to running all of the tests, you can also specify specific tests to run. This is useful when developing a single module. In this example, we will run the tests for the `token` module:
In addition to running all of the tests, you can also specify specific tests to run. This is useful when developing a single module. In this example, we will run the tests for the `project` module:

```
$ pytest awx_collection/test/awx/test_token.py
$ pytest awx_collection/test/awx/test_project.py
============================ test session starts ============================
platform darwin -- Python 3.7.0, pytest-3.6.0, py-1.8.1, pluggy-0.6.0
django: settings: awx.settings.development (from ini)
rootdir: /Users/jowestco/junk/awx, inifile: pytest.ini
plugins: xdist-1.27.0, timeout-1.3.4, pythonpath-0.7.3, mock-1.11.1, forked-1.1.3, django-3.9.0, cov-2.8.1
collected 1 item
collected 1 item

awx_collection/test/awx/test_token.py . [100%]
awx_collection/test/awx/test_project.py . [100%]

========================= 1 passed in 1.72 seconds =========================
```
Expand Down
2 changes: 0 additions & 2 deletions awx_collection/meta/runtime.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ action_groups:
- ad_hoc_command
- ad_hoc_command_cancel
- ad_hoc_command_wait
- application
- bulk_job_launch
- bulk_host_create
- bulk_host_delete
Expand Down Expand Up @@ -42,7 +41,6 @@ action_groups:
- settings
- subscriptions
- team
- token
- user
- workflow_approval
- workflow_job_template_node
Expand Down
10 changes: 0 additions & 10 deletions awx_collection/plugins/doc_fragments/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,6 @@ class ModuleDocFragment(object):
- If value not set, will try environment variable C(CONTROLLER_PASSWORD) and then config files
type: str
aliases: [ tower_password ]
controller_oauthtoken:
description:
- The OAuth token to use.
- This value can be in one of two formats.
- A string which is the token itself. (i.e. bqV5txm97wqJqtkxlMkhQz0pKhRMMX)
- A dictionary structure as returned by the token module.
- If value not set, will try environment variable C(CONTROLLER_OAUTH_TOKEN) and then config files
type: raw
version_added: "3.7.0"
aliases: [ tower_oauthtoken ]
validate_certs:
description:
- Whether to allow insecure connections to AWX.
Expand Down
11 changes: 0 additions & 11 deletions awx_collection/plugins/doc_fragments/auth_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,6 @@ class ModuleDocFragment(object):
version: '4.0.0'
why: Collection name change
alternatives: 'CONTROLLER_PASSWORD'
oauth_token:
description:
- The OAuth token to use.
env:
- name: CONTROLLER_OAUTH_TOKEN
- name: TOWER_OAUTH_TOKEN
deprecated:
collection_name: 'awx.awx'
version: '4.0.0'
why: Collection name change
alternatives: 'CONTROLLER_OAUTH_TOKEN'
verify_ssl:
description:
- Specify whether Ansible should verify the SSL certificate of the controller host.
Expand Down
2 changes: 1 addition & 1 deletion awx_collection/plugins/lookup/controller_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
options:
_terms:
description:
- The endpoint to query, i.e. teams, users, tokens, job_templates, etc.
- The endpoint to query, i.e. teams, users, job_templates, etc.
required: True
query_params:
description:
Expand Down
8 changes: 2 additions & 6 deletions awx_collection/plugins/module_utils/awxkit.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,8 @@ def __init__(self, argument_spec, **kwargs):

def authenticate(self):
try:
if self.oauth_token:
self.connection.login(None, None, token=self.oauth_token)
self.authenticated = True
elif self.username:
self.connection.login(username=self.username, password=self.password)
self.authenticated = True
self.connection.login(username=self.username, password=self.password)
self.authenticated = True
except Exception:
self.fail_json("Failed to authenticate")

Expand Down
141 changes: 12 additions & 129 deletions awx_collection/plugins/module_utils/controller_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from ansible.module_utils.urls import Request, SSLValidationError, ConnectionError
from ansible.module_utils.parsing.convert_bool import boolean as strtobool
from ansible.module_utils.six import PY2
from ansible.module_utils.six import raise_from, string_types
from ansible.module_utils.six import raise_from
from ansible.module_utils.six.moves import StringIO
from ansible.module_utils.six.moves.urllib.error import HTTPError
from ansible.module_utils.six.moves.http_cookiejar import CookieJar
Expand Down Expand Up @@ -55,9 +55,6 @@ class ControllerModule(AnsibleModule):
controller_password=dict(no_log=True, aliases=['tower_password'], required=False, fallback=(env_fallback, ['CONTROLLER_PASSWORD', 'TOWER_PASSWORD'])),
validate_certs=dict(type='bool', aliases=['tower_verify_ssl'], required=False, fallback=(env_fallback, ['CONTROLLER_VERIFY_SSL', 'TOWER_VERIFY_SSL'])),
request_timeout=dict(type='float', required=False, fallback=(env_fallback, ['CONTROLLER_REQUEST_TIMEOUT'])),
controller_oauthtoken=dict(
type='raw', no_log=True, aliases=['tower_oauthtoken'], required=False, fallback=(env_fallback, ['CONTROLLER_OAUTH_TOKEN', 'TOWER_OAUTH_TOKEN'])
),
controller_config_file=dict(type='path', aliases=['tower_config_file'], required=False, default=None),
)
# Associations of these types are ordered and have special consideration in the modified associations function
Expand All @@ -68,15 +65,12 @@ class ControllerModule(AnsibleModule):
'password': 'controller_password',
'verify_ssl': 'validate_certs',
'request_timeout': 'request_timeout',
'oauth_token': 'controller_oauthtoken',
}
host = '127.0.0.1'
username = None
password = None
verify_ssl = True
request_timeout = 10
oauth_token = None
oauth_token_id = None
authenticated = False
config_name = 'tower_cli.cfg'
version_checked = False
Expand Down Expand Up @@ -111,20 +105,6 @@ def __init__(self, argument_spec=None, direct_params=None, error_callback=None,
if direct_value is not None:
setattr(self, short_param, direct_value)

# Perform magic depending on whether controller_oauthtoken is a string or a dict
if self.params.get('controller_oauthtoken'):
token_param = self.params.get('controller_oauthtoken')
if isinstance(token_param, dict):
if 'token' in token_param:
self.oauth_token = self.params.get('controller_oauthtoken')['token']
else:
self.fail_json(msg="The provided dict in controller_oauthtoken did not properly contain the token entry")
elif isinstance(token_param, string_types):
self.oauth_token = self.params.get('controller_oauthtoken')
else:
error_msg = "The provided controller_oauthtoken type was not valid ({0}). Valid options are str or dict.".format(type(token_param).__name__)
self.fail_json(msg=error_msg)

# Perform some basic validation
if not re.match('^https{0,1}://', self.host):
self.host = "https://{0}".format(self.host)
Expand Down Expand Up @@ -312,9 +292,6 @@ class ControllerAPIModule(ControllerModule):
IDENTITY_FIELDS = {'users': 'username', 'workflow_job_template_nodes': 'identifier', 'instances': 'hostname'}
ENCRYPTED_STRING = "$encrypted$"

# which app was used to create the oauth_token
oauth_token_app_key = None

def __init__(self, argument_spec, direct_params=None, error_callback=None, warn_callback=None, **kwargs):
kwargs['supports_check_mode'] = True

Expand All @@ -338,8 +315,7 @@ def get_item_name(self, item, allow_unknown=False):
for field_name in ControllerAPIModule.IDENTITY_FIELDS.values():
if field_name in item:
return item[field_name]

if item.get('type', None) in ('o_auth2_access_token', 'credential_input_source'):
if item.get('type', None) == 'credential_input_source':
return item['id']

if allow_unknown:
Expand Down Expand Up @@ -498,15 +474,12 @@ def make_request(self, method, endpoint, *args, **kwargs):
# Extract the headers, this will be used in a couple of places
headers = kwargs.get('headers', {})

# Authenticate to AWX (if we don't have a token and if not already done so)
if not self.oauth_token and not self.authenticated:
# This method will set a cookie in the cookie jar for us and also an oauth_token when possible
# Authenticate to AWX (if not already done so)
if not self.authenticated:
# This method will set a cookie in the cookie jar for us
self.authenticate(**kwargs)
if self.oauth_token:
# If we have a oauth token, we just use a bearer header
headers['Authorization'] = 'Bearer {0}'.format(self.oauth_token)
elif self.username and self.password:
headers['Authorization'] = self._get_basic_authorization_header()

headers['Authorization'] = self._get_basic_authorization_header()

if method in ['POST', 'PUT', 'PATCH']:
headers.setdefault('Content-Type', 'application/json')
Expand Down Expand Up @@ -665,71 +638,11 @@ def _authenticate_with_basic_auth(self):
},
)

def _authenticate_create_token(self, app_key=None):
# in case of failure and to give a chance to authenticate via other means, should not raise exceptions
# but only warnings
if self.username and self.password:
login_data = {
"description": "Automation Platform Controller Module Token",
"application": None,
"scope": "write",
}

api_token_url = self.build_url("tokens", app_key=app_key).geturl()
try:
response = self.session.open(
'POST',
api_token_url,
validate_certs=self.verify_ssl,
timeout=self.request_timeout,
follow_redirects=True,
data=dumps(login_data),
headers={
"Content-Type": "application/json",
"Authorization": self._get_basic_authorization_header(),
},
)

except Exception as exp:
self.warn("url: {0} - Failed to get token: {1}".format(api_token_url, exp))
return

token_response = None
try:
token_response = response.read()
response_json = loads(token_response)
self.oauth_token_id = response_json['id']
self.oauth_token = response_json['token']
# set the app that received the token create request, this is needed when removing the token at logout
self.oauth_token_app_key = app_key
except Exception as exp:
self.warn(
"url: {0} - Failed to extract token information from login response: {1}, response: {2}".format(
api_token_url, exp, token_response,
)
)
return

return None

def authenticate(self, **kwargs):
# As a temporary solution for version 4.6 try to get a token by using basic authentication from:
# /api/gateway/v1/tokens/ when app_key is gateway
# /api/v2/tokens/ when app_key is None and _COLLECTION_TYPE = "awx"
# /api/controller/v2/tokens/ when app_key is None and _COLLECTION_TYPE != "awx"
for app_key in ["gateway", None]:
# to give a chance to authenticate via basic authentication in case of failure,
# _authenticate_create_token, should not raise exception but only warnings,
self._authenticate_create_token(app_key=app_key)
if self.oauth_token:
break

if not self.oauth_token:
# if not having an oauth_token and when collection_type is awx try to login with basic authentication
try:
self._authenticate_with_basic_auth()
except Exception as exp:
self.fail_json(msg='Failed to get user info: {0}'.format(exp))
try:
self._authenticate_with_basic_auth()
except Exception as exp:
self.fail_json(msg='Failed to get user info: {0}'.format(exp))

self.authenticated = True

Expand Down Expand Up @@ -1080,37 +993,7 @@ def create_or_update_if_needed(
)

def logout(self):
if self.authenticated and self.oauth_token_id:
# Attempt to delete our current token from /api/v2/tokens/
# Post to the tokens endpoint with baisc auth to try and get a token
api_token_url = self.build_url(
"tokens/{0}/".format(self.oauth_token_id),
app_key=self.oauth_token_app_key,
).geturl()

try:
self.session.open(
'DELETE',
api_token_url,
validate_certs=self.verify_ssl,
timeout=self.request_timeout,
follow_redirects=True,
headers={
"Authorization": self._get_basic_authorization_header(),
}
)
self.oauth_token_id = None
self.oauth_token = None
self.authenticated = False
except HTTPError as he:
try:
resp = he.read()
except Exception as e:
resp = 'unknown {0}'.format(e)
self.warn('Failed to release token: {0}, response: {1}'.format(he, resp))
except (Exception) as e:
# Sanity check: Did the server send back some kind of internal error?
self.warn('Failed to release token {0}: {1}'.format(self.oauth_token_id, e))
self.authenticated = False

def is_job_done(self, job_status):
if job_status in ['new', 'pending', 'waiting', 'running']:
Expand Down
Loading
Loading