Embed project path in Container Registry JWT tokens
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
-
Checkout the
master
branch of this repository. -
Open up a Rails console and a terminal window/pane.
-
On your terminal, obtain a JWT token with e.g.
pull
permissions for repositorygitlab-org/gitlab-test/foo
. This will test the flow for external authentication requests, and therefore the changes we're making withinAuth::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" }
-
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 toAuth::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
-
Check out this MR's branch.
-
Repeat the
/jwt/auth
request. This time we see the newmeta
object and itsrepository_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" }
-
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"}]
-
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.
-
I have evaluated the MR acceptance checklist for this MR.