launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #22932
[Merge] lp:~twom/launchpad/branch-permissions-for-gitapi into lp:launchpad
Tom Wardill has proposed merging lp:~twom/launchpad/branch-permissions-for-gitapi into lp:launchpad.
Commit message:
Add GitRuleGrant api to xmlrpc API
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~twom/launchpad/branch-permissions-for-gitapi/+merge/355715
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~twom/launchpad/branch-permissions-for-gitapi into lp:launchpad.
=== added file 'database/schema/patch-2209-85-0.sql'
--- database/schema/patch-2209-85-0.sql 1970-01-01 00:00:00 +0000
+++ database/schema/patch-2209-85-0.sql 2018-09-26 15:35:28 +0000
@@ -0,0 +1,68 @@
+-- Copyright 2018 Canonical Ltd. This software is licensed under the
+-- GNU Affero General Public License version 3 (see the file LICENSE).
+
+SET client_min_messages=ERROR;
+
+CREATE TABLE GitRule (
+ id serial PRIMARY KEY,
+ repository integer NOT NULL REFERENCES gitrepository,
+ position integer NOT NULL,
+ ref_pattern text NOT NULL,
+ creator integer NOT NULL REFERENCES person,
+ date_created timestamp without time zone DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC') NOT NULL,
+ date_last_modified timestamp without time zone DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC') NOT NULL,
+ CONSTRAINT gitrule__repository__position__key UNIQUE (repository, position) DEFERRABLE INITIALLY DEFERRED,
+ CONSTRAINT gitrule__repository__ref_pattern__key UNIQUE (repository, ref_pattern),
+ -- Used by repository_matches_rule constraint on GitRuleGrant.
+ CONSTRAINT gitrule__repository__id__key UNIQUE (repository, id)
+);
+
+COMMENT ON TABLE GitRule IS 'An access rule for a Git repository.';
+COMMENT ON COLUMN GitRule.repository IS 'The repository that this rule is for.';
+COMMENT ON COLUMN GitRule.position IS 'The position of this rule in its repository''s rule order.';
+COMMENT ON COLUMN GitRule.ref_pattern IS 'The pattern of references matched by this rule.';
+COMMENT ON COLUMN GitRule.creator IS 'The user who created this rule.';
+COMMENT ON COLUMN GitRule.date_created IS 'The time when this rule was created.';
+COMMENT ON COLUMN GitRule.date_last_modified IS 'The time when this rule was last modified.';
+
+CREATE TABLE GitRuleGrant (
+ id serial PRIMARY KEY,
+ repository integer NOT NULL REFERENCES gitrepository,
+ rule integer NOT NULL REFERENCES gitrule,
+ grantee_type integer NOT NULL,
+ grantee integer REFERENCES person,
+ can_create boolean DEFAULT false NOT NULL,
+ can_push boolean DEFAULT false NOT NULL,
+ can_force_push boolean DEFAULT false NOT NULL,
+ grantor integer NOT NULL REFERENCES person,
+ date_created timestamp without time zone DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC') NOT NULL,
+ date_last_modified timestamp without time zone DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC') NOT NULL,
+ CONSTRAINT repository_matches_rule FOREIGN KEY (repository, rule) REFERENCES gitrule (repository, id),
+ -- 2 == PERSON
+ CONSTRAINT has_grantee CHECK ((grantee_type = 2) = (grantee IS NOT NULL))
+);
+
+CREATE INDEX gitrulegrant__repository__idx
+ ON GitRuleGrant(repository);
+CREATE UNIQUE INDEX gitrulegrant__rule__grantee_type__key
+ ON GitRuleGrant(rule, grantee_type)
+ -- 2 == PERSON
+ WHERE grantee_type != 2;
+CREATE UNIQUE INDEX gitrulegrant__rule__grantee_type__grantee_key
+ ON GitRuleGrant(rule, grantee_type, grantee)
+ -- 2 == PERSON
+ WHERE grantee_type = 2;
+
+COMMENT ON TABLE GitRuleGrant IS 'An access grant for a Git repository rule.';
+COMMENT ON COLUMN GitRuleGrant.repository IS 'The repository that this grant is for.';
+COMMENT ON COLUMN GitRuleGrant.rule IS 'The rule that this grant is for.';
+COMMENT ON COLUMN GitRuleGrant.grantee_type IS 'The type of entity being granted access.';
+COMMENT ON COLUMN GitRuleGrant.grantee IS 'The person or team being granted access.';
+COMMENT ON COLUMN GitRuleGrant.can_create IS 'Whether creating references is allowed.';
+COMMENT ON COLUMN GitRuleGrant.can_push IS 'Whether pushing references is allowed.';
+COMMENT ON COLUMN GitRuleGrant.can_force_push IS 'Whether force-pushing references is allowed.';
+COMMENT ON COLUMN GitRuleGrant.grantor IS 'The user who created this grant.';
+COMMENT ON COLUMN GitRuleGrant.date_created IS 'The time when this grant was created.';
+COMMENT ON COLUMN GitRuleGrant.date_last_modified IS 'The time when this grant was last modified.';
+
+INSERT INTO LaunchpadDatabaseRevision VALUES (2209, 85, 0);
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2018-09-10 12:45:07 +0000
+++ database/schema/security.cfg 2018-09-26 15:35:28 +0000
@@ -190,6 +190,8 @@
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.gitrulegrant = SELECT, INSERT, UPDATE, DELETE
public.gitsubscription = SELECT, INSERT, UPDATE, DELETE
public.hwdevice = SELECT
public.hwdeviceclass = SELECT, INSERT, DELETE
@@ -2210,6 +2212,8 @@
public.faq = SELECT, UPDATE
public.featureflagchangelogentry = SELECT, UPDATE
public.gitrepository = SELECT, UPDATE
+public.gitrule = SELECT, UPDATE
+public.gitrulegrant = SELECT, UPDATE, DELETE
public.gitsubscription = SELECT, UPDATE, DELETE
public.gpgkey = SELECT, UPDATE
public.hwsubmission = SELECT, UPDATE
=== 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-26 15:35:28 +0000
@@ -919,6 +919,35 @@
<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
+ lp.code.interfaces.gitrule.IGitRuleEditableAttributes" />
+ <require
+ permission="launchpad.Edit"
+ interface="lp.code.interfaces.gitrule.IGitRuleEdit"
+ set_schema="lp.code.interfaces.gitrule.IGitRuleEditableAttributes" />
+ </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.gitrule.GitRuleGrant">
+ <require
+ permission="launchpad.View"
+ interface="lp.code.interfaces.gitrule.IGitRuleGrantView
+ lp.code.interfaces.gitrule.IGitRuleGrantEditableAttributes" />
+ <require
+ permission="launchpad.Edit"
+ interface="lp.code.interfaces.gitrule.IGitRuleGrantEdit"
+ set_schema="lp.code.interfaces.gitrule.IGitRuleGrantEditableAttributes" />
+ </class>
+ <subscriber
+ for="lp.code.interfaces.gitrule.IGitRuleGrant zope.lifecycleevent.interfaces.IObjectModifiedEvent"
+ handler="lp.code.model.gitrule.git_rule_grant_modified"/>
+
<!-- GitCollection -->
<class class="lp.code.model.gitcollection.GenericGitCollection">
@@ -975,6 +1004,9 @@
<adapter factory="lp.code.model.defaultgit.OwnerProjectDefaultGitRepository" />
<adapter factory="lp.code.model.defaultgit.OwnerPackageDefaultGitRepository" />
+ <class class="lp.code.model.gitlookup.GitRuleGrantLookup">
+ <allow interface="lp.code.interfaces.gitlookup.IGitRuleGrantLookup" />
+ </class>
<class class="lp.code.model.gitlookup.GitLookup">
<allow interface="lp.code.interfaces.gitlookup.IGitLookup" />
</class>
@@ -984,6 +1016,11 @@
<allow interface="lp.code.interfaces.gitlookup.IGitLookup" />
</securedutility>
<securedutility
+ class="lp.code.model.gitlookup.GitRuleGrantLookup"
+ provides="lp.code.interfaces.gitlookup.IGitRuleGrantLookup">
+ <allow interface="lp.code.interfaces.gitlookup.IGitRuleGrantLookup" />
+ </securedutility>
+ <securedutility
class="lp.code.model.gitlookup.GitTraverser"
provides="lp.code.interfaces.gitlookup.IGitTraverser">
<allow interface="lp.code.interfaces.gitlookup.IGitTraverser" />
=== 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-26 15:35:28 +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
=== modified file 'lib/lp/code/interfaces/gitapi.py'
--- lib/lp/code/interfaces/gitapi.py 2015-03-31 04:18:22 +0000
+++ lib/lp/code/interfaces/gitapi.py 2018-09-26 15:35:28 +0000
@@ -67,3 +67,9 @@
:returns: An `Unauthorized` fault, as password authentication is
not yet supported.
"""
+
+ def listRefRules(self, repository, user):
+ """Return the list of RefRules for `user` in `repository`
+
+ :returns: A List of rules for the user in the specified repository
+ """
=== modified file 'lib/lp/code/interfaces/gitlookup.py'
--- lib/lp/code/interfaces/gitlookup.py 2015-03-30 14:47:22 +0000
+++ lib/lp/code/interfaces/gitlookup.py 2018-09-26 15:35:28 +0000
@@ -6,6 +6,7 @@
__metaclass__ = type
__all__ = [
'IGitLookup',
+ 'IGitRuleGrantLookup',
'IGitTraversable',
'IGitTraverser',
]
@@ -145,3 +146,14 @@
leading part of a path as a repository, such as external code
browsers.
"""
+
+
+class IGitRuleGrantLookup(Interface):
+ """Utility for looking up a GitRuleGrant by properties"""
+
+ def getByRulesAffectingPerson(repository, grantee_id):
+ """Find all the rules for a repository that affect a Person.
+
+ :param repository: An instance of a GitRepository
+ :param granteed_id: An integer of the id of the Person
+ """
=== 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-26 15:35:28 +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,25 @@
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.
+ """
+
+ def moveRule(rule, position):
+ """Move a rule to a new position in its repository's rule order.
+
+ :param rule: The `IGitRule` to move.
+ :param position: The new position. For example, 0 puts the rule at
+ the start, while `len(repository.rules)` puts the rule at the
+ end.
+ """
+
@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-26 15:35:28 +0000
@@ -0,0 +1,172 @@
+# 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',
+ 'IGitRuleGrant',
+ ]
+
+from lazr.restful.fields import Reference
+from zope.interface import (
+ Attribute,
+ Interface,
+ )
+from zope.schema import (
+ Bool,
+ Choice,
+ Datetime,
+ Int,
+ TextLine,
+ )
+
+from lp import _
+from lp.code.enums import GitGranteeType
+from lp.code.interfaces.gitrepository import IGitRepository
+from lp.services.fields import (
+ PersonChoice,
+ PublicPersonChoice,
+ )
+
+
+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."))
+
+ position = Int(
+ title=_("Position"), required=True, readonly=True,
+ description=_(
+ "The position of this rule in its repository's rule order."))
+
+ creator = PublicPersonChoice(
+ title=_("Creator"), required=True, readonly=True,
+ vocabulary="ValidPerson",
+ 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."))
+
+ grants = Attribute("The access grants for this rule.")
+
+
+class IGitRuleEditableAttributes(Interface):
+ """`IGitRule` attributes that can be edited.
+
+ These attributes need launchpad.View to see, and launchpad.Edit to change.
+ """
+
+ ref_pattern = TextLine(
+ title=_("Pattern"), required=True, readonly=False,
+ description=_("The pattern of references matched by this rule."))
+
+ date_last_modified = Datetime(
+ title=_("Date last modified"), required=True, readonly=True,
+ description=_("The time when this rule was last modified."))
+
+
+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, IGitRuleEditableAttributes, IGitRuleEdit):
+ """An access rule for a Git repository."""
+
+
+class IGitRuleGrantView(Interface):
+ """`IGitRuleGrant` 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 = PublicPersonChoice(
+ title=_("Grantor"), required=True, readonly=True,
+ vocabulary="ValidPerson",
+ description=_("The user who created this grant."))
+
+ grantee_type = Choice(
+ title=_("Grantee type"), required=True, readonly=True,
+ vocabulary=GitGranteeType,
+ description=_("The type of grantee for this grant."))
+
+ grantee = PersonChoice(
+ title=_("Grantee"), required=False, readonly=True,
+ vocabulary="ValidPersonOrTeam",
+ description=_("The person being granted access."))
+
+ date_created = Datetime(
+ title=_("Date created"), required=True, readonly=True,
+ description=_("The time when this grant was created."))
+
+
+class IGitRuleGrantEditableAttributes(Interface):
+ """`IGitRuleGrant` attributes that can be edited.
+
+ These attributes need launchpad.View to see, and launchpad.Edit to change.
+ """
+
+ 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 IGitRuleGrantEdit(Interface):
+ """`IGitRuleGrant` attributes that require launchpad.Edit."""
+
+ def destroySelf():
+ """Delete this access grant."""
+
+
+class IGitRuleGrant(IGitRuleGrantView, IGitRuleGrantEditableAttributes,
+ IGitRuleGrantEdit):
+ """An access grant for a Git repository rule."""
=== modified file 'lib/lp/code/model/gitlookup.py'
--- lib/lp/code/model/gitlookup.py 2018-07-23 10:28:33 +0000
+++ lib/lp/code/model/gitlookup.py 2018-09-26 15:35:28 +0000
@@ -27,6 +27,7 @@
)
from lp.code.interfaces.gitlookup import (
IGitLookup,
+ IGitRuleGrantLookup,
IGitTraversable,
IGitTraverser,
)
@@ -34,6 +35,7 @@
from lp.code.interfaces.gitrepository import IGitRepositorySet
from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
from lp.code.model.gitrepository import GitRepository
+from lp.code.model.gitrule import GitRuleGrant
from lp.registry.errors import NoSuchSourcePackageName
from lp.registry.interfaces.distribution import IDistribution
from lp.registry.interfaces.distributionsourcepackage import (
@@ -372,3 +374,14 @@
if trailing:
trailing_segments.insert(0, trailing)
return repository, "/".join(trailing_segments)
+
+
+@implementer(IGitRuleGrantLookup)
+class GitRuleGrantLookup:
+
+ def getByRulesAffectingPerson(self, repository, grantee):
+ grants = IStore(GitRuleGrant).find(
+ GitRuleGrant,
+ GitRuleGrant.repository == repository)
+ grants = [grant for grant in grants if grantee.inTeam(grant.grantee)]
+ return grants
=== modified file 'lib/lp/code/model/gitrepository.py'
--- lib/lp/code/model/gitrepository.py 2018-09-06 14:25:46 +0000
+++ lib/lp/code/model/gitrepository.py 2018-09-26 15:35:28 +0000
@@ -41,6 +41,7 @@
Bool,
DateTime,
Int,
+ List,
Reference,
Unicode,
)
@@ -112,6 +113,10 @@
GitRef,
GitRefDefault,
)
+from lp.code.model.gitrule import (
+ GitRule,
+ GitRuleGrant,
+ )
from lp.code.model.gitsubscription import GitSubscription
from lp.registry.enums import PersonVisibility
from lp.registry.errors import CannotChangeInformationType
@@ -1121,6 +1126,61 @@
def code_import(self):
return getUtility(ICodeImportSet).getByGitRepository(self)
+ @property
+ def rules(self):
+ """See `IGitRepository`."""
+ return Store.of(self).find(
+ GitRule, GitRule.repository == self).order_by(GitRule.position)
+
+ def _syncRulePositions(self, rules):
+ """Synchronise rule positions with their order in a provided list.
+
+ :param rules: A sequence of `IGitRule`s in the desired order.
+ """
+ # This approach requires fetching all this repository's rules, which
+ # is potentially more work than necessary. However, it has the
+ # benefit of being simple, and because it ensures the correct
+ # position of all rules it tends to be self-correcting.
+ for position, rule in enumerate(rules):
+ if rule.repository != self:
+ raise AssertionError("%r does not belong to %r" % (rule, self))
+ if rule.position != position:
+ removeSecurityProxy(rule).position = position
+
+ def addRule(self, ref_pattern, creator, position=None):
+ """See `IGitRepository`."""
+ rules = list(self.rules)
+ rule = GitRule(
+ repository=self,
+ # -1 isn't a valid position, but _syncRulePositions will correct
+ # it in a moment.
+ position=position if position is not None else -1,
+ ref_pattern=ref_pattern, creator=creator, date_created=DEFAULT)
+ if position is None:
+ rules.append(rule)
+ else:
+ rules.insert(position, rule)
+ self._syncRulePositions(rules)
+ return rule
+
+ def moveRule(self, rule, position):
+ """See `IGitRepository`."""
+ if rule.repository != self:
+ raise ValueError("%r does not belong to %r" % (rule, self))
+ if position < 0:
+ raise ValueError("Negative positions are not supported")
+ if position != rule.position:
+ rules = list(self.rules)
+ rules.remove(rule)
+ rules.insert(position, rule)
+ self._syncRulePositions(rules)
+
+ @property
+ def grants(self):
+ """See `IGitRepository`."""
+ return Store.of(self).find(
+ GitRuleGrant, GitRuleGrant.repository_id == self.id)
+
def canBeDeleted(self):
"""See `IGitRepository`."""
# Can't delete if the repository is associated with anything.
@@ -1252,6 +1312,8 @@
self._deleteRepositorySubscriptions()
self._deleteJobs()
getUtility(IWebhookSet).delete(self.webhooks)
+ self.grants.remove()
+ self.rules.remove()
# Now destroy the repository.
repository_name = self.unique_name
=== 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-26 15:35:28 +0000
@@ -0,0 +1,199 @@
+# 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',
+ 'GitRuleGrant',
+ ]
+
+from lazr.enum import DBItem
+import pytz
+from storm.locals import (
+ Bool,
+ DateTime,
+ Int,
+ Reference,
+ Store,
+ Unicode,
+ )
+from zope.interface import implementer
+from zope.security.proxy import removeSecurityProxy
+
+from lp.code.enums import GitGranteeType
+from lp.code.interfaces.gitrule import (
+ IGitRule,
+ IGitRuleGrant,
+ )
+from lp.registry.interfaces.person import (
+ validate_person,
+ validate_public_person,
+ )
+from lp.services.database.constants import (
+ DEFAULT,
+ UTC_NOW,
+ )
+from lp.services.database.enumcol import DBEnum
+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')
+
+ position = Int(name='position', allow_none=False)
+
+ ref_pattern = Unicode(name='ref_pattern', allow_none=False)
+
+ creator_id = Int(
+ name='creator', allow_none=False, validator=validate_public_person)
+ 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, position, ref_pattern, creator,
+ date_created):
+ super(GitRule, self).__init__()
+ self.repository = repository
+ self.position = position
+ 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(
+ GitRuleGrant, GitRuleGrant.rule_id == self.id)
+
+ def addGrant(self, grantee, grantor, can_create=False, can_push=False,
+ can_force_push=False):
+ """See `IGitRule`."""
+ return GitRuleGrant(
+ 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`."""
+ for grant in self.grants:
+ grant.destroySelf()
+ rules = list(self.repository.rules)
+ Store.of(self).remove(self)
+ rules.remove(self)
+ removeSecurityProxy(self.repository)._syncRulePositions(rules)
+
+
+def git_rule_grant_modified(grant, event):
+ """Update date_last_modified when a GitRuleGrant 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(IGitRuleGrant)
+class GitRuleGrant(StormBase):
+ """See `IGitRuleGrant`."""
+
+ __storm_table__ = 'GitRuleGrant'
+
+ 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 = DBEnum(
+ name='grantee_type', enum=GitGranteeType, allow_none=False)
+
+ grantee_id = Int(
+ name='grantee', allow_none=True, validator=validate_person)
+ 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, validator=validate_public_person)
+ 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
+ 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 "<GitRuleGrant [%s] to %s> for %r" % (
+ ", ".join(permissions), grantee_name, self.rule)
+
+ def destroySelf(self):
+ """See `IGitRuleGrant`."""
+ Store.of(self).remove(self)
=== 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-26 15:35:28 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015-2017 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Tests for Git repositories."""
@@ -23,6 +23,7 @@
from testtools.matchers import (
EndsWith,
LessThan,
+ MatchesListwise,
MatchesSetwise,
MatchesStructure,
)
@@ -537,6 +538,14 @@
transaction.commit()
self.assertRaises(LostObjectError, getattr, webhook, 'target')
+ def test_related_rules_and_grants_deleted(self):
+ rule = self.factory.makeGitRule(repository=self.repository)
+ grant = self.factory.makeGitRuleGrant(rule=rule)
+ self.repository.destroySelf()
+ transaction.commit()
+ self.assertRaises(LostObjectError, getattr, grant, 'rule')
+ self.assertRaises(LostObjectError, getattr, rule, 'repository')
+
class TestGitRepositoryDeletionConsequences(TestCaseWithFactory):
"""Test determination and application of repository deletion
@@ -2179,6 +2188,98 @@
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(list(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/*")
+ self.assertEqual(0, initial_rule.position)
+ with person_logged_in(repository.owner):
+ new_rule = repository.addRule(
+ "refs/heads/stable/*", repository.owner)
+ self.assertEqual(1, new_rule.position)
+ self.assertEqual([initial_rule, new_rule], list(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"),
+ self.factory.makeGitRule(
+ repository=repository, ref_pattern="refs/heads/another"),
+ ]
+ self.assertEqual([0, 1, 2], [rule.position for rule in initial_rules])
+ with person_logged_in(repository.owner):
+ new_rule = repository.addRule(
+ "refs/heads/stable/*", repository.owner, position=1)
+ self.assertEqual(1, new_rule.position)
+ self.assertEqual([0, 2, 3], [rule.position for rule in initial_rules])
+ self.assertEqual(
+ [initial_rules[0], new_rule, initial_rules[1], initial_rules[2]],
+ list(repository.rules))
+
+ def test_moveRule(self):
+ repository = self.factory.makeGitRepository()
+ rules = [
+ self.factory.makeGitRule(
+ repository=repository,
+ ref_pattern=self.factory.getUniqueUnicode(
+ prefix="refs/heads/"))
+ for _ in range(5)]
+ with person_logged_in(repository.owner):
+ self.assertEqual(rules, list(repository.rules))
+ repository.moveRule(rules[0], 4)
+ self.assertEqual(rules[1:] + [rules[0]], list(repository.rules))
+ repository.moveRule(rules[0], 0)
+ self.assertEqual(rules, list(repository.rules))
+ repository.moveRule(rules[2], 1)
+ self.assertEqual(
+ [rules[0], rules[2], rules[1], rules[3], rules[4]],
+ list(repository.rules))
+
+ def test_moveRule_non_negative(self):
+ rule = self.factory.makeGitRule()
+ with person_logged_in(rule.repository.owner):
+ self.assertRaises(ValueError, rule.repository.moveRule, rule, -1)
+
+ 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.makeGitRuleGrant(
+ rule=rule, grantee=self.factory.makePerson())
+ for _ in range(2)
+ ]
+ self.factory.makeGitRuleGrant(
+ 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-26 15:35:28 +0000
@@ -0,0 +1,200 @@
+# 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,
+ IGitRuleGrant,
+ )
+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 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.makeGitRuleGrant(
+ rule=rule, grantee=GitGranteeType.REPOSITORY_OWNER,
+ can_create=True)
+ self.factory.makeGitRuleGrant(
+ rule=rule, grantee=grantees[0], can_push=True)
+ self.factory.makeGitRuleGrant(
+ rule=rule, grantee=grantees[1], can_force_push=True)
+ self.factory.makeGitRuleGrant(
+ 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_push=Is(False),
+ can_force_push=Is(True))))
+
+ def test_destroySelf(self):
+ repository = self.factory.makeGitRepository()
+ rules = [
+ self.factory.makeGitRule(
+ repository=repository,
+ ref_pattern=self.factory.getUniqueUnicode(
+ prefix="refs/heads/"))
+ for _ in range(4)]
+ self.assertEqual([0, 1, 2, 3], [rule.position for rule in rules])
+ self.assertEqual(rules, list(repository.rules))
+ with person_logged_in(repository.owner):
+ rules[1].destroySelf()
+ del rules[1]
+ self.assertEqual([0, 1, 2], [rule.position for rule in rules])
+ self.assertEqual(rules, list(repository.rules))
+
+ def test_destroySelf_removes_grants(self):
+ repository = self.factory.makeGitRepository()
+ rule = self.factory.makeGitRule(repository=repository)
+ grant = self.factory.makeGitRuleGrant(rule=rule)
+ self.assertEqual([grant], list(repository.grants))
+ with person_logged_in(repository.owner):
+ rule.destroySelf()
+ self.assertEqual([], list(repository.grants))
+
+
+class TestGitRuleGrant(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_implements_IGitRuleGrant(self):
+ grant = self.factory.makeGitRuleGrant()
+ verifyObject(IGitRuleGrant, 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.makeGitRuleGrant(
+ 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_push=Is(False),
+ 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.makeGitRuleGrant(
+ 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.makeGitRuleGrant(
+ rule=rule, grantee=GitGranteeType.REPOSITORY_OWNER,
+ can_create=True, can_push=True)
+ self.assertEqual(
+ "<GitRuleGrant [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.makeGitRuleGrant(
+ rule=rule, grantee=grantee, can_push=True)
+ self.assertEqual(
+ "<GitRuleGrant [push] to ~%s> for %r" % (grantee.name, rule),
+ repr(grant))
+
+ def test_destroySelf(self):
+ rule = self.factory.makeGitRule()
+ grants = [
+ self.factory.makeGitRuleGrant(
+ rule=rule, grantee=GitGranteeType.REPOSITORY_OWNER,
+ can_create=True),
+ self.factory.makeGitRuleGrant(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/xmlrpc/git.py'
--- lib/lp/code/xmlrpc/git.py 2018-08-28 13:58:37 +0000
+++ lib/lp/code/xmlrpc/git.py 2018-09-26 15:35:28 +0000
@@ -40,6 +40,7 @@
from lp.code.interfaces.gitjob import IGitRefScanJobSource
from lp.code.interfaces.gitlookup import (
IGitLookup,
+ IGitRuleGrantLookup,
IGitTraverser,
)
from lp.code.interfaces.gitnamespace import (
@@ -325,3 +326,45 @@
else:
# Only macaroons are supported for password authentication.
return faults.Unauthorized()
+
+ def _isRepositoryOwner(self, requester, repository):
+ try:
+ return requester.inTeam(repository.owner)
+ except Unauthorized:
+ return False
+
+ def _listRefRules(self, requester, translated_path):
+ repository = getUtility(IGitLookup).getByHostingPath(translated_path)
+ grants = getUtility(IGitRuleGrantLookup).getByRulesAffectingPerson(
+ repository, requester)
+
+ lines = []
+ for grant in grants:
+ permissions = []
+ if grant.can_create:
+ permissions.append("create")
+ if grant.can_push:
+ permissions.append("push")
+ if grant.can_force_push:
+ permissions.append("force-push")
+ lines.append(
+ {'ref_pattern': grant.rule.ref_pattern,
+ 'permissions': permissions})
+
+ if self._isRepositoryOwner(requester, repository):
+ lines.append({
+ 'ref_pattern': '*',
+ 'permissions': ['create', 'push', 'force-push']})
+ return lines
+
+ def listRefRules(self, translated_path, auth_params):
+ """See `IGitAPI`"""
+ requester_id = auth_params.get("uid")
+ if requester_id is None:
+ requester_id = LAUNCHPAD_ANONYMOUS
+
+ return run_with_login(
+ requester_id,
+ self._listRefRules,
+ translated_path,
+ )
=== modified file 'lib/lp/code/xmlrpc/tests/test_git.py'
--- lib/lp/code/xmlrpc/tests/test_git.py 2018-08-28 14:07:38 +0000
+++ lib/lp/code/xmlrpc/tests/test_git.py 2018-09-26 15:35:28 +0000
@@ -260,6 +260,125 @@
self.assertEqual(
initial_count, getUtility(IAllGitRepositories).count())
+ def test_listRefRules(self):
+ # Test that GitGrantRule (ref rule) can be retrieved for a user
+ requester = self.factory.makePerson()
+ repository = removeSecurityProxy(
+ self.factory.makeGitRepository(
+ owner=requester, information_type=InformationType.USERDATA))
+
+ rule = self.factory.makeGitRule(repository)
+ self.factory.makeGitRuleGrant(
+ rule=rule, grantee=requester, can_push=True, can_create=True)
+
+ results = self.git_api.listRefRules(
+ repository.getInternalPath(),
+ {'uid': requester.id})
+ self.assertEqual(len(results), 2)
+ self.assertEqual(results[0]['ref_pattern'], 'refs/heads/*')
+ self.assertEqual(results[0]['permissions'], ['create', 'push'])
+
+ def test_listRefRules_no_grants(self):
+ # User that has no grants and is not the owner
+ requester = self.factory.makePerson()
+ owner = self.factory.makePerson()
+ repository = removeSecurityProxy(
+ self.factory.makeGitRepository(
+ owner=owner, information_type=InformationType.USERDATA))
+
+ rule = self.factory.makeGitRule(repository)
+ self.factory.makeGitRuleGrant(
+ rule=rule, grantee=owner, can_push=True, can_create=True)
+
+ results = self.git_api.listRefRules(
+ repository.getInternalPath(),
+ {'uid': requester.id})
+ self.assertEqual(len(results), 0)
+
+ def test_listRefRules_owner_has_default(self):
+ owner = self.factory.makePerson()
+ repository = removeSecurityProxy(
+ self.factory.makeGitRepository(
+ owner=owner, information_type=InformationType.USERDATA))
+
+ rule = self.factory.makeGitRule(
+ repository=repository, ref_pattern=u'refs/heads/master')
+ self.factory.makeGitRuleGrant(
+ rule=rule, grantee=owner, can_push=True, can_create=True)
+
+ results = self.git_api.listRefRules(
+ repository.getInternalPath(),
+ {'uid': owner.id})
+ self.assertEqual(len(results), 2)
+ # Default grant should be last in pattern
+ self.assertEqual(results[-1]['ref_pattern'], '*')
+
+ def test_listRefRules_owner_is_team(self):
+ member = self.factory.makePerson()
+ owner = self.factory.makeTeam(members=[member])
+ repository = removeSecurityProxy(
+ self.factory.makeGitRepository(
+ owner=owner, information_type=InformationType.USERDATA))
+
+ results = self.git_api.listRefRules(
+ repository.getInternalPath(),
+ {'uid': member.id})
+
+ # Should have default grant as member of owning team
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[-1]['ref_pattern'], '*')
+
+ def test_listRefRules_owner_is_team_with_grants(self):
+ member = self.factory.makePerson()
+ owner = self.factory.makeTeam(members=[member])
+ repository = removeSecurityProxy(
+ self.factory.makeGitRepository(
+ owner=owner, information_type=InformationType.USERDATA))
+
+ rule = self.factory.makeGitRule(
+ repository=repository, ref_pattern=u'refs/heads/master')
+ self.factory.makeGitRuleGrant(
+ rule=rule, grantee=owner, can_push=True, can_create=True)
+
+ results = self.git_api.listRefRules(
+ repository.getInternalPath(),
+ {'uid': member.id})
+
+ # Should have default grant as member of owning team
+ self.assertEqual(len(results), 2)
+ self.assertEqual(results[-1]['ref_pattern'], '*')
+
+ def test_listRefRules_owner_is_team_with_grants_to_person(self):
+ member = self.factory.makePerson()
+ other_member = self.factory.makePerson()
+ owner = self.factory.makeTeam(members=[member, other_member])
+ repository = removeSecurityProxy(
+ self.factory.makeGitRepository(
+ owner=owner, information_type=InformationType.USERDATA))
+
+ rule = self.factory.makeGitRule(
+ repository=repository, ref_pattern=u'refs/heads/master')
+ self.factory.makeGitRuleGrant(
+ rule=rule, grantee=owner, can_push=True, can_create=True)
+
+ rule = self.factory.makeGitRule(
+ repository=repository, ref_pattern=u'refs/heads/tags')
+ self.factory.makeGitRuleGrant(
+ rule=rule, grantee=member, can_create=True)
+
+ # This should not appear
+ self.factory.makeGitRuleGrant(
+ rule=rule, grantee=other_member, can_push=True)
+
+ results = self.git_api.listRefRules(
+ repository.getInternalPath(),
+ {'uid': member.id})
+
+ # Should have default grant as member of owning team
+ self.assertEqual(len(results), 3)
+ self.assertEqual(results[-1]['ref_pattern'], '*')
+ tags_rule = results[1]
+ self.assertEqual(tags_rule['permissions'], ['create'])
class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory):
"""Tests for the implementation of `IGitAPI`."""
=== modified file 'lib/lp/registry/personmerge.py'
--- lib/lp/registry/personmerge.py 2015-09-16 13:30:33 +0000
+++ lib/lp/registry/personmerge.py 2018-09-26 15:35:28 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Person/team merger implementation."""
@@ -141,6 +141,25 @@
''' % vars())
+def _mergeGitRuleGrant(cur, from_id, to_id):
+ # Update only the GitRuleGrants that will not conflict.
+ cur.execute('''
+ UPDATE GitRuleGrant
+ SET grantee=%(to_id)d
+ WHERE
+ grantee = %(from_id)d
+ AND rule NOT IN (
+ SELECT rule
+ FROM GitRuleGrant
+ WHERE grantee = %(to_id)d
+ )
+ ''' % vars())
+ # and delete those left over.
+ cur.execute('''
+ DELETE FROM GitRuleGrant WHERE grantee = %(from_id)d
+ ''' % vars())
+
+
def _mergeBranches(from_person, to_person):
# This shouldn't use removeSecurityProxy.
branches = getUtility(IBranchCollection).ownedBy(from_person)
@@ -771,8 +790,10 @@
_mergeAccessArtifactGrant(cur, from_id, to_id)
_mergeAccessPolicyGrant(cur, from_id, to_id)
+ _mergeGitRuleGrant(cur, from_id, to_id)
skip.append(('accessartifactgrant', 'grantee'))
skip.append(('accesspolicygrant', 'grantee'))
+ skip.append(('gitrulegrant', 'grantee'))
# Update the Branches that will not conflict, and fudge the names of
# ones that *do* conflict.
=== modified file 'lib/lp/registry/tests/test_personmerge.py'
--- lib/lp/registry/tests/test_personmerge.py 2016-06-28 21:10:18 +0000
+++ lib/lp/registry/tests/test_personmerge.py 2018-09-26 15:35:28 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Tests for merge_people."""
@@ -494,6 +494,41 @@
grantee=person,
date_created=person_grant_date))
+ def test_merge_gitrulegrants(self):
+ # GitRuleGrants are transferred from the duplicate.
+ rule = self.factory.makeGitRule()
+ person = self.factory.makePerson()
+ grant = self.factory.makeGitRuleGrant(rule=rule)
+ self._do_premerge(grant.grantee, person)
+
+ self.assertEqual(grant.grantee, rule.grants.one().grantee)
+ with person_logged_in(person):
+ self._do_merge(grant.grantee, person)
+ self.assertEqual(person, rule.grants.one().grantee)
+
+ def test_merge_gitrulegrants_conflicts(self):
+ # Conflicting GitRuleGrants are deleted.
+ rule = self.factory.makeGitRule()
+
+ person = self.factory.makePerson()
+ person_grant = self.factory.makeGitRuleGrant(rule=rule, grantee=person)
+ person_grant_date = person_grant.date_created
+
+ duplicate = self.factory.makePerson()
+ self.factory.makeGitRuleGrant(rule=rule, grantee=duplicate)
+
+ self._do_premerge(duplicate, person)
+ with person_logged_in(person):
+ self._do_merge(duplicate, person)
+
+ # Only one grant for the rule exists: the retained person's.
+ self.assertThat(
+ rule.grants.one(),
+ MatchesStructure.byEquality(
+ rule=rule,
+ grantee=person,
+ date_created=person_grant_date))
+
def test_mergeAsync(self):
# mergeAsync() creates a new `PersonMergeJob`.
from_person = self.factory.makePerson()
=== modified file 'lib/lp/security.py'
--- lib/lp/security.py 2018-09-15 11:38:45 +0000
+++ lib/lp/security.py 2018-09-26 15:35:28 +0000
@@ -87,6 +87,10 @@
IGitRepository,
user_has_special_git_repository_access,
)
+from lp.code.interfaces.gitrule import (
+ IGitRule,
+ IGitRuleGrant,
+ )
from lp.code.interfaces.sourcepackagerecipe import ISourcePackageRecipe
from lp.code.interfaces.sourcepackagerecipebuild import (
ISourcePackageRecipeBuild,
@@ -2343,6 +2347,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 ViewGitRuleGrant(DelegatedAuthorization):
+ """Anyone who can see a Git repository can see its access grants."""
+ permission = 'launchpad.View'
+ usedfor = IGitRuleGrant
+
+ def __init__(self, obj):
+ super(ViewGitRuleGrant, self).__init__(obj, obj.repository)
+
+
+class EditGitRuleGrant(DelegatedAuthorization):
+ """Anyone who can edit a Git repository can edit its access grants."""
+ permission = 'launchpad.Edit'
+ usedfor = IGitRuleGrant
+
+ def __init__(self, obj):
+ super(EditGitRuleGrant, 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-09-13 15:21:05 +0000
+++ lib/lp/testing/factory.py 2018-09-26 15:35:28 +0000
@@ -1828,6 +1828,31 @@
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 makeGitRuleGrant(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,
=== modified file 'scripts/close-account.py'
--- scripts/close-account.py 2018-05-08 13:44:21 +0000
+++ scripts/close-account.py 2018-09-26 15:35:28 +0000
@@ -1,6 +1,6 @@
#!/usr/bin/python -S
#
-# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Remove personal details of a user from the database, leaving a stub."""
@@ -162,6 +162,9 @@
# Pending items in queues
('POExportRequest', 'person'),
+
+ # Access grants
+ ('GitRuleGrant', 'grantee'),
]
for table, person_id_column in removals:
table_notification(table)
Follow ups