##// END OF EJS Templates
Mostly working, heavily refactored (separate class). Decision: keyring save only when username is fixed
Marcin Kasperski -
r10:d7564724 default
parent child Browse files
Show More
@@ -1,185 +1,248 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 """
3 """
4 Storing HTTP authentication passwords in keyring database.
4 Storing HTTP authentication passwords in keyring database.
5
5
6 Installation method(s):
6 Installation method(s):
7
7
8 1) in ~/.hgrc (or /etc/hgext/...)
8 1) in ~/.hgrc (or /etc/hgext/...)
9
9
10 [extensions]
10 [extensions]
11 ...
11 ...
12 hgext.mercurial_keyring = /path/to/mercurial_keyring.py
12 hgext.mercurial_keyring = /path/to/mercurial_keyring.py
13
13
14
14
15 2) Drop this file to hgext directory and in ~/.hgrc
15 2) Drop this file to hgext directory and in ~/.hgrc
16
16
17 [extensions]
17 [extensions]
18 hgext.mercurial_keyring =
18 hgext.mercurial_keyring =
19
19
20 """
20 """
21
21
22 #import mercurial.demandimport
22 #import mercurial.demandimport
23 #mercurial.demandimport.disable()
23 #mercurial.demandimport.disable()
24
24
25 from mercurial import hg, repo, util
25 from mercurial import hg, repo, util
26 from mercurial.i18n import _
26 from mercurial.i18n import _
27 try:
27 try:
28 from mercurial.url import passwordmgr
28 from mercurial.url import passwordmgr
29 except:
29 except:
30 from mercurial.httprepo import passwordmgr
30 from mercurial.httprepo import passwordmgr
31 from mercurial.httprepo import httprepository
31 from mercurial.httprepo import httprepository
32
32
33 import keyring
33 import keyring
34 import getpass
34 import getpass
35 from urlparse import urlparse
35 from urlparse import urlparse
36 import urllib2
36 import urllib2
37
37
38 KEYRING_SERVICE = "Mercurial"
38 KEYRING_SERVICE = "Mercurial"
39
39
40 ############################################################
40 ############################################################
41
41
42 def monkeypatch_class(name, bases, namespace):
42 def monkeypatch_class(name, bases, namespace):
43 """http://mail.python.org/pipermail/python-dev/2008-January/076194.html"""
43 """http://mail.python.org/pipermail/python-dev/2008-January/076194.html"""
44 assert len(bases) == 1, "Exactly one base class required"
44 assert len(bases) == 1, "Exactly one base class required"
45 base = bases[0]
45 base = bases[0]
46 for name, value in namespace.iteritems():
46 for name, value in namespace.iteritems():
47 if name != "__metaclass__":
47 if name != "__metaclass__":
48 setattr(base, name, value)
48 setattr(base, name, value)
49 return base
49 return base
50
50
51 def monkeypatch_method(cls):
51 def monkeypatch_method(cls):
52 def decorator(func):
52 def decorator(func):
53 setattr(cls, func.__name__, func)
53 setattr(cls, func.__name__, func)
54 return func
54 return func
55 return decorator
55 return decorator
56
56
57 ############################################################
57 ############################################################
58
58
59 class PasswordStore(object):
59 class PasswordStore(object):
60 """
60 """
61 Helper object handling password save&restore. Passwords
61 Helper object handling keyring usage (password save&restore).
62 are saved both in local memory cache, and keyring, and are
63 restored from those.
64 """
62 """
65 def __init__(self):
63 def __init__(self):
66 self.cache = dict()
64 self.cache = dict()
67 def get_password(self, url, username):
65 def get_password(self, url, username):
68 return keyring.get_password(KEYRING_SERVICE,
66 return keyring.get_password(KEYRING_SERVICE,
69 self._format_key(url, username))
67 self._format_key(url, username))
70 def set_password(self, url, username, password):
68 def set_password(self, url, username, password):
71 keyring.set_password(KEYRING_SERVICE,
69 keyring.set_password(KEYRING_SERVICE,
72 self._format_key(url, username),
70 self._format_key(url, username),
73 password)
71 password)
74 def clear_password(self, url, username):
72 def clear_password(self, url, username):
75 self.set_password(url, username, "")
73 self.set_password(url, username, "")
76 def _format_key(self, url, username):
74 def _format_key(self, url, username):
77 return "%s@@%s" % (username, url)
75 return "%s@@%s" % (username, url)
78
76
79 password_store = PasswordStore()
77 password_store = PasswordStore()
80
78
81 ############################################################
79 ############################################################
82
80
81 class PasswordHandler(object):
82 """
83 Actual implementation of password handling (user prompting,
84 configuration file searching, keyring save&restore).
85
86 Object of this class is bound as passwordmgr attribute.
87 """
88 def __init__(self):
89 self.pwd_cache = {}
90 self.last_reply = None
91
92 def find_auth(self, pwmgr, realm, authuri):
93 """
94 Actual implementation of find_user_password
95 """
96 ui = pwmgr.ui
97
98 # If we are called again just after identical previous request,
99 # then the previously returned auth must have been wrong. So we
100 # note this to force password prompt
101 after_bad_auth = (self.last_reply \
102 and (self.last_reply['realm'] == realm) \
103 and (self.last_reply['authuri'] == authuri))
104
105 base_url = self.canonical_url(authuri)
106
107 # Extracting possible username (or password)
108 # stored in directly in repository url
109 user, pwd = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(pwmgr, realm, authuri)
110 if user and pwd:
111 self._debug_reply(ui, _("Auth data found in repository URL"), base_url, user, pwd)
112 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
113 return user, pwd
114
115 # Checking the memory cache (there may be many http calls per command)
116 cache_key = (realm, base_url)
117 if not after_bad_auth:
118 cached_auth = self.pwd_cache.get(cache_key)
119 if cached_auth:
120 user, pwd = cached_auth
121 self._debug_reply(ui, _("Cached auth data found"), base_url, user, pwd)
122 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
123 return user, pwd
124
125 # Loading username and maybe password from [auth]
126 nuser, pwd = self.load_hgrc_auth(ui, base_url)
127 if nuser:
128 if user:
129 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)))
130 user = nuser
131 if pwd:
132 self.pwd_cache[cache_key] = user, pwd
133 self._debug_reply(ui, _("Auth data set in .hg/hgrc"), base_url, user, pwd)
134 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
135 return user, pwd
136 else:
137 ui.debug(_("Username found in .hg/hgrc: %s\n" % user))
138
139 # If username is known, and we are not after failure, we can try keyring
140 if user and not after_bad_auth:
141 pwd = password_store.get_password(base_url, user)
142 if pwd:
143 self.pwd_cache[cache_key] = user, pwd
144 self._debug_reply(ui, _("Keyring password found"), base_url, user, pwd)
145 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
146 return user, pwd
147
148 fixed_user = (user and True or False)
149
150 # Last resort: interactive prompt
151 if not ui.interactive():
152 raise util.Abort(_('mercurial_keyring: http authorization required'))
153 ui.write(_("http authorization required\n"))
154 ui.status(_("realm: %s\n") % realm)
155 if fixed_user:
156 ui.write(_("user: %s (fixed in .hg/hgrc)\n" % user))
157 else:
158 user = ui.prompt(_("user:"), default=None)
159 pwd = ui.getpass(_("password: "))
160
161 if fixed_user:
162 # We save in keyring only if username is fixed. Otherwise we won't
163 # be able to find the password so it does not make any sense to
164 # preserve it
165 ui.debug("Saving password for %s to keyring\n" % user)
166 password_store.set_password(base_url, user, pwd)
167
168 self.pwd_cache[cache_key] = user, pwd
169 self._debug_reply(ui, _("Manually entered password"), base_url, user, pwd)
170 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
171 return user, pwd
172
173 def load_hgrc_auth(self, ui, base_url):
174 """
175 Loading username and possibly password from [auth] in local
176 repo .hgrc
177 """
178 # Lines below unfortunately do not work, readauthtoken
179 # always return None. Why? Because
180 # ui (self.ui of passwordmgr) describes the *remote* repository, so
181 # does *not* contain any option from local .hg/hgrc.
182
183 #auth_token = self.readauthtoken(base_url)
184 #if auth_token:
185 # user, pwd = auth.get('username'), auth.get('password')
186
187 # Workaround: we recreate the repository object
188 repo_root = ui.config("bundle", "mainreporoot")
189 if repo_root:
190 from mercurial.ui import ui as _ui
191 import os
192 local_ui = _ui(ui)
193 local_ui.readconfig(os.path.join(repo_root, ".hg", "hgrc"))
194 local_passwordmgr = passwordmgr(local_ui)
195 auth_token = local_passwordmgr.readauthtoken(base_url)
196 if auth_token:
197 return auth_token.get('username'), auth_token.get('password')
198 return None, None
199
200
201 def canonical_url(self, authuri):
202 """
203 Strips query params from url. Used to convert
204 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
205 to
206 https://repo.machine.com/repos/apps/module
207 """
208 parsed_url = urlparse(authuri)
209 return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc, parsed_url.path)
210
211 def _debug_reply(self, ui, msg, url, user, pwd):
212 ui.debug("%s. Url: %s, user: %s, passwd: %s\n" % (msg, url, user, pwd and '*' * len(pwd) or 'not set'))
213
214 ############################################################
215
216 # The idea: if we are re-asked with exactly the same params
217 # (authuri, not base_url) then password must have been wrong.
218
83 @monkeypatch_method(passwordmgr)
219 @monkeypatch_method(passwordmgr)
84 def find_user_password(self, realm, authuri):
220 def find_user_password(self, realm, authuri):
85 """
221 """
86 keyring-based implementation of username/password query
222 keyring-based implementation of username/password query
87
223
88 Passwords are saved in gnome keyring, OSX/Chain or other platform
224 Passwords are saved in gnome keyring, OSX/Chain or other platform
89 specific storage and keyed by the repository url
225 specific storage and keyed by the repository url
90 """
226 """
91 # Calculate the true remote url. authuri happens to contain things like
227 # Extend object attributes
92 # https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
228 if not hasattr(self, '_pwd_handler'):
93 parsed_url = urlparse(authuri)
229 self._pwd_handler = PasswordHandler()
94 base_url = "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc, parsed_url.path)
95
96 # Extracting possible username/password stored in directly in repository url
97 user, pwd = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(self, realm, authuri)
98
99 # Checking the local cache (single command may repeat the call many
100 # times)
101 if not hasattr(self, '_pwd_cache'):
102 self._pwd_cache = {}
103 cache_key = (realm, base_url)
104 cached_auth = self._pwd_cache.get(cache_key)
105 if cached_auth:
106 self.ui.debug("Found cached auth tokens for %s: %s, %s\n" % (
107 base_url, cached_auth[0], cached_auth[1] and '********' or ''))
108 return cached_auth
109
230
110 # Loading username (and maybe password) from [auth] in local .hg/hgrc
231 return self._pwd_handler.find_auth(self, realm, authuri)
111 if not user:
112 # Lines below unfortunately do not work, readauthtoken
113 # always return None. Why? Because
114 # self.ui here describes the *remote* repository, so
115 # does *not* contain any option from local .hg/hgrc.
116 #
117 #auth_token = self.readauthtoken(base_url)
118 #if auth_token:
119 # user, pwd = auth.get('username'), auth.get('password')
120 #
121 # so - workaround
122 repo_root = self.ui.config("bundle", "mainreporoot")
123 if repo_root:
124 from mercurial.ui import ui as _ui
125 import os
126 local_ui = _ui(self.ui)
127 local_ui.readconfig(os.path.join(repo_root, ".hg", "hgrc"))
128 local_passwordmgr = passwordmgr(local_ui)
129 auth_token = local_passwordmgr.readauthtoken(base_url)
130 if auth_token:
131 user, pwd = auth_token.get('username'), auth_token.get('password')
132 self.ui.debug("Found .hg/hgrc auth tokens: %s, %s\n" % (
133 user, pwd and '********' or ''))
134
232
135 # username still not known? Asking
136 prompted = False
137 if not user:
138 if not self.ui.interactive():
139 raise util.Abort(_('mercurial_keyring: http authorization required'))
140 self.ui.write(_("http authorization required\n"))
141 self.ui.status(_("realm: %s\n") % realm)
142 user = self.ui.prompt(_("user:"), default=None)
143 prompted = True
144
145 # username known and still no password? Time to check keyring
146 if user and not pwd:
147 pwd = password_store.get_password(base_url, user)
148 if pwd:
149 self.ui.debug("Found keyring password for %s\n" % user)
150
151 # password still not known? Asking
152 if not pwd:
153 if not prompted:
154 if not self.ui.interactive():
155 raise util.Abort(_('mercurial_keyring: http authorization required'))
156 self.ui.write(_("http authorization required\n"))
157 self.ui.status(_("realm: %s\n") % realm)
158 pwd = self.ui.getpass(_("password for %s: ") % user)
159 else:
160 pwd = self.ui.getpass(_("password: "))
161 password_store.set_password(base_url, user, pwd)
162 self.ui.debug("Saved keyring password for %s\n" % user)
163
164 self._pwd_cache[cache_key] = (user, pwd)
165
166 self.ui.debug("Returning auth tokens for %s: %s, %s\n" % (
167 base_url, user, pwd and '********' or ''))
168 return user, pwd
169 #return None, None
170
233
171 ############################################################
234 ############################################################
172
235
173 # We patch httprespository.do_cmd to grab information that
236 # We patch httprespository.do_cmd to grab information that
174 # the request failed due to wrong auth - and clear the wrong
237 # the request failed due to wrong auth - and clear the wrong
175 # password
238 # password
176
239
177 orig_do_cmd = httprepository.do_cmd
240 orig_do_cmd = httprepository.do_cmd
178
241
179 @monkeypatch_method(httprepository)
242 @monkeypatch_method(httprepository)
180 def do_cmd(self, cmd, **args):
243 def do_cmd(self, cmd, **args):
181 try:
244 try:
182 orig_do_cmd(self, cmd, **args)
245 orig_do_cmd(self, cmd, **args)
183 except util.Abort, e:
246 except util.Abort, e:
184 self.ui.debug("Authorization failed for %s\n" % self.url())
247 self.ui.debug("Authorization failed for %s\n" % self.url())
185 raise
248 raise
General Comments 0
You need to be logged in to leave comments. Login now