From 1353678f237ee4ceb7345f6e277138aaa2af8db9 Mon Sep 17 00:00:00 2001 From: Chris Hambridge Date: Fri, 17 Sep 2021 15:10:10 -0400 Subject: [PATCH] Enable ansible-galaxy to specify client id override with Keycloak Token (#75601) * Enable ansible-galaxy to specify client id override with Keycloak Token * Specify ability to provide override of client_id * Test client_id can be configured for individual servers * Add issue link to changelog * Document client_id as a config option and add an example Co-authored-by: s-hertel <19572925+s-hertel@users.noreply.github.com> --- ...75593-ansible-galaxy-keycloak-clientid.yml | 2 + .../shared_snippets/galaxy_server_list.txt | 9 +++- lib/ansible/cli/galaxy.py | 7 ++- lib/ansible/galaxy/token.py | 8 +++- test/units/galaxy/test_token.py | 43 +++++++++++++++++++ 5 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 changelogs/fragments/75593-ansible-galaxy-keycloak-clientid.yml diff --git a/changelogs/fragments/75593-ansible-galaxy-keycloak-clientid.yml b/changelogs/fragments/75593-ansible-galaxy-keycloak-clientid.yml new file mode 100644 index 00000000000..bf8ae157283 --- /dev/null +++ b/changelogs/fragments/75593-ansible-galaxy-keycloak-clientid.yml @@ -0,0 +1,2 @@ +minor_changes: + - ansible-galaxy - Allow specification of client_id override value for Keycloak Token (https://github.com/ansible/ansible/issues/75593). diff --git a/docs/docsite/rst/shared_snippets/galaxy_server_list.txt b/docs/docsite/rst/shared_snippets/galaxy_server_list.txt index 47151079e65..abc624514b4 100644 --- a/docs/docsite/rst/shared_snippets/galaxy_server_list.txt +++ b/docs/docsite/rst/shared_snippets/galaxy_server_list.txt @@ -28,7 +28,7 @@ The following example shows how to configure multiple servers: .. code-block:: ini [galaxy] - server_list = automation_hub, my_org_hub, release_galaxy, test_galaxy + server_list = automation_hub, my_org_hub, release_galaxy, test_galaxy, my_galaxy_ng [galaxy_server.automation_hub] url=https://cloud.redhat.com/api/automation-hub/ @@ -48,6 +48,12 @@ The following example shows how to configure multiple servers: url=https://galaxy-dev.ansible.com/ token=my_test_token + [galaxy_server.my_galaxy_ng] + url=http://my_galaxy_ng:8000/api/automation-hub/ + auth_url=http://my_keycloak:8080/auth/realms/myco/protocol/openid-connect/token + client_id=galaxy-ng + token=my_keycloak_access_token + .. note:: You can use the ``--server`` command line argument to select an explicit Galaxy server in the ``server_list`` and the value of this argument should match the name of the server. To use a server not in the server list, set the value to the URL to access that server (all servers in the server list will be ignored). Also you cannot use the ``--api-key`` argument for any of the predefined servers. You can only use the ``api_key`` argument if you did not define a server list or if you specify a URL in the @@ -67,6 +73,7 @@ define the following keys: * ``password``: The password to use, in conjunction with ``username``, for basic authentication. * ``auth_url``: The URL of a Keycloak server 'token_endpoint' if using SSO authentication (for example, Automation Hub). Mutually exclusive with ``username``. Requires ``token``. * ``validate_certs``: Whether or not to verify TLS certificates for the Galaxy server. This defaults to True unless the ``--ignore-certs`` option is provided or ``GALAXY_IGNORE_CERTS`` is configured to True. +* ``client_id``: The Keycloak token's client_id to use for authentication. Requires ``auth_url`` and ``token``. The default ``client_id`` is cloud-services to work with Red Hat SSO. As well as defining these server options in the ``ansible.cfg`` file, you can also define them as environment variables. The environment variable is in the form ``ANSIBLE_GALAXY_SERVER_{{ id }}_{{ key }}`` where ``{{ id }}`` is the upper diff --git a/lib/ansible/cli/galaxy.py b/lib/ansible/cli/galaxy.py index e57db6e7966..cc9a813ef23 100644 --- a/lib/ansible/cli/galaxy.py +++ b/lib/ansible/cli/galaxy.py @@ -62,7 +62,8 @@ SERVER_DEF = [ ('token', False), ('auth_url', False), ('v3', False), - ('validate_certs', False) + ('validate_certs', False), + ('client_id', False), ] @@ -498,6 +499,7 @@ class GalaxyCLI(CLI): # auth_url is used to create the token, but not directly by GalaxyAPI, so # it doesn't need to be passed as kwarg to GalaxyApi auth_url = server_options.pop('auth_url', None) + client_id = server_options.pop('client_id', None) token_val = server_options['token'] or NoTokenSentinel username = server_options['username'] available_api_versions = None @@ -524,7 +526,8 @@ class GalaxyCLI(CLI): if auth_url: server_options['token'] = KeycloakToken(access_token=token_val, auth_url=auth_url, - validate_certs=validate_certs) + validate_certs=validate_certs, + client_id=client_id) else: # The galaxy v1 / github / django / 'Token' server_options['token'] = GalaxyToken(token=token_val) diff --git a/lib/ansible/galaxy/token.py b/lib/ansible/galaxy/token.py index 11a12ce45ad..4455fd01c1f 100644 --- a/lib/ansible/galaxy/token.py +++ b/lib/ansible/galaxy/token.py @@ -50,14 +50,18 @@ class KeycloakToken(object): token_type = 'Bearer' - def __init__(self, access_token=None, auth_url=None, validate_certs=True): + def __init__(self, access_token=None, auth_url=None, validate_certs=True, client_id=None): self.access_token = access_token self.auth_url = auth_url self._token = None self.validate_certs = validate_certs + self.client_id = client_id + if self.client_id is None: + self.client_id = 'cloud-services' def _form_payload(self): - return 'grant_type=refresh_token&client_id=cloud-services&refresh_token=%s' % self.access_token + return 'grant_type=refresh_token&client_id=%s&refresh_token=%s' % (self.client_id, + self.access_token) def get(self): if self._token: diff --git a/test/units/galaxy/test_token.py b/test/units/galaxy/test_token.py index 94449e28e97..13426688e35 100644 --- a/test/units/galaxy/test_token.py +++ b/test/units/galaxy/test_token.py @@ -8,8 +8,10 @@ __metaclass__ = type import os import pytest +from units.compat.mock import MagicMock import ansible.constants as C +from ansible.cli.galaxy import GalaxyCLI, SERVER_DEF from ansible.galaxy.token import GalaxyToken, NoTokenSentinel from ansible.module_utils._text import to_bytes, to_text @@ -32,6 +34,47 @@ def b_token_file(request, tmp_path_factory): C.GALAXY_TOKEN_PATH = orig_token_path +def test_client_id(monkeypatch): + monkeypatch.setattr(C, 'GALAXY_SERVER_LIST', ['server1', 'server2']) + + test_server_config = {option[0]: None for option in SERVER_DEF} + test_server_config.update( + { + 'url': 'http://my_galaxy_ng:8000/api/automation-hub/', + 'auth_url': 'http://my_keycloak:8080/auth/realms/myco/protocol/openid-connect/token', + 'client_id': 'galaxy-ng', + 'token': 'access_token', + } + ) + + test_server_default = {option[0]: None for option in SERVER_DEF} + test_server_default.update( + { + 'url': 'https://cloud.redhat.com/api/automation-hub/', + 'auth_url': 'https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token', + 'token': 'access_token', + } + ) + + get_plugin_options = MagicMock(side_effect=[test_server_config, test_server_default]) + monkeypatch.setattr(C.config, 'get_plugin_options', get_plugin_options) + + cli_args = [ + 'ansible-galaxy', + 'collection', + 'install', + 'namespace.collection:1.0.0', + ] + + galaxy_cli = GalaxyCLI(args=cli_args) + mock_execute_install = MagicMock() + monkeypatch.setattr(galaxy_cli, '_execute_install_collection', mock_execute_install) + galaxy_cli.run() + + assert galaxy_cli.api_servers[0].token.client_id == 'galaxy-ng' + assert galaxy_cli.api_servers[1].token.client_id == 'cloud-services' + + def test_token_explicit(b_token_file): assert GalaxyToken(token="explicit").get() == "explicit"