|
| 1 | +import secrets |
| 2 | + |
| 3 | +import pytest |
| 4 | +import requests |
| 5 | +import yaml |
| 6 | +from playwright.sync_api import Error, sync_playwright |
| 7 | +from settings import DEPLOYMENTS, TEST_DATA |
| 8 | +from suite.utils.policy_resources_utils import delete_policy |
| 9 | +from suite.utils.resources_utils import ( |
| 10 | + create_example_app, |
| 11 | + create_items_from_yaml, |
| 12 | + create_secret, |
| 13 | + create_secret_from_yaml, |
| 14 | + delete_common_app, |
| 15 | + delete_secret, |
| 16 | + replace_configmap_from_yaml, |
| 17 | + wait_before_test, |
| 18 | + wait_until_all_pods_are_ready, |
| 19 | +) |
| 20 | +from suite.utils.vs_vsr_resources_utils import ( |
| 21 | + create_virtual_server_from_yaml, |
| 22 | + delete_virtual_server, |
| 23 | + patch_virtual_server_from_yaml, |
| 24 | +) |
| 25 | + |
| 26 | +username = "nginx-user-" + secrets.token_hex(4) |
| 27 | +password = secrets.token_hex(8) |
| 28 | +keycloak_src = f"{TEST_DATA}/oidc/keycloak.yaml" |
| 29 | +keycloak_vs_src = f"{TEST_DATA}/oidc/virtual-server-idp.yaml" |
| 30 | +oidc_secret_src = f"{TEST_DATA}/oidc/client-secret.yaml" |
| 31 | +pkce_pol_src = f"{TEST_DATA}/oidc/pkce.yaml" |
| 32 | +oidc_vs_src = f"{TEST_DATA}/oidc/virtual-server.yaml" |
| 33 | +orig_vs_src = f"{TEST_DATA}/virtual-server-tls/standard/virtual-server.yaml" |
| 34 | +cm_src = f"{TEST_DATA}/oidc/nginx-config.yaml" |
| 35 | +cm_zs_src = f"{TEST_DATA}/oidc/nginx-config-zs.yaml" |
| 36 | +orig_cm_src = f"{DEPLOYMENTS}/common/nginx-config.yaml" |
| 37 | +svc_src = f"{TEST_DATA}/oidc/nginx-ingress-headless.yaml" |
| 38 | + |
| 39 | + |
| 40 | +class KeycloakSetupPKCE: |
| 41 | + """ |
| 42 | + Attributes: |
| 43 | + secret (str): |
| 44 | + """ |
| 45 | + |
| 46 | + def __init__(self, secret): |
| 47 | + self.secret = secret |
| 48 | + |
| 49 | + |
| 50 | +@pytest.fixture(scope="class") |
| 51 | +def keycloak_setup(request, kube_apis, test_namespace, ingress_controller_endpoint, virtual_server_setup): |
| 52 | + |
| 53 | + # Create Keycloak resources and setup Keycloak idp with PKCE enabled |
| 54 | + secret_name = create_secret_from_yaml( |
| 55 | + kube_apis.v1, virtual_server_setup.namespace, f"{TEST_DATA}/virtual-server-tls/tls-secret.yaml" |
| 56 | + ) |
| 57 | + keycloak_address = "keycloak.example.com" |
| 58 | + create_example_app(kube_apis, "keycloak", test_namespace) |
| 59 | + wait_before_test() |
| 60 | + wait_until_all_pods_are_ready(kube_apis.v1, test_namespace) |
| 61 | + keycloak_vs_name = create_virtual_server_from_yaml(kube_apis.custom_objects, keycloak_vs_src, test_namespace) |
| 62 | + wait_before_test() |
| 63 | + |
| 64 | + # Get token |
| 65 | + url = f"https://{ingress_controller_endpoint.public_ip}:{ingress_controller_endpoint.port_ssl}/realms/master/protocol/openid-connect/token" |
| 66 | + headers = {"Host": keycloak_address, "Content-Type": "application/x-www-form-urlencoded"} |
| 67 | + data = {"username": "admin", "password": "admin", "grant_type": "password", "client_id": "admin-cli"} |
| 68 | + |
| 69 | + response = requests.post(url, headers=headers, data=data, verify=False) |
| 70 | + token = response.json()["access_token"] |
| 71 | + |
| 72 | + # Create a user and set credentials |
| 73 | + create_user_url = f"https://{ingress_controller_endpoint.public_ip}:{ingress_controller_endpoint.port_ssl}/admin/realms/master/users" |
| 74 | + headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}", "Host": keycloak_address} |
| 75 | + user_payload = { |
| 76 | + "username": username, |
| 77 | + "enabled": True, |
| 78 | + "credentials": [{"type": "password", "value": password, "temporary": False}], |
| 79 | + } |
| 80 | + response = requests.post(create_user_url, headers=headers, json=user_payload, verify=False) |
| 81 | + |
| 82 | + # Create client "nginx-plus" |
| 83 | + create_client_url = f"https://{ingress_controller_endpoint.public_ip}:{ingress_controller_endpoint.port_ssl}/admin/realms/master/clients" |
| 84 | + client_payload = { |
| 85 | + "clientId": "nginx-plus-pkce", |
| 86 | + "redirectUris": ["https://virtual-server-tls.example.com:443/_codexch"], |
| 87 | + "standardFlowEnabled": True, |
| 88 | + "directAccessGrantsEnabled": False, |
| 89 | + "publicClient": True, |
| 90 | + "attributes": { |
| 91 | + "post.logout.redirect.uris": "https://virtual-server-tls.example.com:443/*", |
| 92 | + "pkce.code.challenge.method": "S256", |
| 93 | + }, |
| 94 | + "protocol": "openid-connect", |
| 95 | + } |
| 96 | + client_resp = requests.post(create_client_url, headers=headers, json=client_payload, verify=False) |
| 97 | + client_resp.raise_for_status() |
| 98 | + # let's check that it's a 201 somehow |
| 99 | + |
| 100 | + print(f"Keycloak setup complete.") |
| 101 | + |
| 102 | + def fin(): |
| 103 | + if request.config.getoption("--skip-fixture-teardown") == "no": |
| 104 | + print("Delete Keycloak resources") |
| 105 | + delete_virtual_server(kube_apis.custom_objects, keycloak_vs_name, test_namespace) |
| 106 | + delete_common_app(kube_apis, "keycloak", test_namespace) |
| 107 | + delete_secret(kube_apis.v1, secret_name, test_namespace) |
| 108 | + |
| 109 | + request.addfinalizer(fin) |
| 110 | + |
| 111 | + # base64 encoded "pkce-doesnt-support-client-secrets" |
| 112 | + return KeycloakSetupPKCE("cGtjZS1kb2VzbnQtc3VwcG9ydC1jbGllbnQtc2VjcmV0cw==") |
| 113 | + |
| 114 | + |
| 115 | +@pytest.mark.oidc |
| 116 | +@pytest.mark.skip_for_nginx_oss |
| 117 | +@pytest.mark.parametrize( |
| 118 | + "crd_ingress_controller, virtual_server_setup", |
| 119 | + [ |
| 120 | + ( |
| 121 | + { |
| 122 | + "type": "complete", |
| 123 | + "extra_args": [ |
| 124 | + f"-enable-oidc", |
| 125 | + ], |
| 126 | + }, |
| 127 | + {"example": "virtual-server-tls", "app_type": "simple"}, |
| 128 | + ) |
| 129 | + ], |
| 130 | + indirect=True, |
| 131 | +) |
| 132 | +class TestPKCE: |
| 133 | + @pytest.mark.parametrize("configmap", [cm_src, cm_zs_src]) |
| 134 | + def test_pkce( |
| 135 | + self, |
| 136 | + request, |
| 137 | + kube_apis, |
| 138 | + ingress_controller_endpoint, |
| 139 | + ingress_controller_prerequisites, |
| 140 | + crd_ingress_controller, |
| 141 | + test_namespace, |
| 142 | + virtual_server_setup, |
| 143 | + keycloak_setup, |
| 144 | + configmap, |
| 145 | + ): |
| 146 | + |
| 147 | + print(f"Create oidc secret") |
| 148 | + with open(oidc_secret_src) as f: |
| 149 | + secret_data = yaml.safe_load(f) |
| 150 | + secret_data["data"]["client-secret"] = keycloak_setup.secret |
| 151 | + secret_name = create_secret(kube_apis.v1, test_namespace, secret_data) |
| 152 | + |
| 153 | + print(f"Create oidc policy") |
| 154 | + with open(pkce_pol_src) as f: |
| 155 | + doc = yaml.safe_load(f) |
| 156 | + pol = doc["metadata"]["name"] |
| 157 | + doc["spec"]["oidc"]["tokenEndpoint"] = doc["spec"]["oidc"]["tokenEndpoint"].replace("default", test_namespace) |
| 158 | + doc["spec"]["oidc"]["jwksURI"] = doc["spec"]["oidc"]["jwksURI"].replace("default", test_namespace) |
| 159 | + kube_apis.custom_objects.create_namespaced_custom_object("k8s.nginx.org", "v1", test_namespace, "policies", doc) |
| 160 | + print(f"Policy created with name {pol}") |
| 161 | + wait_before_test() |
| 162 | + |
| 163 | + print(f"Create virtual server") |
| 164 | + patch_virtual_server_from_yaml( |
| 165 | + kube_apis.custom_objects, virtual_server_setup.vs_name, oidc_vs_src, test_namespace |
| 166 | + ) |
| 167 | + wait_before_test() |
| 168 | + print(f"Update nginx configmap") |
| 169 | + replace_configmap_from_yaml( |
| 170 | + kube_apis.v1, |
| 171 | + ingress_controller_prerequisites.config_map["metadata"]["name"], |
| 172 | + ingress_controller_prerequisites.namespace, |
| 173 | + configmap, |
| 174 | + ) |
| 175 | + wait_before_test() |
| 176 | + |
| 177 | + if configmap == cm_src: |
| 178 | + print(f"Create headless service") |
| 179 | + create_items_from_yaml(kube_apis, svc_src, ingress_controller_prerequisites.namespace) |
| 180 | + |
| 181 | + with sync_playwright() as playwright: |
| 182 | + run_oidc(playwright.chromium, ingress_controller_endpoint.public_ip, ingress_controller_endpoint.port_ssl) |
| 183 | + |
| 184 | + replace_configmap_from_yaml( |
| 185 | + kube_apis.v1, |
| 186 | + ingress_controller_prerequisites.config_map["metadata"]["name"], |
| 187 | + ingress_controller_prerequisites.namespace, |
| 188 | + cm_src, |
| 189 | + ) |
| 190 | + delete_secret(kube_apis.v1, secret_name, test_namespace) |
| 191 | + delete_policy(kube_apis.custom_objects, pol, test_namespace) |
| 192 | + patch_virtual_server_from_yaml( |
| 193 | + kube_apis.custom_objects, virtual_server_setup.vs_name, orig_vs_src, test_namespace |
| 194 | + ) |
| 195 | + |
| 196 | + |
| 197 | +def run_oidc(browser_type, ip_address, port): |
| 198 | + |
| 199 | + browser = browser_type.launch(headless=True, args=[f"--host-resolver-rules=MAP * {ip_address}:{port}"]) |
| 200 | + context = browser.new_context(ignore_https_errors=True) |
| 201 | + |
| 202 | + try: |
| 203 | + page = context.new_page() |
| 204 | + |
| 205 | + page.goto("https://virtual-server-tls.example.com") |
| 206 | + page.wait_for_selector('input[name="username"]') |
| 207 | + page.fill('input[name="username"]', username) |
| 208 | + page.wait_for_selector('input[name="password"]', timeout=5000) |
| 209 | + page.fill('input[name="password"]', password) |
| 210 | + |
| 211 | + with page.expect_navigation(): |
| 212 | + page.click('input[type="submit"]') |
| 213 | + page.wait_for_load_state("load") |
| 214 | + page_text = page.text_content("body") |
| 215 | + fields_to_check = [ |
| 216 | + "Server address:", |
| 217 | + "Server name:", |
| 218 | + "Date:", |
| 219 | + "Request ID:", |
| 220 | + ] |
| 221 | + for field in fields_to_check: |
| 222 | + assert field in page_text, f"'{field}' not found in page text" |
| 223 | + |
| 224 | + except Error as e: |
| 225 | + assert False, f"Error: {e}" |
| 226 | + |
| 227 | + finally: |
| 228 | + context.close() |
| 229 | + browser.close() |
0 commit comments