Coverage for /pythoncovmergedfiles/medio/medio/src/airflow/build/lib/airflow/security/kerberos.py: 0%
56 statements
« prev ^ index » next coverage.py v7.0.1, created at 2022-12-25 06:11 +0000
« prev ^ index » next coverage.py v7.0.1, created at 2022-12-25 06:11 +0000
1#!/usr/bin/env python
2# Licensed to the Apache Software Foundation (ASF) under one
3# or more contributor license agreements. See the NOTICE file
4# distributed with this work for additional information
5# regarding copyright ownership. The ASF licenses this file
6# to you under the Apache License, Version 2.0 (the
7# "License"); you may not use this file except in compliance
8# with the License. You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing,
13# software distributed under the License is distributed on an
14# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15# KIND, either express or implied. See the License for the
16# specific language governing permissions and limitations
17# under the License.
18from __future__ import annotations
20# Licensed to Cloudera, Inc. under one
21# or more contributor license agreements. See the NOTICE file
22# distributed with this work for additional information
23# regarding copyright ownership. Cloudera, Inc. licenses this file
24# to you under the Apache License, Version 2.0 (the
25# "License"); you may not use this file except in compliance
26# with the License. You may obtain a copy of the License at
27#
28# http://www.apache.org/licenses/LICENSE-2.0
29#
30# Unless required by applicable law or agreed to in writing, software
31# distributed under the License is distributed on an "AS IS" BASIS,
32# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
33# See the License for the specific language governing permissions and
34# limitations under the License.
35"""Kerberos security provider."""
36import logging
37import shlex
38import subprocess
39import sys
40import time
42from airflow.configuration import conf
43from airflow.utils.net import get_hostname
45NEED_KRB181_WORKAROUND: bool | None = None
47log = logging.getLogger(__name__)
50def renew_from_kt(principal: str | None, keytab: str, exit_on_fail: bool = True):
51 """
52 Renew kerberos token from keytab.
54 :param principal: principal
55 :param keytab: keytab file
56 :return: None
57 """
58 # The config is specified in seconds. But we ask for that same amount in
59 # minutes to give ourselves a large renewal buffer.
60 renewal_lifetime = f"{conf.getint('kerberos', 'reinit_frequency')}m"
62 cmd_principal = principal or conf.get_mandatory_value("kerberos", "principal").replace(
63 "_HOST", get_hostname()
64 )
66 if conf.getboolean("kerberos", "forwardable"):
67 forwardable = "-f"
68 else:
69 forwardable = "-F"
71 if conf.getboolean("kerberos", "include_ip"):
72 include_ip = "-a"
73 else:
74 include_ip = "-A"
76 cmdv: list[str] = [
77 conf.get_mandatory_value("kerberos", "kinit_path"),
78 forwardable,
79 include_ip,
80 "-r",
81 renewal_lifetime,
82 "-k", # host ticket
83 "-t",
84 keytab, # specify keytab
85 "-c",
86 conf.get_mandatory_value("kerberos", "ccache"), # specify credentials cache
87 cmd_principal,
88 ]
89 log.info("Re-initialising kerberos from keytab: %s", " ".join(shlex.quote(f) for f in cmdv))
91 with subprocess.Popen(
92 cmdv,
93 stdout=subprocess.PIPE,
94 stderr=subprocess.PIPE,
95 close_fds=True,
96 bufsize=-1,
97 universal_newlines=True,
98 ) as subp:
99 subp.wait()
100 if subp.returncode != 0:
101 log.error(
102 "Couldn't reinit from keytab! `kinit' exited with %s.\n%s\n%s",
103 subp.returncode,
104 "\n".join(subp.stdout.readlines() if subp.stdout else []),
105 "\n".join(subp.stderr.readlines() if subp.stderr else []),
106 )
107 if exit_on_fail:
108 sys.exit(subp.returncode)
109 else:
110 return subp.returncode
112 global NEED_KRB181_WORKAROUND
113 if NEED_KRB181_WORKAROUND is None:
114 NEED_KRB181_WORKAROUND = detect_conf_var()
115 if NEED_KRB181_WORKAROUND:
116 # (From: HUE-640). Kerberos clock have seconds level granularity. Make sure we
117 # renew the ticket after the initial valid time.
118 time.sleep(1.5)
119 ret = perform_krb181_workaround(cmd_principal)
120 if exit_on_fail and ret != 0:
121 sys.exit(ret)
122 else:
123 return ret
124 return 0
127def perform_krb181_workaround(principal: str):
128 """
129 Workaround for Kerberos 1.8.1.
131 :param principal: principal name
132 :return: None
133 """
134 cmdv: list[str] = [
135 conf.get_mandatory_value("kerberos", "kinit_path"),
136 "-c",
137 conf.get_mandatory_value("kerberos", "ccache"),
138 "-R",
139 ] # Renew ticket_cache
141 log.info("Renewing kerberos ticket to work around kerberos 1.8.1: %s", " ".join(cmdv))
143 ret = subprocess.call(cmdv, close_fds=True)
145 if ret != 0:
146 principal = f"{principal or conf.get('kerberos', 'principal')}/{get_hostname()}"
147 ccache = conf.get("kerberos", "ccache")
148 log.error(
149 "Couldn't renew kerberos ticket in order to work around Kerberos 1.8.1 issue. Please check that "
150 "the ticket for '%s' is still renewable:\n $ kinit -f -c %s\nIf the 'renew until' date is the "
151 "same as the 'valid starting' date, the ticket cannot be renewed. Please check your KDC "
152 "configuration, and the ticket renewal policy (maxrenewlife) for the '%s' and `krbtgt' "
153 "principals.",
154 principal,
155 ccache,
156 principal,
157 )
158 return ret
161def detect_conf_var() -> bool:
162 """
163 Autodetect the Kerberos ticket configuration.
165 Return true if the ticket cache contains "conf" information as is found
166 in ticket caches of Kerberos 1.8.1 or later. This is incompatible with the
167 Sun Java Krb5LoginModule in Java6, so we need to take an action to work
168 around it.
169 """
170 ticket_cache = conf.get_mandatory_value("kerberos", "ccache")
172 with open(ticket_cache, "rb") as file:
173 # Note: this file is binary, so we check against a bytearray.
174 return b"X-CACHECONF:" in file.read()
177def run(principal: str | None, keytab: str):
178 """
179 Run the kerbros renewer.
181 :param principal: principal name
182 :param keytab: keytab file
183 :return: None
184 """
185 if not keytab:
186 log.warning("Keytab renewer not starting, no keytab configured")
187 sys.exit(0)
189 while True:
190 renew_from_kt(principal, keytab)
191 time.sleep(conf.getint("kerberos", "reinit_frequency"))