feat: remove collection support for oauth (#15623)

Co-authored-by: Alan Rominger <arominge@redhat.com>
This commit is contained in:
Pablo H.
2024-11-19 21:08:10 +01:00
committed by Alan Rominger
parent 6599f3f827
commit 3ba6e2e394
18 changed files with 62 additions and 824 deletions

View File

@@ -32,11 +32,28 @@ Installing the `tar.gz` involves no special instructions.
## Running ## Running
Non-deprecated modules in this collection have no Python requirements, but 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. in the future. The `DOCUMENTATION` for each module will report this.
You can specify authentication by host, username, and password. 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 ## Release and Upgrade Notes
@@ -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/). - 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 - 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. - 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 - 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. - 0.0.1-devel is the version you should see if installing from source, which is intended for development and expected to be unstable.

View File

@@ -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 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 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%] awx_collection/test/awx/test_completeness.py::test_completeness PASSED [ 1%]
... ...
@@ -124,18 +124,18 @@ FAILED awx_collection/test/awx/test_module_utils.py::test_type_warning - SystemE
make: *** [Makefile:382: test_collection] Error 1 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 ============================ ============================ test session starts ============================
platform darwin -- Python 3.7.0, pytest-3.6.0, py-1.8.1, pluggy-0.6.0 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) django: settings: awx.settings.development (from ini)
rootdir: /Users/jowestco/junk/awx, inifile: pytest.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 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 ========================= ========================= 1 passed in 1.72 seconds =========================
``` ```

View File

@@ -5,7 +5,6 @@ action_groups:
- ad_hoc_command - ad_hoc_command
- ad_hoc_command_cancel - ad_hoc_command_cancel
- ad_hoc_command_wait - ad_hoc_command_wait
- application
- bulk_job_launch - bulk_job_launch
- bulk_host_create - bulk_host_create
- bulk_host_delete - bulk_host_delete
@@ -42,7 +41,6 @@ action_groups:
- settings - settings
- subscriptions - subscriptions
- team - team
- token
- user - user
- workflow_approval - workflow_approval
- workflow_job_template_node - workflow_job_template_node

View File

@@ -32,16 +32,6 @@ options:
- If value not set, will try environment variable C(CONTROLLER_PASSWORD) and then config files - If value not set, will try environment variable C(CONTROLLER_PASSWORD) and then config files
type: str type: str
aliases: [ tower_password ] 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: validate_certs:
description: description:
- Whether to allow insecure connections to AWX. - Whether to allow insecure connections to AWX.

View File

@@ -43,17 +43,6 @@ options:
version: '4.0.0' version: '4.0.0'
why: Collection name change why: Collection name change
alternatives: 'CONTROLLER_PASSWORD' 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: verify_ssl:
description: description:
- Specify whether Ansible should verify the SSL certificate of the controller host. - Specify whether Ansible should verify the SSL certificate of the controller host.

View File

@@ -18,7 +18,7 @@ description:
options: options:
_terms: _terms:
description: 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 required: True
query_params: query_params:
description: description:

View File

@@ -33,13 +33,8 @@ class ControllerAWXKitModule(ControllerModule):
def authenticate(self): def authenticate(self):
try: try:
if self.oauth_token: self.connection.login(username=self.username, password=self.password)
# MERGE: fix conflicts with removal of OAuth2 token from collection branch self.authenticated = True
self.connection.login(None, None)
self.authenticated = True
elif self.username:
self.connection.login(username=self.username, password=self.password)
self.authenticated = True
except Exception: except Exception:
self.fail_json("Failed to authenticate") self.fail_json("Failed to authenticate")

View File

