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
=== modified file 'Makefile'
--- Makefile 2011-09-23 14:01:26 +0000
+++ Makefile 2011-09-29 18:17:25 +0000
@@ -46,7 +46,10 @@
46 ./.virtualenv/bin/nosetests offspring.master46 ./.virtualenv/bin/nosetests offspring.master
4747
48test-web: web install-test-runner48test-web: web install-test-runner
49 ./bin/offspring-web test --settings=offspring.web.settings_test49 ./bin/offspring-web test queuemanager --settings=offspring.web.settings_test
50
51test-web-postgres: web install-test-runner
52 ./bin/offspring-web test queuemanager --settings=offspring.web.settings_devel
5053
51test: master slave web install-test-runner test-web54test: master slave web install-test-runner test-web
52 ./.virtualenv/bin/nosetests -e 'queuemanager.*'55 ./.virtualenv/bin/nosetests -e 'queuemanager.*'
5356
=== modified file 'lib/offspring/web/queuemanager/models.py'
--- lib/offspring/web/queuemanager/models.py 2011-09-28 14:23:09 +0000
+++ lib/offspring/web/queuemanager/models.py 2011-09-29 18:17:25 +0000
@@ -12,14 +12,17 @@
12)12)
13import math13import math
1414
15from django.conf import settings15from django.contrib.auth.models import AnonymousUser, User
16from django.contrib.auth.models import User
17from django.db import (16from django.db import (
18 connection,17 connection,
19 models18 models
20)19)
2120
22from offspring import config21from offspring import config
22from django_group_access.models import (
23 AccessGroup,
24 AccessGroupMixin,
25 AccessManager)
23from offspring.enums import ProjectBuildStates26from offspring.enums import ProjectBuildStates
2427
2528
@@ -74,9 +77,9 @@
74 def get_absolute_url(self):77 def get_absolute_url(self):
75 return "/project-groups/%s/" % self.name78 return "/project-groups/%s/" % self.name
7679
77 @property80 def get_projects(self, user):
78 def projects(self):81 return Project.all_objects.accessible_by_user(user).filter(
79 return Project.objects.filter(project_group=self)82 project_group=self)
8083
81 @property84 @property
82 def builder(self): 85 def builder(self):
@@ -84,11 +87,10 @@
8487
85 @property88 @property
86 def is_active(self):89 def is_active(self):
87 return bool(Project.objects.filter(project_group=self).exclude(is_active=False).count() != 0)90 # Can safely use .all_objects here because we're not returning any
8891 # objects so there's no risk of leaking private stuff.
89 @property92 projects = Project.all_objects.filter(project_group=self)
90 def needs_build(self):93 return projects.exclude(is_active=False).count() > 0
91 return BuildRequest.objects.filter(project__project_group=self).exists()
9294
93 @property95 @property
94 def average_build_time(self):96 def average_build_time(self):
@@ -112,7 +114,81 @@
112 return "UNKNOWN"114 return "UNKNOWN"
113115
114116
115class Project(models.Model):117class PublicOnlyObjectsManager(models.Manager):
118 """A model manager which only returns public objects."""
119
120 def get_query_set(self):
121 return super(PublicOnlyObjectsManager, self).get_query_set().filter(
122 models.Q(**{self.model._get_privacy_attr(): False}))
123
124
125class AccessControlledObjectsManager(AccessManager):
126 """A model manager which provides an extra API for querying objects a
127 given user is allowed to see.
128
129 We extend AccessManager because we want the access-control filters to be
130 applied only for private objects, but also because we allow AnonymousUsers
131 to be passed to accessible_by_user().
132 """
133
134 def _get_accessible_by_user_filter_rules(self, user):
135 rules = super(AccessControlledObjectsManager,
136 self)._get_accessible_by_user_filter_rules(user)
137 return rules | models.Q(**{self.model._get_privacy_attr(): False})
138
139 def accessible_by_user(self, user):
140 if isinstance(user, AnonymousUser):
141 # If we're passed in an AnonymousUser we must not attempt to apply
142 # the usual constraints because they rely on the given user
143 # existing in the DB and that's not the case with AnonymousUsers.
144 query = models.Q(**{self.model._get_privacy_attr(): False})
145 return super(
146 AccessControlledObjectsManager, self).get_query_set().filter(
147 query)
148 else:
149 return super(
150 AccessControlledObjectsManager, self).accessible_by_user(user)
151
152
153class MaybePrivateMixin(object):
154 """A model which may be private or linked to a private one.
155
156 If the model itself is private, it's expected to have an '_is_private'
157 boolean attribute. Otherwise it must have an 'access_relation' attribute
158 pointing to the linked model which has the '_is_private' attribute.
159 """
160 _privacy_attr_name = '_is_private'
161 _is_visible_method_name = '_is_visible_to'
162
163 @classmethod
164 def _get_privacy_attr(self):
165 privacy_attr = self._privacy_attr_name
166 if getattr(self, 'access_relation', None) is not None:
167 privacy_attr = self.access_relation + '__' + privacy_attr
168 return privacy_attr
169
170 def _get_access_controlled_object(self):
171 if getattr(self, 'access_relation', None) is None:
172 return self
173 names = self.access_relation.split('__')
174 obj = self
175 while len(names):
176 name = names.pop(0)
177 obj = getattr(obj, name)
178 return obj
179
180 @property
181 def is_private(self):
182 obj = self._get_access_controlled_object()
183 return getattr(obj, self._privacy_attr_name)
184
185 def is_visible_to(self, user):
186 is_visible_to = getattr(self._get_access_controlled_object(),
187 self._is_visible_method_name)
188 return is_visible_to(user)
189
190
191class Project(AccessGroupMixin, MaybePrivateMixin):
116 #XXX: This should probably be managed in the database.192 #XXX: This should probably be managed in the database.
117 STATUS_CHOICES = (193 STATUS_CHOICES = (
118 (u'devel', u'Active Development'),194 (u'devel', u'Active Development'),
@@ -122,7 +198,15 @@
122 (u'experimental', u'Experimental'),198 (u'experimental', u'Experimental'),
123 (u'obsolete', u'Obsolete'),199 (u'obsolete', u'Obsolete'),
124 )200 )
201 all_objects = AccessControlledObjectsManager()
202 # Using PublicOnlyObjectsManager here we ensure that any oversights when
203 # updating existing uses of Project.objects won't leak private projects.
204 objects = PublicOnlyObjectsManager()
125205
206 _is_private = models.BooleanField(default=False, db_column='is_private')
207 # We allow projects without an owner for backwards compatibility but
208 # there's a DB constraint to ensure private projects always have an owner.
209 owner = models.ForeignKey(User, blank=True, null=True)
126 name = models.SlugField('codename', max_length=200, primary_key=True, unique=True)210 name = models.SlugField('codename', max_length=200, primary_key=True, unique=True)
127 title = models.CharField('project title', max_length=30)211 title = models.CharField('project title', max_length=30)
128 project_group = models.ForeignKey(ProjectGroup, blank=True, null=True)212 project_group = models.ForeignKey(ProjectGroup, blank=True, null=True)
@@ -145,6 +229,16 @@
145 def __unicode__(self):229 def __unicode__(self):
146 return self.display_name()230 return self.display_name()
147231
232 def _is_visible_to(self, user):
233 if not self._is_private or user == self.owner:
234 return True
235 if isinstance(user, AnonymousUser):
236 # Private objects are never visible to anonymous users.
237 return False
238 access_groups = set(self.access_groups.all())
239 user_groups = AccessGroup.objects.filter(members=user)
240 return len(access_groups.intersection(user_groups)) > 0
241
148 def get_absolute_url(self):242 def get_absolute_url(self):
149 return "/projects/%s/" % self.name243 return "/projects/%s/" % self.name
150244
@@ -192,7 +286,7 @@
192 return u""286 return u""
193287
194288
195class Lexbuilder(models.Model):289class Lexbuilder(models.Model, MaybePrivateMixin):
196 name = models.SlugField(max_length=200, unique=True)290 name = models.SlugField(max_length=200, unique=True)
197 uri = models.CharField(max_length=200)291 uri = models.CharField(max_length=200)
198 current_state = models.CharField(max_length=200, default="UNKNOWN", editable=False)292 current_state = models.CharField(max_length=200, default="UNKNOWN", editable=False)
@@ -205,6 +299,13 @@
205 current_job = models.ForeignKey("BuildResult", db_column="current_job_id", editable=False, blank=True, null=True)299 current_job = models.ForeignKey("BuildResult", db_column="current_job_id", editable=False, blank=True, null=True)
206 notes = models.TextField('whiteboard', blank=True, null=True)300 notes = models.TextField('whiteboard', blank=True, null=True)
207301
302 access_relation = 'current_job__project'
303 all_objects = AccessControlledObjectsManager()
304 # Using PublicOnlyObjectsManager here we ensure that any oversights when
305 # updating existing uses of Lexbuilder.objects won't leak anything related
306 # to private projects.
307 objects = PublicOnlyObjectsManager()
308
208 class Meta:309 class Meta:
209 db_table = "lexbuilders"310 db_table = "lexbuilders"
210311
@@ -266,7 +367,7 @@
266 return cursor.fetchone()[0]367 return cursor.fetchone()[0]
267368
268369
269class BuildResult(models.Model):370class BuildResult(models.Model, MaybePrivateMixin):
270 name = models.CharField('build name', max_length=200, blank=True, null=True, editable=False)371 name = models.CharField('build name', max_length=200, blank=True, null=True, editable=False)
271 project = models.ForeignKey(Project, db_column="project_name", editable=False)372 project = models.ForeignKey(Project, db_column="project_name", editable=False)
272 builder = models.ForeignKey(Lexbuilder, blank=True, null=True, editable=False)373 builder = models.ForeignKey(Lexbuilder, blank=True, null=True, editable=False)
@@ -279,6 +380,13 @@
279 dispatched_at = models.DateTimeField('date dispatched', editable=False, null=True, blank=True) 380 dispatched_at = models.DateTimeField('date dispatched', editable=False, null=True, blank=True)
280 notes = models.CharField(max_length=200, null=True, blank=True)381 notes = models.CharField(max_length=200, null=True, blank=True)
281382
383 access_relation = 'project'
384 all_objects = AccessControlledObjectsManager()
385 # Using PublicOnlyObjectsManager here we ensure that any oversights when
386 # updating existing uses of BuildResult.objects won't leak anything
387 # related to private projects.
388 objects = PublicOnlyObjectsManager()
389
282 class Meta:390 class Meta:
283 db_table = "buildresults"391 db_table = "buildresults"
284 get_latest_by = "started_at"392 get_latest_by = "started_at"
@@ -326,13 +434,20 @@
326 recentBuildFailures = staticmethod(recentBuildFailures)434 recentBuildFailures = staticmethod(recentBuildFailures)
327435
328436
329class BuildRequest(models.Model):437class BuildRequest(models.Model, MaybePrivateMixin):
330 project = models.ForeignKey(Project, db_column="project_name")438 project = models.ForeignKey(Project, db_column="project_name")
331 requestor = models.ForeignKey(User, db_column="requestor_id", blank=True, null=True)439 requestor = models.ForeignKey(User, db_column="requestor_id", blank=True, null=True)
332 created_at = models.DateTimeField('date created', auto_now_add=True)440 created_at = models.DateTimeField('date created', auto_now_add=True)
333 score = models.IntegerField(default=10)441 score = models.IntegerField(default=10)
334 reason = models.TextField('request reason', blank=True, max_length=200)442 reason = models.TextField('request reason', blank=True, max_length=200)
335443
444 access_relation = 'project'
445 all_objects = AccessControlledObjectsManager()
446 # Using PublicOnlyObjectsManager here we ensure that any oversights when
447 # updating existing uses of BuildRequest.objects won't leak anything
448 # related to private projects.
449 objects = PublicOnlyObjectsManager()
450
336 class Meta:451 class Meta:
337 db_table = "buildrequests"452 db_table = "buildrequests"
338453
@@ -451,7 +566,7 @@
451 return "%s subscribed to %s" % (self.user, self.project)566 return "%s subscribed to %s" % (self.user, self.project)
452567
453568
454class Release(models.Model):569class Release(models.Model, MaybePrivateMixin):
455 STATUS_PENDING, STATUS_PUBLISHED, STATUS_REMOVED, STATUS_MISSING, STATUS_ERROR = range(5)570 STATUS_PENDING, STATUS_PUBLISHED, STATUS_REMOVED, STATUS_MISSING, STATUS_ERROR = range(5)
456 STATUS_CHOICES = (571 STATUS_CHOICES = (
457 (STATUS_PENDING, 'Pending Publication'),572 (STATUS_PENDING, 'Pending Publication'),
@@ -469,6 +584,13 @@
469 ("GM", "Gold Master"),584 ("GM", "Gold Master"),
470 )585 )
471586
587 access_relation = 'build__project'
588 all_objects = AccessControlledObjectsManager()
589 # Using PublicOnlyObjectsManager here we ensure that any oversights when
590 # updating existing uses of Release.objects won't leak anything related to
591 # private projects.
592 objects = PublicOnlyObjectsManager()
593
472 build = models.OneToOneField(BuildResult, primary_key=True, unique=True)594 build = models.OneToOneField(BuildResult, primary_key=True, unique=True)
473 name = models.CharField('release name', max_length=200)595 name = models.CharField('release name', max_length=200)
474 milestone = models.ForeignKey(LaunchpadProjectMilestone, null=True, blank=True)596 milestone = models.ForeignKey(LaunchpadProjectMilestone, null=True, blank=True)
475597
=== added directory 'lib/offspring/web/queuemanager/sql'
=== added file 'lib/offspring/web/queuemanager/sql/project.postgresql_psycopg2.sql'
--- lib/offspring/web/queuemanager/sql/project.postgresql_psycopg2.sql 1970-01-01 00:00:00 +0000
+++ lib/offspring/web/queuemanager/sql/project.postgresql_psycopg2.sql 2011-09-29 18:17:25 +0000
@@ -0,0 +1,5 @@
1-- XXX: This doesn't apply when running tests because sqlite3 doesn't support
2-- addition of new constraints to existing tables. Need to decide whether
3-- it's preferred to just ignore this in our tests or use postgresql to run
4-- the tests as well.
5ALTER TABLE projects ADD CONSTRAINT "private_project_requires_owner" CHECK (is_private IS FALSE OR owner_id IS NOT NULL);
06
=== modified file 'lib/offspring/web/queuemanager/tests/__init__.py'
--- lib/offspring/web/queuemanager/tests/__init__.py 2011-09-23 13:31:21 +0000
+++ lib/offspring/web/queuemanager/tests/__init__.py 2011-09-29 18:17:25 +0000
@@ -0,0 +1,1 @@
1from .test_models import *
02
=== modified file 'lib/offspring/web/queuemanager/tests/factory.py'
--- lib/offspring/web/queuemanager/tests/factory.py 2011-09-27 19:58:38 +0000
+++ lib/offspring/web/queuemanager/tests/factory.py 2011-09-29 18:17:25 +0000
@@ -1,6 +1,7 @@
1from itertools import count1from itertools import count
22
3from django.contrib.auth.models import User3from django.contrib.auth.models import User
4from django_group_access.models import AccessGroup
45
5from offspring.web.queuemanager.models import (6from offspring.web.queuemanager.models import (
6 BuildRequest,7 BuildRequest,
@@ -41,12 +42,20 @@
41 user.save()42 user.save()
42 return user43 return user
4344
44 def makeProject(self, name=None, title=None):45 def makeProject(self, name=None, title=None, is_private=False,
46 project_group=None, owner=None, access_groups=[]):
45 if name is None:47 if name is None:
46 name = self.getUniqueString()48 name = self.getUniqueString()
47 if title is None:49 if title is None:
48 title = self.getUniqueString()50 title = self.getUniqueString()
49 project = Project(name=name, title=title)51 if owner is None:
52 owner = self.makeUser()
53 project = Project(
54 name=name, title=title, _is_private=is_private, owner=owner,
55 project_group=project_group)
56 project.save()
57 for group in access_groups:
58 project.access_groups.add(group)
50 project.save()59 project.save()
51 return project60 return project
5261
@@ -97,5 +106,14 @@
97 request.save()106 request.save()
98 return request107 return request
99108
109 def makeAccessGroup(self, users):
110 name = self.getUniqueString()
111 group = AccessGroup(name=name)
112 group.save()
113 for user in users:
114 group.members.add(user)
115 group.save()
116 return group
117
100118
101factory = ObjectFactory()119factory = ObjectFactory()
102120
=== added file 'lib/offspring/web/queuemanager/tests/test_models.py'
--- lib/offspring/web/queuemanager/tests/test_models.py 1970-01-01 00:00:00 +0000
+++ lib/offspring/web/queuemanager/tests/test_models.py 2011-09-29 18:17:25 +0000
@@ -0,0 +1,228 @@
1from operator import attrgetter
2
3from django.contrib.auth.models import AnonymousUser
4from django.test import TestCase
5
6from offspring.web.queuemanager.models import (
7 BuildRequest,
8 BuildResult,
9 Lexbuilder,
10 Project,
11 Release)
12from offspring.web.queuemanager.tests.factory import factory
13
14
15class ProjectTests(TestCase):
16
17 def test_default_manager(self):
18 # Make sure .all_objects is the default manager or else the admin UI
19 # will exclude all private objects and we would break model validation
20 # of unique fields if the user reuses a value from an object they're
21 # not allowed to see.
22 self.assertEquals(Project.all_objects, Project._default_manager)
23
24 def test_is_private_for_public_project(self):
25 project = factory.makeProject(is_private=False)
26 self.assertEqual(False, project.is_private)
27
28 def test_is_private_for_private_project(self):
29 private_project = factory.makeProject(is_private=True)
30 self.assertEqual(True, private_project.is_private)
31
32 def test_is_visible_to_anyone_if_public(self):
33 project = factory.makeProject(is_private=False)
34 self.assertTrue(project.is_visible_to(AnonymousUser()))
35
36 def test_is_not_visible_to_anyone_if_private(self):
37 project = factory.makeProject(is_private=True)
38 self.assertFalse(project.is_visible_to(AnonymousUser()))
39
40 def test_is_visible_to_owner_if_private(self):
41 project = factory.makeProject(is_private=True)
42 self.assertTrue(project.is_visible_to(project.owner))
43
44 def test_is_visible_to_member_of_access_group_if_private(self):
45 user = factory.makeUser()
46 group = factory.makeAccessGroup([user])
47 project = factory.makeProject(is_private=True, access_groups=[group])
48 self.assertTrue(project.is_visible_to(user))
49
50 def test_default_model_manager_only_returns_public_projects(self):
51 project = factory.makeProject(is_private=False)
52 project2 = factory.makeProject(is_private=True)
53 self.assertEqual([project], list(Project.objects.all()))
54
55 def test_all_objects_model_manager(self):
56 # Project.all_objects() is a separate model manager which returns
57 # private objects as well. Its .all() method will return public and
58 # private objects regardless of whether or not the current user has
59 # access to them. It is to be used with caution.
60 project = factory.makeProject(is_private=False)
61 project2 = factory.makeProject(is_private=True)
62 self.assertEqual([project, project2], list(Project.all_objects.all()))
63
64 def test_accessible_by_user_with_anonymous_user(self):
65 project = factory.makeProject(is_private=True)
66 project2 = factory.makeProject(is_private=False)
67 self.assertEqual(True, project.is_private)
68 self.assertEqual(False, project2.is_private)
69 objects = Project.all_objects.accessible_by_user(AnonymousUser())
70 self.assertEqual([project2], list(objects))
71
72 def test_accessible_by_user_with_public_project(self):
73 user = factory.makeUser()
74 project = factory.makeProject(is_private=False)
75 self.assertEqual(False, project.is_private)
76 objects = Project.all_objects.accessible_by_user(user)
77 self.assertEqual([project], list(objects))
78
79 def test_accessible_by_user_with_private_project(self):
80 user = factory.makeUser()
81 group = factory.makeAccessGroup([user])
82 project = factory.makeProject(is_private=True, access_groups=[group])
83 # This second project is private but has no access groups set up, so
84 # the user cannot see it.
85 project2 = factory.makeProject(is_private=True)
86 self.assertEqual(True, project.is_private)
87 self.assertEqual(True, project2.is_private)
88 objects = Project.all_objects.accessible_by_user(user)
89 self.assertEqual([project], list(objects))
90
91
92class ReleaseOrLexbuilderOrBuildResultOrBuildRequestTestsMixin(object):
93 """A mixin with tests that work for the following models:
94
95 Release
96 Lexbuilder
97 BuildResult
98 BuildRequest
99
100 You just need to mix this with TestCase in your subclass and define
101 factoryMethod and model.
102 """
103 model = None
104
105 def factoryMethod(self, project):
106 raise NotImplementedError()
107
108 def test_default_manager(self):
109 # Make sure .all_objects is the default manager or else the admin UI
110 # will exclude all private objects and we would break model validation
111 # of unique fields if the user reuses a value from an object they're
112 # not allowed to see.
113 self.assertEquals(self.model.all_objects, self.model._default_manager)
114
115 def test_is_private_for_public_object(self):
116 public_object = self.factoryMethod(
117 factory.makeProject(is_private=False))
118 self.assertFalse(public_object.is_private)
119
120 def test_is_private_for_private_object(self):
121 private_object = self.factoryMethod(
122 factory.makeProject(is_private=True))
123 self.assertTrue(private_object.is_private)
124
125 def test_is_visible_to_anyone_if_public(self):
126 public_object = self.factoryMethod(
127 factory.makeProject(is_private=False))
128 self.assertTrue(public_object.is_visible_to(AnonymousUser()))
129
130 def test_is_not_visible_to_anyone_if_private(self):
131 private_object = self.factoryMethod(
132 factory.makeProject(is_private=True))
133 self.assertFalse(private_object.is_visible_to(AnonymousUser()))
134
135 def test_is_visible_to_owner_if_private(self):
136 # The object we're dealing with inherits the access-control rules from
137 # the project it's related to, so the owner that matters is the
138 # project owner.
139 owner = factory.makeUser()
140 private_object = self.factoryMethod(
141 factory.makeProject(is_private=True, owner=owner))
142 self.assertTrue(private_object.is_visible_to(owner))
143
144 def test_is_visible_to_member_of_access_group_if_private(self):
145 user = factory.makeUser()
146 group = factory.makeAccessGroup([user])
147 private_object = self.factoryMethod(
148 factory.makeProject(is_private=True, access_groups=[group]))
149 self.assertTrue(private_object.is_visible_to(user))
150
151 def test_default_model_manager_only_returns_public_objects(self):
152 private_object = self.factoryMethod(
153 factory.makeProject(is_private=True))
154 public_object = self.factoryMethod(
155 factory.makeProject(is_private=False))
156 self.assertEqual([public_object], list(self.model.objects.all()))
157
158 def test_all_objects_model_manager(self):
159 # self.model.all_objects() is a separate model manager which returns
160 # private objects as well. Its .all() method will return public and
161 # private objects regardless of whether or not the current user has
162 # access to them. It is to be used with caution.
163 public_object = self.factoryMethod(
164 project=factory.makeProject(is_private=False))
165 private_object = self.factoryMethod(
166 project=factory.makeProject(is_private=True))
167 self.assertEqual([public_object, private_object],
168 list(self.model.all_objects.all()))
169
170 def test_accessible_by_user(self):
171 user = factory.makeUser()
172 group = factory.makeAccessGroup([user])
173 visible_object = self.factoryMethod(
174 factory.makeProject(is_private=True, access_groups=[group]))
175 # This object is linked to a private project which has no access
176 # groups set up, so the user cannot see it.
177 invisible_object = self.factoryMethod(
178 project=factory.makeProject(is_private=True))
179 self.assertEqual(
180 [visible_object],
181 list(self.model.all_objects.accessible_by_user(user)))
182
183
184class ReleaseTests(
185 TestCase, ReleaseOrLexbuilderOrBuildResultOrBuildRequestTestsMixin):
186 model = Release
187
188 def factoryMethod(self, project):
189 return factory.makeRelease(
190 build=factory.makeBuildResult(project=project))
191
192
193class LexbuilderTests(
194 TestCase, ReleaseOrLexbuilderOrBuildResultOrBuildRequestTestsMixin):
195 model = Lexbuilder
196
197 def factoryMethod(self, project):
198 return factory.makeLexbuilder(
199 current_job=factory.makeBuildResult(project=project))
200
201
202class BuildResultTests(
203 TestCase, ReleaseOrLexbuilderOrBuildResultOrBuildRequestTestsMixin):
204 model = BuildResult
205 factoryMethod = factory.makeBuildResult
206
207
208class BuildRequestTests(
209 TestCase, ReleaseOrLexbuilderOrBuildResultOrBuildRequestTestsMixin):
210 model = BuildRequest
211 factoryMethod = factory.makeBuildRequest
212
213
214class ProjectGroupTests(TestCase):
215
216 def test_get_projects_filters_private_objects(self):
217 user = factory.makeUser()
218 group = factory.makeProjectGroup()
219 public_project = factory.makeProject(
220 is_private=False, project_group=group)
221 private_project_visible_to_user = factory.makeProject(
222 is_private=True, project_group=group, owner=user)
223 private_project = factory.makeProject(
224 is_private=True, project_group=group)
225 self.assertEqual(
226 sorted([public_project, private_project_visible_to_user],
227 key=attrgetter('name')),
228 sorted(group.get_projects(user), key=attrgetter('name')))
0229
=== modified file 'lib/offspring/web/queuemanager/views.py'
--- lib/offspring/web/queuemanager/views.py 2011-08-13 06:28:28 +0000
+++ lib/offspring/web/queuemanager/views.py 2011-09-29 18:17:25 +0000
@@ -241,6 +241,7 @@
241241
242 pageData = { 242 pageData = {
243 'projectGroup' : pg, 243 'projectGroup' : pg,
244 'projects': pg.get_projects(request.user),
244 'pillar' : 'projects', 245 'pillar' : 'projects',
245 'build_stats' : build_stats,246 'build_stats' : build_stats,
246 'projectgroup_build_results' : projectgroup_build_results,247 'projectgroup_build_results' : projectgroup_build_results,
247248
=== modified file 'lib/offspring/web/settings.py'
--- lib/offspring/web/settings.py 2011-09-23 14:01:26 +0000
+++ lib/offspring/web/settings.py 2011-09-29 18:17:25 +0000
@@ -96,6 +96,7 @@
96 'djcelery',96 'djcelery',
97 'djkombu',97 'djkombu',
98 'piston',98 'piston',
99 'django_group_access',
99 # Offspring Apps100 # Offspring Apps
100 'offspring.web.queuemanager',101 'offspring.web.queuemanager',
101)102)
102103
=== modified file 'lib/offspring/web/templates/queuemanager/projectgroup_details.html'
--- lib/offspring/web/templates/queuemanager/projectgroup_details.html 2010-11-29 08:27:24 +0000
+++ lib/offspring/web/templates/queuemanager/projectgroup_details.html 2011-09-29 18:17:25 +0000
@@ -32,7 +32,7 @@
32 <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>32 <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>
33 </td>33 </td>
34 </tr>34 </tr>
35 {% for project in projectGroup.projects %}35 {% for project in projects %}
36 <tr>36 <tr>
37 <td>37 <td>
38 {% 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>38 {% 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>
3939
=== added directory 'migration'
=== added file 'migration/001-project-privacy.sql'
--- migration/001-project-privacy.sql 1970-01-01 00:00:00 +0000
+++ migration/001-project-privacy.sql 2011-09-29 18:17:25 +0000
@@ -0,0 +1,3 @@
1ALTER TABLE projects ADD COLUMN "is_private" boolean NOT NULL default FALSE;
2ALTER TABLE projects ADD COLUMN "owner_id" integer REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED;
3ALTER TABLE projects ADD CONSTRAINT "private_project_requires_owner" CHECK (is_private IS FALSE OR owner_id IS NOT NULL);
04
=== modified file 'requirements/requirements.web.txt'
--- requirements/requirements.web.txt 2011-08-11 00:16:09 +0000
+++ requirements/requirements.web.txt 2011-09-29 18:17:25 +0000
@@ -11,3 +11,4 @@
11celery>=2.3.111celery>=2.3.1
12django-celery>=2.3.012django-celery>=2.3.0
13django-kombu>=0.9.413django-kombu>=0.9.4
14django-group-access>=0.0.1

Subscribers

People subscribed via source and target branches