Skip to content

Fix 403 error when using job token to access public or internal project

What does this MR do and why?

Problem

Users reported that they face a 403 forbidden error when trying to reference a terraform module from a public or internal project.

After some investigation, it turned out that once a new public or internal project is created, its project_feature.package_registry_access_level is set to ProjectFeature::ENABLED. However, we grant the :read_package ability only if the project's project_feature.package_registry_access_level is ProjectFeature::PUBLIC.

Why does this happen only when authenticating with a job token? because any user authenticated by a job token is restricted from public access unless it is explicitly allowed.

The flow can be summarized as follows:

  • A job in project 1 is trying to get the versions of a terraform module, and this module is in public project 2:
stages:
  - build
build:
  stage: build
  image: curlimages/curl:latest
  script:
    - curl -v "${CI_API_V4_URL}/packages/terraform/modules/v1/public-namespace/infra-registry/aws/versions" --header "Authorization:Bearer $CI_JOB_TOKEN"
  only:
    - main
  • Since project 2 is public & its package registry is enabled, the expected behavior is that the job will succeed and the request will return a 200 response, however, a 403 forbidden response is returned in the above job.
  • When the request goes through GitLab, the authentication gate starts to grant/prevent abilities to the user.
  • This rule would prevent a couple of abilities because the user is authenticated using a job token. As mentioned above, GitLab should check if project 1 can access project 2 using this job token. The prevented :public_access ability is the one that's responsible for making the job token authenticated requests fail while the other token authenticated requests pass. Because in the case of the other token authenticated requests, the :public_access ability is granted.
  • For the :read_package ability, the Packages::Policies::ProjectPolicy is responsible for figuring out if it's allowed or not. And as we can see in this condition, the policy allows it if the project's package_registry_access_level is ProjectFeature::PUBLIC, which is not the case in the newly created public projects; their package_registry_access_level is ProjectFeature::ENABLED.

Solution

Add a new rule in the Packages::Policies::ProjectPolicy to grant the :read_package ability if the project is public or internal and its job_token_package_registry is true.

MR acceptance checklist

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

Screenshots or screen recordings

Screenshots are required for UI changes, and strongly recommended for all other merge requests.

Before After

How to set up and validate locally

  1. In rails console, create a terraform module in a public project & a ci build to test with its token:
# stub file upload
def fixture_file_upload(*args, **kwargs)
  Rack::Test::UploadedFile.new(*args, **kwargs)
end

project = FactoryBot.create(:project, :public)
terraform_module = FactoryBot.create(:terraform_module_package, project: project)

# keep the namespace full path to use it below in the curl command in step 2
full_path = project.namespace.full_path

# keep the terraform_module name to use it below in the curl command in step 2
terraform_module_name = terraform_module.name


ci_build = FactoryBot.create(:ci_build, :running, user: User.first)
# keep the ci_build token to use it below in the curl command in step 2
ci_build_token = ci_build.token
  1. In the terminal, query the versions endpoint for the terraform module you just created:
curl "http://gdk.test:3000/api/v4/packages/terraform/modules/v1/<full_path>/<terraform_module_name>/versions" --header "Authorization:Bearer <ci_build_token>"
  1. On the master branch, the above curl query will result in {"message":"403 Forbidden"} error. Switch to this MR branch and repeat the query: it should return 200 with the correct response:
{"modules":[{"versions":[{"version":"1.0.0","submodules":[],"root":{"dependencies":[],"providers":[{"name":"system","version":""}]}}],"source":"http://gdk.test:3000/namespace12/project-12"}]}

Related to #435647 (closed) & #438586 (closed)

Edited by Moaz Khalifa

Merge request reports