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

Proposed by Paul Sokolovsky
Status: Merged
Merged at revision: 195
Proposed branch: lp:~pfalcon/linaro-license-protection/crowd-auth
Merge into: lp:~linaro-automation/linaro-license-protection/trunk
Diff against target: 749 lines (+244/-136)
16 files modified
README (+23/-11)
license_protected_downloads/buildinfo.py (+13/-7)
license_protected_downloads/group_auth_common.py (+3/-0)
license_protected_downloads/group_auth_crowd.py (+48/-0)
license_protected_downloads/group_auth_openid.py (+19/-32)
license_protected_downloads/tests/BUILD-INFO.txt (+1/-1)
license_protected_downloads/tests/__init__.py (+2/-0)
license_protected_downloads/tests/test_buildinfo.py (+13/-4)
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 (+54/-15)
settings.py (+19/-1)
templates/group_auth_failure.html (+10/-0)
To merge this branch: bzr merge lp:~pfalcon/linaro-license-protection/crowd-auth
Reviewer Review Type Date Requested Status
Milo Casagrande (community) Approve
Georgy Redkozubov Pending
Review via email: mp+167025@code.launchpad.net

This proposal supersedes a proposal from 2013-05-28.

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.

UPDATE 2013-06-03: Finalized patchset re-submitted.

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

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 : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

> 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 : Posted in a previous version of this proposal

> 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).

Revision history for this message
Milo Casagrande (milo) wrote :
Download full text (3.7 KiB)

Hello Paul!

Thanks for pulling this together!
Following are my findings!

On Mon, Jun 3, 2013 at 1:57 PM, Paul Sokolovsky
<email address hidden> wrote:
>
> === modified file 'README'
> --- README 2013-02-15 09:45:08 +0000
> +++ README 2013-06-03 11:56:29 +0000
> @@ -16,7 +16,9 @@
> - * openid restrictions (by Launchpad teams)
> + * group-based access authorization restrictions (using group information
> + from external services, currently OpenID with team extensions is
> + (as used by Launchpad.net) and Atlassian Crowd are supported).

I guess the "is" on the second changed line should be removed.

> + * Auth-Groups: (optional)
> + Names of groups, members of which are allowed to access protected files.

"Name of groups" should be fine, but better have a
real-English-speaking person review for this one :-)

> -Format-Version: 0.1
> +Format-Version: 0.5

> def _set(self, key, value):
> + "key: file pattern, value: list of dicts of field/val pairs"

Can you please split this on two lines?

> === 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:56:29 +0000
> @@ -0,0 +1,52 @@
> +import logging
> +
> +from django.conf import settings
> +from django.shortcuts import redirect
> +
> +import requests
> +
> +
> +log = logging.getLogger(__file__)
> +
> +
> +def upgrade_requests():
> + """Ubuntu 12.04 comes with pretty old requests version. Add convenience
> + methods of newer versions straight to it, to avoid client-side
> + workarounds."""
> + if "json" not in dir(requests.models.Response):
> + def patchy_json(self):
> + import json
> + return json.loads(self.content)
> + requests.models.Response.json = patchy_json
> +
> +# We monkey-patch requests module on first load
> +upgrade_requests()
> +
> +
> +class GroupAuthError(Exception):
> + pass
> +
> +
> +def process_group_auth(request, required_groups):
> + if not required_groups:
> + return True
> + if not request.user.is_authenticated():
> + # Force OpenID login
> + return redirect(settings.LOGIN_URL + "?next=" + request.path)
> +
> + log.warn("Authenticating using Crowd API: %s",
> + request.user.username)
> +
> + auth = (settings.ATLASSIAN_CROWD_API_USERNAME,
> + settings.ATLASSIAN_CROWD_API_PASSWORD)
> + params = {"username": request.user.username}
> + r = requests.get(settings.ATLASSIAN_CROWD_API_URL
> + + "/user/group/nested.json", params=params, auth=auth)
> + if r.status_code != 200:
> + raise GroupAuthError(r.status_code)
> + data = r.json()
> + user_groups = set([x["name"] for x in data["groups"]])
> + for g in required_groups:
> + if g in user_groups:
> + return True
> + return False

I know there is nothing wrong with the for loop, but just a suggestion
to avoid it:

return user_groups.isdisjoint(set(required_groups))

Since you already have a set there in place.

