Merge lp:~cjwatson/launchpad/wsgi-ppa-auth into lp:launchpad

Proposed by Colin Watson
Status: Rejected
Rejected by: Colin Watson
Proposed branch: lp:~cjwatson/launchpad/wsgi-ppa-auth
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/virtualenv-pip
Diff against target: 1113 lines (+768/-71)
20 files modified
Makefile (+1/-0)
configs/development/local-launchpad-apache (+30/-3)
lib/lp/services/config/schema-lazr.conf (+5/-0)
lib/lp/services/memcache/client.py (+13/-51)
lib/lp/services/memcache/testing.py (+25/-4)
lib/lp/services/memcache/timeline.py (+57/-0)
lib/lp/soyuz/doc/archiveauthtoken.txt (+24/-1)
lib/lp/soyuz/interfaces/archiveapi.py (+46/-0)
lib/lp/soyuz/interfaces/archiveauthtoken.py (+16/-6)
lib/lp/soyuz/model/archiveauthtoken.py (+25/-5)
lib/lp/soyuz/wsgi/archiveauth.py (+83/-0)
lib/lp/soyuz/wsgi/tests/test_archiveauth.py (+151/-0)
lib/lp/soyuz/xmlrpc/archive.py (+62/-0)
lib/lp/soyuz/xmlrpc/tests/test_archive.py (+124/-0)
lib/lp/systemhomes.py (+7/-0)
lib/lp/xmlrpc/application.py (+7/-1)
lib/lp/xmlrpc/configure.zcml (+13/-0)
lib/lp/xmlrpc/interfaces.py (+2/-0)
scripts/wsgi-archive-auth.py (+71/-0)
utilities/rocketfuel-setup (+6/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/wsgi-ppa-auth
Reviewer Review Type Date Requested Status
Launchpad code reviewers Pending
Review via email: mp+332125@code.launchpad.net

Commit message

Add a WSGI authenticator for private PPAs.

Description of the change

I got sufficiently annoyed in the process of fixing bug 1722209 to see how hard it would be to fix it properly, since the existing htpasswd scheme has been a thorn in our side for a while now. The answer appears to be "a bit, but not very".

This will let us remove our reliance on htpasswd files, and probably eventually make it easier to do things like issue time-limited tokens (or even macaroons?) to builders.

There are obviously various deployment issues to sort out, and generate-ppa-htaccess still deals with things like deactivation and cancellation emails which we'll need to move elsewhere; it may be possible to just inline the relevant checks into the DB queries in ArchiveAuthTokenSet, since we're always looking at a single subscriber or named token name and so the result set sizes are very small.

The way that mod_wsgi loads the entry point is peculiar. I tried to handle it with the existing buildout-based build system, but that ended up being too fiddly, so I decided to just depend on my virtualenv/pip conversion branch. I considered separating the client code off entirely from Launchpad, but this turns out to be light enough in terms of memory use and startup time that I don't think it's worth the effort at the moment.

To post a comment you must log in.
lp:~cjwatson/launchpad/wsgi-ppa-auth updated
18475. By Colin Watson

Beef up MemcacheFixture a bit to support expiry times and to reject non-str keys.

18476. By Colin Watson

Separate out request-timeline handling so that memcache_client_factory can be used to create a basic client with minimal dependencies.

18477. By Colin Watson

Use memcached instead of timedcache, storing hashed passwords.

18478. By Colin Watson

Merge virtualenv-pip.

Revision history for this message
Colin Watson (cjwatson) wrote :

I've rewritten part of this using memcached rather than timedcache, so the cost of using multiple workers here should now be negligible.

Revision history for this message
Colin Watson (cjwatson) wrote :

Unmerged revisions

18478. By Colin Watson

Merge virtualenv-pip.

18477. By Colin Watson

Use memcached instead of timedcache, storing hashed passwords.

18476. By Colin Watson

Separate out request-timeline handling so that memcache_client_factory can be used to create a basic client with minimal dependencies.

18475. By Colin Watson

Beef up MemcacheFixture a bit to support expiry times and to reject non-str keys.

18474. By Colin Watson

Add a WSGI authenticator for private PPAs.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'Makefile'
--- Makefile 2017-11-19 12:35:56 +0000
+++ Makefile 2017-11-19 12:35:56 +0000
@@ -463,6 +463,7 @@
463 base=local-launchpad; \463 base=local-launchpad; \
464 fi; \464 fi; \
465 sed -e 's,%BRANCH_REWRITE%,$(shell pwd)/scripts/branch-rewrite.py,' \465 sed -e 's,%BRANCH_REWRITE%,$(shell pwd)/scripts/branch-rewrite.py,' \
466 -e 's,%WSGI_ARCHIVE_AUTH%,$(shell pwd)/scripts/wsgi-archive-auth.py,' \
466 -e 's,%LISTEN_ADDRESS%,$(LISTEN_ADDRESS),' \467 -e 's,%LISTEN_ADDRESS%,$(LISTEN_ADDRESS),' \
467 configs/development/local-launchpad-apache > \468 configs/development/local-launchpad-apache > \
468 /etc/apache2/sites-available/$$base469 /etc/apache2/sites-available/$$base
469470
=== modified file 'configs/development/local-launchpad-apache'
--- configs/development/local-launchpad-apache 2017-01-10 17:26:29 +0000
+++ configs/development/local-launchpad-apache 2017-11-19 12:35:56 +0000
@@ -134,7 +134,6 @@
134134
135<VirtualHost %LISTEN_ADDRESS%:80>135<VirtualHost %LISTEN_ADDRESS%:80>
136 ServerName ppa.launchpad.dev136 ServerName ppa.launchpad.dev
137 ServerAlias private-ppa.launchpad.dev
138 LogLevel debug137 LogLevel debug
139138
140 DocumentRoot /var/tmp/ppa139 DocumentRoot /var/tmp/ppa
@@ -147,8 +146,36 @@
147 Deny from all146 Deny from all
148 Allow from 127.0.0.0/255.0.0.0147 Allow from 127.0.0.0/255.0.0.0
149 </IfVersion>148 </IfVersion>
150 AllowOverride AuthConfig149 AllowOverride None
151 Options Indexes150 Options Indexes
151 </Directory>
152</VirtualHost>
153
154<VirtualHost %LISTEN_ADDRESS%:80>
155 ServerName private-ppa.launchpad.dev
156 LogLevel debug
157
158 DocumentRoot /var/tmp/ppa
159 <Directory /var/tmp/ppa/>
160 <IfVersion >= 2.4>
161 <RequireAll>
162 Require ip 127.0.0.0/255.0.0.0
163 Require valid-user
164 </RequireAll>
165 </IfVersion>
166 <IfVersion < 2.4>
167 Order Deny,Allow
168 Deny from all
169 Allow from 127.0.0.0/255.0.0.0
170 Require valid-user
171 Satisfy All
172 </IfVersion>
173 AllowOverride None
174 Options Indexes
175 AuthType Basic
176 AuthName "Token Required"
177 AuthBasicProvider wsgi
178 WSGIAuthUserScript %WSGI_ARCHIVE_AUTH% application-group=lp
152 </Directory>179 </Directory>
153</VirtualHost>180</VirtualHost>
154181
155182
=== modified file 'lib/lp/services/config/schema-lazr.conf'
--- lib/lp/services/config/schema-lazr.conf 2017-09-07 13:25:13 +0000
+++ lib/lp/services/config/schema-lazr.conf 2017-11-19 12:35:56 +0000
@@ -1460,6 +1460,11 @@
1460# datatype: boolean1460# datatype: boolean
1461require_signing_keys: false1461require_signing_keys: false
14621462
1463# The URL to the internal archive API endpoint. This should implement
1464# IArchiveAPI.
1465# datatype: string
1466archive_api_endpoint: http://xmlrpc-private.launchpad.dev:8087/archive
1467
14631468
1464[ppa_apache_log_parser]1469[ppa_apache_log_parser]
1465logs_root: /srv/ppa.launchpad.net-logs1470logs_root: /srv/ppa.launchpad.net-logs
14661471
=== modified file 'lib/lp/services/memcache/client.py'
--- lib/lp/services/memcache/client.py 2017-09-27 10:59:13 +0000
+++ lib/lp/services/memcache/client.py 2017-11-19 12:35:56 +0000
@@ -4,64 +4,26 @@
4"""Launchpad Memcache client."""4"""Launchpad Memcache client."""
55
6__metaclass__ = type6__metaclass__ = type
7__all__ = []7__all__ = [
8 'memcache_client_factory',
9 ]
810
9import logging
10import re11import re
1112
12from lazr.restful.utils import get_current_browser_request
13import memcache
14
15from lp.services import features
16from lp.services.config import config13from lp.services.config import config
17from lp.services.timeline.requesttimeline import get_request_timeline14
1815
1916def memcache_client_factory(timeline=True):
20def memcache_client_factory():
21 """Return a memcache.Client for Launchpad."""17 """Return a memcache.Client for Launchpad."""
22 servers = [18 servers = [
23 (host, int(weight)) for host, weight in re.findall(19 (host, int(weight)) for host, weight in re.findall(
24 r'\((.+?),(\d+)\)', config.memcache.servers)]20 r'\((.+?),(\d+)\)', config.memcache.servers)]
25 assert len(servers) > 0, "Invalid memcached server list %r" % (21 assert len(servers) > 0, "Invalid memcached server list %r" % (
26 config.memcache.servers,)22 config.memcache.servers,)
27 return TimelineRecordingClient(servers)23 if timeline:
2824 from lp.services.memcache.timeline import TimelineRecordingClient
2925 client_factory = TimelineRecordingClient
30class TimelineRecordingClient(memcache.Client):26 else:
3127 import memcache
32 def __get_timeline_action(self, suffix, key):28 client_factory = memcache.Client
33 request = get_current_browser_request()29 return client_factory(servers)
34 timeline = get_request_timeline(request)
35 return timeline.start("memcache-%s" % suffix, key)
36
37 @property
38 def _enabled(self):
39 configured_value = features.getFeatureFlag('memcache')
40 if configured_value is None:
41 return True
42 else:
43 return configured_value
44
45 def get(self, key):
46 if not self._enabled:
47 return None
48 action = self.__get_timeline_action("get", key)
49 try:
50 return memcache.Client.get(self, key)
51 finally:
52 action.finish()
53
54 def set(self, key, value, time=0, min_compress_len=0):
55 if not self._enabled:
56 return None
57 action = self.__get_timeline_action("set", key)
58 try:
59 success = memcache.Client.set(self, key, value, time=time,
60 min_compress_len=min_compress_len)
61 if success:
62 logging.debug("Memcache set succeeded for %s", key)
63 else:
64 logging.warn("Memcache set failed for %s", key)
65 return success
66 finally:
67 action.finish()
6830
=== modified file 'lib/lp/services/memcache/testing.py'
--- lib/lp/services/memcache/testing.py 2016-09-07 03:43:36 +0000
+++ lib/lp/services/memcache/testing.py 2017-11-19 12:35:56 +0000
@@ -1,4 +1,4 @@
1# Copyright 2016 Canonical Ltd. This software is licensed under the1# Copyright 2016-2017 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -6,6 +6,8 @@
6 'MemcacheFixture',6 'MemcacheFixture',
7 ]7 ]
88
9import time as _time
10
9import fixtures11import fixtures
1012
11from lp.services.memcache.interfaces import IMemcacheClient13from lp.services.memcache.interfaces import IMemcacheClient
@@ -22,14 +24,33 @@
22 super(MemcacheFixture, self).setUp()24 super(MemcacheFixture, self).setUp()
23 self.useFixture(ZopeUtilityFixture(self, IMemcacheClient))25 self.useFixture(ZopeUtilityFixture(self, IMemcacheClient))
2426
27 def check_key(self, key):
28 # A subset of the checks performed by the real memcache library;
29 # this one is particularly easy to get wrong in Launchpad code.
30 if not isinstance(key, str):
31 raise TypeError("Key must be str.")
32
25 def get(self, key):33 def get(self, key):
26 return self._cache.get(key)34 self.check_key(key)
35 value, expiry_time = self._cache.get(key, (None, None))
36 if expiry_time and _time.time() >= expiry_time:
37 self.delete(key)
38 return None
39 else:
40 return value
2741
28 def set(self, key, val):42 def set(self, key, val, time=0):
29 self._cache[key] = val43 self.check_key(key)
44 # memcached accepts either delta-seconds from the current time or
45 # absolute epoch-seconds, and tells them apart using a magic
46 # threshold. See memcached/memcached.c:realtime.
47 if time and time <= 60 * 60 * 24 * 30:
48 time = _time.time() + time
49 self._cache[key] = (val, time)
30 return 150 return 1
3151
32 def delete(self, key):52 def delete(self, key):
53 self.check_key(key)
33 self._cache.pop(key, None)54 self._cache.pop(key, None)
34 return 155 return 1
3556
3657
=== added file 'lib/lp/services/memcache/timeline.py'
--- lib/lp/services/memcache/timeline.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/memcache/timeline.py 2017-11-19 12:35:56 +0000
@@ -0,0 +1,57 @@
1# Copyright 2017 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Timeline-friendly Launchpad Memcache client."""
5
6__metaclass__ = type
7__all__ = [
8 'TimelineRecordingClient',
9 ]
10
11import logging
12
13from lazr.restful.utils import get_current_browser_request
14import memcache
15
16from lp.services import features
17from lp.services.timeline.requesttimeline import get_request_timeline
18
19
20class TimelineRecordingClient(memcache.Client):
21
22 def __get_timeline_action(self, suffix, key):
23 request = get_current_browser_request()
24 timeline = get_request_timeline(request)
25 return timeline.start("memcache-%s" % suffix, key)
26
27 @property
28 def _enabled(self):
29 configured_value = features.getFeatureFlag('memcache')
30 if configured_value is None:
31 return True
32 else:
33 return configured_value
34
35 def get(self, key):
36 if not self._enabled:
37 return None
38 action = self.__get_timeline_action("get", key)
39 try:
40 return memcache.Client.get(self, key)
41 finally:
42 action.finish()
43
44 def set(self, key, value, time=0, min_compress_len=0):
45 if not self._enabled:
46 return None
47 action = self.__get_timeline_action("set", key)
48 try:
49 success = memcache.Client.set(self, key, value, time=time,
50 min_compress_len=min_compress_len)
51 if success:
52 logging.debug("Memcache set succeeded for %s", key)
53 else:
54 logging.warn("Memcache set failed for %s", key)
55 return success
56 finally:
57 action.finish()
058
=== modified file 'lib/lp/soyuz/doc/archiveauthtoken.txt'
--- lib/lp/soyuz/doc/archiveauthtoken.txt 2012-04-10 14:01:17 +0000
+++ lib/lp/soyuz/doc/archiveauthtoken.txt 2017-11-19 12:35:56 +0000
@@ -139,12 +139,35 @@
139 ... print token.person.name139 ... print token.person.name
140 bradsmith140 bradsmith
141141
142Tokens can also be retreived by archive and person:142Tokens can also be retrieved by archive and person:
143143
144 >>> print token_set.getActiveTokenForArchiveAndPerson(144 >>> print token_set.getActiveTokenForArchiveAndPerson(
145 ... new_token.archive, new_token.person).token145 ... new_token.archive, new_token.person).token
146 testtoken146 testtoken
147147
148Or by archive and person name:
149
150 >>> print token_set.getActiveTokenForArchiveAndPersonName(
151 ... new_token.archive, "bradsmith").token
152 testtoken
153
154Tokens are only returned if they match a current subscription:
155
156 >>> from zope.security.proxy import removeSecurityProxy
157 >>> from lp.soyuz.enums import ArchiveSubscriberStatus
158 >>> removeSecurityProxy(subscription_to_joe_private_ppa).status = (
159 ... ArchiveSubscriberStatus.EXPIRED)
160
161 >>> print token_set.getActiveTokenForArchiveAndPerson(
162 ... new_token.archive, new_token.person)
163 None
164 >>> print token_set.getActiveTokenForArchiveAndPersonName(
165 ... new_token.archive, "bradsmith")
166 None
167
168 >>> removeSecurityProxy(subscription_to_joe_private_ppa).status = (
169 ... ArchiveSubscriberStatus.CURRENT)
170
148171
149== Amending Tokens ==172== Amending Tokens ==
150173
151174
=== added file 'lib/lp/soyuz/interfaces/archiveapi.py'
--- lib/lp/soyuz/interfaces/archiveapi.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/interfaces/archiveapi.py 2017-11-19 12:35:56 +0000
@@ -0,0 +1,46 @@
1# Copyright 2017 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Interfaces for internal archive APIs."""
5
6from __future__ import absolute_import, print_function, unicode_literals
7
8__metaclass__ = type
9__all__ = [
10 'IArchiveAPI',
11 'IArchiveApplication',
12 ]
13
14from zope.interface import Interface
15
16from lp.services.webapp.interfaces import ILaunchpadApplication
17
18
19class IArchiveApplication(ILaunchpadApplication):
20 """Archive application root."""
21
22
23class IArchiveAPI(Interface):
24 """The Soyuz archive XML-RPC interface to Launchpad.
25
26 Published at "archive" on the private XML-RPC server.
27
28 PPA frontends use this to check archive authorization tokens.
29 """
30
31 def checkArchiveAuthToken(archive_reference, username, password):
32 """Check an archive authorization token.
33
34 :param archive_reference: The reference form of the archive to check.
35 :param username: The username sent using HTTP Basic Authentication;
36 this should either be a `Person.name` or "+" followed by the
37 name of a named authorization token.
38 :param password: The password sent using HTTP Basic Authentication;
39 this should be a corresponding `ArchiveAuthToken.token`.
40
41 :returns: A `NotFound` fault if `archive_reference` does not
42 identify an archive or the username does not identify a valid
43 token for this archive; an `Unauthorized` fault if the password
44 is not equal to the selected token for this archive; otherwise
45 None.
46 """
047
=== modified file 'lib/lp/soyuz/interfaces/archiveauthtoken.py'
--- lib/lp/soyuz/interfaces/archiveauthtoken.py 2016-07-14 16:06:01 +0000
+++ lib/lp/soyuz/interfaces/archiveauthtoken.py 2017-11-19 12:35:56 +0000
@@ -96,23 +96,33 @@
96 :return: An object conforming to `IArchiveAuthToken`.96 :return: An object conforming to `IArchiveAuthToken`.
97 """97 """
9898
99 def getByArchive(archive):99 def getByArchive(archive, valid=False):
100 """Retrieve all the tokens for an archive.100 """Retrieve all the tokens for an archive.
101101
102 :param archive: The context archive.102 :param archive: The context archive.
103 :param valid: If True, only return valid tokens.
103 :return: A result set containing `IArchiveAuthToken`s.104 :return: A result set containing `IArchiveAuthToken`s.
104 """105 """
105106
106 def getActiveTokenForArchiveAndPerson(archive, person):107 def getActiveTokenForArchiveAndPerson(archive, person):
107 """Retrieve an active token for the given archive and person.108 """Retrieve a valid active token for the given archive and person.
108109
109 :param archive: The archive to which the token corresponds.110 :param archive: The archive to which the token corresponds.
110 :param person: The person to which the token corresponds.111 :param person: The person to which the token corresponds.
111 :return: An `IArchiveAuthToken` or None.112 :return: An `IArchiveAuthToken` or None.
112 """113 """
113114
115 def getActiveTokenForArchiveAndPersonName(archive, person_name):
116 """Retrieve a valid active token for the given archive and person name.
117
118 :param archive: The archive to which the token corresponds.
119 :param person_name: The name of the person to which the token
120 corresponds.
121 :return: An `IArchiveAuthToken` or None.
122 """
123
114 def getActiveNamedTokenForArchive(archive, name):124 def getActiveNamedTokenForArchive(archive, name):
115 """Retrieve an active named token for the given archive and name.125 """Retrieve a valid active named token for the given archive and name.
116126
117 :param archive: The archive to which the token corresponds.127 :param archive: The archive to which the token corresponds.
118 :param name: The name of a named authorization token.128 :param name: The name of a named authorization token.
@@ -120,9 +130,9 @@
120 """130 """
121131
122 def getActiveNamedTokensForArchive(archive, names=None):132 def getActiveNamedTokensForArchive(archive, names=None):
123 """Retrieve a subset of active named tokens for the given archive if133 """Retrieve a subset of valid active named tokens for the given
124 `names` is specified, or all active named tokens for the archive if134 archive if `names` is specified, or all valid active named tokens
125 `names` is null.135 for the archive if `names` is null.
126136
127 :param archive: The archive to which the tokens correspond.137 :param archive: The archive to which the tokens correspond.
128 :param names: An optional list of token names.138 :param names: An optional list of token names.
129139
=== modified file 'lib/lp/soyuz/model/archiveauthtoken.py'
--- lib/lp/soyuz/model/archiveauthtoken.py 2016-07-14 16:06:01 +0000
+++ lib/lp/soyuz/model/archiveauthtoken.py 2017-11-19 12:35:56 +0000
@@ -21,8 +21,10 @@
21from storm.store import Store21from storm.store import Store
22from zope.interface import implementer22from zope.interface import implementer
2323
24from lp.registry.model.teammembership import TeamParticipation
24from lp.services.database.constants import UTC_NOW25from lp.services.database.constants import UTC_NOW
25from lp.services.database.interfaces import IStore26from lp.services.database.interfaces import IStore
27from lp.soyuz.enums import ArchiveSubscriberStatus
26from lp.soyuz.interfaces.archiveauthtoken import (28from lp.soyuz.interfaces.archiveauthtoken import (
27 IArchiveAuthToken,29 IArchiveAuthToken,
28 IArchiveAuthTokenSet,30 IArchiveAuthTokenSet,
@@ -85,19 +87,37 @@
85 return IStore(ArchiveAuthToken).find(87 return IStore(ArchiveAuthToken).find(
86 ArchiveAuthToken, ArchiveAuthToken.token == token).one()88 ArchiveAuthToken, ArchiveAuthToken.token == token).one()
8789
88 def getByArchive(self, archive):90 def getByArchive(self, archive, valid=False):
89 """See `IArchiveAuthTokenSet`."""91 """See `IArchiveAuthTokenSet`."""
92 # Circular import.
93 from lp.soyuz.model.archivesubscriber import ArchiveSubscriber
90 store = Store.of(archive)94 store = Store.of(archive)
91 return store.find(95 clauses = [
92 ArchiveAuthToken,
93 ArchiveAuthToken.archive == archive,96 ArchiveAuthToken.archive == archive,
94 ArchiveAuthToken.date_deactivated == None)97 ArchiveAuthToken.date_deactivated == None,
98 ]
99 if valid:
100 clauses.extend([
101 ArchiveAuthToken.archive_id == ArchiveSubscriber.archive_id,
102 ArchiveSubscriber.status == ArchiveSubscriberStatus.CURRENT,
103 ArchiveSubscriber.subscriber_id == TeamParticipation.teamID,
104 TeamParticipation.personID == ArchiveAuthToken.person_id,
105 ])
106 return store.find(ArchiveAuthToken, *clauses)
95107
96 def getActiveTokenForArchiveAndPerson(self, archive, person):108 def getActiveTokenForArchiveAndPerson(self, archive, person):
97 """See `IArchiveAuthTokenSet`."""109 """See `IArchiveAuthTokenSet`."""
98 return self.getByArchive(archive).find(110 return self.getByArchive(archive, valid=True).find(
99 ArchiveAuthToken.person == person).one()111 ArchiveAuthToken.person == person).one()
100112
113 def getActiveTokenForArchiveAndPersonName(self, archive, person_name):
114 """See `IArchiveAuthTokenSet`."""
115 # Circular import.
116 from lp.registry.model.person import Person
117 return self.getByArchive(archive, valid=True).find(
118 ArchiveAuthToken.person == Person.id,
119 Person.name == person_name).one()
120
101 def getActiveNamedTokenForArchive(self, archive, name):121 def getActiveNamedTokenForArchive(self, archive, name):
102 """See `IArchiveAuthTokenSet`."""122 """See `IArchiveAuthTokenSet`."""
103 return self.getByArchive(archive).find(123 return self.getByArchive(archive).find(
104124
=== added directory 'lib/lp/soyuz/wsgi'
=== added file 'lib/lp/soyuz/wsgi/__init__.py'
=== added file 'lib/lp/soyuz/wsgi/archiveauth.py'
--- lib/lp/soyuz/wsgi/archiveauth.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/wsgi/archiveauth.py 2017-11-19 12:35:56 +0000
@@ -0,0 +1,83 @@
1# Copyright 2017 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""WSGI archive authorisation provider.
5
6This is as lightweight as possible, as it runs on PPA frontends.
7"""
8
9from __future__ import absolute_import, print_function, unicode_literals
10
11__metaclass__ = type
12__all__ = [
13 'check_password',
14 ]
15
16import crypt
17from random import SystemRandom
18import string
19import time
20try:
21 from xmlrpc.client import (
22 Fault,
23 ServerProxy,
24 )
25except ImportError:
26 from xmlrpclib import (
27 Fault,
28 ServerProxy,
29 )
30
31from lp.services.config import config
32from lp.services.memcache.client import memcache_client_factory
33
34
35def _get_archive_reference(environ):
36 # Reconstruct the relevant part of the URL. We don't care about where
37 # we're installed.
38 path = environ.get("SCRIPT_NAME") or "/"
39 path_info = environ.get("PATH_INFO", "")
40 path += (path_info if path else path_info[1:])
41 # Extract the first three segments of the path, and rearrange them to
42 # form an archive reference.
43 path_parts = path.lstrip("/").split("/")
44 if len(path_parts) >= 3:
45 return "~%s/%s/%s" % (path_parts[0], path_parts[2], path_parts[1])
46
47
48_sr = SystemRandom()
49
50
51def _crypt_sha256(word):
52 """crypt.crypt(word, crypt.METHOD_SHA256), backported from Python 3.5."""
53 saltchars = string.ascii_letters + string.digits + './'
54 salt = '$5$' + ''.join(_sr.choice(saltchars) for _ in range(16))
55 return crypt.crypt(word, salt)
56
57
58_memcache_client = memcache_client_factory(timeline=False)
59
60
61def check_password(environ, user, password):
62 archive_reference = _get_archive_reference(environ)
63 if archive_reference is None:
64 return None
65 memcache_key = (
66 "archive-auth:%s:%s" % (archive_reference, user)).encode("UTF-8")
67 crypted_password = _memcache_client.get(memcache_key)
68 if (crypted_password and
69 crypt.crypt(password, crypted_password) == crypted_password):
70 return True
71 proxy = ServerProxy(config.personalpackagearchive.archive_api_endpoint)
72 try:
73 proxy.checkArchiveAuthToken(archive_reference, user, password)
74 # Cache positive responses for a minute to reduce database load.
75 _memcache_client.set(
76 memcache_key, _crypt_sha256(password), time.time() + 60)
77 return True
78 except Fault as e:
79 if e.faultCode == 410: # Unauthorized
80 return False
81 else:
82 # Interpret any other fault as NotFound (320).
83 return None
084
=== added directory 'lib/lp/soyuz/wsgi/tests'
=== added file 'lib/lp/soyuz/wsgi/tests/__init__.py'
=== added file 'lib/lp/soyuz/wsgi/tests/test_archiveauth.py'
--- lib/lp/soyuz/wsgi/tests/test_archiveauth.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/wsgi/tests/test_archiveauth.py 2017-11-19 12:35:56 +0000
@@ -0,0 +1,151 @@
1# Copyright 2017 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for the WSGI archive authorisation provider."""
5
6from __future__ import absolute_import, print_function, unicode_literals
7
8__metaclass__ = type
9
10import crypt
11import os.path
12import subprocess
13import time
14
15from fixtures import MonkeyPatch
16from testtools.matchers import Is
17import transaction
18
19from lp.services.config import config
20from lp.services.memcache.testing import MemcacheFixture
21from lp.soyuz.wsgi import archiveauth
22from lp.testing import TestCaseWithFactory
23from lp.testing.layers import ZopelessAppServerLayer
24from lp.xmlrpc import faults
25
26
27class TestWSGIArchiveAuth(TestCaseWithFactory):
28
29 layer = ZopelessAppServerLayer
30
31 def setUp(self):
32 super(TestWSGIArchiveAuth, self).setUp()
33 self.now = time.time()
34 self.useFixture(MonkeyPatch("time.time", lambda: self.now))
35 self.memcache_fixture = self.useFixture(MemcacheFixture())
36 # The WSGI provider doesn't use Zope, so we can't rely on the
37 # fixture substituting a Zope utility.
38 self.useFixture(MonkeyPatch(
39 "lp.soyuz.wsgi.archiveauth._memcache_client",
40 self.memcache_fixture))
41
42 def test_get_archive_reference_short_url(self):
43 self.assertIsNone(archiveauth._get_archive_reference(
44 {"SCRIPT_NAME": "/foo"}))
45
46 def test_get_archive_reference_archive_base(self):
47 self.assertEqual(
48 "~user/ubuntu/ppa",
49 archiveauth._get_archive_reference(
50 {"SCRIPT_NAME": "/user/ppa/ubuntu"}))
51
52 def test_get_archive_reference_inside_archive(self):
53 self.assertEqual(
54 "~user/ubuntu/ppa",
55 archiveauth._get_archive_reference(
56 {"SCRIPT_NAME": "/user/ppa/ubuntu/dists"}))
57
58 def test_check_password_short_url(self):
59 self.assertIsNone(archiveauth.check_password(
60 {"SCRIPT_NAME": "/foo"}, "user", ""))
61 self.assertEqual({}, self.memcache_fixture._cache)
62
63 def test_check_password_not_found(self):
64 self.assertIsNone(archiveauth.check_password(
65 {"SCRIPT_NAME": "/nonexistent/bad/unknown"}, "user", ""))
66 self.assertEqual({}, self.memcache_fixture._cache)
67
68 def test_crypt_sha256(self):
69 crypted_password = archiveauth._crypt_sha256("secret")
70 self.assertEqual(
71 crypted_password, crypt.crypt("secret", crypted_password))
72
73 def makeArchiveAndToken(self):
74 archive = self.factory.makeArchive(private=True)
75 archive_path = "/%s/%s/ubuntu" % (archive.owner.name, archive.name)
76 subscriber = self.factory.makePerson()
77 archive.newSubscription(subscriber, archive.owner)
78 token = archive.newAuthToken(subscriber)
79 transaction.commit()
80 return archive, archive_path, subscriber.name, token.token
81
82 def test_check_password_unauthorized(self):
83 _, archive_path, username, password = self.makeArchiveAndToken()
84 # Test that this returns False, not merely something falsy (e.g.
85 # None).
86 self.assertThat(
87 archiveauth.check_password(
88 {"SCRIPT_NAME": archive_path}, username, password + "-bad"),
89 Is(False))
90 self.assertEqual({}, self.memcache_fixture._cache)
91
92 def test_check_password_success(self):
93 archive, archive_path, username, password = self.makeArchiveAndToken()
94 self.assertThat(
95 archiveauth.check_password(
96 {"SCRIPT_NAME": archive_path}, username, password),
97 Is(True))
98 crypted_password = self.memcache_fixture.get(
99 ("archive-auth:%s:%s" % (archive.reference, username)).encode(
100 "UTF-8"))
101 self.assertEqual(
102 crypted_password, crypt.crypt(password, crypted_password))
103
104 def test_check_password_considers_cache(self):
105 class FakeProxy:
106 def __init__(self, uri):
107 pass
108
109 def checkArchiveAuthToken(self, archive_reference, username,
110 password):
111 raise faults.Unauthorized()
112
113 _, archive_path, username, password = self.makeArchiveAndToken()
114 self.assertThat(
115 archiveauth.check_password(
116 {"SCRIPT_NAME": archive_path}, username, password),
117 Is(True))
118 self.useFixture(
119 MonkeyPatch("lp.soyuz.wsgi.archiveauth.ServerProxy", FakeProxy))
120 # A subsequent check honours the cache.
121 self.assertThat(
122 archiveauth.check_password(
123 {"SCRIPT_NAME": archive_path}, username, password + "-bad"),
124 Is(False))
125 self.assertThat(
126 archiveauth.check_password(
127 {"SCRIPT_NAME": archive_path}, username, password),
128 Is(True))
129 # If we advance time far enough, then the cached result expires.
130 self.now += 60
131 self.assertThat(
132 archiveauth.check_password(
133 {"SCRIPT_NAME": archive_path}, username, password),
134 Is(False))
135
136 def test_script(self):
137 _, archive_path, username, password = self.makeArchiveAndToken()
138 script_path = os.path.join(
139 config.root, "scripts", "wsgi-archive-auth.py")
140
141 def check_via_script(archive_path, username, password):
142 with open(os.devnull, "w") as devnull:
143 return subprocess.call(
144 [script_path, archive_path, username, password],
145 stderr=devnull)
146
147 self.assertEqual(0, check_via_script(archive_path, username, password))
148 self.assertEqual(
149 1, check_via_script(archive_path, username, password + "-bad"))
150 self.assertEqual(
151 2, check_via_script("/nonexistent/bad/unknown", "user", ""))
0152
=== added file 'lib/lp/soyuz/xmlrpc/archive.py'
--- lib/lp/soyuz/xmlrpc/archive.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/xmlrpc/archive.py 2017-11-19 12:35:56 +0000
@@ -0,0 +1,62 @@
1# Copyright 2017 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Implementations of the XML-RPC APIs for Soyuz archives."""
5
6from __future__ import absolute_import, print_function, unicode_literals
7
8__metaclass__ = type
9__all__ = [
10 'ArchiveAPI',
11 ]
12
13from zope.component import getUtility
14from zope.interface import implementer
15from zope.security.proxy import removeSecurityProxy
16
17from lp.soyuz.interfaces.archive import IArchiveSet
18from lp.soyuz.interfaces.archiveapi import IArchiveAPI
19from lp.soyuz.interfaces.archiveauthtoken import IArchiveAuthTokenSet
20from lp.services.webapp import LaunchpadXMLRPCView
21from lp.xmlrpc import faults
22from lp.xmlrpc.helpers import return_fault
23
24
25BUILDD_USER_NAME = "buildd"
26
27
28@implementer(IArchiveAPI)
29class ArchiveAPI(LaunchpadXMLRPCView):
30 """See `IArchiveAPI`."""
31
32 @return_fault
33 def _checkArchiveAuthToken(self, archive_reference, username, password):
34 archive = getUtility(IArchiveSet).getByReference(archive_reference)
35 if archive is None:
36 raise faults.NotFound(
37 message="No archive found for '%s'." % archive_reference)
38 archive = removeSecurityProxy(archive)
39 token_set = getUtility(IArchiveAuthTokenSet)
40 if username == BUILDD_USER_NAME:
41 secret = archive.buildd_secret
42 else:
43 if username.startswith("+"):
44 token = token_set.getActiveNamedTokenForArchive(
45 archive, username[1:])
46 else:
47 token = token_set.getActiveTokenForArchiveAndPersonName(
48 archive, username)
49 if token is None:
50 raise faults.NotFound(
51 message="No valid tokens for '%s' in '%s'." % (
52 username, archive_reference))
53 secret = removeSecurityProxy(token).token
54 if password != secret:
55 raise faults.Unauthorized()
56
57 def checkArchiveAuthToken(self, archive_reference, username, password):
58 """See `IArchiveAPI`."""
59 # This thunk exists because you can't use a decorated function as
60 # the implementation of a method exported over XML-RPC.
61 return self._checkArchiveAuthToken(
62 archive_reference, username, password)
063
=== added directory 'lib/lp/soyuz/xmlrpc/tests'
=== added file 'lib/lp/soyuz/xmlrpc/tests/__init__.py'
=== added file 'lib/lp/soyuz/xmlrpc/tests/test_archive.py'
--- lib/lp/soyuz/xmlrpc/tests/test_archive.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/xmlrpc/tests/test_archive.py 2017-11-19 12:35:56 +0000
@@ -0,0 +1,124 @@
1# Copyright 2017 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for the internal Soyuz archive API."""
5
6from __future__ import absolute_import, print_function, unicode_literals
7
8__metaclass__ = type
9
10from zope.security.proxy import removeSecurityProxy
11
12from lp.services.features.testing import FeatureFixture
13from lp.soyuz.interfaces.archive import NAMED_AUTH_TOKEN_FEATURE_FLAG
14from lp.soyuz.xmlrpc.archive import ArchiveAPI
15from lp.testing import TestCaseWithFactory
16from lp.testing.layers import LaunchpadFunctionalLayer
17from lp.xmlrpc import faults
18
19
20class TestArchiveAPI(TestCaseWithFactory):
21
22 layer = LaunchpadFunctionalLayer
23
24 def setUp(self):
25 super(TestArchiveAPI, self).setUp()
26 self.useFixture(FeatureFixture({NAMED_AUTH_TOKEN_FEATURE_FLAG: "on"}))
27 self.archive_api = ArchiveAPI(None, None)
28
29 def assertNotFound(self, archive_reference, username, password, message):
30 """Assert that an archive auth token check returns NotFound."""
31 fault = self.archive_api.checkArchiveAuthToken(
32 archive_reference, username, password)
33 self.assertEqual(faults.NotFound(message), fault)
34
35 def assertUnauthorized(self, archive_reference, username, password):
36 """Assert that an archive auth token check returns Unauthorized."""
37 fault = self.archive_api.checkArchiveAuthToken(
38 archive_reference, username, password)
39 self.assertEqual(faults.Unauthorized("Authorisation required."), fault)
40
41 def test_checkArchiveAuthToken_unknown_archive(self):
42 self.assertNotFound(
43 "~nonexistent/unknown/bad", "user", "",
44 "No archive found for '~nonexistent/unknown/bad'.")
45
46 def test_checkArchiveAuthToken_no_tokens(self):
47 archive = removeSecurityProxy(self.factory.makeArchive(private=True))
48 self.assertNotFound(
49 archive.reference, "nobody", "",
50 "No valid tokens for 'nobody' in '%s'." % archive.reference)
51
52 def test_checkArchiveAuthToken_no_named_tokens(self):
53 archive = removeSecurityProxy(self.factory.makeArchive(private=True))
54 self.assertNotFound(
55 archive.reference, "+missing", "",
56 "No valid tokens for '+missing' in '%s'." % archive.reference)
57
58 def test_checkArchiveAuthToken_buildd_wrong_password(self):
59 archive = removeSecurityProxy(self.factory.makeArchive(private=True))
60 self.assertUnauthorized(
61 archive.reference, "buildd", archive.buildd_secret + "-bad")
62
63 def test_checkArchiveAuthToken_buildd_correct_password(self):
64 archive = removeSecurityProxy(self.factory.makeArchive(private=True))
65 self.assertIsNone(self.archive_api.checkArchiveAuthToken(
66 archive.reference, "buildd", archive.buildd_secret))
67
68 def test_checkArchiveAuthToken_named_token_wrong_password(self):
69 archive = removeSecurityProxy(self.factory.makeArchive(private=True))
70 token = archive.newNamedAuthToken("special")
71 removeSecurityProxy(token).deactivate()
72 self.assertNotFound(
73 archive.reference, "+special", token.token,
74 "No valid tokens for '+special' in '%s'." % archive.reference)
75
76 def test_checkArchiveAuthToken_named_token_deactivated(self):
77 archive = removeSecurityProxy(self.factory.makeArchive(private=True))
78 token = archive.newNamedAuthToken("special")
79 self.assertIsNone(self.archive_api.checkArchiveAuthToken(
80 archive.reference, "+special", token.token))
81
82 def test_checkArchiveAuthToken_named_token_correct_password(self):
83 archive = removeSecurityProxy(self.factory.makeArchive(private=True))
84 token = archive.newNamedAuthToken("special")
85 self.assertIsNone(self.archive_api.checkArchiveAuthToken(
86 archive.reference, "+special", token.token))
87
88 def test_checkArchiveAuthToken_personal_token_wrong_password(self):
89 archive = removeSecurityProxy(self.factory.makeArchive(private=True))
90 subscriber = self.factory.makePerson()
91 archive.newSubscription(subscriber, archive.owner)
92 token = archive.newAuthToken(subscriber)
93 self.assertUnauthorized(
94 archive.reference, subscriber.name, token.token + "-bad")
95
96 def test_checkArchiveAuthToken_personal_token_deactivated(self):
97 archive = removeSecurityProxy(self.factory.makeArchive(private=True))
98 subscriber = self.factory.makePerson()
99 archive.newSubscription(subscriber, archive.owner)
100 token = archive.newAuthToken(subscriber)
101 removeSecurityProxy(token).deactivate()
102 self.assertNotFound(
103 archive.reference, subscriber.name, token.token,
104 "No valid tokens for '%s' in '%s'." % (
105 subscriber.name, archive.reference))
106
107 def test_checkArchiveAuthToken_personal_token_cancelled(self):
108 archive = removeSecurityProxy(self.factory.makeArchive(private=True))
109 subscriber = self.factory.makePerson()
110 subscription = archive.newSubscription(subscriber, archive.owner)
111 token = archive.newAuthToken(subscriber)
112 removeSecurityProxy(subscription).cancel(archive.owner)
113 self.assertNotFound(
114 archive.reference, subscriber.name, token.token,
115 "No valid tokens for '%s' in '%s'." % (
116 subscriber.name, archive.reference))
117
118 def test_checkArchiveAuthToken_personal_token_correct_password(self):
119 archive = removeSecurityProxy(self.factory.makeArchive(private=True))
120 subscriber = self.factory.makePerson()
121 archive.newSubscription(subscriber, archive.owner)
122 token = archive.newAuthToken(subscriber)
123 self.assertIsNone(self.archive_api.checkArchiveAuthToken(
124 archive.reference, subscriber.name, token.token))
0125
=== modified file 'lib/lp/systemhomes.py'
--- lib/lp/systemhomes.py 2017-05-16 16:33:53 +0000
+++ lib/lp/systemhomes.py 2017-11-19 12:35:56 +0000
@@ -72,6 +72,7 @@
72from lp.services.webapp.publisher import canonical_url72from lp.services.webapp.publisher import canonical_url
73from lp.services.webservice.interfaces import IWebServiceApplication73from lp.services.webservice.interfaces import IWebServiceApplication
74from lp.services.worlddata.interfaces.language import ILanguageSet74from lp.services.worlddata.interfaces.language import ILanguageSet
75from lp.soyuz.interfaces.archiveapi import IArchiveApplication
75from lp.testopenid.interfaces.server import ITestOpenIDApplication76from lp.testopenid.interfaces.server import ITestOpenIDApplication
76from lp.translations.interfaces.translationgroup import ITranslationGroupSet77from lp.translations.interfaces.translationgroup import ITranslationGroupSet
77from lp.translations.interfaces.translations import IRosettaApplication78from lp.translations.interfaces.translations import IRosettaApplication
@@ -80,6 +81,12 @@
80 )81 )
8182
8283
84@implementer(IArchiveApplication)
85class ArchiveApplication:
86
87 title = "Archive API"
88
89
83@implementer(ICodehostingApplication)90@implementer(ICodehostingApplication)
84class CodehostingApplication:91class CodehostingApplication:
85 """Codehosting End-Point."""92 """Codehosting End-Point."""
8693
=== modified file 'lib/lp/xmlrpc/application.py'
--- lib/lp/xmlrpc/application.py 2015-10-26 14:54:43 +0000
+++ lib/lp/xmlrpc/application.py 2017-11-19 12:35:56 +0000
@@ -31,11 +31,12 @@
31from lp.services.features.xmlrpc import IFeatureFlagApplication31from lp.services.features.xmlrpc import IFeatureFlagApplication
32from lp.services.webapp import LaunchpadXMLRPCView32from lp.services.webapp import LaunchpadXMLRPCView
33from lp.services.webapp.interfaces import ILaunchBag33from lp.services.webapp.interfaces import ILaunchBag
34from lp.soyuz.interfaces.archiveapi import IArchiveApplication
34from lp.xmlrpc.interfaces import IPrivateApplication35from lp.xmlrpc.interfaces import IPrivateApplication
3536
3637
37# NOTE: If you add a traversal here, you should update38# NOTE: If you add a traversal here, you should update
38# the regular expression in utilities/page-performance-report.ini39# the regular expression in lp:lp-dev-utils page-performance-report.ini.
39@implementer(IPrivateApplication)40@implementer(IPrivateApplication)
40class PrivateApplication:41class PrivateApplication:
4142
@@ -45,6 +46,11 @@
45 return getUtility(IMailingListApplication)46 return getUtility(IMailingListApplication)
4647
47 @property48 @property
49 def archive(self):
50 """See `IPrivateApplication`."""
51 return getUtility(IArchiveApplication)
52
53 @property
48 def authserver(self):54 def authserver(self):
49 """See `IPrivateApplication`."""55 """See `IPrivateApplication`."""
50 return getUtility(IAuthServerApplication)56 return getUtility(IAuthServerApplication)
5157
=== modified file 'lib/lp/xmlrpc/configure.zcml'
--- lib/lp/xmlrpc/configure.zcml 2015-05-04 14:56:58 +0000
+++ lib/lp/xmlrpc/configure.zcml 2017-11-19 12:35:56 +0000
@@ -22,6 +22,19 @@
22 />22 />
2323
24 <securedutility24 <securedutility
25 class="lp.systemhomes.ArchiveApplication"
26 provides="lp.soyuz.interfaces.archiveapi.IArchiveApplication">
27 <allow interface="lp.soyuz.interfaces.archiveapi.IArchiveApplication"/>
28 </securedutility>
29
30 <xmlrpc:view
31 for="lp.soyuz.interfaces.archiveapi.IArchiveApplication"
32 interface="lp.soyuz.interfaces.archiveapi.IArchiveAPI"
33 class="lp.soyuz.xmlrpc.archive.ArchiveAPI"
34 permission="zope.Public"
35 />
36
37 <securedutility
25 class="lp.systemhomes.CodehostingApplication"38 class="lp.systemhomes.CodehostingApplication"
26 provides="lp.code.interfaces.codehosting.ICodehostingApplication">39 provides="lp.code.interfaces.codehosting.ICodehostingApplication">
27 <allow interface="lp.code.interfaces.codehosting.ICodehostingApplication"/>40 <allow interface="lp.code.interfaces.codehosting.ICodehostingApplication"/>
2841
=== modified file 'lib/lp/xmlrpc/interfaces.py'
--- lib/lp/xmlrpc/interfaces.py 2015-05-04 14:56:58 +0000
+++ lib/lp/xmlrpc/interfaces.py 2017-11-19 12:35:56 +0000
@@ -17,6 +17,8 @@
17class IPrivateApplication(ILaunchpadApplication):17class IPrivateApplication(ILaunchpadApplication):
18 """Launchpad private XML-RPC application root."""18 """Launchpad private XML-RPC application root."""
1919
20 archive = Attribute("Archive XML-RPC end point.""")
21
20 authserver = Attribute("""Old Authserver API end point.""")22 authserver = Attribute("""Old Authserver API end point.""")
2123
22 codeimportscheduler = Attribute("""Code import scheduler end point.""")24 codeimportscheduler = Attribute("""Code import scheduler end point.""")
2325
=== added file 'scripts/wsgi-archive-auth.py'
--- scripts/wsgi-archive-auth.py 1970-01-01 00:00:00 +0000
+++ scripts/wsgi-archive-auth.py 2017-11-19 12:35:56 +0000
@@ -0,0 +1,71 @@
1#!/usr/bin/python
2#
3# Copyright 2017 Canonical Ltd. This software is licensed under the
4# GNU Affero General Public License version 3 (see the file LICENSE).
5
6"""WSGI archive authorisation provider entry point.
7
8Unlike most Launchpad scripts, the #! line of this script does not use -S.
9This is because it is only executed (as opposed to imported) for testing,
10and mod_wsgi does not disable the automatic import of the site module when
11importing this script, so we want the test to imitate mod_wsgi's behaviour
12as closely as possible.
13"""
14
15from __future__ import absolute_import, print_function, unicode_literals
16
17__metaclass__ = type
18__all__ = [
19 'check_password',
20 ]
21
22# mod_wsgi imports this file without a useful sys.path, so we need some
23# acrobatics to set ourselves up properly.
24import os.path
25import sys
26
27scripts_dir = os.path.dirname(os.path.abspath(os.path.realpath(__file__)))
28if scripts_dir not in sys.path:
29 sys.path.insert(0, scripts_dir)
30top = os.path.dirname(scripts_dir)
31
32# We can't stop mod_wsgi importing the site module. Cross fingers and
33# arrange for it to be re-imported.
34sys.modules.pop("site", None)
35sys.modules.pop("sitecustomize", None)
36
37import _pythonpath
38
39from lp.soyuz.wsgi.archiveauth import check_password
40
41
42def main():
43 # Hook for testing, not used by WSGI.
44 from argparse import ArgumentParser
45
46 from lp.services.memcache.testing import MemcacheFixture
47 from lp.soyuz.wsgi import archiveauth
48
49 parser = ArgumentParser()
50 parser.add_argument("archive_path")
51 parser.add_argument("username")
52 parser.add_argument("password")
53 args = parser.parse_args()
54 archiveauth._memcache_client = MemcacheFixture()
55 result = check_password(
56 {"SCRIPT_NAME": args.archive_path}, args.username, args.password)
57 if result is None:
58 print("Archive or user does not exist.", file=sys.stderr)
59 return 2
60 elif result is False:
61 print("Password does not match.", file=sys.stderr)
62 return 1
63 elif result is True:
64 return 0
65 else:
66 print("Unexpected result from check_password: %s" % result)
67 return 3
68
69
70if __name__ == "__main__":
71 sys.exit(main())
072
=== modified file 'utilities/rocketfuel-setup'
--- utilities/rocketfuel-setup 2017-01-10 17:24:08 +0000
+++ utilities/rocketfuel-setup 2017-11-19 12:35:56 +0000
@@ -189,6 +189,12 @@
189 exit 1189 exit 1
190fi190fi
191191
192sudo a2enmod wsgi > /dev/null
193if [ $? -ne 0 ]; then
194 echo "ERROR: Unable to enable wsgi module in Apache2"
195 exit 1
196fi
197
192if [ $DO_WORKSPACE == 0 ]; then198if [ $DO_WORKSPACE == 0 ]; then
193 cat <<EOT199 cat <<EOT
194Branches have not been created, as requested. You will need to do some or all200Branches have not been created, as requested. You will need to do some or all