ansible / awx

AWX provides a web-based user interface, REST API, and task engine built on top of Ansible. It is one of the upstream projects for Red Hat Ansible Automation Platform.
Other
14.12k stars 3.43k forks source link

Unable to use awx collection with LDAP user, due to token creation #10243

Open jhampson-dbre opened 3 years ago

jhampson-dbre commented 3 years ago
ISSUE TYPE
SUMMARY

When trying to schedule a job with awx.awx.tower_schedule, I am unable to authenticate to Tower using username/password with LDAP authentication provider. We are using Ansible Tower, but based on the module error message, this appears to be a problem with awx.awx collection attempting to create an Oauth token instead of using basic auth for the request.

ENVIRONMENT
STEPS TO REPRODUCE

Attempt to schedule a job using awx.awx.tower_schedule with username/password authentication for LDAP user. Instead the task errors with OAuth2 Tokens cannot be created by users associated with an external authentication provider. We are using Ansible Tower, but based on the module error stack trace the problem appears to be with how authentication is implemented for awx.awx.

Example playbook below.

EXPECTED RESULTS

The schedule is created successfully in Tower

ACTUAL RESULTS

awx.awx.tower_schedule module fails with the following error:

    "msg": "Failed to get token: HTTP Error 403: Forbidden",
    "response": "{\"detail\":\"(access_denied) OAuth2 Tokens cannot be created by users associated with an external authentication provider (ldap)\"}"
ADDITIONAL INFORMATION

Playbook used for testing

- hosts: localhost
  connection: local
  gather_facts: false
  tasks:
    - name: Schedule a job test
      awx.awx.tower_schedule:
        name: "Schedule a job test"
        state: present
        unified_job_template: "Demo Job"
        rrule: "{{ query('awx.awx.tower_schedule_rrule', 'week', start_date='2021-12-19 13:05:51') }}"
        tower_host: https://tower.company.example.com/#/login
        tower_username: myuser
        tower_password: mypassword
      register: result

Output from failed playbook run

(demo_venv) [vagrant@a9d8b915b6ea jobs_local]$ ansible-playbook ./scheduler_demo.yml -vvv
ansible-playbook 2.9.21
  config file = /home/vagrant/.ansible.cfg
  configured module search path = ['/home/vagrant/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/vagrant/virtualenvs/demo_venv/lib64/python3.6/site-packages/ansible
  executable location = /home/vagrant/virtualenvs/demo_venv/bin/ansible-playbook
  python version = 3.6.12 (default, Oct 23 2020, 13:36:31) [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)]
Using /home/vagrant/.ansible.cfg as config file
host_list declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
script declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
auto declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
Parsed /etc/ansible/hosts inventory source with ini plugin
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
Skipping callback 'actionable', as we already have a stdout callback.
Skipping callback 'counter_enabled', as we already have a stdout callback.
Skipping callback 'debug', as we already have a stdout callback.
Skipping callback 'dense', as we already have a stdout callback.
Skipping callback 'dense', as we already have a stdout callback.
Skipping callback 'full_skip', as we already have a stdout callback.
Skipping callback 'json', as we already have a stdout callback.
Skipping callback 'minimal', as we already have a stdout callback.
Skipping callback 'null', as we already have a stdout callback.
Skipping callback 'oneline', as we already have a stdout callback.
Skipping callback 'selective', as we already have a stdout callback.
Skipping callback 'skippy', as we already have a stdout callback.
Skipping callback 'stderr', as we already have a stdout callback.
Skipping callback 'unixy', as we already have a stdout callback.
Skipping callback 'yaml', as we already have a stdout callback.

PLAYBOOK: scheduler_demo.yml **********************************************************************************************************************************
1 plays in ./scheduler_demo.yml

PLAY [localhost] ******************************************************************************************************************************************************
META: ran handlers