@@ -6,7 +6,7 @@ from ansible.module_utils.basic import AnsibleModule, env_fallback
from ansible.module_utils.urls import Request, SSLValidationError, ConnectionError from ansible.module_utils.urls import Request, SSLValidationError, ConnectionError
from ansible.module_utils.parsing.convert_bool import boolean as strtobool from ansible.module_utils.parsing.convert_bool import boolean as strtobool
from ansible.module_utils.six import PY2 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 import StringIO
from ansible.module_utils.six.moves.urllib.error import HTTPError from ansible.module_utils.six.moves.urllib.error import HTTPError
from ansible.module_utils.six.moves.http_cookiejar import CookieJar from ansible.module_utils.six.moves.http_cookiejar import CookieJar
@@ -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'])), 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'])), 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'])), 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), 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 # Associations of these types are ordered and have special consideration in the modified associations function
@@ -68,15 +65,12 @@ class ControllerModule(AnsibleModule):
'password': 'controller_password', 'password': 'controller_password',
'verify_ssl': 'validate_certs', 'verify_ssl': 'validate_certs',
'request_timeout': 'request_timeout', 'request_timeout': 'request_timeout',
'oauth_token': 'controller_oauthtoken',
} }
host = '127.0.0.1' host = '127.0.0.1'
username = None username = None
password = None password = None
verify_ssl = True verify_ssl = True
request_timeout = 10 request_timeout = 10
oauth_token = None
oauth_token_id = None
authenticated = False authenticated = False
config_name = 'tower_cli.cfg' config_name = 'tower_cli.cfg'
version_checked = False version_checked = False
@@ -111,20 +105,6 @@ class ControllerModule(AnsibleModule):
if direct_value is not None: if direct_value is not None:
setattr(self, short_param, direct_value) 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 # Perform some basic validation
if not re.match('^https{0,1}://', self.host): if not re.match('^https{0,1}://', self.host):
self.host = "https://{0}".format(self.host) self.host = "https://{0}".format(self.host)
@@ -312,9 +292,6 @@ class ControllerAPIModule(ControllerModule):
IDENTITY_FIELDS = {'users': 'username', 'workflow_job_template_nodes': 'identifier', 'instances': 'hostname'} IDENTITY_FIELDS = {'users': 'username', 'workflow_job_template_nodes': 'identifier', 'instances': 'hostname'}
ENCRYPTED_STRING = "$encrypted$" 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): def __init__(self, argument_spec, direct_params=None, error_callback=None, warn_callback=None, **kwargs):
kwargs['supports_check_mode'] = True kwargs['supports_check_mode'] = True
@@ -338,8 +315,7 @@ class ControllerAPIModule(ControllerModule):
for field_name in ControllerAPIModule.IDENTITY_FIELDS.values(): for field_name in ControllerAPIModule.IDENTITY_FIELDS.values():
if field_name in item: if field_name in item:
return item[field_name] return item[field_name]
if item.get('type', None) == 'credential_input_source':
if item.get('type', None) in ('o_auth2_access_token', 'credential_input_source'):
return item['id'] return item['id']
if allow_unknown: if allow_unknown:
@@ -498,15 +474,12 @@ class ControllerAPIModule(ControllerModule):
# Extract the headers, this will be used in a couple of places # Extract the headers, this will be used in a couple of places
headers = kwargs.get('headers', {}) headers = kwargs.get('headers', {})
# Authenticate to AWX (if we don't have a token and if not already done so) # Authenticate to AWX (if not already done so)
if not self.oauth_token and not self.authenticated: if not self.authenticated:
# This method will set a cookie in the cookie jar for us and also an oauth_token when possible # This method will set a cookie in the cookie jar for us
self.authenticate(**kwargs) self.authenticate(**kwargs)
if self.oauth_token:
# If we have a oauth token, we just use a bearer header headers['Authorization'] = self._get_basic_authorization_header()
headers['Authorization'] = 'Bearer {0}'.format(self.oauth_token)
elif self.username and self.password:
headers['Authorization'] = self._get_basic_authorization_header()
if method in ['POST', 'PUT', 'PATCH']: if method in ['POST', 'PUT', 'PATCH']:
headers.setdefault('Content-Type', 'application/json') headers.setdefault('Content-Type', 'application/json')
@@ -665,71 +638,11 @@ class ControllerAPIModule(ControllerModule):
}, },
) )
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): def authenticate(self, **kwargs):
# As a temporary solution for version 4.6 try to get a token by using basic authentication from: try:
# /api/gateway/v1/tokens/ when app_key is gateway self._authenticate_with_basic_auth()
# /api/v2/tokens/ when app_key is None and _COLLECTION_TYPE = "awx" except Exception as exp:
# /api/controller/v2/tokens/ when app_key is None and _COLLECTION_TYPE != "awx" self.fail_json(msg='Failed to get user info: {0}'.format(exp))
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))
self.authenticated = True self.authenticated = True
@@ -1080,37 +993,7 @@ class ControllerAPIModule(ControllerModule):
) )
def logout(self): def logout(self):
if self.authenticated and self.oauth_token_id: self.authenticated = False
# 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))
def is_job_done(self, job_status): def is_job_done(self, job_status):
if job_status in ['new', 'pending', 'waiting', 'running']: if job_status in ['new', 'pending', 'waiting', 'running']:

