Skip to content

Commit 6e83503

Browse files
committed
add oidc test and test data
1 parent 10ff932 commit 6e83503

File tree

10 files changed

+358
-0
lines changed

10 files changed

+358
-0
lines changed

.github/data/matrix-smoke-plus.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@
7676
"type": "plus",
7777
"marker": "'policies_ac or policies_jwt or policies_mtls'",
7878
"platforms": "linux/arm64, linux/amd64, linux/s390x"
79+
},
80+
{
81+
"label": "OIDC-UI 1/1",
82+
"image": "debian-plus",
83+
"type": "plus",
84+
"marker": "oidc",
85+
"platforms": "linux/arm64, linux/amd64"
7986
}
8087
],
8188
"k8s": []

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ markers =[
4545
"hsts",
4646
"ingresses",
4747
"multi_ns",
48+
"oidc",
4849
"policies",
4950
"policies_rl",
5051
"policies_jwt",
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
apiVersion: v1
2+
kind: Service
3+
metadata:
4+
name: keycloak
5+
labels:
6+
app: keycloak
7+
spec:
8+
ports:
9+
- name: http
10+
port: 8080
11+
targetPort: 8080
12+
selector:
13+
app: keycloak
14+
---
15+
apiVersion: apps/v1
16+
kind: Deployment
17+
metadata:
18+
name: keycloak
19+
labels:
20+
app: keycloak
21+
spec:
22+
replicas: 1
23+
selector:
24+
matchLabels:
25+
app: keycloak
26+
template:
27+
metadata:
28+
labels:
29+
app: keycloak
30+
spec:
31+
containers:
32+
- name: keycloak
33+
image: quay.io/keycloak/keycloak:25.0.2
34+
args: ["start-dev"]
35+
env:
36+
- name: KEYCLOAK_ADMIN
37+
value: "admin"
38+
- name: KEYCLOAK_ADMIN_PASSWORD
39+
value: "admin"
40+
- name: KC_PROXY
41+
value: "edge"
42+
ports:
43+
- name: http
44+
containerPort: 8080
45+
- name: https
46+
containerPort: 8443
47+
readinessProbe:
48+
httpGet:
49+
path: /realms/master
50+
port: 8080

tests/data/oidc/client-secret.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
apiVersion: v1
2+
kind: Secret
3+
metadata:
4+
name: oidc-secret
5+
type: nginx.org/oidc
6+
data:
7+
client-secret:

tests/data/oidc/nginx-config.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
kind: ConfigMap
2+
apiVersion: v1
3+
metadata:
4+
name: nginx-config
5+
namespace: nginx-ingress
6+
data:
7+
stream-snippets: |
8+
server {
9+
listen 12345;
10+
listen [::]:12345;
11+
zone_sync;
12+
zone_sync_server nginx-ingress-headless.nginx-ingress.svc.cluster.local:12345 resolve;
13+
}
14+
resolver-addresses: kube-dns.kube-system.svc.cluster.local
15+
resolver-valid: 5s
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
apiVersion: v1
2+
kind: Service
3+
metadata:
4+
name: nginx-ingress-headless
5+
spec:
6+
clusterIP: None
7+
selector:
8+
app: nginx-ingress

tests/data/oidc/oidc.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
apiVersion: k8s.nginx.org/v1
2+
kind: Policy
3+
metadata:
4+
name: oidc-policy
5+
spec:
6+
oidc:
7+
clientID: nginx-plus
8+
clientSecret: oidc-secret
9+
authEndpoint: https://keycloak.example.com/realms/master/protocol/openid-connect/auth
10+
tokenEndpoint: http://keycloak.default.svc.cluster.local:8080/realms/master/protocol/openid-connect/token
11+
jwksURI: http://keycloak.default.svc.cluster.local:8080/realms/master/protocol/openid-connect/certs
12+
endSessionEndpoint: https://keycloak.example.com/realms/master/protocol/openid-connect/logout
13+
scope: openid+profile+email
14+
accessTokenEnable: true
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
apiVersion: k8s.nginx.org/v1
2+
kind: VirtualServer
3+
metadata:
4+
name: keycloak
5+
spec:
6+
host: keycloak.example.com
7+
tls:
8+
secret: tls-secret
9+
redirect:
10+
enable: true
11+
upstreams:
12+
- name: keycloak
13+
service: keycloak
14+
port: 8080
15+
routes:
16+
- path: /
17+
action:
18+
pass: keycloak
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
apiVersion: k8s.nginx.org/v1
2+
kind: VirtualServer
3+
metadata:
4+
name: virtual-server-tls
5+
spec:
6+
host: virtual-server-tls.example.com
7+
tls:
8+
secret: tls-secret
9+
upstreams:
10+
- name: backend1
11+
service: backend1-svc
12+
port: 80
13+
routes:
14+
- path: /
15+
policies:
16+
- name: oidc-policy
17+
action:
18+
pass: backend1
19+

tests/suite/test_oidc.py

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import base64
2+
3+
import pytest
4+
import requests
5+
import yaml
6+
from _datetime import datetime
7+
from playwright.sync_api import Error, sync_playwright
8+
from settings import DEPLOYMENTS, TEST_DATA
9+
from suite.utils.policy_resources_utils import delete_policy
10+
from suite.utils.resources_utils import (
11+
create_example_app,
12+
create_items_from_yaml,
13+
create_secret,
14+
create_secret_from_yaml,
15+
delete_common_app,
16+
delete_secret,
17+
replace_configmap_from_yaml,
18+
wait_before_test,
19+
wait_until_all_pods_are_ready,
20+
)
21+
from suite.utils.vs_vsr_resources_utils import (
22+
create_virtual_server_from_yaml,
23+
delete_virtual_server,
24+
patch_virtual_server_from_yaml,
25+
)
26+
from urllib3.util import connection
27+
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+
oidc_pol_src = f"{TEST_DATA}/oidc/oidc.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+
orig_cm_src = f"{DEPLOYMENTS}/common/nginx-config.yaml"
36+
svc_src = f"{TEST_DATA}/oidc/nginx-ingress-headless.yaml"
37+
38+
39+
class KeycloakSetup:
40+
"""
41+
Attributes:
42+
secret (str):
43+
"""
44+
45+
def __init__(self, secret):
46+
self.secret = secret
47+
48+
49+
@pytest.fixture(scope="class")
50+
def keycloak_setup(request, kube_apis, test_namespace, ingress_controller_endpoint, virtual_server_setup):
51+
52+
# Create Keycloak resources and setup Keycloak idp
53+
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": "nginx-user",
77+
"enabled": True,
78+
"credentials": [{"type": "password", "value": "test", "temporary": False}],
79+
}
80+
response = requests.post(create_user_url, headers=headers, json=user_payload, verify=False)
81+
82+
# Create client "nginx-plus" and get secret
83+
create_client_url = f"https://{ingress_controller_endpoint.public_ip}:{ingress_controller_endpoint.port_ssl}/realms/master/clients-registrations/default"
84+
client_payload = {
85+
"clientId": "nginx-plus",
86+
"redirectUris": ["https://virtual-server-tls.example.com:443/_codexch"],
87+
"attributes": {"post.logout.redirect.uris": "https://virtual-server-tls.example.com:443/*"},
88+
}
89+
client_resp = requests.post(create_client_url, headers=headers, json=client_payload, verify=False)
90+
client_resp.raise_for_status()
91+
secret = client_resp.json().get("secret")
92+
93+
# Base64 encode the secret
94+
encoded_secret = base64.b64encode(secret.encode()).decode()
95+
96+
print(f"Keycloak setup complete. Base64 encoded client secret")
97+
98+
def fin():
99+
if request.config.getoption("--skip-fixture-teardown") == "no":
100+
print("Delete Keycloak resources")
101+
delete_virtual_server(kube_apis.custom_objects, keycloak_vs_name, test_namespace)
102+
delete_common_app(kube_apis, "keycloak", test_namespace)
103+
delete_secret(kube_apis.v1, secret_name, test_namespace)
104+
request.addfinalizer(fin)
105+
106+
return KeycloakSetup(encoded_secret)
107+
108+
109+
@pytest.mark.oidc
110+
@pytest.mark.skip_for_nginx_oss
111+
@pytest.mark.parametrize(
112+
"crd_ingress_controller, virtual_server_setup",
113+
[
114+
(
115+
{
116+
"type": "complete",
117+
"extra_args": [
118+
f"-enable-oidc",
119+
],
120+
},
121+
{"example": "virtual-server-tls", "app_type": "simple"},
122+
)
123+
],
124+
indirect=True,
125+
)
126+
class TestOIDC:
127+
def test_oidc(
128+
self,
129+
request,
130+
kube_apis,
131+
ingress_controller_endpoint,
132+
ingress_controller_prerequisites,
133+
crd_ingress_controller,
134+
test_namespace,
135+
virtual_server_setup,
136+
keycloak_setup,
137+
):
138+
print(f"Create oidc secret")
139+
with open(oidc_secret_src) as f:
140+
secret_data = yaml.safe_load(f)
141+
secret_data["data"]["client-secret"] = keycloak_setup.secret
142+
secret_name = create_secret(kube_apis.v1, test_namespace, secret_data)
143+
144+
print(f"Create oidc policy")
145+
with open(oidc_pol_src) as f:
146+
doc = yaml.safe_load(f)
147+
pol = doc["metadata"]["name"]
148+
doc["spec"]["oidc"]["tokenEndpoint"] = doc["spec"]["oidc"]["tokenEndpoint"].replace("default", test_namespace)
149+
doc["spec"]["oidc"]["jwksURI"] = doc["spec"]["oidc"]["jwksURI"].replace("default", test_namespace)
150+
kube_apis.custom_objects.create_namespaced_custom_object("k8s.nginx.org", "v1", test_namespace, "policies", doc)
151+
print(f"Policy created with name {pol}")
152+
wait_before_test()
153+
154+
print(f"Create virtual server")
155+
patch_virtual_server_from_yaml(
156+
kube_apis.custom_objects, virtual_server_setup.vs_name, oidc_vs_src, test_namespace
157+
)
158+
wait_before_test()
159+
160+
print(f"Update nginx configmap")
161+
replace_configmap_from_yaml(
162+
kube_apis.v1,
163+
ingress_controller_prerequisites.config_map["metadata"]["name"],
164+
ingress_controller_prerequisites.namespace,
165+
cm_src,
166+
)
167+
wait_before_test()
168+
print(f"Create headless service")
169+
create_items_from_yaml(kube_apis, svc_src, ingress_controller_prerequisites.namespace)
170+
171+
with sync_playwright() as playwright:
172+
run_oidc(playwright.chromium, ingress_controller_endpoint.public_ip)
173+
174+
replace_configmap_from_yaml(
175+
kube_apis.v1,
176+
ingress_controller_prerequisites.config_map["metadata"]["name"],
177+
ingress_controller_prerequisites.namespace,
178+
cm_src,
179+
)
180+
delete_secret(kube_apis.v1, secret_name, test_namespace)
181+
delete_policy(kube_apis.custom_objects, pol, test_namespace)
182+
patch_virtual_server_from_yaml(
183+
kube_apis.custom_objects, virtual_server_setup.vs_name, orig_vs_src, test_namespace
184+
)
185+
186+
187+
def run_oidc(browser_type, ip_address):
188+
189+
browser = browser_type.launch(headless=True, args=[f"--host-resolver-rules=MAP * {ip_address}"])
190+
context = browser.new_context(ignore_https_errors=True)
191+
192+
try:
193+
page = context.new_page()
194+
195+
page.goto("https://virtual-server-tls.example.com")
196+
page.wait_for_selector('input[name="username"]')
197+
page.fill('input[name="username"]', "nginx-user")
198+
page.wait_for_selector('input[name="password"]', timeout=5000)
199+
page.fill('input[name="password"]', "test")
200+
201+
with page.expect_navigation():
202+
page.click('input[type="submit"]')
203+
page.wait_for_load_state("load")
204+
page_text = page.text_content("body")
205+
fields_to_check = [
206+
"Server address:",
207+
"Server name:",
208+
"Date:",
209+
"Request ID:",
210+
]
211+
for field in fields_to_check:
212+
assert field in page_text, f"'{field}' not found in page text"
213+
214+
except Error as e:
215+
assert False, f"Error: {e}"
216+
217+
finally:
218+
context.close()
219+
browser.close()

0 commit comments

Comments
 (0)