diff --git a/app/assets/javascripts/pages/groups/settings/access_tokens/index.js b/app/assets/javascripts/pages/groups/settings/access_tokens/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..dc1bb88bf4b99b7f29022f7769328d60d8026b65
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/settings/access_tokens/index.js
@@ -0,0 +1,3 @@
+import { initExpiresAtField } from '~/access_tokens';
+
+initExpiresAtField();
diff --git a/app/controllers/concerns/access_tokens_actions.rb b/app/controllers/concerns/access_tokens_actions.rb
new file mode 100644
index 0000000000000000000000000000000000000000..451841c43bb4c4fcd1097a1d0618e5a5761696f4
--- /dev/null
+++ b/app/controllers/concerns/access_tokens_actions.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+module AccessTokensActions
+ extend ActiveSupport::Concern
+
+ included do
+ before_action -> { check_permission(:read_resource_access_tokens) }, only: [:index]
+ before_action -> { check_permission(:destroy_resource_access_tokens) }, only: [:revoke]
+ before_action -> { check_permission(:create_resource_access_tokens) }, only: [:create]
+ end
+
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ def index
+ @resource_access_token = PersonalAccessToken.new
+ set_index_vars
+ end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ def create
+ token_response = ResourceAccessTokens::CreateService.new(current_user, resource, create_params).execute
+
+ if token_response.success?
+ @resource_access_token = token_response.payload[:access_token]
+ PersonalAccessToken.redis_store!(key_identity, @resource_access_token.token)
+
+ redirect_to resource_access_tokens_path, notice: _("Your new access token has been created.")
+ else
+ redirect_to resource_access_tokens_path, alert: _("Failed to create new access token: %{token_response_message}") % { token_response_message: token_response.message }
+ end
+ end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ def revoke
+ @resource_access_token = finder.find(params[:id])
+ revoked_response = ResourceAccessTokens::RevokeService.new(current_user, resource, @resource_access_token).execute
+
+ if revoked_response.success?
+ flash[:notice] = _("Revoked access token %{access_token_name}!") % { access_token_name: @resource_access_token.name }
+ else
+ flash[:alert] = _("Could not revoke access token %{access_token_name}.") % { access_token_name: @resource_access_token.name }
+ end
+
+ redirect_to resource_access_tokens_path
+ end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+
+ private
+
+ def check_permission(action)
+ render_404 unless can?(current_user, action, resource)
+ end
+
+ def create_params
+ params.require(:resource_access_token).permit(:name, :expires_at, :access_level, scopes: [])
+ end
+
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ def set_index_vars
+ # Loading resource members so that we can fetch access level of the bot
+ # user in the resource without multiple queries.
+ resource.members.load
+
+ @scopes = Gitlab::Auth.resource_bot_scopes
+ @active_resource_access_tokens = finder(state: 'active').execute.preload_users
+ @inactive_resource_access_tokens = finder(state: 'inactive', sort: 'expires_at_asc').execute.preload_users
+ @new_resource_access_token = PersonalAccessToken.redis_getdel(key_identity)
+ end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+
+ def finder(options = {})
+ PersonalAccessTokensFinder.new({ user: bot_users, impersonation: false }.merge(options))
+ end
+
+ def bot_users
+ resource.bots
+ end
+
+ def key_identity
+ "#{current_user.id}:#{resource.id}"
+ end
+end
diff --git a/app/controllers/groups/settings/access_tokens_controller.rb b/app/controllers/groups/settings/access_tokens_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b9ab2e008ccbc05f931b3bbcb02d1fdb1ca33c88
--- /dev/null
+++ b/app/controllers/groups/settings/access_tokens_controller.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Groups
+ module Settings
+ class AccessTokensController < Groups::ApplicationController
+ include AccessTokensActions
+
+ layout 'group_settings'
+ feature_category :authentication_and_authorization
+
+ alias_method :resource, :group
+
+ def resource_access_tokens_path
+ group_settings_access_tokens_path
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/settings/access_tokens_controller.rb b/app/controllers/projects/settings/access_tokens_controller.rb
index 1ecede4c7a27143b4e058d085a20897f67821973..32916831ecd8fc7f1322feda036098cc1c946a14 100644
--- a/app/controllers/projects/settings/access_tokens_controller.rb
+++ b/app/controllers/projects/settings/access_tokens_controller.rb
@@ -3,77 +3,15 @@
module Projects
module Settings
class AccessTokensController < Projects::ApplicationController
- include ProjectsHelper
+ include AccessTokensActions
layout 'project_settings'
- before_action -> { check_permission(:read_resource_access_tokens) }, only: [:index]
- before_action -> { check_permission(:destroy_resource_access_tokens) }, only: [:revoke]
- before_action -> { check_permission(:create_resource_access_tokens) }, only: [:create]
-
feature_category :authentication_and_authorization
- def index
- @project_access_token = PersonalAccessToken.new
- set_index_vars
- end
-
- def create
- token_response = ResourceAccessTokens::CreateService.new(current_user, @project, create_params).execute
-
- if token_response.success?
- @project_access_token = token_response.payload[:access_token]
- PersonalAccessToken.redis_store!(key_identity, @project_access_token.token)
-
- redirect_to namespace_project_settings_access_tokens_path, notice: _("Your new project access token has been created.")
- else
- redirect_to namespace_project_settings_access_tokens_path, alert: _("Failed to create new project access token: %{token_response_message}") % { token_response_message: token_response.message }
- end
- end
-
- def revoke
- @project_access_token = finder.find(params[:id])
- revoked_response = ResourceAccessTokens::RevokeService.new(current_user, @project, @project_access_token).execute
-
- if revoked_response.success?
- flash[:notice] = _("Revoked project access token %{project_access_token_name}!") % { project_access_token_name: @project_access_token.name }
- else
- flash[:alert] = _("Could not revoke project access token %{project_access_token_name}.") % { project_access_token_name: @project_access_token.name }
- end
-
- redirect_to namespace_project_settings_access_tokens_path
- end
-
- private
-
- def check_permission(action)
- render_404 unless can?(current_user, action, @project)
- end
-
- def create_params
- params.require(:project_access_token).permit(:name, :expires_at, :access_level, scopes: [])
- end
-
- def set_index_vars
- # Loading project members so that we can fetch access level of the bot
- # user in the project without multiple queries.
- @project.project_members.load
-
- @scopes = Gitlab::Auth.resource_bot_scopes
- @active_project_access_tokens = finder(state: 'active').execute.preload_users
- @inactive_project_access_tokens = finder(state: 'inactive', sort: 'expires_at_asc').execute.preload_users
- @new_project_access_token = PersonalAccessToken.redis_getdel(key_identity)
- end
-
- def finder(options = {})
- PersonalAccessTokensFinder.new({ user: bot_users, impersonation: false }.merge(options))
- end
-
- def bot_users
- @project.bots
- end
+ alias_method :resource, :project
- def key_identity
- "#{current_user.id}:#{@project.id}"
+ def resource_access_tokens_path
+ namespace_project_settings_access_tokens_path
end
end
end
diff --git a/app/policies/group_member_policy.rb b/app/policies/group_member_policy.rb
index f7a7286aba73b979852ebb7a7df9b655ea51b8ad..a394b63fc8eeef4b23e2429e81b1d56717c5abca 100644
--- a/app/policies/group_member_policy.rb
+++ b/app/policies/group_member_policy.rb
@@ -5,6 +5,7 @@ class GroupMemberPolicy < BasePolicy
with_scope :subject
condition(:last_owner) { @subject.group.member_last_owner?(@subject) || @subject.group.member_last_blocked_owner?(@subject) }
+ condition(:project_bot) { @subject.user&.project_bot? && @subject.group.member?(@subject.user) }
desc "Membership is users' own"
with_score 0
@@ -20,11 +21,13 @@ class GroupMemberPolicy < BasePolicy
prevent :destroy_group_member
end
- rule { can?(:admin_group_member) }.policy do
+ rule { ~project_bot & can?(:admin_group_member) }.policy do
enable :update_group_member
enable :destroy_group_member
end
+ rule { project_bot & can?(:admin_group_member) }.enable :destroy_project_bot_member
+
rule { is_target_user }.policy do
enable :destroy_group_member
end
diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml
index 9a7a7521cece5bcfd752ccac14ed6fe9f50261d0..d4b7466539863e195a2295f1994a9be49eb074f8 100644
--- a/app/views/groups/settings/_permissions.html.haml
+++ b/app/views/groups/settings/_permissions.html.haml
@@ -29,7 +29,7 @@
checkbox_options: { checked: @group.mentions_disabled? },
help_text: s_('GroupSettings|Prevents group members from being notified if the group is mentioned.')
- = render 'groups/settings/project_access_token_creation', f: f, group: @group
+ = render 'groups/settings/resource_access_token_creation', f: f, group: @group
= render_if_exists 'groups/settings/delayed_project_removal', f: f, group: @group
= render 'groups/settings/ip_restriction_registration_features_cta', f: f
= render_if_exists 'groups/settings/ip_restriction', f: f, group: @group
diff --git a/app/views/groups/settings/_project_access_token_creation.html.haml b/app/views/groups/settings/_project_access_token_creation.html.haml
deleted file mode 100644
index 948b25390ba0da619847938adbb16797686767fe..0000000000000000000000000000000000000000
--- a/app/views/groups/settings/_project_access_token_creation.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-- return unless render_setting_to_allow_project_access_token_creation?(group)
-
-.form-group.gl-mb-3
- - project_access_tokens_link = help_page_path('user/project/settings/project_access_tokens')
- - link_start = ''.html_safe % { url: project_access_tokens_link }
- = f.gitlab_ui_checkbox_component :resource_access_token_creation_allowed,
- s_('GroupSettings|Allow project access token creation'),
- checkbox_options: { checked: group.namespace_settings.resource_access_token_creation_allowed?, data: { qa_selector: 'resource_access_token_creation_allowed_checkbox' } },
- help_text: s_('GroupSettings|Users can create %{link_start}project access tokens%{link_end} for projects in this group.').html_safe % { link_start: link_start, link_end: ''.html_safe }
diff --git a/app/views/groups/settings/_resource_access_token_creation.html.haml b/app/views/groups/settings/_resource_access_token_creation.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..160f8ae1e077a9e2c36d1d575ff1554ca4e92e1d
--- /dev/null
+++ b/app/views/groups/settings/_resource_access_token_creation.html.haml
@@ -0,0 +1,11 @@
+- return unless render_setting_to_allow_project_access_token_creation?(group)
+
+.form-group.gl-mb-3
+ - project_access_tokens_link = help_page_path('user/project/settings/project_access_tokens')
+ - group_access_tokens_link = help_page_path('user/group/settings/group_access_tokens')
+ - link_start_project = ''.html_safe % { url: project_access_tokens_link }
+ - link_start_group = ''.html_safe % { url: group_access_tokens_link }
+ = f.gitlab_ui_checkbox_component :resource_access_token_creation_allowed,
+ s_('GroupSettings|Allow project and group access token creation'),
+ checkbox_options: { checked: group.namespace_settings.resource_access_token_creation_allowed?, data: { qa_selector: 'resource_access_token_creation_allowed_checkbox' } },
+ help_text: s_('GroupSettings|Users can create %{link_start_project}project access tokens%{link_end} and %{link_start_group}group access tokens%{link_end} in this group.').html_safe % { link_start_project: link_start_project, link_start_group: link_start_group, link_end: ''.html_safe }
diff --git a/app/views/groups/settings/access_tokens/index.html.haml b/app/views/groups/settings/access_tokens/index.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..16ea96f0b08548ea308a1abf03c73e4975274958
--- /dev/null
+++ b/app/views/groups/settings/access_tokens/index.html.haml
@@ -0,0 +1,50 @@
+- breadcrumb_title s_('AccessTokens|Access Tokens')
+- page_title _('Group Access Tokens')
+- type = _('group access token')
+- type_plural = _('group access tokens')
+- @content_class = 'limit-container-width' unless fluid_layout
+
+.row.gl-mt-3
+ .col-lg-4
+ %h4.gl-mt-0
+ = page_title
+ %p
+ - link_start = ''.html_safe % { url: help_page_path('user/group/settings/group_access_tokens') }
+ - if current_user.can?(:create_resource_access_tokens, @group)
+ = _('Generate group access tokens scoped to this group for your applications that need access to the GitLab API.')
+ %p
+ = _('You can also use group access tokens with Git to authenticate over HTTP(S). %{link_start}Learn more.%{link_end}').html_safe % { link_start: link_start, link_end: ''.html_safe }
+ - else
+ = _('Group access token creation is disabled in this group. You can still use and manage existing tokens. %{link_start}Learn more.%{link_end}').html_safe % { link_start: link_start, link_end: ''.html_safe }
+ %p
+ - root_group = @group.root_ancestor
+ - if current_user.can?(:admin_group, root_group)
+ - group_settings_link = edit_group_path(root_group)
+ - link_start = ''.html_safe % { url: group_settings_link }
+ = _('You can enable group access token creation in %{link_start}group settings%{link_end}.').html_safe % { link_start: link_start, link_end: ''.html_safe }
+
+ .col-lg-8
+ - if @new_resource_access_token
+ = render 'shared/access_tokens/created_container',
+ type: type,
+ new_token_value: @new_resource_access_token
+
+ - if current_user.can?(:create_resource_access_tokens, @group)
+ = render 'shared/access_tokens/form',
+ type: type,
+ path: group_settings_access_tokens_path(@group),
+ resource: @group,
+ token: @resource_access_token,
+ scopes: @scopes,
+ access_levels: GroupMember.access_level_roles,
+ default_access_level: Gitlab::Access::MAINTAINER,
+ prefix: :resource_access_token,
+ help_path: help_page_path('user/group/settings/group_access_tokens', anchor: 'scopes-for-a-group-access-token')
+
+ = render 'shared/access_tokens/table',
+ active_tokens: @active_resource_access_tokens,
+ resource: @group,
+ type: type,
+ type_plural: type_plural,
+ revoke_route_helper: ->(token) { revoke_group_settings_access_token_path(id: token) },
+ no_active_tokens_message: _('This group has no active access tokens.')
diff --git a/app/views/projects/settings/access_tokens/index.html.haml b/app/views/projects/settings/access_tokens/index.html.haml
index 4e946050881cd4e5100b0e76d236586377379a20..e4b027fcc44db18fd213a7078e321574c34b1105 100644
--- a/app/views/projects/settings/access_tokens/index.html.haml
+++ b/app/views/projects/settings/access_tokens/index.html.haml
@@ -5,7 +5,7 @@
- @content_class = 'limit-container-width' unless fluid_layout
.row.gl-mt-3
- .col-lg-4.profile-settings-sidebar
+ .col-lg-4
%h4.gl-mt-0
= page_title
%p
@@ -24,26 +24,26 @@
= _('You can enable project access token creation in %{link_start}group settings%{link_end}.').html_safe % { link_start: link_start, link_end: ''.html_safe }
.col-lg-8
- - if @new_project_access_token
+ - if @new_resource_access_token
= render 'shared/access_tokens/created_container',
type: type,
- new_token_value: @new_project_access_token
+ new_token_value: @new_resource_access_token
- if current_user.can?(:create_resource_access_tokens, @project)
= render 'shared/access_tokens/form',
type: type,
path: project_settings_access_tokens_path(@project),
- project: @project,
- token: @project_access_token,
+ resource: @project,
+ token: @resource_access_token,
scopes: @scopes,
access_levels: ProjectMember.access_level_roles,
default_access_level: Gitlab::Access::MAINTAINER,
- prefix: :project_access_token,
+ prefix: :resource_access_token,
help_path: help_page_path('user/project/settings/project_access_tokens', anchor: 'scopes-for-a-project-access-token')
= render 'shared/access_tokens/table',
- active_tokens: @active_project_access_tokens,
- project: @project,
+ active_tokens: @active_resource_access_tokens,
+ resource: @project,
type: type,
type_plural: type_plural,
revoke_route_helper: ->(token) { revoke_namespace_project_settings_access_token_path(id: token) },
diff --git a/app/views/shared/access_tokens/_form.html.haml b/app/views/shared/access_tokens/_form.html.haml
index 6c392f0dfe542f222d505c57f88d6a281446a891..a52b7236137fe8b35c7123a8ec5fb9f7ab5ac7c6 100644
--- a/app/views/shared/access_tokens/_form.html.haml
+++ b/app/views/shared/access_tokens/_form.html.haml
@@ -1,7 +1,7 @@
- title = local_assigns.fetch(:title, _('Add a %{type}') % { type: type })
- prefix = local_assigns.fetch(:prefix, :personal_access_token)
- help_path = local_assigns.fetch(:help_path)
-- project = local_assigns.fetch(:project, false)
+- resource = local_assigns.fetch(:resource, false)
- access_levels = local_assigns.fetch(:access_levels, false)
- default_access_level = local_assigns.fetch(:default_access_level, false)
@@ -32,12 +32,12 @@
.js-access-tokens-expires-at
= f.text_field :expires_at, class: 'datepicker gl-datepicker-input form-control gl-form-input', placeholder: 'YYYY-MM-DD', autocomplete: 'off', data: { js_name: 'expiresAt' }
- - if project
+ - if resource
.row
.form-group.col-md-6
= label_tag :access_level, _("Select a role"), class: "label-bold"
.select-wrapper
- = select_tag :"#{prefix}[access_level]", options_for_select(access_levels, default_access_level), class: "form-control project-access-select select-control", data: { qa_selector: 'access_token_access_level' }
+ = select_tag :"#{prefix}[access_level]", options_for_select(access_levels, default_access_level), class: "form-control select-control", data: { qa_selector: 'access_token_access_level' }
= sprite_icon('chevron-down', css_class: "gl-icon gl-absolute gl-top-3 gl-right-3 gl-text-gray-200")
.form-group
diff --git a/app/views/shared/access_tokens/_table.html.haml b/app/views/shared/access_tokens/_table.html.haml
index 1e3432ab08b12b334ba3f264d91e85a0c53061b1..aa579b4a672058d962c3339b720868da9e056144 100644
--- a/app/views/shared/access_tokens/_table.html.haml
+++ b/app/views/shared/access_tokens/_table.html.haml
@@ -1,7 +1,7 @@
- no_active_tokens_message = local_assigns.fetch(:no_active_tokens_message, _('This user has no active %{type}.') % { type: type_plural })
- impersonation = local_assigns.fetch(:impersonation, false)
-- project = local_assigns.fetch(:project, false)
-- personal = !impersonation && !project
+- resource = local_assigns.fetch(:resource, false)
+- personal = !impersonation && !resource
%hr
@@ -30,7 +30,7 @@
= _('Last Used')
= link_to sprite_icon('question-o'), help_page_path('user/profile/personal_access_tokens.md', anchor: 'view-the-last-time-a-token-was-used'), target: '_blank', rel: 'noopener noreferrer'
%th= _('Expires')
- - if project
+ - if resource
%th= _('Role')
%th
%tbody
@@ -54,8 +54,8 @@
= time_ago_with_tooltip(token.expires_at)
- else
%span.token-never-expires-label= _('Never')
- - if project
- %td= project.member(token.user).human_access
+ - if resource
+ %td= resource.member(token.user).human_access
%td= link_to _('Revoke'), revoke_route_helper.call(token), method: :put, class: "gl-button btn btn-danger btn-sm float-right qa-revoke-button #{'btn-danger-secondary' unless token.expires?}", data: { confirm: _('Are you sure you want to revoke this %{type}? This action cannot be undone.') % { type: type } }
- else
.settings-message.text-center
diff --git a/config/routes/group.rb b/config/routes/group.rb
index f7a8747d0cf2fba35814f6ff9173de18ea4be536..c313f7209fb6c487596e49dcc6c4608b7b0bb1ff 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -43,6 +43,12 @@
post :create_deploy_token, path: 'deploy_token/create'
end
+ resources :access_tokens, only: [:index, :create] do
+ member do
+ put :revoke
+ end
+ end
+
resources :integrations, only: [:index, :edit, :update] do
member do
put :test
diff --git a/doc/api/group_access_tokens.md b/doc/api/group_access_tokens.md
index 71c6828de496368095e24aa37837da8fd33b197e..37471b9d89d813979a48f2a3667f050b4b1966fc 100644
--- a/doc/api/group_access_tokens.md
+++ b/doc/api/group_access_tokens.md
@@ -6,13 +6,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Group access tokens API **(FREE)**
-You can read more about [group access tokens](../user/project/settings/project_access_tokens.md#group-access-tokens).
+You can read more about [group access tokens](../user/group/settings/group_access_tokens.md).
## List group access tokens
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77236) in GitLab 14.7.
-Get a list of [group access tokens](../user/project/settings/project_access_tokens.md#group-access-tokens).
+Get a list of [group access tokens](../user/group/settings/group_access_tokens.md).
```plaintext
GET groups/:id/access_tokens
@@ -48,7 +48,7 @@ curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/a
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77236) in GitLab 14.7.
-Create a [group access token](../user/project/settings/project_access_tokens.md#group-access-tokens).
+Create a [group access token](../user/group/settings/group_access_tokens.md).
```plaintext
POST groups/:id/access_tokens
@@ -91,7 +91,7 @@ curl --request POST --header "PRIVATE-TOKEN: " \
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77236) in GitLab 14.7.
-Revoke a [group access token](../user/project/settings/project_access_tokens.md#group-access-tokens).
+Revoke a [group access token](../user/group/settings/group_access_tokens.md).
```plaintext
DELETE groups/:id/access_tokens/:token_id
diff --git a/doc/api/index.md b/doc/api/index.md
index 75081897a653ebad0cb852a262d1ce30b8c9856d..69db971f58c2be7dd625dc02b17d293829296393 100644
--- a/doc/api/index.md
+++ b/doc/api/index.md
@@ -169,24 +169,24 @@ for examples requesting a new access token using a refresh token.
A default refresh setting of two hours is tracked in [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/336598).
-### Personal/project access tokens
+### Personal/project/group access tokens
You can use access tokens to authenticate with the API by passing it in either
the `private_token` parameter or the `PRIVATE-TOKEN` header.
-Example of using the personal or project access token in a parameter:
+Example of using the personal, project, or group access token in a parameter:
```shell
curl "https://gitlab.example.com/api/v4/projects?private_token="
```
-Example of using the personal or project access token in a header:
+Example of using the personal, project, or group access token in a header:
```shell
curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects"
```
-You can also use personal or project access tokens with OAuth-compliant headers:
+You can also use personal, project, or group access tokens with OAuth-compliant headers:
```shell
curl --header "Authorization: Bearer " "https://gitlab.example.com/api/v4/projects"
diff --git a/doc/security/token_overview.md b/doc/security/token_overview.md
index 0ef79bc67a98992b61ce95c05490768da9b3a88e..578bb03563f4c8b131eb88313ccb95db22259939 100644
--- a/doc/security/token_overview.md
+++ b/doc/security/token_overview.md
@@ -93,17 +93,19 @@ This table shows available scopes per token. Scopes can be limited further on to
| | API access | Registry access | Repository access |
|-----------------------------|------------|-----------------|-------------------|
-| Personal access token | ✅ | ✅ | ✅ |
-| OAuth2 token | ✅ | 🚫 | ✅ |
-| Impersonation token | ✅ | ✅ | ✅ |
-| Project access token | ✅(1) | ✅(1) | ✅(1) |
-| Deploy token | 🚫 | ✅ | ✅ |
-| Deploy key | 🚫 | 🚫 | ✅ |
-| Runner registration token | 🚫 | 🚫 | ✴️(2) |
-| Runner authentication token | 🚫 | 🚫 | ✴️(2) |
-| Job token | ✴️(3) | 🚫 | ✅ |
+| Personal access token | ✅ | ✅ | ✅ |
+| OAuth2 token | ✅ | 🚫 | ✅ |
+| Impersonation token | ✅ | ✅ | ✅ |
+| Project access token | ✅(1) | ✅(1) | ✅(1) |
+| Group access token | ✅(2) | ✅(2) | ✅(2) |
+| Deploy token | 🚫 | ✅ | ✅ |
+| Deploy key | 🚫 | 🚫 | ✅ |
+| Runner registration token | 🚫 | 🚫 | ✴️(3) |
+| Runner authentication token | 🚫 | 🚫 | ✴️(3) |
+| Job token | ✴️(4) | 🚫 | ✅ |
1. Limited to the one project.
+1. Limited to the one group.
1. Runner registration and authentication token don't provide direct access to repositories, but can be used to register and authenticate a new runner that may execute jobs which do have access to the repository
1. Limited to certain [endpoints](../ci/jobs/ci_job_token.md).
@@ -113,7 +115,7 @@ Access tokens should be treated like passwords and kept secure.
Adding them to URLs is a security risk. This is especially true when cloning or adding a remote, as Git then writes the URL to its `.git/config` file in plain text. URLs are also generally logged by proxies and application servers, which makes those credentials visible to system administrators.
-Instead, API calls can be passed an access token using headers, like [the `Private-Token` header](../api/index.md#personalproject-access-tokens).
+Instead, API calls can be passed an access token using headers, like [the `Private-Token` header](../api/index.md#personalprojectgroup-access-tokens).
Tokens can also be stored using a [Git credential storage](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage).
diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md
index 2a0a0bcfbb57e89c3588958347bed9631781e731..2a301e6ff5bb1f92e6988f2318b32c69323bac92 100644
--- a/doc/topics/authentication/index.md
+++ b/doc/topics/authentication/index.md
@@ -40,8 +40,9 @@ This page gathers all the resources for the topic **Authentication** within GitL
## API
- [OAuth 2 Tokens](../../api/index.md#oauth2-tokens)
-- [Personal access tokens](../../api/index.md#personalproject-access-tokens)
-- [Project access tokens](../../api/index.md#personalproject-access-tokens)
+- [Personal access tokens](../../api/index.md#personalprojectgroup-access-tokens)
+- [Project access tokens](../../api/index.md#personalprojectgroup-access-tokens)
+- [Group access tokens](../../api/index.md#personalprojectgroup-access-tokens)
- [Impersonation tokens](../../api/index.md#impersonation-tokens)
- [OAuth 2.0 identity provider API](../../api/oauth2.md)
diff --git a/doc/user/group/settings/group_access_tokens.md b/doc/user/group/settings/group_access_tokens.md
new file mode 100644
index 0000000000000000000000000000000000000000..4857a0e74de0f4da7a33ff08aed1c26afd517185
--- /dev/null
+++ b/doc/user/group/settings/group_access_tokens.md
@@ -0,0 +1,142 @@
+---
+stage: Manage
+group: Authentication & Authorization
+info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments"
+type: reference, howto
+---
+
+# Group access tokens
+
+You can use a group access token to authenticate:
+
+- With the [GitLab API](../../../api/index.md#personalprojectgroup-access-tokens).
+- With Git, when using HTTP Basic Authentication.
+
+After you configure a group access token, you don't need a password when you authenticate.
+Instead, you can enter any non-blank value.
+
+Group access tokens are similar to [project access tokens](../../project/settings/project_access_tokens.md)
+and [personal access tokens](../../profile/personal_access_tokens.md), except they are
+associated with a group rather than a project or user.
+
+You can use group access tokens:
+
+- On GitLab SaaS if you have the Premium license tier or higher. Group access tokens are not available with a [trial license](https://about.gitlab.com/free-trial/).
+- On self-managed instances of GitLab, with any license tier. If you have the Free tier:
+ - Review your security and compliance policies around
+ [user self-enrollment](../../admin_area/settings/sign_up_restrictions.md#disable-new-sign-ups).
+ - Consider [disabling group access tokens](#enable-or-disable-group-access-token-creation) to
+ lower potential abuse.
+
+Group access tokens inherit the [default prefix setting](../../admin_area/settings/account_and_limit_settings.md#personal-access-token-prefix)
+configured for personal access tokens.
+
+## Create a group access token using UI
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214045) in GitLab 14.7.
+
+To create a group access token:
+
+1. On the top bar, select **Menu > Groups** and find your group.
+1. On the left sidebar, select **Settings > Access Tokens**.
+1. Enter a name.
+1. Optional. Enter an expiry date for the token. The token will expire on that date at midnight UTC.
+1. Select a role for the token.
+1. Select the [desired scopes](#scopes-for-a-group-access-token).
+1. Select **Create group access token**.
+
+A group access token is displayed. Save the group access token somewhere safe. After you leave or refresh the page, you can't view it again.
+
+## Create a group access token using Rails console
+
+GitLab 14.6 and earlier doesn't support creating group access tokens using the UI
+or API. However, administrators can use a workaround:
+
+1. Run the following commands in a [Rails console](../../../administration/operations/rails_console.md):
+
+ ```ruby
+ # Set the GitLab administration user to use. If user ID 1 is not available or is not an administrator, use 'admin = User.admins.first' instead to select an administrator.
+ admin = User.find(1)
+
+ # Set the group group you want to create a token for. For example, group with ID 109.
+ group = Group.find(109)
+
+ # Create the group bot user. For further group access tokens, the username should be group_#{group.id}_bot#{bot_count}. For example, group_109_bot2 and email address group_109_bot2@example.com.
+ bot = Users::CreateService.new(admin, { name: 'group_token', username: "group_#{group.id}_bot", email: "group_#{group.id}_bot@example.com", user_type: :project_bot }).execute
+
+ # Confirm the group bot.
+ bot.confirm
+
+ # Add the bot to the group with the required role.
+ group.add_user(bot, :maintainer)
+
+ # Give the bot a personal access token.
+ token = bot.personal_access_tokens.create(scopes:[:api, :write_repository], name: 'group_token')
+
+ # Get the token value.
+ gtoken = token.token
+ ```
+
+1. Test if the generated group access token works:
+
+ 1. Use the group access token in the `PRIVATE-TOKEN` header with GitLab REST APIs. For example:
+
+ - [Create an epic](../../../api/epics.md#new-epic) in the group.
+ - [Create a project pipeline](../../../api/pipelines.md#create-a-new-pipeline) in one of the group's projects.
+ - [Create an issue](../../../api/issues.md#new-issue) in one of the group's projects.
+
+ 1. Use the group token to [clone a group's project](../../../gitlab-basics/start-using-git.md#clone-with-https)
+ using HTTPS.
+
+## Revoke a group access token using the UI
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214045) in GitLab 14.7.
+
+To revoke a group access token:
+
+1. On the top bar, select **Menu > Groups** and find your group.
+1. On the left sidebar, select **Settings > Access Tokens**.
+1. Next to the group access token to revoke, select **Revoke**.
+
+## Revoke a group access token using Rails console
+
+GitLab 14.6 and earlier doesn't support revoking group access tokens using the UI
+or API. However, administrators can use a workaround.
+
+To revoke a group access token, run the following command in a [Rails console](../../../administration/operations/rails_console.md):
+
+```ruby
+bot = User.find_by(username: 'group_109_bot') # the owner of the token you want to revoke
+token = bot.personal_access_tokens.last # the token you want to revoke
+token.revoke!
+```
+
+## Scopes for a group access token
+
+The scope determines the actions you can perform when you authenticate with a group access token.
+
+| Scope | Description |
+|:-------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `api` | Grants complete read and write access to the scoped group and related project API, including the [Package Registry](../../packages/package_registry/index.md). |
+| `read_api` | Grants read access to the scoped group and related project API, including the [Package Registry](../../packages/package_registry/index.md). |
+| `read_registry` | Allows read access (pull) to the [Container Registry](../../packages/container_registry/index.md) images if any project within a group is private and authorization is required. |
+| `write_registry` | Allows write access (push) to the [Container Registry](../../packages/container_registry/index.md). |
+| `read_repository` | Allows read access (pull) to all repositories within a group. |
+| `write_repository` | Allows read and write access (pull and push) to all repositories within a group. |
+
+## Enable or disable group access token creation
+
+To enable or disable group access token creation for all sub-groups in a top-level group:
+
+1. On the top bar, select **Menu > Groups** and find your group.
+1. On the left sidebar, select **Settings > General**.
+1. Expand **Permissions and group features**.
+1. Under **Permissions**, turn on or off **Allow project and group access token creation**.
+
+Even when creation is disabled, you can still use and revoke existing group access tokens.
+
+## Bot users
+
+Each time you create a group access token, a bot user is created and added to the group.
+These bot users are similar to [project bot users](../../project/settings/project_access_tokens.md#project-bot-users), but are added to groups instead of projects. For more information, see
+[Project bot users](../../project/settings/project_access_tokens.md#project-bot-users).
diff --git a/doc/user/packages/debian_repository/index.md b/doc/user/packages/debian_repository/index.md
index 89427174dcdcc493199191309924d4423e82d878..a8f0672e376a5caa6d89de4c053b2c3c2bc1d490 100644
--- a/doc/user/packages/debian_repository/index.md
+++ b/doc/user/packages/debian_repository/index.md
@@ -67,7 +67,7 @@ Creating a Debian package is documented [on the Debian Wiki](https://wiki.debian
To create a distribution, publish a package, or install a private package, you need one of the
following:
-- [Personal access token](../../../api/index.md#personalproject-access-tokens)
+- [Personal access token](../../../api/index.md#personalprojectgroup-access-tokens)
- [CI/CD job token](../../../ci/jobs/ci_job_token.md)
- [Deploy token](../../project/deploy_tokens/index.md)
diff --git a/doc/user/packages/generic_packages/index.md b/doc/user/packages/generic_packages/index.md
index 58b012ce656d26501c756cbcd3b42563d5ee4d83..7b44b5bcbb7bf7ceade3a1b2d9353edb1e845acc 100644
--- a/doc/user/packages/generic_packages/index.md
+++ b/doc/user/packages/generic_packages/index.md
@@ -17,13 +17,13 @@ Publish generic files, like release binaries, in your project's Package Registry
## Authenticate to the Package Registry
-To authenticate to the Package Registry, you need either a [personal access token](../../../api/index.md#personalproject-access-tokens),
+To authenticate to the Package Registry, you need either a [personal access token](../../../api/index.md#personalprojectgroup-access-tokens),
[CI/CD job token](../../../ci/jobs/ci_job_token.md), or [deploy token](../../project/deploy_tokens/index.md).
In addition to the standard API authentication mechanisms, the generic package
API allows authentication with HTTP Basic authentication for use with tools that
do not support the other available mechanisms. The `user-id` is not checked and
-may be any value, and the `password` must be either a [personal access token](../../../api/index.md#personalproject-access-tokens),
+may be any value, and the `password` must be either a [personal access token](../../../api/index.md#personalprojectgroup-access-tokens),
a [CI/CD job token](../../../ci/jobs/ci_job_token.md), or a [deploy token](../../project/deploy_tokens/index.md).
## Publish a package file
diff --git a/doc/user/packages/helm_repository/index.md b/doc/user/packages/helm_repository/index.md
index 488345965f904bb67a1c44b2c58ef501b3b6e1e5..73298afc9cde605240ed8e74a908fcab031bd3ce 100644
--- a/doc/user/packages/helm_repository/index.md
+++ b/doc/user/packages/helm_repository/index.md
@@ -30,7 +30,7 @@ Read more in the Helm documentation about these topics:
To authenticate to the Helm repository, you need either:
-- A [personal access token](../../../api/index.md#personalproject-access-tokens) with the scope set to `api`.
+- A [personal access token](../../../api/index.md#personalprojectgroup-access-tokens) with the scope set to `api`.
- A [deploy token](../../project/deploy_tokens/index.md) with the scope set to `read_package_registry`, `write_package_registry`, or both.
- A [CI/CD job token](../../../ci/jobs/ci_job_token.md).
diff --git a/doc/user/packages/terraform_module_registry/index.md b/doc/user/packages/terraform_module_registry/index.md
index b8dc071fc302f6fdd8f17f3cd53b8951df4c6e50..bb9f32d114413ab3af52affa294658b1a9b1c9f5 100644
--- a/doc/user/packages/terraform_module_registry/index.md
+++ b/doc/user/packages/terraform_module_registry/index.md
@@ -15,7 +15,7 @@ as a Terraform module registry.
To authenticate to the Terraform module registry, you need either:
-- A [personal access token](../../../api/index.md#personalproject-access-tokens) with at least `read_api` rights.
+- A [personal access token](../../../api/index.md#personalprojectgroup-access-tokens) with at least `read_api` rights.
- A [CI/CD job token](../../../ci/jobs/ci_job_token.md).
## Publish a Terraform Module
diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md
index a8fbdb2fa60d180e88bee21fb75960df0816d189..45cff326332e39b790cd6ea474822d49d960305b 100644
--- a/doc/user/profile/personal_access_tokens.md
+++ b/doc/user/profile/personal_access_tokens.md
@@ -14,7 +14,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
Personal access tokens can be an alternative to [OAuth2](../../api/oauth2.md) and used to:
-- Authenticate with the [GitLab API](../../api/index.md#personalproject-access-tokens).
+- Authenticate with the [GitLab API](../../api/index.md#personalprojectgroup-access-tokens).
- Authenticate with Git using HTTP Basic Authentication.
In both cases, you authenticate with a personal access token in place of your password.
@@ -33,7 +33,7 @@ Though required, GitLab usernames are ignored when authenticating with a persona
There is an [issue for tracking](https://gitlab.com/gitlab-org/gitlab/-/issues/212953) to make GitLab
use the username.
-For examples of how you can use a personal access token to authenticate with the API, see the [API documentation](../../api/index.md#personalproject-access-tokens).
+For examples of how you can use a personal access token to authenticate with the API, see the [API documentation](../../api/index.md#personalprojectgroup-access-tokens).
Alternately, GitLab administrators can use the API to create [impersonation tokens](../../api/index.md#impersonation-tokens).
Use impersonation tokens to automate authentication as a specific user.
diff --git a/doc/user/project/settings/project_access_tokens.md b/doc/user/project/settings/project_access_tokens.md
index 6199b3c4a984663b080b93abef0c13ebf6605f65..90e9df90593adc18c1a27b4c356f8ca12c3ceb4c 100644
--- a/doc/user/project/settings/project_access_tokens.md
+++ b/doc/user/project/settings/project_access_tokens.md
@@ -14,18 +14,19 @@ type: reference, howto
You can use a project access token to authenticate:
-- With the [GitLab API](../../../api/index.md#personalproject-access-tokens).
+- With the [GitLab API](../../../api/index.md#personalprojectgroup-access-tokens).
- With Git, when using HTTP Basic Authentication.
After you configure a project access token, you don't need a password when you authenticate.
Instead, you can enter any non-blank value.
-Project access tokens are similar to [personal access tokens](../../profile/personal_access_tokens.md),
-except they are associated with a project rather than a user.
+Project access tokens are similar to [group access tokens](../../group/settings/group_access_tokens.md)
+and [personal access tokens](../../profile/personal_access_tokens.md), except they are
+associated with a project rather than a group or user.
You can use project access tokens:
-- On GitLab SaaS if you have the Premium license tier or higher. Personal access tokens are not available with a [trial license](https://about.gitlab.com/free-trial/).
+- On GitLab SaaS if you have the Premium license tier or higher. Project access tokens are not available with a [trial license](https://about.gitlab.com/free-trial/).
- On self-managed instances of GitLab, with any license tier. If you have the Free tier:
- Review your security and compliance policies around
[user self-enrollment](../../admin_area/settings/sign_up_restrictions.md#disable-new-sign-ups).
@@ -79,7 +80,7 @@ To enable or disable project access token creation for all projects in a top-lev
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Settings > General**.
1. Expand **Permissions and group features**.
-1. Under **Permissions**, turn on or off **Allow project access token creation**.
+1. Under **Permissions**, turn on or off **Allow project and group access token creation**.
Even when creation is disabled, you can still use and revoke existing project access tokens.
diff --git a/ee/spec/controllers/projects/settings/access_tokens_controller_spec.rb b/ee/spec/requests/projects/settings/access_tokens_controller_spec.rb
similarity index 51%
rename from ee/spec/controllers/projects/settings/access_tokens_controller_spec.rb
rename to ee/spec/requests/projects/settings/access_tokens_controller_spec.rb
index a5ecf90c89265182ab135596481084f0fbcc4e54..bf72b0016ca043d1007a1df7ae6272e8b762824c 100644
--- a/ee/spec/controllers/projects/settings/access_tokens_controller_spec.rb
+++ b/ee/spec/requests/projects/settings/access_tokens_controller_spec.rb
@@ -5,7 +5,7 @@
RSpec.describe Projects::Settings::AccessTokensController, :saas do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group_with_plan, plan: :bronze_plan) }
- let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:resource) { create(:project, group: group) }
let_it_be(:bot_user) { create(:user, :project_bot) }
before do
@@ -15,49 +15,58 @@
end
before_all do
- project.add_maintainer(bot_user)
- project.add_maintainer(user)
+ resource.add_maintainer(bot_user)
+ resource.add_maintainer(user)
end
shared_examples 'feature unavailable' do
context 'with a free plan' do
let(:group) { create(:group_with_plan, plan: :free_plan) }
- let(:project) { create(:project, group: group) }
+ let(:resource) { create(:project, group: group) }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'when user is not a maintainer with a paid group plan' do
before do
- project.add_developer(user)
+ resource.add_developer(user)
end
it { is_expected.to have_gitlab_http_status(:not_found) }
end
end
- describe '#index' do
- subject { get :index, params: { namespace_id: project.namespace, project_id: project } }
+ describe 'GET /:namespace/:project/-/settings/access_tokens' do
+ subject do
+ get project_settings_access_tokens_path(resource)
+ response
+ end
it_behaves_like 'feature unavailable'
- it_behaves_like 'project access tokens available #index'
+ it_behaves_like 'GET resource access tokens available'
end
- describe '#create' do
+ describe 'POST /:namespace/:project/-/settings/access_tokens' do
let_it_be(:access_token_params) { { name: 'Nerd bot', scopes: ["api"], expires_at: Date.today + 1.month } }
- subject { post :create, params: { namespace_id: project.namespace, project_id: project }.merge(project_access_token: access_token_params) }
+ subject do
+ post project_settings_access_tokens_path(resource), params: { resource_access_token: access_token_params }
+ response
+ end
it_behaves_like 'feature unavailable'
- it_behaves_like 'project access tokens available #create'
+ it_behaves_like 'POST resource access tokens available'
end
- describe '#revoke', :sidekiq_inline do
- let(:project_access_token) { create(:personal_access_token, user: bot_user) }
+ describe 'PUT /:namespace/:project/-/settings/access_tokens/:id', :sidekiq_inline do
+ let(:resource_access_token) { create(:personal_access_token, user: bot_user) }
- subject { put :revoke, params: { namespace_id: project.namespace, project_id: project, id: project_access_token } }
+ subject do
+ put revoke_project_settings_access_token_path(resource, resource_access_token)
+ response
+ end
it_behaves_like 'feature unavailable'
- it_behaves_like 'project access tokens available #revoke'
+ it_behaves_like 'PUT resource access tokens available'
end
end
diff --git a/lib/sidebars/groups/menus/settings_menu.rb b/lib/sidebars/groups/menus/settings_menu.rb
index f0239ca6a1a9bb04a8db4d1030b9afe32e72b62a..810b467ed2d447d629e797298f61b88d2513f990 100644
--- a/lib/sidebars/groups/menus/settings_menu.rb
+++ b/lib/sidebars/groups/menus/settings_menu.rb
@@ -10,6 +10,7 @@ def configure_menu_items
add_item(general_menu_item)
add_item(integrations_menu_item)
+ add_item(access_tokens_menu_item)
add_item(group_projects_menu_item)
add_item(repository_menu_item)
add_item(ci_cd_menu_item)
@@ -56,6 +57,19 @@ def integrations_menu_item
)
end
+ def access_tokens_menu_item
+ unless can?(context.current_user, :read_resource_access_tokens, context.group)
+ return ::Sidebars::NilMenuItem.new(item_id: :access_tokens)
+ end
+
+ ::Sidebars::MenuItem.new(
+ title: _('Access Tokens'),
+ link: group_settings_access_tokens_path(context.group),
+ active_routes: { path: 'access_tokens#index' },
+ item_id: :access_tokens
+ )
+ end
+
def group_projects_menu_item
::Sidebars::MenuItem.new(
title: _('Projects'),
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 9300c67805336c2eb9f75faeac9a9d535aa7d4cb..c3d67ca8f8f2200b743eeb0f3b0dccea0d583cc4 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -9898,13 +9898,13 @@ msgstr ""
msgid "Could not restore the group"
msgstr ""
-msgid "Could not revoke impersonation token %{token_name}."
+msgid "Could not revoke access token %{access_token_name}."
msgstr ""
-msgid "Could not revoke personal access token %{personal_access_token_name}."
+msgid "Could not revoke impersonation token %{token_name}."
msgstr ""
-msgid "Could not revoke project access token %{project_access_token_name}."
+msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
msgid "Could not save configuration. Please refresh the page, or try again later."
@@ -14638,7 +14638,7 @@ msgstr ""
msgid "Failed to create merge request. Please try again."
msgstr ""
-msgid "Failed to create new project access token: %{token_response_message}"
+msgid "Failed to create new access token: %{token_response_message}"
msgstr ""
msgid "Failed to create repository"
@@ -15578,6 +15578,9 @@ msgstr ""
msgid "Generate a default set of labels"
msgstr ""
+msgid "Generate group access tokens scoped to this group for your applications that need access to the GitLab API."
+msgstr ""
+
msgid "Generate key"
msgstr ""
@@ -16676,6 +16679,9 @@ msgstr ""
msgid "Group %{group_name} was successfully created."
msgstr ""
+msgid "Group Access Tokens"
+msgstr ""
+
msgid "Group Git LFS status:"
msgstr ""
@@ -16694,6 +16700,9 @@ msgstr ""
msgid "Group URL"
msgstr ""
+msgid "Group access token creation is disabled in this group. You can still use and manage existing tokens. %{link_start}Learn more.%{link_end}"
+msgstr ""
+
msgid "Group application: %{name}"
msgstr ""
@@ -17105,7 +17114,7 @@ msgstr ""
msgid "GroupSelect|Select a group"
msgstr ""
-msgid "GroupSettings|Allow project access token creation"
+msgid "GroupSettings|Allow project and group access token creation"
msgstr ""
msgid "GroupSettings|Allows creating organizations and contacts and associating them with issues."
@@ -17252,7 +17261,7 @@ msgstr ""
msgid "GroupSettings|Transfer group"
msgstr ""
-msgid "GroupSettings|Users can create %{link_start}project access tokens%{link_end} for projects in this group."
+msgid "GroupSettings|Users can create %{link_start_project}project access tokens%{link_end} and %{link_start_group}group access tokens%{link_end} in this group."
msgstr ""
msgid "GroupSettings|What are badges?"
@@ -30519,13 +30528,13 @@ msgstr ""
msgid "Revoked"
msgstr ""
-msgid "Revoked impersonation token %{token_name}!"
+msgid "Revoked access token %{access_token_name}!"
msgstr ""
-msgid "Revoked personal access token %{personal_access_token_name}!"
+msgid "Revoked impersonation token %{token_name}!"
msgstr ""
-msgid "Revoked project access token %{project_access_token_name}!"
+msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
msgid "RightSidebar|Copy email address"
@@ -36380,6 +36389,9 @@ msgstr ""
msgid "This group has been scheduled for permanent removal on %{date}"
msgstr ""
+msgid "This group has no active access tokens."
+msgstr ""
+
msgid "This group is linked to a subscription"
msgstr ""
@@ -40642,6 +40654,9 @@ msgstr ""
msgid "You can also upload existing files from your computer using the instructions below."
msgstr ""
+msgid "You can also use group access tokens with Git to authenticate over HTTP(S). %{link_start}Learn more.%{link_end}"
+msgstr ""
+
msgid "You can also use project access tokens with Git to authenticate over HTTP(S). %{link_start}Learn more.%{link_end}"
msgstr ""
@@ -40687,6 +40702,9 @@ msgstr ""
msgid "You can enable Registration Features because Service Ping is enabled. To continue using Registration Features in the future, you will also need to register with GitLab via a new cloud licensing service."
msgstr ""
+msgid "You can enable group access token creation in %{link_start}group settings%{link_end}."
+msgstr ""
+
msgid "You can enable project access token creation in %{link_start}group settings%{link_end}."
msgstr ""
@@ -41352,13 +41370,13 @@ msgstr ""
msgid "Your new SCIM token"
msgstr ""
-msgid "Your new comment"
+msgid "Your new access token has been created."
msgstr ""
-msgid "Your new personal access token has been created."
+msgid "Your new comment"
msgstr ""
-msgid "Your new project access token has been created."
+msgid "Your new personal access token has been created."
msgstr ""
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
@@ -42226,6 +42244,12 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "group access token"
+msgstr ""
+
+msgid "group access tokens"
+msgstr ""
+
msgid "group members"
msgstr ""
diff --git a/spec/features/groups/settings/access_tokens_spec.rb b/spec/features/groups/settings/access_tokens_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..20787c4c2f5a84c78d2f9716eb80669508948946
--- /dev/null
+++ b/spec/features/groups/settings/access_tokens_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Group > Settings > Access Tokens', :js do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:bot_user) { create(:user, :project_bot) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:resource_settings_access_tokens_path) { group_settings_access_tokens_path(group) }
+
+ before_all do
+ group.add_owner(user)
+ end
+
+ before do
+ stub_feature_flags(bootstrap_confirmation_modals: false)
+ sign_in(user)
+ end
+
+ def create_resource_access_token
+ group.add_maintainer(bot_user)
+
+ create(:personal_access_token, user: bot_user)
+ end
+
+ context 'when user is not a group owner' do
+ before do
+ group.add_maintainer(user)
+ end
+
+ it_behaves_like 'resource access tokens missing access rights'
+ end
+
+ describe 'token creation' do
+ it_behaves_like 'resource access tokens creation', 'group'
+
+ context 'when token creation is not allowed' do
+ it_behaves_like 'resource access tokens creation disallowed', 'Group access token creation is disabled in this group. You can still use and manage existing tokens.'
+ end
+ end
+
+ describe 'active tokens' do
+ let!(:resource_access_token) { create_resource_access_token }
+
+ it_behaves_like 'active resource access tokens'
+ end
+
+ describe 'inactive tokens' do
+ let!(:resource_access_token) { create_resource_access_token }
+
+ it_behaves_like 'inactive resource access tokens', 'This group has no active access tokens.'
+ end
+end
diff --git a/spec/features/projects/settings/access_tokens_spec.rb b/spec/features/projects/settings/access_tokens_spec.rb
index d8de9e0449eaad58b62c4da35ea5d06920a7fbf1..122bf267021e684829c8c13941d2e5d1c00d522a 100644
--- a/spec/features/projects/settings/access_tokens_spec.rb
+++ b/spec/features/projects/settings/access_tokens_spec.rb
@@ -7,6 +7,7 @@
let_it_be(:bot_user) { create(:user, :project_bot) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:resource_settings_access_tokens_path) { project_settings_access_tokens_path(project) }
before_all do
project.add_maintainer(user)
@@ -17,78 +18,25 @@
sign_in(user)
end
- def create_project_access_token
+ def create_resource_access_token
project.add_maintainer(bot_user)
create(:personal_access_token, user: bot_user)
end
- def active_project_access_tokens
- find('.table.active-tokens')
- end
-
- def no_project_access_tokens_message
- find('.settings-message')
- end
-
- def created_project_access_token
- find('#created-personal-access-token').value
- end
-
context 'when user is not a project maintainer' do
before do
project.add_developer(user)
end
- it 'does not show project access token page' do
- visit project_settings_access_tokens_path(project)
-
- expect(page).to have_content("Page Not Found")
- end
+ it_behaves_like 'resource access tokens missing access rights'
end
describe 'token creation' do
- it 'allows creation of a project access token' do
- name = 'My project access token'
-
- visit project_settings_access_tokens_path(project)
- fill_in 'Token name', with: name
-
- # Set date to 1st of next month
- find_field('Expiration date').click
- find('.pika-next').click
- click_on '1'
-
- # Scopes
- check 'api'
- check 'read_api'
-
- click_on 'Create project access token'
-
- expect(active_project_access_tokens).to have_text(name)
- expect(active_project_access_tokens).to have_text('in')
- expect(active_project_access_tokens).to have_text('api')
- expect(active_project_access_tokens).to have_text('read_api')
- expect(active_project_access_tokens).to have_text('Maintainer')
- expect(created_project_access_token).not_to be_empty
- end
+ it_behaves_like 'resource access tokens creation', 'project'
context 'when token creation is not allowed' do
- before do
- group.namespace_settings.update_column(:resource_access_token_creation_allowed, false)
- end
-
- it 'does not show project access token creation form' do
- visit project_settings_access_tokens_path(project)
-
- expect(page).not_to have_selector('#new_project_access_token')
- end
-
- it 'shows project access token creation disabled text' do
- visit project_settings_access_tokens_path(project)
-
- expect(page).to have_text('Project access token creation is disabled in this group. You can still use and manage existing tokens.')
- end
+ it_behaves_like 'resource access tokens creation disallowed', 'Project access token creation is disabled in this group. You can still use and manage existing tokens.'
context 'with a project in a personal namespace' do
let(:personal_project) { create(:project) }
@@ -97,113 +45,25 @@ def created_project_access_token
personal_project.add_maintainer(user)
end
- it 'shows project access token creation form and text' do
+ it 'shows access token creation form and text' do
visit project_settings_access_tokens_path(personal_project)
- expect(page).to have_selector('#new_project_access_token')
+ expect(page).to have_selector('#new_resource_access_token')
expect(page).to have_text('Generate project access tokens scoped to this project for your applications that need access to the GitLab API.')
end
end
-
- context 'group settings link' do
- context 'when user is not a group owner' do
- before do
- group.add_developer(user)
- end
-
- it 'does not show group settings link' do
- visit project_settings_access_tokens_path(project)
-
- expect(page).not_to have_link('group settings', href: edit_group_path(group))
- end
- end
-
- context 'with nested groups' do
- let(:subgroup) { create(:group, parent: group) }
-
- context 'when user is not a top level group owner' do
- before do
- subgroup.add_owner(user)
- end
-
- it 'does not show group settings link' do
- visit project_settings_access_tokens_path(project)
-
- expect(page).not_to have_link('group settings', href: edit_group_path(group))
- end
- end
- end
-
- context 'when user is a group owner' do
- before do
- group.add_owner(user)
- end
-
- it 'shows group settings link' do
- visit project_settings_access_tokens_path(project)
-
- expect(page).to have_link('group settings', href: edit_group_path(group))
- end
- end
- end
end
end
describe 'active tokens' do
- let!(:project_access_token) { create_project_access_token }
+ let!(:resource_access_token) { create_resource_access_token }
- it 'shows active project access tokens' do
- visit project_settings_access_tokens_path(project)
-
- expect(active_project_access_tokens).to have_text(project_access_token.name)
- end
-
- context 'when User#time_display_relative is false' do
- before do
- user.update!(time_display_relative: false)
- end
-
- it 'shows absolute times for expires_at' do
- visit project_settings_access_tokens_path(project)
-
- expect(active_project_access_tokens).to have_text(PersonalAccessToken.last.expires_at.strftime('%b %-d'))
- end
- end
+ it_behaves_like 'active resource access tokens'
end
describe 'inactive tokens' do
- let!(:project_access_token) { create_project_access_token }
-
- no_active_tokens_text = 'This project has no active access tokens.'
+ let!(:resource_access_token) { create_resource_access_token }
- it 'allows revocation of an active token' do
- visit project_settings_access_tokens_path(project)
- accept_confirm { click_on 'Revoke' }
-
- expect(page).to have_selector('.settings-message')
- expect(no_project_access_tokens_message).to have_text(no_active_tokens_text)
- end
-
- it 'removes expired tokens from active section' do
- project_access_token.update!(expires_at: 5.days.ago)
- visit project_settings_access_tokens_path(project)
-
- expect(page).to have_selector('.settings-message')
- expect(no_project_access_tokens_message).to have_text(no_active_tokens_text)
- end
-
- context 'when resource access token creation is not allowed' do
- before do
- group.namespace_settings.update_column(:resource_access_token_creation_allowed, false)
- end
-
- it 'allows revocation of an active token' do
- visit project_settings_access_tokens_path(project)
- accept_confirm { click_on 'Revoke' }
-
- expect(page).to have_selector('.settings-message')
- expect(no_project_access_tokens_message).to have_text(no_active_tokens_text)
- end
- end
+ it_behaves_like 'inactive resource access tokens', 'This project has no active access tokens.'
end
end
diff --git a/spec/lib/sidebars/groups/menus/settings_menu_spec.rb b/spec/lib/sidebars/groups/menus/settings_menu_spec.rb
index 314c4cdc602e0d38f3158ac9161440959c3be9f9..252da8ea69961c2aa7e1ba9cb1799e618632836d 100644
--- a/spec/lib/sidebars/groups/menus/settings_menu_spec.rb
+++ b/spec/lib/sidebars/groups/menus/settings_menu_spec.rb
@@ -56,6 +56,12 @@
it_behaves_like 'access rights checks'
end
+ describe 'Access Tokens' do
+ let(:item_id) { :access_tokens }
+
+ it_behaves_like 'access rights checks'
+ end
+
describe 'Repository menu' do
let(:item_id) { :repository }
diff --git a/spec/policies/group_member_policy_spec.rb b/spec/policies/group_member_policy_spec.rb
index d283b0ffda561a3a2b8a31a67fb4c51cbcc46c38..50774313aaeea8f4daf45cc7f07e6cc01c9bc0ef 100644
--- a/spec/policies/group_member_policy_spec.rb
+++ b/spec/policies/group_member_policy_spec.rb
@@ -83,6 +83,23 @@ def expect_disallowed(*permissions)
specify { expect_allowed(:read_group) }
end
+ context 'with bot user' do
+ let(:current_user) { create(:user, :project_bot) }
+
+ before do
+ group.add_owner(current_user)
+ end
+
+ specify { expect_allowed(:read_group, :destroy_project_bot_member) }
+ end
+
+ context 'with anonymous bot user' do
+ let(:current_user) { create(:user, :project_bot) }
+ let(:membership) { guest.members.first }
+
+ specify { expect_disallowed(:read_group, :destroy_project_bot_member) }
+ end
+
context 'with one owner' do
let(:current_user) { owner }
@@ -106,6 +123,7 @@ def expect_disallowed(*permissions)
end
specify { expect_allowed(*member_related_permissions) }
+ specify { expect_disallowed(:destroy_project_bot_member) }
end
context 'with the group parent' do
diff --git a/spec/requests/groups/settings/access_tokens_controller_spec.rb b/spec/requests/groups/settings/access_tokens_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..eabdef3c41e984d9ef30e81cbd172d8ac407a38a
--- /dev/null
+++ b/spec/requests/groups/settings/access_tokens_controller_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::Settings::AccessTokensController do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:resource) { create(:group) }
+ let_it_be(:bot_user) { create(:user, :project_bot) }
+
+ before_all do
+ resource.add_owner(user)
+ resource.add_maintainer(bot_user)
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ shared_examples 'feature unavailable' do
+ context 'user is not a owner' do
+ before do
+ resource.add_maintainer(user)
+ end
+
+ it { expect(subject).to have_gitlab_http_status(:not_found) }
+ end
+ end
+
+ describe 'GET /:namespace/-/settings/access_tokens' do
+ subject do
+ get group_settings_access_tokens_path(resource)
+ response
+ end
+
+ it_behaves_like 'feature unavailable'
+ it_behaves_like 'GET resource access tokens available'
+ end
+
+ describe 'POST /:namespace/-/settings/access_tokens' do
+ let(:access_token_params) { { name: 'Nerd bot', scopes: ["api"], expires_at: Date.today + 1.month } }
+
+ subject do
+ post group_settings_access_tokens_path(resource), params: { resource_access_token: access_token_params }
+ response
+ end
+
+ it_behaves_like 'feature unavailable'
+ it_behaves_like 'POST resource access tokens available'
+
+ context 'when group access token creation is disabled' do
+ before do
+ resource.namespace_settings.update_column(:resource_access_token_creation_allowed, false)
+ end
+
+ it { expect(subject).to have_gitlab_http_status(:not_found) }
+
+ it 'does not create the token' do
+ expect { subject }.not_to change { PersonalAccessToken.count }
+ end
+
+ it 'does not add the project bot as a member' do
+ expect { subject }.not_to change { Member.count }
+ end
+
+ it 'does not create the project bot user' do
+ expect { subject }.not_to change { User.count }
+ end
+ end
+
+ context 'with custom access level' do
+ let(:access_token_params) { { name: 'Nerd bot', scopes: ["api"], expires_at: Date.today + 1.month, access_level: 20 } }
+
+ subject { post group_settings_access_tokens_path(resource), params: { resource_access_token: access_token_params } }
+
+ it_behaves_like 'POST resource access tokens available'
+ end
+ end
+
+ describe 'PUT /:namespace/-/settings/access_tokens/:id', :sidekiq_inline do
+ let(:resource_access_token) { create(:personal_access_token, user: bot_user) }
+
+ subject do
+ put revoke_group_settings_access_token_path(resource, resource_access_token)
+ response
+ end
+
+ it_behaves_like 'feature unavailable'
+ it_behaves_like 'PUT resource access tokens available'
+ end
+end
diff --git a/spec/controllers/projects/settings/access_tokens_controller_spec.rb b/spec/requests/projects/settings/access_tokens_controller_spec.rb
similarity index 50%
rename from spec/controllers/projects/settings/access_tokens_controller_spec.rb
rename to spec/requests/projects/settings/access_tokens_controller_spec.rb
index 834a9e276f92a805c5319894a543e7d788d54245..780d1b8caefd5f486810228a9180f109e5cf1692 100644
--- a/spec/controllers/projects/settings/access_tokens_controller_spec.rb
+++ b/spec/requests/projects/settings/access_tokens_controller_spec.rb
@@ -1,16 +1,16 @@
# frozen_string_literal: true
-require('spec_helper')
+require 'spec_helper'
RSpec.describe Projects::Settings::AccessTokensController do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
- let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:resource) { create(:project, group: group) }
let_it_be(:bot_user) { create(:user, :project_bot) }
before_all do
- project.add_maintainer(user)
- project.add_maintainer(bot_user)
+ resource.add_maintainer(user)
+ resource.add_maintainer(bot_user)
end
before do
@@ -20,34 +20,40 @@
shared_examples 'feature unavailable' do
context 'user is not a maintainer' do
before do
- project.add_developer(user)
+ resource.add_developer(user)
end
- it { is_expected.to have_gitlab_http_status(:not_found) }
+ it { expect(subject).to have_gitlab_http_status(:not_found) }
end
end
- describe '#index' do
- subject { get :index, params: { namespace_id: project.namespace, project_id: project } }
+ describe 'GET /:namespace/:project/-/settings/access_tokens' do
+ subject do
+ get project_settings_access_tokens_path(resource)
+ response
+ end
it_behaves_like 'feature unavailable'
- it_behaves_like 'project access tokens available #index'
+ it_behaves_like 'GET resource access tokens available'
end
- describe '#create' do
+ describe 'POST /:namespace/:project/-/settings/access_tokens' do
let(:access_token_params) { { name: 'Nerd bot', scopes: ["api"], expires_at: Date.today + 1.month } }
- subject { post :create, params: { namespace_id: project.namespace, project_id: project }.merge(project_access_token: access_token_params) }
+ subject do
+ post project_settings_access_tokens_path(resource), params: { resource_access_token: access_token_params }
+ response
+ end
it_behaves_like 'feature unavailable'
- it_behaves_like 'project access tokens available #create'
+ it_behaves_like 'POST resource access tokens available'
context 'when project access token creation is disabled' do
before do
group.namespace_settings.update_column(:resource_access_token_creation_allowed, false)
end
- it { is_expected.to have_gitlab_http_status(:not_found) }
+ it { expect(subject).to have_gitlab_http_status(:not_found) }
it 'does not create the token' do
expect { subject }.not_to change { PersonalAccessToken.count }
@@ -65,18 +71,21 @@
context 'with custom access level' do
let(:access_token_params) { { name: 'Nerd bot', scopes: ["api"], expires_at: Date.today + 1.month, access_level: 20 } }
- subject { post :create, params: { namespace_id: project.namespace, project_id: project }.merge(project_access_token: access_token_params) }
+ subject { post project_settings_access_tokens_path(resource), params: { resource_access_token: access_token_params } }
- it_behaves_like 'project access tokens available #create'
+ it_behaves_like 'POST resource access tokens available'
end
end
- describe '#revoke', :sidekiq_inline do
- let(:project_access_token) { create(:personal_access_token, user: bot_user) }
+ describe 'PUT /:namespace/:project/-/settings/access_tokens/:id', :sidekiq_inline do
+ let(:resource_access_token) { create(:personal_access_token, user: bot_user) }
- subject { put :revoke, params: { namespace_id: project.namespace, project_id: project, id: project_access_token } }
+ subject do
+ put revoke_project_settings_access_token_path(resource, resource_access_token)
+ response
+ end
it_behaves_like 'feature unavailable'
- it_behaves_like 'project access tokens available #revoke'
+ it_behaves_like 'PUT resource access tokens available'
end
end
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index 085f1f13c2c04d125f97f4fc95785bc92b1b8c00..279678503890250fea74704c8828127b7b05ae5b 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -142,6 +142,7 @@
nav_sub_items: [
_('General'),
_('Integrations'),
+ _('Access Tokens'),
_('Projects'),
_('Repository'),
_('CI/CD'),
diff --git a/spec/support/shared_examples/features/access_tokens_shared_examples.rb b/spec/support/shared_examples/features/access_tokens_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ae246a87bb617b6741aec24a5f7fbdf51dc40133
--- /dev/null
+++ b/spec/support/shared_examples/features/access_tokens_shared_examples.rb
@@ -0,0 +1,165 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'resource access tokens missing access rights' do
+ it 'does not show access token page' do
+ visit resource_settings_access_tokens_path
+
+ expect(page).to have_content("Page Not Found")
+ end
+end
+
+RSpec.shared_examples 'resource access tokens creation' do |resource_type|
+ def active_resource_access_tokens
+ find('.table.active-tokens')
+ end
+
+ def created_resource_access_token
+ find('#created-personal-access-token').value
+ end
+
+ it 'allows creation of an access token', :aggregate_failures do
+ name = 'My access token'
+
+ visit resource_settings_access_tokens_path
+ fill_in 'Token name', with: name
+
+ # Set date to 1st of next month
+ find_field('Expiration date').click
+ find('.pika-next').click
+ click_on '1'
+
+ # Scopes
+ check 'api'
+ check 'read_api'
+
+ click_on "Create #{resource_type} access token"
+
+ expect(active_resource_access_tokens).to have_text(name)
+ expect(active_resource_access_tokens).to have_text('in')
+ expect(active_resource_access_tokens).to have_text('api')
+ expect(active_resource_access_tokens).to have_text('read_api')
+ expect(active_resource_access_tokens).to have_text('Maintainer')
+ expect(created_resource_access_token).not_to be_empty
+ end
+end
+
+RSpec.shared_examples 'resource access tokens creation disallowed' do |error_message|
+ before do
+ group.namespace_settings.update_column(:resource_access_token_creation_allowed, false)
+ end
+
+ it 'does not show access token creation form' do
+ visit resource_settings_access_tokens_path
+
+ expect(page).not_to have_selector('#new_resource_access_token')
+ end
+
+ it 'shows access token creation disabled text' do
+ visit resource_settings_access_tokens_path
+
+ expect(page).to have_text(error_message)
+ end
+
+ context 'group settings link' do
+ context 'when user is not a group owner' do
+ before do
+ group.add_developer(user)
+ end
+
+ it 'does not show group settings link' do
+ visit resource_settings_access_tokens_path
+
+ expect(page).not_to have_link('group settings', href: edit_group_path(group))
+ end
+ end
+
+ context 'with nested groups' do
+ let(:parent_group) { create(:group) }
+ let(:group) { create(:group, parent: parent_group) }
+
+ context 'when user is not a top level group owner' do
+ before do
+ group.add_owner(user)
+ end
+
+ it 'does not show group settings link' do
+ visit resource_settings_access_tokens_path
+
+ expect(page).not_to have_link('group settings', href: edit_group_path(group))
+ end
+ end
+ end
+
+ context 'when user is a group owner' do
+ before do
+ group.add_owner(user)
+ end
+
+ it 'shows group settings link' do
+ visit resource_settings_access_tokens_path
+
+ expect(page).to have_link('group settings', href: edit_group_path(group))
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'active resource access tokens' do
+ def active_resource_access_tokens
+ find('.table.active-tokens')
+ end
+
+ it 'shows active access tokens' do
+ visit resource_settings_access_tokens_path
+
+ expect(active_resource_access_tokens).to have_text(resource_access_token.name)
+ end
+
+ context 'when User#time_display_relative is false' do
+ before do
+ user.update!(time_display_relative: false)
+ end
+
+ it 'shows absolute times for expires_at' do
+ visit resource_settings_access_tokens_path
+
+ expect(active_resource_access_tokens).to have_text(PersonalAccessToken.last.expires_at.strftime('%b %-d'))
+ end
+ end
+end
+
+RSpec.shared_examples 'inactive resource access tokens' do |no_active_tokens_text|
+ def no_resource_access_tokens_message
+ find('.settings-message')
+ end
+
+ it 'allows revocation of an active token' do
+ visit resource_settings_access_tokens_path
+ accept_confirm { click_on 'Revoke' }
+
+ expect(page).to have_selector('.settings-message')
+ expect(no_resource_access_tokens_message).to have_text(no_active_tokens_text)
+ end
+
+ it 'removes expired tokens from active section' do
+ resource_access_token.update!(expires_at: 5.days.ago)
+ visit resource_settings_access_tokens_path
+
+ expect(page).to have_selector('.settings-message')
+ expect(no_resource_access_tokens_message).to have_text(no_active_tokens_text)
+ end
+
+ context 'when resource access token creation is not allowed' do
+ before do
+ group.namespace_settings.update_column(:resource_access_token_creation_allowed, false)
+ end
+
+ it 'allows revocation of an active token' do
+ visit resource_settings_access_tokens_path
+ accept_confirm { click_on 'Revoke' }
+
+ expect(page).to have_selector('.settings-message')
+ expect(no_resource_access_tokens_message).to have_text(no_active_tokens_text)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb b/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb
similarity index 60%
rename from spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb
rename to spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb
index 9287bbd29fb6a47ce4d02304970f8079e7f4cbb5..6cd871d354c38eaceebeddf7f8056b3dc5292371 100644
--- a/spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb
+++ b/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb
@@ -1,19 +1,19 @@
# frozen_string_literal: true
-RSpec.shared_examples 'project access tokens available #index' do
- let_it_be(:active_project_access_token) { create(:personal_access_token, user: bot_user) }
- let_it_be(:inactive_project_access_token) { create(:personal_access_token, :revoked, user: bot_user) }
+RSpec.shared_examples 'GET resource access tokens available' do
+ let_it_be(:active_resource_access_token) { create(:personal_access_token, user: bot_user) }
+ let_it_be(:inactive_resource_access_token) { create(:personal_access_token, :revoked, user: bot_user) }
- it 'retrieves active project access tokens' do
+ it 'retrieves active resource access tokens' do
subject
- expect(assigns(:active_project_access_tokens)).to contain_exactly(active_project_access_token)
+ expect(assigns(:active_resource_access_tokens)).to contain_exactly(active_resource_access_token)
end
- it 'retrieves inactive project access tokens' do
+ it 'retrieves inactive resource access tokens' do
subject
- expect(assigns(:inactive_project_access_tokens)).to contain_exactly(inactive_project_access_token)
+ expect(assigns(:inactive_resource_access_tokens)).to contain_exactly(inactive_resource_access_token)
end
it 'lists all available scopes' do
@@ -24,15 +24,15 @@
it 'retrieves newly created personal access token value' do
token_value = 'random-value'
- allow(PersonalAccessToken).to receive(:redis_getdel).with("#{user.id}:#{project.id}").and_return(token_value)
+ allow(PersonalAccessToken).to receive(:redis_getdel).with("#{user.id}:#{resource.id}").and_return(token_value)
subject
- expect(assigns(:new_project_access_token)).to eq(token_value)
+ expect(assigns(:new_resource_access_token)).to eq(token_value)
end
end
-RSpec.shared_examples 'project access tokens available #create' do
+RSpec.shared_examples 'POST resource access tokens available' do
def created_token
PersonalAccessToken.order(:created_at).last
end
@@ -40,17 +40,17 @@ def created_token
it 'returns success message' do
subject
- expect(controller).to set_flash[:notice].to match('Your new project access token has been created.')
+ expect(flash[:notice]).to match('Your new access token has been created.')
end
- it 'creates project access token' do
+ it 'creates resource access token' do
access_level = access_token_params[:access_level] || Gitlab::Access::MAINTAINER
subject
expect(created_token.name).to eq(access_token_params[:name])
expect(created_token.scopes).to eq(access_token_params[:scopes])
expect(created_token.expires_at).to eq(access_token_params[:expires_at])
- expect(project.member(created_token.user).access_level).to eq(access_level)
+ expect(resource.member(created_token.user).access_level).to eq(access_level)
end
it 'creates project bot user' do
@@ -90,12 +90,12 @@ def created_token
it 'shows a failure alert' do
subject
- expect(controller).to set_flash[:alert].to match("Failed to create new project access token: Failed!")
+ expect(flash[:alert]).to match("Failed to create new access token: Failed!")
end
end
end
-RSpec.shared_examples 'project access tokens available #revoke' do
+RSpec.shared_examples 'PUT resource access tokens available' do
it 'calls delete user worker' do
expect(DeleteUserWorker).to receive(:perform_async).with(user.id, bot_user.id, skip_authorization: true)
@@ -105,7 +105,7 @@ def created_token
it 'removes membership of bot user' do
subject
- expect(project.reload.bots).not_to include(bot_user)
+ expect(resource.reload.bots).not_to include(bot_user)
end
it 'converts issuables of the bot user to ghost user' do
@@ -121,4 +121,18 @@ def created_token
expect(User.exists?(bot_user.id)).to be_falsy
end
+
+ context 'when unsuccessful' do
+ before do
+ allow_next_instance_of(ResourceAccessTokens::RevokeService) do |service|
+ allow(service).to receive(:execute).and_return ServiceResponse.error(message: 'Failed!')
+ end
+ end
+
+ it 'shows a failure alert' do
+ subject
+
+ expect(flash[:alert]).to include("Could not revoke access token")
+ end
+ end
end
diff --git a/spec/views/shared/access_tokens/_table.html.haml_spec.rb b/spec/views/shared/access_tokens/_table.html.haml_spec.rb
index 0a23768b4f194c63229d00c19802ca2b0b27bd2e..fca2fc3183cd83627efcdab6767c492a83758480 100644
--- a/spec/views/shared/access_tokens/_table.html.haml_spec.rb
+++ b/spec/views/shared/access_tokens/_table.html.haml_spec.rb
@@ -11,7 +11,7 @@
let_it_be(:user) { create(:user) }
let_it_be(:tokens) { [create(:personal_access_token, user: user)] }
- let_it_be(:project) { false }
+ let_it_be(:resource) { false }
before do
stub_licensed_features(enforce_personal_access_token_expiration: true)
@@ -20,8 +20,8 @@
allow(view).to receive(:personal_access_token_expiration_enforced?).and_return(token_expiry_enforced?)
allow(view).to receive(:show_profile_token_expiry_notification?).and_return(true)
- if project
- project.add_maintainer(user)
+ if resource
+ resource.add_maintainer(user)
end
# Forcibly removing scopes from one token as it's not possible to do with the current modal on creation
@@ -34,7 +34,7 @@
type: type,
type_plural: type_plural,
active_tokens: tokens,
- project: project,
+ resource: resource,
impersonation: impersonation,
revoke_route_helper: ->(token) { 'path/' }
}
@@ -80,8 +80,8 @@
end
end
- context 'if project' do
- let_it_be(:project) { create(:project) }
+ context 'if resource is project' do
+ let_it_be(:resource) { create(:project) }
it 'shows the project content', :aggregate_failures do
expect(rendered).to have_selector 'th', text: 'Role'
@@ -92,6 +92,18 @@
end
end
+ context 'if resource is group' do
+ let_it_be(:resource) { create(:group) }
+
+ it 'shows the group content', :aggregate_failures do
+ expect(rendered).to have_selector 'th', text: 'Role'
+ expect(rendered).to have_selector 'td', text: 'Maintainer'
+
+ expect(rendered).not_to have_content 'Personal access tokens are not revoked upon expiration.'
+ expect(rendered).not_to have_content 'To see all the user\'s personal access tokens you must impersonate them first.'
+ end
+ end
+
context 'without tokens' do
let_it_be(:tokens) { [] }