diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-12-11 12:08:10 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-12-11 12:08:10 +0000 |
commit | b86f474bf51e20d2db4cf0895d0a8e0894e31c08 (patch) | |
tree | 061d2a4c749924f5a35fe6199dd1d8982c4b0b27 /app | |
parent | 6b8040dc25fdc5fe614c3796a147517dd50bc7d8 (diff) | |
download | gitlab-ce-b86f474bf51e20d2db4cf0895d0a8e0894e31c08.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
39 files changed, 241 insertions, 157 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.') |