Skip to content

Commit e0bb6be

Browse files
authored
Rewrite kerberos security integration and unit tests (#28092)
1 parent d932406 commit e0bb6be

File tree

1 file changed

+101
-127
lines changed

1 file changed

+101
-127
lines changed

tests/security/test_kerberos.py

Lines changed: 101 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -17,98 +17,68 @@
1717
# under the License.
1818
from __future__ import annotations
1919

20+
import logging
2021
import os
2122
import shlex
22-
import unittest
23-
from argparse import Namespace
23+
from contextlib import nullcontext
2424
from unittest import mock
2525

2626
import pytest
27-
from parameterized import parameterized
2827

2928
from airflow.security import kerberos
3029
from airflow.security.kerberos import renew_from_kt
3130
from 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.\nSTDOUT\nSTDERR",
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

Comments
 (0)