diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-07 21:09:27 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-07 21:09:27 +0300 |
commit | 5cda8c8a420399ca9687c4a981fefd50ce5a1fdd (patch) | |
tree | 6050d7517a36798c9586e153df20a0696c5fcd4f /app | |
parent | 7bbc731c75d0b8bf7c74ba77d521266d2ed0a1fc (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
14 files changed, 206 insertions, 92 deletions
diff --git a/app/assets/javascripts/access_tokens/components/access_token_table_app.vue b/app/assets/javascripts/access_tokens/components/access_token_table_app.vue index e936ad8aa14..5fe285c0896 100644 --- a/app/assets/javascripts/access_tokens/components/access_token_table_app.vue +++ b/app/assets/javascripts/access_tokens/components/access_token_table_app.vue @@ -1,24 +1,23 @@ <script> -import { GlButton, GlIcon, GlLink, GlTable, GlTooltipDirective } from '@gitlab/ui'; +import { GlButton, GlIcon, GlLink, GlPagination, GlTable, GlTooltipDirective } from '@gitlab/ui'; import { helpPagePath } from '~/helpers/help_page_helper'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import { __, s__, sprintf } from '~/locale'; +import { __, sprintf } from '~/locale'; import DomElementListener from '~/vue_shared/components/dom_element_listener.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import UserDate from '~/vue_shared/components/user_date.vue'; - -const FORM_SELECTOR = '#js-new-access-token-form'; -const SUCCESS_EVENT = 'ajax:success'; +import { EVENT_SUCCESS, FIELDS, FORM_SELECTOR, INITIAL_PAGE, PAGE_SIZE } from './constants'; export default { - FORM_SELECTOR, - SUCCESS_EVENT, + EVENT_SUCCESS, + PAGE_SIZE, name: 'AccessTokenTableApp', components: { DomElementListener, GlButton, GlIcon, GlLink, + GlPagination, GlTable, TimeAgoTooltip, UserDate, @@ -39,58 +38,6 @@ export default { revokeButton: __('Revoke'), tokenValidity: __('Token valid until revoked'), }, - fields: [ - { - key: 'name', - label: __('Token name'), - sortable: true, - tdClass: `gl-text-black-normal`, - thClass: `gl-text-black-normal`, - }, - { - formatter(scopes) { - return scopes?.length ? scopes.join(', ') : __('no scopes selected'); - }, - key: 'scopes', - label: __('Scopes'), - sortable: true, - tdClass: `gl-text-black-normal`, - thClass: `gl-text-black-normal`, - }, - { - key: 'createdAt', - label: s__('AccessTokens|Created'), - sortable: true, - tdClass: `gl-text-black-normal`, - thClass: `gl-text-black-normal`, - }, - { - key: 'lastUsedAt', - label: __('Last Used'), - sortable: true, - tdClass: `gl-text-black-normal`, - thClass: `gl-text-black-normal`, - }, - { - key: 'expiresAt', - label: __('Expires'), - sortable: true, - tdClass: `gl-text-black-normal`, - thClass: `gl-text-black-normal`, - }, - { - key: 'role', - label: __('Role'), - tdClass: `gl-text-black-normal`, - thClass: `gl-text-black-normal`, - sortable: true, - }, - { - key: 'action', - label: __('Action'), - thClass: `gl-text-black-normal`, - }, - ], inject: [ 'accessTokenType', 'accessTokenTypePlural', @@ -101,13 +48,15 @@ export default { data() { return { activeAccessTokens: this.initialActiveAccessTokens, + currentPage: INITIAL_PAGE, }; }, computed: { filteredFields() { - return this.showRole - ? this.$options.fields - : this.$options.fields.filter((field) => field.key !== 'role'); + return this.showRole ? FIELDS : FIELDS.filter((field) => field.key !== 'role'); + }, + formSelector() { + return `#${FORM_SELECTOR}`; }, header() { return sprintf(this.$options.i18n.header, { @@ -120,11 +69,15 @@ export default { accessTokenType: this.accessTokenType, }); }, + showPagination() { + return this.activeAccessTokens.length > PAGE_SIZE; + }, }, methods: { onSuccess(event) { const [{ active_access_tokens: activeAccessTokens }] = event.detail; this.activeAccessTokens = convertObjectPropsToCamelCase(activeAccessTokens, { deep: true }); + this.currentPage = INITIAL_PAGE; }, sortingChanged(aRow, bRow, key) { if (['createdAt', 'lastUsedAt', 'expiresAt'].includes(key)) { @@ -144,7 +97,7 @@ export default { </script> <template> - <dom-element-listener :selector="$options.FORM_SELECTOR" @[$options.SUCCESS_EVENT]="onSuccess"> + <dom-element-listener :selector="formSelector" @[$options.EVENT_SUCCESS]="onSuccess"> <div> <hr /> <h5>{{ header }}</h5> @@ -154,6 +107,8 @@ export default { :empty-text="noActiveTokensMessage" :fields="filteredFields" :items="activeAccessTokens" + :per-page="$options.PAGE_SIZE" + :current-page="currentPage" :sort-compare="sortingChanged" show-empty > @@ -199,6 +154,17 @@ export default { /> </template> </gl-table> + <gl-pagination + v-if="showPagination" + v-model="currentPage" + :per-page="$options.PAGE_SIZE" + :total-items="activeAccessTokens.length" + :prev-text="__('Prev')" + :next-text="__('Next')" + :label-next-page="__('Go to next page')" + :label-prev-page="__('Go to previous page')" + align="center" + /> </div> </dom-element-listener> </template> diff --git a/app/assets/javascripts/access_tokens/components/constants.js b/app/assets/javascripts/access_tokens/components/constants.js new file mode 100644 index 00000000000..197f20ae24c --- /dev/null +++ b/app/assets/javascripts/access_tokens/components/constants.js @@ -0,0 +1,61 @@ +import { __, s__ } from '~/locale'; + +export const EVENT_ERROR = 'ajax:error'; +export const EVENT_SUCCESS = 'ajax:success'; +export const FORM_SELECTOR = 'js-new-access-token-form'; + +export const INITIAL_PAGE = 1; +export const PAGE_SIZE = 100; + +export const FIELDS = [ + { + key: 'name', + label: __('Token name'), + sortable: true, + tdClass: `gl-text-black-normal`, + thClass: `gl-text-black-normal`, + }, + { + formatter(scopes) { + return scopes?.length ? scopes.join(', ') : __('no scopes selected'); + }, + key: 'scopes', + label: __('Scopes'), + sortable: true, + tdClass: `gl-text-black-normal`, + thClass: `gl-text-black-normal`, + }, + { + key: 'createdAt', + label: s__('AccessTokens|Created'), + sortable: true, + tdClass: `gl-text-black-normal`, + thClass: `gl-text-black-normal`, + }, + { + key: 'lastUsedAt', + label: __('Last Used'), + sortable: true, + tdClass: `gl-text-black-normal`, + thClass: `gl-text-black-normal`, + }, + { + key: 'expiresAt', + label: __('Expires'), + sortable: true, + tdClass: `gl-text-black-normal`, + thClass: `gl-text-black-normal`, + }, + { + key: 'role', + label: __('Role'), + tdClass: `gl-text-black-normal`, + thClass: `gl-text-black-normal`, + sortable: true, + }, + { + key: 'action', + label: __('Action'), + thClass: `gl-text-black-normal`, + }, +]; diff --git a/app/assets/javascripts/access_tokens/components/new_access_token_app.vue b/app/assets/javascripts/access_tokens/components/new_access_token_app.vue index 5aeabcefad5..a34f3c7dedf 100644 --- a/app/assets/javascripts/access_tokens/components/new_access_token_app.vue +++ b/app/assets/javascripts/access_tokens/components/new_access_token_app.vue @@ -4,15 +4,11 @@ import { createAlert, VARIANT_INFO } from '~/flash'; import { __, n__, sprintf } from '~/locale'; import DomElementListener from '~/vue_shared/components/dom_element_listener.vue'; import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue'; - -const ERROR_EVENT = 'ajax:error'; -const FORM_SELECTOR = '#js-new-access-token-form'; -const SUCCESS_EVENT = 'ajax:success'; +import { EVENT_ERROR, EVENT_SUCCESS, FORM_SELECTOR } from './constants'; export default { - ERROR_EVENT, - FORM_SELECTOR, - SUCCESS_EVENT, + EVENT_ERROR, + EVENT_SUCCESS, name: 'NewAccessTokenApp', components: { DomElementListener, GlAlert, InputCopyToggleVisibility }, i18n: { @@ -50,13 +46,16 @@ export default { name: this.$options.tokenInputId, }; }, + formSelector() { + return `#${FORM_SELECTOR}`; + }, label() { return sprintf(this.$options.i18n.label, { accessTokenType: this.accessTokenType }); }, }, mounted() { /** @type {HTMLFormElement} */ - this.form = document.querySelector(this.$options.FORM_SELECTOR); + this.form = document.getElementById(FORM_SELECTOR); /** @type {HTMLInputElement} */ this.submitButton = this.form.querySelector('input[type=submit]'); @@ -93,9 +92,9 @@ export default { <template> <dom-element-listener - :selector="$options.FORM_SELECTOR" - @[$options.ERROR_EVENT]="onError" - @[$options.SUCCESS_EVENT]="onSuccess" + :selector="formSelector" + @[$options.EVENT_ERROR]="onError" + @[$options.EVENT_SUCCESS]="onSuccess" > <div ref="container"> <template v-if="newToken"> diff --git a/app/assets/javascripts/pages/admin/impersonation_tokens/index.js b/app/assets/javascripts/pages/admin/impersonation_tokens/index.js index 8fbc8dc17bc..d86ac891977 100644 --- a/app/assets/javascripts/pages/admin/impersonation_tokens/index.js +++ b/app/assets/javascripts/pages/admin/impersonation_tokens/index.js @@ -1,8 +1,14 @@ -import { initExpiresAtField } from '~/access_tokens'; +import { + initAccessTokenTableApp, + initExpiresAtField, + initNewAccessTokenApp, +} from '~/access_tokens'; import { initAdminUserActions, initDeleteUserModals } from '~/admin/users'; import initConfirmModal from '~/confirm_modal'; +initAccessTokenTableApp(); +initExpiresAtField(); +initNewAccessTokenApp(); initAdminUserActions(); initDeleteUserModals(); -initExpiresAtField(); initConfirmModal(); diff --git a/app/assets/javascripts/pages/groups/settings/access_tokens/index.js b/app/assets/javascripts/pages/groups/settings/access_tokens/index.js index dc1bb88bf4b..b9f282a123c 100644 --- a/app/assets/javascripts/pages/groups/settings/access_tokens/index.js +++ b/app/assets/javascripts/pages/groups/settings/access_tokens/index.js @@ -1,3 +1,9 @@ -import { initExpiresAtField } from '~/access_tokens'; +import { + initAccessTokenTableApp, + initExpiresAtField, + initNewAccessTokenApp, +} from '~/access_tokens'; +initAccessTokenTableApp(); initExpiresAtField(); +initNewAccessTokenApp(); diff --git a/app/assets/javascripts/pages/projects/settings/access_tokens/index.js b/app/assets/javascripts/pages/projects/settings/access_tokens/index.js index dc1bb88bf4b..b9f282a123c 100644 --- a/app/assets/javascripts/pages/projects/settings/access_tokens/index.js +++ b/app/assets/javascripts/pages/projects/settings/access_tokens/index.js @@ -1,3 +1,9 @@ -import { initExpiresAtField } from '~/access_tokens'; +import { + initAccessTokenTableApp, + initExpiresAtField, + initNewAccessTokenApp, +} from '~/access_tokens'; +initAccessTokenTableApp(); initExpiresAtField(); +initNewAccessTokenApp(); diff --git a/app/models/hooks/web_hook_log.rb b/app/models/hooks/web_hook_log.rb index a95dd0473b6..2f03b3591cf 100644 --- a/app/models/hooks/web_hook_log.rb +++ b/app/models/hooks/web_hook_log.rb @@ -7,6 +7,8 @@ class WebHookLog < ApplicationRecord include CreatedAtFilterable include PartitionedTable + OVERSIZE_REQUEST_DATA = { 'oversize' => true }.freeze + self.primary_key = :id partitioned_by :created_at, strategy: :monthly, retain_for: 3.months @@ -41,6 +43,10 @@ class WebHookLog < ApplicationRecord response_status == WebHookService::InternalErrorResponse::ERROR_MESSAGE end + def oversize? + request_data == OVERSIZE_REQUEST_DATA + end + private def obfuscate_basic_auth diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb index b3729c84dd6..96094a33e64 100644 --- a/app/models/users/callout.rb +++ b/app/models/users/callout.rb @@ -51,12 +51,15 @@ module Users attention_requests_side_nav: 48, minute_limit_banner: 49, preview_user_over_limit_free_plan_alert: 50, # EE-only - user_reached_limit_free_plan_alert: 51 # EE-only + user_reached_limit_free_plan_alert: 51, # EE-only + submit_license_usage_data_banner: 52 # EE-only } validates :feature_name, presence: true, uniqueness: { scope: :user_id }, inclusion: { in: Users::Callout.feature_names.keys } + + scope :with_feature_name, -> (feature_name) { where(feature_name: feature_name) } end end diff --git a/app/services/ci/job_artifacts/destroy_all_expired_service.rb b/app/services/ci/job_artifacts/destroy_all_expired_service.rb index 4070875ffe1..b5dd5b843c6 100644 --- a/app/services/ci/job_artifacts/destroy_all_expired_service.rb +++ b/app/services/ci/job_artifacts/destroy_all_expired_service.rb @@ -60,7 +60,7 @@ module Ci end def destroy_batch(artifacts) - Ci::JobArtifacts::DestroyBatchService.new(artifacts).execute + Ci::JobArtifacts::DestroyBatchService.new(artifacts, skip_projects_on_refresh: true).execute end def loop_timeout? diff --git a/app/services/ci/job_artifacts/destroy_batch_service.rb b/app/services/ci/job_artifacts/destroy_batch_service.rb index e8d21f14ee6..49b65f13804 100644 --- a/app/services/ci/job_artifacts/destroy_batch_service.rb +++ b/app/services/ci/job_artifacts/destroy_batch_service.rb @@ -17,15 +17,20 @@ module Ci # +pick_up_at+:: When to pick up for deletion of files # Returns: # +Hash+:: A hash with status and destroyed_artifacts_count keys - def initialize(job_artifacts, pick_up_at: nil, fix_expire_at: fix_expire_at?) + def initialize(job_artifacts, pick_up_at: nil, fix_expire_at: fix_expire_at?, skip_projects_on_refresh: false) @job_artifacts = job_artifacts.with_destroy_preloads.to_a @pick_up_at = pick_up_at @fix_expire_at = fix_expire_at + @skip_projects_on_refresh = skip_projects_on_refresh end # rubocop: disable CodeReuse/ActiveRecord def execute(update_stats: true) - track_artifacts_undergoing_stats_refresh + if @skip_projects_on_refresh + exclude_artifacts_undergoing_stats_refresh + else + track_artifacts_undergoing_stats_refresh + end # Detect and fix artifacts that had `expire_at` wrongly backfilled by migration # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47723 @@ -169,6 +174,21 @@ module Ci ) end end + + def exclude_artifacts_undergoing_stats_refresh + project_ids = Set.new + + @job_artifacts.reject! do |artifact| + next unless artifact.project.refreshing_build_artifacts_size? + + project_ids << artifact.project_id + end + + Gitlab::ProjectStatsRefreshConflictsLogger.warn_skipped_artifact_deletion_during_stats_refresh( + method: 'Ci::JobArtifacts::DestroyBatchService#execute', + project_ids: project_ids + ) + end end end end diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb index c0727e52cc3..6526e6a4c5e 100644 --- a/app/services/web_hook_service.rb +++ b/app/services/web_hook_service.rb @@ -26,6 +26,12 @@ class WebHookService end REQUEST_BODY_SIZE_LIMIT = 25.megabytes + # Response body is for UI display only. It does not make much sense to save + # whatever the receivers throw back at us + RESPONSE_BODY_SIZE_LIMIT = 8.kilobytes + # The headers are for debugging purpose. They are displayed on the UI only. + RESPONSE_HEADERS_COUNT_LIMIT = 50 + RESPONSE_HEADERS_SIZE_LIMIT = 1.kilobytes attr_accessor :hook, :data, :hook_name, :request_options attr_reader :uniqueness_token @@ -141,7 +147,7 @@ class WebHookService execution_duration: execution_duration, request_headers: build_headers, request_data: data, - response_headers: format_response_headers(response), + response_headers: safe_response_headers(response), response_body: safe_response_body(response), response_status: response.code, internal_error_message: error_message @@ -150,8 +156,21 @@ class WebHookService if @force # executed as part of test - run log-execution inline. ::WebHooks::LogExecutionService.new(hook: hook, log_data: log_data, response_category: category).execute else - ::WebHooks::LogExecutionWorker - .perform_async(hook.id, log_data, category, uniqueness_token) + queue_log_execution_with_retry(log_data, category) + end + end + + def queue_log_execution_with_retry(log_data, category) + retried = false + begin + ::WebHooks::LogExecutionWorker.perform_async(hook.id, log_data, category, uniqueness_token) + rescue Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError + raise if retried + + # Strip request data + log_data[:request_data] = ::WebHookLog::OVERSIZE_REQUEST_DATA + retried = true + retry end end @@ -181,14 +200,19 @@ class WebHookService # Make response headers more stylish # Net::HTTPHeader has downcased hash with arrays: { 'content-type' => ['text/html; charset=utf-8'] } # This method format response to capitalized hash with strings: { 'Content-Type' => 'text/html; charset=utf-8' } - def format_response_headers(response) - response.headers.each_capitalized.to_h + # rubocop:disable Style/HashTransformValues + def safe_response_headers(response) + response.headers.each_capitalized.first(RESPONSE_HEADERS_COUNT_LIMIT).to_h do |header_key, header_value| + [enforce_utf8(header_key), string_size_limit(enforce_utf8(header_value), RESPONSE_HEADERS_SIZE_LIMIT)] + end end + # rubocop:enable Style/HashTransformValues def safe_response_body(response) return '' unless response.body - response.body.encode('UTF-8', invalid: :replace, undef: :replace, replace: '') + response_body = enforce_utf8(response.body) + string_size_limit(response_body, RESPONSE_BODY_SIZE_LIMIT) end def rate_limited? @@ -229,4 +253,12 @@ class WebHookService **Gitlab::ApplicationContext.current ) end + + def string_size_limit(str, limit) + str.truncate_bytes(limit) + end + + def enforce_utf8(str) + Gitlab::EncodingHelper.encode_utf8(str) + end end diff --git a/app/views/admin/hook_logs/show.html.haml b/app/views/admin/hook_logs/show.html.haml index abfabbb5eb6..2ace46c0acf 100644 --- a/app/views/admin/hook_logs/show.html.haml +++ b/app/views/admin/hook_logs/show.html.haml @@ -4,6 +4,9 @@ %hr -= link_to _("Resend Request"), retry_admin_hook_hook_log_path(@hook, @hook_log), method: :post, class: "btn gl-button btn-default float-right gl-ml-3" +- if @hook_log.oversize? + = button_tag _("Resend Request"), class: "btn gl-button btn-default float-right gl-ml-3 has-tooltip", disabled: true, title: _("Request data is too large") +- else + = link_to _("Resend Request"), retry_admin_hook_hook_log_path(@hook, @hook_log), method: :post, class: "btn gl-button btn-default float-right gl-ml-3" = render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log } diff --git a/app/views/projects/hook_logs/show.html.haml b/app/views/projects/hook_logs/show.html.haml index 1f71e1b7055..2e2e7ba848e 100644 --- a/app/views/projects/hook_logs/show.html.haml +++ b/app/views/projects/hook_logs/show.html.haml @@ -7,6 +7,9 @@ %hr -= link_to _("Resend Request"), @hook_log.present.retry_path, method: :post, class: "btn gl-button btn-default float-right gl-ml-3" +- if @hook_log.oversize? + = button_tag _("Resend Request"), class: "btn gl-button btn-default float-right gl-ml-3 has-tooltip", disabled: true, title: _("Request data is too large") +- else + = link_to _("Resend Request"), @hook_log.present.retry_path, method: :post, class: "btn gl-button btn-default float-right gl-ml-3" = render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log } diff --git a/app/views/shared/hook_logs/_content.html.haml b/app/views/shared/hook_logs/_content.html.haml index 932971402a2..8b5b4b6e5fa 100644 --- a/app/views/shared/hook_logs/_content.html.haml +++ b/app/views/shared/hook_logs/_content.html.haml @@ -30,8 +30,11 @@ %h4.gl-mt-6= _('Request') %pre - :escaped - #{Gitlab::Json.pretty_generate(hook_log.request_data)} + - if hook_log.oversize? + = _('Request data is too large') + - else + :escaped + #{Gitlab::Json.pretty_generate(hook_log.request_data)} %h5= _('Headers') %pre |