Commit 9be8fa50 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into 4310-new-components-rows

* master: (54 commits)
  Count of verified wikis is incorrect on Geo secondary
  Remove out-of-context change.
  Resolve CE/EE conflicts related to pipeline seeds
  Document mutual TLS values on settings form
  Validate all credentials used for Mutual TLS
  Document TLS auth for external authorization
  TLS Client auth for external authorization service
  Add fields for credentials for external auth TLS
  Fix LDAP login without user in DB
  Fix unapproved unassigned MR email erroring out
  Put forms from expandable sections into fieldset to ensure proper animation
  Add CHANGELOG
  Perform the repository verification per shard on a secondary node
  Remove gray background from app setting expandable sections
  Improve application settings page according to feedback
  Improves User#owned_projects query performance
  Add test for new README.md in docs
  remove some lint
  personal snippets will now comply with `background_upload`
  Send notification emails when push to a merge request
  ...
parents 866a7f49 a73d2f3f
......@@ -49,7 +49,7 @@ gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-oauth2-generic', '~> 0.2.2'
gem 'omniauth-saml', '~> 1.10'
gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.4'
gem 'omniauth_crowd', '~> 2.2.0'
gem 'omniauth-authentiq', '~> 0.3.1'
gem 'rack-oauth2', '~> 1.2.1'
......
......@@ -547,7 +547,7 @@ GEM
nokogiri (1.8.2)
mini_portile2 (~> 2.3.0)
numerizer (0.1.1)
oauth (0.5.1)
oauth (0.5.4)
oauth2 (1.4.0)
faraday (>= 0.8, < 0.13)
jwt (~> 1.0)
......@@ -602,9 +602,9 @@ GEM
ruby-saml (~> 1.7)
omniauth-shibboleth (1.2.1)
omniauth (>= 1.0.0)
omniauth-twitter (1.2.1)
json (~> 1.3)
omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1)
rack
omniauth_crowd (2.2.3)
activesupport
nokogiri (>= 1.4.4)
......@@ -1157,7 +1157,7 @@ DEPENDENCIES
omniauth-oauth2-generic (~> 0.2.2)
omniauth-saml (~> 1.10)
omniauth-shibboleth (~> 1.2.0)
omniauth-twitter (~> 1.2.0)
omniauth-twitter (~> 1.4)
omniauth_crowd (~> 2.2.0)
org-ruby (~> 0.9.12)
peek (~> 1.0.1)
......
......@@ -11,6 +11,14 @@ module Emails
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end
def push_to_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil, new_commits: [], existing_commits: [])
setup_merge_request_mail(merge_request_id, recipient_id)
@new_commits = new_commits
@existing_commits = existing_commits
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end
def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id)
......
......@@ -175,7 +175,7 @@ class Commit
if safe_message.blank?
no_commit_message
else
safe_message.split("\n", 2).first
safe_message.split(/[\r\n]/, 2).first
end
end
......
......@@ -48,7 +48,7 @@ class NotificationRecipient
when :custom
custom_enabled? || %i[participating mention].include?(@type)
when :watch, :participating
!excluded_watcher_action?
!action_excluded?
when :mention
@type == :mention
else
......@@ -96,13 +96,22 @@ class NotificationRecipient
end
end
def action_excluded?
excluded_watcher_action? || excluded_participating_action?
end
def excluded_watcher_action?
return false unless @custom_action
return false if notification_level == :custom
return false unless @custom_action && notification_level == :watch
NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(@custom_action)
end
def excluded_participating_action?
return false unless @custom_action && notification_level == :participating
NotificationSetting::EXCLUDED_PARTICIPATING_EVENTS.include?(@custom_action)
end
private
def read_ability
......
......@@ -33,6 +33,7 @@ class NotificationSetting < ActiveRecord::Base
:close_issue,
:reassign_issue,
:new_merge_request,
:push_to_merge_request,
:reopen_merge_request,
:close_merge_request,
:reassign_merge_request,
......@@ -41,10 +42,14 @@ class NotificationSetting < ActiveRecord::Base
:success_pipeline
].freeze
EXCLUDED_WATCHER_EVENTS = [
EXCLUDED_PARTICIPATING_EVENTS = [
:success_pipeline
].freeze
EXCLUDED_WATCHER_EVENTS = [
:push_to_merge_request
].push(*EXCLUDED_PARTICIPATING_EVENTS).freeze
def self.find_or_create_for(source)
setting = find_or_initialize_by(source: source)
......
......@@ -641,9 +641,7 @@ class User < ActiveRecord::Base
end
def owned_projects
@owned_projects ||=
Project.where('namespace_id IN (?) OR namespace_id = ?',
owned_groups.select(:id), namespace.id).joins(:namespace)
@owned_projects ||= Project.from("(#{owned_projects_union.to_sql}) AS projects")
end
# Returns projects which user can admin issues on (for example to move an issue to that project).
......@@ -1218,6 +1216,15 @@ class User < ActiveRecord::Base
private
def owned_projects_union
Gitlab::SQL::Union.new([
Project.where(namespace: namespace),
Project.joins(:project_authorizations)
.where("projects.namespace_id <> ?", namespace.id)
.where(project_authorizations: { user_id: id, access_level: Gitlab::Access::OWNER })
], remove_duplicates: false)
end
def ci_projects_union
scope = { access_level: [Gitlab::Access::MASTER, Gitlab::Access::OWNER] }
groups = groups_projects.where(members: scope)
......
......@@ -21,7 +21,7 @@ module MergeRequests
comment_mr_branch_presence_changed
end
comment_mr_with_commits
notify_about_push
mark_mr_as_wip_from_commits
execute_mr_web_hooks
reset_approvals_for_merge_requests
......@@ -158,8 +158,8 @@ module MergeRequests
end
end
# Add comment about pushing new commits to merge requests
def comment_mr_with_commits
# Add comment about pushing new commits to merge requests and send nofitication emails
def notify_about_push
return unless @commits.present?
merge_requests_for_source_branch.each do |merge_request|
......@@ -172,6 +172,8 @@ module MergeRequests
SystemNoteService.add_commits(merge_request, merge_request.project,
@current_user, new_commits,
existing_commits, @oldrev)
notification_service.push_to_merge_request(merge_request, @current_user, new_commits: new_commits, existing_commits: existing_commits)
end
end
......
......@@ -116,6 +116,16 @@ class NotificationService
new_resource_email(merge_request, :new_merge_request_email)
end
def push_to_merge_request(merge_request, current_user, new_commits: [], existing_commits: [])
new_commits = new_commits.map { |c| { short_id: c.short_id, title: c.title } }
existing_commits = existing_commits.map { |c| { short_id: c.short_id, title: c.title } }
recipients = NotificationRecipientService.build_recipients(merge_request, current_user, action: "push_to")
recipients.each do |recipient|
mailer.send(:push_to_merge_request_email, recipient.user.id, merge_request.id, current_user.id, recipient.reason, new_commits: new_commits, existing_commits: existing_commits).deliver_later
end
end
# When merge request text is updated, we should send an email to:
#
# * newly mentioned project team members with notification level higher than Participating
......
......@@ -3,6 +3,7 @@
- if License.feature_available?(:repository_mirrors)
= render partial: 'repository_mirrors_form', locals: { f: f }
%fieldset
%legend Continuous Integration and Deployment
.form-group
......
......@@ -42,10 +42,6 @@
= link_to "(?)", help_page_path("integration/bitbucket")
and GitLab.com
= link_to "(?)", help_page_path("integration/gitlab")
.form-group
= f.label :default_branch_protection, class: 'control-label col-sm-2'
.col-sm-10
= f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control'
-# EE-only
- if ldap_enabled?
......
%h3
New commits were pushed to the merge request
= link_to(@merge_request.to_reference, project_merge_request_url(@merge_request.target_project, @merge_request))
by #{@current_user.name}
- if @existing_commits.any?
- count = @existing_commits.size
%ul
%li
- if count.one?
- commit_id = @existing_commits.first[:short_id]
= link_to(commit_id, project_commit_url(@merge_request.target_project, commit_id))
- else
= link_to(project_compare_url(@merge_request.target_project, from: @existing_commits.first[:short_id], to: @existing_commits.last[:short_id])) do
#{@existing_commits.first[:short_id]}...#{@existing_commits.last[:short_id]}
= precede '&nbsp;- ' do
- commits_text = "#{count} commit".pluralize(count)
#{commits_text} from branch `#{@merge_request.target_branch}`
- if @new_commits.any?
%ul
- @new_commits.each do |commit|
%li
= link_to(commit[:short_id], project_commit_url(@merge_request.target_project, commit[:short_id]))
= precede ' - ' do
#{commit[:title]}
New commits were pushed to the merge request #{@merge_request.to_reference} by #{@current_user.name}
\
#{url_for(project_merge_request_url(@merge_request.target_project, @merge_request))}
\
- if @existing_commits.any?
- count = @existing_commits.size
- commits_id = count.one? ? @existing_commits.first[:short_id] : "#{@existing_commits.first[:short_id]}...#{@existing_commits.last[:short_id]}"
- commits_text = "#{count} commit".pluralize(count)
* #{commits_id} - #{commits_text} from branch `#{@merge_request.target_branch}`
\
- @new_commits.each do |commit|
* #{commit[:short_id]} - #{raw commit[:title]}
......@@ -126,14 +126,18 @@
- cronjob:geo_repository_verification_primary_batch
- cronjob:geo_repository_verification_secondary_scheduler
- cronjob:geo_sidekiq_cron_config
- cronjob:geo_scheduler_per_shard_scheduler
- cronjob:geo_scheduler_primary_per_shard_scheduler
- cronjob:geo_scheduler_secondary_per_shard_scheduler
- cronjob:geo_repository_verification_secondary_shard
- cronjob:historical_data
- cronjob:ldap_all_groups_sync
- cronjob:ldap_sync
- cronjob:update_all_mirrors
- geo:geo_scheduler_base
- geo:geo_scheduler_primary
- geo:geo_scheduler_secondary
- geo:geo_scheduler_scheduler
- geo:geo_scheduler_primary_scheduler
- geo:geo_scheduler_secondary_scheduler
- geo:geo_file_download
- geo:geo_file_removal
- geo:geo_hashed_storage_attachments_migration
......
---
title: Send notification emails when push to a merge request
merge_request: 7610
author: YarNayar
type: feature
---
title: Improves the performance of projects list page
merge_request: 17934
author:
type: performance
---
title: 'Cloning a repository over HTTPS with LDAP credentials causes a HTTP 401 Access denied'
merge_request: !17988
author: Horatiu Eugen Vlad
type: fixed
class AddPushToMergeRequestToNotificationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :notification_settings, :push_to_merge_request, :boolean
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180320182229) do
ActiveRecord::Schema.define(version: 20180323150945) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -186,6 +186,11 @@ ActiveRecord::Schema.define(version: 20180320182229) do
t.boolean "pages_domain_verification_enabled", default: true, null: false
t.float "external_authorization_service_timeout", default: 0.5, null: false
t.boolean "allow_local_requests_from_hooks_and_services", default: false, null: false
t.text "external_auth_client_cert"
t.text "encrypted_external_auth_client_key"
t.string "encrypted_external_auth_client_key_iv"
t.string "encrypted_external_auth_client_key_pass"
t.string "encrypted_external_auth_client_key_pass_iv"
end
create_table "approvals", force: :cascade do |t|
......@@ -1708,6 +1713,7 @@ ActiveRecord::Schema.define(version: 20180320182229) do
t.boolean "merge_merge_request"
t.boolean "failed_pipeline"
t.boolean "success_pipeline"
t.boolean "push_to_merge_request"
end
add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree
......
......@@ -24,6 +24,7 @@ reopen_issue
close_issue
reassign_issue
new_merge_request
push_to_merge_request
reopen_merge_request
close_merge_request
reassign_merge_request
......@@ -75,6 +76,7 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab
| `close_issue` | boolean | no | Enable/disable this notification |
| `reassign_issue` | boolean | no | Enable/disable this notification |
| `new_merge_request` | boolean | no | Enable/disable this notification |
| `push_to_merge_request` | boolean | no | Enable/disable this notification |
| `reopen_merge_request` | boolean | no | Enable/disable this notification |
| `close_merge_request` | boolean | no | Enable/disable this notification |
| `reassign_merge_request` | boolean | no | Enable/disable this notification |
......@@ -141,6 +143,7 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab
| `close_issue` | boolean | no | Enable/disable this notification |
| `reassign_issue` | boolean | no | Enable/disable this notification |
| `new_merge_request` | boolean | no | Enable/disable this notification |
| `push_to_merge_request` | boolean | no | Enable/disable this notification |
| `reopen_merge_request` | boolean | no | Enable/disable this notification |
| `close_merge_request` | boolean | no | Enable/disable this notification |
| `reassign_merge_request` | boolean | no | Enable/disable this notification |
......@@ -164,6 +167,7 @@ Example responses:
"close_issue": false,
"reassign_issue": false,
"new_merge_request": false,
"push_to_merge_request": false,
"reopen_merge_request": false,
"close_merge_request": false,
"reassign_merge_request": false,
......
......@@ -173,7 +173,9 @@ PUT /application/settings
| `external_authorization_service_enabled` | boolean | no | Enable using an external authorization service for accessing projects |
| `external_authorization_service_url` | string | no | URL to which authorization requests will be directed |
| `external_authorization_service_default_label` | string | no | The default classification label to use when requesting authorization and no classification label has been specified on the project |
| `external_authorization_service_timeout` | float | no | The timeout to enforce when performing requests to the external authorization service |
| `external_auth_client_cert` | string | no | The certificate to use to authenticate with the external authorization service |
| `external_auth_client_key` | string | no | Private key for the certificate when authentication is required for the external authorization service, this is encrypted when stored |
| `external_auth_client_key_pass` | string | no | Passphrase to use for the private key when authenticating with the external service this is encrypted when stored |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/application/settings?signup_enabled=false&default_project_visibility=internal
......
......@@ -43,9 +43,21 @@ The available required properties are:
- **External authorization request timeout**: The timeout after which an
authorization request is aborted. When a request times out, access is denied
to the user.
- **Client authentication certificate**: The certificate to use to authenticate
with the external authorization service.
- **Client authentication key**: Private key for the certificate when
authentication is required for the external authorization service, this is
encrypted when stored.
- **Client authentication key password**: Passphrase to use for the private key when authenticating with the external service this is encrypted when stored.
- **Default classification label**: The classification label to use when
requesting authorization if no specific label is defined on the project
When using TLS Authentication with a self signed certificate, the CA certificate
needs to be trused by the openssl installation. When using GitLab installed using
Omnibus, learn to install a custom CA in the
[omnibus documentation][omnibus-ssl-docs]. Alternatively learn where to install
custom certificates using `openssl version -d`.
## How it works
When GitLab requests access, it will send a JSON POST request to the external
......@@ -90,3 +102,5 @@ label defined in the [global settings](#configuration) will be used.
The label will be shown on all project pages in the upper right corner.
![classification label on project page](img/classification_label_on_project_page.png)
[omnibus-ssl-docs]: https://docs.gitlab.com/omnibus/settings/ssl.html
......@@ -67,7 +67,7 @@ Below is the table of events users can be notified of:
### Issue / Merge request events
In all of the below cases, the notification will be sent to:
In most of the below cases, the notification will be sent to:
- Participants:
- the author and assignee of the issue/merge request
- authors of comments on the issue/merge request
......@@ -87,6 +87,7 @@ In all of the below cases, the notification will be sent to:
| Reassign issue | The above, plus the old assignee |
| Reopen issue | |
| New merge request | |
| Push to merge request | Participants and Custom notification level with this event selected |
| Reassign merge request | The above, plus the old assignee |
| Close merge request | |
| Reopen merge request | |
......
......@@ -62,7 +62,7 @@ module Geo
if use_legacy_queries?
legacy_find_verified_wikis
else
find_verified_wikis
fdw_find_verified_wikis
end
relation.count
......@@ -125,10 +125,6 @@ module Geo
Geo::ProjectRegistry.verified_repos
end
def find_verified_wikis
Geo::ProjectRegistry.verified_wikis
end
def find_filtered_failed_project_registries(type = nil)
case type
when 'repository'
......@@ -163,19 +159,7 @@ module Geo
# @return [ActiveRecord::Relation<Geo::ProjectRegistry>]
def fdw_find_enabled_wikis
project_id_matcher =
Geo::Fdw::ProjectFeature.arel_table[:project_id]
.eq(Geo::ProjectRegistry.arel_table[:project_id])
# Only read the IDs of projects with disabled wikis from the remote database
not_exist = Geo::Fdw::ProjectFeature
.where(wiki_access_level: [::ProjectFeature::DISABLED, nil])
.where(project_id_matcher)
.select('1')
.exists
.not
Geo::ProjectRegistry.synced_wikis.where(not_exist)
Geo::ProjectRegistry.synced_wikis.where(fdw_not_disabled_wikis)
end
# @return [ActiveRecord::Relation<Geo::Fdw::Project>]
......@@ -202,6 +186,11 @@ module Geo
).limit(batch_size)
end
# @return [ActiveRecord::Relation<Geo::ProjectRegistry>]
def fdw_find_verified_wikis
Geo::ProjectRegistry.verified_wikis.where(fdw_not_disabled_wikis)
end
def fdw_inner_join_repository_state
local_registry_table
.join(fdw_repository_state_table, Arel::Nodes::InnerJoin)
......@@ -209,6 +198,20 @@ module Geo
.join_sources
end
def fdw_not_disabled_wikis
project_id_matcher =
Geo::Fdw::ProjectFeature.arel_table[:project_id]
.eq(Geo::ProjectRegistry.arel_table[:project_id])
# Only read the IDs of projects with disabled wikis from the remote database
Geo::Fdw::ProjectFeature
.where(wiki_access_level: [::ProjectFeature::DISABLED, nil])
.where(project_id_matcher)
.select('1')
.exists
.not
end
#
# Legacy accessors (non FDW)
#
......@@ -264,9 +267,13 @@ module Geo
legacy_find_project_registries(Geo::ProjectRegistry.verified_repos)
end
# @return [ActiveRecord::Relation<Geo::ProjectRegistry>] list of verified projects
# @return [ActiveRecord::Relation<Geo::ProjectRegistry>] list of verified wikis
def legacy_find_verified_wikis
legacy_find_project_registries(Geo::ProjectRegistry.verified_wikis)
legacy_inner_join_registry_ids(
current_node.projects.with_wiki_enabled,
Geo::ProjectRegistry.verified_wikis.pluck(:project_id),
Project
)
end
# @return [ActiveRecord::Relation<Project>] list of synced projects
......
......@@ -19,6 +19,22 @@ module EE
"external authorization checks.")
end
def external_authorization_client_certificate_help_text
_("The X509 Certificate to use when mutual TLS is required to communicate "\
"with the external authorization service. If left blank, the server "\
"certificate is still validated when accessing over HTTPS.")
end
def external_authorization_client_key_help_text
_("The private key to use when a client certificate is provided. This value "\
"is encrypted at rest.")
end
def external_authorization_client_pass_help_text
_("The passphrase required to decrypt the private key. This is optional "\
"and the value is encrypted at rest.")
end
override :visible_attributes
def visible_attributes
super + [
......@@ -57,7 +73,10 @@ module EE
:external_authorization_service_enabled,
:external_authorization_service_url,
:external_authorization_service_default_label,
:external_authorization_service_timeout
:external_authorization_service_timeout,
:external_auth_client_cert,
:external_auth_client_key,
:external_auth_client_key_pass
]
end
......
......@@ -51,6 +51,28 @@ module EE
validates :external_authorization_service_timeout,
numericality: { greater_than: 0, less_than_or_equal_to: 10 },
if: :external_authorization_service_enabled?
validates :external_auth_client_key,
presence: true,
if: -> (setting) { setting.external_auth_client_cert.present? }
validates_with X509CertificateCredentialsValidator,
certificate: :external_auth_client_cert,
pkey: :external_auth_client_key,
pass: :external_auth_client_key_pass,
if: -> (setting) { setting.external_auth_client_cert.present? }
attr_encrypted :external_auth_client_key,
mode: :per_attribute_iv,
key: ::Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-gcm',
encode: true
attr_encrypted :external_auth_client_key_pass,
mode: :per_attribute_iv,
key: ::Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-gcm',
encode: true
end
module ClassMethods
......
# X509CertificateCredentialsValidator
#
# Custom validator to check if certificate-attribute was signed using the
# private key stored in an attrebute.
#
# This can be used as an `ActiveModel::Validator` as follows:
#
# validates_with X509CertificateCredentialsValidator,
# certificate: :client_certificate,
# pkey: :decrypted_private_key,
# pass: :decrypted_passphrase
#
#
# Required attributes:
# - certificate: The name of the accessor that returns the certificate to check
# - pkey: The name of the accessor that returns the private key
# Optional:
# - pass: The name of the accessor that returns the passphrase to decrypt the
# private key
class X509CertificateCredentialsValidator < ActiveModel::Validator
def initialize(*args)
super
# We can't validate if we don't have a private key or certificate attributes
# in which case this validator is useless.
if options[:pkey].nil? || options[:certificate].nil?
raise 'Provide at least `certificate` and `pkey` attribute names'
end
end
def validate(record)
unless certificate = read_certificate(record)
record.errors.add(options[:certificate], _('is not a valid X509 certificate.'))
end
unless private_key = read_private_key(record)
record.errors.add(options[:pkey], _('could not read private key, is the passphrase correct?'))
end
return if private_key.nil? || certificate.nil?
unless certificate.public_key.fingerprint == private_key.public_key.fingerprint
record.errors.add(options[:pkey], _('private key does not match certificate.'))
end
end
private
def read_private_key(record)
OpenSSL::PKey.read(pkey(record).to_s, pass(record).to_s)
rescue ArgumentError
# When the primary key could not be read, an ArgumentError is raised.
# This hapens when the passed key is not valid or the passphrase is incorrect
nil
end
def read_certificate(record)
OpenSSL::X509::Certificate.new(certificate(record).to_s)
rescue OpenSSL::X509::CertificateError
nil
end
# rubocop:disable GitlabSecurity/PublicSend
#
# Allowing `#public_send` here because we don't want the validator to really
# care about the names of the attributes or where they come from.
#
# The credentials are mostly stored encrypted so we need to go through the
# accessors to get the values, `read_attribute` bypasses those.
def certificate(record)
record.public_send(options[:certificate])
end
def pkey(record)
record.public_send(options[:pkey])
end
def pass(record)
return nil unless options[:pass]
record.public_send(options[:pass])
end
# rubocop:enable GitlabSecurity/PublicSend
end
......@@ -23,6 +23,23 @@
= f.number_field :external_authorization_service_timeout, class: 'form-control', min: 0.001, max: 10, step: 0.001
%span.help-block
= external_authorization_timeout_help_text
= f.label :external_auth_client_cert, _('Client authentication certificate'), class: 'control-label col-sm-2'
.col-sm-10
= f.text_area :external_auth_client_cert, class: 'form-control'
%span.help-block
= external_authorization_client_certificate_help_text
.form-group
= f.label :external_auth_client_key, _('Client authentication key'), class: 'control-label col-sm-2'
.col-sm-10
= f.text_area :external_auth_client_key, class: 'form-control'
%span.help-block
= external_authorization_client_key_help_text
.form-group
= f.label :external_auth_client_key_pass, _('Client authentication key password'), class: 'control-label col-sm-2'
.col-sm-10
= f.password_field :external_auth_client_key_pass, class: 'form-control'
%span.help-block
= external_authorization_client_pass_help_text
.form-group
= f.label :external_authorization_service_default_label, _('Default classification label'), class: 'control-label col-sm-2'
.col-sm-10
......
......@@ -121,6 +121,7 @@
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
%a.muted{ href: user_url(@merge_request.author), style: "color:#333333;text-decoration:none;" }
= @merge_request.author.name
- if @merge_request.assignee
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Assignee
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
......
module Geo
class FileDownloadDispatchWorker < Geo::Scheduler::SecondaryWorker
class FileDownloadDispatchWorker < Geo::Scheduler::Secondary::SchedulerWorker
include CronjobQueue
private
......
module Geo
class RepositoryShardSyncWorker < Geo::Scheduler::SecondaryWorker
class RepositoryShardSyncWorker < Geo::Scheduler::Secondary::SchedulerWorker
sidekiq_options retry: false
attr_accessor :shard_name
......
module Geo
class RepositorySyncWorker
include ApplicationWorker
include CronjobQueue
HEALTHY_SHARD_CHECKS = [
Gitlab::HealthChecks::FsShardsCheck,
Gitlab::HealthChecks::GitalyCheck
].freeze
def perform
return unless Gitlab::Geo.geo_database_configured?
return unless Gitlab::Geo.secondary?
shards = selective_sync_filter(healthy_shards)
Gitlab::Geo::ShardHealthCache.update(shards)
shards.each do |shard_name|
class RepositorySyncWorker < Geo::Scheduler::Secondary::PerShardSchedulerWorker
def schedule_job(shard_name)
RepositoryShardSyncWorker.perform_async(shard_name)
end
end
def healthy_shards
# For now, we need to perform both Gitaly and direct filesystem checks to ensure
# the shard is healthy. We take the intersection of the successful checks
# as the healthy shards.
HEALTHY_SHARD_CHECKS.map(&:readiness)
.map { |check_result| check_result.select(&:success) }
.inject(:&)
.map { |check_result| check_result.labels[:shard] }
.compact
.uniq
end
def selective_sync_filter(shards)
return shards unless ::Gitlab::Geo.current_node&.selective_sync_by_shards?
shards & ::Gitlab::Geo.current_node.selective_sync_shards
end
end
end
module Geo
module RepositoryVerification
module Primary
class BatchWorker
include ApplicationWorker
include CronjobQueue
include ::Gitlab::Utils::StrongMemoize
HEALTHY_SHARD_CHECKS = [
Gitlab::HealthChecks::FsShardsCheck,
Gitlab::HealthChecks::GitalyCheck
].freeze
class BatchWorker < Geo::Scheduler::Primary::PerShardSchedulerWorker
def perform
return unless Feature.enabled?('geo_repository_verification')
return unless Gitlab::Geo.primary?
Gitlab::Geo::ShardHealthCache.update(healthy_shards)
healthy_shards.each do |shard_name|
Geo::RepositoryVerification::Primary::ShardWorker.perform_async(shard_name)
end
super
end
def healthy_shards
strong_memoize(:healthy_shards) do
# For now, we need to perform both Gitaly and direct filesystem checks to ensure
# the shard is healthy. We take the intersection of the successful checks
# as the healthy shards.
HEALTHY_SHARD_CHECKS.map(&:readiness)
.map { |check_result| check_result.select(&:success) }
.inject(:&)
.map { |check_result| check_result.labels[:shard] }
.compact
.uniq
end
def schedule_job(shard_name)
Geo::RepositoryVerification::Primary::ShardWorker.perform_async(shard_name)
end
end
end
......
module Geo
module RepositoryVerification
module Primary
class ShardWorker < Geo::Scheduler::PrimaryWorker
class ShardWorker < Geo::Scheduler::Primary::SchedulerWorker
sidekiq_options retry: false
MAX_CAPACITY = 100
......
module Geo
module RepositoryVerification
module Secondary
class SchedulerWorker < Geo::Scheduler::SecondaryWorker
include CronjobQueue
MAX_CAPACITY = 1000
class SchedulerWorker < Geo::Scheduler::Secondary::PerShardSchedulerWorker
def perform
return unless Feature.enabled?('geo_repository_verification')
super
end
private
def max_capacity
MAX_CAPACITY
end
def load_pending_resources
finder.find_registries_to_verify(batch_size: db_retrieve_batch_size)
.pluck(:id)
end
def schedule_job(registry_id)
job_id = Geo::RepositoryVerification::Secondary::SingleWorker.perform_async(registry_id)
{ id: registry_id, job_id: job_id } if job_id
end
def finder
@finder ||= Geo::ProjectRegistryFinder.new
def schedule_job(shard_name)
Geo::RepositoryVerification::Secondary::ShardWorker.perform_async(shard_name)
end
end
end
......
module Geo
module RepositoryVerification
module Secondary
class ShardWorker < Geo::Scheduler::Secondary::SchedulerWorker
include CronjobQueue
MAX_CAPACITY = 1000
attr_accessor :shard_name
def perform(shard_name)
@shard_name = shard_name
return unless Gitlab::Geo::ShardHealthCache.healthy_shard?(shard_name)
super()
end
private
def worker_metadata
{ shard: shard_name }
end
def max_capacity
MAX_CAPACITY
end
def load_pending_resources
finder.find_registries_to_verify(batch_size: db_retrieve_batch_size)
.pluck(:id)
end
def schedule_job(registry_id)
job_id = Geo::RepositoryVerification::Secondary::SingleWorker.perform_async(registry_id)
{ id: registry_id, job_id: job_id } if job_id
end
def finder
@finder ||= Geo::ProjectRegistryFinder.new
end
end
end
end
end
module Geo
module Scheduler
class PerShardSchedulerWorker
include ApplicationWorker
include CronjobQueue
include ::Gitlab::Utils::StrongMemoize
HEALTHY_SHARD_CHECKS = [
Gitlab::HealthChecks::FsShardsCheck,
Gitlab::HealthChecks::GitalyCheck
].freeze
def perform
Gitlab::Geo::ShardHealthCache.update(eligible_shards)
eligible_shards.each do |shard_name|
schedule_job(shard_name)
end
end
def schedule_job(shard_name)
raise NotImplementedError
end
def eligible_shards
healthy_shards
end
def healthy_shards
strong_memoize(:healthy_shards) do
# For now, we need to perform both Gitaly and direct filesystem checks to ensure
# the shard is healthy. We take the intersection of the successful checks
# as the healthy shards.
HEALTHY_SHARD_CHECKS.map(&:readiness)
.map { |check_result| check_result.select(&:success) }
.inject(:&)
.map { |check_result| check_result.labels[:shard] }
.compact
.uniq
end
end
end
end
end
module Geo
module Scheduler
module Primary
class PerShardSchedulerWorker < Geo::Scheduler::PerShardSchedulerWorker
def perform
return unless Gitlab::Geo.primary?
super
end
end
end
end
end
module Geo
module Scheduler
module Primary
class SchedulerWorker < Geo::Scheduler::SchedulerWorker
def perform
return unless Gitlab::Geo.primary?
super
end
end
end
end
end
module Geo
module Scheduler
class PrimaryWorker < Geo::Scheduler::BaseWorker
def perform
return unless Gitlab::Geo.primary?
super
end
end
end
end
module Geo
module Scheduler
class BaseWorker
class SchedulerWorker
include ApplicationWorker
include GeoQueue
include ExclusiveLeaseGuard
......
module Geo
module Scheduler
module Secondary
class PerShardSchedulerWorker < Geo::Scheduler::PerShardSchedulerWorker
def perform
return unless Gitlab::Geo.geo_database_configured?
return unless Gitlab::Geo.secondary?
super
end
def eligible_shards
selective_sync_filter(healthy_shards)
end
def selective_sync_filter(shards)
return shards unless ::Gitlab::Geo.current_node&.selective_sync_by_shards?
shards & ::Gitlab::Geo.current_node.selective_sync_shards
end
end
end
end
end
module Geo
module Scheduler
module Secondary
class SchedulerWorker < Geo::Scheduler::SchedulerWorker
def perform
return unless Gitlab::Geo.geo_database_configured?
return unless Gitlab::Geo.secondary?
super
end
end
end
end
end
module Geo
module Scheduler
class SecondaryWorker < Geo::Scheduler::BaseWorker
def perform
return unless Gitlab::Geo.geo_database_configured?
return unless Gitlab::Geo.secondary?
super
end
end
end
end
---
title: Fix unapproved unassigned merge request emails failing to send
merge_request: 5092
author:
type: fixed
---
title: Fixed incorrect count of verified wikis on a Geo secondary node
merge_request: 5084
author:
type: fixed
---
title: Geo - Perform the repository verification per shard on a secondary node
merge_request: 5068
author:
type: changed
---
title: Authenticate using TLS certificate for requests to external authorization service
merge_request: 5028
author:
type: added
class AddExternalAuthMutualTlsFieldsToProjectSettings < ActiveRecord::Migration
DOWNTIME = false
def change
add_column :application_settings,
:external_auth_client_cert, :text
add_column :application_settings,
:encrypted_external_auth_client_key, :text
add_column :application_settings,
:encrypted_external_auth_client_key_iv, :string
add_column :application_settings,
:encrypted_external_auth_client_key_pass, :string
add_column :application_settings,
:encrypted_external_auth_client_key_pass_iv, :string
end
end
module EE
module Gitlab
module ExternalAuthorization
extend Config
RequestFailed = Class.new(StandardError)
def self.access_allowed?(user, label)
......@@ -26,28 +28,6 @@ module EE
EE::Gitlab::ExternalAuthorization::Access.new(user, label).load!
end
end
def self.enabled?
::Gitlab::CurrentSettings
.current_application_settings
.external_authorization_service_enabled?
end
def self.perform_check?
enabled? && service_url.present?
end
def self.service_url
::Gitlab::CurrentSettings
.current_application_settings
.external_authorization_service_url
end
def self.timeout
::Gitlab::CurrentSettings
.current_application_settings
.external_authorization_service_timeout
end
end
end
end
......@@ -29,7 +29,7 @@ module EE
end
def load_from_service
response = Client.build(@user, @label).request_access
response = Client.new(@user, @label).request_access
@access = response.successful?
@reason = response.reason
@loaded_at = Time.now
......
Excon.defaults[:ssl_verify_peer] = false
module EE
module Gitlab
module ExternalAuthorization
class Client
include Config
REQUEST_HEADERS = {
'Content-Type' => 'application/json',
'Accept' => 'application/json'
}.freeze
def self.build(user, label)
new(
::EE::Gitlab::ExternalAuthorization.service_url,
::EE::Gitlab::ExternalAuthorization.timeout,
user,
label
)
end
def initialize(url, timeout, user, label)
@url, @timeout, @user, @label = url, timeout, user, label
def initialize(user, label)
@user, @label = user, label
end
def request_access
response = Excon.post(
@url,
headers: REQUEST_HEADERS,
body: body.to_json,
connect_timeout: @timeout,
read_timeout: @timeout,
write_timeout: @timeout
service_url,
post_params
)
EE::Gitlab::ExternalAuthorization::Response.new(response)
rescue Excon::Error => e
......@@ -36,6 +27,22 @@ module EE
private
def post_params
params = { headers: REQUEST_HEADERS,
body: body.to_json,
connect_timeout: timeout,
read_timeout: timeout,
write_timeout: timeout }
if has_tls?
params[:client_cert_data] = client_cert
params[:client_key_data] = client_key
params[:client_key_pass] = client_key_pass
end
params
end
def body
@body ||= begin
body = {
......
module EE
module Gitlab
module ExternalAuthorization
module Config
extend self
def timeout
application_settings.external_authorization_service_timeout
end
def service_url
application_settings.external_authorization_service_url
end
def enabled?
application_settings.external_authorization_service_enabled?
end
def perform_check?
enabled? && service_url.present?
end
def client_cert
application_settings.external_auth_client_cert
end
def client_key
application_settings.external_auth_client_key
end
def client_key_pass
application_settings.external_auth_client_key_pass
end
def has_tls?
client_cert.present? && client_key.present?
end
private
def application_settings
::Gitlab::CurrentSettings.current_application_settings
end
end
end
end
end
......@@ -86,7 +86,10 @@ describe Admin::ApplicationSettingsController do
external_authorization_service_enabled: true,
external_authorization_service_url: 'https://custom.service/',
external_authorization_service_default_label: 'default',
external_authorization_service_timeout: 3
external_authorization_service_timeout: 3,
external_auth_client_cert: File.read('ee/spec/fixtures/passphrase_x509_certificate.crt'),
external_auth_client_key: File.read('ee/spec/fixtures/passphrase_x509_certificate_pk.key'),
external_auth_client_key_pass: "5iveL!fe"
}
end
let(:feature) { :external_authorization_service }
......
......@@ -191,6 +191,69 @@ describe Geo::ProjectRegistryFinder, :geo do
end
end
describe '#count_verified_repositories' do
it 'delegates to #find_verified_repositories' do
expect(subject).to receive(:find_verified_repositories).and_call_original
subject.count_verified_repositories
end
it 'counts projects that verified' do
create(:geo_project_registry, :repository_verified, project: project_repository_verified)
create(:geo_project_registry, :repository_verified, project: build(:project))
create(:geo_project_registry, :repository_verification_failed, project: project_repository_verification_failed)
expect(subject.count_verified_repositories).to eq 2
end
context 'with legacy queries' do
before do
allow(subject).to receive(:use_legacy_queries?).and_return(true)
end
it 'delegates to #legacy_find_verified_repositories' do
expect(subject).to receive(:legacy_find_verified_repositories).and_call_original
subject.count_verified_repositories
end
it 'counts projects that verified' do
create(:geo_project_registry, :repository_verified, project: project_repository_verified)
create(:geo_project_registry, :repository_verified, project: build(:project))
create(:geo_project_registry, :repository_verification_failed, project: project_repository_verification_failed)
expect(subject.count_verified_repositories).to eq 2
end
end
end
describe '#count_verified_wikis' do
before do
allow(subject).to receive(:use_legacy_queries?).and_return(true)
end
it 'delegates to #legacy_find_synced_wikis' do
expect(subject).to receive(:legacy_find_verified_wikis).and_call_original
subject.count_verified_wikis
end
it 'counts wikis that verified' do
create(:geo_project_registry, :wiki_verified, project: project_wiki_verified)
create(:geo_project_registry, :wiki_verified, project: build(:project))
create(:geo_project_registry, :wiki_verification_failed, project: project_wiki_verification_failed)
expect(subject.count_verified_wikis).to eq 2
end
it 'does not count disabled wikis' do
create(:geo_project_registry, :wiki_verified, project: project_wiki_verified)
create(:geo_project_registry, :wiki_verified, project: create(:project, :wiki_disabled))
expect(subject.count_verified_wikis).to eq 1
end
end
describe '#count_verification_failed_repositories' do
it 'delegates to #find_verification_failed_project_registries' do
expect(subject).to receive(:find_verification_failed_project_registries).with('repository').and_call_original
......@@ -424,6 +487,17 @@ describe Geo::ProjectRegistryFinder, :geo do
end
end
describe '#fdw_find_verified_wikis' do
it 'does not count disabled wikis' do
expect(subject).to receive(:fdw_find_verified_wikis).and_call_original
create(:geo_project_registry, :wiki_verified, project: project_wiki_verified)
create(:geo_project_registry, :wiki_verified, project: create(:project, :wiki_disabled))
expect(subject.count_verified_wikis).to eq 1
end
end
describe '#find_unsynced_projects' do
it 'delegates to #fdw_find_unsynced_projects' do
expect(subject).to receive(:fdw_find_unsynced_projects).and_call_original
......
-----BEGIN CERTIFICATE-----
MIIEpTCCAo0CAQEwDQYJKoZIhvcNAQEFBQAwFDESMBAGA1UEAwwJYXV0aG9yaXR5
MB4XDTE4MDMyMzE0MDIwOFoXDTE5MDMyMzE0MDIwOFowHTEbMBkGA1UEAwwSZ2l0
bGFiLXBhc3NwaHJhc2VkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA
zpsWHOewP/khfDsLUWxaRCinrBzVJm2C01bVahKVR3g/JD4vEH901Wod9Pvbh/9e
PEfE+YZmgSUUopbL3JUheMnyW416F43HKE/fPW4+QeuIEceuhCXg20eOXmvnWWNM
0hXZh4hq69rwvMPREC/LkZy/QkTDKhJNLNAqAQu2AJ3C7Yga8hFQYEhx1hpfGtwD
z/Nf3efat9WN/d6yW9hfJ98NCmImTm5l9Pc0YPNWCAf96vsqsNHBrTkFy6CQwkhH
K1ynVYuqnHYxSc4FPCT5SAleD9gR/xFBAHb7pPy4yGxMSEmiWaMjjZCVPsghj1jM
Ej77MTDL3U9LeDfiILhvZ+EeQxqPiFwwG2eaIn3ZEs2Ujvw7Z2VpG9VMcPTnB4jK
ot6qPM1YXnkGWQ6iT0DTPS3h7zg1xIJXI5N2sI6GXuKrXXwZ1wPqzFLKPv+xBjp8
P6dih+EImfReFi9zIO1LqGMY+XmRcqodsb6jzsmBimJkqBtatJM7FuUUUN56wiaj
q9+BWbm+ZdQ2lvqndMljjUjTh6pNERfGAJgkNuLn3X9hXVE0TSpmn0nOgaL5izP3
7FWUt0PTyGgK2zq9SEhZmK2TKckLkKMk/ZBBBVM/nrnjs72IlbsqdcVoTnApytZr
xVYTj1hV7QlAfaU3w/M534qXDiy8+HfX5ksWQMtSklECAwEAATANBgkqhkiG9w0B
AQUFAAOCAgEAMMhzSRq9PqCpui74nwjhmn8Dm2ky7A+MmoXNtk70cS/HWrjzaacb
B/rxsAUp7f0pj4QMMM0ETMFpbNs8+NPd2FRY0PfWE4yyDpvZO2Oj1HZKLHX72Gjn
K5KB9DYlVsXhGPfuFWXpxGWF2Az9hDWnj58M3DOAps+6tHuAtudQUuwf5ENQZWwE
ySpr7yoHm1ykgl0Tsb9ZHi9qLrWRRMNYXRT+gvwP1bba8j9jOtjO/xYiIskwMPLM
W8SFmQxbg0Cvi8Q89PB6zoTNOhPQyoyeSlw9meeZJHAMK2zxeglEm8C4EQ+I9Y6/
yylM5/Sc55TjWAvRFgbsq+OozgMvffk/Q2fzcGF44J9DEQ7nrhmJxJ+X4enLknR5
Hw4+WhdYA+bwjx3YZBNTh9/YMgNPYwQhf5gtcZGTd6X4j6qZfJ6CXBmhkC1Cbfyl
yM7B7i4JAqPWMeDP50pXCgyKlwgw1JuFW+xkbkYQAj7wtggQ6z1Vjb5W8R8kYn9q
LXClVtThEeSV5KkVwNX21aFcUs8qeQ+zsgKqpEyM5oILQQ1gDSxLTtrr2KuN+WJN
wM0acwD45X7gA/aZYpCGkIgHIBq0zIDP1s6IqeebFJjW8lWofhRxOEWomWdRweJG
N7qQ1WCTQxAPGAkDI8QPjaspvnAhFKmpBG/mR5IXLFKDbttu7WNdYDo=
-----END CERTIFICATE-----
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,79CCB506B0FD42A6F1BAE6D72E1CB20C
EuZQOfgaO6LVCNytTHNJmbiq1rbum9xg6ohfBTVt7Cw4+8yLezWva/3sJQtnEk2P
M2yEQYWIiCX+clPkRiRL8WLjRfLTNcYS6QxxuJdpOrowPrBYr4Aig8jBUUBI4VQf
w1ZEUQd0mxQGnyzkKpsudFOntCtZbvbrBsIAQUNLcrKEFk3XW/BqE1Q/ja6WfWqX
b6EKg6DoXi92V90O6sLDfpmTKZq3ThvVDFuWeJ2K/GVp2cs+MkBIBJ8XX+NT1nWg
g+Ok+yaSI/N9ILX4XDgXunJGwcooI8PhHSjkDWRusi8vbo7RFqIKiSF+h6tIwktF
Uss3JESKgXZCQ7upCnHSzK/aWFtwHtXxqOi7esqEZd+1sB0LY+XMnbaxweCMx2Kj
czktKYvoXUs69Whln+yyXULtl5XhJ8lbvlbIG2FbZ9y+/hHOyBqZyeUyCnXDzv8/
0U0iZwreP3XPVMsy578pIdcdL27q+r05j4yjrJfbX3T9xp2u3F9uVubCa4euEBwV
yrFdsxJLKON8pFeDS49m5gHNsHmeZ0sUeTPZVGNXdabVetkOA0eAAGK4zAoqG79L
hEN7cDenz+E4XHp8gMzwwMiVyU4FuAb6SXkfSodctmSTWVbzNBja0FBek3UXy+pn
9qq7cIpe7NY5gzcbyoy9lSkyYVkAm8j6BIYtY1ZUAmtCklC2ADWARTjd7dI7aEbO
QbXxNIq2+O/zMOXfougSPoDP8SLyLuE1p6SwfWV7Dwf119hn+mjWlGzAZDxxHhsR
yYUQCUe0NIKzuUp3WYIx8xIb7/WFwit/JaFaxurjBnhkkEviBn+TgXiuFBO3tv/d
URpZ39rH0mrDsR61pCiIcoNVkQkynHcAFPd5VtaeSJPvZP280uOCPPS31cr6/0LB
1JX3lZoWWCuA+JQjxtZDaDTcvEUbfOQ2rexQQo4uylNkBF9F5WOdQBkKG/AfqBq8
S/TdubYzvpcKhFAlXsI67JdbxGlU4HCsxOLwWzSUYclN4W3l7s7KZ5zxt+MU03Uf
vara9uuZHiKUjZohjXeqcXTc+UyC8VH1dF19M3Cj9RNrwl2xEDUMtIiALBjbGp1E
pu2nPj9NhWf9Vw5MtSszutesxXba2nPmvvGvvZ7N3h/k4NsKL7JdENF7XqkI0D2K
jpO1t6d3cazS1VpMWLZS45kWaM3Y07tVR3V+4Iv9Vo1e9H2u/Z5U4YeJ44sgMsct
dBOAhHdUAI5+P+ocLXiCKo+EcS0cKvz+CC4ux0vvcF3JrTqZJN1U/JxRka2EyJ1B
2Xtu3DF36XpBJcs+MJHjJ+kUn6DHYoYxZa+bB8LX6+FQ+G7ue+Dx/RsGlP7if1nq
DAaM6kZg7/FbFzOZyl5xhwAJMxfgNNU7nSbk9lrvQ4mdwgFjvgGu3jlER4+TcleE
4svXInxp1zK6ES44tI9fXkhPaFkafxAL7eUSyjjEwMC06h+FtqK3mmoKLo5NrGJE
zVl69r2WdoSQEylVN1Kbp+U4YbfncInLJqBq2q5w9ASL/8Rhe8b52q6PuVX/bjoz
0pkSu+At4jVbAhRpER5NGlzG884IaqqvBvMYR5zFJeRroIijyUyH0KslK37/sXRk
ty0yKrkm31De9gDa3+XlgAVDAgbEQmGVwVVcV0IYYJbjIf36lUdGh4+3krwxolr/
vZct5Z7QxfJlBtdOstjz5U9o05yOhjoNrPZJXuKMmWOQjSwr7rRSdqmAABF9IrBf
Pa/ChF1y5j3gJESAFMyiea3kvLq1EbZRaKoybsQE2ctBQ8EQjzUz+OOxVO6GJ4W9
XHyfcviFrpsVcJEpXQlEtGtKdfKLp48cytob1Fu1JOYPDCrafUQINCZP4H3Nt892
zZiTmdwux7pbgf4KbONImN5XkpvdCGjQHSkYMmm5ETRK8s7Fmvt2aBPtlyXxJDOq
iJUqwDV5HZXOnQVE/v/yESKgo2Cb8BWqPZ4/8Ubgu/OADYyv/dtjQel8QQ2FMhO4
2tnwWbBBJk8VpR/vjFHkGSnj+JJfW/vUVQ+06D3wHYhNp7mh4M+37AngwzGCp7k+
9aFwb2FBGghArB03E4lIO/959T0cX95WZ6tZtLLEsf3+ug7PPOSswCqsoPsXzFJH
MgXVGKFXccNSsWol7VvrX/uja7LC1OE+pZNXxCRzSs4aljJBpvQ6Mty0lk2yBC0R
MdujMoZH9PG9U6stwFd+P17tlGrQdRD3H2uimn82Ck+j2l0z0pzN0JB2WBYEyK0O
1MC36wLICWjgIPLPOxDEEBeZPbc24DCcYfs/F/hSCHv/XTJzVVILCX11ShGPSXlI
FL9qyq6jTNh/pVz6NiN/WhUPBFfOSzLRDyU0MRsSHM8b/HPpf3NOI3Ywmmj65c2k
2kle1F2M5ZTL+XvLS61qLJ/8AgXWvDHP3xWuKGG/pM40CRTUkRW6NAokMr2/pEFw
IHTE2+84dOKnUIEczzMY3aqzNmYDCmhOY0jD/Ieb4hy9tN+1lbQ/msYMIJ1w7CFR
38yB/UbDD90NcuDhjrMbzVUv1At2rW7GM9lSbxGOlYDmtMNEL63md1pQ724v4gSE
mzoFcMkqdh+hjFvv11o4H32lF3mPYcXuL+po76tqxGOiUrLKe/ZqkT5XAclYV/7H
k3Me++PCh4ZqXBRPvR8Xr90NETtiFCkBQXLdhNWXrRe2v0EbSX+cYAWk68FQKCHa
HKTz9T7wAvB6QWBXFhH9iCP8rnQLCEhLEhdrt+4v2KFkIVzBgOlMoHsZsMp0sBeq
c5ZVbJdiKik3P/8ZQTn4jmOnQXCEyWx+LU4acks8Aho4lqq9yKq2DZpwbIRED47E
r7R/NUevhqqzEHZ2SGD6EDqRN+bHJEi64vq0ryaEielusYXZqlnFXDHJcfLCmR5X
3bj5pCwQF4ScTukrGQB/c4henG4vlF4CaD0CIIK3W6tH+AoDohYJts6YK49LGxmK
yXiyKNak8zHYBBoRvd2avRHyGuR5yC9KrN8cbC/kZqMDvAyM65pIK+U7exJwYJhv
ezCcbiH3bK3anpiRpdeNOot2ba/Y+/ks+DRC+xs4QDIhrmSEBCsLv1JbcWjtHSaG
lm+1DSVduUk/kN+fBnlfif+TQV9AP3/wb8ekk8jjKXsL7H1tJKHsLLIIvrgrpxjw
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIEnDCCAoQCAQEwDQYJKoZIhvcNAQEFBQAwFDESMBAGA1UEAwwJYXV0aG9yaXR5
MB4XDTE4MDMxOTE1MjYzMloXDTE5MDMxOTE1MjYzMlowFDESMBAGA1UEAwwJbG9j
YWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA+tcM7iphsLlR
ccUph2ixabRYnw1HeLCiA4O9a4O31oVUBuzAn/eVU4jyVWkaBym6MHa8CiDOro9H
OXodITMw+3G1sG/yQZ8Y/5dsOP2hEoSfs63/2FAgFWzrB2HnYSShiN8tBeeDI5cJ
ii4JVMfpfi9cvXZUXFR8+P0XR1HDxx6or6UTK37k2kbDQZ41rv1ng2w0AUZt0LRA
NWVE48zvUWIU0y+2JLP1yhrKj85RRjQc5cMK88zzWSZBcSjDGGeJ4C8B5Zh2gFlQ
+1aJkyyklORR3v/RyYO9prTeXPqQ3x/nNsNkI+cyv0Gle6tk+CkOfE1m0CvNWlNg
b8LdQ0XZsOYLZvxfpHk3gHA5GrHXvn5StkM5xMXpdUCsh22CZZHe/4SeFE64amkf
1/LuqY0LYc5UdG2SeJ0SDauPRAIuAr4OV7+Q/nLdY8haMC6KOtpbAWvKX/Jqq0z1
nUXzQn1JWCNw1QMdq9Uz8wiWOjLTr2D/mIVrVef0pb2mfdtzjzUrYCP0PtnQExPB
rocP6BDXN7Ragcdis5/IfLuCOD6pAkmzy6o8RSvAoEUs9VbPiUfN7WAyU1K1rTYH
KV+zPfWF254nZ2SBeReN9CMKbMJE+TX2chRlq07Q5LDz33h9KXw1LZT8MWRinVJf
RePsQiyHpRBWRG0AhbD+YpiGKHzsat0CAwEAATANBgkqhkiG9w0BAQUFAAOCAgEA
Skp0tbvVsg3RG2pX0GP25j0ix+f78zG0+BJ6LiKGMoCIBtGKitfUjBg83ru/ILpa
fpgrQpNQVUnGQ9tmpnqV605ZBBRUC1CRDsvUnyN6p7+yQAq6Fl+2ZKONHpPk+Bl4
CIewgdkHjTwTpvIM/1DFVCz4R1FxNjY3uqOVcNDczMYEk2Pn2GZNNN35hUHHxWh4
89ZvI+XKuRFZq3cDPA60PySeJJpCRScWGgnkdEX1gTtWH3WUlq9llxIvRexyNyzZ
Yqvcfx5UT75/Pp+JPh9lpUCcKLHeUiadjkiLxu3IcrYa4gYx4lA8jgm7adNEahd0
oMAHoO9DU6XMo7o6tnQH3xQv9RAbQanjuyJR9N7mwmc59bQ6mW+pxCk843GwT73F
slseJ1nE1fQQQD7mn/KGjmeWtxY2ElUjTay9ff9/AgJeQYRW+oH0cSdo8WCpc2+G
+LZtLWfBgFLHseRlmarSe2pP8KmbaTd3q7Bu0GekVQOxYcNX59Pj4muQZDVLh8aX
mSQ+Ifts/ljT649MISHn2AZMR4+BUx63tFcatQhbAGGH5LeFdbaGcaVdsUVyZ9a2
HBmFWNsgEPtcC+WmNzCXbv7jQsLAJXufKG5MnurJgNf/n5uKCmpGsEJDT/KF1k/3
x9YnqM7zTyV6un+LS3HjEJvwQmqPWe+vFAeXWGCoWxE=
-----END CERTIFICATE-----
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----
......@@ -39,7 +39,7 @@ describe EE::Gitlab::ExternalAuthorization::Access, :clean_gitlab_redis_cache do
before do
allow(access).to receive(:load_from_cache)
allow(fake_client).to receive(:request_access).and_return(fake_response)
allow(EE::Gitlab::ExternalAuthorization::Client).to receive(:build) { fake_client }
allow(EE::Gitlab::ExternalAuthorization::Client).to receive(:new) { fake_client }
end
context 'when loading from the webservice' do
......
......@@ -3,7 +3,7 @@ require 'spec_helper'
describe EE::Gitlab::ExternalAuthorization::Client do
let(:user) { build(:user, email: 'dummy_user@example.com') }
let(:dummy_url) { 'https://dummy.net/' }
subject(:client) { described_class.build(user, 'dummy_label') }
subject(:client) { described_class.new(user, 'dummy_label') }
before do
stub_application_setting(external_authorization_service_url: dummy_url)
......@@ -28,7 +28,9 @@ describe EE::Gitlab::ExternalAuthorization::Client do
end
it 'respects the the timeout' do
allow(EE::Gitlab::ExternalAuthorization).to receive(:timeout).and_return(3)
stub_application_setting(
external_authorization_service_timeout: 3
)
expect(Excon).to receive(:post).with(dummy_url,
hash_including(
......@@ -40,6 +42,23 @@ describe EE::Gitlab::ExternalAuthorization::Client do
client.request_access
end
it 'adds the mutual tls params when they are present' do
stub_application_setting(
external_auth_client_cert: 'the certificate data',
external_auth_client_key: 'the key data',
external_auth_client_key_pass: 'open sesame'
)
expected_params = {
client_cert_data: 'the certificate data',
client_key_data: 'the key data',
client_key_pass: 'open sesame'
}
expect(Excon).to receive(:post).with(dummy_url, hash_including(expected_params))
client.request_access
end
it 'returns an expected response' do
expect(Excon).to receive(:post)
......
require 'spec_helper'
require 'email_spec'
describe Notify do
include EmailSpec::Helpers
include EmailSpec::Matchers
include RepoHelpers
include_context 'gitlab email notification'
set(:user) { create(:user) }
set(:current_user) { create(:user, email: "current@email.com") }
set(:assignee) { create(:user, email: 'assignee@example.com', name: 'John Doe') }
set(:merge_request) do
create(:merge_request, source_project: project,
target_project: project,
author: current_user,
assignee: assignee,
description: 'Awesome description')
end
set(:project2) { create(:project, :repository) }
set(:merge_request_without_assignee) do
create(:merge_request, source_project: project2,
author: current_user,
description: 'Awesome description')
end
context 'for a project' do
context 'for merge requests' do
describe "that are new with approver" do
before do
create(:approver, target: merge_request)
end
subject do
described_class.new_merge_request_email(
merge_request.assignee_id, merge_request.id
)
end
it "contains the approvers list" do
is_expected.to have_body_text /#{merge_request.approvers.first.user.name}/
end
end
describe 'that are approved' do
let(:last_approver) { create(:user) }
subject { described_class.approved_merge_request_email(recipient.id, merge_request.id, last_approver.id) }
before do
merge_request.approvals.create(user: merge_request.assignee)
merge_request.approvals.create(user: last_approver)
end
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { merge_request }
end
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'an unsubscribeable thread'
it 'is sent as the last approver' do
sender = subject.header[:from].addrs[0]
expect(sender.display_name).to eq(last_approver.name)
expect(sender.address).to eq(gitlab_sender)
end
it 'has the correct subject' do
is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/
end
it 'contains the new status' do
is_expected.to have_body_text /approved/i
end
it 'contains a link to the merge request' do
is_expected.to have_body_text /#{project_merge_request_path project, merge_request}/
end
it 'contains the names of all of the approvers' do
is_expected.to have_body_text /#{merge_request.assignee.name}/
is_expected.to have_body_text /#{last_approver.name}/
end
context 'when merge request has no assignee' do
before do
merge_request.update(assignee: nil)
end
it 'does not show the assignee' do
is_expected.not_to have_body_text 'Assignee'
end
end
end
describe 'that are unapproved' do
let(:last_unapprover) { create(:user) }
subject { described_class.unapproved_merge_request_email(recipient.id, merge_request.id, last_unapprover.id) }
before do
merge_request.approvals.create(user: merge_request.assignee)
end
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { merge_request }
end
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'an unsubscribeable thread'
it 'is sent as the last unapprover' do
sender = subject.header[:from].addrs[0]
expect(sender.display_name).to eq(last_unapprover.name)
expect(sender.address).to eq(gitlab_sender)
end
it 'has the correct subject' do
is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/
end
it 'contains the new status' do
is_expected.to have_body_text /unapproved/i
end
it 'contains a link to the merge request' do
is_expected.to have_body_text /#{project_merge_request_path project, merge_request}/
end
it 'contains the names of all of the approvers' do
is_expected.to have_body_text /#{merge_request.assignee.name}/
end
end
end
context 'for merge requests without assignee' do
describe 'that are unapproved' do
let(:last_unapprover) { create(:user) }
subject { described_class.unapproved_merge_request_email(recipient.id, merge_request_without_assignee.id, last_unapprover.id) }
before do
merge_request_without_assignee.approvals.create(user: merge_request_without_assignee.assignee)
end
it 'contains the new status' do
is_expected.to have_body_text /unapproved/i
end
end
end
end
end
......@@ -36,6 +36,39 @@ describe ApplicationSetting do
it { is_expected.not_to allow_value(nil).for(:external_authorization_service_default_label) }
it { is_expected.not_to allow_value(11).for(:external_authorization_service_timeout) }
it { is_expected.not_to allow_value(0).for(:external_authorization_service_timeout) }
it { is_expected.not_to allow_value('not a certificate').for(:external_auth_client_cert) }
it { is_expected.to allow_value('').for(:external_auth_client_cert) }
it { is_expected.to allow_value('').for(:external_auth_client_key) }
context 'when setting a valid client certificate for external authorization' do
let(:certificate_data) { File.read('ee/spec/fixtures/passphrase_x509_certificate.crt') }
before do
setting.external_auth_client_cert = certificate_data
end
it 'requires a valid client key when a certificate is set' do
expect(setting).not_to allow_value('fefefe').for(:external_auth_client_key)
end
it 'requires a matching certificate' do
other_private_key = File.read('ee/spec/fixtures/x509_certificate_pk.key')
expect(setting).not_to allow_value(other_private_key).for(:external_auth_client_key)
end
it 'the credentials are valid when the private key can be read and matches the certificate' do
tls_attributes = [:external_auth_client_key_pass,
:external_auth_client_key,
:external_auth_client_cert]
setting.external_auth_client_key = File.read('ee/spec/fixtures/passphrase_x509_certificate_pk.key')
setting.external_auth_client_key_pass = '5iveL!fe'
setting.validate
expect(setting.errors).not_to include(*tls_attributes)
end
end
end
end
......
......@@ -84,7 +84,10 @@ describe API::Settings, 'EE Settings' do
external_authorization_service_enabled: true,
external_authorization_service_url: 'https://custom.service/',
external_authorization_service_default_label: 'default',
external_authorization_service_timeout: 9.99
external_authorization_service_timeout: 9.99,
external_auth_client_cert: File.read('ee/spec/fixtures/passphrase_x509_certificate.crt'),
external_auth_client_key: File.read('ee/spec/fixtures/passphrase_x509_certificate_pk.key'),
external_auth_client_key_pass: "5iveL!fe"
}
end
let(:feature) { :external_authorization_service }
......
require 'spec_helper'
describe X509CertificateCredentialsValidator do
let(:certificate_data) { File.read('ee/spec/fixtures/x509_certificate.crt') }
let(:pkey_data) { File.read('ee/spec/fixtures/x509_certificate_pk.key') }
let(:validatable) do
Class.new do
include ActiveModel::Validations
attr_accessor :certificate, :private_key, :passphrase
def initialize(certificate, private_key, passphrase = nil)
@certificate, @private_key, @passphrase = certificate, private_key, passphrase
end
end
end
subject(:validator) do
described_class.new(certificate: :certificate, pkey: :private_key)
end
it 'is not valid when the certificate is not valid' do
record = validatable.new('not a certificate', nil)
validator.validate(record)
expect(record.errors[:certificate]).to include('is not a valid X509 certificate.')
end
it 'is not valid without a certificate' do
record = validatable.new(nil, nil)
validator.validate(record)
expect(record.errors[:certificate]).not_to be_empty
end
context 'when a valid certificate is passed' do
let(:record) { validatable.new(certificate_data, nil) }
it 'does not track an error for the certificate' do
validator.validate(record)
expect(record.errors[:certificate]).to be_empty
end
it 'adds an error when not passing a correct private key' do
validator.validate(record)
expect(record.errors[:private_key]).to include('could not read private key, is the passphrase correct?')
end
it 'has no error when the private key is correct' do
record.private_key = pkey_data
validator.validate(record)
expect(record.errors).to be_empty
end
end
context 'when using a passphrase' do
let(:passphrase_certificate_data) { File.read('ee/spec/fixtures/passphrase_x509_certificate.crt') }
let(:passphrase_pkey_data) { File.read('ee/spec/fixtures/passphrase_x509_certificate_pk.key') }
let(:record) { validatable.new(passphrase_certificate_data, passphrase_pkey_data, '5iveL!fe') }
subject(:validator) do
described_class.new(certificate: :certificate, pkey: :private_key, pass: :passphrase)
end
it 'is valid with the correct data' do
validator.validate(record)
expect(record.errors).to be_empty
end
it 'adds an error when the passphrase is wrong' do
record.passphrase = 'wrong'
validator.validate(record)
expect(record.errors[:private_key]).not_to be_empty
end
end
end
......@@ -110,7 +110,7 @@ describe Geo::FileDownloadDispatchWorker, :geo do
# 1. A total of 10 files in the queue, and we can load a maximimum of 5 and send 2 at a time.
# 2. We send 2, wait for 1 to finish, and then send again.
it 'attempts to load a new batch without pending downloads' do
stub_const('Geo::Scheduler::BaseWorker::DB_RETRIEVE_BATCH_SIZE', 5)
stub_const('Geo::Scheduler::SchedulerWorker::DB_RETRIEVE_BATCH_SIZE', 5)
secondary.update!(files_max_capacity: 2)
allow_any_instance_of(::Gitlab::Geo::Transfer).to receive(:download_from_primary).and_return(100)
......@@ -142,7 +142,7 @@ describe Geo::FileDownloadDispatchWorker, :geo do
it 'does not stall backfill' do
unsynced = create(:lfs_object, :with_file)
stub_const('Geo::BaseSchedulerWorker::DB_RETRIEVE_BATCH_SIZE', 1)
stub_const('Geo::Scheduler::SchedulerWorker::DB_RETRIEVE_BATCH_SIZE', 1)
expect(Geo::FileDownloadWorker).not_to receive(:perform_async).with(:lfs, failed_registry.file_id)
expect(Geo::FileDownloadWorker).to receive(:perform_async).with(:lfs, unsynced.id)
......
......@@ -17,6 +17,10 @@ describe Geo::RepositorySyncWorker, :geo, :clean_gitlab_redis_cache do
stub_current_geo_node(secondary)
end
around do |example|
Sidekiq::Testing.inline! { example.run }
end
describe '#perform' do
context 'additional shards' do
it 'skips backfill for repositories on other shards' do
......@@ -31,7 +35,7 @@ describe Geo::RepositorySyncWorker, :geo, :clean_gitlab_redis_cache do
expect(Geo::RepositoryShardSyncWorker).to receive(:perform_async).with(project_in_synced_group.repository.storage)
expect(Geo::RepositoryShardSyncWorker).not_to receive(:perform_async).with('broken')
Sidekiq::Testing.inline! { subject.perform }
subject.perform
end
it 'skips backfill for projects on shards excluded by selective sync' do
......@@ -46,7 +50,7 @@ describe Geo::RepositorySyncWorker, :geo, :clean_gitlab_redis_cache do
expect(Geo::RepositoryShardSyncWorker).to receive(:perform_async).with('default')
expect(Geo::RepositoryShardSyncWorker).not_to receive(:perform_async).with('broken')
Sidekiq::Testing.inline! { subject.perform }
subject.perform
end
it 'skips backfill for projects on missing shards' do
......@@ -63,7 +67,7 @@ describe Geo::RepositorySyncWorker, :geo, :clean_gitlab_redis_cache do
expect(Geo::RepositoryShardSyncWorker).to receive(:perform_async).with(project_in_synced_group.repository.storage)
expect(Geo::RepositoryShardSyncWorker).not_to receive(:perform_async).with('unknown')
Sidekiq::Testing.inline! { subject.perform }
subject.perform
end
it 'skips backfill for projects with downed Gitaly server' do
......@@ -81,7 +85,7 @@ describe Geo::RepositorySyncWorker, :geo, :clean_gitlab_redis_cache do
expect(Geo::RepositoryShardSyncWorker).to receive(:perform_async).with(healthy_shard)
expect(Geo::RepositoryShardSyncWorker).not_to receive(:perform_async).with('broken')
Sidekiq::Testing.inline! { subject.perform }
subject.perform
end
end
end
......
require 'spec_helper'
describe Geo::RepositoryVerification::Secondary::SchedulerWorker, :postgresql, :clean_gitlab_redis_cache do
include ::EE::GeoHelpers
set(:healthy_not_verified) { create(:project) }
let!(:secondary) { create(:geo_node) }
let(:healthy_shard) { healthy_not_verified.repository.storage }
before do
stub_current_geo_node(secondary)
end
around do |example|
Sidekiq::Testing.inline! { example.run }
end
describe '#perform' do
context 'when geo_repository_verification is enabled' do
before do
stub_feature_flags(geo_repository_verification: true)
end
it 'skips verification for repositories on other shards' do
unhealthy_not_verified = create(:project, repository_storage: 'broken')
# Make the shard unhealthy
FileUtils.rm_rf(unhealthy_not_verified.repository_storage_path)
expect(Geo::RepositoryVerification::Secondary::ShardWorker).to receive(:perform_async).with(healthy_shard)
expect(Geo::RepositoryVerification::Secondary::ShardWorker).not_to receive(:perform_async).with('broken')
subject.perform
end
it 'skips verification for projects on missing shards' do
missing_not_verified = create(:project)
missing_not_verified.update_column(:repository_storage, 'unknown')
# hide the 'broken' storage for this spec
stub_storage_settings({})
expect(Geo::RepositoryVerification::Secondary::ShardWorker).to receive(:perform_async).with(healthy_shard)
expect(Geo::RepositoryVerification::Secondary::ShardWorker).not_to receive(:perform_async).with('unknown')
subject.perform
end
it 'skips verification for projects with downed Gitaly server' do
create(:project, repository_storage: 'broken')
# Report only one healthy shard
expect(Gitlab::HealthChecks::FsShardsCheck).to receive(:readiness)
.and_return([result(true, healthy_shard), result(true, 'broken')])
expect(Gitlab::HealthChecks::GitalyCheck).to receive(:readiness)
.and_return([result(true, healthy_shard), result(false, 'broken')])
expect(Geo::RepositoryVerification::Secondary::ShardWorker).to receive(:perform_async).with(healthy_shard)
expect(Geo::RepositoryVerification::Secondary::ShardWorker).not_to receive(:perform_async).with('broken')
subject.perform
end
it 'skips verification for projects on shards excluded by selective sync' do
secondary.update!(selective_sync_type: 'shards', selective_sync_shards: [healthy_shard])
# Report both shards as healthy
expect(Gitlab::HealthChecks::FsShardsCheck).to receive(:readiness)
.and_return([result(true, healthy_shard), result(true, 'broken')])
expect(Gitlab::HealthChecks::GitalyCheck).to receive(:readiness)
.and_return([result(true, healthy_shard), result(true, 'broken')])
expect(Geo::RepositoryVerification::Secondary::ShardWorker).to receive(:perform_async).with(healthy_shard)
expect(Geo::RepositoryVerification::Secondary::ShardWorker).not_to receive(:perform_async).with('broken')
subject.perform
end
end
context 'when geo_repository_verification is disabled' do
before do
stub_feature_flags(geo_repository_verification: false)
end
it 'does not schedule jobs' do
expect(Geo::RepositoryVerification::Secondary::ShardWorker)
.not_to receive(:perform_async).with(healthy_shard)
subject.perform
end
end
end
def result(success, shard)
Gitlab::HealthChecks::Result.new(success, nil, { shard: shard })
end
end
require 'spec_helper'
describe Geo::RepositoryVerification::Secondary::ShardWorker, :postgresql, :clean_gitlab_redis_cache do
include ::EE::GeoHelpers
let!(:secondary) { create(:geo_node) }
let(:shard_name) { Gitlab.config.repositories.storages.keys.first }
set(:project) { create(:project) }
before do
stub_current_geo_node(secondary)
end
describe '#perform' do
before do
allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) { true }
allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:renew) { true }
Gitlab::Geo::ShardHealthCache.update([shard_name])
end
it 'schedule job for each project' do
other_project = create(:project)
create(:repository_state, :repository_verified, project: project)
create(:repository_state, :repository_verified, project: other_project)
create(:geo_project_registry, :repository_verification_outdated, project: project)
create(:geo_project_registry, :repository_verification_outdated, project: other_project)
expect(Geo::RepositoryVerification::Secondary::SingleWorker)
.to receive(:perform_async).twice
subject.perform(shard_name)
end
it 'schedule job for projects missing repository verification' do
create(:repository_state, :wiki_verified, project: project)
missing_repository_verification = create(:geo_project_registry, :wiki_verified, project: project)
expect(Geo::RepositoryVerification::Secondary::SingleWorker)
.to receive(:perform_async).with(missing_repository_verification.id)
subject.perform(shard_name)
end
it 'schedule job for projects missing wiki verification' do
create(:repository_state, :repository_verified, project: project)
missing_wiki_verification = create(:geo_project_registry, :repository_verified, project: project)
expect(Geo::RepositoryVerification::Secondary::SingleWorker)
.to receive(:perform_async).with(missing_wiki_verification.id)
subject.perform(shard_name)
end
it 'does not schedule jobs when shard becomes unhealthy' do
create(:repository_state, project: project)
Gitlab::Geo::ShardHealthCache.update([])
expect(Geo::RepositoryVerification::Secondary::SingleWorker)
.not_to receive(:perform_async)
subject.perform(shard_name)
end
it 'does not schedule jobs when no geo database is configured' do
allow(Gitlab::Geo).to receive(:geo_database_configured?) { false }
expect(Geo::RepositoryVerification::Secondary::SingleWorker)
.not_to receive(:perform_async)
subject.perform(shard_name)
# We need to unstub here or the DatabaseCleaner will have issues since it
# will appear as though the tracking DB were not available
allow(Gitlab::Geo).to receive(:geo_database_configured?).and_call_original
end
it 'does not schedule jobs when not running on a secondary' do
allow(Gitlab::Geo).to receive(:primary?) { false }
expect(Geo::RepositoryVerification::Secondary::SingleWorker)
.not_to receive(:perform_async)
subject.perform(shard_name)
end
it 'does not schedule jobs when number of scheduled jobs exceeds capacity' do
create(:project)
is_expected.to receive(:scheduled_job_ids).and_return(1..1000).at_least(:once)
is_expected.not_to receive(:schedule_job)
Sidekiq::Testing.inline! { subject.perform(shard_name) }
end
end
end
......@@ -71,7 +71,11 @@ module Gitlab
authenticators.compact!
user if authenticators.find { |auth| auth.login(login, password) }
# return found user that was authenticated first for given login credentials
authenticators.find do |auth|
authenticated_user = auth.login(login, password)
break authenticated_user if authenticated_user
end
end
end
......
......@@ -8,7 +8,7 @@ module Gitlab
def login(login, password)
return false unless Gitlab::CurrentSettings.password_authentication_enabled_for_git?
user&.valid_password?(password)
return user if user&.valid_password?(password)
end
end
end
......
......@@ -12,30 +12,26 @@ module Gitlab
return unless Gitlab::Auth::LDAP::Config.enabled?
return unless login.present? && password.present?
auth = nil
# loop through providers until valid bind
# return found user that was authenticated by first provider for given login credentials
providers.find do |provider|
auth = new(provider)
auth.login(login, password) # true will exit the loop
break auth.user if auth.login(login, password) # true will exit the loop
end
# If (login, password) was invalid for all providers, the value of auth is now the last
# Gitlab::Auth::LDAP::Authentication instance we tried.
auth.user
end
def self.providers
Gitlab::Auth::LDAP::Config.providers
end
attr_accessor :ldap_user
def login(login, password)
@ldap_user = adapter.bind_as(
result = adapter.bind_as(
filter: user_filter(login),
size: 1,
password: password
)
return unless result
@user = Gitlab::Auth::LDAP::User.find_by_uid_and_provider(result.dn, provider)
end
def adapter
......@@ -56,12 +52,6 @@ module Gitlab
filter
end
def user
return unless ldap_user
Gitlab::Auth::LDAP::User.find_by_uid_and_provider(ldap_user.dn, provider)
end
end
end
end
......
......@@ -12,6 +12,7 @@ module Gitlab
@user = user
end
# Implementation must return user object if login successful
def login(login, password)
raise NotImplementedError
end
......
......@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-03-23 20:21+0100\n"
"PO-Revision-Date: 2018-03-23 20:21+0100\n"
"POT-Creation-Date: 2018-03-26 15:11+0200\n"
"PO-Revision-Date: 2018-03-26 15:11+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
......@@ -864,6 +864,15 @@ msgstr ""
msgid "Click to expand text"
msgstr ""
msgid "Client authentication certificate"
msgstr ""
msgid "Client authentication key"
msgstr ""
msgid "Client authentication key password"
msgstr ""
msgid "Clone repository"
msgstr ""
......@@ -3838,6 +3847,9 @@ msgstr ""
msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
msgstr ""
msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
msgstr ""
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr ""
......@@ -3865,12 +3877,18 @@ msgstr ""
msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
msgstr ""
msgid "The passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
msgstr ""
msgid "The phase of the development lifecycle."
msgstr ""
msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
msgstr ""
msgid "The private key to use when a client certificate is provided. This value is encrypted at rest."
msgstr ""
msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle."
msgstr ""
......@@ -4679,6 +4697,9 @@ msgstr ""
msgid "connecting"
msgstr ""
msgid "could not read private key, is the passphrase correct?"
msgstr ""
msgid "day"
msgid_plural "days"
msgstr[0] ""
......@@ -4699,6 +4720,9 @@ msgstr ""
msgid "is invalid because there is upstream lock"
msgstr ""
msgid "is not a valid X509 certificate."
msgstr ""
msgid "locked by %{path_lock_user_name} %{created_at}"
msgstr ""
......@@ -4710,6 +4734,15 @@ msgstr[1] ""
msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch"
msgstr ""
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB"
msgstr ""
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} increased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB"
msgstr ""
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
msgstr ""
msgid "mrWidget|Add approval"
msgstr ""
......@@ -4755,18 +4788,27 @@ msgstr ""
msgid "mrWidget|Closes"
msgstr ""
msgid "mrWidget|Deployment statistics are not available currently"
msgstr ""
msgid "mrWidget|Did not close"
msgstr ""
msgid "mrWidget|Email patches"
msgstr ""
msgid "mrWidget|Failed to load deployment statistics"
msgstr ""
msgid "mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the"
msgstr ""
msgid "mrWidget|If the %{missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line"
msgstr ""
msgid "mrWidget|Loading deployment statistics"
msgstr ""
msgid "mrWidget|Mentions"
msgstr ""
......@@ -4889,6 +4931,9 @@ msgstr ""
msgid "personal access token"
msgstr ""
msgid "private key does not match certificate."
msgstr ""
msgid "remove due date"
msgstr ""
......
......@@ -3,7 +3,7 @@
cd "$(dirname "$0")/.."
# Use long options (e.g. --header instead of -H) for curl examples in documentation.
echo 'Checking for curl short options...'
echo '=> Checking for cURL short options...'
grep --extended-regexp --recursive --color=auto 'curl (.+ )?-[^- ].*' doc/ >/dev/null 2>&1
if [ $? == 0 ]
then
......@@ -15,7 +15,7 @@ fi
# Ensure that the CHANGELOG.md does not contain duplicate versions
DUPLICATE_CHANGELOG_VERSIONS=$(grep --extended-regexp '^## .+' CHANGELOG.md | sed -E 's| \(.+\)||' | sort -r | uniq -d)
echo 'Checking for CHANGELOG.md duplicate entries...'
echo '=> Checking for CHANGELOG.md duplicate entries...'
if [ "${DUPLICATE_CHANGELOG_VERSIONS}" != "" ]
then
echo '✖ ERROR: Duplicate versions in CHANGELOG.md:' >&2
......@@ -25,7 +25,7 @@ fi
# Make sure no files in doc/ are executable
EXEC_PERM_COUNT=$(find doc/ app/ -type f -perm 755 | wc -l)
echo 'Checking for executable permissions...'
echo '=> Checking for executable permissions...'
if [ "${EXEC_PERM_COUNT}" -ne 0 ]
then
echo '✖ ERROR: Executable permissions should not be used in documentation! Use `chmod 644` to the files in question:' >&2
......@@ -33,5 +33,33 @@ then
exit 1
fi
# Do not use 'README.md', instead use 'index.md'
# Number of 'README.md's as of 2018-03-26
NUMBER_READMES_CE=42
NUMBER_READMES_EE=46
FIND_READMES=$(find doc/ -name "README.md" | wc -l)
echo '=> Checking for new README.md files...'
if [ "${CI_PROJECT_NAME}" == 'gitlab-ce' ]
then
if [ ${FIND_READMES} -ne ${NUMBER_READMES_CE} ]
then
echo
echo ' ✖ ERROR: New README.md file(s) detected, prefer index.md over README.md.' >&2
echo ' https://docs.gitlab.com/ee/development/writing_documentation.html#location-and-naming-documents'
echo
exit 1
fi
elif [ "${CI_PROJECT_NAME}" == 'gitlab-ee' ]
then
if [ ${FIND_READMES} -ne $NUMBER_READMES_EE ]
then
echo
echo ' ✖ ERROR: New README.md file(s) detected, prefer index.md over README.md.' >&2
echo ' https://docs.gitlab.com/ee/development/writing_documentation.html#location-and-naming-documents'
echo
exit 1
fi
fi
echo "✔ Linting passed"
exit 0
......@@ -315,13 +315,19 @@ describe Gitlab::Auth do
it "tries to autheticate with db before ldap" do
expect(Gitlab::Auth::LDAP::Authentication).not_to receive(:login)
gl_auth.find_with_user_password(username, password)
expect(gl_auth.find_with_user_password(username, password)).to eq(user)
end
it "does not find user by using ldap as fallback to for authentication" do
expect(Gitlab::Auth::LDAP::Authentication).to receive(:login).and_return(nil)
expect(gl_auth.find_with_user_password('ldap_user', 'password')).to be_nil
end
it "uses ldap as fallback to for authentication" do
expect(Gitlab::Auth::LDAP::Authentication).to receive(:login)
it "find new user by using ldap as fallback to for authentication" do
expect(Gitlab::Auth::LDAP::Authentication).to receive(:login).and_return(user)
gl_auth.find_with_user_password('ldap_user', 'password')
expect(gl_auth.find_with_user_password('ldap_user', 'password')).to eq(user)
end
end
......
......@@ -314,22 +314,6 @@ describe Notify do
end
end
describe "that are new with approver" do
before do
create(:approver, target: merge_request)
end
subject do
described_class.new_merge_request_email(
merge_request.assignee_id, merge_request.id
)
end
it "contains the approvers list" do
is_expected.to have_body_text /#{merge_request.approvers.first.user.name}/
end
end
describe 'that are new with a description' do
subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id) }
......@@ -416,94 +400,6 @@ describe Notify do
end
end
end
describe 'that are approved' do
let(:last_approver) { create(:user) }
subject { described_class.approved_merge_request_email(recipient.id, merge_request.id, last_approver.id) }
before do
merge_request.approvals.create(user: merge_request.assignee)
merge_request.approvals.create(user: last_approver)
end
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { merge_request }
end
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'an unsubscribeable thread'
it 'is sent as the last approver' do
sender = subject.header[:from].addrs[0]
expect(sender.display_name).to eq(last_approver.name)
expect(sender.address).to eq(gitlab_sender)
end
it 'has the correct subject' do
is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/
end
it 'contains the new status' do
is_expected.to have_body_text /approved/i
end
it 'contains a link to the merge request' do
is_expected.to have_body_text /#{project_merge_request_path project, merge_request}/
end
it 'contains the names of all of the approvers' do
is_expected.to have_body_text /#{merge_request.assignee.name}/
is_expected.to have_body_text /#{last_approver.name}/
end
context 'when merge request has no assignee' do
before do
merge_request.update(assignee: nil)
end
it 'does not show the assignee' do
is_expected.not_to have_body_text 'Assignee'
end
end
end
describe 'that are unapproved' do
let(:last_unapprover) { create(:user) }
subject { described_class.unapproved_merge_request_email(recipient.id, merge_request.id, last_unapprover.id) }
before do
merge_request.approvals.create(user: merge_request.assignee)
end
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { merge_request }
end
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'an unsubscribeable thread'
it 'is sent as the last unapprover' do
sender = subject.header[:from].addrs[0]
expect(sender.display_name).to eq(last_unapprover.name)
expect(sender.address).to eq(gitlab_sender)
end
it 'has the correct subject' do
is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/
end
it 'contains the new status' do
is_expected.to have_body_text /unapproved/i
end
it 'contains a link to the merge request' do
is_expected.to have_body_text /#{project_merge_request_path project, merge_request}/
end
it 'contains the names of all of the approvers' do
is_expected.to have_body_text /#{merge_request.assignee.name}/
end
end
end
context 'for issue notes' do
......
......@@ -58,6 +58,12 @@ class NotifyPreview < ActionMailer::Preview
end
end
# EE-specific start
def unapproved_merge_request
Notify.unapproved_merge_request_email(user.id, merge_request.id, approver.id).message
end
# EE-specific end
private
def project
......@@ -65,7 +71,7 @@ class NotifyPreview < ActionMailer::Preview
end
def merge_request
@merge_request ||= project.merge_requests.find_by(source_branch: 'master', target_branch: 'feature')
@merge_request ||= project.merge_requests.first
end
def user
......@@ -104,4 +110,10 @@ class NotifyPreview < ActionMailer::Preview
pipeline = Ci::Pipeline.last
Notify.pipeline_failed_email(pipeline, pipeline.user.try(:email))
end
# EE-specific start
def approver
@user ||= User.first
end
# EE-specific end
end
......@@ -24,6 +24,14 @@ describe MergeRequests::RefreshService do
merge_when_pipeline_succeeds: true,
merge_user: @user)
@another_merge_request = create(:merge_request,
source_project: @project,
source_branch: 'master',
target_branch: 'test',
target_project: @project,
merge_when_pipeline_succeeds: true,
merge_user: @user)
@fork_merge_request = create(:merge_request,
source_project: @fork_project,
source_branch: 'master',
......@@ -55,9 +63,11 @@ describe MergeRequests::RefreshService do
context 'push to origin repo source branch' do
let(:refresh_service) { service.new(@project, @user) }
let(:notification_service) { spy('notification_service') }
before do
allow(refresh_service).to receive(:execute_hooks)
allow(NotificationService).to receive(:new) { notification_service }
end
it 'executes hooks with update action' do
......@@ -67,6 +77,11 @@ describe MergeRequests::RefreshService do
expect(refresh_service).to have_received(:execute_hooks)
.with(@merge_request, 'update', old_rev: @oldrev)
expect(notification_service).to have_received(:push_to_merge_request)
.with(@merge_request, @user, new_commits: anything, existing_commits: anything)
expect(notification_service).to have_received(:push_to_merge_request)
.with(@another_merge_request, @user, new_commits: anything, existing_commits: anything)
expect(@merge_request.notes).not_to be_empty
expect(@merge_request).to be_open
expect(@merge_request.merge_when_pipeline_succeeds).to be_falsey
......@@ -125,11 +140,13 @@ describe MergeRequests::RefreshService do
context 'push to origin repo source branch when an MR was reopened' do
let(:refresh_service) { service.new(@project, @user) }
let(:notification_service) { spy('notification_service') }
before do
@merge_request.update(state: :opened)
allow(refresh_service).to receive(:execute_hooks)
allow(NotificationService).to receive(:new) { notification_service }
refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
reload_mrs
end
......@@ -137,6 +154,10 @@ describe MergeRequests::RefreshService do
it 'executes hooks with update action' do
expect(refresh_service).to have_received(:execute_hooks)
.with(@merge_request, 'update', old_rev: @oldrev)
expect(notification_service).to have_received(:push_to_merge_request)
.with(@merge_request, @user, new_commits: anything, existing_commits: anything)
expect(notification_service).to have_received(:push_to_merge_request)
.with(@another_merge_request, @user, new_commits: anything, existing_commits: anything)
expect(@merge_request.notes).not_to be_empty
expect(@merge_request).to be_open
......
......@@ -1132,6 +1132,36 @@ describe NotificationService, :mailer do
end
end
describe '#push_to_merge_request' do
before do
update_custom_notification(:push_to_merge_request, @u_guest_custom, resource: project)
update_custom_notification(:push_to_merge_request, @u_custom_global)
end
it do
notification.push_to_merge_request(merge_request, @u_disabled)
should_email(merge_request.assignee)
should_email(@u_guest_custom)
should_email(@u_custom_global)
should_email(@u_participant_mentioned)
should_email(@subscriber)
should_email(@watcher_and_subscriber)
should_not_email(@u_watcher)
should_not_email(@u_guest_watcher)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
should_not_email(@u_lazy_participant)
end
it_behaves_like 'participating notifications' do
let(:participant) { create(:user, username: 'user-participant') }
let(:issuable) { merge_request }
let(:notification_trigger) { notification.push_to_merge_request(merge_request, @u_disabled) }
end
end
describe '#relabel_merge_request' do
let(:group_label_1) { create(:group_label, group: group, title: 'Group Label 1', merge_requests: [merge_request]) }
let(:group_label_2) { create(:group_label, group: group, title: 'Group Label 2') }
......
......@@ -12,7 +12,6 @@ describe AttachmentUploader do
upload_path: %r[uploads/-/system/note/attachment/],
absolute_path: %r[#{CarrierWave.root}/uploads/-/system/note/attachment/]
# EE-specific
context "object_store is REMOTE" do
before do
stub_uploads_object_storage
......
......@@ -12,7 +12,6 @@ describe AvatarUploader do
upload_path: %r[uploads/-/system/user/avatar/],
absolute_path: %r[#{CarrierWave.root}/uploads/-/system/user/avatar/]
# EE-specific
context "object_store is REMOTE" do
before do
stub_uploads_object_storage
......
......@@ -14,7 +14,6 @@ describe NamespaceFileUploader do
upload_path: IDENTIFIER,
absolute_path: %r[#{CarrierWave.root}/uploads/-/system/namespace/\d+/#{IDENTIFIER}]
# EE-specific
context "object_store is REMOTE" do
before do
stub_uploads_object_storage
......
......@@ -14,7 +14,6 @@ describe PersonalFileUploader do
upload_path: IDENTIFIER,
absolute_path: %r[#{CarrierWave.root}/uploads/-/system/personal_snippet/\d+/#{IDENTIFIER}]
# EE-specific
context "object_store is REMOTE" do
before do
stub_uploads_object_storage
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment