Coverage for /pythoncovmergedfiles/medio/medio/src/airflow/build/lib/airflow/security/kerberos.py: 0%

56 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:35 +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 

19 

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 

41 

42from airflow.configuration import conf 

43from airflow.utils.net import get_hostname 

44 

45NEED_KRB181_WORKAROUND: bool | None = None 

46 

47log = logging.getLogger(__name__) 

48 

49 

50def renew_from_kt(principal: str | None, keytab: str, exit_on_fail: bool = True): 

51 """ 

52 Renew kerberos token from keytab. 

53 

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" 

61 

62 cmd_principal = principal or conf.get_mandatory_value("kerberos", "principal").replace( 

63 "_HOST", get_hostname() 

64 ) 

65 

66 if conf.getboolean("kerberos", "forwardable"): 

67 forwardable = "-f" 

68 else: 

69 forwardable = "-F" 

70 

71 if conf.getboolean("kerberos", "include_ip"): 

72 include_ip = "-a" 

73 else: 

74 include_ip = "-A" 

75 

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)) 

90 

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 

111 

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 

125 

126 

127def perform_krb181_workaround(principal: str): 

128 """ 

129 Workaround for Kerberos 1.8.1. 

130 

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 

140 

141 log.info("Renewing kerberos ticket to work around kerberos 1.8.1: %s", " ".join(cmdv)) 

142 

143 ret = subprocess.call(cmdv, close_fds=True) 

144 

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 

159 

160 

161def detect_conf_var() -> bool: 

162 """ 

163 Autodetect the Kerberos ticket configuration. 

164 

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") 

171 

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() 

175 

176 

177def run(principal: str | None, keytab: str): 

178 """ 

179 Run the kerbros renewer. 

180 

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) 

188 

189 while True: 

190 renew_from_kt(principal, keytab) 

191 time.sleep(conf.getint("kerberos", "reinit_frequency"))