> === added directory 'license_protected_downloads/tests/...

Read more...

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

Paul, thanks for working on this and a good job.

Here are my 5cents:

One thought about versions, I think it makes sense to add warning that user is using "old" version of BUILD-INFO.txt with OpenID-Launchpad-Teams which will be deprecated sooner or later.

Another is about tests. May be it is worth renaming test 'test_apply_to_dir(self)' to something like 'test_apply_to_dir_openid_launchpad_teams_field(self)' to better reflect what it does after you've added backwards compatibility with renaming of fields.

And the last one is in license_protected_downloads/buildinfo.py
There is a function that does write BUILD-INFO.txt from an array, it's used in splicing, it writes out Format-Version: 0.1, I guess we need to update it to write latest version (currently = 0.5) since it will be updated during read.

    def write_from_array(cls, build_info_array, file_path):
        with open(file_path, "w") as outfile:
            outfile.write("Format-Version: 0.1\n\n")

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

> I guess the "is" on the second changed line should be removed.

Fixed.

> "Name of groups" should be fine, but better have a real-English-speaking person review for this one :-)

Yep, I paused at that too when wrote that, but somehow did it like that (to emphasize that members are of group, not of names?). Let it be just "List of groups".

> Can you please split this on two lines?

Done.

> I know there is nothing wrong with the for loop, but just a suggestion to avoid it:
> return user_groups.isdisjoint(set(required_groups))

Gotta remember (those who will later look at the code too) that Python now has pretty complete set implementation! Another problem is readability and understandability. In particular, in this case, it should be "return not". All in all, it now requires a comment where previously it didn't. Well, ok, let's go for it.

> Hmmm... shouldn't this be bumped to 0.5?

Makes sense.

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.

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

> I think it makes sense to add warning that user is using "old" version of BUILD-INFO.txt with OpenID-Launchpad-Teams which will be deprecated sooner or later.

Yes, makes sense, I added warning this may happen in the future to README. But when it should happen? It should happen during publishing stage, not in webapp. Or do you hint about updating scripts/publish_to_snapshots.py? Well, that's not directly related with the aim of this patch, which is "adding Crowd API support to l-l-p webapp", and would be just scope creep even further. So, let's leave this for separate patch.

> Another is about tests. May be it is worth renaming test 'test_apply_to_dir(self)' to something like 'test_apply_to_dir_openid_launchpad_teams_field(self)' to better reflect what it does after you've added backwards compatibility with renaming of fields.

Well, that test actually tests how BUILD-INFO.txt applies to dirs, nothing else. It's just that was the only test in testsuite which deals with group field, so I selected it as a template for Auth-Groups field test. It's not ideal, but the whole testsuite is not ideal (well, I'm not sure it's the aim - it just should provide test coverage). Improving that should be a separate effort again.

> There is a function that does write BUILD-INFO.txt from an array, it's used in splicing, it writes out Format-Version: 0.1

Fixed, thanks.

210. By Paul Sokolovsky

write_from_array(): Write 0.5 format files.

Revision history for this message
Milo Casagrande (milo) wrote :

Thanks Paul! Looks good to me.

