diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-01-17 12:17:11 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-01-17 12:17:11 +0000 |
commit | dd33e917374b611cd5a596c7fa51b47af6e153f6 (patch) | |
tree | 7e853f9843f01a2f328f334622645f53e3bb11ff | |
parent | 82b0338672c8f39245fe5f317a84f45ae387d319 (diff) | |
download | gitlab-ce-dd33e917374b611cd5a596c7fa51b47af6e153f6.tar.gz |
Add latest changes from gitlab-org/gitlab@master
56 files changed, 991 insertions, 425 deletions
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 00000000000..dc1bb88bf4b --- /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/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue index 09dbcc1d391..e9a2d7747e2 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue @@ -84,14 +84,6 @@ export default { showNoMatchingResultsMessage() { return Boolean(this.searchKey) && this.visibleLabels.length === 0; }, - shouldHighlighFirstItem() { - return this.searchKey !== '' && this.visibleLabels.length > 0; - }, - }, - updated() { - if (this.shouldHighlighFirstItem) { - this.$refs.labelItem[0]?.$el?.firstChild?.focus(); - } }, methods: { isLabelSelected(label) { @@ -151,14 +143,11 @@ export default { /> <template v-else> <gl-dropdown-item - v-for="(label, index) in visibleLabels" - ref="labelItem" + v-for="label in visibleLabels" :key="label.id" :is-checked="isLabelSelected(label)" :is-check-centered="true" :is-check-item="true" - :active="shouldHighlighFirstItem && index === 0" - active-class="is-focused" data-testid="labels-list" @click.native.capture.stop="handleLabelClick(label)" > diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue index 0bb0e0d9fb0..af0235bfc69 100644 --- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue +++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue @@ -193,7 +193,13 @@ export default { :title="__('This issue is hidden because its author has been banned')" :aria-label="__('Hidden')" /> - <gl-link class="issue-title-text" dir="auto" :href="webUrl" v-bind="issuableTitleProps"> + <gl-link + class="issue-title-text" + dir="auto" + :href="webUrl" + data-qa-selector="issuable_title_link" + v-bind="issuableTitleProps" + > {{ issuable.title }} <gl-icon v-if="isIssuableUrlExternal" name="external-link" class="gl-ml-2" /> </gl-link> diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index 07ef80ef7e1..0e7e52129b4 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -305,13 +305,6 @@ $gl-line-height-42: px-to-rem(42px); } } -// TODO: Move to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1671 -.gl-md-pr-5 { - @include gl-media-breakpoint-up(md) { - padding-right: $gl-spacing-scale-5; - } -} - // Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2600 .gl-pr-10 { padding-right: $gl-spacing-scale-10; diff --git a/app/controllers/concerns/access_tokens_actions.rb b/app/controllers/concerns/access_tokens_actions.rb new file mode 100644 index 00000000000..451841c43bb --- /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 00000000000..b9ab2e008cc --- /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 1ecede4c7a2..32916831ecd 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/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index ba709d3bdfc..81e4ab7014d 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -140,14 +140,13 @@ class MergeRequestsFinder < IssuableFinder # rubocop: disable CodeReuse/ActiveRecord def by_draft(items) - draft_param = params[:draft] || params[:wip] + draft_param = Gitlab::Utils.to_boolean(params.fetch(:draft) { params.fetch(:wip, nil) }) + return items if draft_param.nil? - if draft_param == 'yes' + if draft_param items.where(wip_match(items.arel_table)) - elsif draft_param == 'no' - items.where.not(wip_match(items.arel_table)) else - items + items.where.not(wip_match(items.arel_table)) end end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/graphql/resolvers/merge_requests_resolver.rb b/app/graphql/resolvers/merge_requests_resolver.rb index bd7f1f0774e..6dbcbe0e04d 100644 --- a/app/graphql/resolvers/merge_requests_resolver.rb +++ b/app/graphql/resolvers/merge_requests_resolver.rb @@ -51,6 +51,10 @@ module Resolvers required: false, description: 'Merge request state. If provided, all resolved merge requests will have this state.' + argument :draft, GraphQL::Types::Boolean, + required: false, + description: 'Limit result to draft merge requests.' + argument :labels, [GraphQL::Types::String], required: false, as: :label_name, diff --git a/app/policies/group_member_policy.rb b/app/policies/group_member_policy.rb index f7a7286aba7..a394b63fc8e 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 9a7a7521cec..d4b74665398 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 948b25390ba..00000000000 --- 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 = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.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: '</a>'.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 00000000000..160f8ae1e07 --- /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 = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: project_access_tokens_link } + - link_start_group = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.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: '</a>'.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 00000000000..16ea96f0b08 --- /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 = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.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: '</a>'.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: '</a>'.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 = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.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: '</a>'.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 4e946050881..e4b027fcc44 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: '</a>'.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 6c392f0dfe5..a52b7236137 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 1e3432ab08b..aa579b4a672 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/feature_flags/development/vue_epics_list.yml b/config/feature_flags/development/vue_epics_list.yml deleted file mode 100644 index 22e2a53aeee..00000000000 --- a/config/feature_flags/development/vue_epics_list.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: vue_epics_list -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46769 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/276189 -milestone: '13.9' -type: development -group: group::product planning -default_enabled: false diff --git a/config/routes/group.rb b/config/routes/group.rb index f7a8747d0cf..c313f7209fb 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -43,6 +43,12 @@ constraints(::Constraints::GroupUrlConstrainer.new) do 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/db/post_migrate/20220109134455_add_idx_vulnerability_occurrences_dedup_again.rb b/db/post_migrate/20220109134455_add_idx_vulnerability_occurrences_dedup_again.rb new file mode 100644 index 00000000000..06be8edd707 --- /dev/null +++ b/db/post_migrate/20220109134455_add_idx_vulnerability_occurrences_dedup_again.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddIdxVulnerabilityOccurrencesDedupAgain < Gitlab::Database::Migration[1.0] + TABLE = :vulnerability_occurrences + INDEX_NAME = 'index_vulnerability_occurrences_deduplication' + COLUMNS = %i[project_id report_type project_fingerprint] + + disable_ddl_transaction! + + def up + add_concurrent_index TABLE, COLUMNS, name: INDEX_NAME + end + + def down + # nothing to do here + end +end diff --git a/db/post_migrate/20220114105525_add_index_on_projects_path.rb b/db/post_migrate/20220114105525_add_index_on_projects_path.rb new file mode 100644 index 00000000000..cef38f91b21 --- /dev/null +++ b/db/post_migrate/20220114105525_add_index_on_projects_path.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddIndexOnProjectsPath < Gitlab::Database::Migration[1.0] + disable_ddl_transaction! + + TABLE = :projects + INDEX_NAME = 'index_on_projects_path' + COLUMN = :path + + def up + add_concurrent_index TABLE, COLUMN, name: INDEX_NAME + end + + def down + remove_concurrent_index TABLE, COLUMN, name: INDEX_NAME + end +end diff --git a/db/schema_migrations/20220109134455 b/db/schema_migrations/20220109134455 new file mode 100644 index 00000000000..7a4762e240e --- /dev/null +++ b/db/schema_migrations/20220109134455 @@ -0,0 +1 @@ +fee092680e22e579ea51f776d11bbfd6a49b936e4ab776760a153ce613e7a0cd
\ No newline at end of file diff --git a/db/schema_migrations/20220114105525 b/db/schema_migrations/20220114105525 new file mode 100644 index 00000000000..728820cbaf0 --- /dev/null +++ b/db/schema_migrations/20220114105525 @@ -0,0 +1 @@ +c9c7e8ff40fd3863095bb927f1aea27fecd5ca77dfc284a7673310e3501476c8
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 0a791ffa1fb..8f07dfeada1 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -26859,6 +26859,8 @@ CREATE UNIQUE INDEX index_on_project_id_escalation_policy_name_unique ON inciden CREATE INDEX index_on_projects_lower_path ON projects USING btree (lower((path)::text)); +CREATE INDEX index_on_projects_path ON projects USING btree (path); + CREATE INDEX index_on_routes_lower_path ON routes USING btree (lower((path)::text)); CREATE INDEX index_on_users_lower_email ON users USING btree (lower((email)::text)); diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 966e20e16b4..f78860eb481 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -11095,6 +11095,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="groupmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. | | <a id="groupmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | | <a id="groupmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. | +| <a id="groupmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. | | <a id="groupmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. | | <a id="groupmergerequestsincludesubgroups"></a>`includeSubgroups` | [`Boolean`](#boolean) | Include merge requests belonging to subgroups. | | <a id="groupmergerequestslabels"></a>`labels` | [`[String!]`](#string) | Array of label names. All resolved merge requests will have all of these labels. | @@ -12031,6 +12032,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="mergerequestassigneeassignedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. | | <a id="mergerequestassigneeassignedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | | <a id="mergerequestassigneeassignedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. | +| <a id="mergerequestassigneeassignedmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. | | <a id="mergerequestassigneeassignedmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. | | <a id="mergerequestassigneeassignedmergerequestslabels"></a>`labels` | [`[String!]`](#string) | Array of label names. All resolved merge requests will have all of these labels. | | <a id="mergerequestassigneeassignedmergerequestsmergedafter"></a>`mergedAfter` | [`Time`](#time) | Merge requests merged after this date. | @@ -12062,6 +12064,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="mergerequestassigneeauthoredmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. | | <a id="mergerequestassigneeauthoredmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | | <a id="mergerequestassigneeauthoredmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. | +| <a id="mergerequestassigneeauthoredmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. | | <a id="mergerequestassigneeauthoredmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. | | <a id="mergerequestassigneeauthoredmergerequestslabels"></a>`labels` | [`[String!]`](#string) | Array of label names. All resolved merge requests will have all of these labels. | | <a id="mergerequestassigneeauthoredmergerequestsmergedafter"></a>`mergedAfter` | [`Time`](#time) | Merge requests merged after this date. | @@ -12111,6 +12114,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="mergerequestassigneereviewrequestedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. | | <a id="mergerequestassigneereviewrequestedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | | <a id="mergerequestassigneereviewrequestedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. | +| <a id="mergerequestassigneereviewrequestedmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. | | <a id="mergerequestassigneereviewrequestedmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. | | <a id="mergerequestassigneereviewrequestedmergerequestslabels"></a>`labels` | [`[String!]`](#string) | Array of label names. All resolved merge requests will have all of these labels. | | <a id="mergerequestassigneereviewrequestedmergerequestsmergedafter"></a>`mergedAfter` | [`Time`](#time) | Merge requests merged after this date. | @@ -12283,6 +12287,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="mergerequestreviewerassignedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. | | <a id="mergerequestreviewerassignedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | | <a id="mergerequestreviewerassignedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. | +| <a id="mergerequestreviewerassignedmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. | | <a id="mergerequestreviewerassignedmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. | | <a id="mergerequestreviewerassignedmergerequestslabels"></a>`labels` | [`[String!]`](#string) | Array of label names. All resolved merge requests will have all of these labels. | | <a id="mergerequestreviewerassignedmergerequestsmergedafter"></a>`mergedAfter` | [`Time`](#time) | Merge requests merged after this date. | @@ -12314,6 +12319,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="mergerequestreviewerauthoredmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. | | <a id="mergerequestreviewerauthoredmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | | <a id="mergerequestreviewerauthoredmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. | +| <a id="mergerequestreviewerauthoredmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. | | <a id="mergerequestreviewerauthoredmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. | | <a id="mergerequestreviewerauthoredmergerequestslabels"></a>`labels` | [`[String!]`](#string) | Array of label names. All resolved merge requests will have all of these labels. | | <a id="mergerequestreviewerauthoredmergerequestsmergedafter"></a>`mergedAfter` | [`Time`](#time) | Merge requests merged after this date. | @@ -12363,6 +12369,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="mergerequestreviewerreviewrequestedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. | | <a id="mergerequestreviewerreviewrequestedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | | <a id="mergerequestreviewerreviewrequestedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. | +| <a id="mergerequestreviewerreviewrequestedmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. | | <a id="mergerequestreviewerreviewrequestedmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. | | <a id="mergerequestreviewerreviewrequestedmergerequestslabels"></a>`labels` | [`[String!]`](#string) | Array of label names. All resolved merge requests will have all of these labels. | | <a id="mergerequestreviewerreviewrequestedmergerequestsmergedafter"></a>`mergedAfter` | [`Time`](#time) | Merge requests merged after this date. | @@ -13799,6 +13806,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="projectmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. | | <a id="projectmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | | <a id="projectmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. | +| <a id="projectmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. | | <a id="projectmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. | | <a id="projectmergerequestslabels"></a>`labels` | [`[String!]`](#string) | Array of label names. All resolved merge requests will have all of these labels. | | <a id="projectmergerequestsmergedafter"></a>`mergedAfter` | [`Time`](#time) | Merge requests merged after this date. | @@ -15462,6 +15470,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="usercoreassignedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. | | <a id="usercoreassignedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | | <a id="usercoreassignedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. | +| <a id="usercoreassignedmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. | | <a id="usercoreassignedmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. | | <a id="usercoreassignedmergerequestslabels"></a>`labels` | [`[String!]`](#string) | Array of label names. All resolved merge requests will have all of these labels. | | <a id="usercoreassignedmergerequestsmergedafter"></a>`mergedAfter` | [`Time`](#time) | Merge requests merged after this date. | @@ -15493,6 +15502,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="usercoreauthoredmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. | | <a id="usercoreauthoredmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | | <a id="usercoreauthoredmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. | +| <a id="usercoreauthoredmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. | | <a id="usercoreauthoredmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. | | <a id="usercoreauthoredmergerequestslabels"></a>`labels` | [`[String!]`](#string) | Array of label names. All resolved merge requests will have all of these labels. | | <a id="usercoreauthoredmergerequestsmergedafter"></a>`mergedAfter` | [`Time`](#time) | Merge requests merged after this date. | @@ -15542,6 +15552,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="usercorereviewrequestedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. | | <a id="usercorereviewrequestedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | | <a id="usercorereviewrequestedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. | +| <a id="usercorereviewrequestedmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. | | <a id="usercorereviewrequestedmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. | | <a id="usercorereviewrequestedmergerequestslabels"></a>`labels` | [`[String!]`](#string) | Array of label names. All resolved merge requests will have all of these labels. | | <a id="usercorereviewrequestedmergerequestsmergedafter"></a>`mergedAfter` | [`Time`](#time) | Merge requests merged after this date. | @@ -18694,6 +18705,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="userassignedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. | | <a id="userassignedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | | <a id="userassignedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. | +| <a id="userassignedmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. | | <a id="userassignedmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. | | <a id="userassignedmergerequestslabels"></a>`labels` | [`[String!]`](#string) | Array of label names. All resolved merge requests will have all of these labels. | | <a id="userassignedmergerequestsmergedafter"></a>`mergedAfter` | [`Time`](#time) | Merge requests merged after this date. | @@ -18725,6 +18737,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="userauthoredmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. | | <a id="userauthoredmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | | <a id="userauthoredmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. | +| <a id="userauthoredmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. | | <a id="userauthoredmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. | | <a id="userauthoredmergerequestslabels"></a>`labels` | [`[String!]`](#string) | Array of label names. All resolved merge requests will have all of these labels. | | <a id="userauthoredmergerequestsmergedafter"></a>`mergedAfter` | [`Time`](#time) | Merge requests merged after this date. | @@ -18774,6 +18787,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="userreviewrequestedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. | | <a id="userreviewrequestedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. | | <a id="userreviewrequestedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. | +| <a id="userreviewrequestedmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. | | <a id="userreviewrequestedmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. | | <a id="userreviewrequestedmergerequestslabels"></a>`labels` | [`[String!]`](#string) | Array of label names. All resolved merge requests will have all of these labels. | | <a id="userreviewrequestedmergerequestsmergedafter"></a>`mergedAfter` | [`Time`](#time) | Merge requests merged after this date. | diff --git a/doc/api/group_access_tokens.md b/doc/api/group_access_tokens.md index 71c6828de49..37471b9d89d 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: <your_access_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: <your_access_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 75081897a65..69db971f58c 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=<your_access_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: <your_access_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 <your_access_token>" "https://gitlab.example.com/api/v4/projects" diff --git a/doc/security/token_overview.md b/doc/security/token_overview.md index 0ef79bc67a9..578bb03563f 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 2a0a0bcfbb5..2a301e6ff5b 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/epics/manage_epics.md b/doc/user/group/epics/manage_epics.md index f53e41d38af..caca10a05a2 100644 --- a/doc/user/group/epics/manage_epics.md +++ b/doc/user/group/epics/manage_epics.md @@ -164,6 +164,7 @@ than 1000. The cached value is rounded to thousands or millions and updated ever > - Searching by the user's reaction emoji [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/325630) in GitLab 13.11. > - Sorting by epic titles [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/331625) in GitLab 14.1. > - Searching by milestone and confidentiality [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/268372) in GitLab 14.2 [with a flag](../../../administration/feature_flags.md) named `vue_epics_list`. Disabled by default. +> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/276189) in GitLab 14.7. You can search for an epic from the list of epics using filtered search bar based on following parameters: 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 00000000000..4857a0e74de --- /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 89427174dcd..a8f0672e376 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 58b012ce656..7b44b5bcbb7 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 488345965f9..73298afc9cd 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 b8dc071fc30..bb9f32d1144 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 a8fbdb2fa60..45cff326332 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 6199b3c4a98..90e9df90593 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/doc/user/search/img/code_search.png b/doc/user/search/img/code_search.png Binary files differnew file mode 100644 index 00000000000..7c62bb6921b --- /dev/null +++ b/doc/user/search/img/code_search.png diff --git a/doc/user/search/img/project_code_search.png b/doc/user/search/img/project_code_search.png Binary files differdeleted file mode 100644 index 5412f614a74..00000000000 --- a/doc/user/search/img/project_code_search.png +++ /dev/null diff --git a/doc/user/search/index.md b/doc/user/search/index.md index 46c14860956..0e2be455a0c 100644 --- a/doc/user/search/index.md +++ b/doc/user/search/index.md @@ -270,8 +270,10 @@ search, or choose a specific group or project. To search through code or other documents in a single project, you can use the search field on the top-right of your screen while the project page is open. +Code search shows only the first result in the file. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/327052) +in GitLab 14.7, you can access Git blame from any line that returned a result from the code search: - + ### SHA search diff --git a/lib/sidebars/groups/menus/settings_menu.rb b/lib/sidebars/groups/menus/settings_menu.rb index f0239ca6a1a..810b467ed2d 100644 --- a/lib/sidebars/groups/menus/settings_menu.rb +++ b/lib/sidebars/groups/menus/settings_menu.rb @@ -10,6 +10,7 @@ module Sidebars 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 @@ module Sidebars ) 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 9300c678053..c3d67ca8f8f 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 00000000000..20787c4c2f5 --- /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 d8de9e0449e..122bf267021 100644 --- a/spec/features/projects/settings/access_tokens_spec.rb +++ b/spec/features/projects/settings/access_tokens_spec.rb @@ -7,6 +7,7 @@ RSpec.describe 'Project > Settings > Access Tokens', :js do 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 @@ RSpec.describe 'Project > Settings > Access Tokens', :js do 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 @@ RSpec.describe 'Project > Settings > Access Tokens', :js do 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/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 03639bc0b98..0b6c438fd02 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -278,33 +278,38 @@ RSpec.describe MergeRequestsFinder do end describe 'draft state' do - let!(:wip_merge_request1) { create(:merge_request, :simple, author: user, source_project: project5, target_project: project5, title: 'WIP: thing') } - let!(:wip_merge_request2) { create(:merge_request, :simple, author: user, source_project: project6, target_project: project6, title: 'wip thing') } - let!(:wip_merge_request3) { create(:merge_request, :simple, author: user, source_project: project1, target_project: project1, title: '[wip] thing') } - let!(:wip_merge_request4) { create(:merge_request, :simple, author: user, source_project: project1, target_project: project2, title: 'wip: thing') } - let!(:draft_merge_request1) { create(:merge_request, :simple, author: user, source_branch: 'draft1', source_project: project5, target_project: project5, title: 'Draft: thing') } - let!(:draft_merge_request2) { create(:merge_request, :simple, author: user, source_branch: 'draft2', source_project: project6, target_project: project6, title: '[draft] thing') } - let!(:draft_merge_request3) { create(:merge_request, :simple, author: user, source_branch: 'draft3', source_project: project1, target_project: project1, title: '(draft) thing') } - let!(:draft_merge_request4) { create(:merge_request, :simple, author: user, source_branch: 'draft4', source_project: project1, target_project: project2, title: 'Draft - thing') } - - [:wip, :draft].each do |draft_param_key| - it "filters by #{draft_param_key}" do - params = { draft_param_key => 'yes' } + shared_examples 'draft MRs filtering' do |draft_param_key, draft_param_value, title_prefix, draft_only| + it "filters by #{draft_param_key} => #{draft_param_value}" do + merge_request1.reload.update!(title: "#{title_prefix} #{merge_request1.title}") + + params = { draft_param_key => draft_param_value } merge_requests = described_class.new(user, params).execute - expect(merge_requests).to contain_exactly( - merge_request4, merge_request5, wip_merge_request1, wip_merge_request2, wip_merge_request3, wip_merge_request4, - draft_merge_request1, draft_merge_request2, draft_merge_request3, draft_merge_request4 - ) + if draft_only + expect(merge_requests).to contain_exactly(merge_request1, merge_request4, merge_request5) + else + expect(merge_requests).to contain_exactly(merge_request2, merge_request3) + end end + end - it "filters by not #{draft_param_key}" do - params = { draft_param_key => 'no' } - - merge_requests = described_class.new(user, params).execute + { + wip: ["WIP:", "wip", "[wip]"], + draft: ["Draft:", "Draft -", "[Draft]", "(Draft)"] + }.each do |draft_param_key, title_prefixes| + title_prefixes.each do |title_prefix| + it_behaves_like 'draft MRs filtering', draft_param_key, 1, title_prefix, true + it_behaves_like 'draft MRs filtering', draft_param_key, '1', title_prefix, true + it_behaves_like 'draft MRs filtering', draft_param_key, true, title_prefix, true + it_behaves_like 'draft MRs filtering', draft_param_key, 'true', title_prefix, true + it_behaves_like 'draft MRs filtering', draft_param_key, 'yes', title_prefix, true - expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request3) + it_behaves_like 'draft MRs filtering', draft_param_key, 0, title_prefix, false + it_behaves_like 'draft MRs filtering', draft_param_key, '0', title_prefix, false + it_behaves_like 'draft MRs filtering', draft_param_key, false, title_prefix, false + it_behaves_like 'draft MRs filtering', draft_param_key, 'false', title_prefix, false + it_behaves_like 'draft MRs filtering', draft_param_key, 'no', title_prefix, false end it "returns all items if no valid #{draft_param_key} param exists" do @@ -313,43 +318,41 @@ RSpec.describe MergeRequestsFinder do merge_requests = described_class.new(user, params).execute expect(merge_requests).to contain_exactly( - merge_request1, merge_request2, merge_request3, merge_request4, - merge_request5, wip_merge_request1, wip_merge_request2, wip_merge_request3, wip_merge_request4, - draft_merge_request1, draft_merge_request2, draft_merge_request3, draft_merge_request4 + merge_request1, merge_request2, merge_request3, merge_request4, merge_request5 ) end end + end - context 'filter by deployment' do - let_it_be(:project_with_repo) { create(:project, :repository) } + context 'filter by deployment' do + let_it_be(:project_with_repo) { create(:project, :repository) } - it 'returns the relevant merge requests' do - deployment1 = create( - :deployment, - project: project_with_repo, - sha: project_with_repo.commit.id - ) - deployment2 = create( - :deployment, - project: project_with_repo, - sha: project_with_repo.commit.id - ) - deployment1.link_merge_requests(MergeRequest.where(id: [merge_request1.id, merge_request2.id])) - deployment2.link_merge_requests(MergeRequest.where(id: merge_request3.id)) + it 'returns the relevant merge requests' do + deployment1 = create( + :deployment, + project: project_with_repo, + sha: project_with_repo.commit.id + ) + deployment2 = create( + :deployment, + project: project_with_repo, + sha: project_with_repo.commit.id + ) + deployment1.link_merge_requests(MergeRequest.where(id: [merge_request1.id, merge_request2.id])) + deployment2.link_merge_requests(MergeRequest.where(id: merge_request3.id)) - params = { deployment_id: deployment1.id } - merge_requests = described_class.new(user, params).execute + params = { deployment_id: deployment1.id } + merge_requests = described_class.new(user, params).execute - expect(merge_requests).to contain_exactly(merge_request1, merge_request2) - end + expect(merge_requests).to contain_exactly(merge_request1, merge_request2) + end - context 'when a deployment does not contain any merge requests' do - it 'returns an empty result' do - params = { deployment_id: create(:deployment, project: project_with_repo, sha: project_with_repo.commit.sha).id } - merge_requests = described_class.new(user, params).execute + context 'when a deployment does not contain any merge requests' do + it 'returns an empty result' do + params = { deployment_id: create(:deployment, project: project_with_repo, sha: project_with_repo.commit.sha).id } + merge_requests = described_class.new(user, params).execute - expect(merge_requests).to be_empty - end + expect(merge_requests).to be_empty end end end diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js index 080b13e56b6..6f5a4b7e613 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js @@ -110,19 +110,6 @@ describe('DropdownContentsLabelsView', () => { }); }); - it('first item is active when search is not empty', async () => { - createComponent({ - queryHandler: jest.fn().mockResolvedValue(workspaceLabelsQueryResponse), - searchKey: 'Label', - }); - await makeObserverAppear(); - await waitForPromises(); - await nextTick(); - - expect(findLabelsList().exists()).toBe(true); - expect(findFirstLabel().attributes('active')).toBe('true'); - }); - it('when search returns 0 results', async () => { createComponent({ queryHandler: jest.fn().mockResolvedValue({ diff --git a/spec/graphql/resolvers/merge_requests_resolver_spec.rb b/spec/graphql/resolvers/merge_requests_resolver_spec.rb index a931b0a3f77..1d0eac30a23 100644 --- a/spec/graphql/resolvers/merge_requests_resolver_spec.rb +++ b/spec/graphql/resolvers/merge_requests_resolver_spec.rb @@ -172,6 +172,28 @@ RSpec.describe Resolvers::MergeRequestsResolver do end end + context 'with draft argument' do + before do + merge_request_4.update!(title: MergeRequest.wip_title(merge_request_4.title)) + end + + context 'with draft: true argument' do + it 'takes one argument' do + result = resolve_mr(project, draft: true) + + expect(result).to contain_exactly(merge_request_4) + end + end + + context 'with draft: false argument' do + it 'takes one argument' do + result = resolve_mr(project, draft: false) + + expect(result).not_to contain_exactly(merge_request_1, merge_request_2, merge_request_3, merge_request_5, merge_request_6) + end + end + end + context 'with label argument' do let_it_be(:label) { merge_request_6.labels.first } let_it_be(:with_label) { create(:labeled_merge_request, :closed, labels: [label], **common_attrs) } diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index e103f6eb34b..cd216232569 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -289,6 +289,7 @@ RSpec.describe GitlabSchema.types['Project'] do :source_branches, :target_branches, :state, + :draft, :labels, :before, :after, diff --git a/spec/lib/sidebars/groups/menus/settings_menu_spec.rb b/spec/lib/sidebars/groups/menus/settings_menu_spec.rb index 314c4cdc602..252da8ea699 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 @@ RSpec.describe Sidebars::Groups::Menus::SettingsMenu do 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 d283b0ffda5..50774313aae 100644 --- a/spec/policies/group_member_policy_spec.rb +++ b/spec/policies/group_member_policy_spec.rb @@ -83,6 +83,23 @@ RSpec.describe GroupMemberPolicy do 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 @@ RSpec.describe GroupMemberPolicy do 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 00000000000..eabdef3c41e --- /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 index 834a9e276f9..780d1b8caef 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 @@ RSpec.describe Projects::Settings::AccessTokensController do 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 @@ RSpec.describe Projects::Settings::AccessTokensController do 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 085f1f13c2c..27967850389 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -142,6 +142,7 @@ RSpec.shared_context 'group navbar structure' do 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 00000000000..ae246a87bb6 --- /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 index 9287bbd29fb..6cd871d354c 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 @@ RSpec.shared_examples 'project access tokens available #index' do 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 @@ RSpec.shared_examples 'project access tokens available #create' do 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 @@ RSpec.shared_examples 'project access tokens available #create' do 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 @@ RSpec.shared_examples 'project access tokens available #revoke' do 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 @@ RSpec.shared_examples 'project access tokens available #revoke' do 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 0a23768b4f1..fca2fc3183c 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 @@ RSpec.describe 'shared/access_tokens/_table.html.haml' do 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 @@ RSpec.describe 'shared/access_tokens/_table.html.haml' do 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 @@ RSpec.describe 'shared/access_tokens/_table.html.haml' do type: type, type_plural: type_plural, active_tokens: tokens, - project: project, + resource: resource, impersonation: impersonation, revoke_route_helper: ->(token) { 'path/' } } @@ -80,8 +80,8 @@ RSpec.describe 'shared/access_tokens/_table.html.haml' do 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 @@ RSpec.describe 'shared/access_tokens/_table.html.haml' do 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) { [] } |