Skip to content

Commit

Permalink
Merge pull request #66 from yftacherzog/RHTAPWATCH-825-use-oidc-auth
Browse files Browse the repository at this point in the history
chore(RHTAPWATCH-825): authenticate with oidc
  • Loading branch information
yftacherzog authored Feb 12, 2024
2 parents 10e0563 + a2e555a commit e64b684
Show file tree
Hide file tree
Showing 9 changed files with 499 additions and 401 deletions.
13 changes: 5 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,14 @@ Within the container, all the tools will be available in `$PATH`.

### Checking for ODCS connectivity:

You will need to obtain a keytab file with credentials for an account that
can use ODCS. The `odcs_ping` tool can then be used to check the connectivity.
You will need to obtain an OIDC service account that can use ODCS. The `odcs_ping` tool
can then be used to check the connectivity.
```
podman run -it --rm \
-e KRB5CCNAME=/tmp/foo \
-e KRB5_CLIENT_KTNAME=/tmp/kt \
-v $KT_PATH:/tmp/kt:Z \
-e CLIENT_ID=<client_id> \
-e CLIENT_SECRET=<client-secret> \
appstudio-tools odcs_ping
```
(Here we assume the path to a local copy of an appropriate keytab file was
stored in the `$KT_PATH` variable).

If everything goes well, the output will resemble the following (Some
parts omitted for brevity):
Expand All @@ -45,5 +42,5 @@ Allowed groups:
Attempting to generate a compose
Compose done:
repo file at: http://.../odcs-1234567.repo
owner: your-keytab-user-here
owner: your-oidc-user-here
```
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ types-requests = "*"
odcs = {extras = ["client"], version = "*"}
appstudio-tools = {file = ".", editable = true}
pylint-pytest = "*"
authlib = "*"

[dev-packages]
pytest = "*"
Expand Down
729 changes: 350 additions & 379 deletions Pipfile.lock

Large diffs are not rendered by default.

16 changes: 15 additions & 1 deletion generate_compose/odcs_compose_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,23 @@
type=click.Path(path_type=Path),
required=True,
)
@click.option(
"--client-id",
help="Client ID used for authenticating with ODCS",
type=click.STRING,
envvar="CLIENT_ID",
)
@click.option(
"--client-secret",
help="Client secret used for generating OIDC token",
type=click.STRING,
envvar="CLIENT_SECRET",
)
def main(
compose_dir_path: Path,
compose_input_yaml_path: Path,
client_id: str,
client_secret: str,
):
"""
Get inputs from container and content_sets YAMLs and relay them to an ODCS
Expand All @@ -45,7 +59,7 @@ def main(
configurations_generator=ODCSConfigurationsGenerator(
compose_inputs=compose_inputs,
),
requester=ODCSRequester(),
requester=ODCSRequester(client_id=client_id, client_secret=client_secret),
fetcher=ODCSFetcher(compose_dir_path=compose_dir_path),
)
compose_generator()
Expand Down
23 changes: 20 additions & 3 deletions generate_compose/odcs_ping.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

import click
import requests
from odcs.client.odcs import ODCS, AuthMech # type: ignore

from .odcs_session import get_odcs_session


def check_about(odcs):
Expand Down Expand Up @@ -56,9 +57,25 @@ def check_new_compose(odcs):
default="https://odcs.engineering.redhat.com",
metavar="URL",
)
def main(server):
@click.option(
"--client-id",
help="Client ID used for authenticating with ODCS",
type=click.STRING,
envvar="CLIENT_ID",
)
@click.option(
"--client-secret",
help="Client secret used for generating OIDC token",
type=click.STRING,
envvar="CLIENT_SECRET",
)
def main(server: str, client_id: str, client_secret: str):
"""Check connectivity to ODCS"""
odcs = ODCS(server, auth_mech=AuthMech.Kerberos)
odcs = get_odcs_session(
client_id=client_id,
client_secret=client_secret,
odcs_server=server,
)

check_about(odcs)
print()
Expand Down
19 changes: 13 additions & 6 deletions generate_compose/odcs_requester.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"""Request a new ODCS compose"""

from dataclasses import dataclass
from typing import Callable

from odcs.client.odcs import ODCS, AuthMech # type: ignore
from odcs.client.odcs import ODCS # type: ignore

from .odcs_configurations_generator import ODCSComposesConfigs
from .odcs_session import get_odcs_session
from .protocols import ComposeRequester, ODCSRequestReferences


Expand All @@ -15,18 +17,23 @@ class ODCSRequester(ComposeRequester):
to the remote compose location.
"""

odcs: ODCS = ODCS(
"https://odcs.engineering.redhat.com/", auth_mech=AuthMech.Kerberos
)
client_id: str
client_secret: str
odcs_session_getter: Callable[[str, str, str], ODCS] = get_odcs_session

def __call__(self, compose_configs: ODCSComposesConfigs) -> ODCSRequestReferences:
odcs: ODCS = self.odcs_session_getter(
self.client_id,
self.client_secret,
"https://odcs.engineering.redhat.com/",
)
composes = [
self.odcs.request_compose(config.spec, **config.additional_args)
odcs.request_compose(config.spec, **config.additional_args)
for config in compose_configs.configs
]

finished_composes = [
self.odcs.wait_for_compose(compose["id"]) for compose in composes
odcs.wait_for_compose(compose["id"]) for compose in composes
]

failed_composes = [
Expand Down
32 changes: 32 additions & 0 deletions generate_compose/odcs_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Authenticate with ODCS using OIDC"""

from typing import Callable

from authlib.integrations.requests_client import OAuth2Session # type: ignore
from odcs.client.odcs import ODCS, AuthMech # type: ignore


def get_odcs_session(
client_id: str,
client_secret: str,
odcs_server: str = "https://odcs.engineering.redhat.com",
oidc_token_url: str = (
"https://auth.redhat.com/auth/realms/EmployeeIDP/protocol/openid-connect/token"
),
session_fetcher: Callable[[str, str, str, str], OAuth2Session] = OAuth2Session,
) -> ODCS:
"""Authenticate using OIDC and return an authenticated ODCS client"""
oidc_client = session_fetcher(
client_id, client_secret, "client_secret_basic", "openid"
)

try:
token = oidc_client.fetch_token(
url=oidc_token_url, grant_type="client_credentials"
)
except Exception as ex:
raise RuntimeError("Failed fetching OIDC token") from ex

return ODCS(
odcs_server, auth_mech=AuthMech.OpenIDC, openidc_token=token["access_token"]
)
29 changes: 25 additions & 4 deletions tests/test_odcs_requester.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from requests.exceptions import HTTPError

from generate_compose.odcs_requester import ODCSRequester
from generate_compose.odcs_session import get_odcs_session
from generate_compose.protocols import (
ODCSComposeConfig,
ODCSComposesConfigs,
Expand Down Expand Up @@ -96,7 +97,12 @@ def test_odcs_requester(
"""test ODCSRequester.__call__"""
num_of_composes: int = len(composes_configs.configs)
mock_odcs = create_odcs_mock(num_of_composes)
odcs_requester = ODCSRequester(odcs=mock_odcs)
odcs_session_getter = create_autospec(get_odcs_session, return_value=mock_odcs)
odcs_requester = ODCSRequester(
client_id="some-client",
client_secret="some-secret",
odcs_session_getter=odcs_session_getter,
)
req_ref = odcs_requester(compose_configs=composes_configs)

expected_calls = [
Expand All @@ -121,7 +127,12 @@ def test_odcs_requester_compose_failure_should_raise(
) -> None:
"""test ODCSRequester.__call__ raise an exception when the compose fails"""
mock_odcs = create_odcs_mock(exception=True)
odcs_requester = ODCSRequester(odcs=mock_odcs)
odcs_session_getter = create_autospec(get_odcs_session, return_value=mock_odcs)
odcs_requester = ODCSRequester(
client_id="some-client",
client_secret="some-secret",
odcs_session_getter=odcs_session_getter,
)
composes_config = ODCSComposesConfigs([ODCSComposeConfig(spec=compose_source)])
with pytest.raises(HTTPError):
odcs_requester(compose_configs=composes_config)
Expand All @@ -136,7 +147,12 @@ def test_odcs_requester_compose_timeout_failure_should_raise(
mock_odcs = create_odcs_mock(exception=False)
mock_odcs.wait_for_compose.side_effect = RuntimeError

odcs_requester = ODCSRequester(odcs=mock_odcs)
odcs_session_getter = create_autospec(get_odcs_session, return_value=mock_odcs)
odcs_requester = ODCSRequester(
client_id="some-client",
client_secret="some-secret",
odcs_session_getter=odcs_session_getter,
)
composes_config = ODCSComposesConfigs([ODCSComposeConfig(spec=compose_source)])

with pytest.raises(RuntimeError):
Expand Down Expand Up @@ -205,7 +221,12 @@ def test_odcs_requester_failed_generating(
successfully"""

mock_odcs = create_odcs_mock(num_of_composes=2, num_of_failed=num_of_failed)
odcs_requester = ODCSRequester(odcs=mock_odcs)
odcs_session_getter = create_autospec(get_odcs_session, return_value=mock_odcs)
odcs_requester = ODCSRequester(
client_id="some-client",
client_secret="some-secret",
odcs_session_getter=odcs_session_getter,
)

with pytest.raises(RuntimeError) as ex:
odcs_requester(compose_configs=composes_configs)
Expand Down
38 changes: 38 additions & 0 deletions tests/test_odcs_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Test odcs_session"""

from unittest.mock import create_autospec, sentinel

import pytest
from authlib.integrations.requests_client import OAuth2Session # type: ignore

from generate_compose.odcs_session import get_odcs_session


class TestODCSSession:
"""Test odcs_session.py"""

def test_get_odcs_session(self) -> None:
"""test get_odcs_session"""
mock = create_autospec(OAuth2Session)
mock.return_value.fetch_token.return_value = {"access_token": sentinel.token}
odcs = get_odcs_session(
client_id="some-client",
client_secret="some-secret",
odcs_server=sentinel.url,
session_fetcher=mock,
)
assert odcs.server_url == sentinel.url
assert odcs._openidc_token == sentinel.token # pylint: disable=protected-access

def test_get_odcs_session_fail_fetching_token(self) -> None:
"""test get_odcs_session fails with proper message"""
mock = create_autospec(OAuth2Session)
mock.return_value.fetch_token.side_effect = Exception
with pytest.raises(RuntimeError) as ex:
get_odcs_session(
client_id="some-client",
client_secret="some-secret",
odcs_server=sentinel.url,
session_fetcher=mock,
)
assert str(ex.value) == "Failed fetching OIDC token"

0 comments on commit e64b684

Please sign in to comment.