diff --git a/app/controllers/concerns/oauth_applications.rb b/app/controllers/concerns/oauth_applications.rb index 42d0948adf808d075242b6b017f8c35f9d39bbfb..5a78deecfea9186d6e9c9d6a854368d658e659e8 100644 --- a/app/controllers/concerns/oauth_applications.rb +++ b/app/controllers/concerns/oauth_applications.rb @@ -25,7 +25,11 @@ def get_created_session def load_scopes @scopes ||= Doorkeeper::OAuth::Scopes.from_array( - Doorkeeper.configuration.scopes.to_a - [::Gitlab::Auth::AI_WORKFLOW.to_s] - [::Gitlab::Auth::DYNAMIC_USER.to_s] + Doorkeeper.configuration.scopes.to_a - [ + ::Gitlab::Auth::AI_WORKFLOW.to_s, + ::Gitlab::Auth::DYNAMIC_USER.to_s, + ::Gitlab::Auth::SELF_ROTATE_SCOPE.to_s + ] ) end diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index 624955ed912926c29a2d13d69ec05c89c46375d7..201da8bd9fcd77ba59027af6a7f7f92454a5ac05 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -81,6 +81,7 @@ en: k8s_proxy: Grants permission to perform Kubernetes API calls using the agent for Kubernetes. ai_features: Access to API endpoints needed for GitLab Duo features read_service_ping: Grant access to download Service Ping payload via API when authenticated as an admin user + self_rotate: Grants permission for token to rotate itself user:*: Grants access only if both the token owner and user: has access to the resource. scope_desc: api: Grants complete read/write access to the API, including all groups and projects, the container registry, the dependency proxy, and the package registry. @@ -102,6 +103,7 @@ en: manage_runner: Grants access to manage the runners. k8s_proxy: Grants permission to perform Kubernetes API calls using the agent for Kubernetes. read_service_ping: Grant access to download Service Ping payload via API when authenticated as an admin user + self_rotate: Grants permission for token to rotate itself. group_access_token_scope_desc: api: Grants complete read and write access to the scoped group and related project API, including the container registry, the dependency proxy, and the package registry. read_api: Grants read access to the scoped group and related project API, including the package registry. @@ -121,6 +123,7 @@ en: create_runner: Grants permission to create runners in a group. manage_runner: Grants access to manage the runners in a group. k8s_proxy: Grants permission to perform Kubernetes API calls using the agent for Kubernetes in a group. + self_rotate: Grants permission for token to rotate itself. project_access_token_scope_desc: api: Grants complete read and write access to the scoped project API, including the container registry, the dependency proxy, and the package registry. read_api: Grants read access to the scoped project API, including the Package Registry. @@ -134,6 +137,7 @@ en: manage_runner: Grants access to manage the runners. k8s_proxy: Grants permission to perform Kubernetes API calls using the agent for Kubernetes. ai_features: Grants access to GitLab Duo related API endpoints. + self_rotate: Grants permission for token to rotate itself. flash: applications: create: diff --git a/doc/api/group_access_tokens.md b/doc/api/group_access_tokens.md index d1a2ce430e7715cf18aa84de9d6b87b600804cdc..971928f0d6476a010c91f7df97ff0a0a1c7346aa 100644 --- a/doc/api/group_access_tokens.md +++ b/doc/api/group_access_tokens.md @@ -146,14 +146,21 @@ curl --request POST --header "PRIVATE-TOKEN: " \ ## Rotate a group access token +Rotate a group access token. Revokes the previous token and creates a new token that expires in one week. + +You can either: + +- Use the group access token ID. +- In GitLab 17.9 and later, pass the group access token to the API in a request header. + +### Use a group access token ID + > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/403042) in GitLab 16.0 Prerequisites: - You must have a [personal access token with the `api` scope](../user/profile/personal_access_tokens.md#personal-access-token-scopes). -Rotate a group access token. Revokes the previous token and creates a new token that expires in one week. - In GitLab 16.6 and later, you can use the `expires_at` parameter to set a different expiry date. This non-default expiry date can be up to a maximum of one year from the rotation date. ```plaintext @@ -167,7 +174,8 @@ POST /groups/:id/access_tokens/:token_id/rotate | `expires_at` | date | no | Expiration date of the access token in ISO format (`YYYY-MM-DD`). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/416795) in GitLab 16.6. If undefined, the token expires after one week. | ```shell -curl --request POST --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/groups//access_tokens//rotate" +curl --request POST --header "PRIVATE-TOKEN: " \ +"https://gitlab.example.com/api/v4/groups//access_tokens//rotate" ``` Example response: @@ -178,6 +186,7 @@ Example response: "name": "Rotated Token", "revoked": false, "created_at": "2023-08-01T15:00:00.000Z", + "description": "Test group access token", "scopes": ["api"], "user_id": 1337, "last_used_at": null, @@ -188,15 +197,69 @@ Example response: } ``` -### Responses +#### Responses - `200: OK` if existing token is successfully revoked and the new token is created. - `400: Bad Request` if not rotated successfully. - `401: Unauthorized` if either the: - User does not have access to the token with the specified ID. - Token with the specified ID does not exist. +- `401: Unauthorized` if any of the following conditions are true: + - You do not have access to the specified token. + - The specified token does not exist. + - You're authenticating with a group access token. Use [`/groups/:id/access_tokens/self/rotate`](#use-a-request-header). instead. - `404: Not Found` if the user is an administrator but the token with the specified ID does not exist. +### Use a request header + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/178111) in GitLab 17.9 + +Requires: + +- `api` or `self_rotate` scope. + +In GitLab 16.6 and later, you can use the `expires_at` parameter to set a different expiry date. This non-default expiry date is subject to the [maximum allowable lifetime limits](../user/profile/personal_access_tokens.md#access-token-expiration). + +```plaintext +POST /groups/:id/access_tokens/self/rotate +``` + +```shell +curl --request POST --header "PRIVATE-TOKEN: " \ +"https://gitlab.example.com/api/v4/groups//access_tokens/self/rotate" +``` + +Example response: + +```json +{ + "id": 42, + "name": "Rotated Token", + "revoked": false, + "created_at": "2025-01-19T15:00:00.000Z", + "description": "Test group access token", + "scopes": ["read_api","self_rotate"], + "user_id": 1337, + "last_used_at": null, + "active": true, + "expires_at": "2025-01-26", + "access_level": 30, + "token": "s3cr3t" +} +``` + +#### Responses + +- `200: OK` if the existing group access token is successfully revoked and the new token successfully created. +- `400: Bad Request` if not rotated successfully. +- `401: Unauthorized` if any of the following conditions are true: + - The token does not exist. + - The token has expired. + - The token was revoked. + - The token is not a group access token associated with the specified group. +- `403: Forbidden` if the token is not allowed to rotate itself. +- `405: Method Not Allowed` if the token is not an access token. + ### Automatic reuse detection Refer to [automatic reuse detection for personal access tokens](personal_access_tokens.md#automatic-reuse-detection) diff --git a/doc/api/personal_access_tokens.md b/doc/api/personal_access_tokens.md index c5fdfd89d76a7424cbd6d4ca66f6d0b2d726f2da..df5bcc40ff0c8bfcd0d81db17b47e7bb2216bd9e 100644 --- a/doc/api/personal_access_tokens.md +++ b/doc/api/personal_access_tokens.md @@ -275,7 +275,7 @@ Example response: Requires: -- `api` scope. +- `api` or `self_rotate` scope. In GitLab 16.6 and later, you can use the `expires_at` parameter to set a different expiry date. This non-default expiry date is subject to the [maximum allowable lifetime limits](../user/profile/personal_access_tokens.md#access-token-expiration). @@ -300,7 +300,7 @@ Example response: "user_id": 1337, "last_used_at": null, "active": true, - "expires_at": "2023-08-15", + "expires_at": "2023-08-08", "token": "s3cr3t" } ``` diff --git a/doc/api/project_access_tokens.md b/doc/api/project_access_tokens.md index 85c67846822b42b280aeca749f2d9081d9055a4c..7f2c5dd1cb780fd24e021adcd8128617c1afd0cc 100644 --- a/doc/api/project_access_tokens.md +++ b/doc/api/project_access_tokens.md @@ -152,14 +152,21 @@ curl --request POST --header "PRIVATE-TOKEN: " \ ## Rotate a project access token +Rotate a project access token. Revokes the previous token and creates a new token that expires in one week. + +You can either: + +- Use a project access token ID. +- In GitLab 17.9 and later, pass the project access token to the API in a request header. + +### Use a project access token ID + > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/403042) in GitLab 16.0 Prerequisites: - You must have a [personal access token with the `api` scope](../user/profile/personal_access_tokens.md#personal-access-token-scopes). -Rotate a project access token. Revokes the previous token and creates a new token that expires in one week. - In GitLab 16.6 and later, you can use the `expires_at` parameter to set a different expiry date. This non-default expiry date can be up to a maximum of one year from the rotation date. ```plaintext @@ -173,7 +180,8 @@ POST /projects/:id/access_tokens/:token_id/rotate | `expires_at` | date | no | Expiration date of the access token in ISO format (`YYYY-MM-DD`). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/416795) in GitLab 16.6. If undefined, the token expires after one week. | ```shell -curl --request POST --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects//access_tokens//rotate" +curl --request POST --header "PRIVATE-TOKEN: " \ +"https://gitlab.example.com/api/v4/projects//access_tokens//rotate" ``` Example response: @@ -184,6 +192,7 @@ Example response: "name": "Rotated Token", "revoked": false, "created_at": "2023-08-01T15:00:00.000Z", + "description": "Test project access token", "scopes": ["api"], "user_id": 1337, "last_used_at": null, @@ -194,15 +203,69 @@ Example response: } ``` -### Responses +#### Responses - `200: OK` if the existing token is successfully revoked and the new token is successfully created. - `400: Bad Request` if not rotated successfully. - `401: Unauthorized` if either the: - User does not have access to the token with the specified ID. - Token with the specified ID does not exist. +- `401: Unauthorized` if any of the following conditions are true: + - You do not have access to the specified token. + - The specified token does not exist. + - You're authenticating with a project access token. Use [`/projects/:id/access_tokens/self/rotate`](#use-a-request-header). instead. - `404: Not Found` if the user is an administrator but the token with the specified ID does not exist. +### Use a request header + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/178111) in GitLab 17.9 + +Requires: + +- `api` or `self_rotate` scope. + +In GitLab 16.6 and later, you can use the `expires_at` parameter to set a different expiry date. This non-default expiry date is subject to the [maximum allowable lifetime limits](../user/profile/personal_access_tokens.md#access-token-expiration). + +```plaintext +POST /projects/:id/access_tokens/self/rotate +``` + +```shell +curl --request POST --header "PRIVATE-TOKEN: " \ +"https://gitlab.example.com/api/v4/projects//access_tokens/self/rotate" +``` + +Example response: + +```json +{ + "id": 42, + "name": "Rotated Token", + "revoked": false, + "created_at": "2025-01-19T15:00:00.000Z", + "description": "Test project access token", + "scopes": ["read_api","self_rotate"], + "user_id": 1337, + "last_used_at": null, + "active": true, + "expires_at": "2025-01-26", + "access_level": 30, + "token": "s3cr3t" +} +``` + +#### Responses + +- `200: OK` if the existing project access token is successfully revoked and the new token successfully created. +- `400: Bad Request` if not rotated successfully. +- `401: Unauthorized` if any of the following conditions are true: + - The token does not exist. + - The token has expired. + - The token was revoked. + - The token is not a project access token associated with the specified project. +- `403: Forbidden` if the token is not allowed to rotate itself. +- `405: Method Not Allowed` if the token is not a project access token. + ### Automatic reuse detection Refer to [automatic reuse detection for personal access tokens](personal_access_tokens.md#automatic-reuse-detection) diff --git a/doc/user/group/settings/group_access_tokens.md b/doc/user/group/settings/group_access_tokens.md index 3b19d21dcc7b059058b18c90f793fda9a2f8d3d0..b924d77747306b303b0bc3214a242d3ffb174c8d 100644 --- a/doc/user/group/settings/group_access_tokens.md +++ b/doc/user/group/settings/group_access_tokens.md @@ -147,6 +147,7 @@ To revoke or rotate a group access token: > - `k8s_proxy` [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/422408) in GitLab 16.4 [with a flag](../../../administration/feature_flags.md) named `k8s_proxy_pat`. Enabled by default. > - Feature flag `k8s_proxy_pat` [removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131518) in GitLab 16.5. +> - `self_rotate` [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/178111) in GitLab 17.9. Enabled by default. The scope determines the actions you can perform when you authenticate with a group access token. @@ -162,6 +163,7 @@ The scope determines the actions you can perform when you authenticate with a gr | `manage_runner` | Grants permission to manage runners in a group. | | `ai_features` | Grants permission to perform API actions for GitLab Duo. This scope is designed to work with the GitLab Duo Plugin for JetBrains. For all other extensions, see scope requirements. | | `k8s_proxy` | Grants permission to perform Kubernetes API calls using the agent for Kubernetes in a group. | +| `self_rotate` | Grants permission to rotate this token using the [personal access token API](../../../api/personal_access_tokens.md#use-a-request-header). Does not allow rotation of other tokens. | ## Restrict the creation of group access tokens diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md index 4db5d9d8e8d8986c9d23f251c1bbc7748326d57f..e6f8b9a15a2f47e5a6ccc396b6b3b828795df4ba 100644 --- a/doc/user/profile/personal_access_tokens.md +++ b/doc/user/profile/personal_access_tokens.md @@ -187,6 +187,7 @@ To view the last time a token was used, and the IP addresses from where the toke > - Feature flag `k8s_proxy_pat` [removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131518) in GitLab 16.5. > - `read_service_ping` [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/42692#note_1222832412) in GitLab 17.1. > - `manage_runner` [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/460721) in GitLab 17.1. +> - `self_rotate` [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/178111) in GitLab 17.9. Enabled by default. A personal access token can perform actions based on the assigned scopes. @@ -205,6 +206,7 @@ A personal access token can perform actions based on the assigned scopes. | `manage_runner` | Grants permission to manage runners. | | `ai_features` | This scope:
- Grants permission to perform API actions for features like GitLab Duo, Code Suggestions API and Duo Chat API.
- Does not work for GitLab Self-Managed versions 16.5, 16.6, and 16.7.
For GitLab Duo plugin for JetBrains, this scope:
- Supports users with AI features enabled in the GitLab Duo plugin for JetBrains.
- Addresses a security vulnerability in JetBrains IDE plugins that could expose personal access tokens.
- Is designed to minimize potential risks for GitLab Duo plugin users by limiting the impact of compromised tokens.
For all other extensions, see the individual scope requirements in their documentation. | | `k8s_proxy` | Grants permission to perform Kubernetes API calls using the agent for Kubernetes. | +| `self_rotate` | Grants permission to rotate this token using the [personal access token API](../../api/personal_access_tokens.md#use-a-request-header). Does not allow rotation of other tokens. | | `read_service_ping`| Grant access to download Service Ping payload through the API when authenticated as an admin use. | WARNING: diff --git a/doc/user/project/settings/project_access_tokens.md b/doc/user/project/settings/project_access_tokens.md index d80d793510e4565ba158c6899e9bbb1157dd3909..50fa99110750829e7c19bf166375c391fced8ce5 100644 --- a/doc/user/project/settings/project_access_tokens.md +++ b/doc/user/project/settings/project_access_tokens.md @@ -100,6 +100,7 @@ To revoke or rotate a project access token: > - `k8s_proxy` [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/422408) in GitLab 16.4 [with a flag](../../../administration/feature_flags.md) named `k8s_proxy_pat`. Enabled by default. > - Feature flag `k8s_proxy_pat` [removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131518) in GitLab 16.5. +> - `self_rotate` [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/178111) in GitLab 17.9. Enabled by default. The scope determines the actions you can perform when you authenticate with a project access token. @@ -118,6 +119,7 @@ See the warning in [create a project access token](#create-a-project-access-toke | `manage_runner` | Grants permission to manage runners in the project. | | `ai_features` | Grants permission to perform API actions for GitLab Duo. This scope is designed to work with the GitLab Duo Plugin for JetBrains. For all other extensions, see scope requirements. | | `k8s_proxy` | Grants permission to perform Kubernetes API calls using the agent for Kubernetes in the project. | +| `self_rotate` | Grants permission to rotate this token using the [personal access token API](../../../api/personal_access_tokens.md#use-a-request-header). Does not allow rotation of other tokens. | ## Restrict the creation of project access tokens diff --git a/lib/api/api.rb b/lib/api/api.rb index aed3a2befd7c7235dcd110a5596a695678be64a4..e15eaf166c2c742829afd3528679126193605e95 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -332,6 +332,7 @@ def initialize(location_url) mount ::API::Release::Links mount ::API::RemoteMirrors mount ::API::Repositories + mount ::API::ResourceAccessTokens::SelfRotation mount ::API::ResourceAccessTokens mount ::API::ResourceMilestoneEvents mount ::API::RpmProjectPackages diff --git a/lib/api/personal_access_tokens/self_rotation.rb b/lib/api/personal_access_tokens/self_rotation.rb index da21fca8b554be5689c42db671481850129db76d..320af5f1a314ad57465d1c8a1f9d738131a21bbd 100644 --- a/lib/api/personal_access_tokens/self_rotation.rb +++ b/lib/api/personal_access_tokens/self_rotation.rb @@ -10,6 +10,7 @@ class SelfRotation < ::API::Base helpers ::API::Helpers::PersonalAccessTokensHelpers allow_access_with_scope :api + allow_access_with_scope :self_rotate before { authenticate! } @@ -33,7 +34,6 @@ class SelfRotation < ::API::Base end post 'self/rotate' do not_allowed! unless access_token.is_a? PersonalAccessToken - forbidden! if current_user.project_bot? new_token = rotate_token(access_token, declared_params) diff --git a/lib/api/resource_access_tokens/self_rotation.rb b/lib/api/resource_access_tokens/self_rotation.rb new file mode 100644 index 0000000000000000000000000000000000000000..a2ec067bd1e9e87f048f778bf83d8223cce11638 --- /dev/null +++ b/lib/api/resource_access_tokens/self_rotation.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module API + class ResourceAccessTokens + class SelfRotation < ::API::Base + include APIGuard + + feature_category :system_access + + helpers ::API::Helpers::PersonalAccessTokensHelpers + helpers ::API::ResourceAccessTokens.helpers + + allow_access_with_scope :api + allow_access_with_scope :self_rotate + + before { authenticate! } + + %w[project group].each do |source_type| + resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + desc 'Rotate a resource access token' do + detail 'Rotates a resource access token by passing it to the API in a header' + success code: 200, model: Entities::ResourceAccessTokenWithToken + failure [ + { code: 400, message: 'Bad Request' }, + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 405, message: 'Method not allowed' } + ] + tags %w[personal_access_tokens] + end + params do + requires :id, type: String, desc: "The #{source_type} ID" + optional :expires_at, + type: Date, + desc: "The expiration date of the token", + documentation: { example: '2021-01-31' } + end + post ':id/access_tokens/self/rotate' do + not_allowed! unless access_token.is_a? PersonalAccessToken + not_allowed! unless current_user.project_bot? + + resource = find_source(source_type, params[:id]) + token = find_token(resource, access_token.id) + + unauthorized! unless token + + new_token = rotate_token(token, declared_params) + + present new_token, with: Entities::ResourceAccessTokenWithToken, resource: resource + end + end + end + end + end +end diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index b7dec9bce85fcbd9c57c7b323685550a225058c1..d16ee796bea04127040f07481417be8ac7073999 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -7,6 +7,9 @@ module Auth # Scopes used for GitLab internal API (Kubernetes cluster access) K8S_PROXY_SCOPE = :k8s_proxy + # Scopes used for token allowed to rotate themselves + SELF_ROTATE_SCOPE = :self_rotate + # Scopes used for GitLab API access API_SCOPE = :api READ_API_SCOPE = :read_api @@ -17,7 +20,8 @@ module Auth API_SCOPE, READ_API_SCOPE, READ_USER_SCOPE, CREATE_RUNNER_SCOPE, MANAGE_RUNNER_SCOPE, - K8S_PROXY_SCOPE + K8S_PROXY_SCOPE, + SELF_ROTATE_SCOPE ].freeze # Scopes for Duo diff --git a/lib/gitlab/auth/auth_finders.rb b/lib/gitlab/auth/auth_finders.rb index be3dbb507e583907cb9ad423064761432665dcf2..beb13c7e981114d7465059337d673caf7791cbfd 100644 --- a/lib/gitlab/auth/auth_finders.rb +++ b/lib/gitlab/auth/auth_finders.rb @@ -480,8 +480,7 @@ def revoke_token_family(token) end def access_token_rotation_request? - current_request.path.match(%r{access_tokens/\d+/rotate$}) || - current_request.path.match(%r{/personal_access_tokens/self/rotate$}) + current_request.path.match(%r{access_tokens/(\d+|self)/rotate$}) end # To prevent Rack Attack from incorrectly rate limiting diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 06b33d5e9cb50ebe28ec9ccda1babae360b76bb8..1747a04c643a9fa80157f0728ee19d5c18aa9833 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -15,7 +15,7 @@ describe 'constants' do it 'API_SCOPES contains all scopes for API access' do - expect(subject::API_SCOPES).to match_array %i[api read_user read_api create_runner manage_runner k8s_proxy] + expect(subject::API_SCOPES).to match_array %i[api read_user read_api create_runner manage_runner k8s_proxy self_rotate] end it 'ADMIN_SCOPES contains all scopes for ADMIN access' do @@ -52,6 +52,7 @@ expect(subject.all_available_scopes).to match_array %i[ api read_user read_api read_repository read_service_ping write_repository read_registry write_registry sudo admin_mode read_observability write_observability create_runner manage_runner k8s_proxy ai_features + self_rotate ] end @@ -60,7 +61,7 @@ expect(subject.available_scopes_for(user)).to match_array %i[ api read_user read_api read_repository write_repository read_registry write_registry - create_runner manage_runner k8s_proxy ai_features + create_runner manage_runner k8s_proxy ai_features self_rotate ] end @@ -69,7 +70,7 @@ expect(subject.available_scopes_for(user)).to match_array %i[ api read_user read_api read_repository read_service_ping write_repository read_registry write_registry - sudo admin_mode create_runner manage_runner k8s_proxy ai_features + sudo admin_mode create_runner manage_runner k8s_proxy ai_features self_rotate ] end @@ -77,6 +78,7 @@ expect(subject.available_scopes_for(project)).to match_array %i[ api read_api read_repository write_repository read_registry write_registry read_observability write_observability create_runner manage_runner k8s_proxy ai_features + self_rotate ] end @@ -86,6 +88,7 @@ expect(subject.available_scopes_for(group)).to match_array %i[ api read_api read_repository write_repository read_registry write_registry read_observability write_observability create_runner manage_runner k8s_proxy ai_features + self_rotate ] end @@ -110,6 +113,7 @@ read_repository read_service_ping read_user + self_rotate sudo user:* write_observability @@ -131,7 +135,7 @@ expect(subject.available_scopes_for(group)).to match_array %i[ api read_api read_repository write_repository read_registry write_registry create_runner manage_runner - k8s_proxy ai_features + k8s_proxy ai_features self_rotate ] end @@ -143,7 +147,7 @@ expect(subject.available_scopes_for(project)).to match_array %i[ api read_api read_repository write_repository read_registry write_registry create_runner manage_runner - k8s_proxy ai_features + k8s_proxy ai_features self_rotate ] end end @@ -164,6 +168,7 @@ expect(subject.available_scopes_for(group)).to match_array %i[ api read_api read_repository write_repository read_registry write_registry read_observability write_observability create_runner manage_runner k8s_proxy ai_features + self_rotate ] end @@ -172,7 +177,7 @@ expect(subject.available_scopes_for(user)).to match_array %i[ api read_user read_api read_repository write_repository read_registry write_registry read_service_ping - sudo admin_mode create_runner manage_runner k8s_proxy ai_features + sudo admin_mode create_runner manage_runner k8s_proxy ai_features self_rotate ] end @@ -180,6 +185,7 @@ expect(subject.available_scopes_for(project)).to match_array %i[ api read_api read_repository write_repository read_registry write_registry read_observability write_observability create_runner manage_runner k8s_proxy ai_features + self_rotate ] end @@ -192,6 +198,7 @@ expect(subject.available_scopes_for(other_group)).to match_array %i[ api read_api read_repository write_repository read_registry write_registry create_runner manage_runner k8s_proxy ai_features + self_rotate ] end @@ -204,7 +211,7 @@ expect(subject.available_scopes_for(other_project)).to match_array %i[ api read_api read_repository write_repository read_registry write_registry - create_runner manage_runner k8s_proxy ai_features + create_runner manage_runner k8s_proxy ai_features self_rotate ] end end diff --git a/spec/requests/api/personal_access_tokens/self_rotation_spec.rb b/spec/requests/api/personal_access_tokens/self_rotation_spec.rb index 697f8f3a07fb79f2f74786e841e80bf94b5a71d6..a162bfb1ff78a5ce5a16bc334e66e9943e93c2ed 100644 --- a/spec/requests/api/personal_access_tokens/self_rotation_spec.rb +++ b/spec/requests/api/personal_access_tokens/self_rotation_spec.rb @@ -55,7 +55,7 @@ let(:current_user) { create(:admin) } let(:token) { create(:personal_access_token, scopes: [scope], user: current_user) } - if [Gitlab::Auth::API_SCOPE].include? scope + if [Gitlab::Auth::API_SCOPE, Gitlab::Auth::SELF_ROTATE_SCOPE].include? scope it_behaves_like 'rotating token succeeds' else it_behaves_like 'rotating token denied', :forbidden @@ -87,12 +87,21 @@ let(:current_user) { create(:user) } let(:token) { create(:personal_access_token, scopes: [scope], user: current_user) } - if [Gitlab::Auth::API_SCOPE].include? scope + if [Gitlab::Auth::API_SCOPE, Gitlab::Auth::SELF_ROTATE_SCOPE].include? scope it_behaves_like 'rotating token succeeds' else it_behaves_like 'rotating token denied', :forbidden end end + + context "with '#{scope}' and 'self_rotate' scoped token" do + let(:current_user) { create(:user) } + let(:token) do + create(:personal_access_token, scopes: [scope, Gitlab::Auth::SELF_ROTATE_SCOPE], user: current_user) + end + + it_behaves_like 'rotating token succeeds' + end end end @@ -141,7 +150,7 @@ context "with a '#{scope}' scoped token" do let(:token) { create(:oauth_access_token, scopes: [scope]) } - if [Gitlab::Auth::API_SCOPE].include? scope + if [Gitlab::Auth::API_SCOPE, Gitlab::Auth::SELF_ROTATE_SCOPE].include? scope it_behaves_like 'rotating token denied', :method_not_allowed else it_behaves_like 'rotating token denied', :forbidden @@ -170,26 +179,38 @@ context 'when current_user is a project bot' do let(:current_user) { create(:user, :project_bot) } - it_behaves_like 'rotating token denied', :forbidden + it_behaves_like 'rotating token succeeds' context 'when expiry is defined' do let(:expiry_date) { Date.today + 1.month } let(:params) { { expires_at: expiry_date } } - it_behaves_like 'rotating token denied', :forbidden + it_behaves_like 'rotating token succeeds' end context 'with impersonated token' do let(:token) { create(:personal_access_token, :impersonation, user: current_user) } - it_behaves_like 'rotating token denied', :forbidden + it_behaves_like 'rotating token succeeds' end Gitlab::Auth.resource_bot_scopes.each do |scope| context "with a '#{scope}' scoped token" do let(:token) { create(:personal_access_token, scopes: [scope], user: current_user) } - it_behaves_like 'rotating token denied', :forbidden + if [Gitlab::Auth::API_SCOPE, Gitlab::Auth::SELF_ROTATE_SCOPE].include? scope + it_behaves_like 'rotating token succeeds' + else + it_behaves_like 'rotating token denied', :forbidden + end + end + + context "with '#{scope}' and 'self_rotate' scoped token" do + let(:token) do + create(:personal_access_token, scopes: [scope, Gitlab::Auth::SELF_ROTATE_SCOPE], user: current_user) + end + + it_behaves_like 'rotating token succeeds' end end end diff --git a/spec/requests/api/resource_access_tokens/self_rotation_spec.rb b/spec/requests/api/resource_access_tokens/self_rotation_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..62f3dadb679ff31654e4cc783b638f08844d8659 --- /dev/null +++ b/spec/requests/api/resource_access_tokens/self_rotation_spec.rb @@ -0,0 +1,189 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::ResourceAccessTokens::SelfRotation, feature_category: :system_access do + let(:token) { create(:personal_access_token, user: current_user) } + let(:expiry_date) { Time.zone.today + 1.week } + let(:params) { {} } + + let_it_be(:current_user) { create(:user, :project_bot) } + let_it_be(:other_user) { create(:user, :project_bot) } + + subject(:rotate_token) { post(api(path, personal_access_token: token), params: params) } + + shared_examples 'rotating token succeeds' do + it 'rotate token', :aggregate_failures do + rotate_token + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['token']).not_to eq(token.token) + expect(json_response['expires_at']).to eq(expiry_date.to_s) + expect(token.reload).to be_revoked + end + end + + shared_examples 'rotating token denied' do |status| + it 'cannot rotate token' do + rotate_token + + expect(response).to have_gitlab_http_status(status) + end + end + + shared_examples 'rotating resource access token' do |source_type| + let(:resource_id) { resource.id } + let(:path) { "/#{source_type}s/#{resource_id}/access_tokens/self/rotate" } + + describe "POST #{source_type}s/:id/access_tokens/self/rotate" do + context 'when token is not a valid resource token' do + context 'when token is a personal access token' do + before do + resource.add_guest(current_user) + end + + context 'when current_user is an administrator', :enable_admin_mode do + let_it_be(:current_user) { create(:admin) } + + it_behaves_like 'rotating token denied', :method_not_allowed + end + + context 'when current_user is not an administrator' do + let_it_be(:current_user) { create(:user) } + + it_behaves_like 'rotating token denied', :method_not_allowed + end + end + + context 'when token is invalid' do + let(:token) { instance_double(PersonalAccessToken, token: 'invalidtoken') } + + it_behaves_like 'rotating token denied', :unauthorized + end + + context 'with a revoked token' do + let(:token) { create(:personal_access_token, :revoked, user: current_user) } + + it_behaves_like 'rotating token denied', :unauthorized + end + + context 'with an expired token' do + let(:token) { create(:personal_access_token, expires_at: 1.day.ago, user: current_user) } + + it_behaves_like 'rotating token denied', :unauthorized + end + + context 'with a rotated token' do + let(:token) { create(:personal_access_token, :revoked, user: current_user) } + let!(:child_token) { create(:personal_access_token, previous_personal_access_token_id: token.id) } + + it_behaves_like 'rotating token denied', :unauthorized + + it 'revokes token family' do + rotate_token + + expect(child_token.reload).to be_revoked + end + end + + context 'with an OAuth token' do + subject(:rotate_token) { post(api(path, oauth_access_token: token), params: params) } + + context 'with default scope' do + let(:token) { create(:oauth_access_token) } + + it_behaves_like 'rotating token denied', :forbidden + end + + context 'with api or self_rotate scope' do + let(:token) do + create(:oauth_access_token, scopes: [Gitlab::Auth::API_SCOPE, Gitlab::Auth::SELF_ROTATE_SCOPE]) + end + + it_behaves_like 'rotating token denied', :method_not_allowed + end + end + + context 'with a deploy token' do + let(:token) { create(:deploy_token) } + let(:headers) { { Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => token.token } } + + subject(:rotate_token) { post(api(path), params: params, headers: headers) } + + it_behaves_like 'rotating token denied', :unauthorized + end + + context 'with a job token' do + let(:job) { create(:ci_build, :running, user: current_user) } + + subject(:rotate_token) { post(api(path, job_token: job.token), params: params) } + + it_behaves_like 'rotating token denied', :unauthorized + end + end + + context "when token is a valid #{source_type} token" do + it_behaves_like 'rotating token succeeds' + + context 'when expiry is defined' do + let(:expiry_date) { Time.zone.today + 1.month } + let(:params) { { expires_at: expiry_date } } + + it_behaves_like 'rotating token succeeds' + end + + Gitlab::Auth.resource_bot_scopes.each do |scope| + context "with a '#{scope}' scoped token" do + let(:token) { create(:personal_access_token, scopes: [scope], user: current_user) } + + if [Gitlab::Auth::API_SCOPE, Gitlab::Auth::SELF_ROTATE_SCOPE].include? scope + it_behaves_like 'rotating token succeeds' + else + it_behaves_like 'rotating token denied', :forbidden + end + end + + context "with '#{scope}' and 'self_rotate' scoped token" do + let(:token) do + create(:personal_access_token, scopes: [scope, Gitlab::Auth::SELF_ROTATE_SCOPE], user: current_user) + end + + it_behaves_like 'rotating token succeeds' + end + end + end + + context "when token does not belong to the resource" do + Gitlab::VisibilityLevel.string_values.each do |visibility| + context "when resource visibility is '#{visibility}'" do + let_it_be(:resource) { create(source_type, visibility) } + + let(:token) { create(:personal_access_token, user: other_user) } + + if Gitlab::VisibilityLevel.level_value(visibility) != Gitlab::VisibilityLevel::PRIVATE + it_behaves_like 'rotating token denied', :unauthorized + else + it_behaves_like 'rotating token denied', :not_found + end + end + end + end + end + end + + context 'when the resource is a project' do + let_it_be(:resource) { create(:project) } + + before_all { resource.add_guest(current_user) } + + it_behaves_like 'rotating resource access token', 'project' + end + + context 'when the resource is a group' do + let_it_be(:resource) { create(:group) } + + before_all { resource.add_guest(current_user) } + + it_behaves_like 'rotating resource access token', 'group' + end +end diff --git a/spec/requests/groups/settings/access_tokens_controller_spec.rb b/spec/requests/groups/settings/access_tokens_controller_spec.rb index ecccbc7f9e086741eb4820f0d9b7c6cb10c9c9c4..af93e4a6519ed88d32a35f2fa4e6d33cf926ed0a 100644 --- a/spec/requests/groups/settings/access_tokens_controller_spec.rb +++ b/spec/requests/groups/settings/access_tokens_controller_spec.rb @@ -120,6 +120,7 @@ it 'sets available scopes' do expect(assigns(:scopes)).to include(Gitlab::Auth::K8S_PROXY_SCOPE) + expect(assigns(:scopes)).to include(Gitlab::Auth::SELF_ROTATE_SCOPE) end end end diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb index 561a3154ae6e59227f847483b4a87c48478608af..70f846a5b65a9546eeb6240bafeeb6303f66b915 100644 --- a/spec/requests/openid_connect_spec.rb +++ b/spec/requests/openid_connect_spec.rb @@ -274,7 +274,8 @@ def request_user_info! let(:expected_scopes) do %w[ admin_mode api read_user read_api read_repository write_repository sudo openid profile email - read_observability write_observability create_runner manage_runner k8s_proxy ai_features read_service_ping ai_workflows user:* + read_observability write_observability create_runner manage_runner k8s_proxy ai_features read_service_ping ai_workflows + user:* self_rotate ] end diff --git a/spec/requests/projects/settings/access_tokens_controller_spec.rb b/spec/requests/projects/settings/access_tokens_controller_spec.rb index 62855e9a7e47e37b0c13b57f8f6afcd13e0fd4b8..d91e356167cbaf9022371e718284e6bc9fc507ec 100644 --- a/spec/requests/projects/settings/access_tokens_controller_spec.rb +++ b/spec/requests/projects/settings/access_tokens_controller_spec.rb @@ -121,6 +121,7 @@ it 'sets available scopes' do expect(assigns(:scopes)).to include(Gitlab::Auth::K8S_PROXY_SCOPE) + expect(assigns(:scopes)).to include(Gitlab::Auth::SELF_ROTATE_SCOPE) end end end