Document 401 responses to a missing permission in the REST API

What does this MR do?

The status code table on the REST API troubleshooting page says that 401 Unauthorized means "The user isn't authenticated. A valid user token is necessary." Several endpoints also return 401 Unauthorized to an authenticated user who doesn't have permission for the request, and their own pages already say so. Merge a merge request lists 401 with "This user does not have permission to accept this merge request.", Reset approvals for a merge request says "Human users receive a 401 Unauthorized response.", and List all personal access tokens returns 401 "if a non-administrator uses the user_id attribute to filter for other users". The body of those responses is {"message":"401 Unauthorized"}, the same body GitLab returns for a token that doesn't exist, so a client that relies on the table tells its user to check a token that works.

This MR keeps the text of the 401 row and adds that some endpoints also return this status code for a missing permission, with a link to a new Status code 401 section next to Status code 400. The section says when the API returns 401 because of the token, gives the three examples above with links to their pages, shows the body they share with an unknown token, and explains how to tell the two apart: retrieve the current user with the same token. If that returns 200 OK, GitLab accepts the token and the original request was refused for another reason. A 403 Forbidden also means GitLab accepts the token, which then can't read the current user, as with a fine-grained personal access token that has no permission to read the user account. If GET /user also returns 401 Unauthorized, GitLab doesn't accept the token. The section also says that this check doesn't apply to CI/CD job tokens, which can't access GET /user. The 403 Forbidden row is unchanged.

The MR documents the behavior and does not change it. The API style guide says "We must not make breaking changes to our REST API v4, even in major GitLab releases." and lists "Changing any status code other than 500" as a breaking change, so I'm not proposing to move these endpoints to 403 here. When I added cancel_auto_merge beside the deprecated cancel_merge_when_pipeline_succeeds route in !255702 (merged), the new route answered this refusal with 403 and the old one kept its 401. I list the places I found that return 401 for a missing permission below in case they are useful.

I found this while maintaining gitlab-mcp-server, an MCP server for GitLab. A refused merge request approval reached it as 401, and it reported that the token might be invalid or expired.

Evidence, file and line on master at 7f4e1ed0
  • lib/api/helpers.rb:617-619: unauthorized! renders 401 with the message 401 Unauthorized.
  • lib/api/helpers.rb:1037-1040 and lib/gitlab/auth/auth_finders.rb:434: a personal access token that doesn't exist raises UnauthorizedError, which initial_current_user answers with the same unauthorized!. That is why the two bodies are identical.
  • The three examples on the page: lib/api/merge_requests.rb:905 (unauthorized! unless merge_request.can_be_merged_by?(current_user)), lib/api/merge_request_approvals.rb:148 (unauthorized! unless current_user.can?(:reset_merge_request_approvals, merge_request) && ...), and lib/api/helpers/personal_access_tokens_helpers.rb:80 (restrict_non_admins!, which lib/api/personal_access_tokens.rb:11 calls for non-administrators).
  • lib/api/users.rb:1356 and lib/api/api_guard.rb:235-238: GET /user requires the read_user permission of a fine-grained personal access token, and a token without it gets 403 with insufficient_granular_scope. A legacy token whose scopes don't include one that lib/api/api.rb:58-59 and lib/api/users.rb:9-12 allow on this route, such as a token with only self_rotate, gets 403 with insufficient_scope (lib/gitlab/auth/auth_finders.rb:231, rendered at lib/api/api_guard.rb:217-223).
  • lib/gitlab/auth/auth_finders.rb:98: a CI/CD job token is only read on routes that set job_token_allowed, and lib/api/users.rb sets it on no route. The Users API is also not in the list of endpoints a job token can access in doc/ci/jobs/ci_job_token.md.

Measured on GitLab.com on 2026-10-08 with read-only requests. My account is not a Maintainer of this project, and lib/api/remote_mirrors.rb:12 and lib/api/resource_access_tokens.rb:32 refuse a non-Maintainer with unauthorized!:

