Skip to content

Commit

Permalink
support group permission to codeComponents in gitlab_permission (#4392)
Browse files Browse the repository at this point in the history
  • Loading branch information
mehfuz authored Jun 3, 2024
1 parent 5e50ded commit 9f4c1fa
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 11 deletions.
23 changes: 23 additions & 0 deletions reconcile/gitlab_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
from reconcile.utils import batches
from reconcile.utils.defer import defer
from reconcile.utils.gitlab_api import GitLabApi
from reconcile.utils.unleash import get_feature_toggle_state

QONTRACT_INTEGRATION = "gitlab-permissions"
APP_SRE_GROUP_NAME = "app-sre"
PAGE_SIZE = 100


Expand Down Expand Up @@ -50,6 +52,19 @@ def run(dry_run, thread_pool_size=10, defer=None):
if defer:
defer(gl.cleanup)
repos = queries.get_repos(server=gl.server, exclude_manage_permissions=True)
share_with_group_enabled = get_feature_toggle_state(
"gitlab-permissions-share-with-group",
default=False,
)
if share_with_group_enabled:
share_project_with_group(gl, repos, dry_run)
else:
share_project_with_group_members(gl, repos, thread_pool_size, dry_run)


def share_project_with_group_members(
gl: GitLabApi, repos: list[str], thread_pool_size: int, dry_run: bool
) -> None:
app_sre = gl.get_app_sre_group_users()
results = threaded.run(
get_members_to_add, repos, thread_pool_size, gl=gl, app_sre=app_sre
Expand All @@ -61,6 +76,14 @@ def run(dry_run, thread_pool_size=10, defer=None):
gl.add_project_member(m["repo"], m["user"])


def share_project_with_group(gl: GitLabApi, repos: list[str], dry_run: bool) -> None:
group_id, shared_projects = gl.get_group_id_and_shared_projects(APP_SRE_GROUP_NAME)
shared_project_repos = {project["web_url"] for project in shared_projects}
repos_to_share = set(repos) - shared_project_repos
for repo in repos_to_share:
gl.share_project_with_group(repo_url=repo, group_id=group_id, dry_run=dry_run)


def early_exit_desired_state(*args, **kwargs) -> dict[str, Any]:
instance = queries.get_gitlab_instance()
return {
Expand Down
57 changes: 57 additions & 0 deletions reconcile/test/test_gitlab_permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from unittest.mock import MagicMock, create_autospec

import pytest
from gitlab.v4.objects import CurrentUser, GroupMember
from pytest_mock import MockerFixture

from reconcile import gitlab_permissions
from reconcile.utils.gitlab_api import GitLabApi


@pytest.fixture()
def mocked_queries(mocker: MockerFixture) -> MagicMock:
queries = mocker.patch("reconcile.gitlab_permissions.queries")
queries.get_gitlab_instance.return_value = {}
queries.get_app_interface_settings.return_value = {}
queries.get_repos.return_value = ["https://test-gitlab.com"]
return queries


@pytest.fixture()
def mocked_gl() -> MagicMock:
gl = create_autospec(GitLabApi)
gl.server = "test_server"
gl.user = create_autospec(CurrentUser)
gl.user.username = "test_name"
return gl


def test_run_share_with_members(
mocked_queries: MagicMock, mocker: MockerFixture, mocked_gl: MagicMock
) -> None:
mocker.patch("reconcile.gitlab_permissions.GitLabApi").return_value = mocked_gl
mocked_gl.get_app_sre_group_users.return_value = [
create_autospec(GroupMember, id=123, username="test_name2")
]
mocker.patch(
"reconcile.gitlab_permissions.get_feature_toggle_state"
).return_value = False
mocked_gl.get_project_maintainers.return_value = ["test_name"]

gitlab_permissions.run(False, thread_pool_size=1)
mocked_gl.add_project_member.assert_called_once()


def test_run_share_with_group(
mocked_queries: MagicMock, mocker: MockerFixture, mocked_gl: MagicMock
) -> None:
mocker.patch("reconcile.gitlab_permissions.GitLabApi").return_value = mocked_gl
mocker.patch(
"reconcile.gitlab_permissions.get_feature_toggle_state"
).return_value = True
mocked_gl.get_group_id_and_shared_projects.return_value = (
1234,
[{"web_url": "https://test.com"}],
)
gitlab_permissions.run(False, thread_pool_size=1)
mocked_gl.share_project_with_group.assert_called_once()
70 changes: 69 additions & 1 deletion reconcile/test/utils/test_gitlab_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
ProjectIssueNoteManager,
ProjectLabel,
ProjectLabelManager,
ProjectManager,
ProjectMember,
ProjectMemberManager,
ProjectMergeRequest,
ProjectMergeRequestManager,
ProjectMergeRequestNote,
Expand Down Expand Up @@ -466,5 +469,70 @@ def test_get_group_members(
assert mocked_gitlab_api.get_group_if_exists("group") is group

assert mocked_gitlab_api.get_group_members("group") == [
{"user": "small", "access_level": None}
{"user": "small", "access_level": "owner"}
]


def test_share_project_with_group_positive(
mocked_gitlab_api: GitLabApi,
mocked_gl: Any,
):
projects = create_autospec(ProjectManager)
project = create_autospec(Project)
project.members = create_autospec(ProjectMemberManager)
project.members.all.return_value = [
create_autospec(ProjectMember, id=mocked_gitlab_api.user.id, access_level=40)
]
projects.get.return_value = project
mocked_gl.projects = projects
mocked_gitlab_api.share_project_with_group("test_repo", 1111, False)
project.share.assert_called_once_with(1111, 40)


def test_share_project_with_group_errored(
mocker: MockerFixture,
mocked_gitlab_api: GitLabApi,
mocked_gl: Any,
):
projects = create_autospec(ProjectManager)
project = create_autospec(Project)
project.members = create_autospec(ProjectMemberManager)
project.members.all.return_value = []
projects.get.return_value = project
mocked_gl.projects = projects
mocked_logger = mocker.patch("reconcile.utils.gitlab_api.logging")
mocked_gitlab_api.share_project_with_group("test_repo", 1111, False, "maintainer")
mocked_logger.error.assert_called_once_with(
"%s is not shared with %s as %s",
"test_repo",
mocked_gitlab_api.user.username,
"maintainer",
)


def test_get_group_id_and_shared_projects(mocked_gitlab_api: GitLabApi, mocked_gl: Any):
groups = create_autospec(GroupManager)
groups.get.return_value = create_autospec(
Group,
id=1234,
shared_projects=[
{
"shared_with_groups": [
{
"group_id": 1234,
"group_access_level": 40,
},
{"group_id": 1244, "group_access_level": 50},
],
"web_url": "https://xyz.com",
},
{
"web_url": "https://xyz.com",
"shared_with_groups": [{"group_id": 1234, "group_access_level": 30}],
},
],
)
mocked_gl.groups = groups
id, shared_projects = mocked_gitlab_api.get_group_id_and_shared_projects("test")
assert id == 1234
assert shared_projects[0]["web_url"] == "https://xyz.com"
67 changes: 57 additions & 10 deletions reconcile/utils/gitlab_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@

import gitlab
import urllib3
from gitlab.const import (
DEVELOPER_ACCESS,
GUEST_ACCESS,
MAINTAINER_ACCESS,
OWNER_ACCESS,
REPORTER_ACCESS,
)
from gitlab.v4.objects import (
CurrentUser,
Group,
Expand Down Expand Up @@ -250,6 +257,46 @@ def get_group_if_exists(self, group_name: str) -> Group | None:
except gitlab.exceptions.GitlabGetError:
return None

def share_project_with_group(
self, repo_url: str, group_id: int, dry_run: bool, access: str = "maintainer"
) -> None:
project = self.get_project(repo_url)
if project is None:
return None
access_level = self.get_access_level(access)
# check if we have 'access_level' access so we can add the group with same role.
members = self.get_items(
project.members.all, query_parameters={"user_ids": self.user.id}
)
if not any(
self.user.id == member.id and member.access_level >= access_level
for member in members
):
logging.error(
"%s is not shared with %s as %s",
repo_url,
self.user.username,
access,
)
return None
logging.info(["add_group_as_maintainer", repo_url, group_id])
if not dry_run:
gitlab_request.labels(integration=INTEGRATION_NAME).inc()
project.share(group_id, access_level)

def get_group_id_and_shared_projects(
self, group_name: str
) -> tuple[int, list[dict]]:
gitlab_request.labels(integration=INTEGRATION_NAME).inc()
group = self.gl.groups.get(group_name)
return group.id, [
project
for project in group.shared_projects
for shared_group in project["shared_with_groups"]
if shared_group["group_id"] == group.id
and shared_group["group_access_level"] >= MAINTAINER_ACCESS
]

@staticmethod
def _is_bot_username(username: str) -> bool:
"""crudely checking for the username
Expand Down Expand Up @@ -331,30 +378,30 @@ def change_access(self, group, username, access):

@staticmethod
def get_access_level_string(access_level):
if access_level == gitlab.OWNER_ACCESS:
if access_level == OWNER_ACCESS:
return "owner"
if access_level == gitlab.MAINTAINER_ACCESS:
if access_level == MAINTAINER_ACCESS:
return "maintainer"
if access_level == gitlab.DEVELOPER_ACCESS:
if access_level == DEVELOPER_ACCESS:
return "developer"
if access_level == gitlab.REPORTER_ACCESS:
if access_level == REPORTER_ACCESS:
return "reporter"
if access_level == gitlab.GUEST_ACCESS:
if access_level == GUEST_ACCESS:
return "guest"

@staticmethod
def get_access_level(access):
access = access.lower()
if access == "owner":
return gitlab.OWNER_ACCESS
return OWNER_ACCESS
if access == "maintainer":
return gitlab.MAINTAINER_ACCESS
return MAINTAINER_ACCESS
if access == "developer":
return gitlab.DEVELOPER_ACCESS
return DEVELOPER_ACCESS
if access == "reporter":
return gitlab.REPORTER_ACCESS
return REPORTER_ACCESS
if access == "guest":
return gitlab.GUEST_ACCESS
return GUEST_ACCESS

def get_group_id_and_projects(self, group_name: str) -> tuple[str, list[str]]:
gitlab_request.labels(integration=INTEGRATION_NAME).inc()
Expand Down

0 comments on commit 9f4c1fa

Please sign in to comment.