Say when Guests and Planners can view pipelines and merge requests

What does this MR do?

The Project CI/CD table on the roles and permissions page gives the Planner role a check on seven view rows: the list of jobs, job logs, pipelines, viewing and downloading artifacts, environments, and the pipelines tab of a merge request. None of their footnotes names Planner, so the table reads as if a Planner can see all of them on any project. The Planner role has no CI/CD read permission of its own. For CI/CD it has exactly the permissions Guest has: jobs, pipelines, and artifacts depend on Project-based pipeline visibility, and environments depend on the project being public. This MR adds Planner to the footnotes with the conditions the policy applies, and changes no check mark.

While checking those rows I found two more statements about the same setting that the policy does not match, and one more place where the table leaves out a condition on the Planner role, in the merge request rows, so this MR corrects them too. The View existing artifacts row also gives Planners a check with no condition, and its footnote says that Guests see existing artifacts only in public projects. The pipeline settings page says that when the setting is cleared, the pipelines of an internal project stay visible to every authenticated user. Each of the four changes is a commit of its own, in this order:

  1. "Say when Planner can view pipelines in the permissions table": the Planner footnotes on the seven view rows of the Project CI/CD table, and the description of Project-based pipeline visibility above it.
  2. "Say when Guests and Planners can see that artifacts exist": the footnote of the View existing artifacts row.
  3. "Say what internal projects show without pipeline visibility": the Internal bullet of doc/ci/pipelines/settings.md.
  4. "Say when Planner can view merge requests in the permissions table": the footnotes of five rows of the Project merge requests table.

What the CI/CD footnotes say now:

  • View list of jobs, View job logs and job details page, and View pipelines and pipeline details pages: "Guests and Planners: Only if Project-based pipeline visibility is enabled."
  • View artifacts and Download artifacts: the same, with the existing artifacts:public: false condition.
  • View environments gets a footnote of its own: "Non-members, Guests, and Planners: Only if the project is public." A Planner does not see the environments of a private project.
  • View pipelines tab in MR gets a footnote of its own. Non-members and Guests keep "Only if the project is public", and Planners need a public project or Project-based pipeline visibility. Of the CI/CD rows this MR changes, this is the one where Planner and Guest differ: a Planner can open the merge requests of a private project, unless they are set to Only Project Members (the last commit), and the tab lists pipelines when the user can read pipelines on the project.
  • View existing artifacts gets a footnote of its own: "Non-members: Only if the project is public." and "Guests and Planners: Only if the project is public or Project-based pipeline visibility is enabled." This row is not View artifacts under another name. It was called "See that artifacts exist" until !167236 (merged) renamed it, and it says when a user can learn that a job has artifacts, not when the user can open them. On a public project, the Branches and Tags pages show every user which jobs have artifacts, whatever the setting, while opening them needs the setting. On a private project, a Guest or a Planner learns it only from the pages the setting opens to them (release evidence aside, see the evidence below). Its old footnote, which after the first commit no other row uses, is removed.
  • The description of Project-based pipeline visibility above the table names the Guest and Planner roles.

The setting is on by default (public_builds defaults to true), so with default settings a Planner does see jobs and pipelines. The page was wrong for projects that turn the setting off, and for environments on any private project. The report in #632031 (closed), a user moved from Developer to Planner on a private project who lost pipeline status and environments in merge requests, fits this: the environments are gone on any private project, and the pipeline status is gone when the setting is cleared. doc/ci/pipelines/settings.md already says it: with the setting cleared, the pipelines of a private project are visible to Reporter or higher only.

It is the same kind of fix as !257915 (merged) on this page.

The third commit corrects doc/ci/pipelines/settings.md, which describes the setting. It said that with the setting cleared, the pipelines of an internal project stay visible to all authenticated users except external users. They do not: the pipeline list and the pipeline pages need the permission the setting withholds, so for those users an internal project behaves like a public one. They can view the status of pipelines and jobs when viewing merge requests or commits, which is what the page already said of public projects and now says of internal ones.

