Enable ansible-galaxy to specify client id override with Keycloak Token
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 00000000000000..bf8ae157283bd6
--- /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 47151079e65a15..abc624514b4669 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 e57db6e7966bf7..cc9a813ef23ba0 100644
--- a/lib/ansible/cli/galaxy.py
+++ b/lib/ansible/cli/galaxy.py
@@ -62,7 +62,8 @@
     ('token', False),
     ('auth_url', False),
     ('v3', False),
-    ('validate_certs', False)
+    ('validate_certs', False),
+    ('client_id', False),
 ]
 
 
@@ -498,6 +499,7 @@ def server_config_def(section, key, required):
             # 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 @@ def server_config_def(section, key, required):
                     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 11a12ce45ad9c7..4455fd01c1f24b 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 94449e28e9758d..13426688e35292 100644
--- a/test/units/galaxy/test_token.py
+++ b/test/units/galaxy/test_token.py
@@ -8,8 +8,10 @@
 
 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"
 

* Specify ability to provide override of client_id

##### SUMMARY
GalaxyNG just added the capability to use Keycloak for user integration/authentication, instead of using DRF tokens it would be great to be able to use Keycloak tokens with ansible-galaxy CLI. ansible-galaxy CLI has the ability to use Keycloak tokens today in order to work with Public Automation Hub on console.redhat.com. However, it won't work with GalaxyNG + Keycloak because the client ID in the code for refreshing tokens is hardcoded to cloud-services to work with the Red Hat SSO.

It would be great to be able to supply an override configuration variable for the client ID.

Fixes #75593

##### ISSUE TYPE
- Feature Pull Request

##### COMPONENT NAME
lib/ansible/galaxy

##### ADDITIONAL INFORMATION
As described in the galaxy [download a collection section](https://docs.ansible.com/ansible/latest/galaxy/user_guide.html#downloading-a-collection-from-automation-hub)

Instead of supplying:
```
[galaxy]
server_list = automation_hub

[galaxy_server.automation_hub]
url=https://cloud.redhat.com/api/automation-hub/
auth_url=https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token
token=my_ah_token
```

You could supply the following:
```
[galaxy]
server_list = my_galaxy_ng

[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
```
