Merge lp:~cjwatson/launchpad/git-basic-model into lp:launchpad

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: no longer in the source branch.
Merged at revision: 17350
Proposed branch: lp:~cjwatson/launchpad/git-basic-model
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/git-personmerge-whitelist
Diff against target: 1363 lines (+1062/-29)
19 files modified
configs/development/launchpad-lazr.conf (+4/-0)
lib/lp/code/configure.zcml (+31/-0)
lib/lp/code/errors.py (+31/-0)
lib/lp/code/interfaces/gitrepository.py (+372/-0)
lib/lp/code/interfaces/hasgitrepositories.py (+40/-0)
lib/lp/code/model/branch.py (+1/-2)
lib/lp/code/model/gitrepository.py (+390/-0)
lib/lp/code/model/hasgitrepositories.py (+28/-0)
lib/lp/code/model/tests/test_hasgitrepositories.py (+34/-0)
lib/lp/registry/configure.zcml (+5/-0)
lib/lp/registry/interfaces/distributionsourcepackage.py (+3/-1)
lib/lp/registry/interfaces/person.py (+2/-1)
lib/lp/registry/interfaces/product.py (+2/-1)
lib/lp/registry/model/distributionsourcepackage.py (+2/-1)
lib/lp/registry/model/person.py (+2/-1)
lib/lp/registry/model/product.py (+2/-1)
lib/lp/registry/tests/test_product.py (+6/-5)
lib/lp/security.py (+86/-16)
lib/lp/services/config/schema-lazr.conf (+21/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/git-basic-model
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+248976@code.launchpad.net

This proposal supersedes a proposal from 2015-02-06.

Commit message

Very basic preliminary model for GitRepository. Make Product, DistributionSourcePackage, and Person implement IHasGitRepositories.

Description of the change

Very basic preliminary model for GitRepository. Make Product, DistributionSourcePackage, and Person implement IHasGitRepositories.

This doesn't really do anything useful by itself. In particular, in the cause of getting this chunk of changes down to a reasonable size, I left out the GitNamespace stuff that makes it possible to actually create GitRepository objects (the general architecture is fairly similar to Branch*), which also means that most of the tests are deferred to a later branch when we have enough infrastructure to support creating test objects.

This branch implements one of the candidate proposals for Git URL layouts in Launchpad. I'm not assuming that this will be the final layout; it's relatively easy to change later.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Needs Fixing (code)
Revision history for this message
William Grant (wgrant) :
Revision history for this message
William Grant (wgrant) :
Revision history for this message
Colin Watson (cjwatson) :
Revision history for this message
William Grant (wgrant) :
review: Needs Fixing (code)
Revision history for this message
Colin Watson (cjwatson) :
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'configs/development/launchpad-lazr.conf'
--- configs/development/launchpad-lazr.conf 2014-02-27 08:39:44 +0000
+++ configs/development/launchpad-lazr.conf 2015-02-19 18:43:50 +0000
@@ -48,6 +48,10 @@
48access_log: /var/tmp/bazaar.launchpad.dev/codehosting-access.log48access_log: /var/tmp/bazaar.launchpad.dev/codehosting-access.log
49blacklisted_hostnames:49blacklisted_hostnames:
50use_forking_daemon: True50use_forking_daemon: True
51internal_git_api_endpoint: http://git.launchpad.dev:19417/
52git_browse_root: https://git.launchpad.dev/
53git_anon_root: git://git.launchpad.dev/
54git_ssh_root: git+ssh://git.launchpad.dev/
5155
52[codeimport]56[codeimport]
53bazaar_branch_store: file:///tmp/bazaar-branches57bazaar_branch_store: file:///tmp/bazaar-branches
5458
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml 2015-02-09 11:38:30 +0000
+++ lib/lp/code/configure.zcml 2015-02-19 18:43:50 +0000
@@ -807,6 +807,37 @@
807 <adapter factory="lp.code.model.linkedbranch.PackageLinkedBranch" />807 <adapter factory="lp.code.model.linkedbranch.PackageLinkedBranch" />
808 <adapter factory="lp.code.model.linkedbranch.DistributionPackageLinkedBranch" />808 <adapter factory="lp.code.model.linkedbranch.DistributionPackageLinkedBranch" />
809809
810 <!-- GitRepository -->
811
812 <class class="lp.code.model.gitrepository.GitRepository">
813 <require
814 permission="launchpad.View"
815 interface="lp.app.interfaces.launchpad.IPrivacy
816 lp.code.interfaces.gitrepository.IGitRepositoryView
817 lp.code.interfaces.gitrepository.IGitRepositoryModerateAttributes" />
818 <require
819 permission="launchpad.Moderate"
820 interface="lp.code.interfaces.gitrepository.IGitRepositoryModerate"
821 set_schema="lp.code.interfaces.gitrepository.IGitRepositoryModerateAttributes" />
822 <require
823 permission="launchpad.Edit"
824 interface="lp.code.interfaces.gitrepository.IGitRepositoryEdit" />
825 </class>
826 <subscriber
827 for="lp.code.interfaces.gitrepository.IGitRepository zope.lifecycleevent.interfaces.IObjectModifiedEvent"
828 handler="lp.code.model.gitrepository.git_repository_modified"/>
829
830 <!-- GitRepositorySet -->
831
832 <class class="lp.code.model.gitrepository.GitRepositorySet">
833 <allow interface="lp.code.interfaces.gitrepository.IGitRepositorySet" />
834 </class>
835 <securedutility
836 class="lp.code.model.gitrepository.GitRepositorySet"
837 provides="lp.code.interfaces.gitrepository.IGitRepositorySet">
838 <allow interface="lp.code.interfaces.gitrepository.IGitRepositorySet" />
839 </securedutility>
840
810 <lp:help-folder folder="help" name="+help-code" />841 <lp:help-folder folder="help" name="+help-code" />
811842
812 <!-- Diffs -->843 <!-- Diffs -->
813844
=== modified file 'lib/lp/code/errors.py'
--- lib/lp/code/errors.py 2013-12-20 05:38:18 +0000
+++ lib/lp/code/errors.py 2015-02-19 18:43:50 +0000
@@ -28,6 +28,8 @@
28 'CodeImportNotInReviewedState',28 'CodeImportNotInReviewedState',
29 'ClaimReviewFailed',29 'ClaimReviewFailed',
30 'DiffNotFound',30 'DiffNotFound',
31 'GitDefaultConflict',
32 'GitTargetError',
31 'InvalidBranchMergeProposal',33 'InvalidBranchMergeProposal',
32 'InvalidMergeQueueConfig',34 'InvalidMergeQueueConfig',
33 'InvalidNamespace',35 'InvalidNamespace',
@@ -312,6 +314,35 @@
312 """Raised when the user specifies an unrecognized branch type."""314 """Raised when the user specifies an unrecognized branch type."""
313315
314316
317class GitTargetError(Exception):
318 """Raised when there is an error determining a Git repository target."""
319
320
321@error_status(httplib.CONFLICT)
322class GitDefaultConflict(Exception):
323 """Raised when trying to set a Git repository as the default for
324 something that already has a default."""
325
326 def __init__(self, existing_repository, target, owner=None):
327 params = {
328 "unique_name": existing_repository.unique_name,
329 "target": target.displayname,
330 "owner": owner.displayname,
331 }
332 if owner is None:
333 message = (
334 "The default repository for '%(target)s' is already set to "
335 "%(unique_name)s." % params)
336 else:
337 message = (
338 "%(owner)'s default repository for '%(target)s' is already "
339 "set to %(unique_name)s." % params)
340 self.existing_repository = existing_repository
341 self.target = target
342 self.owner = owner
343 Exception.__init__(self, message)
344
345
315@error_status(httplib.BAD_REQUEST)346@error_status(httplib.BAD_REQUEST)
316class CodeImportNotInReviewedState(Exception):347class CodeImportNotInReviewedState(Exception):
317 """Raised when the user requests an import of a non-automatic import."""348 """Raised when the user requests an import of a non-automatic import."""
318349
=== added file 'lib/lp/code/interfaces/gitrepository.py'
--- lib/lp/code/interfaces/gitrepository.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/interfaces/gitrepository.py 2015-02-19 18:43:50 +0000
@@ -0,0 +1,372 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Git repository interfaces."""
5
6__metaclass__ = type
7
8__all__ = [
9 'GitIdentityMixin',
10 'git_repository_name_validator',
11 'IGitRepository',
12 'IGitRepositorySet',
13 'user_has_special_git_repository_access',
14 ]
15
16import re
17
18from lazr.restful.fields import Reference
19from zope.interface import (
20 Attribute,
21 Interface,
22 )
23from zope.schema import (
24 Bool,
25 Choice,
26 Datetime,
27 Int,
28 Text,
29 TextLine,
30 )
31
32from lp import _
33from lp.app.enums import InformationType
34from lp.app.validators import LaunchpadValidationError
35from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
36from lp.registry.interfaces.role import IPersonRoles
37from lp.services.fields import (
38 PersonChoice,
39 PublicPersonChoice,
40 )
41
42
43GIT_REPOSITORY_NAME_VALIDATION_ERROR_MESSAGE = _(
44 "Git repository names must start with a number or letter. The characters "
45 "+, -, _, . and @ are also allowed after the first character. Repository "
46 "names must not end with \".git\".")
47
48
49# This is a copy of the pattern in database/schema/patch-2209-61-0.sql.
50# Don't change it without changing that.
51valid_git_repository_name_pattern = re.compile(
52 r"^(?i)[a-z0-9][a-z0-9+\.\-@_]*\Z")
53
54
55def valid_git_repository_name(name):
56 """Return True iff the name is valid as a Git repository name.
57
58 The rules for what is a valid Git repository name are described in
59 GIT_REPOSITORY_NAME_VALIDATION_ERROR_MESSAGE.
60 """
61 if (not name.endswith(".git") and
62 valid_git_repository_name_pattern.match(name)):
63 return True
64 return False
65
66
67def git_repository_name_validator(name):
68 """Return True if the name is valid, or raise a LaunchpadValidationError.
69 """
70 if not valid_git_repository_name(name):
71 raise LaunchpadValidationError(
72 _("Invalid Git repository name '${name}'. ${message}",
73 mapping={
74 "name": name,
75 "message": GIT_REPOSITORY_NAME_VALIDATION_ERROR_MESSAGE,
76 }))
77 return True
78
79
80class IGitRepositoryView(Interface):
81 """IGitRepository attributes that require launchpad.View permission."""
82
83 id = Int(title=_("ID"), readonly=True, required=True)
84
85 date_created = Datetime(
86 title=_("Date created"), required=True, readonly=True)
87
88 date_last_modified = Datetime(
89 title=_("Date last modified"), required=True, readonly=True)
90
91 registrant = PublicPersonChoice(
92 title=_("Registrant"), required=True, readonly=True,
93 vocabulary="ValidPersonOrTeam",
94 description=_("The person who registered this Git repository."))
95
96 owner = PersonChoice(
97 title=_("Owner"), required=True, readonly=False,
98 vocabulary="AllUserTeamsParticipationPlusSelf",
99 description=_(
100 "The owner of this Git repository. This controls who can modify "
101 "the repository."))
102
103 target = Reference(
104 title=_("Target"), required=True, readonly=True,
105 schema=IHasGitRepositories,
106 description=_("The target of the repository."))
107
108 information_type = Choice(
109 title=_("Information type"), vocabulary=InformationType,
110 required=True, readonly=True, default=InformationType.PUBLIC,
111 description=_(
112 "The type of information contained in this repository."))
113
114 owner_default = Bool(
115 title=_("Owner default"), required=True, readonly=True,
116 description=_(
117 "Whether this repository is the default for its owner and "
118 "target."))
119
120 target_default = Bool(
121 title=_("Target default"), required=True, readonly=True,
122 description=_(
123 "Whether this repository is the default for its target."))
124
125 unique_name = Text(
126 title=_("Unique name"), readonly=True,
127 description=_(
128 "Unique name of the repository, including the owner and project "
129 "names."))
130
131 display_name = Text(
132 title=_("Display name"), readonly=True,
133 description=_("Display name of the repository."))
134
135 shortened_path = Attribute(
136 "The shortest reasonable version of the path to this repository.")
137
138 git_identity = Text(
139 title=_("Git identity"), readonly=True,
140 description=_(
141 "If this is the default repository for some target, then this is "
142 "'lp:' plus a shortcut version of the path via that target. "
143 "Otherwise it is simply 'lp:' plus the unique name."))
144
145 def setOwnerDefault(value):
146 """Set whether this repository is the default for its owner-target.
147
148 This is for internal use; the caller should ensure permission to
149 edit the owner, should arrange to remove any existing owner-target
150 default, and should check that this repository is attached to the
151 desired target.
152
153 :param value: True if this repository should be the owner-target
154 default, otherwise False.
155 """
156
157 def setTargetDefault(value):
158 """Set whether this repository is the default for its target.
159
160 This is for internal use; the caller should ensure permission to
161 edit the target, should arrange to remove any existing target
162 default, and should check that this repository is attached to the
163 desired target.
164
165 :param value: True if this repository should be the target default,
166 otherwise False.
167 """
168
169 def getCodebrowseUrl():
170 """Construct a browsing URL for this Git repository."""
171
172 def visibleByUser(user):
173 """Can the specified user see this repository?"""
174
175 def getAllowedInformationTypes(user):
176 """Get a list of acceptable `InformationType`s for this repository.
177
178 If the user is a Launchpad admin, any type is acceptable.
179 """
180
181 def getInternalPath():
182 """Get the internal path to this repository.
183
184 This is used on the storage backend.
185 """
186
187 def getRepositoryDefaults():
188 """Return a sorted list of `ICanHasDefaultGitRepository` objects.
189
190 There is one result for each related object for which this
191 repository is the default. For example, in the case where a
192 repository is the default for a project and is also its owner's
193 default repository for that project, the objects for both the
194 project and the person-project are returned.
195
196 More important related objects are sorted first.
197 """
198
199 # Marker for references to Git URL layouts: ##GITNAMESPACE##
200 def getRepositoryIdentities():
201 """A list of aliases for a repository.
202
203 Returns a list of tuples of path and context object. There is at
204 least one alias for any repository, and that is the repository
205 itself. For default repositories, the context object is the
206 appropriate default object.
207
208 Where a repository is the default for a product or a distribution
209 source package, the repository is available through a number of
210 different URLs. These URLs are the aliases for the repository.
211
212 For example, a repository which is the default for the 'fooix'
213 project and which is also its owner's default repository for that
214 project is accessible using:
215 fooix - the context object is the project fooix
216 ~fooix-owner/fooix - the context object is the person-project
217 ~fooix-owner and fooix
218 ~fooix-owner/fooix/+git/fooix - the unique name of the repository
219 where the context object is the repository itself.
220 """
221
222
223class IGitRepositoryModerateAttributes(Interface):
224 """IGitRepository attributes that can be edited by more than one community.
225 """
226
227 # XXX cjwatson 2015-01-29: Add some advice about default repository
228 # naming.
229 name = TextLine(
230 title=_("Name"), required=True,
231 constraint=git_repository_name_validator,
232 description=_(
233 "The repository name. Keep very short, unique, and descriptive, "
234 "because it will be used in URLs."))
235
236
237class IGitRepositoryModerate(Interface):
238 """IGitRepository methods that can be called by more than one community."""
239
240 def transitionToInformationType(information_type, user,
241 verify_policy=True):
242 """Set the information type for this repository.
243
244 :param information_type: The `InformationType` to transition to.
245 :param user: The `IPerson` who is making the change.
246 :param verify_policy: Check if the new information type complies
247 with the `IGitNamespacePolicy`.
248 """
249
250
251class IGitRepositoryEdit(Interface):
252 """IGitRepository methods that require launchpad.Edit permission."""
253
254 def setOwner(new_owner, user):
255 """Set the owner of the repository to be `new_owner`."""
256
257 def setTarget(target, user):
258 """Set the target of the repository."""
259
260 def destroySelf():
261 """Delete the specified repository."""
262
263
264class IGitRepository(IGitRepositoryView, IGitRepositoryModerateAttributes,
265 IGitRepositoryModerate, IGitRepositoryEdit):
266 """A Git repository."""
267
268 private = Bool(
269 title=_("Private"), required=False, readonly=True,
270 description=_("This repository is visible only to its subscribers."))
271
272
273class IGitRepositorySet(Interface):
274 """Interface representing the set of Git repositories."""
275
276 def new(registrant, owner, target, name, information_type=None,
277 date_created=None):
278 """Create a Git repository and return it.
279
280 :param registrant: The `IPerson` who registered the new repository.
281 :param owner: The `IPerson` who owns the new repository.
282 :param target: The `IProduct`, `IDistributionSourcePackage`, or
283 `IPerson` that the new repository is associated with.
284 :param name: The repository name.
285 :param information_type: Set the repository's information type to
286 one different from the target's default. The type must conform
287 to the target's code sharing policy. (optional)
288 """
289
290 # Marker for references to Git URL layouts: ##GITNAMESPACE##
291 def getByPath(user, path):
292 """Find a repository by its path.
293
294 Any of these forms may be used, with or without a leading slash:
295 Unique names:
296 ~OWNER/PROJECT/+git/NAME
297 ~OWNER/DISTRO/+source/SOURCE/+git/NAME
298 ~OWNER/+git/NAME
299 Owner-target default aliases:
300 ~OWNER/PROJECT
301 ~OWNER/DISTRO/+source/SOURCE
302 Official aliases:
303 PROJECT
304 DISTRO/+source/SOURCE
305
306 Return None if no match was found.
307 """
308
309 def getDefaultRepository(target, owner=None):
310 """Get the default repository for a target or owner-target.
311
312 :param target: An `IHasGitRepositories`.
313 :param owner: An `IPerson`, in which case search for that person's
314 default repository for this target; or None, in which case
315 search for the overall default repository for this target.
316
317 :raises GitTargetError: if `target` is an `IPerson`.
318 :return: An `IGitRepository`, or None.
319 """
320
321 def getRepositories():
322 """Return an empty collection of repositories.
323
324 This only exists to keep lazr.restful happy.
325 """
326
327
328class GitIdentityMixin:
329 """This mixin class determines Git repository paths.
330
331 Used by both the model GitRepository class and the browser repository
332 listing item. This allows the browser code to cache the associated
333 context objects which reduces query counts.
334 """
335
336 @property
337 def shortened_path(self):
338 """See `IGitRepository`."""
339 path, context = self.getRepositoryIdentities()[0]
340 return path
341
342 @property
343 def git_identity(self):
344 """See `IGitRepository`."""
345 return "lp:" + self.shortened_path
346
347 def getRepositoryDefaults(self):
348 """See `IGitRepository`."""
349 # XXX cjwatson 2015-02-06: This will return shortcut defaults once
350 # they're implemented.
351 return []
352
353 def getRepositoryIdentities(self):
354 """See `IGitRepository`."""
355 identities = [
356 (default.path, default.context)
357 for default in self.getRepositoryDefaults()]
358 identities.append((self.unique_name, self))
359 return identities
360
361
362def user_has_special_git_repository_access(user):
363 """Admins have special access.
364
365 :param user: An `IPerson` or None.
366 """
367 if user is None:
368 return False
369 roles = IPersonRoles(user)
370 if roles.in_admin:
371 return True
372 return False
0373
=== added file 'lib/lp/code/interfaces/hasgitrepositories.py'
--- lib/lp/code/interfaces/hasgitrepositories.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/interfaces/hasgitrepositories.py 2015-02-19 18:43:50 +0000
@@ -0,0 +1,40 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Interfaces relating to targets of Git repositories."""
5
6__metaclass__ = type
7
8__all__ = [
9 'IHasGitRepositories',
10 ]
11
12from zope.interface import Interface
13
14
15class IHasGitRepositories(Interface):
16 """An object that has related Git repositories.
17
18 A project contains Git repositories, a source package on a distribution
19 contains branches, and a person contains "personal" branches.
20 """
21
22 def getGitRepositories(visible_by_user=None, eager_load=False):
23 """Returns all Git repositories related to this object.
24
25 :param visible_by_user: Normally the user who is asking.
26 :param eager_load: If True, load related objects for the whole
27 collection.
28 :returns: A list of `IGitRepository` objects.
29 """
30
31 def createGitRepository(registrant, owner, name, information_type=None):
32 """Create a Git repository for this target and return it.
33
34 :param registrant: The `IPerson` who registered the new repository.
35 :param owner: The `IPerson` who owns the new repository.
36 :param name: The repository name.
37 :param information_type: Set the repository's information type to
38 one different from the target's default. The type must conform
39 to the target's code sharing policy. (optional)
40 """
041
=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py 2014-01-15 00:59:48 +0000
+++ lib/lp/code/model/branch.py 2015-02-19 18:43:50 +0000
@@ -208,7 +208,6 @@
208 mirror_status_message = StringCol(default=None)208 mirror_status_message = StringCol(default=None)
209 information_type = EnumCol(209 information_type = EnumCol(
210 enum=InformationType, default=InformationType.PUBLIC)210 enum=InformationType, default=InformationType.PUBLIC)
211 access_policy = IntCol()
212211
213 @property212 @property
214 def private(self):213 def private(self):
@@ -1661,7 +1660,7 @@
16611660
1662 policy_grant_query = Coalesce(1661 policy_grant_query = Coalesce(
1663 ArrayIntersects(1662 ArrayIntersects(
1664 Array(branch_class.access_policy),1663 Array(SQL('%s.access_policy' % branch_class.__storm_table__)),
1665 Select(1664 Select(
1666 ArrayAgg(AccessPolicyGrant.policy_id),1665 ArrayAgg(AccessPolicyGrant.policy_id),
1667 tables=(AccessPolicyGrant,1666 tables=(AccessPolicyGrant,
16681667
=== added file 'lib/lp/code/model/gitrepository.py'
--- lib/lp/code/model/gitrepository.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/model/gitrepository.py 2015-02-19 18:43:50 +0000
@@ -0,0 +1,390 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4__metaclass__ = type
5__all__ = [
6 'get_git_repository_privacy_filter',
7 'GitRepository',
8 'GitRepositorySet',
9 ]
10
11from bzrlib import urlutils
12import pytz
13from storm.expr import (
14 Coalesce,
15 Join,
16 Or,
17 Select,
18 SQL,
19 )
20from storm.locals import (
21 Bool,
22 DateTime,
23 Int,
24 Reference,
25 Unicode,
26 )
27from zope.component import getUtility
28from zope.interface import implements
29
30from lp.app.enums import (
31 InformationType,
32 PRIVATE_INFORMATION_TYPES,
33 PUBLIC_INFORMATION_TYPES,
34 )
35from lp.app.interfaces.informationtype import IInformationType
36from lp.app.interfaces.launchpad import IPrivacy
37from lp.app.interfaces.services import IService
38from lp.code.errors import (
39 GitDefaultConflict,
40 GitTargetError,
41 )
42from lp.code.interfaces.gitrepository import (
43 GitIdentityMixin,
44 IGitRepository,
45 IGitRepositorySet,
46 user_has_special_git_repository_access,
47 )
48from lp.registry.errors import CannotChangeInformationType
49from lp.registry.interfaces.accesspolicy import (
50 IAccessArtifactSource,
51 IAccessPolicySource,
52 )
53from lp.registry.interfaces.distributionsourcepackage import (
54 IDistributionSourcePackage,
55 )
56from lp.registry.interfaces.product import IProduct
57from lp.registry.interfaces.role import IHasOwner
58from lp.registry.interfaces.sharingjob import (
59 IRemoveArtifactSubscriptionsJobSource,
60 )
61from lp.registry.model.accesspolicy import (
62 AccessPolicyGrant,
63 reconcile_access_for_artifact,
64 )
65from lp.registry.model.teammembership import TeamParticipation
66from lp.services.config import config
67from lp.services.database.constants import (
68 DEFAULT,
69 UTC_NOW,
70 )
71from lp.services.database.enumcol import EnumCol
72from lp.services.database.interfaces import IStore
73from lp.services.database.stormbase import StormBase
74from lp.services.database.stormexpr import (
75 Array,
76 ArrayAgg,
77 ArrayIntersects,
78 )
79from lp.services.propertycache import cachedproperty
80
81
82def git_repository_modified(repository, event):
83 """Update the date_last_modified property when a GitRepository is modified.
84
85 This method is registered as a subscriber to `IObjectModifiedEvent`
86 events on Git repositories.
87 """
88 repository.date_last_modified = UTC_NOW
89
90
91class GitRepository(StormBase, GitIdentityMixin):
92 """See `IGitRepository`."""
93
94 __storm_table__ = 'GitRepository'
95
96 implements(IGitRepository, IHasOwner, IPrivacy, IInformationType)
97
98 id = Int(primary=True)
99
100 date_created = DateTime(
101 name='date_created', tzinfo=pytz.UTC, allow_none=False)
102 date_last_modified = DateTime(
103 name='date_last_modified', tzinfo=pytz.UTC, allow_none=False)
104
105 registrant_id = Int(name='registrant', allow_none=False)
106 registrant = Reference(registrant_id, 'Person.id')
107
108 owner_id = Int(name='owner', allow_none=False)
109 owner = Reference(owner_id, 'Person.id')
110
111 project_id = Int(name='project', allow_none=True)
112 project = Reference(project_id, 'Product.id')
113
114 distribution_id = Int(name='distribution', allow_none=True)
115 distribution = Reference(distribution_id, 'Distribution.id')
116
117 sourcepackagename_id = Int(name='sourcepackagename', allow_none=True)
118 sourcepackagename = Reference(sourcepackagename_id, 'SourcePackageName.id')
119
120 name = Unicode(name='name', allow_none=False)
121
122 information_type = EnumCol(enum=InformationType, notNull=True)
123 owner_default = Bool(name='owner_default', allow_none=False)
124 target_default = Bool(name='target_default', allow_none=False)
125
126 def __init__(self, registrant, owner, target, name, information_type,
127 date_created):
128 super(GitRepository, self).__init__()
129 self.registrant = registrant
130 self.owner = owner
131 self.name = name
132 self.information_type = information_type
133 self.date_created = date_created
134 self.date_last_modified = date_created
135 self.project = None
136 self.distribution = None
137 self.sourcepackagename = None
138 if IProduct.providedBy(target):
139 self.project = target
140 elif IDistributionSourcePackage.providedBy(target):
141 self.distribution = target.distribution
142 self.sourcepackagename = target.sourcepackagename
143 self.owner_default = False
144 self.target_default = False
145
146 # Marker for references to Git URL layouts: ##GITNAMESPACE##
147 @property
148 def unique_name(self):
149 names = {"owner": self.owner.name, "repository": self.name}
150 if self.project is not None:
151 fmt = "~%(owner)s/%(project)s"
152 names["project"] = self.project.name
153 elif self.distribution is not None:
154 fmt = "~%(owner)s/%(distribution)s/+source/%(source)s"
155 names["distribution"] = self.distribution.name
156 names["source"] = self.sourcepackagename.name
157 else:
158 fmt = "~%(owner)s"
159 fmt += "/+git/%(repository)s"
160 return fmt % names
161
162 def __repr__(self):
163 return "<GitRepository %r (%d)>" % (self.unique_name, self.id)
164
165 @cachedproperty
166 def target(self):
167 """See `IGitRepository`."""
168 if self.project is not None:
169 return self.project
170 elif self.distribution is not None:
171 return self.distribution.getSourcePackage(self.sourcepackagename)
172 else:
173 return self.owner
174
175 def setTarget(self, target, user):
176 """See `IGitRepository`."""
177 # XXX cjwatson 2015-02-06: Fill this in once IGitNamespace is in
178 # place.
179 raise NotImplementedError
180
181 def setOwnerDefault(self, value):
182 """See `IGitRepository`."""
183 if value:
184 # Check for an existing owner-target default.
185 existing = getUtility(IGitRepositorySet).getDefaultRepository(
186 self.target, owner=self.owner)
187 if existing is not None:
188 raise GitDefaultConflict(
189 existing, self.target, owner=self.owner)
190 self.owner_default = value
191
192 def setTargetDefault(self, value):
193 """See `IGitRepository`."""
194 if value:
195 # Check for an existing target default.
196 existing = getUtility(IGitRepositorySet).getDefaultRepository(
197 self.target)
198 if existing is not None:
199 raise GitDefaultConflict(existing, self.target)
200 self.target_default = value
201
202 @property
203 def display_name(self):
204 return self.git_identity
205
206 def getInternalPath(self):
207 """See `IGitRepository`."""
208 # This may need to change later to improve support for sharding.
209 return str(self.id)
210
211 def getCodebrowseUrl(self):
212 """See `IGitRepository`."""
213 return urlutils.join(
214 config.codehosting.git_browse_root, self.unique_name)
215
216 @property
217 def private(self):
218 return self.information_type in PRIVATE_INFORMATION_TYPES
219
220 def _reconcileAccess(self):
221 """Reconcile the repository's sharing information.
222
223 Takes the information_type and target and makes the related
224 AccessArtifact and AccessPolicyArtifacts match.
225 """
226 wanted_links = None
227 pillars = []
228 # For private personal repositories, we calculate the wanted grants.
229 if (not self.project and not self.distribution and
230 not self.information_type in PUBLIC_INFORMATION_TYPES):
231 aasource = getUtility(IAccessArtifactSource)
232 [abstract_artifact] = aasource.ensure([self])
233 wanted_links = set(
234 (abstract_artifact, policy) for policy in
235 getUtility(IAccessPolicySource).findByTeam([self.owner]))
236 else:
237 # We haven't yet quite worked out how distribution privacy
238 # works, so only work for projects for now.
239 if self.project is not None:
240 pillars = [self.project]
241 reconcile_access_for_artifact(
242 self, self.information_type, pillars, wanted_links)
243
244 @cachedproperty
245 def _known_viewers(self):
246 """A set of known persons able to view this repository.
247
248 This method must return an empty set or repository searches will
249 trigger late evaluation. Any 'should be set on load' properties
250 must be done by the repository search.
251
252 If you are tempted to change this method, don't. Instead see
253 visibleByUser which defines the just-in-time policy for repository
254 visibility, and IGitCollection which honours visibility rules.
255 """
256 return set()
257
258 def visibleByUser(self, user):
259 """See `IGitRepository`."""
260 if self.information_type in PUBLIC_INFORMATION_TYPES:
261 return True
262 elif user is None:
263 return False
264 elif user.id in self._known_viewers:
265 return True
266 else:
267 # XXX cjwatson 2015-02-06: Fill this in once IGitCollection is
268 # in place.
269 return False
270
271 def getAllowedInformationTypes(self, user):
272 """See `IGitRepository`."""
273 if user_has_special_git_repository_access(user):
274 # Admins can set any type.
275 types = set(PUBLIC_INFORMATION_TYPES + PRIVATE_INFORMATION_TYPES)
276 else:
277 # Otherwise the permitted types are defined by the namespace.
278 # XXX cjwatson 2015-01-19: Define permitted types properly. For
279 # now, non-admins only get public repository access.
280 types = set(PUBLIC_INFORMATION_TYPES)
281 return types
282
283 def transitionToInformationType(self, information_type, user,
284 verify_policy=True):
285 """See `IGitRepository`."""
286 if self.information_type == information_type:
287 return
288 if (verify_policy and
289 information_type not in self.getAllowedInformationTypes(user)):
290 raise CannotChangeInformationType("Forbidden by project policy.")
291 self.information_type = information_type
292 self._reconcileAccess()
293 # XXX cjwatson 2015-02-05: Once we have repository subscribers, we
294 # need to grant them access if necessary. For now, treat the owner
295 # as always subscribed, which is just about enough to make the
296 # GitCollection tests pass.
297 if information_type in PRIVATE_INFORMATION_TYPES:
298 # Grant the subscriber access if they can't see the repository.
299 service = getUtility(IService, "sharing")
300 blind_subscribers = service.getPeopleWithoutAccess(
301 self, [self.owner])
302 if len(blind_subscribers):
303 service.ensureAccessGrants(
304 blind_subscribers, user, gitrepositories=[self],
305 ignore_permissions=True)
306 # As a result of the transition, some subscribers may no longer have
307 # access to the repository. We need to run a job to remove any such
308 # subscriptions.
309 getUtility(IRemoveArtifactSubscriptionsJobSource).create(user, [self])
310
311 def setOwner(self, new_owner, user):
312 """See `IGitRepository`."""
313 # XXX cjwatson 2015-02-06: Fill this in once IGitNamespace is in
314 # place.
315 raise NotImplementedError
316
317 def destroySelf(self):
318 raise NotImplementedError
319
320
321class GitRepositorySet:
322 """See `IGitRepositorySet`."""
323
324 implements(IGitRepositorySet)
325
326 def new(self, registrant, owner, target, name, information_type=None,
327 date_created=DEFAULT):
328 """See `IGitRepositorySet`."""
329 # XXX cjwatson 2015-02-06: Fill this in once IGitNamespace is in
330 # place.
331 raise NotImplementedError
332
333 def getByPath(self, user, path):
334 """See `IGitRepositorySet`."""
335 # XXX cjwatson 2015-02-06: Fill this in once IGitLookup is in place.
336 raise NotImplementedError
337
338 def getDefaultRepository(self, target, owner=None):
339 """See `IGitRepositorySet`."""
340 clauses = []
341 if IProduct.providedBy(target):
342 clauses.append(GitRepository.project == target)
343 elif IDistributionSourcePackage.providedBy(target):
344 clauses.append(GitRepository.distribution == target.distribution)
345 clauses.append(
346 GitRepository.sourcepackagename == target.sourcepackagename)
347 else:
348 raise GitTargetError(
349 "Personal repositories cannot be defaults for any target.")
350 if owner is not None:
351 clauses.append(GitRepository.owner == owner)
352 clauses.append(GitRepository.owner_default == True)
353 else:
354 clauses.append(GitRepository.target_default == True)
355 return IStore(GitRepository).find(GitRepository, *clauses).one()
356
357 def getRepositories(self):
358 """See `IGitRepositorySet`."""
359 return []
360
361
362def get_git_repository_privacy_filter(user):
363 public_filter = GitRepository.information_type.is_in(
364 PUBLIC_INFORMATION_TYPES)
365
366 if user is None:
367 return [public_filter]
368
369 artifact_grant_query = Coalesce(
370 ArrayIntersects(
371 SQL("GitRepository.access_grants"),
372 Select(
373 ArrayAgg(TeamParticipation.teamID),
374 tables=TeamParticipation,
375 where=(TeamParticipation.person == user)
376 )), False)
377
378 policy_grant_query = Coalesce(
379 ArrayIntersects(
380 Array(SQL("GitRepository.access_policy")),
381 Select(
382 ArrayAgg(AccessPolicyGrant.policy_id),
383 tables=(AccessPolicyGrant,
384 Join(TeamParticipation,
385 TeamParticipation.teamID ==
386 AccessPolicyGrant.grantee_id)),
387 where=(TeamParticipation.person == user)
388 )), False)
389
390 return [Or(public_filter, artifact_grant_query, policy_grant_query)]
0391
=== added file 'lib/lp/code/model/hasgitrepositories.py'
--- lib/lp/code/model/hasgitrepositories.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/model/hasgitrepositories.py 2015-02-19 18:43:50 +0000
@@ -0,0 +1,28 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4__metaclass__ = type
5__all__ = [
6 'HasGitRepositoriesMixin',
7 ]
8
9from zope.component import getUtility
10
11from lp.code.interfaces.gitrepository import IGitRepositorySet
12
13
14class HasGitRepositoriesMixin:
15 """A mixin implementation for `IHasGitRepositories`."""
16
17 def createGitRepository(self, registrant, owner, name,
18 information_type=None):
19 """See `IHasGitRepositories`."""
20 return getUtility(IGitRepositorySet).new(
21 registrant, owner, self, name,
22 information_type=information_type)
23
24 def getGitRepositories(self, visible_by_user=None, eager_load=False):
25 """See `IHasGitRepositories`."""
26 # XXX cjwatson 2015-02-06: Fill this in once IGitCollection is in
27 # place.
28 raise NotImplementedError
029
=== added file 'lib/lp/code/model/tests/test_hasgitrepositories.py'
--- lib/lp/code/model/tests/test_hasgitrepositories.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/model/tests/test_hasgitrepositories.py 2015-02-19 18:43:50 +0000
@@ -0,0 +1,34 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for classes that implement IHasGitRepositories."""
5
6__metaclass__ = type
7
8from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
9from lp.testing import (
10 TestCaseWithFactory,
11 verifyObject,
12 )
13from lp.testing.layers import DatabaseFunctionalLayer
14
15
16class TestIHasGitRepositories(TestCaseWithFactory):
17 """Test that the correct objects implement the interface."""
18
19 layer = DatabaseFunctionalLayer
20
21 def test_project_implements_hasgitrepositories(self):
22 # Projects should implement IHasGitRepositories.
23 project = self.factory.makeProduct()
24 verifyObject(IHasGitRepositories, project)
25
26 def test_dsp_implements_hasgitrepositories(self):
27 # DistributionSourcePackages should implement IHasGitRepositories.
28 dsp = self.factory.makeDistributionSourcePackage()
29 verifyObject(IHasGitRepositories, dsp)
30
31 def test_person_implements_hasgitrepositories(self):
32 # People should implement IHasGitRepositories.
33 person = self.factory.makePerson()
34 verifyObject(IHasGitRepositories, person)
035
=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml 2015-02-09 17:42:48 +0000
+++ lib/lp/registry/configure.zcml 2015-02-19 18:43:50 +0000
@@ -556,6 +556,11 @@
556 bug_reporting_guidelines556 bug_reporting_guidelines
557 enable_bugfiling_duplicate_search557 enable_bugfiling_duplicate_search
558 "/>558 "/>
559
560 <!-- IHasGitRepositories -->
561
562 <allow
563 interface="lp.code.interfaces.hasgitrepositories.IHasGitRepositories" />
559 </class>564 </class>
560 <adapter565 <adapter
561 provides="lp.registry.interfaces.distribution.IDistribution"566 provides="lp.registry.interfaces.distribution.IDistribution"
562567
=== modified file 'lib/lp/registry/interfaces/distributionsourcepackage.py'
--- lib/lp/registry/interfaces/distributionsourcepackage.py 2014-11-28 22:28:40 +0000
+++ lib/lp/registry/interfaces/distributionsourcepackage.py 2015-02-19 18:43:50 +0000
@@ -34,6 +34,7 @@
34 IHasBranches,34 IHasBranches,
35 IHasMergeProposals,35 IHasMergeProposals,
36 )36 )
37from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
37from lp.registry.interfaces.distribution import IDistribution38from lp.registry.interfaces.distribution import IDistribution
38from lp.registry.interfaces.role import IHasDrivers39from lp.registry.interfaces.role import IHasDrivers
39from lp.soyuz.enums import ArchivePurpose40from lp.soyuz.enums import ArchivePurpose
@@ -42,7 +43,8 @@
42class IDistributionSourcePackage(IHeadingContext, IBugTarget, IHasBranches,43class IDistributionSourcePackage(IHeadingContext, IBugTarget, IHasBranches,
43 IHasMergeProposals, IHasOfficialBugTags,44 IHasMergeProposals, IHasOfficialBugTags,
44 IStructuralSubscriptionTarget,45 IStructuralSubscriptionTarget,
45 IQuestionTarget, IHasDrivers):46 IQuestionTarget, IHasDrivers,
47 IHasGitRepositories):
46 """Represents a source package in a distribution.48 """Represents a source package in a distribution.
4749
48 Create IDistributionSourcePackages by invoking50 Create IDistributionSourcePackages by invoking
4951
=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py 2015-01-30 18:24:07 +0000
+++ lib/lp/registry/interfaces/person.py 2015-02-19 18:43:50 +0000
@@ -111,6 +111,7 @@
111 IHasMergeProposals,111 IHasMergeProposals,
112 IHasRequestedReviews,112 IHasRequestedReviews,
113 )113 )
114from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
114from lp.code.interfaces.hasrecipes import IHasRecipes115from lp.code.interfaces.hasrecipes import IHasRecipes
115from lp.registry.enums import (116from lp.registry.enums import (
116 EXCLUSIVE_TEAM_POLICY,117 EXCLUSIVE_TEAM_POLICY,
@@ -688,7 +689,7 @@
688 IHasMergeProposals, IHasMugshot,689 IHasMergeProposals, IHasMugshot,
689 IHasLocation, IHasRequestedReviews, IObjectWithLocation,690 IHasLocation, IHasRequestedReviews, IObjectWithLocation,
690 IHasBugs, IHasRecipes, IHasTranslationImports,691 IHasBugs, IHasRecipes, IHasTranslationImports,
691 IPersonSettings, IQuestionsPerson):692 IPersonSettings, IQuestionsPerson, IHasGitRepositories):
692 """IPerson attributes that require launchpad.View permission."""693 """IPerson attributes that require launchpad.View permission."""
693 account = Object(schema=IAccount)694 account = Object(schema=IAccount)
694 accountID = Int(title=_('Account ID'), required=True, readonly=True)695 accountID = Int(title=_('Account ID'), required=True, readonly=True)
695696
=== modified file 'lib/lp/registry/interfaces/product.py'
--- lib/lp/registry/interfaces/product.py 2015-01-30 18:24:07 +0000
+++ lib/lp/registry/interfaces/product.py 2015-02-19 18:43:50 +0000
@@ -102,6 +102,7 @@
102 IHasCodeImports,102 IHasCodeImports,
103 IHasMergeProposals,103 IHasMergeProposals,
104 )104 )
105from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
105from lp.code.interfaces.hasrecipes import IHasRecipes106from lp.code.interfaces.hasrecipes import IHasRecipes
106from lp.registry.enums import (107from lp.registry.enums import (
107 BranchSharingPolicy,108 BranchSharingPolicy,
@@ -475,7 +476,7 @@
475 IHasMugshot, IHasSprints, IHasTranslationImports,476 IHasMugshot, IHasSprints, IHasTranslationImports,
476 ITranslationPolicy, IKarmaContext, IMakesAnnouncements,477 ITranslationPolicy, IKarmaContext, IMakesAnnouncements,
477 IOfficialBugTagTargetPublic, IHasOOPSReferences,478 IOfficialBugTagTargetPublic, IHasOOPSReferences,
478 IHasRecipes, IHasCodeImports, IServiceUsage):479 IHasRecipes, IHasCodeImports, IServiceUsage, IHasGitRepositories):
479 """Public IProduct properties."""480 """Public IProduct properties."""
480481
481 registrant = exported(482 registrant = exported(
482483
=== modified file 'lib/lp/registry/model/distributionsourcepackage.py'
--- lib/lp/registry/model/distributionsourcepackage.py 2014-11-27 20:52:37 +0000
+++ lib/lp/registry/model/distributionsourcepackage.py 2015-02-19 18:43:50 +0000
@@ -43,6 +43,7 @@
43 HasBranchesMixin,43 HasBranchesMixin,
44 HasMergeProposalsMixin,44 HasMergeProposalsMixin,
45 )45 )
46from lp.code.model.hasgitrepositories import HasGitRepositoriesMixin
46from lp.registry.interfaces.distributionsourcepackage import (47from lp.registry.interfaces.distributionsourcepackage import (
47 IDistributionSourcePackage,48 IDistributionSourcePackage,
48 )49 )
@@ -119,7 +120,7 @@
119 HasBranchesMixin,120 HasBranchesMixin,
120 HasCustomLanguageCodesMixin,121 HasCustomLanguageCodesMixin,
121 HasMergeProposalsMixin,122 HasMergeProposalsMixin,
122 HasDriversMixin):123 HasDriversMixin, HasGitRepositoriesMixin):
123 """This is a "Magic Distribution Source Package". It is not an124 """This is a "Magic Distribution Source Package". It is not an
124 SQLObject, but instead it represents a source package with a particular125 SQLObject, but instead it represents a source package with a particular
125 name in a particular distribution. You can then ask it all sorts of126 name in a particular distribution. You can then ask it all sorts of
126127
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2015-01-28 16:10:51 +0000
+++ lib/lp/registry/model/person.py 2015-02-19 18:43:50 +0000
@@ -146,6 +146,7 @@
146 HasMergeProposalsMixin,146 HasMergeProposalsMixin,
147 HasRequestedReviewsMixin,147 HasRequestedReviewsMixin,
148 )148 )
149from lp.code.model.hasgitrepositories import HasGitRepositoriesMixin
149from lp.registry.enums import (150from lp.registry.enums import (
150 EXCLUSIVE_TEAM_POLICY,151 EXCLUSIVE_TEAM_POLICY,
151 INCLUSIVE_TEAM_POLICY,152 INCLUSIVE_TEAM_POLICY,
@@ -476,7 +477,7 @@
476class Person(477class Person(
477 SQLBase, HasBugsBase, HasSpecificationsMixin, HasTranslationImportsMixin,478 SQLBase, HasBugsBase, HasSpecificationsMixin, HasTranslationImportsMixin,
478 HasBranchesMixin, HasMergeProposalsMixin, HasRequestedReviewsMixin,479 HasBranchesMixin, HasMergeProposalsMixin, HasRequestedReviewsMixin,
479 QuestionsPersonMixin):480 QuestionsPersonMixin, HasGitRepositoriesMixin):
480 """A Person."""481 """A Person."""
481482
482 implements(IPerson, IHasIcon, IHasLogo, IHasMugshot)483 implements(IPerson, IHasIcon, IHasLogo, IHasMugshot)
483484
=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py 2015-01-29 16:28:30 +0000
+++ lib/lp/registry/model/product.py 2015-02-19 18:43:50 +0000
@@ -124,6 +124,7 @@
124 HasCodeImportsMixin,124 HasCodeImportsMixin,
125 HasMergeProposalsMixin,125 HasMergeProposalsMixin,
126 )126 )
127from lp.code.model.hasgitrepositories import HasGitRepositoriesMixin
127from lp.code.model.sourcepackagerecipe import SourcePackageRecipe128from lp.code.model.sourcepackagerecipe import SourcePackageRecipe
128from lp.code.model.sourcepackagerecipedata import SourcePackageRecipeData129from lp.code.model.sourcepackagerecipedata import SourcePackageRecipeData
129from lp.registry.enums import (130from lp.registry.enums import (
@@ -361,7 +362,7 @@
361 OfficialBugTagTargetMixin, HasBranchesMixin,362 OfficialBugTagTargetMixin, HasBranchesMixin,
362 HasCustomLanguageCodesMixin, HasMergeProposalsMixin,363 HasCustomLanguageCodesMixin, HasMergeProposalsMixin,
363 HasCodeImportsMixin, InformationTypeMixin,364 HasCodeImportsMixin, InformationTypeMixin,
364 TranslationPolicyMixin):365 TranslationPolicyMixin, HasGitRepositoriesMixin):
365 """A Product."""366 """A Product."""
366367
367 implements(368 implements(
368369
=== modified file 'lib/lp/registry/tests/test_product.py'
--- lib/lp/registry/tests/test_product.py 2015-01-29 16:28:30 +0000
+++ lib/lp/registry/tests/test_product.py 2015-02-19 18:43:50 +0000
@@ -858,10 +858,10 @@
858 'getCustomLanguageCode', 'getDefaultBugInformationType',858 'getCustomLanguageCode', 'getDefaultBugInformationType',
859 'getDefaultSpecificationInformationType',859 'getDefaultSpecificationInformationType',
860 'getEffectiveTranslationPermission', 'getExternalBugTracker',860 'getEffectiveTranslationPermission', 'getExternalBugTracker',
861 'getFAQ', 'getFirstEntryToImport', 'getLinkedBugWatches',861 'getFAQ', 'getFirstEntryToImport', 'getGitRepositories',
862 'getMergeProposals', 'getMilestone', 'getMilestonesAndReleases',862 'getLinkedBugWatches', 'getMergeProposals', 'getMilestone',
863 'getQuestion', 'getQuestionLanguages', 'getPackage', 'getRelease',863 'getMilestonesAndReleases', 'getQuestion', 'getQuestionLanguages',
864 'getSeries', 'getSubscription',864 'getPackage', 'getRelease', 'getSeries', 'getSubscription',
865 'getSubscriptions', 'getSupportedLanguages', 'getTimeline',865 'getSubscriptions', 'getSupportedLanguages', 'getTimeline',
866 'getTopContributors', 'getTopContributorsGroupedByCategory',866 'getTopContributors', 'getTopContributorsGroupedByCategory',
867 'getTranslationGroups', 'getTranslationImportQueueEntries',867 'getTranslationGroups', 'getTranslationImportQueueEntries',
@@ -902,7 +902,8 @@
902 'launchpad.Edit': set((902 'launchpad.Edit': set((
903 'addOfficialBugTag', 'removeOfficialBugTag',903 'addOfficialBugTag', 'removeOfficialBugTag',
904 'setBranchSharingPolicy', 'setBugSharingPolicy',904 'setBranchSharingPolicy', 'setBugSharingPolicy',
905 'setSpecificationSharingPolicy', 'checkInformationType')),905 'setSpecificationSharingPolicy', 'checkInformationType',
906 'createGitRepository')),
906 'launchpad.Moderate': set((907 'launchpad.Moderate': set((
907 'is_permitted', 'license_approved', 'project_reviewed',908 'is_permitted', 'license_approved', 'project_reviewed',
908 'reviewer_whiteboard', 'setAliases')),909 'reviewer_whiteboard', 'setAliases')),
909910
=== modified file 'lib/lp/security.py'
--- lib/lp/security.py 2015-01-06 04:52:44 +0000
+++ lib/lp/security.py 2015-02-19 18:43:50 +0000
@@ -83,6 +83,10 @@
83 )83 )
84from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference84from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
85from lp.code.interfaces.diff import IPreviewDiff85from lp.code.interfaces.diff import IPreviewDiff
86from lp.code.interfaces.gitrepository import (
87 IGitRepository,
88 user_has_special_git_repository_access,
89 )
86from lp.code.interfaces.sourcepackagerecipe import ISourcePackageRecipe90from lp.code.interfaces.sourcepackagerecipe import ISourcePackageRecipe
87from lp.code.interfaces.sourcepackagerecipebuild import (91from lp.code.interfaces.sourcepackagerecipebuild import (
88 ISourcePackageRecipeBuild,92 ISourcePackageRecipeBuild,
@@ -1151,14 +1155,37 @@
11511155
11521156
1153class EditDistributionSourcePackage(AuthorizationBase):1157class EditDistributionSourcePackage(AuthorizationBase):
1154 """DistributionSourcePackage is not editable.
1155
1156 But EditStructuralSubscription needs launchpad.Edit defined on all
1157 targets.
1158 """
1159 permission = 'launchpad.Edit'1158 permission = 'launchpad.Edit'
1160 usedfor = IDistributionSourcePackage1159 usedfor = IDistributionSourcePackage
11611160
1161 def _checkUpload(self, user, archive, distroseries):
1162 # We use verifyUpload() instead of checkUpload() because we don't
1163 # have a pocket. It returns the reason the user can't upload or
1164 # None if they are allowed.
1165 if distroseries is None:
1166 return False
1167 reason = archive.verifyUpload(
1168 user.person, sourcepackagename=self.obj.sourcepackagename,
1169 component=None, distroseries=distroseries, strict_component=False)
1170 return reason is None
1171
1172 def checkAuthenticated(self, user):
1173 """Anyone who can upload a package can edit it.
1174
1175 Checking upload permission requires a distroseries; a reasonable
1176 approximation is to check whether the user can upload the package to
1177 the current series.
1178 """
1179 if user.in_admin:
1180 return True
1181
1182 distribution = self.obj.distribution
1183 if user.inTeam(distribution.owner):
1184 return True
1185
1186 return self._checkUpload(
1187 user, distribution.main_archive, distribution.currentseries)
1188
11621189
1163class BugTargetOwnerOrBugSupervisorOrAdmins(AuthorizationBase):1190class BugTargetOwnerOrBugSupervisorOrAdmins(AuthorizationBase):
1164 """Product's owner and bug supervisor can set official bug tags."""1191 """Product's owner and bug supervisor can set official bug tags."""
@@ -2176,6 +2203,57 @@
2176 return user.in_admin2203 return user.in_admin
21772204
21782205
2206class ViewGitRepository(AuthorizationBase):
2207 """Controls visibility of Git repositories.
2208
2209 A person can see the repository if the repository is public, they are
2210 the owner of the repository, they are in the team that owns the
2211 repository, they have an access grant to the repository, or they are a
2212 Launchpad administrator.
2213 """
2214 permission = 'launchpad.View'
2215 usedfor = IGitRepository
2216
2217 def checkAuthenticated(self, user):
2218 return self.obj.visibleByUser(user.person)
2219
2220 def checkUnauthenticated(self):
2221 return self.obj.visibleByUser(None)
2222
2223
2224class EditGitRepository(AuthorizationBase):
2225 """The owner or admins can edit Git repositories."""
2226 permission = 'launchpad.Edit'
2227 usedfor = IGitRepository
2228
2229 def checkAuthenticated(self, user):
2230 # XXX cjwatson 2015-01-23: People who can upload source packages to
2231 # a distribution should be able to push to the corresponding
2232 # "official" repositories, once those are defined.
2233 return (
2234 user.inTeam(self.obj.owner) or
2235 user_has_special_git_repository_access(user.person))
2236
2237
2238class ModerateGitRepository(EditGitRepository):
2239 """The owners, project owners, and admins can moderate Git repositories."""
2240 permission = 'launchpad.Moderate'
2241
2242 def checkAuthenticated(self, user):
2243 if super(ModerateGitRepository, self).checkAuthenticated(user):
2244 return True
2245 target = self.obj.target
2246 if (target is not None and IProduct.providedBy(target) and
2247 user.inTeam(target.owner)):
2248 return True
2249 return user.in_commercial_admin
2250
2251
2252class AdminGitRepository(AdminByAdminsTeam):
2253 """The admins can administer Git repositories."""
2254 usedfor = IGitRepository
2255
2256
2179class AdminDistroSeriesTranslations(AuthorizationBase):2257class AdminDistroSeriesTranslations(AuthorizationBase):
2180 permission = 'launchpad.TranslationsAdmin'2258 permission = 'launchpad.TranslationsAdmin'
2181 usedfor = IDistroSeries2259 usedfor = IDistroSeries
@@ -2858,8 +2936,7 @@
2858 usedfor = IPublisherConfig2936 usedfor = IPublisherConfig
28592937
28602938
2861class EditSourcePackage(AuthorizationBase):2939class EditSourcePackage(EditDistributionSourcePackage):
2862 permission = 'launchpad.Edit'
2863 usedfor = ISourcePackage2940 usedfor = ISourcePackage
28642941
2865 def checkAuthenticated(self, user):2942 def checkAuthenticated(self, user):
@@ -2871,15 +2948,8 @@
2871 if user.inTeam(distribution.owner):2948 if user.inTeam(distribution.owner):
2872 return True2949 return True
28732950
2874 # We use verifyUpload() instead of checkUpload() because2951 return self._checkUpload(
2875 # we don't have a pocket.2952 user, distribution.main_archive, self.obj.distroseries)
2876 # It returns the reason the user can't upload
2877 # or None if they are allowed.
2878 reason = distribution.main_archive.verifyUpload(
2879 user.person, distroseries=self.obj.distroseries,
2880 sourcepackagename=self.obj.sourcepackagename,
2881 component=None, strict_component=False)
2882 return reason is None
28832953
28842954
2885class ViewLiveFS(DelegatedAuthorization):2955class ViewLiveFS(DelegatedAuthorization):
28862956
=== modified file 'lib/lp/services/config/schema-lazr.conf'
--- lib/lp/services/config/schema-lazr.conf 2014-08-05 08:58:14 +0000
+++ lib/lp/services/config/schema-lazr.conf 2015-02-19 18:43:50 +0000
@@ -335,6 +335,27 @@
335# of shutting down and so should not receive any more connections.335# of shutting down and so should not receive any more connections.
336web_status_port = tcp:8022336web_status_port = tcp:8022
337337
338# The URL of the internal Git hosting API endpoint.
339internal_git_api_endpoint: none
340
341# The URL prefix for links to the Git code browser. Links are formed by
342# appending the repository's path to the root URL.
343#
344# datatype: urlbase
345git_browse_root: none
346
347# The URL prefix for anonymous Git protocol fetches. Links are formed by
348# appending the repository's path to the root URL.
349#
350# datatype: urlbase
351git_anon_root: none
352
353# The URL prefix for Git-over-SSH. Links are formed by appending the
354# repository's path to the root URL.
355#
356# datatype: urlbase
357git_ssh_root: none
358
338359
339[codeimport]360[codeimport]
340# Where the Bazaar imports are stored.361# Where the Bazaar imports are stored.