The last commit is about the Project merge requests table, which gives the Planner role a check on viewing, searching, and approving merge requests, adding internal notes, and commenting, and nothing in the table says when a Planner loses them. A project can set Merge requests to Only Project Members, and for that level GitLab makes Reporter the minimum role, so a Planner loses all five, as a Guest loses the two it has. That is how the role was designed: !169256 (merged), which added it, says that when a public project sets its merge requests to private, Planners have no access to them, the same as Guests, and the merge request policy spec holds Planner to it. The same minimum applies to an internal or private project whose merge requests are set to Only Project Members, which includes a project made private on its settings page up to GitLab 17.7, since that also set its merge requests to that level. So this commit documents the behavior rather than changing it:

  • View a merge request and Search merge requests and comments: their footnote now ends with "Users with the Guest or Planner role can't perform this action if merge requests are set to Only Project Members."
  • Approve merge requests: its footnote says the same for Planners.
  • Add internal note and Comment and add suggestions get a footnote that says it for Planners.
  • The view and search footnote also said that external users need at least the Reporter role on an internal project. For merge requests the Planner role is enough, which the repository footnote already says for the same case, so it now says Planner.
Evidence, file and line on master at ec3ecb22 (every line cited is the same at df1d6875, the master this branch is rebased onto)

What the Planner role has

  • config/authz/roles/planner.yml:6-7 inherits from guest. Its project permissions add planning permissions plus download_code (:37), read_code (:45) and read_merge_request (:48), and nothing for CI/CD.
  • config/authz/roles/guest.yml:14-16 grants _read_public_build, _read_public_pipeline and _read_public_pipeline_schedule.
  • read_build (:60), read_commit_status (:65), read_deployment (:72), read_environment (:75) and read_pipeline (:92) first appear in config/authz/roles/reporter.yml.
  • The conditionally_enables expansion (lib/authz/role.rb:31-38 and :144-156) adds a _read_public_* permission to a role that already holds the broad one, never the reverse, so it gives Guest and Planner nothing.

How the setting gates them

  • app/policies/project_policy.rb:24 defines public_builds ("Project-based pipeline visibility enabled"). :749-754 prevent the four _read_public_* permissions when it is off, and :756-757 turn _read_public_build and _read_public_pipeline into read_build and read_pipeline.
  • app/policies/ci/pipeline_policy.rb:37-40 prevents read_pipeline on a pipeline without read_build, and app/controllers/projects/pipelines_controller.rb:17-19 requires read_build for the pipeline list and page.
  • app/policies/ci/build_policy.rb:141-143 gates read_job_artifacts on read_build, and on _read_developer_job_artifact (config/authz/roles/developer.yml:11) when artifacts:public: false is set.
  • spec/policies/project_policy_spec.rb:2428-2481 already holds Guest and Planner to the same answer under public_builds, for read_ci_cd_analytics.

Environments

  • On a public project, project_policy.rb:317-319 enables the public_anonymous permissions for everyone, which include read_environment (config/authz/roles/public_anonymous.yml:47) and read_pipeline (:103), but for jobs only _read_public_build (:8). On a private project no rule gives a Guest or a Planner read_environment.

The merge request pipelines tab

  • Ci::PipelinesForMergeRequestFinder#execute (app/finders/ci/pipelines_for_merge_request_finder.rb:20-30) lists pipelines when the user can read_pipeline on the project (:79-89). On a public project everyone can (public_anonymous.yml:103). On a private project a Planner can only through _read_public_pipeline, which needs the setting. A Guest cannot open the merge requests of a private project, because read_merge_request is not in guest.yml.

View existing artifacts

What the row means:

  • !79614 (merged) split the old artifacts row in two after the checks recorded in #321946 (closed): "See that artifacts exist", for non-members and Guests only if the project is public, and "View and download artifacts", which also needed the setting (then called Public pipelines). The checks in #321946 (closed) list "See that artifacts exist in MR pipelines tab" as available to non-members and Guests of a public project whose pipelines are private.
  • !167236 (merged) renamed the row to "View existing artifacts" when it put one action on each line of the table, without changing its footnote.

Where a user without read_build can still see that artifacts exist:

  • The merge request pipelines tab no longer shows it on master: the artifacts menu of each pipeline (app/assets/javascripts/ci/pipelines_page/components/pipeline_operations.vue:170) loads downloadable_artifacts (app/controllers/projects/pipelines_controller.rb:220-225), which keeps only what read_job_artifacts allows (app/serializers/ci/downloadable_artifact_entity.rb:11).
  • The Branches and Tags pages do. Their source code dropdown lists, under Download artifacts, each job of the latest pipeline of the ref that has artifacts which have not expired (app/views/projects/buttons/_download.html.haml:8-15, previous_artifacts in app/helpers/tree_helper.rb:256-263, Ci::Pipeline#latest_builds_with_artifacts in app/models/ci/pipeline.rb:1503-1508), to a user who can download_code. The pipeline comes from Ci::CommitStatusesFinder (app/views/projects/branches/_branch.html.haml:6, app/views/projects/tags/_tag.html.haml:3-4, app/controllers/projects/branches_controller.rb:38-39, app/controllers/projects/tags_controller.rb:40-41), which checks only read_pipeline on the project (app/finders/ci/commit_statuses_finder.rb:16). The links go to the artifacts of the latest successful job, which need read_build (app/controllers/projects/artifacts_controller.rb:13).
  • A public project gives download_code (public_anonymous.yml:13) and read_pipeline (:103) to every user (project_policy.rb:317-319), and ~public_builds prevents only the _read_public_* permissions (:749-754). So on a public project non-members, Guests, and Planners see that list whatever the setting.
  • On a private project, a Guest or a Planner gets read_pipeline and read_build only from _read_public_pipeline and _read_public_build (guest.yml:14-15), which the setting gates. A Planner also has download_code (planner.yml:37), but with the setting cleared Ci::CommitStatusesFinder returns nothing, so the list stays empty.

Where read_build shows them, which is what the setting opens to Guests and Planners:

  • The job list and the job page: app/controllers/projects/jobs_controller.rb:16.
  • The pipeline page: app/controllers/projects/pipelines_controller.rb:19 and :333-335.
  • The project artifacts page: app/controllers/projects/artifacts_controller.rb:13.
  • A merge request: its exposed artifacts need read_build on the head pipeline (app/controllers/projects/merge_requests_controller.rb:24-32 and :702-704), and the widget lists the artifacts of its pipeline only where read_job_artifacts allows (app/serializers/merge_requests/pipeline_entity.rb:37-44).
  • read_job_artifacts starts from read_build on the project, for a job (app/policies/ci/build_policy.rb:141) and for an artifact (app/policies/ci/job_artifact_policy.rb:23-25 and :48).
  • GraphQL: Project.jobs and Project.job authorize read_build (app/graphql/types/project_type.rb:506-518), and the jobs and job artifacts of a pipeline sit behind the pipeline's own read_pipeline (app/graphql/types/ci/pipeline_type.rb:12), which Ci::PipelinePolicy withholds without read_build.
  • REST: the job endpoints and the artifact downloads call authorize_read_builds! (lib/api/ci/jobs.rb:65 and :91, lib/api/ci/job_artifacts.rb:16-17), and the jobs of a pipeline go through Ci::JobsFinder, which refuses without read_build (app/finders/ci/jobs_finder.rb:66-68).

One more place names the artifacts of jobs: release evidence (Ultimate). Its JSON holds a download link for each report artifact of the release pipeline (ee/app/serializers/evidences/build_artifact_entity.rb:7-9, ee/app/serializers/ee/evidences/release_entity.rb:9-13, documented in doc/user/project/releases/release_evidence.md), and anyone who can read the release, the code, the milestones, and the issues can read it (app/policies/releases/evidence_policy.rb:7-9 and :19-25), which includes a Planner on a private project whatever the setting. Following a link still needs read_job_artifacts. I read that as part of release evidence rather than as this row.

Internal projects with the setting cleared

  • On an internal project, every user who is not external gets the permissions of the public_authenticated role (app/policies/project_policy.rb:321-323). permissions(:project) includes the roles a role inherits (lib/authz/role.rb:67-70, :92-101 and :122-123), and public_authenticated inherits from public_anonymous (config/authz/roles/public_authenticated.yml:4-5), so these users hold read_pipeline (public_anonymous.yml:103). They reach read_build only through _read_public_build (public_authenticated.yml:13), which project_policy.rb:749-754 prevents when the setting is cleared.
  • The pipeline list and the pipeline page need read_build (app/controllers/projects/pipelines_controller.rb:18-19), and Ci::PipelinePolicy withholds read_pipeline on a pipeline without it (app/policies/ci/pipeline_policy.rb:37-40).
  • The pipelines API spec already asserts the outcome: its visibility table answers 403 to a non-member and to a Guest of an internal project with the setting cleared (spec/support/shared_examples/requests/api/pipelines/visibility_table_shared_examples.rb:134-135), and 200 with the setting on (:121-122). It runs from spec/requests/api/ci/pipelines_spec.rb:39.
  • What those users keep is the project-level read_pipeline, which the pipeline lists of a merge request (Ci::PipelinesForMergeRequestFinder, above) and of a commit (app/controllers/projects/commit_controller.rb:19) check. That is what a public project gives the same users, as the page already says.

Merge requests set to Only Project Members

  • app/models/project_feature.rb:44-50: PRIVATE_FEATURES_MIN_ACCESS_LEVEL sets merge_requests: Gitlab::Access::REPORTER. The value is older than the Planner role: it came with the 11.7.1 security fix that stopped group Guests from seeing merge requests (changelogs/archive-11.md:1344).
  • app/policies/project_policy.rb:283-287 defines a <feature>_disabled condition from access_allowed_to?, which at the PRIVATE level compares the member's access level with that minimum (:1033-1048). Planner is 15 and Reporter 20.
  • :534-558: merge_requests_disabled prevents read_merge_request, approve_merge_request, and the other merge request permissions, whatever the role grants (config/authz/roles/planner.yml:48 grants read_merge_request). app/policies/merge_request_policy.rb:14-18 then prevents create_note and mark_note_as_internal on the merge request.
  • Search: Basic search keeps the projects that ProjectFeature.with_feature_available_for_user returns (app/models/project_feature.rb:136-158), which applies the same minimum at the PRIVATE level, and Advanced search relaxes it for Planners at the ENABLED level only (ee/lib/search/elastic/merge_request_query_builder.rb:12-20).
  • Design: !169256 (merged), which added the Planner role, lists for a public project "Merge Requests are private: No access to the project's MR (same as Guest)".
  • External users: an external user gets nothing from the internal project rules (project_policy.rb:38-43 and :321-323), and no project policy rule takes merge requests away from one, so on an internal project an external Planner has the read_merge_request of planner.yml:48 and an external Guest has nothing. spec/finders/merge_requests_finder_spec.rb:1387-1395 holds the merge request list to it. The search side was aligned for this case by !257014 (merged) (Basic search), !253160 (merged) (Advanced search), and !257800 (merged) (comments, whose probe covered external Planners). !253160 (merged) left the Reporter wording in place because comment search still left external Planners out at the time, and !257800 (merged) closed that. The repository footnote says Planner for the same case (doc/user/permissions.md, [^project-repositories-guest-role-limited]).
  • Specs: spec/policies/merge_request_policy_spec.rb:224-228 and :390-394 expect a Planner to be denied every permission in mr_perms (:61-69: read, approve, create a note, mark a note as internal, and the rest) when merge requests are private, for a direct and for an inherited member. spec/finders/merge_requests_finder_spec.rb:1429-1447 expects the merge request list to leave such a project out for a Planner and to include it for a Reporter, and :1412-1418 to include a private project's merge requests for a Planner when they are not restricted. I ran those five examples and the external Planner one (:1387-1395) on master at 07c80d80: 6 examples, 0 failures.
  • Approval: the Planner in those policy specs is not an approver to begin with, so they do not show the case the approval footnote describes. A group approval rule enables approve_merge_request for a Planner of the group (ee/app/policies/ee/merge_request_policy.rb:106-108), and MergeRequestPolicy delegates to the project (app/policies/issuable_policy.rb:4), so the project's prevent :approve_merge_request (project_policy.rb:535) still applies. I checked it with a local spec that is not part of this MR, set up as the approval rules page describes: a Planner and a Reporter in a group that an approval rule names for a protected branch, the group invited to a public project with the Reporter role. With merge requests set to Everyone With Access, the Planner can approve. With Only Project Members, the Planner can't approve, read, comment, or mark a note as internal, and the Reporter can still approve. 3 examples, 0 failures on master at 07c80d80.
  • Private projects: up to GitLab 17.7, changing a project's visibility to private on its settings page also set its merge requests to Only Project Members. !175070 (merged) removed that in 17.8, and the settings page of a private project shows the merge requests level without letting you change it (app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue:681-689). A project created private starts at Everyone With Access, which is the case merge_requests_finder_spec.rb:1412-1418 covers.
What I left out
  • View vulnerabilities in a pipeline is not changed here. Its Guest, Planner, and Reporter cells were wrong for a reason that has nothing to do with Planner, so I corrected the row in !260471 (merged), which is now merged. The row has checks for the Security Manager, Developer, and Maintainer roles only, no footnote, and a link to the pipeline security report. This branch is rebased onto master with !260471 (merged) in it, and the rebase conflicted only at that row and in the footnote list. I kept the row and the removal of its footnote as !260471 (merged) left them, with this MR's footnotes added.
  • Internal projects in the permissions table: the footnotes say "public", but on an internal project every user who is not external gets the same view permissions as on a public one (project_policy.rb:321-323). I kept the page's convention.
  • Branches and Tags pages on the pipeline settings page: with the setting cleared, the page says that other users see the status of pipelines and jobs only when viewing merge requests or commits. The Branches and Tags pages show them the status of the latest pipeline of each ref too, and which of its jobs have artifacts, through the same Ci::CommitStatusesFinder. The internal bullet now uses the sentence the public bullet already had, and I left the wording of both alone.
  • Changing the behavior instead of the table: if Planners should keep merge requests set to Only Project Members, the change is lowering PRIVATE_FEATURES_MIN_ACCESS_LEVEL[:merge_requests] to Planner, the way !259718 (merged) lowers the private project repository minimum. That would contradict the design in !169256 (merged), so I documented the current behavior.
  • The View pipelines tab in MR and View allowed and denied licenses in MR rows of other tables don't repeat the Only Project Members condition: their footnotes assume the merge request can be opened, and the merge request table now says when it cannot.
Overlap with open merge requests

When I checked on 2026-10-07, no open merge request other than this one changed doc/user/permissions.md or doc/ci/pipelines/settings.md, among the 563 labeled documentation and the 126 that a title search for "permission", "role", "pipeline", or "artifact" returns. Later that day I opened !260471 (merged), which changed both pages and has since been merged (see What I left out). On 2026-10-08 I checked again the 768 open merge requests updated since that first check, and none of them but this one changes either page. No open merge request has "pipeline visibility", "public pipelines", or public_builds in its title, and #632031 (closed) has no linked merge request other than this one. Of the open merge requests with "Planner" in the title, none touches the merge request rows: !259718 (merged) changes the private project repository minimum in app/models/project_feature.rb and no documentation, and !259719 and !259952 are about agent widgets.

Lint
  • markdownlint-cli2 0.23.2 with the repository configuration, on both pages: 0 issues.
  • Vale 3.21.0 as scripts/lint-doc.sh runs it (--minAlertLevel error), on both pages: 0 errors. At every level, doc/ci/pipelines/settings.md has the same 4 suggestions as master at df1d6875, and doc/user/permissions.md the same 3 warnings and 21 suggestions, by rule and matched text, none on a changed line. The reading level of doc/user/permissions.md goes from 8.53 to 8.49, and that of doc/ci/pipelines/settings.md from 7.58 to 7.60.
  • lychee --offline --no-progress --include-fragments doc tooling/docs/api/tags, as the docs-lint links job runs it: 0 errors.
  • scripts/lint-doc.sh doc/user/permissions.md doc/ci/pipelines/settings.md in the lint image: passed.
  • scripts/lint/commit_linter.rb from master, on each of the four commit messages: no problems.
Where this comes from

I maintain gitlab-mcp-server, an MCP server that exposes the GitLab API to AI assistants. While writing the hint its pipeline tools give when GitLab refuses to show a pipeline, I checked which roles can see one, and the table and the policy disagreed for Planner. The pipelines tab of a merge request led me to the merge request rows.

doc/user/permissions.md belongs to Software Supply Chain Security: Authorization, and its technical writer in .gitlab/CODEOWNERS and lib/tasks/gitlab/tw/codeowners.rake is idurham, who also reviewed !257915 (merged), so he seems the right reviewer. doc/ci/pipelines/settings.md, which only the third commit changes, belongs to Verify: Pipeline Execution, whose technical writer in the same two files is lyspin.

Closes #632031 (closed)

Author's checklist

If you are a GitLab team member and only adding documentation, do not add any of the following labels:

  • ~"frontend"
  • ~"backend"
  • ~"type::bug"
  • ~"database"

These labels cause the MR to be added to code verification QA issues.

Reviewer's checklist

Documentation-related MRs should be reviewed by a Technical Writer for a non-blocking review, based on Documentation Guidelines and the Style Guide.

If you aren't sure which tech writer to ask, use roulette or ask in the #docs Slack channel.

  • If the content requires it, ensure the information is reviewed by a subject matter expert.
  • Technical writer review items:
    • Ensure docs metadata is present and up-to-date.
    • Ensure the appropriate labels are added to this MR.
    • Ensure a release milestone is set.
    • If relevant to this MR, ensure content topic type principles are in use, including:
      • The headings should be something you'd do a Google search for. Instead of Default behavior, say something like Default behavior when you close an issue.
      • The headings (other than the page title) should be active. Instead of Configuring GDK, say something like Configure GDK.
      • Any task steps should be written as a numbered list.
      • If the content still needs to be edited for topic types, you can create a follow-up issue with the docs-technical-debt label.
  • Review by assigned maintainer, who can always request/require the reviews above. Maintainer's review can occur before or after a technical writer review.
Edited by José M. Requena Plens

Merge request reports

Loading
Loading