Merge lp:~pfalcon/linaro-license-protection/crowd-auth into lp:~linaro-automation/linaro-license-protection/trunk

Proposed by Paul Sokolovsky
Status: Superseded
Proposed branch: lp:~pfalcon/linaro-license-protection/crowd-auth
Merge into: lp:~linaro-automation/linaro-license-protection/trunk
Diff against target: 653 lines (+209/-131)
13 files modified
README (+23/-11)
license_protected_downloads/buildinfo.py (+10/-6)
license_protected_downloads/group_auth_crowd.py (+52/-0)
license_protected_downloads/group_auth_openid.py (+19/-32)
license_protected_downloads/tests/__init__.py (+2/-0)
license_protected_downloads/tests/test_buildinfo.py (+12/-3)
license_protected_downloads/tests/test_openid_auth.py (+11/-20)
license_protected_downloads/tests/test_splicebuildinfos.py (+3/-3)
license_protected_downloads/tests/test_views.py (+18/-0)
license_protected_downloads/tests/testserver_root/build-info/subdir2/BUILD-INFO.txt (+7/-0)
license_protected_downloads/tests/testserver_root/build-info/write-test/BUILD-INFO.txt (+0/-42)
license_protected_downloads/views.py (+43/-14)
settings.py (+9/-0)
To merge this branch: bzr merge lp:~pfalcon/linaro-license-protection/crowd-auth
Reviewer Review Type Date Requested Status
Georgy Redkozubov Approve
Milo Casagrande (community) Approve
Review via email: mp+166047@code.launchpad.net

This proposal has been superseded by a proposal from 2013-06-03.

Description of the change

These are changes to switch l-l-p to Crowd API for group access authorization. Expectably, I didn't tear off old "OpenID teams" code, but rather went for adding new Crowd authz code, and allow app to be configured for which of them to use, allowing seamless transition and reverting if needed.

Well, to support switching to another authz method at all, and such "pluggable" architecture in particular, it of course first took some refactoring on existing codebase.

Actually, I submit this MRs with just refactoring existing setup, which doesn't change current authz method. That's to get early comments, while I'm working on Crowd authz "plugin" specifically.

Suggested review mode is to look at the commit individually.

To post a comment you must log in.
Revision history for this message
Milo Casagrande (milo) wrote :

Paul,

thanks for kick-starting this work!
Overall the work looks great, and the refactoring too.

+class ViewHelpersTests(BaseServeViewTest):
+ def test_auth_group_error(self):
+ groups = ["linaro", "batman", "catwoman", "joker"]
+ request = Mock()
+ request.path = "mock_path"
+ response = views.group_auth_failed_response(request, groups)
+ self.assertIsNotNone(response)
+ self.assertTrue(isinstance(response, HttpResponse))
+ self.assertContains(response,
+ "You need to be the member of one of the linaro batman, catwoman "
+ "or joker teams in Launchpad.",
+ status_code=403)

Not something I'm really against, but I would be more interested in testing only
the status code I get, not the kind of "string" I will receive (to me that looks
already more than an unit test). I would test something like that:

self.failUnlessEqual(response.status_code, 403)

(not sure that that is working code though)

+def group_auth_failed_response(request, auth_groups):
+ """Construct a nice response detailing list of auth groups that
+ will allow access to the requested file."""
+ if len(auth_groups) > 1:
+ groups_string = "one of the " + auth_groups.pop(0) + " "
+ if len(auth_groups) > 1:
+ groups_string += ", ".join(auth_groups[0:-1])
+
+ groups_string += " or " + auth_groups[-1] + " teams"
+ else:
+ groups_string = "the " + auth_groups[0] + " team"

I personally try to avoid concatenating strings in that way. Why not something like:

if len(auth_group) > 1:
    groups_string = "one of the %s" % auth_groups.pop(0)
    if len(auth_groups) > 1:
        groups_string += (" %s" % ", ".join(auth_groups[0:-1])).rstrip()
    groups_string += " or %s teams" % auth_groups[-1]
else:
    groups_string = "the %s team" % auth_groups[0]

I prefer it since it is clearer what you are trying to achieve and you can easily spot spurious white spaces.

I'm approving since what I listed are not really blockers, and functionalities are OK, but if you feel to apply them, do while merging.

Another thing I noticed, but not related to this code review: we are missing a license for all the code in linaro-license-protection, and all license header too. We should probably apply one at one point.

