~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to lp_registration.py

  • Committer: Martin Pool
  • Date: 2006-03-22 19:21:20 UTC
  • mto: (1668.1.8 bzr-0.8.mbp)
  • mto: This revision was merged to the branch mainline in revision 1710.
  • Revision ID: mbp@sourcefrog.net-20060322192120-133f1e99d4c79477
Update xmlrpc api

Prompt for user password when registering

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006 Canonical Ltd
 
1
# Copyright (C) 2006 by Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
 
 
17
 
 
18
 
import os
19
 
import socket
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
 
 
18
from getpass import getpass
20
19
from urlparse import urlsplit, urlunsplit
21
 
import urllib
 
20
from urllib import unquote, quote
22
21
import xmlrpclib
23
22
 
24
 
from bzrlib.lazy_import import lazy_import
25
 
lazy_import(globals(), """
26
 
from bzrlib import urlutils
27
 
""")
28
 
 
29
 
from bzrlib import (
30
 
    config,
31
 
    errors,
32
 
    __version__ as _bzrlib_version,
33
 
    )
34
 
 
35
 
# for testing, do
36
 
'''
37
 
export BZR_LP_XMLRPC_URL=http://xmlrpc.staging.launchpad.net/bazaar/
38
 
'''
39
 
 
40
 
class InvalidLaunchpadInstance(errors.BzrError):
41
 
 
42
 
    _fmt = "%(lp_instance)s is not a valid Launchpad instance."
43
 
 
44
 
    def __init__(self, lp_instance):
45
 
        errors.BzrError.__init__(self, lp_instance=lp_instance)
46
 
 
47
 
 
48
 
class NotLaunchpadBranch(errors.BzrError):
49
 
 
50
 
    _fmt = "%(url)s is not registered on Launchpad."
51
 
 
52
 
    def __init__(self, url):
53
 
        errors.BzrError.__init__(self, url=url)
54
 
 
55
 
 
56
 
class LaunchpadService(object):
57
 
    """A service to talk to Launchpad via XMLRPC.
58
 
 
59
 
    See http://bazaar-vcs.org/Specs/LaunchpadRpc for the methods we can call.
60
 
    """
61
 
 
62
 
    LAUNCHPAD_DOMAINS = {
63
 
        'production': 'launchpad.net',
64
 
        'edge': 'edge.launchpad.net',
65
 
        'staging': 'staging.launchpad.net',
66
 
        'demo': 'demo.launchpad.net',
67
 
        'dev': 'launchpad.dev',
68
 
        }
69
 
 
70
 
    # NB: these should always end in a slash to avoid xmlrpclib appending
71
 
    # '/RPC2'
72
 
    LAUNCHPAD_INSTANCE = {}
73
 
    for instance, domain in LAUNCHPAD_DOMAINS.iteritems():
74
 
        LAUNCHPAD_INSTANCE[instance] = 'https://xmlrpc.%s/bazaar/' % domain
75
 
 
76
 
    # We use edge as the default because:
77
 
    # Beta users get redirected to it
78
 
    # All users can use it
79
 
    # There is a bug in the launchpad side where redirection causes an OOPS.
80
 
    DEFAULT_INSTANCE = 'edge'
81
 
    DEFAULT_SERVICE_URL = LAUNCHPAD_INSTANCE[DEFAULT_INSTANCE]
82
 
 
83
 
    transport = None
84
 
    registrant_email = None
85
 
    registrant_password = None
86
 
 
87
 
 
88
 
    def __init__(self, transport=None, lp_instance=None):
89
 
        """Construct a new service talking to the launchpad rpc server"""
90
 
        self._lp_instance = lp_instance
91
 
        if transport is None:
92
 
            uri_type = urllib.splittype(self.service_url)[0]
93
 
            if uri_type == 'https':
94
 
                transport = xmlrpclib.SafeTransport()
95
 
            else:
96
 
                transport = xmlrpclib.Transport()
97
 
            transport.user_agent = 'bzr/%s (xmlrpclib/%s)' \
98
 
                    % (_bzrlib_version, xmlrpclib.__version__)
99
 
        self.transport = transport
100
 
 
101
 
 
102
 
    @property