View File

@@ -1,162 +0,0 @@
#!/usr/bin/python
# coding: utf-8 -*-
# (c) 2020,Geoffrey Bachelot <bachelotg@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'}
DOCUMENTATION = '''
---
module: application
author: "Geoffrey Bacheot (@jffz)"
short_description: create, update, or destroy Automation Platform Controller applications
description:
- Create, update, or destroy Automation Platform Controller applications. See
U(https://www.ansible.com/tower) for an overview.
options:
name:
description:
- Name of the application.
required: True
type: str
new_name:
description:
- Setting this option will change the existing name (looked up via the name field.
type: str
description:
description:
- Description of the application.
type: str
authorization_grant_type:
description:
- The grant type the user must use for acquire tokens for this application.
choices: ["password", "authorization-code"]
type: str
required: False
client_type:
description:
- Set to public or confidential depending on how secure the client device is.
choices: ["public", "confidential"]
type: str
required: False
organization:
description:
- Name, ID, or named URL of organization for application.
type: str
required: True
redirect_uris:
description:
- Allowed urls list, space separated. Required when authorization-grant-type=authorization-code
type: list
elements: str
state:
description:
- Desired state of the resource.
default: "present"
choices: ["present", "absent", "exists"]
type: str
skip_authorization:
description:
- Set True to skip authorization step for completely trusted applications.
type: bool
extends_documentation_fragment: awx.awx.auth
'''
EXAMPLES = '''
- name: Add Foo application
application:
name: "Foo"
description: "Foo bar application"
organization: "test"
state: present
authorization_grant_type: password
client_type: public
- name: Add Foo application
application:
name: "Foo"
description: "Foo bar application"
organization: "test"
state: present
authorization_grant_type: authorization-code
client_type: confidential
redirect_uris:
- http://tower.com/api/v2/
'''
from ..module_utils.controller_api import ControllerAPIModule
def main():
# Any additional arguments that are not fields of the item can be added here
argument_spec = dict(
name=dict(required=True),
new_name=dict(),
description=dict(),
authorization_grant_type=dict(choices=["password", "authorization-code"]),
client_type=dict(choices=['public', 'confidential']),
organization=dict(required=True),
redirect_uris=dict(type="list", elements='str'),
state=dict(choices=['present', 'absent', 'exists'], default='present'),
skip_authorization=dict(type='bool'),
)
# Create a module for ourselves
module = ControllerAPIModule(argument_spec=argument_spec)
# Extract our parameters
name = module.params.get('name')
new_name = module.params.get("new_name")
description = module.params.get('description')
authorization_grant_type = module.params.get('authorization_grant_type')
client_type = module.params.get('client_type')
organization = module.params.get('organization')
redirect_uris = module.params.get('redirect_uris')
skip_authorization = module.params.get('skip_authorization')
state = module.params.get('state')
# Attempt to look up the related items the user specified (these will fail the module if not found)
org_id = module.resolve_name_to_id('organizations', organization)
# Attempt to look up application based on the provided name and org ID
application = module.get_one('applications', name_or_id=name, check_exists=(state == 'exists'), **{'data': {'organization': org_id}})
if state == 'absent':
# If the state was absent we can let the module delete it if needed, the module will handle exiting from this
module.delete_if_needed(application)
# Create the data that gets sent for create and update
application_fields = {
'name': new_name if new_name else (module.get_item_name(application) if application else name),
'organization': org_id,
}
if authorization_grant_type is not None:
application_fields['authorization_grant_type'] = authorization_grant_type
if client_type is not None:
application_fields['client_type'] = client_type
if description is not None:
application_fields['description'] = description
if redirect_uris is not None:
application_fields['redirect_uris'] = ' '.join(redirect_uris)
if skip_authorization is not None:
application_fields['skip_authorization'] = skip_authorization
response = module.create_or_update_if_needed(application, application_fields, endpoint='applications', item_type='application', auto_exit=False)
if 'client_id' in response:
module.json_output['client_id'] = response['client_id']
if 'client_secret' in response:
module.json_output['client_secret'] = response['client_secret']
module.exit_json(**module.json_output)
if __name__ == '__main__':
main()