GET /api/v4/user                                  (unknown token)  401 {"message":"401 Unauthorized"}
GET /api/v4/projects/278964/remote_mirrors        (my token)       401 {"message":"401 Unauthorized"}
GET /api/v4/projects/278964/access_tokens         (my token)       401 {"message":"401 Unauthorized"}
GET /api/v4/user                                  (my token)       200

None of the three 401 responses carried a WWW-Authenticate header.

Places where the REST API returns 401 for a missing permission, on master at 7f4e1ed0

Each of these answers 401 to an authenticated user who lacks a permission, a role or a license, or who as a non-administrator names a token they can't see. These are the ones I found, and the list is not exhaustive:

  • Merge requests: merge (lib/api/merge_requests.rb:905), cancel auto-merge through the deprecated cancel_merge_when_pipeline_succeeds route (lib/api/merge_requests.rb:957), approve (lib/api/merge_request_approvals.rb:102, on any refusal of MergeRequests::ApprovalService, such as "User is not eligible to approve" at app/services/merge_requests/approval_service.rb:9), and reset approvals (lib/api/merge_request_approvals.rb:148). Its replacement, cancel_auto_merge (!255702 (merged)), answers the same refusal with 403 (lib/api/merge_requests.rb:979).
  • Merge trains: add a merge request when the service answers :forbidden (ee/lib/api/merge_trains.rb:173).
  • Remote mirrors: every route (lib/api/remote_mirrors.rb:12).
  • Project and group access tokens: list and get without read_resource_access_tokens (lib/api/resource_access_tokens.rb:32 and :63), rotate by ID for a non-administrator who can't manage the resource's tokens or names a token that doesn't exist (:200), and self-rotate for a bot token that is not one of the resource's (lib/api/resource_access_tokens/self_rotation.rb:46).
  • Personal access tokens: get and rotate by ID for a non-administrator, when the token doesn't exist or they can't read it (lib/api/personal_access_tokens.rb:71 and :100), and user_id naming another user (lib/api/helpers/personal_access_tokens_helpers.rb:80).
  • Emoji reactions: delete one somebody else awarded (lib/api/award_emoji.rb:136).
  • Groups: update by a user who can administer the group's runners but not the group, when the request changes anything other than shared_runners_setting (lib/api/groups.rb:100). A user with neither permission gets 403 from authorize_any! at lib/api/groups.rb:377, in the same route.
  • Projects: create a fork relationship when the project's namespace is not a valid fork target for the user, with the reason Target Namespace (lib/api/projects.rb:956). Forking a project into such a namespace answers 404 instead (:687).
  • External status checks: every route when the project lacks the licensed feature (ee/lib/api/status_checks.rb:16), the list without read_external_status_check (ee/lib/api/status_checks.rb:67), and the update, whose service builds its own 401 (ee/app/services/external_status_checks/update_service.rb:40). The create and delete routes are the subject of !260486 (merged).
  • Security: security scans (ee/lib/api/security_scans.rb:55), project security settings (ee/lib/api/project_security_settings.rb:30 and :56), and group security settings (ee/lib/api/group_security_settings.rb:37).
  • SAML group links: all four routes (ee/lib/api/saml_group_links.rb:44, :76, :115 and :144). #383531 reports a 401 from one of these routes as misleading.
  • MLflow: check_api_write! and check_api_model_registry_write! (lib/api/ml/mlflow/api_helpers.rb:15 and :23).
  • Code Suggestions: direct access for a user who isn't allowed to use Code Suggestions (ee/lib/api/code_suggestions.rb:296).
  • authorize_manage_saml! in ee/lib/ee/api/helpers.rb:191-193 does the same, but no API route calls it.
  • #383531: a 401 from the SAML group links API, reported as misleading.
  • #624056: agreeing a consistent 401 and 403 error shape for the modular feature APIs.

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.

Merge request reports

Loading
Loading