1717# under the License.
1818from __future__ import annotations
1919
20+ import logging
2021import os
2122import shlex
22- import unittest
23- from argparse import Namespace
23+ from contextlib import nullcontext
2424from unittest import mock
2525
2626import pytest
27- from parameterized import parameterized
2827
2928from airflow .security import kerberos
3029from airflow .security .kerberos import renew_from_kt
3130from tests .test_utils .config import conf_vars
3231
33- KRB5_KTNAME = os .environ .get ("KRB5_KTNAME" )
3432
33+ @pytest .mark .integration ("kerberos" )
34+ class TestKerberosIntegration :
35+ @classmethod
36+ def setup_class (cls ):
37+ assert "KRB5_KTNAME" in os .environ , "Missing KRB5_KTNAME environment variable"
38+ cls .keytab = os .environ ["KRB5_KTNAME" ]
3539
36- @unittest .skipIf (KRB5_KTNAME is None , "Skipping Kerberos API tests due to missing KRB5_KTNAME" )
37- class TestKerberos (unittest .TestCase ):
38- def setUp (self ):
39- self .args = Namespace (
40- keytab = KRB5_KTNAME , principal = None , pid = None , daemon = None , stdout = None , stderr = None , log_file = None
41- )
42-
43- @conf_vars ({("kerberos" , "keytab" ): KRB5_KTNAME })
44- def test_renew_from_kt (self ):
45- """
46- We expect no result, but a successful run. No more TypeError
47- """
48- assert renew_from_kt (principal = self .args .principal , keytab = self .args .keytab ) is None
49-
50- @conf_vars ({("kerberos" , "keytab" ): KRB5_KTNAME , ("kerberos" , "include_ip" ): "" })
51- def test_renew_from_kt_include_ip_empty (self ):
52- """
53- We expect no result, but a successful run.
54- """
55- assert renew_from_kt (principal = self .args .principal , keytab = self .args .keytab ) is None
56-
57- @conf_vars ({("kerberos" , "keytab" ): KRB5_KTNAME , ("kerberos" , "include_ip" ): "False" })
58- def test_renew_from_kt_include_ip_false (self ):
59- """
60- We expect no result, but a successful run.
61- """
62- assert renew_from_kt (principal = self .args .principal , keytab = self .args .keytab ) is None
63-
64- @conf_vars ({("kerberos" , "keytab" ): KRB5_KTNAME , ("kerberos" , "include_ip" ): "True" })
65- def test_renew_from_kt_include_ip_true (self ):
66- """
67- We expect no result, but a successful run.
68- """
69- assert renew_from_kt (principal = self .args .principal , keytab = self .args .keytab ) is None
70-
71- # Validate forwardable kerberos option
72- @conf_vars ({("kerberos" , "keytab" ): KRB5_KTNAME , ("kerberos" , "forwardable" ): "" })
73- def test_renew_from_kt_forwardable_empty (self ):
74- """
75- We expect no result, but a successful run.
76- """
77- assert renew_from_kt (principal = self .args .principal , keytab = self .args .keytab ) is None
78-
79- @conf_vars ({("kerberos" , "keytab" ): KRB5_KTNAME , ("kerberos" , "forwardable" ): "False" })
80- def test_renew_from_kt_forwardable_false (self ):
81- """
82- We expect no result, but a successful run.
83- """
84- assert renew_from_kt (principal = self .args .principal , keytab = self .args .keytab ) is None
85-
86- @conf_vars ({("kerberos" , "keytab" ): KRB5_KTNAME , ("kerberos" , "forwardable" ): "True" })
87- def test_renew_from_kt_forwardable_true (self ):
88- """
89- We expect no result, but a successful run.
90- """
91- assert renew_from_kt (principal = self .args .principal , keytab = self .args .keytab ) is None
92-
93- @conf_vars ({("kerberos" , "keytab" ): "" })
94- def test_args_from_cli (self ):
95- """
96- We expect no result, but a run with sys.exit(1) because keytab not exist.
97- """
98- with pytest .raises (SystemExit ) as ctx :
99- renew_from_kt (principal = self .args .principal , keytab = self .args .keytab )
100-
101- with self .assertLogs (kerberos .log ) as log :
102- assert (
103- f"kinit: krb5_init_creds_set_keytab: Failed to find [email protected] in " 104- f"keytab FILE:{ self .args .keytab } (unknown enctype)" in log .output
105- )
106-
107- assert ctx .value .code == 1
108-
40+ @pytest .mark .parametrize (
41+ "kerberos_config" ,
42+ [
43+ pytest .param ({}, id = "default-config" ),
44+ pytest .param ({("kerberos" , "include_ip" ): "True" }, id = "explicit-include-ip" ),
45+ pytest .param ({("kerberos" , "include_ip" ): "False" }, id = "explicit-not-include-ip" ),
46+ pytest .param ({("kerberos" , "forwardable" ): "True" }, id = "explicit-forwardable" ),
47+ pytest .param ({("kerberos" , "forwardable" ): "False" }, id = "explicit-not-forwardable" ),
48+ ],
49+ )
50+ def test_renew_from_kt (self , kerberos_config ):
51+ """We expect return 0 (exit code) and successful run."""
52+ with conf_vars (kerberos_config ):
53+ assert renew_from_kt (principal = None , keytab = self .keytab ) == 0
10954
110- class TestKerberosUnit (unittest .TestCase ):
111- @parameterized .expand (
55+ @pytest .mark .parametrize (
56+ "exit_on_fail, expected_context" ,
57+ [
58+ pytest .param (True , pytest .raises (SystemExit ), id = "exit-on-fail" ),
59+ pytest .param (False , nullcontext (), id = "return-code-of-fail" ),
60+ ],
61+ )
62+ def test_args_from_cli (self , exit_on_fail , expected_context , caplog ):
63+ """Test exit code if keytab not exist."""
64+ keytab = "/not/exists/keytab"
65+ result = None
66+
67+ with mock .patch .dict (os .environ , KRB5_KTNAME = keytab ), conf_vars ({("kerberos" , "keytab" ): keytab }):
68+ with expected_context as ctx :
69+ with caplog .at_level (logging .ERROR , logger = kerberos .log .name ):
70+ caplog .clear ()
71+ result = renew_from_kt (principal = None , keytab = keytab , exit_on_fail = exit_on_fail )
72+
73+ # If `exit_on_fail` set to True than exit code in exception, otherwise in function return
74+ exit_code = ctx .value .code if exit_on_fail else result
75+ assert exit_code == 1
76+ assert caplog .record_tuples
77+
78+
79+ class TestKerberos :
80+ @pytest .mark .parametrize (
81+ "kerberos_config, expected_cmd" ,
11282 [
11383 (
11484 {("kerberos" , "reinit_frequency" ): "42" },
@@ -158,31 +128,27 @@ class TestKerberosUnit(unittest.TestCase):
158128 "test-principal" ,
159129 ],
160130 ),
161- ]
131+ ],
162132 )
163- def test_renew_from_kt (self , kerberos_config , expected_cmd ):
164- with self .assertLogs (kerberos .log ) as log_ctx , conf_vars (kerberos_config ), mock .patch (
165- "airflow.security.kerberos.subprocess"
166- ) as mock_subprocess , mock .patch (
167- "airflow.security.kerberos.NEED_KRB181_WORKAROUND" , None
168- ), mock .patch (
169- "airflow.security.kerberos.open" , mock .mock_open (read_data = b"X-CACHECONF:" )
170- ), mock .patch (
171- "time.sleep" , return_value = None
172- ):
133+ @mock .patch ("time.sleep" , return_value = None )
134+ @mock .patch ("airflow.security.kerberos.open" , mock .mock_open (read_data = b"X-CACHECONF:" ))
135+ @mock .patch ("airflow.security.kerberos.NEED_KRB181_WORKAROUND" , None )
136+ @mock .patch ("airflow.security.kerberos.subprocess" )
137+ def test_renew_from_kt (self , mock_subprocess , mock_sleep , kerberos_config , expected_cmd , caplog ):
138+ expected_cmd_text = " " .join (shlex .quote (f ) for f in expected_cmd )
139+
140+ with conf_vars (kerberos_config ), caplog .at_level (logging .INFO , logger = kerberos .log .name ):
141+ caplog .clear ()
173142 mock_subprocess .Popen .return_value .__enter__ .return_value .returncode = 0
174143 mock_subprocess .call .return_value = 0
175144 renew_from_kt (principal = "test-principal" , keytab = "keytab" )
176145
177- assert mock_subprocess .Popen .call_args [0 ][0 ] == expected_cmd
178-
179- expected_cmd_text = " " .join (shlex .quote (f ) for f in expected_cmd )
180- assert log_ctx .output == [
181- f"INFO:airflow.security.kerberos:Re-initialising kerberos from keytab: { expected_cmd_text } " ,
182- "INFO:airflow.security.kerberos:Renewing kerberos ticket to work around kerberos 1.8.1: "
183- "kinit -c /tmp/airflow_krb5_ccache -R" ,
146+ assert caplog .messages == [
147+ f"Re-initialising kerberos from keytab: { expected_cmd_text } " ,
148+ "Renewing kerberos ticket to work around kerberos 1.8.1: kinit -c /tmp/airflow_krb5_ccache -R" ,
184149 ]
185150
151+ assert mock_subprocess .Popen .call_args [0 ][0 ] == expected_cmd
186152 assert mock_subprocess .mock_calls == [
187153 mock .call .Popen (
188154 expected_cmd ,
@@ -201,17 +167,17 @@ def test_renew_from_kt(self, kerberos_config, expected_cmd):
201167 @mock .patch ("airflow.security.kerberos.subprocess" )
202168 @mock .patch ("airflow.security.kerberos.NEED_KRB181_WORKAROUND" , None )
203169 @mock .patch ("airflow.security.kerberos.open" , mock .mock_open (read_data = b"" ))
204- def test_renew_from_kt_without_workaround (self , mock_subprocess ):
170+ def test_renew_from_kt_without_workaround (self , mock_subprocess , caplog ):
205171 mock_subprocess .Popen .return_value .__enter__ .return_value .returncode = 0
206172 mock_subprocess .call .return_value = 0
207173
208- with self .assertLogs (kerberos .log ) as log_ctx :
174+ with caplog .at_level (logging .INFO , logger = kerberos .log .name ):
175+ caplog .clear ()
209176 renew_from_kt (principal = "test-principal" , keytab = "keytab" )
210-
211- assert log_ctx .output == [
212- "INFO:airflow.security.kerberos:Re-initialising kerberos from keytab: "
213- "kinit -f -a -r 3600m -k -t keytab -c /tmp/airflow_krb5_ccache test-principal"
214- ]
177+ assert caplog .messages == [
178+ "Re-initialising kerberos from keytab: "
179+ "kinit -f -a -r 3600m -k -t keytab -c /tmp/airflow_krb5_ccache test-principal"
180+ ]
215181
216182 assert mock_subprocess .mock_calls == [
217183 mock .call .Popen (
@@ -241,21 +207,24 @@ def test_renew_from_kt_without_workaround(self, mock_subprocess):
241207
242208 @mock .patch ("airflow.security.kerberos.subprocess" )
243209 @mock .patch ("airflow.security.kerberos.NEED_KRB181_WORKAROUND" , None )
244- def test_renew_from_kt_failed (self , mock_subprocess ):
210+ def test_renew_from_kt_failed (self , mock_subprocess , caplog ):
245211 mock_subp = mock_subprocess .Popen .return_value .__enter__ .return_value
246212 mock_subp .returncode = 1
247213 mock_subp .stdout = mock .MagicMock (name = "stdout" , ** {"readlines.return_value" : ["STDOUT" ]})
248214 mock_subp .stderr = mock .MagicMock (name = "stderr" , ** {"readlines.return_value" : ["STDERR" ]})
249215
250- with self .assertLogs (kerberos .log ) as log_ctx , self .assertRaises (SystemExit ):
216+ with pytest .raises (SystemExit ) as ctx :
217+ caplog .clear ()
251218 renew_from_kt (principal = "test-principal" , keytab = "keytab" )
219+ assert ctx .value .code == 1
252220
253- assert log_ctx .output == [
254- "INFO:airflow.security.kerberos:Re-initialising kerberos from keytab: "
221+ log_records = [record for record in caplog .record_tuples if record [0 ] == kerberos .log .name ]
222+ assert len (log_records ) == 2 , log_records
223+ assert [lr [1 ] for lr in log_records ] == [logging .INFO , logging .ERROR ]
224+ assert [lr [2 ] for lr in log_records ] == [
225+ "Re-initialising kerberos from keytab: "
255226 "kinit -f -a -r 3600m -k -t keytab -c /tmp/airflow_krb5_ccache test-principal" ,
256- "ERROR:airflow.security.kerberos:Couldn't reinit from keytab! `kinit' exited with 1.\n "
257- "STDOUT\n "
258- "STDERR" ,
227+ "Couldn't reinit from keytab! `kinit' exited with 1.\n STDOUT\n STDERR" ,
259228 ]
260229
261230 assert mock_subprocess .mock_calls == [
@@ -289,22 +258,25 @@ def test_renew_from_kt_failed(self, mock_subprocess):
289258 @mock .patch ("airflow.security.kerberos.open" , mock .mock_open (read_data = b"X-CACHECONF:" ))
290259 @mock .patch ("airflow.security.kerberos.get_hostname" , return_value = "HOST" )
291260 @mock .patch ("time.sleep" , return_value = None )
292- def test_renew_from_kt_failed_workaround (self , mock_sleep , mock_getfqdn , mock_subprocess ):
261+ def test_renew_from_kt_failed_workaround (self , mock_sleep , mock_getfqdn , mock_subprocess , caplog ):
293262 mock_subprocess .Popen .return_value .__enter__ .return_value .returncode = 0
294263 mock_subprocess .call .return_value = 1
295264
296- with self .assertLogs (kerberos .log ) as log_ctx , self .assertRaises (SystemExit ):
265+ with pytest .raises (SystemExit ) as ctx :
266+ caplog .clear ()
297267 renew_from_kt (principal = "test-principal" , keytab = "keytab" )
268+ assert ctx .value .code == 1
298269
299- assert log_ctx .output == [
300- "INFO:airflow.security.kerberos:Re-initialising kerberos from keytab: "
270+ log_records = [record for record in caplog .record_tuples if record [0 ] == kerberos .log .name ]
271+ assert len (log_records ) == 3 , log_records
272+ assert [lr [1 ] for lr in log_records ] == [logging .INFO , logging .INFO , logging .ERROR ]
273+ assert [lr [2 ] for lr in log_records ] == [
274+ "Re-initialising kerberos from keytab: "
301275 "kinit -f -a -r 3600m -k -t keytab -c /tmp/airflow_krb5_ccache test-principal" ,
302- "INFO:airflow.security.kerberos:Renewing kerberos ticket to work around kerberos 1.8.1: "
303- "kinit -c /tmp/airflow_krb5_ccache -R" ,
304- "ERROR:airflow.security.kerberos:Couldn't renew kerberos ticket in order to work around "
276+ "Renewing kerberos ticket to work around kerberos 1.8.1: kinit -c /tmp/airflow_krb5_ccache -R" ,
277+ "Couldn't renew kerberos ticket in order to work around "
305278 "Kerberos 1.8.1 issue. Please check that the ticket for 'test-principal/HOST' is still "
306- "renewable:\n "
307- " $ kinit -f -c /tmp/airflow_krb5_ccache\n "
279+ "renewable:\n $ kinit -f -c /tmp/airflow_krb5_ccache\n "
308280 "If the 'renew until' date is the same as the 'valid starting' date, the ticket cannot be "
309281 "renewed. Please check your KDC configuration, and the ticket renewal policy (maxrenewlife) for "
310282 "the 'test-principal/HOST' and `krbtgt' principals." ,
@@ -337,19 +309,21 @@ def test_renew_from_kt_failed_workaround(self, mock_sleep, mock_getfqdn, mock_su
337309 mock .call .call (["kinit" , "-c" , "/tmp/airflow_krb5_ccache" , "-R" ], close_fds = True ),
338310 ]
339311
340- def test_run_without_keytab (self ):
341- with self .assertLogs (kerberos .log ) as log_ctx , self .assertRaises (SystemExit ):
342- kerberos .run (principal = "test-principal" , keytab = None )
343- assert log_ctx .output == [
344- "WARNING:airflow.security.kerberos:Keytab renewer not starting, no keytab configured"
345- ]
312+ def test_run_without_keytab (self , caplog ):
313+ with pytest .raises (SystemExit ) as ctx :
314+ with caplog .at_level (logging .WARNING , logger = kerberos .log .name ):
315+ caplog .clear ()
316+ kerberos .run (principal = "test-principal" , keytab = None )
317+ assert ctx .value .code == 0
318+ assert caplog .messages == ["Keytab renewer not starting, no keytab configured" ]
346319
347320 @mock .patch ("airflow.security.kerberos.renew_from_kt" )
348321 @mock .patch ("time.sleep" , return_value = None )
349322 def test_run (self , mock_sleep , mock_renew_from_kt ):
350323 mock_renew_from_kt .side_effect = [1 , 1 , SystemExit (42 )]
351- with self . assertRaises (SystemExit ):
324+ with pytest . raises (SystemExit ) as ctx :
352325 kerberos .run (principal = "test-principal" , keytab = "/tmp/keytab" )
326+ assert ctx .value .code == 42
353327 assert mock_renew_from_kt .mock_calls == [
354328 mock .call ("test-principal" , "/tmp/keytab" ),
355329 mock .call ("test-principal" , "/tmp/keytab" ),
0 commit comments