View File

@@ -1,208 +0,0 @@
#!/usr/bin/python
# coding: utf-8 -*-
# (c) 2020, John Westcott IV <john.westcott.iv@redhat.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'}
DOCUMENTATION = '''
---
module: token
author: "John Westcott IV (@john-westcott-iv)"
version_added: "2.3.0"
short_description: create, update, or destroy Automation Platform Controller tokens.
description:
- Create or destroy Automation Platform Controller tokens. See
U(https://www.ansible.com/tower) for an overview.
- In addition, the module sets an Ansible fact which can be passed into other
controller modules as the parameter controller_oauthtoken. See examples for usage.
- Because of the sensitive nature of tokens, the created token value is only available once
through the Ansible fact. (See RETURN for details)
- Due to the nature of tokens this module is not idempotent. A second will
with the same parameters will create a new token.
- If you are creating a temporary token for use with modules you should delete the token
when you are done with it. See the example for how to do it.
options:
description:
description:
- Optional description of this access token.
required: False
type: str
application:
description:
- The application name, ID, or named URL tied to this token.
required: False
type: str
scope:
description:
- Allowed scopes, further restricts user's permissions. Must be a simple space-separated string with allowed scopes ['read', 'write'].
required: False
type: str
choices: ["read", "write"]
existing_token:
description: The data structure produced from token in create mode to be used with state absent.
type: dict
existing_token_id:
description: A token ID (number) which can be used to delete an arbitrary token with state absent.
type: str
state:
description:
- Desired state of the resource.
choices: ["present", "absent"]
default: "present"
type: str
extends_documentation_fragment: awx.awx.auth
'''
EXAMPLES = '''
- block:
- name: Create a new token using an existing token
token:
description: '{{ token_description }}'
scope: "write"
state: present
controller_oauthtoken: "{{ my_existing_token }}"
- name: Delete this token
token:
existing_token: "{{ controller_token }}"
state: absent
- name: Create a new token using username/password
token:
description: '{{ token_description }}'
scope: "write"
state: present
controller_username: "{{ my_username }}"
controller_password: "{{ my_password }}"
- name: Use our new token to make another call
job_list:
controller_oauthtoken: "{{ controller_token }}"
always:
- name: Delete our Token with the token we created
token:
existing_token: "{{ controller_token }}"
state: absent
when: token is defined
- name: Delete a token by its id
token:
existing_token_id: 4
state: absent
'''
RETURN = '''
controller_token:
type: dict
description: An Ansible Fact variable representing a token object which can be used for auth in subsequent modules. See examples for usage.
contains:
token:
description: The token that was generated. This token can never be accessed again, make sure this value is noted before it is lost.
type: str
id:
description: The numeric ID of the token created
type: str
returned: on successful create
'''
from ..module_utils.controller_api import ControllerAPIModule
def return_token(module, last_response):
# A token is special because you can never get the actual token ID back from the API.
# So the default module return would give you an ID but then the token would forever be masked on you.
# This method will return the entire token object we got back so that a user has access to the token
module.json_output['ansible_facts'] = {
'controller_token': last_response,
'tower_token': last_response,
}
module.exit_json(**module.json_output)
def main():
# Any additional arguments that are not fields of the item can be added here
argument_spec = dict(
description=dict(),
application=dict(),
scope=dict(choices=['read', 'write']),
existing_token=dict(type='dict', no_log=False),
existing_token_id=dict(),
state=dict(choices=['present', 'absent'], default='present'),
)
# Create a module for ourselves
module = ControllerAPIModule(
argument_spec=argument_spec,
mutually_exclusive=[
('existing_token', 'existing_token_id'),
],
# If we are state absent make sure one of existing_token or existing_token_id are present
required_if=[
[
'state',
'absent',
('existing_token', 'existing_token_id'),
True,
],
],
)
# Extract our parameters
description = module.params.get('description')
application = module.params.get('application')
scope = module.params.get('scope')
existing_token = module.params.get('existing_token')
existing_token_id = module.params.get('existing_token_id')
state = module.params.get('state')
if state == 'absent':
if not existing_token:
existing_token = module.get_one(
'tokens',
**{
'data': {
'id': existing_token_id,
}
}
)
# If the state was absent we can let the module delete it if needed, the module will handle exiting from this
module.delete_if_needed(existing_token)
# Attempt to look up the related items the user specified (these will fail the module if not found)
application_id = None
if application:
application_id = module.resolve_name_to_id('applications', application)
# Create the data that gets sent for create and update
new_fields = {}
if description is not None:
new_fields['description'] = description
if application is not None:
new_fields['application'] = application_id
if scope is not None:
new_fields['scope'] = scope
# If the state was present and we can let the module build or update the existing item, this will return on its own
module.create_or_update_if_needed(
None,
new_fields,
endpoint='tokens',
item_type='token',
associations={},
on_create=return_token,
)
if __name__ == '__main__':
main()