review: Approve
Revision history for this message
Georgy Redkozubov (gesha) wrote :

Looks good.
+1 to Milo's comment on tests, afaics, you've missed a ',' in comparison string on line 202. Should be
202 + "You need to be the member of one of the linaro, batman, catwoman "

Otherwise approved.

review: Approve
Revision history for this message
Paul Sokolovsky (pfalcon) wrote :

> Not something I'm really against, but I would be more interested in testing only
the status code I get, not the kind of "string" I will receive (to me that looks
already more than an unit test). I would test something like that:

Well, if looking in more detail, what I did is factored out that string construction out of OpenIdAuth class. The was also test_openid_auth which exactly did that string test. Consequently, I moved that string test where its new routine belongs. I didn't add anything, I don't feel comfortable removing anything either - my idea was to preserve original semantics and tests which cover it.

> I personally try to avoid concatenating strings in that way.

Again, routine wasn't written by me ;-)

Revision history for this message
Paul Sokolovsky (pfalcon) wrote :

> afaics, you've missed a ',' in comparison string on line

Ok, let me fire up bzr blame. Well, Englishman can't be wrong with English usage - it was done by James ;-).

And of course, I'm in principle all for fixing side issues, but: 1) They may need more consideration (in this case, both test "simplification" and grammar), and 2) They should be done separately, to not skew topic branch context.

Re: 2), if we used git, I could just apply quick fix to main branch, and rebase mine, but with bzr, it just better wait in queue (to not introduce twisted merges).

195. By Paul Sokolovsky

Implement dynamic loading of settings.GROUP_AUTH_MODULE properly.

__import__() has peculiar behavior, so importlib.import_module() instead.
Caveat: Python 2.7 only.

196. By Paul Sokolovsky

Add "auth-groups" to list of known fields.

Mark "openid-launchpad-teams" as deprecated.

197. By Paul Sokolovsky

Simplify logic in BuildInfo.get().

198. By Paul Sokolovsky

Consistently handle "openid-launchpad-teams" -> "auth-groups" field rename.

Map old field name to new during parsing. Consistently use "auth-groups"
everywhere in the code.

199. By Paul Sokolovsky

Add parsing test for new "auth-groups" field.

200. By Paul Sokolovsky

Update README (also includes BUILD-INFO.txt spec) for latest changes.

In particular, format version is bumped to 0.5 (we should have 1.0 soon).

201. By Paul Sokolovsky

Rename openid_auth => group_auth_openid, for all auth mods to sort together.

202. By Paul Sokolovsky

Log auth atempt against Launchpad teams.

For helping with group migration.

203. By Paul Sokolovsky

PEP8 fixes.

204. By Paul Sokolovsky

Remove file which is regenerated from scratch every time.

205. By Paul Sokolovsky

Add Crowd API group auth plugin.

206. By Paul Sokolovsky

Typos/grammar.

207. By Paul Sokolovsky

Improve docstring for _set().

208. By Paul Sokolovsky

We use Auth-Groups directive in this file, so it's format 0.5 .

209. By Paul Sokolovsky

Applied isdisjoint() "optimization", with credit to Milo.

210. By Paul Sokolovsky

write_from_array(): Write 0.5 format files.

211. By Paul Sokolovsky

Handle errors during group auth properly.

212. By Paul Sokolovsky

First step toward normal logging setup.

213. By Paul Sokolovsky

Fix exception logging.

214. By Paul Sokolovsky

More test fixes for 0.5 format version.

215. By Paul Sokolovsky

