##// END OF EJS Templates
docs
Marcin Kasperski -
r14:89cb440a default
parent child Browse files
Show More
@@ -1,318 +1,324 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 #
2 #
3 # mercurial_keyring: save passwords in password database
3 # mercurial_keyring: save passwords in password database
4 #
4 #
5 # Copyright 2009 Marcin Kasperski <Marcin.Kasperski@mekk.waw.pl>
5 # Copyright 2009 Marcin Kasperski <Marcin.Kasperski@mekk.waw.pl>
6 #
6 #
7 # This software may be used and distributed according to the terms
7 # This software may be used and distributed according to the terms
8 # of the GNU General Public License, incorporated herein by reference.
8 # of the GNU General Public License, incorporated herein by reference.
9
9
10
11 """
10 """
12 =================
11 =================
13 mercurial_keyring
12 mercurial_keyring
14 =================
13 =================
15
14
16 Mercurial extension to securely save HTTP authentication passwords
15 Mercurial extension to securely save HTTP authentication passwords
17 in password databases (Gnome Keyring, KDE KWallet, OSXKeyChain,
16 in password databases (Gnome Keyring, KDE KWallet, OSXKeyChain,
18 specific solutions for Win32 and command line). Uses and wraps
17 specific solutions for Win32 and command line). Uses and wraps
19 services of the keyring_ library.
18 services of the keyring_ library.
20
19
21 .. _keyring: http://pypi.python.org/pypi/keyring
20 .. _keyring: http://pypi.python.org/pypi/keyring
22
21
23 How does it work
22 How does it work
24 ================
23 ================
25
24
26 The extension prompts for the password on the first pull/push as it is
25 The extension prompts for the password on the first pull/push as it is
27 done by default, but saves the given password (keyed by the
26 done by default, but saves the given password (keyed by the
28 combination of username and remote repository url) in the password
27 combination of username and remote repository url) in the password
29 database. On the next run it checks for the username in ``.hg/hgrc``,
28 database. On the next run it checks for the username in ``.hg/hgrc``,
30 then for suitable password in the password database, and uses those
29 then for suitable password in the password database, and uses those
31 credentials if found.
30 credentials if found.
32
31
33 In case password turns out incorrect (either because it was invalid,
32 In case password turns out incorrect (either because it was invalid,
34 or because it was changed on the server) it just prompts the user
33 or because it was changed on the server) it just prompts the user
35 again.
34 again.
36
35
37 Installation
36 Installation
38 ============
37 ============
39
38
40 Install keyring library:
39 Install keyring library:
41
40
42 ::
41 ::
43
42
44 easy_install keyring
43 easy_install keyring
45
44
46 (or ``pip keyring``)
45 (or ``pip keyring``)
47
46
48 Either save mercurial_keyring.py anywhere and put the following
47 Either save mercurial_keyring.py anywhere and put the following
49 in ~/.hgrc (or /etc/mercurial/hgrc):
48 in ~/.hgrc (or /etc/mercurial/hgrc):
50
49
51 ::
50 ::
52
51
53 [extensions]
52 [extensions]
54 hgext.mercurial_keyring = /path/to/mercurial_keyring.py
53 hgext.mercurial_keyring = /path/to/mercurial_keyring.py
55
54
56 or save mercurial_keyring.py to mercurial/hgext directory and use
55 or save mercurial_keyring.py to mercurial/hgext directory and use
57
56
58 ::
57 ::
59
58
60 [extensions]
59 [extensions]
61 hgext.mercurial_keyring =
60 hgext.mercurial_keyring =
62
61
63 Password backend configuration
62 Password backend configuration
64 ==============================
63 ==============================
65
64
66 The library should usually pick the most appropriate password backend
65 The library should usually pick the most appropriate password backend
67 without configuration. Still, if necessary, it can be configured using
66 without configuration. Still, if necessary, it can be configured using
68 ``~/keyringrc.cfg`` file (``keyringrc.cfg`` in the home directory of
67 ``~/keyringrc.cfg`` file (``keyringrc.cfg`` in the home directory of
69 the current user). Refer to keyring_ docs for more details.
68 the current user). Refer to keyring_ docs for more details.
70
69
71 *I considered handling similar options in hgrc, but decided that
70 *I considered handling similar options in hgrc, but decided that
72 single person may use more than one keyring-based script. Still, I am
71 single person may use more than one keyring-based script. Still, I am
73 open to suggestions.*
72 open to suggestions.*
74
73
75 Repository configuration
74 Repository configuration
76 ========================
75 ========================
77
76
78 Edit repository-local ``.hg/hgrc`` and save there the remote repository
77 Edit repository-local ``.hg/hgrc`` and save there the remote repository
79 path and the username, but do not save the password. For example:
78 path and the username, but do not save the password. For example:
80
79
81 ::
80 ::
82
81
83 [paths]
82 [paths]
84 myremote = https://my.server.com/hgrepo/someproject
83 myremote = https://my.server.com/hgrepo/someproject
85
84
86 [auth]
85 [auth]
87 myremote.schemes = http https
86 myremote.schemes = http https
88 myremote.prefix = my.server.com/hgrepo
87 myremote.prefix = my.server.com/hgrepo
89 myremote.username = mekk
88 myremote.username = mekk
90
89
90 Simpler form with url-embedded name can also be used:
91
92 ::
93
94 [paths]
95 bitbucket = https://User@bitbucket.org/User/project_name/
96
91 Note: if both username and password are given in ``.hg/hgrc``, extension
97 Note: if both username and password are given in ``.hg/hgrc``, extension
92 will use them without using the password database. If username is not
98 will use them without using the password database. If username is not
93 given, extension will prompt for credentials every time, also without
99 given, extension will prompt for credentials every time, also without
94 saving the password.
100 saving the password.
95
101
96 Usage
102 Usage
97 =====
103 =====
98
104
99 Configure the repository as above, then just pull and push.
105 Configure the repository as above, then just pull and push.
100 You should be asked for the password only once (per every
106 You should be asked for the password only once (per every
101 username+remote_repository_url combination).
107 username+remote_repository_url combination).
102
108
103 Implementation details
109 Implementation details
104 ======================
110 ======================
105
111
106 The extension is monkey-patching the mercurial passwordmgr class
112 The extension is monkey-patching the mercurial passwordmgr class
107 to replace the find_user_password method.
113 to replace the find_user_password method.
108
114
109 """
115 """
110
116
111 from mercurial import hg, repo, util
117 from mercurial import hg, repo, util
112 from mercurial.i18n import _
118 from mercurial.i18n import _
113 try:
119 try:
114 from mercurial.url import passwordmgr
120 from mercurial.url import passwordmgr
115 except:
121 except:
116 from mercurial.httprepo import passwordmgr
122 from mercurial.httprepo import passwordmgr
117 from mercurial.httprepo import httprepository
123 from mercurial.httprepo import httprepository
118
124
119 import keyring
125 import keyring
120 import getpass
126 import getpass
121 from urlparse import urlparse
127 from urlparse import urlparse
122 import urllib2
128 import urllib2
123
129
124 KEYRING_SERVICE = "Mercurial"
130 KEYRING_SERVICE = "Mercurial"
125
131
126 ############################################################
132 ############################################################
127
133
128 def monkeypatch_method(cls):
134 def monkeypatch_method(cls):
129 def decorator(func):
135 def decorator(func):
130 setattr(cls, func.__name__, func)
136 setattr(cls, func.__name__, func)
131 return func
137 return func
132 return decorator
138 return decorator
133
139
134 ############################################################
140 ############################################################
135
141
136 class PasswordStore(object):
142 class PasswordStore(object):
137 """
143 """
138 Helper object handling keyring usage (password save&restore,
144 Helper object handling keyring usage (password save&restore,
139 the way passwords are keyed in the keyring).
145 the way passwords are keyed in the keyring).
140 """
146 """
141 def __init__(self):
147 def __init__(self):
142 self.cache = dict()
148 self.cache = dict()
143 def get_password(self, url, username):
149 def get_password(self, url, username):
144 return keyring.get_password(KEYRING_SERVICE,
150 return keyring.get_password(KEYRING_SERVICE,
145 self._format_key(url, username))
151 self._format_key(url, username))
146 def set_password(self, url, username, password):
152 def set_password(self, url, username, password):
147 keyring.set_password(KEYRING_SERVICE,
153 keyring.set_password(KEYRING_SERVICE,
148 self._format_key(url, username),
154 self._format_key(url, username),
149 password)
155 password)
150 def clear_password(self, url, username):
156 def clear_password(self, url, username):
151 self.set_password(url, username, "")
157 self.set_password(url, username, "")
152 def _format_key(self, url, username):
158 def _format_key(self, url, username):
153 return "%s@@%s" % (username, url)
159 return "%s@@%s" % (username, url)
154
160
155 password_store = PasswordStore()
161 password_store = PasswordStore()
156
162
157 ############################################################
163 ############################################################
158
164
159 class PasswordHandler(object):
165 class PasswordHandler(object):
160 """
166 """
161 Actual implementation of password handling (user prompting,
167 Actual implementation of password handling (user prompting,
162 configuration file searching, keyring save&restore).
168 configuration file searching, keyring save&restore).
163
169
164 Object of this class is bound as passwordmgr attribute.
170 Object of this class is bound as passwordmgr attribute.
165 """
171 """
166 def __init__(self):
172 def __init__(self):
167 self.pwd_cache = {}
173 self.pwd_cache = {}
168 self.last_reply = None
174 self.last_reply = None
169
175
170 def find_auth(self, pwmgr, realm, authuri):
176 def find_auth(self, pwmgr, realm, authuri):
171 """
177 """
172 Actual implementation of find_user_password - different
178 Actual implementation of find_user_password - different
173 ways of obtaining the username and password.
179 ways of obtaining the username and password.
174 """
180 """
175 ui = pwmgr.ui
181 ui = pwmgr.ui
176
182
177 # If we are called again just after identical previous
183 # If we are called again just after identical previous
178 # request, then the previously returned auth must have been
184 # request, then the previously returned auth must have been
179 # wrong. So we note this to force password prompt (and avoid
185 # wrong. So we note this to force password prompt (and avoid
180 # reusing bad password indifinitely).
186 # reusing bad password indifinitely).
181 after_bad_auth = (self.last_reply \
187 after_bad_auth = (self.last_reply \
182 and (self.last_reply['realm'] == realm) \
188 and (self.last_reply['realm'] == realm) \
183 and (self.last_reply['authuri'] == authuri))
189 and (self.last_reply['authuri'] == authuri))
184
190
185 # Strip arguments to get actual remote repository url.
191 # Strip arguments to get actual remote repository url.
186 base_url = self.canonical_url(authuri)
192 base_url = self.canonical_url(authuri)
187
193
188 # Extracting possible username (or password)
194 # Extracting possible username (or password)
189 # stored directly in repository url
195 # stored directly in repository url
190 user, pwd = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(pwmgr, realm, authuri)
196 user, pwd = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(pwmgr, realm, authuri)
191 if user and pwd:
197 if user and pwd:
192 self._debug_reply(ui, _("Auth data found in repository URL"), base_url, user, pwd)
198 self._debug_reply(ui, _("Auth data found in repository URL"), base_url, user, pwd)
193 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
199 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
194 return user, pwd
200 return user, pwd
195
201
196 # Checking the memory cache (there may be many http calls per command)
202 # Checking the memory cache (there may be many http calls per command)
197 cache_key = (realm, base_url)
203 cache_key = (realm, base_url)
198 if not after_bad_auth:
204 if not after_bad_auth:
199 cached_auth = self.pwd_cache.get(cache_key)
205 cached_auth = self.pwd_cache.get(cache_key)
200 if cached_auth:
206 if cached_auth:
201 user, pwd = cached_auth
207 user, pwd = cached_auth
202 self._debug_reply(ui, _("Cached auth data found"), base_url, user, pwd)
208 self._debug_reply(ui, _("Cached auth data found"), base_url, user, pwd)
203 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
209 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
204 return user, pwd
210 return user, pwd
205
211
206 # Loading username and maybe password from [auth] in .hg/hgrc
212 # Loading username and maybe password from [auth] in .hg/hgrc
207 nuser, pwd = self.load_hgrc_auth(ui, base_url)
213 nuser, pwd = self.load_hgrc_auth(ui, base_url)
208 if nuser:
214 if nuser:
209 if user:
215 if user:
210 raise util.Abort(_('mercurial_keyring: username for %s specified both in repository path (%s) and in .hg/hgrc/[auth] (%s). Please, leave only one of those' % (base_url, user, nuser)))
216 raise util.Abort(_('mercurial_keyring: username for %s specified both in repository path (%s) and in .hg/hgrc/[auth] (%s). Please, leave only one of those' % (base_url, user, nuser)))
211 user = nuser
217 user = nuser
212 if pwd:
218 if pwd:
213 self.pwd_cache[cache_key] = user, pwd
219 self.pwd_cache[cache_key] = user, pwd
214 self._debug_reply(ui, _("Auth data set in .hg/hgrc"), base_url, user, pwd)
220 self._debug_reply(ui, _("Auth data set in .hg/hgrc"), base_url, user, pwd)
215 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
221 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
216 return user, pwd
222 return user, pwd
217 else:
223 else:
218 ui.debug(_("Username found in .hg/hgrc: %s\n" % user))
224 ui.debug(_("Username found in .hg/hgrc: %s\n" % user))
219
225
220 # Loading password from keyring.
226 # Loading password from keyring.
221 # Only if username is known (so we know the key) and we are not after failure (so
227 # Only if username is known (so we know the key) and we are not after failure (so
222 # we don't reuse the bad password).
228 # we don't reuse the bad password).
223 if user and not after_bad_auth:
229 if user and not after_bad_auth:
224 pwd = password_store.get_password(base_url, user)
230 pwd = password_store.get_password(base_url, user)
225 if pwd:
231 if pwd:
226 self.pwd_cache[cache_key] = user, pwd
232 self.pwd_cache[cache_key] = user, pwd
227 self._debug_reply(ui, _("Keyring password found"), base_url, user, pwd)
233 self._debug_reply(ui, _("Keyring password found"), base_url, user, pwd)
228 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
234 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
229 return user, pwd
235 return user, pwd
230
236
231 # Is the username permanently set?
237 # Is the username permanently set?
232 fixed_user = (user and True or False)
238 fixed_user = (user and True or False)
233
239
234 # Last resort: interactive prompt
240 # Last resort: interactive prompt
235 if not ui.interactive():
241 if not ui.interactive():
236 raise util.Abort(_('mercurial_keyring: http authorization required'))
242 raise util.Abort(_('mercurial_keyring: http authorization required'))
237 ui.write(_("http authorization required\n"))
243 ui.write(_("http authorization required\n"))
238 ui.status(_("realm: %s\n") % realm)
244 ui.status(_("realm: %s\n") % realm)
239 if fixed_user:
245 if fixed_user:
240 ui.write(_("user: %s (fixed in .hg/hgrc)\n" % user))
246 ui.write(_("user: %s (fixed in .hg/hgrc)\n" % user))
241 else:
247 else:
242 user = ui.prompt(_("user:"), default=None)
248 user = ui.prompt(_("user:"), default=None)
243 pwd = ui.getpass(_("password: "))
249 pwd = ui.getpass(_("password: "))
244
250
245 if fixed_user:
251 if fixed_user:
246 # Saving password to the keyring.
252 # Saving password to the keyring.
247 # It is done only if username is fixed. Otherwise we won't
253 # It is done only if username is fixed. Otherwise we won't
248 # be able to find the password so it does not make much sense to
254 # be able to find the password so it does not make much sense to
249 # preserve it
255 # preserve it
250 ui.debug("Saving password for %s to keyring\n" % user)
256 ui.debug("Saving password for %s to keyring\n" % user)
251 password_store.set_password(base_url, user, pwd)
257 password_store.set_password(base_url, user, pwd)
252
258
253 # Saving password to the memory cache
259 # Saving password to the memory cache
254 self.pwd_cache[cache_key] = user, pwd
260 self.pwd_cache[cache_key] = user, pwd
255
261
256 self._debug_reply(ui, _("Manually entered password"), base_url, user, pwd)
262 self._debug_reply(ui, _("Manually entered password"), base_url, user, pwd)
257 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
263 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
258 return user, pwd
264 return user, pwd
259
265
260 def load_hgrc_auth(self, ui, base_url):
266 def load_hgrc_auth(self, ui, base_url):
261 """
267 """
262 Loading username and possibly password from [auth] in local
268 Loading username and possibly password from [auth] in local
263 repo .hgrc
269 repo .hgrc
264 """
270 """
265 # Theoretically 3 lines below should do.
271 # Theoretically 3 lines below should do.
266 #
272 #
267 # Unfortunately they do not work, readauthtoken always return
273 # Unfortunately they do not work, readauthtoken always return
268 # None. Why? Because ui (self.ui of passwordmgr) describes the
274 # None. Why? Because ui (self.ui of passwordmgr) describes the
269 # *remote* repository, so does *not* contain any option from
275 # *remote* repository, so does *not* contain any option from
270 # local .hg/hgrc.
276 # local .hg/hgrc.
271
277
272 #auth_token = self.readauthtoken(base_url)
278 #auth_token = self.readauthtoken(base_url)
273 #if auth_token:
279 #if auth_token:
274 # user, pwd = auth.get('username'), auth.get('password')
280 # user, pwd = auth.get('username'), auth.get('password')
275
281
276 # Workaround: we recreate the repository object
282 # Workaround: we recreate the repository object
277 repo_root = ui.config("bundle", "mainreporoot")
283 repo_root = ui.config("bundle", "mainreporoot")
278 if repo_root:
284 if repo_root:
279 from mercurial.ui import ui as _ui
285 from mercurial.ui import ui as _ui
280 import os
286 import os
281 local_ui = _ui(ui)
287 local_ui = _ui(ui)
282 local_ui.readconfig(os.path.join(repo_root, ".hg", "hgrc"))
288 local_ui.readconfig(os.path.join(repo_root, ".hg", "hgrc"))
283 local_passwordmgr = passwordmgr(local_ui)
289 local_passwordmgr = passwordmgr(local_ui)
284 auth_token = local_passwordmgr.readauthtoken(base_url)
290 auth_token = local_passwordmgr.readauthtoken(base_url)
285 if auth_token:
291 if auth_token:
286 return auth_token.get('username'), auth_token.get('password')
292 return auth_token.get('username'), auth_token.get('password')
287 return None, None
293 return None, None
288
294
289
295
290 def canonical_url(self, authuri):
296 def canonical_url(self, authuri):
291 """
297 """
292 Strips query params from url. Used to convert
298 Strips query params from url. Used to convert
293 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
299 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
294 to
300 to
295 https://repo.machine.com/repos/apps/module
301 https://repo.machine.com/repos/apps/module
296 """
302 """
297 parsed_url = urlparse(authuri)
303 parsed_url = urlparse(authuri)
298 return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc, parsed_url.path)
304 return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc, parsed_url.path)
299
305
300 def _debug_reply(self, ui, msg, url, user, pwd):
306 def _debug_reply(self, ui, msg, url, user, pwd):
301 ui.debug("%s. Url: %s, user: %s, passwd: %s\n" % (msg, url, user, pwd and '*' * len(pwd) or 'not set'))
307 ui.debug("%s. Url: %s, user: %s, passwd: %s\n" % (msg, url, user, pwd and '*' * len(pwd) or 'not set'))
302
308
303 ############################################################
309 ############################################################
304
310
305 @monkeypatch_method(passwordmgr)
311 @monkeypatch_method(passwordmgr)
306 def find_user_password(self, realm, authuri):
312 def find_user_password(self, realm, authuri):
307 """
313 """
308 keyring-based implementation of username/password query
314 keyring-based implementation of username/password query
309
315
310 Passwords are saved in gnome keyring, OSX/Chain or other platform
316 Passwords are saved in gnome keyring, OSX/Chain or other platform
311 specific storage and keyed by the repository url
317 specific storage and keyed by the repository url
312 """
318 """
313 # Extend object attributes
319 # Extend object attributes
314 if not hasattr(self, '_pwd_handler'):
320 if not hasattr(self, '_pwd_handler'):
315 self._pwd_handler = PasswordHandler()
321 self._pwd_handler = PasswordHandler()
316
322
317 return self._pwd_handler.find_auth(self, realm, authuri)
323 return self._pwd_handler.find_auth(self, realm, authuri)
318
324
General Comments 0
You need to be logged in to leave comments. Login now