View File

@@ -1,29 +0,0 @@
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import pytest
from awx.main.models import Organization
from awx.main.models.oauth import OAuth2Application
@pytest.mark.django_db
def test_create_application(run_module, admin_user):
org = Organization.objects.create(name='foo')
module_args = {
'name': 'foo_app',
'description': 'barfoo',
'state': 'present',
'authorization_grant_type': 'password',
'client_type': 'public',
'organization': 'foo',
}
result = run_module('application', module_args, admin_user)
assert result.get('changed'), result
application = OAuth2Application.objects.get(name='foo_app')
assert application.description == 'barfoo'
assert application.organization_id == org.id

View File

@@ -21,7 +21,9 @@ read_only_endpoints_with_modules = ['settings', 'role', 'project_update', 'workf
# If a module should not be created for an endpoint and the endpoint is not read-only add it here # If a module should not be created for an endpoint and the endpoint is not read-only add it here
# THINK HARD ABOUT DOING THIS # THINK HARD ABOUT DOING THIS
no_module_for_endpoint = [ no_module_for_endpoint = [
'application', # Usage of OAuth tokens is deprecated
'constructed_inventory', # This is a view for inventory with kind=constructed 'constructed_inventory', # This is a view for inventory with kind=constructed
'token', # Usage of OAuth tokens is deprecated
] ]
# Some modules work on the related fields of an endpoint. These modules will not have an auto-associated endpoint # Some modules work on the related fields of an endpoint. These modules will not have an auto-associated endpoint
@@ -61,8 +63,6 @@ ignore_parameters = ['state', 'new_name', 'update_secrets', 'copy_from', 'is_int
no_api_parameter_ok = { no_api_parameter_ok = {
# The wait is for whether or not to wait for a project update on change # The wait is for whether or not to wait for a project update on change
'project': ['wait', 'interval', 'update_project'], 'project': ['wait', 'interval', 'update_project'],
# Existing_token and id are for working with an existing tokens
'token': ['existing_token', 'existing_token_id'],
# /survey spec is now how we handle associations # /survey spec is now how we handle associations
# We take an organization here to help with the lookups only # We take an organization here to help with the lookups only
'job_template': ['survey_spec', 'organization'], 'job_template': ['survey_spec', 'organization'],

View File

@@ -20,7 +20,6 @@ def test_create_organization(run_module, admin_user):
'controller_username': None, 'controller_username': None,
'controller_password': None, 'controller_password': None,
'validate_certs': None, 'validate_certs': None,
'controller_oauthtoken': None,
'controller_config_file': None, 'controller_config_file': None,
} }
@@ -53,7 +52,6 @@ def test_galaxy_credential_order(run_module, admin_user):
'controller_username': None, 'controller_username': None,
'controller_password': None, 'controller_password': None,
'validate_certs': None, 'validate_certs': None,
'controller_oauthtoken': None,
'controller_config_file': None, 'controller_config_file': None,
'galaxy_credentials': cred_ids, 'galaxy_credentials': cred_ids,
} }
@@ -78,7 +76,6 @@ def test_galaxy_credential_order(run_module, admin_user):
'controller_username': None, 'controller_username': None,
'controller_password': None, 'controller_password': None,
'validate_certs': None, 'validate_certs': None,
'controller_oauthtoken': None,
'controller_config_file': None, 'controller_config_file': None,
'galaxy_credentials': cred_ids, 'galaxy_credentials': cred_ids,
} }

