Skip to content

Commit

Permalink
inventory: add Service Account authentication method
Browse files Browse the repository at this point in the history
Add a new "service_account" authentication method to access Red Hat
Insights using a Service Account rather than username & password.

The new "client_id", "client_secret", and "client_scopes" (optional)
options define the details of the service account, and the new
"oidc_endpoint" option defines which OpenID endpoint provides the OAuth
authentication.

Signed-off-by: Pino Toscano <[email protected]>
  • Loading branch information
ptoscano authored and DuckBoss committed Jun 3, 2024
1 parent 138e61f commit 25a0213
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 1 deletion.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ changes, and bug fixes. Internal changes such as for CI, style/lint, and so on
are purposely not mentioned here.

## [unreleased]
### Added
- `inventory` plugin: new authentication method using a Red Hat service account:
there is a new `authentication` option for choosing the authentication method,
and the `client_id`, `client_secret`, and `client_scopes` options to set the
details of the service account. The default authentication method is still set
to `basic` (i.e. using `user` and `password`) for compatibility.

### Changed
- `inventory` plugin: drop the explicit list of types for the default value
of the `staleness` option, so all the types available in Inventory are
Expand Down
102 changes: 101 additions & 1 deletion plugins/inventory/insights.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
- Uses a YAML configuration file that ends with ``insights.(yml|yaml)``.
extends_documentation_fragment:
- constructed
notes:
- |
A service account used to access Red Hat Insights must be in a group
with at least the `Inventory Hosts Viewer` role; in case any of the
`get_patches`, `get_system_advisories`, and `get_system_packages`
options is enabled, then also the `Patch viewer` role is required.
options:
plugin:
description: >
Expand All @@ -22,7 +28,7 @@
The authentication method used for the Insights Inventory server.
required: false
default: 'basic'
choices: ['basic']
choices: ['basic', 'service_account']
user:
description: >
Red Hat username; required for the 'basic' authentication method.
Expand All @@ -33,6 +39,34 @@
Red Hat password; required for the 'basic' authentication method.
env:
- name: INSIGHTS_PASSWORD
client_id:
description: >
Red Hat service account client ID; required for the 'service_account'
authentication method.
type: str
env:
- name: INSIGHTS_CLIENT_ID
client_secret:
description: >
Red Hat service account client secret; required for the
'service_account' authentication method.
type: str
env:
- name: INSIGHTS_CLIENT_SECRET
client_scopes:
description: >
Red Hat service account client scopes; used by the 'service_account'
authentication method.
default: ['api.console']
type: list
elements: str
env:
- name: INSIGHTS_CLIENT_SCOPES
oidc_endpoint:
description: >
OpenID Connect URL for 'service_account' authentication method.
default: https://sso.redhat.com/auth/realms/redhat-external
type: str
server:
description: Inventory server to connect to
default: https://console.redhat.com
Expand Down Expand Up @@ -92,6 +126,12 @@
user: "insights username"
password: "insights password"
# Authentication using a service account; either specify these keys, or set
# the "INSIGHTS_CLIENT_ID" and "INSIGHTS_CLIENT_SECRET" environment variables
authentication: service_account
client_id: "service account client-id"
client_secret: "service account client-secret"
# Create groups for patching
get_patches: true
groups:
Expand Down Expand Up @@ -122,6 +162,20 @@
REQUESTS_IMPORT_ERROR = None


if not REQUESTS_IMPORT_ERROR:
class BearerAuth(requests.auth.AuthBase):
"""
Simple bearer authentication method for requests.
"""

def __init__(self, token):
self.token = token

def __call__(self, r):
r.headers['Authorization'] = f'Bearer {self.token}'
return r


class InventoryModule(BaseInventoryPlugin, Constructable):
''' Host inventory parser for ansible using foreman as source. '''

Expand All @@ -140,6 +194,47 @@ def ensure_authentication_option(self, option):
(option, self.get_option('authentication'))
)

def get_oauth_token(self):
"""
Contact the OpenID Connect URL to fetch an OAuth 2.0 token using
the OAuth 2.0 Token Endpoint.
"""
session = requests.Session()

headers = {
"Accept": "application/json",
}
url = "%s/.well-known/openid-configuration" % (self.get_option('oidc_endpoint'))
response = session.get(url, headers=headers)
if response.status_code != 200:
raise AnsibleError("http error (%s): %s" %
(response.status_code, response.text))
response_json = response.json()

client_scopes = self.get_option('client_scopes')
scopes_supported = response_json["scopes_supported"]
for client_scope in client_scopes:
if client_scope not in scopes_supported:
raise AnsibleError("client scope '%s' not allowed; allowed: %s" %
(client_scope, scopes_supported))

token_endpoint = response_json["token_endpoint"]
data = {
"client_id": self.get_option('client_id'),
"client_secret": self.get_option('client_secret'),
"grant_type": "client_credentials",
"scope": " ".join(client_scopes),
}
headers = {
"Content-Type": "application/x-www-form-urlencoded",
}
response = session.post(token_endpoint, headers=headers, data=data)
if response.status_code != 200:
raise AnsibleError("http error (%s): %s" %
(response.status_code, response.text))

return response.json()["access_token"]

def create_requests_authentication(self):
"""
Create the authentication object for requests according to the options
Expand All @@ -152,6 +247,11 @@ def create_requests_authentication(self):
return requests.auth.HTTPBasicAuth(
self.get_option('user'), self.get_option('password')
)
elif authentication == 'service_account':
self.ensure_authentication_option('client_id')
self.ensure_authentication_option('client_secret')
self.ensure_authentication_option('client_scopes')
return BearerAuth(self.get_oauth_token())

def get_patches(self, stale, get_system_advisories, get_system_packages, filter_tags):
def get_system_patching_info(system_id, info):
Expand Down

0 comments on commit 25a0213

Please sign in to comment.