Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-03-03 00:20:18 +00:00
parent bd091da6d5
commit 475d5a7a17
65 changed files with 1441 additions and 646 deletions

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
9d98618a1e9fd0474c45ac54420fc64a1d90ad77f36be594337e5b117fccdadb

View File

@ -0,0 +1 @@
1593e935601ae1f2ab788109687bb40bad026f3f425339a39c8d13d3e4c7e306

View File

@ -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))
);

View File

@ -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. |

View File

@ -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)

View File

@ -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

View File

@ -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.

View File

@ -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:

View File

@ -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

View File

@ -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)],

View File

@ -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

View File

@ -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?

View File

@ -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

222
lib/gitlab/color.rb Normal file
View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -277,7 +277,7 @@ RSpec.describe Banzai::Filter::References::LabelReferenceFilter do
end
context 'References with html entities' do
let!(:label) { create(:label, name: '&lt;html&gt;', project: project) }
let!(:label) { create(:label, title: '&lt;html&gt;', project: project) }
it 'links to a valid reference' do
doc = reference_filter('See ~"&lt;html&gt;"')

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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? }

View File

@ -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

View File

@ -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

View File

@ -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']}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -13,6 +13,8 @@ module MigrationsHelpers
def self.name
table_name.singularize.camelcase
end
yield self if block_given?
end
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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