summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-01-17 12:17:11 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-01-17 12:17:11 +0000
commitdd33e917374b611cd5a596c7fa51b47af6e153f6 (patch)
tree7e853f9843f01a2f328f334622645f53e3bb11ff
parent82b0338672c8f39245fe5f317a84f45ae387d319 (diff)
downloadgitlab-ce-dd33e917374b611cd5a596c7fa51b47af6e153f6.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/pages/groups/settings/access_tokens/index.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue13
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue8
-rw-r--r--app/assets/stylesheets/utilities.scss7
-rw-r--r--app/controllers/concerns/access_tokens_actions.rb83
-rw-r--r--app/controllers/groups/settings/access_tokens_controller.rb18
-rw-r--r--app/controllers/projects/settings/access_tokens_controller.rb70
-rw-r--r--app/finders/merge_requests_finder.rb9
-rw-r--r--app/graphql/resolvers/merge_requests_resolver.rb4
-rw-r--r--app/policies/group_member_policy.rb5
-rw-r--r--app/views/groups/settings/_permissions.html.haml2
-rw-r--r--app/views/groups/settings/_project_access_token_creation.html.haml9
-rw-r--r--app/views/groups/settings/_resource_access_token_creation.html.haml11
-rw-r--r--app/views/groups/settings/access_tokens/index.html.haml50
-rw-r--r--app/views/projects/settings/access_tokens/index.html.haml16
-rw-r--r--app/views/shared/access_tokens/_form.html.haml6
-rw-r--r--app/views/shared/access_tokens/_table.html.haml10
-rw-r--r--config/feature_flags/development/vue_epics_list.yml8
-rw-r--r--config/routes/group.rb6
-rw-r--r--db/post_migrate/20220109134455_add_idx_vulnerability_occurrences_dedup_again.rb17
-rw-r--r--db/post_migrate/20220114105525_add_index_on_projects_path.rb17
-rw-r--r--db/schema_migrations/202201091344551
-rw-r--r--db/schema_migrations/202201141055251
-rw-r--r--db/structure.sql2
-rw-r--r--doc/api/graphql/reference/index.md14
-rw-r--r--doc/api/group_access_tokens.md8
-rw-r--r--doc/api/index.md8
-rw-r--r--doc/security/token_overview.md22
-rw-r--r--doc/topics/authentication/index.md5
-rw-r--r--doc/user/group/epics/manage_epics.md1
-rw-r--r--doc/user/group/settings/group_access_tokens.md142
-rw-r--r--doc/user/packages/debian_repository/index.md2
-rw-r--r--doc/user/packages/generic_packages/index.md4
-rw-r--r--doc/user/packages/helm_repository/index.md2
-rw-r--r--doc/user/packages/terraform_module_registry/index.md2
-rw-r--r--doc/user/profile/personal_access_tokens.md4
-rw-r--r--doc/user/project/settings/project_access_tokens.md11
-rw-r--r--doc/user/search/img/code_search.pngbin0 -> 113383 bytes
-rw-r--r--doc/user/search/img/project_code_search.pngbin24924 -> 0 bytes
-rw-r--r--doc/user/search/index.md4
-rw-r--r--lib/sidebars/groups/menus/settings_menu.rb14
-rw-r--r--locale/gitlab.pot48
-rw-r--r--spec/features/groups/settings/access_tokens_spec.rb53
-rw-r--r--spec/features/projects/settings/access_tokens_spec.rb162
-rw-r--r--spec/finders/merge_requests_finder_spec.rb101
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view_spec.js13
-rw-r--r--spec/graphql/resolvers/merge_requests_resolver_spec.rb22
-rw-r--r--spec/graphql/types/project_type_spec.rb1
-rw-r--r--spec/lib/sidebars/groups/menus/settings_menu_spec.rb6
-rw-r--r--spec/policies/group_member_policy_spec.rb18
-rw-r--r--spec/requests/groups/settings/access_tokens_controller_spec.rb90
-rw-r--r--spec/requests/projects/settings/access_tokens_controller_spec.rb (renamed from spec/controllers/projects/settings/access_tokens_controller_spec.rb)47
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb1
-rw-r--r--spec/support/shared_examples/features/access_tokens_shared_examples.rb165
-rw-r--r--spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb (renamed from spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb)46
-rw-r--r--spec/views/shared/access_tokens/_table.html.haml_spec.rb24
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
new file mode 100644
index 00000000000..7c62bb6921b
--- /dev/null
+++ b/doc/user/search/img/code_search.png
Binary files differ
diff --git a/doc/user/search/img/project_code_search.png b/doc/user/search/img/project_code_search.png
deleted file mode 100644
index 5412f614a74..00000000000
--- a/doc/user/search/img/project_code_search.png
+++ /dev/null
Binary files differ
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:
-![code search results](img/project_code_search.png)
+![code search results](img/code_search.png)
### 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) { [] }