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 DbAuthNotFoundError(DbAuthError): 

38 """Credentials file does not exist or no match was found in it.""" 

39 

40 

41class DbAuthPermissionsError(DbAuthError): 

42 """Credentials file has incorrect permissions.""" 

43 

44 

45class DbAuth: 

46 """Retrieves authentication information for database connections. 

47 

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

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

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

51 

52 Parameters 

53 ---------- 

54 path : `str` or None, optional 

55 Path to configuration file. 

56 envVar : `str` or None, optional 

57 Name of environment variable pointing to configuration file. 

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

59 Authentication configuration. 

60 

61 Notes 

62 ----- 

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

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

65 """ 

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

67 if authList is not None: 

68 self.authList = authList 

69 return 

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

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

72 elif path is None: 

73 raise DbAuthNotFoundError( 

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

75 else: 

76 secretPath = os.path.expanduser(path) 

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

78 raise DbAuthNotFoundError(f"No DbAuth configuration file: {secretPath}") 

79 mode = os.stat(secretPath).st_mode 

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

81 raise DbAuthPermissionsError( 

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

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

84 

85 try: 

86 with open(secretPath) as secretFile: 

87 self.authList = yaml.safe_load(secretFile) 

88 except Exception as exc: 

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

90 

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

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

93 

94 This function matches elements from the database connection URL with 

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

96 

97 Parameters 

98 ---------- 

99 drivername : `str` 

100 Name of database backend driver from connection URL. 

101 username : `str` or None 

102 Username from connection URL if present. 

103 host : `str` 

104 Host name from connection URL if present. 

105 port : `str` or `int` or None 

106 Port from connection URL if present. 

107 database : `str` 

108 Database name from connection URL. 

109 

110 Returns 

111 ------- 

112 username: `str` 

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

114 present. 

115 password: `str` 

116 Password to use for database connection. 

117 

118 Raises 

119 ------ 

120 DbAuthError 

121 Raised if the input is missing elements, an authorization 

122 dictionary is missing elements, the authorization file is 

123 misconfigured, or no matching authorization is found. 

124 

125 Notes 

126 ----- 

127 The list of authorization configuration dictionaries is tested in 

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

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

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

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

132 ``username`` item. 

133 

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

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

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

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

138 

139 Examples 

140 -------- 

141 

142 The connection URL 

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

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

145 include: 

146 

147 * ``postgresql://*`` 

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

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

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

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

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

153 

154 Note that the connection URL 

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

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

157 port for the connection is 5432. 

158 """ 

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

160 raise DbAuthError("Missing drivername parameter") 

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

162 raise DbAuthError("Missing host parameter") 

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

164 raise DbAuthError("Missing database parameter") 

165 

166 for authDict in self.authList: 

167 

168 # Check for mandatory entries 

169 if "url" not in authDict: 

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

171 

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

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

174 

175 # Check for same database backend type/driver 

176 if components.scheme == "": 

177 raise DbAuthError( 

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

179 if drivername != components.scheme: 

180 continue 

181 

182 # Check for same database name 

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

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

185 continue 

186 

187 # Check username 

188 if components.username is not None: 

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

190 continue 

191 if username != components.username: 

192 continue 

193 

194 # Check hostname 

195 if components.hostname is None: 

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

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

198 continue 

199 

200 # Check port 

201 if components.port is not None and \ 

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

203 continue 

204 

205 # Don't override username from connection string 

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

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

208 else: 

209 if "username" not in authDict: 

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

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

212 

213 raise DbAuthNotFoundError( 

214 "No matching DbAuth configuration for: " 

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

216 

217 def getUrl(self, url): 

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

219 

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

221 

222 Parameters 

223 ---------- 

224 url : `str` 

225 Database connection URL. 

226 

227 Returns 

228 ------- 

229 url : `str` 

230 Database connection URL with username and password. 

231 

232 Raises 

233 ------ 

234 DbAuthError 

235 Raised if the input is missing elements, an authorization 

236 dictionary is missing elements, the authorization file is 

237 misconfigured, or no matching authorization is found. 

238 

239 See also 

240 -------- 

241 getAuth 

242 """ 

243 components = urllib.parse.urlparse(url) 

244 username, password = self.getAuth( 

245 components.scheme, 

246 components.username, components.hostname, components.port, 

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

248 hostname = components.hostname 

249 if ":" in hostname: # ipv6 

250 hostname = f"[{hostname}]" 

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

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

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

254 if components.port is not None: 

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

256 return urllib.parse.urlunparse(( 

257 components.scheme, netloc, components.path, components.params, 

258 components.query, components.fragment))