Merge lp:~salgado/offspring/private-projects into lp:~linaro-automation/offspring/linaro

Proposed by Guilherme Salgado
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
Reviewer Review Type Date Requested Status
Cody A.W. Somerville Pending
Linaro Infrastructure Pending
Review via email: mp+76752@code.launchpad.net

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_by_user(user)). This way we don't risk leaking any private data if we forget to update views that don't take privacy into account.

NOTE: In order to try this branch you'll need to download http://pypi.python.org/packages/source/d/django-group-access/django-group-access-0.0.1.tar.gz into your .download-cache (that's only until we get it added to https://code.launchpad.net/~oem-solutions-releng/offspring/offspring-deps/)

To post a comment you must log in.
Revision history for this message
James Tunnicliffe (dooferlad) wrote :

Looks good.

Revision history for this message
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.

Revision history for this message
James Tunnicliffe (dooferlad) wrote :

Yep, http://bazaar.launchpad.net/~salgado/offspring/private-projects/revision/68
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://code.launchpad.net/~salgado/offspring/private-projects/+merge/76752
> 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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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

Subscribers

People subscribed via source and target branches