View File

@@ -1,131 +0,0 @@
---
- name: Generate a test id
set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate names
set_fact:
app1_name: "AWX-Collection-tests-application-app1-{{ test_id }}"
app2_name: "AWX-Collection-tests-application-app2-{{ test_id }}"
app3_name: "AWX-Collection-tests-application-app3-{{ test_id }}"
- block:
- name: Create an application
application:
name: "{{ app1_name }}"
authorization_grant_type: "password"
client_type: "public"
organization: "Default"
state: present
register: result
- assert:
that:
- "result is changed"
- name: Run an application with exists
application:
name: "{{ app1_name }}"
authorization_grant_type: "password"
client_type: "public"
organization: "Default"
state: exists
register: result
- assert:
that:
- "result is not changed"
- name: Delete our application
application:
name: "{{ app1_name }}"
organization: "Default"
state: absent
register: result
- assert:
that:
- "result is changed"
- name: Run an application with exists
application:
name: "{{ app1_name }}"
authorization_grant_type: "password"
client_type: "public"
organization: "Default"
state: exists
register: result
- assert:
that:
- "result is changed"
- name: Delete our application
application:
name: "{{ app1_name }}"
organization: "Default"
state: absent
register: result
- assert:
that:
- "result is changed"
- name: Create a second application
application:
name: "{{ app2_name }}"
authorization_grant_type: "authorization-code"
client_type: "confidential"
organization: "Default"
description: "Another application"
redirect_uris:
- http://tower.com/api/v2/
- http://tower.com/api/v2/teams
state: present
register: result
- assert:
that:
- "result is changed"
- name: Create an all trusting application
application:
name: "{{ app3_name }}"
organization: "Default"
description: "All Trusting Application"
skip_authorization: true
authorization_grant_type: "password"
client_type: "confidential"
state: present
register: result
- assert:
that:
- "result is changed"
- "'client_secret' in result"
- name: Rename an inventory
application:
name: "{{ app3_name }}"
new_name: "{{ app3_name }}a"
organization: Default
state: present
register: result
- assert:
that:
- result.changed
always:
- name: Delete our application
application:
name: "{{ item }}"
organization: "Default"
state: absent
register: result
loop:
- "{{ app1_name }}"
- "{{ app2_name }}"
- "{{ app3_name }}"
- "{{ app3_name }}a"

View File

@@ -1,115 +0,0 @@
---
- name: Generate a test ID
set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate names
set_fact:
token_description: "AWX-Collection-tests-token-description-{{ test_id }}"
- name: Try to use a token as a dict which is missing the token parameter
job_list:
controller_oauthtoken:
not_token: "This has no token entry"
register: results
ignore_errors: true
- assert:
that:
- results is failed
- '"The provided dict in controller_oauthtoken did not properly contain the token entry" == results.msg'
- name: Try to use a token as a list
job_list:
controller_oauthtoken:
- dummy_token
register: results
ignore_errors: true
- assert:
that:
- results is failed
- '"The provided controller_oauthtoken type was not valid (list). Valid options are str or dict." == results.msg'
- name: Try to delete a token with no existing_token or existing_token_id
token:
state: absent
register: results
ignore_errors: true
- assert:
that:
- results is failed
# We don't assert a message here because it's handled by ansible
- name: Try to delete a token with both existing_token or existing_token_id
token:
existing_token:
id: 1234
existing_token_id: 1234
state: absent
register: results
ignore_errors: true
- assert:
that:
- results is failed
# We don't assert a message here because it's handled by ansible
- block:
- name: Create a Token
token:
description: '{{ token_description }}'
scope: "write"
state: present
register: new_token
- name: Validate our token works by token
job_list:
controller_oauthtoken: "{{ controller_token.token }}"
register: job_list
- name: Validate our token works by object
job_list:
controller_oauthtoken: "{{ controller_token }}"
register: job_list
always:
- name: Delete our Token with our own token
token:
existing_token: "{{ controller_token }}"
controller_oauthtoken: "{{ controller_token }}"
state: absent
when: controller_token is defined
register: results
- assert:
that:
- results is changed or results is skipped
- block:
- name: Create a second token
token:
description: '{{ token_description }}'
scope: "write"
state: present
register: results
- assert:
that:
- results is changed
always:
- name: Delete the second Token with our own token
token:
existing_token_id: "{{ controller_token['id'] }}"
controller_oauthtoken: "{{ controller_token }}"
state: absent
when: controller_token is defined
register: results
- assert:
that:
- results is changed or resuslts is skipped

