Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# This file is part of daf_butler 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <http://www.gnu.org/licenses/>. 

21 

22import fnmatch 

23import os 

24import stat 

25import urllib.parse 

26import yaml 

27 

28__all__ = ["DbAuth", "DbAuthError", "DbAuthPermissionsError"] 

29 

30 

31class DbAuthError(RuntimeError): 

32 """A problem has occurred retrieving database authentication information. 

33 """ 

34 pass 

35 

36 

37class DbAuthPermissionsError(DbAuthError): 

38 """Credentials file has incorrect permissions.""" 

39 

40 

41class DbAuth: 

42 """Retrieves authentication information for database connections. 

43 

44 The authorization configuration is taken from the ``authList`` parameter 

45 or a (group- and world-inaccessible) YAML file located at a path specified 

46 by the given environment variable or at a default path location. 

47 

48 Parameters 

49 ---------- 

50 path : `str` or None, optional 

51 Path to configuration file. 

52 envVar : `str` or None, optional 

53 Name of environment variable pointing to configuration file. 

54 authList : `list` [`dict`] or None, optional 

55 Authentication configuration. 

56 

57 Notes 

58 ----- 

59 At least one of ``path``, ``envVar``, or ``authList`` must be provided; 

60 generally ``path`` should be provided as a default location. 

61 """ 

62 def __init__(self, path=None, envVar=None, authList=None): 

63 if authList is not None: 

64 self.authList = authList 

65 return 

66 if envVar is not None and envVar in os.environ: 

67 secretPath = os.path.expanduser(os.environ[envVar]) 

68 elif path is None: 

69 raise DbAuthError( 

70 "No default path provided to DbAuth configuration file") 

71 else: 

72 secretPath = os.path.expanduser(path) 

73 if not os.path.isfile(secretPath): 

74 raise DbAuthError("No DbAuth configuration file: " + secretPath) 

75 mode = os.stat(secretPath).st_mode 

76 if mode & (stat.S_IRWXG | stat.S_IRWXO) != 0: 

77 raise DbAuthPermissionsError( 

78 "DbAuth configuration file {} has incorrect permissions: " 

79 "{:o}".format(secretPath, mode)) 

80 

81 try: 

82 with open(secretPath) as secretFile: 

83 self.authList = yaml.safe_load(secretFile) 

84 except Exception as exc: 

85 raise DbAuthError(f"Unable to load DbAuth configuration file: {secretPath}.") from exc 

86 

87 def getAuth(self, drivername, username, host, port, database): 

88 """Retrieve a username and password for a database connection. 

89 

90 This function matches elements from the database connection URL with 

91 glob-like URL patterns in a list of configuration dictionaries. 

92 

93 Parameters 

94 ---------- 

95 drivername : `str` 

96 Name of database backend driver from connection URL. 

97 username : `str` or None 

98 Username from connection URL if present. 

99 host : `str` 

100 Host name from connection URL if present. 

101 port : `str` or `int` or None 

102 Port from connection URL if present. 

103 database : `str` 

104 Database name from connection URL. 

105 

106 Returns 

107 ------- 

108 username: `str` 

109 Username to use for database connection; same as parameter if 

110 present. 

111 password: `str` 

112 Password to use for database connection. 

113 

114 Raises 

115 ------ 

116 DbAuthError 

117 Raised if the input is missing elements, an authorization 

118 dictionary is missing elements, the authorization file is 

119 misconfigured, or no matching authorization is found. 

120 

121 Notes 

122 ----- 

123 The list of authorization configuration dictionaries is tested in 

124 order, with the first matching dictionary used. Each dictionary must 

125 contain a ``url`` item with a pattern to match against the database 

126 connection URL and a ``password`` item. If no username is provided in 

127 the database connection URL, the dictionary must also contain a 

128 ``username`` item. 

129 

130 Glob-style patterns (using "``*``" and "``?``" as wildcards) can be 

131 used to match the host and database name portions of the connection 

132 URL. For the username, port, and database name portions, omitting them 

133 from the pattern matches against any value in the connection URL. 

134 

135 Examples 

136 -------- 

137 

138 The connection URL 

139 ``postgresql://user@host.example.com:5432/my_database`` matches against 

140 the identical string as a pattern. Other patterns that would match 

141 include: 

142 

143 * ``postgresql://*`` 

144 * ``postgresql://*.example.com`` 

145 * ``postgresql://*.example.com/my_*`` 

146 * ``postgresql://host.example.com/my_database`` 

147 * ``postgresql://host.example.com:5432/my_database`` 

148 * ``postgresql://user@host.example.com/my_database`` 

149 

150 Note that the connection URL 

151 ``postgresql://host.example.com/my_database`` would not match against 

152 the pattern ``postgresql://host.example.com:5432``, even if the default 

153 port for the connection is 5432. 

154 """ 