review: Approve
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.

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-04 18:22: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
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+ List 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-04 18:22: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,9 @@
94 os.path.join(cls.get_search_path(path), "BUILD-INFO.txt"))
95
96 def _set(self, key, value):
97+ """Record set of directives applying to a file pattern
98+ key: file pattern
99+ value: list of dicts of field/val pairs"""
100 if key in self.build_info_array[self.index]:
101 # A repeated key indicates we have found another chunk of
102 # build-info
103@@ -91,10 +97,7 @@
104 if index > self.max_index:
105 return False
106 block = self.file_info_array[index]
107- for key in block:
108- if field == key:
109- return block[field]
110- return False
111+ return block.get(field, False)
112
113 def parseLine(self, line):
114 values = line.split(":", 1)
115@@ -108,6 +111,9 @@
116 raise IncorrectDataFormatException(
117 "Field '%s' not allowed." % field)
118 else:
119+ # Rename any deprecated field names to new names
120+ field_renames = {"openid-launchpad-teams": "auth-groups"}
121+ field = field_renames.get(field, field)
122 return {field: value}
123
124 def isValidField(self, field_name):
125@@ -189,7 +195,7 @@
126 @classmethod
127 def write_from_array(cls, build_info_array, file_path):
128 with open(file_path, "w") as outfile:
129- outfile.write("Format-Version: 0.1\n\n")
130+ outfile.write("Format-Version: 0.5\n\n")
131 for key in build_info_array[0]:
132 if key != "format-version":
133 outfile.write("Files-Pattern: %s\n" % key)
134
135=== added file 'license_protected_downloads/group_auth_common.py'
136--- license_protected_downloads/group_auth_common.py 1970-01-01 00:00:00 +0000
137+++ license_protected_downloads/group_auth_common.py 2013-06-04 18:22:27 +0000
138@@ -0,0 +1,3 @@
139+class GroupAuthError(Exception):
140+ "Unexpected (infastructure) error during group authorization check."
141+ pass
142
143=== added file 'license_protected_downloads/group_auth_crowd.py'
144--- license_protected_downloads/group_auth_crowd.py 1970-01-01 00:00:00 +0000
145+++ license_protected_downloads/group_auth_crowd.py 2013-06-04 18:22:27 +0000
146@@ -0,0 +1,48 @@
147+import logging
148+
149+from django.conf import settings
150+from django.shortcuts import redirect
151+import requests
152+
153+from group_auth_common import GroupAuthError
154+
155+
156+log = logging.getLogger(__file__)
157+
158+
159+def upgrade_requests():
160+ """Ubuntu 12.04 comes with pretty old requests version. Add convenience
161+ methods of newer versions straight to it, to avoid client-side
162+ workarounds."""
163+ if "json" not in dir(requests.models.Response):
164+ def patchy_json(self):
165+ import json
166+ return json.loads(self.content)
167+ requests.models.Response.json = patchy_json
168+
169+# We monkey-patch requests module on first load
170+upgrade_requests()
171+
172+
173+def process_group_auth(request, required_groups):
174+ if not required_groups:
175+ return True
176+ if not request.user.is_authenticated():
177+ # Force OpenID login
178+ return redirect(settings.LOGIN_URL + "?next=" + request.path)
179+
180+ log.warn("Authenticating using Crowd API: %s",
181+ request.user.username)
182+
183+ auth = (settings.ATLASSIAN_CROWD_API_USERNAME,
184+ settings.ATLASSIAN_CROWD_API_PASSWORD)
185+ params = {"username": request.user.username}
186+ r = requests.get(settings.ATLASSIAN_CROWD_API_URL
187+ + "/user/group/nested.json", params=params, auth=auth)
188+ if r.status_code != 200:
189+ raise GroupAuthError(r.status_code)
190+ data = r.json()
191+ user_groups = set([x["name"] for x in data["groups"]])
192+
193+ # If groups don't intersect, access denied
194+ return not user_groups.isdisjoint(required_groups)
195
196=== renamed file 'license_protected_downloads/openid_auth.py' => 'license_protected_downloads/group_auth_openid.py'
197--- license_protected_downloads/openid_auth.py 2012-08-23 13:37:29 +0000
198+++ license_protected_downloads/group_auth_openid.py 2013-06-04 18:22:27 +0000
199@@ -1,16 +1,21 @@
200+import logging
201+
202 from django.conf import settings
203-from django.shortcuts import redirect, render_to_response
204+from django.shortcuts import redirect
205 from django.contrib.auth.models import Group
206-import bzr_version
207-
208-
209-class OpenIDAuth:
210-
211- @classmethod
212- def process_openid_auth(cls, request, openid_teams):
213+
214+
215+log = logging.getLogger(__file__)
216+
217+
218+def process_group_auth(request, openid_teams):
219+ """Returns True if access granted, False if denied and Response
220+ object if not enough authentication information available and
221+ user should authenticate first (by following that Response).
222+ """
223
224 if not openid_teams:
225- return None
226+ return True
227
228 for openid_team in openid_teams:
229 Group.objects.get_or_create(name=openid_team)
230@@ -19,28 +24,10 @@
231 # Force OpenID login
232 return redirect(settings.LOGIN_URL + "?next=" + request.path)
233
234+ log.warn("Authenticating using Launchpad OpenID Teams: %s",
235+ request.user.username)
236 for group in request.user.groups.all():
237 if group.name in openid_teams:
238- return None
239-
240- # Construct a nice string of openid teams that will allow access to
241- # the requested file
242- if len(openid_teams) > 1:
243- teams_string = "one of the " + openid_teams.pop(0) + " "
244- if len(openid_teams) > 1:
245- teams_string += ", ".join(openid_teams[0:-1])
246-
247- teams_string += " or " + openid_teams[-1] + " teams"
248- else:
249- teams_string = "the " + openid_teams[0] + " team"
250-
251- response = render_to_response(
252- 'openid_forbidden_template.html',
253- {'login': settings.LOGIN_URL + "?next=" + request.path,
254- 'authenticated': request.user.is_authenticated(),
255- 'openid_teams': teams_string,
256- 'revno': bzr_version.get_my_bzr_revno(),
257- })
258-
259- response.status_code = 403
260- return response
261+ return True
262+
263+ return False
264
265=== modified file 'license_protected_downloads/tests/BUILD-INFO.txt'
266--- license_protected_downloads/tests/BUILD-INFO.txt 2012-06-26 18:50:23 +0000
267+++ license_protected_downloads/tests/BUILD-INFO.txt 2013-06-04 18:22:27 +0000
268@@ -1,4 +1,4 @@
269-Format-Version: 0.1
270+Format-Version: 0.5
271
272 Files-Pattern: *.txt
273 Build-Name: landing-protected
274
275=== modified file 'license_protected_downloads/tests/__init__.py'
276--- license_protected_downloads/tests/__init__.py 2013-04-19 12:13:03 +0000
277+++ license_protected_downloads/tests/__init__.py 2013-06-04 18:22:27 +0000
278@@ -11,6 +11,7 @@
279 FileViewTests,
280 HowtoViewTests,
281 ViewTests,
282+ ViewHelpersTests,
283 )
284 from license_protected_downloads.tests.test_openid_auth import TestOpenIDAuth
285 from license_protected_downloads.tests.test_custom_commands \
286@@ -32,4 +33,5 @@
287 'TestPep8': TestPep8,
288 'TestPyflakes': TestPyflakes,
289 'ViewTests': ViewTests,
290+ 'ViewHelpersTests': ViewHelpersTests,
291 }
292
293=== modified file 'license_protected_downloads/tests/test_buildinfo.py'
294--- license_protected_downloads/tests/test_buildinfo.py 2013-04-19 12:13:03 +0000
295+++ license_protected_downloads/tests/test_buildinfo.py 2013-06-04 18:22:27 +0000
296@@ -27,7 +27,16 @@
297 self.assertEquals(build_info.getInfoForFile(),
298 [{'build-name': 'landing-protected',
299 'license-type': 'protected',
300- 'openid-launchpad-teams': 'linaro'}])
301+ 'auth-groups': 'linaro'}])
302+
303+ def test_apply_to_dir_auth_groups_field(self):
304+ dir_path = THIS_DIRECTORY + \
305+ '/testserver_root/build-info/subdir2'
306+ build_info = BuildInfo(dir_path)
307+ self.assertEquals(build_info.getInfoForFile(),
308+ [{'build-name': 'landing-protected',
309+ 'license-type': 'protected',
310+ 'auth-groups': 'linaro'}])
311
312 def test_apply_to_nonexistent_file(self):
313 with self.assertRaises(IOError):
314@@ -41,14 +50,14 @@
315 def test_getFormatVersion(self):
316 build_info = BuildInfo(self.buildinfo_file_path)
317
318- self.assertEqual("0.1", build_info.getFormatVersion())
319+ self.assertEqual("0.5", build_info.getFormatVersion())
320
321 def test_get_emptyField(self):
322 value = "notempty"
323 build_info = BuildInfo(self.buildinfo_file_path)
324 for pair in build_info.file_info_array:
325- if "openid-launchpad-teams" in pair:
326- value = pair["openid-launchpad-teams"]
327+ if "auth-groups" in pair:
328+ value = pair["auth-groups"]
329
330 self.assertFalse(value)
331
332
333=== modified file 'license_protected_downloads/tests/test_openid_auth.py'
334--- license_protected_downloads/tests/test_openid_auth.py 2012-08-23 13:37:29 +0000
335+++ license_protected_downloads/tests/test_openid_auth.py 2013-06-04 18:22:27 +0000
336@@ -2,7 +2,7 @@
337 from django.http import HttpResponse
338 from mock import Mock, patch
339
340-from license_protected_downloads.openid_auth import OpenIDAuth
341+from license_protected_downloads import group_auth_openid as openid_auth
342
343
344 class TestOpenIDAuth(TestCase):
345@@ -27,14 +27,14 @@
346 def test_check_team_membership_no_teams(self):
347 mock_request = self.make_mock_request()
348 openid_teams = []
349- self.assertIsNone(
350- OpenIDAuth.process_openid_auth(mock_request, openid_teams))
351+ self.assertTrue(
352+ openid_auth.process_group_auth(mock_request, openid_teams))
353
354 def test_check_team_membership_no_authentication(self):
355 mock_request = self.make_mock_request()
356 mock_request.user.is_authenticated.return_value = False
357 openid_teams = ["linaro"]
358- response = OpenIDAuth.process_openid_auth(mock_request, openid_teams)
359+ response = openid_auth.process_group_auth(mock_request, openid_teams)
360 self.assertIsNotNone(response)
361 self.assertTrue(isinstance(response, HttpResponse))
362 self.assertEquals(302, response.status_code)
363@@ -45,8 +45,8 @@
364 mock_request.user.groups.all.return_value = [
365 self.make_mock_group("linaro")]
366 openid_teams = ["linaro"]
367- response = OpenIDAuth.process_openid_auth(mock_request, openid_teams)
368- self.assertIsNone(response)
369+ response = openid_auth.process_group_auth(mock_request, openid_teams)
370+ self.assertTrue(response)
371
372 def test_check_no_team_membership_authed(self):
373 mock_request = self.make_mock_request()
374@@ -54,12 +54,8 @@
375 mock_request.user.groups.all.return_value = [
376 self.make_mock_group("another-group")]
377 openid_teams = ["linaro"]
378- response = OpenIDAuth.process_openid_auth(mock_request, openid_teams)
379- self.assertIsNotNone(response)
380- self.assertTrue(isinstance(response, HttpResponse))
381- self.assertContains(response,
382- "You need to be the member of the linaro team in Launchpad.",
383- status_code=403)
384+ response = openid_auth.process_group_auth(mock_request, openid_teams)
385+ self.assertFalse(response)
386
387 def test_check_no_team_membership_authed_multi_teams(self):
388 mock_request = self.make_mock_request()
389@@ -67,13 +63,8 @@
390 mock_request.user.groups.all.return_value = [
391 self.make_mock_group("another-group")]
392 openid_teams = ["linaro", "batman", "catwoman", "joker"]
393- response = OpenIDAuth.process_openid_auth(mock_request, openid_teams)
394- self.assertIsNotNone(response)
395- self.assertTrue(isinstance(response, HttpResponse))
396- self.assertContains(response,
397- "You need to be the member of one of the linaro batman, catwoman "
398- "or joker teams in Launchpad.",
399- status_code=403)
400+ response = openid_auth.process_group_auth(mock_request, openid_teams)
401+ self.assertFalse(response)
402
403 @patch("django.contrib.auth.models.Group.objects.get_or_create")
404 def test_auto_adding_groups(self, get_or_create_mock):
405@@ -83,7 +74,7 @@
406 self.make_mock_group("another-group")]
407
408 openid_teams = ["linaro", "linaro-infrastructure"]
409- OpenIDAuth.process_openid_auth(mock_request, openid_teams)
410+ openid_auth.process_group_auth(mock_request, openid_teams)
411
412 expected = [
413 ((), {'name': 'linaro'}), ((), {'name': 'linaro-infrastructure'})]
414
415=== modified file 'license_protected_downloads/tests/test_splicebuildinfos.py'
416--- license_protected_downloads/tests/test_splicebuildinfos.py 2013-04-19 12:13:03 +0000
417+++ license_protected_downloads/tests/test_splicebuildinfos.py 2013-06-04 18:22:27 +0000
418@@ -20,16 +20,16 @@
419 'test-protected.txt':
420 [{'license-type': 'protected',
421 'build-name': 'landing-protected',
422- 'openid-launchpad-teams': 'linaro'}],
423+ 'auth-groups': 'linaro'}],
424 'test-protected-2.txt':
425 [{'license-type': 'protected',
426 'build-name': 'landing-protected',
427- 'openid-launchpad-teams': 'linaro'}]}
428+ 'auth-groups': 'linaro'}]}
429
430 result = {'test-protected.txt, test-protected-2.txt':
431 [{'license-type': 'protected',
432 'build-name': 'landing-protected',
433- 'openid-launchpad-teams': 'linaro'}]}
434+ 'auth-groups': 'linaro'}]}
435
436 build_info_res = SpliceBuildInfos.merge_duplicates(build_info_dict)
437 self.assertEquals(build_info_res, result)
438
439=== modified file 'license_protected_downloads/tests/test_views.py'
440--- license_protected_downloads/tests/test_views.py 2013-04-02 09:58:42 +0000
441+++ license_protected_downloads/tests/test_views.py 2013-06-04 18:22:27 +0000
442@@ -2,6 +2,7 @@
443
444 from django.conf import settings
445 from django.test import Client, TestCase
446+from django.http import HttpResponse
447 import hashlib
448 import os
449 import tempfile
450@@ -10,6 +11,8 @@
451 import urlparse
452 import json
453
454+from mock import Mock
455+
456 from license_protected_downloads import bzr_version
457 from license_protected_downloads.buildinfo import BuildInfo
458 from license_protected_downloads.config import INTERNAL_HOSTS
459@@ -19,6 +22,7 @@
460 from license_protected_downloads.views import _process_include_tags
461 from license_protected_downloads.views import _sizeof_fmt
462 from license_protected_downloads.views import is_same_parent_dir
463+from license_protected_downloads import views
464
465 THIS_DIRECTORY = os.path.dirname(os.path.abspath(__file__))
466 TESTSERVER_ROOT = os.path.join(THIS_DIRECTORY, "testserver_root")
467@@ -903,5 +907,19 @@
468 self.assertEqual(response.status_code, 200)
469
470
471+class ViewHelpersTests(BaseServeViewTest):
472+ def test_auth_group_error(self):
473+ groups = ["linaro", "batman", "catwoman", "joker"]
474+ request = Mock()
475+ request.path = "mock_path"
476+ response = views.group_auth_failed_response(request, groups)
477+ self.assertIsNotNone(response)
478+ self.assertTrue(isinstance(response, HttpResponse))
479+ self.assertContains(response,
480+ "You need to be the member of one of the linaro batman, catwoman "
481+ "or joker teams in Launchpad.",
482+ status_code=403)
483+
484+
485 if __name__ == '__main__':
486 unittest.main()
487
488=== added directory 'license_protected_downloads/tests/testserver_root/build-info/subdir2'
489=== added file 'license_protected_downloads/tests/testserver_root/build-info/subdir2/BUILD-INFO.txt'
490--- license_protected_downloads/tests/testserver_root/build-info/subdir2/BUILD-INFO.txt 1970-01-01 00:00:00 +0000
491+++ license_protected_downloads/tests/testserver_root/build-info/subdir2/BUILD-INFO.txt 2013-06-04 18:22:27 +0000
492@@ -0,0 +1,7 @@
493+Format-Version: 0.5
494+
495+
496+Files-Pattern: *
497+Build-Name: landing-protected
498+License-Type: protected
499+Auth-Groups: linaro
500
501=== added file 'license_protected_downloads/tests/testserver_root/build-info/subdir2/testfile.txt'
502=== removed file 'license_protected_downloads/tests/testserver_root/build-info/write-test/BUILD-INFO.txt'
503--- license_protected_downloads/tests/testserver_root/build-info/write-test/BUILD-INFO.txt 2013-04-19 12:13:03 +0000
504+++ license_protected_downloads/tests/testserver_root/build-info/write-test/BUILD-INFO.txt 1970-01-01 00:00:00 +0000
505@@ -1,42 +0,0 @@
506-Format-Version: 0.1
507-
508-Files-Pattern: *openid*
509-license-type: protected
510-build-name: landing-protected
511-openid-launchpad-teams: linaro
512-
513-Files-Pattern: *panda*
514-license-type: open
515-build-name: landing-panda
516-
517-Files-Pattern: *.txt
518-license-type: protected
519-build-name: landing-protected
520-openid-launchpad-teams:
521-theme: stericsson
522-collect-user-data: yes
523-license-text: <p>IMPORTANT — PLEASE READ THE FOLLOWING AGREEMENT CAREFULLY.</p>
524- <p>
525- THIS IS A LEGALLY BINDING AGREEMENT
526- </p>
527-
528-Files-Pattern: *origen*
529-theme: samsung
530-license-type: protected
531-build-name: landing-origen
532-license-text: <p>IMPORTANT — PLEASE READ THE FOLLOWING AGREEMENT CAREFULLY.</p>
533- <p>
534- THIS IS A LEGALLY BINDING AGREEMENT BETWEEN YOU, an individual or a
535- legal entity, (“LICENSEE”) AND HAL 1000.
536- </p>
537-
538-Files-Pattern: *snowball*
539-theme: stericsson
540-license-type: protected
541-build-name: landing-snowball
542-license-text: <p>IMPORTANT — PLEASE READ THE FOLLOWING AGREEMENT CAREFULLY.</p>
543- <p>
544- THIS IS A LEGALLY BINDING AGREEMENT BETWEEN YOU, an individual or a
545- legal entity, (“LICENSEE”) AND ME, THE TEST SERVER.
546- </p>
547-
548
549=== modified file 'license_protected_downloads/views.py'
550--- license_protected_downloads/views.py 2013-05-09 11:15:33 +0000
551+++ license_protected_downloads/views.py 2013-06-04 18:22:27 +0000
552@@ -1,3 +1,4 @@
553+import logging
554 import glob
555 import hashlib
556 import json
557@@ -23,9 +24,12 @@
558 from buildinfo import BuildInfo, IncorrectDataFormatException
559 from render_text_files import RenderTextFiles
560 from models import License
561-from openid_auth import OpenIDAuth
562+# Load group auth "plugin" dynamically
563+import importlib
564+group_auth = importlib.import_module(settings.GROUP_AUTH_MODULE)
565 from BeautifulSoup import BeautifulSoup
566 import config
567+from group_auth_common import GroupAuthError
568
569
570 LINARO_INCLUDE_FILE_RE = re.compile(
571@@ -33,6 +37,8 @@
572 LINARO_INCLUDE_FILE_RE1 = re.compile(
573 r'<linaro:include file="(?P<file_name>.*)">(.*)</linaro:include>')
574
575+log = logging.getLogger(__file__)
576+
577
578 def _hidden_file(file_name):
579 hidden_files = ["BUILD-INFO.txt", "EULA.txt", r"^\.", "HEADER.html"]
580@@ -242,7 +248,7 @@
581 license_type = build_info.get("license-type")
582 license_text = build_info.get("license-text")
583 theme = build_info.get("theme")
584- openid_teams = build_info.get("openid-launchpad-teams")
585+ auth_groups = build_info.get("auth-groups")
586 max_index = build_info.max_index
587 elif os.path.isfile(open_eula_path):
588 return "OPEN"
589@@ -256,7 +262,7 @@
590 license_type = "protected"
591 license_file = os.path.join(settings.PROJECT_ROOT,
592 'templates/licenses/' + theme + '.txt')
593- openid_teams = False
594+ auth_groups = False
595 with open(license_file, "r") as infile:
596 license_text = infile.read()
597 elif _check_special_eula(path):
598@@ -264,7 +270,7 @@
599 license_type = "protected"
600 license_file = os.path.join(settings.PROJECT_ROOT,
601 'templates/licenses/' + theme + '.txt')
602- openid_teams = False
603+ auth_groups = False
604 with open(license_file, "r") as infile:
605 license_text = infile.read()
606 elif _check_special_eula(base_path + "/*"):
607@@ -279,7 +285,7 @@
608 return "OPEN"
609
610 # File matches a license, isn't open.
611- if openid_teams:
612+ if auth_groups:
613 return "OPEN"
614 elif license_text:
615 for i in range(max_index):
616@@ -392,6 +398,30 @@
617 return response
618
619
620+def group_auth_failed_response(request, auth_groups):
621+ """Construct a nice response detailing list of auth groups that
622+ will allow access to the requested file."""
623+ if len(auth_groups) > 1:
624+ groups_string = "one of the " + auth_groups.pop(0) + " "
625+ if len(auth_groups) > 1:
626+ groups_string += ", ".join(auth_groups[0:-1])
627+
628+ groups_string += " or " + auth_groups[-1] + " teams"
629+ else:
630+ groups_string = "the " + auth_groups[0] + " team"
631+
632+ response = render_to_response(
633+ 'openid_forbidden_template.html',
634+ {'login': settings.LOGIN_URL + "?next=" + request.path,
635+ 'authenticated': request.user.is_authenticated(),
636+ 'openid_teams': groups_string,
637+ 'revno': bzr_version.get_my_bzr_revno(),
638+ })
639+
640+ response.status_code = 403
641+ return response
642+
643+
644 def file_server(request, path):
645 """Serve up a file / directory listing or license page as required"""
646 path = iri_to_uri(path)
647@@ -415,16 +445,25 @@
648 return HttpResponseForbidden(
649 "Error parsing BUILD-INFO.txt")
650
651- launchpad_teams = build_info.get("openid-launchpad-teams")
652- if launchpad_teams:
653- launchpad_teams = launchpad_teams.split(",")
654- launchpad_teams = [team.strip() for team in launchpad_teams]
655- # TODO: use logging!
656- print "Checking membership in OpenID groups:", launchpad_teams
657- openid_response = OpenIDAuth.process_openid_auth(
658- request, launchpad_teams)
659- if openid_response:
660- return openid_response
661+ auth_groups = build_info.get("auth-groups")
662+ if auth_groups:
663+ auth_groups = auth_groups.split(",")
664+ auth_groups = [g.strip() for g in auth_groups]
665+ log.info("Checking membership in auth groups: %s", auth_groups)
666+ try:
667+ response = group_auth.process_group_auth(request, auth_groups)
668+ except GroupAuthError:
669+ log.exception("GroupAuthError")
670+ response = render_to_response('group_auth_failure.html')
671+ response.status_code = 500
672+ return response
673+
674+ if response == False:
675+ return group_auth_failed_response(request, auth_groups)
676+ elif response == True:
677+ pass
678+ else:
679+ return response
680
681 if target_type == "dir":
682 # Generate a link to the parent directory (if one exists)
683
684=== modified file 'settings.py'
685--- settings.py 2013-02-26 19:38:30 +0000
686+++ settings.py 2013-06-04 18:22:27 +0000
687@@ -124,6 +124,10 @@
688 LOGIN_URL = '/linaro-openid/login/'
689 LOGIN_REDIRECT_URL = '/'
690
691+# Name of "plugin" module to use for group authentication
692+GROUP_AUTH_MODULE = 'license_protected_downloads.group_auth_openid'
693+
694+# Config for django_openid_auth.auth.OpenIDBackend
695 OPENID_CREATE_USERS = True
696 OPENID_SSO_SERVER_URL = 'https://login.launchpad.net/'
697 OPENID_UPDATE_DETAILS_FROM_SREG = True
698@@ -132,6 +136,11 @@
699 OPENID_USE_AS_ADMIN_LOGIN = True
700 OPENID_USE_EMAIL_FOR_USERNAME = True
701
702+ATLASSIAN_CROWD_API_URL = \
703+ "https://login.linaro.org:8443/crowd/rest/usermanagement/1"
704+ATLASSIAN_CROWD_API_USERNAME = None
705+ATLASSIAN_CROWD_API_PASSWORD = None
706+
707 # A sample logging configuration. The only tangible logging
708 # performed by this configuration is to send an email to
709 # the site admins on every HTTP 500 error.
710@@ -144,7 +153,11 @@
711 'mail_admins': {
712 'level': 'ERROR',
713 'class': 'django.utils.log.AdminEmailHandler'
714- }
715+ },
716+ 'console': {
717+ 'level': 'DEBUG',
718+ 'class': 'logging.StreamHandler',
719+ },
720 },
721 'loggers': {
722 'django.request': {
723@@ -152,6 +165,11 @@
724 'level': 'ERROR',
725 'propagate': True,
726 },
727+ # Root logger
728+ '': {
729+ 'level': 'DEBUG',
730+ 'handlers': ['console'],
731+ },
732 }
733 }
734
735
736=== added file 'templates/group_auth_failure.html'
737--- templates/group_auth_failure.html 1970-01-01 00:00:00 +0000
738+++ templates/group_auth_failure.html 2013-06-04 18:22:27 +0000
739@@ -0,0 +1,10 @@
740+{% extends "header.html" %}
741+
742+{% block content %}
743+ <h1>Group Authentication Error</h1>
744+ <p>
745+ Unexpected group authentication error happened. Error details were logged.
746+ Please contact administrator for resolution.
747+ </p>
748+</table>
749+{% endblock %}

Subscribers

People subscribed via source and target branches