diff --git a/app/assets/javascripts/blob/components/blob_header_filepath.vue b/app/assets/javascripts/blob/components/blob_header_filepath.vue new file mode 100644 index 00000000000..6c6a22e2b36 --- /dev/null +++ b/app/assets/javascripts/blob/components/blob_header_filepath.vue @@ -0,0 +1,47 @@ + + diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js index 789b3131d11..b468254b0cf 100644 --- a/app/assets/javascripts/monitoring/constants.js +++ b/app/assets/javascripts/monitoring/constants.js @@ -110,8 +110,8 @@ export const timeRanges = [ duration: { seconds: 60 * 60 * 24 * 7 * 1 }, }, { - label: __('2 weeks'), - duration: { seconds: 60 * 60 * 24 * 7 * 2 }, + label: __('1 month'), + duration: { seconds: 60 * 60 * 24 * 30 }, }, ]; diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss index fbe241df32f..dbcb5086d70 100644 --- a/app/assets/stylesheets/framework/snippets.scss +++ b/app/assets/stylesheets/framework/snippets.scss @@ -32,10 +32,6 @@ .snippet-file-content { border-radius: 3px; - - .file-title-flex-parent .btn-clipboard { - line-height: 28px; - } } .snippet-header { diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index be0311f584f..781b6c09458 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -321,6 +321,16 @@ } } +.gpg-popover-certificate-details { + ul { + padding-left: $gl-padding; + } + + li.unstyled { + list-style-type: none; + } +} + .gpg-popover-status { display: flex; align-items: center; diff --git a/app/models/commit.rb b/app/models/commit.rb index f2a6a8b6cbb..46222bbc4cd 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -25,7 +25,7 @@ class Commit attr_accessor :redacted_description_html attr_accessor :redacted_title_html attr_accessor :redacted_full_title_html - attr_reader :gpg_commit, :container + attr_reader :container delegate :repository, to: :container delegate :project, to: :repository, allow_nil: true @@ -123,7 +123,6 @@ class Commit @raw = raw_commit @container = container - @gpg_commit = Gitlab::Gpg::Commit.new(self) if container end delegate \ @@ -320,13 +319,34 @@ class Commit ) end - def signature - return @signature if defined?(@signature) - - @signature = gpg_commit.signature + def has_signature? + signature_type && signature_type != :NONE end - delegate :has_signature?, to: :gpg_commit + def raw_signature_type + strong_memoize(:raw_signature_type) do + next unless @raw.instance_of?(Gitlab::Git::Commit) + + @raw.raw_commit.signature_type if defined? @raw.raw_commit.signature_type + end + end + + def signature_type + @signature_type ||= raw_signature_type || :NONE + end + + def signature + strong_memoize(:signature) do + case signature_type + when :PGP + Gitlab::Gpg::Commit.new(self).signature + when :X509 + Gitlab::X509::Commit.new(self).signature + else + nil + end + end + end def revert_branch_name "revert-#{short_id}" diff --git a/app/models/concerns/x509_serial_number_attribute.rb b/app/models/concerns/x509_serial_number_attribute.rb new file mode 100644 index 00000000000..d2a5c736604 --- /dev/null +++ b/app/models/concerns/x509_serial_number_attribute.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module X509SerialNumberAttribute + extend ActiveSupport::Concern + + class_methods do + def x509_serial_number_attribute(name) + return if ENV['STATIC_VERIFICATION'] + + validate_binary_column_exists!(name) unless Rails.env.production? + + attribute(name, Gitlab::Database::X509SerialNumberAttribute.new) + end + + # This only gets executed in non-production environments as an additional check to ensure + # the column is the correct type. In production it should behave like any other attribute. + # See https://gitlab.com/gitlab-org/gitlab/merge_requests/5502 for more discussion + def validate_binary_column_exists!(name) + return unless database_exists? + + unless table_exists? + warn "WARNING: x509_serial_number_attribute #{name.inspect} is invalid since the table doesn't exist - you may need to run database migrations" + return + end + + column = columns.find { |c| c.name == name.to_s } + + unless column + warn "WARNING: x509_serial_number_attribute #{name.inspect} is invalid since the column doesn't exist - you may need to run database migrations" + return + end + + unless column.type == :binary + raise ArgumentError.new("x509_serial_number_attribute #{name.inspect} is invalid since the column type is not :binary") + end + rescue => error + Gitlab::AppLogger.error "X509SerialNumberAttribute initialization: #{error.message}" + raise + end + + def database_exists? + Gitlab::Database.exists? + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index aa7e825d516..11bfa485ae9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -190,6 +190,12 @@ class User < ApplicationRecord validate :owns_commit_email, if: :commit_email_changed? validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id } + validates :theme_id, allow_nil: true, inclusion: { in: Gitlab::Themes.valid_ids, + message: _("%{placeholder} is not a valid theme") % { placeholder: '%{value}' } } + + validates :color_scheme_id, allow_nil: true, inclusion: { in: Gitlab::ColorSchemes.valid_ids, + message: _("%{placeholder} is not a valid color scheme") % { placeholder: '%{value}' } } + before_validation :sanitize_attrs before_validation :set_notification_email, if: :new_record? before_validation :set_public_email, if: :public_email_changed? diff --git a/app/models/x509_certificate.rb b/app/models/x509_certificate.rb new file mode 100644 index 00000000000..43927e65db1 --- /dev/null +++ b/app/models/x509_certificate.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class X509Certificate < ApplicationRecord + include X509SerialNumberAttribute + + x509_serial_number_attribute :serial_number + + enum certificate_status: { + good: 0, + revoked: 1 + } + + belongs_to :x509_issuer, class_name: 'X509Issuer', foreign_key: 'x509_issuer_id', optional: false + + has_many :x509_commit_signatures, inverse_of: 'x509_certificate' + + # rfc 5280 - 4.2.1.2 Subject Key Identifier + validates :subject_key_identifier, presence: true, format: { with: /\A(\h{2}:){19}\h{2}\z/ } + # rfc 5280 - 4.1.2.6 Subject + validates :subject, presence: true + # rfc 5280 - 4.1.2.6 Subject (subjectAltName contains the email address) + validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP } + # rfc 5280 - 4.1.2.2 Serial number + validates :serial_number, presence: true, numericality: { only_integer: true } + + validates :x509_issuer_id, presence: true + + def self.safe_create!(attributes) + create_with(attributes) + .safe_find_or_create_by!(subject_key_identifier: attributes[:subject_key_identifier]) + end +end diff --git a/app/models/x509_commit_signature.rb b/app/models/x509_commit_signature.rb new file mode 100644 index 00000000000..ed7c638cecc --- /dev/null +++ b/app/models/x509_commit_signature.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +class X509CommitSignature < ApplicationRecord + include ShaAttribute + + sha_attribute :commit_sha + + enum verification_status: { + unverified: 0, + verified: 1 + } + + belongs_to :project, class_name: 'Project', foreign_key: 'project_id', optional: false + belongs_to :x509_certificate, class_name: 'X509Certificate', foreign_key: 'x509_certificate_id', optional: false + + validates :commit_sha, presence: true + validates :project_id, presence: true + validates :x509_certificate_id, presence: true + + scope :by_commit_sha, ->(shas) { where(commit_sha: shas) } + + def self.safe_create!(attributes) + create_with(attributes) + .safe_find_or_create_by!(commit_sha: attributes[:commit_sha]) + end + + # Find commits that are lacking a signature in the database at present + def self.unsigned_commit_shas(commit_shas) + return [] if commit_shas.empty? + + signed = by_commit_sha(commit_shas).pluck(:commit_sha) + commit_shas - signed + end + + def commit + project.commit(commit_sha) + end + + def x509_commit + return unless commit + + Gitlab::X509::Commit.new(commit) + end +end diff --git a/app/models/x509_issuer.rb b/app/models/x509_issuer.rb new file mode 100644 index 00000000000..514b38808ef --- /dev/null +++ b/app/models/x509_issuer.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class X509Issuer < ApplicationRecord + has_many :x509_certificates, inverse_of: 'x509_issuer' + + # rfc 5280 - 4.2.1.1 Authority Key Identifier + validates :subject_key_identifier, presence: true, format: { with: /\A(\h{2}:){19}\h{2}\z/ } + # rfc 5280 - 4.1.2.4 Issuer + validates :subject, presence: true + # rfc 5280 - 4.2.1.14 CRL Distribution Points + # cRLDistributionPoints extension using URI:http + validates :crl_url, presence: true, public_url: true + + def self.safe_create!(attributes) + create_with(attributes) + .safe_find_or_create_by!(subject_key_identifier: attributes[:subject_key_identifier]) + end +end diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb index 69f1f9eb31f..e1cc1f8c834 100644 --- a/app/services/git/branch_hooks_service.rb +++ b/app/services/git/branch_hooks_service.rb @@ -6,7 +6,7 @@ module Git execute_branch_hooks super.tap do - enqueue_update_gpg_signatures + enqueue_update_signatures end end @@ -103,14 +103,22 @@ module Git end end - def enqueue_update_gpg_signatures - unsigned = GpgSignature.unsigned_commit_shas(limited_commits.map(&:sha)) + def unsigned_x509_shas(commits) + X509CommitSignature.unsigned_commit_shas(commits.map(&:sha)) + end + + def unsigned_gpg_shas(commits) + GpgSignature.unsigned_commit_shas(commits.map(&:sha)) + end + + def enqueue_update_signatures + unsigned = unsigned_x509_shas(commits) & unsigned_gpg_shas(commits) return if unsigned.empty? signable = Gitlab::Git::Commit.shas_with_signatures(project.repository, unsigned) return if signable.empty? - CreateGpgSignatureWorker.perform_async(signable, project.id) + CreateCommitSignatureWorker.perform_async(signable, project.id) end # It's not sufficient to just check for a blank SHA as it's possible for the diff --git a/app/views/projects/commit/_signature.html.haml b/app/views/projects/commit/_signature.html.haml index 145bc629380..aa7c90bad66 100644 --- a/app/views/projects/commit/_signature.html.haml +++ b/app/views/projects/commit/_signature.html.haml @@ -1,2 +1,3 @@ - if signature - = render partial: "projects/commit/#{signature.verification_status}_signature_badge", locals: { signature: signature } + - uri = "projects/commit/#{"x509/" if signature.instance_of?(X509CommitSignature)}" + = render partial: "#{uri}#{signature.verification_status}_signature_badge", locals: { signature: signature } diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml index cbd998c60ef..776ce48d4bc 100644 --- a/app/views/projects/commit/_signature_badge.html.haml +++ b/app/views/projects/commit/_signature_badge.html.haml @@ -17,12 +17,18 @@ - content = capture do - if show_user .clearfix - = render partial: 'projects/commit/signature_badge_user', locals: { signature: signature } + - uri_signature_badge_user = "projects/commit/#{"x509/" if signature.instance_of?(X509CommitSignature)}signature_badge_user" + = render partial: "#{uri_signature_badge_user}", locals: { signature: signature } - = _('GPG Key ID:') - %span.monospace= signature.gpg_key_primary_keyid + - if signature.instance_of?(X509CommitSignature) + = render partial: "projects/commit/x509/certificate_details", locals: { signature: signature } - = link_to(_('Learn more about signing commits'), help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link') + = link_to(_('Learn more about x509 signed commits'), help_page_path('user/project/repository/x509_signed_commits/index.md'), class: 'gpg-popover-help-link') + - else + = _('GPG Key ID:') + %span.monospace= signature.gpg_key_primary_keyid + + = link_to(_('Learn more about signing commits'), help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link') %button{ tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } } = label diff --git a/app/views/projects/commit/x509/_certificate_details.html.haml b/app/views/projects/commit/x509/_certificate_details.html.haml new file mode 100644 index 00000000000..2357c6d803b --- /dev/null +++ b/app/views/projects/commit/x509/_certificate_details.html.haml @@ -0,0 +1,17 @@ +.gpg-popover-certificate-details + %strong= _('Certificate Subject') + %ul + - signature.x509_certificate.subject.split(",").each do |i| + - if i.start_with?("CN", "O") + %li= i + %li= _('Subject Key Identifier:') + %li.unstyled= signature.x509_certificate.subject_key_identifier.gsub(":", " ") + +.gpg-popover-certificate-details + %strong= _('Certificate Issuer') + %ul + - signature.x509_certificate.x509_issuer.subject.split(",").each do |i| + - if i.start_with?("CN", "OU", "O") + %li= i + %li= _('Subject Key Identifier:') + %li.unstyled= signature.x509_certificate.x509_issuer.subject_key_identifier.gsub(":", " ") diff --git a/app/views/projects/commit/x509/_signature_badge_user.html.haml b/app/views/projects/commit/x509/_signature_badge_user.html.haml new file mode 100644 index 00000000000..b64ccba2a18 --- /dev/null +++ b/app/views/projects/commit/x509/_signature_badge_user.html.haml @@ -0,0 +1,19 @@ +- user = signature.commit.committer +- user_email = signature.x509_certificate.email + +- if user + = link_to user_path(user), class: 'gpg-popover-user-link' do + %div + = user_avatar_without_link(user: user, size: 32) + + %div + %strong= user.name + %div= user.to_reference + +- else + = mail_to user_email do + %div + = user_avatar_without_link(user_email: user_email, size: 32) + + %div + %strong= user_email diff --git a/app/views/projects/commit/x509/_unverified_signature_badge.html.haml b/app/views/projects/commit/x509/_unverified_signature_badge.html.haml new file mode 100644 index 00000000000..680cc32c7e6 --- /dev/null +++ b/app/views/projects/commit/x509/_unverified_signature_badge.html.haml @@ -0,0 +1,6 @@ +- title = capture do + = _('This commit was signed with an unverified signature.').html_safe + +- locals = { signature: signature, title: title, label: _('Unverified'), css_class: 'invalid', icon: 'status_notfound_borderless', show_user: true } + += render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/views/projects/commit/x509/_verified_signature_badge.html.haml b/app/views/projects/commit/x509/_verified_signature_badge.html.haml new file mode 100644 index 00000000000..4964b1b8ee7 --- /dev/null +++ b/app/views/projects/commit/x509/_verified_signature_badge.html.haml @@ -0,0 +1,6 @@ +- title = capture do + = _('This commit was signed with a verified signature and the committer email is verified to belong to the same user.').html_safe + +- locals = { signature: signature, title: title, label: _('Verified'), css_class: 'valid', icon: 'status_success_borderless', show_user: true } + += render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 0426a0b8fbb..35852742b0d 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -699,14 +699,14 @@ :latency_sensitive: true :resource_boundary: :unknown :weight: 2 -- :name: create_evidence - :feature_category: :release_governance +- :name: create_commit_signature + :feature_category: :source_code_management :has_external_dependencies: :latency_sensitive: :resource_boundary: :unknown :weight: 2 -- :name: create_gpg_signature - :feature_category: :source_code_management +- :name: create_evidence + :feature_category: :release_governance :has_external_dependencies: :latency_sensitive: :resource_boundary: :unknown diff --git a/app/workers/create_gpg_signature_worker.rb b/app/workers/create_commit_signature_worker.rb similarity index 80% rename from app/workers/create_gpg_signature_worker.rb rename to app/workers/create_commit_signature_worker.rb index 2043c3c8e77..027fea3e402 100644 --- a/app/workers/create_gpg_signature_worker.rb +++ b/app/workers/create_commit_signature_worker.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class CreateGpgSignatureWorker +class CreateCommitSignatureWorker include ApplicationWorker feature_category :source_code_management @@ -23,7 +23,12 @@ class CreateGpgSignatureWorker # This calculates and caches the signature in the database commits.each do |commit| - Gitlab::Gpg::Commit.new(commit).signature + case commit.signature_type + when :PGP + Gitlab::Gpg::Commit.new(commit).signature + when :X509 + Gitlab::X509::Commit.new(commit).signature + end rescue => e Rails.logger.error("Failed to create signature for commit #{commit.id}. Error: #{e.message}") # rubocop:disable Gitlab/RailsLogger end diff --git a/changelogs/unreleased/ak-bump-retention.yml b/changelogs/unreleased/ak-bump-retention.yml new file mode 100644 index 00000000000..04272f8ca0a --- /dev/null +++ b/changelogs/unreleased/ak-bump-retention.yml @@ -0,0 +1,5 @@ +--- +title: Extend logs retention to period from 15 to 30 days +merge_request: 24466 +author: +type: changed diff --git a/changelogs/unreleased/expose-more-user-preferences-in-api.yml b/changelogs/unreleased/expose-more-user-preferences-in-api.yml new file mode 100644 index 00000000000..88bd9b71304 --- /dev/null +++ b/changelogs/unreleased/expose-more-user-preferences-in-api.yml @@ -0,0 +1,5 @@ +--- +title: Expose theme and color scheme user preferences in API +merge_request: 24409 +author: +type: changed diff --git a/changelogs/unreleased/feat-x509-signed-commits.yml b/changelogs/unreleased/feat-x509-signed-commits.yml new file mode 100644 index 00000000000..4011d9c63cc --- /dev/null +++ b/changelogs/unreleased/feat-x509-signed-commits.yml @@ -0,0 +1,5 @@ +--- +title: x509 signed commits using openssl +merge_request: 17773 +author: Roger Meier +type: added diff --git a/config/feature_categories.yml b/config/feature_categories.yml index ce6bc891d10..924bdb58682 100644 --- a/config/feature_categories.yml +++ b/config/feature_categories.yml @@ -38,6 +38,7 @@ - dependency_scanning - design_management - devops_score +- digital_experience_management - disaster_recovery - dynamic_application_security_testing - epics @@ -96,7 +97,6 @@ - static_site_editor - status_page - subgroups -- synthetic_monitoring - system_testing - teams - templates diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 9a920a3e5e4..1cb19d18a0d 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -42,12 +42,12 @@ - 2 - - container_repository - 1 +- - create_commit_signature + - 2 - - create_evidence - 2 - - create_github_webhook - 2 -- - create_gpg_signature - - 2 - - create_note_diff_file - 1 - - cronjob diff --git a/db/migrate/20190926225633_create_x509_signatures.rb b/db/migrate/20190926225633_create_x509_signatures.rb new file mode 100644 index 00000000000..88f6b03afba --- /dev/null +++ b/db/migrate/20190926225633_create_x509_signatures.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class CreateX509Signatures < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def change + create_table :x509_issuers do |t| + t.timestamps_with_timezone null: false + + t.string :subject_key_identifier, index: true, null: false, unique: true, limit: 255 + t.string :subject, null: false, limit: 255 + t.string :crl_url, null: false, limit: 255 + end + + create_table :x509_certificates do |t| + t.timestamps_with_timezone null: false + + t.string :subject_key_identifier, index: true, null: false, unique: true, limit: 255 + t.string :subject, null: false, limit: 255 + t.string :email, null: false, limit: 255 + t.binary :serial_number, null: false + + t.integer :certificate_status, limit: 2, default: 0, null: false + + t.references :x509_issuer, index: true, null: false, foreign_key: { on_delete: :cascade } + end + + create_table :x509_commit_signatures do |t| + t.timestamps_with_timezone null: false + + t.references :project, index: true, null: false, foreign_key: { on_delete: :cascade } + t.references :x509_certificate, index: true, null: false, foreign_key: { on_delete: :cascade } + + t.binary :commit_sha, index: true, null: false + t.integer :verification_status, limit: 2, default: 0, null: false + end + end +end diff --git a/db/post_migrate/20200206091544_migrate_create_commit_signature_worker_sidekiq_queue.rb b/db/post_migrate/20200206091544_migrate_create_commit_signature_worker_sidekiq_queue.rb new file mode 100644 index 00000000000..eec9abf4a31 --- /dev/null +++ b/db/post_migrate/20200206091544_migrate_create_commit_signature_worker_sidekiq_queue.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class MigrateCreateCommitSignatureWorkerSidekiqQueue < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + sidekiq_queue_migrate 'create_gpg_signature', to: 'create_commit_signature' + end + + def down + sidekiq_queue_migrate 'create_commit_signature', to: 'create_gpg_signature' + end +end diff --git a/db/schema.rb b/db/schema.rb index f55f3df31cf..35422461741 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_02_05_143231) do +ActiveRecord::Schema.define(version: 2020_02_06_091544) do # These are extensions that must be enabled in order to support this database enable_extension "pg_trgm" @@ -4482,6 +4482,40 @@ ActiveRecord::Schema.define(version: 2020_02_05_143231) do t.index ["type"], name: "index_web_hooks_on_type" end + create_table "x509_certificates", force: :cascade do |t| + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false + t.string "subject_key_identifier", limit: 255, null: false + t.string "subject", limit: 255, null: false + t.string "email", limit: 255, null: false + t.binary "serial_number", null: false + t.integer "certificate_status", limit: 2, default: 0, null: false + t.bigint "x509_issuer_id", null: false + t.index ["subject_key_identifier"], name: "index_x509_certificates_on_subject_key_identifier" + t.index ["x509_issuer_id"], name: "index_x509_certificates_on_x509_issuer_id" + end + + create_table "x509_commit_signatures", force: :cascade do |t| + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false + t.bigint "project_id", null: false + t.bigint "x509_certificate_id", null: false + t.binary "commit_sha", null: false + t.integer "verification_status", limit: 2, default: 0, null: false + t.index ["commit_sha"], name: "index_x509_commit_signatures_on_commit_sha" + t.index ["project_id"], name: "index_x509_commit_signatures_on_project_id" + t.index ["x509_certificate_id"], name: "index_x509_commit_signatures_on_x509_certificate_id" + end + + create_table "x509_issuers", force: :cascade do |t| + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false + t.string "subject_key_identifier", limit: 255, null: false + t.string "subject", limit: 255, null: false + t.string "crl_url", limit: 255, null: false + t.index ["subject_key_identifier"], name: "index_x509_issuers_on_subject_key_identifier" + end + create_table "zoom_meetings", force: :cascade do |t| t.bigint "project_id", null: false t.bigint "issue_id", null: false @@ -4973,6 +5007,9 @@ ActiveRecord::Schema.define(version: 2020_02_05_143231) do add_foreign_key "vulnerability_scanners", "projects", on_delete: :cascade add_foreign_key "web_hook_logs", "web_hooks", on_delete: :cascade add_foreign_key "web_hooks", "projects", name: "fk_0c8ca6d9d1", on_delete: :cascade + add_foreign_key "x509_certificates", "x509_issuers", on_delete: :cascade + add_foreign_key "x509_commit_signatures", "projects", on_delete: :cascade + add_foreign_key "x509_commit_signatures", "x509_certificates", on_delete: :cascade add_foreign_key "zoom_meetings", "issues", on_delete: :cascade add_foreign_key "zoom_meetings", "projects", on_delete: :cascade end diff --git a/doc/api/users.md b/doc/api/users.md index e147637ca59..52614a09b70 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -385,6 +385,8 @@ Parameters: - `skip_confirmation` (optional) - Skip confirmation - true or false (default) - `external` (optional) - Flags the user as external - true or false (default) - `avatar` (optional) - Image file for user's avatar +- `theme_id` (optional) - The GitLab theme for the user (see [the user preference docs](../user/profile/preferences.md#navigation-theme) for more information) +- `color_scheme_id` (optional) - User's color scheme for the file viewer (see [the user preference docs](../user/profile/preferences.md#syntax-highlighting-theme) for more information) - `private_profile` (optional) - User's profile is private - true, false (default), or null (will be converted to false) - `shared_runners_minutes_limit` (optional) - Pipeline minutes quota for this user **(STARTER)** - `extra_shared_runners_minutes_limit` (optional) - Extra pipeline minutes quota for this user **(STARTER)** @@ -423,6 +425,8 @@ Parameters: - `shared_runners_minutes_limit` (optional) - Pipeline minutes quota for this user - `extra_shared_runners_minutes_limit` (optional) - Extra pipeline minutes quota for this user - `avatar` (optional) - Image file for user's avatar +- `theme_id` (optional) - The GitLab theme for the user (see [the user preference docs](../user/profile/preferences.md#navigation-theme) for more information) +- `color_scheme_id` (optional) - User's color scheme for the file viewer (see [the user preference docs](../user/profile/preferences.md#syntax-highlighting-theme) for more information) - `private_profile` (optional) - User's profile is private - true, false (default), or null (will be converted to false) - `shared_runners_minutes_limit` (optional) - Pipeline minutes quota for this user **(STARTER)** - `extra_shared_runners_minutes_limit` (optional) - Extra pipeline minutes quota for this user **(STARTER)** diff --git a/doc/development/contributing/style_guides.md b/doc/development/contributing/style_guides.md index 022c40a9d1b..873d90dcf79 100644 --- a/doc/development/contributing/style_guides.md +++ b/doc/development/contributing/style_guides.md @@ -30,7 +30,7 @@ Our codebase style is defined and enforced by [RuboCop](https://github.com/ruboc You can check for any offenses locally with `bundle exec rubocop --parallel`. On the CI, this is automatically checked by the `static-analysis` jobs. -For RuboCop rules that we have not taken a decision yet, we follow the +For RuboCop rules that we have not taken a decision on yet, we follow the [Ruby Style Guide](https://github.com/rubocop-hq/ruby-style-guide), [Rails Style Guide](https://github.com/rubocop-hq/rails-style-guide), and [RSpec Style Guide](https://github.com/rubocop-hq/rspec-style-guide) as general diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md index 89ca40bfbb1..20aa6648c65 100644 --- a/doc/user/clusters/applications.md +++ b/doc/user/clusters/applications.md @@ -437,7 +437,7 @@ Filebeat will run as a DaemonSet on each node in your cluster, and it will ship GitLab will then connect to Elasticsearch for logs instead of the Kubernetes API, and you will have access to more advanced querying capabilities. -Log data is automatically deleted after 15 days using [Curator](https://www.elastic.co/guide/en/elasticsearch/client/curator/5.5/about.html). +Log data is automatically deleted after 30 days using [Curator](https://www.elastic.co/guide/en/elasticsearch/client/curator/5.5/about.html). To enable log shipping, install Elastic Stack into the cluster with the **Install** button. diff --git a/doc/user/project/repository/x509_signed_commits/index.md b/doc/user/project/repository/x509_signed_commits/index.md new file mode 100644 index 00000000000..421c2f60350 --- /dev/null +++ b/doc/user/project/repository/x509_signed_commits/index.md @@ -0,0 +1,100 @@ +--- +type: concepts, howto +--- + +# Signing commits with x509 + +[x509](https://en.wikipedia.org/wiki/X.509) is a standard format for public key +certificates issued by a public or private Public Key Infrastructure (PKI). +Personal x509 certificates are used for authentication or signing purposes +such as SMIME, but beside that, Git supports signing of commits and tags +with x509 certificates in a similar way as with [GPG](../gpg_signed_commits/index.md). +The main difference is the trust anchor which is the PKI for x509 certificates +instead of a web of trust with GPG. + +## How GitLab handles x509 + +GitLab uses its own certificate store and therefore defines the trust chain. + +For a commit to be *verified* by GitLab: + +- The signing certificate email must match a verified email address used by the committer in GitLab. +- The Certificate Authority has to be trusted by the GitLab instance, see also + [Omnibus install custom public certificates](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates). +- The signing time has to be within the time range of the [certificate validity](https://www.rfc-editor.org/rfc/rfc5280.html#section-4.1.2.5) + which is usually up to three years. +- The signing time is equal or later then commit time. + +NOTE: **Note:** There is no certificate revocation list check in place at the moment. + +## Obtaining an x509 key pair + +If your organization has Public Key Infrastructure (PKI), that PKI will provide +an S/MIME key. + +If you do not have an S/MIME key pair from a PKI, you can either create your +own self-signed one, or purchase one. MozillaZine keeps a nice collection +of [S/MIME-capable signing authorities](http://kb.mozillazine.org/Getting_an_SMIME_certificate) +and some of them generate keys for free. + +## Associating your x509 certificate with Git + +To take advantage of X509 signing, you will need Git 2.19.0 or later. You can +check your Git version with: + +```sh +git --version +``` + +If you have the correct version, you can proceed to configure Git. + +### Linux + +Configure Git to use your key for signing: + +```sh +signingkey = $( gpgsm --list-secret-keys | egrep '(key usage|ID)' | grep -B 1 digitalSignature | awk '/ID/ {print $2}' ) +git config --global user.signingkey $signingkey +git config --global gpg.format x509 +``` + +### Windows and MacOS + +Install [smimesign](https://github.com/github/smimesign) by downloading the +installer or via `brew install smimesign` on MacOS. + +Get the ID of your certificate with `smimesign --list-keys` and set your +signingkey `git config --global user.signingkey ID`, then configure x509: + +```sh +git config --global gpg.x509.program smimesign +git config --global gpg.format x509 +``` + +## Signing commits + +After you have [associated your x509 certificate with Git](#associating-your-x509-certificate-with-git) you +can start signing your commits: + +1. Commit like you used to, the only difference is the addition of the `-S` flag: + + ```sh + git commit -S -m "feat: x509 signed commits" + ``` + +1. Push to GitLab and check that your commits [are verified](#verifying-commits). + +If you don't want to type the `-S` flag every time you commit, you can tell Git +to sign your commits automatically: + +```sh +git config --global commit.gpgsign true +``` + +## Verifying commits + +To verify that a commit is signed, you can use the `--show-signature` flag: + +```sh +git log --show-signature +``` diff --git a/lib/api/users.rb b/lib/api/users.rb index da64ff7e306..4892a1fdd5b 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -52,6 +52,8 @@ module API optional :external, type: Boolean, desc: 'Flag indicating the user is an external user' # TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab/issues/14960 optional :avatar, type: File, desc: 'Avatar image for user' # rubocop:disable Scalability/FileUploads + optional :theme_id, type: Integer, default: 1, desc: 'The GitLab theme for the user' + optional :color_scheme_id, type: Integer, default: 1, desc: 'The color scheme for the file viewer' optional :private_profile, type: Boolean, desc: 'Flag indicating the user has a private profile' all_or_none_of :extern_uid, :provider diff --git a/lib/gitlab/color_schemes.rb b/lib/gitlab/color_schemes.rb index 881e5dbc923..620b4a8aee6 100644 --- a/lib/gitlab/color_schemes.rb +++ b/lib/gitlab/color_schemes.rb @@ -66,5 +66,9 @@ module Gitlab default end end + + def self.valid_ids + SCHEMES.map(&:id) + end end end diff --git a/lib/gitlab/database/x509_serial_number_attribute.rb b/lib/gitlab/database/x509_serial_number_attribute.rb new file mode 100644 index 00000000000..e12f64787e7 --- /dev/null +++ b/lib/gitlab/database/x509_serial_number_attribute.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Gitlab + module Database + # Class for casting binary data to int. + # + # Using X509SerialNumberAttribute allows you to store X509 certificate + # serial number values as binary while still using integer to access them. + # rfc 5280 - 4.1.2.2 Serial number (20 octets is the maximum), could be: + # - 1461501637330902918203684832716283019655932542975 + # - 0xffffffffffffffffffffffffffffffffffffffff + class X509SerialNumberAttribute < ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Bytea + PACK_FORMAT = 'H*' + + def deserialize(value) + value = super(value) + value ? value.unpack1(PACK_FORMAT).to_i : nil + end + + def serialize(value) + arg = value ? [value.to_s].pack(PACK_FORMAT) : nil + super(arg) + end + end + end +end diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb index 800e94e51e8..1abbd6dc45b 100644 --- a/lib/gitlab/gpg/commit.rb +++ b/lib/gitlab/gpg/commit.rb @@ -2,36 +2,9 @@ module Gitlab module Gpg - class Commit - include Gitlab::Utils::StrongMemoize - - def initialize(commit) - @commit = commit - - repo = commit.container.repository.raw_repository - @signature_data = Gitlab::Git::Commit.extract_signature_lazily(repo, commit.sha || commit.id) - - lazy_signature - end - - def signature_text - strong_memoize(:signature_text) do - @signature_data&.itself && @signature_data[0] # rubocop:disable Lint/SafeNavigationConsistency - end - end - - def signed_text - strong_memoize(:signed_text) do - @signature_data&.itself && @signature_data[1] # rubocop:disable Lint/SafeNavigationConsistency - end - end - - def has_signature? - !!(signature_text && signed_text) - end - + class Commit < Gitlab::SignedCommit def signature - return unless has_signature? + super return @signature if @signature diff --git a/lib/gitlab/signed_commit.rb b/lib/gitlab/signed_commit.rb new file mode 100644 index 00000000000..809e0a3f034 --- /dev/null +++ b/lib/gitlab/signed_commit.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Gitlab + class SignedCommit + include Gitlab::Utils::StrongMemoize + + def initialize(commit) + @commit = commit + + if commit.project + repo = commit.project.repository.raw_repository + @signature_data = Gitlab::Git::Commit.extract_signature_lazily(repo, commit.sha || commit.id) + end + + lazy_signature + end + + def signature + return unless @commit.has_signature? + end + + def signature_text + strong_memoize(:signature_text) do + @signature_data.itself ? @signature_data[0] : nil + end + end + + def signed_text + strong_memoize(:signed_text) do + @signature_data.itself ? @signature_data[1] : nil + end + end + end +end diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb index 63860b9cb26..5992f24f4e9 100644 --- a/lib/gitlab/themes.rb +++ b/lib/gitlab/themes.rb @@ -77,6 +77,10 @@ module Gitlab end end + def self.valid_ids + THEMES.map(&:id) + end + private def default_id diff --git a/lib/gitlab/x509/commit.rb b/lib/gitlab/x509/commit.rb new file mode 100644 index 00000000000..ce298b80a4c --- /dev/null +++ b/lib/gitlab/x509/commit.rb @@ -0,0 +1,199 @@ +# frozen_string_literal: true +require 'openssl' +require 'digest' + +module Gitlab + module X509 + class Commit < Gitlab::SignedCommit + def signature + super + + return @signature if @signature + + cached_signature = lazy_signature&.itself + return @signature = cached_signature if cached_signature.present? + + @signature = create_cached_signature! + end + + def update_signature!(cached_signature) + cached_signature.update!(attributes) + @signature = cached_signature + end + + private + + def lazy_signature + BatchLoader.for(@commit.sha).batch do |shas, loader| + X509CommitSignature.by_commit_sha(shas).each do |signature| + loader.call(signature.commit_sha, signature) + end + end + end + + def verified_signature + strong_memoize(:verified_signature) { verified_signature? } + end + + def cert + strong_memoize(:cert) do + signer_certificate(p7) if valid_signature? + end + end + + def cert_store + strong_memoize(:cert_store) do + store = OpenSSL::X509::Store.new + store.set_default_paths + # valid_signing_time? checks the time attributes already + # this flag is required, otherwise expired certificates would become + # unverified when notAfter within certificate attribute is reached + store.flags = OpenSSL::X509::V_FLAG_NO_CHECK_TIME + store + end + end + + def p7 + strong_memoize(:p7) do + pkcs7_text = signature_text.sub('-----BEGIN SIGNED MESSAGE-----', '-----BEGIN PKCS7-----') + pkcs7_text = pkcs7_text.sub('-----END SIGNED MESSAGE-----', '-----END PKCS7-----') + + OpenSSL::PKCS7.new(pkcs7_text) + rescue + nil + end + end + + def valid_signing_time? + # rfc 5280 - 4.1.2.5 Validity + # check if signed_time is within the time range (notBefore/notAfter) + # non-rfc - git specific check: signed_time >= commit_time + p7.signers[0].signed_time.between?(cert.not_before, cert.not_after) && + p7.signers[0].signed_time >= @commit.created_at + end + + def valid_signature? + p7.verify([], cert_store, signed_text, OpenSSL::PKCS7::NOVERIFY) + rescue + nil + end + + def verified_signature? + # verify has multiple options but only a boolean return value + # so first verify without certificate chain + if valid_signature? + if valid_signing_time? + # verify with system certificate chain + p7.verify([], cert_store, signed_text) + else + false + end + else + nil + end + rescue + nil + end + + def signer_certificate(p7) + p7.certificates.each do |cert| + next if cert.serial != p7.signers[0].serial + + return cert + end + end + + def certificate_crl + extension = get_certificate_extension('crlDistributionPoints') + extension.split('URI:').each do |item| + item.strip + + if item.start_with?("http") + return item.strip + end + end + end + + def get_certificate_extension(extension) + cert.extensions.each do |ext| + if ext.oid == extension + return ext.value + end + end + end + + def issuer_subject_key_identifier + get_certificate_extension('authorityKeyIdentifier').gsub("keyid:", "").delete!("\n") + end + + def certificate_subject_key_identifier + get_certificate_extension('subjectKeyIdentifier') + end + + def certificate_issuer + cert.issuer.to_s(OpenSSL::X509::Name::RFC2253) + end + + def certificate_subject + cert.subject.to_s(OpenSSL::X509::Name::RFC2253) + end + + def certificate_email + get_certificate_extension('subjectAltName').split('email:')[1] + end + + def issuer_attributes + return if verified_signature.nil? + + { + subject_key_identifier: issuer_subject_key_identifier, + subject: certificate_issuer, + crl_url: certificate_crl + } + end + + def certificate_attributes + return if verified_signature.nil? + + issuer = X509Issuer.safe_create!(issuer_attributes) + + { + subject_key_identifier: certificate_subject_key_identifier, + subject: certificate_subject, + email: certificate_email, + serial_number: cert.serial, + x509_issuer_id: issuer.id + } + end + + def attributes + return if verified_signature.nil? + + certificate = X509Certificate.safe_create!(certificate_attributes) + + { + commit_sha: @commit.sha, + project: @commit.project, + x509_certificate_id: certificate.id, + verification_status: verification_status + } + end + + def verification_status + if verified_signature && certificate_email == @commit.committer_email + :verified + else + :unverified + end + end + + def create_cached_signature! + return if verified_signature.nil? + + return X509CommitSignature.new(attributes) if Gitlab::Database.read_only? + + X509CommitSignature.safe_create!(attributes) + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a4127c54a6d..537ec76b9ea 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -361,6 +361,12 @@ msgstr "" msgid "%{percent}%{percentSymbol} complete" msgstr "" +msgid "%{placeholder} is not a valid color scheme" +msgstr "" + +msgid "%{placeholder} is not a valid theme" +msgstr "" + msgid "%{primary} (%{secondary})" msgstr "" @@ -618,6 +624,9 @@ msgid_plural "%d minutes" msgstr[0] "" msgstr[1] "" +msgid "1 month" +msgstr "" + msgid "1 open issue" msgid_plural "%{issues} open issues" msgstr[0] "" @@ -655,9 +664,6 @@ msgstr "" msgid "1st contribution!" msgstr "" -msgid "2 weeks" -msgstr "" - msgid "20-29 contributions" msgstr "" @@ -3188,6 +3194,12 @@ msgstr "" msgid "Certificate (PEM)" msgstr "" +msgid "Certificate Issuer" +msgstr "" + +msgid "Certificate Subject" +msgstr "" + msgid "Change assignee" msgstr "" @@ -11124,6 +11136,9 @@ msgstr "" msgid "Learn more about the dependency list" msgstr "" +msgid "Learn more about x509 signed commits" +msgstr "" + msgid "Learn more in the" msgstr "" @@ -18167,6 +18182,9 @@ msgstr "" msgid "Subgroups and projects" msgstr "" +msgid "Subject Key Identifier:" +msgstr "" + msgid "Subkeys" msgstr "" diff --git a/qa/qa.rb b/qa/qa.rb index 685cab36b05..29205c63251 100644 --- a/qa/qa.rb +++ b/qa/qa.rb @@ -16,6 +16,8 @@ module QA module Flow autoload :Login, 'qa/flow/login' autoload :Project, 'qa/flow/project' + autoload :Saml, 'qa/flow/saml' + autoload :User, 'qa/flow/user' end ## @@ -431,6 +433,7 @@ module QA autoload :NodeJs, 'qa/service/docker_run/node_js' autoload :GitlabRunner, 'qa/service/docker_run/gitlab_runner' autoload :MailHog, 'qa/service/docker_run/mail_hog' + autoload :SamlIdp, 'qa/service/docker_run/saml_idp' end end diff --git a/qa/qa/flow/saml.rb b/qa/qa/flow/saml.rb new file mode 100644 index 00000000000..0b9f9f94fbd --- /dev/null +++ b/qa/qa/flow/saml.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module QA + module Flow + module Saml + module_function + + def page + Capybara.current_session + end + + def logout_from_idp(saml_idp_service) + Runtime::Logger.debug("Logging out of IDP by visiting \"#{saml_idp_service.idp_sign_out_url}\"") + + Support::Waiter.wait_until(sleep_interval: 1, reload_page: page) do + page.visit saml_idp_service.idp_sign_out_url + page.has_content?("You have been logged out.") + end + end + + def enable_saml_sso(group, saml_idp_service) + page.visit Runtime::Scenario.gitlab_address + + Page::Main::Login.perform(&:sign_in_using_credentials) unless Page::Main::Menu.perform(&:signed_in?) + + visit_saml_sso_settings(group) + + Support::Retrier.retry_on_exception do + EE::Page::Group::Settings::SamlSSO.perform do |saml_sso| + saml_sso.set_id_provider_sso_url(saml_idp_service.idp_sso_url) + saml_sso.set_cert_fingerprint(saml_idp_service.idp_certificate_fingerprint) + saml_sso.click_save_changes + + saml_sso.user_login_url_link_text + end + end + end + + def visit_saml_sso_settings(group, direct: false) + if direct + page.visit "#{group.web_url}/-/saml" + else + group.visit! + + Page::Group::Menu.perform(&:go_to_saml_sso_group_settings) + end + # The toggle buttons take a moment to switch to the correct status. + # I am not sure of a better, less complex way to wait for them to reflect their actual status. + sleep 2 + end + + def run_saml_idp_service(group_name) + service = Service::DockerRun::SamlIdp.new(Runtime::Scenario.gitlab_address, group_name).tap do |runner| + runner.pull + runner.register! + end + + service + end + + def remove_saml_idp_service(saml_idp_service) + saml_idp_service.remove! + end + + def login_to_idp_if_required(username, password) + Vendor::SAMLIdp::Page::Login.perform { |login_page| login_page.login_if_required(username, password) } + end + end + end +end diff --git a/qa/qa/flow/user.rb b/qa/qa/flow/user.rb new file mode 100644 index 00000000000..acc77cb9830 --- /dev/null +++ b/qa/qa/flow/user.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module QA + module Flow + module User + module_function + + def page + Capybara.current_session + end + + def confirm_user(username) + Flow::Login.while_signed_in_as_admin do + Page::Main::Menu.perform(&:go_to_admin_area) + Page::Admin::Menu.perform(&:go_to_users_overview) + Page::Admin::Overview::Users::Index.perform do |index| + index.search_user(username) + index.click_user(username) + end + + Page::Admin::Overview::Users::Show.perform(&:confirm_user) + end + end + end + end +end diff --git a/qa/qa/resource/members.rb b/qa/qa/resource/members.rb index c738a91a77f..38a620a5427 100644 --- a/qa/qa/resource/members.rb +++ b/qa/qa/resource/members.rb @@ -11,6 +11,10 @@ module QA post Runtime::API::Request.new(api_client, api_members_path).url, { user_id: user.id, access_level: access_level } end + def remove_member(user) + delete Runtime::API::Request.new(api_client, "#{api_members_path}/#{user.id}").url + end + def list_members JSON.parse(get(Runtime::API::Request.new(api_client, api_members_path).url).body) end diff --git a/qa/qa/resource/sandbox.rb b/qa/qa/resource/sandbox.rb index 54c13071cef..7b427af6b74 100644 --- a/qa/qa/resource/sandbox.rb +++ b/qa/qa/resource/sandbox.rb @@ -63,6 +63,10 @@ module QA '/groups' end + def api_delete_path + "/groups/#{id}" + end + def api_post_body { path: path, diff --git a/qa/qa/runtime/feature.rb b/qa/qa/runtime/feature.rb index 25fc02a887e..9cb2c925b19 100644 --- a/qa/qa/runtime/feature.rb +++ b/qa/qa/runtime/feature.rb @@ -38,6 +38,8 @@ module QA end raise SetFeatureError, "#{key} was not enabled!" unless is_enabled + + QA::Runtime::Logger.info("Successfully enabled and verified feature flag: #{key}") end end diff --git a/qa/qa/service/docker_run/saml_idp.rb b/qa/qa/service/docker_run/saml_idp.rb new file mode 100644 index 00000000000..a0638bbcc2e --- /dev/null +++ b/qa/qa/service/docker_run/saml_idp.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +module QA + module Service + module DockerRun + class SamlIdp < Base + def initialize(gitlab_host, group) + @image = 'jamedjo/test-saml-idp' + @name = 'saml-idp-server' + @gitlab_host = gitlab_host + @group = group + super() + end + + def idp_base_url + "https://#{host_name}:8443/simplesaml" + end + + def idp_sso_url + "#{idp_base_url}/saml2/idp/SSOService.php" + end + + def idp_sign_out_url + "#{idp_base_url}/module.php/core/authenticate.php?as=example-userpass&logout" + end + + def idp_signed_out_url + "#{idp_base_url}/logout.php" + end + + def idp_metadata_url + "#{idp_base_url}/saml2/idp/metadata.php" + end + + def idp_issuer + idp_metadata_url + end + + def idp_certificate_fingerprint + QA::Runtime::Env.simple_saml_fingerprint || '119b9e027959cdb7c662cfd075d9e2ef384e445f' + end + + def host_name + return 'localhost' unless QA::Runtime::Env.running_in_ci? + + super + end + + def register! + command = <<~CMD.tr("\n", ' ') + docker run -d --rm + --network #{network} + --hostname #{host_name} + --name #{@name} + --env SIMPLESAMLPHP_SP_ENTITY_ID=#{@gitlab_host}/groups/#{@group} + --env SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE=#{@gitlab_host}/groups/#{@group}/-/saml/callback + --publish 8080:8080 + --publish 8443:8443 + #{@image} + CMD + + command.gsub!("--network #{network} ", '') unless QA::Runtime::Env.running_in_ci? + + shell command + end + end + end + end +end diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb index bd50811726a..d1a4a9a0058 100644 --- a/spec/controllers/projects/compare_controller_spec.rb +++ b/spec/controllers/projects/compare_controller_spec.rb @@ -281,7 +281,7 @@ describe Projects::CompareController do context 'when the user has access to the project' do render_views - let(:signature_commit) { build(:commit, project: project, safe_message: "message", sha: 'signature_commit') } + let(:signature_commit) { project.commit_by(oid: '0b4bc9a49b562e85de7cc9e834518ea6828729b9') } let(:non_signature_commit) { build(:commit, project: project, safe_message: "message", sha: 'non_signature_commit') } before do diff --git a/spec/factories/x509_certificate.rb b/spec/factories/x509_certificate.rb new file mode 100644 index 00000000000..819ad0704dc --- /dev/null +++ b/spec/factories/x509_certificate.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :x509_certificate do + subject_key_identifier { 'BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC:BC' } + subject { 'CN=gitlab@example.org,OU=Example,O=World' } + + email { 'gitlab@example.org' } + serial_number { 278969561018901340486471282831158785578 } + x509_issuer + end +end diff --git a/spec/factories/x509_commit_signature.rb b/spec/factories/x509_commit_signature.rb new file mode 100644 index 00000000000..a342b240690 --- /dev/null +++ b/spec/factories/x509_commit_signature.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :x509_commit_signature do + commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) } + project + x509_certificate + verification_status { :verified } + end +end diff --git a/spec/factories/x509_issuer.rb b/spec/factories/x509_issuer.rb new file mode 100644 index 00000000000..e003b16ad86 --- /dev/null +++ b/spec/factories/x509_issuer.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :x509_issuer do + subject_key_identifier { 'AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB' } + subject { 'CN=PKI,OU=Example,O=World' } + + crl_url { 'http://example.com/pki.crl' } + end +end diff --git a/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap b/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap new file mode 100644 index 00000000000..7382a3a4cf7 --- /dev/null +++ b/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Blob Header Filepath rendering matches the snapshot 1`] = ` +
+ +
+`; diff --git a/spec/frontend/blob/components/blob_header_filepath_spec.js b/spec/frontend/blob/components/blob_header_filepath_spec.js new file mode 100644 index 00000000000..d029ba2a7a4 --- /dev/null +++ b/spec/frontend/blob/components/blob_header_filepath_spec.js @@ -0,0 +1,90 @@ +import { shallowMount } from '@vue/test-utils'; +import BlobHeaderFilepath from '~/blob/components/blob_header_filepath.vue'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import { Blob as MockBlob } from './mock_data'; +import { numberToHumanSize } from '~/lib/utils/number_utils'; + +const mockHumanReadableSize = 'a lot'; +jest.mock('~/lib/utils/number_utils', () => ({ + numberToHumanSize: jest.fn(() => mockHumanReadableSize), +})); + +describe('Blob Header Filepath', () => { + let wrapper; + + function createComponent(blobProps = {}, options = {}) { + wrapper = shallowMount(BlobHeaderFilepath, { + propsData: { + blob: Object.assign({}, MockBlob, blobProps), + }, + ...options, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + describe('rendering', () => { + it('matches the snapshot', () => { + createComponent(); + expect(wrapper.element).toMatchSnapshot(); + }); + + it('renders regular name', () => { + createComponent(); + expect( + wrapper + .find('.js-blob-header-filepath') + .text() + .trim(), + ).toBe(MockBlob.name); + }); + + it('does not fail if the name is empty', () => { + const emptyName = ''; + createComponent({ name: emptyName }); + expect(wrapper.find('.js-blob-header-filepath').exists()).toBe(false); + }); + + it('renders copy-to-clipboard icon that copies path of the Blob', () => { + createComponent(); + const btn = wrapper.find(ClipboardButton); + expect(btn.exists()).toBe(true); + expect(btn.vm.text).toBe(MockBlob.path); + }); + + it('renders filesize in a human-friendly format', () => { + createComponent(); + expect(numberToHumanSize).toHaveBeenCalled(); + expect(wrapper.vm.blobSize).toBe(mockHumanReadableSize); + }); + + it('renders a slot and prepends its contents to the existing one', () => { + const slotContent = 'Foo Bar'; + createComponent( + {}, + { + scopedSlots: { + filepathPrepend: `${slotContent}`, + }, + }, + ); + + expect(wrapper.text()).toContain(slotContent); + expect( + wrapper + .text() + .trim() + .substring(0, slotContent.length), + ).toBe(slotContent); + }); + }); + + describe('functionality', () => { + it('sets gfm value correctly on the clipboard-button', () => { + createComponent(); + expect(wrapper.vm.gfmCopyText).toBe('`dummy.md`'); + }); + }); +}); diff --git a/spec/frontend/blob/components/mock_data.js b/spec/frontend/blob/components/mock_data.js new file mode 100644 index 00000000000..4f7b297aba0 --- /dev/null +++ b/spec/frontend/blob/components/mock_data.js @@ -0,0 +1,29 @@ +export const Blob = { + binary: false, + highlightedData: + '

\nThis one is dummy

\n

\nAnd has sub-header

\n

Even some stupid text here

', + name: 'dummy.md', + path: 'dummy.md', + rawPath: '/flightjs/flight/snippets/51/raw', + size: 75, + simpleViewer: { + collapsed: false, + fileType: 'text', + loadAsync: true, + loadingPartialName: 'loading', + renderError: null, + tooLarge: false, + type: 'simple', + }, + richViewer: { + collapsed: false, + fileType: 'markup', + loadAsync: true, + loadingPartialName: 'loading', + renderError: null, + tooLarge: false, + type: 'rich', + }, +}; + +export default {}; diff --git a/spec/lib/gitlab/x509/commit_spec.rb b/spec/lib/gitlab/x509/commit_spec.rb new file mode 100644 index 00000000000..9cddf27ddce --- /dev/null +++ b/spec/lib/gitlab/x509/commit_spec.rb @@ -0,0 +1,208 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Gitlab::X509::Commit do + describe '#signature' do + let(:signature) { described_class.new(commit).signature } + + let(:user1_certificate_attributes) do + { + subject_key_identifier: X509Helpers::User1.certificate_subject_key_identifier, + subject: X509Helpers::User1.certificate_subject, + email: X509Helpers::User1.certificate_email, + serial_number: X509Helpers::User1.certificate_serial + } + end + + let(:user1_issuer_attributes) do + { + subject_key_identifier: X509Helpers::User1.issuer_subject_key_identifier, + subject: X509Helpers::User1.certificate_issuer, + crl_url: X509Helpers::User1.certificate_crl + } + end + + shared_examples 'returns the cached signature on second call' do + it 'returns the cached signature on second call' do + x509_commit = described_class.new(commit) + + expect(x509_commit).to receive(:create_cached_signature).and_call_original + signature + + # consecutive call + expect(x509_commit).not_to receive(:create_cached_signature).and_call_original + signature + end + end + + let!(:project) { create :project, :repository, path: X509Helpers::User1.path } + let!(:commit_sha) { X509Helpers::User1.commit } + + context 'unsigned commit' do + let!(:commit) { create :commit, project: project, sha: commit_sha } + + it 'returns nil' do + expect(described_class.new(commit).signature).to be_nil + end + end + + context 'valid signature from known user' do + let!(:commit) { create :commit, project: project, sha: commit_sha, created_at: Time.utc(2019, 1, 1, 20, 15, 0), committer_email: X509Helpers::User1.emails.first } + + let!(:user) { create(:user, email: X509Helpers::User1.emails.first) } + + before do + allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) + .with(Gitlab::Git::Repository, commit_sha) + .and_return( + [ + X509Helpers::User1.signed_commit_signature, + X509Helpers::User1.signed_commit_base_data + ] + ) + end + + it 'returns an unverified signature' do + expect(signature).to have_attributes( + commit_sha: commit_sha, + project: project, + verification_status: 'unverified' + ) + expect(signature.x509_certificate).to have_attributes(user1_certificate_attributes) + expect(signature.x509_certificate.x509_issuer).to have_attributes(user1_issuer_attributes) + expect(signature.persisted?).to be_truthy + end + end + + context 'verified signature from known user' do + let!(:commit) { create :commit, project: project, sha: commit_sha, created_at: Time.utc(2019, 1, 1, 20, 15, 0), committer_email: X509Helpers::User1.emails.first } + + let!(:user) { create(:user, email: X509Helpers::User1.emails.first) } + + before do + allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) + .with(Gitlab::Git::Repository, commit_sha) + .and_return( + [ + X509Helpers::User1.signed_commit_signature, + X509Helpers::User1.signed_commit_base_data + ] + ) + end + + context 'with trusted certificate store' do + before do + store = OpenSSL::X509::Store.new + certificate = OpenSSL::X509::Certificate.new X509Helpers::User1.trust_cert + store.add_cert(certificate) + allow(OpenSSL::X509::Store).to receive(:new) + .and_return( + store + ) + end + + it 'returns a verified signature' do + expect(signature).to have_attributes( + commit_sha: commit_sha, + project: project, + verification_status: 'verified' + ) + expect(signature.x509_certificate).to have_attributes(user1_certificate_attributes) + expect(signature.x509_certificate.x509_issuer).to have_attributes(user1_issuer_attributes) + expect(signature.persisted?).to be_truthy + end + end + + context 'without trusted certificate within store' do + before do + store = OpenSSL::X509::Store.new + allow(OpenSSL::X509::Store).to receive(:new) + .and_return( + store + ) + end + + it 'returns an unverified signature' do + expect(signature).to have_attributes( + commit_sha: commit_sha, + project: project, + verification_status: 'unverified' + ) + expect(signature.x509_certificate).to have_attributes(user1_certificate_attributes) + expect(signature.x509_certificate.x509_issuer).to have_attributes(user1_issuer_attributes) + expect(signature.persisted?).to be_truthy + end + end + end + + context 'unverified signature from unknown user' do + let!(:commit) { create :commit, project: project, sha: commit_sha, created_at: Time.utc(2019, 1, 1, 20, 15, 0), committer_email: X509Helpers::User1.emails.first } + + before do + allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) + .with(Gitlab::Git::Repository, commit_sha) + .and_return( + [ + X509Helpers::User1.signed_commit_signature, + X509Helpers::User1.signed_commit_base_data + ] + ) + end + + it 'returns an unverified signature' do + expect(signature).to have_attributes( + commit_sha: commit_sha, + project: project, + verification_status: 'unverified' + ) + expect(signature.x509_certificate).to have_attributes(user1_certificate_attributes) + expect(signature.x509_certificate.x509_issuer).to have_attributes(user1_issuer_attributes) + expect(signature.persisted?).to be_truthy + end + end + + context 'invalid signature' do + let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: X509Helpers::User1.emails.first } + + let!(:user) { create(:user, email: X509Helpers::User1.emails.first) } + + before do + allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) + .with(Gitlab::Git::Repository, commit_sha) + .and_return( + [ + # Corrupt the key + X509Helpers::User1.signed_commit_signature.tr('A', 'B'), + X509Helpers::User1.signed_commit_base_data + ] + ) + end + + it 'returns nil' do + expect(described_class.new(commit).signature).to be_nil + end + end + + context 'invalid commit message' do + let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: X509Helpers::User1.emails.first } + + let!(:user) { create(:user, email: X509Helpers::User1.emails.first) } + + before do + allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) + .with(Gitlab::Git::Repository, commit_sha) + .and_return( + [ + X509Helpers::User1.signed_commit_signature, + # Corrupt the commit message + 'x' + ] + ) + end + + it 'returns nil' do + expect(described_class.new(commit).signature).to be_nil + end + end + end +end diff --git a/spec/migrations/migrate_create_commit_signature_worker_sidekiq_queue_spec.rb b/spec/migrations/migrate_create_commit_signature_worker_sidekiq_queue_spec.rb new file mode 100644 index 00000000000..3d7803b7563 --- /dev/null +++ b/spec/migrations/migrate_create_commit_signature_worker_sidekiq_queue_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20200206091544_migrate_create_commit_signature_worker_sidekiq_queue.rb') + +describe MigrateCreateCommitSignatureWorkerSidekiqQueue, :sidekiq, :redis do + include Gitlab::Database::MigrationHelpers + include StubWorker + + context 'when there are jobs in the queue' do + it 'correctly migrates queue when migrating up' do + Sidekiq::Testing.disable! do + stub_worker(queue: 'create_commit_signature').perform_async('Something', [1]) + stub_worker(queue: 'create_gpg_signature').perform_async('Something', [1]) + + described_class.new.up + + expect(sidekiq_queue_length('create_gpg_signature')).to eq 0 + expect(sidekiq_queue_length('create_commit_signature')).to eq 2 + end + end + + it 'correctly migrates queue when migrating down' do + Sidekiq::Testing.disable! do + stub_worker(queue: 'create_gpg_signature').perform_async('Something', [1]) + + described_class.new.down + + expect(sidekiq_queue_length('create_gpg_signature')).to eq 1 + expect(sidekiq_queue_length('create_commit_signature')).to eq 0 + end + end + end + + context 'when there are no jobs in the queues' do + it 'does not raise error when migrating up' do + expect { described_class.new.up }.not_to raise_error + end + + it 'does not raise error when migrating down' do + expect { described_class.new.down }.not_to raise_error + end + end +end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index c09f5bc4f4d..ada25005064 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -671,4 +671,25 @@ eos expect(commit2.merge_requests).to contain_exactly(merge_request1) end end + + describe 'signed commits' do + let(:gpg_signed_commit) { project.commit_by(oid: '0b4bc9a49b562e85de7cc9e834518ea6828729b9') } + let(:x509_signed_commit) { project.commit_by(oid: '189a6c924013fc3fe40d6f1ec1dc20214183bc97') } + let(:unsigned_commit) { project.commit_by(oid: '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51') } + let!(:commit) { create(:commit, project: project) } + + it 'returns signature_type properly' do + expect(gpg_signed_commit.signature_type).to eq(:PGP) + expect(x509_signed_commit.signature_type).to eq(:X509) + expect(unsigned_commit.signature_type).to eq(:NONE) + expect(commit.signature_type).to eq(:NONE) + end + + it 'returns has_signature? properly' do + expect(gpg_signed_commit.has_signature?).to be_truthy + expect(x509_signed_commit.has_signature?).to be_truthy + expect(unsigned_commit.has_signature?).to be_falsey + expect(commit.has_signature?).to be_falsey + end + end end diff --git a/spec/models/concerns/x509_serial_number_attribute_spec.rb b/spec/models/concerns/x509_serial_number_attribute_spec.rb new file mode 100644 index 00000000000..18a1d85204c --- /dev/null +++ b/spec/models/concerns/x509_serial_number_attribute_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe X509SerialNumberAttribute do + let(:model) { Class.new { include X509SerialNumberAttribute } } + + before do + columns = [ + double(:column, name: 'name', type: :text), + double(:column, name: 'serial_number', type: :binary) + ] + + allow(model).to receive(:columns).and_return(columns) + end + + describe '#x509_serial_number_attribute' do + context 'when in non-production' do + before do + stub_rails_env('development') + end + + context 'when the table exists' do + before do + allow(model).to receive(:table_exists?).and_return(true) + end + + it 'defines a x509 serial number attribute for a binary column' do + expect(model).to receive(:attribute) + .with(:serial_number, an_instance_of(Gitlab::Database::X509SerialNumberAttribute)) + + model.x509_serial_number_attribute(:serial_number) + end + + it 'raises ArgumentError when the column type is not :binary' do + expect { model.x509_serial_number_attribute(:name) }.to raise_error(ArgumentError) + end + end + + context 'when the table does not exist' do + it 'allows the attribute to be added and issues a warning' do + allow(model).to receive(:table_exists?).and_return(false) + + expect(model).not_to receive(:columns) + expect(model).to receive(:attribute) + expect(model).to receive(:warn) + + model.x509_serial_number_attribute(:name) + end + end + + context 'when the column does not exist' do + it 'allows the attribute to be added and issues a warning' do + allow(model).to receive(:table_exists?).and_return(true) + + expect(model).to receive(:columns) + expect(model).to receive(:attribute) + expect(model).to receive(:warn) + + model.x509_serial_number_attribute(:no_name) + end + end + + context 'when other execeptions are raised' do + it 'logs and re-rasises the error' do + allow(model).to receive(:table_exists?).and_raise(ActiveRecord::NoDatabaseError.new('does not exist')) + + expect(model).not_to receive(:columns) + expect(model).not_to receive(:attribute) + expect(Gitlab::AppLogger).to receive(:error) + + expect { model.x509_serial_number_attribute(:name) }.to raise_error(ActiveRecord::NoDatabaseError) + end + end + end + + context 'when in production' do + before do + stub_rails_env('production') + end + + it 'defines a x509 serial number attribute' do + expect(model).not_to receive(:table_exists?) + expect(model).not_to receive(:columns) + expect(model).to receive(:attribute).with(:serial_number, an_instance_of(Gitlab::Database::X509SerialNumberAttribute)) + + model.x509_serial_number_attribute(:serial_number) + end + end + end +end diff --git a/spec/models/x509_certificate_spec.rb b/spec/models/x509_certificate_spec.rb new file mode 100644 index 00000000000..187d37334a1 --- /dev/null +++ b/spec/models/x509_certificate_spec.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe X509Certificate do + describe 'validation' do + it { is_expected.to validate_presence_of(:subject_key_identifier) } + it { is_expected.to validate_presence_of(:subject) } + it { is_expected.to validate_presence_of(:email) } + it { is_expected.to validate_presence_of(:serial_number) } + it { is_expected.to validate_presence_of(:x509_issuer_id) } + end + + describe 'associations' do + it { is_expected.to belong_to(:x509_issuer).required } + end + + describe '.safe_create!' do + let(:subject_key_identifier) { 'CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD' } + let(:subject) { 'CN=gitlab@example.com,OU=Example,O=World' } + let(:email) { 'gitlab@example.com' } + let(:serial_number) { '123456789' } + let(:issuer) { create(:x509_issuer) } + + let(:attributes) do + { + subject_key_identifier: subject_key_identifier, + subject: subject, + email: email, + serial_number: serial_number, + x509_issuer_id: issuer.id + } + end + + it 'creates a new certificate if it was not found' do + expect { described_class.safe_create!(attributes) }.to change { described_class.count }.by(1) + end + + it 'assigns the correct attributes when creating' do + certificate = described_class.safe_create!(attributes) + + expect(certificate.subject_key_identifier).to eq(subject_key_identifier) + expect(certificate.subject).to eq(subject) + expect(certificate.email).to eq(email) + end + end + + describe 'validators' do + it 'accepts correct subject_key_identifier' do + subject_key_identifiers = [ + 'AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB', + 'CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD' + ] + + subject_key_identifiers.each do |identifier| + expect(build(:x509_certificate, subject_key_identifier: identifier)).to be_valid + end + end + + it 'rejects invalid subject_key_identifier' do + subject_key_identifiers = [ + 'AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB', + 'CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:GG', + 'random string', + '12321342545356434523412341245452345623453542345234523453245' + ] + + subject_key_identifiers.each do |identifier| + expect(build(:x509_certificate, subject_key_identifier: identifier)).to be_invalid + end + end + + it 'accepts correct email address' do + emails = [ + 'smime@example.org', + 'smime@example.com' + ] + + emails.each do |email| + expect(build(:x509_certificate, email: email)).to be_valid + end + end + + it 'rejects invalid email' do + emails = [ + 'this is not an email', + '@example.org' + ] + + emails.each do |email| + expect(build(:x509_certificate, email: email)).to be_invalid + end + end + + it 'accepts valid serial_number' do + expect(build(:x509_certificate, serial_number: 123412341234)).to be_valid + + # rfc 5280 - 4.1.2.2 Serial number (20 octets is the maximum) + expect(build(:x509_certificate, serial_number: 1461501637330902918203684832716283019655932542975)).to be_valid + expect(build(:x509_certificate, serial_number: 'ffffffffffffffffffffffffffffffffffffffff'.to_i(16))).to be_valid + end + + it 'rejects invalid serial_number' do + expect(build(:x509_certificate, serial_number: "sgsgfsdgdsfg")).to be_invalid + end + end +end diff --git a/spec/models/x509_commit_signature_spec.rb b/spec/models/x509_commit_signature_spec.rb new file mode 100644 index 00000000000..a2f72228a86 --- /dev/null +++ b/spec/models/x509_commit_signature_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe X509CommitSignature do + let(:commit_sha) { '189a6c924013fc3fe40d6f1ec1dc20214183bc97' } + let(:project) { create(:project, :public, :repository) } + let!(:commit) { create(:commit, project: project, sha: commit_sha) } + let(:x509_certificate) { create(:x509_certificate) } + let(:x509_signature) { create(:x509_commit_signature, commit_sha: commit_sha) } + + it_behaves_like 'having unique enum values' + + describe 'validation' do + it { is_expected.to validate_presence_of(:commit_sha) } + it { is_expected.to validate_presence_of(:project_id) } + it { is_expected.to validate_presence_of(:x509_certificate_id) } + end + + describe 'associations' do + it { is_expected.to belong_to(:project).required } + it { is_expected.to belong_to(:x509_certificate).required } + end + + describe '.safe_create!' do + let(:attributes) do + { + commit_sha: commit_sha, + project: project, + x509_certificate_id: x509_certificate.id, + verification_status: "verified" + } + end + + it 'finds a signature by commit sha if it existed' do + x509_signature + + expect(described_class.safe_create!(commit_sha: commit_sha)).to eq(x509_signature) + end + + it 'creates a new signature if it was not found' do + expect { described_class.safe_create!(attributes) }.to change { described_class.count }.by(1) + end + + it 'assigns the correct attributes when creating' do + signature = described_class.safe_create!(attributes) + + expect(signature.project).to eq(project) + expect(signature.commit_sha).to eq(commit_sha) + expect(signature.x509_certificate_id).to eq(x509_certificate.id) + end + end +end diff --git a/spec/models/x509_issuer_spec.rb b/spec/models/x509_issuer_spec.rb new file mode 100644 index 00000000000..f1067cad655 --- /dev/null +++ b/spec/models/x509_issuer_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe X509Issuer do + describe 'validation' do + it { is_expected.to validate_presence_of(:subject_key_identifier) } + it { is_expected.to validate_presence_of(:subject) } + it { is_expected.to validate_presence_of(:crl_url) } + end + + describe '.safe_create!' do + let(:issuer_subject_key_identifier) { 'AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB' } + let(:issuer_subject) { 'CN=PKI,OU=Example,O=World' } + let(:issuer_crl_url) { 'http://example.com/pki.crl' } + + let(:attributes) do + { + subject_key_identifier: issuer_subject_key_identifier, + subject: issuer_subject, + crl_url: issuer_crl_url + } + end + + it 'creates a new issuer if it was not found' do + expect { described_class.safe_create!(attributes) }.to change { described_class.count }.by(1) + end + + it 'assigns the correct attributes when creating' do + issuer = described_class.safe_create!(attributes) + + expect(issuer.subject_key_identifier).to eq(issuer_subject_key_identifier) + expect(issuer.subject).to eq(issuer_subject) + expect(issuer.crl_url).to eq(issuer_crl_url) + end + end + + describe 'validators' do + it 'accepts correct subject_key_identifier' do + subject_key_identifiers = [ + 'AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB', + 'CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD' + ] + + subject_key_identifiers.each do |identifier| + expect(build(:x509_issuer, subject_key_identifier: identifier)).to be_valid + end + end + + it 'rejects invalid subject_key_identifier' do + subject_key_identifiers = [ + 'AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB', + 'CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:CD:GG', + 'random string', + '12321342545356434523412341245452345623453542345234523453245' + ] + + subject_key_identifiers.each do |identifier| + expect(build(:x509_issuer, subject_key_identifier: identifier)).to be_invalid + end + end + + it 'accepts valid crl_url' do + expect(build(:x509_issuer, crl_url: "https://pki.example.org")).to be_valid + end + + it 'rejects invalid crl_url' do + expect(build(:x509_issuer, crl_url: "ht://pki.example.org")).to be_invalid + end + end +end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index f6ff2020c79..aa8dd021707 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -461,7 +461,7 @@ describe API::Users do end it "creates user with optional attributes" do - optional_attributes = { confirm: true } + optional_attributes = { confirm: true, theme_id: 2, color_scheme_id: 4 } attributes = attributes_for(:user).merge(optional_attributes) post api('/users', admin), params: attributes @@ -576,6 +576,15 @@ describe API::Users do expect(response).to have_gitlab_http_status(400) end + it "doesn't create user with invalid optional attributes" do + optional_attributes = { theme_id: 50, color_scheme_id: 50 } + attributes = attributes_for(:user).merge(optional_attributes) + + post api('/users', admin), params: attributes + + expect(response).to have_gitlab_http_status(400) + end + it 'returns 400 error if user does not validate' do post api('/users', admin), params: { @@ -824,6 +833,34 @@ describe API::Users do expect(user.reload.email).not_to eq('invalid email') end + it "updates theme id" do + put api("/users/#{user.id}", admin), params: { theme_id: 5 } + + expect(response).to have_gitlab_http_status(200) + expect(user.reload.theme_id).to eq(5) + end + + it "does not update invalid theme id" do + put api("/users/#{user.id}", admin), params: { theme_id: 50 } + + expect(response).to have_gitlab_http_status(400) + expect(user.reload.theme_id).not_to eq(50) + end + + it "updates color scheme id" do + put api("/users/#{user.id}", admin), params: { color_scheme_id: 5 } + + expect(response).to have_gitlab_http_status(200) + expect(user.reload.color_scheme_id).to eq(5) + end + + it "does not update invalid color scheme id" do + put api("/users/#{user.id}", admin), params: { color_scheme_id: 50 } + + expect(response).to have_gitlab_http_status(400) + expect(user.reload.color_scheme_id).not_to eq(50) + end + context 'when the current user is not an admin' do it "is not available" do expect do diff --git a/spec/services/git/branch_hooks_service_spec.rb b/spec/services/git/branch_hooks_service_spec.rb index 8dc8c804ea5..ae0506ad442 100644 --- a/spec/services/git/branch_hooks_service_spec.rb +++ b/spec/services/git/branch_hooks_service_spec.rb @@ -214,23 +214,23 @@ describe Git::BranchHooksService do end end - describe 'GPG signatures' do + describe 'signatures' do context 'when the commit has a signature' do context 'when the signature is already cached' do before do create(:gpg_signature, commit_sha: commit.id) end - it 'does not queue a CreateGpgSignatureWorker' do - expect(CreateGpgSignatureWorker).not_to receive(:perform_async) + it 'does not queue a CreateCommitSignatureWorker' do + expect(CreateCommitSignatureWorker).not_to receive(:perform_async) service.execute end end context 'when the signature is not yet cached' do - it 'queues a CreateGpgSignatureWorker' do - expect(CreateGpgSignatureWorker).to receive(:perform_async).with([commit.id], project.id) + it 'queues a CreateCommitSignatureWorker' do + expect(CreateCommitSignatureWorker).to receive(:perform_async).with([commit.id], project.id) service.execute end @@ -240,7 +240,7 @@ describe Git::BranchHooksService do .to receive(:shas_with_signatures) .and_return([sample_commit.id, another_sample_commit.id]) - expect(CreateGpgSignatureWorker) + expect(CreateCommitSignatureWorker) .to receive(:perform_async) .with([sample_commit.id, another_sample_commit.id], project.id) @@ -257,8 +257,8 @@ describe Git::BranchHooksService do .and_return([]) end - it 'does not queue a CreateGpgSignatureWorker' do - expect(CreateGpgSignatureWorker) + it 'does not queue a CreateCommitSignatureWorker' do + expect(CreateCommitSignatureWorker) .not_to receive(:perform_async) .with(sample_commit.id, project.id) diff --git a/spec/support/helpers/x509_helpers.rb b/spec/support/helpers/x509_helpers.rb new file mode 100644 index 00000000000..f72b518134c --- /dev/null +++ b/spec/support/helpers/x509_helpers.rb @@ -0,0 +1,208 @@ +# frozen_string_literal: true + +module X509Helpers + module User1 + extend self + + def commit + 'a4df3c87f040f5fa693d4d55a89b6af74e22cb56' + end + + def path + 'gitlab-test' + end + + def trust_cert + <<~TRUSTCERTIFICATE + -----BEGIN CERTIFICATE----- + MIIGVTCCBD2gAwIBAgIEdikH4zANBgkqhkiG9w0BAQsFADCBmTELMAkGA1UEBhMC + REUxDzANBgNVBAgMBkJheWVybjERMA8GA1UEBwwITXVlbmNoZW4xEDAOBgNVBAoM + B1NpZW1lbnMxETAPBgNVBAUTCFpaWlpaWkExMR0wGwYDVQQLDBRTaWVtZW5zIFRy + dXN0IENlbnRlcjEiMCAGA1UEAwwZU2llbWVucyBSb290IENBIFYzLjAgMjAxNjAe + Fw0xNjA2MDYxMzMwNDhaFw0yODA2MDYxMzMwNDhaMIGZMQswCQYDVQQGEwJERTEP + MA0GA1UECAwGQmF5ZXJuMREwDwYDVQQHDAhNdWVuY2hlbjEQMA4GA1UECgwHU2ll + bWVuczERMA8GA1UEBRMIWlpaWlpaQTExHTAbBgNVBAsMFFNpZW1lbnMgVHJ1c3Qg + Q2VudGVyMSIwIAYDVQQDDBlTaWVtZW5zIFJvb3QgQ0EgVjMuMCAyMDE2MIICIjAN + BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp2k2PcfRBu1yeXUxG3UoEDDTFtgF + zGVNIq4j4g6niE7hxZzoferzgC6bK3y+lOQFfNkctFzjq6N+JvH535KnN4vXvNoO + /Rvrn38XtUC8ms2/1MlzvFDMh0Rt1HzemJYsSUXPvj5EMjGVzeQu1/GZhN6XlRrc + SgMSeuwAGN4IX/0QIyxaArxlDZks6zSOA+s9t2PBp6vPZcqA9y4RZLc33nQmdwZg + onEYK55xS1QFY2/zuZGQtB73e69IsrAxP+ZzrivlpbgKkEb1kt0qd7rLkp/HnM9J + IDFc6uo8dAUCA/oR40Yfe2+8hyKoTrFbTvxC2SqxoBolAemZ2rnckuQ1RInbCQNp + pBJJr/Hg78yvIp65gP6mZsyhL6ZLLXjL+ICIUTU86OedkJ7j9o4vdrwBn8AugENy + 8jAMu06k9CFbe7QoEynlRvm5VoYMSBsMqn7lAmuBcuMHdEdXu/qN/ULRLGkx1QRc + gqf7+QszYla8QEaTtxQKWfdAU0Fyg0ROagrBtFjuDjsMeLK6LM17K3FFM3pghISj + o4A8+y2fSbKKnMvU1z3Zey6vnGSwZKOxMJy5/aWuERbegQ07iH0jaA7S/gKZhOKO + uDHD9qOBYfKou6wC+xdWyPGFPOq8BQRkWrSEeQW9FxhyYhhcCdcRh+hpZ4eHgRLM + KkiFrljndwyB4eUCAwEAAaOBojCBnzAfBgNVHSMEGDAWgBRwbaBQ7KnQLGedGRX+ + /QRzNcPi1DAPBgNVHRMBAf8EBTADAQH/MDwGA1UdIAQ1MDMwMQYEVR0gADApMCcG + CCsGAQUFBwIBFhtodHRwOi8vd3d3LnNpZW1lbnMuY29tL3BraS8wDgYDVR0PAQH/ + BAQDAgEGMB0GA1UdDgQWBBRwbaBQ7KnQLGedGRX+/QRzNcPi1DANBgkqhkiG9w0B + AQsFAAOCAgEAHAxI694Yl16uKvWUdGDoglYLXmTxkVHOSci3TxzdEsAJ6WEf7kbj + 6zSQxGcAOz7nvto80rOZzlCluoO5K5fD7a4nEKl+tuBPrgtcEE8nkspPJF6DwjHQ + Lmh219YxktZ1D7egLaRCGvxbPjkb3Wuh4vLqzZHr8twcauMxMyqRTN5F2+F43MY0 + AeBIb9QIMYsxxLBxsSeg4aajGwhdj5FmDFUFbGlyIjd0FfnXxvMuRtWpUWOu4Tya + kA0AX/q6uM/L9SFIwmzTO7+2AHW/m/HrCmWb6R4VYWAgppp+jhUViW5l1uLB3i4m + 5IaJHZilU/DwQ5FnkuP2xqLvZ7AF3uXBlldOAbE1327uGIhYgp40Oi7PIHH+vgwg + JOXQJ3SMwEzYmxCNsyLKAJb2Gs1IpwEpz7lpitl7i/DeUlPZSAo+1SLzc7P35muX + ukCeh1vR7LJdCeYQpDpKeUYjKaNXr2/rZlMFmOGXLBKQvTNcI2I5WTIbVQ1sxhWN + 0FS+INH6jUypiwh0WH2R1Bo0HY3Lq4zJJ3Ct/12ocQ78+JfENXI8glOs3H07jyng + afEj0ba23cn4HnV8s4T0jt8KZYlNkSNlSJ5kgTaZjmdLbTbt24OO4f3WNRrINwKC + VzrN1ydSBGHNOsb/muR5axK/dHN2TEycRJPO6kSaVclLhMTxEmhRBUE= + -----END CERTIFICATE----- + TRUSTCERTIFICATE + end + + def signed_commit_signature + <<~SIGNATURE + -----BEGIN SIGNED MESSAGE----- + MIISUgYJKoZIhvcNAQcCoIISQzCCEj8CAQExDTALBglghkgBZQMEAgEwCwYJKoZI + hvcNAQcBoIIP3TCCB2kwggVRoAMCAQICBGvn1/4wDQYJKoZIhvcNAQELBQAwgZ8x + CzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCYXllcm4xETAPBgNVBAcMCE11ZW5jaGVu + MRAwDgYDVQQKDAdTaWVtZW5zMREwDwYDVQQFEwhaWlpaWlpBMjEdMBsGA1UECwwU + U2llbWVucyBUcnVzdCBDZW50ZXIxKDAmBgNVBAMMH1NpZW1lbnMgSXNzdWluZyBD + QSBFRSBBdXRoIDIwMTYwHhcNMTcwMjAzMDY1MzUyWhcNMjAwMjAzMDY1MzUyWjBb + MREwDwYDVQQFEwhaMDAwTldESDEOMAwGA1UEKgwFUm9nZXIxDjAMBgNVBAQMBU1l + aWVyMRAwDgYDVQQKDAdTaWVtZW5zMRQwEgYDVQQDDAtNZWllciBSb2dlcjCCASIw + DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIpqbpRAtn+vetgVb+APuoVOytZx + TmfWovp22nsmJQwE8ZgrJihRjIez0wjD3cvSREvWUXsvbiyxrSHmmwycRCV9YGi1 + Y9vaYRKOrWhT64Xv6wq6oq8VoA5J3z6V5P6Tkj7g9Q3OskRuSbhFQY89VUdsea+N + mcv/XrwtQR0SekfSZw9k0LhbauE69SWRV26O03raengjecbbkS+GTlP30/CqPzzQ + 4Ac2TmmVF7RlkGRB05mJqHS+nDK7Lmcr7jD0e92YW+v8Lft4Qu3MpFTYVa7zk712 + 5xWAgedyOaJb6TpJEz8KRX8v3i0PilQnuKAqZFkLjNcydOox0AtYRW1P2iMCAwEA + AaOCAu4wggLqMB0GA1UdDgQWBBTsALUoAlzTpaGrwqE0gYSqv5vP+DBDBgNVHREE + PDA6oCMGCisGAQQBgjcUAgOgFQwTci5tZWllckBzaWVtZW5zLmNvbYETci5tZWll + ckBzaWVtZW5zLmNvbTAOBgNVHQ8BAf8EBAMCB4AwKQYDVR0lBCIwIAYIKwYBBQUH + AwIGCCsGAQUFBwMEBgorBgEEAYI3FAICMIHKBgNVHR8EgcIwgb8wgbyggbmggbaG + Jmh0dHA6Ly9jaC5zaWVtZW5zLmNvbS9wa2k/WlpaWlpaQTIuY3JshkFsZGFwOi8v + Y2wuc2llbWVucy5uZXQvQ049WlpaWlpaQTIsTD1QS0k/Y2VydGlmaWNhdGVSZXZv + Y2F0aW9uTGlzdIZJbGRhcDovL2NsLnNpZW1lbnMuY29tL0NOPVpaWlpaWkEyLG89 + VHJ1c3RjZW50ZXI/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDBFBgNVHSAEPjA8 + MDoGDSsGAQQBoWkHAgIDAQEwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3dy5zaWVt + ZW5zLmNvbS9wa2kvMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUvb0qQyI9SEpX + fpgxF6lwne6fqJkwggEEBggrBgEFBQcBAQSB9zCB9DAyBggrBgEFBQcwAoYmaHR0 + cDovL2FoLnNpZW1lbnMuY29tL3BraT9aWlpaWlpBMi5jcnQwQQYIKwYBBQUHMAKG + NWxkYXA6Ly9hbC5zaWVtZW5zLm5ldC9DTj1aWlpaWlpBMixMPVBLST9jQUNlcnRp + ZmljYXRlMEkGCCsGAQUFBzAChj1sZGFwOi8vYWwuc2llbWVucy5jb20vQ049Wlpa + WlpaQTIsbz1UcnVzdGNlbnRlcj9jQUNlcnRpZmljYXRlMDAGCCsGAQUFBzABhiRo + dHRwOi8vb2NzcC5wa2ktc2VydmljZXMuc2llbWVucy5jb20wDQYJKoZIhvcNAQEL + BQADggIBAFY2sbX8DKjKlp0OdH+7Ak21ZdRr6p6JIXzQShWpuFr3wYTpM47+WYVe + arBekf8eS08feM+TWw6FHt/VNMpn5fLr20jHn7h+j3ClerAxQbx8J6BxhwJ/4DMy + 0cCdbe/fpfJyD/8TGdjnxwAgoq9iPuy1ueVnevygnLcuq1+se6EWJm9v1zrwB0LH + rE4/NaSCi06+KGg0D9yiigma9yErRZCiaFvqYXUEl7iGpu2OM9o38gZfGzkKaPtQ + e9BzRs6ndmvNpQQGLXvOlHn6DIsOuBHJp66A+wumRO2AC8rs1rc4NAIjCFRrz8k1 + kzb+ibFiTklWG69+At5/nb06BO/0ER4U18sSpmvOsFKNKPXzLkAn8O8ZzB+8afxy + egiIJFxYaqoJcQq3CCv8Xp7tp6I+ojr1ui0jK0yqJq6QfgS8FCXIJ+EErNYuoerx + ba6amD83e524sdMhCfD5dw6IeEY7LUl465ifUm+v5W3jStfa+0cQXnLZNGsC85nP + Lw5cXVIE3LfoSO3kWH45MfcX32fuqmyP2N3k+/+IOfUpSdT1iR1pEu0g/mow7lGj + CZngjmMpoto/Qi3l/n1KPWfmB09FZlUhHcGsHbK8+mrkqpv6HW3tKDSorah98aLM + Wvu1IXTrU9fOyBqt92i0e5buH+/9NHia0i6k79kwQy5wu6Q21GgUMIIIbDCCBlSg + AwIBAgIEL4jNizANBgkqhkiG9w0BAQsFADCBmTELMAkGA1UEBhMCREUxDzANBgNV + BAgMBkJheWVybjERMA8GA1UEBwwITXVlbmNoZW4xEDAOBgNVBAoMB1NpZW1lbnMx + ETAPBgNVBAUTCFpaWlpaWkExMR0wGwYDVQQLDBRTaWVtZW5zIFRydXN0IENlbnRl + cjEiMCAGA1UEAwwZU2llbWVucyBSb290IENBIFYzLjAgMjAxNjAeFw0xNjA3MjAx + MzA5MDhaFw0yMjA3MjAxMzA5MDhaMIGfMQswCQYDVQQGEwJERTEPMA0GA1UECAwG + QmF5ZXJuMREwDwYDVQQHDAhNdWVuY2hlbjEQMA4GA1UECgwHU2llbWVuczERMA8G + A1UEBRMIWlpaWlpaQTIxHTAbBgNVBAsMFFNpZW1lbnMgVHJ1c3QgQ2VudGVyMSgw + JgYDVQQDDB9TaWVtZW5zIElzc3VpbmcgQ0EgRUUgQXV0aCAyMDE2MIICIjANBgkq + hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAy1aUq88DjZYPge0vZnAr3KJHmMi0o5mp + hy54Xr592Vtf8u/B3TCyD+iGCYANPYUq4sG18qXcVxGadz7zeEm6RI7jKKl3URAv + zFGiYForZE0JKxwo956T/diLLpH1vHEQDbp8AjNK7aGoltZnm/Jn6IVQy9iBY0SE + lRIBhUlppS4/J2PHtKEvQVYJfkAwTtHuGpvPaesoJ8bHA0KhEZ4+/kIYQebaNDf0 + ltTmXd4Z8zeUhE25d9MzoFnQUg+F01ewMfc0OsEFheKWP6dmo0MSLWARXxjI3K2R + THtJU5hxjb/+SA2wlfpqwNIAkTECDBfqYxHReAT8PeezvzEkNZ9RrXl9qj0Cm2iZ + AjY1SL+asuxrGvFwEW/ZKJ2ARY/ot1cHh/I79srzh/jFieShVHbT6s6fyKXmkUjB + OEnybUKUqcvNuOXnwEiJ/9jKT5UVBWTDxbEQucAarVNFBEf557o9ievbT+VAZKZ8 + F4tJge6jl2y19eppflresr7Xui9wekK2LYcLOF3X/MOCFq/9VyQDyE7X9KNGtEx7 + 4V6J2QpbbRJryvavh3b0eQEtqDc65eiEaP8awqOErN8EEYh7Gdx4Um3QFcm1TBhk + ZTdQdLlWv4LvIBnXiBEWRczQYEIm5wv5ZkyPwdL39Xwc72esPPBu8FtQFVcQlRdG + I2t5Ywefq48CAwEAAaOCArIwggKuMIIBBQYIKwYBBQUHAQEEgfgwgfUwQQYIKwYB + BQUHMAKGNWxkYXA6Ly9hbC5zaWVtZW5zLm5ldC9DTj1aWlpaWlpBMSxMPVBLST9j + QUNlcnRpZmljYXRlMDIGCCsGAQUFBzAChiZodHRwOi8vYWguc2llbWVucy5jb20v + cGtpP1paWlpaWkExLmNydDBKBggrBgEFBQcwAoY+bGRhcDovL2FsLnNpZW1lbnMu + Y29tL3VpZD1aWlpaWlpBMSxvPVRydXN0Y2VudGVyP2NBQ2VydGlmaWNhdGUwMAYI + KwYBBQUHMAGGJGh0dHA6Ly9vY3NwLnBraS1zZXJ2aWNlcy5zaWVtZW5zLmNvbTAf + BgNVHSMEGDAWgBRwbaBQ7KnQLGedGRX+/QRzNcPi1DASBgNVHRMBAf8ECDAGAQH/ + AgEAMEAGA1UdIAQ5MDcwNQYIKwYBBAGhaQcwKTAnBggrBgEFBQcCARYbaHR0cDov + L3d3dy5zaWVtZW5zLmNvbS9wa2kvMIHHBgNVHR8Egb8wgbwwgbmggbaggbOGP2xk + YXA6Ly9jbC5zaWVtZW5zLm5ldC9DTj1aWlpaWlpBMSxMPVBLST9hdXRob3JpdHlS + ZXZvY2F0aW9uTGlzdIYmaHR0cDovL2NoLnNpZW1lbnMuY29tL3BraT9aWlpaWlpB + MS5jcmyGSGxkYXA6Ly9jbC5zaWVtZW5zLmNvbS91aWQ9WlpaWlpaQTEsbz1UcnVz + dGNlbnRlcj9hdXRob3JpdHlSZXZvY2F0aW9uTGlzdDAzBgNVHSUELDAqBggrBgEF + BQcDAgYIKwYBBQUHAwQGCisGAQQBgjcUAgIGCCsGAQUFBwMJMA4GA1UdDwEB/wQE + AwIBBjAdBgNVHQ4EFgQUvb0qQyI9SEpXfpgxF6lwne6fqJkwDQYJKoZIhvcNAQEL + BQADggIBAEQB0qDUmU8rX9KVJA/0zxJUmIeE9zeldih8TKrf4UNzS1+2Cqn4agO7 + MxRG1d52/pL4uKenffwwYy2dP912PwLjCDOL7jvojjQKx/qpVUXF7XWsg8hAQec3 + 7Ras/jGPcPQ3OehbkcKcmXI4MrF0Haqo3q1n29gjlJ0fGn2fF1/CBnybPuODAjWG + o9mZodXfz0woGSxkftC6nTmAV2GCvIU+j5hNKpzEzo8c1KwLVeXtB4PAqioRW1BX + Ngjc7HQbvX/C39RnpOM3RdITw2KKXFxeKBMXdiDuFz/2CzO8HxKH9EVWEcSRbTnd + E5iEB4CZzcvfzl9X5AwrKkiIziOiEoiv21ooWeFWfR9V2dgYIE7G1TFwsQ4p0/w5 + xBHSzqP8TCJp1MQTw42/t8uUXoFEGqk5FKQWoIaFf7N//FLAn8r+7vxNhF5s+tMl + VsdKnXn3q8THB3JSnbb/AWGL9rjPK3vh2d3c0I5cWuKXexPLp74ynl2XUbiOXKE7 + XPUZ9qgK0G9JrrFMm4x1aID9Y9jqYeEz6krYjdFHo5BOVGso6SqWVJE48TxJ5KVv + FUb4OxhOAw118Tco0XA7H1G3c2/AKJvIku3cRuj8eLe/cpKqUqQl8uikIZs7POaO + +9eJsOnNPmUiwumJgwAo3Ka4ALteKZLbGmKvuo/2ueKCQ29F5rnOMYICOzCCAjcC + AQEwgagwgZ8xCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCYXllcm4xETAPBgNVBAcM + CE11ZW5jaGVuMRAwDgYDVQQKDAdTaWVtZW5zMREwDwYDVQQFEwhaWlpaWlpBMjEd + MBsGA1UECwwUU2llbWVucyBUcnVzdCBDZW50ZXIxKDAmBgNVBAMMH1NpZW1lbnMg + SXNzdWluZyBDQSBFRSBBdXRoIDIwMTYCBGvn1/4wCwYJYIZIAWUDBAIBoGkwHAYJ + KoZIhvcNAQkFMQ8XDTE5MDYyMDEwNDIwNlowLwYJKoZIhvcNAQkEMSIEIHPHp00z + IZ93dAl/uwOnixzuAtf1fUTyxFFaq/5yzc+0MBgGCSqGSIb3DQEJAzELBgkqhkiG + 9w0BBwEwCwYJKoZIhvcNAQEBBIIBAD8Or5F/A/vpeNPv1YOrGzTrMU5pbn6o8t2+ + Hqn+hAdjbD26HqjYQN/nyXNBpgXiV4P5vEVNVpmViAAXGsWKM3BJx7GdH/uUwDnj + upvoViXYtzQ92UC2Xzqo7uOg2ryMbDIFNfLosvy4a7NfDLYoMsVYrgOKpDrfOLsS + 1VNUjlyftm7vKigkJLrPIEmXrZSVEqsdKvFhcSxS55lm0lVd/fTCAi7TXR2FZWbc + TrsTrZx2YdIJDwN04szzBjnQ7yJ4jBLYz1GMBe22xDD10UA4XdBYK07rkcabrv/t + kUMI7uN/KeiKPeSvWCn3AUqH6TIFa9WU+tI4U2A2BsUMn6Bq9TY= + -----END SIGNED MESSAGE----- + SIGNATURE + end + + def signed_commit_base_data + <<~SIGNEDDATA + tree 84c167013d2ee86e8a88ac6011df0b178d261a23 + parent e63f41fe459e62e1228fcef60d7189127aeba95a + author Roger Meier 1561027326 +0200 + committer Roger Meier 1561027326 +0200 + + feat: add a smime signed commit + SIGNEDDATA + end + + def certificate_crl + 'http://ch.siemens.com/pki?ZZZZZZA2.crl' + end + + def certificate_serial + 1810356222 + end + + def certificate_subject_key_identifier + 'EC:00:B5:28:02:5C:D3:A5:A1:AB:C2:A1:34:81:84:AA:BF:9B:CF:F8' + end + + def issuer_subject_key_identifier + 'BD:BD:2A:43:22:3D:48:4A:57:7E:98:31:17:A9:70:9D:EE:9F:A8:99' + end + + def certificate_email + 'r.meier@siemens.com' + end + + def certificate_issuer + 'CN=Siemens Issuing CA EE Auth 2016,OU=Siemens Trust Center,serialNumber=ZZZZZZA2,O=Siemens,L=Muenchen,ST=Bayern,C=DE' + end + + def certificate_subject + 'CN=Meier Roger,O=Siemens,SN=Meier,GN=Roger,serialNumber=Z000NWDH' + end + + def names + ['Roger Meier'] + end + + def emails + ['r.meier@siemens.com'] + end + end +end diff --git a/spec/workers/create_gpg_signature_worker_spec.rb b/spec/workers/create_commit_signature_worker_spec.rb similarity index 56% rename from spec/workers/create_gpg_signature_worker_spec.rb rename to spec/workers/create_commit_signature_worker_spec.rb index 2504a6474db..d7235fcd907 100644 --- a/spec/workers/create_gpg_signature_worker_spec.rb +++ b/spec/workers/create_commit_signature_worker_spec.rb @@ -2,13 +2,14 @@ require 'spec_helper' -describe CreateGpgSignatureWorker do +describe CreateCommitSignatureWorker do let(:project) { create(:project, :repository) } let(:commits) { project.repository.commits('HEAD', limit: 3).commits } let(:commit_shas) { commits.map(&:id) } let(:gpg_commit) { instance_double(Gitlab::Gpg::Commit) } + let(:x509_commit) { instance_double(Gitlab::X509::Commit) } - context 'when GpgKey is found' do + context 'when a signature is found' do before do allow(Project).to receive(:find_by).with(id: project.id).and_return(project) allow(project).to receive(:commits_by).with(oids: commit_shas).and_return(commits) @@ -18,6 +19,7 @@ describe CreateGpgSignatureWorker do it 'calls Gitlab::Gpg::Commit#signature' do commits.each do |commit| + allow(commit).to receive(:signature_type).and_return(:PGP) expect(Gitlab::Gpg::Commit).to receive(:new).with(commit).and_return(gpg_commit).once end @@ -31,13 +33,46 @@ describe CreateGpgSignatureWorker do allow(Gitlab::Gpg::Commit).to receive(:new).and_return(gpg_commit) allow(Gitlab::Gpg::Commit).to receive(:new).with(commits.first).and_raise(StandardError) + allow(commits[1]).to receive(:signature_type).and_return(:PGP) + allow(commits[2]).to receive(:signature_type).and_return(:PGP) + expect(gpg_commit).to receive(:signature).twice subject end + + it 'calls Gitlab::X509::Commit#signature' do + commits.each do |commit| + allow(commit).to receive(:signature_type).and_return(:X509) + expect(Gitlab::X509::Commit).to receive(:new).with(commit).and_return(x509_commit).once + end + + expect(x509_commit).to receive(:signature).exactly(commits.size).times + + subject + end + + it 'can recover from exception and continue the X509 signature process' do + allow(x509_commit).to receive(:signature) + allow(Gitlab::X509::Commit).to receive(:new).and_return(x509_commit) + allow(Gitlab::X509::Commit).to receive(:new).with(commits.first).and_raise(StandardError) + + allow(commits[1]).to receive(:signature_type).and_return(:X509) + allow(commits[2]).to receive(:signature_type).and_return(:X509) + + expect(x509_commit).to receive(:signature).twice + + subject + end end context 'handles when a string is passed in for the commit SHA' do + before do + allow(Project).to receive(:find_by).with(id: project.id).and_return(project) + allow(project).to receive(:commits_by).with(oids: Array(commit_shas.first)).and_return(commits) + allow(commits.first).to receive(:signature_type).and_return(:PGP) + end + it 'creates a signature once' do allow(Gitlab::Gpg::Commit).to receive(:new).with(commits.first).and_return(gpg_commit) @@ -67,5 +102,11 @@ describe CreateGpgSignatureWorker do described_class.new.perform(commit_shas, nonexisting_project_id) end + + it 'does not call Gitlab::X509::Commit#signature' do + expect_any_instance_of(Gitlab::X509::Commit).not_to receive(:signature) + + described_class.new.perform(commit_shas, nonexisting_project_id) + end end end diff --git a/vendor/elastic_stack/values.yaml b/vendor/elastic_stack/values.yaml index ccbff1ab38d..8b71e5b2c2c 100644 --- a/vendor/elastic_stack/values.yaml +++ b/vendor/elastic_stack/values.yaml @@ -48,7 +48,7 @@ elasticsearch-curator: 1: action: delete_indices description: >- - Delete indices older than 15 days (based on index name), for filebeat- + Delete indices older than 30 days (based on index name), for filebeat- prefixed indices. Ignore the error if the filter does not result in an actionable list of indices (ignore_empty_list) and exit cleanly. options: @@ -62,7 +62,7 @@ elasticsearch-curator: direction: older timestring: '%Y.%m.%d' unit: days - unit_count: 15 + unit_count: 30 elasticsearch-exporter: enabled: false