Skip to content

Commit 3243735

Browse files
fix: cli: url-encode path components of the URL
In the CLI we need to make sure the components put into the path portion of the URL are url-encoded. Otherwise they will be interpreted as part of the path. For example can specify the project ID as a path, but in the URL it must be url-encoded or it doesn't work. Also stop adding the components of the path as query parameters in the URL. Closes: #783
1 parent c4d074f commit 3243735

File tree

2 files changed

+82
-2
lines changed

2 files changed

+82
-2
lines changed

gitlab/v4/cli.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def __init__(
3939
self.action = action.lower()
4040
self.gl = gl
4141
self.args = args
42+
self.parent_args: Dict[str, Any] = {}
4243
self.mgr_cls: Union[
4344
Type[gitlab.mixins.CreateMixin],
4445
Type[gitlab.mixins.DeleteMixin],
@@ -53,7 +54,10 @@ def __init__(
5354
# the class _path attribute, and replace the value with the result.
5455
if TYPE_CHECKING:
5556
assert self.mgr_cls._path is not None
56-
self.mgr_cls._path = self.mgr_cls._path.format(**self.args)
57+
58+
self._process_from_parent_attributes()
59+
60+
self.mgr_cls._path = self.mgr_cls._path.format(**self.parent_args)
5761
self.mgr = self.mgr_cls(gl)
5862

5963
if self.mgr_cls._types:
@@ -63,6 +67,17 @@ def __init__(
6367
obj.set_from_cli(self.args[attr_name])
6468
self.args[attr_name] = obj.get()
6569

70+
def _process_from_parent_attributes(self) -> None:
71+
"""Items in the path need to be url-encoded. There is a 1:1 mapping from
72+
mgr_cls._from_parent_attrs <--> mgr_cls._path. Those values must be url-encoded
73+
as they may contain a slash '/'."""
74+
for key in self.mgr_cls._from_parent_attrs:
75+
if key in self.args:
76+
self.parent_args[key] = gitlab.utils.clean_str_id(self.args[key])
77+
# If we don't delete it then it will be added to the URL as a
78+
# query-string
79+
del self.args[key]
80+
6681
def __call__(self) -> Any:
6782
# Check for a method that matches object + action
6883
method = f"do_{self.what}_{self.action}"
@@ -85,7 +100,7 @@ def do_custom(self) -> Any:
85100
data = {}
86101
if self.mgr._from_parent_attrs:
87102
for k in self.mgr._from_parent_attrs:
88-
data[k] = self.args[k]
103+
data[k] = self.parent_args[k]
89104
if not issubclass(self.cls, gitlab.mixins.GetWithoutIdMixin):
90105
if TYPE_CHECKING:
91106
assert isinstance(self.cls._id_attr, str)

tests/functional/cli/test_cli_variables.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
import copy
2+
3+
import pytest
4+
import responses
5+
6+
from gitlab import config
7+
from gitlab.const import DEFAULT_URL
8+
9+
110
def test_list_instance_variables(gitlab_cli, gl):
211
cmd = ["variable", "list"]
312
ret = gitlab_cli(cmd)
@@ -17,3 +26,59 @@ def test_list_project_variables(gitlab_cli, project):
1726
ret = gitlab_cli(cmd)
1827

1928
assert ret.success
29+
30+
31+
def test_list_project_variables_with_path(gitlab_cli, project):
32+
cmd = ["project-variable", "list", "--project-id", project.path_with_namespace]
33+
ret = gitlab_cli(cmd)
34+
35+
assert ret.success
36+
37+
38+
@pytest.fixture
39+
def resp_get_project():
40+
return {
41+
"method": responses.GET,
42+
"url": f"{DEFAULT_URL}/api/v4/projects/1",
43+
"json": {"name": "name", "path": "test-path", "id": 1},
44+
"content_type": "application/json",
45+
"status": 200,
46+
}
47+
48+
49+
@pytest.mark.script_launch_mode("inprocess")
50+
@responses.activate
51+
def test_list_project_variables_with_path_url_check(
52+
monkeypatch, script_runner, resp_get_project
53+
):
54+
monkeypatch.setattr(config, "_DEFAULT_FILES", [])
55+
resp_get_project_in_ci = copy.deepcopy(resp_get_project)
56+
resp_get_project_in_ci.update(
57+
url=f"{DEFAULT_URL}/api/v4/projects/project%2Fwith%2Fa%2Fnamespace/variables"
58+
)
59+
resp_get_project_in_ci.update(
60+
json=[
61+
{
62+
"variable_type": "env_var",
63+
"key": "TEST_VARIABLE_1",
64+
"value": "TEST_1",
65+
"protected": False,
66+
"masked": True,
67+
"environment_scope": "*",
68+
},
69+
{
70+
"variable_type": "env_var",
71+
"key": "TEST_VARIABLE_2",
72+
"value": "TEST_2",
73+
"protected": False,
74+
"masked": False,
75+
"environment_scope": "*",
76+
},
77+
]
78+
)
79+
80+
responses.add(**resp_get_project_in_ci)
81+
ret = script_runner.run(
82+
"gitlab", "project-variable", "list", "--project-id", "project/with/a/namespace"
83+
)
84+
assert ret.success

0 commit comments

Comments
 (0)