View File

@@ -220,7 +220,6 @@
user: user:
controller_username: "{{ username }}-orgadmin" controller_username: "{{ username }}-orgadmin"
controller_password: "{{ username }}-orgadmin" controller_password: "{{ username }}-orgadmin"
controller_oauthtoken: false # Hack for CI where we use oauth in config file
username: "{{ username }}" username: "{{ username }}"
first_name: Joe first_name: Joe
password: "{{ 65535 | random | to_uuid }}" password: "{{ 65535 | random | to_uuid }}"

View File

@@ -82,11 +82,6 @@ options:
choices: ["present", "absent"] choices: ["present", "absent"]
default: "present" default: "present"
type: str type: str
controller_oauthtoken:
description:
- The OAuth token to use.
required: False
type: str
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
''' '''

View File

@@ -37,11 +37,28 @@ This collection should be installed from [Content Hub](https://cloud.redhat.com/
## Running ## Running
Non-deprecated modules in this collection have no Python requirements, but 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. in the future. The `DOCUMENTATION` for each module will report this.
You can specify authentication by host, username, and password. 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 ## Release and Upgrade Notes
@@ -52,12 +69,14 @@ Notable releases of the `{{ collection_namespace }}.{{ collection_package }}` co
- 11.0.0 has no non-deprecated modules that depend on the deprecated `tower-cli` [PyPI](https://pypi.org/project/ansible-tower-cli/). - 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 - 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. - 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 - 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. - 0.0.1-devel is the version you should see if installing from source, which is intended for development and expected to be unstable.
{% else %} {% else %}
- 3.7.0 initial release - 3.7.0 initial release
- 4.0.0 ansible.tower renamed to ansible.controller - 4.0.0 ansible.tower renamed to ansible.controller
- tower_ prefix is dropped from the module names, e.g. tower_inventory becomes inventory - tower_ prefix is dropped from the module names, e.g. tower_inventory becomes inventory
- 4.7.0 "token" module has been removed as oauth is no longer supported, use basic auth instead
{% endif %} {% endif %}
The following notes are changes that may require changes to playbooks: The following notes are changes that may require changes to playbooks:
@@ -90,7 +109,7 @@ The following notes are changes that may require changes to playbooks:
- The `notification_configuration` parameter of `tower_notification_template` has changed from a string to a dict. Please use the `lookup` plugin to read an existing file into a dict. - The `notification_configuration` parameter of `tower_notification_template` has changed from a string to a dict. Please use the `lookup` plugin to read an existing file into a dict.
- `tower_credential` no longer supports passing a file name to `ssh_key_data`. - `tower_credential` no longer supports passing a file name to `ssh_key_data`.
- The HipChat `notification_type` has been removed and can no longer be created using the `tower_notification_template` module. - The HipChat `notification_type` has been removed and can no longer be created using the `tower_notification_template` module.
- Lookup plugins now always reutrn a list, and if you want a scalar value use `lookup` as opposed to `query` - Lookup plugins now always return a list, and if you want a scalar value use `lookup` as opposed to `query`
{% if collection_package | lower() == "awx" %} {% if collection_package | lower() == "awx" %}
## Running Unit Tests ## Running Unit Tests