155 if drivername is None or drivername == "": 

156 raise DbAuthError("Missing drivername parameter") 

157 if host is None or host == "": 

158 raise DbAuthError("Missing host parameter") 

159 if database is None or database == "": 

160 raise DbAuthError("Missing database parameter") 

161 

162 for authDict in self.authList: 

163 

164 # Check for mandatory entries 

165 if "url" not in authDict: 

166 raise DbAuthError("Missing URL in DbAuth configuration") 

167 

168 # Parse pseudo-URL from db-auth.yaml 

169 components = urllib.parse.urlparse(authDict["url"]) 

170 

171 # Check for same database backend type/driver 

172 if components.scheme == "": 

173 raise DbAuthError( 

174 "Missing database driver in URL: " + authDict["url"]) 

175 if drivername != components.scheme: 

176 continue 

177 

178 # Check for same database name 

179 if components.path != "" and components.path != "/": 

180 if not fnmatch.fnmatch(database, components.path.lstrip("/")): 

181 continue 

182 

183 # Check username 

184 if components.username is not None: 

185 if username is None or username == "": 

186 continue 

187 if username != components.username: 

188 continue 

189 

190 # Check hostname 

191 if components.hostname is None: 

192 raise DbAuthError("Missing host in URL: " + authDict["url"]) 

193 if not fnmatch.fnmatch(host, components.hostname): 

194 continue 

195 

196 # Check port 

197 if components.port is not None and \ 

198 (port is None or str(port) != str(components.port)): 

199 continue 

200 

201 # Don't override username from connection string 

202 if username is not None and username != "": 

203 return (username, authDict["password"]) 

204 else: 

205 if "username" not in authDict: 

206 return (None, authDict["password"]) 

207 return (authDict["username"], authDict["password"]) 

208 

209 raise DbAuthError( 

210 "No matching DbAuth configuration for: " 

211 f"({drivername}, {username}, {host}, {port}, {database})") 

212 

213 def getUrl(self, url): 

214 """Fill in a username and password in a database connection URL. 

215 

216 This function parses the URL and calls `getAuth`. 

217 

218 Parameters 

219 ---------- 

220 url : `str` 

221 Database connection URL. 

222 

223 Returns 

224 ------- 

225 url : `str` 

226 Database connection URL with username and password. 

227 

228 Raises 

229 ------ 

230 DbAuthError 

231 Raised if the input is missing elements, an authorization 

232 dictionary is missing elements, the authorization file is 

233 misconfigured, or no matching authorization is found. 

234 

235 See also 

236 -------- 

237 getAuth 

238 """ 

239 components = urllib.parse.urlparse(url) 

240 username, password = self.getAuth( 

241 components.scheme, 

242 components.username, components.hostname, components.port, 

243 components.path.lstrip("/")) 

244 hostname = components.hostname 

245 if ":" in hostname: # ipv6 

246 hostname = f"[{hostname}]" 

247 netloc = "{}:{}@{}".format( 

248 urllib.parse.quote(username, safe=""), 

249 urllib.parse.quote(password, safe=""), hostname) 

250 if components.port is not None: 

251 netloc += ":" + str(components.port) 

252 return urllib.parse.urlunparse(( 

253 components.scheme, netloc, components.path, components.params, 

254 components.query, components.fragment))