103
 
    def service_url(self):
104
 
        """Return the http or https url for the xmlrpc server.
105
 
 
106
 
        This does not include the username/password credentials.
107
 
        """
108
 
        key = 'BZR_LP_XMLRPC_URL'
109
 
        if key in os.environ:
110
 
            return os.environ[key]
111
 
        elif self._lp_instance is not None:
112
 
            try:
113
 
                return self.LAUNCHPAD_INSTANCE[self._lp_instance]
114
 
            except KeyError:
115
 
                raise InvalidLaunchpadInstance(self._lp_instance)
116
 
        else:
117
 
            return self.DEFAULT_SERVICE_URL
118
 
 
119
 
    def get_proxy(self, authenticated):
120
 
        """Return the proxy for XMLRPC requests."""
121
 
        if authenticated:
122
 
            # auth info must be in url
123
 
            # TODO: if there's no registrant email perhaps we should
124
 
            # just connect anonymously?
125
 
            scheme, hostinfo, path = urlsplit(self.service_url)[:3]
126
 
            if '@' in hostinfo:
127
 
                raise AssertionError(hostinfo)
128
 
            if self.registrant_email is None:
129
 
                raise AssertionError()
130
 
            if self.registrant_password is None:
131
 
                raise AssertionError()
132
 
            # TODO: perhaps fully quote the password to make it very slightly
133
 
            # obscured
134
 
            # TODO: can we perhaps add extra Authorization headers
135
 
            # directly to the request, rather than putting this into
136
 
            # the url?  perhaps a bit more secure against accidentally
137
 
            # revealing it.  std66 s3.2.1 discourages putting the
138
 
            # password in the url.
139
 
            hostinfo = '%s:%s@%s' % (urllib.quote(self.registrant_email),
140
 
                                     urllib.quote(self.registrant_password),
141
 
                                     hostinfo)
142
 
            url = urlunsplit((scheme, hostinfo, path, '', ''))
143
 
        else:
144
 
            url = self.service_url
145
 
        return xmlrpclib.ServerProxy(url, transport=self.transport)
146
 
 
147
 
    def gather_user_credentials(self):
148
 
        """Get the password from the user."""
149
 
        the_config = config.GlobalConfig()
150
 
        self.registrant_email = the_config.user_email()
151
 
        if self.registrant_password is None:
152
 
            auth = config.AuthenticationConfig()
153
 
            scheme, hostinfo = urlsplit(self.service_url)[:2]
154
 
            prompt = 'launchpad.net password for %s: ' % \
155
 
                    self.registrant_email
156
 
            # We will reuse http[s] credentials if we can, prompt user
157
 
            # otherwise
158
 
            self.registrant_password = auth.get_password(scheme, hostinfo,
159
 
                                                         self.registrant_email,
160
 
                                                         prompt=prompt)
161
 
 
162
 
    def send_request(self, method_name, method_params, authenticated):
163
 
        proxy = self.get_proxy(authenticated)
164
 
        method = getattr(proxy, method_name)
165
 
        try:
166
 
            result = method(*method_params)
167
 
        except xmlrpclib.ProtocolError, e:
168
 
            if e.errcode == 301:
169
 
                # TODO: This can give a ProtocolError representing a 301 error, whose
170
 
                # e.headers['location'] tells where to go and e.errcode==301; should
171
 
                # probably log something and retry on the new url.
172
 
                raise NotImplementedError("should resend request to %s, but this isn't implemented"
173
 
                        % e.headers.get('Location', 'NO-LOCATION-PRESENT'))
174
 
            else:
175
 
                # we don't want to print the original message because its
176
 
                # str representation includes the plaintext password.
177
 
                # TODO: print more headers to help in tracking down failures
178
 
                raise errors.BzrError("xmlrpc protocol error connecting to %s: %s %s"
179
 
                        % (self.service_url, e.errcode, e.errmsg))
180
 
        except socket.gaierror, e:
181
 
            raise errors.ConnectionError(
182
 
                "Could not resolve '%s'" % self.domain,
183
 
                orig_error=e)
184
 
        return result
185
 
 
186
 
    @property
187
 
    def domain(self):
