From a1a5cb34efe5218b562c69e621fc586625b3ed0a Mon Sep 17 00:00:00 2001 From: Fabio Huser Date: Wed, 29 Dec 2021 13:55:23 +0100 Subject: [PATCH] Add group level access token UI This commit is a follow up to https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77236 and adds the group level counterpart to the existing Project Access Token UI. Group Access Tokens can already be created by instance administrators via rails console or users via GitLab REST API but not yet on the UI itself. The new page allows group owners to list, create and delete said tokens. Closes https://gitlab.com/gitlab-org/gitlab/-/issues/214045 Changelog: added --- .../groups/settings/access_tokens/index.js | 3 + .../concerns/access_tokens_actions.rb | 83 +++++++++ .../settings/access_tokens_controller.rb | 18 ++ .../settings/access_tokens_controller.rb | 70 +------- app/policies/group_member_policy.rb | 5 +- .../groups/settings/_permissions.html.haml | 2 +- .../_project_access_token_creation.html.haml | 9 - .../_resource_access_token_creation.html.haml | 11 ++ .../settings/access_tokens/index.html.haml | 50 ++++++ .../settings/access_tokens/index.html.haml | 16 +- .../shared/access_tokens/_form.html.haml | 6 +- .../shared/access_tokens/_table.html.haml | 10 +- config/routes/group.rb | 6 + doc/api/group_access_tokens.md | 8 +- doc/api/index.md | 8 +- doc/security/token_overview.md | 22 +-- doc/topics/authentication/index.md | 5 +- .../group/settings/group_access_tokens.md | 142 +++++++++++++++ doc/user/packages/debian_repository/index.md | 2 +- doc/user/packages/generic_packages/index.md | 4 +- doc/user/packages/helm_repository/index.md | 2 +- .../terraform_module_registry/index.md | 2 +- doc/user/profile/personal_access_tokens.md | 4 +- .../project/settings/project_access_tokens.md | 11 +- .../settings/access_tokens_controller_spec.rb | 39 +++-- lib/sidebars/groups/menus/settings_menu.rb | 14 ++ locale/gitlab.pot | 48 +++-- .../groups/settings/access_tokens_spec.rb | 53 ++++++ .../projects/settings/access_tokens_spec.rb | 162 ++--------------- .../groups/menus/settings_menu_spec.rb | 6 + spec/policies/group_member_policy_spec.rb | 18 ++ .../settings/access_tokens_controller_spec.rb | 90 ++++++++++ .../settings/access_tokens_controller_spec.rb | 47 +++-- .../navbar_structure_context.rb | 1 + .../features/access_tokens_shared_examples.rb | 165 ++++++++++++++++++ ...ccess_tokens_controller_shared_examples.rb | 46 +++-- .../access_tokens/_table.html.haml_spec.rb | 24 ++- 37 files changed, 868 insertions(+), 344 deletions(-) create mode 100644 app/assets/javascripts/pages/groups/settings/access_tokens/index.js create mode 100644 app/controllers/concerns/access_tokens_actions.rb create mode 100644 app/controllers/groups/settings/access_tokens_controller.rb delete mode 100644 app/views/groups/settings/_project_access_token_creation.html.haml create mode 100644 app/views/groups/settings/_resource_access_token_creation.html.haml create mode 100644 app/views/groups/settings/access_tokens/index.html.haml create mode 100644 doc/user/group/settings/group_access_tokens.md rename ee/spec/{controllers => requests}/projects/settings/access_tokens_controller_spec.rb (51%) create mode 100644 spec/features/groups/settings/access_tokens_spec.rb create mode 100644 spec/requests/groups/settings/access_tokens_controller_spec.rb rename spec/{controllers => requests}/projects/settings/access_tokens_controller_spec.rb (50%) create mode 100644 spec/support/shared_examples/features/access_tokens_shared_examples.rb rename spec/support/shared_examples/{controllers => requests}/access_tokens_controller_shared_examples.rb (60%) 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 00000000000000..dc1bb88bf4b99b --- /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 00000000000000..451841c43bb4c4 --- /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 00000000000000..b9ab2e008ccbc0 --- /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 1ecede4c7a2714..32916831ecd8fc 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 f7a7286aba73b9..a394b63fc8eeef 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 9a7a7521cece5b..d4b7466539863e 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 948b25390ba0da..00000000000000 --- 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 00000000000000..160f8ae1e077a9 --- /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 00000000000000..16ea96f0b08548 --- /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 4e946050881cd4..e4b027fcc44db1 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 6c392f0dfe542f..a52b7236137fe8 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 1e3432ab08b12b..aa579b4a672058 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 f7a8747d0cf2fb..c313f7209fb6c4 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 71c6828de49636..37471b9d89d813 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 75081897a653eb..69db971f58c2be 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 0ef79bc67a9899..578bb03563f4c8 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 2a0a0bcfbb57e8..2a301e6ff5bb1f 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 00000000000000..4857a0e74de0f4 --- /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 89427174dcdcc4..a8f0672e376a5c 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 58b012ce656d26..7b44b5bcbb7bf7 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 488345965f904b..73298afc9cde60 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 b8dc071fc302f6..bb9f32d114413a 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 a8fbdb2fa60d18..45cff326332e39 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 6199b3c4a98466..90e9df90593adc 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 a5ecf90c892651..bf72b0016ca043 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 f0239ca6a1a9bb..810b467ed2d447 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 9300c67805336c..c3d67ca8f8f220 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 00000000000000..20787c4c2f5a84 --- /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 d8de9e0449eaad..122bf267021e68 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 314c4cdc602e0d..252da8ea69961c 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 d283b0ffda561a..50774313aaeea8 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 00000000000000..eabdef3c41e984 --- /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 834a9e276f92a8..780d1b8caefd5f 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 085f1f13c2c04d..27967850389025 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 00000000000000..ae246a87bb617b --- /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 9287bbd29fb6a4..6cd871d354c38e 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 0a23768b4f194c..fca2fc3183cd83 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) { [] } -- GitLab