Merge pull request #7116 from john-westcott-iv/token_modules

Initial cut at tower_token module

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot] 2020-06-08 12:26:43 +00:00 committed by GitHub
commit 824d798d81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 364 additions and 3 deletions

View File

@ -31,8 +31,12 @@ options:
tower_oauthtoken:
description:
- The Tower 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 tower_token module.
- If value not set, will try environment variable C(TOWER_OAUTH_TOKEN) and then config files
type: str
type: raw
version_added: "3.7"
validate_certs:
description:
- Whether to allow insecure connections to Tower or AWX.

View File

@ -3,7 +3,7 @@ __metaclass__ = type
from ansible.module_utils.basic import AnsibleModule, env_fallback
from ansible.module_utils.urls import Request, SSLValidationError, ConnectionError
from ansible.module_utils.six import PY2
from ansible.module_utils.six import PY2, string_types
from ansible.module_utils.six.moves import StringIO
from ansible.module_utils.six.moves.urllib.parse import urlparse, urlencode
from ansible.module_utils.six.moves.urllib.error import HTTPError
@ -47,7 +47,7 @@ class TowerModule(AnsibleModule):
tower_username=dict(required=False, fallback=(env_fallback, ['TOWER_USERNAME'])),
tower_password=dict(no_log=True, required=False, fallback=(env_fallback, ['TOWER_PASSWORD'])),
validate_certs=dict(type='bool', aliases=['tower_verify_ssl'], required=False, fallback=(env_fallback, ['TOWER_VERIFY_SSL'])),
tower_oauthtoken=dict(type='str', no_log=True, required=False, fallback=(env_fallback, ['TOWER_OAUTH_TOKEN'])),
tower_oauthtoken=dict(type='raw', no_log=True, required=False, fallback=(env_fallback, ['TOWER_OAUTH_TOKEN'])),
tower_config_file=dict(type='path', required=False, default=None),
)
short_params = {
@ -96,6 +96,20 @@ class TowerModule(AnsibleModule):
if direct_value is not None:
setattr(self, short_param, direct_value)
# Perform magic depending on whether tower_oauthtoken is a string or a dict
if self.params.get('tower_oauthtoken'):
token_param = self.params.get('tower_oauthtoken')
if type(token_param) is dict:
if 'token' in token_param:
self.oauth_token = self.params.get('tower_oauthtoken')['token']
else:
self.fail_json(msg="The provided dict in tower_oauthtoken did not properly contain the token entry")
elif isinstance(token_param, string_types):
self.oauth_token = self.params.get('tower_oauthtoken')
else:
error_msg = "The provided tower_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)
@ -504,6 +518,9 @@ class TowerModule(AnsibleModule):
item_name = existing_item['username']
elif 'identifier' in existing_item:
item_name = existing_item['identifier']
elif item_type == 'o_auth2_access_token':
# An oauth2 token has no name, instead we will use its id for any of the messages
item_name = existing_item['id']
else:
self.fail_json(msg="Unable to process delete of {0} due to missing name".format(item_type))

View File

@ -0,0 +1,201 @@
#!/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: tower_token
author: "John Westcott IV (@john-westcott-iv)"
version_added: "2.3"
short_description: create, update, or destroy Ansible Tower tokens.
description:
- Create or destroy Ansible Tower 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
tower_* modules as the parameter tower_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 in Tower 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
default: ''
application:
description:
- The application 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
default: 'write'
choices: ["read", "write"]
existing_token:
description: The data structure produced from tower_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
tower_token:
description: '{{ token_description }}'
scope: "write"
state: present
tower_oauthtoken: "{{ my_existing_token }}"
- name: Delete this token
tower_token:
existing_token: "{{ tower_token }}"
state: absent
- name: Create a new token using username/password
tower_token:
description: '{{ token_description }}'
scope: "write"
state: present
tower_username: "{{ my_username }}"
tower_password: "{{ my_password }}"
- name: Use our new token to make another call
tower_job_list:
tower_oauthtoken: "{{ tower_token }}"
always:
- name: Delete our Token with the token we created
tower_token:
existing_token: "{{ tower_token }}"
state: absent
when: tower_token is defined
- name: Delete a token by its id
tower_token:
existing_token_id: 4
state: absent
'''
RETURN = '''
tower_token:
type: dict
description: An Ansible Fact variable representing a Tower 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.tower_api import TowerModule
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'] = {
'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'], default='write'),
existing_token=dict(type='dict'),
existing_token_id=dict(),
state=dict(choices=['present', 'absent'], default='present'),
)
# Create a module for ourselves
module = TowerModule(
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

@ -0,0 +1,29 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import pytest
from awx.main.models import OAuth2AccessToken
@pytest.mark.django_db
def test_create_token(run_module, admin_user):
module_args = {
'description': 'barfoo',
'state': 'present',
'scope': 'read',
'tower_host': None,
'tower_username': None,
'tower_password': None,
'validate_certs': None,
'tower_oauthtoken': None,
'tower_config_file': None,
}
result = run_module('tower_token', module_args, admin_user)
assert result.get('changed'), result
tokens = OAuth2AccessToken.objects.filter(description='barfoo')
assert len(tokens) == 1, 'Tokens with description of barfoo != 0: {0}'.format(len(tokens))
assert tokens[0].scope == 'read', 'Token was not given read access'

View File

@ -0,0 +1,110 @@
---
- name: Generate names
set_fact:
token_description: "AWX-Collection-tests-tower_token-description-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
- name: Try to use a token as a dict which is missing the token parameter
tower_job_list:
tower_oauthtoken:
not_token: "This has no token entry"
register: results
ignore_errors: true
- assert:
that:
- results is failed
- '"The provided dict in tower_oauthtoken did not properly contain the token entry" == results.msg'
- name: Try to use a token as a list
tower_job_list:
tower_oauthtoken:
- dummy_token
register: results
ignore_errors: true
- assert:
that:
- results is failed
- '"The provided tower_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
tower_token:
state: absent
register: results
ignore_errors: true
- assert:
that:
- results is failed
# We don't assert a message here because it handled by ansible
- name: Try to delete a token with both existing_token or existing_token_id
tower_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 handled by ansible
- block:
- name: Create a Token
tower_token:
description: '{{ token_description }}'
scope: "write"
state: present
register: new_token
- name: Validate our token works by token
tower_job_list:
tower_oauthtoken: "{{ tower_token.token }}"
register: job_list
- name: Validate out token works by object
tower_job_list:
tower_oauthtoken: "{{ tower_token }}"
register: job_list
always:
- name: Delete our Token with our own token
tower_token:
existing_token: "{{ tower_token }}"
tower_oauthtoken: "{{ tower_token }}"
state: absent
when: tower_token is defined
register: results
- assert:
that:
- results is changed or results is skipped
- block:
- name: Create a second token
tower_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
tower_token:
existing_token_id: "{{ tower_token['id'] }}"
tower_oauthtoken: "{{ tower_token }}"
state: absent
when: tower_token is defined
register: results
- assert:
that:
- results is changed or resuslts is skipped