188
 
        if self._lp_instance is None:
189
 
            instance = self.DEFAULT_INSTANCE
190
 
        else:
191
 
            instance = self._lp_instance
192
 
        return self.LAUNCHPAD_DOMAINS[instance]
193
 
 
194
 
    def get_web_url_from_branch_url(self, branch_url, _request_factory=None):
195
 
        """Get the Launchpad web URL for the given branch URL.
196
 
 
197
 
        :raise errors.InvalidURL: if 'branch_url' cannot be identified as a
198
 
            Launchpad branch URL.
199
 
        :return: The URL of the branch on Launchpad.
200
 
        """
201
 
        scheme, hostinfo, path = urlsplit(branch_url)[:3]
202
 
        if _request_factory is None:
203
 
            _request_factory = ResolveLaunchpadPathRequest
204
 
        if scheme == 'lp':
205
 
            resolve = _request_factory(path)
206
 
            try:
207
 
                result = resolve.submit(self)
208
 
            except xmlrpclib.Fault, fault:
209
 
                raise errors.InvalidURL(branch_url, str(fault))
210
 
            branch_url = result['urls'][0]
211
 
            path = urlsplit(branch_url)[2]
212
 
        else:
213
 
            domains = (
214
 
                'bazaar.%s' % domain
215
 
                for domain in self.LAUNCHPAD_DOMAINS.itervalues())
216
 
            if hostinfo not in domains:
217
 
                raise NotLaunchpadBranch(branch_url)
218
 
        return urlutils.join('https://code.%s' % self.domain, path)
219
 
 
220
 
 
221
 
class BaseRequest(object):
222
 
    """Base request for talking to a XMLRPC server."""
223
 
 
224
 
    # Set this to the XMLRPC method name.
225
 
    _methodname = None
226
 
    _authenticated = True
227
 
 
228
 
    def _request_params(self):
229
 
        """Return the arguments to pass to the method"""
230
 
        raise NotImplementedError(self._request_params)
231
 
 
232
 
    def submit(self, service):
233
 
        """Submit request to Launchpad XMLRPC server.
234
 
 
235
 
        :param service: LaunchpadService indicating where to send
236
 
            the request and the authentication credentials.
237
 
        """
238
 
        return service.send_request(self._methodname, self._request_params(),
239
 
                                    self._authenticated)
240
 
 
241
 
 
242
 
class DryRunLaunchpadService(LaunchpadService):
243
 
    """Service that just absorbs requests without sending to server.
244
 
 
245
 
    The dummy service does not need authentication.
246
 
    """
247
 
 
248
 
    def send_request(self, method_name, method_params, authenticated):
249
 
        pass
250
 
 
251
 
    def gather_user_credentials(self):
252
 
        pass
253
 
 
254
 
 
255
 
class BranchRegistrationRequest(BaseRequest):
 
23
import bzrlib.config
 
24
 
 
25
# TODO: use last component of the branch's url as the default id?
 
26
 
 
27
# TODO: Allow server url to be overridden by an environment variable for
 
28
# testing; similarly for user email and password.
 
29
 
 
30
class BranchRegistrationRequest(object):
256
31
    """Request to tell Launchpad about a bzr branch."""
257
32
 
258
33
    _methodname = 'register_branch'
259
34
 
260
 
    def __init__(self, branch_url,
261
 
                 branch_name='',
262
 
                 branch_title='',
263
 
                 branch_description='',
264
 
                 author_email='',
265
 
                 product_name='',
266
 
                 ):
267
 
        if not branch_url:
268
 
            raise errors.InvalidURL(branch_url, "You need to specify a non-empty branch URL.")
 
35
    # NB: this should always end in a slash to avoid xmlrpclib appending
 
36
    # '/RPC2'
 
37
    DEFAULT_SERVICE_URL = 'http://xmlrpc.launchpad.net/bazaar/'
 
38
 
 
39
    # None means to use the xmlrpc default, which is almost always what you
 
40
    # want.  But it might be useful for testing.
 
41
 
 
42
    def __init__(self, branch_url, branch_id):
 
43
        assert branch_url
269
44
        self.branch_url = branch_url
270
 
        if branch_name:
