Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
bd091da6d5
commit
475d5a7a17
|
@ -6,9 +6,9 @@ module Types
|
|||
graphql_name 'DiffPositionInput'
|
||||
|
||||
argument :new_line, GraphQL::Types::Int, required: false,
|
||||
description: copy_field_description(Types::Notes::DiffPositionType, :new_line)
|
||||
description: "#{copy_field_description(Types::Notes::DiffPositionType, :new_line)} Please see the [REST API Documentation](https://docs.gitlab.com/ee/api/discussions.html#create-a-new-thread-in-the-merge-request-diff) for more information on how to use this field."
|
||||
argument :old_line, GraphQL::Types::Int, required: false,
|
||||
description: copy_field_description(Types::Notes::DiffPositionType, :old_line)
|
||||
description: "#{copy_field_description(Types::Notes::DiffPositionType, :old_line)} Please see the [REST API Documentation](https://docs.gitlab.com/ee/api/discussions.html#create-a-new-thread-in-the-merge-request-diff) for more information on how to use this field."
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -61,7 +61,7 @@ module LabelsHelper
|
|||
render_label_text(
|
||||
label.name,
|
||||
suffix: suffix,
|
||||
css_class: "gl-label-text #{text_color_class_for_bg(label.color)}",
|
||||
css_class: "gl-label-text #{label.text_color_class}",
|
||||
bg_color: label.color
|
||||
)
|
||||
end
|
||||
|
@ -114,30 +114,8 @@ module LabelsHelper
|
|||
end
|
||||
end
|
||||
|
||||
def text_color_class_for_bg(bg_color)
|
||||
if light_color?(bg_color)
|
||||
'gl-label-text-dark'
|
||||
else
|
||||
'gl-label-text-light'
|
||||
end
|
||||
end
|
||||
|
||||
def text_color_for_bg(bg_color)
|
||||
if light_color?(bg_color)
|
||||
'#333333'
|
||||
else
|
||||
'#FFFFFF'
|
||||
end
|
||||
end
|
||||
|
||||
def light_color?(color)
|
||||
if color.length == 4
|
||||
r, g, b = color[1, 4].scan(/./).map { |v| (v * 2).hex }
|
||||
else
|
||||
r, g, b = color[1, 7].scan(/.{2}/).map(&:hex)
|
||||
end
|
||||
|
||||
(r + g + b) > 500
|
||||
::Gitlab::Color.of(bg_color).contrast
|
||||
end
|
||||
|
||||
def labels_filter_path_with_defaults(only_group_labels: false, include_ancestor_groups: true, include_descendant_groups: false)
|
||||
|
|
|
@ -49,6 +49,16 @@ class Integration < ApplicationRecord
|
|||
|
||||
serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize
|
||||
|
||||
attr_encrypted :encrypted_properties_tmp,
|
||||
attribute: :encrypted_properties,
|
||||
mode: :per_attribute_iv,
|
||||
key: Settings.attr_encrypted_db_key_base_32,
|
||||
algorithm: 'aes-256-gcm',
|
||||
marshal: true,
|
||||
marshaler: ::Gitlab::Json,
|
||||
encode: false,
|
||||
encode_iv: false
|
||||
|
||||
alias_attribute :type, :type_new
|
||||
|
||||
default_value_for :active, false
|
||||
|
@ -67,6 +77,8 @@ class Integration < ApplicationRecord
|
|||
default_value_for :wiki_page_events, true
|
||||
|
||||
after_initialize :initialize_properties
|
||||
after_initialize :copy_properties_to_encrypted_properties
|
||||
before_save :copy_properties_to_encrypted_properties
|
||||
|
||||
after_commit :reset_updated_properties
|
||||
|
||||
|
@ -123,8 +135,10 @@ class Integration < ApplicationRecord
|
|||
|
||||
def #{arg}=(value)
|
||||
self.properties ||= {}
|
||||
self.encrypted_properties_tmp = properties
|
||||
updated_properties['#{arg}'] = #{arg} unless #{arg}_changed?
|
||||
self.properties['#{arg}'] = value
|
||||
self.encrypted_properties_tmp['#{arg}'] = value
|
||||
end
|
||||
|
||||
def #{arg}_changed?
|
||||
|
@ -354,6 +368,12 @@ class Integration < ApplicationRecord
|
|||
self.properties = {} if has_attribute?(:properties) && properties.nil?
|
||||
end
|
||||
|
||||
def copy_properties_to_encrypted_properties
|
||||
self.encrypted_properties_tmp = properties
|
||||
rescue ActiveModel::MissingAttributeError
|
||||
# ignore - in a record built from using a restricted select list
|
||||
end
|
||||
|
||||
def title
|
||||
# implement inside child
|
||||
end
|
||||
|
@ -394,7 +414,21 @@ class Integration < ApplicationRecord
|
|||
# return a hash of columns => values suitable for passing to insert_all
|
||||
def to_integration_hash
|
||||
column = self.class.attribute_aliases.fetch('type', 'type')
|
||||
as_json(except: %w[id instance project_id group_id]).merge(column => type)
|
||||
copy_properties_to_encrypted_properties
|
||||
|
||||
as_json(except: %w[id instance project_id group_id encrypted_properties_tmp])
|
||||
.merge(column => type)
|
||||
.merge(reencrypt_properties)
|
||||
end
|
||||
|
||||
def reencrypt_properties
|
||||
unless properties.nil? || properties.empty?
|
||||
alg = self.class.encrypted_attributes[:encrypted_properties_tmp][:algorithm]
|
||||
iv = generate_iv(alg)
|
||||
ep = self.class.encrypt(:encrypted_properties_tmp, properties, { iv: iv })
|
||||
end
|
||||
|
||||
{ 'encrypted_properties' => ep, 'encrypted_properties_iv' => iv }
|
||||
end
|
||||
|
||||
def to_data_fields_hash
|
||||
|
|
|
@ -12,8 +12,9 @@ class Label < ApplicationRecord
|
|||
|
||||
cache_markdown_field :description, pipeline: :single_line
|
||||
|
||||
DEFAULT_COLOR = '#6699cc'
|
||||
DEFAULT_COLOR = ::Gitlab::Color.of('#6699cc')
|
||||
|
||||
attribute :color, ::Gitlab::Database::Type::Color.new
|
||||
default_value_for :color, DEFAULT_COLOR
|
||||
|
||||
has_many :lists, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||
|
@ -22,9 +23,9 @@ class Label < ApplicationRecord
|
|||
has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
|
||||
has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest'
|
||||
|
||||
before_validation :strip_whitespace_from_title_and_color
|
||||
before_validation :strip_whitespace_from_title
|
||||
|
||||
validates :color, color: true, allow_blank: false
|
||||
validates :color, color: true, presence: true
|
||||
|
||||
# Don't allow ',' for label titles
|
||||
validates :title, presence: true, format: { with: /\A[^,]+\z/ }
|
||||
|
@ -212,7 +213,7 @@ class Label < ApplicationRecord
|
|||
end
|
||||
|
||||
def text_color
|
||||
LabelsHelper.text_color_for_bg(self.color)
|
||||
color.contrast
|
||||
end
|
||||
|
||||
def title=(value)
|
||||
|
@ -285,8 +286,8 @@ class Label < ApplicationRecord
|
|||
CGI.unescapeHTML(Sanitize.clean(value.to_s))
|
||||
end
|
||||
|
||||
def strip_whitespace_from_title_and_color
|
||||
%w(color title).each { |attr| self[attr] = self[attr]&.strip }
|
||||
def strip_whitespace_from_title
|
||||
self[:title] = title&.strip
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -126,36 +126,26 @@ module Namespaces
|
|||
end
|
||||
|
||||
def self_and_descendants_with_comparison_operators(include_self: true)
|
||||
base = all.select(
|
||||
:traversal_ids,
|
||||
'LEAD (namespaces.traversal_ids, 1) OVER (ORDER BY namespaces.traversal_ids ASC) next_traversal_ids'
|
||||
)
|
||||
base = all.select(:traversal_ids)
|
||||
base_cte = Gitlab::SQL::CTE.new(:descendants_base_cte, base)
|
||||
|
||||
namespaces = Arel::Table.new(:namespaces)
|
||||
|
||||
# Bound the search space to ourselves (optional) and descendants.
|
||||
#
|
||||
# WHERE (base_cte.next_traversal_ids IS NULL OR base_cte.next_traversal_ids > namespaces.traversal_ids)
|
||||
# AND next_traversal_ids_sibling(base_cte.traversal_ids) > namespaces.traversal_ids
|
||||
# WHERE next_traversal_ids_sibling(base_cte.traversal_ids) > namespaces.traversal_ids
|
||||
records = unscoped
|
||||
.distinct
|
||||
.with(base_cte.to_arel)
|
||||
.from([base_cte.table, namespaces])
|
||||
.where(base_cte.table[:next_traversal_ids].eq(nil).or(base_cte.table[:next_traversal_ids].gt(namespaces[:traversal_ids])))
|
||||
.where(next_sibling_func(base_cte.table[:traversal_ids]).gt(namespaces[:traversal_ids]))
|
||||
|
||||
# AND base_cte.traversal_ids <= namespaces.traversal_ids
|
||||
records = if include_self
|
||||
records.where(base_cte.table[:traversal_ids].lteq(namespaces[:traversal_ids]))
|
||||
else
|
||||
records.where(base_cte.table[:traversal_ids].lt(namespaces[:traversal_ids]))
|
||||
end
|
||||
|
||||
records_cte = Gitlab::SQL::CTE.new(:descendants_cte, records)
|
||||
|
||||
unscoped
|
||||
.unscope(where: [:type])
|
||||
.with(base_cte.to_arel, records_cte.to_arel)
|
||||
.from(records_cte.alias_to(namespaces))
|
||||
if include_self
|
||||
records.where(base_cte.table[:traversal_ids].lteq(namespaces[:traversal_ids]))
|
||||
else
|
||||
records.where(base_cte.table[:traversal_ids].lt(namespaces[:traversal_ids]))
|
||||
end
|
||||
end
|
||||
|
||||
def next_sibling_func(*args)
|
||||
|
|
|
@ -14,6 +14,10 @@ class LabelPresenter < Gitlab::View::Presenter::Delegated
|
|||
end
|
||||
end
|
||||
|
||||
def text_color_class
|
||||
"gl-label-text-#{label.color.contrast.luminosity}"
|
||||
end
|
||||
|
||||
def destroy_path
|
||||
case label
|
||||
when GroupLabel then group_label_path(label.group, label)
|
||||
|
|
|
@ -57,7 +57,8 @@ module Analytics
|
|||
def html_description(event)
|
||||
options = {}
|
||||
if event.label_based?
|
||||
options[:label_html] = render_label(event.label, link: '', small: true, tooltip: true)
|
||||
label = event.label.present
|
||||
options[:label_html] = render_label(label, link: '', small: true, tooltip: true)
|
||||
end
|
||||
|
||||
content_tag(:p) { event.html_description(options).html_safe }
|
||||
|
|
|
@ -4,7 +4,9 @@ class LabelEntity < Grape::Entity
|
|||
expose :id
|
||||
|
||||
expose :title
|
||||
expose :color
|
||||
expose :color do |label|
|
||||
label.color.to_s
|
||||
end
|
||||
expose :description
|
||||
expose :group_id
|
||||
expose :project_id
|
||||
|
|
|
@ -2,162 +2,8 @@
|
|||
|
||||
module Labels
|
||||
class BaseService < ::BaseService
|
||||
COLOR_NAME_TO_HEX = {
|
||||
black: '#000000',
|
||||
silver: '#C0C0C0',
|
||||
gray: '#808080',
|
||||
white: '#FFFFFF',
|
||||
maroon: '#800000',
|
||||
red: '#FF0000',
|
||||
purple: '#800080',
|
||||
fuchsia: '#FF00FF',
|
||||
green: '#008000',
|
||||
lime: '#00FF00',
|
||||
olive: '#808000',
|
||||
yellow: '#FFFF00',
|
||||
navy: '#000080',
|
||||
blue: '#0000FF',
|
||||
teal: '#008080',
|
||||
aqua: '#00FFFF',
|
||||
orange: '#FFA500',
|
||||
aliceblue: '#F0F8FF',
|
||||
antiquewhite: '#FAEBD7',
|
||||
aquamarine: '#7FFFD4',
|
||||
azure: '#F0FFFF',
|
||||
beige: '#F5F5DC',
|
||||
bisque: '#FFE4C4',
|
||||
blanchedalmond: '#FFEBCD',
|
||||
blueviolet: '#8A2BE2',
|
||||
brown: '#A52A2A',
|
||||
burlywood: '#DEB887',
|
||||
cadetblue: '#5F9EA0',
|
||||
chartreuse: '#7FFF00',
|
||||
chocolate: '#D2691E',
|
||||
coral: '#FF7F50',
|
||||
cornflowerblue: '#6495ED',
|
||||
cornsilk: '#FFF8DC',
|
||||
crimson: '#DC143C',
|
||||
darkblue: '#00008B',
|
||||
darkcyan: '#008B8B',
|
||||
darkgoldenrod: '#B8860B',
|
||||
darkgray: '#A9A9A9',
|
||||
darkgreen: '#006400',
|
||||
darkgrey: '#A9A9A9',
|
||||
darkkhaki: '#BDB76B',
|
||||
darkmagenta: '#8B008B',
|
||||
darkolivegreen: '#556B2F',
|
||||
darkorange: '#FF8C00',
|
||||
darkorchid: '#9932CC',
|
||||
darkred: '#8B0000',
|
||||
darksalmon: '#E9967A',
|
||||
darkseagreen: '#8FBC8F',
|
||||
darkslateblue: '#483D8B',
|
||||
darkslategray: '#2F4F4F',
|
||||
darkslategrey: '#2F4F4F',
|
||||
darkturquoise: '#00CED1',
|
||||
darkviolet: '#9400D3',
|
||||
deeppink: '#FF1493',
|
||||
deepskyblue: '#00BFFF',
|
||||
dimgray: '#696969',
|
||||
dimgrey: '#696969',
|
||||
dodgerblue: '#1E90FF',
|
||||
firebrick: '#B22222',
|
||||
floralwhite: '#FFFAF0',
|
||||
forestgreen: '#228B22',
|
||||
gainsboro: '#DCDCDC',
|
||||
ghostwhite: '#F8F8FF',
|
||||
gold: '#FFD700',
|
||||
goldenrod: '#DAA520',
|
||||
greenyellow: '#ADFF2F',
|
||||
grey: '#808080',
|
||||
honeydew: '#F0FFF0',
|
||||
hotpink: '#FF69B4',
|
||||
indianred: '#CD5C5C',
|
||||
indigo: '#4B0082',
|
||||
ivory: '#FFFFF0',
|
||||
khaki: '#F0E68C',
|
||||
lavender: '#E6E6FA',
|
||||
lavenderblush: '#FFF0F5',
|
||||
lawngreen: '#7CFC00',
|
||||
lemonchiffon: '#FFFACD',
|
||||
lightblue: '#ADD8E6',
|
||||
lightcoral: '#F08080',
|
||||
lightcyan: '#E0FFFF',
|
||||
lightgoldenrodyellow: '#FAFAD2',
|
||||
lightgray: '#D3D3D3',
|
||||
lightgreen: '#90EE90',
|
||||
lightgrey: '#D3D3D3',
|
||||
lightpink: '#FFB6C1',
|
||||
lightsalmon: '#FFA07A',
|
||||
lightseagreen: '#20B2AA',
|
||||
lightskyblue: '#87CEFA',
|
||||
lightslategray: '#778899',
|
||||
lightslategrey: '#778899',
|
||||
lightsteelblue: '#B0C4DE',
|
||||
lightyellow: '#FFFFE0',
|
||||
limegreen: '#32CD32',
|
||||
linen: '#FAF0E6',
|
||||
mediumaquamarine: '#66CDAA',
|
||||
mediumblue: '#0000CD',
|
||||
mediumorchid: '#BA55D3',
|
||||
mediumpurple: '#9370DB',
|
||||
mediumseagreen: '#3CB371',
|
||||
mediumslateblue: '#7B68EE',
|
||||
mediumspringgreen: '#00FA9A',
|
||||
mediumturquoise: '#48D1CC',
|
||||
mediumvioletred: '#C71585',
|
||||
midnightblue: '#191970',
|
||||
mintcream: '#F5FFFA',
|
||||
mistyrose: '#FFE4E1',
|
||||
moccasin: '#FFE4B5',
|
||||
navajowhite: '#FFDEAD',
|
||||
oldlace: '#FDF5E6',
|
||||
olivedrab: '#6B8E23',
|
||||
orangered: '#FF4500',
|
||||
orchid: '#DA70D6',
|
||||
palegoldenrod: '#EEE8AA',
|
||||
palegreen: '#98FB98',
|
||||
paleturquoise: '#AFEEEE',
|
||||
palevioletred: '#DB7093',
|
||||
papayawhip: '#FFEFD5',
|
||||
peachpuff: '#FFDAB9',
|
||||
peru: '#CD853F',
|
||||
pink: '#FFC0CB',
|
||||
plum: '#DDA0DD',
|
||||
powderblue: '#B0E0E6',
|
||||
rosybrown: '#BC8F8F',
|
||||
royalblue: '#4169E1',
|
||||
saddlebrown: '#8B4513',
|
||||
salmon: '#FA8072',
|
||||
sandybrown: '#F4A460',
|
||||
seagreen: '#2E8B57',
|
||||
seashell: '#FFF5EE',
|
||||
sienna: '#A0522D',
|
||||
skyblue: '#87CEEB',
|
||||
slateblue: '#6A5ACD',
|
||||
slategray: '#708090',
|
||||
slategrey: '#708090',
|
||||
snow: '#FFFAFA',
|
||||
springgreen: '#00FF7F',
|
||||
steelblue: '#4682B4',
|
||||
tan: '#D2B48C',
|
||||
thistle: '#D8BFD8',
|
||||
tomato: '#FF6347',
|
||||
turquoise: '#40E0D0',
|
||||
violet: '#EE82EE',
|
||||
wheat: '#F5DEB3',
|
||||
whitesmoke: '#F5F5F5',
|
||||
yellowgreen: '#9ACD32',
|
||||
rebeccapurple: '#663399'
|
||||
}.freeze
|
||||
|
||||
def convert_color_name_to_hex
|
||||
color = params[:color]
|
||||
color_name = color.strip.downcase
|
||||
|
||||
return color if color_name.start_with?('#')
|
||||
|
||||
COLOR_NAME_TO_HEX[color_name.to_sym] || color
|
||||
::Gitlab::Color.of(params[:color])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -21,7 +21,10 @@ module Security
|
|||
source_reports.first.type,
|
||||
source_reports.first.pipeline,
|
||||
source_reports.first.created_at
|
||||
).tap { |report| report.errors = source_reports.flat_map(&:errors) }
|
||||
).tap do |report|
|
||||
report.errors = source_reports.flat_map(&:errors)
|
||||
report.warnings = source_reports.flat_map(&:warnings)
|
||||
end
|
||||
end
|
||||
|
||||
def copy_resources_to_target_report
|
||||
|
|
|
@ -12,11 +12,13 @@
|
|||
# end
|
||||
#
|
||||
class ColorValidator < ActiveModel::EachValidator
|
||||
PATTERN = /\A\#(?:[0-9A-Fa-f]{3}){1,2}\Z/.freeze
|
||||
|
||||
def validate_each(record, attribute, value)
|
||||
unless value =~ PATTERN
|
||||
record.errors.add(attribute, "must be a valid color code")
|
||||
case value
|
||||
when NilClass then return
|
||||
when ::Gitlab::Color then return if value.valid?
|
||||
when ::String then return if ::Gitlab::Color.new(value).valid?
|
||||
end
|
||||
|
||||
record.errors.add(attribute, "must be a valid color code")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -113,7 +113,7 @@
|
|||
%span.gl-text-gray-500
|
||||
= _("no name set")
|
||||
%td= registration[:created_at].to_date.to_s(:medium)
|
||||
%td= link_to _('Delete'), registration[:delete_path], method: :delete, class: "gl-button btn btn-danger float-right", data: { confirm: _('Are you sure you want to delete this device? This action cannot be undone.') }
|
||||
%td= link_to _('Delete'), registration[:delete_path], method: :delete, class: "gl-button btn btn-danger float-right", data: { confirm: _('Are you sure you want to delete this device? This action cannot be undone.'), confirm_btn_variant: "danger" }, aria: { label: _('Delete') }
|
||||
|
||||
- else
|
||||
.settings-message.text-center
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: enforce_security_report_validation
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79798
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/351000
|
||||
milestone: '14.8'
|
||||
name: show_report_validation_warnings
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80930
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353125
|
||||
milestone: '14.9'
|
||||
type: development
|
||||
group: group::threat insights
|
||||
default_enabled: false
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIntegrationsEncryptedProperties < Gitlab::Database::Migration[1.0]
|
||||
def change
|
||||
add_column :integrations, :encrypted_properties, :binary
|
||||
add_column :integrations, :encrypted_properties_iv, :binary
|
||||
end
|
||||
end
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class EncryptIntegrationProperties < Gitlab::Database::Migration[1.0]
|
||||
disable_ddl_transaction!
|
||||
MIGRATION = 'EncryptIntegrationProperties'
|
||||
BATCH_SIZE = 1_000
|
||||
INTERVAL = 2.minutes.to_i
|
||||
|
||||
def up
|
||||
queue_background_migration_jobs_by_range_at_intervals(
|
||||
define_batchable_model('integrations').all,
|
||||
MIGRATION,
|
||||
INTERVAL,
|
||||
track_jobs: true,
|
||||
batch_size: BATCH_SIZE
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
# this migration is not reversible
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
9d98618a1e9fd0474c45ac54420fc64a1d90ad77f36be594337e5b117fccdadb
|
|
@ -0,0 +1 @@
|
|||
1593e935601ae1f2ab788109687bb40bad026f3f425339a39c8d13d3e4c7e306
|
|
@ -16093,6 +16093,8 @@ CREATE TABLE integrations (
|
|||
type_new text,
|
||||
vulnerability_events boolean DEFAULT false NOT NULL,
|
||||
archive_trace_events boolean DEFAULT false NOT NULL,
|
||||
encrypted_properties bytea,
|
||||
encrypted_properties_iv bytea,
|
||||
CONSTRAINT check_a948a0aa7e CHECK ((char_length(type_new) <= 255))
|
||||
);
|
||||
|
||||
|
|
|
@ -15255,6 +15255,7 @@ Represents the security scan information.
|
|||
| ---- | ---- | ----------- |
|
||||
| <a id="scanerrors"></a>`errors` | [`[String!]!`](#string) | List of errors. |
|
||||
| <a id="scanname"></a>`name` | [`String!`](#string) | Name of the scan. |
|
||||
| <a id="scanwarnings"></a>`warnings` | [`[String!]!`](#string) | List of warnings. |
|
||||
|
||||
### `ScanExecutionPolicy`
|
||||
|
||||
|
@ -19814,8 +19815,8 @@ Input type for DastSiteProfile authentication.
|
|||
| ---- | ---- | ----------- |
|
||||
| <a id="diffpositioninputbasesha"></a>`baseSha` | [`String`](#string) | Merge base of the branch the comment was made on. |
|
||||
| <a id="diffpositioninputheadsha"></a>`headSha` | [`String!`](#string) | SHA of the HEAD at the time the comment was made. |
|
||||
| <a id="diffpositioninputnewline"></a>`newLine` | [`Int`](#int) | Line on HEAD SHA that was changed. |
|
||||
| <a id="diffpositioninputoldline"></a>`oldLine` | [`Int`](#int) | Line on start SHA that was changed. |
|
||||
| <a id="diffpositioninputnewline"></a>`newLine` | [`Int`](#int) | Line on HEAD SHA that was changed. Please see the [REST API Documentation](https://docs.gitlab.com/ee/api/discussions.html#create-a-new-thread-in-the-merge-request-diff) for more information on how to use this field. |
|
||||
| <a id="diffpositioninputoldline"></a>`oldLine` | [`Int`](#int) | Line on start SHA that was changed. Please see the [REST API Documentation](https://docs.gitlab.com/ee/api/discussions.html#create-a-new-thread-in-the-merge-request-diff) for more information on how to use this field. |
|
||||
| <a id="diffpositioninputpaths"></a>`paths` | [`DiffPathsInput!`](#diffpathsinput) | The paths of the file that was changed. Both of the properties of this input are optional, but at least one of them is required. |
|
||||
| <a id="diffpositioninputstartsha"></a>`startSha` | [`String!`](#string) | SHA of the branch being compared against. |
|
||||
|
||||
|
|
|
@ -460,8 +460,7 @@ parameter when using `check_allowed_absolute_path!()`.
|
|||
To use a combination of both checks, follow the example below:
|
||||
|
||||
```ruby
|
||||
path = Gitlab::Utils.check_path_traversal!(path)
|
||||
Gitlab::Utils.check_allowed_absolute_path!(path, path_allowlist)
|
||||
Gitlab::Utils.check_allowed_absolute_path_and_path_traversal!(path, path_allowlist)
|
||||
```
|
||||
|
||||
In the REST API, we have the [`FilePath`](https://gitlab.com/gitlab-org/security/gitlab/-/blob/master/lib/api/validations/validators/file_path.rb)
|
||||
|
|
|
@ -33,10 +33,10 @@ To enable 2FA for all users:
|
|||
If you want 2FA enforcement to take effect during the next sign-in attempt,
|
||||
change the grace period to `0`.
|
||||
|
||||
## Disable 2FA enforcement through rails console
|
||||
## Disable 2FA enforcement through Rails console
|
||||
|
||||
Using the [rails console](../administration/operations/rails_console.md), enforcing 2FA for
|
||||
all user can be disabled. Connect to the rails console and run:
|
||||
Using the [Rails console](../administration/operations/rails_console.md), enforcing 2FA for
|
||||
all user can be disabled. Connect to the Rails console and run:
|
||||
|
||||
```ruby
|
||||
Gitlab::CurrentSettings.update!('require_two_factor_authentication': false)
|
||||
|
@ -108,13 +108,10 @@ reactivate 2FA from scratch if they want to use it again.
|
|||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/270554) in GitLab 13.7.
|
||||
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/299088) from GitLab Free to GitLab Premium in 13.9.
|
||||
> - It's [deployed behind a feature flag](../user/feature_flags.md), disabled by default.
|
||||
> - It's disabled on GitLab.com.
|
||||
> - It's not recommended for production use.
|
||||
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-2fa-for-git-operations).
|
||||
> - It's deployed behind a feature flag, disabled by default.
|
||||
|
||||
WARNING:
|
||||
This feature might not be available to you. Check the **version history** note above for details.
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `two_factor_for_cli`. On GitLab.com, this feature is not available. The feature is not ready for production use. This feature flag also affects [session duration for Git Operations when 2FA is enabled](../user/admin_area/settings/account_and_limit_settings.md#customize-session-duration-for-git-operations-when-2fa-is-enabled).
|
||||
|
||||
Two-factor authentication can be enforced for Git over SSH operations. However, we recommend using
|
||||
[ED25519_SK](../ssh/index.md#ed25519_sk-ssh-keys) or [ECDSA_SK](../ssh/index.md#ecdsa_sk-ssh-keys) SSH keys instead.
|
||||
|
@ -135,30 +132,6 @@ After the OTP is verified, Git over SSH operations can be used for a session dur
|
|||
Once an OTP is verified, anyone can run Git over SSH with that private SSH key for
|
||||
the configured [session duration](../user/admin_area/settings/account_and_limit_settings.md#customize-session-duration-for-git-operations-when-2fa-is-enabled).
|
||||
|
||||
### Enable or disable 2FA for Git operations
|
||||
|
||||
2FA for Git operations is under development and not
|
||||
ready for production use. It is deployed behind a feature flag that is
|
||||
**disabled by default**. [GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md)
|
||||
can enable it.
|
||||
|
||||
To enable it:
|
||||
|
||||
```ruby
|
||||
Feature.enable(:two_factor_for_cli)
|
||||
```
|
||||
|
||||
To disable it:
|
||||
|
||||
```ruby
|
||||
Feature.disable(:two_factor_for_cli)
|
||||
```
|
||||
|
||||
The feature flag affects these features:
|
||||
|
||||
- [Two-factor Authentication (2FA) for Git over SSH operations](#2fa-for-git-over-ssh-operations).
|
||||
- [Customize session duration for Git Operations when 2FA is enabled](../user/admin_area/settings/account_and_limit_settings.md#customize-session-duration-for-git-operations-when-2fa-is-enabled).
|
||||
|
||||
<!-- ## Troubleshooting
|
||||
|
||||
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
|
||||
|
|
|
@ -178,14 +178,9 @@ nginx['client_max_body_size'] = "200m"
|
|||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/296669) in GitLab 13.9.
|
||||
> - It's deployed behind a feature flag, disabled by default.
|
||||
> - It's disabled on GitLab.com.
|
||||
> - It's not recommended for production use.
|
||||
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](../../../security/two_factor_authentication.md#enable-or-disable-2fa-for-git-operations).
|
||||
|
||||
NOTE:
|
||||
This feature is under development and not ready for production use. It is deployed
|
||||
behind a feature flag that is **disabled by default**. To use it in GitLab
|
||||
self-managed instances, ask a GitLab administrator to [enable it](../../../security/two_factor_authentication.md#enable-or-disable-2fa-for-git-operations).
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `two_factor_for_cli`. On GitLab.com, this feature is not available. This feature is not ready for production use. This feature flag also affects [2FA for Git over SSH operations](../../../security/two_factor_authentication.md#2fa-for-git-over-ssh-operations).
|
||||
|
||||
GitLab administrators can choose to customize the session duration (in minutes) for Git operations when 2FA is enabled. The default is 15 and this can be set to a value between 1 and 10080.
|
||||
|
||||
|
|
|
@ -33,13 +33,14 @@ usernames. A GitLab administrator can configure the GitLab instance to
|
|||
|
||||
## Project members permissions
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/219299) in GitLab 14.8, personal namespace owners appear with Owner role in new projects in their namespace. Introduced [with a flag](../administration/feature_flags.md) named `personal_project_owner_with_owner_access`. Disabled by default.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/219299) in GitLab 14.8, personal namespace owners appear with Owner role in new projects in their namespace. Introduced [with a flag](../administration/feature_flags.md) named `personal_project_owner_with_owner_access`. Disabled by default.
|
||||
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/351919) in GitLab 14.9.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, personal namespace owners appearing with the Owner role in new projects in their namespace is disabled. To make it available,
|
||||
ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `personal_project_owner_with_owner_access`.
|
||||
The feature is not ready for production use.
|
||||
On GitLab.com, this feature is not available.
|
||||
On GitLab.com, this feature is available.
|
||||
|
||||
A user's role determines what permissions they have on a project. The Owner role provides all permissions but is
|
||||
available only:
|
||||
|
|
|
@ -3,7 +3,11 @@
|
|||
module API
|
||||
module Entities
|
||||
class LabelBasic < Grape::Entity
|
||||
expose :id, :name, :color, :description, :description_html, :text_color
|
||||
expose :id, :name, :description, :description_html, :text_color
|
||||
|
||||
expose :color do |label, options|
|
||||
label.color.to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,8 +8,7 @@ module API
|
|||
options = @option.is_a?(Hash) ? @option : {}
|
||||
path_allowlist = options.fetch(:allowlist, [])
|
||||
path = params[attr_name]
|
||||
path = Gitlab::Utils.check_path_traversal!(path)
|
||||
Gitlab::Utils.check_allowed_absolute_path!(path, path_allowlist)
|
||||
Gitlab::Utils.check_allowed_absolute_path_and_path_traversal!(path, path_allowlist)
|
||||
rescue StandardError
|
||||
raise Grape::Exceptions::Validation.new(
|
||||
params: [@scope.full_name(attr_name)],
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
# Migrates the integration.properties column from plaintext to encrypted text.
|
||||
class EncryptIntegrationProperties
|
||||
# The Integration model, with just the relevant bits.
|
||||
class Integration < ActiveRecord::Base
|
||||
include EachBatch
|
||||
|
||||
ALGORITHM = 'aes-256-gcm'
|
||||
|
||||
self.table_name = 'integrations'
|
||||
self.inheritance_column = :_type_disabled
|
||||
|
||||
scope :with_properties, -> { where.not(properties: nil) }
|
||||
scope :not_already_encrypted, -> { where(encrypted_properties: nil) }
|
||||
scope :for_batch, ->(range) { where(id: range) }
|
||||
|
||||
attr_encrypted :encrypted_properties_tmp,
|
||||
attribute: :encrypted_properties,
|
||||
mode: :per_attribute_iv,
|
||||
key: ::Settings.attr_encrypted_db_key_base_32,
|
||||
algorithm: ALGORITHM,
|
||||
marshal: true,
|
||||
marshaler: ::Gitlab::Json,
|
||||
encode: false,
|
||||
encode_iv: false
|
||||
|
||||
# See 'Integration#reencrypt_properties'
|
||||
def encrypt_properties
|
||||
data = ::Gitlab::Json.parse(properties)
|
||||
iv = generate_iv(ALGORITHM)
|
||||
ep = self.class.encrypt(:encrypted_properties_tmp, data, { iv: iv })
|
||||
|
||||
[ep, iv]
|
||||
end
|
||||
end
|
||||
|
||||
def perform(start_id, stop_id)
|
||||
batch_query = Integration.with_properties.not_already_encrypted.for_batch(start_id..stop_id)
|
||||
encrypt_batch(batch_query)
|
||||
mark_job_as_succeeded(start_id, stop_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mark_job_as_succeeded(*arguments)
|
||||
Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
|
||||
self.class.name.demodulize,
|
||||
arguments
|
||||
)
|
||||
end
|
||||
|
||||
# represent binary string as a PSQL binary literal:
|
||||
# https://www.postgresql.org/docs/9.4/datatype-binary.html
|
||||
def bytea(value)
|
||||
"'\\x#{value.unpack1('H*')}'::bytea"
|
||||
end
|
||||
|
||||
def encrypt_batch(batch_query)
|
||||
values = batch_query.select(:id, :properties).map do |record|
|
||||
encrypted_properties, encrypted_properties_iv = record.encrypt_properties
|
||||
"(#{record.id}, #{bytea(encrypted_properties)}, #{bytea(encrypted_properties_iv)})"
|
||||
end
|
||||
|
||||
return if values.empty?
|
||||
|
||||
Integration.connection.execute(<<~SQL.squish)
|
||||
WITH cte(cte_id, cte_encrypted_properties, cte_encrypted_properties_iv)
|
||||
AS #{::Gitlab::Database::AsWithMaterialized.materialized_if_supported} (
|
||||
SELECT *
|
||||
FROM (VALUES #{values.join(',')}) AS t (id, encrypted_properties, encrypted_properties_iv)
|
||||
)
|
||||
UPDATE #{Integration.table_name}
|
||||
SET encrypted_properties = cte_encrypted_properties
|
||||
, encrypted_properties_iv = cte_encrypted_properties_iv
|
||||
FROM cte
|
||||
WHERE cte_id = id
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -42,14 +42,19 @@ module Gitlab
|
|||
attr_reader :json_data, :report, :validate
|
||||
|
||||
def valid?
|
||||
if Feature.enabled?(:enforce_security_report_validation)
|
||||
if !validate || schema_validator.valid?
|
||||
report.schema_validation_status = :valid_schema
|
||||
true
|
||||
if Feature.enabled?(:show_report_validation_warnings)
|
||||
# We want validation to happen regardless of VALIDATE_SCHEMA CI variable
|
||||
schema_validation_passed = schema_validator.valid?
|
||||
|
||||
if validate
|
||||
schema_validator.errors.each { |error| report.add_error('Schema', error) } unless schema_validation_passed
|
||||
|
||||
schema_validation_passed
|
||||
else
|
||||
report.schema_validation_status = :invalid_schema
|
||||
schema_validator.errors.each { |error| report.add_error('Schema', error) }
|
||||
false
|
||||
# We treat all schema validation errors as warnings
|
||||
schema_validator.errors.each { |error| report.add_warning('Schema', error) }
|
||||
|
||||
true
|
||||
end
|
||||
else
|
||||
return true if !validate || schema_validator.valid?
|
||||
|
|
|
@ -6,7 +6,7 @@ module Gitlab
|
|||
module Security
|
||||
class Report
|
||||
attr_reader :created_at, :type, :pipeline, :findings, :scanners, :identifiers
|
||||
attr_accessor :scan, :scanned_resources, :errors, :analyzer, :version, :schema_validation_status
|
||||
attr_accessor :scan, :scanned_resources, :errors, :analyzer, :version, :schema_validation_status, :warnings
|
||||
|
||||
delegate :project_id, to: :pipeline
|
||||
|
||||
|
@ -19,6 +19,7 @@ module Gitlab
|
|||
@identifiers = {}
|
||||
@scanned_resources = []
|
||||
@errors = []
|
||||
@warnings = []
|
||||
end
|
||||
|
||||
def commit_sha
|
||||
|
@ -29,6 +30,10 @@ module Gitlab
|
|||
errors << { type: type, message: message }
|
||||
end
|
||||
|
||||
def add_warning(type, message)
|
||||
warnings << { type: type, message: message }
|
||||
end
|
||||
|
||||
def errored?
|
||||
errors.present?
|
||||
end
|
||||
|
|
|
@ -0,0 +1,222 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
class Color
|
||||
PATTERN = /\A\#(?:[0-9A-Fa-f]{3}){1,2}\Z/.freeze
|
||||
|
||||
def initialize(value)
|
||||
@value = value&.strip&.freeze
|
||||
end
|
||||
|
||||
module Constants
|
||||
DARK = Color.new('#333333')
|
||||
LIGHT = Color.new('#FFFFFF')
|
||||
|
||||
COLOR_NAME_TO_HEX = {
|
||||
black: '#000000',
|
||||
silver: '#C0C0C0',
|
||||
gray: '#808080',
|
||||
white: '#FFFFFF',
|
||||
maroon: '#800000',
|
||||
red: '#FF0000',
|
||||
purple: '#800080',
|
||||
fuchsia: '#FF00FF',
|
||||
green: '#008000',
|
||||
lime: '#00FF00',
|
||||
olive: '#808000',
|
||||
yellow: '#FFFF00',
|
||||
navy: '#000080',
|
||||
blue: '#0000FF',
|
||||
teal: '#008080',
|
||||
aqua: '#00FFFF',
|
||||
orange: '#FFA500',
|
||||
aliceblue: '#F0F8FF',
|
||||
antiquewhite: '#FAEBD7',
|
||||
aquamarine: '#7FFFD4',
|
||||
azure: '#F0FFFF',
|
||||
beige: '#F5F5DC',
|
||||
bisque: '#FFE4C4',
|
||||
blanchedalmond: '#FFEBCD',
|
||||
blueviolet: '#8A2BE2',
|
||||
brown: '#A52A2A',
|
||||
burlywood: '#DEB887',
|
||||
cadetblue: '#5F9EA0',
|
||||
chartreuse: '#7FFF00',
|
||||
chocolate: '#D2691E',
|
||||
coral: '#FF7F50',
|
||||
cornflowerblue: '#6495ED',
|
||||
cornsilk: '#FFF8DC',
|
||||
crimson: '#DC143C',
|
||||
darkblue: '#00008B',
|
||||
darkcyan: '#008B8B',
|
||||
darkgoldenrod: '#B8860B',
|
||||
darkgray: '#A9A9A9',
|
||||
darkgreen: '#006400',
|
||||
darkgrey: '#A9A9A9',
|
||||
darkkhaki: '#BDB76B',
|
||||
darkmagenta: '#8B008B',
|
||||
darkolivegreen: '#556B2F',
|
||||
darkorange: '#FF8C00',
|
||||
darkorchid: '#9932CC',
|
||||
darkred: '#8B0000',
|
||||
darksalmon: '#E9967A',
|
||||
darkseagreen: '#8FBC8F',
|
||||
darkslateblue: '#483D8B',
|
||||
darkslategray: '#2F4F4F',
|
||||
darkslategrey: '#2F4F4F',
|
||||
darkturquoise: '#00CED1',
|
||||
darkviolet: '#9400D3',
|
||||
deeppink: '#FF1493',
|
||||
deepskyblue: '#00BFFF',
|
||||
dimgray: '#696969',
|
||||
dimgrey: '#696969',
|
||||
dodgerblue: '#1E90FF',
|
||||
firebrick: '#B22222',
|
||||
floralwhite: '#FFFAF0',
|
||||
forestgreen: '#228B22',
|
||||
gainsboro: '#DCDCDC',
|
||||
ghostwhite: '#F8F8FF',
|
||||
gold: '#FFD700',
|
||||
goldenrod: '#DAA520',
|
||||
greenyellow: '#ADFF2F',
|
||||
grey: '#808080',
|
||||
honeydew: '#F0FFF0',
|
||||
hotpink: '#FF69B4',
|
||||
indianred: '#CD5C5C',
|
||||
indigo: '#4B0082',
|
||||
ivory: '#FFFFF0',
|
||||
khaki: '#F0E68C',
|
||||
lavender: '#E6E6FA',
|
||||
lavenderblush: '#FFF0F5',
|
||||
lawngreen: '#7CFC00',
|
||||
lemonchiffon: '#FFFACD',
|
||||
lightblue: '#ADD8E6',
|
||||
lightcoral: '#F08080',
|
||||
lightcyan: '#E0FFFF',
|
||||
lightgoldenrodyellow: '#FAFAD2',
|
||||
lightgray: '#D3D3D3',
|
||||
lightgreen: '#90EE90',
|
||||
lightgrey: '#D3D3D3',
|
||||
lightpink: '#FFB6C1',
|
||||
lightsalmon: '#FFA07A',
|
||||
lightseagreen: '#20B2AA',
|
||||
lightskyblue: '#87CEFA',
|
||||
lightslategray: '#778899',
|
||||
lightslategrey: '#778899',
|
||||
lightsteelblue: '#B0C4DE',
|
||||
lightyellow: '#FFFFE0',
|
||||
limegreen: '#32CD32',
|
||||
linen: '#FAF0E6',
|
||||
mediumaquamarine: '#66CDAA',
|
||||
mediumblue: '#0000CD',
|
||||
mediumorchid: '#BA55D3',
|
||||
mediumpurple: '#9370DB',
|
||||
mediumseagreen: '#3CB371',
|
||||
mediumslateblue: '#7B68EE',
|
||||
mediumspringgreen: '#00FA9A',
|
||||
mediumturquoise: '#48D1CC',
|
||||
mediumvioletred: '#C71585',
|
||||
midnightblue: '#191970',
|
||||
mintcream: '#F5FFFA',
|
||||
mistyrose: '#FFE4E1',
|
||||
moccasin: '#FFE4B5',
|
||||
navajowhite: '#FFDEAD',
|
||||
oldlace: '#FDF5E6',
|
||||
olivedrab: '#6B8E23',
|
||||
orangered: '#FF4500',
|
||||
orchid: '#DA70D6',
|
||||
palegoldenrod: '#EEE8AA',
|
||||
palegreen: '#98FB98',
|
||||
paleturquoise: '#AFEEEE',
|
||||
palevioletred: '#DB7093',
|
||||
papayawhip: '#FFEFD5',
|
||||
peachpuff: '#FFDAB9',
|
||||
peru: '#CD853F',
|
||||
pink: '#FFC0CB',
|
||||
plum: '#DDA0DD',
|
||||
powderblue: '#B0E0E6',
|
||||
rosybrown: '#BC8F8F',
|
||||
royalblue: '#4169E1',
|
||||
saddlebrown: '#8B4513',
|
||||
salmon: '#FA8072',
|
||||
sandybrown: '#F4A460',
|
||||
seagreen: '#2E8B57',
|
||||
seashell: '#FFF5EE',
|
||||
sienna: '#A0522D',
|
||||
skyblue: '#87CEEB',
|
||||
slateblue: '#6A5ACD',
|
||||
slategray: '#708090',
|
||||
slategrey: '#708090',
|
||||
snow: '#FFFAFA',
|
||||
springgreen: '#00FF7F',
|
||||
steelblue: '#4682B4',
|
||||
tan: '#D2B48C',
|
||||
thistle: '#D8BFD8',
|
||||
tomato: '#FF6347',
|
||||
turquoise: '#40E0D0',
|
||||
violet: '#EE82EE',
|
||||
wheat: '#F5DEB3',
|
||||
whitesmoke: '#F5F5F5',
|
||||
yellowgreen: '#9ACD32',
|
||||
rebeccapurple: '#663399'
|
||||
}.stringify_keys.transform_values { Color.new(_1) }.freeze
|
||||
end
|
||||
|
||||
def self.of(color)
|
||||
raise ArgumentError, 'No color spec' unless color
|
||||
return color if color.is_a?(self)
|
||||
|
||||
color = color.to_s.strip
|
||||
Constants::COLOR_NAME_TO_HEX[color.downcase] || new(color)
|
||||
end
|
||||
|
||||
def to_s
|
||||
@value.to_s
|
||||
end
|
||||
|
||||
def as_json(_options = nil)
|
||||
to_s
|
||||
end
|
||||
|
||||
def eql(other)
|
||||
return false unless other.is_a?(self.class)
|
||||
|
||||
to_s == other.to_s
|
||||
end
|
||||
alias_method :==, :eql
|
||||
|
||||
def valid?
|
||||
PATTERN.match?(@value)
|
||||
end
|
||||
|
||||
def light?
|
||||
valid? && rgb.sum > 500
|
||||
end
|
||||
|
||||
def luminosity
|
||||
return :light if light?
|
||||
|
||||
:dark
|
||||
end
|
||||
|
||||
def contrast
|
||||
return Constants::DARK if light?
|
||||
|
||||
Constants::LIGHT
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def rgb
|
||||
return [] unless valid?
|
||||
|
||||
@rgb ||= begin
|
||||
if @value.length == 4
|
||||
@value[1, 4].scan(/./).map { |v| (v * 2).hex }
|
||||
else
|
||||
@value[1, 7].scan(/.{2}/).map(&:hex)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Database
|
||||
module Type
|
||||
class Color < ActiveModel::Type::Value
|
||||
def serialize(value)
|
||||
value.to_s if value
|
||||
end
|
||||
|
||||
def serializable?(value)
|
||||
value.nil? || value.is_a?(::String) || value.is_a?(::Gitlab::Color)
|
||||
end
|
||||
|
||||
def cast_value(value)
|
||||
::Gitlab::Color.new(value.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -16,6 +16,9 @@ module Gitlab
|
|||
# @return [Boolean, String, Array, Hash]
|
||||
# @raise [JSON::ParserError] raised if parsing fails
|
||||
def parse(string, opts = {})
|
||||
# Parse nil as nil
|
||||
return if string.nil?
|
||||
|
||||
# First we should ensure this really is a string, not some other
|
||||
# type which purports to be a string. This handles some legacy
|
||||
# usage of the JSON class.
|
||||
|
@ -30,6 +33,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
alias_method :parse!, :parse
|
||||
alias_method :load, :parse
|
||||
|
||||
# Restricted method for converting a Ruby object to JSON. If you
|
||||
# need to pass options to this, you should use `.generate` instead,
|
||||
|
@ -67,6 +71,14 @@ module Gitlab
|
|||
::JSON.pretty_generate(object, opts)
|
||||
end
|
||||
|
||||
# The standard parser error we should be returning. Defined in a method
|
||||
# so we can potentially override it later.
|
||||
#
|
||||
# @return [JSON::ParserError]
|
||||
def parser_error
|
||||
::JSON::ParserError
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Convert JSON string into Ruby through toggleable adapters.
|
||||
|
@ -134,14 +146,6 @@ module Gitlab
|
|||
opts
|
||||
end
|
||||
|
||||
# The standard parser error we should be returning. Defined in a method
|
||||
# so we can potentially override it later.
|
||||
#
|
||||
# @return [JSON::ParserError]
|
||||
def parser_error
|
||||
::JSON::ParserError
|
||||
end
|
||||
|
||||
# @param [Nil, Boolean] an extracted :legacy_mode key from the opts hash
|
||||
# @return [Boolean]
|
||||
def legacy_mode_enabled?(arg_value)
|
||||
|
|
|
@ -25,8 +25,8 @@ module Gitlab
|
|||
log_queries(id, data, 'active-record')
|
||||
log_queries(id, data, 'gitaly')
|
||||
log_queries(id, data, 'redis')
|
||||
rescue StandardError => err
|
||||
logger.error(message: "failed to process request id #{id}: #{err.message}")
|
||||
rescue StandardError => e
|
||||
logger.error(message: "failed to process request id #{id}: #{e.message}")
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -34,6 +34,8 @@ module Gitlab
|
|||
def request(id)
|
||||
# Peek gem stores request data under peek:requests:request_id key
|
||||
json_data = @redis.get("peek:requests:#{id}")
|
||||
raise "No data for #{id}" if json_data.nil?
|
||||
|
||||
Gitlab::Json.parse(json_data)
|
||||
end
|
||||
|
||||
|
|
|
@ -37,6 +37,13 @@ module Gitlab
|
|||
raise StandardError, "path #{path} is not allowed"
|
||||
end
|
||||
|
||||
def check_allowed_absolute_path_and_path_traversal!(path, path_allowlist)
|
||||
traversal_path = check_path_traversal!(path)
|
||||
raise StandardError, "path is not a string!" unless traversal_path.is_a?(String)
|
||||
|
||||
check_allowed_absolute_path!(traversal_path, path_allowlist)
|
||||
end
|
||||
|
||||
def decode_path(encoded_path)
|
||||
decoded = CGI.unescape(encoded_path)
|
||||
if decoded != CGI.unescape(decoded)
|
||||
|
|
|
@ -65,7 +65,7 @@ module QA
|
|||
end
|
||||
|
||||
def marked_for_deletion?
|
||||
!parse_body(api_get_from("#{api_get_path}"))[:marked_for_deletion_on].nil?
|
||||
!parse_body(api_get_from(api_get_path.to_s))[:marked_for_deletion_on].nil?
|
||||
end
|
||||
|
||||
# Get group badges
|
||||
|
@ -84,22 +84,6 @@ module QA
|
|||
end
|
||||
end
|
||||
|
||||
# Get group members
|
||||
#
|
||||
# @return [Array<QA::Resource::User>]
|
||||
def members
|
||||
parse_body(api_get_from("#{api_get_path}/members")).map do |member|
|
||||
User.init do |resource|
|
||||
resource.api_client = api_client
|
||||
resource.id = member[:id]
|
||||
resource.name = member[:name]
|
||||
resource.username = member[:username]
|
||||
resource.email = member[:email]
|
||||
resource.access_level = member[:access_level]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# API get path
|
||||
#
|
||||
# @return [String]
|
||||
|
|
|
@ -36,6 +36,10 @@ module QA
|
|||
next QA::Runtime::Logger.debug("#{resource.class.name} reused as :#{reuse_as} has already been removed.") unless resource.exists?
|
||||
next if resource.respond_to?(:marked_for_deletion?) && resource.marked_for_deletion?
|
||||
|
||||
if resource.reload!.api_resource[:marked_for_deletion_on].present?
|
||||
next QA::Runtime::Logger.debug("#{resource.class.name} reused as :#{reuse_as} is already scheduled to be removed.")
|
||||
end
|
||||
|
||||
resource.method(:remove_via_api!).super_method.call
|
||||
end
|
||||
end
|
||||
|
|
|
@ -147,39 +147,6 @@ module QA
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with group members' do
|
||||
let(:member) do
|
||||
Resource::User.fabricate_via_api! do |usr|
|
||||
usr.api_client = admin_api_client
|
||||
usr.hard_delete_on_api_removal = true
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
member.set_public_email
|
||||
source_group.add_member(member, Resource::Members::AccessLevel::DEVELOPER)
|
||||
|
||||
imported_group # trigger import
|
||||
end
|
||||
|
||||
after do
|
||||
member.remove_via_api!
|
||||
end
|
||||
|
||||
it(
|
||||
'adds members for imported group',
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347609'
|
||||
) do
|
||||
expect { imported_group.import_status }.to eventually_eq('finished').within(import_wait_duration)
|
||||
|
||||
imported_member = imported_group.reload!.members.find { |usr| usr.username == member.username }
|
||||
aggregate_failures do
|
||||
expect(imported_member).not_to be_nil
|
||||
expect(imported_member.access_level).to eq(Resource::Members::AccessLevel::DEVELOPER)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'gitlab_project_migration_common'
|
||||
|
||||
module QA
|
||||
RSpec.describe 'Manage' do
|
||||
describe 'Gitlab migration' do
|
||||
include_context 'with gitlab project migration'
|
||||
|
||||
let(:member) do
|
||||
Resource::User.fabricate_via_api! do |usr|
|
||||
usr.api_client = admin_api_client
|
||||
usr.hard_delete_on_api_removal = true
|
||||
end
|
||||
end
|
||||
|
||||
let(:imported_group_member) do
|
||||
imported_group.reload!.list_members.find { |usr| usr['username'] == member.username }
|
||||
end
|
||||
|
||||
let(:imported_project_member) do
|
||||
imported_project.reload!.list_members.find { |usr| usr['username'] == member.username }
|
||||
end
|
||||
|
||||
before do
|
||||
member.set_public_email
|
||||
end
|
||||
|
||||
after do
|
||||
member.remove_via_api!
|
||||
end
|
||||
|
||||
context 'with group member' do
|
||||
before do
|
||||
source_group.add_member(member, Resource::Members::AccessLevel::DEVELOPER)
|
||||
end
|
||||
|
||||
it 'member retains indirect membership in imported project' do
|
||||
expect_import_finished
|
||||
|
||||
aggregate_failures do
|
||||
expect(imported_project_member).to be_nil
|
||||
expect(imported_group_member&.fetch('access_level')).to eq(
|
||||
Resource::Members::AccessLevel::DEVELOPER
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with project member' do
|
||||
before do
|
||||
source_project.add_member(member, Resource::Members::AccessLevel::DEVELOPER)
|
||||
end
|
||||
|
||||
it 'member retains direct membership in imported project' do
|
||||
expect_import_finished
|
||||
|
||||
aggregate_failures do
|
||||
expect(imported_group_member).to be_nil
|
||||
expect(imported_project_member&.fetch('access_level')).to eq(
|
||||
Resource::Members::AccessLevel::DEVELOPER
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -20,6 +20,10 @@ RSpec.describe QA::Resource::ReusableCollection do
|
|||
end
|
||||
|
||||
def exists?() end
|
||||
|
||||
def reload!
|
||||
Struct.new(:api_resource).new({ marked_for_deletion_on: false })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -88,8 +92,22 @@ RSpec.describe QA::Resource::ReusableCollection do
|
|||
it 'removes each instance of each resource class' do
|
||||
described_class.remove_all_via_api!
|
||||
|
||||
expect(a_resource_instance.removed).to be true
|
||||
expect(another_resource_instance.removed).to be true
|
||||
expect(a_resource_instance.removed).to be_truthy
|
||||
expect(another_resource_instance.removed).to be_truthy
|
||||
end
|
||||
|
||||
context 'when a resource is marked for deletion' do
|
||||
before do
|
||||
marked_for_deletion = Struct.new(:api_resource).new({ marked_for_deletion_on: true })
|
||||
|
||||
allow(a_resource_instance).to receive(:reload!).and_return(marked_for_deletion)
|
||||
allow(another_resource_instance).to receive(:reload!).and_return(marked_for_deletion)
|
||||
end
|
||||
|
||||
it 'does not remove the resource' do
|
||||
expect(a_resource_instance.removed).to be_falsey
|
||||
expect(another_resource_instance.removed).to be_falsy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -353,7 +353,16 @@ RSpec.describe Projects::ServicesController do
|
|||
|
||||
it 'does not modify integration' do
|
||||
expect { put :update, params: project_params.merge(service: integration_params) }
|
||||
.not_to change { project.prometheus_integration.reload.attributes }
|
||||
.not_to change { prometheus_integration_as_data }
|
||||
end
|
||||
|
||||
def prometheus_integration_as_data
|
||||
pi = project.prometheus_integration.reload
|
||||
attrs = pi.attributes.except('encrypted_properties',
|
||||
'encrypted_properties_iv',
|
||||
'encrypted_properties_tmp')
|
||||
|
||||
[attrs, pi.encrypted_properties_tmp]
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -114,16 +114,16 @@ RSpec.describe LabelsHelper do
|
|||
|
||||
describe 'text_color_for_bg' do
|
||||
it 'uses light text on dark backgrounds' do
|
||||
expect(text_color_for_bg('#222E2E')).to eq('#FFFFFF')
|
||||
expect(text_color_for_bg('#222E2E')).to be_color('#FFFFFF')
|
||||
end
|
||||
|
||||
it 'uses dark text on light backgrounds' do
|
||||
expect(text_color_for_bg('#EEEEEE')).to eq('#333333')
|
||||
expect(text_color_for_bg('#EEEEEE')).to be_color('#333333')
|
||||
end
|
||||
|
||||
it 'supports RGB triplets' do
|
||||
expect(text_color_for_bg('#FFF')).to eq '#333333'
|
||||
expect(text_color_for_bg('#000')).to eq '#FFFFFF'
|
||||
expect(text_color_for_bg('#FFF')).to be_color '#333333'
|
||||
expect(text_color_for_bg('#000')).to be_color '#FFFFFF'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -277,7 +277,7 @@ RSpec.describe Banzai::Filter::References::LabelReferenceFilter do
|
|||
end
|
||||
|
||||
context 'References with html entities' do
|
||||
let!(:label) { create(:label, name: '<html>', project: project) }
|
||||
let!(:label) { create(:label, title: '<html>', project: project) }
|
||||
|
||||
it 'links to a valid reference' do
|
||||
doc = reference_filter('See ~"<html>"')
|
||||
|
|
|
@ -43,7 +43,7 @@ RSpec.describe BulkImports::Common::Pipelines::LabelsPipeline do
|
|||
|
||||
expect(label.title).to eq('Label 1')
|
||||
expect(label.description).to eq('Label 1')
|
||||
expect(label.color).to eq('#6699cc')
|
||||
expect(label.color).to be_color('#6699cc')
|
||||
expect(File.directory?(tmpdir)).to eq(false)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::EncryptIntegrationProperties do
|
||||
let(:integrations) do
|
||||
table(:integrations) do |integrations|
|
||||
integrations.send :attr_encrypted, :encrypted_properties_tmp,
|
||||
attribute: :encrypted_properties,
|
||||
mode: :per_attribute_iv,
|
||||
key: ::Settings.attr_encrypted_db_key_base_32,
|
||||
algorithm: 'aes-256-gcm',
|
||||
marshal: true,
|
||||
marshaler: ::Gitlab::Json,
|
||||
encode: false,
|
||||
encode_iv: false
|
||||
end
|
||||
end
|
||||
|
||||
let!(:no_properties) { integrations.create! }
|
||||
let!(:with_plaintext_1) { integrations.create!(properties: json_props(1)) }
|
||||
let!(:with_plaintext_2) { integrations.create!(properties: json_props(2)) }
|
||||
let!(:with_encrypted) do
|
||||
x = integrations.new
|
||||
x.properties = nil
|
||||
x.encrypted_properties_tmp = some_props(3)
|
||||
x.save!
|
||||
x
|
||||
end
|
||||
|
||||
let(:start_id) { integrations.minimum(:id) }
|
||||
let(:end_id) { integrations.maximum(:id) }
|
||||
|
||||
it 'ensures all properties are encrypted', :aggregate_failures do
|
||||
described_class.new.perform(start_id, end_id)
|
||||
|
||||
props = integrations.all.to_h do |record|
|
||||
[record.id, [Gitlab::Json.parse(record.properties), record.encrypted_properties_tmp]]
|
||||
end
|
||||
|
||||
expect(integrations.count).to eq(4)
|
||||
|
||||
expect(props).to match(
|
||||
no_properties.id => both(be_nil),
|
||||
with_plaintext_1.id => both(eq some_props(1)),
|
||||
with_plaintext_2.id => both(eq some_props(2)),
|
||||
with_encrypted.id => match([be_nil, eq(some_props(3))])
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def both(obj)
|
||||
match [obj, obj]
|
||||
end
|
||||
|
||||
def some_props(id)
|
||||
HashWithIndifferentAccess.new({ id: id, foo: 1, bar: true, baz: %w[a string array] })
|
||||
end
|
||||
|
||||
def json_props(id)
|
||||
some_props(id).to_json
|
||||
end
|
||||
end
|
|
@ -26,8 +26,6 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
|
|||
allow(parser).to receive(:tracking_data).and_return(tracking_data)
|
||||
allow(parser).to receive(:create_flags).and_return(vulnerability_flags_data)
|
||||
end
|
||||
|
||||
artifact.each_blob { |blob| described_class.parse!(blob, report, vulnerability_finding_signatures_enabled) }
|
||||
end
|
||||
|
||||
describe 'schema validation' do
|
||||
|
@ -40,13 +38,24 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
|
|||
allow(validator_class).to receive(:new).and_call_original
|
||||
end
|
||||
|
||||
context 'when enforce_security_report_validation is enabled' do
|
||||
context 'when show_report_validation_warnings is enabled' do
|
||||
before do
|
||||
stub_feature_flags(enforce_security_report_validation: true)
|
||||
stub_feature_flags(show_report_validation_warnings: true)
|
||||
end
|
||||
|
||||
context 'when the validate flag is set as `true`' do
|
||||
let(:validate) { true }
|
||||
context 'when the validate flag is set to `false`' do
|
||||
let(:validate) { false }
|
||||
let(:valid?) { false }
|
||||
let(:errors) { ['foo'] }
|
||||
|
||||
before do
|
||||
allow_next_instance_of(validator_class) do |instance|
|
||||
allow(instance).to receive(:valid?).and_return(valid?)
|
||||
allow(instance).to receive(:errors).and_return(errors)
|
||||
end
|
||||
|
||||
allow(parser).to receive_messages(create_scanner: true, create_scan: true)
|
||||
end
|
||||
|
||||
it 'instantiates the validator with correct params' do
|
||||
parse_report
|
||||
|
@ -54,26 +63,9 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
|
|||
expect(validator_class).to have_received(:new).with(report.type, {})
|
||||
end
|
||||
|
||||
context 'when the report data is valid according to the schema' do
|
||||
let(:valid?) { true }
|
||||
|
||||
before do
|
||||
allow_next_instance_of(validator_class) do |instance|
|
||||
allow(instance).to receive(:valid?).and_return(valid?)
|
||||
allow(instance).to receive(:errors).and_return([])
|
||||
end
|
||||
|
||||
allow(parser).to receive_messages(create_scanner: true, create_scan: true)
|
||||
end
|
||||
|
||||
it 'does not add errors to the report' do
|
||||
expect { parse_report }.not_to change { report.errors }.from([])
|
||||
end
|
||||
|
||||
it 'adds the schema validation status to the report' do
|
||||
parse_report
|
||||
|
||||
expect(report.schema_validation_status).to eq(:valid_schema)
|
||||
context 'when the report data is not valid according to the schema' do
|
||||
it 'adds warnings to the report' do
|
||||
expect { parse_report }.to change { report.warnings }.from([]).to([{ message: 'foo', type: 'Schema' }])
|
||||
end
|
||||
|
||||
it 'keeps the execution flow as normal' do
|
||||
|
@ -84,26 +76,46 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when the report data is not valid according to the schema' do
|
||||
let(:valid?) { false }
|
||||
context 'when the report data is valid according to the schema' do
|
||||
let(:valid?) { true }
|
||||
let(:errors) { [] }
|
||||
|
||||
before do
|
||||
allow_next_instance_of(validator_class) do |instance|
|
||||
allow(instance).to receive(:valid?).and_return(valid?)
|
||||
allow(instance).to receive(:errors).and_return(['foo'])
|
||||
end
|
||||
|
||||
allow(parser).to receive_messages(create_scanner: true, create_scan: true)
|
||||
it 'does not add warnings to the report' do
|
||||
expect { parse_report }.not_to change { report.errors }
|
||||
end
|
||||
|
||||
it 'adds errors to the report' do
|
||||
expect { parse_report }.to change { report.errors }.from([]).to([{ message: 'foo', type: 'Schema' }])
|
||||
end
|
||||
|
||||
it 'adds the schema validation status to the report' do
|
||||
it 'keeps the execution flow as normal' do
|
||||
parse_report
|
||||
|
||||
expect(report.schema_validation_status).to eq(:invalid_schema)
|
||||
expect(parser).to have_received(:create_scanner)
|
||||
expect(parser).to have_received(:create_scan)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the validate flag is set to `true`' do
|
||||
let(:validate) { true }
|
||||
let(:valid?) { false }
|
||||
let(:errors) { ['foo'] }
|
||||
|
||||
before do
|
||||
allow_next_instance_of(validator_class) do |instance|
|
||||
allow(instance).to receive(:valid?).and_return(valid?)
|
||||
allow(instance).to receive(:errors).and_return(errors)
|
||||
end
|
||||
|
||||
allow(parser).to receive_messages(create_scanner: true, create_scan: true)
|
||||
end
|
||||
|
||||
it 'instantiates the validator with correct params' do
|
||||
parse_report
|
||||
|
||||
expect(validator_class).to have_received(:new).with(report.type, {})
|
||||
end
|
||||
|
||||
context 'when the report data is not valid according to the schema' do
|
||||
it 'adds errors to the report' do
|
||||
expect { parse_report }.to change { report.errors }.from([]).to([{ message: 'foo', type: 'Schema' }])
|
||||
end
|
||||
|
||||
it 'does not try to create report entities' do
|
||||
|
@ -113,12 +125,28 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
|
|||
expect(parser).not_to have_received(:create_scan)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the report data is valid according to the schema' do
|
||||
let(:valid?) { true }
|
||||
let(:errors) { [] }
|
||||
|
||||
it 'does not add errors to the report' do
|
||||
expect { parse_report }.not_to change { report.errors }.from([])
|
||||
end
|
||||
|
||||
it 'keeps the execution flow as normal' do
|
||||
parse_report
|
||||
|
||||
expect(parser).to have_received(:create_scanner)
|
||||
expect(parser).to have_received(:create_scan)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when enforce_security_report_validation is disabled' do
|
||||
context 'when show_report_validation_warnings is disabled' do
|
||||
before do
|
||||
stub_feature_flags(enforce_security_report_validation: false)
|
||||
stub_feature_flags(show_report_validation_warnings: false)
|
||||
end
|
||||
|
||||
context 'when the validate flag is set as `false`' do
|
||||
|
@ -181,277 +209,283 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'parsing finding.name' do
|
||||
let(:artifact) { build(:ci_job_artifact, :common_security_report_with_blank_names) }
|
||||
|
||||
context 'when message is provided' do
|
||||
it 'sets message from the report as a finding name' do
|
||||
finding = report.findings.find { |x| x.compare_key == 'CVE-1020' }
|
||||
expected_name = Gitlab::Json.parse(finding.raw_metadata)['message']
|
||||
|
||||
expect(finding.name).to eq(expected_name)
|
||||
end
|
||||
context 'report parsing' do
|
||||
before do
|
||||
artifact.each_blob { |blob| described_class.parse!(blob, report, vulnerability_finding_signatures_enabled) }
|
||||
end
|
||||
|
||||
context 'when message is not provided' do
|
||||
context 'and name is provided' do
|
||||
it 'sets name from the report as a name' do
|
||||
finding = report.findings.find { |x| x.compare_key == 'CVE-1030' }
|
||||
expected_name = Gitlab::Json.parse(finding.raw_metadata)['name']
|
||||
describe 'parsing finding.name' do
|
||||
let(:artifact) { build(:ci_job_artifact, :common_security_report_with_blank_names) }
|
||||
|
||||
context 'when message is provided' do
|
||||
it 'sets message from the report as a finding name' do
|
||||
finding = report.findings.find { |x| x.compare_key == 'CVE-1020' }
|
||||
expected_name = Gitlab::Json.parse(finding.raw_metadata)['message']
|
||||
|
||||
expect(finding.name).to eq(expected_name)
|
||||
end
|
||||
end
|
||||
|
||||
context 'and name is not provided' do
|
||||
context 'when CVE identifier exists' do
|
||||
it 'combines identifier with location to create name' do
|
||||
finding = report.findings.find { |x| x.compare_key == 'CVE-2017-11429' }
|
||||
expect(finding.name).to eq("CVE-2017-11429 in yarn.lock")
|
||||
context 'when message is not provided' do
|
||||
context 'and name is provided' do
|
||||
it 'sets name from the report as a name' do
|
||||
finding = report.findings.find { |x| x.compare_key == 'CVE-1030' }
|
||||
expected_name = Gitlab::Json.parse(finding.raw_metadata)['name']
|
||||
|
||||
expect(finding.name).to eq(expected_name)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when CWE identifier exists' do
|
||||
it 'combines identifier with location to create name' do
|
||||
finding = report.findings.find { |x| x.compare_key == 'CWE-2017-11429' }
|
||||
expect(finding.name).to eq("CWE-2017-11429 in yarn.lock")
|
||||
context 'and name is not provided' do
|
||||
context 'when CVE identifier exists' do
|
||||
it 'combines identifier with location to create name' do
|
||||
finding = report.findings.find { |x| x.compare_key == 'CVE-2017-11429' }
|
||||
expect(finding.name).to eq("CVE-2017-11429 in yarn.lock")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when neither CVE nor CWE identifier exist' do
|
||||
it 'combines identifier with location to create name' do
|
||||
finding = report.findings.find { |x| x.compare_key == 'OTHER-2017-11429' }
|
||||
expect(finding.name).to eq("other-2017-11429 in yarn.lock")
|
||||
context 'when CWE identifier exists' do
|
||||
it 'combines identifier with location to create name' do
|
||||
finding = report.findings.find { |x| x.compare_key == 'CWE-2017-11429' }
|
||||
expect(finding.name).to eq("CWE-2017-11429 in yarn.lock")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when neither CVE nor CWE identifier exist' do
|
||||
it 'combines identifier with location to create name' do
|
||||
finding = report.findings.find { |x| x.compare_key == 'OTHER-2017-11429' }
|
||||
expect(finding.name).to eq("other-2017-11429 in yarn.lock")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'parsing finding.details' do
|
||||
context 'when details are provided' do
|
||||
it 'sets details from the report' do
|
||||
finding = report.findings.find { |x| x.compare_key == 'CVE-1020' }
|
||||
expected_details = Gitlab::Json.parse(finding.raw_metadata)['details']
|
||||
describe 'parsing finding.details' do
|
||||
context 'when details are provided' do
|
||||
it 'sets details from the report' do
|
||||
finding = report.findings.find { |x| x.compare_key == 'CVE-1020' }
|
||||
expected_details = Gitlab::Json.parse(finding.raw_metadata)['details']
|
||||
|
||||
expect(finding.details).to eq(expected_details)
|
||||
expect(finding.details).to eq(expected_details)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when details are not provided' do
|
||||
it 'sets empty hash' do
|
||||
finding = report.findings.find { |x| x.compare_key == 'CVE-1030' }
|
||||
expect(finding.details).to eq({})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when details are not provided' do
|
||||
it 'sets empty hash' do
|
||||
finding = report.findings.find { |x| x.compare_key == 'CVE-1030' }
|
||||
expect(finding.details).to eq({})
|
||||
describe 'top-level scanner' do
|
||||
it 'is the primary scanner' do
|
||||
expect(report.primary_scanner.external_id).to eq('gemnasium')
|
||||
expect(report.primary_scanner.name).to eq('Gemnasium')
|
||||
expect(report.primary_scanner.vendor).to eq('GitLab')
|
||||
expect(report.primary_scanner.version).to eq('2.18.0')
|
||||
end
|
||||
|
||||
it 'returns nil report has no scanner' do
|
||||
empty_report = Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago)
|
||||
described_class.parse!({}.to_json, empty_report)
|
||||
|
||||
expect(empty_report.primary_scanner).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'top-level scanner' do
|
||||
it 'is the primary scanner' do
|
||||
expect(report.primary_scanner.external_id).to eq('gemnasium')
|
||||
expect(report.primary_scanner.name).to eq('Gemnasium')
|
||||
expect(report.primary_scanner.vendor).to eq('GitLab')
|
||||
expect(report.primary_scanner.version).to eq('2.18.0')
|
||||
end
|
||||
describe 'parsing scanners' do
|
||||
subject(:scanner) { report.findings.first.scanner }
|
||||
|
||||
it 'returns nil report has no scanner' do
|
||||
empty_report = Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago)
|
||||
described_class.parse!({}.to_json, empty_report)
|
||||
|
||||
expect(empty_report.primary_scanner).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'parsing scanners' do
|
||||
subject(:scanner) { report.findings.first.scanner }
|
||||
|
||||
context 'when vendor is not missing in scanner' do
|
||||
it 'returns scanner with parsed vendor value' do
|
||||
expect(scanner.vendor).to eq('GitLab')
|
||||
context 'when vendor is not missing in scanner' do
|
||||
it 'returns scanner with parsed vendor value' do
|
||||
expect(scanner.vendor).to eq('GitLab')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'parsing scan' do
|
||||
it 'returns scan object for each finding' do
|
||||
scans = report.findings.map(&:scan)
|
||||
describe 'parsing scan' do
|
||||
it 'returns scan object for each finding' do
|
||||
scans = report.findings.map(&:scan)
|
||||
|
||||
expect(scans.map(&:status).all?('success')).to be(true)
|
||||
expect(scans.map(&:start_time).all?('placeholder-value')).to be(true)
|
||||
expect(scans.map(&:end_time).all?('placeholder-value')).to be(true)
|
||||
expect(scans.size).to eq(3)
|
||||
expect(scans.first).to be_a(::Gitlab::Ci::Reports::Security::Scan)
|
||||
expect(scans.map(&:status).all?('success')).to be(true)
|
||||
expect(scans.map(&:start_time).all?('placeholder-value')).to be(true)
|
||||
expect(scans.map(&:end_time).all?('placeholder-value')).to be(true)
|
||||
expect(scans.size).to eq(3)
|
||||
expect(scans.first).to be_a(::Gitlab::Ci::Reports::Security::Scan)
|
||||
end
|
||||
|
||||
it 'returns nil when scan is not a hash' do
|
||||
empty_report = Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago)
|
||||
described_class.parse!({}.to_json, empty_report)
|
||||
|
||||
expect(empty_report.scan).to be(nil)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns nil when scan is not a hash' do
|
||||
empty_report = Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago)
|
||||
described_class.parse!({}.to_json, empty_report)
|
||||
describe 'parsing schema version' do
|
||||
it 'parses the version' do
|
||||
expect(report.version).to eq('14.0.2')
|
||||
end
|
||||
|
||||
expect(empty_report.scan).to be(nil)
|
||||
end
|
||||
end
|
||||
it 'returns nil when there is no version' do
|
||||
empty_report = Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago)
|
||||
described_class.parse!({}.to_json, empty_report)
|
||||
|
||||
describe 'parsing schema version' do
|
||||
it 'parses the version' do
|
||||
expect(report.version).to eq('14.0.2')
|
||||
expect(empty_report.version).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns nil when there is no version' do
|
||||
empty_report = Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago)
|
||||
described_class.parse!({}.to_json, empty_report)
|
||||
describe 'parsing analyzer' do
|
||||
it 'associates analyzer with report' do
|
||||
expect(report.analyzer.id).to eq('common-analyzer')
|
||||
expect(report.analyzer.name).to eq('Common Analyzer')
|
||||
expect(report.analyzer.version).to eq('2.0.1')
|
||||
expect(report.analyzer.vendor).to eq('Common')
|
||||
end
|
||||
|
||||
expect(empty_report.version).to be_nil
|
||||
end
|
||||
end
|
||||
it 'returns nil when analyzer data is not available' do
|
||||
empty_report = Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago)
|
||||
described_class.parse!({}.to_json, empty_report)
|
||||
|
||||
describe 'parsing analyzer' do
|
||||
it 'associates analyzer with report' do
|
||||
expect(report.analyzer.id).to eq('common-analyzer')
|
||||
expect(report.analyzer.name).to eq('Common Analyzer')
|
||||
expect(report.analyzer.version).to eq('2.0.1')
|
||||
expect(report.analyzer.vendor).to eq('Common')
|
||||
expect(empty_report.analyzer).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns nil when analyzer data is not available' do
|
||||
empty_report = Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago)
|
||||
described_class.parse!({}.to_json, empty_report)
|
||||
describe 'parsing flags' do
|
||||
it 'returns flags object for each finding' do
|
||||
flags = report.findings.first.flags
|
||||
|
||||
expect(empty_report.analyzer).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'parsing flags' do
|
||||
it 'returns flags object for each finding' do
|
||||
flags = report.findings.first.flags
|
||||
|
||||
expect(flags).to contain_exactly(
|
||||
have_attributes(type: 'flagged-as-likely-false-positive', origin: 'post analyzer X', description: 'static string to sink'),
|
||||
expect(flags).to contain_exactly(
|
||||
have_attributes(type: 'flagged-as-likely-false-positive', origin: 'post analyzer X', description: 'static string to sink'),
|
||||
have_attributes(type: 'flagged-as-likely-false-positive', origin: 'post analyzer Y', description: 'integer to sink')
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'parsing links' do
|
||||
it 'returns links object for each finding', :aggregate_failures do
|
||||
links = report.findings.flat_map(&:links)
|
||||
|
||||
expect(links.map(&:url)).to match_array(['https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1020', 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1030'])
|
||||
expect(links.map(&:name)).to match_array([nil, 'CVE-1030'])
|
||||
expect(links.size).to eq(2)
|
||||
expect(links.first).to be_a(::Gitlab::Ci::Reports::Security::Link)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'parsing evidence' do
|
||||
it 'returns evidence object for each finding', :aggregate_failures do
|
||||
evidences = report.findings.map(&:evidence)
|
||||
|
||||
expect(evidences.first.data).not_to be_empty
|
||||
expect(evidences.first.data["summary"]).to match(/The Origin header was changed/)
|
||||
expect(evidences.size).to eq(3)
|
||||
expect(evidences.compact.size).to eq(2)
|
||||
expect(evidences.first).to be_a(::Gitlab::Ci::Reports::Security::Evidence)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'setting the uuid' do
|
||||
let(:finding_uuids) { report.findings.map(&:uuid) }
|
||||
let(:uuid_1) do
|
||||
Security::VulnerabilityUUID.generate(
|
||||
report_type: "sast",
|
||||
primary_identifier_fingerprint: report.findings[0].identifiers.first.fingerprint,
|
||||
location_fingerprint: location.fingerprint,
|
||||
project_id: pipeline.project_id
|
||||
)
|
||||
end
|
||||
|
||||
let(:uuid_2) do
|
||||
Security::VulnerabilityUUID.generate(
|
||||
report_type: "sast",
|
||||
primary_identifier_fingerprint: report.findings[1].identifiers.first.fingerprint,
|
||||
location_fingerprint: location.fingerprint,
|
||||
project_id: pipeline.project_id
|
||||
)
|
||||
end
|
||||
|
||||
let(:expected_uuids) { [uuid_1, uuid_2, nil] }
|
||||
|
||||
it 'sets the UUIDv5 for findings', :aggregate_failures do
|
||||
allow_next_instance_of(Gitlab::Ci::Reports::Security::Report) do |report|
|
||||
allow(report).to receive(:type).and_return('sast')
|
||||
|
||||
expect(finding_uuids).to match_array(expected_uuids)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'parsing tracking' do
|
||||
let(:tracking_data) do
|
||||
{
|
||||
'type' => 'source',
|
||||
'items' => [
|
||||
'signatures' => [
|
||||
{ 'algorithm' => 'hash', 'value' => 'hash_value' },
|
||||
{ 'algorithm' => 'location', 'value' => 'location_value' },
|
||||
{ 'algorithm' => 'scope_offset', 'value' => 'scope_offset_value' }
|
||||
]
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
context 'with valid tracking information' do
|
||||
it 'creates signatures for each algorithm' do
|
||||
finding = report.findings.first
|
||||
expect(finding.signatures.size).to eq(3)
|
||||
expect(finding.signatures.map(&:algorithm_type).to_set).to eq(Set['hash', 'location', 'scope_offset'])
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid tracking information' do
|
||||
describe 'parsing links' do
|
||||
it 'returns links object for each finding', :aggregate_failures do
|
||||
links = report.findings.flat_map(&:links)
|
||||
|
||||
expect(links.map(&:url)).to match_array(['https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1020', 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1030'])
|
||||
expect(links.map(&:name)).to match_array([nil, 'CVE-1030'])
|
||||
expect(links.size).to eq(2)
|
||||
expect(links.first).to be_a(::Gitlab::Ci::Reports::Security::Link)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'parsing evidence' do
|
||||
it 'returns evidence object for each finding', :aggregate_failures do
|
||||
evidences = report.findings.map(&:evidence)
|
||||
|
||||
expect(evidences.first.data).not_to be_empty
|
||||
expect(evidences.first.data["summary"]).to match(/The Origin header was changed/)
|
||||
expect(evidences.size).to eq(3)
|
||||
expect(evidences.compact.size).to eq(2)
|
||||
expect(evidences.first).to be_a(::Gitlab::Ci::Reports::Security::Evidence)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'setting the uuid' do
|
||||
let(:finding_uuids) { report.findings.map(&:uuid) }
|
||||
let(:uuid_1) do
|
||||
Security::VulnerabilityUUID.generate(
|
||||
report_type: "sast",
|
||||
primary_identifier_fingerprint: report.findings[0].identifiers.first.fingerprint,
|
||||
location_fingerprint: location.fingerprint,
|
||||
project_id: pipeline.project_id
|
||||
)
|
||||
end
|
||||
|
||||
let(:uuid_2) do
|
||||
Security::VulnerabilityUUID.generate(
|
||||
report_type: "sast",
|
||||
primary_identifier_fingerprint: report.findings[1].identifiers.first.fingerprint,
|
||||
location_fingerprint: location.fingerprint,
|
||||
project_id: pipeline.project_id
|
||||
)
|
||||
end
|
||||
|
||||
let(:expected_uuids) { [uuid_1, uuid_2, nil] }
|
||||
|
||||
it 'sets the UUIDv5 for findings', :aggregate_failures do
|
||||
allow_next_instance_of(Gitlab::Ci::Reports::Security::Report) do |report|
|
||||
allow(report).to receive(:type).and_return('sast')
|
||||
|
||||
expect(finding_uuids).to match_array(expected_uuids)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'parsing tracking' do
|
||||
let(:tracking_data) do
|
||||
{
|
||||
'type' => 'source',
|
||||
'items' => [
|
||||
'signatures' => [
|
||||
{ 'algorithm' => 'hash', 'value' => 'hash_value' },
|
||||
{ 'algorithm' => 'location', 'value' => 'location_value' },
|
||||
{ 'algorithm' => 'INVALID', 'value' => 'scope_offset_value' }
|
||||
]
|
||||
]
|
||||
'type' => 'source',
|
||||
'items' => [
|
||||
'signatures' => [
|
||||
{ 'algorithm' => 'hash', 'value' => 'hash_value' },
|
||||
{ 'algorithm' => 'location', 'value' => 'location_value' },
|
||||
{ 'algorithm' => 'scope_offset', 'value' => 'scope_offset_value' }
|
||||
]
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
it 'ignores invalid algorithm types' do
|
||||
finding = report.findings.first
|
||||
expect(finding.signatures.size).to eq(2)
|
||||
expect(finding.signatures.map(&:algorithm_type).to_set).to eq(Set['hash', 'location'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with valid tracking information' do
|
||||
it 'creates signatures for each signature algorithm' do
|
||||
finding = report.findings.first
|
||||
expect(finding.signatures.size).to eq(3)
|
||||
expect(finding.signatures.map(&:algorithm_type)).to eq(%w[hash location scope_offset])
|
||||
|
||||
signatures = finding.signatures.index_by(&:algorithm_type)
|
||||
expected_values = tracking_data['items'][0]['signatures'].index_by { |x| x['algorithm'] }
|
||||
expect(signatures['hash'].signature_value).to eq(expected_values['hash']['value'])
|
||||
expect(signatures['location'].signature_value).to eq(expected_values['location']['value'])
|
||||
expect(signatures['scope_offset'].signature_value).to eq(expected_values['scope_offset']['value'])
|
||||
context 'with valid tracking information' do
|
||||
it 'creates signatures for each algorithm' do
|
||||
finding = report.findings.first
|
||||
expect(finding.signatures.size).to eq(3)
|
||||
expect(finding.signatures.map(&:algorithm_type).to_set).to eq(Set['hash', 'location', 'scope_offset'])
|
||||
end
|
||||
end
|
||||
|
||||
it 'sets the uuid according to the higest priority signature' do
|
||||
finding = report.findings.first
|
||||
highest_signature = finding.signatures.max_by(&:priority)
|
||||
context 'with invalid tracking information' do
|
||||
let(:tracking_data) do
|
||||
{
|
||||
'type' => 'source',
|
||||
'items' => [
|
||||
'signatures' => [
|
||||
{ 'algorithm' => 'hash', 'value' => 'hash_value' },
|
||||
{ 'algorithm' => 'location', 'value' => 'location_value' },
|
||||
{ 'algorithm' => 'INVALID', 'value' => 'scope_offset_value' }
|
||||
]
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
identifiers = if vulnerability_finding_signatures_enabled
|
||||
"#{finding.report_type}-#{finding.primary_identifier.fingerprint}-#{highest_signature.signature_hex}-#{report.project_id}"
|
||||
else
|
||||
"#{finding.report_type}-#{finding.primary_identifier.fingerprint}-#{finding.location.fingerprint}-#{report.project_id}"
|
||||
end
|
||||
it 'ignores invalid algorithm types' do
|
||||
finding = report.findings.first
|
||||
expect(finding.signatures.size).to eq(2)
|
||||
expect(finding.signatures.map(&:algorithm_type).to_set).to eq(Set['hash', 'location'])
|
||||
end
|
||||
end
|
||||
|
||||
expect(finding.uuid).to eq(Gitlab::UUID.v5(identifiers))
|
||||
context 'with valid tracking information' do
|
||||
it 'creates signatures for each signature algorithm' do
|
||||
finding = report.findings.first
|
||||
expect(finding.signatures.size).to eq(3)
|
||||
expect(finding.signatures.map(&:algorithm_type)).to eq(%w[hash location scope_offset])
|
||||
|
||||
signatures = finding.signatures.index_by(&:algorithm_type)
|
||||
expected_values = tracking_data['items'][0]['signatures'].index_by { |x| x['algorithm'] }
|
||||
expect(signatures['hash'].signature_value).to eq(expected_values['hash']['value'])
|
||||
expect(signatures['location'].signature_value).to eq(expected_values['location']['value'])
|
||||
expect(signatures['scope_offset'].signature_value).to eq(expected_values['scope_offset']['value'])
|
||||
end
|
||||
|
||||
it 'sets the uuid according to the higest priority signature' do
|
||||
finding = report.findings.first
|
||||
highest_signature = finding.signatures.max_by(&:priority)
|
||||
|
||||
identifiers = if vulnerability_finding_signatures_enabled
|
||||
"#{finding.report_type}-#{finding.primary_identifier.fingerprint}-#{highest_signature.signature_hex}-#{report.project_id}"
|
||||
else
|
||||
"#{finding.report_type}-#{finding.primary_identifier.fingerprint}-#{finding.location.fingerprint}-#{report.project_id}"
|
||||
end
|
||||
|
||||
expect(finding.uuid).to eq(Gitlab::UUID.v5(identifiers))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -158,6 +158,16 @@ RSpec.describe Gitlab::Ci::Reports::Security::Report do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#add_warning' do
|
||||
context 'when the message is given' do
|
||||
it 'adds a new warning to report' do
|
||||
expect { report.add_warning('foo', 'bar') }.to change { report.warnings }
|
||||
.from([])
|
||||
.to([{ type: 'foo', message: 'bar' }])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'errored?' do
|
||||
subject { report.errored? }
|
||||
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'fast_spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Color do
|
||||
describe ".of" do
|
||||
described_class::Constants::COLOR_NAME_TO_HEX.each do |name, value|
|
||||
it "parses #{name} to #{value}" do
|
||||
expect(described_class.of(name)).to eq(value)
|
||||
end
|
||||
end
|
||||
|
||||
it 'parses hex literals as colors' do
|
||||
expect(described_class.of('#fff')).to eq(described_class.new('#fff'))
|
||||
expect(described_class.of('#fefefe')).to eq(described_class.new('#fefefe'))
|
||||
end
|
||||
|
||||
it 'raises if the input is nil' do
|
||||
expect { described_class.of(nil) }.to raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it 'returns an invalid color if the input is not valid' do
|
||||
expect(described_class.of('unknown color')).not_to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
describe '#new' do
|
||||
it 'handles nil values' do
|
||||
expect(described_class.new(nil)).to eq(described_class.new(nil))
|
||||
end
|
||||
|
||||
it 'strips input' do
|
||||
expect(described_class.new(' abc ')).to eq(described_class.new('abc'))
|
||||
end
|
||||
end
|
||||
|
||||
describe '#valid?' do
|
||||
described_class::Constants::COLOR_NAME_TO_HEX.each_key do |name|
|
||||
specify "#{name} is a valid color" do
|
||||
expect(described_class.of(name)).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
specify '#fff is a valid color' do
|
||||
expect(described_class.new('#fff')).to be_valid
|
||||
end
|
||||
|
||||
specify '#ffffff is a valid color' do
|
||||
expect(described_class.new('#ffffff')).to be_valid
|
||||
end
|
||||
|
||||
specify '#ABCDEF is a valid color' do
|
||||
expect(described_class.new('#ABCDEF')).to be_valid
|
||||
end
|
||||
|
||||
specify '#123456 is a valid color' do
|
||||
expect(described_class.new('#123456')).to be_valid
|
||||
end
|
||||
|
||||
specify '#1234567 is not a valid color' do
|
||||
expect(described_class.new('#1234567')).not_to be_valid
|
||||
end
|
||||
|
||||
specify 'fff is not a valid color' do
|
||||
expect(described_class.new('fff')).not_to be_valid
|
||||
end
|
||||
|
||||
specify '#deadbeaf is not a valid color' do
|
||||
expect(described_class.new('#deadbeaf')).not_to be_valid
|
||||
end
|
||||
|
||||
specify '#a1b2c3 is a valid color' do
|
||||
expect(described_class.new('#a1b2c3')).to be_valid
|
||||
end
|
||||
|
||||
specify 'nil is not a valid color' do
|
||||
expect(described_class.new(nil)).not_to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
describe '#light?' do
|
||||
specify '#fff is light' do
|
||||
expect(described_class.new('#fff')).to be_light
|
||||
end
|
||||
|
||||
specify '#a7a7a7 is light' do
|
||||
expect(described_class.new('#a7a7a7')).to be_light
|
||||
end
|
||||
|
||||
specify '#a6a7a7 is dark' do
|
||||
expect(described_class.new('#a6a7a7')).not_to be_light
|
||||
end
|
||||
|
||||
specify '#000 is dark' do
|
||||
expect(described_class.new('#000')).not_to be_light
|
||||
end
|
||||
|
||||
specify 'invalid colors are not light' do
|
||||
expect(described_class.new('not-a-color')).not_to be_light
|
||||
end
|
||||
end
|
||||
|
||||
describe '#contrast' do
|
||||
context 'with light colors' do
|
||||
it 'is dark' do
|
||||
%w[#fff #fefefe #a7a7a7].each do |hex|
|
||||
expect(described_class.new(hex)).to have_attributes(
|
||||
contrast: described_class::Constants::DARK,
|
||||
luminosity: :light
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with dark colors' do
|
||||
it 'is light' do
|
||||
%w[#000 #a6a7a7].each do |hex|
|
||||
expect(described_class.new(hex)).to have_attributes(
|
||||
contrast: described_class::Constants::LIGHT,
|
||||
luminosity: :dark
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'as_json' do
|
||||
it 'serializes correctly' do
|
||||
expect(described_class.new('#f0f1f2').as_json).to eq('#f0f1f2')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ::Gitlab::Database::Type::Color do
|
||||
subject(:type) { described_class.new }
|
||||
|
||||
let(:color) { ::Gitlab::Color.of('red') }
|
||||
|
||||
it 'serializes by calling #to_s' do
|
||||
expect(type.serialize(color)).to eq(color.to_s)
|
||||
end
|
||||
|
||||
it 'serializes nil to nil' do
|
||||
expect(type.serialize(nil)).to be_nil
|
||||
end
|
||||
|
||||
it 'casts by calling Color::new' do
|
||||
expect(type.cast('#fff')).to eq(::Gitlab::Color.new('#fff'))
|
||||
end
|
||||
|
||||
it 'accepts colors as arguments to cast' do
|
||||
expect(type.cast(color)).to eq(color)
|
||||
end
|
||||
|
||||
it 'allows nil database values' do
|
||||
expect(type.cast(nil)).to be_nil
|
||||
end
|
||||
|
||||
it 'tells us what is serializable' do
|
||||
[nil, 'foo', color].each do |value|
|
||||
expect(type.serializable?(value)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
it 'tells us what is not serializable' do
|
||||
[0, 3.2, true, Time.current, { some: 'hash' }].each do |value|
|
||||
expect(type.serializable?(value)).to be false
|
||||
end
|
||||
end
|
||||
end
|
|
@ -7,7 +7,7 @@ RSpec.describe Gitlab::Utils do
|
|||
|
||||
delegate :to_boolean, :boolean_to_yes_no, :slugify, :random_string, :which,
|
||||
:ensure_array_from_string, :to_exclusive_sentence, :bytes_to_megabytes,
|
||||
:append_path, :check_path_traversal!, :allowlisted?, :check_allowed_absolute_path!, :decode_path, :ms_to_round_sec, to: :described_class
|
||||
:append_path, :check_path_traversal!, :allowlisted?, :check_allowed_absolute_path!, :decode_path, :ms_to_round_sec, :check_allowed_absolute_path_and_path_traversal!, to: :described_class
|
||||
|
||||
describe '.check_path_traversal!' do
|
||||
it 'detects path traversal in string without any separators' do
|
||||
|
@ -58,6 +58,65 @@ RSpec.describe Gitlab::Utils do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.check_allowed_absolute_path_and_path_traversal!' do
|
||||
let(:allowed_paths) { %w[/home/foo ./foo .test/foo ..test/foo dir/..foo.rb dir/.foo.rb] }
|
||||
|
||||
it 'detects path traversal in string without any separators' do
|
||||
expect { check_allowed_absolute_path_and_path_traversal!('.', allowed_paths) }.to raise_error(/Invalid path/)
|
||||
expect { check_allowed_absolute_path_and_path_traversal!('..', allowed_paths) }.to raise_error(/Invalid path/)
|
||||
end
|
||||
|
||||
it 'detects path traversal at the start of the string' do
|
||||
expect { check_allowed_absolute_path_and_path_traversal!('../foo', allowed_paths) }.to raise_error(/Invalid path/)
|
||||
expect { check_allowed_absolute_path_and_path_traversal!('..\\foo', allowed_paths) }.to raise_error(/Invalid path/)
|
||||
end
|
||||
|
||||
it 'detects path traversal at the start of the string, even to just the subdirectory' do
|
||||
expect { check_allowed_absolute_path_and_path_traversal!('../', allowed_paths) }.to raise_error(/Invalid path/)
|
||||
expect { check_allowed_absolute_path_and_path_traversal!('..\\', allowed_paths) }.to raise_error(/Invalid path/)
|
||||
expect { check_allowed_absolute_path_and_path_traversal!('/../', allowed_paths) }.to raise_error(/Invalid path/)
|
||||
expect { check_allowed_absolute_path_and_path_traversal!('\\..\\', allowed_paths) }.to raise_error(/Invalid path/)
|
||||
end
|
||||
|
||||
it 'detects path traversal in the middle of the string' do
|
||||
expect { check_allowed_absolute_path_and_path_traversal!('foo/../../bar', allowed_paths) }.to raise_error(/Invalid path/)
|
||||
expect { check_allowed_absolute_path_and_path_traversal!('foo\\..\\..\\bar', allowed_paths) }.to raise_error(/Invalid path/)
|
||||
expect { check_allowed_absolute_path_and_path_traversal!('foo/..\\bar', allowed_paths) }.to raise_error(/Invalid path/)
|
||||
expect { check_allowed_absolute_path_and_path_traversal!('foo\\../bar', allowed_paths) }.to raise_error(/Invalid path/)
|
||||
expect { check_allowed_absolute_path_and_path_traversal!('foo/..\\..\\..\\..\\../bar', allowed_paths) }.to raise_error(/Invalid path/)
|
||||
end
|
||||
|
||||
it 'detects path traversal at the end of the string when slash-terminates' do
|
||||
expect { check_allowed_absolute_path_and_path_traversal!('foo/../', allowed_paths) }.to raise_error(/Invalid path/)
|
||||
expect { check_allowed_absolute_path_and_path_traversal!('foo\\..\\', allowed_paths) }.to raise_error(/Invalid path/)
|
||||
end
|
||||
|
||||
it 'detects path traversal at the end of the string' do
|
||||
expect { check_allowed_absolute_path_and_path_traversal!('foo/..', allowed_paths) }.to raise_error(/Invalid path/)
|
||||
expect { check_allowed_absolute_path_and_path_traversal!('foo\\..', allowed_paths) }.to raise_error(/Invalid path/)
|
||||
end
|
||||
|
||||
it 'does not return errors for a safe string' do
|
||||
expect(check_allowed_absolute_path_and_path_traversal!('./foo', allowed_paths)).to be_nil
|
||||
expect(check_allowed_absolute_path_and_path_traversal!('.test/foo', allowed_paths)).to be_nil
|
||||
expect(check_allowed_absolute_path_and_path_traversal!('..test/foo', allowed_paths)).to be_nil
|
||||
expect(check_allowed_absolute_path_and_path_traversal!('dir/..foo.rb', allowed_paths)).to be_nil
|
||||
expect(check_allowed_absolute_path_and_path_traversal!('dir/.foo.rb', allowed_paths)).to be_nil
|
||||
end
|
||||
|
||||
it 'raises error for a non-string' do
|
||||
expect {check_allowed_absolute_path_and_path_traversal!(nil, allowed_paths)}.to raise_error(StandardError)
|
||||
end
|
||||
|
||||
it 'raises an exception if an absolute path is not allowed' do
|
||||
expect { check_allowed_absolute_path!('/etc/passwd', allowed_paths) }.to raise_error(StandardError)
|
||||
end
|
||||
|
||||
it 'does nothing for an allowed absolute path' do
|
||||
expect(check_allowed_absolute_path!('/home/foo', allowed_paths)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '.allowlisted?' do
|
||||
let(:allowed_paths) { ['/home/foo', '/foo/bar', '/etc/passwd']}
|
||||
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
require_migration!
|
||||
|
||||
RSpec.describe EncryptIntegrationProperties, :migration, schema: 20220204193000 do
|
||||
subject(:migration) { described_class.new }
|
||||
|
||||
let(:integrations) { table(:integrations) }
|
||||
|
||||
before do
|
||||
stub_const("#{described_class.name}::BATCH_SIZE", 2)
|
||||
end
|
||||
|
||||
it 'correctly schedules background migrations', :aggregate_failures do
|
||||
# update required
|
||||
record1 = integrations.create!(properties: some_props)
|
||||
record2 = integrations.create!(properties: some_props)
|
||||
record3 = integrations.create!(properties: some_props)
|
||||
record4 = integrations.create!(properties: nil)
|
||||
record5 = integrations.create!(properties: nil)
|
||||
|
||||
Sidekiq::Testing.fake! do
|
||||
freeze_time do
|
||||
migrate!
|
||||
|
||||
expect(described_class::MIGRATION).to be_scheduled_migration(record1.id, record2.id)
|
||||
expect(described_class::MIGRATION).to be_scheduled_migration(record3.id, record4.id)
|
||||
expect(described_class::MIGRATION).to be_scheduled_migration(record5.id, record5.id)
|
||||
|
||||
expect(BackgroundMigrationWorker.jobs.size).to eq(3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def some_props
|
||||
{ iid: generate(:iid), url: generate(:url), username: generate(:username) }.to_json
|
||||
end
|
||||
end
|
|
@ -843,4 +843,82 @@ RSpec.describe Integration do
|
|||
expect(subject.password_fields).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'encrypted_properties' do
|
||||
let(:properties) { { foo: 1, bar: true } }
|
||||
let(:db_props) { properties.stringify_keys }
|
||||
let(:record) { create(:integration, :instance, properties: properties) }
|
||||
|
||||
it 'contains the same data as properties' do
|
||||
expect(record).to have_attributes(
|
||||
properties: db_props,
|
||||
encrypted_properties_tmp: db_props
|
||||
)
|
||||
end
|
||||
|
||||
it 'is persisted' do
|
||||
encrypted_properties = described_class.id_in(record.id)
|
||||
|
||||
expect(encrypted_properties).to contain_exactly have_attributes(encrypted_properties_tmp: db_props)
|
||||
end
|
||||
|
||||
it 'is updated when using prop_accessors' do
|
||||
some_integration = Class.new(described_class) do
|
||||
prop_accessor :foo
|
||||
end
|
||||
|
||||
record = some_integration.new
|
||||
|
||||
record.foo = 'the foo'
|
||||
|
||||
expect(record.encrypted_properties_tmp).to eq({ 'foo' => 'the foo' })
|
||||
end
|
||||
|
||||
it 'saves correctly using insert_all' do
|
||||
hash = record.to_integration_hash
|
||||
hash[:project_id] = project
|
||||
|
||||
expect do
|
||||
described_class.insert_all([hash])
|
||||
end.to change(described_class, :count).by(1)
|
||||
|
||||
expect(described_class.last).to have_attributes(encrypted_properties_tmp: db_props)
|
||||
end
|
||||
|
||||
it 'is part of the to_integration_hash' do
|
||||
hash = record.to_integration_hash
|
||||
|
||||
expect(hash).to include('encrypted_properties' => be_present, 'encrypted_properties_iv' => be_present)
|
||||
expect(hash['encrypted_properties']).not_to eq(record.encrypted_properties)
|
||||
expect(hash['encrypted_properties_iv']).not_to eq(record.encrypted_properties_iv)
|
||||
|
||||
decrypted = described_class.decrypt(:encrypted_properties_tmp,
|
||||
hash['encrypted_properties'],
|
||||
{ iv: hash['encrypted_properties_iv'] })
|
||||
|
||||
expect(decrypted).to eq db_props
|
||||
end
|
||||
|
||||
context 'when the properties are empty' do
|
||||
let(:properties) { {} }
|
||||
|
||||
it 'is part of the to_integration_hash' do
|
||||
hash = record.to_integration_hash
|
||||
|
||||
expect(hash).to include('encrypted_properties' => be_nil, 'encrypted_properties_iv' => be_nil)
|
||||
end
|
||||
|
||||
it 'saves correctly using insert_all' do
|
||||
hash = record.to_integration_hash
|
||||
hash[:project_id] = project
|
||||
|
||||
expect do
|
||||
described_class.insert_all([hash])
|
||||
end.to change(described_class, :count).by(1)
|
||||
|
||||
expect(described_class.last).not_to eq record
|
||||
expect(described_class.last).to have_attributes(encrypted_properties_tmp: db_props)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -67,24 +67,21 @@ RSpec.describe Label do
|
|||
label = described_class.new(color: ' #abcdef ')
|
||||
label.valid?
|
||||
|
||||
expect(label.color).to eq('#abcdef')
|
||||
expect(label.color).to be_color('#abcdef')
|
||||
end
|
||||
|
||||
it 'uses default color if color is missing' do
|
||||
label = described_class.new(color: nil)
|
||||
|
||||
expect(label.color).to be(Label::DEFAULT_COLOR)
|
||||
expect(label.color).to be_color(Label::DEFAULT_COLOR)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#text_color' do
|
||||
it 'uses default color if color is missing' do
|
||||
expect(LabelsHelper).to receive(:text_color_for_bg).with(Label::DEFAULT_COLOR)
|
||||
.and_return(spy)
|
||||
|
||||
label = described_class.new(color: nil)
|
||||
|
||||
label.text_color
|
||||
expect(label.text_color).to eq(Label::DEFAULT_COLOR.contrast)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -140,7 +140,7 @@ RSpec.describe API::GroupLabels do
|
|||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response['name']).to eq(group_label1.name)
|
||||
expect(json_response['color']).to eq(group_label1.color)
|
||||
expect(json_response['color']).to be_color(group_label1.color)
|
||||
expect(json_response['description']).to eq(group_label1.description)
|
||||
end
|
||||
end
|
||||
|
@ -156,7 +156,7 @@ RSpec.describe API::GroupLabels do
|
|||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
expect(json_response['name']).to eq(valid_new_label_title)
|
||||
expect(json_response['color']).to eq('#FFAABB')
|
||||
expect(json_response['color']).to be_color('#FFAABB')
|
||||
expect(json_response['description']).to eq('test')
|
||||
end
|
||||
|
||||
|
@ -169,7 +169,7 @@ RSpec.describe API::GroupLabels do
|
|||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
expect(json_response['name']).to eq(valid_new_label_title)
|
||||
expect(json_response['color']).to eq('#FFAABB')
|
||||
expect(json_response['color']).to be_color('#FFAABB')
|
||||
expect(json_response['description']).to be_nil
|
||||
end
|
||||
|
||||
|
@ -276,7 +276,7 @@ RSpec.describe API::GroupLabels do
|
|||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response['name']).to eq(valid_new_label_title)
|
||||
expect(json_response['color']).to eq('#FFFFFF')
|
||||
expect(json_response['color']).to be_color('#FFFFFF')
|
||||
expect(json_response['description']).to eq('test')
|
||||
end
|
||||
|
||||
|
@ -332,7 +332,7 @@ RSpec.describe API::GroupLabels do
|
|||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response['name']).to eq(valid_new_label_title)
|
||||
expect(json_response['color']).to eq('#FFFFFF')
|
||||
expect(json_response['color']).to be_color('#FFFFFF')
|
||||
expect(json_response['description']).to eq('test')
|
||||
end
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ RSpec.describe API::Labels do
|
|||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response['name']).to eq(valid_label_title_2)
|
||||
expect(json_response['color']).to eq(label1.color)
|
||||
expect(json_response['color']).to be_color(label1.color)
|
||||
end
|
||||
|
||||
it "returns 200 if colors is changed (#{route_type} route)" do
|
||||
|
@ -42,7 +42,7 @@ RSpec.describe API::Labels do
|
|||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response['name']).to eq(label1.name)
|
||||
expect(json_response['color']).to eq('#FFFFFF')
|
||||
expect(json_response['color']).to be_color('#FFFFFF')
|
||||
end
|
||||
|
||||
it "returns 200 if a priority is added (#{route_type} route)" do
|
||||
|
@ -86,7 +86,7 @@ RSpec.describe API::Labels do
|
|||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response['name']).to eq(valid_label_title_2)
|
||||
expect(json_response['color']).to eq('#FFFFFF')
|
||||
expect(json_response['color']).to be_color('#FFFFFF')
|
||||
expect(json_response['description']).to eq('test')
|
||||
end
|
||||
|
||||
|
@ -266,8 +266,8 @@ RSpec.describe API::Labels do
|
|||
'open_merge_requests_count' => 0,
|
||||
'name' => group_label.name,
|
||||
'description' => nil,
|
||||
'color' => a_string_matching(/^#\h{6}$/),
|
||||
'text_color' => a_string_matching(/^#\h{6}$/),
|
||||
'color' => a_valid_color,
|
||||
'text_color' => a_valid_color,
|
||||
'priority' => nil,
|
||||
'subscribed' => false,
|
||||
'is_project_label' => false)
|
||||
|
@ -277,8 +277,8 @@ RSpec.describe API::Labels do
|
|||
'open_merge_requests_count' => 1,
|
||||
'name' => priority_label.name,
|
||||
'description' => nil,
|
||||
'color' => a_string_matching(/^#\h{6}$/),
|
||||
'text_color' => a_string_matching(/^#\h{6}$/),
|
||||
'color' => a_valid_color,
|
||||
'text_color' => a_valid_color,
|
||||
'priority' => 3,
|
||||
'subscribed' => false,
|
||||
'is_project_label' => true)
|
||||
|
@ -336,7 +336,7 @@ RSpec.describe API::Labels do
|
|||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
expect(json_response['name']).to eq(valid_label_title_2)
|
||||
expect(json_response['color']).to eq('#FFAABB')
|
||||
expect(json_response['color']).to be_color('#FFAABB')
|
||||
expect(json_response['description']).to eq('test')
|
||||
expect(json_response['priority']).to eq(2)
|
||||
end
|
||||
|
@ -350,7 +350,7 @@ RSpec.describe API::Labels do
|
|||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
expect(json_response['name']).to eq(valid_label_title_2)
|
||||
expect(json_response['color']).to eq('#FFAABB')
|
||||
expect(json_response['color']).to be_color('#FFAABB')
|
||||
expect(json_response['description']).to be_nil
|
||||
expect(json_response['priority']).to be_nil
|
||||
end
|
||||
|
@ -365,7 +365,7 @@ RSpec.describe API::Labels do
|
|||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
expect(json_response['name']).to eq(valid_label_title_2)
|
||||
expect(json_response['color']).to eq('#FFAABB')
|
||||
expect(json_response['color']).to be_color('#FFAABB')
|
||||
expect(json_response['description']).to be_nil
|
||||
expect(json_response['priority']).to eq(3)
|
||||
end
|
||||
|
@ -552,7 +552,7 @@ RSpec.describe API::Labels do
|
|||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response['name']).to eq(label1.name)
|
||||
expect(json_response['color']).to eq(label1.color)
|
||||
expect(json_response['color']).to be_color(label1.color.to_s)
|
||||
end
|
||||
|
||||
context 'if group label already exists' do
|
||||
|
|
|
@ -40,7 +40,7 @@ RSpec.describe LabelSerializer do
|
|||
expect(subject.keys).to eq([:id, :title, :color, :project_id, :text_color])
|
||||
expect(subject[:id]).to eq(resource.id)
|
||||
expect(subject[:title]).to eq(resource.title)
|
||||
expect(subject[:color]).to eq(resource.color)
|
||||
expect(subject[:color]).to be_color(resource.color)
|
||||
expect(subject[:text_color]).to eq(resource.text_color)
|
||||
expect(subject[:project_id]).to eq(resource.project_id)
|
||||
end
|
||||
|
|
|
@ -13,14 +13,23 @@ RSpec.describe BulkCreateIntegrationService do
|
|||
let_it_be(:excluded_project) { create(:project, group: excluded_group) }
|
||||
|
||||
let(:instance_integration) { create(:jira_integration, :instance) }
|
||||
let(:excluded_attributes) { %w[id project_id group_id inherit_from_id instance template created_at updated_at] }
|
||||
let(:excluded_attributes) do
|
||||
%w[
|
||||
id project_id group_id inherit_from_id instance template
|
||||
created_at updated_at
|
||||
encrypted_properties encrypted_properties_iv
|
||||
]
|
||||
end
|
||||
|
||||
shared_examples 'creates integration from batch ids' do
|
||||
def attributes(record)
|
||||
record.reload.attributes.except(*excluded_attributes)
|
||||
end
|
||||
|
||||
it 'updates the inherited integrations' do
|
||||
described_class.new(integration, batch, association).execute
|
||||
|
||||
expect(created_integration.attributes.except(*excluded_attributes))
|
||||
.to eq(integration.reload.attributes.except(*excluded_attributes))
|
||||
expect(attributes(created_integration)).to eq attributes(integration)
|
||||
end
|
||||
|
||||
context 'integration with data fields' do
|
||||
|
@ -29,8 +38,8 @@ RSpec.describe BulkCreateIntegrationService do
|
|||
it 'updates the data fields from inherited integrations' do
|
||||
described_class.new(integration, batch, association).execute
|
||||
|
||||
expect(created_integration.reload.data_fields.attributes.except(*excluded_attributes))
|
||||
.to eq(integration.reload.data_fields.attributes.except(*excluded_attributes))
|
||||
expect(attributes(created_integration.data_fields))
|
||||
.to eq attributes(integration.data_fields)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,7 +14,7 @@ RSpec.describe Labels::CreateService do
|
|||
let(:unknown_color) { 'unknown' }
|
||||
let(:no_color) { '' }
|
||||
|
||||
let(:expected_saved_color) { hex_color }
|
||||
let(:expected_saved_color) { ::Gitlab::Color.of(hex_color) }
|
||||
|
||||
context 'in a project' do
|
||||
context 'with color in hex-code' do
|
||||
|
@ -47,7 +47,6 @@ RSpec.describe Labels::CreateService do
|
|||
context 'with color surrounded by spaces' do
|
||||
it 'creates a label' do
|
||||
label = described_class.new(params_with(spaced_color)).execute(project: project)
|
||||
|
||||
expect(label).to be_persisted
|
||||
expect(label.color).to eq expected_saved_color
|
||||
end
|
||||
|
|
|
@ -202,7 +202,7 @@ RSpec.describe Labels::PromoteService do
|
|||
|
||||
expect(new_label.title).to eq(promoted_label_name)
|
||||
expect(new_label.description).to eq(promoted_description)
|
||||
expect(new_label.color).to eq(promoted_color)
|
||||
expect(new_label.color).to be_color(promoted_color)
|
||||
end
|
||||
|
||||
it_behaves_like 'promoting a project label to a group label'
|
||||
|
|
|
@ -13,7 +13,7 @@ RSpec.describe Labels::UpdateService do
|
|||
let(:unknown_color) { 'unknown' }
|
||||
let(:no_color) { '' }
|
||||
|
||||
let(:expected_saved_color) { hex_color }
|
||||
let(:expected_saved_color) { ::Gitlab::Color.of(hex_color) }
|
||||
|
||||
before do
|
||||
@label = Labels::CreateService.new(title: 'Initial', color: '#000000').execute(project: project)
|
||||
|
|
|
@ -23,11 +23,11 @@ RSpec.describe Projects::CreateService, '#execute' do
|
|||
end
|
||||
|
||||
it 'creates labels on project creation' do
|
||||
created_label = project.labels.last
|
||||
|
||||
expect(created_label.type).to eq('ProjectLabel')
|
||||
expect(created_label.project_id).to eq(project.id)
|
||||
expect(created_label.title).to eq('bug')
|
||||
expect(project.labels).to include have_attributes(
|
||||
type: eq('ProjectLabel'),
|
||||
project_id: eq(project.id),
|
||||
title: eq('bug')
|
||||
)
|
||||
end
|
||||
|
||||
context 'using gitlab project import' do
|
||||
|
|
|
@ -153,7 +153,18 @@ RSpec.describe Security::MergeReportsService, '#execute' do
|
|||
report_2.add_error('zoo', 'baz')
|
||||
end
|
||||
|
||||
it { is_expected.to eq([{ type: 'foo', message: 'bar' }, { type: 'zoo', message: 'baz' }]) }
|
||||
it { is_expected.to match_array([{ type: 'foo', message: 'bar' }, { type: 'zoo', message: 'baz' }]) }
|
||||
end
|
||||
|
||||
describe 'warnings on target report' do
|
||||
subject { merged_report.warnings }
|
||||
|
||||
before do
|
||||
report_1.add_warning('foo', 'bar')
|
||||
report_2.add_warning('zoo', 'baz')
|
||||
end
|
||||
|
||||
it { is_expected.to match_array([{ type: 'foo', message: 'bar' }, { type: 'zoo', message: 'baz' }]) }
|
||||
end
|
||||
|
||||
it 'copies scanners into target report and eliminates duplicates' do
|
||||
|
|
|
@ -13,6 +13,8 @@ module MigrationsHelpers
|
|||
def self.name
|
||||
table_name.singularize.camelcase
|
||||
end
|
||||
|
||||
yield self if block_given?
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Assert that this value is a valid color equal to the argument
|
||||
#
|
||||
# ```
|
||||
# expect(value).to be_color('#fff')
|
||||
# ```
|
||||
RSpec::Matchers.define :be_color do |expected|
|
||||
match do |actual|
|
||||
next false unless actual.present?
|
||||
|
||||
if expected
|
||||
::Gitlab::Color.of(actual) == ::Gitlab::Color.of(expected)
|
||||
else
|
||||
::Gitlab::Color.of(actual).valid?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec::Matchers.alias_matcher :a_valid_color, :be_color
|
|
@ -244,6 +244,16 @@ RSpec.shared_examples 'namespace traversal scopes' do
|
|||
|
||||
it { is_expected.to contain_exactly(group_2, nested_group_2, deep_nested_group_2) }
|
||||
end
|
||||
|
||||
context 'with nested query groups' do
|
||||
let!(:nested_group_1b) { create(:group, parent: group_1) }
|
||||
let!(:deep_nested_group_1b) { create(:group, parent: nested_group_1b) }
|
||||
let(:group1_hierarchy) { [group_1, nested_group_1, deep_nested_group_1, nested_group_1b, deep_nested_group_1b] }
|
||||
|
||||
subject { described_class.where(id: [group_1, nested_group_1]).self_and_descendants }
|
||||
|
||||
it { is_expected.to match_array group1_hierarchy }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.self_and_descendants' do
|
||||
|
|
|
@ -70,7 +70,7 @@ RSpec.shared_examples 'incident management label service' do
|
|||
expect(execute).to be_success
|
||||
expect(execute.payload).to eq(label: label)
|
||||
expect(label.title).to eq(title)
|
||||
expect(label.color).to eq(color)
|
||||
expect(label.color).to be_color(color)
|
||||
expect(label.description).to eq(description)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,7 +23,12 @@ RSpec.describe ColorValidator do
|
|||
'#ffff' | false
|
||||
'#000111222' | false
|
||||
'invalid' | false
|
||||
'red' | false
|
||||
'000' | false
|
||||
nil | true # use presence to validate non-nil
|
||||
'' | false
|
||||
Time.current | false
|
||||
::Gitlab::Color.of(:red) | true
|
||||
end
|
||||
|
||||
with_them do
|
||||
|
@ -41,4 +46,22 @@ RSpec.describe ColorValidator do
|
|||
Timeout.timeout(5.seconds) { subject.valid? }
|
||||
end.not_to raise_error
|
||||
end
|
||||
|
||||
context 'when color must be present' do
|
||||
subject do
|
||||
Class.new do
|
||||
include ActiveModel::Model
|
||||
include ActiveModel::Validations
|
||||
attr_accessor :color
|
||||
|
||||
validates :color, color: true, presence: true
|
||||
end.new
|
||||
end
|
||||
|
||||
it 'rejects nil' do
|
||||
subject.color = nil
|
||||
|
||||
expect(subject).not_to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue