summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatt Martz <matt@sivel.net>2025-01-07 19:02:22 +0100
committerGitHub <noreply@github.com>2025-01-07 19:02:22 +0100
commit3a33d8a4c156576d4bef7d8e163ba859f4a73197 (patch)
tree76ce08ebbc72cf5421842ce6c4a70589e5bc8b98
parentFix uri integration test on Python 3.13 (#84518) (diff)
downloadansible-3a33d8a4c156576d4bef7d8e163ba859f4a73197.tar.xz
ansible-3a33d8a4c156576d4bef7d8e163ba859f4a73197.zip
Add Keycloak service account auth capability to ansible-galaxy (#83145)
-rw-r--r--changelogs/fragments/ansible-galaxy-keycloak-service-accounts.yml2
-rwxr-xr-xlib/ansible/cli/galaxy.py21
-rw-r--r--lib/ansible/config/manager.py1
-rw-r--r--lib/ansible/galaxy/token.py36
4 files changed, 39 insertions, 21 deletions
diff --git a/changelogs/fragments/ansible-galaxy-keycloak-service-accounts.yml b/changelogs/fragments/ansible-galaxy-keycloak-service-accounts.yml
new file mode 100644
index 0000000000..2b9a2fb96e
--- /dev/null
+++ b/changelogs/fragments/ansible-galaxy-keycloak-service-accounts.yml
@@ -0,0 +1,2 @@
+minor_changes:
+- ansible-galaxy - Add support for Keycloak service accounts
diff --git a/lib/ansible/cli/galaxy.py b/lib/ansible/cli/galaxy.py
index 5e2bef6f15..76e566f4a5 100755
--- a/lib/ansible/cli/galaxy.py
+++ b/lib/ansible/cli/galaxy.py
@@ -639,6 +639,7 @@ class GalaxyCLI(CLI):
# it doesn't need to be passed as kwarg to GalaxyApi, same for others we pop here
auth_url = server_options.pop('auth_url')
client_id = server_options.pop('client_id')
+ client_secret = server_options.pop('client_secret')
token_val = server_options['token'] or NoTokenSentinel
username = server_options['username']
api_version = server_options.pop('api_version')
@@ -664,15 +665,17 @@ class GalaxyCLI(CLI):
if username:
server_options['token'] = BasicAuthToken(username, server_options['password'])
else:
- if token_val:
- if auth_url:
- server_options['token'] = KeycloakToken(access_token=token_val,
- auth_url=auth_url,
- validate_certs=validate_certs,
- client_id=client_id)
- else:
- # The galaxy v1 / github / django / 'Token'
- server_options['token'] = GalaxyToken(token=token_val)
+ if auth_url:
+ server_options['token'] = KeycloakToken(
+ access_token=token_val,
+ auth_url=auth_url,
+ validate_certs=validate_certs,
+ client_id=client_id,
+ client_secret=client_secret,
+ )
+ elif token_val:
+ # The galaxy v1 / github / django / 'Token'
+ server_options['token'] = GalaxyToken(token=token_val)
server_options.update(galaxy_options)
config_servers.append(GalaxyAPI(
diff --git a/lib/ansible/config/manager.py b/lib/ansible/config/manager.py
index 818219b130..4838ed5944 100644
--- a/lib/ansible/config/manager.py
+++ b/lib/ansible/config/manager.py
@@ -40,6 +40,7 @@ GALAXY_SERVER_DEF = [
('api_version', False, 'int'),
('validate_certs', False, 'bool'),
('client_id', False, 'str'),
+ ('client_secret', False, 'str'),
('timeout', False, 'int'),
]
diff --git a/lib/ansible/galaxy/token.py b/lib/ansible/galaxy/token.py
index 9b82ad6c62..1efc40f987 100644
--- a/lib/ansible/galaxy/token.py
+++ b/lib/ansible/galaxy/token.py
@@ -26,6 +26,7 @@ import os
import time
from stat import S_IRUSR, S_IWUSR
from urllib.error import HTTPError
+from urllib.parse import urlencode
from ansible import constants as C
from ansible.galaxy.api import GalaxyError
@@ -47,7 +48,7 @@ class KeycloakToken(object):
token_type = 'Bearer'
- def __init__(self, access_token=None, auth_url=None, validate_certs=True, client_id=None):
+ def __init__(self, access_token=None, auth_url=None, validate_certs=True, client_id=None, client_secret=None):
self.access_token = access_token
self.auth_url = auth_url
self._token = None
@@ -55,11 +56,26 @@ class KeycloakToken(object):
self.client_id = client_id
if self.client_id is None:
self.client_id = 'cloud-services'
+ self.client_secret = client_secret
self._expiration = None
def _form_payload(self):
- return 'grant_type=refresh_token&client_id=%s&refresh_token=%s' % (self.client_id,
- self.access_token)
+ payload = {
+ 'client_id': self.client_id,
+ }
+ if self.client_secret:
+ payload['client_secret'] = self.client_secret
+ payload['scope'] = 'api.console'
+ payload['grant_type'] = 'client_credentials'
+ if self.access_token:
+ display.warning(
+ 'Found both a client_secret and access_token for galaxy authentication, ignoring access_token'
+ )
+ else:
+ payload['refresh_token'] = self.access_token
+ payload['grant_type'] = 'refresh_token'
+
+ return urlencode(payload)
def get(self):
if self._expiration and time.time() >= self._expiration:
@@ -68,16 +84,9 @@ class KeycloakToken(object):
if self._token:
return self._token
- # - build a request to POST to auth_url
- # - body is form encoded
- # - 'refresh_token' is the offline token stored in ansible.cfg
- # - 'grant_type' is 'refresh_token'
- # - 'client_id' is 'cloud-services'
- # - should probably be based on the contents of the
- # offline_ticket's JWT payload 'aud' (audience)
- # or 'azp' (Authorized party - the party to which the ID Token was issued)
payload = self._form_payload()
+ display.vvv(f'Authenticating via {self.auth_url}')
try:
resp = open_url(to_native(self.auth_url),
data=payload,
@@ -86,15 +95,18 @@ class KeycloakToken(object):
http_agent=user_agent())
except HTTPError as e:
raise GalaxyError(e, 'Unable to get access token')
+ display.vvv('Authentication successful')
data = json.load(resp)
# So that we have a buffer, expire the token in ~2/3 the given value
expires_in = data['expires_in'] // 3 * 2
self._expiration = time.time() + expires_in
+ display.vvv(f'Authentication token expires in {expires_in} seconds')
- # - extract 'access_token'
self._token = data.get('access_token')
+ if token_type := data.get('token_type'):
+ self.token_type = token_type
return self._token