Reject non-object entries in the pipeline variables param

What does this MR do and why?

POST /api/v4/projects/:id/pipeline returns 500 when an entry in the variables array is not an object. The body {"ref":"main","variables":["FOO=bar"]} raises TypeError: no implicit conversion of Symbol into Integer.

The cause is in lib/api/ci/helpers/pipelines_helpers.rb, which declares optional :variables, type: Array with a nested block. Grape does not check element types, and the nested block skips any element that does not respond to key?. The raw array then reaches pipeline creation, which looks each entry up by key.

The fix adds an object_elements validator in lib/api/validations/validators/object_elements.rb, registered in config/initializers/grape_validators.rb. It returns 400 and names the index, for example variables[0] is not an object. It follows the existing no_blank_elements validator, which plugs the same gap.

Reported from gitlab.com, client user agent gitlab-mcp/0.2.0.

Why the param keeps type: Array

Grape's README documents type: Array with a nested block as "values of hashes in an array", so the declaration already states the intent. Grape does not enforce it.

Array[JSON] also parses JSON-encoded strings and wraps a single object into an array, so two bodies that return 400 today would start returning 201.

request body type: Array (today) this MR Array[JSON]
[{"key":"K","value":"V"}] 201 201 201
["K=V"] 201, then 500 400 variables[0] is not an object 400 variables is invalid
[["K","V"]] 201, then 500 400 variables[0] is not an object 400 variables is invalid
[null] 201, then 500 400 variables[0] is not an object 400 variables is invalid
{"key":"K"} (bare object) 400 400 400
"[{\"key\":\"K\"}]" (JSON string) 400 400 201
"{\"key\":\"K\"}" (JSON string, single object) 400 400 201

Array[JSON] also collapses the item schema in doc/api/openapi/openapi_v3.yaml to a bare type: object, dropping 22 lines of key, value and variable_type schema. With the validator that file is unchanged.

Impact

  • The API returns 400 instead of 500 for a malformed variables array. Well formed requests are unaffected. Form encoded bodies behave as they do today.
  • The REST create pipeline endpoint was the only way a non-object entry could reach pipeline creation, so fixing the API layer closes the whole path.
Callers checked, and two related defects

The other callers all build hashes themselves: the trigger API in app/services/ci/pipeline_trigger_service.rb, git push options in lib/ci/pipeline_creation/push_options.rb, the GraphQL mutation in app/graphql/mutations/ci/pipeline/create.rb, and the run pipeline UI in app/controllers/projects/pipelines_controller.rb.

Before !253383 (merged) this input raised only for users who can set pipeline variables. That merge request added an observation which reads the raw list before permission filtering, so the failure became unconditional in 19.4.

validate_uniqueness in lib/gitlab/ci/pipeline/chain/build/associations.rb still reads var[:key] with no string key fallback, which makes duplicate keys undetectable for git push options. That is a separate defect, tracked in #627531 (closed), and out of scope here.

Tests

  • spec/lib/api/validations/validators/object_elements_spec.rb: accepted and rejected element shapes, and the index naming in the error.
  • spec/requests/api/ci/pipelines_spec.rb: three cases pinning the 400 for a string, an array and a null entry. All three return 500 without the validator.

How to set up and validate locally

  1. POST to /api/v4/projects/:id/pipeline with header Content-Type: application/json and body {"ref": "<default branch>", "variables": ["FOO=bar"]}.
  2. Expect a 400 with variables[0] is not an object.
  3. Repeat with variables: [{"key": "FOO", "value": "bar"}] and expect a 201.

Merge request acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Oleg Yakovenko

Merge request reports

Loading
Loading