Merge lp:~pfalcon/linaro-license-protection/crowd-auth into lp:~linaro-automation/linaro-license-protection/trunk
- crowd-auth
- Merge into trunk
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 |
Related bugs: |
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.
Commit message
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.
Milo Casagrande (milo) wrote : Posted in a previous version of this proposal | # |
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.
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 ;-)
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).
Milo Casagrande (milo) wrote : | # |
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-
> -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_
> --- license_
> +++ license_
> @@ -0,0 +1,52 @@
> +import logging
> +
> +from django.conf import settings
> +from django.shortcuts import redirect
> +
> +import requests
> +
> +
> +log = logging.
> +
> +
> +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.
> + def patchy_json(self):
> + import json
> + return json.loads(
> + requests.
> +
> +# We monkey-patch requests module on first load
> +upgrade_requests()
> +
> +
> +class GroupAuthError(
> + pass
> +
> +
> +def process_
> + if not required_groups:
> + return True
> + if not request.
> + # Force OpenID login
> + return redirect(
> +
> + log.warn(
> + request.
> +
> + auth = (settings.
> + settings.
> + params = {"username": request.
> + r = requests.
> + + "/user/
> + if r.status_code != 200:
> + raise GroupAuthError(
> + 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.
Since you already have a set there in place.
> === added directory 'license_
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-
Another is about tests. May be it is worth renaming test 'test_apply_
And the last one is in license_
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_
with open(file_path, "w") as outfile:
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-
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.
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.
Paul Sokolovsky (pfalcon) wrote : | # |
> I think it makes sense to add warning that user is using "old" version of BUILD-INFO.txt with OpenID-
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/
> Another is about tests. May be it is worth renaming test 'test_apply_
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.
Milo Casagrande (milo) wrote : | # |
Thanks Paul! Looks good to me.
- 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
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 %} |
Paul,
thanks for kick-starting this work!
Overall the work looks great, and the refactoring too.
+class ViewHelpersTest s(BaseServeView Test): group_error( self): auth_failed_ response( request, groups) tNone(response) (isinstance( response, HttpResponse)) ains(response,
+ def test_auth_
+ groups = ["linaro", "batman", "catwoman", "joker"]
+ request = Mock()
+ request.path = "mock_path"
+ response = views.group_
+ self.assertIsNo
+ self.assertTrue
+ self.assertCont
+ "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.failUnless Equal(response. status_ code, 403)
(not sure that that is working code though)
+def group_auth_ failed_ response( request, auth_groups): auth_groups[ 0:-1])
+ """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(
+
+ 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 += (" %s" % ", ".join( auth_groups[ 0:-1])) .rstrip( )
groups_string = "one of the %s" % auth_groups.pop(0)
if len(auth_groups) > 1:
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.