|
15 | 15 | import time |
16 | 16 | import zipfile |
17 | 17 | from typing import Optional |
| 18 | +from urllib.parse import urlparse |
18 | 19 | from uuid import UUID, uuid4 |
19 | 20 |
|
20 | 21 | import requests |
21 | 22 | from ducktape.cluster.cluster import ClusterNode |
22 | 23 | from ducktape.mark import matrix |
23 | 24 | from ducktape.services.service import Service |
24 | 25 | from ducktape.utils.util import wait_until |
| 26 | +from keycloak import KeycloakOpenID |
25 | 27 |
|
26 | 28 | from rptest.clients.rpk import RpkTool |
27 | 29 | from rptest.services.admin import ( |
|
31 | 33 | DebugBundleStartConfigParams, |
32 | 34 | ) |
33 | 35 | from rptest.services.cluster import cluster |
| 36 | +from rptest.services.keycloak import DEFAULT_REALM, KeycloakService |
34 | 37 | from rptest.services.redpanda import ( |
35 | 38 | LoggingConfig, |
36 | 39 | MetricSamples, |
|
39 | 42 | SecurityConfig, |
40 | 43 | TLSProvider, |
41 | 44 | ) |
42 | | -from rptest.services.redpanda_types import SaslCredentials |
| 45 | +from rptest.services.redpanda_types import OAuthBearerCredentials, SaslCredentials |
43 | 46 | from rptest.services.tls import Certificate, CertificateAuthority, TLSCertManager |
44 | 47 | from rptest.tests.redpanda_test import RedpandaTest |
45 | 48 | from rptest.util import wait_until_result |
46 | 49 |
|
| 50 | +_OIDC_TOKEN_AUDIENCE = "account" |
| 51 | + |
47 | 52 |
|
48 | 53 | class DebugBundleErrorCode: |
49 | 54 | SUCCESS = 0 |
@@ -734,3 +739,98 @@ def test_validate_tls(self): |
734 | 739 | ) |
735 | 740 | content = self._retrieve_file(node=node) |
736 | 741 | self._validate_topic_name_in_response(content, self.topic_name, True) |
| 742 | + |
| 743 | + |
| 744 | +class DebugBundleOAuthBearerAuthn(DebugBundleTestBase): |
| 745 | + """ |
| 746 | + End-to-end test verifying OAUTHBEARER authentication for the debug bundle. |
| 747 | +
|
| 748 | + The broker receives {mechanism: OAUTHBEARER, token: JWT} in the start |
| 749 | + request and forwards it as -Xsasl.mechanism=OAUTHBEARER and |
| 750 | + -Xpass=token:<JWT> to the rpk subprocess. The subprocess uses those |
| 751 | + credentials to authenticate to Kafka, producing a successful bundle. |
| 752 | + """ |
| 753 | + |
| 754 | + _CLIENT_ID = "debug-bundle-test" |
| 755 | + _TOPIC = "oauthbearer_test_topic" |
| 756 | + |
| 757 | + def __init__(self, context): |
| 758 | + # Allocate the Keycloak node now so we can derive its hostname for |
| 759 | + # oidc_discovery_url before creating the Redpanda service. The node |
| 760 | + # is not started until setUp() so that self.logger is available for |
| 761 | + # error handling by then. |
| 762 | + self.keycloak = KeycloakService(context) |
| 763 | + kc_node = self.keycloak.nodes[0] |
| 764 | + kc_discovery_url = self.keycloak.get_discovery_url(kc_node) |
| 765 | + |
| 766 | + security = SecurityConfig() |
| 767 | + security.enable_sasl = True |
| 768 | + security.sasl_mechanisms = ["SCRAM", "OAUTHBEARER"] |
| 769 | + security.kafka_enable_authorization = False |
| 770 | + security.endpoint_authn_method = "sasl" |
| 771 | + |
| 772 | + super().__init__( |
| 773 | + context, |
| 774 | + num_brokers=1, |
| 775 | + security=security, |
| 776 | + extra_rp_conf={ |
| 777 | + "oidc_discovery_url": kc_discovery_url, |
| 778 | + "oidc_token_audience": _OIDC_TOKEN_AUDIENCE, |
| 779 | + }, |
| 780 | + ) |
| 781 | + |
| 782 | + self.super_admin = Admin(self.redpanda) |
| 783 | + self.rpk = RpkTool( |
| 784 | + self.redpanda, |
| 785 | + username=self.redpanda.SUPERUSER_CREDENTIALS.username, |
| 786 | + password=self.redpanda.SUPERUSER_CREDENTIALS.password, |
| 787 | + sasl_mechanism=self.redpanda.SUPERUSER_CREDENTIALS.algorithm, |
| 788 | + ) |
| 789 | + |
| 790 | + def setUp(self): |
| 791 | + kc_node = self.keycloak.nodes[0] |
| 792 | + try: |
| 793 | + self.keycloak.start_node(kc_node) |
| 794 | + except Exception as e: |
| 795 | + self.logger.error(f"Keycloak failed to start: {e}") |
| 796 | + self.keycloak.clean_node(kc_node) |
| 797 | + raise |
| 798 | + |
| 799 | + super().setUp() |
| 800 | + |
| 801 | + self.keycloak.admin.create_client(self._CLIENT_ID) |
| 802 | + self.oauth_cfg = self.keycloak.generate_oauth_config(kc_node, self._CLIENT_ID) |
| 803 | + self.rpk.create_topic(self._TOPIC) |
| 804 | + |
| 805 | + def _get_access_token(self) -> str: |
| 806 | + cfg = self.oauth_cfg |
| 807 | + parsed = urlparse(cfg.token_endpoint) |
| 808 | + openid = KeycloakOpenID( |
| 809 | + server_url=f"{parsed.scheme}://{parsed.netloc}", |
| 810 | + client_id=cfg.client_id, |
| 811 | + client_secret_key=cfg.client_secret, |
| 812 | + realm_name=DEFAULT_REALM, |
| 813 | + verify=False, |
| 814 | + ) |
| 815 | + return openid.token(grant_type="client_credentials")["access_token"] |
| 816 | + |
| 817 | + @cluster(num_nodes=2) |
| 818 | + def test_oauthbearer_auth(self): |
| 819 | + """ |
| 820 | + Verify that the broker correctly forwards the bearer token to the rpk |
| 821 | + subprocess, which uses OAUTHBEARER to authenticate to Kafka and produce |
| 822 | + a successful debug bundle containing the expected topic metadata. |
| 823 | + """ |
| 824 | + node = self.redpanda.started_nodes()[0] |
| 825 | + token = self._get_access_token() |
| 826 | + |
| 827 | + job_id = uuid4() |
| 828 | + config = DebugBundleStartConfigParams( |
| 829 | + authentication=OAuthBearerCredentials(token=token) |
| 830 | + ) |
| 831 | + self._run_debug_bundle( |
| 832 | + job_id=job_id, node=node, config=config, admin=self.super_admin |
| 833 | + ) |
| 834 | + |
| 835 | + content = self._retrieve_file(node=node, admin=self.super_admin) |
| 836 | + self._validate_topic_name_in_response(content, self._TOPIC, True) |
0 commit comments