From 893b3c15be49bb775eb58887acc5fe263e4595f2 Mon Sep 17 00:00:00 2001 From: averevki Date: Tue, 20 Feb 2024 22:57:06 +0100 Subject: [PATCH 1/2] Add service account object --- testsuite/openshift/service_account.py | 31 ++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 testsuite/openshift/service_account.py diff --git a/testsuite/openshift/service_account.py b/testsuite/openshift/service_account.py new file mode 100644 index 00000000..c2c7fe37 --- /dev/null +++ b/testsuite/openshift/service_account.py @@ -0,0 +1,31 @@ +"""Service Account object for OpenShift""" + +from testsuite.openshift import OpenShiftObject +from testsuite.openshift.client import OpenShiftClient + + +class ServiceAccount(OpenShiftObject): + """Service account object for OpenShift""" + + def __init__(self, openshift: OpenShiftClient, model: dict): + self.openshift = openshift + super().__init__(model, context=openshift.context) + + @classmethod + def create_instance(cls, openshift: OpenShiftClient, name: str, labels: dict[str, str] = None): + """Creates new instance of service account""" + model = { + "kind": "ServiceAccount", + "apiVersion": "v1", + "metadata": { + "name": name, + "labels": labels, + }, + } + + return cls(openshift, model) + + def get_auth_token(self, audiences: list[str] = None) -> str: + """Requests and returns bound token for service account""" + audiences_args = [f"--audience={a}" for a in audiences or []] + return self.openshift.do_action("create", "token", self.name(), *audiences_args).out().strip() From 74ae472d198bc17b5d9b33ebfd114348d3477e49 Mon Sep 17 00:00:00 2001 From: averevki Date: Tue, 20 Feb 2024 22:57:24 +0100 Subject: [PATCH 2/2] Add kubernetes token-review identity tests --- testsuite/httpx/auth.py | 3 +- testsuite/policy/authorization/sections.py | 4 +-- .../identity/token_review/__init__.py | 0 .../identity/token_review/conftest.py | 32 +++++++++++++++++++ .../identity/token_review/test_audiences.py | 30 +++++++++++++++++ .../identity/token_review/test_host.py | 27 ++++++++++++++++ 6 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 testsuite/tests/kuadrant/authorino/identity/token_review/__init__.py create mode 100644 testsuite/tests/kuadrant/authorino/identity/token_review/conftest.py create mode 100644 testsuite/tests/kuadrant/authorino/identity/token_review/test_audiences.py create mode 100644 testsuite/tests/kuadrant/authorino/identity/token_review/test_host.py diff --git a/testsuite/httpx/auth.py b/testsuite/httpx/auth.py index 622778df..a09875a4 100644 --- a/testsuite/httpx/auth.py +++ b/testsuite/httpx/auth.py @@ -6,7 +6,6 @@ from httpx import Auth, Request, URL, Response from testsuite.oidc.rhsso import User -from testsuite.openshift.api_key import APIKey from testsuite.oidc import Token TokenType = Union[Token, Callable[[], Token]] @@ -57,7 +56,7 @@ def auth_flow(self, request: Request) -> Generator[Request, Response, None]: class HeaderApiKeyAuth(Auth): """Auth class for authentication with API key""" - def __init__(self, api_key: APIKey, prefix: str = "APIKEY") -> None: + def __init__(self, api_key, prefix: str = "APIKEY") -> None: super().__init__() self.api_key = str(api_key) self.prefix = prefix diff --git a/testsuite/policy/authorization/sections.py b/testsuite/policy/authorization/sections.py index ebc34a9f..c5fcd6fb 100644 --- a/testsuite/policy/authorization/sections.py +++ b/testsuite/policy/authorization/sections.py @@ -115,13 +115,13 @@ def add_mtls(self, name: str, selector: Selector, **common_features): self.add_item(name, {"x509": {"selector": asdict(selector)}, **common_features}) @modify - def add_kubernetes(self, name: str, audiences: list[str], **common_features): + def add_kubernetes(self, name: str, audiences: list[str] = None, **common_features): """Adds Kubernetes identity Args: :param name: name of the identity :param audiences: token audiences """ - self.add_item(name, {"kubernetesTokenReview": {"audiences": audiences}}, **common_features) + self.add_item(name, {"kubernetesTokenReview": {"audiences": audiences} if audiences else {}}, **common_features) @modify def add_oidc(self, name, endpoint, *, ttl: int = 0, credentials: Credentials = None, **common_features): diff --git a/testsuite/tests/kuadrant/authorino/identity/token_review/__init__.py b/testsuite/tests/kuadrant/authorino/identity/token_review/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/testsuite/tests/kuadrant/authorino/identity/token_review/conftest.py b/testsuite/tests/kuadrant/authorino/identity/token_review/conftest.py new file mode 100644 index 00000000..2c71b369 --- /dev/null +++ b/testsuite/tests/kuadrant/authorino/identity/token_review/conftest.py @@ -0,0 +1,32 @@ +"""Conftest for kubernetes token-review tests""" + +import pytest + +from testsuite.httpx.auth import HeaderApiKeyAuth +from testsuite.openshift.service_account import ServiceAccount + + +@pytest.fixture(scope="module") +def create_service_account(request, openshift, blame, module_label): + """Creates and returns service account""" + + def _create_service_account(name): + service_account = ServiceAccount.create_instance(openshift, blame(name), labels={"app": module_label}) + request.addfinalizer(service_account.delete) + service_account.commit() + return service_account + + return _create_service_account + + +@pytest.fixture(scope="module") +def service_account_token(create_service_account, audience): + """Create service account and request its bound token with the hostname as audience""" + service_account = create_service_account("tkn-rev") + return service_account.get_auth_token(audience) + + +@pytest.fixture(scope="module") +def auth(service_account_token): + """Create request auth with service account token as API key""" + return HeaderApiKeyAuth(service_account_token, "Bearer") diff --git a/testsuite/tests/kuadrant/authorino/identity/token_review/test_audiences.py b/testsuite/tests/kuadrant/authorino/identity/token_review/test_audiences.py new file mode 100644 index 00000000..2cc2ef1d --- /dev/null +++ b/testsuite/tests/kuadrant/authorino/identity/token_review/test_audiences.py @@ -0,0 +1,30 @@ +"""Test kubernetes token-review authorization with bound sa token that should contain all specified audiences""" + +import pytest + +pytestmark = [pytest.mark.authorino] + + +TEST_AUDIENCES = ["test-aud1", "test-aud2", "test-aud3"] + + +@pytest.fixture(scope="module") +def authorization(authorization): + """Add kubernetes token-review identity with custom audiences specified""" + authorization.identity.add_kubernetes("token-review-aud", TEST_AUDIENCES) + return authorization + + +@pytest.fixture(scope="module") +def audience(): + """Return custom audiences for the service account bound token""" + return TEST_AUDIENCES + + +def test_custom_audience(client, auth): + """Test kubernetes token-review by adding custom audiences to the sa token and using it for the request""" + response = client.get("/get") + assert response.status_code == 401 + + response = client.get("/get", auth=auth) + assert response.status_code == 200 diff --git a/testsuite/tests/kuadrant/authorino/identity/token_review/test_host.py b/testsuite/tests/kuadrant/authorino/identity/token_review/test_host.py new file mode 100644 index 00000000..bdd776eb --- /dev/null +++ b/testsuite/tests/kuadrant/authorino/identity/token_review/test_host.py @@ -0,0 +1,27 @@ +"""Test kubernetes token-review authorization with bound sa token that should contain host as audience by default""" + +import pytest + +pytestmark = [pytest.mark.authorino] + + +@pytest.fixture(scope="module") +def authorization(authorization): + """Add kubernetes token-review identity without any audiences""" + authorization.identity.add_kubernetes("token-review-host") + return authorization + + +@pytest.fixture(scope="module") +def audience(hostname): + """Return hostname as only audience for the service account bound token""" + return [hostname.hostname] + + +def test_host_audience(client, auth): + """Test kubernetes token-review by adding hostname audience to the sa token and using it for the request""" + response = client.get("/get") + assert response.status_code == 401 + + response = client.get("/get", auth=auth) + assert response.status_code == 200