Skip to content

Embed project path in Container Registry JWT tokens

João Pereira requested to merge 406795 into master

What does this MR do and why?

This MR updates the Container Registry JWT tokens emitted by Rails so that they include a new project_path attribute. This attribute is filled with the full path of the project that the target container repository belongs to. Because we expect the need to embed additional metadata in these JWT tokens in the future (e.g. #394965 (closed)) this new project_path attribute has been encapsulated in a meta object, purely for organization reasons.

The information about the project path will then be parsed by the registry on a future iteration and used for different purposes, such as #406795 (closed) and logging/metrics (so far we were unable to aggregate data by GitLab project on the registry side because they have disjoint datasets and the latter knows nothing about the former).

Related to #406795 (closed).

How to set up and validate locally

We'll assume the existence of the gitlab-org/gitlab-test project in your GDK instance and use it for these tests. You can pick any other project, as long as the Container Registry feature is enabled for it.

We'll also assume that your GDK Rails instance is listening at gdk.test:3000 and you have a user root with a PAT foobar, with read permissions on the container registry. Your GDK registry setup must have auth_enabled: true (docs).

We need to query the Rails API and decode a few JWT tokens during these tests. To do so you can either decode it with a JWT debugger on the web (e.g. https://jwt.io/) or use a CLI tool such as jwt-cli. We'll use the latter. We'll also use httpie to make the requests and jq to parse the token from the response, but you can do that with any other tools or manually.

Current behavior

  1. Checkout the master branch of this repository.

  2. Open up a Rails console and a terminal window/pane.

  3. On your terminal, obtain a JWT token with e.g. pull permissions for repository gitlab-org/gitlab-test/foo. This will test the flow for external authentication requests, and therefore the changes we're making within Auth::ContainerRegistryAuthenticationService#process_repository_access:

    http -a root:foobar http://gdk.test:3000/jwt/auth \
        client_id=="docker" \
        service=="container_registry" \
        scope=="repository:gitlab-org/gitlab-test/foo:pull" | jq -r '.token' | jwt decode -

    Output (timestamp fields will vary, the important part is access):

    Token header
    ------------
    ...
    
    Token claims
    ------------
    {
      "access": [
        {
          "actions": [
            "pull"
          ],
          "name": "gitlab-org/gitlab-test/foo",
          "type": "repository"
        }
      ],
      "aud": "container_registry",
      "auth_type": "gitlab_or_ldap",
      "exp": 1683211351,
      "iat": 1683211051,
      "iss": "gitlab-issuer",
      "jti": "a5c7ef75-b55b-492b-b046-5e41174f9f79",
      "nbf": 1683211046,
      "sub": "root"
    }
  4. On the Rails console, go ahead and request an e.g. full_access_token for the same repository. This will simulate the emission of a token for internal (i.e. Rails backend) use, and therefore will test the changes we've made to Auth::ContainerRegistryAuthenticationService#access_token:

    [1] pry(main)> pp JWT.decode(Auth::ContainerRegistryAuthenticationService.full_access_token('gitlab-org/gitlab-test/foo'), nil, false)
    [{"access"=>
      [{"type"=>"repository",
        "name"=>"gitlab-org/gitlab-test/foo",
        "actions"=>["*"]}],
      "jti"=>"1d334ca7-4d4d-4821-afbe-29cc4cd4a886",
      "aud"=>"container_registry",
      "iss"=>"gitlab-issuer",
      "iat"=>1683211616,
      "nbf"=>1683211611,
      "exp"=>1683211916},
    {"kid"=>"45EW:46S7:ZVAG:JLZA:CTLV:WT47:YBBK:FYTI:UDQO:WVAB:KQVT:RXNB",
      "typ"=>"JWT",
      "alg"=>"RS256"}]

New behavior

  1. Check out this MR's branch.

  2. Repeat the /jwt/auth request. This time we see the new meta object and its repository_path attribute being filled:

    Token header
    ------------
    ...
    
    Token claims
    ------------
    {
      "access": [
        {
          "actions": [
            "pull"
          ],
          "meta": {
            "project_path": "gitlab-org/gitlab-test"
          },
          "name": "gitlab-org/gitlab-test/foo",
          "type": "repository"
        }
      ],
      "aud": "container_registry",
      "auth_type": "gitlab_or_ldap",
      "exp": 1683211548,
      "iat": 1683211248,
      "iss": "gitlab-issuer",
      "jti": "7d2bdf89-a662-44b5-8172-d19717148869",
      "nbf": 1683211243,
      "sub": "root"
    }
  3. Do the same on the Rails console:

    [1] pry(main)> pp JWT.decode(Auth::ContainerRegistryAuthenticationService.full_access_token('gitlab-org/gitlab-test/foo'), nil, false)
    [{"access"=>
      [{"type"=>"repository",
        "name"=>"gitlab-org/gitlab-test/foo",
        "actions"=>["*"],
        "meta"=>{"project_path"=>"gitlab-org/gitlab-test"}}],
      "jti"=>"0e023603-8030-46fe-af0d-c53799a5a299",
      "aud"=>"container_registry",
      "iss"=>"gitlab-issuer",
      "iat"=>1683211762,
      "nbf"=>1683211757,
      "exp"=>1683212062},
    {"kid"=>"45EW:46S7:ZVAG:JLZA:CTLV:WT47:YBBK:FYTI:UDQO:WVAB:KQVT:RXNB",
      "typ"=>"JWT",
      "alg"=>"RS256"}]
  4. You can also check that the registry is OK with receiving the modifying tokens (unknown attributes are ignored) by e.g. listing the tags of a given container repository, which internally will trigger the generation of a full_access_token and send a request to the registry with it:

    [7] pry(main)> repo = ContainerRepository.find_by_path(ContainerRegistry::Path.new('gitlab-org/gitlab-test/foo'))
    => #<ContainerRepository:0x000000014354b9f8
      id: 5,
      project_id: 2,
      name: "foo",
      created_at: Wed, 15 Mar 2023 13:00:17.206705000 UTC +00:00,
      updated_at: Wed, 15 Mar 2023 13:00:17.206705000 UTC +00:00,
      status: nil,
      expiration_policy_started_at: Mon, 13 Mar 2023 15:11:37.871130000 UTC +00:00,
      expiration_policy_cleanup_status: "cleanup_unfinished",
      expiration_policy_completed_at: nil,
      migration_pre_import_started_at: nil,
      migration_pre_import_done_at: nil,
      migration_import_started_at: nil,
      migration_import_done_at: nil,
      migration_aborted_at: nil,
      migration_skipped_at: nil,
    [8] pry(main)> repo.tags.count
    => 24

MR acceptance checklist

This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by João Pereira

Merge request reports