summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/docs.gitlab-ci.yml3
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock8
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js22
-rw-r--r--app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue168
-rw-r--r--app/assets/javascripts/project_find_file.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/split_button.vue6
-rw-r--r--app/finders/groups_finder.rb2
-rw-r--r--app/finders/issues_finder.rb2
-rw-r--r--app/finders/keys_finder.rb32
-rw-r--r--app/finders/merge_request_target_project_finder.rb20
-rw-r--r--app/finders/personal_access_tokens_finder.rb16
-rw-r--r--app/helpers/merge_requests_helper.rb2
-rw-r--r--app/helpers/users_helper.rb8
-rw-r--r--app/models/ci/build.rb2
-rw-r--r--app/models/issue.rb2
-rw-r--r--app/models/key.rb4
-rw-r--r--app/models/personal_access_token.rb3
-rw-r--r--app/models/project.rb1
-rw-r--r--app/models/project_feature.rb2
-rw-r--r--app/models/user.rb4
-rw-r--r--app/policies/base_policy.rb1
-rw-r--r--app/views/clusters/clusters/_advanced_settings.html.haml3
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml2
-rw-r--r--app/views/shared/projects/_list.html.haml1
-rw-r--r--changelogs/unreleased/12390-document-sbt-support.yml5
-rw-r--r--changelogs/unreleased/12811-add-pip-version-dep-scanning-flag.yml5
-rw-r--r--changelogs/unreleased/31662-fix-excessive-find-file-escaping.yml5
-rw-r--r--changelogs/unreleased/34261-toggle-service-desk.yml5
-rw-r--r--changelogs/unreleased/34457-remove-n-plus-1-search.yml5
-rw-r--r--changelogs/unreleased/chore-rename-user-full-private-access.yml5
-rw-r--r--config/helpers/vendor_dll_hash.js23
-rw-r--r--config/webpack.config.js43
-rw-r--r--config/webpack.vendor.config.js71
-rw-r--r--db/migrate/20191205060723_add_index_to_keys.rb17
-rw-r--r--db/schema.rb1
-rw-r--r--doc/README.md1
-rw-r--r--doc/administration/compliance.md1
-rw-r--r--doc/administration/high_availability/redis.md2
-rw-r--r--doc/administration/index.md1
-rw-r--r--doc/api/projects.md1
-rw-r--r--doc/user/admin_area/credentials_inventory.md19
-rw-r--r--doc/user/admin_area/img/credentials_inventory_v12_6.pngbin0 -> 164125 bytes
-rw-r--r--doc/user/application_security/dependency_scanning/index.md2
-rw-r--r--doc/user/infrastructure/index.md6
-rw-r--r--doc/user/project/operations/index.md1
-rw-r--r--jest.config.js1
-rw-r--r--lib/api/groups.rb2
-rw-r--r--lib/api/helpers.rb4
-rw-r--r--lib/api/helpers/project_snapshots_helpers.rb2
-rw-r--r--lib/api/keys.rb6
-rw-r--r--lib/api/pages.rb2
-rw-r--r--lib/api/pages_domains.rb2
-rw-r--r--lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml1
-rw-r--r--lib/gitlab/visibility_level.rb2
-rw-r--r--lib/gitlab/webpack/manifest.rb3
-rw-r--r--lib/tasks/gitlab/assets.rake8
-rw-r--r--locale/gitlab.pot60
-rw-r--r--package.json1
-rw-r--r--scripts/utils.sh3
-rw-r--r--spec/factories/merge_requests.rb4
-rw-r--r--spec/features/groups/clusters/user_spec.rb8
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb8
-rw-r--r--spec/features/projects/clusters/user_spec.rb8
-rw-r--r--spec/finders/keys_finder_spec.rb153
-rw-r--r--spec/finders/merge_request_target_project_finder_spec.rb16
-rw-r--r--spec/finders/personal_access_tokens_finder_spec.rb10
-rw-r--r--spec/frontend/boards/boards_store_spec.js1
-rw-r--r--spec/frontend/clusters/clusters_bundle_spec.js1
-rw-r--r--spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap80
-rw-r--r--spec/frontend/clusters/components/remove_cluster_confirmation_spec.js57
-rw-r--r--spec/frontend/commit/commit_pipeline_status_component_spec.js1
-rw-r--r--spec/frontend/create_cluster/eks_cluster/store/actions_spec.js11
-rw-r--r--spec/frontend/ide/components/preview/clientside_spec.js4
-rw-r--r--spec/frontend/ide/stores/modules/pipelines/actions_spec.js2
-rw-r--r--spec/frontend/issuables_list/components/issuables_list_app_spec.js1
-rw-r--r--spec/frontend/jest_self_check/mocks_spec.js43
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js1
-rw-r--r--spec/frontend/pages/admin/users/components/user_modal_manager_spec.js4
-rw-r--r--spec/frontend/project_find_file_spec.js45
-rw-r--r--spec/frontend/registry/list/components/app_spec.js1
-rw-r--r--spec/frontend/repository/components/table/row_spec.js1
-rw-r--r--spec/frontend/sentry/sentry_config_spec.js7
-rw-r--r--spec/frontend/sidebar/confidential_issue_sidebar_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap1
-rw-r--r--spec/models/key_spec.rb26
-rw-r--r--spec/models/personal_access_token_spec.rb12
-rw-r--r--spec/models/project_spec.rb4
-rw-r--r--spec/models/user_spec.rb8
-rw-r--r--spec/requests/projects/merge_requests/creations_spec.rb28
91 files changed, 1024 insertions, 160 deletions
diff --git a/.gitlab/ci/docs.gitlab-ci.yml b/.gitlab/ci/docs.gitlab-ci.yml
index 07375fca611..cd0e4085e10 100644
--- a/.gitlab/ci/docs.gitlab-ci.yml
+++ b/.gitlab/ci/docs.gitlab-ci.yml
@@ -24,7 +24,8 @@
- apk add --update openssl
- wget $CI_PROJECT_URL/raw/$CI_COMMIT_SHA/scripts/trigger-build-docs
- chmod 755 trigger-build-docs
- - gem install gitlab --no-document
+ - gem install httparty --no-document --version 0.17.3
+ - gem install gitlab --no-document --version 4.13.0
# Always trigger a docs build in gitlab-docs only on docs-only branches.
# Useful to preview the docs changes live.
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index 0c83f87eac6..5bd19309528 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -244,7 +244,9 @@ webpack-dev-server:
dependencies: ["setup-test-env", "compile-assets pull-cache"]
variables:
WEBPACK_MEMORY_TEST: "true"
+ WEBPACK_VENDOR_DLL: "true"
script:
+ - yarn webpack-vendor
- node --expose-gc node_modules/.bin/webpack-dev-server --config config/webpack.config.js
artifacts:
name: webpack-dev-server
diff --git a/Gemfile b/Gemfile
index 219c190c26d..b6f57297c07 100644
--- a/Gemfile
+++ b/Gemfile
@@ -102,7 +102,7 @@ gem 'hashie-forbidden_attributes'
gem 'kaminari', '~> 1.0'
# HAML
-gem 'hamlit', '~> 2.10.0'
+gem 'hamlit', '~> 2.11.0'
# Files attachments
gem 'carrierwave', '~> 1.3'
diff --git a/Gemfile.lock b/Gemfile.lock
index 812b21a836d..0e322705862 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -471,7 +471,7 @@ GEM
rainbow
rubocop (>= 0.50.0)
sysexits (~> 1.1)
- hamlit (2.10.0)
+ hamlit (2.11.0)
temple (>= 0.8.2)
thor
tilt
@@ -1029,10 +1029,10 @@ GEM
daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0, >= 1.0.4)
rack (>= 1, < 3)
- thor (0.19.4)
+ thor (0.20.3)
thread_safe (0.3.6)
thrift (0.11.0.0)
- tilt (2.0.9)
+ tilt (2.0.10)
timecop (0.8.1)
timfel-krb5-auth (0.8.3)
toml (0.2.0)
@@ -1224,7 +1224,7 @@ DEPENDENCIES
gssapi
guard-rspec
haml_lint (~> 0.34.0)
- hamlit (~> 2.10.0)
+ hamlit (~> 2.11.0)
hangouts-chat (~> 0.0.5)
hashie-forbidden_attributes
health_check (~> 2.6.0)
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index f4a7e64ceee..d990d2677a8 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -12,6 +12,7 @@ import { APPLICATION_STATUS, INGRESS, INGRESS_DOMAIN_SUFFIX, CROSSPLANE } from '
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
import Applications from './components/applications.vue';
+import RemoveClusterConfirmation from './components/remove_cluster_confirmation.vue';
import setupToggleButtons from '../toggle_buttons';
import initProjectSelectDropdown from '~/project_select';
@@ -144,6 +145,8 @@ export default class Clusters {
() => this.handlePollError(),
);
}
+
+ this.initRemoveClusterActions();
}
initApplications(type) {
@@ -205,6 +208,25 @@ export default class Clusters {
});
}
+ initRemoveClusterActions() {
+ const el = document.querySelector('#js-cluster-remove-actions');
+ if (el && el.dataset) {
+ const { clusterName, clusterPath } = el.dataset;
+
+ this.removeClusterAction = new Vue({
+ el,
+ render(createElement) {
+ return createElement(RemoveClusterConfirmation, {
+ props: {
+ clusterName,
+ clusterPath,
+ },
+ });
+ },
+ });
+ }
+ }
+
handleClusterEnvironmentsSuccess(data) {
this.store.toggleFetchEnvironments(false);
this.store.updateEnvironments(data.data);
diff --git a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
new file mode 100644
index 00000000000..c31ba7ef14a
--- /dev/null
+++ b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
@@ -0,0 +1,168 @@
+<script>
+import _ from 'underscore';
+import SplitButton from '~/vue_shared/components/split_button.vue';
+import { GlModal, GlButton, GlFormInput } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
+import csrf from '~/lib/utils/csrf';
+
+const splitButtonActionItems = [
+ {
+ title: s__('ClusterIntegration|Remove integration and resources'),
+ description: s__(
+ 'ClusterIntegration|Deletes all GitLab resources attached to this cluster during removal',
+ ),
+ eventName: 'remove-cluster-and-cleanup',
+ },
+ {
+ title: s__('ClusterIntegration|Remove integration'),
+ description: s__(
+ 'ClusterIntegration|Removes cluster from project but keeps associated resources',
+ ),
+ eventName: 'remove-cluster',
+ },
+];
+
+export default {
+ splitButtonActionItems,
+ components: {
+ SplitButton,
+ GlModal,
+ GlButton,
+ GlFormInput,
+ },
+ props: {
+ clusterPath: {
+ type: String,
+ required: true,
+ },
+ clusterName: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ enteredClusterName: '',
+ confirmCleanup: false,
+ };
+ },
+ computed: {
+ csrfToken() {
+ return csrf.token;
+ },
+ modalTitle() {
+ return this.confirmCleanup
+ ? s__('ClusterIntegration|Remove integration and resources?')
+ : s__('ClusterIntegration|Remove integration?');
+ },
+ warningMessage() {
+ return this.confirmCleanup
+ ? s__(
+ 'ClusterIntegration|You are about to remove your cluster integration and all GitLab-created resources associated with this cluster.',
+ )
+ : s__('ClusterIntegration|You are about to remove your cluster integration.');
+ },
+ warningToBeRemoved() {
+ return s__(`ClusterIntegration|
+ This will permanently delete the following resources:
+ <ul>
+ <li>All installed applications and related resources</li>
+ <li>The <code>gitlab-managed-apps</code> namespace</li>
+ <li>Any project namespaces</li>
+ <li><code>clusterroles</code></li>
+ <li><code>clusterrolebindings</code></li>
+ </ul>
+ `);
+ },
+ confirmationTextLabel() {
+ return sprintf(
+ this.confirmCleanup
+ ? s__(
+ 'ClusterIntegration|To remove your integration and resources, type %{clusterName} to confirm:',
+ )
+ : s__('ClusterIntegration|To remove your integration, type %{clusterName} to confirm:'),
+ {
+ clusterName: `<code>${_.escape(this.clusterName)}</code>`,
+ },
+ false,
+ );
+ },
+ canSubmit() {
+ return this.enteredClusterName === this.clusterName;
+ },
+ },
+ methods: {
+ handleClickRemoveCluster(cleanup = false) {
+ this.confirmCleanup = cleanup;
+ this.$refs.modal.show();
+ },
+ handleCancel() {
+ this.$refs.modal.hide();
+ this.enteredClusterName = '';
+ },
+ handleSubmit(cleanup = false) {
+ this.$refs.cleanup.name = cleanup === true ? 'cleanup' : 'no_cleanup';
+ this.$refs.form.submit();
+ this.enteredClusterName = '';
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <split-button
+ :action-items="$options.splitButtonActionItems"
+ menu-class="dropdown-menu-large"
+ variant="danger"
+ @remove-cluster="handleClickRemoveCluster(false)"
+ @remove-cluster-and-cleanup="handleClickRemoveCluster(true)"
+ />
+ <gl-modal
+ ref="modal"
+ size="lg"
+ modal-id="delete-cluster-modal"
+ :title="modalTitle"
+ kind="danger"
+ >
+ <template>
+ <p>{{ warningMessage }}</p>
+ <div v-if="confirmCleanup" v-html="warningToBeRemoved"></div>
+ <strong v-html="confirmationTextLabel"></strong>
+ <form ref="form" :action="clusterPath" method="post" class="append-bottom-20">
+ <input ref="method" type="hidden" name="_method" value="delete" />
+ <input :value="csrfToken" type="hidden" name="authenticity_token" />
+ <input ref="cleanup" type="hidden" name="cleanup" value="true" />
+ <gl-form-input
+ v-model="enteredClusterName"
+ autofocus
+ type="text"
+ name="confirm_cluster_name_input"
+ autocomplete="off"
+ />
+ </form>
+ <span v-if="confirmCleanup">{{
+ s__(
+ 'ClusterIntegration|If you do not wish to delete all associated GitLab resources, you can simply remove the integration.',
+ )
+ }}</span>
+ </template>
+ <template slot="modal-footer">
+ <gl-button variant="secondary" @click="handleCancel">{{ s__('Cancel') }}</gl-button>
+ <template v-if="confirmCleanup">
+ <gl-button :disabled="!canSubmit" variant="warning" @click="handleSubmit">{{
+ s__('ClusterIntegration|Remove integration')
+ }}</gl-button>
+ <gl-button :disabled="!canSubmit" variant="danger" @click="handleSubmit(true)">{{
+ s__('ClusterIntegration|Remove integration and resources')
+ }}</gl-button>
+ </template>
+ <template v-else>
+ <gl-button :disabled="!canSubmit" variant="danger" @click="handleSubmit">{{
+ s__('ClusterIntegration|Remove integration')
+ }}</gl-button>
+ </template>
+ </template>
+ </gl-modal>
+ </div>
+</template>
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index 81c4f6711e7..d6cdd37a2c3 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -4,6 +4,7 @@ import $ from 'jquery';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import sanitize from 'sanitize-html';
import axios from '~/lib/utils/axios_utils';
+import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import flash from '~/flash';
import { __ } from '~/locale';
@@ -116,7 +117,7 @@ export default class ProjectFindFile {
if (searchText) {
matches = fuzzaldrinPlus.match(filePath, searchText);
}
- const blobItemUrl = `${this.options.blobUrlTemplate}/${encodeURIComponent(filePath)}`;
+ const blobItemUrl = joinPaths(this.options.blobUrlTemplate, escapeFileUrl(filePath));
const html = ProjectFindFile.makeHtml(filePath, matches, blobItemUrl);
results.push(this.element.find('.tree-table > tbody').append(html));
}
diff --git a/app/assets/javascripts/vue_shared/components/split_button.vue b/app/assets/javascripts/vue_shared/components/split_button.vue
index f7dc00a345c..9aacde49264 100644
--- a/app/assets/javascripts/vue_shared/components/split_button.vue
+++ b/app/assets/javascripts/vue_shared/components/split_button.vue
@@ -26,6 +26,11 @@ export default {
required: false,
default: '',
},
+ variant: {
+ type: String,
+ required: false,
+ default: 'secondary',
+ },
},
data() {
@@ -53,6 +58,7 @@ export default {
:menu-class="`dropdown-menu-selectable ${menuClass}`"
split
:text="dropdownToggleText"
+ :variant="variant"
v-bind="$attrs"
@click="triggerEvent"
>
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
index 7d419103b1c..54715557399 100644
--- a/app/finders/groups_finder.rb
+++ b/app/finders/groups_finder.rb
@@ -45,7 +45,7 @@ class GroupsFinder < UnionFinder
def all_groups
return [owned_groups] if params[:owned]
return [groups_with_min_access_level] if min_access_level?
- return [Group.all] if current_user&.full_private_access? && all_available?
+ return [Group.all] if current_user&.can_read_all_resources? && all_available?
groups = []
groups << Gitlab::ObjectHierarchy.new(groups_for_ancestors, groups_for_descendants).all_objects if current_user
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index 74e89a1e66c..641b4422db9 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -127,7 +127,7 @@ class IssuesFinder < IssuableFinder
return @user_can_see_all_confidential_issues if defined?(@user_can_see_all_confidential_issues)
return @user_can_see_all_confidential_issues = false if current_user.blank?
- return @user_can_see_all_confidential_issues = true if current_user.full_private_access?
+ return @user_can_see_all_confidential_issues = true if current_user.can_read_all_resources?
@user_can_see_all_confidential_issues =
if project? && project
diff --git a/app/finders/keys_finder.rb b/app/finders/keys_finder.rb
index d6ba7cb290d..6fd914c88cd 100644
--- a/app/finders/keys_finder.rb
+++ b/app/finders/keys_finder.rb
@@ -15,15 +15,43 @@ class KeysFinder
def execute
raise GitLabAccessDeniedError unless current_user.admin?
- raise InvalidFingerprint unless valid_fingerprint_param?
- Key.where(fingerprint_query).first # rubocop: disable CodeReuse/ActiveRecord
+ keys = by_key_type
+ keys = by_user(keys)
+ keys = sort(keys)
+
+ by_fingerprint(keys)
end
private
attr_reader :current_user, :params
+ def by_key_type
+ if params[:key_type] == 'ssh'
+ Key.regular_keys
+ else
+ Key.all
+ end
+ end
+
+ def sort(keys)
+ keys.order_last_used_at_desc
+ end
+
+ def by_user(keys)
+ return keys unless params[:user]
+
+ keys.for_user(params[:user])
+ end
+
+ def by_fingerprint(keys)
+ return keys unless params[:fingerprint].present?
+ raise InvalidFingerprint unless valid_fingerprint_param?
+
+ keys.where(fingerprint_query).first # rubocop: disable CodeReuse/ActiveRecord
+ end
+
def valid_fingerprint_param?
if fingerprint_type == "sha256"
Base64.decode64(fingerprint).length == 32
diff --git a/app/finders/merge_request_target_project_finder.rb b/app/finders/merge_request_target_project_finder.rb
index 5f0589f6c8b..85a73e0c6ff 100644
--- a/app/finders/merge_request_target_project_finder.rb
+++ b/app/finders/merge_request_target_project_finder.rb
@@ -11,15 +11,23 @@ class MergeRequestTargetProjectFinder
end
# rubocop: disable CodeReuse/ActiveRecord
- def execute
- if @source_project.fork_network
- @source_project.fork_network.projects
- .public_or_visible_to_user(current_user)
- .non_archived
- .with_feature_available_for_user(:merge_requests, current_user)
+ def execute(include_routes: false)
+ if source_project.fork_network
+ include_routes ? projects.inc_routes : projects
else
Project.where(id: source_project)
end
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ private
+
+ def projects
+ source_project
+ .fork_network
+ .projects
+ .public_or_visible_to_user(current_user)
+ .non_archived
+ .with_feature_available_for_user(:merge_requests, current_user)
+ end
end
diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb
index bd95dcd323f..7b15a3b0c10 100644
--- a/app/finders/personal_access_tokens_finder.rb
+++ b/app/finders/personal_access_tokens_finder.rb
@@ -13,18 +13,26 @@ class PersonalAccessTokensFinder
tokens = PersonalAccessToken.all
tokens = by_user(tokens)
tokens = by_impersonation(tokens)
- by_state(tokens)
+ tokens = by_state(tokens)
+
+ sort(tokens)
end
private
- # rubocop: disable CodeReuse/ActiveRecord
def by_user(tokens)
return tokens unless @params[:user]
- tokens.where(user: @params[:user])
+ tokens.for_user(@params[:user])
+ end
+
+ def sort(tokens)
+ available_sort_orders = PersonalAccessToken.simple_sorts.keys
+
+ return tokens unless available_sort_orders.include?(params[:sort])
+
+ tokens.order_by(params[:sort])
end
- # rubocop: enable CodeReuse/ActiveRecord
def by_impersonation(tokens)
case @params[:impersonation]
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 46db3b78fd0..7940ec1162b 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -88,7 +88,7 @@ module MergeRequestsHelper
def target_projects(project)
MergeRequestTargetProjectFinder.new(current_user: current_user, source_project: project)
- .execute
+ .execute(include_routes: true)
end
def merge_request_button_visibility(merge_request, closed)
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 195e4154c67..e87bb27cf62 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -44,6 +44,14 @@ module UsersHelper
current_user_menu_items.include?(item)
end
+ # Used to preload when you are rendering many projects and checking access
+ #
+ # rubocop: disable CodeReuse/ActiveRecord: `projects` can be array which also responds to pluck
+ def load_max_project_member_accesses(projects)
+ current_user&.max_member_access_for_project_ids(projects.pluck(:id))
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
def max_project_member_access(project)
current_user&.max_member_access_for_project(project.id) || Gitlab::Access::NO_ACCESS
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index b6c71f81a49..ba87369f30b 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -887,7 +887,7 @@ module Ci
def each_report(report_types)
job_artifacts_for_types(report_types).each do |report_artifact|
report_artifact.each_blob do |blob|
- yield report_artifact.file_type, blob
+ yield report_artifact.file_type, blob, report_artifact
end
end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index f16fd8e63ec..88df3baa809 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -242,7 +242,7 @@ class Issue < ApplicationRecord
return false unless readable_by?(user)
- user.full_private_access? ||
+ user.can_read_all_resources? ||
::Gitlab::ExternalAuthorization.access_allowed?(
user, project.external_authorization_classification_label)
end
diff --git a/app/models/key.rb b/app/models/key.rb
index f66aa4fb329..e549c59b58f 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -39,6 +39,10 @@ class Key < ApplicationRecord
alias_attribute :fingerprint_md5, :fingerprint
+ scope :preload_users, -> { preload(:user) }
+ scope :for_user, -> (user) { where(user: user) }
+ scope :order_last_used_at_desc, -> { reorder(::Gitlab::Database.nulls_last_order('last_used_at', 'DESC')) }
+
def self.regular_keys
where(type: ['Key', nil])
end
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 9ccc90fb74d..af079f7ebc4 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -3,6 +3,7 @@
class PersonalAccessToken < ApplicationRecord
include Expirable
include TokenAuthenticatable
+ include Sortable
add_authentication_token_field :token, digest: true
@@ -20,6 +21,8 @@ class PersonalAccessToken < ApplicationRecord
scope :inactive, -> { where("revoked = true OR expires_at < NOW()") }
scope :with_impersonation, -> { where(impersonation: true) }
scope :without_impersonation, -> { where(impersonation: false) }
+ scope :for_user, -> (user) { where(user: user) }
+ scope :preload_users, -> { preload(:user) }
validates :scopes, presence: true
validate :validate_scopes
diff --git a/app/models/project.rb b/app/models/project.rb
index 5ed47032dab..bc8757ea888 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -408,6 +408,7 @@ class Project < ApplicationRecord
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
+ scope :inc_routes, -> { includes(:route, namespace: :route) }
scope :with_statistics, -> { includes(:statistics) }
scope :with_service, ->(service) { joins(service).eager_load(service) }
scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index caa65d32c86..4973c7761c1 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -186,7 +186,7 @@ class ProjectFeature < ApplicationRecord
def team_access?(user, feature)
return unless user
- return true if user.full_private_access?
+ return true if user.can_read_all_resources?
project.team.member?(user, ProjectFeature.required_minimum_access_level(feature))
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 441ad1e70be..18bf5ceaa0e 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1473,9 +1473,7 @@ class User < ApplicationRecord
self.admin = (new_level == 'admin')
end
- # Does the user have access to all private groups & projects?
- # Overridden in EE to also check auditor?
- def full_private_access?
+ def can_read_all_resources?
can?(:read_all_resources)
end
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
index 8f5c6957a20..3a16f7dc239 100644
--- a/app/policies/base_policy.rb
+++ b/app/policies/base_policy.rb
@@ -40,6 +40,7 @@ class BasePolicy < DeclarativePolicy::Base
prevent :read_cross_project
end
+ # Policy extended in EE to also enable auditors
rule { admin }.enable :read_all_resources
rule { default }.enable :read_cross_project
diff --git a/app/views/clusters/clusters/_advanced_settings.html.haml b/app/views/clusters/clusters/_advanced_settings.html.haml
index 5e34b457231..77f7c478ffa 100644
--- a/app/views/clusters/clusters/_advanced_settings.html.haml
+++ b/app/views/clusters/clusters/_advanced_settings.html.haml
@@ -41,4 +41,5 @@
= s_('ClusterIntegration|Remove Kubernetes cluster integration')
%p
= s_("ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster.")
- = link_to(s_('ClusterIntegration|Remove integration'), clusterable.cluster_path(@cluster), method: :delete, class: 'btn btn-danger', data: { confirm: s_("ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster.")})
+
+ #js-cluster-remove-actions{ data: { cluster_path: clusterable.cluster_path(@cluster), cluster_name: @cluster.name } }
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 5bb92c8d1cf..71fef5df5bc 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -182,6 +182,8 @@
%strong.fly-out-top-item-name
= _('Deploy Keys')
+ = render_if_exists 'layouts/nav/sidebar/credentials_link'
+
= nav_link(controller: :services) do
= link_to admin_application_settings_services_path do
.nav-icon-container
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index 59b4facdbe5..fab7ee9d763 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -35,6 +35,7 @@
.js-projects-list-holder{ data: { qa_selector: 'projects_list' } }
- if any_projects?(projects)
- load_pipeline_status(projects) if pipeline_status
+ - load_max_project_member_accesses(projects) # Prime cache used in shared/projects/project view rendered below
%ul.projects-list{ class: css_classes }
- projects.each_with_index do |project, i|
- css_class = (i >= projects_limit) || project.pending_delete? ? 'hide' : nil
diff --git a/changelogs/unreleased/12390-document-sbt-support.yml b/changelogs/unreleased/12390-document-sbt-support.yml
new file mode 100644
index 00000000000..d2938738a26
--- /dev/null
+++ b/changelogs/unreleased/12390-document-sbt-support.yml
@@ -0,0 +1,5 @@
+---
+title: Document support for sbt dependency scanning
+merge_request: 21588
+author:
+type: added
diff --git a/changelogs/unreleased/12811-add-pip-version-dep-scanning-flag.yml b/changelogs/unreleased/12811-add-pip-version-dep-scanning-flag.yml
new file mode 100644
index 00000000000..0911c0e0e0a
--- /dev/null
+++ b/changelogs/unreleased/12811-add-pip-version-dep-scanning-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Add CI variable to set the version of pip when scanning dependencies of Python projects
+merge_request: 21218
+author:
+type: added
diff --git a/changelogs/unreleased/31662-fix-excessive-find-file-escaping.yml b/changelogs/unreleased/31662-fix-excessive-find-file-escaping.yml
new file mode 100644
index 00000000000..5ca551c63cb
--- /dev/null
+++ b/changelogs/unreleased/31662-fix-excessive-find-file-escaping.yml
@@ -0,0 +1,5 @@
+---
+title: Fix project file finder url encoding file path separators
+merge_request: 21861
+author:
+type: fixed
diff --git a/changelogs/unreleased/34261-toggle-service-desk.yml b/changelogs/unreleased/34261-toggle-service-desk.yml
new file mode 100644
index 00000000000..4cb3b4760d3
--- /dev/null
+++ b/changelogs/unreleased/34261-toggle-service-desk.yml
@@ -0,0 +1,5 @@
+---
+title: Support toggling service desk from API
+merge_request: 21627
+author:
+type: changed
diff --git a/changelogs/unreleased/34457-remove-n-plus-1-search.yml b/changelogs/unreleased/34457-remove-n-plus-1-search.yml
new file mode 100644
index 00000000000..d94e0ec3822
--- /dev/null
+++ b/changelogs/unreleased/34457-remove-n-plus-1-search.yml
@@ -0,0 +1,5 @@
+---
+title: Remove an N+1 call rendering projects search results
+merge_request: 21626
+author:
+type: performance
diff --git a/changelogs/unreleased/chore-rename-user-full-private-access.yml b/changelogs/unreleased/chore-rename-user-full-private-access.yml
new file mode 100644
index 00000000000..6bedfd6581b
--- /dev/null
+++ b/changelogs/unreleased/chore-rename-user-full-private-access.yml
@@ -0,0 +1,5 @@
+---
+title: Rename User#full_private_access? to User#can_read_all_resources?
+merge_request: 21668
+author: Diego Louzán
+type: other
diff --git a/config/helpers/vendor_dll_hash.js b/config/helpers/vendor_dll_hash.js
new file mode 100644
index 00000000000..cfd7be66ad3
--- /dev/null
+++ b/config/helpers/vendor_dll_hash.js
@@ -0,0 +1,23 @@
+const fs = require('fs');
+const path = require('path');
+const crypto = require('crypto');
+
+const CACHE_PATHS = [
+ './config/webpack.config.js',
+ './config/webpack.vendor.config.js',
+ './package.json',
+ './yarn.lock',
+];
+
+const resolvePath = file => path.resolve(__dirname, '../..', file);
+const readFile = file => fs.readFileSync(file);
+const fileHash = buffer =>
+ crypto
+ .createHash('md5')
+ .update(buffer)
+ .digest('hex');
+
+module.exports = () => {
+ const fileBuffers = CACHE_PATHS.map(resolvePath).map(readFile);
+ return fileHash(Buffer.concat(fileBuffers)).substr(0, 12);
+};
diff --git a/config/webpack.config.js b/config/webpack.config.js
index c0be2f66ca7..d85fa84c32f 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -1,6 +1,6 @@
+const fs = require('fs');
const path = require('path');
const glob = require('glob');
-const fs = require('fs');
const webpack = require('webpack');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin;
@@ -8,8 +8,10 @@ const CompressionPlugin = require('compression-webpack-plugin');
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const CopyWebpackPlugin = require('copy-webpack-plugin');
+const vendorDllHash = require('./helpers/vendor_dll_hash');
const ROOT_PATH = path.resolve(__dirname, '..');
+const VENDOR_DLL = process.env.WEBPACK_VENDOR_DLL && process.env.WEBPACK_VENDOR_DLL !== 'false';
const CACHE_PATH = process.env.WEBPACK_CACHE_PATH || path.join(ROOT_PATH, 'tmp/cache');
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
const IS_DEV_SERVER = process.env.WEBPACK_DEV_SERVER === 'true';
@@ -113,6 +115,25 @@ if (IS_EE) {
});
}
+// if there is a compiled DLL with a matching hash string, use it
+let dll;
+
+if (VENDOR_DLL && !IS_PRODUCTION) {
+ const dllHash = vendorDllHash();
+ const dllCachePath = path.join(ROOT_PATH, `tmp/cache/webpack-dlls/${dllHash}`);
+ if (fs.existsSync(dllCachePath)) {
+ console.log(`Using vendor DLL found at: ${dllCachePath}`);
+ dll = {
+ manifestPath: path.join(dllCachePath, 'vendor.dll.manifest.json'),
+ cacheFrom: dllCachePath,
+ cacheTo: path.join(ROOT_PATH, `public/assets/webpack/dll.${dllHash}/`),
+ publicPath: `dll.${dllHash}/vendor.dll.bundle.js`,
+ };
+ } else {
+ console.log(`Warning: No vendor DLL found at: ${dllCachePath}. DllPlugin disabled.`);
+ }
+}
+
module.exports = {
mode: IS_PRODUCTION ? 'production' : 'development',
@@ -267,6 +288,11 @@ module.exports = {
modules: false,
assets: true,
});
+
+ // tell our rails helper where to find the DLL files
+ if (dll) {
+ stats.dllAssets = dll.publicPath;
+ }
return JSON.stringify(stats, null, 2);
},
}),
@@ -286,6 +312,21 @@ module.exports = {
jQuery: 'jquery',
}),
+ // reference our compiled DLL modules
+ dll &&
+ new webpack.DllReferencePlugin({
+ context: ROOT_PATH,
+ manifest: dll.manifestPath,
+ }),
+
+ dll &&
+ new CopyWebpackPlugin([
+ {
+ from: dll.cacheFrom,
+ to: dll.cacheTo,
+ },
+ ]),
+
!IS_EE &&
new webpack.NormalModuleReplacementPlugin(/^ee_component\/(.*)\.vue/, resource => {
resource.request = path.join(
diff --git a/config/webpack.vendor.config.js b/config/webpack.vendor.config.js
new file mode 100644
index 00000000000..bddbf067d7c
--- /dev/null
+++ b/config/webpack.vendor.config.js
@@ -0,0 +1,71 @@
+const path = require('path');
+const webpack = require('webpack');
+const vendorDllHash = require('./helpers/vendor_dll_hash');
+
+const ROOT_PATH = path.resolve(__dirname, '..');
+
+const dllHash = vendorDllHash();
+const dllCachePath = path.join(ROOT_PATH, `tmp/cache/webpack-dlls/${dllHash}`);
+const dllPublicPath = `/assets/webpack/dll.${dllHash}/`;
+
+module.exports = {
+ mode: 'development',
+ resolve: {
+ extensions: ['.js'],
+ },
+
+ context: ROOT_PATH,
+
+ entry: {
+ vendor: [
+ 'jquery',
+ 'pdfjs-dist/build/pdf',
+ 'pdfjs-dist/build/pdf.worker.min',
+ 'sql.js',
+ 'core-js',
+ 'echarts',
+ 'lodash',
+ 'underscore',
+ 'vuex',
+ 'pikaday',
+ 'vue/dist/vue.esm.js',
+ 'at.js',
+ 'jed',
+ 'mermaid',
+ 'katex',
+ 'three',
+ 'select2',
+ 'moment',
+ 'aws-sdk',
+ 'sanitize-html',
+ 'bootstrap/dist/js/bootstrap.js',
+ 'sortablejs/modular/sortable.esm.js',
+ 'popper.js',
+ 'apollo-client',
+ 'source-map',
+ 'mousetrap',
+ ],
+ },
+
+ output: {
+ path: dllCachePath,
+ publicPath: dllPublicPath,
+ filename: '[name].dll.bundle.js',
+ chunkFilename: '[name].dll.chunk.js',
+ library: '[name]_[hash]',
+ },
+
+ plugins: [
+ new webpack.DllPlugin({
+ path: path.join(dllCachePath, '[name].dll.manifest.json'),
+ name: '[name]_[hash]',
+ }),
+ ],
+
+ node: {
+ fs: 'empty', // sqljs requires fs
+ setImmediate: false,
+ },
+
+ devtool: 'cheap-module-source-map',
+};
diff --git a/db/migrate/20191205060723_add_index_to_keys.rb b/db/migrate/20191205060723_add_index_to_keys.rb
new file mode 100644
index 00000000000..8e8c725f62e
--- /dev/null
+++ b/db/migrate/20191205060723_add_index_to_keys.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexToKeys < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :keys, :last_used_at, order: { last_used_at: 'DESC NULLS LAST' }
+ end
+
+ def down
+ remove_concurrent_index :keys, :last_used_at
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 870eb22e1f1..7b0ecce825c 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -2217,6 +2217,7 @@ ActiveRecord::Schema.define(version: 2019_12_14_175727) do
t.index ["fingerprint"], name: "index_keys_on_fingerprint", unique: true
t.index ["fingerprint_sha256"], name: "index_keys_on_fingerprint_sha256"
t.index ["id", "type"], name: "index_on_deploy_keys_id_and_type_and_public", unique: true, where: "(public = true)"
+ t.index ["last_used_at"], name: "index_keys_on_last_used_at", order: "DESC NULLS LAST"
t.index ["user_id"], name: "index_keys_on_user_id"
end
diff --git a/doc/README.md b/doc/README.md
index fb343ae91bb..1cdb5bc7b47 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -315,6 +315,7 @@ The following documentation relates to the DevOps **Configure** stage:
| [Protected variables](ci/variables/README.md#protected-environment-variables) | Restrict variables to protected branches and tags. |
| [Serverless](user/project/clusters/serverless/index.md) | Run serverless workloads on Kubernetes. |
| [Slack slash commands](user/project/integrations/slack_slash_commands.md) | Enable and use slash commands from within Slack. |
+| [Manage your infrastructure with Terraform](user/infrastructure/index.md) | Manage your infrastructure as you run your CI/CD pipeline. |
<div align="right">
<a type="button" class="btn btn-default" href="#overview">
diff --git a/doc/administration/compliance.md b/doc/administration/compliance.md
index 246addb6dc9..44e1cc8059a 100644
--- a/doc/administration/compliance.md
+++ b/doc/administration/compliance.md
@@ -16,3 +16,4 @@ GitLab’s [security features](../security/README.md) may also help you meet rel
|**[LDAP group sync filters](auth/ldap-ee.md#group-sync)**<br>GitLab Enterprise Edition Premium gives more flexibility to synchronize with LDAP based on filters, meaning you can leverage LDAP attributes to map GitLab permissions.|Premium+||
|**[Audit logs](audit_events.md)**<br>To maintain the integrity of your code, GitLab Enterprise Edition Premium gives admins the ability to view any modifications made within the GitLab server in an advanced audit log system, so you can control, analyze and track every change.|Premium+||
|**[Auditor users](auditor_users.md)**<br>Auditor users are users who are given read-only access to all projects, groups, and other resources on the GitLab instance.|Premium+||
+|**[Credentials inventory](../user/admin_area/credentials_inventory.md)**<br>With a credentials inventory, GitLab administrators can keep track of the credentials used by all of the users in their GitLab instance. |Ultimate||
diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md
index 72968cfed56..9b733034f5b 100644
--- a/doc/administration/high_availability/redis.md
+++ b/doc/administration/high_availability/redis.md
@@ -71,7 +71,7 @@ Omnibus:
redis['port'] = 6379
redis['password'] = 'SECRET_PASSWORD_HERE'
- gitlab_rails['auto_migrate'] = false
+ gitlab_rails['enable'] = false
```
1. [Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
diff --git a/doc/administration/index.md b/doc/administration/index.md
index 1652b287258..2a9980cddb3 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -124,6 +124,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
basic Postfix mail server with IMAP authentication on Ubuntu for incoming
emails.
- [Abuse reports](../user/admin_area/abuse_reports.md): View and resolve abuse reports from your users.
+- [Credentials Inventory](../user/admin_area/credentials_inventory.md): With Credentials inventory, GitLab administrators can keep track of the credentials used by their users in their GitLab self-managed instance. **(ULTIMATE ONLY)**
## Project settings
diff --git a/doc/api/projects.md b/doc/api/projects.md
index f2b5ed65cda..b49fac8d2c9 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -1135,6 +1135,7 @@ PUT /projects/:id
| `only_mirror_protected_branches` | boolean | no | **(STARTER)** Only mirror protected branches |
| `mirror_overwrites_diverged_branches` | boolean | no | **(STARTER)** Pull mirror overwrites diverged branches |
| `packages_enabled` | boolean | no | **(PREMIUM ONLY)** Enable or disable packages repository feature |
+| `service_desk_enabled` | boolean | no | **(PREMIUM ONLY)** Enable or disable service desk feature |
NOTE: **Note:** If your HTTP repository is not publicly accessible,
add authentication information to the URL: `https://username:password@gitlab.company.com/group/project.git`
diff --git a/doc/user/admin_area/credentials_inventory.md b/doc/user/admin_area/credentials_inventory.md
new file mode 100644
index 00000000000..30ebbb5b6db
--- /dev/null
+++ b/doc/user/admin_area/credentials_inventory.md
@@ -0,0 +1,19 @@
+# Credentials inventory **(ULTIMATE ONLY)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/20912) in GitLab 12.6.
+
+## Overview
+
+GitLab administrators are responsible for the overall security of their instance. To assist, GitLab provides a Credentials inventory to keep track of all the credentials that can be used to access their self-managed instance.
+
+Using Credentials inventory, GitLab administrators can see all the personal access tokens and SSH keys that exist in their instance and:
+
+- Who they belong to.
+- Their access scope.
+- Their usage pattern.
+
+To access the Credentials inventory, navigate to **Admin Area > Credentials**.
+
+The following is an example of the Credentials inventory page:
+
+![Credentials inventory page](img/credentials_inventory_v12_6.png)
diff --git a/doc/user/admin_area/img/credentials_inventory_v12_6.png b/doc/user/admin_area/img/credentials_inventory_v12_6.png
new file mode 100644
index 00000000000..ff46db61cdb
--- /dev/null
+++ b/doc/user/admin_area/img/credentials_inventory_v12_6.png
Binary files differ
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index 3f13d5983aa..01feaaac423 100644
--- a/doc/user/application_security/dependency_scanning/index.md
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -64,6 +64,7 @@ The following languages and dependency managers are supported.
| Python ([Pipfile](https://pipenv.kennethreitz.org/en/latest/basics/)) | not currently ([issue](https://gitlab.com/gitlab-org/gitlab/issues/11756 "Pipfile.lock support for Dependency Scanning"))| not available |
| Python ([poetry](https://poetry.eustace.io/)) | not currently ([issue](https://gitlab.com/gitlab-org/gitlab/issues/7006 "Support Poetry in Dependency Scanning")) | not available |
| Ruby ([gem](https://rubygems.org/)) | yes | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium), [bundler-audit](https://github.com/rubysec/bundler-audit) |
+| Scala ([sbt](https://www.scala-sbt.org/)) | yes | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) |
## Configuration
@@ -133,6 +134,7 @@ using environment variables.
| `DS_ANALYZER_IMAGE_PREFIX` | Override the name of the Docker registry providing the official default images (proxy). Read more about [customizing analyzers](analyzers.md). |
| `DS_ANALYZER_IMAGE_TAG` | Override the Docker tag of the official default images. Read more about [customizing analyzers](analyzers.md). |
| `DS_PYTHON_VERSION` | Version of Python. If set to 2, dependencies are installed using Python 2.7 instead of Python 3.6. ([Introduced](https://gitlab.com/gitlab-org/gitlab/issues/12296) in GitLab 12.1)|
+| `DS_PIP_VERSION` | Force the install of a specific pip version (example: `"19.3"`), otherwise the pip installed in the docker image is used. |
| `DS_PIP_DEPENDENCY_PATH` | Path to load Python pip dependencies from. ([Introduced](https://gitlab.com/gitlab-org/gitlab/issues/12412) in GitLab 12.2) |
| `DS_DEFAULT_ANALYZERS` | Override the names of the official default images. Read more about [customizing analyzers](analyzers.md). |
| `DS_DISABLE_DIND` | Disable Docker in Docker and run analyzers [individually](#disabling-docker-in-docker-for-dependency-scanning).|
diff --git a/doc/user/infrastructure/index.md b/doc/user/infrastructure/index.md
new file mode 100644
index 00000000000..a50cdf1cf0e
--- /dev/null
+++ b/doc/user/infrastructure/index.md
@@ -0,0 +1,6 @@
+# Infrastructure as Code
+
+GitLab can be used to manage infrastructure as code. The following are some examples:
+
+- [A generic tutorial for Terraform with GitLab](https://medium.com/@timhberry/terraform-pipelines-in-gitlab-415b9d842596).
+- [Terraform at GitLab](https://about.gitlab.com/blog/2019/11/12/gitops-part-2/).
diff --git a/doc/user/project/operations/index.md b/doc/user/project/operations/index.md
index 2da9c3e70cf..df7ce61525e 100644
--- a/doc/user/project/operations/index.md
+++ b/doc/user/project/operations/index.md
@@ -6,6 +6,7 @@ your applications:
- Collect [Prometheus metrics](../integrations/prometheus_library/index.md).
- Deploy to different [environments](../../../ci/environments.md).
- Connect your project to a [Kubernetes cluster](../clusters/index.md).
+- Manage your infrastructure with [Infrastructure as Code](../../infrastructure/index.md) approaches.
- Discover and view errors generated by your applications with [Error Tracking](error_tracking.md).
- Create, toggle, and remove [Feature Flags](feature_flags.md). **(PREMIUM)**
- [Trace](tracing.md) the performance and health of a deployed application. **(ULTIMATE)**
diff --git a/jest.config.js b/jest.config.js
index 79522552c6d..59e09c85b5a 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -55,6 +55,7 @@ if (IS_EE) {
// eslint-disable-next-line import/no-commonjs
module.exports = {
+ clearMocks: true,
testMatch,
moduleFileExtensions: ['js', 'json', 'vue'],
moduleNameMapper,
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 6c88b61eee8..52fa3f8a68e 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -31,7 +31,7 @@ module API
find_params = params.slice(:all_available, :custom_attributes, :owned, :min_access_level)
find_params[:parent] = find_group!(parent_id) if parent_id
find_params[:all_available] =
- find_params.fetch(:all_available, current_user&.full_private_access?)
+ find_params.fetch(:all_available, current_user&.can_read_all_resources?)
groups = GroupsFinder.new(current_user, find_params).execute
groups = groups.search(params[:search]) if params[:search].present?
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index d15784bb1ab..37cb6d6a639 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -213,9 +213,9 @@ module API
unauthorized! unless Devise.secure_compare(secret_token, input)
end
- def authenticated_with_full_private_access!
+ def authenticated_with_can_read_all_resources!
authenticate!
- forbidden! unless current_user.full_private_access?
+ forbidden! unless current_user.can_read_all_resources?
end
def authenticated_as_admin!
diff --git a/lib/api/helpers/project_snapshots_helpers.rb b/lib/api/helpers/project_snapshots_helpers.rb
index 13cec1bfd5c..e708dbf0156 100644
--- a/lib/api/helpers/project_snapshots_helpers.rb
+++ b/lib/api/helpers/project_snapshots_helpers.rb
@@ -6,7 +6,7 @@ module API
prepend_if_ee('::EE::API::Helpers::ProjectSnapshotsHelpers') # rubocop: disable Cop/InjectEnterpriseEditionModule
def authorize_read_git_snapshot!
- authenticated_with_full_private_access!
+ authenticated_with_can_read_all_resources!
end
def send_git_snapshot(repository)
diff --git a/lib/api/keys.rb b/lib/api/keys.rb
index 8f2fd8cbae2..8f837107192 100644
--- a/lib/api/keys.rb
+++ b/lib/api/keys.rb
@@ -24,9 +24,11 @@ module API
requires :fingerprint, type: String, desc: 'Search for a SSH fingerprint'
end
get do
- authenticated_with_full_private_access!
+ authenticated_with_can_read_all_resources!
- key = KeysFinder.new(current_user, params).execute
+ finder_params = params.merge(key_type: 'ssh')
+
+ key = KeysFinder.new(current_user, finder_params).execute
not_found!('Key') unless key
present key, with: Entities::SSHKeyWithUser, current_user: current_user
diff --git a/lib/api/pages.rb b/lib/api/pages.rb
index e049493b10d..39c8f1e6bdf 100644
--- a/lib/api/pages.rb
+++ b/lib/api/pages.rb
@@ -4,7 +4,7 @@ module API
class Pages < Grape::API
before do
require_pages_config_enabled!
- authenticated_with_full_private_access!
+ authenticated_with_can_read_all_resources!
end
params do
diff --git a/lib/api/pages_domains.rb b/lib/api/pages_domains.rb
index 2d02a4e624c..9f8c1e4f916 100644
--- a/lib/api/pages_domains.rb
+++ b/lib/api/pages_domains.rb
@@ -37,7 +37,7 @@ module API
resource :pages do
before do
require_pages_config_enabled!
- authenticated_with_full_private_access!
+ authenticated_with_can_read_all_resources!
end
desc "Get all pages domains" do
diff --git a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
index bdbce9edd97..e531f6316e1 100644
--- a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
@@ -47,6 +47,7 @@ dependency_scanning:
DS_PULL_ANALYZER_IMAGE_TIMEOUT \
DS_RUN_ANALYZER_TIMEOUT \
DS_PYTHON_VERSION \
+ DS_PIP_VERSION \
DS_PIP_DEPENDENCY_PATH \
PIP_INDEX_URL \
PIP_EXTRA_INDEX_URL \
diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb
index a1d462ea9f5..082d93aa354 100644
--- a/lib/gitlab/visibility_level.rb
+++ b/lib/gitlab/visibility_level.rb
@@ -29,7 +29,7 @@ module Gitlab
def levels_for_user(user = nil)
return [PUBLIC] unless user
- if user.full_private_access?
+ if user.can_read_all_resources?
[PRIVATE, INTERNAL, PUBLIC]
elsif user.external?
[PUBLIC]
diff --git a/lib/gitlab/webpack/manifest.rb b/lib/gitlab/webpack/manifest.rb
index 1d2aff5e5b4..d2c01bbd55e 100644
--- a/lib/gitlab/webpack/manifest.rb
+++ b/lib/gitlab/webpack/manifest.rb
@@ -12,11 +12,12 @@ module Gitlab
def entrypoint_paths(source)
raise ::Webpack::Rails::Manifest::WebpackError, manifest["errors"] unless manifest_bundled?
+ dll_assets = manifest.fetch("dllAssets", [])
entrypoint = manifest["entrypoints"][source]
if entrypoint && entrypoint["assets"]
# Can be either a string or an array of strings.
# Do not include source maps as they are not javascript
- [entrypoint["assets"]].flatten.reject { |p| p =~ /.*\.map$/ }.map do |p|
+ [dll_assets, entrypoint["assets"]].flatten.reject { |p| p =~ /.*\.map$/ }.map do |p|
"/#{::Rails.configuration.webpack.public_path}/#{p}"
end
else
diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake
index 7a42e4e92a0..3aa1dc403d6 100644
--- a/lib/tasks/gitlab/assets.rake
+++ b/lib/tasks/gitlab/assets.rake
@@ -8,6 +8,7 @@ namespace :gitlab do
yarn:check
gettext:po_to_json
rake:assets:precompile
+ gitlab:assets:vendor
webpack:compile
gitlab:assets:fix_urls
].each(&Gitlab::TaskHelpers.method(:invoke_and_time_task))
@@ -49,5 +50,12 @@ namespace :gitlab do
end
end
end
+
+ desc 'GitLab | Assets | Compile vendor assets'
+ task :vendor do
+ unless system('yarn webpack-vendor')
+ abort 'Error: Unable to compile webpack DLL.'.color(:red)
+ end
+ end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 86f838f0eb9..d0cb07b3859 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1184,6 +1184,12 @@ msgstr ""
msgid "AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running."
msgstr ""
+msgid "AdminCredentials|Personal Access Tokens"
+msgstr ""
+
+msgid "AdminCredentials|SSH Keys"
+msgstr ""
+
msgid "AdminDashboard|Error loading the statistics. Please try again"
msgstr ""
@@ -1328,6 +1334,9 @@ msgstr ""
msgid "AdminUsers|New user"
msgstr ""
+msgid "AdminUsers|No credentials found"
+msgstr ""
+
msgid "AdminUsers|No users found"
msgstr ""
@@ -3571,6 +3580,9 @@ msgstr ""
msgid "ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}."
msgstr ""
+msgid "ClusterIntegration| This will permanently delete the following resources: <ul> <li>All installed applications and related resources</li> <li>The <code>gitlab-managed-apps</code> namespace</li> <li>Any project namespaces</li> <li><code>clusterroles</code></li> <li><code>clusterrolebindings</code></li> </ul>"
+msgstr ""
+
msgid "ClusterIntegration| can be used instead of a custom domain."
msgstr ""
@@ -3658,9 +3670,6 @@ msgstr ""
msgid "ClusterIntegration|Apply for credit"
msgstr ""
-msgid "ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster."
-msgstr ""
-
msgid "ClusterIntegration|Authenticate with AWS"
msgstr ""
@@ -3790,6 +3799,9 @@ msgstr ""
msgid "ClusterIntegration|Crossplane enables declarative provisioning of managed services from your cloud of choice using %{kubectl} or %{gitlabIntegrationLink}. Crossplane runs inside your Kubernetes cluster and supports secure connectivity and secrets management between app containers and the cloud services they depend on."
msgstr ""
+msgid "ClusterIntegration|Deletes all GitLab resources attached to this cluster during removal"
+msgstr ""
+
msgid "ClusterIntegration|Did you know?"
msgstr ""
@@ -3889,6 +3901,9 @@ msgstr ""
msgid "ClusterIntegration|If you are setting up multiple clusters and are using Auto DevOps, %{help_link_start}read this first%{help_link_end}."
msgstr ""
+msgid "ClusterIntegration|If you do not wish to delete all associated GitLab resources, you can simply remove the integration."
+msgstr ""
+
msgid "ClusterIntegration|In order to view the health of your cluster, you must first install Prometheus below."
msgstr ""
@@ -4111,9 +4126,21 @@ msgstr ""
msgid "ClusterIntegration|Remove integration"
msgstr ""
+msgid "ClusterIntegration|Remove integration and resources"
+msgstr ""
+
+msgid "ClusterIntegration|Remove integration and resources?"
+msgstr ""
+
+msgid "ClusterIntegration|Remove integration?"
+msgstr ""
+
msgid "ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster."
msgstr ""
+msgid "ClusterIntegration|Removes cluster from project but keeps associated resources"
+msgstr ""
+
msgid "ClusterIntegration|Replace this with your own hostname if you want. If you do so, point hostname to Ingress IP Address from above."
msgstr ""
@@ -4282,6 +4309,12 @@ msgstr ""
msgid "ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint."
msgstr ""
+msgid "ClusterIntegration|To remove your integration and resources, type %{clusterName} to confirm:"
+msgstr ""
+
+msgid "ClusterIntegration|To remove your integration, type %{clusterName} to confirm:"
+msgstr ""
+
msgid "ClusterIntegration|Toggle Kubernetes cluster"
msgstr ""
@@ -4306,6 +4339,12 @@ msgstr ""
msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
+msgid "ClusterIntegration|You are about to remove your cluster integration and all GitLab-created resources associated with this cluster."
+msgstr ""
+
+msgid "ClusterIntegration|You are about to remove your cluster integration."
+msgstr ""
+
msgid "ClusterIntegration|You are about to uninstall %{appTitle} from your cluster."
msgstr ""
@@ -5117,6 +5156,9 @@ msgstr ""
msgid "Created At"
msgstr ""
+msgid "Created On"
+msgstr ""
+
msgid "Created a branch and a merge request to resolve this issue."
msgstr ""
@@ -5165,6 +5207,9 @@ msgstr ""
msgid "Creation date"
msgstr ""
+msgid "Credentials"
+msgstr ""
+
msgid "Critical vulnerabilities present"
msgstr ""
@@ -7208,6 +7253,9 @@ msgstr ""
msgid "Expand up"
msgstr ""
+msgid "Expiration"
+msgstr ""
+
msgid "Expiration date"
msgstr ""
@@ -10237,6 +10285,9 @@ msgstr[1] ""
msgid "Last %{days} days"
msgstr ""
+msgid "Last Accessed On"
+msgstr ""
+
msgid "Last Pipeline"
msgstr ""
@@ -11721,6 +11772,9 @@ msgstr ""
msgid "No Milestone"
msgstr ""
+msgid "No Scopes"
+msgstr ""
+
msgid "No Tag"
msgstr ""
diff --git a/package.json b/package.json
index e60ae6d5a80..aa11e35d3e6 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"stylelint-create-utility-map": "node scripts/frontend/stylelint/stylelint-utility-map.js",
"test": "node scripts/frontend/test",
"webpack": "NODE_OPTIONS=\"--max-old-space-size=3584\" webpack --config config/webpack.config.js",
+ "webpack-vendor": "NODE_OPTIONS=\"--max-old-space-size=3584\" webpack --config config/webpack.vendor.config.js",
"webpack-prod": "NODE_OPTIONS=\"--max-old-space-size=3584\" NODE_ENV=production webpack --config config/webpack.config.js"
},
"dependencies": {
diff --git a/scripts/utils.sh b/scripts/utils.sh
index 83b7173e140..7eae9531f74 100644
--- a/scripts/utils.sh
+++ b/scripts/utils.sh
@@ -34,7 +34,8 @@ function install_api_client_dependencies_with_apt() {
}
function install_gitlab_gem() {
- gem install gitlab --no-document
+ gem install httparty --no-document --version 0.17.3
+ gem install gitlab --no-document --version 4.13.0
}
function echoerr() {
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index 42248dc1165..2344ffffa65 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -188,6 +188,10 @@ FactoryBot.define do
end
end
+ trait :sequence_source_branch do
+ sequence(:source_branch) { |n| "feature#{n}" }
+ end
+
after(:build) do |merge_request|
target_project = merge_request.target_project
source_project = merge_request.source_project
diff --git a/spec/features/groups/clusters/user_spec.rb b/spec/features/groups/clusters/user_spec.rb
index e06f2efe183..ceec50e4f58 100644
--- a/spec/features/groups/clusters/user_spec.rb
+++ b/spec/features/groups/clusters/user_spec.rb
@@ -115,11 +115,11 @@ describe 'User Cluster', :js do
end
end
- context 'when user destroy the cluster' do
+ context 'when user destroys the cluster' do
before do
- page.accept_confirm do
- click_link 'Remove integration'
- end
+ click_button 'Remove integration and resources'
+ fill_in 'confirm_cluster_name_input', with: cluster.name
+ click_button 'Remove integration'
end
it 'user sees creation form with the successful message' do
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index 741f46cef45..7c8b2640e89 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -131,11 +131,11 @@ describe 'Gcp Cluster', :js, :do_not_mock_admin_mode do
end
end
- context 'when user destroy the cluster' do
+ context 'when user destroys the cluster' do
before do
- page.accept_confirm do
- click_link 'Remove integration'
- end
+ click_button 'Remove integration and resources'
+ fill_in 'confirm_cluster_name_input', with: cluster.name
+ click_button 'Remove integration'
end
it 'user sees creation form with the successful message' do
diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb
index bdaeda83926..38efcf758e1 100644
--- a/spec/features/projects/clusters/user_spec.rb
+++ b/spec/features/projects/clusters/user_spec.rb
@@ -101,11 +101,11 @@ describe 'User Cluster', :js do
end
end
- context 'when user destroy the cluster' do
+ context 'when user destroys the cluster' do
before do
- page.accept_confirm do
- click_link 'Remove integration'
- end
+ click_button 'Remove integration and resources'
+ fill_in 'confirm_cluster_name_input', with: cluster.name
+ click_button 'Remove integration'
end
it 'user sees creation form with the successful message' do
diff --git a/spec/finders/keys_finder_spec.rb b/spec/finders/keys_finder_spec.rb
index 147e6ee3d84..f80abdcdb38 100644
--- a/spec/finders/keys_finder_spec.rb
+++ b/spec/finders/keys_finder_spec.rb
@@ -3,74 +3,145 @@
require 'spec_helper'
describe KeysFinder do
- subject(:keys_finder) { described_class.new(user, params) }
+ subject { described_class.new(user, params).execute }
let(:user) { create(:user) }
- let(:fingerprint_type) { 'md5' }
- let(:fingerprint) { 'ba:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d1' }
-
- let(:params) do
- {
- type: fingerprint_type,
- fingerprint: fingerprint
- }
- end
+ let(:params) { {} }
- let!(:key) do
- create(:key, user: user,
+ let!(:key_1) do
+ create(:personal_key,
+ last_used_at: 7.days.ago,
+ user: user,
key: 'ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt1016k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=',
fingerprint: 'ba:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d1',
- fingerprint_sha256: 'nUhzNyftwADy8AH3wFY31tAKs7HufskYTte2aXo/lCg'
- )
+ fingerprint_sha256: 'nUhzNyftwADy8AH3wFY31tAKs7HufskYTte2aXo/lCg')
end
+ let!(:key_2) { create(:personal_key, last_used_at: nil, user: user) }
+ let!(:key_3) { create(:personal_key, last_used_at: 2.days.ago) }
+
context 'with a regular user' do
it 'raises GitLabAccessDeniedError' do
- expect do
- keys_finder.execute
- end.to raise_error(KeysFinder::GitLabAccessDeniedError)
+ expect { subject }.to raise_error(KeysFinder::GitLabAccessDeniedError)
end
end
context 'with an admin user' do
let(:user) {create(:admin)}
- context 'with invalid MD5 fingerprint' do
- let(:fingerprint) { '11:11:11:11' }
+ context 'key_type' do
+ let!(:deploy_key) { create(:deploy_key) }
- it 'raises InvalidFingerprint' do
- expect { keys_finder.execute }
- .to raise_error(KeysFinder::InvalidFingerprint)
- end
- end
+ context 'when `key_type` is `ssh`' do
+ before do
+ params[:key_type] = 'ssh'
+ end
- context 'with invalid SHA fingerprint' do
- let(:fingerprint_type) { 'sha256' }
- let(:fingerprint) { 'nUhzNyftwAAKs7HufskYTte2g' }
+ it 'returns only SSH keys' do
+ expect(subject).to contain_exactly(key_1, key_2, key_3)
+ end
+ end
- it 'raises InvalidFingerprint' do
- expect { keys_finder.execute }
- .to raise_error(KeysFinder::InvalidFingerprint)
+ context 'when `key_type` is not specified' do
+ it 'returns all types of keys' do
+ expect(subject).to contain_exactly(key_1, key_2, key_3, deploy_key)
+ end
end
end
- context 'with valid MD5 params' do
- it 'returns key if the fingerprint is found' do
- result = keys_finder.execute
+ context 'fingerprint' do
+ context 'with invalid fingerprint' do
+ context 'with invalid MD5 fingerprint' do
+ before do
+ params[:fingerprint] = '11:11:11:11'
+ end
+
+ it 'raises InvalidFingerprint' do
+ expect { subject }.to raise_error(KeysFinder::InvalidFingerprint)
+ end
+ end
+
+ context 'with invalid SHA fingerprint' do
+ before do
+ params[:fingerprint] = 'nUhzNyftwAAKs7HufskYTte2g'
+ end
+
+ it 'raises InvalidFingerprint' do
+ expect { subject }.to raise_error(KeysFinder::InvalidFingerprint)
+ end
+ end
+ end
- expect(result).to eq(key)
- expect(key.user).to eq(user)
+ context 'with valid fingerprints' do
+ context 'with valid MD5 params' do
+ context 'with an existent fingerprint' do
+ before do
+ params[:fingerprint] = 'ba:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d1'
+ end
+
+ it 'returns the key' do
+ expect(subject).to eq(key_1)
+ expect(subject.user).to eq(user)
+ end
+ end
+
+ context 'with a non-existent fingerprint' do
+ before do
+ params[:fingerprint] = 'bb:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d2'
+ end
+
+ it 'returns nil' do
+ expect(subject).to be_nil
+ end
+ end
+ end
+
+ context 'with valid SHA256 params' do
+ context 'with an existent fingerprint' do
+ before do
+ params[:fingerprint] = 'SHA256:nUhzNyftwADy8AH3wFY31tAKs7HufskYTte2aXo/lCg'
+ end
+
+ it 'returns key' do
+ expect(subject).to eq(key_1)
+ expect(subject.user).to eq(user)
+ end
+ end
+
+ context 'with a non-existent fingerprint' do
+ before do
+ params[:fingerprint] = 'SHA256:xTjuFqftwADy8AH3wFY31tAKs7HufskYTte2aXi/mNp'
+ end
+
+ it 'returns nil' do
+ expect(subject).to be_nil
+ end
+ end
+ end
end
end
- context 'with valid SHA256 params' do
- let(:fingerprint) { 'ba:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d1' }
+ context 'user' do
+ context 'without user' do
+ it 'contains ssh_keys of all users in the system' do
+ expect(subject).to contain_exactly(key_1, key_2, key_3)
+ end
+ end
+
+ context 'with user' do
+ before do
+ params[:user] = user
+ end
- it 'returns key if the fingerprint is found' do
- result = keys_finder.execute
+ it 'contains ssh_keys of only the specified users' do
+ expect(subject).to contain_exactly(key_1, key_2)
+ end
+ end
+ end
- expect(result).to eq(key)
- expect(key.user).to eq(user)
+ context 'sort order' do
+ it 'sorts in last_used_at_desc order' do
+ expect(subject).to eq([key_3, key_1, key_2])
end
end
end
diff --git a/spec/finders/merge_request_target_project_finder_spec.rb b/spec/finders/merge_request_target_project_finder_spec.rb
index 1d78b7ba4e3..7fef16d1040 100644
--- a/spec/finders/merge_request_target_project_finder_spec.rb
+++ b/spec/finders/merge_request_target_project_finder_spec.rb
@@ -27,6 +27,22 @@ describe MergeRequestTargetProjectFinder do
expect(finder.execute).to contain_exactly(other_fork, forked_project)
end
+
+ it 'does not include routes by default' do
+ row = finder.execute.first
+
+ expect(row.association(:route).loaded?).to be_falsey
+ expect(row.association(:namespace).loaded?).to be_falsey
+ expect(row.namespace.association(:route).loaded?).to be_falsey
+ end
+
+ it 'includes routes when requested' do
+ row = finder.execute(include_routes: true).first
+
+ expect(row.association(:route).loaded?).to be_truthy
+ expect(row.association(:namespace).loaded?).to be_truthy
+ expect(row.namespace.association(:route).loaded?).to be_truthy
+ end
end
context 'public projects' do
diff --git a/spec/finders/personal_access_tokens_finder_spec.rb b/spec/finders/personal_access_tokens_finder_spec.rb
index a44daf585ba..ce8ef80bb99 100644
--- a/spec/finders/personal_access_tokens_finder_spec.rb
+++ b/spec/finders/personal_access_tokens_finder_spec.rb
@@ -26,6 +26,16 @@ describe PersonalAccessTokensFinder do
revoked_impersonation_token, expired_impersonation_token)
end
+ describe 'with sort order' do
+ before do
+ params[:sort] = 'id_asc'
+ end
+
+ it 'sorts records as per the specified sort order' do
+ expect(subject).to match_array(PersonalAccessToken.all.order(id: :asc))
+ end
+ end
+
describe 'without impersonation' do
before do
params[:impersonation] = false
diff --git a/spec/frontend/boards/boards_store_spec.js b/spec/frontend/boards/boards_store_spec.js
index 3588197ebdc..bf3d81d3117 100644
--- a/spec/frontend/boards/boards_store_spec.js
+++ b/spec/frontend/boards/boards_store_spec.js
@@ -41,7 +41,6 @@ describe('boardsStore', () => {
afterEach(() => {
axiosMock.restore();
- jest.clearAllMocks();
});
const setupDefaultResponses = () => {
diff --git a/spec/frontend/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js
index 7b1d96c8da5..d7c648bcd20 100644
--- a/spec/frontend/clusters/clusters_bundle_spec.js
+++ b/spec/frontend/clusters/clusters_bundle_spec.js
@@ -46,7 +46,6 @@ describe('Clusters', () => {
afterEach(() => {
cluster.destroy();
mock.restore();
- jest.clearAllMocks();
});
describe('class constructor', () => {
diff --git a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
new file mode 100644
index 00000000000..8f406c62824
--- /dev/null
+++ b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
@@ -0,0 +1,80 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Remove cluster confirmation modal renders splitbutton with modal included 1`] = `
+<div>
+ <div
+ class="dropdown btn-group b-dropdown gl-dropdown"
+ >
+ <button
+ class="btn btn-danger"
+ type="button"
+ >
+
+ Remove integration and resources
+
+ <!---->
+ </button>
+ <button
+ aria-expanded="false"
+ aria-haspopup="true"
+ class="btn dropdown-toggle btn-danger dropdown-toggle-split"
+ type="button"
+ >
+ <span
+ class="sr-only"
+ >
+ Toggle Dropdown
+ </span>
+ </button>
+ <ul
+ class="dropdown-menu dropdown-menu-selectable dropdown-menu-large"
+ role="menu"
+ tabindex="-1"
+ >
+ <li>
+ <button
+ class="dropdown-item is-active"
+ role="menuitem"
+ type="button"
+ >
+ <strong>
+ Remove integration and resources
+ </strong>
+
+ <div>
+ Deletes all GitLab resources attached to this cluster during removal
+ </div>
+ </button>
+ </li>
+
+ <li>
+ <hr
+ aria-orientation="horizontal"
+ class="dropdown-divider"
+ role="separator"
+ />
+ </li>
+ <li>
+ <button
+ class="dropdown-item"
+ role="menuitem"
+ type="button"
+ >
+ <strong>
+ Remove integration
+ </strong>
+
+ <div>
+ Removes cluster from project but keeps associated resources
+ </div>
+ </button>
+ </li>
+
+ <!---->
+
+ </ul>
+ </div>
+
+ <!---->
+</div>
+`;
diff --git a/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js b/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js
new file mode 100644
index 00000000000..b5aead238ad
--- /dev/null
+++ b/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js
@@ -0,0 +1,57 @@
+import { mount } from '@vue/test-utils';
+import { GlModal } from '@gitlab/ui';
+import SplitButton from '~/vue_shared/components/split_button.vue';
+import RemoveClusterConfirmation from '~/clusters/components/remove_cluster_confirmation.vue';
+
+describe('Remove cluster confirmation modal', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = mount(RemoveClusterConfirmation, {
+ propsData: {
+ clusterPath: 'clusterPath',
+ clusterName: 'clusterName',
+ ...props,
+ },
+ sync: false,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders splitbutton with modal included', () => {
+ createComponent();
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('split button dropdown', () => {
+ const findModal = () => wrapper.find(GlModal).vm;
+ const findSplitButton = () => wrapper.find(SplitButton).vm;
+
+ beforeEach(() => {
+ createComponent({ clusterName: 'my-test-cluster' });
+ jest.spyOn(findModal(), 'show').mockReturnValue();
+ });
+
+ it('opens modal with "cleanup" option', () => {
+ findSplitButton().$emit('remove-cluster-and-cleanup');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findModal().show).toHaveBeenCalled();
+ expect(wrapper.vm.confirmCleanup).toEqual(true);
+ });
+ });
+
+ it('opens modal without "cleanup" option', () => {
+ findSplitButton().$emit('remove-cluster');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findModal().show).toHaveBeenCalled();
+ expect(wrapper.vm.confirmCleanup).toEqual(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/commit/commit_pipeline_status_component_spec.js b/spec/frontend/commit/commit_pipeline_status_component_spec.js
index 36fc6ee52a8..a2a6d405eab 100644
--- a/spec/frontend/commit/commit_pipeline_status_component_spec.js
+++ b/spec/frontend/commit/commit_pipeline_status_component_spec.js
@@ -44,7 +44,6 @@ describe('Commit pipeline status component', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
- jest.clearAllMocks();
});
describe('Visibility management', () => {
diff --git a/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
index fda1f71b1f9..1139f094705 100644
--- a/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
@@ -272,14 +272,11 @@ describe('EKS Cluster Store Actions', () => {
payload = { name: ['Create cluster failed'] };
});
- it('commits createClusterError mutation', () => {
+ it('commits createClusterError mutation and displays flash message', () =>
testAction(actions.createClusterError, payload, state, [
{ type: CREATE_CLUSTER_ERROR, payload },
- ]);
- });
-
- it('creates a flash that displays the create cluster error', () => {
- expect(createFlash).toHaveBeenCalledWith(payload.name[0]);
- });
+ ]).then(() => {
+ expect(createFlash).toHaveBeenCalledWith(payload.name[0]);
+ }));
});
});
diff --git a/spec/frontend/ide/components/preview/clientside_spec.js b/spec/frontend/ide/components/preview/clientside_spec.js
index 6a33f4998c5..5cb9e598fc4 100644
--- a/spec/frontend/ide/components/preview/clientside_spec.js
+++ b/spec/frontend/ide/components/preview/clientside_spec.js
@@ -68,10 +68,6 @@ describe('IDE clientside preview', () => {
jest.useRealTimers();
});
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
afterEach(() => {
wrapper.destroy();
});
diff --git a/spec/frontend/ide/stores/modules/pipelines/actions_spec.js b/spec/frontend/ide/stores/modules/pipelines/actions_spec.js
index a58c7b8f819..b08d1cd01da 100644
--- a/spec/frontend/ide/stores/modules/pipelines/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/pipelines/actions_spec.js
@@ -111,8 +111,6 @@ describe('IDE pipelines actions', () => {
});
describe('fetchLatestPipeline', () => {
- beforeEach(() => {});
-
afterEach(() => {
stopPipelinePolling();
clearEtagPoll();
diff --git a/spec/frontend/issuables_list/components/issuables_list_app_spec.js b/spec/frontend/issuables_list/components/issuables_list_app_spec.js
index 666ccc07416..621e8b8aa54 100644
--- a/spec/frontend/issuables_list/components/issuables_list_app_spec.js
+++ b/spec/frontend/issuables_list/components/issuables_list_app_spec.js
@@ -72,7 +72,6 @@ describe('Issuables list component', () => {
afterEach(() => {
wrapper.destroy();
mockAxios.restore();
- jest.clearAllMocks();
window.location = oldLocation;
});
diff --git a/spec/frontend/jest_self_check/mocks_spec.js b/spec/frontend/jest_self_check/mocks_spec.js
new file mode 100644
index 00000000000..f1e9e12e633
--- /dev/null
+++ b/spec/frontend/jest_self_check/mocks_spec.js
@@ -0,0 +1,43 @@
+import * as textUtils from '~/lib/utils/text_utility';
+
+jest.mock('~/lib/utils/text_utility');
+
+describe('does restore mocks config work?', () => {
+ describe('shared spy', () => {
+ const spy = jest.fn();
+
+ beforeEach(() => {
+ spy();
+ });
+
+ it('is only called once', () => {
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+
+ it('is only called once B', () => {
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+
+ it('is only called once C', () => {
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('module mock', () => {
+ beforeEach(() => {
+ textUtils.humanize('');
+ });
+
+ it('is only called once', () => {
+ expect(textUtils.humanize).toHaveBeenCalledTimes(1);
+ });
+
+ it('is only called once B', () => {
+ expect(textUtils.humanize).toHaveBeenCalledTimes(1);
+ });
+
+ it('is only called once C', () => {
+ expect(textUtils.humanize).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index 10d92e9535c..7652f48474d 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -50,7 +50,6 @@ describe('issue_comment_form component', () => {
afterEach(() => {
axiosMock.restore();
wrapper.destroy();
- jest.clearAllMocks();
});
describe('user is logged in', () => {
diff --git a/spec/frontend/pages/admin/users/components/user_modal_manager_spec.js b/spec/frontend/pages/admin/users/components/user_modal_manager_spec.js
index 7653fffc502..c88a182660d 100644
--- a/spec/frontend/pages/admin/users/components/user_modal_manager_spec.js
+++ b/spec/frontend/pages/admin/users/components/user_modal_manager_spec.js
@@ -83,10 +83,6 @@ describe('Users admin page Modal Manager', () => {
jest.spyOn(document, 'removeEventListener');
});
- afterEach(() => {
- jest.clearAllMocks();
- });
-
afterAll(() => {
jest.restoreAllMocks();
});
diff --git a/spec/frontend/project_find_file_spec.js b/spec/frontend/project_find_file_spec.js
index b697ca01046..b4c6d202e14 100644
--- a/spec/frontend/project_find_file_spec.js
+++ b/spec/frontend/project_find_file_spec.js
@@ -42,21 +42,23 @@ describe('ProjectFindFile', () => {
}));
const files = [
- 'fileA.txt',
- 'fileB.txt',
- 'fi#leC.txt',
- 'folderA/fileD.txt',
- 'folder#B/fileE.txt',
- 'folde?rC/fil#F.txt',
+ { path: 'fileA.txt', escaped: 'fileA.txt' },
+ { path: 'fileB.txt', escaped: 'fileB.txt' },
+ { path: 'fi#leC.txt', escaped: 'fi%23leC.txt' },
+ { path: 'folderA/fileD.txt', escaped: 'folderA/fileD.txt' },
+ { path: 'folder#B/fileE.txt', escaped: 'folder%23B/fileE.txt' },
+ { path: 'folde?rC/fil#F.txt', escaped: 'folde%3FrC/fil%23F.txt' },
];
- beforeEach(() => {
+ beforeEach(done => {
// Create a mock adapter for stubbing axios API requests
mock = new MockAdapter(axios);
element = $(TEMPLATE);
- mock.onGet(FILE_FIND_URL).replyOnce(200, files);
+ mock.onGet(FILE_FIND_URL).replyOnce(200, files.map(x => x.path));
getProjectFindFileInstance(); // This triggers a load / axios call + subsequent render in the constructor
+
+ setImmediate(done);
});
afterEach(() => {
@@ -65,26 +67,19 @@ describe('ProjectFindFile', () => {
sanitize.mockClear();
});
- it('loads and renders elements from remote server', done => {
- setImmediate(() => {
- expect(findFiles()).toEqual(
- files.map(text => ({
- text,
- href: `${BLOB_URL_TEMPLATE}/${encodeURIComponent(text)}`,
- })),
- );
-
- done();
- });
+ it('loads and renders elements from remote server', () => {
+ expect(findFiles()).toEqual(
+ files.map(({ path, escaped }) => ({
+ text: path,
+ href: `${BLOB_URL_TEMPLATE}/${escaped}`,
+ })),
+ );
});
- it('sanitizes search text', done => {
+ it('sanitizes search text', () => {
const searchText = element.find('.file-finder-input').val();
- setImmediate(() => {
- expect(sanitize).toHaveBeenCalledTimes(1);
- expect(sanitize).toHaveBeenCalledWith(searchText);
- done();
- });
+ expect(sanitize).toHaveBeenCalledTimes(1);
+ expect(sanitize).toHaveBeenCalledWith(searchText);
});
});
diff --git a/spec/frontend/registry/list/components/app_spec.js b/spec/frontend/registry/list/components/app_spec.js
index f2733ac9fef..5072a285f83 100644
--- a/spec/frontend/registry/list/components/app_spec.js
+++ b/spec/frontend/registry/list/components/app_spec.js
@@ -52,7 +52,6 @@ describe('Registry List', () => {
});
afterEach(() => {
- jest.clearAllMocks();
Vue.config.silent = false;
wrapper.destroy();
});
diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js
index aa0b9385f1a..94fa8b1e363 100644
--- a/spec/frontend/repository/components/table/row_spec.js
+++ b/spec/frontend/repository/components/table/row_spec.js
@@ -35,7 +35,6 @@ function factory(propsData = {}) {
describe('Repository table row component', () => {
afterEach(() => {
vm.destroy();
- jest.clearAllMocks();
});
it('renders table row', () => {
diff --git a/spec/frontend/sentry/sentry_config_spec.js b/spec/frontend/sentry/sentry_config_spec.js
index 62b8bbd50a2..bcc7f29b98d 100644
--- a/spec/frontend/sentry/sentry_config_spec.js
+++ b/spec/frontend/sentry/sentry_config_spec.js
@@ -54,8 +54,7 @@ describe('SentryConfig', () => {
});
it('should not call setUser if there is no current user ID', () => {
- jest.clearAllMocks();
-
+ SentryConfig.setUser.mockClear();
options.currentUserId = undefined;
SentryConfig.init(options);
@@ -167,8 +166,6 @@ describe('SentryConfig', () => {
describe('if no err is provided', () => {
beforeEach(() => {
- jest.clearAllMocks();
-
SentryConfig.handleSentryErrors(event, req, config);
});
@@ -191,8 +188,6 @@ describe('SentryConfig', () => {
beforeEach(() => {
req.responseText = undefined;
- jest.clearAllMocks();
-
SentryConfig.handleSentryErrors(event, req, config, err);
});
diff --git a/spec/frontend/sidebar/confidential_issue_sidebar_spec.js b/spec/frontend/sidebar/confidential_issue_sidebar_spec.js
index 68dde14880a..432ec111e52 100644
--- a/spec/frontend/sidebar/confidential_issue_sidebar_spec.js
+++ b/spec/frontend/sidebar/confidential_issue_sidebar_spec.js
@@ -42,7 +42,6 @@ describe('Confidential Issue Sidebar Block', () => {
};
beforeEach(() => {
- jest.clearAllMocks();
jest.spyOn(window.location, 'reload').mockImplementation();
});
diff --git a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
index 95296de5a5d..530428ef27c 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
@@ -5,6 +5,7 @@ exports[`SplitButton renders actionItems 1`] = `
menu-class="dropdown-menu-selectable "
split="true"
text="professor"
+ variant="secondary"
>
<gldropdownitem-stub
active="true"
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 559dc95768a..2dd9583087f 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -50,6 +50,32 @@ describe Key, :mailer do
end
end
+ describe 'scopes' do
+ describe '.for_user' do
+ let(:user_1) { create(:user) }
+ let(:key_of_user_1) { create(:personal_key, user: user_1) }
+
+ before do
+ create_list(:personal_key, 2, user: create(:user))
+ end
+
+ it 'returns keys of the specified user only' do
+ expect(described_class.for_user(user_1)).to contain_exactly(key_of_user_1)
+ end
+ end
+
+ describe '.order_last_used_at_desc' do
+ it 'sorts by last_used_at descending, with null values at last' do
+ key_1 = create(:personal_key, last_used_at: 7.days.ago)
+ key_2 = create(:personal_key, last_used_at: nil)
+ key_3 = create(:personal_key, last_used_at: 2.days.ago)
+
+ expect(described_class.order_last_used_at_desc)
+ .to eq([key_3, key_1, key_2])
+ end
+ end
+ end
+
context "validation of uniqueness (based on fingerprint uniqueness)" do
let(:user) { create(:user) }
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index aaf9ecb8089..b16d1f58be5 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -21,6 +21,18 @@ describe PersonalAccessToken do
end
end
+ describe 'scopes' do
+ describe '.for_user' do
+ it 'returns personal access tokens of specified user only' do
+ user_1 = create(:user)
+ token_of_user_1 = create(:personal_access_token, user: user_1)
+ create_list(:personal_access_token, 2)
+
+ expect(described_class.for_user(user_1)).to contain_exactly(token_of_user_1)
+ end
+ end
+ end
+
describe ".active?" do
let(:active_personal_access_token) { build(:personal_access_token) }
let(:revoked_personal_access_token) { build(:personal_access_token, :revoked) }
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index e5c30e4ca46..99d7e4d156f 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1786,11 +1786,11 @@ describe Project do
end
end
- describe '.including_namespace_and_owner' do
+ describe '.eager_load_namespace_and_owner' do
it 'eager loads the namespace and namespace owner' do
create(:project)
- row = described_class.eager_load_namespace_and_owner.to_a.first
+ row = described_class.eager_load_namespace_and_owner.first
recorder = ActiveRecord::QueryRecorder.new { row.namespace.owner }
expect(recorder.count).to be_zero
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index db26b872045..749d80ebfc2 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -2894,11 +2894,11 @@ describe User, :do_not_mock_admin_mode do
end
end
- describe '#full_private_access?' do
+ describe '#can_read_all_resources?' do
it 'returns false for regular user' do
user = build(:user)
- expect(user.full_private_access?).to be_falsy
+ expect(user.can_read_all_resources?).to be_falsy
end
context 'for admin user' do
@@ -2908,7 +2908,7 @@ describe User, :do_not_mock_admin_mode do
context 'when admin mode is disabled' do
it 'returns false' do
- expect(user.full_private_access?).to be_falsy
+ expect(user.can_read_all_resources?).to be_falsy
end
end
@@ -2919,7 +2919,7 @@ describe User, :do_not_mock_admin_mode do
end
it 'returns true' do
- expect(user.full_private_access?).to be_truthy
+ expect(user.can_read_all_resources?).to be_truthy
end
end
end
diff --git a/spec/requests/projects/merge_requests/creations_spec.rb b/spec/requests/projects/merge_requests/creations_spec.rb
new file mode 100644
index 00000000000..d192e1bca7f
--- /dev/null
+++ b/spec/requests/projects/merge_requests/creations_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'merge requests creations' do
+ describe 'GET /:namespace/:project/merge_requests/new' do
+ include ProjectForksHelper
+
+ let(:project) { create(:project, :repository) }
+ let(:user) { project.owner }
+
+ before do
+ login_as(user)
+ end
+
+ def get_new
+ get namespace_project_new_merge_request_path(namespace_id: project.namespace, project_id: project)
+ end
+
+ it 'avoids N+1 DB queries even with forked projects' do
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) { get_new }
+
+ 5.times { fork_project(project, user) }
+
+ expect { get_new }.not_to exceed_query_limit(control)
+ end
+ end
+end