271
 
            self.branch_name = branch_name
 
45
        if branch_id:
 
46
            self.branch_id = branch_id
272
47
        else:
273
 
            self.branch_name = self._find_default_branch_name(self.branch_url)
274
 
        self.branch_title = branch_title
275
 
        self.branch_description = branch_description
276
 
        self.author_email = author_email
277
 
        self.product_name = product_name
 
48
            self.branch_id = self._find_default_branch_id(self.branch_url)
 
49
        self.branch_title = ''
 
50
        self.branch_description = ''
 
51
        self.author_email = ''
 
52
        self.product_name = ''
 
53
        self.service_url = self.DEFAULT_SERVICE_URL
 
54
        self.registrant_email = 'testuser@launchpad.net'
 
55
        self.registrant_password = 'testpassword'
278
56
 
279
57
    def _request_params(self):
280
58
        """Return xmlrpc request parameters"""
281
59
        # This must match the parameter tuple expected by Launchpad for this
282
60
        # method
283
61
        return (self.branch_url,
284
 
                self.branch_name,
 
62
                self.branch_id,
285
63
                self.branch_title,
286
64
                self.branch_description,
287
65
                self.author_email,
288
66
                self.product_name,
289
67
               )
290
68
 
291
 
    def _find_default_branch_name(self, branch_url):
 
69
    def submit(self, transport=None):
 
70
        """Submit registration request to the server.
 
71
        
 
72
        The particular server to use is set in self.service_url; this 
 
73
        should only need to be changed for testing.
 
74
 
 
75
        :param transport: If non-null, use a special xmlrpclib.Transport
 
76
            to send the request.  This has no connection to bzrlib
 
77
            Transports.
 
78
        """
 
79
        # auth info must be in url
 
80
        scheme, hostinfo, path = urlsplit(self.service_url)[:3]
 
81
        assert '@' not in hostinfo
 
82
        hostinfo = '%s:%s@%s' % (quote(self.registrant_email),
 
83
                                 quote(self.registrant_password),
 
84
                                 hostinfo)
 
85
        url = urlunsplit((scheme, hostinfo, path, '', ''))
 
86
        proxy = xmlrpclib.ServerProxy(url, transport=transport)
 
87
        proxy.register_branch(*self._request_params())
 
88
 
 
89
    def _find_default_branch_id(self, branch_url):
292
90
        i = branch_url.rfind('/')
293
91
        return branch_url[i+1:]
294
92
 
295
 
 
296
 
class BranchBugLinkRequest(BaseRequest):
297
 
    """Request to link a bzr branch in Launchpad to a bug."""
298
 
 
299
 
    _methodname = 'link_branch_to_bug'
300
 
 
301
 
    def __init__(self, branch_url, bug_id):
302
 
        self.bug_id = bug_id
303
 
        self.branch_url = branch_url
304
 
 
305
 
    def _request_params(self):
306
 
        """Return xmlrpc request parameters"""
307
 
        # This must match the parameter tuple expected by Launchpad for this
308
 
        # method
309
 
        return (self.branch_url, self.bug_id, '')
310
 
 
311
 
 
312
 
class ResolveLaunchpadPathRequest(BaseRequest):
313
 
    """Request to resolve the path component of an lp: URL."""
314
 
 
315
 
    _methodname = 'resolve_lp_path'
316
 
    _authenticated = False
317
 
 
318
 
    def __init__(self, path):
319
 
        if not path:
320
 
            raise errors.InvalidURL(path=path,
321
 
                                    extra="You must specify a project.")
322
 
        self.path = path
323
 
 
324
 
    def _request_params(self):
325
 
        """Return xmlrpc request parameters"""
326
 
        return (self.path,)
 
93
def register_interactive(branch_url):
 
94
    """Register a branch, prompting for a password if needed."""
 
95
    rego = BranchRegistrationRequest(branch_url, '')
 
96
    config = bzrlib.config.GlobalConfig()
 
97
    rego.registrant_email = config.user_email()
 
98
    prompt = 'launchpad.net password for %s: ' % \
 
99
            rego.registrant_email
 
100
    rego.registrant_password = getpass(prompt)
 
101
    rego.submit()