diff --git a/tests/unit/oidc/forms/test_gitlab.py b/tests/unit/oidc/forms/test_gitlab.py index a8a0f1ea5723..707db19c9f23 100644 --- a/tests/unit/oidc/forms/test_gitlab.py +++ b/tests/unit/oidc/forms/test_gitlab.py @@ -116,6 +116,45 @@ def test_validate_basic_invalid_fields(self, monkeypatch, data): # We're testing only the basic validation here. assert not form.validate() + @pytest.mark.parametrize( + "project_name", + ["invalid.git", "invalid.atom", "invalid--project"], + ) + def test_reserved_project_names(self, project_name): + + data = MultiDict( + { + "namespace": "some", + "workflow_filepath": "subfolder/some-workflow.yml", + "project": project_name, + } + ) + + form = gitlab.GitLabPublisherForm(data) + assert not form.validate() + + @pytest.mark.parametrize( + "namespace", + [ + "invalid.git", + "invalid.atom", + "consecutive--special-characters", + "must-end-with-non-special-characters-", + ], + ) + def test_reserved_organization_names(self, namespace): + + data = MultiDict( + { + "namespace": namespace, + "workflow_filepath": "subfolder/some-workflow.yml", + "project": "valid-project", + } + ) + + form = gitlab.GitLabPublisherForm(data) + assert not form.validate() + @pytest.mark.parametrize( "workflow_filepath", [ diff --git a/warehouse/locale/messages.pot b/warehouse/locale/messages.pot index d58b052f03c7..c2e53f065d36 100644 --- a/warehouse/locale/messages.pot +++ b/warehouse/locale/messages.pot @@ -369,7 +369,7 @@ msgid "Select project" msgstr "" #: warehouse/manage/forms.py:495 warehouse/oidc/forms/_core.py:23 -#: warehouse/oidc/forms/gitlab.py:44 +#: warehouse/oidc/forms/gitlab.py:58 msgid "Specify project name" msgstr "" @@ -574,7 +574,8 @@ msgstr "" msgid "Expired invitation for '${username}' deleted." msgstr "" -#: warehouse/oidc/forms/_core.py:25 warehouse/oidc/forms/gitlab.py:46 +#: warehouse/oidc/forms/_core.py:25 warehouse/oidc/forms/gitlab.py:61 +#: warehouse/oidc/forms/gitlab.py:65 msgid "Invalid project name" msgstr "" @@ -678,26 +679,30 @@ msgid "Workflow filename must be a filename only, without directories" msgstr "" #: warehouse/oidc/forms/gitlab.py:33 +msgid "Name ends with .git or .atom" +msgstr "" + +#: warehouse/oidc/forms/gitlab.py:42 msgid "Specify GitLab namespace (username or group/subgroup)" msgstr "" -#: warehouse/oidc/forms/gitlab.py:37 +#: warehouse/oidc/forms/gitlab.py:47 warehouse/oidc/forms/gitlab.py:51 msgid "Invalid GitLab username or group/subgroup name." msgstr "" -#: warehouse/oidc/forms/gitlab.py:54 +#: warehouse/oidc/forms/gitlab.py:73 msgid "Specify top-level pipeline file path" msgstr "" -#: warehouse/oidc/forms/gitlab.py:63 +#: warehouse/oidc/forms/gitlab.py:82 msgid "Invalid environment name" msgstr "" -#: warehouse/oidc/forms/gitlab.py:78 +#: warehouse/oidc/forms/gitlab.py:97 msgid "Top-level pipeline file path must end with .yml or .yaml" msgstr "" -#: warehouse/oidc/forms/gitlab.py:82 +#: warehouse/oidc/forms/gitlab.py:101 msgid "Top-level pipeline file path cannot start or end with /" msgstr "" diff --git a/warehouse/oidc/forms/gitlab.py b/warehouse/oidc/forms/gitlab.py index a861dadd2d01..eb940d781a81 100644 --- a/warehouse/oidc/forms/gitlab.py +++ b/warehouse/oidc/forms/gitlab.py @@ -11,6 +11,7 @@ # limitations under the License. import re +import typing import wtforms @@ -19,10 +20,18 @@ from warehouse.oidc.forms._core import PendingPublisherMixin # https://docs.gitlab.com/ee/user/reserved_names.html#limitations-on-project-and-group-names -_VALID_GITLAB_PROJECT = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9-_.]*$") -_VALID_GITLAB_NAMESPACE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9-_./]*$") +_VALID_GITLAB_PROJECT = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_.-]*[a-zA-Z0-9]$") +_VALID_GITLAB_NAMESPACE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9-_./+]*[a-zA-Z0-9]$") _VALID_GITLAB_ENVIRONMENT = re.compile(r"^[a-zA-Z0-9\-_/${} ]+$") +_CONSECUTIVE_SPECIAL_CHARACTERS = re.compile(r"(?!.*[._-]{2})") + + +def ends_with_atom_or_git(form: forms.Form, field: wtforms.Field) -> None: + field_value = typing.cast(str, field.data).lower() + if field_value.endswith(".atom") or field_value.endswith(".git"): + raise wtforms.validators.ValidationError(_("Name ends with .git or .atom")) + class GitLabPublisherBase(forms.Form): __params__ = ["namespace", "project", "workflow_filepath", "environment"] @@ -32,19 +41,29 @@ class GitLabPublisherBase(forms.Form): wtforms.validators.InputRequired( message=_("Specify GitLab namespace (username or group/subgroup)"), ), + ends_with_atom_or_git, wtforms.validators.Regexp( _VALID_GITLAB_NAMESPACE, message=_("Invalid GitLab username or group/subgroup name."), ), + wtforms.validators.Regexp( + _CONSECUTIVE_SPECIAL_CHARACTERS, + message=_("Invalid GitLab username or group/subgroup name."), + ), ] ) project = wtforms.StringField( validators=[ wtforms.validators.InputRequired(message=_("Specify project name")), + ends_with_atom_or_git, wtforms.validators.Regexp( _VALID_GITLAB_PROJECT, message=_("Invalid project name") ), + wtforms.validators.Regexp( + _CONSECUTIVE_SPECIAL_CHARACTERS, + message=_("Invalid project name"), + ), ] )