summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/commons/polyfills.js1
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/cluster_form_dropdown.vue22
-rw-r--r--app/assets/javascripts/jobs/components/sidebar.vue2
-rw-r--r--app/assets/javascripts/jobs/components/trigger_block.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker_input.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/embed.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/empty_state.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/panel_type.vue2
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle.js2
-rw-r--r--app/assets/javascripts/notes.js4
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue2
-rw-r--r--app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue2
-rw-r--r--app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue2
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue2
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue2
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue2
-rw-r--r--app/assets/javascripts/notes/stores/actions.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue138
-rw-r--r--app/controllers/admin/sessions_controller.rb29
-rw-r--r--app/controllers/application_controller.rb6
-rw-r--r--app/controllers/concerns/enforces_admin_authentication.rb1
-rw-r--r--app/controllers/concerns/initializes_current_user_mode.rb13
-rw-r--r--app/controllers/concerns/sessionless_authentication.rb4
-rw-r--r--app/controllers/oauth/applications_controller.rb1
-rw-r--r--app/controllers/oauth/authorizations_controller.rb2
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb25
-rw-r--r--app/graphql/types/project_type.rb2
-rw-r--r--app/helpers/container_expiration_policies_helper.rb21
-rw-r--r--app/helpers/nav_helper.rb2
-rw-r--r--app/models/container_expiration_policy.rb41
-rw-r--r--app/models/namespace.rb4
-rw-r--r--app/models/project.rb7
-rw-r--r--app/models/user.rb4
-rw-r--r--app/views/admin/sessions/_signin_box.html.haml11
-rw-r--r--app/views/admin/sessions/new.html.haml15
-rw-r--r--changelogs/unreleased/15398-mvc-container-registry-tag-expiration-policies.yml5
-rw-r--r--changelogs/unreleased/22465-rack-attack-authenticate-runner-requests.yml5
-rw-r--r--changelogs/unreleased/34261-service-desk-to-graphql.yml5
-rw-r--r--changelogs/unreleased/36326-auto-focus-search-bar.yml5
-rw-r--r--changelogs/unreleased/feat-support-omniauth-for-admin-mode.yml5
-rw-r--r--config/initializers/rack_attack_new.rb12
-rw-r--r--db/migrate/20191119231621_create_container_expiration_policies.rb21
-rw-r--r--db/schema.rb13
-rw-r--r--doc/administration/geo/replication/database.md4
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql10
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json28
-rw-r--r--doc/api/graphql/reference/index.md2
-rw-r--r--doc/api/jobs.md54
-rw-r--r--lib/api/api_guard.rb6
-rw-r--r--lib/gitlab/auth/auth_finders.rb (renamed from lib/gitlab/auth/user_auth_finders.rb)16
-rw-r--r--lib/gitlab/auth/current_user_mode.rb56
-rw-r--r--lib/gitlab/auth/request_authenticator.rb8
-rw-r--r--lib/gitlab/import_export/import_export.yml1
-rw-r--r--locale/gitlab.pot36
-rw-r--r--qa/.gitignore1
-rw-r--r--qa/qa.rb1
-rw-r--r--qa/qa/resource/api_fabricator.rb2
-rw-r--r--qa/qa/resource/group.rb4
-rw-r--r--qa/qa/runtime/ip_address.rb33
-rw-r--r--qa/qa/support/api.rb1
-rw-r--r--spec/controllers/admin/sessions_controller_spec.rb74
-rw-r--r--spec/controllers/application_controller_spec.rb1
-rw-r--r--spec/controllers/oauth/applications_controller_spec.rb6
-rw-r--r--spec/controllers/omniauth_callbacks_controller_spec.rb107
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb2
-rw-r--r--spec/features/projects/environments/environments_spec.rb6
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb6
-rw-r--r--spec/frontend/create_cluster/eks_cluster/components/cluster_form_dropdown_spec.js11
-rw-r--r--spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js55
-rw-r--r--spec/frontend/vue_shared/components/table_pagination_spec.js324
-rw-r--r--spec/graphql/types/project_type_spec.rb2
-rw-r--r--spec/helpers/container_expiration_policies_helper_spec.rb47
-rw-r--r--spec/helpers/nav_helper_spec.rb17
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js2
-rw-r--r--spec/javascripts/environments/environments_app_spec.js4
-rw-r--r--spec/javascripts/environments/folder/environments_folder_view_spec.js4
-rw-r--r--spec/javascripts/pipelines/pipelines_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/dropdown/dropdown_search_input_spec.js51
-rw-r--r--spec/lib/gitlab/auth/auth_finders_spec.rb (renamed from spec/lib/gitlab/auth/user_auth_finders_spec.rb)68
-rw-r--r--spec/lib/gitlab/auth/current_user_mode_spec.rb115
-rw-r--r--spec/lib/gitlab/auth/request_authenticator_spec.rb24
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml10
-rw-r--r--spec/models/container_expiration_policy_spec.rb41
-rw-r--r--spec/models/namespace_spec.rb7
-rw-r--r--spec/models/project_spec.rb8
-rw-r--r--spec/models/user_spec.rb1
-rw-r--r--spec/requests/api/helpers_spec.rb22
-rw-r--r--spec/requests/rack_attack_global_spec.rb12
-rw-r--r--spec/support/helpers/admin_mode_helpers.rb2
-rw-r--r--spec/support/matchers/graphql_matchers.rb13
-rw-r--r--spec/views/admin/sessions/new.html.haml_spec.rb35
-rw-r--r--spec/views/layouts/application.html.haml_spec.rb1
97 files changed, 1179 insertions, 634 deletions
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index 7a6ad3dc771..dd300b8a307 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -4,6 +4,7 @@ import 'core-js/es/array/find';
import 'core-js/es/array/find-index';
import 'core-js/es/array/from';
import 'core-js/es/array/includes';
+import 'core-js/es/number/is-integer';
import 'core-js/es/object/assign';
import 'core-js/es/object/values';
import 'core-js/es/object/entries';
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/cluster_form_dropdown.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/cluster_form_dropdown.vue
index 67ca419bf81..2f7fcfcb755 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/cluster_form_dropdown.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/cluster_form_dropdown.vue
@@ -1,4 +1,5 @@
<script>
+import $ from 'jquery';
import { GlIcon } from '@gitlab/ui';
import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
@@ -106,6 +107,7 @@ export default {
data() {
return {
searchQuery: '',
+ focusOnSearch: false,
};
},
computed: {
@@ -141,6 +143,18 @@ export default {
return itemsProp(this.selectedItems, this.valueProperty).join(', ');
},
},
+ mounted() {
+ $(this.$refs.dropdown)
+ .on('shown.bs.dropdown', () => {
+ this.focusOnSearch = true;
+ })
+ .on('hidden.bs.dropdown', () => {
+ this.focusOnSearch = false;
+ });
+ },
+ beforeDestroy() {
+ $(this.$refs.dropdown).off();
+ },
methods: {
getItemsOrEmptyList() {
return this.items || [];
@@ -170,7 +184,7 @@ export default {
<template>
<div>
- <div class="js-gcp-machine-type-dropdown dropdown">
+ <div ref="dropdown" class="dropdown">
<dropdown-hidden-input :name="fieldName" :value="selectedItemsValues" />
<dropdown-button
:class="{ 'border-danger': hasErrors }"
@@ -179,7 +193,11 @@ export default {
:toggle-text="toggleText"
/>
<div class="dropdown-menu dropdown-select">
- <dropdown-search-input v-model="searchQuery" :placeholder-text="searchFieldPlaceholder" />
+ <dropdown-search-input
+ v-model="searchQuery"
+ :focused="focusOnSearch"
+ :placeholder-text="searchFieldPlaceholder"
+ />
<div class="dropdown-content">
<ul>
<li v-if="!results.length">
diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue
index 06477477aad..1df57f1aa14 100644
--- a/app/assets/javascripts/jobs/components/sidebar.vue
+++ b/app/assets/javascripts/jobs/components/sidebar.vue
@@ -1,8 +1,8 @@
<script>
-import { __, sprintf } from '~/locale';
import _ from 'underscore';
import { mapActions, mapState } from 'vuex';
import { GlLink, GlButton } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import Icon from '~/vue_shared/components/icon.vue';
diff --git a/app/assets/javascripts/jobs/components/trigger_block.vue b/app/assets/javascripts/jobs/components/trigger_block.vue
index 922f64d93fe..5edb8ff555b 100644
--- a/app/assets/javascripts/jobs/components/trigger_block.vue
+++ b/app/assets/javascripts/jobs/components/trigger_block.vue
@@ -1,6 +1,6 @@
<script>
-import { __ } from '~/locale';
import { GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
const HIDDEN_VALUE = '••••••';
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
index ed01d0ee553..0d442f14aea 100644
--- a/app/assets/javascripts/monitoring/components/charts/time_series.vue
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -1,9 +1,9 @@
<script>
-import { s__, __ } from '~/locale';
import _ from 'underscore';
import { GlLink, GlButton, GlTooltip, GlResizeObserverDirective } from '@gitlab/ui';
import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import dateFormat from 'dateformat';
+import { s__, __ } from '~/locale';
import { roundOffFloat } from '~/lib/utils/common_utils';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import Icon from '~/vue_shared/components/icon.vue';
diff --git a/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker_input.vue b/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker_input.vue
index 0388a6190d9..c3beae18726 100644
--- a/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker_input.vue
+++ b/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker_input.vue
@@ -1,7 +1,7 @@
<script>
import _ from 'underscore';
-import { s__, sprintf } from '~/locale';
import { GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
import { dateFormats } from '~/monitoring/constants';
const inputGroupText = {
diff --git a/app/assets/javascripts/monitoring/components/embed.vue b/app/assets/javascripts/monitoring/components/embed.vue
index dae1fbad547..86f5559af8f 100644
--- a/app/assets/javascripts/monitoring/components/embed.vue
+++ b/app/assets/javascripts/monitoring/components/embed.vue
@@ -1,7 +1,7 @@
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
-import { getParameterValues, removeParams } from '~/lib/utils/url_utility';
import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
+import { getParameterValues, removeParams } from '~/lib/utils/url_utility';
import GraphGroup from './graph_group.vue';
import { sidebarAnimationDuration } from '../constants';
import { getTimeDiff } from '../utils';
diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue
index ab8c9712ce4..728910dd633 100644
--- a/app/assets/javascripts/monitoring/components/empty_state.vue
+++ b/app/assets/javascripts/monitoring/components/empty_state.vue
@@ -1,6 +1,6 @@
<script>
-import { __ } from '~/locale';
import { GlEmptyState } from '@gitlab/ui';
+import { __ } from '~/locale';
export default {
components: {
diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue
index 080fe6f2b4b..d0dce4f5116 100644
--- a/app/assets/javascripts/monitoring/components/panel_type.vue
+++ b/app/assets/javascripts/monitoring/components/panel_type.vue
@@ -1,7 +1,6 @@
<script>
import { mapState } from 'vuex';
import _ from 'underscore';
-import { __ } from '~/locale';
import {
GlDropdown,
GlDropdownItem,
@@ -9,6 +8,7 @@ import {
GlModalDirective,
GlTooltipDirective,
} from '@gitlab/ui';
+import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import MonitorTimeSeriesChart from './charts/time_series.vue';
import MonitorAnomalyChart from './charts/anomaly.vue';
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
index a14145d480b..d296f5b7a66 100644
--- a/app/assets/javascripts/monitoring/monitoring_bundle.js
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
+import Dashboard from 'ee_else_ce/monitoring/components/dashboard.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import { getParameterValues } from '~/lib/utils/url_utility';
-import Dashboard from 'ee_else_ce/monitoring/components/dashboard.vue';
import store from './stores';
Vue.use(GlToast);
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index defa278c089..fcd5b391b38 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -16,10 +16,10 @@ import Cookies from 'js-cookie';
import Autosize from 'autosize';
import 'jquery.caret'; // required by at.js
import 'at.js';
-import AjaxCache from '~/lib/utils/ajax_cache';
import Vue from 'vue';
-import syntaxHighlight from '~/syntax_highlight';
import { GlSkeletonLoading } from '@gitlab/ui';
+import AjaxCache from '~/lib/utils/ajax_cache';
+import syntaxHighlight from '~/syntax_highlight';
import axios from './lib/utils/axios_utils';
import { getLocationHash } from './lib/utils/url_utility';
import Flash from './flash';
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index df537ba1ed2..fe22737c7fc 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -1,10 +1,10 @@
<script>
/* eslint-disable @gitlab/vue-i18n/no-bare-strings */
import { mapState, mapActions } from 'vuex';
+import { GlSkeletonLoading } from '@gitlab/ui';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
-import { GlSkeletonLoading } from '@gitlab/ui';
import { getDiffMode } from '~/diffs/store/utils';
import { diffViewerModes } from '~/ide/constants';
diff --git a/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue b/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue
index 07a5bda6bcb..f87ca097b40 100644
--- a/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue
+++ b/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue
@@ -1,6 +1,6 @@
<script>
-import icon from '~/vue_shared/components/icon.vue';
import { GlTooltipDirective } from '@gitlab/ui';
+import icon from '~/vue_shared/components/icon.vue';
export default {
name: 'JumpToNextDiscussionButton',
diff --git a/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue b/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
index f03e6fd73d7..1d1529bfa5b 100644
--- a/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
+++ b/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
@@ -1,6 +1,6 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
import { GlTooltipDirective, GlButton } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
export default {
name: 'ResolveWithIssueButton',
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 89d434a60ba..dc514f00801 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -1,8 +1,8 @@
<script>
import { mapGetters } from 'vuex';
-import Icon from '~/vue_shared/components/icon.vue';
import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import resolvedStatusMixin from 'ee_else_ce/batch_comments/mixins/resolved_status';
+import Icon from '~/vue_shared/components/icon.vue';
import ReplyButton from './note_actions/reply_button.vue';
export default {
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 222badf70d1..b024884bea0 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -1,7 +1,7 @@
<script>
-import { mergeUrlParams } from '~/lib/utils/url_utility';
import { mapGetters, mapActions } from 'vuex';
import noteFormMixin from 'ee_else_ce/notes/mixins/note_form';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 62d401d4911..0151a3f10a5 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -1,10 +1,10 @@
<script>
import { mapActions, mapGetters } from 'vuex';
import { GlTooltipDirective } from '@gitlab/ui';
+import diffLineNoteFormMixin from 'ee_else_ce/notes/mixins/diff_line_note_form';
import { s__, __ } from '~/locale';
import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave';
import icon from '~/vue_shared/components/icon.vue';
-import diffLineNoteFormMixin from 'ee_else_ce/notes/mixins/diff_line_note_form';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index fa8fc7d02e4..b3dae69d0bc 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -2,9 +2,9 @@
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import { escape } from 'underscore';
+import draftMixin from 'ee_else_ce/notes/mixins/draft';
import { truncateSha } from '~/lib/utils/text_utility';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
-import draftMixin from 'ee_else_ce/notes/mixins/draft';
import { __, s__, sprintf } from '../../locale';
import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 7df99610132..be2adb07526 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -1,6 +1,5 @@
<script>
import { mapGetters, mapActions } from 'vuex';
-import { __ } from '~/locale';
import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility';
import Flash from '../../flash';
import * as constants from '../constants';
@@ -14,6 +13,7 @@ import placeholderNote from '../../vue_shared/components/notes/placeholder_note.
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
+import { __ } from '~/locale';
import initUserPopovers from '../../user_popovers';
export default {
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 82c291379ec..97b0269509a 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import $ from 'jquery';
-import axios from '~/lib/utils/axios_utils';
import Visibility from 'visibilityjs';
+import axios from '~/lib/utils/axios_utils';
import TaskList from '../../task_list';
import Flash from '../../flash';
import Poll from '../../lib/utils/poll';
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue
index c01c7cc4ccc..610bce9a705 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue
@@ -8,6 +8,11 @@ export default {
required: true,
default: __('Search'),
},
+ focused: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return { searchQuery: this.value };
@@ -16,6 +21,11 @@ export default {
searchQuery(query) {
this.$emit('input', query);
},
+ focused(val) {
+ if (val) {
+ this.$refs.searchInput.focus();
+ }
+ },
},
};
</script>
@@ -23,6 +33,7 @@ export default {
<template>
<div class="dropdown-input">
<input
+ ref="searchInput"
v-model="searchQuery"
:placeholder="placeholderText"
class="dropdown-input-field"
diff --git a/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue b/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue
index e89638130f5..29a4a90a59f 100644
--- a/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue
+++ b/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue
@@ -1,15 +1,18 @@
<script>
+import { GlPagination } from '@gitlab/ui';
import {
- PAGINATION_UI_BUTTON_LIMIT,
- UI_LIMIT,
- SPREAD,
PREV,
NEXT,
- FIRST,
- LAST,
+ LABEL_FIRST_PAGE,
+ LABEL_PREV_PAGE,
+ LABEL_NEXT_PAGE,
+ LABEL_LAST_PAGE,
} from '~/vue_shared/components/pagination/constants';
export default {
+ components: {
+ GlPagination,
+ },
props: {
/**
This function will take the information given by the pagination component
@@ -46,113 +49,34 @@ export default {
},
},
computed: {
- prev() {
- return this.pageInfo.previousPage;
- },
- next() {
- return this.pageInfo.nextPage;
- },
- getItems() {
- const { totalPages, nextPage, previousPage, page } = this.pageInfo;
- const items = [];
-
- if (page > 1) {
- items.push({ title: FIRST, first: true });
- }
-
- if (previousPage) {
- items.push({ title: PREV, prev: true });
- } else {
- items.push({ title: PREV, disabled: true, prev: true });
- }
-
- if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true });
-
- if (totalPages) {
- const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
- const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, totalPages);
-
- for (let i = start; i <= end; i += 1) {
- const isActive = i === page;
- items.push({ title: i, active: isActive, page: true });
- }
-
- if (totalPages - page > PAGINATION_UI_BUTTON_LIMIT) {
- items.push({ title: SPREAD, separator: true, page: true });
- }
- }
-
- if (nextPage) {
- items.push({ title: NEXT, next: true });
- } else {
- items.push({ title: NEXT, disabled: true, next: true });
- }
-
- if (totalPages && totalPages - page >= 1) {
- items.push({ title: LAST, last: true });
- }
-
- return items;
- },
showPagination() {
return this.pageInfo.nextPage || this.pageInfo.previousPage;
},
},
- methods: {
- changePage(text, isDisabled) {
- if (isDisabled) return;
-
- const { totalPages, nextPage, previousPage } = this.pageInfo;
-
- switch (text) {
- case SPREAD:
- break;
- case LAST:
- this.change(totalPages);
- break;
- case NEXT:
- this.change(nextPage);
- break;
- case PREV:
- this.change(previousPage);
- break;
- case FIRST:
- this.change(1);
- break;
- default:
- this.change(Number(text));
- break;
- }
- },
- hideOnSmallScreen(item) {
- return !item.first && !item.last && !item.next && !item.prev && !item.active;
- },
- },
+ prevText: PREV,
+ nextText: NEXT,
+ labelFirstPage: LABEL_FIRST_PAGE,
+ labelPrevPage: LABEL_PREV_PAGE,
+ labelNextPage: LABEL_NEXT_PAGE,
+ labelLastPage: LABEL_LAST_PAGE,
};
</script>
<template>
- <div v-if="showPagination" class="gl-pagination prepend-top-default">
- <ul class="pagination justify-content-center">
- <li
- v-for="(item, index) in getItems"
- :key="index"
- :class="{
- page: item.page,
- 'js-previous-button': item.prev,
- 'js-next-button': item.next,
- 'js-last-button': item.last,
- 'js-first-button': item.first,
- 'd-none d-md-block': hideOnSmallScreen(item),
- separator: item.separator,
- active: item.active,
- disabled: item.disabled || item.separator,
- }"
- class="page-item"
- >
- <button type="button" class="page-link" @click="changePage(item.title, item.disabled)">
- {{ item.title }}
- </button>
- </li>
- </ul>
- </div>
+ <gl-pagination
+ v-if="showPagination"
+ class="justify-content-center prepend-top-default"
+ v-bind="$attrs"
+ :value="pageInfo.page"
+ :per-page="pageInfo.perPage"
+ :total-items="pageInfo.total"
+ :prev-page="pageInfo.previousPage"
+ :prev-text="$options.prevText"
+ :next-page="pageInfo.nextPage"
+ :next-text="$options.nextText"
+ :label-first-page="$options.labelFirstPage"
+ :label-prev-page="$options.labelPrevPage"
+ :label-next-page="$options.labelNextPage"
+ :label-last-page="$options.labelLastPage"
+ @input="change"
+ />
</template>
diff --git a/app/controllers/admin/sessions_controller.rb b/app/controllers/admin/sessions_controller.rb
index 1f946e41995..f9587655a8d 100644
--- a/app/controllers/admin/sessions_controller.rb
+++ b/app/controllers/admin/sessions_controller.rb
@@ -6,17 +6,23 @@ class Admin::SessionsController < ApplicationController
before_action :user_is_admin!
def new
- # Renders a form in which the admin can enter their password
+ if current_user_mode.admin_mode?
+ redirect_to redirect_path, notice: _('Admin mode already enabled')
+ else
+ current_user_mode.request_admin_mode! unless current_user_mode.admin_mode_requested?
+ store_location_for(:redirect, redirect_path)
+ end
end
def create
if current_user_mode.enable_admin_mode!(password: params[:password])
- redirect_location = stored_location_for(:redirect) || admin_root_path
- redirect_to safe_redirect_path(redirect_location)
+ redirect_to redirect_path, notice: _('Admin mode enabled')
else
- flash.now[:alert] = _('Invalid Login or password')
+ flash.now[:alert] = _('Invalid login or password')
render :new
end
+ rescue Gitlab::Auth::CurrentUserMode::NotRequestedError
+ redirect_to new_admin_session_path, alert: _('Re-authentication period expired or never requested. Please try again')
end
def destroy
@@ -30,4 +36,19 @@ class Admin::SessionsController < ApplicationController
def user_is_admin!
render_404 unless current_user&.admin?
end
+
+ def redirect_path
+ redirect_to_path = safe_redirect_path(stored_location_for(:redirect)) || safe_redirect_path_for_url(request.referer)
+
+ if redirect_to_path &&
+ excluded_redirect_paths.none? { |excluded| redirect_to_path.include?(excluded) }
+ redirect_to_path
+ else
+ admin_root_path
+ end
+ end
+
+ def excluded_redirect_paths
+ [new_admin_session_path, admin_session_path]
+ end
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index ee2b3741ac9..33ae778769a 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -16,6 +16,7 @@ class ApplicationController < ActionController::Base
include ConfirmEmailWarning
include Gitlab::Tracking::ControllerConcern
include Gitlab::Experimentation::ControllerConcern
+ include InitializesCurrentUserMode
before_action :authenticate_user!, except: [:route_not_found]
before_action :enforce_terms!, if: :should_enforce_terms?
@@ -41,7 +42,6 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception, prepend: true
helper_method :can?
- helper_method :current_user_mode
helper_method :import_sources_enabled?, :github_import_enabled?,
:gitea_import_enabled?, :github_import_configured?,
:gitlab_import_enabled?, :gitlab_import_configured?,
@@ -546,10 +546,6 @@ class ApplicationController < ActionController::Base
end
end
- def current_user_mode
- @current_user_mode ||= Gitlab::Auth::CurrentUserMode.new(current_user)
- end
-
# A user requires a role and have the setup_for_company attribute set when they are part of the experimental signup
# flow (executed by the Growth team). Users are redirected to the welcome page when their role is required and the
# experiment is enabled for the current user.
diff --git a/app/controllers/concerns/enforces_admin_authentication.rb b/app/controllers/concerns/enforces_admin_authentication.rb
index e731211f423..527759de0bb 100644
--- a/app/controllers/concerns/enforces_admin_authentication.rb
+++ b/app/controllers/concerns/enforces_admin_authentication.rb
@@ -18,6 +18,7 @@ module EnforcesAdminAuthentication
return unless Feature.enabled?(:user_mode_in_session)
unless current_user_mode.admin_mode?
+ current_user_mode.request_admin_mode!
store_location_for(:redirect, request.fullpath) if storable_location?
redirect_to(new_admin_session_path, notice: _('Re-authentication required'))
end
diff --git a/app/controllers/concerns/initializes_current_user_mode.rb b/app/controllers/concerns/initializes_current_user_mode.rb
new file mode 100644
index 00000000000..df7cea5c754
--- /dev/null
+++ b/app/controllers/concerns/initializes_current_user_mode.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module InitializesCurrentUserMode
+ extend ActiveSupport::Concern
+
+ included do
+ helper_method :current_user_mode
+ end
+
+ def current_user_mode
+ @current_user_mode ||= Gitlab::Auth::CurrentUserMode.new(current_user)
+ end
+end
diff --git a/app/controllers/concerns/sessionless_authentication.rb b/app/controllers/concerns/sessionless_authentication.rb
index f644923443b..d5c26fca957 100644
--- a/app/controllers/concerns/sessionless_authentication.rb
+++ b/app/controllers/concerns/sessionless_authentication.rb
@@ -33,6 +33,8 @@ module SessionlessAuthentication
end
def enable_admin_mode!
- current_user_mode.enable_admin_mode!(skip_password_validation: true) if Feature.enabled?(:user_mode_in_session)
+ return unless Feature.enabled?(:user_mode_in_session)
+
+ current_user_mode.enable_sessionless_admin_mode!
end
end
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index 8dd51ce1d64..bbf0bdd3662 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -6,6 +6,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
include PageLayoutHelper
include OauthApplications
include Gitlab::Experimentation::ControllerConcern
+ include InitializesCurrentUserMode
before_action :verify_user_oauth_applications_enabled, except: :index
before_action :authenticate_user!
diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb
index e65726dffbf..2a4e659c5b9 100644
--- a/app/controllers/oauth/authorizations_controller.rb
+++ b/app/controllers/oauth/authorizations_controller.rb
@@ -2,6 +2,8 @@
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
include Gitlab::Experimentation::ControllerConcern
+ include InitializesCurrentUserMode
+
layout 'profile'
# Overridden from Doorkeeper::AuthorizationsController to
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index eca58748cc5..92f36c031f1 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -4,6 +4,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
include AuthenticatesWithTwoFactor
include Devise::Controllers::Rememberable
include AuthHelper
+ include InitializesCurrentUserMode
protect_from_forgery except: [:kerberos, :saml, :cas3, :failure], with: :exception, prepend: true
@@ -94,8 +95,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
return render_403 unless link_provider_allowed?(oauth['provider'])
log_audit_event(current_user, with: oauth['provider'])
- identity_linker ||= auth_module::IdentityLinker.new(current_user, oauth, session)
+ if Feature.enabled?(:user_mode_in_session)
+ return admin_mode_flow if current_user_mode.admin_mode_requested?
+ end
+
+ identity_linker ||= auth_module::IdentityLinker.new(current_user, oauth, session)
link_identity(identity_linker)
if identity_linker.changed?
@@ -239,6 +244,24 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
store_location_for(:user, uri.to_s)
end
end
+
+ def admin_mode_flow
+ if omniauth_identity_matches_current_user?
+ current_user_mode.enable_admin_mode!(skip_password_validation: true)
+
+ redirect_to stored_location_for(:redirect) || admin_root_path, notice: _('Admin mode enabled')
+ else
+ fail_admin_mode_invalid_credentials
+ end
+ end
+
+ def omniauth_identity_matches_current_user?
+ current_user.matches_identity?(oauth['provider'], oauth['uid'])
+ end
+
+ def fail_admin_mode_invalid_credentials
+ redirect_to new_admin_session_path, alert: _('Invalid login or password')
+ end
end
OmniauthCallbacksController.prepend_if_ee('EE::OmniauthCallbacksController')
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index a11676770a9..bd80ad7ff74 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -159,3 +159,5 @@ module Types
resolver: Resolvers::Projects::SnippetsResolver
end
end
+
+Types::ProjectType.prepend_if_ee('::EE::Types::ProjectType')
diff --git a/app/helpers/container_expiration_policies_helper.rb b/app/helpers/container_expiration_policies_helper.rb
new file mode 100644
index 00000000000..17791e7b0ff
--- /dev/null
+++ b/app/helpers/container_expiration_policies_helper.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module ContainerExpirationPoliciesHelper
+ def cadence_options
+ ContainerExpirationPolicy.cadence_options.map do |key, val|
+ { key: key.to_s, label: val }
+ end
+ end
+
+ def keep_n_options
+ ContainerExpirationPolicy.keep_n_options.map do |key, val|
+ { key: key, label: val }
+ end
+ end
+
+ def older_than_options
+ ContainerExpirationPolicy.older_than_options.map do |key, val|
+ { key: key.to_s, label: val }
+ end
+ end
+end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 2ce45cec878..6013475acb1 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -87,7 +87,7 @@ module NavHelper
end
if Feature.enabled?(:user_mode_in_session)
- if current_user&.admin? && current_user_mode&.admin_mode?
+ if current_user_mode.admin_mode?
links << :admin_mode
end
end
diff --git a/app/models/container_expiration_policy.rb b/app/models/container_expiration_policy.rb
new file mode 100644
index 00000000000..f60a0179c83
--- /dev/null
+++ b/app/models/container_expiration_policy.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+class ContainerExpirationPolicy < ApplicationRecord
+ belongs_to :project, inverse_of: :container_expiration_policy
+
+ validates :project, presence: true
+ validates :enabled, inclusion: { in: [true, false] }
+ validates :cadence, presence: true, inclusion: { in: ->(_) { self.cadence_options.stringify_keys } }
+ validates :older_than, inclusion: { in: ->(_) { self.older_than_options.stringify_keys } }, allow_nil: true
+ validates :keep_n, inclusion: { in: ->(_) { self.keep_n_options.keys } }, allow_nil: true
+
+ def self.keep_n_options
+ {
+ 1 => _('%{tags} tag per image name') % { tags: 1 },
+ 5 => _('%{tags} tags per image name') % { tags: 5 },
+ 10 => _('%{tags} tags per image name') % { tags: 10 },
+ 25 => _('%{tags} tags per image name') % { tags: 25 },
+ 50 => _('%{tags} tags per image name') % { tags: 50 },
+ 100 => _('%{tags} tags per image name') % { tags: 100 }
+ }
+ end
+
+ def self.cadence_options
+ {
+ '1d': _('Every day'),
+ '7d': _('Every week'),
+ '14d': _('Every two weeks'),
+ '1month': _('Every month'),
+ '3month': _('Every three months')
+ }
+ end
+
+ def self.older_than_options
+ {
+ '7d': _('%{days} days until tags are automatically removed') % { days: 7 },
+ '14d': _('%{days} days until tags are automatically removed') % { days: 14 },
+ '30d': _('%{days} days until tags are automatically removed') % { days: 30 },
+ '90d': _('%{days} days until tags are automatically removed') % { days: 90 }
+ }
+ end
+end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 5663ebf8ba1..d5a7c172fec 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -123,8 +123,10 @@ class Namespace < ApplicationRecord
def find_by_pages_host(host)
gitlab_host = "." + Settings.pages.host.downcase
- name = host.downcase.delete_suffix(gitlab_host)
+ host = host.downcase
+ return unless host.ends_with?(gitlab_host)
+ name = host.delete_suffix(gitlab_host)
Namespace.find_by_full_path(name)
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 1cf69bf8403..84289e1d5ac 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -97,8 +97,11 @@ class Project < ApplicationRecord
unless: :ci_cd_settings,
if: proc { ProjectCiCdSetting.available? }
+ after_create :create_container_expiration_policy,
+ unless: :container_expiration_policy
+
after_create :create_pages_metadatum,
- unless: :pages_metadatum
+ unless: :pages_metadatum
after_create :set_timestamps_for_create
after_update :update_forks_visibility_level
@@ -248,6 +251,7 @@ class Project < ApplicationRecord
# which is not managed by the DB. Hence we're still using dependent: :destroy
# here.
has_many :container_repositories, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_one :container_expiration_policy, inverse_of: :project
has_many :commit_statuses
# The relation :all_pipelines is intended to be used when we want to get the
@@ -305,6 +309,7 @@ class Project < ApplicationRecord
accepts_nested_attributes_for :import_data
accepts_nested_attributes_for :auto_devops, update_only: true
accepts_nested_attributes_for :ci_cd_settings, update_only: true
+ accepts_nested_attributes_for :container_expiration_policy, update_only: true
accepts_nested_attributes_for :remote_mirrors,
allow_destroy: true,
diff --git a/app/models/user.rb b/app/models/user.rb
index fd02db86582..6a29de20d86 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -996,6 +996,10 @@ class User < ApplicationRecord
@ldap_identity ||= identities.find_by(["provider LIKE ?", "ldap%"])
end
+ def matches_identity?(provider, extern_uid)
+ identities.where(provider: provider, extern_uid: extern_uid).exists?
+ end
+
def project_deploy_keys
DeployKey.in_projects(authorized_projects.select(:id)).distinct(:id)
end
diff --git a/app/views/admin/sessions/_signin_box.html.haml b/app/views/admin/sessions/_signin_box.html.haml
deleted file mode 100644
index 1d19915d3c5..00000000000
--- a/app/views/admin/sessions/_signin_box.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-- if any_form_based_providers_enabled?
-
- - if password_authentication_enabled_for_web?
- .login-box.tab-pane{ id: 'login-pane', role: 'tabpanel' }
- .login-body
- = render 'admin/sessions/new_base'
-
-- elsif password_authentication_enabled_for_web?
- .login-box.tab-pane.active{ id: 'login-pane', role: 'tabpanel' }
- .login-body
- = render 'admin/sessions/new_base'
diff --git a/app/views/admin/sessions/new.html.haml b/app/views/admin/sessions/new.html.haml
index 73028e78ea5..a1d440f2cfd 100644
--- a/app/views/admin/sessions/new.html.haml
+++ b/app/views/admin/sessions/new.html.haml
@@ -7,9 +7,16 @@
#signin-container
= render 'admin/sessions/tabs_normal'
.tab-content
- - if password_authentication_enabled_for_web?
- = render 'admin/sessions/signin_box'
- - else
- -# Show a message if none of the mechanisms above are enabled
+ - if !current_user.require_password_creation_for_web?
+ .login-box.tab-pane.active{ id: 'login-pane', role: 'tabpanel' }
+ .login-body
+ = render 'admin/sessions/new_base'
+
+ - if omniauth_enabled? && button_based_providers_enabled?
+ .clearfix
+ = render 'devise/shared/omniauth_box'
+
+ -# Show a message if none of the mechanisms above are enabled
+ - if current_user.require_password_creation_for_web? && !omniauth_enabled?
.prepend-top-default.center
= _('No authentication methods configured.')
diff --git a/changelogs/unreleased/15398-mvc-container-registry-tag-expiration-policies.yml b/changelogs/unreleased/15398-mvc-container-registry-tag-expiration-policies.yml
new file mode 100644
index 00000000000..d71a5f171c7
--- /dev/null
+++ b/changelogs/unreleased/15398-mvc-container-registry-tag-expiration-policies.yml
@@ -0,0 +1,5 @@
+---
+title: Create container expiration policies for projects
+merge_request: 20412
+author:
+type: added
diff --git a/changelogs/unreleased/22465-rack-attack-authenticate-runner-requests.yml b/changelogs/unreleased/22465-rack-attack-authenticate-runner-requests.yml
new file mode 100644
index 00000000000..7d3cbcc2a1c
--- /dev/null
+++ b/changelogs/unreleased/22465-rack-attack-authenticate-runner-requests.yml
@@ -0,0 +1,5 @@
+---
+title: Authenticate runner requests in Rack::Attack
+merge_request: 21311
+author:
+type: fixed
diff --git a/changelogs/unreleased/34261-service-desk-to-graphql.yml b/changelogs/unreleased/34261-service-desk-to-graphql.yml
new file mode 100644
index 00000000000..b055b443822
--- /dev/null
+++ b/changelogs/unreleased/34261-service-desk-to-graphql.yml
@@ -0,0 +1,5 @@
+---
+title: Add service desk information to project graphQL endpoint
+merge_request: 20722
+author:
+type: changed
diff --git a/changelogs/unreleased/36326-auto-focus-search-bar.yml b/changelogs/unreleased/36326-auto-focus-search-bar.yml
new file mode 100644
index 00000000000..5c5921140b4
--- /dev/null
+++ b/changelogs/unreleased/36326-auto-focus-search-bar.yml
@@ -0,0 +1,5 @@
+---
+title: Autofocus cluster dropdown search input
+merge_request: 21440
+author:
+type: changed
diff --git a/changelogs/unreleased/feat-support-omniauth-for-admin-mode.yml b/changelogs/unreleased/feat-support-omniauth-for-admin-mode.yml
new file mode 100644
index 00000000000..271499700e3
--- /dev/null
+++ b/changelogs/unreleased/feat-support-omniauth-for-admin-mode.yml
@@ -0,0 +1,5 @@
+---
+title: Add OmniAuth authentication support to admin mode feature
+merge_request: 18214
+author: Diego Louzán
+type: added
diff --git a/config/initializers/rack_attack_new.rb b/config/initializers/rack_attack_new.rb
index 6d29bb1cd8b..267d4c1eda9 100644
--- a/config/initializers/rack_attack_new.rb
+++ b/config/initializers/rack_attack_new.rb
@@ -113,11 +113,15 @@ class Rack::Attack
class Request
def unauthenticated?
- !authenticated_user_id([:api, :rss, :ics])
+ !(authenticated_user_id([:api, :rss, :ics]) || authenticated_runner_id)
end
def authenticated_user_id(request_formats)
- Gitlab::Auth::RequestAuthenticator.new(self).user(request_formats)&.id
+ request_authenticator.user(request_formats)&.id
+ end
+
+ def authenticated_runner_id
+ request_authenticator.runner&.id
end
def api_request?
@@ -150,6 +154,10 @@ class Rack::Attack
private
+ def request_authenticator
+ @request_authenticator ||= Gitlab::Auth::RequestAuthenticator.new(self)
+ end
+
def protected_paths
Gitlab::CurrentSettings.current_application_settings.protected_paths
end
diff --git a/db/migrate/20191119231621_create_container_expiration_policies.rb b/db/migrate/20191119231621_create_container_expiration_policies.rb
new file mode 100644
index 00000000000..d7108870cf1
--- /dev/null
+++ b/db/migrate/20191119231621_create_container_expiration_policies.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class CreateContainerExpirationPolicies < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def change
+ create_table :container_expiration_policies, id: false, primary_key: :project_id do |t|
+ t.timestamps_with_timezone null: false
+ t.datetime_with_timezone :next_run_at
+ t.references :project, primary_key: true, default: nil, index: false, foreign_key: { on_delete: :cascade }
+ t.string :name_regex, limit: 255
+ t.string :cadence, null: false, limit: 12, default: '7d'
+ t.string :older_than, limit: 12
+ t.integer :keep_n
+ t.boolean :enabled, null: false, default: false
+ end
+
+ add_index :container_expiration_policies, [:next_run_at, :enabled],
+ name: 'index_container_expiration_policies_on_next_run_at_and_enabled'
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index da0cfd80ac2..deebbcb430d 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -1224,6 +1224,18 @@ ActiveRecord::Schema.define(version: 2019_12_06_122926) do
t.index ["note_id"], name: "index_commit_user_mentions_on_note_id", unique: true
end
+ create_table "container_expiration_policies", primary_key: "project_id", id: :bigint, default: nil, force: :cascade do |t|
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.datetime_with_timezone "next_run_at"
+ t.string "name_regex", limit: 255
+ t.string "cadence", limit: 12, default: "7d", null: false
+ t.string "older_than", limit: 12
+ t.integer "keep_n"
+ t.boolean "enabled", default: false, null: false
+ t.index ["next_run_at", "enabled"], name: "index_container_expiration_policies_on_next_run_at_and_enabled"
+ end
+
create_table "container_repositories", id: :serial, force: :cascade do |t|
t.integer "project_id", null: false
t.string "name", null: false
@@ -4410,6 +4422,7 @@ ActiveRecord::Schema.define(version: 2019_12_06_122926) do
add_foreign_key "clusters_kubernetes_namespaces", "environments", on_delete: :nullify
add_foreign_key "clusters_kubernetes_namespaces", "projects", on_delete: :nullify
add_foreign_key "commit_user_mentions", "notes", on_delete: :cascade
+ add_foreign_key "container_expiration_policies", "projects", on_delete: :cascade
add_foreign_key "container_repositories", "projects"
add_foreign_key "dependency_proxy_blobs", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "dependency_proxy_group_settings", "namespaces", column: "group_id", on_delete: :cascade
diff --git a/doc/administration/geo/replication/database.md b/doc/administration/geo/replication/database.md
index dd7cf9f5710..72c3692716b 100644
--- a/doc/administration/geo/replication/database.md
+++ b/doc/administration/geo/replication/database.md
@@ -155,8 +155,8 @@ There is an [issue where support is being discussed](https://gitlab.com/gitlab-o
| `postgresql['md5_auth_cidr_addresses']` | **Secondary** node's public or VPC private addresses. |
If you are using Google Cloud Platform, SoftLayer, or any other vendor that
- provides a virtual private cloud (VPC) you can use the **secondary** node's private
- address (corresponds to "internal address" for Google Cloud Platform) for
+ provides a virtual private cloud (VPC) you can use the **primary** and **secondary** nodes
+ private addresses (corresponds to "internal address" for Google Cloud Platform) for
`postgresql['md5_auth_cidr_addresses']` and `postgresql['listen_address']`.
The `listen_address` option opens PostgreSQL up to network connections with the interface
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 0d34e3136dc..7089ee8e51d 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -4552,6 +4552,16 @@ type Project {
): SentryDetailedError
"""
+ E-mail address of the service desk.
+ """
+ serviceDeskAddress: String
+
+ """
+ Indicates if the project has service desk enabled.
+ """
+ serviceDeskEnabled: Boolean
+
+ """
Indicates if shared runners are enabled on the project
"""
sharedRunnersEnabled: Boolean
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 833a230fd3f..aa7951e6bf7 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -1315,6 +1315,34 @@
"deprecationReason": null
},
{
+ "name": "serviceDeskAddress",
+ "description": "E-mail address of the service desk.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "serviceDeskEnabled",
+ "description": "Indicates if the project has service desk enabled.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "sharedRunnersEnabled",
"description": "Indicates if shared runners are enabled on the project",
"args": [
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index b96666915f9..ec9b586d065 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -668,6 +668,8 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `mergeRequest` | MergeRequest | A single merge request of the project |
| `issue` | Issue | A single issue of the project |
| `sentryDetailedError` | SentryDetailedError | Detailed version of a Sentry error on the project |
+| `serviceDeskEnabled` | Boolean | Indicates if the project has service desk enabled. |
+| `serviceDeskAddress` | String | E-mail address of the service desk. |
### ProjectPermissions
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
index bafcfd110d3..5f661ac4c76 100644
--- a/doc/api/jobs.md
+++ b/doc/api/jobs.md
@@ -2,7 +2,7 @@
## List project jobs
-Get a list of jobs in a project.
+Get a list of jobs in a project. Jobs are sorted in descending order of their IDs.
```
GET /projects/:id/jobs
@@ -33,13 +33,23 @@ Example of response
},
"coverage": null,
"allow_failure": false,
- "created_at": "2015-12-24T15:51:21.727Z",
- "started_at": "2015-12-24T17:54:24.729Z",
- "finished_at": "2015-12-24T17:54:24.921Z",
- "duration": 0.192,
- "artifacts_expire_at": "2016-01-23T17:54:24.921Z",
- "id": 6,
- "name": "rspec:other",
+ "created_at": "2015-12-24T15:51:21.802Z",
+ "started_at": "2015-12-24T17:54:27.722Z",
+ "finished_at": "2015-12-24T17:54:27.895Z",
+ "duration": 0.173,
+ "artifacts_file": {
+ "filename": "artifacts.zip",
+ "size": 1000
+ },
+ "artifacts": [
+ {"file_type": "archive", "size": 1000, "filename": "artifacts.zip", "file_format": "zip"},
+ {"file_type": "metadata", "size": 186, "filename": "metadata.gz", "file_format": "gzip"},
+ {"file_type": "trace", "size": 1500, "filename": "job.log", "file_format": "raw"},
+ {"file_type": "junit", "size": 750, "filename": "junit.xml.gz", "file_format": "gzip"}
+ ],
+ "artifacts_expire_at": "2016-01-23T17:54:27.895Z",
+ "id": 7,
+ "name": "teaspoon",
"pipeline": {
"id": 6,
"ref": "master",
@@ -52,7 +62,7 @@ Example of response
"stage": "test",
"status": "failed",
"tag": false,
- "web_url": "https://example.com/foo/bar/-/jobs/6",
+ "web_url": "https://example.com/foo/bar/-/jobs/7",
"user": {
"id": 1,
"name": "Administrator",
@@ -83,23 +93,13 @@ Example of response
},
"coverage": null,
"allow_failure": false,
- "created_at": "2015-12-24T15:51:21.802Z",
- "started_at": "2015-12-24T17:54:27.722Z",
- "finished_at": "2015-12-24T17:54:27.895Z",
- "duration": 0.173,
- "artifacts_file": {
- "filename": "artifacts.zip",
- "size": 1000
- },
- "artifacts": [
- {"file_type": "archive", "size": 1000, "filename": "artifacts.zip", "file_format": "zip"},
- {"file_type": "metadata", "size": 186, "filename": "metadata.gz", "file_format": "gzip"},
- {"file_type": "trace", "size": 1500, "filename": "job.log", "file_format": "raw"},
- {"file_type": "junit", "size": 750, "filename": "junit.xml.gz", "file_format": "gzip"}
- ],
- "artifacts_expire_at": "2016-01-23T17:54:27.895Z",
- "id": 7,
- "name": "teaspoon",
+ "created_at": "2015-12-24T15:51:21.727Z",
+ "started_at": "2015-12-24T17:54:24.729Z",
+ "finished_at": "2015-12-24T17:54:24.921Z",
+ "duration": 0.192,
+ "artifacts_expire_at": "2016-01-23T17:54:24.921Z",
+ "id": 6,
+ "name": "rspec:other",
"pipeline": {
"id": 6,
"ref": "master",
@@ -112,7 +112,7 @@ Example of response
"stage": "test",
"status": "failed",
"tag": false,
- "web_url": "https://example.com/foo/bar/-/jobs/7",
+ "web_url": "https://example.com/foo/bar/-/jobs/6",
"user": {
"id": 1,
"name": "Administrator",
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index d1535c43808..0769e464d26 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -44,7 +44,7 @@ module API
# Helper Methods for Grape Endpoint
module HelperMethods
prepend_if_ee('EE::API::APIGuard::HelperMethods') # rubocop: disable Cop/InjectEnterpriseEditionModule
- include Gitlab::Auth::UserAuthFinders
+ include Gitlab::Auth::AuthFinders
def find_current_user!
user = find_user_from_sources
@@ -56,7 +56,9 @@ module API
# Set admin mode for API requests (if admin)
if Feature.enabled?(:user_mode_in_session)
- Gitlab::Auth::CurrentUserMode.new(user).enable_admin_mode!(skip_password_validation: true)
+ current_user_mode = Gitlab::Auth::CurrentUserMode.new(user)
+
+ current_user_mode.enable_sessionless_admin_mode!
end
user
diff --git a/lib/gitlab/auth/user_auth_finders.rb b/lib/gitlab/auth/auth_finders.rb
index 983682baab1..6210aca739a 100644
--- a/lib/gitlab/auth/user_auth_finders.rb
+++ b/lib/gitlab/auth/auth_finders.rb
@@ -17,8 +17,8 @@ module Gitlab
end
end
- module UserAuthFinders
- prepend_if_ee('::EE::Gitlab::Auth::UserAuthFinders') # rubocop: disable Cop/InjectEnterpriseEditionModule
+ module AuthFinders
+ prepend_if_ee('::EE::Gitlab::Auth::AuthFinders') # rubocop: disable Cop/InjectEnterpriseEditionModule
include Gitlab::Utils::StrongMemoize
@@ -26,6 +26,7 @@ module Gitlab
PRIVATE_TOKEN_PARAM = :private_token
JOB_TOKEN_HEADER = "HTTP_JOB_TOKEN".freeze
JOB_TOKEN_PARAM = :job_token
+ RUNNER_TOKEN_PARAM = :token
# Check the Rails session for valid authentication details
def find_user_from_warden
@@ -85,6 +86,15 @@ module Gitlab
access_token.user || raise(UnauthorizedError)
end
+ def find_runner_from_token
+ return unless api_request?
+
+ token = current_request.params[RUNNER_TOKEN_PARAM].presence
+ return unless token
+
+ ::Ci::Runner.find_by_token(token) || raise(UnauthorizedError)
+ end
+
def validate_access_token!(scopes: [])
return unless access_token
@@ -201,7 +211,7 @@ module Gitlab
end
def api_request?
- current_request.path.starts_with?("/api/")
+ current_request.path.starts_with?('/api/')
end
def archive_request?
diff --git a/lib/gitlab/auth/current_user_mode.rb b/lib/gitlab/auth/current_user_mode.rb
index df5039f50c1..cb39baaa6cc 100644
--- a/lib/gitlab/auth/current_user_mode.rb
+++ b/lib/gitlab/auth/current_user_mode.rb
@@ -8,9 +8,13 @@ module Gitlab
# an administrator must have explicitly enabled admin-mode
# e.g. on web access require re-authentication
class CurrentUserMode
+ NotRequestedError = Class.new(StandardError)
+
SESSION_STORE_KEY = :current_user_mode
ADMIN_MODE_START_TIME_KEY = 'admin_mode'
+ ADMIN_MODE_REQUESTED_TIME_KEY = 'admin_mode_requested'
MAX_ADMIN_MODE_TIME = 6.hours
+ ADMIN_MODE_REQUESTED_GRACE_PERIOD = 5.minutes
def initialize(user)
@user = user
@@ -19,8 +23,16 @@ module Gitlab
def admin_mode?
return false unless user
- Gitlab::SafeRequestStore.fetch(request_store_key) do
- user&.admin? && any_session_with_admin_mode?
+ Gitlab::SafeRequestStore.fetch(admin_mode_rs_key) do
+ user.admin? && any_session_with_admin_mode?
+ end
+ end
+
+ def admin_mode_requested?
+ return false unless user
+
+ Gitlab::SafeRequestStore.fetch(admin_mode_requested_rs_key) do
+ user.admin? && admin_mode_requested_in_grace_period?
end
end
@@ -28,20 +40,45 @@ module Gitlab
return unless user&.admin?
return unless skip_password_validation || user&.valid_password?(password)
+ raise NotRequestedError unless admin_mode_requested?
+
+ reset_request_store
+
+ current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = nil
current_session_data[ADMIN_MODE_START_TIME_KEY] = Time.now
end
+ def enable_sessionless_admin_mode!
+ request_admin_mode! && enable_admin_mode!(skip_password_validation: true)
+ end
+
def disable_admin_mode!
+ return unless user&.admin?
+
+ reset_request_store
+
+ current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = nil
current_session_data[ADMIN_MODE_START_TIME_KEY] = nil
- Gitlab::SafeRequestStore.delete(request_store_key)
+ end
+
+ def request_admin_mode!
+ return unless user&.admin?
+
+ reset_request_store
+
+ current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = Time.now
end
private
attr_reader :user
- def request_store_key
- @request_store_key ||= { res: :current_user_mode, user: user.id }
+ def admin_mode_rs_key
+ @admin_mode_rs_key ||= { res: :current_user_mode, user: user.id, method: :admin_mode? }
+ end
+
+ def admin_mode_requested_rs_key
+ @admin_mode_requested_rs_key ||= { res: :current_user_mode, user: user.id, method: :admin_mode_requested? }
end
def current_session_data
@@ -61,6 +98,15 @@ module Gitlab
Gitlab::NamespacedSessionStore.new(SESSION_STORE_KEY, session.with_indifferent_access )
end
end
+
+ def admin_mode_requested_in_grace_period?
+ current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY].to_i > ADMIN_MODE_REQUESTED_GRACE_PERIOD.ago.to_i
+ end
+
+ def reset_request_store
+ Gitlab::SafeRequestStore.delete(admin_mode_rs_key)
+ Gitlab::SafeRequestStore.delete(admin_mode_requested_rs_key)
+ end
end
end
end
diff --git a/lib/gitlab/auth/request_authenticator.rb b/lib/gitlab/auth/request_authenticator.rb
index aca8804b04c..9b1b7b8e879 100644
--- a/lib/gitlab/auth/request_authenticator.rb
+++ b/lib/gitlab/auth/request_authenticator.rb
@@ -5,7 +5,7 @@
module Gitlab
module Auth
class RequestAuthenticator
- include UserAuthFinders
+ include AuthFinders
attr_reader :request
@@ -23,6 +23,12 @@ module Gitlab
find_user_from_warden
end
+ def runner
+ find_runner_from_token
+ rescue Gitlab::Auth::AuthenticationError
+ nil
+ end
+
def find_sessionless_user(request_format)
find_user_from_web_access_token(request_format) ||
find_user_from_feed_token(request_format) ||
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index b3a1d472e73..2fd4f18b756 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -73,6 +73,7 @@ tree:
- :auto_devops
- :triggers
- :pipeline_schedules
+ - :container_expiration_policy
- :services
- protected_branches:
- :merge_access_levels
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 82509e3c880..5c417aa1f29 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -234,6 +234,9 @@ msgstr[1] ""
msgid "%{count} related %{pluralized_subject}: %{links}"
msgstr ""
+msgid "%{days} days until tags are automatically removed"
+msgstr ""
+
msgid "%{description}- Sentry event: %{errorUrl}- First seen: %{firstSeen}- Last seen: %{lastSeen} %{countLabel}: %{count}%{userCountLabel}: %{userCount}"
msgstr ""
@@ -380,6 +383,12 @@ msgstr[1] ""
msgid "%{tabname} changed"
msgstr ""
+msgid "%{tags} tag per image name"
+msgstr ""
+
+msgid "%{tags} tags per image name"
+msgstr ""
+
msgid "%{tag}-evidence.json"
msgstr ""
@@ -1134,9 +1143,15 @@ msgstr ""
msgid "Admin Section"
msgstr ""
+msgid "Admin mode already enabled"
+msgstr ""
+
msgid "Admin mode disabled"
msgstr ""
+msgid "Admin mode enabled"
+msgstr ""
+
msgid "Admin notes"
msgstr ""
@@ -7050,12 +7065,27 @@ msgstr ""
msgid "Every %{action} attempt has failed: %{job_error_message}. Please try again."
msgstr ""
+msgid "Every day"
+msgstr ""
+
msgid "Every day (at 4:00am)"
msgstr ""
+msgid "Every month"
+msgstr ""
+
msgid "Every month (on the 1st at 4:00am)"
msgstr ""
+msgid "Every three months"
+msgstr ""
+
+msgid "Every two weeks"
+msgstr ""
+
+msgid "Every week"
+msgstr ""
+
msgid "Every week (Sundays at 4:00am)"
msgstr ""
@@ -9678,6 +9708,9 @@ msgstr ""
msgid "Invalid input, please avoid emojis"
msgstr ""
+msgid "Invalid login or password"
+msgstr ""
+
msgid "Invalid pin code"
msgstr ""
@@ -14397,6 +14430,9 @@ msgstr ""
msgid "Raw blob request rate limit per minute"
msgstr ""
+msgid "Re-authentication period expired or never requested. Please try again"
+msgstr ""
+
msgid "Re-authentication required"
msgstr ""
diff --git a/qa/.gitignore b/qa/.gitignore
index b0ae074ac07..7bc4effd8a8 100644
--- a/qa/.gitignore
+++ b/qa/.gitignore
@@ -1,3 +1,4 @@
tmp/
.ruby-version
+.ruby-gemset
urls.yml
diff --git a/qa/qa.rb b/qa/qa.rb
index 178771a0275..249736fc8b0 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -35,6 +35,7 @@ module QA
autoload :Logger, 'qa/runtime/logger'
autoload :GPG, 'qa/runtime/gpg'
autoload :MailHog, 'qa/runtime/mail_hog'
+ autoload :IPAddress, 'qa/runtime/ip_address'
module API
autoload :Client, 'qa/runtime/api/client'
diff --git a/qa/qa/resource/api_fabricator.rb b/qa/qa/resource/api_fabricator.rb
index 3862bd68c40..e6057433b55 100644
--- a/qa/qa/resource/api_fabricator.rb
+++ b/qa/qa/resource/api_fabricator.rb
@@ -100,7 +100,7 @@ module QA
url = Runtime::API::Request.new(api_client, api_delete_path).url
response = delete(url)
- unless response.code == HTTP_STATUS_NO_CONTENT
+ unless [HTTP_STATUS_NO_CONTENT, HTTP_STATUS_ACCEPTED].include? response.code
raise ResourceNotDeletedError, "Resource at #{url} could not be deleted (#{response.code}): `#{response}`."
end
diff --git a/qa/qa/resource/group.rb b/qa/qa/resource/group.rb
index 7511396251d..c12e9dd146b 100644
--- a/qa/qa/resource/group.rb
+++ b/qa/qa/resource/group.rb
@@ -70,6 +70,10 @@ module QA
}
end
+ def api_delete_path
+ "/groups/#{id}"
+ end
+
def full_path
sandbox.path + ' / ' + path
end
diff --git a/qa/qa/runtime/ip_address.rb b/qa/qa/runtime/ip_address.rb
new file mode 100644
index 00000000000..f370882e5c7
--- /dev/null
+++ b/qa/qa/runtime/ip_address.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+require 'socket'
+
+module QA
+ module Runtime
+ module IPAddress
+ include Support::Api
+ HostUnreachableError = Class.new(StandardError)
+
+ LOOPBACK_ADDRESS = '127.0.0.1'
+ PUBLIC_IP_ADDRESS_API = "https://api.ipify.org"
+
+ def fetch_current_ip_address
+ # When running on CI against a live environment such as staging.gitlab.com,
+ # we use the public facing IP address
+ ip_address = if Env.running_in_ci? && !URI.parse(Scenario.gitlab_address).host.include?('test')
+ response = get(PUBLIC_IP_ADDRESS_API)
+ raise HostUnreachableError, "#{PUBLIC_IP_ADDRESS_API} is unreachable" unless response.code == Support::Api::HTTP_STATUS_OK
+
+ response.body
+ elsif page.current_host.include?('localhost')
+ LOOPBACK_ADDRESS
+ else
+ Socket.ip_address_list.detect { |intf| intf.ipv4_private? }.ip_address
+ end
+
+ QA::Runtime::Logger.info "Current IP address: #{ip_address}"
+
+ ip_address
+ end
+ end
+ end
+end
diff --git a/qa/qa/support/api.rb b/qa/qa/support/api.rb
index cd496efb4db..90924ffd40e 100644
--- a/qa/qa/support/api.rb
+++ b/qa/qa/support/api.rb
@@ -6,6 +6,7 @@ module QA
HTTP_STATUS_OK = 200
HTTP_STATUS_CREATED = 201
HTTP_STATUS_NO_CONTENT = 204
+ HTTP_STATUS_ACCEPTED = 202
def post(url, payload)
RestClient::Request.execute(
diff --git a/spec/controllers/admin/sessions_controller_spec.rb b/spec/controllers/admin/sessions_controller_spec.rb
index c1cb57c0b9d..bd0bb0bd81f 100644
--- a/spec/controllers/admin/sessions_controller_spec.rb
+++ b/spec/controllers/admin/sessions_controller_spec.rb
@@ -17,7 +17,7 @@ describe Admin::SessionsController, :do_not_mock_admin_mode do
get :new
expect(response).to have_gitlab_http_status(:not_found)
- expect(controller.send(:current_user_mode).admin_mode?).to be(false)
+ expect(controller.current_user_mode.admin_mode?).to be(false)
end
end
@@ -28,7 +28,21 @@ describe Admin::SessionsController, :do_not_mock_admin_mode do
get :new
expect(response).to render_template :new
- expect(controller.send(:current_user_mode).admin_mode?).to be(false)
+ expect(controller.current_user_mode.admin_mode?).to be(false)
+ end
+
+ context 'already in admin mode' do
+ before do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: user.password)
+ end
+
+ it 'redirects to original location' do
+ get :new
+
+ expect(response).to redirect_to(admin_root_path)
+ expect(controller.current_user_mode.admin_mode?).to be(true)
+ end
end
end
end
@@ -39,7 +53,7 @@ describe Admin::SessionsController, :do_not_mock_admin_mode do
post :create
expect(response).to have_gitlab_http_status(:not_found)
- expect(controller.send(:current_user_mode).admin_mode?).to be(false)
+ expect(controller.current_user_mode.admin_mode?).to be(false)
end
end
@@ -47,24 +61,60 @@ describe Admin::SessionsController, :do_not_mock_admin_mode do
let(:user) { create(:admin) }
it 'sets admin mode with a valid password' do
- expect(controller.send(:current_user_mode).admin_mode?).to be(false)
+ expect(controller.current_user_mode.admin_mode?).to be(false)
controller.store_location_for(:redirect, admin_root_path)
+
+ # triggering the auth form will request admin mode
+ get :new
+
post :create, params: { password: user.password }
expect(response).to redirect_to admin_root_path
- expect(controller.send(:current_user_mode).admin_mode?).to be(true)
+ expect(controller.current_user_mode.admin_mode?).to be(true)
end
it 'fails with an invalid password' do
- expect(controller.send(:current_user_mode).admin_mode?).to be(false)
+ expect(controller.current_user_mode.admin_mode?).to be(false)
controller.store_location_for(:redirect, admin_root_path)
+ # triggering the auth form will request admin mode
+ get :new
+
post :create, params: { password: '' }
expect(response).to render_template :new
- expect(controller.send(:current_user_mode).admin_mode?).to be(false)
+ expect(controller.current_user_mode.admin_mode?).to be(false)
+ end
+
+ it 'fails if not requested first' do
+ expect(controller.current_user_mode.admin_mode?).to be(false)
+
+ controller.store_location_for(:redirect, admin_root_path)
+
+ # do not trigger the auth form
+
+ post :create, params: { password: user.password }
+
+ expect(response).to redirect_to(new_admin_session_path)
+ expect(controller.current_user_mode.admin_mode?).to be(false)
+ end
+
+ it 'fails if request period expired' do
+ expect(controller.current_user_mode.admin_mode?).to be(false)
+
+ controller.store_location_for(:redirect, admin_root_path)
+
+ # triggering the auth form will request admin mode
+ get :new
+
+ Timecop.freeze(Gitlab::Auth::CurrentUserMode::ADMIN_MODE_REQUESTED_GRACE_PERIOD.from_now) do
+ post :create, params: { password: user.password }
+
+ expect(response).to redirect_to(new_admin_session_path)
+ expect(controller.current_user_mode.admin_mode?).to be(false)
+ end
end
end
end
@@ -75,7 +125,7 @@ describe Admin::SessionsController, :do_not_mock_admin_mode do
get :destroy
expect(response).to have_gitlab_http_status(404)
- expect(controller.send(:current_user_mode).admin_mode?).to be(false)
+ expect(controller.current_user_mode.admin_mode?).to be(false)
end
end
@@ -83,15 +133,17 @@ describe Admin::SessionsController, :do_not_mock_admin_mode do
let(:user) { create(:admin) }
it 'disables admin mode and redirects to main page' do
- expect(controller.send(:current_user_mode).admin_mode?).to be(false)
+ expect(controller.current_user_mode.admin_mode?).to be(false)
+
+ get :new
post :create, params: { password: user.password }
- expect(controller.send(:current_user_mode).admin_mode?).to be(true)
+ expect(controller.current_user_mode.admin_mode?).to be(true)
get :destroy
expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(root_path)
- expect(controller.send(:current_user_mode).admin_mode?).to be(false)
+ expect(controller.current_user_mode.admin_mode?).to be(false)
end
end
end
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 9c6ea13f948..e72ab16f62a 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -814,6 +814,7 @@ describe ApplicationController do
context 'that re-authenticated' do
before do
+ Gitlab::Auth::CurrentUserMode.new(user).request_admin_mode!
Gitlab::Auth::CurrentUserMode.new(user).enable_admin_mode!(password: user.password)
end
diff --git a/spec/controllers/oauth/applications_controller_spec.rb b/spec/controllers/oauth/applications_controller_spec.rb
index df836c2c3e3..270a2fcc1d6 100644
--- a/spec/controllers/oauth/applications_controller_spec.rb
+++ b/spec/controllers/oauth/applications_controller_spec.rb
@@ -62,6 +62,12 @@ describe Oauth::ApplicationsController do
end
end
+ context 'Helpers' do
+ it 'current_user_mode available' do
+ expect(subject.current_user_mode).not_to be_nil
+ end
+ end
+
def disable_user_oauth
allow(Gitlab::CurrentSettings.current_application_settings).to receive(:user_oauth_applications?).and_return(false)
end
diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb
index 521dbe7ee23..6c5f36804e8 100644
--- a/spec/controllers/omniauth_callbacks_controller_spec.rb
+++ b/spec/controllers/omniauth_callbacks_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe OmniauthCallbacksController, type: :controller do
+describe OmniauthCallbacksController, type: :controller, do_not_mock_admin_mode: true do
include LoginHelpers
describe 'omniauth' do
@@ -336,4 +336,109 @@ describe OmniauthCallbacksController, type: :controller do
end
end
end
+
+ describe 'enable admin mode' do
+ include_context 'custom session'
+
+ let(:provider) { :auth0 }
+ let(:extern_uid) { 'my-uid' }
+ let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider) }
+
+ def reauthenticate_and_check_admin_mode(expected_admin_mode:)
+ # Initially admin mode disabled
+ expect(subject.current_user_mode.admin_mode?).to be(false)
+
+ # Trigger OmniAuth admin mode flow and expect admin mode status
+ post provider
+
+ expect(request.env['warden']).to be_authenticated
+ expect(subject.current_user_mode.admin_mode?).to be(expected_admin_mode)
+ end
+
+ context 'user and admin mode requested by the same user' do
+ before do
+ sign_in user
+
+ mock_auth_hash(provider.to_s, extern_uid, user.email, additional_info: {})
+ stub_omniauth_provider(provider, context: request)
+ end
+
+ context 'with a regular user' do
+ it 'cannot be enabled' do
+ reauthenticate_and_check_admin_mode(expected_admin_mode: false)
+
+ expect(response).to redirect_to(root_path)
+ end
+ end
+
+ context 'with an admin user' do
+ let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider, access_level: :admin) }
+
+ context 'when requested first' do
+ before do
+ subject.current_user_mode.request_admin_mode!
+ end
+
+ it 'can be enabled' do
+ reauthenticate_and_check_admin_mode(expected_admin_mode: true)
+
+ expect(response).to redirect_to(admin_root_path)
+ end
+ end
+
+ context 'when not requested first' do
+ it 'cannot be enabled' do
+ reauthenticate_and_check_admin_mode(expected_admin_mode: false)
+
+ expect(response).to redirect_to(root_path)
+ end
+ end
+ end
+ end
+
+ context 'user and admin mode requested by different users' do
+ let(:reauth_extern_uid) { 'another_uid' }
+ let(:reauth_user) { create(:omniauth_user, extern_uid: reauth_extern_uid, provider: provider) }
+
+ before do
+ sign_in user
+
+ mock_auth_hash(provider.to_s, reauth_extern_uid, reauth_user.email, additional_info: {})
+ stub_omniauth_provider(provider, context: request)
+ end
+
+ context 'with a regular user' do
+ it 'cannot be enabled' do
+ reauthenticate_and_check_admin_mode(expected_admin_mode: false)
+
+ expect(response).to redirect_to(profile_account_path)
+ end
+ end
+
+ context 'with an admin user' do
+ let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider, access_level: :admin) }
+ let(:reauth_user) { create(:omniauth_user, extern_uid: reauth_extern_uid, provider: provider, access_level: :admin) }
+
+ context 'when requested first' do
+ before do
+ subject.current_user_mode.request_admin_mode!
+ end
+
+ it 'cannot be enabled' do
+ reauthenticate_and_check_admin_mode(expected_admin_mode: false)
+
+ expect(response).to redirect_to(new_admin_session_path)
+ end
+ end
+
+ context 'when not requested first' do
+ it 'cannot be enabled' do
+ reauthenticate_and_check_admin_mode(expected_admin_mode: false)
+
+ expect(response).to redirect_to(profile_account_path)
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index ba7374d5040..741f46cef45 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe 'Gcp Cluster', :js do
+describe 'Gcp Cluster', :js, :do_not_mock_admin_mode do
include GoogleApi::CloudPlatformHelpers
let(:project) { create(:project) }
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index 382b5f3cac0..01687674309 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -49,11 +49,11 @@ describe 'Environments page', :js do
it 'renders second page of pipelines' do
visit_environments(project, scope: 'available')
- find('.js-next-button').click
+ find('.page-link.next-page-item').click
wait_for_requests
- expect(page).to have_selector('.gl-pagination .page', count: 2)
- expect(find('.gl-pagination .page-item.active .page-link').text).to eq("2")
+ expect(page).to have_selector('.gl-pagination .page-link', count: 4)
+ expect(find('.gl-pagination .page-link.active').text).to eq("2")
end
end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index f5558a1f2ec..b4c9eb7ebec 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -592,15 +592,15 @@ describe 'Pipelines', :js do
visit project_pipelines_path(project, page: '2')
wait_for_requests
- expect(page).to have_selector('.gl-pagination .page', count: 2)
+ expect(page).to have_selector('.gl-pagination .page-link', count: 4)
end
it 'shows updated content' do
visit project_pipelines_path(project)
wait_for_requests
- page.find('.js-next-button .page-link').click
+ page.find('.page-link.next-page-item').click
- expect(page).to have_selector('.gl-pagination .page', count: 2)
+ expect(page).to have_selector('.gl-pagination .page-link', count: 4)
end
end
end
diff --git a/spec/frontend/create_cluster/eks_cluster/components/cluster_form_dropdown_spec.js b/spec/frontend/create_cluster/eks_cluster/components/cluster_form_dropdown_spec.js
index 7ba35358442..c9cdd728509 100644
--- a/spec/frontend/create_cluster/eks_cluster/components/cluster_form_dropdown_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/components/cluster_form_dropdown_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import $ from 'jquery';
import { GlIcon } from '@gitlab/ui';
import ClusterFormDropdown from '~/create_cluster/eks_cluster/components/cluster_form_dropdown.vue';
@@ -169,4 +170,14 @@ describe('ClusterFormDropdown', () => {
expect(vm.findAll('.js-dropdown-item').length).toEqual(1);
expect(vm.find('.js-dropdown-item').text()).toEqual(secondItem.name);
});
+
+ it('focuses dropdown search input when dropdown is displayed', () => {
+ const dropdownEl = vm.find('.dropdown').element;
+
+ expect(vm.find(DropdownSearchInput).props('focused')).toBe(false);
+
+ $(dropdownEl).trigger('shown.bs.dropdown');
+
+ expect(vm.find(DropdownSearchInput).props('focused')).toBe(true);
+ });
});
diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js
new file mode 100644
index 00000000000..0d0e4ae4349
--- /dev/null
+++ b/spec/frontend/vue_shared/components/dropdown/dropdown_search_input_spec.js
@@ -0,0 +1,55 @@
+import { mount } from '@vue/test-utils';
+import DropdownSearchInputComponent from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
+
+describe('DropdownSearchInputComponent', () => {
+ let wrapper;
+
+ const defaultProps = {
+ placeholderText: 'Search something',
+ };
+ const buildVM = (propsData = defaultProps) => {
+ wrapper = mount(DropdownSearchInputComponent, {
+ propsData,
+ });
+ };
+ const findInputEl = () => wrapper.find('.dropdown-input-field');
+
+ beforeEach(() => {
+ buildVM();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('template', () => {
+ it('renders input element with type `search`', () => {
+ expect(findInputEl().exists()).toBe(true);
+ expect(findInputEl().attributes('type')).toBe('search');
+ });
+
+ it('renders search icon element', () => {
+ expect(wrapper.find('.fa-search.dropdown-input-search').exists()).toBe(true);
+ });
+
+ it('renders clear search icon element', () => {
+ expect(wrapper.find('.fa-times.dropdown-input-clear.js-dropdown-input-clear').exists()).toBe(
+ true,
+ );
+ });
+
+ it('displays custom placeholder text', () => {
+ expect(findInputEl().attributes('placeholder')).toBe(defaultProps.placeholderText);
+ });
+
+ it('focuses input element when focused property equals true', () => {
+ const inputEl = findInputEl().element;
+
+ jest.spyOn(inputEl, 'focus');
+
+ wrapper.setProps({ focused: true });
+
+ expect(inputEl.focus).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/table_pagination_spec.js b/spec/frontend/vue_shared/components/table_pagination_spec.js
index 0a9ff36b2fb..8105d1fcef3 100644
--- a/spec/frontend/vue_shared/components/table_pagination_spec.js
+++ b/spec/frontend/vue_shared/components/table_pagination_spec.js
@@ -1,5 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
+import { GlPagination } from '@gitlab/ui';
describe('Pagination component', () => {
let wrapper;
@@ -12,15 +13,6 @@ describe('Pagination component', () => {
});
};
- const findFirstButtonLink = () => wrapper.find('.js-first-button .page-link');
- const findPreviousButton = () => wrapper.find('.js-previous-button');
- const findPreviousButtonLink = () => wrapper.find('.js-previous-button .page-link');
- const findNextButton = () => wrapper.find('.js-next-button');
- const findNextButtonLink = () => wrapper.find('.js-next-button .page-link');
- const findLastButtonLink = () => wrapper.find('.js-last-button .page-link');
- const findPages = () => wrapper.findAll('.page');
- const findSeparator = () => wrapper.find('.separator');
-
beforeEach(() => {
spy = jest.fn();
});
@@ -46,290 +38,54 @@ describe('Pagination component', () => {
expect(wrapper.isEmpty()).toBe(true);
});
- describe('prev button', () => {
- it('should be disabled and non clickable', () => {
- mountComponent({
- pageInfo: {
- nextPage: 2,
- page: 1,
- perPage: 20,
- previousPage: NaN,
- total: 84,
- totalPages: 5,
- },
- change: spy,
- });
-
- expect(findPreviousButton().classes()).toContain('disabled');
- findPreviousButtonLink().trigger('click');
- expect(spy).not.toHaveBeenCalled();
- });
-
- it('should be disabled and non clickable when total and totalPages are NaN', () => {
- mountComponent({
- pageInfo: {
- nextPage: 2,
- page: 1,
- perPage: 20,
- previousPage: NaN,
- total: NaN,
- totalPages: NaN,
- },
- change: spy,
- });
- expect(findPreviousButton().classes()).toContain('disabled');
- findPreviousButtonLink().trigger('click');
- expect(spy).not.toHaveBeenCalled();
- });
-
- it('should be enabled and clickable', () => {
- mountComponent({
- pageInfo: {
- nextPage: 3,
- page: 2,
- perPage: 20,
- previousPage: 1,
- total: 84,
- totalPages: 5,
- },
- change: spy,
- });
- findPreviousButtonLink().trigger('click');
- expect(spy).toHaveBeenCalledWith(1);
- });
-
- it('should be enabled and clickable when total and totalPages are NaN', () => {
- mountComponent({
- pageInfo: {
- nextPage: 3,
- page: 2,
- perPage: 20,
- previousPage: 1,
- total: NaN,
- totalPages: NaN,
- },
- change: spy,
- });
- findPreviousButtonLink().trigger('click');
- expect(spy).toHaveBeenCalledWith(1);
- });
- });
-
- describe('first button', () => {
- it('should call the change callback with the first page', () => {
- mountComponent({
- pageInfo: {
- nextPage: 3,
- page: 2,
- perPage: 20,
- previousPage: 1,
- total: 84,
- totalPages: 5,
- },
- change: spy,
- });
- const button = findFirstButtonLink();
- expect(button.text().trim()).toEqual('« First');
- button.trigger('click');
- expect(spy).toHaveBeenCalledWith(1);
- });
-
- it('should call the change callback with the first page when total and totalPages are NaN', () => {
- mountComponent({
- pageInfo: {
- nextPage: 3,
- page: 2,
- perPage: 20,
- previousPage: 1,
- total: NaN,
- totalPages: NaN,
- },
- change: spy,
- });
- const button = findFirstButtonLink();
- expect(button.text().trim()).toEqual('« First');
- button.trigger('click');
- expect(spy).toHaveBeenCalledWith(1);
- });
- });
-
- describe('last button', () => {
- it('should call the change callback with the last page', () => {
- mountComponent({
- pageInfo: {
- nextPage: 3,
- page: 2,
- perPage: 20,
- previousPage: 1,
- total: 84,
- totalPages: 5,
- },
- change: spy,
- });
- const button = findLastButtonLink();
- expect(button.text().trim()).toEqual('Last »');
- button.trigger('click');
- expect(spy).toHaveBeenCalledWith(5);
- });
-
- it('should not render', () => {
- mountComponent({
- pageInfo: {
- nextPage: 3,
- page: 2,
- perPage: 20,
- previousPage: 1,
- total: NaN,
- totalPages: NaN,
- },
- change: spy,
- });
- expect(findLastButtonLink().exists()).toBe(false);
- });
- });
-
- describe('next button', () => {
- it('should be disabled and non clickable', () => {
- mountComponent({
- pageInfo: {
- nextPage: NaN,
- page: 5,
- perPage: 20,
- previousPage: 4,
- total: 84,
- totalPages: 5,
- },
- change: spy,
- });
- expect(
- findNextButton()
- .text()
- .trim(),
- ).toEqual('Next ›');
- findNextButtonLink().trigger('click');
- expect(spy).not.toHaveBeenCalled();
- });
-
- it('should be disabled and non clickable when total and totalPages are NaN', () => {
- mountComponent({
- pageInfo: {
- nextPage: NaN,
- page: 5,
- perPage: 20,
- previousPage: 4,
- total: NaN,
- totalPages: NaN,
- },
- change: spy,
- });
- expect(
- findNextButton()
- .text()
- .trim(),
- ).toEqual('Next ›');
- findNextButtonLink().trigger('click');
- expect(spy).not.toHaveBeenCalled();
- });
-
- it('should be enabled and clickable', () => {
- mountComponent({
- pageInfo: {
- nextPage: 4,
- page: 3,
- perPage: 20,
- previousPage: 2,
- total: 84,
- totalPages: 5,
- },
- change: spy,
- });
- findNextButtonLink().trigger('click');
- expect(spy).toHaveBeenCalledWith(4);
+ it('renders if there is a next page', () => {
+ mountComponent({
+ pageInfo: {
+ nextPage: 2,
+ page: 1,
+ perPage: 20,
+ previousPage: NaN,
+ total: 15,
+ totalPages: 1,
+ },
+ change: spy,
});
- it('should be enabled and clickable when total and totalPages are NaN', () => {
- mountComponent({
- pageInfo: {
- nextPage: 4,
- page: 3,
- perPage: 20,
- previousPage: 2,
- total: NaN,
- totalPages: NaN,
- },
- change: spy,
- });
- findNextButtonLink().trigger('click');
- expect(spy).toHaveBeenCalledWith(4);
- });
+ expect(wrapper.isEmpty()).toBe(false);
});
- describe('numbered buttons', () => {
- it('should render 5 pages', () => {
- mountComponent({
- pageInfo: {
- nextPage: 4,
- page: 3,
- perPage: 20,
- previousPage: 2,
- total: 84,
- totalPages: 5,
- },
- change: spy,
- });
- expect(findPages().length).toEqual(5);
+ it('renders if there is a prev page', () => {
+ mountComponent({
+ pageInfo: {
+ nextPage: NaN,
+ page: 2,
+ perPage: 20,
+ previousPage: 1,
+ total: 15,
+ totalPages: 1,
+ },
+ change: spy,
});
- it('should not render any page', () => {
- mountComponent({
- pageInfo: {
- nextPage: 4,
- page: 3,
- perPage: 20,
- previousPage: 2,
- total: NaN,
- totalPages: NaN,
- },
- change: spy,
- });
- expect(findPages().length).toEqual(0);
- });
+ expect(wrapper.isEmpty()).toBe(false);
});
+ });
- describe('spread operator', () => {
- it('should render', () => {
- mountComponent({
- pageInfo: {
- nextPage: 4,
- page: 3,
- perPage: 20,
- previousPage: 2,
- total: 84,
- totalPages: 10,
- },
- change: spy,
- });
- expect(
- findSeparator()
- .text()
- .trim(),
- ).toEqual('...');
- });
-
- it('should not render', () => {
- mountComponent({
- pageInfo: {
- nextPage: 4,
- page: 3,
- perPage: 20,
- previousPage: 2,
- total: NaN,
- totalPages: NaN,
- },
- change: spy,
- });
- expect(findSeparator().exists()).toBe(false);
+ describe('events', () => {
+ it('calls change method when page changes', () => {
+ mountComponent({
+ pageInfo: {
+ nextPage: NaN,
+ page: 2,
+ perPage: 20,
+ previousPage: 1,
+ total: 15,
+ totalPages: 1,
+ },
+ change: spy,
});
+ wrapper.find(GlPagination).vm.$emit('input', 3);
+ expect(spy).toHaveBeenCalledWith(3);
});
});
});
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index 5d1a5fe1987..a3c51f24307 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -25,7 +25,7 @@ describe GitlabSchema.types['Project'] do
issue pipelines removeSourceBranchAfterMerge sentryDetailedError snippets
]
- is_expected.to have_graphql_fields(*expected_fields)
+ is_expected.to include_graphql_fields(*expected_fields)
end
describe 'issue field' do
diff --git a/spec/helpers/container_expiration_policies_helper_spec.rb b/spec/helpers/container_expiration_policies_helper_spec.rb
new file mode 100644
index 00000000000..3eb1234d82b
--- /dev/null
+++ b/spec/helpers/container_expiration_policies_helper_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ContainerExpirationPoliciesHelper do
+ describe '#keep_n_options' do
+ it 'returns keep_n options formatted for dropdown usage' do
+ expected_result = [
+ { key: 1, label: '1 tag per image name' },
+ { key: 5, label: '5 tags per image name' },
+ { key: 10, label: '10 tags per image name' },
+ { key: 25, label: '25 tags per image name' },
+ { key: 50, label: '50 tags per image name' },
+ { key: 100, label: '100 tags per image name' }
+ ]
+
+ expect(helper.keep_n_options).to eq(expected_result)
+ end
+ end
+
+ describe '#cadence_options' do
+ it 'returns cadence options formatted for dropdown usage' do
+ expected_result = [
+ { key: '1d', label: 'Every day' },
+ { key: '7d', label: 'Every week' },
+ { key: '14d', label: 'Every two weeks' },
+ { key: '1month', label: 'Every month' },
+ { key: '3month', label: 'Every three months' }
+ ]
+
+ expect(helper.cadence_options).to eq(expected_result)
+ end
+ end
+
+ describe '#older_than_options' do
+ it 'returns older_than options formatted for dropdown usage' do
+ expected_result = [
+ { key: '7d', label: '7 days until tags are automatically removed' },
+ { key: '14d', label: '14 days until tags are automatically removed' },
+ { key: '30d', label: '30 days until tags are automatically removed' },
+ { key: '90d', label: '90 days until tags are automatically removed' }
+ ]
+
+ expect(helper.older_than_options).to eq(expected_result)
+ end
+ end
+end
diff --git a/spec/helpers/nav_helper_spec.rb b/spec/helpers/nav_helper_spec.rb
index 882a125a0da..8d7572c5b5f 100644
--- a/spec/helpers/nav_helper_spec.rb
+++ b/spec/helpers/nav_helper_spec.rb
@@ -42,6 +42,7 @@ describe NavHelper, :do_not_mock_admin_mode do
context 'with admin mode enabled' do
before do
+ current_user_mode.request_admin_mode!
current_user_mode.enable_admin_mode!(password: user.password)
end
@@ -62,6 +63,7 @@ describe NavHelper, :do_not_mock_admin_mode do
context 'with admin mode enabled' do
before do
+ current_user_mode.request_admin_mode!
current_user_mode.enable_admin_mode!(password: user.password)
end
@@ -89,11 +91,18 @@ describe NavHelper, :do_not_mock_admin_mode do
end
end
- it 'returns only the sign in and search when the user is not logged in' do
- allow(helper).to receive(:current_user).and_return(nil)
- allow(helper).to receive(:can?).with(nil, :read_cross_project) { true }
+ context 'when the user is not logged in' do
+ let(:current_user_mode) { Gitlab::Auth::CurrentUserMode.new(nil) }
- expect(helper.header_links).to contain_exactly(:sign_in, :search)
+ before do
+ allow(helper).to receive(:current_user).and_return(nil)
+ allow(helper).to receive(:current_user_mode).and_return(current_user_mode)
+ allow(helper).to receive(:can?).with(nil, :read_cross_project) { true }
+ end
+
+ it 'returns only the sign in and search when the user is not logged in' do
+ expect(helper.header_links).to contain_exactly(:sign_in, :search)
+ end
end
end
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
index e2bbada3a49..29bdf05b8cf 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -83,7 +83,7 @@ describe('Pipelines table in Commits and Merge requests', function() {
};
vm.$nextTick(() => {
- vm.$el.querySelector('.js-next-button .page-link').click();
+ vm.$el.querySelector('.next-page-item').click();
expect(vm.updateContent).toHaveBeenCalledWith({ page: '2' });
done();
diff --git a/spec/javascripts/environments/environments_app_spec.js b/spec/javascripts/environments/environments_app_spec.js
index 9c8da4970f4..75526c2ba74 100644
--- a/spec/javascripts/environments/environments_app_spec.js
+++ b/spec/javascripts/environments/environments_app_spec.js
@@ -92,13 +92,13 @@ describe('Environment', () => {
describe('pagination', () => {
it('should render pagination', () => {
- expect(component.$el.querySelectorAll('.gl-pagination li').length).toEqual(5);
+ expect(component.$el.querySelectorAll('.gl-pagination li').length).toEqual(9);
});
it('should make an API request when page is clicked', done => {
spyOn(component, 'updateContent');
setTimeout(() => {
- component.$el.querySelector('.gl-pagination li:nth-child(5) .page-link').click();
+ component.$el.querySelector('.gl-pagination li:nth-child(3) .page-link').click();
expect(component.updateContent).toHaveBeenCalledWith({ scope: 'available', page: '2' });
done();
diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js
index d217bbb3078..6530201240f 100644
--- a/spec/javascripts/environments/folder/environments_folder_view_spec.js
+++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js
@@ -115,7 +115,9 @@ describe('Environments Folder View', () => {
it('should make an API request when changing page', done => {
spyOn(component, 'updateContent');
setTimeout(() => {
- component.$el.querySelector('.gl-pagination .js-last-button .page-link').click();
+ component.$el
+ .querySelector('.gl-pagination .page-item:nth-last-of-type(2) .page-link')
+ .click();
expect(component.updateContent).toHaveBeenCalledWith({
scope: component.scope,
diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js
index e1123cc7248..5cd91413c5f 100644
--- a/spec/javascripts/pipelines/pipelines_spec.js
+++ b/spec/javascripts/pipelines/pipelines_spec.js
@@ -446,7 +446,7 @@ describe('Pipelines', () => {
};
vm.$nextTick(() => {
- vm.$el.querySelector('.js-next-button .page-link').click();
+ vm.$el.querySelector('.next-page-item').click();
expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'all', page: '2' });
diff --git a/spec/javascripts/vue_shared/components/dropdown/dropdown_search_input_spec.js b/spec/javascripts/vue_shared/components/dropdown/dropdown_search_input_spec.js
deleted file mode 100644
index 456f310d10c..00000000000
--- a/spec/javascripts/vue_shared/components/dropdown/dropdown_search_input_spec.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import Vue from 'vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import dropdownSearchInputComponent from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
-
-const componentConfig = {
- placeholderText: 'Search something',
-};
-
-const createComponent = (config = componentConfig) => {
- const Component = Vue.extend(dropdownSearchInputComponent);
-
- return mountComponent(Component, config);
-};
-
-describe('DropdownSearchInputComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('template', () => {
- it('renders input element with type `search`', () => {
- const inputEl = vm.$el.querySelector('input.dropdown-input-field');
-
- expect(inputEl).not.toBeNull();
- expect(inputEl.getAttribute('type')).toBe('search');
- });
-
- it('renders search icon element', () => {
- expect(vm.$el.querySelector('.fa-search.dropdown-input-search')).not.toBeNull();
- });
-
- it('renders clear search icon element', () => {
- expect(
- vm.$el.querySelector('.fa-times.dropdown-input-clear.js-dropdown-input-clear'),
- ).not.toBeNull();
- });
-
- it('displays custom placeholder text', () => {
- const inputEl = vm.$el.querySelector('input.dropdown-input-field');
-
- expect(inputEl.getAttribute('placeholder')).toBe(componentConfig.placeholderText);
- });
- });
-});
diff --git a/spec/lib/gitlab/auth/user_auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb
index 125039edcf8..3d10f411310 100644
--- a/spec/lib/gitlab/auth/user_auth_finders_spec.rb
+++ b/spec/lib/gitlab/auth/auth_finders_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe Gitlab::Auth::UserAuthFinders do
+describe Gitlab::Auth::AuthFinders do
include described_class
let(:user) { create(:user) }
@@ -196,13 +196,13 @@ describe Gitlab::Auth::UserAuthFinders do
context 'when validate_access_token! returns valid' do
it 'returns user' do
- env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ env[described_class::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect(find_user_from_access_token).to eq user
end
it 'returns exception if token has no user' do
- env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ env[described_class::PRIVATE_TOKEN_HEADER] = personal_access_token.token
allow_any_instance_of(PersonalAccessToken).to receive(:user).and_return(nil)
expect { find_user_from_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
@@ -228,7 +228,7 @@ describe Gitlab::Auth::UserAuthFinders do
let(:personal_access_token) { create(:personal_access_token, user: user) }
before do
- env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ env[described_class::PRIVATE_TOKEN_HEADER] = personal_access_token.token
end
it 'returns exception if token has no user' do
@@ -279,7 +279,7 @@ describe Gitlab::Auth::UserAuthFinders do
context 'passed as header' do
it 'returns token if valid personal_access_token' do
- env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ env[described_class::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect(find_personal_access_token).to eq personal_access_token
end
@@ -287,7 +287,7 @@ describe Gitlab::Auth::UserAuthFinders do
context 'passed as param' do
it 'returns token if valid personal_access_token' do
- set_param(Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_PARAM, personal_access_token.token)
+ set_param(described_class::PRIVATE_TOKEN_PARAM, personal_access_token.token)
expect(find_personal_access_token).to eq personal_access_token
end
@@ -298,7 +298,7 @@ describe Gitlab::Auth::UserAuthFinders do
end
it 'returns exception if invalid personal_access_token' do
- env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = 'invalid_token'
+ env[described_class::PRIVATE_TOKEN_HEADER] = 'invalid_token'
expect { find_personal_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
end
@@ -379,4 +379,58 @@ describe Gitlab::Auth::UserAuthFinders do
end
end
end
+
+ describe '#find_runner_from_token' do
+ let(:runner) { create(:ci_runner) }
+
+ context 'with API requests' do
+ before do
+ env['SCRIPT_NAME'] = '/api/endpoint'
+ end
+
+ it 'returns the runner if token is valid' do
+ set_param(:token, runner.token)
+
+ expect(find_runner_from_token).to eq(runner)
+ end
+
+ it 'returns nil if token is not present' do
+ expect(find_runner_from_token).to be_nil
+ end
+
+ it 'returns nil if token is blank' do
+ set_param(:token, '')
+
+ expect(find_runner_from_token).to be_nil
+ end
+
+ it 'returns exception if invalid token' do
+ set_param(:token, 'invalid_token')
+
+ expect { find_runner_from_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ end
+ end
+
+ context 'without API requests' do
+ before do
+ env['SCRIPT_NAME'] = 'url.ics'
+ end
+
+ it 'returns nil if token is valid' do
+ set_param(:token, runner.token)
+
+ expect(find_runner_from_token).to be_nil
+ end
+
+ it 'returns nil if token is blank' do
+ expect(find_runner_from_token).to be_nil
+ end
+
+ it 'returns nil if invalid token' do
+ set_param(:token, 'invalid_token')
+
+ expect(find_runner_from_token).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/auth/current_user_mode_spec.rb b/spec/lib/gitlab/auth/current_user_mode_spec.rb
index b93d460cf48..3b3db0f7315 100644
--- a/spec/lib/gitlab/auth/current_user_mode_spec.rb
+++ b/spec/lib/gitlab/auth/current_user_mode_spec.rb
@@ -62,69 +62,90 @@ describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode do
context 'when the user is an admin' do
let(:user) { build(:user, :admin) }
- it 'is false by default' do
- expect(subject.admin_mode?).to be(false)
- end
-
- it 'cannot be enabled with an invalid password' do
- subject.enable_admin_mode!(password: nil)
-
- expect(subject.admin_mode?).to be(false)
- end
+ context 'when admin mode not requested' do
+ it 'is false by default' do
+ expect(subject.admin_mode?).to be(false)
+ end
- it 'can be enabled with a valid password' do
- subject.enable_admin_mode!(password: user.password)
+ it 'raises exception if we try to enable it' do
+ expect do
+ subject.enable_admin_mode!(password: user.password)
+ end.to raise_error(::Gitlab::Auth::CurrentUserMode::NotRequestedError)
- expect(subject.admin_mode?).to be(true)
+ expect(subject.admin_mode?).to be(false)
+ end
end
- it 'can be disabled' do
- subject.enable_admin_mode!(password: user.password)
- subject.disable_admin_mode!
-
- expect(subject.admin_mode?).to be(false)
- end
+ context 'when admin mode requested first' do
+ before do
+ subject.request_admin_mode!
+ end
- it 'will expire in the future' do
- subject.enable_admin_mode!(password: user.password)
- expect(subject.admin_mode?).to be(true), 'admin mode is not active in the present'
+ it 'is false by default' do
+ expect(subject.admin_mode?).to be(false)
+ end
- Timecop.freeze(Gitlab::Auth::CurrentUserMode::MAX_ADMIN_MODE_TIME.from_now) do
- # in the future this will be a new request, simulate by clearing the RequestStore
- Gitlab::SafeRequestStore.clear!
+ it 'cannot be enabled with an invalid password' do
+ subject.enable_admin_mode!(password: nil)
- expect(subject.admin_mode?).to be(false), 'admin mode did not expire in the future'
+ expect(subject.admin_mode?).to be(false)
end
- end
- context 'skipping password validation' do
it 'can be enabled with a valid password' do
- subject.enable_admin_mode!(password: user.password, skip_password_validation: true)
+ subject.enable_admin_mode!(password: user.password)
expect(subject.admin_mode?).to be(true)
end
- it 'can be enabled with an invalid password' do
- subject.enable_admin_mode!(skip_password_validation: true)
+ it 'can be disabled' do
+ subject.enable_admin_mode!(password: user.password)
+ subject.disable_admin_mode!
- expect(subject.admin_mode?).to be(true)
+ expect(subject.admin_mode?).to be(false)
end
- end
- context 'with two independent sessions' do
- let(:another_session) { {} }
- let(:another_subject) { described_class.new(user) }
+ it 'will expire in the future' do
+ subject.enable_admin_mode!(password: user.password)
+ expect(subject.admin_mode?).to be(true), 'admin mode is not active in the present'
- before do
- allow(ActiveSession).to receive(:list_sessions).with(user).and_return([session, another_session])
+ Timecop.freeze(Gitlab::Auth::CurrentUserMode::MAX_ADMIN_MODE_TIME.from_now) do
+ # in the future this will be a new request, simulate by clearing the RequestStore
+ Gitlab::SafeRequestStore.clear!
+
+ expect(subject.admin_mode?).to be(false), 'admin mode did not expire in the future'
+ end
end
- it 'can be enabled in one and seen in the other' do
- Gitlab::Session.with_session(another_session) do
- another_subject.enable_admin_mode!(password: user.password)
+ context 'skipping password validation' do
+ it 'can be enabled with a valid password' do
+ subject.enable_admin_mode!(password: user.password, skip_password_validation: true)
+
+ expect(subject.admin_mode?).to be(true)
end
- expect(subject.admin_mode?).to be(true)
+ it 'can be enabled with an invalid password' do
+ subject.enable_admin_mode!(skip_password_validation: true)
+
+ expect(subject.admin_mode?).to be(true)
+ end
+ end
+
+ context 'with two independent sessions' do
+ let(:another_session) { {} }
+ let(:another_subject) { described_class.new(user) }
+
+ before do
+ allow(ActiveSession).to receive(:list_sessions).with(user).and_return([session, another_session])
+ end
+
+ it 'can be enabled in one and seen in the other' do
+ Gitlab::Session.with_session(another_session) do
+ another_subject.request_admin_mode!
+ another_subject.enable_admin_mode!(password: user.password)
+ end
+
+ expect(subject.admin_mode?).to be(true)
+ end
end
end
end
@@ -134,16 +155,28 @@ describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode do
let(:user) { build(:user, :admin) }
it 'creates a timestamp in the session' do
+ subject.request_admin_mode!
subject.enable_admin_mode!(password: user.password)
expect(session).to include(expected_session_entry(be_within(1.second).of Time.now))
end
end
+ describe '#enable_sessionless_admin_mode!' do
+ let(:user) { build(:user, :admin) }
+
+ it 'enabled admin mode without password' do
+ subject.enable_sessionless_admin_mode!
+
+ expect(subject.admin_mode?).to be(true)
+ end
+ end
+
describe '#disable_admin_mode!' do
let(:user) { build(:user, :admin) }
it 'sets the session timestamp to nil' do
+ subject.request_admin_mode!
subject.disable_admin_mode!
expect(session).to include(expected_session_entry(be_nil))
diff --git a/spec/lib/gitlab/auth/request_authenticator_spec.rb b/spec/lib/gitlab/auth/request_authenticator_spec.rb
index f7fff389d88..4dbcd0df302 100644
--- a/spec/lib/gitlab/auth/request_authenticator_spec.rb
+++ b/spec/lib/gitlab/auth/request_authenticator_spec.rb
@@ -66,4 +66,28 @@ describe Gitlab::Auth::RequestAuthenticator do
expect(subject.find_sessionless_user([:api])).to be_blank
end
end
+
+ describe '#runner' do
+ let!(:runner) { build(:ci_runner) }
+
+ it 'returns the runner using #find_runner_from_token' do
+ expect_any_instance_of(described_class)
+ .to receive(:find_runner_from_token)
+ .and_return(runner)
+
+ expect(subject.runner).to eq runner
+ end
+
+ it 'returns nil if no runner is found' do
+ expect(subject.runner).to be_blank
+ end
+
+ it 'rescue Gitlab::Auth::AuthenticationError exceptions' do
+ expect_any_instance_of(described_class)
+ .to receive(:find_runner_from_token)
+ .and_raise(Gitlab::Auth::UnauthorizedError)
+
+ expect(subject.runner).to be_blank
+ end
+ end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 26793f28bd8..8d436fb28e0 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -443,6 +443,7 @@ project:
- downstream_project_subscriptions
- service_desk_setting
- import_failures
+- container_expiration_policy
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index fa6bf14bf64..bf8c079f027 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -773,3 +773,13 @@ ZoomMeeting:
ServiceDeskSetting:
- project_id
- issue_template_key
+ContainerExpirationPolicy:
+- created_at
+- updated_at
+- next_run_at
+- project_id
+- name_regex
+- cadence
+- older_than
+- keep_n
+- enabled
diff --git a/spec/models/container_expiration_policy_spec.rb b/spec/models/container_expiration_policy_spec.rb
new file mode 100644
index 00000000000..1ce76490448
--- /dev/null
+++ b/spec/models/container_expiration_policy_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ContainerExpirationPolicy, type: :model do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+
+ describe '#enabled' do
+ it { is_expected.to allow_value(true).for(:enabled) }
+ it { is_expected.to allow_value(false).for(:enabled) }
+ it { is_expected.not_to allow_value(nil).for(:enabled) }
+ end
+
+ describe '#cadence' do
+ it { is_expected.to validate_presence_of(:cadence) }
+
+ it { is_expected.to allow_value('1d').for(:cadence) }
+ it { is_expected.to allow_value('1month').for(:cadence) }
+ it { is_expected.not_to allow_value('123asdf').for(:cadence) }
+ it { is_expected.not_to allow_value(nil).for(:cadence) }
+ end
+
+ describe '#older_than' do
+ it { is_expected.to allow_value('7d').for(:older_than) }
+ it { is_expected.to allow_value('14d').for(:older_than) }
+ it { is_expected.to allow_value(nil).for(:older_than) }
+ it { is_expected.not_to allow_value('123asdf').for(:older_than) }
+ end
+
+ describe '#keep_n' do
+ it { is_expected.to allow_value(10).for(:keep_n) }
+ it { is_expected.to allow_value(nil).for(:keep_n) }
+ it { is_expected.not_to allow_value('foo').for(:keep_n) }
+ end
+ end
+end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index c93e6aafd75..3089afd8d8a 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -199,6 +199,13 @@ describe Namespace do
expect(described_class.find_by_pages_host(host)).to eq(namespace)
end
+
+ it "returns no result if the provided host is not subdomain of the Pages host" do
+ create(:namespace, name: 'namespace.io')
+ host = "namespace.io"
+
+ expect(described_class.find_by_pages_host(host)).to eq(nil)
+ end
end
describe '#ancestors_upto' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index e7abdf847e1..feb06f4ffc9 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -62,6 +62,7 @@ describe Project do
it { is_expected.to have_one(:external_wiki_service) }
it { is_expected.to have_one(:project_feature) }
it { is_expected.to have_one(:project_repository) }
+ it { is_expected.to have_one(:container_expiration_policy) }
it { is_expected.to have_one(:statistics).class_name('ProjectStatistics') }
it { is_expected.to have_one(:import_data).class_name('ProjectImportData') }
it { is_expected.to have_one(:last_event).class_name('Event') }
@@ -137,6 +138,13 @@ describe Project do
expect(project.ci_cd_settings).to be_persisted
end
+ it 'automatically creates a container expiration policy row' do
+ project = create(:project)
+
+ expect(project.container_expiration_policy).to be_an_instance_of(ContainerExpirationPolicy)
+ expect(project.container_expiration_policy).to be_persisted
+ end
+
it 'automatically creates a Pages metadata row' do
project = create(:project)
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 4f9dfbd9103..9e8aa6a95e8 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -2839,6 +2839,7 @@ describe User, :do_not_mock_admin_mode do
context 'when admin mode is enabled' do
before do
+ Gitlab::Auth::CurrentUserMode.new(user).request_admin_mode!
Gitlab::Auth::CurrentUserMode.new(user).enable_admin_mode!(password: user.password)
end
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index bbfe40041a1..0c53c04ba40 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -146,13 +146,13 @@ describe API::Helpers do
let(:personal_access_token) { create(:personal_access_token, user: user) }
it "returns a 401 response for an invalid token" do
- env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = 'invalid token'
+ env[Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER] = 'invalid token'
expect { current_user }.to raise_error /401/
end
it "returns a 403 response for a user without access" do
- env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ env[Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
expect { current_user }.to raise_error /403/
@@ -160,7 +160,7 @@ describe API::Helpers do
it 'returns a 403 response for a user who is blocked' do
user.block!
- env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ env[Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect { current_user }.to raise_error /403/
end
@@ -168,7 +168,7 @@ describe API::Helpers do
context 'when terms are enforced' do
before do
enforce_terms
- env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ env[Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
end
it 'returns a 403 when a user has not accepted the terms' do
@@ -183,27 +183,27 @@ describe API::Helpers do
end
it "sets current_user" do
- env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ env[Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect(current_user).to eq(user)
end
it "does not allow tokens without the appropriate scope" do
personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user'])
- env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ env[Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect { current_user }.to raise_error Gitlab::Auth::InsufficientScopeError
end
it 'does not allow revoked tokens' do
personal_access_token.revoke!
- env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ env[Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect { current_user }.to raise_error Gitlab::Auth::RevokedError
end
it 'does not allow expired tokens' do
personal_access_token.update!(expires_at: 1.day.ago)
- env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ env[Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect { current_user }.to raise_error Gitlab::Auth::ExpiredError
end
@@ -213,7 +213,7 @@ describe API::Helpers do
before do
stub_config_setting(impersonation_enabled: false)
- env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ env[Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
end
it 'does not allow impersonation tokens' do
@@ -478,7 +478,7 @@ describe API::Helpers do
context 'passed as param' do
before do
- set_param(Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_PARAM, token.token)
+ set_param(Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_PARAM, token.token)
end
it_behaves_like 'sudo'
@@ -486,7 +486,7 @@ describe API::Helpers do
context 'passed as header' do
before do
- env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = token.token
+ env[Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER] = token.token
end
it_behaves_like 'sudo'
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index 59113687d1c..9e62d84d313 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -100,6 +100,18 @@ describe 'Rack Attack global throttles' do
end
end
+ context 'when the request is authenticated by a runner token' do
+ let(:request_jobs_url) { '/api/v4/jobs/request' }
+ let(:runner) { create(:ci_runner) }
+
+ it 'does not cont as unauthenticated' do
+ (1 + requests_per_period).times do
+ post request_jobs_url, params: { token: runner.token }
+ expect(response).to have_http_status 204
+ end
+ end
+ end
+
it 'logs RackAttack info into structured logs' do
requests_per_period.times do
get url_that_does_not_require_authentication
diff --git a/spec/support/helpers/admin_mode_helpers.rb b/spec/support/helpers/admin_mode_helpers.rb
index de8ffe40536..e995a7d4f5e 100644
--- a/spec/support/helpers/admin_mode_helpers.rb
+++ b/spec/support/helpers/admin_mode_helpers.rb
@@ -3,7 +3,7 @@
# Helper for enabling admin mode in tests
module AdminModeHelper
- # Users are logged in by default in user mode and have to switch to admin
+ # Administrators are logged in by default in user mode and have to switch to admin
# mode for accessing any administrative functionality. This helper lets a user
# be in admin mode without requiring a second authentication step (provided
# the user is an admin)
diff --git a/spec/support/matchers/graphql_matchers.rb b/spec/support/matchers/graphql_matchers.rb
index ebba5d8a73c..dbf457a9200 100644
--- a/spec/support/matchers/graphql_matchers.rb
+++ b/spec/support/matchers/graphql_matchers.rb
@@ -28,6 +28,19 @@ RSpec::Matchers.define :have_graphql_fields do |*expected|
end
end
+RSpec::Matchers.define :include_graphql_fields do |*expected|
+ expected_field_names = expected.map { |name| GraphqlHelpers.fieldnamerize(name) }
+
+ match do |kls|
+ expect(kls.fields.keys).to include(*expected_field_names)
+ end
+
+ failure_message do |kls|
+ missing = expected_field_names - kls.fields.keys
+ "is missing fields: <#{missing.inspect}>" if missing.any?
+ end
+end
+
RSpec::Matchers.define :have_graphql_field do |field_name, args = {}|
match do |kls|
field = kls.fields[GraphqlHelpers.fieldnamerize(field_name)]
diff --git a/spec/views/admin/sessions/new.html.haml_spec.rb b/spec/views/admin/sessions/new.html.haml_spec.rb
index 57255748988..b3208296c80 100644
--- a/spec/views/admin/sessions/new.html.haml_spec.rb
+++ b/spec/views/admin/sessions/new.html.haml_spec.rb
@@ -3,29 +3,44 @@
require 'spec_helper'
describe 'admin/sessions/new.html.haml' do
- context 'admin has password set' do
- before do
- allow(view).to receive(:password_authentication_enabled_for_web?).and_return(true)
- end
+ let(:user) { create(:admin) }
+
+ before do
+ allow(view).to receive(:current_user).and_return(user)
+ allow(view).to receive(:omniauth_enabled?).and_return(false)
+ end
- it "shows enter password form" do
+ context 'internal admin user' do
+ it 'shows enter password form' do
render
expect(rendered).to have_css('#login-pane.active')
expect(rendered).to have_selector('input[name="password"]')
end
+
+ it 'warns authentication not possible if password not set' do
+ allow(user).to receive(:require_password_creation_for_web?).and_return(true)
+
+ render
+
+ expect(rendered).not_to have_css('#login-pane')
+ expect(rendered).to have_content _('No authentication methods configured.')
+ end
end
- context 'admin has no password set' do
+ context 'omniauth authentication enabled' do
before do
- allow(view).to receive(:password_authentication_enabled_for_web?).and_return(false)
+ allow(view).to receive(:omniauth_enabled?).and_return(true)
+ allow(view).to receive(:button_based_providers_enabled?).and_return(true)
end
- it "warns authentication not possible" do
+ it 'shows omniauth form' do
render
- expect(rendered).not_to have_css('#login-pane')
- expect(rendered).to have_content 'No authentication methods configured'
+ expect(rendered).to have_css('.omniauth-container')
+ expect(rendered).to have_content _('Sign in with')
+
+ expect(rendered).not_to have_content _('No authentication methods configured.')
end
end
end
diff --git a/spec/views/layouts/application.html.haml_spec.rb b/spec/views/layouts/application.html.haml_spec.rb
index bdd4a97a1f5..4270bbf1924 100644
--- a/spec/views/layouts/application.html.haml_spec.rb
+++ b/spec/views/layouts/application.html.haml_spec.rb
@@ -11,6 +11,7 @@ describe 'layouts/application' do
allow(view).to receive(:session).and_return({})
allow(view).to receive(:user_signed_in?).and_return(true)
allow(view).to receive(:current_user).and_return(user)
+ allow(view).to receive(:current_user_mode).and_return(Gitlab::Auth::CurrentUserMode.new(user))
end
context 'body data elements for pageview context' do