Skip to content

fix(machinery): hide inaccessible translation service IDs - #22362

Merged
nijel merged 1 commit into
mainfrom
machine-api
Oct 25, 2026
Merged

nijel merged 1 commit into
mainfrom
machine-api

Conversation

@nijel

@nijel nijel commented Oct 25, 2026

Copy link
Copy Markdown
Member

Resolve Translation Services serializer references within editable scopes so private projects and installations cannot be probed by ID. Cover inherited exclusions and permission boundaries with API tests.

@nijel nijel added this to the 2026.11 milestone Oct 25, 2026
@nijel nijel self-assigned this Oct 25, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-25T12:06:48.515317Z a7c839f New commits
🔒 Security Review ✅ Completed 2026-10-25T08:00:26.746675Z 72f9a2b PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@nijel
nijel enabled auto-merge (rebase) October 25, 2026 08:22
@codecov

codecov Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
13219 2 13217 987
View the top 2 failed test(s) by shortest run time
weblate.api.tests.ProjectAPITest::test_machinery_scope_reference_respects_project_2fa
Stack Traces | 0.211s run time
self = <weblate.api.tests.ProjectAPITest testMethod=test_machinery_scope_reference_respects_project_2fa>

    def test_machinery_scope_reference_respects_project_2fa(self) -> None:
        self.grant_perm_to_user("project.edit", project=self.project)
        self.project.enforced_2fa = True
        self.project.save(update_fields=["enforced_2fa"])
        self.user.clear_permissions_cache()
        self.assertFalse(self.user.has_perm("project.edit", self.project))
        response = self.do_request(
            "api:machinery-installation-list",
            method="post",
            code=400,
            request={"engine": "weblate", "project": self.project.pk},
            format="json",
        )
        self.assertEqual(response.data["errors"][0]["attr"], "project")
        self.assertEqual(response.data["errors"][0]["code"], "does_not_exist")
    
>       created = self.do_request(
            "api:machinery-installation-list",
            method="post",
            code=201,
            superuser=True,
            request={
                "engine": "weblate",
                "project": self.project.pk,
                "configuration": {"key": "secret"},
            },
            format="json",
        )

weblate/api/tests.py:9057: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
weblate/api/tests.py:468: in do_request
    self.assertEqual(
E   AssertionError: 400 != 201 : Unexpected status code 400: b'{"type":"validation_error","errors":[{"code":"invalid","detail":"Exclude the inherited installation before enabling this service.","attr":"engine"}]}'
weblate.api.tests.ProjectAPITest::test_machinery_exclusion_installation_reference_permissions
Stack Traces | 0.37s run time
self = <weblate.api.tests.ProjectAPITest testMethod=test_machinery_exclusion_installation_reference_permissions>

    def test_machinery_exclusion_installation_reference_permissions(self) -> None:
        self.grant_perm_to_user("project.edit", project=self.project)
        self.user.clear_permissions_cache()
        hidden_project = Project.objects.create(
            name="Hidden installation project",
            slug="hidden-installation-project",
            access_control=Project.ACCESS_PRIVATE,
        )
        hidden_installation = MachineryInstallation.objects.create(
            engine="dummy", project=hidden_project
        )
        hidden_workspace = Workspace.objects.create(
            name="Hidden installation workspace"
        )
        hidden_workspace_installation = MachineryInstallation.objects.create(
            engine="dummy", workspace=hidden_workspace
        )
        missing_id = hidden_workspace_installation.pk + 1000000
        self.assertFalse(MachineryInstallation.objects.filter(pk=missing_id).exists())
        for installation_id in (
            hidden_installation.pk,
            hidden_workspace_installation.pk,
            missing_id,
        ):
            with self.subTest(installation_id=installation_id):
                response = self.do_request(
                    "api:machinery-exclusion-list",
                    method="post",
                    code=400,
                    request={
                        "project": self.project.pk,
                        "installation": installation_id,
                    },
                    format="json",
                )
                self.assertEqual(response.data["errors"][0]["attr"], "installation")
                self.assertEqual(response.data["errors"][0]["code"], "does_not_exist")
    
>       site_installation = MachineryInstallation.objects.create(engine="weblate")
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

weblate/api/tests.py:8940: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.venv/lib/python3.14.../db/models/manager.py:87: in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../db/models/query.py:669: in create
    obj.save(force_insert=True, using=self.db)
weblate/machinery/models.py:101: in save
    self.full_clean()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <MachineryInstallation: weblate:None>, exclude = {'engine'}
validate_unique = True, validate_constraints = True

    def full_clean(self, exclude=None, validate_unique=True, validate_constraints=True):
        """
        Call clean_fields(), clean(), validate_unique(), and
        validate_constraints() on the model. Raise a ValidationError for any
        errors that occur.
        """
        errors = {}
        if exclude is None:
            exclude = set()
        else:
            exclude = set(exclude)
    
        try:
            self.clean_fields(exclude=exclude)
        except ValidationError as e:
            errors = e.update_error_dict(errors)
    
        # Form.clean() is run even if other validation fails, so do the
        # same with Model.clean() for consistency.
        try:
            self.clean()
        except ValidationError as e:
            errors = e.update_error_dict(errors)
    
        # Run unique checks, but only for fields that passed validation.
        if validate_unique:
            for name in errors:
                if name != NON_FIELD_ERRORS and name not in exclude:
                    exclude.add(name)
            try:
                self.validate_unique(exclude=exclude)
            except ValidationError as e:
                errors = e.update_error_dict(errors)
    
        # Run constraints checks, but only for fields that passed validation.
        if validate_constraints:
            for name in errors:
                if name != NON_FIELD_ERRORS and name not in exclude:
                    exclude.add(name)
            try:
                self.validate_constraints(exclude=exclude)
            except ValidationError as e:
                errors = e.update_error_dict(errors)
    
        if errors:
>           raise ValidationError(errors)
E           django.core.exceptions.ValidationError: {'engine': ['This service is already installed here.']}

.venv/lib/python3.14.../db/models/base.py:1712: ValidationError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Resolve Translation Services serializer references within editable scopes so private projects and installations cannot be probed by ID. Cover inherited exclusions and permission boundaries with API tests.
@nijel
nijel merged commit 5b90912 into main Oct 25, 2026
43 of 44 checks passed
@nijel
nijel deleted the machine-api branch October 25, 2026 13:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant