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:
- "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.
- "Say when Guests and Planners can see that artifacts exist": the footnote of the View existing artifacts row.
- "Say what internal projects show without pipeline visibility": the Internal bullet of
doc/ci/pipelines/settings.md. - "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: falsecondition. - 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-7inherits fromguest. Its project permissions add planning permissions plusdownload_code(:37),read_code(:45) andread_merge_request(:48), and nothing for CI/CD.config/authz/roles/guest.yml:14-16grants_read_public_build,_read_public_pipelineand_read_public_pipeline_schedule.read_build(:60),read_commit_status(:65),read_deployment(:72),read_environment(:75) andread_pipeline(:92) first appear inconfig/authz/roles/reporter.yml.- The
conditionally_enablesexpansion (lib/authz/role.rb:31-38and: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:24definespublic_builds("Project-based pipeline visibility enabled").:749-754prevent the four_read_public_*permissions when it is off, and:756-757turn_read_public_buildand_read_public_pipelineintoread_buildandread_pipeline.app/policies/ci/pipeline_policy.rb:37-40preventsread_pipelineon a pipeline withoutread_build, andapp/controllers/projects/pipelines_controller.rb:17-19requiresread_buildfor the pipeline list and page.app/policies/ci/build_policy.rb:141-143gatesread_job_artifactsonread_build, and on_read_developer_job_artifact(config/authz/roles/developer.yml:11) whenartifacts:public: falseis set.spec/policies/project_policy_spec.rb:2428-2481already holds Guest and Planner to the same answer underpublic_builds, forread_ci_cd_analytics.
Environments
- On a public project,
project_policy.rb:317-319enables thepublic_anonymouspermissions for everyone, which includeread_environment(config/authz/roles/public_anonymous.yml:47) andread_pipeline(:103), but for jobs only_read_public_build(:8). On a private project no rule gives a Guest or a Plannerread_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 canread_pipelineon 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, becauseread_merge_requestis not inguest.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) loadsdownloadable_artifacts(app/controllers/projects/pipelines_controller.rb:220-225), which keeps only whatread_job_artifactsallows (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_artifactsinapp/helpers/tree_helper.rb:256-263,Ci::Pipeline#latest_builds_with_artifactsinapp/models/ci/pipeline.rb:1503-1508), to a user who candownload_code. The pipeline comes fromCi::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 onlyread_pipelineon the project (app/finders/ci/commit_statuses_finder.rb:16). The links go to the artifacts of the latest successful job, which needread_build(app/controllers/projects/artifacts_controller.rb:13). - A public project gives
download_code(public_anonymous.yml:13) andread_pipeline(:103) to every user (project_policy.rb:317-319), and~public_buildsprevents 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_pipelineandread_buildonly from_read_public_pipelineand_read_public_build(guest.yml:14-15), which the setting gates. A Planner also hasdownload_code(planner.yml:37), but with the setting clearedCi::CommitStatusesFinderreturns 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:19and:333-335. - The project artifacts page:
app/controllers/projects/artifacts_controller.rb:13. - A merge request: its exposed artifacts need
read_buildon the head pipeline (app/controllers/projects/merge_requests_controller.rb:24-32and:702-704), and the widget lists the artifacts of its pipeline only whereread_job_artifactsallows (app/serializers/merge_requests/pipeline_entity.rb:37-44). read_job_artifactsstarts fromread_buildon the project, for a job (app/policies/ci/build_policy.rb:141) and for an artifact (app/policies/ci/job_artifact_policy.rb:23-25and:48).- GraphQL:
Project.jobsandProject.jobauthorizeread_build(app/graphql/types/project_type.rb:506-518), and the jobs and job artifacts of a pipeline sit behind the pipeline's ownread_pipeline(app/graphql/types/ci/pipeline_type.rb:12), whichCi::PipelinePolicywithholds withoutread_build. - REST: the job endpoints and the artifact downloads call
authorize_read_builds!(lib/api/ci/jobs.rb:65and:91,lib/api/ci/job_artifacts.rb:16-17), and the jobs of a pipeline go throughCi::JobsFinder, which refuses withoutread_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_authenticatedrole (app/policies/project_policy.rb:321-323).permissions(:project)includes the roles a role inherits (lib/authz/role.rb:67-70,:92-101and:122-123), andpublic_authenticatedinherits frompublic_anonymous(config/authz/roles/public_authenticated.yml:4-5), so these users holdread_pipeline(public_anonymous.yml:103). They reachread_buildonly through_read_public_build(public_authenticated.yml:13), whichproject_policy.rb:749-754prevents when the setting is cleared. - The pipeline list and the pipeline page need
read_build(app/controllers/projects/pipelines_controller.rb:18-19), andCi::PipelinePolicywithholdsread_pipelineon 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 fromspec/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_LEVELsetsmerge_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-287defines a<feature>_disabledcondition fromaccess_allowed_to?, which at thePRIVATElevel compares the member's access level with that minimum (:1033-1048). Planner is 15 and Reporter 20.:534-558:merge_requests_disabledpreventsread_merge_request,approve_merge_request, and the other merge request permissions, whatever the role grants (config/authz/roles/planner.yml:48grantsread_merge_request).app/policies/merge_request_policy.rb:14-18then preventscreate_noteandmark_note_as_internalon the merge request.- Search: Basic search keeps the projects that
ProjectFeature.with_feature_available_for_userreturns (app/models/project_feature.rb:136-158), which applies the same minimum at thePRIVATElevel, and Advanced search relaxes it for Planners at theENABLEDlevel 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-43and:321-323), and no project policy rule takes merge requests away from one, so on an internal project an external Planner has theread_merge_requestofplanner.yml:48and an external Guest has nothing.spec/finders/merge_requests_finder_spec.rb:1387-1395holds 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-228and:390-394expect a Planner to be denied every permission inmr_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-1447expects the merge request list to leave such a project out for a Planner and to include it for a Reporter, and:1412-1418to 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 at07c80d80: 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_requestfor a Planner of the group (ee/app/policies/ee/merge_request_policy.rb:106-108), andMergeRequestPolicydelegates to the project (app/policies/issuable_policy.rb:4), so the project'sprevent :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 at07c80d80. - 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 casemerge_requests_finder_spec.rb:1412-1418covers.
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.shruns it (--minAlertLevel error), on both pages: 0 errors. At every level,doc/ci/pipelines/settings.mdhas the same 4 suggestions as master atdf1d6875, anddoc/user/permissions.mdthe same 3 warnings and 21 suggestions, by rule and matched text, none on a changed line. The reading level ofdoc/user/permissions.mdgoes from 8.53 to 8.49, and that ofdoc/ci/pipelines/settings.mdfrom 7.58 to 7.60. lychee --offline --no-progress --include-fragments doc tooling/docs/api/tags, as thedocs-lint linksjob runs it: 0 errors.scripts/lint-doc.sh doc/user/permissions.md doc/ci/pipelines/settings.mdin the lint image: passed.scripts/lint/commit_linter.rbfrom 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.
Related issues
Closes #632031 (closed)
Author's checklist
- Optional. Consider taking the GitLab Technical Writing Fundamentals course.
- Follow the:
- If you're adding a new page, add the product availability details under the H1 topic title.
- If you are a GitLab team member, request a review based on:
- The documentation page's metadata.
- The associated Technical Writer.
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 likeDefault behavior when you close an issue. - The headings (other than the page title) should be active. Instead of
Configuring GDK, say something likeConfigure 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.
- The headings should be something you'd do a Google search for. Instead of
- Review by assigned maintainer, who can always request/require the reviews above. Maintainer's review can occur before or after a technical writer review.