launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #22877
[Merge] lp:~cjwatson/launchpad/git-permissions-model into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/git-permissions-model into lp:launchpad.
Commit message:
Add basic GitRule and GitGrant models.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1517559 in Launchpad itself: "git fine-grained permissions"
https://bugs.launchpad.net/launchpad/+bug/1517559
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/git-permissions-model/+merge/354201
See https://docs.google.com/document/d/1JW_D_Tgo4X2-vPMZtShSbi3cm1iOsGcNIzeOpa5E_wA for the design and https://code.launchpad.net/~cjwatson/launchpad/db-git-permissions/+merge/354200 for schema changes.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/git-permissions-model into lp:launchpad.
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2018-07-31 12:41:29 +0000
+++ database/schema/security.cfg 2018-09-03 17:08:57 +0000
@@ -187,9 +187,11 @@
public.featureflag = SELECT, INSERT, UPDATE, DELETE
public.featureflagchangelogentry = SELECT, INSERT, UPDATE
public.flatpackagesetinclusion = SELECT, INSERT, UPDATE, DELETE
+public.gitgrant = SELECT, INSERT, UPDATE, DELETE
public.gitjob = SELECT, INSERT, UPDATE, DELETE
public.gitref = SELECT, INSERT, UPDATE, DELETE
public.gitrepository = SELECT, INSERT, UPDATE, DELETE
+public.gitrule = SELECT, INSERT, UPDATE, DELETE
public.gitsubscription = SELECT, INSERT, UPDATE, DELETE
public.hwdevice = SELECT
public.hwdeviceclass = SELECT, INSERT, DELETE
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml 2018-05-16 17:33:18 +0000
+++ lib/lp/code/configure.zcml 2018-09-03 17:08:57 +0000
@@ -919,6 +919,33 @@
<allow interface="lp.code.interfaces.gitref.IGitRefRemoteSet" />
</securedutility>
+ <!-- Git repository access rules -->
+
+ <class class="lp.code.model.gitrule.GitRule">
+ <require
+ permission="launchpad.View"
+ interface="lp.code.interfaces.gitrule.IGitRuleView" />
+ <require
+ permission="launchpad.Edit"
+ interface="lp.code.interfaces.gitrule.IGitRuleEdit" />
+ </class>
+ <subscriber
+ for="lp.code.interfaces.gitrule.IGitRule zope.lifecycleevent.interfaces.IObjectModifiedEvent"
+ handler="lp.code.model.gitrule.git_rule_modified"/>
+ <class class="lp.code.model.gitgrant.GitGrant">
+ <require
+ permission="launchpad.View"
+ interface="lp.code.interfaces.gitgrant.IGitGrantView
+ lp.code.interfaces.gitgrant.IGitGrantEditableAttributes" />
+ <require
+ permission="launchpad.Edit"
+ interface="lp.code.interfaces.gitgrant.IGitGrantEdit"
+ set_schema="lp.code.interfaces.gitgrant.IGitGrantEditableAttributes" />
+ </class>
+ <subscriber
+ for="lp.code.interfaces.gitgrant.IGitGrant zope.lifecycleevent.interfaces.IObjectModifiedEvent"
+ handler="lp.code.model.gitgrant.git_grant_modified"/>
+
<!-- GitCollection -->
<class class="lp.code.model.gitcollection.GenericGitCollection">
=== modified file 'lib/lp/code/enums.py'
--- lib/lp/code/enums.py 2017-06-15 01:02:11 +0000
+++ lib/lp/code/enums.py 2018-09-03 17:08:57 +0000
@@ -21,6 +21,7 @@
'CodeImportReviewStatus',
'CodeReviewNotificationLevel',
'CodeReviewVote',
+ 'GitGranteeType',
'GitObjectType',
'GitRepositoryType',
'NON_CVS_RCS_TYPES',
@@ -175,6 +176,26 @@
""")
+class GitGranteeType(DBEnumeratedType):
+ """Git Grantee Type
+
+ Access grants for Git repositories can be made to various kinds of
+ grantees.
+ """
+
+ REPOSITORY_OWNER = DBItem(1, """
+ Repository owner
+
+ A grant to the owner of the associated repository.
+ """)
+
+ PERSON = DBItem(2, """
+ Person
+
+ A grant to a particular person or team.
+ """)
+
+
class BranchLifecycleStatusFilter(EnumeratedType):
"""Branch Lifecycle Status Filter
=== added file 'lib/lp/code/interfaces/gitgrant.py'
--- lib/lp/code/interfaces/gitgrant.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/interfaces/gitgrant.py 2018-09-03 17:08:57 +0000
@@ -0,0 +1,95 @@
+# Copyright 2018 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Git repository access grants."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'IGitGrant',
+ ]
+
+from lazr.restful.fields import Reference
+from zope.interface import Interface
+from zope.schema import (
+ Bool,
+ Choice,
+ Datetime,
+ Int,
+ )
+
+from lp import _
+from lp.code.enums import GitGranteeType
+from lp.code.interfaces.gitrepository import IGitRepository
+from lp.code.interfaces.gitrule import IGitRule
+from lp.registry.interfaces.person import IPerson
+
+
+class IGitGrantView(Interface):
+ """`IGitGrant` attributes that require launchpad.View."""
+
+ id = Int(title=_("ID"), readonly=True, required=True)
+
+ repository = Reference(
+ title=_("Repository"), required=True, readonly=True,
+ schema=IGitRepository,
+ description=_("The repository that this grant is for."))
+
+ rule = Reference(
+ title=_("Rule"), required=True, readonly=True,
+ schema=IGitRule,
+ description=_("The rule that this grant is for."))
+
+ grantor = Reference(
+ title=_("Grantor"), required=True, readonly=True,
+ schema=IPerson,
+ description=_("The user who created this grant."))
+
+ date_created = Datetime(
+ title=_("Date created"), required=True, readonly=True,
+ description=_("The time when this grant was created."))
+
+
+class IGitGrantEditableAttributes(Interface):
+ """`IGitGrant` attributes that can be edited.
+
+ These attributes need launchpad.View to see, and launchpad.Edit to change.
+ """
+
+ grantee_type = Choice(
+ title=_("Grantee type"), required=True, readonly=False,
+ vocabulary=GitGranteeType,
+ description=_("The type of grantee for this grant."))
+
+ grantee = Reference(
+ title=_("Grantee"), required=False, readonly=False,
+ schema=IPerson,
+ description=_("The person being granted access."))
+
+ can_create = Bool(
+ title=_("Can create"), required=True, readonly=False,
+ description=_("Whether creating references is allowed."))
+
+ can_push = Bool(
+ title=_("Can push"), required=True, readonly=False,
+ description=_("Whether pushing references is allowed."))
+
+ can_force_push = Bool(
+ title=_("Can force-push"), required=True, readonly=False,
+ description=_("Whether force-pushing references is allowed."))
+
+ date_last_modified = Datetime(
+ title=_("Date last modified"), required=True, readonly=True,
+ description=_("The time when this grant was last modified."))
+
+
+class IGitGrantEdit(Interface):
+ """`IGitGrant` attributes that require launchpad.Edit."""
+
+ def destroySelf():
+ """Delete this access grant."""
+
+
+class IGitGrant(IGitGrantView, IGitGrantEditableAttributes, IGitGrantEdit):
+ """An access grant for a Git repository rule."""
=== modified file 'lib/lp/code/interfaces/gitrepository.py'
--- lib/lp/code/interfaces/gitrepository.py 2018-08-23 17:03:05 +0000
+++ lib/lp/code/interfaces/gitrepository.py 2018-09-03 17:08:57 +0000
@@ -266,6 +266,10 @@
# Really ICodeImport, patched in _schema_circular_imports.py.
schema=Interface))
+ rules = Attribute("The access rules for this repository.")
+
+ grants = Attribute("The access grants for this repository.")
+
@operation_parameters(
path=TextLine(title=_("A string to look up as a path.")))
# Really IGitRef, patched in _schema_circular_imports.py.
@@ -715,6 +719,16 @@
This may be helpful in cases where a previous scan crashed.
"""
+ def addRule(ref_pattern, creator, position=None):
+ """Add an access rule to this repository.
+
+ :param ref_pattern: The reference pattern that the new rule should
+ match.
+ :param creator: The `IPerson` who is adding the rule.
+ :param position: The list position at which to insert the rule, or
+ None to append it.
+ """
+
@export_read_operation()
@operation_for_version("devel")
def canBeDeleted():
=== added file 'lib/lp/code/interfaces/gitrule.py'
--- lib/lp/code/interfaces/gitrule.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/interfaces/gitrule.py 2018-09-03 17:08:57 +0000
@@ -0,0 +1,83 @@
+# Copyright 2018 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Git repository access rules."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'IGitRule',
+ ]
+
+from lazr.restful.fields import Reference
+from zope.interface import (
+ Attribute,
+ Interface,
+ )
+from zope.schema import (
+ Datetime,
+ Int,
+ TextLine,
+ )
+
+from lp import _
+from lp.code.interfaces.gitrepository import IGitRepository
+from lp.registry.interfaces.person import IPerson
+
+
+class IGitRuleView(Interface):
+ """`IGitRule` attributes that require launchpad.View."""
+
+ id = Int(title=_("ID"), readonly=True, required=True)
+
+ repository = Reference(
+ title=_("Repository"), required=True, readonly=True,
+ schema=IGitRepository,
+ description=_("The repository that this rule is for."))
+
+ ref_pattern = TextLine(
+ title=_("Pattern"), required=True, readonly=False,
+ description=_("The pattern of references matched by this rule."))
+
+ creator = Reference(
+ title=_("Creator"), required=True, readonly=True,
+ schema=IPerson,
+ description=_("The user who created this rule."))
+
+ date_created = Datetime(
+ title=_("Date created"), required=True, readonly=True,
+ description=_("The time when this rule was created."))
+
+ date_last_modified = Datetime(
+ title=_("Date last modified"), required=True, readonly=True,
+ description=_("The time when this rule was last modified."))
+
+ grants = Attribute("The access grants for this rule.")
+
+
+class IGitRuleEdit(Interface):
+ """`IGitRule` attributes that require launchpad.Edit."""
+
+ def addGrant(grantee, grantor, can_create=False, can_push=False,
+ can_force_push=False):
+ """Add an access grant to this rule.
+
+ :param grantee: The `IPerson` who is being granted permission, or an
+ item of `GitGranteeType` other than `GitGranteeType.PERSON` to
+ grant permission to some other kind of entity.
+ :param grantor: The `IPerson` who is granting permission.
+ :param can_create: Whether the grantee can create references
+ matching this rule.
+ :param can_push: Whether the grantee can push references matching
+ this rule.
+ :param can_force_push: Whether the grantee can force-push references
+ matching this rule.
+ """
+
+ def destroySelf():
+ """Delete this rule."""
+
+
+class IGitRule(IGitRuleView, IGitRuleEdit):
+ """An access rule for a Git repository."""
=== added file 'lib/lp/code/model/gitgrant.py'
--- lib/lp/code/model/gitgrant.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/model/gitgrant.py 2018-09-03 17:08:57 +0000
@@ -0,0 +1,113 @@
+# Copyright 2018 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Git repository access grants."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'GitGrant',
+ ]
+
+from lazr.enum import DBItem
+import pytz
+from storm.locals import (
+ Bool,
+ DateTime,
+ Int,
+ Reference,
+ Store,
+ )
+from zope.interface import implementer
+
+from lp.code.enums import GitGranteeType
+from lp.code.interfaces.gitgrant import IGitGrant
+from lp.services.database.constants import UTC_NOW
+from lp.services.database.enumcol import EnumCol
+from lp.services.database.stormbase import StormBase
+
+
+def git_grant_modified(grant, event):
+ """Update date_last_modified when a GitGrant is modified.
+
+ This method is registered as a subscriber to `IObjectModifiedEvent`
+ events on Git repository grants.
+ """
+ if event.edited_fields:
+ grant.date_last_modified = UTC_NOW
+
+
+@implementer(IGitGrant)
+class GitGrant(StormBase):
+ """See `IGitGrant`."""
+
+ __storm_table__ = 'GitGrant'
+
+ id = Int(primary=True)
+
+ repository_id = Int(name='repository', allow_none=False)
+ repository = Reference(repository_id, 'GitRepository.id')
+
+ rule_id = Int(name='rule', allow_none=False)
+ rule = Reference(rule_id, 'GitRule.id')
+
+ grantee_type = EnumCol(
+ dbName='grantee_type', enum=GitGranteeType, notNull=True)
+
+ grantee_id = Int(name='grantee', allow_none=True)
+ grantee = Reference(grantee_id, 'Person.id')
+
+ can_create = Bool(name='can_create', allow_none=False)
+ can_push = Bool(name='can_push', allow_none=False)
+ can_force_push = Bool(name='can_force_push', allow_none=False)
+
+ grantor_id = Int(name='grantor', allow_none=False)
+ grantor = Reference(grantor_id, 'Person.id')
+
+ date_created = DateTime(
+ name='date_created', tzinfo=pytz.UTC, allow_none=False)
+ date_last_modified = DateTime(
+ name='date_last_modified', tzinfo=pytz.UTC, allow_none=False)
+
+ def __init__(self, rule, grantee, can_create, can_push, can_force_push,
+ grantor, date_created):
+ if isinstance(grantee, DBItem) and grantee.enum == GitGranteeType:
+ if grantee == GitGranteeType.PERSON:
+ raise ValueError(
+ "grantee may not be GitGranteeType.PERSON; pass a person "
+ "object instead")
+ grantee_type = grantee
+ grantee = None
+ else:
+ grantee_type = GitGranteeType.PERSON
+
+ self.repository = rule.repository
+ self.rule = rule
+ self.grantee_type = grantee_type
+ self.grantee = grantee
+ self.can_create = can_create
+ self.can_push = can_push or can_force_push
+ self.can_force_push = can_force_push
+ self.grantor = grantor
+ self.date_created = date_created
+ self.date_last_modified = date_created
+
+ def __repr__(self):
+ permissions = []
+ if self.can_create:
+ permissions.append("create")
+ if self.can_push:
+ permissions.append("push")
+ if self.can_force_push:
+ permissions.append("force-push")
+ if self.grantee_type == GitGranteeType.PERSON:
+ grantee_name = "~%s" % self.grantee.name
+ else:
+ grantee_name = self.grantee_type.title.lower()
+ return "<GitGrant [%s] to %s> for %r" % (
+ ", ".join(permissions), grantee_name, self.rule)
+
+ def destroySelf(self):
+ """See `IGitGrant`."""
+ Store.of(self).remove(self)
=== modified file 'lib/lp/code/model/gitrepository.py'
--- lib/lp/code/model/gitrepository.py 2018-08-31 13:14:39 +0000
+++ lib/lp/code/model/gitrepository.py 2018-09-03 17:08:57 +0000
@@ -41,6 +41,7 @@
Bool,
DateTime,
Int,
+ List,
Reference,
Unicode,
)
@@ -108,10 +109,12 @@
from lp.code.interfaces.revision import IRevisionSet
from lp.code.mail.branch import send_git_repository_modified_notifications
from lp.code.model.branchmergeproposal import BranchMergeProposal
+from lp.code.model.gitgrant import GitGrant
from lp.code.model.gitref import (
GitRef,
GitRefDefault,
)
+from lp.code.model.gitrule import GitRule
from lp.code.model.gitsubscription import GitSubscription
from lp.registry.enums import PersonVisibility
from lp.registry.errors import CannotChangeInformationType
@@ -280,6 +283,8 @@
_default_branch = Unicode(name='default_branch', allow_none=True)
+ rule_order = List(type=Int())
+
def __init__(self, repository_type, registrant, owner, target, name,
information_type, date_created, reviewer=None,
description=None):
@@ -1118,6 +1123,42 @@
def code_import(self):
return getUtility(ICodeImportSet).getByGitRepository(self)
+ @property
+ def rules(self):
+ """See `IGitRepository`."""
+ if not self.rule_order:
+ return []
+ rules = {
+ rule.id: rule
+ for rule in Store.of(self).find(
+ GitRule, GitRule.id.is_in(self.rule_order))
+ }
+ for rule_id in self.rule_order:
+ if rule_id not in rules:
+ raise AssertionError(
+ "GitRule ID %d is in rule_order for %r but does not "
+ "exist" % (rule_id, self))
+ return [rules[rule_id] for rule_id in self.rule_order]
+
+ def addRule(self, ref_pattern, creator, position=None):
+ """See `IGitRepository`."""
+ rule = GitRule(
+ repository=self, ref_pattern=ref_pattern, creator=creator,
+ date_created=DEFAULT)
+ Store.of(rule).flush()
+ if self.rule_order is None:
+ self.rule_order = []
+ if position is None:
+ self.rule_order.append(rule.id)
+ else:
+ self.rule_order.insert(position, rule.id)
+ return rule
+
+ @property
+ def grants(self):
+ """See `IGitRepository`."""
+ return Store.of(self).find(GitGrant, GitGrant.repository_id == self.id)
+
def canBeDeleted(self):
"""See `IGitRepository`."""
# Can't delete if the repository is associated with anything.
=== added file 'lib/lp/code/model/gitrule.py'
--- lib/lp/code/model/gitrule.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/model/gitrule.py 2018-09-03 17:08:57 +0000
@@ -0,0 +1,90 @@
+# Copyright 2018 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Git repository access rules."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'GitRule',
+ ]
+
+import pytz
+from storm.locals import (
+ DateTime,
+ Int,
+ Reference,
+ Store,
+ Unicode,
+ )
+from zope.interface import implementer
+
+from lp.code.interfaces.gitrule import IGitRule
+from lp.code.model.gitgrant import GitGrant
+from lp.services.database.constants import (
+ DEFAULT,
+ UTC_NOW,
+ )
+from lp.services.database.stormbase import StormBase
+
+
+def git_rule_modified(rule, event):
+ """Update date_last_modified when a GitRule is modified.
+
+ This method is registered as a subscriber to `IObjectModifiedEvent`
+ events on Git repository rules.
+ """
+ if event.edited_fields:
+ rule.date_last_modified = UTC_NOW
+
+
+@implementer(IGitRule)
+class GitRule(StormBase):
+ """See `IGitRule`."""
+
+ __storm_table__ = 'GitRule'
+
+ id = Int(primary=True)
+
+ repository_id = Int(name='repository', allow_none=False)
+ repository = Reference(repository_id, 'GitRepository.id')
+
+ ref_pattern = Unicode(name='ref_pattern', allow_none=False)
+
+ creator_id = Int(name='creator', allow_none=False)
+ creator = Reference(creator_id, 'Person.id')
+
+ date_created = DateTime(
+ name='date_created', tzinfo=pytz.UTC, allow_none=False)
+ date_last_modified = DateTime(
+ name='date_last_modified', tzinfo=pytz.UTC, allow_none=False)
+
+ def __init__(self, repository, ref_pattern, creator, date_created):
+ super(GitRule, self).__init__()
+ self.repository = repository
+ self.ref_pattern = ref_pattern
+ self.creator = creator
+ self.date_created = date_created
+ self.date_last_modified = date_created
+
+ def __repr__(self):
+ return "<GitRule '%s'> for %r" % (self.ref_pattern, self.repository)
+
+ @property
+ def grants(self):
+ """See `IGitRule`."""
+ return Store.of(self).find(GitGrant, GitGrant.rule_id == self.id)
+
+ def addGrant(self, grantee, grantor, can_create=False, can_push=False,
+ can_force_push=False):
+ """See `IGitRule`."""
+ return GitGrant(
+ rule=self, grantee=grantee, can_create=can_create,
+ can_push=can_push, can_force_push=can_force_push, grantor=grantor,
+ date_created=DEFAULT)
+
+ def destroySelf(self):
+ """See `IGitRule`."""
+ self.repository.rule_order.remove(self.id)
+ Store.of(self).remove(self)
=== added file 'lib/lp/code/model/tests/test_gitgrant.py'
--- lib/lp/code/model/tests/test_gitgrant.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/model/tests/test_gitgrant.py 2018-09-03 17:08:57 +0000
@@ -0,0 +1,106 @@
+# Copyright 2018 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for Git repository access grants."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+
+from storm.store import Store
+from testtools.matchers import (
+ Equals,
+ Is,
+ MatchesSetwise,
+ MatchesStructure,
+ )
+
+from lp.code.enums import GitGranteeType
+from lp.code.interfaces.gitgrant import IGitGrant
+from lp.services.database.sqlbase import get_transaction_timestamp
+from lp.testing import (
+ person_logged_in,
+ TestCaseWithFactory,
+ verifyObject,
+ )
+from lp.testing.layers import DatabaseFunctionalLayer
+
+
+class TestGitGrant(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_implements_IGitGrant(self):
+ grant = self.factory.makeGitGrant()
+ verifyObject(IGitGrant, grant)
+
+ def test_properties_owner(self):
+ owner = self.factory.makeTeam()
+ member = self.factory.makePerson(member_of=[owner])
+ rule = self.factory.makeGitRule(owner=owner)
+ grant = self.factory.makeGitGrant(
+ rule=rule, grantee=GitGranteeType.REPOSITORY_OWNER, grantor=member,
+ can_create=True, can_force_push=True)
+ now = get_transaction_timestamp(Store.of(grant))
+ self.assertThat(grant, MatchesStructure(
+ repository=Equals(rule.repository),
+ rule=Equals(rule),
+ grantee_type=Equals(GitGranteeType.REPOSITORY_OWNER),
+ grantee=Is(None),
+ can_create=Is(True),
+ # can_force_push implies can_push.
+ can_push=Is(True),
+ can_force_push=Is(True),
+ grantor=Equals(member),
+ date_created=Equals(now),
+ date_last_modified=Equals(now)))
+
+ def test_properties_person(self):
+ owner = self.factory.makeTeam()
+ member = self.factory.makePerson(member_of=[owner])
+ rule = self.factory.makeGitRule(owner=owner)
+ grantee = self.factory.makePerson()
+ grant = self.factory.makeGitGrant(
+ rule=rule, grantee=grantee, grantor=member, can_push=True)
+ now = get_transaction_timestamp(Store.of(rule))
+ self.assertThat(grant, MatchesStructure(
+ repository=Equals(rule.repository),
+ rule=Equals(rule),
+ grantee_type=Equals(GitGranteeType.PERSON),
+ grantee=Equals(grantee),
+ can_create=Is(False),
+ can_push=Is(True),
+ can_force_push=Is(False),
+ grantor=Equals(member),
+ date_created=Equals(now),
+ date_last_modified=Equals(now)))
+
+ def test_repr_owner(self):
+ rule = self.factory.makeGitRule()
+ grant = self.factory.makeGitGrant(
+ rule=rule, grantee=GitGranteeType.REPOSITORY_OWNER,
+ can_create=True, can_push=True)
+ self.assertEqual(
+ "<GitGrant [create, push] to repository owner> for %r" % rule,
+ repr(grant))
+
+ def test_repr_person(self):
+ rule = self.factory.makeGitRule()
+ grantee = self.factory.makePerson()
+ grant = self.factory.makeGitGrant(
+ rule=rule, grantee=grantee, can_push=True)
+ self.assertEqual(
+ "<GitGrant [push] to ~%s> for %r" % (grantee.name, rule),
+ repr(grant))
+
+ def test_destroySelf(self):
+ rule = self.factory.makeGitRule()
+ grants = [
+ self.factory.makeGitGrant(
+ rule=rule, grantee=GitGranteeType.REPOSITORY_OWNER,
+ can_create=True),
+ self.factory.makeGitGrant(rule=rule, can_push=True),
+ ]
+ with person_logged_in(rule.repository.owner):
+ grants[1].destroySelf()
+ self.assertThat(rule.grants, MatchesSetwise(Equals(grants[0])))
=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
--- lib/lp/code/model/tests/test_gitrepository.py 2018-08-31 14:25:40 +0000
+++ lib/lp/code/model/tests/test_gitrepository.py 2018-09-03 17:08:57 +0000
@@ -23,6 +23,7 @@
from testtools.matchers import (
EndsWith,
LessThan,
+ MatchesListwise,
MatchesSetwise,
MatchesStructure,
)
@@ -2179,6 +2180,72 @@
self.assertEqual('Some text', ret)
+class TestGitRepositoryRules(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_rules(self):
+ repository = self.factory.makeGitRepository()
+ other_repository = self.factory.makeGitRepository()
+ self.factory.makeGitRule(
+ repository=repository, ref_pattern="refs/heads/*")
+ self.factory.makeGitRule(
+ repository=repository, ref_pattern="refs/heads/stable/*")
+ self.factory.makeGitRule(
+ repository=other_repository, ref_pattern="refs/heads/*")
+ self.assertThat(repository.rules, MatchesListwise([
+ MatchesStructure.byEquality(
+ repository=repository,
+ ref_pattern="refs/heads/*"),
+ MatchesStructure.byEquality(
+ repository=repository,
+ ref_pattern="refs/heads/stable/*"),
+ ]))
+
+ def test_addRule_append(self):
+ repository = self.factory.makeGitRepository()
+ initial_rule = self.factory.makeGitRule(
+ repository=repository, ref_pattern="refs/heads/*")
+ with person_logged_in(repository.owner):
+ new_rule = repository.addRule(
+ "refs/heads/stable/*", repository.owner)
+ self.assertEqual(
+ [initial_rule.id, new_rule.id],
+ removeSecurityProxy(repository).rule_order)
+ self.assertEqual([initial_rule, new_rule], repository.rules)
+
+ def test_addRule_insert(self):
+ repository = self.factory.makeGitRepository()
+ initial_rules = [
+ self.factory.makeGitRule(
+ repository=repository, ref_pattern="refs/heads/*"),
+ self.factory.makeGitRule(
+ repository=repository, ref_pattern="refs/heads/protected"),
+ ]
+ with person_logged_in(repository.owner):
+ new_rule = repository.addRule(
+ "refs/heads/stable/*", repository.owner, position=1)
+ self.assertEqual(
+ [initial_rules[0].id, new_rule.id, initial_rules[1].id],
+ removeSecurityProxy(repository).rule_order)
+ self.assertEqual(
+ [initial_rules[0], new_rule, initial_rules[1]], repository.rules)
+
+ def test_grants(self):
+ repository = self.factory.makeGitRepository()
+ other_repository = self.factory.makeGitRepository()
+ rule = self.factory.makeGitRule(repository=repository)
+ other_rule = self.factory.makeGitRule(repository=other_repository)
+ grants = [
+ self.factory.makeGitGrant(
+ rule=rule, grantee=self.factory.makePerson())
+ for _ in range(2)
+ ]
+ self.factory.makeGitGrant(
+ rule=other_rule, grantee=self.factory.makePerson())
+ self.assertContentEqual(grants, repository.grants)
+
+
class TestGitRepositorySet(TestCaseWithFactory):
layer = DatabaseFunctionalLayer
=== added file 'lib/lp/code/model/tests/test_gitrule.py'
--- lib/lp/code/model/tests/test_gitrule.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/model/tests/test_gitrule.py 2018-09-03 17:08:57 +0000
@@ -0,0 +1,93 @@
+# Copyright 2018 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for Git repository access rules."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+
+from storm.store import Store
+from testtools.matchers import (
+ Equals,
+ Is,
+ MatchesSetwise,
+ MatchesStructure,
+ )
+
+from lp.code.enums import GitGranteeType
+from lp.code.interfaces.gitrule import IGitRule
+from lp.services.database.sqlbase import get_transaction_timestamp
+from lp.testing import (
+ TestCaseWithFactory,
+ verifyObject,
+ )
+from lp.testing.layers import DatabaseFunctionalLayer
+
+
+class TestGitRule(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_implements_IGitRule(self):
+ rule = self.factory.makeGitRule()
+ verifyObject(IGitRule, rule)
+
+ def test_properties(self):
+ owner = self.factory.makeTeam()
+ member = self.factory.makePerson(member_of=[owner])
+ repository = self.factory.makeGitRepository(owner=owner)
+ rule = self.factory.makeGitRule(
+ repository=repository, ref_pattern="refs/heads/stable/*",
+ creator=member)
+ now = get_transaction_timestamp(Store.of(rule))
+ self.assertThat(rule, MatchesStructure.byEquality(
+ repository=repository,
+ ref_pattern="refs/heads/stable/*",
+ creator=member,
+ date_created=now,
+ date_last_modified=now))
+
+ def test_repr(self):
+ repository = self.factory.makeGitRepository()
+ rule = self.factory.makeGitRule(repository=repository)
+ self.assertEqual(
+ "<GitRule 'refs/heads/*'> for %r" % repository, repr(rule))
+
+ def test_grants(self):
+ rule = self.factory.makeGitRule()
+ other_rule = self.factory.makeGitRule(
+ repository=rule.repository, ref_pattern="refs/heads/stable/*")
+ grantees = [self.factory.makePerson() for _ in range(2)]
+ self.factory.makeGitGrant(
+ rule=rule, grantee=GitGranteeType.REPOSITORY_OWNER,
+ can_create=True)
+ self.factory.makeGitGrant(
+ rule=rule, grantee=grantees[0], can_push=True)
+ self.factory.makeGitGrant(
+ rule=rule, grantee=grantees[1], can_force_push=True)
+ self.factory.makeGitGrant(
+ rule=other_rule, grantee=grantees[0], can_push=True)
+ self.assertThat(rule.grants, MatchesSetwise(
+ MatchesStructure(
+ rule=Equals(rule),
+ grantee_type=Equals(GitGranteeType.REPOSITORY_OWNER),
+ grantee=Is(None),
+ can_create=Is(True),
+ can_push=Is(False),
+ can_force_push=Is(False)),
+ MatchesStructure(
+ rule=Equals(rule),
+ grantee_type=Equals(GitGranteeType.PERSON),
+ grantee=Equals(grantees[0]),
+ can_create=Is(False),
+ can_push=Is(True),
+ can_force_push=Is(False)),
+ MatchesStructure(
+ rule=Equals(rule),
+ grantee_type=Equals(GitGranteeType.PERSON),
+ grantee=Equals(grantees[1]),
+ can_create=Is(False),
+ # can_force_push implies can_push.
+ can_push=Is(True),
+ can_force_push=Is(True))))
=== modified file 'lib/lp/security.py'
--- lib/lp/security.py 2018-06-15 13:21:14 +0000
+++ lib/lp/security.py 2018-09-03 17:08:57 +0000
@@ -82,11 +82,13 @@
from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
from lp.code.interfaces.diff import IPreviewDiff
from lp.code.interfaces.gitcollection import IGitCollection
+from lp.code.interfaces.gitgrant import IGitGrant
from lp.code.interfaces.gitref import IGitRef
from lp.code.interfaces.gitrepository import (
IGitRepository,
user_has_special_git_repository_access,
)
+from lp.code.interfaces.gitrule import IGitRule
from lp.code.interfaces.sourcepackagerecipe import ISourcePackageRecipe
from lp.code.interfaces.sourcepackagerecipebuild import (
ISourcePackageRecipeBuild,
@@ -2343,6 +2345,42 @@
super(EditGitRef, self).__init__(obj, obj.repository)
+class ViewGitRule(DelegatedAuthorization):
+ """Anyone who can see a Git repository can see its access rules."""
+ permission = 'launchpad.View'
+ usedfor = IGitRule
+
+ def __init__(self, obj):
+ super(ViewGitRule, self).__init__(obj, obj.repository)
+
+
+class EditGitRule(DelegatedAuthorization):
+ """Anyone who can edit a Git repository can edit its access rules."""
+ permission = 'launchpad.Edit'
+ usedfor = IGitRule
+
+ def __init__(self, obj):
+ super(EditGitRule, self).__init__(obj, obj.repository)
+
+
+class ViewGitGrant(DelegatedAuthorization):
+ """Anyone who can see a Git repository can see its access grants."""
+ permission = 'launchpad.View'
+ usedfor = IGitGrant
+
+ def __init__(self, obj):
+ super(ViewGitGrant, self).__init__(obj, obj.repository)
+
+
+class EditGitGrant(DelegatedAuthorization):
+ """Anyone who can edit a Git repository can edit its access grants."""
+ permission = 'launchpad.Edit'
+ usedfor = IGitGrant
+
+ def __init__(self, obj):
+ super(EditGitGrant, self).__init__(obj, obj.repository)
+
+
class AdminDistroSeriesTranslations(AuthorizationBase):
permission = 'launchpad.TranslationsAdmin'
usedfor = IDistroSeries
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2018-08-23 09:30:24 +0000
+++ lib/lp/testing/factory.py 2018-09-03 17:08:57 +0000
@@ -1828,6 +1828,30 @@
path = self.getUniqueString('refs/heads/path').decode('utf-8')
return getUtility(IGitRefRemoteSet).new(repository_url, path)
+ def makeGitRule(self, repository=None, ref_pattern=u"refs/heads/*",
+ creator=None, position=None, **repository_kwargs):
+ """Create a Git repository access rule."""
+ if repository is None:
+ repository = self.makeGitRepository(**repository_kwargs)
+ if creator is None:
+ creator = repository.owner
+ with person_logged_in(creator):
+ return repository.addRule(ref_pattern, creator, position=position)
+
+ def makeGitGrant(self, rule=None, grantee=None, grantor=None,
+ can_create=False, can_push=False, can_force_push=False):
+ """Create a Git repository access grant."""
+ if rule is None:
+ rule = self.makeGitRule()
+ if grantee is None:
+ grantee = self.makePerson()
+ if grantor is None:
+ grantor = rule.repository.owner
+ with person_logged_in(grantor):
+ return rule.addGrant(
+ grantee, grantor, can_create=can_create, can_push=can_push,
+ can_force_push=can_force_push)
+
def makeBug(self, target=None, owner=None, bug_watch_url=None,
information_type=None, date_closed=None, title=None,
date_created=None, description=None, comment=None,
Follow ups