Repository: python-gitlab/python-gitlab

Issue:
Support /projects/:id/job_token_scope/allowlist
I would like to access the job token allow list through the Gitlab API. 

Docs: 
- [Get a project’s CI/CD job token inbound allowlist](https://docs.gitlab.com/ee/api/project_job_token_scopes.html#get-a-projects-cicd-job-token-inbound-allowlist)
- [Allow access to your project with a job token](https://docs.gitlab.com/ee/ci/jobs/ci_job_token.html#allow-access-to-your-project-with-a-job-token)

API endpoint: 
```
GET/POST/DELETE /projects/:id/job_token_scope/allowlist
```

I currently have the following manager to access the job token scope allow list:

```
class ProjectJobTokenScopeAllowlistManager(
    gitlab.mixins.ListMixin,
    gitlab.mixins.CreateMixin,
    gitlab.mixins.DeleteMixin,
    gitlab.base.RESTManager,
):
    _path = "/projects/{project_id}/job_token_scope/allowlist"
    _obj_cls = gitlab.v4.objects.Project
    _from_parent_attrs = {"project_id": "id"}
    _create_attrs = gitlab.types.RequiredOptional(required=("target_project_id",))
```


Discussion:

Resolving patch:
diff --git a/gitlab/v4/objects/job_token_scope.py b/gitlab/v4/objects/job_token_scope.py
--- a/gitlab/v4/objects/job_token_scope.py
+++ b/gitlab/v4/objects/job_token_scope.py
@@ -2,12 +2,17 @@
 
 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 @@
 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",))


Test patch:
diff --git a/tests/functional/api/test_project_job_token_scope.py b/tests/functional/api/test_project_job_token_scope.py
new file mode 100644
--- /dev/null
+++ b/tests/functional/api/test_project_job_token_scope.py
@@ -0,0 +1,116 @@
+# 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()
diff --git a/tests/unit/objects/test_job_token_scope.py b/tests/unit/objects/test_job_token_scope.py
--- a/tests/unit/objects/test_job_token_scope.py
+++ b/tests/unit/objects/test_job_token_scope.py
@@ -6,12 +6,65 @@
 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