Add scope checks when using Dependency Proxy with group access tokens
🌱
Context We added Dependency Proxy support for group access tokens in !128583 (merged) but didn't add checks on the token scopes.
This MR adds those checks, and also adds missing controller specs for deploy tokens.
Dependency Proxy Authentication Overview
This blog post gives a brief overview of Dependency Proxy authentication.
To pull an image from Dependency Proxy, the user does a docker login <gitlab_url> -p <token>
, and then docker pull <gitlab_url>/<group_path>/dependency_proxy/containers/<image_name>:<image_tag>
What happens behind the scenes:
docker login
:
-
/v2/
->Groups::DependencyProxyAuthController#authenticate
-
/jwt/auth
->JwtController#auth
docker pull
:
-
/v2/
->Groups::DependencyProxyAuthController#authenticate
-
/jwt/auth
->JwtController#auth
-
/v2/<group_path>/dependency_proxy/containers/<image_name>/manifests/<image_tag>
->Groups::DependencyProxyForContainersController#manifest
Problem
docker login
and docker pull
requests from a group access token with insufficient scopes for Dependency Proxy are not rejected.
When dealing with a group access token in a policy (in GroupPolicy, to be specific), we are passed only the token user and the dependency proxy group. We do not have a handle to the actual token. We considered doing bot_user.personal_access_tokens.first
but decided it will not work, because it is possible to add tokens even to a bot user. As such, we cannot determine if a group access token has the correct scopes in a policy code. We'll have to do the checking in the controller.
Solution
- Inside the
authenticate_with_http_basic
block inJwtController
, memoizeraw_token
- Include
raw_token
in the parameters passed toAuth::DependencyProxyAuthenticationService
- Do
PersonalAccessToken.find_by_token(raw_token)
inAuth::DependencyProxyAuthenticationService
- In the policy, because we cannot get a handle to the token, we only check if the project bot user is a member of the subject (dependency proxy group)
In JwtController#auth
we verify:
- if the token is an active token
- if the token has the required scopes for dependency proxy
- we do not verify if the token user has the correct access level to the dependency proxy group
In GroupPolicy
we verify:
- if the token is an active token
- if the token user is a member of the dependency proxy group
- Because we do not have a handle to the token,
- we cannot verify scopes
- we cannot verify if the token is active or not
Taken together, our checks in JwtController#auth
and in GroupPolicy
will correctly reject
- tokens that are not active
- tokens without the required scopes for dependency proxy
- tokens that are not a group access token for the dependency proxy group
MR Summary
- Modify
JwtController
to pass the raw token toAuth::DependencyProxyAuthenticationService
- Modify
Auth::DependencyProxyAuthenticationService
- reject the login if the passed project bot or deploy token does not have the required scopes. Also refactored to make the 3 checks clearer (human user, group access token user, deploy token) - Refactor the dependency proxy policy to make it clearer what we're checking for:
- a valid human user, or
- a valid group access token, or
- a valid deploy token
- Modify
Auth::DependencyProxyAuthenticationService
specs- Instead of simply testing for a non-nil token, test that we're putting in the correct content in the encoded token (user_id for personal access tokens, deploy_token for deploy tokens)
- Update the group policy specs to test that we pass/fail correctly for human users, group access token, and deploy tokens
- Update the
Groups::DependencyProxyForContainers
specs- Add missing specs that validates that deploy tokens are authorized uploads
- Add specs for invalid group access tokens
- Add specs for valid group access tokens
How to set up and validate locally
Enable Dependency Proxy for a group
docker login
A group access token with insufficient scopes should fail - Create a group access token for the group. Give it only the
read_registry
scope. - Clear docker credentials:
docker logout http://gdk.test:3000
- Login:
docker login http://gdk.test:3000 -p <group_access_token>
. The login should fail.
docker pull
A group access token with sufficient scopes should be able to - Create another group access token, this time give it both
read_registry
andwrite_registry
scopes. - Login with the newer token. It should be successful.
- Pull an image:
docker pull gdk.test:3000/<group-namespace>/dependency_proxy/containers/alpine:latest
. This should also be successful. - Open the group dependency proxy page (Group home -> Operate -> Dependency Proxy, or
http://gdk.test:3000/groups/<group-namespace>/-/dependency_proxy
), you should see the pulled image in the list of images
docker login
and docker pull
A revoked group access token, even with sufficient scopes, should fail - Revoke the token used for the successful pull in the previous step. From the Rails console, run
PersonalAccessToken.last.revoke!
- Try the
docker pull
again:docker pull gdk.test:3000/<group-namespace>/dependency_proxy/containers/alpine:latest
. This time the operation should fail with a forbidden response. This confirms that we do our auth checks for every operation, even if the user is already logged in. - Try
docker login
again with the token. This time the login should fail.
api
scope should be able to docker pull
A group access token with The api
scope grants read_registry
and write_registry
access (documentation).
- Create a new group access token, this time give it only the
api
scope. -
docker login
anddocker pull
should be both successful just like A group access token with sufficient scopes
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.
Related to #332411