TASK [Schedule a job test] ********************************************************************************************************************************************
task path: /workspaces/ansible/app/rootfs/jobs_local/scheduler_demo.yml:6
Using module file /home/vagrant/.ansible/collections/ansible_collections/awx/awx/plugins/modules/tower_schedule.py
Pipelining is enabled.
<127.0.0.1> ESTABLISH LOCAL CONNECTION FOR USER: vagrant
<127.0.0.1> EXEC /bin/sh -c '/home/vagrant/virtualenvs/demo_venv/bin/python3 && sleep 0'
The full traceback is:
  File "/tmp/ansible_awx.awx.tower_schedule_payload_35r03c84/ansible_awx.awx.tower_schedule_payload.zip/ansible_collections/awx/awx/plugins/module_utils/tower_api.py", line 322, in authenticate
    headers={'Content-Type': 'application/json'},
  File "/tmp/ansible_awx.awx.tower_schedule_payload_35r03c84/ansible_awx.awx.tower_schedule_payload.zip/ansible/module_utils/urls.py", line 1294, in open
    r = urllib_request.urlopen(*urlopen_args)
  File "/opt/rh/rh-python36/root/usr/lib64/python3.6/urllib/request.py", line 223, in urlopen
    return opener.open(url, data, timeout)
  File "/opt/rh/rh-python36/root/usr/lib64/python3.6/urllib/request.py", line 532, in open
    response = meth(req, response)
  File "/opt/rh/rh-python36/root/usr/lib64/python3.6/urllib/request.py", line 642, in http_response
    'http', request, response, code, msg, hdrs)
  File "/opt/rh/rh-python36/root/usr/lib64/python3.6/urllib/request.py", line 570, in error
    return self._call_chain(*args)
  File "/opt/rh/rh-python36/root/usr/lib64/python3.6/urllib/request.py", line 504, in _call_chain
    result = func(*args)
  File "/opt/rh/rh-python36/root/usr/lib64/python3.6/urllib/request.py", line 650, in http_error_default
    raise HTTPError(req.full_url, code, msg, hdrs, fp)
fatal: [localhost]: FAILED! => {
    "changed": false,
    "invocation": {
        "module_args": {
            "description": null,
            "diff_mode": null,
            "enabled": null,
            "extra_data": null,
            "inventory": null,
            "job_tags": null,
            "job_type": null,
            "limit": null,
            "name": "Schedule a job test",
            "new_name": null,
            "rrule": "DTSTART;TZID=America/New_York:20211219T130551 RRULE:FREQ=WEEKLY;INTERVAL=1",
            "scm_branch": null,
            "skip_tags": null,
            "state": "present",
            "tower_config_file": null,
            "tower_host": "https://tower.company.example.com/#/login",
            "tower_oauthtoken": null,
            "tower_password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
            "tower_username": "myuser",
            "unified_job_template": "Demo Job",
            "validate_certs": null,
            "verbosity": null
        }
    },
    "msg": "Failed to get token: HTTP Error 403: Forbidden",
    "response": "{\"detail\":\"(access_denied) OAuth2 Tokens cannot be created by users associated with an external authentication provider (ldap)\"}"
}

PLAY RECAP ************************************************************************************************************************************************************
localhost                  : ok=0    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0   
hassenius commented 3 years ago

I have this problem too with tower_job_launch. I can successfully call the API using the same credentials via requests.post("<api_endpoint>/job_templates/<template_id>/launch/",auth=(username, password)), but when I try use the ansible module it fails with the same message as above

bharathak commented 3 years ago

I have this problem too, with awx login and POST on https://awx-host.domain.com/api/v2/tokens/

Version 19.2.2

On AWX CLI