Pyflakes fixes.

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'README'
2--- README 2013-02-15 09:45:08 +0000
3+++ README 2013-06-03 11:47:27 +0000
4@@ -16,7 +16,9 @@
5 * makes use of the regular directory structure on disk
6 * click through licensing with each file potentially having a different
7 license/theme
8- * openid restrictions (by Launchpad teams)
9+ * group-based access authorization restrictions (using group information
10+ from external services, currently OpenID with team extensions is
11+ (as used by Launchpad.net) and Atlassian Crowd are supported).
12 * post-processing of all uploads (a script that manages uploads)
13 * per-IP pass-through (for automatic services like a test framework)
14
15@@ -46,12 +48,12 @@
16 syntax).
17
18 BUILD-INFO.txt file will allow one to specify the access restrictions
19-(such as click-through licensing and OpenID-restrictions) and influence
20+(such as click-through licensing and required groups) and influence
21 the display (such as license-theme) of a particular build.
22
23-WARNING: if you want a build type to be protected by OpenID, you need to
24-ensure that the appropriate team is added to the list of django groups
25-in the database. Only django admins can do that.
26+WARNING: if you want a build to be protected by OpenID-based groups, you
27+need to ensure that the appropriate team is added to the list of django
28+groups in the database. Only django admins can do that.
29
30 Next step is to ensure that
31
32@@ -108,15 +110,15 @@
33 a placeholder and will be ignored. To be added later by build services?
34 * License-Type: (required)
35 open - Open builds. No license page is displayed.
36- protected - EULA protected builds. If 'OpenID-Launchpad-Teams' is defined
37- then OpenID protection is used, otherwise simple Accept/Decline license
38+ protected - EULA protected builds. If 'Auth-Groups' is defined
39+ then group authorization is used, otherwise simple Accept/Decline license
40 page is displayed before accessing protected files.
41 * Theme: (required only if License-Type is 'protected')
42 Acceptable values are: stericsson, samsung.
43 Theme name for selecting proper theming on download and license pages.
44- * OpenID-Launchpad-Teams: (optional)
45- LP team names, members of which are allowed to access protected files. No
46- OpenID protection if absent.
47+ * Auth-Groups: (optional)
48+ Names of groups, members of which are allowed to access protected files.
49+ No group-based protection if absent.
50 * Collect-User-Data: (optional)
51 Acceptable values are: yes, no.
52 Defaults to 'no' if not present. If the field is set to 'yes' then
53@@ -131,10 +133,20 @@
54 - Field names are case insensitive.
55 - Fields order doesn't matter, except 'Files-Pattern'
56
57+History of BUILD-INFO.txt format changes
58+........................................
59+
60+Changes from format version 0.1 to 0.5:
61+
62+1. Field "OpenID-Launchpad-Teams" is deprecated and renamed to
63+"Auth-Groups". Old name is supported for archived builds, however
64+it may lead to warnings, and at later time to errors, during
65+publishing new builds, so client usage should be upgraded.
66+
67 BUILD-INFO.txt example:
68 .......................
69
70-Format-Version: 0.1
71+Format-Version: 0.5
72
73 Files-Pattern: *.img, *.tar.bz2
74 Build-Name: landing-snowball
75
76=== modified file 'license_protected_downloads/buildinfo.py'
77--- license_protected_downloads/buildinfo.py 2013-05-29 13:41:12 +0000
78+++ license_protected_downloads/buildinfo.py 2013-06-03 11:47:27 +0000
79@@ -14,8 +14,11 @@
80 self.file_info_array = [{}]
81 self.fields_defined = [
82 "format-version", "files-pattern", "build-name", "theme",
83- "license-type", "openid-launchpad-teams", "collect-user-data",
84- "license-text"]
85+ "license-type", "auth-groups", "collect-user-data",
86+ "license-text",
87+ # Deprecated
88+ "openid-launchpad-teams",
89+ ]
90 self.full_file_name = fn
91 self.search_path = self.get_search_path(fn)
92 self.fname = os.path.basename(fn)
93@@ -40,6 +43,7 @@
94 os.path.join(cls.get_search_path(path), "BUILD-INFO.txt"))
95
96 def _set(self, key, value):
97+ "key: file pattern, value: list of dicts of field/val pairs"
98 if key in self.build_info_array[self.index]:
99 # A repeated key indicates we have found another chunk of
100 # build-info
101@@ -91,10 +95,7 @@
102 if index > self.max_index:
103 return False
104 block = self.file_info_array[index]
105- for key in block:
106- if field == key:
107- return block[field]
108- return False
109+ return block.get(field, False)
110
111 def parseLine(self, line):
112 values = line.split(":", 1)
113@@ -108,6 +109,9 @@
114 raise IncorrectDataFormatException(
115 "Field '%s' not allowed." % field)
116 else:
117+ # Rename any deprecated field names to new names
118+ field_renames = {"openid-launchpad-teams": "auth-groups"}
119+ field = field_renames.get(field, field)
120 return {field: value}
121
122 def isValidField(self, field_name):
123
124=== added file 'license_protected_downloads/group_auth_crowd.py'
125--- license_protected_downloads/group_auth_crowd.py 1970-01-01 00:00:00 +0000
126+++ license_protected_downloads/group_auth_crowd.py 2013-06-03 11:47:27 +0000
127@@ -0,0 +1,52 @@
128+import logging
129+
130+from django.conf import settings
131+from django.shortcuts import redirect
132+
133+import requests
134+
135+
136+log = logging.getLogger(__file__)
137+
138+
139+def upgrade_requests():
140+ """Ubuntu 12.04 comes with pretty old requests version. Add convenience
141+ methods of newer versions straight to it, to avoid client-side
142+ workarounds."""
143+ if "json" not in dir(requests.models.Response):
144+ def patchy_json(self):
145+ import json
146+ return json.loads(self.content)
147+ requests.models.Response.json = patchy_json
148+
149+# We monkey-patch requests module on first load
150+upgrade_requests()
151+
152+
153+class GroupAuthError(Exception):
154+ pass
155+
156+
157+def process_group_auth(request, required_groups):
158+ if not required_groups:
159+ return True
160+ if not request.user.is_authenticated():
161+ # Force OpenID login
162+ return redirect(settings.LOGIN_URL + "?next=" + request.path)
163+
164+ log.warn("Authenticating using Crowd API: %s",
165+ request.user.username)
166+
167+ auth = (settings.ATLASSIAN_CROWD_API_USERNAME,
168+ settings.ATLASSIAN_CROWD_API_PASSWORD)
169+ params = {"username": request.user.username}
170+ r = requests.get(settings.ATLASSIAN_CROWD_API_URL
171+ + "/user/group/nested.json", params=params, auth=auth)
172+ if r.status_code != 200:
173+ raise GroupAuthError(r.status_code)
174+ data = r.json()
175+ user_groups = set([x["name"] for x in data["groups"]])
176+ for g in required_groups:
177+ if g in user_groups:
178+ return True
179+ return False
180
181=== renamed file 'license_protected_downloads/openid_auth.py' => 'license_protected_downloads/group_auth_openid.py'
182--- license_protected_downloads/openid_auth.py 2012-08-23 13:37:29 +0000
183+++ license_protected_downloads/group_auth_openid.py 2013-06-03 11:47:27 +0000
184@@ -1,16 +1,21 @@
185+import logging
186+
187 from django.conf import settings
188-from django.shortcuts import redirect, render_to_response
189+from django.shortcuts import redirect
190 from django.contrib.auth.models import Group
191-import bzr_version
192-
193-
194-class OpenIDAuth:
195-
196- @classmethod
197- def process_openid_auth(cls, request, openid_teams):
198+
199+
200+log = logging.getLogger(__file__)
201+
202+
203+def process_group_auth(request, openid_teams):
204+ """Returns True if access granted, False if denied and Response
205+ object if not enough authentication information available and
206+ user should authenticate first (by following that Response).
207+ """
208
209 if not openid_teams:
210- return None
211+ return True
212
213 for openid_team in openid_teams:
214 Group.objects.get_or_create(name=openid_team)
215@@ -19,28 +24,10 @@
216 # Force OpenID login
217 return redirect(settings.LOGIN_URL + "?next=" + request.path)
218
219+ log.warn("Authenticating using Launchpad OpenID Teams: %s",
220+ request.user.username)
221 for group in request.user.groups.all():
222 if group.name in openid_teams:
223- return None
224-
225- # Construct a nice string of openid teams that will allow access to
226- # the requested file
227- if len(openid_teams) > 1:
228- teams_string = "one of the " + openid_teams.pop(0) + " "
229- if len(openid_teams) > 1:
230- teams_string += ", ".join(openid_teams[0:-1])
231-
232- teams_string += " or " + openid_teams[-1] + " teams"
233- else:
234- teams_string = "the " + openid_teams[0] + " team"
235-
236- response = render_to_response(
237- 'openid_forbidden_template.html',
238- {'login': settings.LOGIN_URL + "?next=" + request.path,
239- 'authenticated': request.user.is_authenticated(),
240- 'openid_teams': teams_string,
241- 'revno': bzr_version.get_my_bzr_revno(),
242- })
243-
244- response.status_code = 403
245- return response
246+ return True
247+
248+ return False
249
250=== modified file 'license_protected_downloads/tests/__init__.py'
251--- license_protected_downloads/tests/__init__.py 2013-04-19 12:13:03 +0000
252+++ license_protected_downloads/tests/__init__.py 2013-06-03 11:47:27 +0000
253@@ -11,6 +11,7 @@
254 FileViewTests,
255 HowtoViewTests,
256 ViewTests,
257+ ViewHelpersTests,
258 )
259 from license_protected_downloads.tests.test_openid_auth import TestOpenIDAuth
260 from license_protected_downloads.tests.test_custom_commands \
261@@ -32,4 +33,5 @@
262 'TestPep8': TestPep8,
263 'TestPyflakes': TestPyflakes,
264 'ViewTests': ViewTests,
265+ 'ViewHelpersTests': ViewHelpersTests,
266 }
267
268=== modified file 'license_protected_downloads/tests/test_buildinfo.py'
269--- license_protected_downloads/tests/test_buildinfo.py 2013-04-19 12:13:03 +0000
270+++ license_protected_downloads/tests/test_buildinfo.py 2013-06-03 11:47:27 +0000
271@@ -27,7 +27,16 @@
272 self.assertEquals(build_info.getInfoForFile(),
273 [{'build-name': 'landing-protected',
274 'license-type': 'protected',
275- 'openid-launchpad-teams': 'linaro'}])
276+ 'auth-groups': 'linaro'}])
277+
278+ def test_apply_to_dir_auth_groups_field(self):
279+ dir_path = THIS_DIRECTORY + \
280+ '/testserver_root/build-info/subdir2'
281+ build_info = BuildInfo(dir_path)
282+ self.assertEquals(build_info.getInfoForFile(),
283+ [{'build-name': 'landing-protected',
284+ 'license-type': 'protected',
285+ 'auth-groups': 'linaro'}])
286
287 def test_apply_to_nonexistent_file(self):
288 with self.assertRaises(IOError):
289@@ -47,8 +56,8 @@
290 value = "notempty"
291 build_info = BuildInfo(self.buildinfo_file_path)
292 for pair in build_info.file_info_array:
293- if "openid-launchpad-teams" in pair:
294- value = pair["openid-launchpad-teams"]
295+ if "auth-groups" in pair:
296+ value = pair["auth-groups"]
297
298 self.assertFalse(value)
299
300
301=== modified file 'license_protected_downloads/tests/test_openid_auth.py'
302--- license_protected_downloads/tests/test_openid_auth.py 2012-08-23 13:37:29 +0000
303+++ license_protected_downloads/tests/test_openid_auth.py 2013-06-03 11:47:27 +0000
304@@ -2,7 +2,7 @@
305 from django.http import HttpResponse
306 from mock import Mock, patch
307
308-from license_protected_downloads.openid_auth import OpenIDAuth
309+from license_protected_downloads import group_auth_openid as openid_auth
310
311
312 class TestOpenIDAuth(TestCase):
313@@ -27,14 +27,14 @@
314 def test_check_team_membership_no_teams(self):
315 mock_request = self.make_mock_request()
316 openid_teams = []
317- self.assertIsNone(
318- OpenIDAuth.process_openid_auth(mock_request, openid_teams))
319+ self.assertTrue(
320+ openid_auth.process_group_auth(mock_request, openid_teams))
321
322 def test_check_team_membership_no_authentication(self):
323 mock_request = self.make_mock_request()
324 mock_request.user.is_authenticated.return_value = False
325 openid_teams = ["linaro"]
326- response = OpenIDAuth.process_openid_auth(mock_request, openid_teams)
327+ response = openid_auth.process_group_auth(mock_request, openid_teams)
328 self.assertIsNotNone(response)
329 self.assertTrue(isinstance(response, HttpResponse))
330 self.assertEquals(302, response.status_code)
331@@ -45,8 +45,8 @@
332 mock_request.user.groups.all.return_value = [
333 self.make_mock_group("linaro")]
334 openid_teams = ["linaro"]
335- response = OpenIDAuth.process_openid_auth(mock_request, openid_teams)
336- self.assertIsNone(response)
337+ response = openid_auth.process_group_auth(mock_request, openid_teams)
338+ self.assertTrue(response)
339
340 def test_check_no_team_membership_authed(self):
341 mock_request = self.make_mock_request()
342@@ -54,12 +54,8 @@
343 mock_request.user.groups.all.return_value = [
344 self.make_mock_group("another-group")]
345 openid_teams = ["linaro"]
346- response = OpenIDAuth.process_openid_auth(mock_request, openid_teams)
347- self.assertIsNotNone(response)
348- self.assertTrue(isinstance(response, HttpResponse))
349- self.assertContains(response,
350- "You need to be the member of the linaro team in Launchpad.",
351- status_code=403)
352+ response = openid_auth.process_group_auth(mock_request, openid_teams)
353+ self.assertFalse(response)
354
355 def test_check_no_team_membership_authed_multi_teams(self):
356 mock_request = self.make_mock_request()
357@@ -67,13 +63,8 @@
358 mock_request.user.groups.all.return_value = [
359 self.make_mock_group("another-group")]
360 openid_teams = ["linaro", "batman", "catwoman", "joker"]
361- response = OpenIDAuth.process_openid_auth(mock_request, openid_teams)
362- self.assertIsNotNone(response)
363- self.assertTrue(isinstance(response, HttpResponse))
364- self.assertContains(response,
365- "You need to be the member of one of the linaro batman, catwoman "
366- "or joker teams in Launchpad.",
367- status_code=403)
368+ response = openid_auth.process_group_auth(mock_request, openid_teams)
369+ self.assertFalse(response)
370
371 @patch("django.contrib.auth.models.Group.objects.get_or_create")
372 def test_auto_adding_groups(self, get_or_create_mock):
373@@ -83,7 +74,7 @@
374 self.make_mock_group("another-group")]
375
376 openid_teams = ["linaro", "linaro-infrastructure"]
377- OpenIDAuth.process_openid_auth(mock_request, openid_teams)
378+ openid_auth.process_group_auth(mock_request, openid_teams)
379
380 expected = [
381 ((), {'name': 'linaro'}), ((), {'name': 'linaro-infrastructure'})]
382
383=== modified file 'license_protected_downloads/tests/test_splicebuildinfos.py'
384--- license_protected_downloads/tests/test_splicebuildinfos.py 2013-04-19 12:13:03 +0000
385+++ license_protected_downloads/tests/test_splicebuildinfos.py 2013-06-03 11:47:27 +0000
386@@ -20,16 +20,16 @@
387 'test-protected.txt':
388 [{'license-type': 'protected',
389 'build-name': 'landing-protected',
390- 'openid-launchpad-teams': 'linaro'}],
391+ 'auth-groups': 'linaro'}],
392 'test-protected-2.txt':
393 [{'license-type': 'protected',
394 'build-name': 'landing-protected',
395- 'openid-launchpad-teams': 'linaro'}]}
396+ 'auth-groups': 'linaro'}]}
397
398 result = {'test-protected.txt, test-protected-2.txt':
399 [{'license-type': 'protected',
400 'build-name': 'landing-protected',
401- 'openid-launchpad-teams': 'linaro'}]}
402+ 'auth-groups': 'linaro'}]}
403
404 build_info_res = SpliceBuildInfos.merge_duplicates(build_info_dict)
405 self.assertEquals(build_info_res, result)
406
407=== modified file 'license_protected_downloads/tests/test_views.py'
408--- license_protected_downloads/tests/test_views.py 2013-04-02 09:58:42 +0000
409+++ license_protected_downloads/tests/test_views.py 2013-06-03 11:47:27 +0000
410@@ -2,6 +2,7 @@
411
412 from django.conf import settings
413 from django.test import Client, TestCase
414+from django.http import HttpResponse
415 import hashlib
416 import os
417 import tempfile
418@@ -10,6 +11,8 @@
419 import urlparse
420 import json
421
422+from mock import Mock
423+
424 from license_protected_downloads import bzr_version
425 from license_protected_downloads.buildinfo import BuildInfo
426 from license_protected_downloads.config import INTERNAL_HOSTS
427@@ -19,6 +22,7 @@
428 from license_protected_downloads.views import _process_include_tags
429 from license_protected_downloads.views import _sizeof_fmt
430 from license_protected_downloads.views import is_same_parent_dir
431+from license_protected_downloads import views
432
433 THIS_DIRECTORY = os.path.dirname(os.path.abspath(__file__))
434 TESTSERVER_ROOT = os.path.join(THIS_DIRECTORY, "testserver_root")
435@@ -903,5 +907,19 @@
436 self.assertEqual(response.status_code, 200)
437
438
439+class ViewHelpersTests(BaseServeViewTest):
440+ def test_auth_group_error(self):
441+ groups = ["linaro", "batman", "catwoman", "joker"]
442+ request = Mock()
443+ request.path = "mock_path"
444+ response = views.group_auth_failed_response(request, groups)
445+ self.assertIsNotNone(response)
446+ self.assertTrue(isinstance(response, HttpResponse))
447+ self.assertContains(response,
448+ "You need to be the member of one of the linaro batman, catwoman "
449+ "or joker teams in Launchpad.",
450+ status_code=403)
451+
452+
453 if __name__ == '__main__':
454 unittest.main()
455
456=== added directory 'license_protected_downloads/tests/testserver_root/build-info/subdir2'
457=== added file 'license_protected_downloads/tests/testserver_root/build-info/subdir2/BUILD-INFO.txt'
458--- license_protected_downloads/tests/testserver_root/build-info/subdir2/BUILD-INFO.txt 1970-01-01 00:00:00 +0000
459+++ license_protected_downloads/tests/testserver_root/build-info/subdir2/BUILD-INFO.txt 2013-06-03 11:47:27 +0000
460@@ -0,0 +1,7 @@
461+Format-Version: 0.1
462+
463+
464+Files-Pattern: *
465+Build-Name: landing-protected
466+License-Type: protected
467+Auth-Groups: linaro
468
469=== added file 'license_protected_downloads/tests/testserver_root/build-info/subdir2/testfile.txt'
470=== removed file 'license_protected_downloads/tests/testserver_root/build-info/write-test/BUILD-INFO.txt'
471--- license_protected_downloads/tests/testserver_root/build-info/write-test/BUILD-INFO.txt 2013-04-19 12:13:03 +0000
472+++ license_protected_downloads/tests/testserver_root/build-info/write-test/BUILD-INFO.txt 1970-01-01 00:00:00 +0000
473@@ -1,42 +0,0 @@
474-Format-Version: 0.1
475-
476-Files-Pattern: *openid*
477-license-type: protected
478-build-name: landing-protected
479-openid-launchpad-teams: linaro
480-
481-Files-Pattern: *panda*
482-license-type: open
483-build-name: landing-panda
484-
485-Files-Pattern: *.txt
486-license-type: protected
487-build-name: landing-protected
488-openid-launchpad-teams:
489-theme: stericsson
490-collect-user-data: yes
491-license-text: <p>IMPORTANT — PLEASE READ THE FOLLOWING AGREEMENT CAREFULLY.</p>
492- <p>
493- THIS IS A LEGALLY BINDING AGREEMENT
494- </p>
495-
496-Files-Pattern: *origen*
497-theme: samsung
498-license-type: protected
499-build-name: landing-origen
500-license-text: <p>IMPORTANT — PLEASE READ THE FOLLOWING AGREEMENT CAREFULLY.</p>
501- <p>
502- THIS IS A LEGALLY BINDING AGREEMENT BETWEEN YOU, an individual or a
503- legal entity, (“LICENSEE”) AND HAL 1000.
504- </p>
505-
506-Files-Pattern: *snowball*
507-theme: stericsson
508-license-type: protected
509-build-name: landing-snowball
510-license-text: <p>IMPORTANT — PLEASE READ THE FOLLOWING AGREEMENT CAREFULLY.</p>
511- <p>
512- THIS IS A LEGALLY BINDING AGREEMENT BETWEEN YOU, an individual or a
513- legal entity, (“LICENSEE”) AND ME, THE TEST SERVER.
514- </p>
515-
516
517=== modified file 'license_protected_downloads/views.py'
518--- license_protected_downloads/views.py 2013-05-09 11:15:33 +0000
519+++ license_protected_downloads/views.py 2013-06-03 11:47:27 +0000
520@@ -23,7 +23,9 @@
521 from buildinfo import BuildInfo, IncorrectDataFormatException
522 from render_text_files import RenderTextFiles
523 from models import License
524-from openid_auth import OpenIDAuth
525+# Load group auth "plugin" dynamically
526+import importlib
527+group_auth = importlib.import_module(settings.GROUP_AUTH_MODULE)
528 from BeautifulSoup import BeautifulSoup
529 import config
530
531@@ -242,7 +244,7 @@
532 license_type = build_info.get("license-type")
533 license_text = build_info.get("license-text")
534 theme = build_info.get("theme")
535- openid_teams = build_info.get("openid-launchpad-teams")
536+ auth_groups = build_info.get("auth-groups")
537 max_index = build_info.max_index
538 elif os.path.isfile(open_eula_path):
539 return "OPEN"
540@@ -256,7 +258,7 @@
541 license_type = "protected"
542 license_file = os.path.join(settings.PROJECT_ROOT,
543 'templates/licenses/' + theme + '.txt')
544- openid_teams = False
545+ auth_groups = False
546 with open(license_file, "r") as infile:
547 license_text = infile.read()
548 elif _check_special_eula(path):
549@@ -264,7 +266,7 @@
550 license_type = "protected"
551 license_file = os.path.join(settings.PROJECT_ROOT,
552 'templates/licenses/' + theme + '.txt')
553- openid_teams = False
554+ auth_groups = False
555 with open(license_file, "r") as infile:
556 license_text = infile.read()
557 elif _check_special_eula(base_path + "/*"):
558@@ -279,7 +281,7 @@
559 return "OPEN"
560
561 # File matches a license, isn't open.
562- if openid_teams:
563+ if auth_groups:
564 return "OPEN"
565 elif license_text:
566 for i in range(max_index):
567@@ -392,6 +394,30 @@
568 return response
569
570
571+def group_auth_failed_response(request, auth_groups):
572+ """Construct a nice response detailing list of auth groups that
573+ will allow access to the requested file."""
574+ if len(auth_groups) > 1:
575+ groups_string = "one of the " + auth_groups.pop(0) + " "
576+ if len(auth_groups) > 1:
577+ groups_string += ", ".join(auth_groups[0:-1])
578+
579+ groups_string += " or " + auth_groups[-1] + " teams"
580+ else:
581+ groups_string = "the " + auth_groups[0] + " team"
582+
583+ response = render_to_response(
584+ 'openid_forbidden_template.html',
585+ {'login': settings.LOGIN_URL + "?next=" + request.path,
586+ 'authenticated': request.user.is_authenticated(),
587+ 'openid_teams': groups_string,
588+ 'revno': bzr_version.get_my_bzr_revno(),
589+ })
590+
591+ response.status_code = 403
592+ return response
593+
594+
595 def file_server(request, path):
596 """Serve up a file / directory listing or license page as required"""
597 path = iri_to_uri(path)
598@@ -415,16 +441,19 @@
599 return HttpResponseForbidden(
600 "Error parsing BUILD-INFO.txt")
601
602- launchpad_teams = build_info.get("openid-launchpad-teams")
603- if launchpad_teams:
604- launchpad_teams = launchpad_teams.split(",")
605- launchpad_teams = [team.strip() for team in launchpad_teams]
606+ auth_groups = build_info.get("auth-groups")
607+ if auth_groups:
608+ auth_groups = auth_groups.split(",")
609+ auth_groups = [g.strip() for g in auth_groups]
610 # TODO: use logging!
611- print "Checking membership in OpenID groups:", launchpad_teams
612- openid_response = OpenIDAuth.process_openid_auth(
613- request, launchpad_teams)
614- if openid_response:
615- return openid_response
616+ print "Checking membership in auth groups:", auth_groups
617+ response = group_auth.process_group_auth(request, auth_groups)
618+ if response == False:
619+ return group_auth_failed_response(request, auth_groups)
620+ elif response == True:
621+ pass
622+ else:
623+ return response
624
625 if target_type == "dir":
626 # Generate a link to the parent directory (if one exists)
627
628=== modified file 'settings.py'
629--- settings.py 2013-02-26 19:38:30 +0000
630+++ settings.py 2013-06-03 11:47:27 +0000
631@@ -124,6 +124,10 @@
632 LOGIN_URL = '/linaro-openid/login/'
633 LOGIN_REDIRECT_URL = '/'
634
635+# Name of "plugin" module to use for group authentication
636+GROUP_AUTH_MODULE = 'license_protected_downloads.group_auth_openid'
637+
638+# Config for django_openid_auth.auth.OpenIDBackend
639 OPENID_CREATE_USERS = True
640 OPENID_SSO_SERVER_URL = 'https://login.launchpad.net/'
641 OPENID_UPDATE_DETAILS_FROM_SREG = True
642@@ -132,6 +136,11 @@
643 OPENID_USE_AS_ADMIN_LOGIN = True
644 OPENID_USE_EMAIL_FOR_USERNAME = True
645
646+ATLASSIAN_CROWD_API_URL = \
647+ "https://login.linaro.org:8443/crowd/rest/usermanagement/1"
648+ATLASSIAN_CROWD_API_USERNAME = None
649+ATLASSIAN_CROWD_API_PASSWORD = None
650+
651 # A sample logging configuration. The only tangible logging
652 # performed by this configuration is to send an email to
653 # the site admins on every HTTP 500 error.

Subscribers

People subscribed via source and target branches