Unverified Commit 2d1b7499 authored by Tim Knight's avatar Tim Knight Committed by GitHub
Browse files

feat(job_token_scope): support Groups in job token allowlist API (#2816)



* feat(job_token_scope): support job token access allowlist API

Signed-off-by: default avatarTim Knight <tim.knight1@engineering.digital.dwp.gov.uk>
l.dwp.gov.uk>
Co-authored-by: default avatarNejc Habjan <nejc.habjan@siemens.com>
parent c5d0404a
Loading
Loading
Loading
Loading
+50 −0
Original line number Diff line number Diff line
@@ -49,3 +49,53 @@ Refresh the current state of job token scope::
    scope.refresh()
    print(scope.inbound_enabled)
    # False

Get a project's CI/CD job token inbound allowlist::

    allowlist = scope.allowlist.list()

Add a project to the project's inbound allowlist::

    allowed_project = scope.allowlist.create({"target_project_id": 42})

Remove a project from the project's inbound allowlist::

    allowed_project.delete()
    # or directly using a project ID
    scope.allowlist.delete(42)

.. warning::

   Similar to above, the ID attributes you receive from the create and list
   APIs are not consistent (in create() the id is returned as ``source_project_id`` whereas list() returns as ``id``). To safely retrieve the ID of the allowlisted project
   regardless of how the object was created, always use its ``.get_id()`` method.

Using ``.get_id()``::

    resp = allowlist.create({"target_project_id": 2})
    allowlist_id = resp.get_id()

    allowlists = project.allowlist.list()
    for allowlist in allowlists:
      allowlist_id == allowlist.get_id()

Get a project's CI/CD job token inbound groups allowlist::

    allowlist = scope.groups_allowlist.list()

Add a project to the project's inbound groups allowlist::

    allowed_project = scope.groups_allowlist.create({"target_project_id": 42})

Remove a project from the project's inbound agroups llowlist::

    allowed_project.delete()
    # or directly using a Group ID
    scope.groups_allowlist.delete(42)

.. warning::

   Similar to above, the ID attributes you receive from the create and list
   APIs are not consistent. To safely retrieve the ID of the allowlisted group
   regardless of how the object was created, always use its ``.get_id()`` method.
+48 −0
Original line number Diff line number Diff line
@@ -2,12 +2,17 @@ from typing import Any, cast

from gitlab.base import RESTManager, RESTObject
from gitlab.mixins import (
    CreateMixin,
    DeleteMixin,
    GetWithoutIdMixin,
    ListMixin,
    ObjectDeleteMixin,
    RefreshMixin,
    SaveMixin,
    UpdateMethod,
    UpdateMixin,
)
from gitlab.types import RequiredOptional

__all__ = [
    "ProjectJobTokenScope",
@@ -18,6 +23,9 @@ __all__ = [
class ProjectJobTokenScope(RefreshMixin, SaveMixin, RESTObject):
    _id_attr = None

    allowlist: "AllowlistProjectManager"
    groups_allowlist: "AllowlistGroupManager"


class ProjectJobTokenScopeManager(GetWithoutIdMixin, UpdateMixin, RESTManager):
    _path = "/projects/{project_id}/job_token_scope"
@@ -27,3 +35,43 @@ class ProjectJobTokenScopeManager(GetWithoutIdMixin, UpdateMixin, RESTManager):

    def get(self, **kwargs: Any) -> ProjectJobTokenScope:
        return cast(ProjectJobTokenScope, super().get(**kwargs))


class AllowlistProject(ObjectDeleteMixin, RESTObject):
    _id_attr = "target_project_id"  # note: only true for create endpoint

    def get_id(self) -> int:
        """Returns the id of the resource. This override deals with
        the fact that either an `id` or a `target_project_id` attribute
        is returned by the server depending on the endpoint called."""
        target_project_id = cast(int, super().get_id())
        if target_project_id is not None:
            return target_project_id
        return cast(int, self.id)


class AllowlistProjectManager(ListMixin, CreateMixin, DeleteMixin, RESTManager):
    _path = "/projects/{project_id}/job_token_scope/allowlist"
    _obj_cls = AllowlistProject
    _from_parent_attrs = {"project_id": "project_id"}
    _create_attrs = RequiredOptional(required=("target_project_id",))


class AllowlistGroup(ObjectDeleteMixin, RESTObject):
    _id_attr = "target_group_id"  # note: only true for create endpoint

    def get_id(self) -> int:
        """Returns the id of the resource. This override deals with
        the fact that either an `id` or a `target_group_id` attribute
        is returned by the server depending on the endpoint called."""
        target_group_id = cast(int, super().get_id())
        if target_group_id is not None:
            return target_group_id
        return cast(int, self.id)


class AllowlistGroupManager(ListMixin, CreateMixin, DeleteMixin, RESTManager):
    _path = "/projects/{project_id}/job_token_scope/groups_allowlist"
    _obj_cls = AllowlistGroup
    _from_parent_attrs = {"project_id": "project_id"}
    _create_attrs = RequiredOptional(required=("target_group_id",))
+116 −0
Original line number Diff line number Diff line
# https://docs.gitlab.com/ee/ci/jobs/ci_job_token.html#allow-any-project-to-access-your-project
def test_enable_limit_access_to_this_project(gl, project):
    scope = project.job_token_scope.get()

    scope.enabled = True
    scope.save()

    scope.refresh()

    assert scope.inbound_enabled


def test_disable_limit_access_to_this_project(gl, project):
    scope = project.job_token_scope.get()

    scope.enabled = False
    scope.save()

    scope.refresh()

    assert not scope.inbound_enabled


def test_add_project_to_job_token_scope_allowlist(gl, project):
    project_to_add = gl.projects.create({"name": "Ci_Cd_token_add_proj"})

    scope = project.job_token_scope.get()
    resp = scope.allowlist.create({"target_project_id": project_to_add.id})

    assert resp.source_project_id == project.id
    assert resp.target_project_id == project_to_add.id

    project_to_add.delete()


def test_projects_job_token_scope_allowlist_contains_added_project_name(gl, project):
    scope = project.job_token_scope.get()
    project_name = "Ci_Cd_token_named_proj"
    project_to_add = gl.projects.create({"name": project_name})
    scope.allowlist.create({"target_project_id": project_to_add.id})

    scope.refresh()
    assert any(allowed.name == project_name for allowed in scope.allowlist.list())

    project_to_add.delete()


def test_remove_project_by_id_from_projects_job_token_scope_allowlist(gl, project):
    scope = project.job_token_scope.get()

    project_to_add = gl.projects.create({"name": "Ci_Cd_token_remove_proj"})

    scope.allowlist.create({"target_project_id": project_to_add.id})

    scope.refresh()

    scope.allowlist.delete(project_to_add.id)

    scope.refresh()
    assert not any(
        allowed.id == project_to_add.id for allowed in scope.allowlist.list()
    )

    project_to_add.delete()


def test_add_group_to_job_token_scope_allowlist(gl, project):
    group_to_add = gl.groups.create(
        {"name": "add_group", "path": "allowlisted-add-test"}
    )

    scope = project.job_token_scope.get()
    resp = scope.groups_allowlist.create({"target_group_id": group_to_add.id})

    assert resp.source_project_id == project.id
    assert resp.target_group_id == group_to_add.id

    group_to_add.delete()


def test_projects_job_token_scope_groups_allowlist_contains_added_group_name(
    gl, project
):
    scope = project.job_token_scope.get()
    group_name = "list_group"
    group_to_add = gl.groups.create(
        {"name": group_name, "path": "allowlisted-add-and-list-test"}
    )

    scope.groups_allowlist.create({"target_group_id": group_to_add.id})

    scope.refresh()
    assert any(allowed.name == group_name for allowed in scope.groups_allowlist.list())

    group_to_add.delete()


def test_remove_group_by_id_from_projects_job_token_scope_groups_allowlist(gl, project):
    scope = project.job_token_scope.get()

    group_to_add = gl.groups.create(
        {"name": "delete_group", "path": "allowlisted-delete-test"}
    )

    scope.groups_allowlist.create({"target_group_id": group_to_add.id})

    scope.refresh()

    scope.groups_allowlist.delete(group_to_add.id)

    scope.refresh()
    assert not any(
        allowed.name == group_to_add.name for allowed in scope.groups_allowlist.list()
    )

    group_to_add.delete()
+139 −0
Original line number Diff line number Diff line
@@ -6,12 +6,65 @@ import pytest
import responses

from gitlab.v4.objects import ProjectJobTokenScope
from gitlab.v4.objects.job_token_scope import (
    AllowlistGroupManager,
    AllowlistProjectManager,
)

job_token_scope_content = {
    "inbound_enabled": True,
    "outbound_enabled": False,
}

project_allowlist_content = [
    {
        "id": 4,
        "description": "",
        "name": "Diaspora Client",
        "name_with_namespace": "Diaspora / Diaspora Client",
        "path": "diaspora-client",
        "path_with_namespace": "diaspora/diaspora-client",
        "created_at": "2013-09-30T13:46:02Z",
        "default_branch": "main",
        "tag_list": ["example", "disapora client"],
        "topics": ["example", "disapora client"],
        "ssh_url_to_repo": "git@gitlab.example.com:diaspora/diaspora-client.git",
        "http_url_to_repo": "https://gitlab.example.com/diaspora/diaspora-client.git",
        "web_url": "https://gitlab.example.com/diaspora/diaspora-client",
        "avatar_url": "https://gitlab.example.com/uploads/project/avatar/4/uploads/avatar.png",
        "star_count": 0,
        "last_activity_at": "2013-09-30T13:46:02Z",
        "namespace": {
            "id": 2,
            "name": "Diaspora",
            "path": "diaspora",
            "kind": "group",
            "full_path": "diaspora",
            "parent_id": "",
            "avatar_url": "",
            "web_url": "https://gitlab.example.com/diaspora",
        },
    }
]

project_allowlist_created_content = {
    "target_project_id": 2,
    "project_id": 1,
}

groups_allowlist_content = [
    {
        "id": 4,
        "web_url": "https://gitlab.example.com/groups/diaspora/diaspora-group",
        "name": "namegroup",
    }
]

group_allowlist_created_content = {
    "target_group_id": 4,
    "project_id": 1,
}


@pytest.fixture
def resp_get_job_token_scope():
@@ -26,6 +79,58 @@ def resp_get_job_token_scope():
        yield rsps


@pytest.fixture
def resp_get_allowlist():
    with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
        rsps.add(
            method=responses.GET,
            url="http://localhost/api/v4/projects/1/job_token_scope/allowlist",
            json=project_allowlist_content,
            content_type="application/json",
            status=200,
        )
        yield rsps


@pytest.fixture
def resp_add_to_allowlist():
    with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
        rsps.add(
            method=responses.POST,
            url="http://localhost/api/v4/projects/1/job_token_scope/allowlist",
            json=project_allowlist_created_content,
            content_type="application/json",
            status=200,
        )
        yield rsps


@pytest.fixture
def resp_get_groups_allowlist():
    with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
        rsps.add(
            method=responses.GET,
            url="http://localhost/api/v4/projects/1/job_token_scope/groups_allowlist",
            json=groups_allowlist_content,
            content_type="application/json",
            status=200,
        )
        yield rsps


@pytest.fixture
def resp_add_to_groups_allowlist():
    with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
        rsps.add(
            method=responses.POST,
            url="http://localhost/api/v4/projects/1/job_token_scope/groups_allowlist",
            json=group_allowlist_created_content,
            content_type="application/json",
            status=200,
        )
        yield rsps


@pytest.fixture
def resp_patch_job_token_scope():
    with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
@@ -61,3 +166,37 @@ def test_save_job_token_scope(job_token_scope, resp_patch_job_token_scope):

def test_update_job_token_scope(project, resp_patch_job_token_scope):
    project.job_token_scope.update(new_data={"enabled": False})


def test_get_projects_allowlist(job_token_scope, resp_get_allowlist):
    allowlist = job_token_scope.allowlist
    assert isinstance(allowlist, AllowlistProjectManager)

    allowlist_content = allowlist.list()
    assert isinstance(allowlist_content, list)
    assert allowlist_content[0].get_id() == 4


def test_add_project_to_allowlist(job_token_scope, resp_add_to_allowlist):
    allowlist = job_token_scope.allowlist
    assert isinstance(allowlist, AllowlistProjectManager)

    resp = allowlist.create({"target_project_id": 2})
    assert resp.get_id() == 2


def test_get_groups_allowlist(job_token_scope, resp_get_groups_allowlist):
    allowlist = job_token_scope.groups_allowlist
    assert isinstance(allowlist, AllowlistGroupManager)

    allowlist_content = allowlist.list()
    assert isinstance(allowlist_content, list)
    assert allowlist_content[0].get_id() == 4


def test_add_group_to_allowlist(job_token_scope, resp_add_to_groups_allowlist):
    allowlist = job_token_scope.groups_allowlist
    assert isinstance(allowlist, AllowlistGroupManager)

    resp = allowlist.create({"target_group_id": 4})
    assert resp.get_id() == 4