Merge lp:~salgado/offspring/private-projects into lp:~linaro-automation/offspring/linaro
- private-projects
- Merge into linaro
Status: | Merged |
---|---|
Approved by: | James Tunnicliffe |
Approved revision: | no longer in the source branch. |
Merged at revision: | 55 |
Proposed branch: | lp:~salgado/offspring/private-projects |
Merge into: | lp:~linaro-automation/offspring/linaro |
Prerequisite: | lp:~salgado/offspring/object-factory |
Diff against target: |
630 lines (+402/-19) 11 files modified
Makefile (+4/-1) lib/offspring/web/queuemanager/models.py (+137/-15) lib/offspring/web/queuemanager/sql/project.postgresql_psycopg2.sql (+5/-0) lib/offspring/web/queuemanager/tests/__init__.py (+1/-0) lib/offspring/web/queuemanager/tests/factory.py (+20/-2) lib/offspring/web/queuemanager/tests/test_models.py (+228/-0) lib/offspring/web/queuemanager/views.py (+1/-0) lib/offspring/web/settings.py (+1/-0) lib/offspring/web/templates/queuemanager/projectgroup_details.html (+1/-1) migration/001-project-privacy.sql (+3/-0) requirements/requirements.web.txt (+1/-0) |
To merge this branch: | bzr merge lp:~salgado/offspring/private-projects |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Cody A.W. Somerville | Pending | ||
Linaro Infrastructure | Pending | ||
Review via email: mp+76752@code.launchpad.net |
Commit message
Description of the change
This branch adds support for private projects on models of web.queuemanager. It adds two new attributes (is_private and owner) to Project, and also makes Project inherit from AccessGroupMixin, so that we can allow people other than the owner to see certain private projects.
The access control is enforced by the new model managers, when looking up projects or their related objects. In order to make it harder to accidentally leak private objects, the model manager (accessible via .objects) of Project and all its related models will only return public stuff, and there's a new manager (accessible via .all_objects) which provides an API to query only the objects a given user is allowed to see (.accessible_
NOTE: In order to try this branch you'll need to download http://
James Tunnicliffe (dooferlad) wrote : | # |
Guilherme Salgado (salgado) wrote : | # |
Thanks for the review, James.
I've just noticed, though, that we must ensure the .all_objects manager is the one set as ._default_manager (used only internally by Django) or else the admin UI would hide all private objects and we would break model validation of unique fields if the user reuses a value from an object they are not allowed to see. The revision I've just pushed addresses that and it'd be great if you could review it as well.
James Tunnicliffe (dooferlad) wrote : | # |
Yep, http://
looks fine.
On 29 September 2011 19:15, Guilherme Salgado
<email address hidden> wrote:
> Thanks for the review, James.
>
> I've just noticed, though, that we must ensure the .all_objects manager is the one set as ._default_manager (used only internally by Django) or else the admin UI would hide all private objects and we would break model validation of unique fields if the user reuses a value from an object they are not allowed to see. The revision I've just pushed addresses that and it'd be great if you could review it as well.
> --
> https:/
> Your team Linaro Infrastructure is requested to review the proposed merge of lp:~salgado/offspring/private-projects into lp:~linaro-infrastructure/offspring/linaro.
>
--
James Tunnicliffe
Preview Diff
1 | === modified file 'Makefile' |
2 | --- Makefile 2011-09-23 14:01:26 +0000 |
3 | +++ Makefile 2011-09-29 18:17:25 +0000 |
4 | @@ -46,7 +46,10 @@ |
5 | ./.virtualenv/bin/nosetests offspring.master |
6 | |
7 | test-web: web install-test-runner |
8 | - ./bin/offspring-web test --settings=offspring.web.settings_test |
9 | + ./bin/offspring-web test queuemanager --settings=offspring.web.settings_test |
10 | + |
11 | +test-web-postgres: web install-test-runner |
12 | + ./bin/offspring-web test queuemanager --settings=offspring.web.settings_devel |
13 | |
14 | test: master slave web install-test-runner test-web |
15 | ./.virtualenv/bin/nosetests -e 'queuemanager.*' |
16 | |
17 | === modified file 'lib/offspring/web/queuemanager/models.py' |
18 | --- lib/offspring/web/queuemanager/models.py 2011-09-28 14:23:09 +0000 |
19 | +++ lib/offspring/web/queuemanager/models.py 2011-09-29 18:17:25 +0000 |
20 | @@ -12,14 +12,17 @@ |
21 | ) |
22 | import math |
23 | |
24 | -from django.conf import settings |
25 | -from django.contrib.auth.models import User |
26 | +from django.contrib.auth.models import AnonymousUser, User |
27 | from django.db import ( |
28 | connection, |
29 | models |
30 | ) |
31 | |
32 | from offspring import config |
33 | +from django_group_access.models import ( |
34 | + AccessGroup, |
35 | + AccessGroupMixin, |
36 | + AccessManager) |
37 | from offspring.enums import ProjectBuildStates |
38 | |
39 | |
40 | @@ -74,9 +77,9 @@ |
41 | def get_absolute_url(self): |
42 | return "/project-groups/%s/" % self.name |
43 | |
44 | - @property |
45 | - def projects(self): |
46 | - return Project.objects.filter(project_group=self) |
47 | + def get_projects(self, user): |
48 | + return Project.all_objects.accessible_by_user(user).filter( |
49 | + project_group=self) |
50 | |
51 | @property |
52 | def builder(self): |
53 | @@ -84,11 +87,10 @@ |
54 | |
55 | @property |
56 | def is_active(self): |
57 | - return bool(Project.objects.filter(project_group=self).exclude(is_active=False).count() != 0) |
58 | - |
59 | - @property |
60 | - def needs_build(self): |
61 | - return BuildRequest.objects.filter(project__project_group=self).exists() |
62 | + # Can safely use .all_objects here because we're not returning any |
63 | + # objects so there's no risk of leaking private stuff. |
64 | + projects = Project.all_objects.filter(project_group=self) |
65 | + return projects.exclude(is_active=False).count() > 0 |
66 | |
67 | @property |
68 | def average_build_time(self): |
69 | @@ -112,7 +114,81 @@ |
70 | return "UNKNOWN" |
71 | |
72 | |
73 | -class Project(models.Model): |
74 | +class PublicOnlyObjectsManager(models.Manager): |
75 | + """A model manager which only returns public objects.""" |
76 | + |
77 | + def get_query_set(self): |
78 | + return super(PublicOnlyObjectsManager, self).get_query_set().filter( |
79 | + models.Q(**{self.model._get_privacy_attr(): False})) |
80 | + |
81 | + |
82 | +class AccessControlledObjectsManager(AccessManager): |
83 | + """A model manager which provides an extra API for querying objects a |
84 | + given user is allowed to see. |
85 | + |
86 | + We extend AccessManager because we want the access-control filters to be |
87 | + applied only for private objects, but also because we allow AnonymousUsers |
88 | + to be passed to accessible_by_user(). |
89 | + """ |
90 | + |
91 | + def _get_accessible_by_user_filter_rules(self, user): |
92 | + rules = super(AccessControlledObjectsManager, |
93 | + self)._get_accessible_by_user_filter_rules(user) |
94 | + return rules | models.Q(**{self.model._get_privacy_attr(): False}) |
95 | + |
96 | + def accessible_by_user(self, user): |
97 | + if isinstance(user, AnonymousUser): |
98 | + # If we're passed in an AnonymousUser we must not attempt to apply |
99 | + # the usual constraints because they rely on the given user |
100 | + # existing in the DB and that's not the case with AnonymousUsers. |
101 | + query = models.Q(**{self.model._get_privacy_attr(): False}) |
102 | + return super( |
103 | + AccessControlledObjectsManager, self).get_query_set().filter( |
104 | + query) |
105 | + else: |
106 | + return super( |
107 | + AccessControlledObjectsManager, self).accessible_by_user(user) |
108 | + |
109 | + |
110 | +class MaybePrivateMixin(object): |
111 | + """A model which may be private or linked to a private one. |
112 | + |
113 | + If the model itself is private, it's expected to have an '_is_private' |
114 | + boolean attribute. Otherwise it must have an 'access_relation' attribute |
115 | + pointing to the linked model which has the '_is_private' attribute. |
116 | + """ |
117 | + _privacy_attr_name = '_is_private' |
118 | + _is_visible_method_name = '_is_visible_to' |
119 | + |
120 | + @classmethod |
121 | + def _get_privacy_attr(self): |
122 | + privacy_attr = self._privacy_attr_name |
123 | + if getattr(self, 'access_relation', None) is not None: |
124 | + privacy_attr = self.access_relation + '__' + privacy_attr |
125 | + return privacy_attr |
126 | + |
127 | + def _get_access_controlled_object(self): |
128 | + if getattr(self, 'access_relation', None) is None: |
129 | + return self |
130 | + names = self.access_relation.split('__') |
131 | + obj = self |
132 | + while len(names): |
133 | + name = names.pop(0) |
134 | + obj = getattr(obj, name) |
135 | + return obj |
136 | + |
137 | + @property |
138 | + def is_private(self): |
139 | + obj = self._get_access_controlled_object() |
140 | + return getattr(obj, self._privacy_attr_name) |
141 | + |
142 | + def is_visible_to(self, user): |
143 | + is_visible_to = getattr(self._get_access_controlled_object(), |
144 | + self._is_visible_method_name) |
145 | + return is_visible_to(user) |
146 | + |
147 | + |
148 | +class Project(AccessGroupMixin, MaybePrivateMixin): |
149 | #XXX: This should probably be managed in the database. |
150 | STATUS_CHOICES = ( |
151 | (u'devel', u'Active Development'), |
152 | @@ -122,7 +198,15 @@ |
153 | (u'experimental', u'Experimental'), |
154 | (u'obsolete', u'Obsolete'), |
155 | ) |
156 | + all_objects = AccessControlledObjectsManager() |
157 | + # Using PublicOnlyObjectsManager here we ensure that any oversights when |
158 | + # updating existing uses of Project.objects won't leak private projects. |
159 | + objects = PublicOnlyObjectsManager() |
160 | |
161 | + _is_private = models.BooleanField(default=False, db_column='is_private') |
162 | + # We allow projects without an owner for backwards compatibility but |
163 | + # there's a DB constraint to ensure private projects always have an owner. |
164 | + owner = models.ForeignKey(User, blank=True, null=True) |
165 | name = models.SlugField('codename', max_length=200, primary_key=True, unique=True) |
166 | title = models.CharField('project title', max_length=30) |
167 | project_group = models.ForeignKey(ProjectGroup, blank=True, null=True) |
168 | @@ -145,6 +229,16 @@ |
169 | def __unicode__(self): |
170 | return self.display_name() |
171 | |
172 | + def _is_visible_to(self, user): |
173 | + if not self._is_private or user == self.owner: |
174 | + return True |
175 | + if isinstance(user, AnonymousUser): |
176 | + # Private objects are never visible to anonymous users. |
177 | + return False |
178 | + access_groups = set(self.access_groups.all()) |
179 | + user_groups = AccessGroup.objects.filter(members=user) |
180 | + return len(access_groups.intersection(user_groups)) > 0 |
181 | + |
182 | def get_absolute_url(self): |
183 | return "/projects/%s/" % self.name |
184 | |
185 | @@ -192,7 +286,7 @@ |
186 | return u"" |
187 | |
188 | |
189 | -class Lexbuilder(models.Model): |
190 | +class Lexbuilder(models.Model, MaybePrivateMixin): |
191 | name = models.SlugField(max_length=200, unique=True) |
192 | uri = models.CharField(max_length=200) |
193 | current_state = models.CharField(max_length=200, default="UNKNOWN", editable=False) |
194 | @@ -205,6 +299,13 @@ |
195 | current_job = models.ForeignKey("BuildResult", db_column="current_job_id", editable=False, blank=True, null=True) |
196 | notes = models.TextField('whiteboard', blank=True, null=True) |
197 | |
198 | + access_relation = 'current_job__project' |
199 | + all_objects = AccessControlledObjectsManager() |
200 | + # Using PublicOnlyObjectsManager here we ensure that any oversights when |
201 | + # updating existing uses of Lexbuilder.objects won't leak anything related |
202 | + # to private projects. |
203 | + objects = PublicOnlyObjectsManager() |
204 | + |
205 | class Meta: |
206 | db_table = "lexbuilders" |
207 | |
208 | @@ -266,7 +367,7 @@ |
209 | return cursor.fetchone()[0] |
210 | |
211 | |
212 | -class BuildResult(models.Model): |
213 | +class BuildResult(models.Model, MaybePrivateMixin): |
214 | name = models.CharField('build name', max_length=200, blank=True, null=True, editable=False) |
215 | project = models.ForeignKey(Project, db_column="project_name", editable=False) |
216 | builder = models.ForeignKey(Lexbuilder, blank=True, null=True, editable=False) |
217 | @@ -279,6 +380,13 @@ |
218 | dispatched_at = models.DateTimeField('date dispatched', editable=False, null=True, blank=True) |
219 | notes = models.CharField(max_length=200, null=True, blank=True) |
220 | |
221 | + access_relation = 'project' |
222 | + all_objects = AccessControlledObjectsManager() |
223 | + # Using PublicOnlyObjectsManager here we ensure that any oversights when |
224 | + # updating existing uses of BuildResult.objects won't leak anything |
225 | + # related to private projects. |
226 | + objects = PublicOnlyObjectsManager() |
227 | + |
228 | class Meta: |
229 | db_table = "buildresults" |
230 | get_latest_by = "started_at" |
231 | @@ -326,13 +434,20 @@ |
232 | recentBuildFailures = staticmethod(recentBuildFailures) |
233 | |
234 | |
235 | -class BuildRequest(models.Model): |
236 | +class BuildRequest(models.Model, MaybePrivateMixin): |
237 | project = models.ForeignKey(Project, db_column="project_name") |
238 | requestor = models.ForeignKey(User, db_column="requestor_id", blank=True, null=True) |
239 | created_at = models.DateTimeField('date created', auto_now_add=True) |
240 | score = models.IntegerField(default=10) |
241 | reason = models.TextField('request reason', blank=True, max_length=200) |
242 | |
243 | + access_relation = 'project' |
244 | + all_objects = AccessControlledObjectsManager() |
245 | + # Using PublicOnlyObjectsManager here we ensure that any oversights when |
246 | + # updating existing uses of BuildRequest.objects won't leak anything |
247 | + # related to private projects. |
248 | + objects = PublicOnlyObjectsManager() |
249 | + |
250 | class Meta: |
251 | db_table = "buildrequests" |
252 | |
253 | @@ -451,7 +566,7 @@ |
254 | return "%s subscribed to %s" % (self.user, self.project) |
255 | |
256 | |
257 | -class Release(models.Model): |
258 | +class Release(models.Model, MaybePrivateMixin): |
259 | STATUS_PENDING, STATUS_PUBLISHED, STATUS_REMOVED, STATUS_MISSING, STATUS_ERROR = range(5) |
260 | STATUS_CHOICES = ( |
261 | (STATUS_PENDING, 'Pending Publication'), |
262 | @@ -469,6 +584,13 @@ |
263 | ("GM", "Gold Master"), |
264 | ) |
265 | |
266 | + access_relation = 'build__project' |
267 | + all_objects = AccessControlledObjectsManager() |
268 | + # Using PublicOnlyObjectsManager here we ensure that any oversights when |
269 | + # updating existing uses of Release.objects won't leak anything related to |
270 | + # private projects. |
271 | + objects = PublicOnlyObjectsManager() |
272 | + |
273 | build = models.OneToOneField(BuildResult, primary_key=True, unique=True) |
274 | name = models.CharField('release name', max_length=200) |
275 | milestone = models.ForeignKey(LaunchpadProjectMilestone, null=True, blank=True) |
276 | |
277 | === added directory 'lib/offspring/web/queuemanager/sql' |
278 | === added file 'lib/offspring/web/queuemanager/sql/project.postgresql_psycopg2.sql' |
279 | --- lib/offspring/web/queuemanager/sql/project.postgresql_psycopg2.sql 1970-01-01 00:00:00 +0000 |
280 | +++ lib/offspring/web/queuemanager/sql/project.postgresql_psycopg2.sql 2011-09-29 18:17:25 +0000 |
281 | @@ -0,0 +1,5 @@ |
282 | +-- XXX: This doesn't apply when running tests because sqlite3 doesn't support |
283 | +-- addition of new constraints to existing tables. Need to decide whether |
284 | +-- it's preferred to just ignore this in our tests or use postgresql to run |
285 | +-- the tests as well. |
286 | +ALTER TABLE projects ADD CONSTRAINT "private_project_requires_owner" CHECK (is_private IS FALSE OR owner_id IS NOT NULL); |
287 | |
288 | === modified file 'lib/offspring/web/queuemanager/tests/__init__.py' |
289 | --- lib/offspring/web/queuemanager/tests/__init__.py 2011-09-23 13:31:21 +0000 |
290 | +++ lib/offspring/web/queuemanager/tests/__init__.py 2011-09-29 18:17:25 +0000 |
291 | @@ -0,0 +1,1 @@ |
292 | +from .test_models import * |
293 | |
294 | === modified file 'lib/offspring/web/queuemanager/tests/factory.py' |
295 | --- lib/offspring/web/queuemanager/tests/factory.py 2011-09-27 19:58:38 +0000 |
296 | +++ lib/offspring/web/queuemanager/tests/factory.py 2011-09-29 18:17:25 +0000 |
297 | @@ -1,6 +1,7 @@ |
298 | from itertools import count |
299 | |
300 | from django.contrib.auth.models import User |
301 | +from django_group_access.models import AccessGroup |
302 | |
303 | from offspring.web.queuemanager.models import ( |
304 | BuildRequest, |
305 | @@ -41,12 +42,20 @@ |
306 | user.save() |
307 | return user |
308 | |
309 | - def makeProject(self, name=None, title=None): |
310 | + def makeProject(self, name=None, title=None, is_private=False, |
311 | + project_group=None, owner=None, access_groups=[]): |
312 | if name is None: |
313 | name = self.getUniqueString() |
314 | if title is None: |
315 | title = self.getUniqueString() |
316 | - project = Project(name=name, title=title) |
317 | + if owner is None: |
318 | + owner = self.makeUser() |
319 | + project = Project( |
320 | + name=name, title=title, _is_private=is_private, owner=owner, |
321 | + project_group=project_group) |
322 | + project.save() |
323 | + for group in access_groups: |
324 | + project.access_groups.add(group) |
325 | project.save() |
326 | return project |
327 | |
328 | @@ -97,5 +106,14 @@ |
329 | request.save() |
330 | return request |
331 | |
332 | + def makeAccessGroup(self, users): |
333 | + name = self.getUniqueString() |
334 | + group = AccessGroup(name=name) |
335 | + group.save() |
336 | + for user in users: |
337 | + group.members.add(user) |
338 | + group.save() |
339 | + return group |
340 | + |
341 | |
342 | factory = ObjectFactory() |
343 | |
344 | === added file 'lib/offspring/web/queuemanager/tests/test_models.py' |
345 | --- lib/offspring/web/queuemanager/tests/test_models.py 1970-01-01 00:00:00 +0000 |
346 | +++ lib/offspring/web/queuemanager/tests/test_models.py 2011-09-29 18:17:25 +0000 |
347 | @@ -0,0 +1,228 @@ |
348 | +from operator import attrgetter |
349 | + |
350 | +from django.contrib.auth.models import AnonymousUser |
351 | +from django.test import TestCase |
352 | + |
353 | +from offspring.web.queuemanager.models import ( |
354 | + BuildRequest, |
355 | + BuildResult, |
356 | + Lexbuilder, |
357 | + Project, |
358 | + Release) |
359 | +from offspring.web.queuemanager.tests.factory import factory |
360 | + |
361 | + |
362 | +class ProjectTests(TestCase): |
363 | + |
364 | + def test_default_manager(self): |
365 | + # Make sure .all_objects is the default manager or else the admin UI |
366 | + # will exclude all private objects and we would break model validation |
367 | + # of unique fields if the user reuses a value from an object they're |
368 | + # not allowed to see. |
369 | + self.assertEquals(Project.all_objects, Project._default_manager) |
370 | + |
371 | + def test_is_private_for_public_project(self): |
372 | + project = factory.makeProject(is_private=False) |
373 | + self.assertEqual(False, project.is_private) |
374 | + |
375 | + def test_is_private_for_private_project(self): |
376 | + private_project = factory.makeProject(is_private=True) |
377 | + self.assertEqual(True, private_project.is_private) |
378 | + |
379 | + def test_is_visible_to_anyone_if_public(self): |
380 | + project = factory.makeProject(is_private=False) |
381 | + self.assertTrue(project.is_visible_to(AnonymousUser())) |
382 | + |
383 | + def test_is_not_visible_to_anyone_if_private(self): |
384 | + project = factory.makeProject(is_private=True) |
385 | + self.assertFalse(project.is_visible_to(AnonymousUser())) |
386 | + |
387 | + def test_is_visible_to_owner_if_private(self): |
388 | + project = factory.makeProject(is_private=True) |
389 | + self.assertTrue(project.is_visible_to(project.owner)) |
390 | + |
391 | + def test_is_visible_to_member_of_access_group_if_private(self): |
392 | + user = factory.makeUser() |
393 | + group = factory.makeAccessGroup([user]) |
394 | + project = factory.makeProject(is_private=True, access_groups=[group]) |
395 | + self.assertTrue(project.is_visible_to(user)) |
396 | + |
397 | + def test_default_model_manager_only_returns_public_projects(self): |
398 | + project = factory.makeProject(is_private=False) |
399 | + project2 = factory.makeProject(is_private=True) |
400 | + self.assertEqual([project], list(Project.objects.all())) |
401 | + |
402 | + def test_all_objects_model_manager(self): |
403 | + # Project.all_objects() is a separate model manager which returns |
404 | + # private objects as well. Its .all() method will return public and |
405 | + # private objects regardless of whether or not the current user has |
406 | + # access to them. It is to be used with caution. |
407 | + project = factory.makeProject(is_private=False) |
408 | + project2 = factory.makeProject(is_private=True) |
409 | + self.assertEqual([project, project2], list(Project.all_objects.all())) |
410 | + |
411 | + def test_accessible_by_user_with_anonymous_user(self): |
412 | + project = factory.makeProject(is_private=True) |
413 | + project2 = factory.makeProject(is_private=False) |
414 | + self.assertEqual(True, project.is_private) |
415 | + self.assertEqual(False, project2.is_private) |
416 | + objects = Project.all_objects.accessible_by_user(AnonymousUser()) |
417 | + self.assertEqual([project2], list(objects)) |
418 | + |
419 | + def test_accessible_by_user_with_public_project(self): |
420 | + user = factory.makeUser() |
421 | + project = factory.makeProject(is_private=False) |
422 | + self.assertEqual(False, project.is_private) |
423 | + objects = Project.all_objects.accessible_by_user(user) |
424 | + self.assertEqual([project], list(objects)) |
425 | + |
426 | + def test_accessible_by_user_with_private_project(self): |
427 | + user = factory.makeUser() |
428 | + group = factory.makeAccessGroup([user]) |
429 | + project = factory.makeProject(is_private=True, access_groups=[group]) |
430 | + # This second project is private but has no access groups set up, so |
431 | + # the user cannot see it. |
432 | + project2 = factory.makeProject(is_private=True) |
433 | + self.assertEqual(True, project.is_private) |
434 | + self.assertEqual(True, project2.is_private) |
435 | + objects = Project.all_objects.accessible_by_user(user) |
436 | + self.assertEqual([project], list(objects)) |
437 | + |
438 | + |
439 | +class ReleaseOrLexbuilderOrBuildResultOrBuildRequestTestsMixin(object): |
440 | + """A mixin with tests that work for the following models: |
441 | + |
442 | + Release |
443 | + Lexbuilder |
444 | + BuildResult |
445 | + BuildRequest |
446 | + |
447 | + You just need to mix this with TestCase in your subclass and define |
448 | + factoryMethod and model. |
449 | + """ |
450 | + model = None |
451 | + |
452 | + def factoryMethod(self, project): |
453 | + raise NotImplementedError() |
454 | + |
455 | + def test_default_manager(self): |
456 | + # Make sure .all_objects is the default manager or else the admin UI |
457 | + # will exclude all private objects and we would break model validation |
458 | + # of unique fields if the user reuses a value from an object they're |
459 | + # not allowed to see. |
460 | + self.assertEquals(self.model.all_objects, self.model._default_manager) |
461 | + |
462 | + def test_is_private_for_public_object(self): |
463 | + public_object = self.factoryMethod( |
464 | + factory.makeProject(is_private=False)) |
465 | + self.assertFalse(public_object.is_private) |
466 | + |
467 | + def test_is_private_for_private_object(self): |
468 | + private_object = self.factoryMethod( |
469 | + factory.makeProject(is_private=True)) |
470 | + self.assertTrue(private_object.is_private) |
471 | + |
472 | + def test_is_visible_to_anyone_if_public(self): |
473 | + public_object = self.factoryMethod( |
474 | + factory.makeProject(is_private=False)) |
475 | + self.assertTrue(public_object.is_visible_to(AnonymousUser())) |
476 | + |
477 | + def test_is_not_visible_to_anyone_if_private(self): |
478 | + private_object = self.factoryMethod( |
479 | + factory.makeProject(is_private=True)) |
480 | + self.assertFalse(private_object.is_visible_to(AnonymousUser())) |
481 | + |
482 | + def test_is_visible_to_owner_if_private(self): |
483 | + # The object we're dealing with inherits the access-control rules from |
484 | + # the project it's related to, so the owner that matters is the |
485 | + # project owner. |
486 | + owner = factory.makeUser() |
487 | + private_object = self.factoryMethod( |
488 | + factory.makeProject(is_private=True, owner=owner)) |
489 | + self.assertTrue(private_object.is_visible_to(owner)) |
490 | + |
491 | + def test_is_visible_to_member_of_access_group_if_private(self): |
492 | + user = factory.makeUser() |
493 | + group = factory.makeAccessGroup([user]) |
494 | + private_object = self.factoryMethod( |
495 | + factory.makeProject(is_private=True, access_groups=[group])) |
496 | + self.assertTrue(private_object.is_visible_to(user)) |
497 | + |
498 | + def test_default_model_manager_only_returns_public_objects(self): |
499 | + private_object = self.factoryMethod( |
500 | + factory.makeProject(is_private=True)) |
501 | + public_object = self.factoryMethod( |
502 | + factory.makeProject(is_private=False)) |
503 | + self.assertEqual([public_object], list(self.model.objects.all())) |
504 | + |
505 | + def test_all_objects_model_manager(self): |
506 | + # self.model.all_objects() is a separate model manager which returns |
507 | + # private objects as well. Its .all() method will return public and |
508 | + # private objects regardless of whether or not the current user has |
509 | + # access to them. It is to be used with caution. |
510 | + public_object = self.factoryMethod( |
511 | + project=factory.makeProject(is_private=False)) |
512 | + private_object = self.factoryMethod( |
513 | + project=factory.makeProject(is_private=True)) |
514 | + self.assertEqual([public_object, private_object], |
515 | + list(self.model.all_objects.all())) |
516 | + |
517 | + def test_accessible_by_user(self): |
518 | + user = factory.makeUser() |
519 | + group = factory.makeAccessGroup([user]) |
520 | + visible_object = self.factoryMethod( |
521 | + factory.makeProject(is_private=True, access_groups=[group])) |
522 | + # This object is linked to a private project which has no access |
523 | + # groups set up, so the user cannot see it. |
524 | + invisible_object = self.factoryMethod( |
525 | + project=factory.makeProject(is_private=True)) |
526 | + self.assertEqual( |
527 | + [visible_object], |
528 | + list(self.model.all_objects.accessible_by_user(user))) |
529 | + |
530 | + |
531 | +class ReleaseTests( |
532 | + TestCase, ReleaseOrLexbuilderOrBuildResultOrBuildRequestTestsMixin): |
533 | + model = Release |
534 | + |
535 | + def factoryMethod(self, project): |
536 | + return factory.makeRelease( |
537 | + build=factory.makeBuildResult(project=project)) |
538 | + |
539 | + |
540 | +class LexbuilderTests( |
541 | + TestCase, ReleaseOrLexbuilderOrBuildResultOrBuildRequestTestsMixin): |
542 | + model = Lexbuilder |
543 | + |
544 | + def factoryMethod(self, project): |
545 | + return factory.makeLexbuilder( |
546 | + current_job=factory.makeBuildResult(project=project)) |
547 | + |
548 | + |
549 | +class BuildResultTests( |
550 | + TestCase, ReleaseOrLexbuilderOrBuildResultOrBuildRequestTestsMixin): |
551 | + model = BuildResult |
552 | + factoryMethod = factory.makeBuildResult |
553 | + |
554 | + |
555 | +class BuildRequestTests( |
556 | + TestCase, ReleaseOrLexbuilderOrBuildResultOrBuildRequestTestsMixin): |
557 | + model = BuildRequest |
558 | + factoryMethod = factory.makeBuildRequest |
559 | + |
560 | + |
561 | +class ProjectGroupTests(TestCase): |
562 | + |
563 | + def test_get_projects_filters_private_objects(self): |
564 | + user = factory.makeUser() |
565 | + group = factory.makeProjectGroup() |
566 | + public_project = factory.makeProject( |
567 | + is_private=False, project_group=group) |
568 | + private_project_visible_to_user = factory.makeProject( |
569 | + is_private=True, project_group=group, owner=user) |
570 | + private_project = factory.makeProject( |
571 | + is_private=True, project_group=group) |
572 | + self.assertEqual( |
573 | + sorted([public_project, private_project_visible_to_user], |
574 | + key=attrgetter('name')), |
575 | + sorted(group.get_projects(user), key=attrgetter('name'))) |
576 | |
577 | === modified file 'lib/offspring/web/queuemanager/views.py' |
578 | --- lib/offspring/web/queuemanager/views.py 2011-08-13 06:28:28 +0000 |
579 | +++ lib/offspring/web/queuemanager/views.py 2011-09-29 18:17:25 +0000 |
580 | @@ -241,6 +241,7 @@ |
581 | |
582 | pageData = { |
583 | 'projectGroup' : pg, |
584 | + 'projects': pg.get_projects(request.user), |
585 | 'pillar' : 'projects', |
586 | 'build_stats' : build_stats, |
587 | 'projectgroup_build_results' : projectgroup_build_results, |
588 | |
589 | === modified file 'lib/offspring/web/settings.py' |
590 | --- lib/offspring/web/settings.py 2011-09-23 14:01:26 +0000 |
591 | +++ lib/offspring/web/settings.py 2011-09-29 18:17:25 +0000 |
592 | @@ -96,6 +96,7 @@ |
593 | 'djcelery', |
594 | 'djkombu', |
595 | 'piston', |
596 | + 'django_group_access', |
597 | # Offspring Apps |
598 | 'offspring.web.queuemanager', |
599 | ) |
600 | |
601 | === modified file 'lib/offspring/web/templates/queuemanager/projectgroup_details.html' |
602 | --- lib/offspring/web/templates/queuemanager/projectgroup_details.html 2010-11-29 08:27:24 +0000 |
603 | +++ lib/offspring/web/templates/queuemanager/projectgroup_details.html 2011-09-29 18:17:25 +0000 |
604 | @@ -32,7 +32,7 @@ |
605 | <h3>Project</h3></td><td></td><td><h3>Last Build</h3></td><td width="30%"><h3>Date</h3></td><td width="15%"><h3>Result</h3> |
606 | </td> |
607 | </tr> |
608 | - {% for project in projectGroup.projects %} |
609 | + {% for project in projects %} |
610 | <tr> |
611 | <td> |
612 | {% if project.needs_build %}<img src="/assets/images/33.png" title="Pending Build">{% else %}{% ifequal project.latest_build.result "FAILED" %}<img src="/assets/images/34.png" title="Build Failure">{% else %} {% ifequal project.latest_build.result "COMPLETED" %} <img src="/assets/images/35.png" title="Build Successful"> {% else %} <img src="/assets/images/36.png" title="Not yet built">{% endifequal %}{% endifequal %}{% endif %}</td><td> <a href="{% url offspring.web.queuemanager.views.project_details project.name %}">{{ project.title }}</a> |
613 | |
614 | === added directory 'migration' |
615 | === added file 'migration/001-project-privacy.sql' |
616 | --- migration/001-project-privacy.sql 1970-01-01 00:00:00 +0000 |
617 | +++ migration/001-project-privacy.sql 2011-09-29 18:17:25 +0000 |
618 | @@ -0,0 +1,3 @@ |
619 | +ALTER TABLE projects ADD COLUMN "is_private" boolean NOT NULL default FALSE; |
620 | +ALTER TABLE projects ADD COLUMN "owner_id" integer REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED; |
621 | +ALTER TABLE projects ADD CONSTRAINT "private_project_requires_owner" CHECK (is_private IS FALSE OR owner_id IS NOT NULL); |
622 | |
623 | === modified file 'requirements/requirements.web.txt' |
624 | --- requirements/requirements.web.txt 2011-08-11 00:16:09 +0000 |
625 | +++ requirements/requirements.web.txt 2011-09-29 18:17:25 +0000 |
626 | @@ -11,3 +11,4 @@ |
627 | celery>=2.3.1 |
628 | django-celery>=2.3.0 |
629 | django-kombu>=0.9.4 |
630 | +django-group-access>=0.0.1 |
Looks good.