From 67f81f18830188e010cf3edc845f09be7d959d9d Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon
Date: Jul 31 2020 18:47:49 +0000
Subject: Add a collaborator level to projects.
The collaborators are collaborators that are only granted limited access
to the project (for example, one or a few branches in the repository).
They are also provided ticket access.
They are not granted full commit on the entire project.
Signed-off-by: Pierre-Yves Chibon
---
diff --git a/alembic/versions/2b39a728a38f_add_branch_info_to_projects_groups.py b/alembic/versions/2b39a728a38f_add_branch_info_to_projects_groups.py
new file mode 100644
index 0000000..7adbcd9
--- /dev/null
+++ b/alembic/versions/2b39a728a38f_add_branch_info_to_projects_groups.py
@@ -0,0 +1,30 @@
+"""Add branch info to projects_groups
+
+Revision ID: 2b39a728a38f
+Revises: 318a4793b360
+Create Date: 2020-03-26 21:50:45.899760
+
+"""
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '2b39a728a38f'
+down_revision = '318a4793b360'
+
+
+def upgrade():
+ ''' Add the column branches to the table projects_groups.
+ '''
+ op.add_column(
+ 'projects_groups',
+ sa.Column('branches', sa.Text, nullable=True)
+ )
+
+
+def downgrade():
+ ''' Drop the column branches from the table projects_groups.
+ '''
+ op.drop_column('projects_groups', 'branches')
diff --git a/alembic/versions/318a4793b360_add_branch_info_to_project_user.py b/alembic/versions/318a4793b360_add_branch_info_to_project_user.py
new file mode 100644
index 0000000..2b61738
--- /dev/null
+++ b/alembic/versions/318a4793b360_add_branch_info_to_project_user.py
@@ -0,0 +1,30 @@
+"""Add branch info to user_projects
+
+Revision ID: 318a4793b360
+Revises: 060b20d6d6e6
+Create Date: 2020-03-26 21:49:17.632967
+
+"""
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '318a4793b360'
+down_revision = '060b20d6d6e6'
+
+
+def upgrade():
+ ''' Add the column branches to the table user_projects.
+ '''
+ op.add_column(
+ 'user_projects',
+ sa.Column('branches', sa.Text, nullable=True)
+ )
+
+
+def downgrade():
+ ''' Drop the column branches from the table user_projects.
+ '''
+ op.drop_column('user_projects', 'branches')
diff --git a/pagure/flask_app.py b/pagure/flask_app.py
index ceefd5b..efeb564 100644
--- a/pagure/flask_app.py
+++ b/pagure/flask_app.py
@@ -395,6 +395,11 @@ def set_request():
flask.g.repo_obj = pygit2.Repository(flask.g.reponame)
flask.g.repo_admin = pagure.utils.is_repo_admin(flask.g.repo)
flask.g.repo_committer = pagure.utils.is_repo_committer(flask.g.repo)
+ if flask.g.authenticated and not flask.g.repo_committer:
+ flask.g.repo_committer = flask.g.fas_user.username in [
+ u.user.username for u in flask.g.repo.collaborators
+ ]
+
flask.g.repo_user = pagure.utils.is_repo_user(flask.g.repo)
flask.g.branches = sorted(flask.g.repo_obj.listall_branches())
diff --git a/pagure/forms.py b/pagure/forms.py
index 83e6d53..366d8ae 100644
--- a/pagure/forms.py
+++ b/pagure/forms.py
@@ -632,6 +632,10 @@ class AddUserForm(PagureForm):
'Access Level *',
[wtforms.validators.DataRequired()],
)
+ branches = wtforms.StringField(
+ 'Git branches *',
+ [wtforms.validators.Optional()],
+ )
class AddUserToGroupForm(PagureForm):
@@ -666,6 +670,10 @@ class AddGroupForm(PagureForm):
'Access Level *',
[wtforms.validators.DataRequired()],
)
+ branches = wtforms.StringField(
+ 'Git branches *',
+ [wtforms.validators.Optional()],
+ )
class ConfirmationForm(PagureForm):
diff --git a/pagure/internal/__init__.py b/pagure/internal/__init__.py
index 96f1a81..9a65e1e 100644
--- a/pagure/internal/__init__.py
+++ b/pagure/internal/__init__.py
@@ -126,7 +126,7 @@ def lookup_ssh_key():
@PV.route("/ssh/checkaccess/", methods=["POST"])
@internal_access_only
def check_ssh_access():
- """ Determines whether a user has any access to the requested repo. """
+ """ Determines whether a user has read access to the requested repo. """
gitdir = flask.request.form["gitdir"]
remoteuser = flask.request.form["username"]
_auth_log.info(
@@ -192,8 +192,9 @@ def check_ssh_access():
)
return flask.jsonify({"access": False})
- _log.info("Access granted to %s on: %s" % (remoteuser, project.fullname))
-
+ _log.info(
+ "Read access granted to %s on: %s" % (remoteuser, project.fullname)
+ )
return flask.jsonify(
{
"access": True,
diff --git a/pagure/lib/git_auth.py b/pagure/lib/git_auth.py
index 92bb4ea..b354a51 100644
--- a/pagure/lib/git_auth.py
+++ b/pagure/lib/git_auth.py
@@ -27,7 +27,7 @@ import pagure.lib.model_base
import pagure.lib.query
from pagure.config import config as pagure_config
from pagure.lib import model
-from pagure.utils import is_repo_committer, lookup_deploykey
+from pagure.utils import is_repo_collaborator, lookup_deploykey
# logging.config.dictConfig(pagure_config.get('LOGGING') or {'version': 1})
@@ -901,7 +901,9 @@ class PagureGitAuth(GitAuthHelper):
return False
# Determine whether the current user is allowed to push
- is_committer = is_repo_committer(project, username, session)
+ is_committer = is_repo_collaborator(
+ project, refname, username, session
+ )
deploykey = lookup_deploykey(project, username)
if deploykey is not None:
self.info("Deploykey used. Push access: %s" % deploykey.pushaccess)
diff --git a/pagure/lib/model.py b/pagure/lib/model.py
index 9d366e3..9c23143 100644
--- a/pagure/lib/model.py
+++ b/pagure/lib/model.py
@@ -142,7 +142,7 @@ def create_default_status(session, acls=None):
session.rollback()
_log.debug("ACL %s could not be added", acl)
- for access in ["ticket", "commit", "admin"]:
+ for access in ["ticket", "collaborator", "commit", "admin"]:
access_obj = AccessLevels(access=access)
session.add(access_obj)
try:
@@ -443,6 +443,13 @@ class Project(BASE):
viewonly=True,
)
+ collaborators = relation(
+ "ProjectUser",
+ primaryjoin="and_(projects.c.id==user_projects.c.project_id,\
+ user_projects.c.access=='collaborator')",
+ viewonly=True,
+ )
+
groups = relation(
"PagureGroup",
secondary="projects_groups",
@@ -479,6 +486,13 @@ class Project(BASE):
viewonly=True,
)
+ collaborator_groups = relation(
+ "ProjectGroup",
+ primaryjoin="and_(projects.c.id==projects_groups.c.project_id,\
+ projects_groups.c.access=='collaborator')",
+ viewonly=True,
+ )
+
def __repr__(self):
return (
"Project(%s, name:%s, namespace:%s, url:%s, is_fork:%s, "
@@ -905,7 +919,7 @@ class Project(BASE):
:type combine: boolean
"""
- if access not in ["admin", "commit", "ticket"]:
+ if access not in ["admin", "commit", "collaborator", "ticket"]:
raise pagure.exceptions.AccessLevelNotFound(
"The access level does not exist"
)
@@ -915,6 +929,8 @@ class Project(BASE):
return self.admins
elif access == "commit":
return self.committers
+ elif access == "collaborator":
+ return [u.user for u in self.collaborators]
elif access == "ticket":
return self.users
else:
@@ -924,11 +940,20 @@ class Project(BASE):
committers = set(self.committers)
admins = set(self.admins)
return list(committers - admins)
- elif access == "ticket":
+ elif access == "collaborator":
+ admins = set(self.admins)
committers = set(self.committers)
+ return list(
+ set([u.user for u in self.collaborators])
+ - committers
+ - admins
+ )
+ elif access == "ticket":
admins = set(self.admins)
+ committers = set(self.committers)
+ collaborators = set([u.user for u in self.collaborators])
users = set(self.users)
- return list(users - committers - admins)
+ return list(users - collaborators - committers - admins)
def get_project_groups(self, access, combine=True):
""" Returns the list of groups of the project according
@@ -951,7 +976,7 @@ class Project(BASE):
:type combine: boolean
"""
- if access not in ["admin", "commit", "ticket"]:
+ if access not in ["admin", "commit", "collaborator", "ticket"]:
raise pagure.exceptions.AccessLevelNotFound(
"The access level does not exist"
)
@@ -961,6 +986,8 @@ class Project(BASE):
return self.admin_groups
elif access == "commit":
return self.committer_groups
+ elif access == "collaborator":
+ return self.collaborator_groups
elif access == "ticket":
return self.groups
else:
@@ -970,11 +997,18 @@ class Project(BASE):
committers = set(self.committer_groups)
admins = set(self.admin_groups)
return list(committers - admins)
+ elif access == "collaborator":
+ committers = set(self.committer_groups)
+ admins = set(self.admin_groups)
+ return list(
+ set(self.collaborator_groups) - committers - admins
+ )
elif access == "ticket":
committers = set(self.committer_groups)
admins = set(self.admin_groups)
+ collaborators = set(self.collaborator_groups)
groups = set(self.groups)
- return list(groups - committers - admins)
+ return list(groups - collaborators - committers - admins)
@property
def access_users(self):
@@ -989,6 +1023,9 @@ class Project(BASE):
self.get_project_users(access="commit", combine=False),
key=lambda u: u.user,
),
+ "collaborator": sorted(
+ self.get_project_users(access="collaborator", combine=False)
+ ),
"ticket": sorted(
self.get_project_users(access="ticket", combine=False),
key=lambda u: u.user,
@@ -1028,6 +1065,9 @@ class Project(BASE):
self.get_project_groups(access="commit", combine=False),
key=lambda x: x.group_name,
),
+ "collaborator": sorted(
+ self.get_project_groups(access="collaborator", combine=False)
+ ),
"ticket": sorted(
self.get_project_groups(access="ticket", combine=False),
key=lambda x: x.group_name,
@@ -1171,6 +1211,7 @@ class ProjectUser(BASE):
),
nullable=False,
)
+ branches = sa.Column(sa.Text, nullable=True,)
project = relation(
"Project",
@@ -2707,6 +2748,7 @@ class ProjectGroup(BASE):
),
nullable=False,
)
+ branches = sa.Column(sa.Text, nullable=True,)
project = relation(
"Project",
diff --git a/pagure/lib/query.py b/pagure/lib/query.py
index 48a72bf..8ee4df6 100644
--- a/pagure/lib/query.py
+++ b/pagure/lib/query.py
@@ -1098,7 +1098,13 @@ def add_sshkey_to_project_or_user(
def add_user_to_project(
- session, project, new_user, user, access="admin", required_groups=None
+ session,
+ project,
+ new_user,
+ user,
+ access="admin",
+ branches=None,
+ required_groups=None,
):
""" Add a specified user to a specified project with a specified access
"""
@@ -1127,15 +1133,20 @@ def add_user_to_project(
)
users.add(project.user.user)
- if new_user in users:
+ if new_user in users and access != "collaborator":
raise pagure.exceptions.PagureException(
"This user is already listed on this project with the same access"
)
+ # Reset the branches to None if the user isn't a collaborator
+ if access != "collaborator":
+ branches = None
+
# user has some access on project, so update to new access
if new_user_obj in project.users:
access_obj = get_obj_access(session, project, new_user_obj)
access_obj.access = access
+ access_obj.branches = branches
project.date_modified = datetime.datetime.utcnow()
update_read_only_mode(session, project, read_only=True)
session.add(access_obj)
@@ -1149,6 +1160,7 @@ def add_user_to_project(
project=project.to_json(public=True),
new_user=new_user_obj.username,
new_access=access,
+ new_branches=branches,
agent=user_obj.username,
),
)
@@ -1156,7 +1168,10 @@ def add_user_to_project(
return "User access updated"
project_user = model.ProjectUser(
- project_id=project.id, user_id=new_user_obj.id, access=access
+ project_id=project.id,
+ user_id=new_user_obj.id,
+ access=access,
+ branches=branches,
)
project.date_modified = datetime.datetime.utcnow()
session.add(project_user)
@@ -1173,6 +1188,7 @@ def add_user_to_project(
project=project.to_json(public=True),
new_user=new_user_obj.username,
access=access,
+ branches=branches,
agent=user_obj.username,
),
)
@@ -1186,6 +1202,7 @@ def add_group_to_project(
new_group,
user,
access="admin",
+ branches=None,
create=False,
is_admin=False,
):
@@ -1228,15 +1245,20 @@ def add_group_to_project(
]
)
- if new_group in groups:
+ if new_group in groups and access != "collaborator":
raise pagure.exceptions.PagureException(
"This group already has this access on this project"
)
+ # Reset the branches to None if the group isn't a collaborator
+ if access != "collaborator":
+ branches = None
+
# the group already has some access, update to new access
if group_obj in project.groups:
access_obj = get_obj_access(session, project, group_obj)
access_obj.access = access
+ access_obj.branches = branches
session.add(access_obj)
project.date_modified = datetime.datetime.utcnow()
update_read_only_mode(session, project, read_only=True)
@@ -1250,6 +1272,7 @@ def add_group_to_project(
project=project.to_json(public=True),
new_group=group_obj.group_name,
new_access=access,
+ new_branches=branches,
agent=user,
),
)
@@ -1257,7 +1280,10 @@ def add_group_to_project(
return "Group access updated"
project_group = model.ProjectGroup(
- project_id=project.id, group_id=group_obj.id, access=access
+ project_id=project.id,
+ group_id=group_obj.id,
+ access=access,
+ branches=branches,
)
session.add(project_group)
# Make sure we won't have SQLAlchemy error before we continue
@@ -1274,6 +1300,7 @@ def add_group_to_project(
project=project.to_json(public=True),
new_group=group_obj.group_name,
access=access,
+ branches=branches,
agent=user,
),
)
@@ -2380,6 +2407,7 @@ def search_projects(
sqlalchemy.or_(
model.ProjectUser.access == "admin",
model.ProjectUser.access == "commit",
+ model.ProjectUser.access == "collaborator",
),
)
)
@@ -2394,6 +2422,7 @@ def search_projects(
sqlalchemy.or_(
model.ProjectGroup.access == "admin",
model.ProjectGroup.access == "commit",
+ model.ProjectGroup.access == "collaborator",
),
)
)
@@ -2409,6 +2438,7 @@ def search_projects(
sqlalchemy.or_(
model.ProjectGroup.access == "admin",
model.ProjectGroup.access == "commit",
+ model.ProjectGroup.access == "collaborator",
),
)
)
@@ -2455,6 +2485,7 @@ def search_projects(
sqlalchemy.or_(
model.ProjectUser.access == "admin",
model.ProjectUser.access == "commit",
+ model.ProjectUser.access == "collaborator",
),
)
)
@@ -2470,6 +2501,7 @@ def search_projects(
sqlalchemy.or_(
model.ProjectGroup.access == "admin",
model.ProjectGroup.access == "commit",
+ model.ProjectGroup.access == "collaborator",
),
)
)
@@ -2486,6 +2518,7 @@ def search_projects(
sqlalchemy.or_(
model.ProjectGroup.access == "admin",
model.ProjectGroup.access == "commit",
+ model.ProjectGroup.access == "collaborator",
),
)
)
@@ -2591,7 +2624,7 @@ def list_users_projects(
projects = session.query(sqlalchemy.distinct(model.Project.id))
if acls is None:
- acls = ["main admin", "admin", "commit", "ticket"]
+ acls = ["main admin", "admin", "collaborator", "commit", "ticket"]
if username is not None:
diff --git a/pagure/templates/_access_levels_descriptions.html b/pagure/templates/_access_levels_descriptions.html
new file mode 100644
index 0000000..992502e
--- /dev/null
+++ b/pagure/templates/_access_levels_descriptions.html
@@ -0,0 +1,34 @@
+
Access Levels
+
+Ticket: A user or a group with this level of access can only edit metadata
+ of an issue. This includes changing the status of an issue, adding/removing
+ tags from them, adding/removing assignees and every other option which can
+ be accessed when you click "Edit Metadata" button in an issue page. However,
+ this user can not "create" a new tag or "delete" an existing tag because,
+ that would involve access to settings page of the project which this user
+ won't have. It also won't be able to "delete" the issue because, it falls
+ outside of "Edit Metadata".
+
+
+Collaborator: A user or a group with this level of access can do everything what
+ a user/group with ticket access can do + it can commit to some branches in the project.
+ These branches are defined here using their name or a pattern and needs to be comma separated.
+ Some examples:
+
+
master,features/*
+
el*
+
master,f*
+
+
+
+Commit: A user or a group with this level of access can do everything what
+ a user/group with ticket access can do + it can do everything on the project
+ which doesn't include access to settings page. It can "Edit Metadata" of an issue
+ just like a user with ticket access would do, can merge a pull request, can push
+ to the main repository directly, delete an issue, cancel a pull request etc.
+
+
+Admin: The user/group with this access has access to everything on the project.
+ All the "users" of the project that have been added till now are having this access.
+ They can change the settings of the project, add/remove users/groups on the project.
+
- Ticket: A user or a group with this level of access can only edit metadata
- of an issue. This includes changing the status of an issue, adding/removing
- tags from them, adding/removing assignees and every other option which can
- be accessed when you click "Edit Metadata" button in an issue page. However,
- this user can not "create" a new tag or "delete" an existing tag because,
- that would involve access to settings page of the project which this user
- won't have. It also won't be able to "delete" the issue because, it falls
- outside of "Edit Metadata".
-
-
- Commit: A user or a group with this level of access can do everything what
- a user/group with ticket access can do + it can do everything on the project
- which doesn't include access to settings page. It can "Edit Metadata" of an issue
- just like a user with ticket access would do, can merge a pull request, can push
- to the main repository directly, delete an issue, cancel a pull request etc.
-
-
- Admin: The user/group with this access has access to everything on the project.
- All the "users" of the project that have been added till now are having this access.
- They can change the settings of the project, add/remove users/groups on the project.
-
- Ticket: A user or a group with this level of access can only edit metadata
- of an issue. This includes changing the status of an issue, adding/removing
- tags from them, adding/removing assignees and every other option which can
- be accessed when you click "Edit Metadata" button in an issue page. However,
- this user can not "create" a new tag or "delete" an existing tag because,
- that would involve access to settings page of the project which this user
- won't have. It also won't be able to "delete" the issue because, it falls
- outside of "Edit Metadata".
-
-
- Commit: A user or a group with this level of access can do everything what
- a user/group with ticket access can do + it can do everything on the project
- which doesn't include access to settings page. It can "Edit Metadata" of an issue
- just like a user with ticket access would do, can merge a pull request, can push
- to the main repository directly, delete an issue, cancel a pull request etc.
-
-
- Admin: The user/group with this access has access to everything on the project.
- All the "users" of the project that have been added till now are having this access.
- They can change the settings of the project, add/remove users/groups on the project.
-