Skip to content

Commit 54985c4

Browse files
authored
Merge pull request lightspeed-core#992 from tisnik/lcore-694-auth-skip-for-health-probes
LCORE-694: auth skip for health probes
2 parents 9888320 + 4d27f8e commit 54985c4

2 files changed

Lines changed: 282 additions & 1 deletion

File tree

src/authentication/k8s.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from kubernetes.client.rest import ApiException
1111
from kubernetes.config import ConfigException
1212

13-
from authentication.interface import AuthInterface
13+
from authentication.interface import NO_AUTH_TUPLE, AuthInterface
1414
from authentication.utils import extract_user_token
1515
from configuration import configuration
1616
from constants import DEFAULT_VIRTUAL_PATH
@@ -240,6 +240,12 @@ async def __call__(self, request: Request) -> tuple[str, str, bool, str]:
240240
Raises:
241241
HTTPException: If authentication or authorization fails.
242242
"""
243+
# LCORE-694: Config option to skip authorization for readiness and liveness probe
244+
if not request.headers.get("Authorization"):
245+
if configuration.authentication_configuration.skip_for_health_probes:
246+
if request.url.path in ("/readiness", "/liveness"):
247+
return NO_AUTH_TUPLE
248+
243249
token = extract_user_token(request.headers)
244250
user_info = get_user_info(token)
245251

tests/unit/authentication/test_k8s.py

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
K8sClientSingleton,
1919
)
2020

21+
from configuration import AppConfig
22+
2123

2224
class MockK8sResponseStatus:
2325
"""Mock Kubernetes Response Status.
@@ -193,6 +195,279 @@ async def test_auth_dependency_invalid_token(mocker: MockerFixture) -> None:
193195
assert detail["cause"] == "Invalid or expired Kubernetes token"
194196

195197

