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
=== modified file 'README'
--- README 2013-02-15 09:45:08 +0000
+++ README 2013-06-03 11:47:27 +0000
@@ -16,7 +16,9 @@
16 * makes use of the regular directory structure on disk16 * makes use of the regular directory structure on disk
17 * click through licensing with each file potentially having a different17 * click through licensing with each file potentially having a different
18 license/theme18 license/theme
19 * openid restrictions (by Launchpad teams)19 * group-based access authorization restrictions (using group information
20 from external services, currently OpenID with team extensions is
21 (as used by Launchpad.net) and Atlassian Crowd are supported).
20 * post-processing of all uploads (a script that manages uploads)22 * post-processing of all uploads (a script that manages uploads)
21 * per-IP pass-through (for automatic services like a test framework)23 * per-IP pass-through (for automatic services like a test framework)
2224
@@ -46,12 +48,12 @@
46syntax).48syntax).
4749
48BUILD-INFO.txt file will allow one to specify the access restrictions50BUILD-INFO.txt file will allow one to specify the access restrictions
49(such as click-through licensing and OpenID-restrictions) and influence51(such as click-through licensing and required groups) and influence
50the display (such as license-theme) of a particular build.52the display (such as license-theme) of a particular build.
5153
52WARNING: if you want a build type to be protected by OpenID, you need to54WARNING: if you want a build to be protected by OpenID-based groups, you
53ensure that the appropriate team is added to the list of django groups55need to ensure that the appropriate team is added to the list of django
54in the database. Only django admins can do that.56groups in the database. Only django admins can do that.
5557
56Next step is to ensure that58Next step is to ensure that
5759
@@ -108,15 +110,15 @@
108 a placeholder and will be ignored. To be added later by build services?110 a placeholder and will be ignored. To be added later by build services?
109 * License-Type: (required)111 * License-Type: (required)
110 open - Open builds. No license page is displayed.112 open - Open builds. No license page is displayed.
111 protected - EULA protected builds. If 'OpenID-Launchpad-Teams' is defined113 protected - EULA protected builds. If 'Auth-Groups' is defined
112 then OpenID protection is used, otherwise simple Accept/Decline license114 then group authorization is used, otherwise simple Accept/Decline license
113 page is displayed before accessing protected files.115 page is displayed before accessing protected files.
114 * Theme: (required only if License-Type is 'protected')116 * Theme: (required only if License-Type is 'protected')
115 Acceptable values are: stericsson, samsung.117 Acceptable values are: stericsson, samsung.
116 Theme name for selecting proper theming on download and license pages.118 Theme name for selecting proper theming on download and license pages.
117 * OpenID-Launchpad-Teams: (optional)119 * Auth-Groups: (optional)
118 LP team names, members of which are allowed to access protected files. No120 Names of groups, members of which are allowed to access protected files.
119 OpenID protection if absent.121 No group-based protection if absent.
120 * Collect-User-Data: (optional)122 * Collect-User-Data: (optional)
121 Acceptable values are: yes, no.123 Acceptable values are: yes, no.
122 Defaults to 'no' if not present. If the field is set to 'yes' then124 Defaults to 'no' if not present. If the field is set to 'yes' then
@@ -131,10 +133,20 @@
131- Field names are case insensitive.133- Field names are case insensitive.
132- Fields order doesn't matter, except 'Files-Pattern'134- Fields order doesn't matter, except 'Files-Pattern'
133135
136History of BUILD-INFO.txt format changes
137........................................
138
139Changes from format version 0.1 to 0.5:
140
1411. Field "OpenID-Launchpad-Teams" is deprecated and renamed to
142"Auth-Groups". Old name is supported for archived builds, however
143it may lead to warnings, and at later time to errors, during
144publishing new builds, so client usage should be upgraded.
145
134BUILD-INFO.txt example:146BUILD-INFO.txt example:
135.......................147.......................
136148
137Format-Version: 0.1149Format-Version: 0.5
138150
139Files-Pattern: *.img, *.tar.bz2151Files-Pattern: *.img, *.tar.bz2
140Build-Name: landing-snowball152Build-Name: landing-snowball
141153
=== modified file 'license_protected_downloads/buildinfo.py'
--- license_protected_downloads/buildinfo.py 2013-05-29 13:41:12 +0000
+++ license_protected_downloads/buildinfo.py 2013-06-03 11:47:27 +0000
@@ -14,8 +14,11 @@
14 self.file_info_array = [{}]14 self.file_info_array = [{}]
15 self.fields_defined = [15 self.fields_defined = [
16 "format-version", "files-pattern", "build-name", "theme",16 "format-version", "files-pattern", "build-name", "theme",
17 "license-type", "openid-launchpad-teams", "collect-user-data",17 "license-type", "auth-groups", "collect-user-data",
18 "license-text"]18 "license-text",
19 # Deprecated
20 "openid-launchpad-teams",
21 ]
19 self.full_file_name = fn22 self.full_file_name = fn
20 self.search_path = self.get_search_path(fn)23 self.search_path = self.get_search_path(fn)
21 self.fname = os.path.basename(fn)24 self.fname = os.path.basename(fn)
@@ -40,6 +43,7 @@
40 os.path.join(cls.get_search_path(path), "BUILD-INFO.txt"))43 os.path.join(cls.get_search_path(path), "BUILD-INFO.txt"))
4144
42 def _set(self, key, value):45 def _set(self, key, value):
46 "key: file pattern, value: list of dicts of field/val pairs"
43 if key in self.build_info_array[self.index]:47 if key in self.build_info_array[self.index]:
44 # A repeated key indicates we have found another chunk of48 # A repeated key indicates we have found another chunk of
45 # build-info49 # build-info
@@ -91,10 +95,7 @@
91 if index > self.max_index:95 if index > self.max_index:
92 return False96 return False
93 block = self.file_info_array[index]97 block = self.file_info_array[index]
94 for key in block:98 return block.get(field, False)
95 if field == key:
96 return block[field]
97 return False
9899
99 def parseLine(self, line):100 def parseLine(self, line):
100 values = line.split(":", 1)101 values = line.split(":", 1)
@@ -108,6 +109,9 @@
108 raise IncorrectDataFormatException(109 raise IncorrectDataFormatException(
109 "Field '%s' not allowed." % field)110 "Field '%s' not allowed." % field)
110 else:111 else:
112 # Rename any deprecated field names to new names
113 field_renames = {"openid-launchpad-teams": "auth-groups"}
114 field = field_renames.get(field, field)
111 return {field: value}115 return {field: value}
112116
113 def isValidField(self, field_name):117 def isValidField(self, field_name):
114118
=== added file 'license_protected_downloads/group_auth_crowd.py'
--- license_protected_downloads/group_auth_crowd.py 1970-01-01 00:00:00 +0000
+++ license_protected_downloads/group_auth_crowd.py 2013-06-03 11:47:27 +0000
@@ -0,0 +1,52 @@
1import logging
2
3from django.conf import settings
4from django.shortcuts import redirect
5
6import requests
7
8
9log = logging.getLogger(__file__)
10
11
12def upgrade_requests():
13 """Ubuntu 12.04 comes with pretty old requests version. Add convenience
14 methods of newer versions straight to it, to avoid client-side
15 workarounds."""
16 if "json" not in dir(requests.models.Response):
17 def patchy_json(self):
18 import json
19 return json.loads(self.content)
20 requests.models.Response.json = patchy_json
21
22# We monkey-patch requests module on first load
23upgrade_requests()
24
25
26class GroupAuthError(Exception):
27 pass
28
29
30def process_group_auth(request, required_groups):
31 if not required_groups:
32 return True
33 if not request.user.is_authenticated():
34 # Force OpenID login
35 return redirect(settings.LOGIN_URL + "?next=" + request.path)
36
37 log.warn("Authenticating using Crowd API: %s",
38 request.user.username)
39
40 auth = (settings.ATLASSIAN_CROWD_API_USERNAME,
41 settings.ATLASSIAN_CROWD_API_PASSWORD)
42 params = {"username": request.user.username}
43 r = requests.get(settings.ATLASSIAN_CROWD_API_URL
44 + "/user/group/nested.json", params=params, auth=auth)
45 if r.status_code != 200:
46 raise GroupAuthError(r.status_code)
47 data = r.json()
48 user_groups = set([x["name"] for x in data["groups"]])
49 for g in required_groups:
50 if g in user_groups:
51 return True
52 return False
053
=== renamed file 'license_protected_downloads/openid_auth.py' => 'license_protected_downloads/group_auth_openid.py'
--- license_protected_downloads/openid_auth.py 2012-08-23 13:37:29 +0000
+++ license_protected_downloads/group_auth_openid.py 2013-06-03 11:47:27 +0000
@@ -1,16 +1,21 @@
1import logging
2
1from django.conf import settings3from django.conf import settings
2from django.shortcuts import redirect, render_to_response4from django.shortcuts import redirect
3from django.contrib.auth.models import Group5from django.contrib.auth.models import Group
4import bzr_version6
57
68log = logging.getLogger(__file__)
7class OpenIDAuth:9
810
9 @classmethod11def process_group_auth(request, openid_teams):
10 def process_openid_auth(cls, request, openid_teams):12 """Returns True if access granted, False if denied and Response
13 object if not enough authentication information available and
14 user should authenticate first (by following that Response).
15 """
1116
12 if not openid_teams:17 if not openid_teams:
13 return None18 return True
1419
15 for openid_team in openid_teams:20 for openid_team in openid_teams:
16 Group.objects.get_or_create(name=openid_team)21 Group.objects.get_or_create(name=openid_team)
@@ -19,28 +24,10 @@
19 # Force OpenID login24 # Force OpenID login
20 return redirect(settings.LOGIN_URL + "?next=" + request.path)25 return redirect(settings.LOGIN_URL + "?next=" + request.path)
2126
27 log.warn("Authenticating using Launchpad OpenID Teams: %s",
28 request.user.username)
22 for group in request.user.groups.all():29 for group in request.user.groups.all():
23 if group.name in openid_teams:30 if group.name in openid_teams:
24 return None31 return True
2532
26 # Construct a nice string of openid teams that will allow access to33 return False
27 # the requested file
28 if len(openid_teams) > 1:
29 teams_string = "one of the " + openid_teams.pop(0) + " "
30 if len(openid_teams) > 1:
31 teams_string += ", ".join(openid_teams[0:-1])
32
33 teams_string += " or " + openid_teams[-1] + " teams"
34 else:
35 teams_string = "the " + openid_teams[0] + " team"
36
37 response = render_to_response(
38 'openid_forbidden_template.html',
39 {'login': settings.LOGIN_URL + "?next=" + request.path,
40 'authenticated': request.user.is_authenticated(),
41 'openid_teams': teams_string,
42 'revno': bzr_version.get_my_bzr_revno(),
43 })
44
45 response.status_code = 403
46 return response
4734
=== modified file 'license_protected_downloads/tests/__init__.py'
--- license_protected_downloads/tests/__init__.py 2013-04-19 12:13:03 +0000
+++ license_protected_downloads/tests/__init__.py 2013-06-03 11:47:27 +0000
@@ -11,6 +11,7 @@
11 FileViewTests,11 FileViewTests,
12 HowtoViewTests,12 HowtoViewTests,
13 ViewTests,13 ViewTests,
14 ViewHelpersTests,
14 )15 )
15from license_protected_downloads.tests.test_openid_auth import TestOpenIDAuth16from license_protected_downloads.tests.test_openid_auth import TestOpenIDAuth
16from license_protected_downloads.tests.test_custom_commands \17from license_protected_downloads.tests.test_custom_commands \
@@ -32,4 +33,5 @@
32 'TestPep8': TestPep8,33 'TestPep8': TestPep8,
33 'TestPyflakes': TestPyflakes,34 'TestPyflakes': TestPyflakes,
34 'ViewTests': ViewTests,35 'ViewTests': ViewTests,
36 'ViewHelpersTests': ViewHelpersTests,
35}37}
3638
=== modified file 'license_protected_downloads/tests/test_buildinfo.py'
--- license_protected_downloads/tests/test_buildinfo.py 2013-04-19 12:13:03 +0000
+++ license_protected_downloads/tests/test_buildinfo.py 2013-06-03 11:47:27 +0000
@@ -27,7 +27,16 @@
27 self.assertEquals(build_info.getInfoForFile(),27 self.assertEquals(build_info.getInfoForFile(),
28 [{'build-name': 'landing-protected',28 [{'build-name': 'landing-protected',
29 'license-type': 'protected',29 'license-type': 'protected',
30 'openid-launchpad-teams': 'linaro'}])30 'auth-groups': 'linaro'}])
31
32 def test_apply_to_dir_auth_groups_field(self):
33 dir_path = THIS_DIRECTORY + \
34 '/testserver_root/build-info/subdir2'
35 build_info = BuildInfo(dir_path)
36 self.assertEquals(build_info.getInfoForFile(),
37 [{'build-name': 'landing-protected',
38 'license-type': 'protected',
39 'auth-groups': 'linaro'}])
3140
32 def test_apply_to_nonexistent_file(self):41 def test_apply_to_nonexistent_file(self):
33 with self.assertRaises(IOError):42 with self.assertRaises(IOError):
@@ -47,8 +56,8 @@
47 value = "notempty"56 value = "notempty"
48 build_info = BuildInfo(self.buildinfo_file_path)57 build_info = BuildInfo(self.buildinfo_file_path)
49 for pair in build_info.file_info_array:58 for pair in build_info.file_info_array:
50 if "openid-launchpad-teams" in pair:59 if "auth-groups" in pair:
51 value = pair["openid-launchpad-teams"]60 value = pair["auth-groups"]
5261
53 self.assertFalse(value)62 self.assertFalse(value)
5463
5564
=== modified file 'license_protected_downloads/tests/test_openid_auth.py'
--- license_protected_downloads/tests/test_openid_auth.py 2012-08-23 13:37:29 +0000
+++ license_protected_downloads/tests/test_openid_auth.py 2013-06-03 11:47:27 +0000
@@ -2,7 +2,7 @@
2from django.http import HttpResponse2from django.http import HttpResponse
3from mock import Mock, patch3from mock import Mock, patch
44
5from license_protected_downloads.openid_auth import OpenIDAuth5from license_protected_downloads import group_auth_openid as openid_auth
66
77
8class TestOpenIDAuth(TestCase):8class TestOpenIDAuth(TestCase):
@@ -27,14 +27,14 @@
27 def test_check_team_membership_no_teams(self):27 def test_check_team_membership_no_teams(self):
28 mock_request = self.make_mock_request()28 mock_request = self.make_mock_request()
29 openid_teams = []29 openid_teams = []
30 self.assertIsNone(30 self.assertTrue(
31 OpenIDAuth.process_openid_auth(mock_request, openid_teams))31 openid_auth.process_group_auth(mock_request, openid_teams))
3232
33 def test_check_team_membership_no_authentication(self):33 def test_check_team_membership_no_authentication(self):
34 mock_request = self.make_mock_request()34 mock_request = self.make_mock_request()
35 mock_request.user.is_authenticated.return_value = False35 mock_request.user.is_authenticated.return_value = False
36 openid_teams = ["linaro"]36 openid_teams = ["linaro"]
37 response = OpenIDAuth.process_openid_auth(mock_request, openid_teams)37 response = openid_auth.process_group_auth(mock_request, openid_teams)
38 self.assertIsNotNone(response)38 self.assertIsNotNone(response)
39 self.assertTrue(isinstance(response, HttpResponse))39 self.assertTrue(isinstance(response, HttpResponse))
40 self.assertEquals(302, response.status_code)40 self.assertEquals(302, response.status_code)
@@ -45,8 +45,8 @@
45 mock_request.user.groups.all.return_value = [45 mock_request.user.groups.all.return_value = [
46 self.make_mock_group("linaro")]46 self.make_mock_group("linaro")]
47 openid_teams = ["linaro"]47 openid_teams = ["linaro"]
48 response = OpenIDAuth.process_openid_auth(mock_request, openid_teams)48 response = openid_auth.process_group_auth(mock_request, openid_teams)
49 self.assertIsNone(response)49 self.assertTrue(response)
5050
51 def test_check_no_team_membership_authed(self):51 def test_check_no_team_membership_authed(self):
52 mock_request = self.make_mock_request()52 mock_request = self.make_mock_request()
@@ -54,12 +54,8 @@
54 mock_request.user.groups.all.return_value = [54 mock_request.user.groups.all.return_value = [
55 self.make_mock_group("another-group")]55 self.make_mock_group("another-group")]
56 openid_teams = ["linaro"]56 openid_teams = ["linaro"]
57 response = OpenIDAuth.process_openid_auth(mock_request, openid_teams)57 response = openid_auth.process_group_auth(mock_request, openid_teams)
58 self.assertIsNotNone(response)58 self.assertFalse(response)
59 self.assertTrue(isinstance(response, HttpResponse))
60 self.assertContains(response,
61 "You need to be the member of the linaro team in Launchpad.",
62 status_code=403)
6359
64 def test_check_no_team_membership_authed_multi_teams(self):60 def test_check_no_team_membership_authed_multi_teams(self):
65 mock_request = self.make_mock_request()61 mock_request = self.make_mock_request()
@@ -67,13 +63,8 @@
67 mock_request.user.groups.all.return_value = [63 mock_request.user.groups.all.return_value = [
68 self.make_mock_group("another-group")]64 self.make_mock_group("another-group")]
69 openid_teams = ["linaro", "batman", "catwoman", "joker"]65 openid_teams = ["linaro", "batman", "catwoman", "joker"]
70 response = OpenIDAuth.process_openid_auth(mock_request, openid_teams)66 response = openid_auth.process_group_auth(mock_request, openid_teams)
71 self.assertIsNotNone(response)67 self.assertFalse(response)
72 self.assertTrue(isinstance(response, HttpResponse))
73 self.assertContains(response,
74 "You need to be the member of one of the linaro batman, catwoman "
75 "or joker teams in Launchpad.",
76 status_code=403)
7768
78 @patch("django.contrib.auth.models.Group.objects.get_or_create")69 @patch("django.contrib.auth.models.Group.objects.get_or_create")
79 def test_auto_adding_groups(self, get_or_create_mock):70 def test_auto_adding_groups(self, get_or_create_mock):
@@ -83,7 +74,7 @@
83 self.make_mock_group("another-group")]74 self.make_mock_group("another-group")]
8475
85 openid_teams = ["linaro", "linaro-infrastructure"]76 openid_teams = ["linaro", "linaro-infrastructure"]
86 OpenIDAuth.process_openid_auth(mock_request, openid_teams)77 openid_auth.process_group_auth(mock_request, openid_teams)
8778
88 expected = [79 expected = [
89 ((), {'name': 'linaro'}), ((), {'name': 'linaro-infrastructure'})]80 ((), {'name': 'linaro'}), ((), {'name': 'linaro-infrastructure'})]
9081
=== modified file 'license_protected_downloads/tests/test_splicebuildinfos.py'
--- license_protected_downloads/tests/test_splicebuildinfos.py 2013-04-19 12:13:03 +0000
+++ license_protected_downloads/tests/test_splicebuildinfos.py 2013-06-03 11:47:27 +0000
@@ -20,16 +20,16 @@
20 'test-protected.txt':20 'test-protected.txt':
21 [{'license-type': 'protected',21 [{'license-type': 'protected',
22 'build-name': 'landing-protected',22 'build-name': 'landing-protected',
23 'openid-launchpad-teams': 'linaro'}],23 'auth-groups': 'linaro'}],
24 'test-protected-2.txt':24 'test-protected-2.txt':
25 [{'license-type': 'protected',25 [{'license-type': 'protected',
26 'build-name': 'landing-protected',26 'build-name': 'landing-protected',
27 'openid-launchpad-teams': 'linaro'}]}27 'auth-groups': 'linaro'}]}
2828
29 result = {'test-protected.txt, test-protected-2.txt':29 result = {'test-protected.txt, test-protected-2.txt':
30 [{'license-type': 'protected',30 [{'license-type': 'protected',
31 'build-name': 'landing-protected',31 'build-name': 'landing-protected',
32 'openid-launchpad-teams': 'linaro'}]}32 'auth-groups': 'linaro'}]}
3333
34 build_info_res = SpliceBuildInfos.merge_duplicates(build_info_dict)34 build_info_res = SpliceBuildInfos.merge_duplicates(build_info_dict)
35 self.assertEquals(build_info_res, result)35 self.assertEquals(build_info_res, result)
3636
=== modified file 'license_protected_downloads/tests/test_views.py'
--- license_protected_downloads/tests/test_views.py 2013-04-02 09:58:42 +0000
+++ license_protected_downloads/tests/test_views.py 2013-06-03 11:47:27 +0000
@@ -2,6 +2,7 @@
22
3from django.conf import settings3from django.conf import settings
4from django.test import Client, TestCase4from django.test import Client, TestCase
5from django.http import HttpResponse
5import hashlib6import hashlib
6import os7import os
7import tempfile8import tempfile
@@ -10,6 +11,8 @@
10import urlparse11import urlparse
11import json12import json
1213
14from mock import Mock
15
13from license_protected_downloads import bzr_version16from license_protected_downloads import bzr_version
14from license_protected_downloads.buildinfo import BuildInfo17from license_protected_downloads.buildinfo import BuildInfo
15from license_protected_downloads.config import INTERNAL_HOSTS18from license_protected_downloads.config import INTERNAL_HOSTS
@@ -19,6 +22,7 @@
19from license_protected_downloads.views import _process_include_tags22from license_protected_downloads.views import _process_include_tags
20from license_protected_downloads.views import _sizeof_fmt23from license_protected_downloads.views import _sizeof_fmt
21from license_protected_downloads.views import is_same_parent_dir24from license_protected_downloads.views import is_same_parent_dir
25from license_protected_downloads import views
2226
23THIS_DIRECTORY = os.path.dirname(os.path.abspath(__file__))27THIS_DIRECTORY = os.path.dirname(os.path.abspath(__file__))
24TESTSERVER_ROOT = os.path.join(THIS_DIRECTORY, "testserver_root")28TESTSERVER_ROOT = os.path.join(THIS_DIRECTORY, "testserver_root")
@@ -903,5 +907,19 @@
903 self.assertEqual(response.status_code, 200)907 self.assertEqual(response.status_code, 200)
904908
905909
910class ViewHelpersTests(BaseServeViewTest):
911 def test_auth_group_error(self):
912 groups = ["linaro", "batman", "catwoman", "joker"]
913 request = Mock()
914 request.path = "mock_path"
915 response = views.group_auth_failed_response(request, groups)
916 self.assertIsNotNone(response)
917 self.assertTrue(isinstance(response, HttpResponse))
918 self.assertContains(response,
919 "You need to be the member of one of the linaro batman, catwoman "
920 "or joker teams in Launchpad.",
921 status_code=403)
922
923
906if __name__ == '__main__':924if __name__ == '__main__':
907 unittest.main()925 unittest.main()
908926
=== added directory 'license_protected_downloads/tests/testserver_root/build-info/subdir2'
=== added file 'license_protected_downloads/tests/testserver_root/build-info/subdir2/BUILD-INFO.txt'
--- license_protected_downloads/tests/testserver_root/build-info/subdir2/BUILD-INFO.txt 1970-01-01 00:00:00 +0000
+++ license_protected_downloads/tests/testserver_root/build-info/subdir2/BUILD-INFO.txt 2013-06-03 11:47:27 +0000
@@ -0,0 +1,7 @@
1Format-Version: 0.1
2
3
4Files-Pattern: *
5Build-Name: landing-protected
6License-Type: protected
7Auth-Groups: linaro
08
=== added file 'license_protected_downloads/tests/testserver_root/build-info/subdir2/testfile.txt'
=== removed file 'license_protected_downloads/tests/testserver_root/build-info/write-test/BUILD-INFO.txt'
--- license_protected_downloads/tests/testserver_root/build-info/write-test/BUILD-INFO.txt 2013-04-19 12:13:03 +0000
+++ license_protected_downloads/tests/testserver_root/build-info/write-test/BUILD-INFO.txt 1970-01-01 00:00:00 +0000
@@ -1,42 +0,0 @@
1Format-Version: 0.1
2
3Files-Pattern: *openid*
4license-type: protected
5build-name: landing-protected
6openid-launchpad-teams: linaro
7
8Files-Pattern: *panda*
9license-type: open
10build-name: landing-panda
11
12Files-Pattern: *.txt
13license-type: protected
14build-name: landing-protected
15openid-launchpad-teams:
16theme: stericsson
17collect-user-data: yes
18license-text: <p>IMPORTANT — PLEASE READ THE FOLLOWING AGREEMENT CAREFULLY.</p>
19 <p>
20 THIS IS A LEGALLY BINDING AGREEMENT
21 </p>
22
23Files-Pattern: *origen*
24theme: samsung
25license-type: protected
26build-name: landing-origen
27license-text: <p>IMPORTANT — PLEASE READ THE FOLLOWING AGREEMENT CAREFULLY.</p>
28 <p>
29 THIS IS A LEGALLY BINDING AGREEMENT BETWEEN YOU, an individual or a
30 legal entity, (“LICENSEE”) AND HAL 1000.
31 </p>
32
33Files-Pattern: *snowball*
34theme: stericsson
35license-type: protected
36build-name: landing-snowball
37license-text: <p>IMPORTANT — PLEASE READ THE FOLLOWING AGREEMENT CAREFULLY.</p>
38 <p>
39 THIS IS A LEGALLY BINDING AGREEMENT BETWEEN YOU, an individual or a
40 legal entity, (“LICENSEE”) AND ME, THE TEST SERVER.
41 </p>
42
430
=== modified file 'license_protected_downloads/views.py'
--- license_protected_downloads/views.py 2013-05-09 11:15:33 +0000
+++ license_protected_downloads/views.py 2013-06-03 11:47:27 +0000
@@ -23,7 +23,9 @@
23from buildinfo import BuildInfo, IncorrectDataFormatException23from buildinfo import BuildInfo, IncorrectDataFormatException
24from render_text_files import RenderTextFiles24from render_text_files import RenderTextFiles
25from models import License25from models import License
26from openid_auth import OpenIDAuth26# Load group auth "plugin" dynamically
27import importlib
28group_auth = importlib.import_module(settings.GROUP_AUTH_MODULE)
27from BeautifulSoup import BeautifulSoup29from BeautifulSoup import BeautifulSoup
28import config30import config
2931
@@ -242,7 +244,7 @@
242 license_type = build_info.get("license-type")244 license_type = build_info.get("license-type")
243 license_text = build_info.get("license-text")245 license_text = build_info.get("license-text")
244 theme = build_info.get("theme")246 theme = build_info.get("theme")
245 openid_teams = build_info.get("openid-launchpad-teams")247 auth_groups = build_info.get("auth-groups")
246 max_index = build_info.max_index248 max_index = build_info.max_index
247 elif os.path.isfile(open_eula_path):249 elif os.path.isfile(open_eula_path):
248 return "OPEN"250 return "OPEN"
@@ -256,7 +258,7 @@
256 license_type = "protected"258 license_type = "protected"
257 license_file = os.path.join(settings.PROJECT_ROOT,259 license_file = os.path.join(settings.PROJECT_ROOT,
258 'templates/licenses/' + theme + '.txt')260 'templates/licenses/' + theme + '.txt')
259 openid_teams = False261 auth_groups = False
260 with open(license_file, "r") as infile:262 with open(license_file, "r") as infile:
261 license_text = infile.read()263 license_text = infile.read()
262 elif _check_special_eula(path):264 elif _check_special_eula(path):
@@ -264,7 +266,7 @@
264 license_type = "protected"266 license_type = "protected"
265 license_file = os.path.join(settings.PROJECT_ROOT,267 license_file = os.path.join(settings.PROJECT_ROOT,
266 'templates/licenses/' + theme + '.txt')268 'templates/licenses/' + theme + '.txt')
267 openid_teams = False269 auth_groups = False
268 with open(license_file, "r") as infile:270 with open(license_file, "r") as infile:
269 license_text = infile.read()271 license_text = infile.read()
270 elif _check_special_eula(base_path + "/*"):272 elif _check_special_eula(base_path + "/*"):
@@ -279,7 +281,7 @@
279 return "OPEN"281 return "OPEN"
280282
281 # File matches a license, isn't open.283 # File matches a license, isn't open.
282 if openid_teams:284 if auth_groups:
283 return "OPEN"285 return "OPEN"
284 elif license_text:286 elif license_text:
285 for i in range(max_index):287 for i in range(max_index):
@@ -392,6 +394,30 @@
392 return response394 return response
393395
394396
397def group_auth_failed_response(request, auth_groups):
398 """Construct a nice response detailing list of auth groups that
399 will allow access to the requested file."""
400 if len(auth_groups) > 1:
401 groups_string = "one of the " + auth_groups.pop(0) + " "
402 if len(auth_groups) > 1:
403 groups_string += ", ".join(auth_groups[0:-1])
404
405 groups_string += " or " + auth_groups[-1] + " teams"
406 else:
407 groups_string = "the " + auth_groups[0] + " team"
408
409 response = render_to_response(
410 'openid_forbidden_template.html',
411 {'login': settings.LOGIN_URL + "?next=" + request.path,
412 'authenticated': request.user.is_authenticated(),
413 'openid_teams': groups_string,
414 'revno': bzr_version.get_my_bzr_revno(),
415 })
416
417 response.status_code = 403
418 return response
419
420
395def file_server(request, path):421def file_server(request, path):
396 """Serve up a file / directory listing or license page as required"""422 """Serve up a file / directory listing or license page as required"""
397 path = iri_to_uri(path)423 path = iri_to_uri(path)
@@ -415,16 +441,19 @@
415 return HttpResponseForbidden(441 return HttpResponseForbidden(
416 "Error parsing BUILD-INFO.txt")442 "Error parsing BUILD-INFO.txt")
417443
418 launchpad_teams = build_info.get("openid-launchpad-teams")444 auth_groups = build_info.get("auth-groups")
419 if launchpad_teams:445 if auth_groups:
420 launchpad_teams = launchpad_teams.split(",")446 auth_groups = auth_groups.split(",")
421 launchpad_teams = [team.strip() for team in launchpad_teams]447 auth_groups = [g.strip() for g in auth_groups]
422 # TODO: use logging!448 # TODO: use logging!
423 print "Checking membership in OpenID groups:", launchpad_teams449 print "Checking membership in auth groups:", auth_groups
424 openid_response = OpenIDAuth.process_openid_auth(450 response = group_auth.process_group_auth(request, auth_groups)
425 request, launchpad_teams)451 if response == False:
426 if openid_response:452 return group_auth_failed_response(request, auth_groups)
427 return openid_response453 elif response == True:
454 pass
455 else:
456 return response
428457
429 if target_type == "dir":458 if target_type == "dir":
430 # Generate a link to the parent directory (if one exists)459 # Generate a link to the parent directory (if one exists)
431460
=== modified file 'settings.py'
--- settings.py 2013-02-26 19:38:30 +0000
+++ settings.py 2013-06-03 11:47:27 +0000
@@ -124,6 +124,10 @@
124LOGIN_URL = '/linaro-openid/login/'124LOGIN_URL = '/linaro-openid/login/'
125LOGIN_REDIRECT_URL = '/'125LOGIN_REDIRECT_URL = '/'
126126
127# Name of "plugin" module to use for group authentication
128GROUP_AUTH_MODULE = 'license_protected_downloads.group_auth_openid'
129
130# Config for django_openid_auth.auth.OpenIDBackend
127OPENID_CREATE_USERS = True131OPENID_CREATE_USERS = True
128OPENID_SSO_SERVER_URL = 'https://login.launchpad.net/'132OPENID_SSO_SERVER_URL = 'https://login.launchpad.net/'
129OPENID_UPDATE_DETAILS_FROM_SREG = True133OPENID_UPDATE_DETAILS_FROM_SREG = True
@@ -132,6 +136,11 @@
132OPENID_USE_AS_ADMIN_LOGIN = True136OPENID_USE_AS_ADMIN_LOGIN = True
133OPENID_USE_EMAIL_FOR_USERNAME = True137OPENID_USE_EMAIL_FOR_USERNAME = True
134138
139ATLASSIAN_CROWD_API_URL = \
140 "https://login.linaro.org:8443/crowd/rest/usermanagement/1"
141ATLASSIAN_CROWD_API_USERNAME = None
142ATLASSIAN_CROWD_API_PASSWORD = None
143
135# A sample logging configuration. The only tangible logging144# A sample logging configuration. The only tangible logging
136# performed by this configuration is to send an email to145# performed by this configuration is to send an email to
137# the site admins on every HTTP 500 error.146# the site admins on every HTTP 500 error.

Subscribers

People subscribed via source and target branches