awx login --conf.host https://awx-host.domain.com --conf.username abc--conf.password xyz --conf.insecure
usage: awx login [-h] [--description TEXT] [--conf.client_id TEXT] [--conf.client_secret TEXT] [--conf.scope {read,write}]
                 [--conf.host https://example.awx.org] [--conf.token TEXT] [--conf.username TEXT] [--conf.password TEXT] [-k]

optional arguments:
  -h, --help            show this help message and exit

OAuth2.0 Options:
  --description TEXT    description of the generated OAuth2.0 token
  --conf.client_id TEXT
  --conf.client_secret TEXT
  --conf.scope {read,write}

authentication:
  --conf.host https://example.awx.org
  --conf.token TEXT     an OAuth2.0 token (get one by using `awx login`)
  --conf.username TEXT
  --conf.password TEXT
  -k, --conf.insecure   Allow insecure server connections when using SSL
Error retrieving an OAuth2.0 token (<class 'awxkit.exceptions.NotFound'>).

via CURL

 curl -u abc:xyz -k -X POST https://awx-host.domain.com/api/v2/tokens/
{"detail":"(access_denied) OAuth2 Tokens cannot be created by users associated with an external authentication provider (ldap)"}
thestevenbell commented 3 years ago

I have the same issue. Managing AWX via the the API works because the username and password can be passed instead of relying on a token. Is it possible to configure the collection awx.awx module to use the provided username and password if no token can be obtained? Enabling this functionality would be preferable to setting the ALLOW_OAUTH2_FOR_EXTERNAL_USERS configuration as outlined in the note here https://docs.ansible.com/ansible-tower/latest/html/administration/oauth2_token_auth.html#revoke-an-access-token due to the issues with the tokens not checking against the LDAP server for authorization.

oehlrich9 commented 3 years ago

In my opinion, this is not a big change.

A good start would be to value the 'use_token' property from the tower-cli configuration file. This requires to introduce this parameter similar to the verify_ssl parameter. Then we need to add another way of authentication with the Basic header:

if not self.oauth_token and not self.authenticated and self.use_token:
    # This method will set a cookie in the cookie jar for us and also an oauth_token
    self.authenticate(**kwargs)
elif self.oauth_token:
    # If we have a oauth token, we just use a bearer header. 
    # We assume that we only have a oauth token if we also want to use it
    headers['Authorization'] = 'Bearer {0}'.format(self.oauth_token)
if not self.use_token:
    # If we don't want to use a oauth token, we will use Basic auth
    authstring = self.username+":"+self.password
    base64auth = base64.b64encode(authstring.encode('ascii'))
    headers['Authorization'] = 'Basic {0}'.format(base64auth.decode('ascii'))

I'm not sure if we need any updates on other positions (besides from module inputs, as we want to set this option manually as well), but this works for me pretty good.

Is there a reason that speaks against Basic auth for this module? Otherwise I might find some time to create a PR for that.

jhampson-dbre commented 3 years ago

I can't see any reason that awx-cli should not support basic auth. The awx-cli documentation states that "awx allows you to specify your username and password on every invocation", but this is not really true since no matter, what awx-cli will attempt to use an OAuth token. As it is now, the usefulness of awx-cli is relatively limited in LDAP integrated environments.

Flonka commented 2 years ago

I am also looking for a way to configure the awx.awx collection to not try and use a token, due to the current default setting to not allow oauth2 for external users (LDAP).

rchicoli commented 2 years ago

I am also having the same problem, for whom has admin permissions to AWX or Tower could allow the external LDAP users to create tokens. Mentioned by @thestevenbell

Settings -> System -> ALLOW EXTERNAL USERS TO CREATE OAUTH2 TOKENS -> Toggle the button

Sadly that is not my case and the internal policy is against it due to specific reasons.

Then I might want to Open a PR and hopefully it will be merged, as we are only extending the functionality of the Controller API calls in the module_utils.

Please let me know @oehlrich9 if you are still willing to do so, otherwise I would take over. Any objections against this feature?

azrdev commented 2 years ago

Can we rephrase this issue to sth like "awx collection requires right to create oauth2 token" ?

charlottecampbell193 commented 5 months ago

Hi, is there a plan to resolve this issue? I can see there is a closed issue that allowed the user to force basic auth and looks like there was a pull request but I cannot see anything about why it wasn't merged in. https://github.com/ansible/awx/pull/11906