Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-06-07 21:09:27 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-06-07 21:09:27 +0300
commit5cda8c8a420399ca9687c4a981fefd50ce5a1fdd (patch)
tree6050d7517a36798c9586e153df20a0696c5fcd4f /app
parent7bbc731c75d0b8bf7c74ba77d521266d2ed0a1fc (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/access_tokens/components/access_token_table_app.vue92
-rw-r--r--app/assets/javascripts/access_tokens/components/constants.js61
-rw-r--r--app/assets/javascripts/access_tokens/components/new_access_token_app.vue21
-rw-r--r--app/assets/javascripts/pages/admin/impersonation_tokens/index.js10
-rw-r--r--app/assets/javascripts/pages/groups/settings/access_tokens/index.js8
-rw-r--r--app/assets/javascripts/pages/projects/settings/access_tokens/index.js8
-rw-r--r--app/models/hooks/web_hook_log.rb6
-rw-r--r--app/models/users/callout.rb5
-rw-r--r--app/services/ci/job_artifacts/destroy_all_expired_service.rb2
-rw-r--r--app/services/ci/job_artifacts/destroy_batch_service.rb24
-rw-r--r--app/services/web_hook_service.rb44
-rw-r--r--app/views/admin/hook_logs/show.html.haml5
-rw-r--r--app/views/projects/hook_logs/show.html.haml5
-rw-r--r--app/views/shared/hook_logs/_content.html.haml7
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