198+
async def test_auth_dependency_no_token(mocker: MockerFixture) -> None:
199+
"""Test the auth dependency without a token."""
200+
dependency = K8SAuthDependency()
201+
202+
# Mock the Kubernetes API calls
203+
mock_authn_api = mocker.patch("authentication.k8s.K8sClientSingleton.get_authn_api")
204+
mock_authz_api = mocker.patch("authentication.k8s.K8sClientSingleton.get_authz_api")
205+
206+
# Setup mock responses for invalid token
207+
mock_authn_api.return_value.create_token_review.return_value = MockK8sResponse(
208+
authenticated=False
209+
)
210+
mock_authz_api.return_value.create_subject_access_review.return_value = (
211+
MockK8sResponse(allowed=False)
212+
)
213+
214+
# Simulate a request with an invalid token
215+
request = Request(
216+
scope={
217+
"type": "http",
218+
"headers": [],
219+
}
220+
)
221+
222+
# Expect an HTTPException for invalid tokens
223+
with pytest.raises(HTTPException) as exc_info:
224+
await dependency(request)
225+
226+
# Check if the correct status code is returned for unauthorized access
227+
assert exc_info.value.status_code == 401
228+
detail = cast(dict[str, str], exc_info.value.detail)
229+
assert detail["response"] == ("Missing or invalid credentials provided by client")
230+
assert detail["cause"] == "No Authorization header found"
231+
232+
233+
async def test_auth_dependency_no_token_readiness_liveness_endpoints_1(
234+
mocker: MockerFixture,
235+
) -> None:
236+
"""Test the auth dependency without a token for readiness and liveness endpoints.
237+
238+
For this test the skip_for_health_probes configuration parameter is set to
239+
True.
240+
"""
241+
config_dict = {
242+
"name": "test",
243+
"service": {
244+
"host": "localhost",
245+
"port": 8080,
246+
"auth_enabled": False,
247+
"workers": 1,
248+
"color_log": True,
249+
"access_log": True,
250+
},
251+
"llama_stack": {
252+
"api_key": "test-key",
253+
"url": "http://test.com:1234",
254+
"use_as_library_client": False,
255+
},
256+
"authentication": {
257+
"module": "k8s",
258+
"skip_for_health_probes": True,
259+
},
260+
"user_data_collection": {
261+
"feedback_enabled": False,
262+
"feedback_storage": ".",
263+
"transcripts_enabled": False,
264+
"transcripts_storage": ".",
265+
},
266+
}
267+
cfg = AppConfig()
268+
cfg.init_from_dict(config_dict)
269+
# Update configuration for this test
270+
mocker.patch("authentication.k8s.configuration", cfg)
271+
272+
dependency = K8SAuthDependency()
273+
274+
# Mock the Kubernetes API calls
275+
mock_authn_api = mocker.patch("authentication.k8s.K8sClientSingleton.get_authn_api")
276+
mock_authz_api = mocker.patch("authentication.k8s.K8sClientSingleton.get_authz_api")
277+
278+
# Setup mock responses for invalid token
279+
mock_authn_api.return_value.create_token_review.return_value = MockK8sResponse(
280+
authenticated=False
281+
)
282+
mock_authz_api.return_value.create_subject_access_review.return_value = (
283+
MockK8sResponse(allowed=False)
284+
)
285+
286+
paths = ("/readiness", "/liveness")
287+
288+
for path in paths:
289+
# Simulate a request with an invalid token
290+
request = Request(
291+
scope={
292+
"type": "http",
293+
"headers": [],
294+
"path": path,
295+
}
296+
)
297+
298+
user_uid, username, skip_userid_check, token = await dependency(request)
299+
300+
# Check if the correct user info has been returned
301+
assert user_uid == "00000000-0000-0000-0000-000"
302+
assert username == "lightspeed-user"
303+
assert skip_userid_check is True
304+
assert token == ""
305+
306+
307+
async def test_auth_dependency_no_token_readiness_liveness_endpoints_2(
308+
mocker: MockerFixture,
309+
) -> None:
310+
"""Test the auth dependency without a token.
311+
312+
For this test the skip_for_health_probes configuration parameter is set to
313+
False.
314+
"""
315+
316+
config_dict = {
317+
"name": "test",
318+
"service": {
319+
"host": "localhost",
320+
"port": 8080,
321+
"auth_enabled": False,
322+
"workers": 1,
323+
"color_log": True,
324+
"access_log": True,
325+
},
326+
"llama_stack": {
327+
"api_key": "test-key",
328+
"url": "http://test.com:1234",
329+
"use_as_library_client": False,
330+
},
331+
"authentication": {
332+
"module": "k8s",
333+
"skip_for_health_probes": False,
334+
},
335+
"user_data_collection": {
336+
"feedback_enabled": False,
337+
"feedback_storage": ".",
338+
"transcripts_enabled": False,
339+
"transcripts_storage": ".",
340+
},
341+
}
342+
cfg = AppConfig()
343+
cfg.init_from_dict(config_dict)
344+
# Update configuration for this test
345+
mocker.patch("authentication.k8s.configuration", cfg)
346+
dependency = K8SAuthDependency()
347+
348+
# Mock the Kubernetes API calls
349+
mock_authn_api = mocker.patch("authentication.k8s.K8sClientSingleton.get_authn_api")
350+
mock_authz_api = mocker.patch("authentication.k8s.K8sClientSingleton.get_authz_api")
351+
352+
# Setup mock responses for invalid token
353+
mock_authn_api.return_value.create_token_review.return_value = MockK8sResponse(
354+
authenticated=False
355+
)
356+
mock_authz_api.return_value.create_subject_access_review.return_value = (
357+
MockK8sResponse(allowed=False)
358+
)
359+
360+
# Simulate a request with an invalid token
361+
request = Request(
362+
scope={
363+
"type": "http",
364+
"headers": [],
365+
}
366+
)
367+
368+
paths = ("/readiness", "/liveness")
369+
370+
for path in paths:
371+
# Simulate a request with an invalid token
372+
request = Request(
373+
scope={
374+
"type": "http",
375+
"headers": [],
376+
"path": path,
377+
}
378+
)
379+
380+
# Expect an HTTPException for invalid tokens
381+
with pytest.raises(HTTPException) as exc_info:
382+
await dependency(request)
383+
384+
# Check if the correct status code is returned for unauthorized access
385+
assert exc_info.value.status_code == 401
386+
detail = cast(dict[str, str], exc_info.value.detail)
387+
assert detail["response"] == (
388+
"Missing or invalid credentials provided by client"
389+
)
390+
assert detail["cause"] == "No Authorization header found"
391+
392+
393+
async def test_auth_dependency_no_token_normal_endpoints(
394+
mocker: MockerFixture,
395+
) -> None:
396+
"""Test the auth dependency without a token for endpoints different to readiness and liveness.
397+
398+
For this test the skip_for_health_probes configuration parameter is set to
399+
True.
400+
"""
401+
config_dict = {
402+
"name": "test",
403+
"service": {
404+
"host": "localhost",
405+
"port": 8080,
406+
"auth_enabled": False,
407+
"workers": 1,
408+
"color_log": True,
409+
"access_log": True,
410+
},
411+
"llama_stack": {
412+
"api_key": "test-key",
413+
"url": "http://test.com:1234",
414+
"use_as_library_client": False,
415+
},
416+
"authentication": {
417+
"module": "k8s",
418+
"skip_for_health_probes": True,
419+
},
420+
"user_data_collection": {
421+
"feedback_enabled": False,
422+
"feedback_storage": ".",
423+
"transcripts_enabled": False,
424+
"transcripts_storage": ".",
425+
},
426+
}
427+
cfg = AppConfig()
428+
cfg.init_from_dict(config_dict)
429+
# Update configuration for this test
430+
mocker.patch("authentication.k8s.configuration", cfg)
431+
432+
dependency = K8SAuthDependency()
433+
434+
# Mock the Kubernetes API calls
435+
mock_authn_api = mocker.patch("authentication.k8s.K8sClientSingleton.get_authn_api")
436+
mock_authz_api = mocker.patch("authentication.k8s.K8sClientSingleton.get_authz_api")
437+
438+
# Setup mock responses for invalid token
439+
mock_authn_api.return_value.create_token_review.return_value = MockK8sResponse(
440+
authenticated=False
441+
)
442+
mock_authz_api.return_value.create_subject_access_review.return_value = (
443+
MockK8sResponse(allowed=False)
444+
)
445+
446+
paths = ("/", "/v1/info")
447+
448+
for path in paths:
449+
# Simulate a request with an invalid token
450+
request = Request(
451+
scope={
452+
"type": "http",
453+
"headers": [],
454+
"path": path,
455+
}
456+
)
457+
458+
# Expect an HTTPException for invalid tokens
459+
with pytest.raises(HTTPException) as exc_info:
460+
await dependency(request)
461+
462+
# Check if the correct status code is returned for unauthorized access
463+
assert exc_info.value.status_code == 401
464+
detail = cast(dict[str, str], exc_info.value.detail)
465+
assert detail["response"] == (
466+
"Missing or invalid credentials provided by client"
467+
)
468+
assert detail["cause"] == "No Authorization header found"
469+
470+
196471
async def test_cluster_id_is_used_for_kube_admin(mocker: MockerFixture) -> None:
197472
"""Test the cluster id is used as user_id when user is kube:admin."""
198473
dependency = K8SAuthDependency()

0 commit comments

Comments
 (0)