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:
- 'ee/app/views/errors/kerberos_denied.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/_sync_button.html.haml'
- 'ee/app/views/groups/hooks/edit.html.haml'
- 'ee/app/views/groups/ldap_group_links/index.html.haml'
- 'ee/app/views/layouts/nav/ee/admin/_new_monitoring_sidebar.html.haml'
......@@ -362,7 +361,6 @@ linters:
- '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/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/issuable/_approvals.html.haml'
- 'ee/app/views/shared/issuable/_board_create_list_dropdown.html.haml'
......
......@@ -186,31 +186,21 @@ RSpec/ExpectChange:
# Offense count: 47
RSpec/ExpectGitlabTracking:
Exclude:
- 'ee/spec/controllers/groups/analytics/coverage_reports_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/services/epics/issue_promote_service_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/tags_controller_spec.rb'
- 'spec/controllers/projects/settings/operations_controller_spec.rb'
- 'spec/controllers/registrations_controller_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/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/requests/api/container_repositories_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/tracking_shared_examples.rb'
- 'spec/support/snowplow.rb'
# Offense count: 751
RSpec/ExpectInHook:
......
<script>
import { GlForm, GlFormGroup, GlFormInput, GlFormTextarea } from '@gitlab/ui';
import AccessorUtilities from '~/lib/utils/accessor';
export default {
components: {
......@@ -19,55 +18,25 @@ export default {
required: true,
},
},
data() {
return {
editable: {
title: this.title,
description: this.description,
},
};
},
computed: {
editableStorageKey() {
return this.getId('local-storage', 'editable');
},
hasLocalStorage() {
return AccessorUtilities.isLocalStorageAccessSafe();
},
},
mounted() {
this.initCachedEditable();
this.preSelect();
},
methods: {
getId(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() {
this.$nextTick(() => {
this.$refs.title.$el.select();
});
},
resetCachedEditable() {
if (this.hasLocalStorage) {
window.localStorage.removeItem(this.editableStorageKey);
}
},
onUpdate() {
const payload = { ...this.editable };
onUpdate(field, value) {
const payload = {
title: this.title,
description: this.description,
[field]: value,
};
this.$emit('updateSettings', payload);
if (this.hasLocalStorage) {
window.localStorage.setItem(this.editableStorageKey, JSON.stringify(payload));
}
},
},
};
......@@ -83,9 +52,9 @@ export default {
<gl-form-input
:id="getId('control', 'title')"
ref="title"
v-model.lazy="editable.title"
:value="title"
type="text"
@input="onUpdate"
@input="onUpdate('title', $event)"
/>
</gl-form-group>
......@@ -96,8 +65,8 @@ export default {
>
<gl-form-textarea
:id="getId('control', 'description')"
v-model.lazy="editable.description"
@input="onUpdate"
:value="description"
@input="onUpdate('description', $event)"
/>
</gl-form-group>
</gl-form>
......
<script>
import { GlModal } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import EditMetaControls from './edit_meta_controls.vue';
import { MR_META_LOCAL_STORAGE_KEY } from '../constants';
export default {
components: {
GlModal,
EditMetaControls,
LocalStorageSync,
},
props: {
sourcePath: {
......@@ -17,6 +21,7 @@ export default {
},
data() {
return {
clearStorage: false,
mergeRequestMeta: {
title: sprintf(s__(`StaticSiteEditor|Update %{sourcePath} file`), {
sourcePath: this.sourcePath,
......@@ -51,7 +56,7 @@ export default {
},
onPrimary() {
this.$emit('primary', this.mergeRequestMeta);
this.$refs.editMetaControls.resetCachedEditable();
this.clearStorage = true;
},
onSecondary() {
this.hide();
......@@ -60,6 +65,7 @@ export default {
this.mergeRequestMeta = { ...mergeRequestMeta };
},
},
storageKey: MR_META_LOCAL_STORAGE_KEY,
};
</script>
......@@ -75,6 +81,12 @@ export default {
@secondary="onSecondary"
@hide="() => $emit('hide')"
>
<local-storage-sync
v-model="mergeRequestMeta"
:storage-key="$options.storageKey"
:clear="clearStorage"
as-json
/>
<edit-meta-controls
ref="editMetaControls"
:title="mergeRequestMeta.title"
......
......@@ -21,3 +21,5 @@ export const TRACKING_ACTION_CREATE_MERGE_REQUEST = 'create_merge_request';
export const TRACKING_ACTION_INITIALIZE_EDITOR = 'initialize_editor';
export const DEFAULT_IMAGE_UPLOAD_PATH = 'source/images/uploads/';
export const MR_META_LOCAL_STORAGE_KEY = 'sse-merge-request-meta-storage-key';
<script>
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 simplePoll from '~/lib/utils/simple_poll';
import { __ } from '~/locale';
......@@ -36,6 +45,9 @@ export default {
GlSprintf,
GlLink,
GlButton,
GlButtonGroup,
GlDropdown,
GlDropdownItem,
MergeTrainHelperText: () =>
import('ee_component/vue_merge_request_widget/components/merge_train_helper_text.vue'),
MergeImmediatelyConfirmationDialog: () =>
......@@ -43,6 +55,9 @@ export default {
'ee_component/vue_merge_request_widget/components/merge_immediately_confirmation_dialog.vue'
),
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [readyToMergeMixin],
props: {
mr: { type: Object, required: true },
......@@ -283,7 +298,7 @@ export default {
<status-icon :status="iconClass" />
<div class="media-body">
<div class="mr-widget-body-controls media space-children">
<span class="btn-group">
<gl-button-group>
<gl-button
size="medium"
category="primary"
......@@ -294,54 +309,33 @@ export default {
@click="handleMergeButtonClick(isAutoMergeAvailable)"
>{{ mergeButtonText }}</gl-button
>
<button
<gl-dropdown
v-if="shouldShowMergeImmediatelyDropdown"
v-gl-tooltip.hover.focus="__('Select merge moment')"
:disabled="isMergeButtonDisabled"
type="button"
class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
data-toggle="dropdown"
variant="info"
data-qa-selector="merge_moment_dropdown"
:aria-label="__('Select 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"
toggle-class="btn-icon js-merge-moment"
>
<li>
<a
class="auto_merge_enabled qa-merge-when-pipeline-succeeds-option"
href="#"
@click.prevent="handleMergeButtonClick(true)"
>
<span class="media">
<gl-icon name="status_success" class="merge-opt-icon" aria-hidden="true" />
<span class="media-body merge-opt-title">{{ autoMergeText }}</span>
</span>
</a>
</li>
<li>
<merge-immediately-confirmation-dialog
ref="confirmationDialog"
:docs-url="mr.mergeImmediatelyDocsPath"
@mergeImmediately="onMergeImmediatelyConfirmation"
/>
<a
class="accept-merge-request js-merge-immediately-button"
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>
<template #button-content>
<gl-icon name="chevron-down" class="mr-0" />
<span class="sr-only">{{ __('Select merge moment') }}</span>
</template>
<gl-dropdown-item
icon-name="warning"
button-class="accept-merge-request js-merge-immediately-button"
data-qa-selector="merge_immediately_option"
@click="handleMergeImmediatelyButtonClick"
>
{{ __('Merge immediately') }}
</gl-dropdown-item>
<merge-immediately-confirmation-dialog
ref="confirmationDialog"
:docs-url="mr.mergeImmediatelyDocsPath"
@mergeImmediately="onMergeImmediatelyConfirmation"
/>
</gl-dropdown>
</gl-button-group>
<div class="media-body-wrap space-children">
<template v-if="shouldShowMergeControls">
<label v-if="mr.canRemoveSourceBranch">
......
......@@ -22,11 +22,21 @@ export default {
required: false,
default: true,
},
clear: {
type: Boolean,
required: false,
default: false,
},
},
watch: {
value(newVal) {
this.saveValue(this.serialize(newVal));
},
clear(newVal) {
if (newVal) {
localStorage.removeItem(this.storageKey);
}
},
},
mounted() {
// On mount, trigger update if we actually have a localStorageValue
......
......@@ -113,10 +113,6 @@
content: '\f0da';
}
.fa-refresh::before {
content: '\f021';
}
.fa-chevron-up::before {
content: '\f077';
}
......
......@@ -56,3 +56,7 @@
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
validates :raw_blob_request_limit,
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,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
......@@ -409,6 +412,7 @@ class ApplicationSetting < ApplicationRecord
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_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!
......
......@@ -1059,7 +1059,7 @@ module Ci
jwt = Gitlab::Ci::Jwt.for_build(self)
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)
end
end
......
......@@ -21,6 +21,7 @@ class GroupMember < Member
scope :of_groups, ->(groups) { where(source_id: groups.select(:id)) }
scope :of_ldap_type, -> { where(ldap: true) }
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_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 @@
= succeed ':' do
= link_to note.author_name, user_url(note.author)
- if discussion.nil?
commented
= link_to 'commented', target_url
- else
- if note.start_of_discussion?
started a new
......
......@@ -74,4 +74,4 @@
- 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')
= 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?
%button.btn.disabled{ type: 'button', data: { toggle: 'tooltip', container: 'body', qa_selector: 'updating_button' }, title: _('Updating') }
= icon("refresh spin")
%button.btn.btn-icon.gl-button.disabled{ type: 'button', data: { toggle: 'tooltip', container: 'body', qa_selector: 'updating_button' }, title: _('Updating') }
= sprite_icon("retry", css_class: "spin")
- 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
= icon("refresh")
= 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
= 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
resources :abuse_reports, only: [:new, :create]
# JWKS (JSON Web Key Set) endpoint
# Used by third parties to verify CI_JOB_JWT, placeholder route
# in case we decide to move away from doorkeeper-openid_connect
get 'jwks' => 'doorkeeper/openid_connect/discovery#keys'
# Used by third parties to verify CI_JOB_JWT
get 'jwks' => 'jwks#index'
draw :snippets
draw :profile
......
......@@ -7,4 +7,7 @@ ApplicationSetting.create_from_defaults
puts "Enable hashed storage for every new projects.".color(:green)
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 '.'
......@@ -24,3 +24,7 @@ if ENV['GITLAB_PROMETHEUS_METRICS_ENABLED'].present?
settings.prometheus_metrics_enabled = value
save(settings, 'Prometheus metrics enabled flag')
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 (
require_admin_approval_after_user_signup boolean DEFAULT false NOT NULL,
help_page_documentation_base_url text,
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_51700b31b5 CHECK ((char_length(default_branch_name) <= 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_d03919528d CHECK ((char_length(container_registry_vendor) <= 255)),
CONSTRAINT check_d820146492 CHECK ((char_length(spam_check_endpoint_url) <= 255)),
......@@ -15857,6 +15860,7 @@ CREATE TABLE security_findings (
confidence smallint NOT NULL,
project_fingerprint text NOT NULL,
deduplicated boolean DEFAULT false NOT NULL,
"position" integer,
CONSTRAINT check_b9508c6df8 CHECK ((char_length(project_fingerprint) <= 40))
);
......@@ -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 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_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.
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
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:
......
......@@ -19,7 +19,6 @@ module EE
private
# rubocop: disable CodeReuse/ActiveRecord
override :authorize_list_type_resource!
def authorize_list_type_resource!(board, params)
super
......@@ -27,7 +26,7 @@ module EE
if params[:milestone_id]
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!'
end
end
......@@ -35,12 +34,11 @@ module EE
if params[:assignee_id]
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!'
end
end
end
# rubocop: enable CodeReuse/ActiveRecord
override :create_list_params
def create_list_params(args)
......
......@@ -118,11 +118,14 @@ module EE
strong_memoize(:security_report) do
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|
::Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, report)
end
end
# This will remove the duplicated findings within the artifact itself
::Security::MergeReportsService.new(report).execute
end
end
......
......@@ -20,8 +20,9 @@ module Security
enum severity: Vulnerabilities::Finding::SEVERITY_LEVELS, _prefix: :severity
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 }) }
end
end
......@@ -31,21 +31,22 @@ module Security
end
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
def store_finding!(report_finding)
def store_finding!(report_finding, position)
return if report_finding.scanner.blank?
security_scan.findings.create!(finding_data(report_finding))
security_scan.findings.create!(finding_data(report_finding, position))
end
def finding_data(report_finding)
def finding_data(report_finding, position)
{
severity: report_finding.severity,
confidence: report_finding.confidence,
project_fingerprint: report_finding.project_fingerprint,
scanner: persisted_scanner_for(report_finding.scanner)
scanner: persisted_scanner_for(report_finding.scanner),
position: position
}
end
......
......@@ -43,21 +43,19 @@ module Security
security_scan.findings.update_all(deduplicated: false)
security_scan.findings
.by_project_fingerprint(deduplicated_project_fingerprints)
.by_position(register_finding_keys)
.update_all(deduplicated: true)
end
end
def deduplicated_project_fingerprints
register_finding_keys.map(&:project_fingerprint)
end
# This method registers all finding keys and
# returns the positions of unique findings
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
def register_keys(keys)
keys.map { |key| known_keys.add?(key) }.all?
keys.all? { |key| known_keys.add?(key) }
end
end
end
- if @group.ldap_sync_started?
%span.btn.disabled
= icon("refresh spin")
Syncing&hellip;
%span.btn.gl-button.disabled
= sprite_icon("retry", css_class: 'spin gl-mr-2')
= _('Syncing…')
- elsif @group.ldap_sync_pending?
%span.btn.disabled
= icon("refresh spin")
Pending sync&hellip;
%span.btn.gl-button.disabled
= sprite_icon("retry", css_class: 'spin gl-mr-2')
= ('Pending sync…')
- else
= link_to sync_group_ldap_path(@group), method: :put, class: 'btn qa-sync-now-button' do
= icon("refresh")
Sync now
= link_to sync_group_ldap_path(@group), method: :put, class: 'btn btn-default gl-button qa-sync-now-button' do
= sprite_icon("retry", css_class: "gl-mr-2")
= _('Sync now')
- if @group.ldap_sync_ready? && @group.ldap_sync_last_successful_update_at
%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 @@
- 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')
- 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?
%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
= 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
= icon("refresh")
%button.js-delete-mirror.js-delete-pull-mirror.btn.btn-danger{ type: 'button', data: { toggle: 'tooltip', container: 'body' }, title: _('Remove') }= sprite_icon('remove')
= 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
= sprite_icon("retry")
%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 @@
.gl-mb-3
- if @project.import_state.mirror_update_due?
%span.btn.disabled
= icon("refresh spin")
Update Scheduled&hellip;
= sprite_icon('retry', css_class: 'spin')
= ('Update Scheduled…')
- elsif @project.import_state.updating_mirror?
%span.btn.disabled
= icon("refresh spin")
Updating&hellip;
= sprite_icon('retry', css_class: 'spin')
= ('Updating…')
- elsif can?(current_user, :admin_project, @project)
= link_to update_now_project_mirror_path(@project), method: :post, class: 'btn' do
= icon("refresh")
Update Now
= sprite_icon('retry')
= ('Update Now')
- else
%span.btn.disabled
= icon("refresh")
Update Now
= sprite_icon('retry')
= ('Update Now')
- if @project.mirror_last_update_succeeded?
%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
end
# Overrides API::BoardsResponses authorize_list_type_resource!
# rubocop: disable CodeReuse/ActiveRecord
def authorize_list_type_resource!
# rubocop: disable CodeReuse/ActiveRecord
if params[:label_id] && !available_labels_for(board_parent).exists?(params[:label_id])
render_api_error!({ error: 'Label not found!' }, 400)
end
# rubocop: enable CodeReuse/ActiveRecord
if milestone_id = params[:milestone_id]
if params[:milestone_id]
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)
end
end
if assignee_id = params[:assignee_id]
if params[:assignee_id]
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)
end
end
end
# rubocop: enable CodeReuse/ActiveRecord
# Overrides API::BoardsResponses list_creation_params
params :list_creation_params do
......
......@@ -59,16 +59,16 @@ RSpec.describe Groups::Analytics::CoverageReportsController do
stub_licensed_features(group_coverage_reports: true)
end
it 'responds 200 with CSV coverage data' do
expect(Gitlab::Tracking).to receive(:event).with(
described_class.name,
'download_code_coverage_csv',
it 'responds 200 with CSV coverage data', :snowplow do
get :index, params: valid_request_params
expect_snowplow_event(
category: described_class.name,
action: 'download_code_coverage_csv',
label: 'group_id',
value: group.id
)
get :index, params: valid_request_params
expect(response).to have_gitlab_http_status(:ok)
expect(csv_response).to eq([
%w[date group_name project_name coverage],
......
......@@ -148,15 +148,15 @@ RSpec.describe RegistrationsController do
update_registration
end
it 'tracks a signed_up event' do
expect(Gitlab::Tracking).to receive(:event).with(
'Growth::Conversion::Experiment::OnboardingIssues',
'signed_up',
it 'tracks a signed_up event', :snowplow do
update_registration
expect_snowplow_event(
category: 'Growth::Conversion::Experiment::OnboardingIssues',
action: 'signed_up',
label: anything,
property: "#{group_type}_group"
)
update_registration
end
end
end
......@@ -176,10 +176,10 @@ RSpec.describe RegistrationsController do
update_registration
end
it 'does not track a signed_up event' do
expect(Gitlab::Tracking).not_to receive(:event)
it 'does not track a signed_up event', :snowplow do
update_registration
expect_no_snowplow_event
end
end
end
......@@ -196,10 +196,10 @@ RSpec.describe RegistrationsController do
update_registration
end
it 'does not track a signed_up event' do
expect(Gitlab::Tracking).not_to receive(:event)
it 'does not track a signed_up event', :snowplow do
update_registration
expect_no_snowplow_event
end
end
end
......
......@@ -8,5 +8,6 @@ FactoryBot.define do
severity { :critical }
confidence { :high }
project_fingerprint { generate(:project_fingerprint) }
sequence :position
end
end
......@@ -41,7 +41,7 @@ RSpec.describe 'Merge requests > User merges immediately', :js do
def open_warning_dialog
find('.mr-widget-body .dropdown-toggle').click
click_link 'Merge immediately'
click_button 'Merge immediately'
expect(page).to have_selector('#merge-immediately-confirmation-dialog')
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 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';
......@@ -57,8 +57,9 @@ describe('ReadyToMerge', () => {
mergeTrainsCount: 0,
};
const factory = (mrUpdates = {}) => {
wrapper = shallowMount(ReadyToMerge, {
const factory = (mrUpdates = {}, shallow = true) => {
const func = shallow ? shallowMount : mount;
wrapper = func(ReadyToMerge, {
propsData: {
mr: { ...mr, ...mrUpdates },
service,
......@@ -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', () => {
factory({ preferredAutoMergeStrategy: MT_MERGE_STRATEGY });
factory({ preferredAutoMergeStrategy: MT_MERGE_STRATEGY }, false);
return clickMergeImmediately().then(() => {
expect(dialog.vm.show).toHaveBeenCalled();
expect(vm.handleMergeButtonClick).not.toHaveBeenCalled();
......@@ -345,7 +346,7 @@ describe('ReadyToMerge', () => {
});
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()
.then(() => {
dialog.vm.$emit('mergeImmediately');
......@@ -357,10 +358,13 @@ describe('ReadyToMerge', () => {
});
it('should not ask for confirmation in non-merge train scenarios', () => {
factory({
isPipelineActive: true,
onlyAllowMergeIfPipelineSucceeds: false,
});
factory(
{
isPipelineActive: true,
onlyAllowMergeIfPipelineSucceeds: false,
},
false,
);
return clickMergeImmediately().then(() => {
expect(dialog.vm.show).not.toHaveBeenCalled();
expect(vm.handleMergeButtonClick).toHaveBeenCalled();
......
......@@ -256,7 +256,9 @@ RSpec.describe Ci::JobArtifact do
clear_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
......@@ -10,15 +10,16 @@ RSpec.describe Security::Finding do
describe 'validations' do
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) }
end
describe '.by_project_fingerprint' do
let!(:finding_1) { create(:security_finding) }
let!(:finding_2) { create(:security_finding) }
describe '.by_position' do
let!(:finding_1) { create(:security_finding, position: 0) }
let!(:finding_2) { create(:security_finding, position: 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) }
end
......
......@@ -58,9 +58,10 @@ RSpec.describe API::VisualReviewDiscussions do
stub_feature_flags(notes_create_service_tracking: false)
end
it 'does not track any events' do
expect(Gitlab::Tracking).not_to receive(:event)
it 'does not track any events', :snowplow do
request
expect_no_snowplow_event
end
end
......
......@@ -5,12 +5,13 @@ require 'spec_helper'
RSpec.describe Security::StoreFindingsMetadataService do
let_it_be(:security_scan) { create(:security_scan) }
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(:report) do
build(
:ci_reports_security_report,
findings: [security_finding],
findings: [security_finding_1, security_finding_2],
scanners: [security_scanner]
)
end
......@@ -36,10 +37,12 @@ RSpec.describe Security::StoreFindingsMetadataService do
end
it 'creates the security finding entries in database' do
expect { store_findings }.to change { security_scan.findings.count }.by(1)
.and change { security_scan.findings.last&.severity }.to(security_finding.severity.to_s)
.and change { security_scan.findings.last&.confidence }.to(security_finding.confidence.to_s)
.and change { security_scan.findings.last&.project_fingerprint }.to(security_finding.project_fingerprint)
expect { store_findings }.to change { security_scan.findings.count }.by(2)
.and change { security_scan.findings.first&.severity }.to(security_finding_1.severity.to_s)
.and change { security_scan.findings.first&.confidence }.to(security_finding_1.confidence.to_s)
.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
context 'when the scanners already exist in the database' do
......
......@@ -49,16 +49,16 @@ RSpec.describe Security::StoreScanService 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(:duplicated_security_finding) do
let_it_be(:unique_security_finding) do
create(:security_finding,
scan: security_scan,
project_fingerprint: 'd533c3a12403b6c6033a50b53f9c73f894a40fc6')
position: 0)
end
let_it_be(:unique_security_finding) do
let_it_be(:duplicated_security_finding) do
create(:security_finding,
scan: security_scan,
project_fingerprint: 'b9c0d1cdc7cb9c180ebb6981abbddc2df0172509')
position: 5)
end
it 'does not create a new security scan' do
......@@ -89,12 +89,12 @@ RSpec.describe Security::StoreScanService do
end
context 'when the security scan does not exist for the artifact' do
let(:duplicated_finding_attribute) do
-> { Security::Finding.by_project_fingerprint('d533c3a12403b6c6033a50b53f9c73f894a40fc6').first&.deduplicated }
let(:unique_finding_attribute) do
-> { Security::Finding.by_position(0).first&.deduplicated }
end
let(:unique_finding_attribute) do
-> { Security::Finding.by_project_fingerprint('b9c0d1cdc7cb9c180ebb6981abbddc2df0172509').first&.deduplicated }
let(:duplicated_finding_attribute) do
-> { Security::Finding.by_position(5).first&.deduplicated }
end
before do
......
......@@ -6,6 +6,8 @@ module Gitlab
NOT_BEFORE_TIME = 5
DEFAULT_EXPIRE_TIME = 60 * 5
NoSigningKeyError = Class.new(StandardError)
def self.for_build(build)
self.new(build, ttl: build.metadata_timeout).encoded
end
......@@ -27,7 +29,7 @@ module Gitlab
private
attr_reader :build, :ttl, :key_data
attr_reader :build, :ttl
def reserved_claims
now = Time.now.to_i
......@@ -60,7 +62,17 @@ module Gitlab
end
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
def public_key
......
......@@ -44,7 +44,7 @@ module Gitlab
end
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 category' unless events_in_same_category?(events)
......@@ -141,7 +141,7 @@ module Gitlab
end
def event_for(event_name)
known_events.find { |event| event[:name] == event_name }
known_events.find { |event| event[:name] == event_name.to_s }
end
def events_for(event_names)
......
......@@ -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."
msgstr ""
msgid "Successfully synced %{synced_timeago}."
msgstr ""
msgid "Successfully unblocked"
msgstr ""
......@@ -25693,12 +25696,18 @@ msgstr ""
msgid "Sync information"
msgstr ""
msgid "Sync now"
msgstr ""
msgid "Synced"
msgstr ""
msgid "Synchronization disabled"
msgstr ""
msgid "Syncing…"
msgstr ""
msgid "System"
msgstr ""
......
......@@ -23,7 +23,6 @@ module QA
element :merge_button
element :fast_forward_message, 'Fast-forward merge without a merge commit' # rubocop:disable QA/ElementWithPattern
element :merge_moment_dropdown
element :merge_when_pipeline_succeeds_option
element :merge_immediately_option
end
......
......@@ -319,10 +319,10 @@ RSpec.describe GroupsController, factory_default: :keep do
stub_experiment(onboarding_issues: false)
end
it 'does not track anything' do
expect(Gitlab::Tracking).not_to receive(:event)
it 'does not track anything', :snowplow do
create_namespace
expect_no_snowplow_event
end
end
......@@ -336,15 +336,15 @@ RSpec.describe GroupsController, factory_default: :keep do
stub_experiment_for_user(onboarding_issues: false)
end
it 'tracks the event with the "created_namespace" action with the "control_group" property' do
expect(Gitlab::Tracking).to receive(:event).with(
'Growth::Conversion::Experiment::OnboardingIssues',
'created_namespace',
it 'tracks the event with the "created_namespace" action with the "control_group" property', :snowplow do
create_namespace
expect_snowplow_event(
category: 'Growth::Conversion::Experiment::OnboardingIssues',
action: 'created_namespace',
label: anything,
property: 'control_group'
)
create_namespace
end
end
......@@ -353,15 +353,15 @@ RSpec.describe GroupsController, factory_default: :keep do
stub_experiment_for_user(onboarding_issues: true)
end
it 'tracks the event with the "created_namespace" action with the "experimental_group" property' do
expect(Gitlab::Tracking).to receive(:event).with(
'Growth::Conversion::Experiment::OnboardingIssues',
'created_namespace',
it 'tracks the event with the "created_namespace" action with the "experimental_group" property', :snowplow do
create_namespace
expect_snowplow_event(
category: 'Growth::Conversion::Experiment::OnboardingIssues',
action: 'created_namespace',
label: anything,
property: 'experimental_group'
)
create_namespace
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
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
......@@ -34,7 +34,7 @@ RSpec.describe 'Merge requests > User merges immediately', :js do
find('.dropdown-toggle').click
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')
......
......@@ -93,19 +93,6 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do
it_behaves_like 'Merge when pipeline succeeds activator'
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
context 'when merge when pipeline succeeds is enabled' do
......
import { shallowMount } from '@vue/test-utils';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { GlFormInput, GlFormTextarea } from '@gitlab/ui';
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
import { mergeRequestMeta } from '../mock_data';
describe('~/static_site_editor/components/edit_meta_controls.vue', () => {
useLocalStorageSpy();
let wrapper;
let mockSelect;
let mockGlFormInputTitleInstance;
......@@ -86,14 +83,5 @@ describe('~/static_site_editor/components/edit_meta_controls.vue', () => {
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 { 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 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';
describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
useLocalStorageSpy();
let wrapper;
let resetCachedEditable;
let mockEditMetaControlsInstance;
......@@ -30,6 +32,11 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
const findGlModal = () => wrapper.find(GlModal);
const findEditMetaControls = () => wrapper.find(EditMetaControls);
const findLocalStorageSync = () => wrapper.find(LocalStorageSync);
beforeEach(() => {
localStorage.setItem(MR_META_LOCAL_STORAGE_KEY);
});
beforeEach(() => {
buildWrapper();
......@@ -43,6 +50,16 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
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', () => {
expect(findGlModal().exists()).toBe(true);
});
......@@ -63,18 +80,32 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
expect(findEditMetaControls().props('description')).toBe(description);
});
it('emits the primary event with mergeRequestMeta', () => {
findGlModal().vm.$emit('primary', mergeRequestMeta);
expect(wrapper.emitted('primary')).toEqual([[mergeRequestMeta]]);
});
describe('when save button is clicked', () => {
beforeEach(() => {
findGlModal().vm.$emit('primary', mergeRequestMeta);
});
it('calls resetCachedEditable on EditMetaControls when primary emits', () => {
findGlModal().vm.$emit('primary', mergeRequestMeta);
expect(mockEditMetaControlsInstance.resetCachedEditable).toHaveBeenCalled();
it('removes merge request meta from local storage', () => {
expect(findLocalStorageSync().props().clear).toBe(true);
});
it('emits the primary event with mergeRequestMeta', () => {
expect(wrapper.emitted('primary')).toEqual([[mergeRequestMeta]]);
});
});
it('emits the hide event', () => {
findGlModal().vm.$emit('hide');
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', () => {
});
});
});
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,32 +93,65 @@ RSpec.describe Gitlab::Ci::Jwt do
end
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
let(:rsa_key) { OpenSSL::PKey::RSA.generate(1024) }
let(:rsa_key_data) { rsa_key.to_s }
subject(:jwt) { described_class.for_build(build) }
it 'generates JWT with key id' do
_payload, headers = JWT.decode(jwt, rsa_key.public_key, true, { algorithm: 'RS256' })
expect(headers['kid']).to eq(rsa_key.public_key.to_jwk['kid'])
end
it 'generates JWT for the given job with ttl equal to build timeout' do
expect(build).to receive(:metadata_timeout).and_return(3_600)
payload, _headers = JWT.decode(jwt, rsa_key.public_key, true, { algorithm: 'RS256' })
ttl = payload["exp"] - payload["iat"]
expect(ttl).to eq(3_600)
end
it 'generates JWT for the given job with default ttl if build timeout is not set' do
expect(build).to receive(:metadata_timeout).and_return(nil)
payload, _headers = JWT.decode(jwt, rsa_key.public_key, true, { algorithm: 'RS256' })
ttl = payload["exp"] - payload["iat"]
it 'generates JWT with key id' do
_payload, headers = JWT.decode(jwt, rsa_key.public_key, true, { algorithm: 'RS256' })
expect(ttl).to eq(5.minutes.to_i)
end
end
context 'when signing key is missing' do
let(:rsa_key_data) { nil }
expect(headers['kid']).to eq(rsa_key.public_key.to_jwk['kid'])
it 'raises NoSigningKeyError' do
expect { jwt }.to raise_error described_class::NoSigningKeyError
end
end
end
it 'generates JWT for the given job with ttl equal to build timeout' do
expect(build).to receive(:metadata_timeout).and_return(3_600)
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)
payload, _headers = JWT.decode(jwt, rsa_key.public_key, true, { algorithm: 'RS256' })
ttl = payload["exp"] - payload["iat"]
allow(Rails.application.secrets).to receive(:openid_connect_signing_key).and_return(rsa_key_data)
end
expect(ttl).to eq(3_600)
it_behaves_like 'generating JWT for build'
end
it 'generates JWT for the given job with default ttl if build timeout is not set' do
expect(build).to receive(:metadata_timeout).and_return(nil)
context 'when ci_jwt_signing_key feature flag is enabled' do
before do
stub_feature_flags(ci_jwt_signing_key: true)
payload, _headers = JWT.decode(jwt, rsa_key.public_key, true, { algorithm: 'RS256' })
ttl = payload["exp"] - payload["iat"]
stub_application_setting(ci_jwt_signing_key: rsa_key_data)
end
expect(ttl).to eq(5.minutes.to_i)
it_behaves_like 'generating JWT for build'
end
end
end
......@@ -77,6 +77,12 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
stub_application_setting(usage_ping_enabled: true)
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
expect { described_class.track_event(entity1, different_aggregation, Date.current) } .to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownAggregation)
end
......@@ -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) }
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
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) }
......
......@@ -619,6 +619,7 @@ RSpec.describe Notify do
let(:mailer) do
mailer = described_class.new
mailer.instance_variable_set(:@note, mail_thread_note)
mailer.instance_variable_set(:@target_url, "https://some.link")
mailer
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
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
context 'static objects external storage' do
......
......@@ -2464,7 +2464,7 @@ RSpec.describe Ci::Build do
end
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.yaml_variables = []
end
......@@ -2482,12 +2482,17 @@ RSpec.describe Ci::Build do
end
context 'when CI_JOB_JWT generation fails' 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::ErrorTracking).to receive(:track_exception)
expect { subject }.not_to raise_error
expect(subject.pluck(:key)).not_to include('CI_JOB_JWT')
[
OpenSSL::PKey::RSAError,
Gitlab::Ci::Jwt::NoSigningKeyError
].each do |reason_to_fail|
it 'CI_JOB_JWT is not included' do
expect(Gitlab::Ci::Jwt).to receive(:for_build).and_raise(reason_to_fail)
expect(Gitlab::ErrorTracking).to receive(:track_exception)
expect { subject }.not_to raise_error
expect(subject.pluck(:key)).not_to include('CI_JOB_JWT')
end
end
end
......
......@@ -4,9 +4,10 @@ require 'spec_helper'
RSpec.describe GroupMember 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
user_1 = create(:user)
user_2 = create(:user)
group_1 = create(:group)
group_2 = create(:group)
......@@ -25,6 +26,15 @@ RSpec.describe GroupMember do
expect(described_class.of_ldap_type).to eq([group_member])
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
describe '.access_level_roles' do
......
......@@ -3,7 +3,6 @@
require 'spec_helper'
# 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_webfinger GET /.well-known/webfinger(.:format) doorkeeper/openid_connect/discovery#webfinger
RSpec.describe Doorkeeper::OpenidConnect::DiscoveryController, 'routing' do
......@@ -18,10 +17,6 @@ RSpec.describe Doorkeeper::OpenidConnect::DiscoveryController, 'routing' do
it "to #keys" do
expect(get('/oauth/discovery/keys')).to route_to('doorkeeper/openid_connect/discovery#keys')
end
it "/-/jwks" do
expect(get('/-/jwks')).to route_to('doorkeeper/openid_connect/discovery#keys')
end
end
# oauth_userinfo GET /oauth/userinfo(.:format) doorkeeper/openid_connect/userinfo#show
......
......@@ -369,3 +369,10 @@ RSpec.describe RunnerSetupController, 'routing' do
expect(get("/-/runner_setup/platforms")).to route_to('runner_setup#platforms')
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
expect(application.status_reason).to be_nil
end
it 'tracks application install' do
expect(Gitlab::Tracking).to receive(:event).with('cluster:applications', "cluster_application_helm_installed")
it 'tracks application install', :snowplow do
service.execute
expect_snowplow_event(category: 'cluster:applications', action: 'cluster_application_helm_installed')
end
end
......
......@@ -46,10 +46,15 @@ RSpec.describe Issues::ZoomLinkService do
expect(ZoomMeeting.canonical_meeting_url(issue)).to eq(zoom_link)
end
it 'tracks the add event' do
expect(Gitlab::Tracking).to receive(:event)
.with('IncidentManagement::ZoomIntegration', 'add_zoom_meeting', label: 'Issue ID', value: issue.id)
it 'tracks the add event', :snowplow do
result
expect_snowplow_event(
category: 'IncidentManagement::ZoomIntegration',
action: 'add_zoom_meeting',
label: 'Issue ID',
value: issue.id
)
end
it 'creates a zoom_link_added notification' do
......@@ -180,10 +185,15 @@ RSpec.describe Issues::ZoomLinkService do
expect(ZoomMeeting.canonical_meeting_url(issue)).to eq(nil)
end
it 'tracks the remove event' do
expect(Gitlab::Tracking).to receive(:event)
.with('IncidentManagement::ZoomIntegration', 'remove_zoom_meeting', label: 'Issue ID', value: issue.id)
it 'tracks the remove event', :snowplow do
result
expect_snowplow_event(
category: 'IncidentManagement::ZoomIntegration',
action: 'remove_zoom_meeting',
label: 'Issue ID',
value: issue.id
)
end
end
......
......@@ -36,10 +36,10 @@ module SnowplowHelpers
# would not pass any arguments when using **kwargs.
# https://gitlab.com/gitlab-org/gitlab/-/issues/263430
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)
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)
end
end
......@@ -56,6 +56,6 @@ module SnowplowHelpers
# end
# end
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
......@@ -13,7 +13,7 @@ RSpec.configure do |config|
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
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