Commit 8acc6a6b authored by Miguel Rincon's avatar Miguel Rincon

Merge branch 'Move-handlers-outside-domcontentloaded' into 'master'

[RUN AS-IF-FOSS] Unwrap from DOMContentLoaded in Monitor:Health pages

See merge request gitlab-org/gitlab!45571
parents 8002f99e 86836c3d
...@@ -329,7 +329,6 @@ linters: ...@@ -329,7 +329,6 @@ linters:
- 'ee/app/views/errors/kerberos_denied.html.haml' - 'ee/app/views/errors/kerberos_denied.html.haml'
- 'ee/app/views/groups/ee/_settings_nav.html.haml' - 'ee/app/views/groups/ee/_settings_nav.html.haml'
- 'ee/app/views/groups/group_members/_ldap_sync.html.haml' - 'ee/app/views/groups/group_members/_ldap_sync.html.haml'
- 'ee/app/views/groups/group_members/_sync_button.html.haml'
- 'ee/app/views/groups/hooks/edit.html.haml' - 'ee/app/views/groups/hooks/edit.html.haml'
- 'ee/app/views/groups/ldap_group_links/index.html.haml' - 'ee/app/views/groups/ldap_group_links/index.html.haml'
- 'ee/app/views/layouts/nav/ee/admin/_new_monitoring_sidebar.html.haml' - 'ee/app/views/layouts/nav/ee/admin/_new_monitoring_sidebar.html.haml'
...@@ -362,7 +361,6 @@ linters: ...@@ -362,7 +361,6 @@ linters:
- 'ee/app/views/projects/services/gitlab_slack_application/_help.html.haml' - 'ee/app/views/projects/services/gitlab_slack_application/_help.html.haml'
- 'ee/app/views/projects/services/gitlab_slack_application/_slack_integration_form.html.haml' - 'ee/app/views/projects/services/gitlab_slack_application/_slack_integration_form.html.haml'
- 'ee/app/views/projects/settings/slacks/edit.html.haml' - 'ee/app/views/projects/settings/slacks/edit.html.haml'
- 'ee/app/views/shared/_mirror_update_button.html.haml'
- 'ee/app/views/shared/epic/_search_bar.html.haml' - 'ee/app/views/shared/epic/_search_bar.html.haml'
- 'ee/app/views/shared/issuable/_approvals.html.haml' - 'ee/app/views/shared/issuable/_approvals.html.haml'
- 'ee/app/views/shared/issuable/_board_create_list_dropdown.html.haml' - 'ee/app/views/shared/issuable/_board_create_list_dropdown.html.haml'
......
...@@ -186,31 +186,21 @@ RSpec/ExpectChange: ...@@ -186,31 +186,21 @@ RSpec/ExpectChange:
# Offense count: 47 # Offense count: 47
RSpec/ExpectGitlabTracking: RSpec/ExpectGitlabTracking:
Exclude: Exclude:
- 'ee/spec/controllers/groups/analytics/coverage_reports_controller_spec.rb'
- 'ee/spec/controllers/projects/settings/operations_controller_spec.rb' - 'ee/spec/controllers/projects/settings/operations_controller_spec.rb'
- 'ee/spec/controllers/registrations_controller_spec.rb'
- 'ee/spec/requests/api/visual_review_discussions_spec.rb' - 'ee/spec/requests/api/visual_review_discussions_spec.rb'
- 'ee/spec/services/epics/issue_promote_service_spec.rb' - 'ee/spec/services/epics/issue_promote_service_spec.rb'
- 'spec/controllers/groups/registry/repositories_controller_spec.rb' - 'spec/controllers/groups/registry/repositories_controller_spec.rb'
- 'spec/controllers/groups_controller_spec.rb'
- 'spec/controllers/projects/registry/repositories_controller_spec.rb' - 'spec/controllers/projects/registry/repositories_controller_spec.rb'
- 'spec/controllers/projects/registry/tags_controller_spec.rb' - 'spec/controllers/projects/registry/tags_controller_spec.rb'
- 'spec/controllers/projects/settings/operations_controller_spec.rb' - 'spec/controllers/projects/settings/operations_controller_spec.rb'
- 'spec/controllers/registrations_controller_spec.rb' - 'spec/controllers/registrations_controller_spec.rb'
- 'spec/lib/api/helpers_spec.rb' - 'spec/lib/api/helpers_spec.rb'
- 'spec/lib/gitlab/experimentation_spec.rb'
- 'spec/mailers/notify_spec.rb'
- 'spec/models/project_services/prometheus_service_spec.rb'
- 'spec/requests/api/project_container_repositories_spec.rb' - 'spec/requests/api/project_container_repositories_spec.rb'
- 'spec/services/clusters/applications/check_installation_progress_service_spec.rb'
- 'spec/services/issues/zoom_link_service_spec.rb'
- 'spec/support/helpers/snowplow_helpers.rb'
- 'spec/support/shared_examples/controllers/trackable_shared_examples.rb' - 'spec/support/shared_examples/controllers/trackable_shared_examples.rb'
- 'spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb' - 'spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb'
- 'spec/support/shared_examples/requests/api/discussions_shared_examples.rb' - 'spec/support/shared_examples/requests/api/discussions_shared_examples.rb'
- 'spec/support/shared_examples/requests/api/packages_shared_examples.rb' - 'spec/support/shared_examples/requests/api/packages_shared_examples.rb'
- 'spec/support/shared_examples/requests/api/tracking_shared_examples.rb' - 'spec/support/shared_examples/requests/api/tracking_shared_examples.rb'
- 'spec/support/snowplow.rb'
# Offense count: 751 # Offense count: 751
RSpec/ExpectInHook: RSpec/ExpectInHook:
......
<script> <script>
import { GlForm, GlFormGroup, GlFormInput, GlFormTextarea } from '@gitlab/ui'; import { GlForm, GlFormGroup, GlFormInput, GlFormTextarea } from '@gitlab/ui';
import AccessorUtilities from '~/lib/utils/accessor';
export default { export default {
components: { components: {
...@@ -19,55 +18,25 @@ export default { ...@@ -19,55 +18,25 @@ export default {
required: true, required: true,
}, },
}, },
data() {
return {
editable: {
title: this.title,
description: this.description,
},
};
},
computed: {
editableStorageKey() {
return this.getId('local-storage', 'editable');
},
hasLocalStorage() {
return AccessorUtilities.isLocalStorageAccessSafe();
},
},
mounted() { mounted() {
this.initCachedEditable();
this.preSelect(); this.preSelect();
}, },
methods: { methods: {
getId(type, key) { getId(type, key) {
return `sse-merge-request-meta-${type}-${key}`; return `sse-merge-request-meta-${type}-${key}`;
}, },
initCachedEditable() {
if (this.hasLocalStorage) {
const cachedEditable = JSON.parse(localStorage.getItem(this.editableStorageKey));
if (cachedEditable) {
this.editable = cachedEditable;
}
}
},
preSelect() { preSelect() {
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.title.$el.select(); this.$refs.title.$el.select();
}); });
}, },
resetCachedEditable() { onUpdate(field, value) {
if (this.hasLocalStorage) { const payload = {
window.localStorage.removeItem(this.editableStorageKey); title: this.title,
} description: this.description,
}, [field]: value,
onUpdate() { };
const payload = { ...this.editable };
this.$emit('updateSettings', payload); this.$emit('updateSettings', payload);
if (this.hasLocalStorage) {
window.localStorage.setItem(this.editableStorageKey, JSON.stringify(payload));
}
}, },
}, },
}; };
...@@ -83,9 +52,9 @@ export default { ...@@ -83,9 +52,9 @@ export default {
<gl-form-input <gl-form-input
:id="getId('control', 'title')" :id="getId('control', 'title')"
ref="title" ref="title"
v-model.lazy="editable.title" :value="title"
type="text" type="text"
@input="onUpdate" @input="onUpdate('title', $event)"
/> />
</gl-form-group> </gl-form-group>
...@@ -96,8 +65,8 @@ export default { ...@@ -96,8 +65,8 @@ export default {
> >
<gl-form-textarea <gl-form-textarea
:id="getId('control', 'description')" :id="getId('control', 'description')"
v-model.lazy="editable.description" :value="description"
@input="onUpdate" @input="onUpdate('description', $event)"
/> />
</gl-form-group> </gl-form-group>
</gl-form> </gl-form>
......
<script> <script>
import { GlModal } from '@gitlab/ui'; import { GlModal } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import EditMetaControls from './edit_meta_controls.vue'; import EditMetaControls from './edit_meta_controls.vue';
import { MR_META_LOCAL_STORAGE_KEY } from '../constants';
export default { export default {
components: { components: {
GlModal, GlModal,
EditMetaControls, EditMetaControls,
LocalStorageSync,
}, },
props: { props: {
sourcePath: { sourcePath: {
...@@ -17,6 +21,7 @@ export default { ...@@ -17,6 +21,7 @@ export default {
}, },
data() { data() {
return { return {
clearStorage: false,
mergeRequestMeta: { mergeRequestMeta: {
title: sprintf(s__(`StaticSiteEditor|Update %{sourcePath} file`), { title: sprintf(s__(`StaticSiteEditor|Update %{sourcePath} file`), {
sourcePath: this.sourcePath, sourcePath: this.sourcePath,
...@@ -51,7 +56,7 @@ export default { ...@@ -51,7 +56,7 @@ export default {
}, },
onPrimary() { onPrimary() {
this.$emit('primary', this.mergeRequestMeta); this.$emit('primary', this.mergeRequestMeta);
this.$refs.editMetaControls.resetCachedEditable(); this.clearStorage = true;
}, },
onSecondary() { onSecondary() {
this.hide(); this.hide();
...@@ -60,6 +65,7 @@ export default { ...@@ -60,6 +65,7 @@ export default {
this.mergeRequestMeta = { ...mergeRequestMeta }; this.mergeRequestMeta = { ...mergeRequestMeta };
}, },
}, },
storageKey: MR_META_LOCAL_STORAGE_KEY,
}; };
</script> </script>
...@@ -75,6 +81,12 @@ export default { ...@@ -75,6 +81,12 @@ export default {
@secondary="onSecondary" @secondary="onSecondary"
@hide="() => $emit('hide')" @hide="() => $emit('hide')"
> >
<local-storage-sync
v-model="mergeRequestMeta"
:storage-key="$options.storageKey"
:clear="clearStorage"
as-json
/>
<edit-meta-controls <edit-meta-controls
ref="editMetaControls" ref="editMetaControls"
:title="mergeRequestMeta.title" :title="mergeRequestMeta.title"
......
...@@ -21,3 +21,5 @@ export const TRACKING_ACTION_CREATE_MERGE_REQUEST = 'create_merge_request'; ...@@ -21,3 +21,5 @@ export const TRACKING_ACTION_CREATE_MERGE_REQUEST = 'create_merge_request';
export const TRACKING_ACTION_INITIALIZE_EDITOR = 'initialize_editor'; export const TRACKING_ACTION_INITIALIZE_EDITOR = 'initialize_editor';
export const DEFAULT_IMAGE_UPLOAD_PATH = 'source/images/uploads/'; export const DEFAULT_IMAGE_UPLOAD_PATH = 'source/images/uploads/';
export const MR_META_LOCAL_STORAGE_KEY = 'sse-merge-request-meta-storage-key';
<script> <script>
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { GlIcon, GlButton, GlSprintf, GlLink } from '@gitlab/ui'; import {
GlIcon,
GlButton,
GlButtonGroup,
GlDropdown,
GlDropdownItem,
GlSprintf,
GlLink,
GlTooltipDirective,
} from '@gitlab/ui';
import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_to_merge'; import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_to_merge';
import simplePoll from '~/lib/utils/simple_poll'; import simplePoll from '~/lib/utils/simple_poll';
import { __ } from '~/locale'; import { __ } from '~/locale';
...@@ -36,6 +45,9 @@ export default { ...@@ -36,6 +45,9 @@ export default {
GlSprintf, GlSprintf,
GlLink, GlLink,
GlButton, GlButton,
GlButtonGroup,
GlDropdown,
GlDropdownItem,
MergeTrainHelperText: () => MergeTrainHelperText: () =>
import('ee_component/vue_merge_request_widget/components/merge_train_helper_text.vue'), import('ee_component/vue_merge_request_widget/components/merge_train_helper_text.vue'),
MergeImmediatelyConfirmationDialog: () => MergeImmediatelyConfirmationDialog: () =>
...@@ -43,6 +55,9 @@ export default { ...@@ -43,6 +55,9 @@ export default {
'ee_component/vue_merge_request_widget/components/merge_immediately_confirmation_dialog.vue' 'ee_component/vue_merge_request_widget/components/merge_immediately_confirmation_dialog.vue'
), ),
}, },
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [readyToMergeMixin], mixins: [readyToMergeMixin],
props: { props: {
mr: { type: Object, required: true }, mr: { type: Object, required: true },
...@@ -283,7 +298,7 @@ export default { ...@@ -283,7 +298,7 @@ export default {
<status-icon :status="iconClass" /> <status-icon :status="iconClass" />
<div class="media-body"> <div class="media-body">
<div class="mr-widget-body-controls media space-children"> <div class="mr-widget-body-controls media space-children">
<span class="btn-group"> <gl-button-group>
<gl-button <gl-button
size="medium" size="medium"
category="primary" category="primary"
...@@ -294,54 +309,33 @@ export default { ...@@ -294,54 +309,33 @@ export default {
@click="handleMergeButtonClick(isAutoMergeAvailable)" @click="handleMergeButtonClick(isAutoMergeAvailable)"
>{{ mergeButtonText }}</gl-button >{{ mergeButtonText }}</gl-button
> >
<button <gl-dropdown
v-if="shouldShowMergeImmediatelyDropdown" v-if="shouldShowMergeImmediatelyDropdown"
v-gl-tooltip.hover.focus="__('Select merge moment')"
:disabled="isMergeButtonDisabled" :disabled="isMergeButtonDisabled"
type="button" variant="info"
class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
data-toggle="dropdown"
data-qa-selector="merge_moment_dropdown" data-qa-selector="merge_moment_dropdown"
:aria-label="__('Select merge moment')" toggle-class="btn-icon js-merge-moment"
>
<i class="fa fa-chevron-down" aria-hidden="true"></i>
</button>
<ul
v-if="shouldShowMergeImmediatelyDropdown"
class="dropdown-menu dropdown-menu-right"
role="menu"
> >
<li> <template #button-content>
<a <gl-icon name="chevron-down" class="mr-0" />
class="auto_merge_enabled qa-merge-when-pipeline-succeeds-option" <span class="sr-only">{{ __('Select merge moment') }}</span>
href="#" </template>
@click.prevent="handleMergeButtonClick(true)" <gl-dropdown-item
icon-name="warning"
button-class="accept-merge-request js-merge-immediately-button"
data-qa-selector="merge_immediately_option"
@click="handleMergeImmediatelyButtonClick"
> >
<span class="media"> {{ __('Merge immediately') }}
<gl-icon name="status_success" class="merge-opt-icon" aria-hidden="true" /> </gl-dropdown-item>
<span class="media-body merge-opt-title">{{ autoMergeText }}</span>
</span>
</a>
</li>
<li>
<merge-immediately-confirmation-dialog <merge-immediately-confirmation-dialog
ref="confirmationDialog" ref="confirmationDialog"
:docs-url="mr.mergeImmediatelyDocsPath" :docs-url="mr.mergeImmediatelyDocsPath"
@mergeImmediately="onMergeImmediatelyConfirmation" @mergeImmediately="onMergeImmediatelyConfirmation"
/> />
<a </gl-dropdown>
class="accept-merge-request js-merge-immediately-button" </gl-button-group>
data-qa-selector="merge_immediately_option"
href="#"
@click.prevent="handleMergeImmediatelyButtonClick"
>
<span class="media">
<gl-icon name="status_warning" class="merge-opt-icon" aria-hidden="true" />
<span class="media-body merge-opt-title">{{ __('Merge immediately') }}</span>
</span>
</a>
</li>
</ul>
</span>
<div class="media-body-wrap space-children"> <div class="media-body-wrap space-children">
<template v-if="shouldShowMergeControls"> <template v-if="shouldShowMergeControls">
<label v-if="mr.canRemoveSourceBranch"> <label v-if="mr.canRemoveSourceBranch">
......
...@@ -22,11 +22,21 @@ export default { ...@@ -22,11 +22,21 @@ export default {
required: false, required: false,
default: true, default: true,
}, },
clear: {
type: Boolean,
required: false,
default: false,
},
}, },
watch: { watch: {
value(newVal) { value(newVal) {
this.saveValue(this.serialize(newVal)); this.saveValue(this.serialize(newVal));
}, },
clear(newVal) {
if (newVal) {
localStorage.removeItem(this.storageKey);
}
},
}, },
mounted() { mounted() {
// On mount, trigger update if we actually have a localStorageValue // On mount, trigger update if we actually have a localStorageValue
......
...@@ -113,10 +113,6 @@ ...@@ -113,10 +113,6 @@
content: '\f0da'; content: '\f0da';
} }
.fa-refresh::before {
content: '\f021';
}
.fa-chevron-up::before { .fa-chevron-up::before {
content: '\f077'; content: '\f077';
} }
......
...@@ -56,3 +56,7 @@ ...@@ -56,3 +56,7 @@
vertical-align: text-bottom; vertical-align: text-bottom;
} }
} }
.spin {
animation: spinner-rotate 2s infinite linear;
}
# frozen_string_literal: true
class JwksController < ActionController::Base # rubocop:disable Rails/ApplicationController
def index
render json: { keys: keys }
end
private
def keys
[
# We keep openid_connect_signing_key so that we can seamlessly
# replace it with ci_jwt_signing_key and remove it on the next release.
# TODO: Remove openid_connect_signing_key in 13.7
# https://gitlab.com/gitlab-org/gitlab/-/issues/221031
Rails.application.secrets.openid_connect_signing_key,
Gitlab::CurrentSettings.ci_jwt_signing_key
].compact.map do |key_data|
OpenSSL::PKey::RSA.new(key_data)
.public_key
.to_jwk
.slice(:kty, :kid, :e, :n)
.merge(use: 'sig', alg: 'RS256')
end
end
end
...@@ -384,6 +384,9 @@ class ApplicationSetting < ApplicationRecord ...@@ -384,6 +384,9 @@ class ApplicationSetting < ApplicationRecord
validates :raw_blob_request_limit, validates :raw_blob_request_limit,
numericality: { only_integer: true, greater_than_or_equal_to: 0 } numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :ci_jwt_signing_key,
rsa_key: true, allow_nil: true
attr_encrypted :asset_proxy_secret_key, attr_encrypted :asset_proxy_secret_key,
mode: :per_attribute_iv, mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated, key: Settings.attr_encrypted_db_key_base_truncated,
...@@ -409,6 +412,7 @@ class ApplicationSetting < ApplicationRecord ...@@ -409,6 +412,7 @@ class ApplicationSetting < ApplicationRecord
attr_encrypted :recaptcha_site_key, encryption_options_base_truncated_aes_256_gcm attr_encrypted :recaptcha_site_key, encryption_options_base_truncated_aes_256_gcm
attr_encrypted :slack_app_secret, encryption_options_base_truncated_aes_256_gcm attr_encrypted :slack_app_secret, encryption_options_base_truncated_aes_256_gcm
attr_encrypted :slack_app_verification_token, encryption_options_base_truncated_aes_256_gcm attr_encrypted :slack_app_verification_token, encryption_options_base_truncated_aes_256_gcm
attr_encrypted :ci_jwt_signing_key, encryption_options_base_truncated_aes_256_gcm
before_validation :ensure_uuid! before_validation :ensure_uuid!
......
...@@ -1059,7 +1059,7 @@ module Ci ...@@ -1059,7 +1059,7 @@ module Ci
jwt = Gitlab::Ci::Jwt.for_build(self) jwt = Gitlab::Ci::Jwt.for_build(self)
variables.append(key: 'CI_JOB_JWT', value: jwt, public: false, masked: true) variables.append(key: 'CI_JOB_JWT', value: jwt, public: false, masked: true)
rescue OpenSSL::PKey::RSAError => e rescue OpenSSL::PKey::RSAError, Gitlab::Ci::Jwt::NoSigningKeyError => e
Gitlab::ErrorTracking.track_exception(e) Gitlab::ErrorTracking.track_exception(e)
end end
end end
......
...@@ -21,6 +21,7 @@ class GroupMember < Member ...@@ -21,6 +21,7 @@ class GroupMember < Member
scope :of_groups, ->(groups) { where(source_id: groups.select(:id)) } scope :of_groups, ->(groups) { where(source_id: groups.select(:id)) }
scope :of_ldap_type, -> { where(ldap: true) } scope :of_ldap_type, -> { where(ldap: true) }
scope :count_users_by_group_id, -> { group(:source_id).count } scope :count_users_by_group_id, -> { group(:source_id).count }
scope :with_user, -> (user) { where(user: user) }
after_create :update_two_factor_requirement, unless: :invite? after_create :update_two_factor_requirement, unless: :invite?
after_destroy :update_two_factor_requirement, unless: :invite? after_destroy :update_two_factor_requirement, unless: :invite?
......
# frozen_string_literal: true
# RsaKeyValidator
#
# Custom validator for RSA private keys.
#
# class Project < ActiveRecord::Base
# validates :signing_key, rsa_key: true
# end
#
class RsaKeyValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless valid_rsa_key?(value)
record.errors.add(attribute, "is not a valid RSA key")
end
end
private
def valid_rsa_key?(value)
return false unless value
OpenSSL::PKey::RSA.new(value)
rescue OpenSSL::PKey::RSAError
false
end
end
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
= succeed ':' do = succeed ':' do
= link_to note.author_name, user_url(note.author) = link_to note.author_name, user_url(note.author)
- if discussion.nil? - if discussion.nil?
commented = link_to 'commented', target_url
- else - else
- if note.start_of_discussion? - if note.start_of_discussion?
started a new started a new
......
...@@ -74,4 +74,4 @@ ...@@ -74,4 +74,4 @@
- if mirror.ssh_key_auth? - if mirror.ssh_key_auth?
= clipboard_button(text: mirror.ssh_public_key, class: 'gl-button btn btn-default', title: _('Copy SSH public key'), qa_selector: 'copy_public_key_button') = clipboard_button(text: mirror.ssh_public_key, class: 'gl-button btn btn-default', title: _('Copy SSH public key'), qa_selector: 'copy_public_key_button')
= render 'shared/remote_mirror_update_button', remote_mirror: mirror = render 'shared/remote_mirror_update_button', remote_mirror: mirror
%button.js-delete-mirror.qa-delete-mirror.rspec-delete-mirror.btn.gl-button.btn-danger{ type: 'button', data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' }, title: _('Remove') }= sprite_icon('remove') %button.js-delete-mirror.qa-delete-mirror.rspec-delete-mirror.btn.btn-icon.gl-button.btn-danger{ type: 'button', data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' }, title: _('Remove') }= sprite_icon('remove')
- if remote_mirror.update_in_progress? - if remote_mirror.update_in_progress?
%button.btn.disabled{ type: 'button', data: { toggle: 'tooltip', container: 'body', qa_selector: 'updating_button' }, title: _('Updating') } %button.btn.btn-icon.gl-button.disabled{ type: 'button', data: { toggle: 'tooltip', container: 'body', qa_selector: 'updating_button' }, title: _('Updating') }
= icon("refresh spin") = sprite_icon("retry", css_class: "spin")
- elsif remote_mirror.enabled? - elsif remote_mirror.enabled?
= link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn qa-update-now-button rspec-update-now-button", data: { toggle: 'tooltip', container: 'body' }, title: _('Update now') do = link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn btn-icon gl-button qa-update-now-button rspec-update-now-button", data: { toggle: 'tooltip', container: 'body' }, title: _('Update now') do
= icon("refresh") = sprite_icon("retry")
---
title: Add CI JWT signing key to application_setings
merge_request: 43950
author:
type: added
---
title: Fix incorrect code in Load Performance Testing docs
merge_request: 45877
author:
type: other
---
title: Add link to the note on the email sent after adding a comment on an issue
merge_request: 45511
author:
type: changed
---
title: Replace fa-refresh icon with GitLab SVG
merge_request: 45777
author:
type: changed
---
title: Add `position` column into security_findings table
merge_request: 44815
author:
type: fixed
---
name: ci_jwt_signing_key
introduced_by_url:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/258546
type: development
group: group::release management
default_enabled: false
...@@ -175,9 +175,8 @@ Rails.application.routes.draw do ...@@ -175,9 +175,8 @@ Rails.application.routes.draw do
resources :abuse_reports, only: [:new, :create] resources :abuse_reports, only: [:new, :create]
# JWKS (JSON Web Key Set) endpoint # JWKS (JSON Web Key Set) endpoint
# Used by third parties to verify CI_JOB_JWT, placeholder route # Used by third parties to verify CI_JOB_JWT
# in case we decide to move away from doorkeeper-openid_connect get 'jwks' => 'jwks#index'
get 'jwks' => 'doorkeeper/openid_connect/discovery#keys'
draw :snippets draw :snippets
draw :profile draw :profile
......
...@@ -7,4 +7,7 @@ ApplicationSetting.create_from_defaults ...@@ -7,4 +7,7 @@ ApplicationSetting.create_from_defaults
puts "Enable hashed storage for every new projects.".color(:green) puts "Enable hashed storage for every new projects.".color(:green)
ApplicationSetting.current_without_cache.update!(hashed_storage_enabled: true) ApplicationSetting.current_without_cache.update!(hashed_storage_enabled: true)
puts "Generate CI JWT signing key".color(:green)
ApplicationSetting.current_without_cache.update!(ci_jwt_signing_key: OpenSSL::PKey::RSA.new(2048).to_pem)
print '.' print '.'
...@@ -24,3 +24,7 @@ if ENV['GITLAB_PROMETHEUS_METRICS_ENABLED'].present? ...@@ -24,3 +24,7 @@ if ENV['GITLAB_PROMETHEUS_METRICS_ENABLED'].present?
settings.prometheus_metrics_enabled = value settings.prometheus_metrics_enabled = value
save(settings, 'Prometheus metrics enabled flag') save(settings, 'Prometheus metrics enabled flag')
end end
settings = Gitlab::CurrentSettings.current_application_settings
settings.ci_jwt_signing_key = OpenSSL::PKey::RSA.new(2048).to_pem
save(settings, 'CI JWT signing key')
# frozen_string_literal: true
class AddCiJwtSigningKeyToApplicationSettings < ActiveRecord::Migration[6.0]
DOWNTIME = false
# rubocop:disable Migration/AddLimitToTextColumns
# limit is added in 20201001011937_add_text_limit_to_application_settings_encrypted_ci_jwt_signing_key_iv
def change
add_column :application_settings, :encrypted_ci_jwt_signing_key, :text
add_column :application_settings, :encrypted_ci_jwt_signing_key_iv, :text
end
# rubocop:enable Migration/AddLimitToTextColumns
end
# frozen_string_literal: true
class AddTextLimitToApplicationSettingsEncryptedCiJwtSigningKeyIv < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_text_limit :application_settings, :encrypted_ci_jwt_signing_key_iv, 255
end
def down
remove_text_limit :application_settings, :encrypted_ci_jwt_signing_key_iv
end
end
# frozen_string_literal: true
class GenerateCiJwtSigningKey < ActiveRecord::Migration[6.0]
DOWNTIME = false
class ApplicationSetting < ActiveRecord::Base
self.table_name = 'application_settings'
attr_encrypted :ci_jwt_signing_key, {
mode: :per_attribute_iv,
key: Rails.application.secrets.db_key_base[0..31],
algorithm: 'aes-256-gcm',
encode: true
}
end
def up
ApplicationSetting.reset_column_information
ApplicationSetting.find_each do |application_setting|
application_setting.update(ci_jwt_signing_key: OpenSSL::PKey::RSA.new(2048).to_pem)
end
end
def down
ApplicationSetting.reset_column_information
ApplicationSetting.find_each do |application_setting|
application_setting.update_columns(encrypted_ci_jwt_signing_key: nil, encrypted_ci_jwt_signing_key_iv: nil)
end
end
end
# frozen_string_literal: true
class AddPositionIntoSecurityFindings < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_column :security_findings, :position, :integer
end
end
def down
with_lock_retries do
remove_column :security_findings, :position
end
end
end
# frozen_string_literal: true
class AddUniqueIndexOnScanIdAndPositionOfSecurityFindings < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INDEX_NAME = 'index_security_findings_on_scan_id_and_position'
disable_ddl_transaction!
def up
add_concurrent_index :security_findings, [:scan_id, :position], unique: true, name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :security_findings, INDEX_NAME
end
end
b7b49ca4c021b7caa9f8612ad9b69d4ec6d79894db2e43266bfe26f2e0bffe08
\ No newline at end of file
8af89bb3e63bfca24cee8fdf6f0dd587fae7d81bfeaf6d427f84c7b37c9664ba
\ No newline at end of file
966f6e95189b551cba0ef548cb410911c0beee30d0a265ae21d90321ecbb2a00
\ No newline at end of file
d0ca8f0dbe0cf0fbbdd715867f3ae20862683433d919ee5cd942086d21f3b44d
\ No newline at end of file
f19ab0de07415e728849ef4e56804909a3a4a57ad8f55fe71a27bc43c535ac66
\ No newline at end of file
...@@ -9294,9 +9294,12 @@ CREATE TABLE application_settings ( ...@@ -9294,9 +9294,12 @@ CREATE TABLE application_settings (
require_admin_approval_after_user_signup boolean DEFAULT false NOT NULL, require_admin_approval_after_user_signup boolean DEFAULT false NOT NULL,
help_page_documentation_base_url text, help_page_documentation_base_url text,
automatic_purchased_storage_allocation boolean DEFAULT false NOT NULL, automatic_purchased_storage_allocation boolean DEFAULT false NOT NULL,
encrypted_ci_jwt_signing_key text,
encrypted_ci_jwt_signing_key_iv text,
CONSTRAINT check_2dba05b802 CHECK ((char_length(gitpod_url) <= 255)), CONSTRAINT check_2dba05b802 CHECK ((char_length(gitpod_url) <= 255)),
CONSTRAINT check_51700b31b5 CHECK ((char_length(default_branch_name) <= 255)), CONSTRAINT check_51700b31b5 CHECK ((char_length(default_branch_name) <= 255)),
CONSTRAINT check_57123c9593 CHECK ((char_length(help_page_documentation_base_url) <= 255)), CONSTRAINT check_57123c9593 CHECK ((char_length(help_page_documentation_base_url) <= 255)),
CONSTRAINT check_85a39b68ff CHECK ((char_length(encrypted_ci_jwt_signing_key_iv) <= 255)),
CONSTRAINT check_9c6c447a13 CHECK ((char_length(maintenance_mode_message) <= 255)), CONSTRAINT check_9c6c447a13 CHECK ((char_length(maintenance_mode_message) <= 255)),
CONSTRAINT check_d03919528d CHECK ((char_length(container_registry_vendor) <= 255)), CONSTRAINT check_d03919528d CHECK ((char_length(container_registry_vendor) <= 255)),
CONSTRAINT check_d820146492 CHECK ((char_length(spam_check_endpoint_url) <= 255)), CONSTRAINT check_d820146492 CHECK ((char_length(spam_check_endpoint_url) <= 255)),
...@@ -15857,6 +15860,7 @@ CREATE TABLE security_findings ( ...@@ -15857,6 +15860,7 @@ CREATE TABLE security_findings (
confidence smallint NOT NULL, confidence smallint NOT NULL,
project_fingerprint text NOT NULL, project_fingerprint text NOT NULL,
deduplicated boolean DEFAULT false NOT NULL, deduplicated boolean DEFAULT false NOT NULL,
"position" integer,
CONSTRAINT check_b9508c6df8 CHECK ((char_length(project_fingerprint) <= 40)) CONSTRAINT check_b9508c6df8 CHECK ((char_length(project_fingerprint) <= 40))
); );
...@@ -21528,6 +21532,8 @@ CREATE INDEX index_security_findings_on_project_fingerprint ON security_findings ...@@ -21528,6 +21532,8 @@ CREATE INDEX index_security_findings_on_project_fingerprint ON security_findings
CREATE INDEX index_security_findings_on_scan_id_and_deduplicated ON security_findings USING btree (scan_id, deduplicated); CREATE INDEX index_security_findings_on_scan_id_and_deduplicated ON security_findings USING btree (scan_id, deduplicated);
CREATE UNIQUE INDEX index_security_findings_on_scan_id_and_position ON security_findings USING btree (scan_id, "position");
CREATE INDEX index_security_findings_on_scanner_id ON security_findings USING btree (scanner_id); CREATE INDEX index_security_findings_on_scanner_id ON security_findings USING btree (scanner_id);
CREATE INDEX index_security_findings_on_severity ON security_findings USING btree (severity); CREATE INDEX index_security_findings_on_severity ON security_findings USING btree (severity);
......
...@@ -156,7 +156,7 @@ The best approach is to capture the dynamic URL in a [`.env` file](https://docs. ...@@ -156,7 +156,7 @@ The best approach is to capture the dynamic URL in a [`.env` file](https://docs.
as a job artifact to be shared, then use a custom environment variable we've provided named `K6_DOCKER_OPTIONS` as a job artifact to be shared, then use a custom environment variable we've provided named `K6_DOCKER_OPTIONS`
to configure the k6 Docker container to use the file. With this, k6 can then use any to configure the k6 Docker container to use the file. With this, k6 can then use any
environment variables from the `.env` file in scripts using standard JavaScript, environment variables from the `.env` file in scripts using standard JavaScript,
such as: ``http.get(`${__ENV.ENVIRONMENT_URL`})``. such as: ``http.get(`${__ENV.ENVIRONMENT_URL}`)``.
For example: For example:
......
...@@ -19,7 +19,6 @@ module EE ...@@ -19,7 +19,6 @@ module EE
private private
# rubocop: disable CodeReuse/ActiveRecord
override :authorize_list_type_resource! override :authorize_list_type_resource!
def authorize_list_type_resource!(board, params) def authorize_list_type_resource!(board, params)
super super
...@@ -27,7 +26,7 @@ module EE ...@@ -27,7 +26,7 @@ module EE
if params[:milestone_id] if params[:milestone_id]
milestones = ::Boards::MilestonesFinder.new(board, current_user).execute milestones = ::Boards::MilestonesFinder.new(board, current_user).execute
unless milestones.where(id: params[:milestone_id]).exists? unless milestones.id_in(params[:milestone_id]).exists?
raise ::Gitlab::Graphql::Errors::ArgumentError, 'Milestone not found!' raise ::Gitlab::Graphql::Errors::ArgumentError, 'Milestone not found!'
end end
end end
...@@ -35,12 +34,11 @@ module EE ...@@ -35,12 +34,11 @@ module EE
if params[:assignee_id] if params[:assignee_id]
users = ::Boards::UsersFinder.new(board, current_user).execute users = ::Boards::UsersFinder.new(board, current_user).execute
unless users.where(user_id: params[:assignee_id]).exists? unless users.with_user(params[:assignee_id]).exists?
raise ::Gitlab::Graphql::Errors::ArgumentError, 'User not found!' raise ::Gitlab::Graphql::Errors::ArgumentError, 'User not found!'
end end
end end
end end
# rubocop: enable CodeReuse/ActiveRecord
override :create_list_params override :create_list_params
def create_list_params(args) def create_list_params(args)
......
...@@ -118,11 +118,14 @@ module EE ...@@ -118,11 +118,14 @@ module EE
strong_memoize(:security_report) do strong_memoize(:security_report) do
next unless file_type.in?(SECURITY_REPORT_FILE_TYPES) next unless file_type.in?(SECURITY_REPORT_FILE_TYPES)
::Gitlab::Ci::Reports::Security::Report.new(file_type, nil, nil).tap do |report| report = ::Gitlab::Ci::Reports::Security::Report.new(file_type, nil, nil).tap do |report|
each_blob do |blob| each_blob do |blob|
::Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, report) ::Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, report)
end end
end end
# This will remove the duplicated findings within the artifact itself
::Security::MergeReportsService.new(report).execute
end end
end end
......
...@@ -20,8 +20,9 @@ module Security ...@@ -20,8 +20,9 @@ module Security
enum severity: Vulnerabilities::Finding::SEVERITY_LEVELS, _prefix: :severity enum severity: Vulnerabilities::Finding::SEVERITY_LEVELS, _prefix: :severity
validates :project_fingerprint, presence: true, length: { maximum: 40 } validates :project_fingerprint, presence: true, length: { maximum: 40 }
validates :position, presence: true
scope :by_project_fingerprint, -> (fingerprints) { where(project_fingerprint: fingerprints) } scope :by_position, -> (positions) { where(position: positions) }
scope :by_build_ids, -> (build_ids) { joins(scan: :build).where(ci_builds: { id: build_ids }) } scope :by_build_ids, -> (build_ids) { joins(scan: :build).where(ci_builds: { id: build_ids }) }
end end
end end
...@@ -31,21 +31,22 @@ module Security ...@@ -31,21 +31,22 @@ module Security
end end
def store_findings def store_findings
report_findings.each { |report_finding| store_finding!(report_finding) } report_findings.each_with_index { |report_finding, position| store_finding!(report_finding, position) }
end end
def store_finding!(report_finding) def store_finding!(report_finding, position)
return if report_finding.scanner.blank? return if report_finding.scanner.blank?
security_scan.findings.create!(finding_data(report_finding)) security_scan.findings.create!(finding_data(report_finding, position))
end end
def finding_data(report_finding) def finding_data(report_finding, position)
{ {
severity: report_finding.severity, severity: report_finding.severity,
confidence: report_finding.confidence, confidence: report_finding.confidence,
project_fingerprint: report_finding.project_fingerprint, project_fingerprint: report_finding.project_fingerprint,
scanner: persisted_scanner_for(report_finding.scanner) scanner: persisted_scanner_for(report_finding.scanner),
position: position
} }
end end
......
...@@ -43,21 +43,19 @@ module Security ...@@ -43,21 +43,19 @@ module Security
security_scan.findings.update_all(deduplicated: false) security_scan.findings.update_all(deduplicated: false)
security_scan.findings security_scan.findings
.by_project_fingerprint(deduplicated_project_fingerprints) .by_position(register_finding_keys)
.update_all(deduplicated: true) .update_all(deduplicated: true)
end end
end end
def deduplicated_project_fingerprints # This method registers all finding keys and
register_finding_keys.map(&:project_fingerprint) # returns the positions of unique findings
end
def register_finding_keys def register_finding_keys
@register_finding_keys ||= security_report.findings.select { |finding| register_keys(finding.keys) } @register_finding_keys ||= security_report.findings.map.with_index { |finding, index| register_keys(finding.keys) && index }.compact
end end
def register_keys(keys) def register_keys(keys)
keys.map { |key| known_keys.add?(key) }.all? keys.all? { |key| known_keys.add?(key) }
end end
end end
end end
- if @group.ldap_sync_started? - if @group.ldap_sync_started?
%span.btn.disabled %span.btn.gl-button.disabled
= icon("refresh spin") = sprite_icon("retry", css_class: 'spin gl-mr-2')
Syncing&hellip; = _('Syncing…')
- elsif @group.ldap_sync_pending? - elsif @group.ldap_sync_pending?
%span.btn.disabled %span.btn.gl-button.disabled
= icon("refresh spin") = sprite_icon("retry", css_class: 'spin gl-mr-2')
Pending sync&hellip; = ('Pending sync…')
- else - else
= link_to sync_group_ldap_path(@group), method: :put, class: 'btn qa-sync-now-button' do = link_to sync_group_ldap_path(@group), method: :put, class: 'btn btn-default gl-button qa-sync-now-button' do
= icon("refresh") = sprite_icon("retry", css_class: "gl-mr-2")
Sync now = _('Sync now')
- if @group.ldap_sync_ready? && @group.ldap_sync_last_successful_update_at - if @group.ldap_sync_ready? && @group.ldap_sync_last_successful_update_at
%p.inline.gl-ml-3 %p.inline.gl-ml-3
Successfully synced #{time_ago_with_tooltip(@group.ldap_sync_last_successful_update_at)}. = _('Successfully synced %{synced_timeago}.').html_safe % { synced_timeago: time_ago_with_tooltip(@group.ldap_sync_last_successful_update_at) }
...@@ -21,10 +21,10 @@ ...@@ -21,10 +21,10 @@
- if ssh_public_key - if ssh_public_key
= clipboard_button(text: ssh_public_key, class: 'btn btn-default rspec-copy-ssh-public-key', title: _('Copy SSH public key'), qa_selector: 'copy_public_key_button') = clipboard_button(text: ssh_public_key, class: 'btn btn-default rspec-copy-ssh-public-key', title: _('Copy SSH public key'), qa_selector: 'copy_public_key_button')
- if import_state.mirror_update_due? || import_state.updating_mirror? - if import_state.mirror_update_due? || import_state.updating_mirror?
%button.btn.disabled{ type: 'button', data: { container: 'body', toggle: 'tooltip', qa_selector: 'updating_button' }, title: _('Updating') }= icon("refresh spin") %button.btn.btn-icon.gl-button.disabled{ type: 'button', data: { container: 'body', toggle: 'tooltip', qa_selector: 'updating_button' }, title: _('Updating') }= sprite_icon("retry", css_class: "spin")
- elsif @project.archived? - elsif @project.archived?
%button.btn.disabled{ type: 'button', data: { container: 'body', toggle: 'tooltip', qa_selector: 'update_now_button' }, title: _('This Project is currently archived and read-only. Please unarchive the project first if you want to resume Pull mirroring') }= icon("refresh") %button.btn.btn-icon.gl-button.disabled{ type: 'button', data: { container: 'body', toggle: 'tooltip', qa_selector: 'update_now_button' }, title: _('This Project is currently archived and read-only. Please unarchive the project first if you want to resume Pull mirroring') }= sprite_icon("retry")
- else - else
= link_to update_now_project_mirror_path(@project), method: :post, class: 'btn js-force-update-mirror', data: { container: 'body', toggle: 'tooltip', qa_selector: 'update_now_button' }, title: _('Update now') do = link_to update_now_project_mirror_path(@project), method: :post, class: 'btn gl-button btn-icon js-force-update-mirror', data: { container: 'body', toggle: 'tooltip', qa_selector: 'update_now_button' }, title: _('Update now') do
= icon("refresh") = sprite_icon("retry")
%button.js-delete-mirror.js-delete-pull-mirror.btn.btn-danger{ type: 'button', data: { toggle: 'tooltip', container: 'body' }, title: _('Remove') }= sprite_icon('remove') %button.js-delete-mirror.js-delete-pull-mirror.btn.btn-icon.gl-button.btn-danger{ type: 'button', data: { toggle: 'tooltip', container: 'body' }, title: _('Remove') }= sprite_icon('remove')
...@@ -2,20 +2,20 @@ ...@@ -2,20 +2,20 @@
.gl-mb-3 .gl-mb-3
- if @project.import_state.mirror_update_due? - if @project.import_state.mirror_update_due?
%span.btn.disabled %span.btn.disabled
= icon("refresh spin") = sprite_icon('retry', css_class: 'spin')
Update Scheduled&hellip; = ('Update Scheduled…')
- elsif @project.import_state.updating_mirror? - elsif @project.import_state.updating_mirror?
%span.btn.disabled %span.btn.disabled
= icon("refresh spin") = sprite_icon('retry', css_class: 'spin')
Updating&hellip; = ('Updating…')
- elsif can?(current_user, :admin_project, @project) - elsif can?(current_user, :admin_project, @project)
= link_to update_now_project_mirror_path(@project), method: :post, class: 'btn' do = link_to update_now_project_mirror_path(@project), method: :post, class: 'btn' do
= icon("refresh") = sprite_icon('retry')
Update Now = ('Update Now')
- else - else
%span.btn.disabled %span.btn.disabled
= icon("refresh") = sprite_icon('retry')
Update Now = ('Update Now')
- if @project.mirror_last_update_succeeded? - if @project.mirror_last_update_succeeded?
%p.inline.gl-ml-3 %p.inline.gl-ml-3
Successfully updated #{time_ago_with_tooltip(@project.import_state.last_successful_update_at)}. = ('Successfully updated %{last_updated_timeago}.').html_safe % { last_updated_timeago: time_ago_with_tooltip(@project.import_state.last_successful_update_at) }
...@@ -41,29 +41,29 @@ module EE ...@@ -41,29 +41,29 @@ module EE
end end
# Overrides API::BoardsResponses authorize_list_type_resource! # Overrides API::BoardsResponses authorize_list_type_resource!
# rubocop: disable CodeReuse/ActiveRecord
def authorize_list_type_resource! def authorize_list_type_resource!
# rubocop: disable CodeReuse/ActiveRecord
if params[:label_id] && !available_labels_for(board_parent).exists?(params[:label_id]) if params[:label_id] && !available_labels_for(board_parent).exists?(params[:label_id])
render_api_error!({ error: 'Label not found!' }, 400) render_api_error!({ error: 'Label not found!' }, 400)
end end
# rubocop: enable CodeReuse/ActiveRecord
if milestone_id = params[:milestone_id] if params[:milestone_id]
milestones = ::Boards::MilestonesFinder.new(board, current_user).execute milestones = ::Boards::MilestonesFinder.new(board, current_user).execute
unless milestones.find_by(id: milestone_id) unless milestones.id_in(params[:milestone_id]).exists?
render_api_error!({ error: 'Milestone not found!' }, 400) render_api_error!({ error: 'Milestone not found!' }, 400)
end end
end end
if assignee_id = params[:assignee_id] if params[:assignee_id]
users = ::Boards::UsersFinder.new(board, current_user).execute users = ::Boards::UsersFinder.new(board, current_user).execute
unless users.find_by(user_id: assignee_id) unless users.with_user(params[:assignee_id]).exists?
render_api_error!({ error: 'User not found!' }, 400) render_api_error!({ error: 'User not found!' }, 400)
end end
end end
end end
# rubocop: enable CodeReuse/ActiveRecord
# Overrides API::BoardsResponses list_creation_params # Overrides API::BoardsResponses list_creation_params
params :list_creation_params do params :list_creation_params do
......
...@@ -59,16 +59,16 @@ RSpec.describe Groups::Analytics::CoverageReportsController do ...@@ -59,16 +59,16 @@ RSpec.describe Groups::Analytics::CoverageReportsController do
stub_licensed_features(group_coverage_reports: true) stub_licensed_features(group_coverage_reports: true)
end end
it 'responds 200 with CSV coverage data' do it 'responds 200 with CSV coverage data', :snowplow do
expect(Gitlab::Tracking).to receive(:event).with( get :index, params: valid_request_params
described_class.name,
'download_code_coverage_csv', expect_snowplow_event(
category: described_class.name,
action: 'download_code_coverage_csv',
label: 'group_id', label: 'group_id',
value: group.id value: group.id
) )
get :index, params: valid_request_params
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(csv_response).to eq([ expect(csv_response).to eq([
%w[date group_name project_name coverage], %w[date group_name project_name coverage],
......
...@@ -148,15 +148,15 @@ RSpec.describe RegistrationsController do ...@@ -148,15 +148,15 @@ RSpec.describe RegistrationsController do
update_registration update_registration
end end
it 'tracks a signed_up event' do it 'tracks a signed_up event', :snowplow do
expect(Gitlab::Tracking).to receive(:event).with( update_registration
'Growth::Conversion::Experiment::OnboardingIssues',
'signed_up', expect_snowplow_event(
category: 'Growth::Conversion::Experiment::OnboardingIssues',
action: 'signed_up',
label: anything, label: anything,
property: "#{group_type}_group" property: "#{group_type}_group"
) )
update_registration
end end
end end
end end
...@@ -176,10 +176,10 @@ RSpec.describe RegistrationsController do ...@@ -176,10 +176,10 @@ RSpec.describe RegistrationsController do
update_registration update_registration
end end
it 'does not track a signed_up event' do it 'does not track a signed_up event', :snowplow do
expect(Gitlab::Tracking).not_to receive(:event)
update_registration update_registration
expect_no_snowplow_event
end end
end end
end end
...@@ -196,10 +196,10 @@ RSpec.describe RegistrationsController do ...@@ -196,10 +196,10 @@ RSpec.describe RegistrationsController do
update_registration update_registration
end end
it 'does not track a signed_up event' do it 'does not track a signed_up event', :snowplow do
expect(Gitlab::Tracking).not_to receive(:event)
update_registration update_registration
expect_no_snowplow_event
end end
end end
end end
......
...@@ -8,5 +8,6 @@ FactoryBot.define do ...@@ -8,5 +8,6 @@ FactoryBot.define do
severity { :critical } severity { :critical }
confidence { :high } confidence { :high }
project_fingerprint { generate(:project_fingerprint) } project_fingerprint { generate(:project_fingerprint) }
sequence :position
end end
end end
...@@ -41,7 +41,7 @@ RSpec.describe 'Merge requests > User merges immediately', :js do ...@@ -41,7 +41,7 @@ RSpec.describe 'Merge requests > User merges immediately', :js do
def open_warning_dialog def open_warning_dialog
find('.mr-widget-body .dropdown-toggle').click find('.mr-widget-body .dropdown-toggle').click
click_link 'Merge immediately' click_button 'Merge immediately'
expect(page).to have_selector('#merge-immediately-confirmation-dialog') expect(page).to have_selector('#merge-immediately-confirmation-dialog')
end end
......
import { shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import { MERGE_DISABLED_TEXT_UNAPPROVED } from 'ee/vue_merge_request_widget/mixins/ready_to_merge'; import { MERGE_DISABLED_TEXT_UNAPPROVED } from 'ee/vue_merge_request_widget/mixins/ready_to_merge';
import MergeImmediatelyConfirmationDialog from 'ee/vue_merge_request_widget/components/merge_immediately_confirmation_dialog.vue'; import MergeImmediatelyConfirmationDialog from 'ee/vue_merge_request_widget/components/merge_immediately_confirmation_dialog.vue';
import MergeTrainHelperText from 'ee/vue_merge_request_widget/components/merge_train_helper_text.vue'; import MergeTrainHelperText from 'ee/vue_merge_request_widget/components/merge_train_helper_text.vue';
...@@ -57,8 +57,9 @@ describe('ReadyToMerge', () => { ...@@ -57,8 +57,9 @@ describe('ReadyToMerge', () => {
mergeTrainsCount: 0, mergeTrainsCount: 0,
}; };
const factory = (mrUpdates = {}) => { const factory = (mrUpdates = {}, shallow = true) => {
wrapper = shallowMount(ReadyToMerge, { const func = shallow ? shallowMount : mount;
wrapper = func(ReadyToMerge, {
propsData: { propsData: {
mr: { ...mr, ...mrUpdates }, mr: { ...mr, ...mrUpdates },
service, service,
...@@ -337,7 +338,7 @@ describe('ReadyToMerge', () => { ...@@ -337,7 +338,7 @@ describe('ReadyToMerge', () => {
}; };
it('should show a warning dialog asking for confirmation if the user is trying to skip the merge train', () => { it('should show a warning dialog asking for confirmation if the user is trying to skip the merge train', () => {
factory({ preferredAutoMergeStrategy: MT_MERGE_STRATEGY }); factory({ preferredAutoMergeStrategy: MT_MERGE_STRATEGY }, false);
return clickMergeImmediately().then(() => { return clickMergeImmediately().then(() => {
expect(dialog.vm.show).toHaveBeenCalled(); expect(dialog.vm.show).toHaveBeenCalled();
expect(vm.handleMergeButtonClick).not.toHaveBeenCalled(); expect(vm.handleMergeButtonClick).not.toHaveBeenCalled();
...@@ -345,7 +346,7 @@ describe('ReadyToMerge', () => { ...@@ -345,7 +346,7 @@ describe('ReadyToMerge', () => {
}); });
it('should perform the merge when the user confirms their intent to merge immediately', () => { it('should perform the merge when the user confirms their intent to merge immediately', () => {
factory({ preferredAutoMergeStrategy: MT_MERGE_STRATEGY }); factory({ preferredAutoMergeStrategy: MT_MERGE_STRATEGY }, false);
return clickMergeImmediately() return clickMergeImmediately()
.then(() => { .then(() => {
dialog.vm.$emit('mergeImmediately'); dialog.vm.$emit('mergeImmediately');
...@@ -357,10 +358,13 @@ describe('ReadyToMerge', () => { ...@@ -357,10 +358,13 @@ describe('ReadyToMerge', () => {
}); });
it('should not ask for confirmation in non-merge train scenarios', () => { it('should not ask for confirmation in non-merge train scenarios', () => {
factory({ factory(
{
isPipelineActive: true, isPipelineActive: true,
onlyAllowMergeIfPipelineSucceeds: false, onlyAllowMergeIfPipelineSucceeds: false,
}); },
false,
);
return clickMergeImmediately().then(() => { return clickMergeImmediately().then(() => {
expect(dialog.vm.show).not.toHaveBeenCalled(); expect(dialog.vm.show).not.toHaveBeenCalled();
expect(vm.handleMergeButtonClick).toHaveBeenCalled(); expect(vm.handleMergeButtonClick).toHaveBeenCalled();
......
...@@ -256,7 +256,9 @@ RSpec.describe Ci::JobArtifact do ...@@ -256,7 +256,9 @@ RSpec.describe Ci::JobArtifact do
clear_security_report clear_security_report
job_artifact.security_report job_artifact.security_report
expect(::Gitlab::Ci::Reports::Security::Report).to have_received(:new).once # This entity class receives the call twice
# because of the way MergeReportsService is implemented.
expect(::Gitlab::Ci::Reports::Security::Report).to have_received(:new).twice
end end
end end
end end
...@@ -10,15 +10,16 @@ RSpec.describe Security::Finding do ...@@ -10,15 +10,16 @@ RSpec.describe Security::Finding do
describe 'validations' do describe 'validations' do
it { is_expected.to validate_presence_of(:project_fingerprint) } it { is_expected.to validate_presence_of(:project_fingerprint) }
it { is_expected.to validate_presence_of(:position) }
it { is_expected.to validate_length_of(:project_fingerprint).is_at_most(40) } it { is_expected.to validate_length_of(:project_fingerprint).is_at_most(40) }
end end
describe '.by_project_fingerprint' do describe '.by_position' do
let!(:finding_1) { create(:security_finding) } let!(:finding_1) { create(:security_finding, position: 0) }
let!(:finding_2) { create(:security_finding) } let!(:finding_2) { create(:security_finding, position: 1) }
let(:expected_findings) { [finding_1] } let(:expected_findings) { [finding_1] }
subject { described_class.by_project_fingerprint(finding_1.project_fingerprint) } subject { described_class.by_position(finding_1.position) }
it { is_expected.to match_array(expected_findings) } it { is_expected.to match_array(expected_findings) }
end end
......
...@@ -58,9 +58,10 @@ RSpec.describe API::VisualReviewDiscussions do ...@@ -58,9 +58,10 @@ RSpec.describe API::VisualReviewDiscussions do
stub_feature_flags(notes_create_service_tracking: false) stub_feature_flags(notes_create_service_tracking: false)
end end
it 'does not track any events' do it 'does not track any events', :snowplow do
expect(Gitlab::Tracking).not_to receive(:event)
request request
expect_no_snowplow_event
end end
end end
......
...@@ -5,12 +5,13 @@ require 'spec_helper' ...@@ -5,12 +5,13 @@ require 'spec_helper'
RSpec.describe Security::StoreFindingsMetadataService do RSpec.describe Security::StoreFindingsMetadataService do
let_it_be(:security_scan) { create(:security_scan) } let_it_be(:security_scan) { create(:security_scan) }
let_it_be(:project) { security_scan.project } let_it_be(:project) { security_scan.project }
let_it_be(:security_finding) { build(:ci_reports_security_finding) } let_it_be(:security_finding_1) { build(:ci_reports_security_finding) }
let_it_be(:security_finding_2) { build(:ci_reports_security_finding) }
let_it_be(:security_scanner) { build(:ci_reports_security_scanner) } let_it_be(:security_scanner) { build(:ci_reports_security_scanner) }
let_it_be(:report) do let_it_be(:report) do
build( build(
:ci_reports_security_report, :ci_reports_security_report,
findings: [security_finding], findings: [security_finding_1, security_finding_2],
scanners: [security_scanner] scanners: [security_scanner]
) )
end end
...@@ -36,10 +37,12 @@ RSpec.describe Security::StoreFindingsMetadataService do ...@@ -36,10 +37,12 @@ RSpec.describe Security::StoreFindingsMetadataService do
end end
it 'creates the security finding entries in database' do it 'creates the security finding entries in database' do
expect { store_findings }.to change { security_scan.findings.count }.by(1) expect { store_findings }.to change { security_scan.findings.count }.by(2)
.and change { security_scan.findings.last&.severity }.to(security_finding.severity.to_s) .and change { security_scan.findings.first&.severity }.to(security_finding_1.severity.to_s)
.and change { security_scan.findings.last&.confidence }.to(security_finding.confidence.to_s) .and change { security_scan.findings.first&.confidence }.to(security_finding_1.confidence.to_s)
.and change { security_scan.findings.last&.project_fingerprint }.to(security_finding.project_fingerprint) .and change { security_scan.findings.first&.project_fingerprint }.to(security_finding_1.project_fingerprint)
.and change { security_scan.findings.first&.position }.to(0)
.and change { security_scan.findings.last&.position }.to(1)
end end
context 'when the scanners already exist in the database' do context 'when the scanners already exist in the database' do
......
...@@ -49,16 +49,16 @@ RSpec.describe Security::StoreScanService do ...@@ -49,16 +49,16 @@ RSpec.describe Security::StoreScanService do
context 'when the security scan already exists for the artifact' do context 'when the security scan already exists for the artifact' do
let_it_be(:security_scan) { create(:security_scan, build: artifact.job, scan_type: :sast) } let_it_be(:security_scan) { create(:security_scan, build: artifact.job, scan_type: :sast) }
let_it_be(:duplicated_security_finding) do let_it_be(:unique_security_finding) do
create(:security_finding, create(:security_finding,
scan: security_scan, scan: security_scan,
project_fingerprint: 'd533c3a12403b6c6033a50b53f9c73f894a40fc6') position: 0)
end end
let_it_be(:unique_security_finding) do let_it_be(:duplicated_security_finding) do
create(:security_finding, create(:security_finding,
scan: security_scan, scan: security_scan,
project_fingerprint: 'b9c0d1cdc7cb9c180ebb6981abbddc2df0172509') position: 5)
end end
it 'does not create a new security scan' do it 'does not create a new security scan' do
...@@ -89,12 +89,12 @@ RSpec.describe Security::StoreScanService do ...@@ -89,12 +89,12 @@ RSpec.describe Security::StoreScanService do
end end
context 'when the security scan does not exist for the artifact' do context 'when the security scan does not exist for the artifact' do
let(:duplicated_finding_attribute) do let(:unique_finding_attribute) do
-> { Security::Finding.by_project_fingerprint('d533c3a12403b6c6033a50b53f9c73f894a40fc6').first&.deduplicated } -> { Security::Finding.by_position(0).first&.deduplicated }
end end
let(:unique_finding_attribute) do let(:duplicated_finding_attribute) do
-> { Security::Finding.by_project_fingerprint('b9c0d1cdc7cb9c180ebb6981abbddc2df0172509').first&.deduplicated } -> { Security::Finding.by_position(5).first&.deduplicated }
end end
before do before do
......
...@@ -6,6 +6,8 @@ module Gitlab ...@@ -6,6 +6,8 @@ module Gitlab
NOT_BEFORE_TIME = 5 NOT_BEFORE_TIME = 5
DEFAULT_EXPIRE_TIME = 60 * 5 DEFAULT_EXPIRE_TIME = 60 * 5
NoSigningKeyError = Class.new(StandardError)
def self.for_build(build) def self.for_build(build)
self.new(build, ttl: build.metadata_timeout).encoded self.new(build, ttl: build.metadata_timeout).encoded
end end
...@@ -27,7 +29,7 @@ module Gitlab ...@@ -27,7 +29,7 @@ module Gitlab
private private
attr_reader :build, :ttl, :key_data attr_reader :build, :ttl
def reserved_claims def reserved_claims
now = Time.now.to_i now = Time.now.to_i
...@@ -60,7 +62,17 @@ module Gitlab ...@@ -60,7 +62,17 @@ module Gitlab
end end
def key def key
@key ||= OpenSSL::PKey::RSA.new(Rails.application.secrets.openid_connect_signing_key) @key ||= begin
key_data = if Feature.enabled?(:ci_jwt_signing_key, build.project)
Gitlab::CurrentSettings.ci_jwt_signing_key
else
Rails.application.secrets.openid_connect_signing_key
end
raise NoSigningKeyError unless key_data
OpenSSL::PKey::RSA.new(key_data)
end
end end
def public_key def public_key
......
...@@ -44,7 +44,7 @@ module Gitlab ...@@ -44,7 +44,7 @@ module Gitlab
end end
def unique_events(event_names:, start_date:, end_date:) def unique_events(event_names:, start_date:, end_date:)
events = events_for(Array(event_names)) events = events_for(Array(event_names).map(&:to_s))
raise 'Events should be in same slot' unless events_in_same_slot?(events) raise 'Events should be in same slot' unless events_in_same_slot?(events)
raise 'Events should be in same category' unless events_in_same_category?(events) raise 'Events should be in same category' unless events_in_same_category?(events)
...@@ -141,7 +141,7 @@ module Gitlab ...@@ -141,7 +141,7 @@ module Gitlab
end end
def event_for(event_name) def event_for(event_name)
known_events.find { |event| event[:name] == event_name } known_events.find { |event| event[:name] == event_name.to_s }
end end
def events_for(event_names) def events_for(event_names)
......
...@@ -25558,6 +25558,9 @@ msgstr "" ...@@ -25558,6 +25558,9 @@ msgstr ""
msgid "Successfully scheduled a pipeline to run. Go to the %{pipelines_link_start}Pipelines page%{pipelines_link_end} for details." msgid "Successfully scheduled a pipeline to run. Go to the %{pipelines_link_start}Pipelines page%{pipelines_link_end} for details."
msgstr "" msgstr ""
msgid "Successfully synced %{synced_timeago}."
msgstr ""
msgid "Successfully unblocked" msgid "Successfully unblocked"
msgstr "" msgstr ""
...@@ -25693,12 +25696,18 @@ msgstr "" ...@@ -25693,12 +25696,18 @@ msgstr ""
msgid "Sync information" msgid "Sync information"
msgstr "" msgstr ""
msgid "Sync now"
msgstr ""
msgid "Synced" msgid "Synced"
msgstr "" msgstr ""
msgid "Synchronization disabled" msgid "Synchronization disabled"
msgstr "" msgstr ""
msgid "Syncing…"
msgstr ""
msgid "System" msgid "System"
msgstr "" msgstr ""
......
...@@ -23,7 +23,6 @@ module QA ...@@ -23,7 +23,6 @@ module QA
element :merge_button element :merge_button
element :fast_forward_message, 'Fast-forward merge without a merge commit' # rubocop:disable QA/ElementWithPattern element :fast_forward_message, 'Fast-forward merge without a merge commit' # rubocop:disable QA/ElementWithPattern
element :merge_moment_dropdown element :merge_moment_dropdown
element :merge_when_pipeline_succeeds_option
element :merge_immediately_option element :merge_immediately_option
end end
......
...@@ -319,10 +319,10 @@ RSpec.describe GroupsController, factory_default: :keep do ...@@ -319,10 +319,10 @@ RSpec.describe GroupsController, factory_default: :keep do
stub_experiment(onboarding_issues: false) stub_experiment(onboarding_issues: false)
end end
it 'does not track anything' do it 'does not track anything', :snowplow do
expect(Gitlab::Tracking).not_to receive(:event)
create_namespace create_namespace
expect_no_snowplow_event
end end
end end
...@@ -336,15 +336,15 @@ RSpec.describe GroupsController, factory_default: :keep do ...@@ -336,15 +336,15 @@ RSpec.describe GroupsController, factory_default: :keep do
stub_experiment_for_user(onboarding_issues: false) stub_experiment_for_user(onboarding_issues: false)
end end
it 'tracks the event with the "created_namespace" action with the "control_group" property' do it 'tracks the event with the "created_namespace" action with the "control_group" property', :snowplow do
expect(Gitlab::Tracking).to receive(:event).with( create_namespace
'Growth::Conversion::Experiment::OnboardingIssues',
'created_namespace', expect_snowplow_event(
category: 'Growth::Conversion::Experiment::OnboardingIssues',
action: 'created_namespace',
label: anything, label: anything,
property: 'control_group' property: 'control_group'
) )
create_namespace
end end
end end
...@@ -353,15 +353,15 @@ RSpec.describe GroupsController, factory_default: :keep do ...@@ -353,15 +353,15 @@ RSpec.describe GroupsController, factory_default: :keep do
stub_experiment_for_user(onboarding_issues: true) stub_experiment_for_user(onboarding_issues: true)
end end
it 'tracks the event with the "created_namespace" action with the "experimental_group" property' do it 'tracks the event with the "created_namespace" action with the "experimental_group" property', :snowplow do
expect(Gitlab::Tracking).to receive(:event).with( create_namespace
'Growth::Conversion::Experiment::OnboardingIssues',
'created_namespace', expect_snowplow_event(
category: 'Growth::Conversion::Experiment::OnboardingIssues',
action: 'created_namespace',
label: anything, label: anything,
property: 'experimental_group' property: 'experimental_group'
) )
create_namespace
end end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe JwksController do
describe 'GET #index' do
let(:ci_jwt_signing_key) { OpenSSL::PKey::RSA.generate(1024) }
let(:ci_jwk) { ci_jwt_signing_key.to_jwk }
let(:oidc_jwk) { OpenSSL::PKey::RSA.new(Rails.application.secrets.openid_connect_signing_key).to_jwk }
before do
stub_application_setting(ci_jwt_signing_key: ci_jwt_signing_key.to_s)
end
it 'returns signing keys used to sign CI_JOB_JWT' do
get :index
expect(response).to have_gitlab_http_status(:ok)
ids = json_response['keys'].map { |jwk| jwk['kid'] }
expect(ids).to contain_exactly(ci_jwk['kid'], oidc_jwk['kid'])
end
it 'does not leak private key data' do
get :index
aggregate_failures do
json_response['keys'].each do |jwk|
expect(jwk.keys).to contain_exactly('kty', 'kid', 'e', 'n', 'use', 'alg')
expect(jwk['use']).to eq('sig')
expect(jwk['alg']).to eq('RS256')
end
end
end
end
end
...@@ -62,4 +62,11 @@ RSpec.describe 'seed production settings' do ...@@ -62,4 +62,11 @@ RSpec.describe 'seed production settings' do
end end
end end
end end
context 'CI JWT signing key' do
it 'writes valid RSA key to the database' do
expect { load(settings_file) }.to change { settings.reload.ci_jwt_signing_key }.from(nil)
expect { OpenSSL::PKey::RSA.new(settings.ci_jwt_signing_key) }.not_to raise_error
end
end
end end
...@@ -34,7 +34,7 @@ RSpec.describe 'Merge requests > User merges immediately', :js do ...@@ -34,7 +34,7 @@ RSpec.describe 'Merge requests > User merges immediately', :js do
find('.dropdown-toggle').click find('.dropdown-toggle').click
Sidekiq::Testing.fake! do Sidekiq::Testing.fake! do
click_link 'Merge immediately' click_button 'Merge immediately'
expect(find('.accept-merge-request.btn-info')).to have_content('Merge in progress') expect(find('.accept-merge-request.btn-info')).to have_content('Merge in progress')
......
...@@ -93,19 +93,6 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do ...@@ -93,19 +93,6 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do
it_behaves_like 'Merge when pipeline succeeds activator' it_behaves_like 'Merge when pipeline succeeds activator'
end end
end end
describe 'enabling Merge when pipeline succeeds via dropdown' do
it 'activates the Merge when pipeline succeeds feature' do
wait_for_requests
find('.js-merge-moment').click
click_link 'Merge when pipeline succeeds'
expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds"
expect(page).to have_content "The source branch will not be deleted"
expect(page).to have_link "Cancel automatic merge"
end
end
end end
context 'when merge when pipeline succeeds is enabled' do context 'when merge when pipeline succeeds is enabled' do
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { GlFormInput, GlFormTextarea } from '@gitlab/ui'; import { GlFormInput, GlFormTextarea } from '@gitlab/ui';
import EditMetaControls from '~/static_site_editor/components/edit_meta_controls.vue'; import EditMetaControls from '~/static_site_editor/components/edit_meta_controls.vue';
...@@ -8,8 +7,6 @@ import EditMetaControls from '~/static_site_editor/components/edit_meta_controls ...@@ -8,8 +7,6 @@ import EditMetaControls from '~/static_site_editor/components/edit_meta_controls
import { mergeRequestMeta } from '../mock_data'; import { mergeRequestMeta } from '../mock_data';
describe('~/static_site_editor/components/edit_meta_controls.vue', () => { describe('~/static_site_editor/components/edit_meta_controls.vue', () => {
useLocalStorageSpy();
let wrapper; let wrapper;
let mockSelect; let mockSelect;
let mockGlFormInputTitleInstance; let mockGlFormInputTitleInstance;
...@@ -86,14 +83,5 @@ describe('~/static_site_editor/components/edit_meta_controls.vue', () => { ...@@ -86,14 +83,5 @@ describe('~/static_site_editor/components/edit_meta_controls.vue', () => {
expect(wrapper.emitted('updateSettings')[0][0]).toMatchObject(newSettings); expect(wrapper.emitted('updateSettings')[0][0]).toMatchObject(newSettings);
}); });
it('should remember the input changes', () => {
findGlFormInputTitle().vm.$emit('input', newTitle);
findGlFormTextAreaDescription().vm.$emit('input', newDescription);
const newSettings = { title: newTitle, description: newDescription };
expect(localStorage.setItem).toHaveBeenCalledWith(storageKey, JSON.stringify(newSettings));
});
}); });
}); });
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui'; import { GlModal } from '@gitlab/ui';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import EditMetaModal from '~/static_site_editor/components/edit_meta_modal.vue'; import EditMetaModal from '~/static_site_editor/components/edit_meta_modal.vue';
import EditMetaControls from '~/static_site_editor/components/edit_meta_controls.vue'; import EditMetaControls from '~/static_site_editor/components/edit_meta_controls.vue';
import { MR_META_LOCAL_STORAGE_KEY } from '~/static_site_editor/constants';
import { sourcePath, mergeRequestMeta } from '../mock_data'; import { sourcePath, mergeRequestMeta } from '../mock_data';
describe('~/static_site_editor/components/edit_meta_modal.vue', () => { describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
useLocalStorageSpy();
let wrapper; let wrapper;
let resetCachedEditable; let resetCachedEditable;
let mockEditMetaControlsInstance; let mockEditMetaControlsInstance;
...@@ -30,6 +32,11 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => { ...@@ -30,6 +32,11 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
const findGlModal = () => wrapper.find(GlModal); const findGlModal = () => wrapper.find(GlModal);
const findEditMetaControls = () => wrapper.find(EditMetaControls); const findEditMetaControls = () => wrapper.find(EditMetaControls);
const findLocalStorageSync = () => wrapper.find(LocalStorageSync);
beforeEach(() => {
localStorage.setItem(MR_META_LOCAL_STORAGE_KEY);
});
beforeEach(() => { beforeEach(() => {
buildWrapper(); buildWrapper();
...@@ -43,6 +50,16 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => { ...@@ -43,6 +50,16 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
wrapper = null; wrapper = null;
}); });
it('initializes initial merge request meta with local storage data', async () => {
const localStorageMeta = { title: 'stored title', description: 'stored description' };
findLocalStorageSync().vm.$emit('input', localStorageMeta);
await wrapper.vm.$nextTick();
expect(findEditMetaControls().props()).toEqual(localStorageMeta);
});
it('renders the modal', () => { it('renders the modal', () => {
expect(findGlModal().exists()).toBe(true); expect(findGlModal().exists()).toBe(true);
}); });
...@@ -63,18 +80,32 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => { ...@@ -63,18 +80,32 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
expect(findEditMetaControls().props('description')).toBe(description); expect(findEditMetaControls().props('description')).toBe(description);
}); });
it('emits the primary event with mergeRequestMeta', () => { describe('when save button is clicked', () => {
beforeEach(() => {
findGlModal().vm.$emit('primary', mergeRequestMeta); findGlModal().vm.$emit('primary', mergeRequestMeta);
expect(wrapper.emitted('primary')).toEqual([[mergeRequestMeta]]);
}); });
it('calls resetCachedEditable on EditMetaControls when primary emits', () => { it('removes merge request meta from local storage', () => {
findGlModal().vm.$emit('primary', mergeRequestMeta); expect(findLocalStorageSync().props().clear).toBe(true);
expect(mockEditMetaControlsInstance.resetCachedEditable).toHaveBeenCalled(); });
it('emits the primary event with mergeRequestMeta', () => {
expect(wrapper.emitted('primary')).toEqual([[mergeRequestMeta]]);
});
}); });
it('emits the hide event', () => { it('emits the hide event', () => {
findGlModal().vm.$emit('hide'); findGlModal().vm.$emit('hide');
expect(wrapper.emitted('hide')).toEqual([[]]); expect(wrapper.emitted('hide')).toEqual([[]]);
}); });
it('stores merge request meta changes in local storage when changes happen', async () => {
const newMeta = { title: 'new title', description: 'new description' };
findEditMetaControls().vm.$emit('updateSettings', newMeta);
await wrapper.vm.$nextTick();
expect(findLocalStorageSync().props('value')).toEqual(newMeta);
});
}); });
...@@ -239,4 +239,30 @@ describe('Local Storage Sync', () => { ...@@ -239,4 +239,30 @@ describe('Local Storage Sync', () => {
}); });
}); });
}); });
it('clears localStorage when clear property is true', async () => {
const storageKey = 'key';
const value = 'initial';
createComponent({
props: {
storageKey,
},
});
wrapper.setProps({
value,
});
await wrapper.vm.$nextTick();
expect(localStorage.getItem(storageKey)).toBe(value);
wrapper.setProps({
clear: true,
});
await wrapper.vm.$nextTick();
expect(localStorage.getItem(storageKey)).toBe(null);
});
}); });
...@@ -93,9 +93,10 @@ RSpec.describe Gitlab::Ci::Jwt do ...@@ -93,9 +93,10 @@ RSpec.describe Gitlab::Ci::Jwt do
end end
describe '.for_build' do describe '.for_build' do
let(:rsa_key) { OpenSSL::PKey::RSA.new(Rails.application.secrets.openid_connect_signing_key) } shared_examples 'generating JWT for build' do
context 'when signing key is present' do
subject(:jwt) { described_class.for_build(build) } let(:rsa_key) { OpenSSL::PKey::RSA.generate(1024) }
let(:rsa_key_data) { rsa_key.to_s }
it 'generates JWT with key id' do it 'generates JWT with key id' do
_payload, headers = JWT.decode(jwt, rsa_key.public_key, true, { algorithm: 'RS256' }) _payload, headers = JWT.decode(jwt, rsa_key.public_key, true, { algorithm: 'RS256' })
...@@ -121,4 +122,36 @@ RSpec.describe Gitlab::Ci::Jwt do ...@@ -121,4 +122,36 @@ RSpec.describe Gitlab::Ci::Jwt do
expect(ttl).to eq(5.minutes.to_i) expect(ttl).to eq(5.minutes.to_i)
end end
end end
context 'when signing key is missing' do
let(:rsa_key_data) { nil }
it 'raises NoSigningKeyError' do
expect { jwt }.to raise_error described_class::NoSigningKeyError
end
end
end
subject(:jwt) { described_class.for_build(build) }
context 'when ci_jwt_signing_key feature flag is disabled' do
before do
stub_feature_flags(ci_jwt_signing_key: false)
allow(Rails.application.secrets).to receive(:openid_connect_signing_key).and_return(rsa_key_data)
end
it_behaves_like 'generating JWT for build'
end
context 'when ci_jwt_signing_key feature flag is enabled' do
before do
stub_feature_flags(ci_jwt_signing_key: true)
stub_application_setting(ci_jwt_signing_key: rsa_key_data)
end
it_behaves_like 'generating JWT for build'
end
end
end end
...@@ -77,6 +77,12 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s ...@@ -77,6 +77,12 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
stub_application_setting(usage_ping_enabled: true) stub_application_setting(usage_ping_enabled: true)
end end
it 'tracks event when using symbol' do
expect(Gitlab::Redis::HLL).to receive(:add)
described_class.track_event(entity1, :g_analytics_contribution)
end
it "raise error if metrics don't have same aggregation" do it "raise error if metrics don't have same aggregation" do
expect { described_class.track_event(entity1, different_aggregation, Date.current) } .to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownAggregation) expect { described_class.track_event(entity1, different_aggregation, Date.current) } .to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownAggregation)
end end
...@@ -201,6 +207,10 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s ...@@ -201,6 +207,10 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
it { expect(described_class.unique_events(event_names: weekly_event, start_date: 4.weeks.ago, end_date: 3.weeks.ago)).to eq(1) } it { expect(described_class.unique_events(event_names: weekly_event, start_date: 4.weeks.ago, end_date: 3.weeks.ago)).to eq(1) }
end end
context 'when using symbol as parameter' do
it { expect(described_class.unique_events(event_names: weekly_event.to_sym, start_date: 4.weeks.ago, end_date: 3.weeks.ago)).to eq(1) }
end
context 'when using daily aggregation' do context 'when using daily aggregation' do
it { expect(described_class.unique_events(event_names: daily_event, start_date: 7.days.ago, end_date: Date.current)).to eq(2) } it { expect(described_class.unique_events(event_names: daily_event, start_date: 7.days.ago, end_date: Date.current)).to eq(2) }
it { expect(described_class.unique_events(event_names: daily_event, start_date: 28.days.ago, end_date: Date.current)).to eq(3) } it { expect(described_class.unique_events(event_names: daily_event, start_date: 28.days.ago, end_date: Date.current)).to eq(3) }
......
...@@ -619,6 +619,7 @@ RSpec.describe Notify do ...@@ -619,6 +619,7 @@ RSpec.describe Notify do
let(:mailer) do let(:mailer) do
mailer = described_class.new mailer = described_class.new
mailer.instance_variable_set(:@note, mail_thread_note) mailer.instance_variable_set(:@note, mail_thread_note)
mailer.instance_variable_set(:@target_url, "https://some.link")
mailer mailer
end end
......
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20201008013434_generate_ci_jwt_signing_key.rb')
RSpec.describe GenerateCiJwtSigningKey do
let(:application_settings) do
Class.new(ActiveRecord::Base) do
self.table_name = 'application_settings'
attr_encrypted :ci_jwt_signing_key, {
mode: :per_attribute_iv,
key: Rails.application.secrets.db_key_base[0..31],
algorithm: 'aes-256-gcm',
encode: true
}
end
end
it 'generates JWT signing key' do
application_settings.create!
reversible_migration do |migration|
migration.before -> {
settings = application_settings.first
expect(settings.ci_jwt_signing_key).to be_nil
expect(settings.encrypted_ci_jwt_signing_key).to be_nil
expect(settings.encrypted_ci_jwt_signing_key_iv).to be_nil
}
migration.after -> {
settings = application_settings.first
expect(settings.encrypted_ci_jwt_signing_key).to be_present
expect(settings.encrypted_ci_jwt_signing_key_iv).to be_present
expect { OpenSSL::PKey::RSA.new(settings.ci_jwt_signing_key) }.not_to raise_error
}
end
end
end
...@@ -647,6 +647,23 @@ RSpec.describe ApplicationSetting do ...@@ -647,6 +647,23 @@ RSpec.describe ApplicationSetting do
end end
end end
end end
describe '#ci_jwt_signing_key' do
it { is_expected.not_to allow_value('').for(:ci_jwt_signing_key) }
it { is_expected.not_to allow_value('invalid RSA key').for(:ci_jwt_signing_key) }
it { is_expected.to allow_value(nil).for(:ci_jwt_signing_key) }
it { is_expected.to allow_value(OpenSSL::PKey::RSA.new(1024).to_pem).for(:ci_jwt_signing_key) }
it 'is encrypted' do
subject.ci_jwt_signing_key = OpenSSL::PKey::RSA.new(1024).to_pem
aggregate_failures do
expect(subject.encrypted_ci_jwt_signing_key).to be_present
expect(subject.encrypted_ci_jwt_signing_key_iv).to be_present
expect(subject.encrypted_ci_jwt_signing_key).not_to eq(subject.ci_jwt_signing_key)
end
end
end
end end
context 'static objects external storage' do context 'static objects external storage' do
......
...@@ -2464,7 +2464,7 @@ RSpec.describe Ci::Build do ...@@ -2464,7 +2464,7 @@ RSpec.describe Ci::Build do
end end
before do before do
allow(Gitlab::Ci::Jwt).to receive(:for_build).with(build).and_return('ci.job.jwt') allow(Gitlab::Ci::Jwt).to receive(:for_build).and_return('ci.job.jwt')
build.set_token('my-token') build.set_token('my-token')
build.yaml_variables = [] build.yaml_variables = []
end end
...@@ -2482,14 +2482,19 @@ RSpec.describe Ci::Build do ...@@ -2482,14 +2482,19 @@ RSpec.describe Ci::Build do
end end
context 'when CI_JOB_JWT generation fails' do context 'when CI_JOB_JWT generation fails' do
[
OpenSSL::PKey::RSAError,
Gitlab::Ci::Jwt::NoSigningKeyError
].each do |reason_to_fail|
it 'CI_JOB_JWT is not included' do it 'CI_JOB_JWT is not included' do
expect(Gitlab::Ci::Jwt).to receive(:for_build).and_raise(OpenSSL::PKey::RSAError, 'Neither PUB key nor PRIV key: not enough data') expect(Gitlab::Ci::Jwt).to receive(:for_build).and_raise(reason_to_fail)
expect(Gitlab::ErrorTracking).to receive(:track_exception) expect(Gitlab::ErrorTracking).to receive(:track_exception)
expect { subject }.not_to raise_error expect { subject }.not_to raise_error
expect(subject.pluck(:key)).not_to include('CI_JOB_JWT') expect(subject.pluck(:key)).not_to include('CI_JOB_JWT')
end end
end end
end
describe 'variables ordering' do describe 'variables ordering' do
context 'when variables hierarchy is stubbed' do context 'when variables hierarchy is stubbed' do
......
...@@ -4,9 +4,10 @@ require 'spec_helper' ...@@ -4,9 +4,10 @@ require 'spec_helper'
RSpec.describe GroupMember do RSpec.describe GroupMember do
context 'scopes' do context 'scopes' do
let_it_be(:user_1) { create(:user) }
let_it_be(:user_2) { create(:user) }
it 'counts users by group ID' do it 'counts users by group ID' do
user_1 = create(:user)
user_2 = create(:user)
group_1 = create(:group) group_1 = create(:group)
group_2 = create(:group) group_2 = create(:group)
...@@ -25,6 +26,15 @@ RSpec.describe GroupMember do ...@@ -25,6 +26,15 @@ RSpec.describe GroupMember do
expect(described_class.of_ldap_type).to eq([group_member]) expect(described_class.of_ldap_type).to eq([group_member])
end end
end end
describe '.with_user' do
it 'returns requested user' do
group_member = create(:group_member, user: user_2)
create(:group_member, user: user_1)
expect(described_class.with_user(user_2)).to eq([group_member])
end
end
end end
describe '.access_level_roles' do describe '.access_level_roles' do
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
require 'spec_helper' require 'spec_helper'
# oauth_discovery_keys GET /oauth/discovery/keys(.:format) doorkeeper/openid_connect/discovery#keys # oauth_discovery_keys GET /oauth/discovery/keys(.:format) doorkeeper/openid_connect/discovery#keys
# jwks GET /-/jwks(.:format) doorkeeper/openid_connect/discovery#keys
# oauth_discovery_provider GET /.well-known/openid-configuration(.:format) doorkeeper/openid_connect/discovery#provider # oauth_discovery_provider GET /.well-known/openid-configuration(.:format) doorkeeper/openid_connect/discovery#provider
# oauth_discovery_webfinger GET /.well-known/webfinger(.:format) doorkeeper/openid_connect/discovery#webfinger # oauth_discovery_webfinger GET /.well-known/webfinger(.:format) doorkeeper/openid_connect/discovery#webfinger
RSpec.describe Doorkeeper::OpenidConnect::DiscoveryController, 'routing' do RSpec.describe Doorkeeper::OpenidConnect::DiscoveryController, 'routing' do
...@@ -18,10 +17,6 @@ RSpec.describe Doorkeeper::OpenidConnect::DiscoveryController, 'routing' do ...@@ -18,10 +17,6 @@ RSpec.describe Doorkeeper::OpenidConnect::DiscoveryController, 'routing' do
it "to #keys" do it "to #keys" do
expect(get('/oauth/discovery/keys')).to route_to('doorkeeper/openid_connect/discovery#keys') expect(get('/oauth/discovery/keys')).to route_to('doorkeeper/openid_connect/discovery#keys')
end end
it "/-/jwks" do
expect(get('/-/jwks')).to route_to('doorkeeper/openid_connect/discovery#keys')
end
end end
# oauth_userinfo GET /oauth/userinfo(.:format) doorkeeper/openid_connect/userinfo#show # oauth_userinfo GET /oauth/userinfo(.:format) doorkeeper/openid_connect/userinfo#show
......
...@@ -369,3 +369,10 @@ RSpec.describe RunnerSetupController, 'routing' do ...@@ -369,3 +369,10 @@ RSpec.describe RunnerSetupController, 'routing' do
expect(get("/-/runner_setup/platforms")).to route_to('runner_setup#platforms') expect(get("/-/runner_setup/platforms")).to route_to('runner_setup#platforms')
end end
end end
# jwks GET /-/jwks(.:format) jwks#index
RSpec.describe JwksController, "routing" do
it "to #index" do
expect(get('/-/jwks')).to route_to('jwks#index')
end
end
...@@ -161,10 +161,10 @@ RSpec.describe Clusters::Applications::CheckInstallationProgressService, '#execu ...@@ -161,10 +161,10 @@ RSpec.describe Clusters::Applications::CheckInstallationProgressService, '#execu
expect(application.status_reason).to be_nil expect(application.status_reason).to be_nil
end end
it 'tracks application install' do it 'tracks application install', :snowplow do
expect(Gitlab::Tracking).to receive(:event).with('cluster:applications', "cluster_application_helm_installed")
service.execute service.execute
expect_snowplow_event(category: 'cluster:applications', action: 'cluster_application_helm_installed')
end end
end end
......
...@@ -46,10 +46,15 @@ RSpec.describe Issues::ZoomLinkService do ...@@ -46,10 +46,15 @@ RSpec.describe Issues::ZoomLinkService do
expect(ZoomMeeting.canonical_meeting_url(issue)).to eq(zoom_link) expect(ZoomMeeting.canonical_meeting_url(issue)).to eq(zoom_link)
end end
it 'tracks the add event' do it 'tracks the add event', :snowplow do
expect(Gitlab::Tracking).to receive(:event)
.with('IncidentManagement::ZoomIntegration', 'add_zoom_meeting', label: 'Issue ID', value: issue.id)
result result
expect_snowplow_event(
category: 'IncidentManagement::ZoomIntegration',
action: 'add_zoom_meeting',
label: 'Issue ID',
value: issue.id
)
end end
it 'creates a zoom_link_added notification' do it 'creates a zoom_link_added notification' do
...@@ -180,10 +185,15 @@ RSpec.describe Issues::ZoomLinkService do ...@@ -180,10 +185,15 @@ RSpec.describe Issues::ZoomLinkService do
expect(ZoomMeeting.canonical_meeting_url(issue)).to eq(nil) expect(ZoomMeeting.canonical_meeting_url(issue)).to eq(nil)
end end
it 'tracks the remove event' do it 'tracks the remove event', :snowplow do
expect(Gitlab::Tracking).to receive(:event)
.with('IncidentManagement::ZoomIntegration', 'remove_zoom_meeting', label: 'Issue ID', value: issue.id)
result result
expect_snowplow_event(
category: 'IncidentManagement::ZoomIntegration',
action: 'remove_zoom_meeting',
label: 'Issue ID',
value: issue.id
)
end end
end end
......
...@@ -36,10 +36,10 @@ module SnowplowHelpers ...@@ -36,10 +36,10 @@ module SnowplowHelpers
# would not pass any arguments when using **kwargs. # would not pass any arguments when using **kwargs.
# https://gitlab.com/gitlab-org/gitlab/-/issues/263430 # https://gitlab.com/gitlab-org/gitlab/-/issues/263430
if kwargs.present? if kwargs.present?
expect(Gitlab::Tracking).to have_received(:event) expect(Gitlab::Tracking).to have_received(:event) # rubocop:disable RSpec/ExpectGitlabTracking
.with(category, action, **kwargs).at_least(:once) .with(category, action, **kwargs).at_least(:once)
else else
expect(Gitlab::Tracking).to have_received(:event) expect(Gitlab::Tracking).to have_received(:event) # rubocop:disable RSpec/ExpectGitlabTracking
.with(category, action).at_least(:once) .with(category, action).at_least(:once)
end end
end end
...@@ -56,6 +56,6 @@ module SnowplowHelpers ...@@ -56,6 +56,6 @@ module SnowplowHelpers
# end # end
# end # end
def expect_no_snowplow_event def expect_no_snowplow_event
expect(Gitlab::Tracking).not_to have_received(:event) expect(Gitlab::Tracking).not_to have_received(:event) # rubocop:disable RSpec/ExpectGitlabTracking
end end
end end
...@@ -13,7 +13,7 @@ RSpec.configure do |config| ...@@ -13,7 +13,7 @@ RSpec.configure do |config|
stub_application_setting(snowplow_enabled: true) stub_application_setting(snowplow_enabled: true)
allow(Gitlab::Tracking).to receive(:event).and_call_original allow(Gitlab::Tracking).to receive(:event).and_call_original # rubocop:disable RSpec/ExpectGitlabTracking
end end
config.after(:each, :snowplow) do config.after(:each, :snowplow) do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe RsaKeyValidator do
let(:validatable) do
Class.new do
include ActiveModel::Validations
attr_accessor :signing_key
validates :signing_key, rsa_key: true
def initialize(signing_key)
@signing_key = signing_key
end
end
end
subject(:validator) { described_class.new(attributes: [:signing_key]) }
it 'is not valid when invalid RSA key is provided' do
record = validatable.new('invalid RSA key')
validator.validate(record)
aggregate_failures do
expect(record).not_to be_valid
expect(record.errors[:signing_key]).to include('is not a valid RSA key')
end
end
it 'is valid when valid RSA key is provided' do
record = validatable.new(OpenSSL::PKey::RSA.new(1024).to_pem)
validator.validate(record)
expect(record).to be_valid
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment