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