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

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

# This file is part of daf_butler 

# 

# Developed for the LSST Data Management System. 

# This product includes software developed by the LSST Project 

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

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

# for details of code ownership. 

# 

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

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

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

# (at your option) any later version. 

# 

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

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

# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

# GNU General Public License for more details. 

# 

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

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

 

import fnmatch 

import os 

import stat 

import urllib.parse 

import yaml 

 

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

 

 

class DbAuthError(RuntimeError): 

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

""" 

pass 

 

 

class DbAuthPermissionsError(DbAuthError): 

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

 

 

class DbAuth: 

"""Retrieves authentication information for database connections. 

 

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

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

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

 

Parameters 

---------- 

path : `str` or None, optional 

Path to configuration file. 

envVar : `str` or None, optional 

Name of environment variable pointing to configuration file. 

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

Authentication configuration. 

 

Notes 

----- 

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

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

""" 

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

if authList is not None: 

self.authList = authList 

return 

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

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

elif path is None: 

raise DbAuthError( 

"No default path provided to DbAuth configuration file") 

else: 

secretPath = os.path.expanduser(path) 

if not os.path.isfile(secretPath): 

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

mode = os.stat(secretPath).st_mode 

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

raise DbAuthPermissionsError( 

"DbAuth configuration file {} has incorrect permissions: " 

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

 

try: 

with open(secretPath) as secretFile: 

self.authList = yaml.safe_load(secretFile) 

except Exception as exc: 

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

 

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

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

 

This function matches elements from the database connection URL with 

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

 

Parameters 

---------- 

drivername : `str` 

Name of database backend driver from connection URL. 

username : `str` or None 

Username from connection URL if present. 

host : `str` 

Host name from connection URL if present. 

port : `str` or `int` or None 

Port from connection URL if present. 

database : `str` 

Database name from connection URL. 

 

Returns 

------- 

username: `str` 

Username to use for database connection; same as parameter if 

present. 

password: `str` 

Password to use for database connection. 

 

Raises 

------ 

DbAuthError 

Raised if the input is missing elements, an authorization 

dictionary is missing elements, the authorization file is 

misconfigured, or no matching authorization is found. 

 

Notes 

----- 

The list of authorization configuration dictionaries is tested in 

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

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

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

the database connection URL, the dictionary must also contain a 

``username`` item. 

 

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

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

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

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

 

Examples 

-------- 

 

The connection URL 

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

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

include: 

 

* ``postgresql://*`` 

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

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

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

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

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

 

Note that the connection URL 

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

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

port for the connection is 5432. 

""" 

if drivername is None or drivername == "": 

raise DbAuthError("Missing drivername parameter") 

if host is None or host == "": 

raise DbAuthError("Missing host parameter") 

if database is None or database == "": 

raise DbAuthError("Missing database parameter") 

 

for authDict in self.authList: 

 

# Check for mandatory entries 

if "url" not in authDict: 

raise DbAuthError("Missing URL in DbAuth configuration") 

 

# Parse pseudo-URL from db-auth.yaml 

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

 

# Check for same database backend type/driver 

if components.scheme == "": 

raise DbAuthError( 

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

if drivername != components.scheme: 

continue 

 

# Check for same database name 

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

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

continue 

 

# Check username 

if components.username is not None: 

if username is None or username == "": 

continue 

if username != components.username: 

continue 

 

# Check hostname 

if components.hostname is None: 

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

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

continue 

 

# Check port 

if components.port is not None and \ 

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

continue 

 

# Don't override username from connection string 

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

return (username, authDict["password"]) 

else: 

if "username" not in authDict: 

return (None, authDict["password"]) 

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

 

raise DbAuthError( 

"No matching DbAuth configuration for: " 

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

 

def getUrl(self, url): 

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

 

This function parses the URL and calls `getAuth`. 

 

Parameters 

---------- 

url : `str` 

Database connection URL. 

 

Returns 

------- 

url : `str` 

Database connection URL with username and password. 

 

Raises 

------ 

DbAuthError 

Raised if the input is missing elements, an authorization 

dictionary is missing elements, the authorization file is 

misconfigured, or no matching authorization is found. 

 

See also 

-------- 

getAuth 

""" 

components = urllib.parse.urlparse(url) 

username, password = self.getAuth( 

components.scheme, 

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

components.path.lstrip("/")) 

hostname = components.hostname 

if ":" in hostname: # ipv6 

hostname = f"[{hostname}]" 

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

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

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

if components.port is not None: 

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

return urllib.parse.urlunparse(( 

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

components.query, components.fragment))