summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorLin Jen-Shin <godfat@godfat.org>2018-03-03 00:10:21 +0800
committerLin Jen-Shin <godfat@godfat.org>2018-03-03 00:10:21 +0800
commit6c5a7d5305e257244168799df0420359d0ad7b57 (patch)
tree197f0293855b02cccfb97e3f319594530b285344 /lib
parent461ecbcf07f0785b5ea50c62b114bf8217ac5199 (diff)
parent9b704ef327cc0224bf09c1e8d8d27df88ab13734 (diff)
downloadgitlab-ce-6c5a7d5305e257244168799df0420359d0ad7b57.tar.gz
Merge remote-tracking branch 'upstream/master' into 42572-release-controller
* upstream/master: (889 commits) SlackService - respect `notify_only_default_branch` for push events Clarify usage ping wording in admin area Update incoming emails documents Allow to include also descendant group labels Update docs on grouping CI jobs Support additional LabelsFinder parameters for group labels Extend Cluster Applications to install GitLab Runner to Kubernetes cluster Remove registry list webpack entry point Remove trailing newline that was causing an EE conflict Small fixes in Vuex docs Remove u2f webpack bundle Update documentation WRT to request parameters remove common_vue CommonsChunk config Fetch commit signatures from Gitaly in batches migrate stl_viewer to dynamic import migrate sketch_viewer to dynamic import migrate pdf_viewer to dynamic import migrate notebook_viewer to dynamic import migrate balsamiq_viewer to dynamic import Add some strings that were missing in gitlab.pot ...
Diffstat (limited to 'lib')
-rw-r--r--lib/api/access_requests.rb8
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/commits.rb39
-rw-r--r--lib/api/entities.rb28
-rw-r--r--lib/api/groups.rb29
-rw-r--r--lib/api/helpers.rb2
-rw-r--r--lib/api/helpers/custom_attributes.rb28
-rw-r--r--lib/api/helpers/runner.rb18
-rw-r--r--lib/api/internal.rb2
-rw-r--r--lib/api/members.rb14
-rw-r--r--lib/api/merge_requests.rb2
-rw-r--r--lib/api/pages_domains.rb10
-rw-r--r--lib/api/project_hooks.rb1
-rw-r--r--lib/api/project_import.rb69
-rw-r--r--lib/api/projects.rb20
-rw-r--r--lib/api/runner.rb4
-rw-r--r--lib/api/search.rb12
-rw-r--r--lib/api/services.rb71
-rw-r--r--lib/api/settings.rb2
-rw-r--r--lib/api/todos.rb2
-rw-r--r--lib/api/users.rb9
-rw-r--r--lib/api/v3/entities.rb10
-rw-r--r--lib/api/v3/members.rb2
-rw-r--r--lib/api/v3/project_hooks.rb1
-rw-r--r--lib/api/v3/todos.rb2
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb15
-rw-r--r--lib/banzai/filter/html_entity_filter.rb2
-rw-r--r--lib/banzai/filter/issuable_state_filter.rb6
-rw-r--r--lib/banzai/filter/milestone_reference_filter.rb2
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb34
-rw-r--r--lib/banzai/redactor.rb46
-rw-r--r--lib/banzai/reference_parser/issuable_parser.rb2
-rw-r--r--lib/banzai/reference_parser/issue_parser.rb25
-rw-r--r--lib/bitbucket/connection.rb2
-rw-r--r--lib/gitlab/asciidoc.rb3
-rw-r--r--lib/gitlab/auth.rb4
-rw-r--r--lib/gitlab/auth/ldap/access.rb89
-rw-r--r--lib/gitlab/auth/ldap/adapter.rb110
-rw-r--r--lib/gitlab/auth/ldap/auth_hash.rb48
-rw-r--r--lib/gitlab/auth/ldap/authentication.rb72
-rw-r--r--lib/gitlab/auth/ldap/config.rb237
-rw-r--r--lib/gitlab/auth/ldap/dn.rb303
-rw-r--r--lib/gitlab/auth/ldap/person.rb122
-rw-r--r--lib/gitlab/auth/ldap/user.rb54
-rw-r--r--lib/gitlab/auth/o_auth/auth_hash.rb92
-rw-r--r--lib/gitlab/auth/o_auth/provider.rb56
-rw-r--r--lib/gitlab/auth/o_auth/session.rb21
-rw-r--r--lib/gitlab/auth/o_auth/user.rb246
-rw-r--r--lib/gitlab/auth/request_authenticator.rb8
-rw-r--r--lib/gitlab/auth/saml/auth_hash.rb19
-rw-r--r--lib/gitlab/auth/saml/config.rb21
-rw-r--r--lib/gitlab/auth/saml/user.rb52
-rw-r--r--lib/gitlab/background_migration/create_fork_network_memberships_range.rb15
-rw-r--r--lib/gitlab/background_migration/migrate_build_stage.rb48
-rw-r--r--lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb496
-rw-r--r--lib/gitlab/background_migration/populate_untracked_uploads.rb160
-rw-r--r--lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb201
-rw-r--r--lib/gitlab/background_migration/prepare_untracked_uploads.rb15
-rw-r--r--lib/gitlab/checks/change_access.rb41
-rw-r--r--lib/gitlab/checks/commit_check.rb61
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/base.rb25
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/equals.rb26
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/null.rb25
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/operator.rb15
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/string.rb25
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/value.rb15
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/variable.rb25
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexer.rb59
-rw-r--r--lib/gitlab/ci/pipeline/expression/parser.rb40
-rw-r--r--lib/gitlab/ci/pipeline/expression/statement.rb42
-rw-r--r--lib/gitlab/ci/pipeline/expression/token.rb28
-rw-r--r--lib/gitlab/contributions_calendar.rb6
-rw-r--r--lib/gitlab/cross_project_access.rb67
-rw-r--r--lib/gitlab/cross_project_access/check_collection.rb47
-rw-r--r--lib/gitlab/cross_project_access/check_info.rb66
-rw-r--r--lib/gitlab/cross_project_access/class_methods.rb48
-rw-r--r--lib/gitlab/cycle_analytics/base_query.rb7
-rw-r--r--lib/gitlab/cycle_analytics/base_stage.rb29
-rw-r--r--lib/gitlab/cycle_analytics/production_helper.rb4
-rw-r--r--lib/gitlab/cycle_analytics/test_stage.rb6
-rw-r--r--lib/gitlab/cycle_analytics/usage_data.rb72
-rw-r--r--lib/gitlab/database/median.rb130
-rw-r--r--lib/gitlab/diff/highlight.rb12
-rw-r--r--lib/gitlab/encoding_helper.rb6
-rw-r--r--lib/gitlab/file_finder.rb5
-rw-r--r--lib/gitlab/git/blob.rb6
-rw-r--r--lib/gitlab/git/commit.rb61
-rw-r--r--lib/gitlab/git/repository.rb142
-rw-r--r--lib/gitlab/git/tree.rb23
-rw-r--r--lib/gitlab/git/wiki.rb11
-rw-r--r--lib/gitlab/git_access.rb17
-rw-r--r--lib/gitlab/git_access_wiki.rb8
-rw-r--r--lib/gitlab/gitaly_client.rb23
-rw-r--r--lib/gitlab/gitaly_client/blob_service.rb26
-rw-r--r--lib/gitlab/gitaly_client/blobs_stitcher.rb47
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb60
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb27
-rw-r--r--lib/gitlab/github_import/importer/repository_importer.rb1
-rw-r--r--lib/gitlab/gon_helper.rb3
-rw-r--r--lib/gitlab/gpg/commit.rb24
-rw-r--r--lib/gitlab/import_export/import_export.yml2
-rw-r--r--lib/gitlab/import_export/importer.rb5
-rw-r--r--lib/gitlab/import_export/project_creator.rb23
-rw-r--r--lib/gitlab/import_export/wiki_restorer.rb23
-rw-r--r--lib/gitlab/job_waiter.rb8
-rw-r--r--lib/gitlab/kubernetes/config_map.rb37
-rw-r--r--lib/gitlab/kubernetes/helm/api.rb9
-rw-r--r--lib/gitlab/kubernetes/helm/base_command.rb40
-rw-r--r--lib/gitlab/kubernetes/helm/init_command.rb19
-rw-r--r--lib/gitlab/kubernetes/helm/install_command.rb53
-rw-r--r--lib/gitlab/kubernetes/helm/pod.rb50
-rw-r--r--lib/gitlab/ldap/access.rb87
-rw-r--r--lib/gitlab/ldap/adapter.rb108
-rw-r--r--lib/gitlab/ldap/auth_hash.rb46
-rw-r--r--lib/gitlab/ldap/authentication.rb70
-rw-r--r--lib/gitlab/ldap/config.rb235
-rw-r--r--lib/gitlab/ldap/dn.rb301
-rw-r--r--lib/gitlab/ldap/person.rb135
-rw-r--r--lib/gitlab/ldap/user.rb52
-rw-r--r--lib/gitlab/metrics/sidekiq_metrics_exporter.rb2
-rw-r--r--lib/gitlab/metrics/transaction.rb5
-rw-r--r--lib/gitlab/middleware/go.rb10
-rw-r--r--lib/gitlab/middleware/multipart.rb8
-rw-r--r--lib/gitlab/o_auth.rb6
-rw-r--r--lib/gitlab/o_auth/auth_hash.rb90
-rw-r--r--lib/gitlab/o_auth/provider.rb54
-rw-r--r--lib/gitlab/o_auth/session.rb19
-rw-r--r--lib/gitlab/o_auth/user.rb235
-rw-r--r--lib/gitlab/plugin.rb26
-rw-r--r--lib/gitlab/plugin_logger.rb7
-rw-r--r--lib/gitlab/profiler.rb1
-rw-r--r--lib/gitlab/project_search_results.rb5
-rw-r--r--lib/gitlab/prometheus/metric_group.rb7
-rw-r--r--lib/gitlab/prometheus/queries/additional_metrics_deployment_query.rb3
-rw-r--r--lib/gitlab/prometheus/queries/additional_metrics_environment_query.rb1
-rw-r--r--lib/gitlab/prometheus/queries/deployment_query.rb2
-rw-r--r--lib/gitlab/prometheus/queries/matched_metrics_query.rb2
-rw-r--r--lib/gitlab/prometheus/queries/query_additional_metrics.rb8
-rw-r--r--lib/gitlab/prometheus_client.rb66
-rw-r--r--lib/gitlab/query_limiting.rb2
-rw-r--r--lib/gitlab/query_limiting/active_support_subscriber.rb6
-rw-r--r--lib/gitlab/query_limiting/transaction.rb8
-rw-r--r--lib/gitlab/quick_actions/command_definition.rb15
-rw-r--r--lib/gitlab/quick_actions/dsl.rb5
-rw-r--r--lib/gitlab/quick_actions/extractor.rb10
-rw-r--r--lib/gitlab/regex.rb8
-rw-r--r--lib/gitlab/saml/auth_hash.rb17
-rw-r--r--lib/gitlab/saml/config.rb19
-rw-r--r--lib/gitlab/saml/user.rb50
-rw-r--r--lib/gitlab/search_results.rb7
-rw-r--r--lib/gitlab/setup_helper.rb2
-rw-r--r--lib/gitlab/shell.rb12
-rw-r--r--lib/gitlab/sidekiq_middleware/memory_killer.rb67
-rw-r--r--lib/gitlab/sidekiq_middleware/shutdown.rb133
-rw-r--r--lib/gitlab/slash_commands/base_command.rb7
-rw-r--r--lib/gitlab/slash_commands/command.rb5
-rw-r--r--lib/gitlab/sql/pattern.rb14
-rw-r--r--lib/gitlab/ssh_public_key.rb2
-rw-r--r--lib/gitlab/usage_data.rb5
-rw-r--r--lib/gitlab/user_access.rb4
-rw-r--r--lib/google_api/cloud_platform/client.rb6
-rw-r--r--lib/haml_lint/inline_javascript.rb6
-rw-r--r--lib/tasks/gemojione.rake2
-rw-r--r--lib/tasks/gitlab/check.rake6
-rw-r--r--lib/tasks/gitlab/cleanup.rake2
-rw-r--r--lib/tasks/gitlab/gitaly.rake4
-rw-r--r--lib/tasks/lint.rake49
-rw-r--r--lib/tasks/migrate/setup_postgresql.rake2
-rw-r--r--lib/tasks/plugins.rake16
169 files changed, 4307 insertions, 2467 deletions
diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb
index 60ae5e6b9a2..ae13c248171 100644
--- a/lib/api/access_requests.rb
+++ b/lib/api/access_requests.rb
@@ -53,7 +53,10 @@ module API
put ':id/access_requests/:user_id/approve' do
source = find_source(source_type, params[:id])
- member = ::Members::ApproveAccessRequestService.new(source, current_user, declared_params).execute
+ access_requester = source.requesters.find_by!(user_id: params[:user_id])
+ member = ::Members::ApproveAccessRequestService
+ .new(current_user, declared_params)
+ .execute(access_requester)
status :created
present member, with: Entities::Member
@@ -70,8 +73,7 @@ module API
member = source.requesters.find_by!(user_id: params[:user_id])
destroy_conditionally!(member) do
- ::Members::DestroyService.new(source, current_user, params)
- .execute(:requesters)
+ ::Members::DestroyService.new(current_user).execute(member)
end
end
end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index e953f3d2eca..754549f72f0 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -138,6 +138,7 @@ module API
mount ::API::PagesDomains
mount ::API::Pipelines
mount ::API::PipelineSchedules
+ mount ::API::ProjectImport
mount ::API::ProjectHooks
mount ::API::Projects
mount ::API::ProjectMilestones
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index d8fd6a6eb06..982f45425a3 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -18,25 +18,28 @@ module API
optional :since, type: DateTime, desc: 'Only commits after or on this date will be returned'
optional :until, type: DateTime, desc: 'Only commits before or on this date will be returned'
optional :path, type: String, desc: 'The file path'
+ optional :all, type: Boolean, desc: 'Every commit will be returned'
use :pagination
end
get ':id/repository/commits' do
path = params[:path]
before = params[:until]
after = params[:since]
- ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
+ ref = params[:ref_name] || user_project.try(:default_branch) || 'master' unless params[:all]
offset = (params[:page] - 1) * params[:per_page]
+ all = params[:all]
commits = user_project.repository.commits(ref,
path: path,
limit: params[:per_page],
offset: offset,
before: before,
- after: after)
+ after: after,
+ all: all)
commit_count =
- if path || before || after
- user_project.repository.count_commits(ref: ref, path: path, before: before, after: after)
+ if all || path || before || after
+ user_project.repository.count_commits(ref: ref, path: path, before: before, after: after, all: all)
else
# Cacheable commit count.
user_project.repository.commit_count_for_ref(ref)
@@ -97,13 +100,16 @@ module API
end
params do
requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag'
+ use :pagination
end
get ':id/repository/commits/:sha/diff', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
commit = user_project.commit(params[:sha])
not_found! 'Commit' unless commit
- present commit.raw_diffs.to_a, with: Entities::Diff
+ raw_diffs = ::Kaminari.paginate_array(commit.raw_diffs.to_a)
+
+ present paginate(raw_diffs), with: Entities::Diff
end
desc "Get a commit's comments" do
@@ -156,6 +162,27 @@ module API
end
end
+ desc 'Get all references a commit is pushed to' do
+ detail 'This feature was introduced in GitLab 10.6'
+ success Entities::BasicRef
+ end
+ params do
+ requires :sha, type: String, desc: 'A commit sha'
+ optional :type, type: String, values: %w[branch tag all], default: 'all', desc: 'Scope'
+ use :pagination
+ end
+ get ':id/repository/commits/:sha/refs', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
+ commit = user_project.commit(params[:sha])
+ not_found!('Commit') unless commit
+
+ refs = []
+ refs.concat(user_project.repository.branch_names_contains(commit.id).map {|name| { type: 'branch', name: name }}) unless params[:type] == 'tag'
+ refs.concat(user_project.repository.tag_names_contains(commit.id).map {|name| { type: 'tag', name: name }}) unless params[:type] == 'branch'
+ refs = Kaminari.paginate_array(refs)
+
+ present paginate(refs), with: Entities::BasicRef
+ end
+
desc 'Post comment to commit' do
success Entities::CommitNote
end
@@ -165,7 +192,7 @@ module API
optional :path, type: String, desc: 'The file path'
given :path do
requires :line, type: Integer, desc: 'The line number'
- requires :line_type, type: String, values: %w(new old), default: 'new', desc: 'The type of the line'
+ requires :line_type, type: String, values: %w[new old], default: 'new', desc: 'The type of the line'
end
end
post ':id/repository/commits/:sha/comments', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 7838de13c56..c88fcf9472e 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -22,6 +22,7 @@ module API
end
expose :avatar_path, if: ->(user, options) { options.fetch(:only_path, false) && user.avatar_path }
+ expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes
expose :web_url do |user, options|
Gitlab::Routing.url_helpers.user_url(user)
@@ -70,7 +71,7 @@ module API
end
class ProjectHook < Hook
- expose :project_id, :issues_events
+ expose :project_id, :issues_events, :confidential_issues_events
expose :note_events, :pipeline_events, :wiki_page_events
expose :job_events
end
@@ -90,6 +91,13 @@ module API
expose :created_at
end
+ class ProjectImportStatus < ProjectIdentity
+ expose :import_status
+
+ # TODO: Use `expose_nil` once we upgrade the grape-entity gem
+ expose :import_error, if: lambda { |status, _ops| status.import_error }
+ end
+
class BasicProjectDetails < ProjectIdentity
include ::API::ProjectsRelationBuilder
@@ -109,6 +117,8 @@ module API
expose :star_count, :forks_count
expose :last_activity_at
+ expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes
+
def self.preload_relation(projects_relation, options = {})
projects_relation.preload(:project_feature, :route)
.preload(namespace: [:route, :owner],
@@ -230,6 +240,8 @@ module API
expose :parent_id
end
+ expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes
+
expose :statistics, if: :statistics do
with_options format_with: -> (value) { value.to_i } do
expose :storage_size
@@ -274,6 +286,11 @@ module API
expose :stats, using: Entities::CommitStats, if: :stats
expose :status
expose :last_pipeline, using: 'API::Entities::PipelineBasic'
+ expose :project_id
+ end
+
+ class BasicRef < Grape::Entity
+ expose :type, :name
end
class Branch < Grape::Entity
@@ -1137,6 +1154,10 @@ module API
expose :domain
expose :url
expose :project_id
+ expose :verified?, as: :verified
+ expose :verification_code, as: :verification_code
+ expose :enabled_until
+
expose :certificate,
as: :certificate_expiration,
if: ->(pages_domain, _) { pages_domain.certificate? },
@@ -1148,6 +1169,10 @@ module API
class PagesDomain < Grape::Entity
expose :domain
expose :url
+ expose :verified?, as: :verified
+ expose :verification_code, as: :verification_code
+ expose :enabled_until
+
expose :certificate,
if: ->(pages_domain, _) { pages_domain.certificate? },
using: PagesDomainCertificate do |pages_domain|
@@ -1172,6 +1197,7 @@ module API
expose :id
expose :ref
expose :startline
+ expose :project_id
end
end
end
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index b81f07a1770..4a4df1b8b9e 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -1,6 +1,7 @@
module API
class Groups < Grape::API
include PaginationParams
+ include Helpers::CustomAttributes
before { authenticate_non_get! }
@@ -67,6 +68,8 @@ module API
}
groups = groups.with_statistics if options[:statistics]
+ groups, options = with_custom_attributes(groups, options)
+
present paginate(groups), options
end
end
@@ -79,6 +82,7 @@ module API
end
params do
use :group_list_params
+ use :with_custom_attributes
end
get do
groups = find_groups(params)
@@ -142,9 +146,20 @@ module API
desc 'Get a single group, with containing projects.' do
success Entities::GroupDetail
end
+ params do
+ use :with_custom_attributes
+ end
get ":id" do
group = find_group!(params[:id])
- present group, with: Entities::GroupDetail, current_user: current_user
+
+ options = {
+ with: Entities::GroupDetail,
+ current_user: current_user
+ }
+
+ group, options = with_custom_attributes(group, options)
+
+ present group, options
end
desc 'Remove a group.'
@@ -175,12 +190,19 @@ module API
optional :starred, type: Boolean, default: false, desc: 'Limit by starred status'
use :pagination
+ use :with_custom_attributes
end
get ":id/projects" do
projects = find_group_projects(params)
- entity = params[:simple] ? Entities::BasicProjectDetails : Entities::Project
- present entity.prepare_relation(projects), with: entity, current_user: current_user
+ options = {
+ with: params[:simple] ? Entities::BasicProjectDetails : Entities::Project,
+ current_user: current_user
+ }
+
+ projects, options = with_custom_attributes(projects, options)
+
+ present options[:with].prepare_relation(projects), options
end
desc 'Get a list of subgroups in this group.' do
@@ -188,6 +210,7 @@ module API
end
params do
use :group_list_params
+ use :with_custom_attributes
end
get ":id/subgroups" do
groups = find_groups(params)
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 6134ad2bfc7..e4fca77ab5d 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -172,7 +172,7 @@ module API
def find_project_snippet(id)
finder_params = { project: user_project }
- SnippetsFinder.new(current_user, finder_params).execute.find(id)
+ SnippetsFinder.new(current_user, finder_params).find(id)
end
def find_merge_request_with_access(iid, access_level = :read_merge_request)
diff --git a/lib/api/helpers/custom_attributes.rb b/lib/api/helpers/custom_attributes.rb
new file mode 100644
index 00000000000..70e4eda95f8
--- /dev/null
+++ b/lib/api/helpers/custom_attributes.rb
@@ -0,0 +1,28 @@
+module API
+ module Helpers
+ module CustomAttributes
+ extend ActiveSupport::Concern
+
+ included do
+ helpers do
+ params :with_custom_attributes do
+ optional :with_custom_attributes, type: Boolean, default: false, desc: 'Include custom attributes in the response'
+ end
+
+ def with_custom_attributes(collection_or_resource, options = {})
+ options = options.merge(
+ with_custom_attributes: params[:with_custom_attributes] &&
+ can?(current_user, :read_custom_attribute)
+ )
+
+ if options[:with_custom_attributes] && collection_or_resource.is_a?(ActiveRecord::Relation)
+ collection_or_resource = collection_or_resource.includes(:custom_attributes)
+ end
+
+ [collection_or_resource, options]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb
index fbe30192a16..35ac0b4cbca 100644
--- a/lib/api/helpers/runner.rb
+++ b/lib/api/helpers/runner.rb
@@ -9,16 +9,22 @@ module API
Gitlab::CurrentSettings.runners_registration_token)
end
- def get_runner_version_from_params
- return unless params['info'].present?
+ def authenticate_runner!
+ forbidden! unless current_runner
- attributes_for_keys(%w(name version revision platform architecture), params['info'])
+ current_runner
+ .update_cached_info(get_runner_details_from_request)
end
- def authenticate_runner!
- forbidden! unless current_runner
+ def get_runner_details_from_request
+ return get_runner_ip unless params['info'].present?
+
+ attributes_for_keys(%w(name version revision platform architecture), params['info'])
+ .merge(get_runner_ip)
+ end
- current_runner.update_cached_info(get_runner_version_from_params)
+ def get_runner_ip
+ { ip_address: request.ip }
end
def current_runner
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 9285fb90cdc..b3660e4a1d0 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -13,7 +13,7 @@ module API
# key_id - ssh key id for Git over SSH
# user_id - user id for Git over HTTP
# protocol - Git access protocol being used, e.g. HTTP or SSH
- # project - project path with namespace
+ # project - project full_path (not path on disk)
# action - git action (git-upload-pack or git-receive-pack)
# changes - changes as "oldrev newrev ref", see Gitlab::ChangesList
post "/allowed" do
diff --git a/lib/api/members.rb b/lib/api/members.rb
index bc1de37284a..8b12986d09e 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -81,12 +81,16 @@ module API
source = find_source(source_type, params.delete(:id))
authorize_admin_source!(source_type, source)
- member = source.members.find_by!(user_id: params.delete(:user_id))
+ member = source.members.find_by!(user_id: params[:user_id])
+ updated_member =
+ ::Members::UpdateService
+ .new(current_user, declared_params(include_missing: false))
+ .execute(member)
- if member.update_attributes(declared_params(include_missing: false))
- present member, with: Entities::Member
+ if updated_member.valid?
+ present updated_member, with: Entities::Member
else
- render_validation_error!(member)
+ render_validation_error!(updated_member)
end
end
@@ -99,7 +103,7 @@ module API
member = source.members.find_by!(user_id: params[:user_id])
destroy_conditionally!(member) do
- ::Members::DestroyService.new(source, current_user, declared_params).execute
+ ::Members::DestroyService.new(current_user).execute(member)
end
end
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 719afa09295..4ffd4895c7e 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -48,6 +48,8 @@ module API
optional :scope, type: String, values: %w[created-by-me assigned-to-me all],
desc: 'Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all`'
optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji'
+ optional :source_branch, type: String, desc: 'Return merge requests with the given source branch'
+ optional :target_branch, type: String, desc: 'Return merge requests with the given target branch'
optional :search, type: String, desc: 'Search merge requests for text present in the title or description'
use :pagination
end
diff --git a/lib/api/pages_domains.rb b/lib/api/pages_domains.rb
index d7b613a717e..ba33993d852 100644
--- a/lib/api/pages_domains.rb
+++ b/lib/api/pages_domains.rb
@@ -2,6 +2,8 @@ module API
class PagesDomains < Grape::API
include PaginationParams
+ PAGES_DOMAINS_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(domain: API::NO_SLASH_URL_PART_REGEX)
+
before do
authenticate!
end
@@ -48,7 +50,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
before do
require_pages_enabled!
end
@@ -71,7 +73,7 @@ module API
params do
requires :domain, type: String, desc: 'The domain'
end
- get ":id/pages/domains/:domain", requirements: { domain: %r{[^/]+} } do
+ get ":id/pages/domains/:domain", requirements: PAGES_DOMAINS_ENDPOINT_REQUIREMENTS do
authorize! :read_pages, user_project
present pages_domain, with: Entities::PagesDomain
@@ -105,7 +107,7 @@ module API
optional :certificate, allow_blank: false, types: [File, String], desc: 'The certificate'
optional :key, allow_blank: false, types: [File, String], desc: 'The key'
end
- put ":id/pages/domains/:domain", requirements: { domain: %r{[^/]+} } do
+ put ":id/pages/domains/:domain", requirements: PAGES_DOMAINS_ENDPOINT_REQUIREMENTS do
authorize! :update_pages, user_project
pages_domain_params = declared(params, include_parent_namespaces: false)
@@ -126,7 +128,7 @@ module API
params do
requires :domain, type: String, desc: 'The domain'
end
- delete ":id/pages/domains/:domain", requirements: { domain: %r{[^/]+} } do
+ delete ":id/pages/domains/:domain", requirements: PAGES_DOMAINS_ENDPOINT_REQUIREMENTS do
authorize! :update_pages, user_project
status 204
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index 86066e2b58f..f82241058e5 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -10,6 +10,7 @@ module API
requires :url, type: String, desc: "The URL to send the request to"
optional :push_events, type: Boolean, desc: "Trigger hook on push events"
optional :issues_events, type: Boolean, desc: "Trigger hook on issues events"
+ optional :confidential_issues_events, type: Boolean, desc: "Trigger hook on confidential issues events"
optional :merge_requests_events, type: Boolean, desc: "Trigger hook on merge request events"
optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events"
optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events"
diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb
new file mode 100644
index 00000000000..a509c1f32c1
--- /dev/null
+++ b/lib/api/project_import.rb
@@ -0,0 +1,69 @@
+module API
+ class ProjectImport < Grape::API
+ include PaginationParams
+
+ helpers do
+ def import_params
+ declared_params(include_missing: false)
+ end
+
+ def file_is_valid?
+ import_params[:file] && import_params[:file]['tempfile'].respond_to?(:read)
+ end
+
+ def validate_file!
+ render_api_error!('The file is invalid', 400) unless file_is_valid?
+ end
+ end
+
+ before do
+ forbidden! unless Gitlab::CurrentSettings.import_sources.include?('gitlab_project')
+ end
+
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ params do
+ requires :path, type: String, desc: 'The new project path and name'
+ requires :file, type: File, desc: 'The project export file to be imported'
+ optional :namespace, type: String, desc: "The ID or name of the namespace that the project will be imported into. Defaults to the current user's namespace."
+ end
+ desc 'Create a new project import' do
+ detail 'This feature was introduced in GitLab 10.6.'
+ success Entities::ProjectImportStatus
+ end
+ post 'import' do
+ validate_file!
+
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42437')
+
+ namespace = if import_params[:namespace]
+ find_namespace!(import_params[:namespace])
+ else
+ current_user.namespace
+ end
+
+ project_params = {
+ path: import_params[:path],
+ namespace_id: namespace.id,
+ file: import_params[:file]['tempfile']
+ }
+
+ project = ::Projects::GitlabProjectsImportService.new(current_user, project_params).execute
+
+ render_api_error!(project.errors.full_messages&.first, 400) unless project.saved?
+
+ present project, with: Entities::ProjectImportStatus
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ desc 'Get a project export status' do
+ detail 'This feature was introduced in GitLab 10.6.'
+ success Entities::ProjectImportStatus
+ end
+ get ':id/import' do
+ present user_project, with: Entities::ProjectImportStatus
+ end
+ end
+ end
+end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 5b481121a10..b552b0e0c5d 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -3,6 +3,7 @@ require_dependency 'declarative_policy'
module API
class Projects < Grape::API
include PaginationParams
+ include Helpers::CustomAttributes
before { authenticate_non_get! }
@@ -80,6 +81,7 @@ module API
projects = projects.with_merge_requests_enabled if params[:with_merge_requests_enabled]
projects = projects.with_statistics if params[:statistics]
projects = paginate(projects)
+ projects, options = with_custom_attributes(projects, options)
if current_user
project_members = current_user.project_members.preload(:source, user: [notification_settings: :source])
@@ -107,6 +109,7 @@ module API
requires :user_id, type: String, desc: 'The ID or username of the user'
use :collection_params
use :statistics_params
+ use :with_custom_attributes
end
get ":user_id/projects" do
user = find_user(params[:user_id])
@@ -127,6 +130,7 @@ module API
params do
use :collection_params
use :statistics_params
+ use :with_custom_attributes
end
get do
present_projects load_projects
@@ -196,11 +200,19 @@ module API
end
params do
use :statistics_params
+ use :with_custom_attributes
end
get ":id" do
- entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails
- present user_project, with: entity, current_user: current_user,
- user_can_admin_project: can?(current_user, :admin_project, user_project), statistics: params[:statistics]
+ options = {
+ with: current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails,
+ current_user: current_user,
+ user_can_admin_project: can?(current_user, :admin_project, user_project),
+ statistics: params[:statistics]
+ }
+
+ project, options = with_custom_attributes(user_project, options)
+
+ present project, options
end
desc 'Fork new project for the current user or provided namespace.' do
@@ -242,6 +254,7 @@ module API
end
params do
use :collection_params
+ use :with_custom_attributes
end
get ':id/forks' do
forks = ForkProjectsFinder.new(user_project, params: project_finder_params, current_user: current_user).execute
@@ -258,6 +271,7 @@ module API
[
:jobs_enabled,
:resolve_outdated_diff_discussions,
+ :ci_config_path,
:container_registry_enabled,
:default_branch,
:description,
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index 5469cba69a6..91cdc564002 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -16,7 +16,8 @@ module API
optional :tag_list, type: Array[String], desc: %q(List of Runner's tags)
end
post '/' do
- attributes = attributes_for_keys [:description, :locked, :run_untagged, :tag_list]
+ attributes = attributes_for_keys([:description, :locked, :run_untagged, :tag_list])
+ .merge(get_runner_details_from_request)
runner =
if runner_registration_token_valid?
@@ -30,7 +31,6 @@ module API
return forbidden! unless runner
if runner.id
- runner.update(get_runner_version_from_params)
present runner, with: Entities::RunnerRegistrationDetails
else
not_found!
diff --git a/lib/api/search.rb b/lib/api/search.rb
index 9f08fd96a3b..3556ad98c52 100644
--- a/lib/api/search.rb
+++ b/lib/api/search.rb
@@ -11,7 +11,7 @@ module API
projects: Entities::BasicProjectDetails,
milestones: Entities::Milestone,
notes: Entities::Note,
- commits: Entities::Commit,
+ commits: Entities::CommitDetail,
blobs: Entities::Blob,
wiki_blobs: Entities::Blob,
snippet_titles: Entities::Snippet,
@@ -35,7 +35,7 @@ module API
def process_results(results)
case params[:scope]
when 'wiki_blobs'
- paginate(results).map { |blob| Gitlab::ProjectSearchResults.parse_search_result(blob) }
+ paginate(results).map { |blob| Gitlab::ProjectSearchResults.parse_search_result(blob, user_project) }
when 'blobs'
paginate(results).map { |blob| blob[1] }
else
@@ -85,9 +85,7 @@ module API
use :pagination
end
get ':id/-/search' do
- find_group!(params[:id])
-
- present search(group_id: params[:id]), with: entity
+ present search(group_id: user_group.id), with: entity
end
end
@@ -106,9 +104,7 @@ module API
use :pagination
end
get ':id/-/search' do
- find_project!(params[:id])
-
- present search(project_id: params[:id]), with: entity
+ present search(project_id: user_project.id), with: entity
end
end
end
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 51e33e2c686..6c97659166d 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -1,6 +1,7 @@
+# frozen_string_literal: true
module API
class Services < Grape::API
- chat_notification_settings = [
+ CHAT_NOTIFICATION_SETTINGS = [
{
required: true,
name: :webhook,
@@ -19,9 +20,9 @@ module API
type: String,
desc: 'The default chat channel'
}
- ]
+ ].freeze
- chat_notification_flags = [
+ CHAT_NOTIFICATION_FLAGS = [
{
required: false,
name: :notify_only_broken_pipelines,
@@ -34,9 +35,9 @@ module API
type: Boolean,
desc: 'Send notifications only for the default branch'
}
- ]
+ ].freeze
- chat_notification_channels = [
+ CHAT_NOTIFICATION_CHANNELS = [
{
required: false,
name: :push_channel,
@@ -85,9 +86,9 @@ module API
type: String,
desc: 'The name of the channel to receive wiki_page_events notifications'
}
- ]
+ ].freeze
- chat_notification_events = [
+ CHAT_NOTIFICATION_EVENTS = [
{
required: false,
name: :push_events,
@@ -136,7 +137,7 @@ module API
type: Boolean,
desc: 'Enable notifications for wiki_page_events'
}
- ]
+ ].freeze
services = {
'asana' => [
@@ -627,10 +628,10 @@ module API
}
],
'slack' => [
- chat_notification_settings,
- chat_notification_flags,
- chat_notification_channels,
- chat_notification_events
+ CHAT_NOTIFICATION_SETTINGS,
+ CHAT_NOTIFICATION_FLAGS,
+ CHAT_NOTIFICATION_CHANNELS,
+ CHAT_NOTIFICATION_EVENTS
].flatten,
'microsoft-teams' => [
{
@@ -641,10 +642,10 @@ module API
}
],
'mattermost' => [
- chat_notification_settings,
- chat_notification_flags,
- chat_notification_channels,
- chat_notification_events
+ CHAT_NOTIFICATION_SETTINGS,
+ CHAT_NOTIFICATION_FLAGS,
+ CHAT_NOTIFICATION_CHANNELS,
+ CHAT_NOTIFICATION_EVENTS
].flatten,
'teamcity' => [
{
@@ -724,7 +725,22 @@ module API
]
end
- trigger_services = {
+ SERVICES = services.freeze
+ SERVICE_CLASSES = service_classes.freeze
+
+ SERVICE_CLASSES.each do |service|
+ event_names = service.try(:event_names) || next
+ event_names.each do |event_name|
+ SERVICES[service.to_param.tr("_", "-")] << {
+ required: false,
+ name: event_name.to_sym,
+ type: String,
+ desc: ServicesHelper.service_event_description(event_name)
+ }
+ end
+ end
+
+ TRIGGER_SERVICES = {
'mattermost-slash-commands' => [
{
name: :token,
@@ -756,22 +772,9 @@ module API
end
end
- services.each do |service_slug, settings|
+ SERVICES.each do |service_slug, settings|
desc "Set #{service_slug} service for project"
params do
- service_classes.each do |service|
- event_names = service.try(:event_names) || next
- event_names.each do |event_name|
- services[service.to_param.tr("_", "-")] << {
- required: false,
- name: event_name.to_sym,
- type: String,
- desc: ServicesHelper.service_event_description(event_name)
- }
- end
- end
- services.freeze
-
settings.each do |setting|
if setting[:required]
requires setting[:name], type: setting[:type], desc: setting[:desc]
@@ -794,7 +797,7 @@ module API
desc "Delete a service for project"
params do
- requires :service_slug, type: String, values: services.keys, desc: 'The name of the service'
+ requires :service_slug, type: String, values: SERVICES.keys, desc: 'The name of the service'
end
delete ":id/services/:service_slug" do
service = user_project.find_or_initialize_service(params[:service_slug].underscore)
@@ -814,7 +817,7 @@ module API
success Entities::ProjectService
end
params do
- requires :service_slug, type: String, values: services.keys, desc: 'The name of the service'
+ requires :service_slug, type: String, values: SERVICES.keys, desc: 'The name of the service'
end
get ":id/services/:service_slug" do
service = user_project.find_or_initialize_service(params[:service_slug].underscore)
@@ -822,7 +825,7 @@ module API
end
end
- trigger_services.each do |service_slug, settings|
+ TRIGGER_SERVICES.each do |service_slug, settings|
helpers do
def slash_command_service(project, service_slug, params)
project.services.active.where(template: false).find do |service|
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index cee4d309816..152df23a327 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -147,7 +147,7 @@ module API
attrs[:password_authentication_enabled_for_web] = attrs.delete(:password_authentication_enabled)
end
- if current_settings.update_attributes(attrs)
+ if ApplicationSettings::UpdateService.new(current_settings, current_user, attrs).execute
present current_settings, with: Entities::ApplicationSetting
else
render_validation_error!(current_settings)
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
index ffccfebe752..c6dbcf84e3a 100644
--- a/lib/api/todos.rb
+++ b/lib/api/todos.rb
@@ -60,7 +60,7 @@ module API
end
post ':id/mark_as_done' do
TodoService.new.mark_todos_as_done_by_ids(params[:id], current_user)
- todo = Todo.find(params[:id])
+ todo = current_user.todos.find(params[:id])
present todo, with: Entities::Todo, current_user: current_user
end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 3cc12724b8a..3920171205f 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -2,6 +2,7 @@ module API
class Users < Grape::API
include PaginationParams
include APIGuard
+ include Helpers::CustomAttributes
allow_access_with_scope :read_user, if: -> (request) { request.get? }
@@ -70,6 +71,7 @@ module API
use :sort_params
use :pagination
+ use :with_custom_attributes
end
get do
authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?)
@@ -94,8 +96,9 @@ module API
entity = current_user&.admin? ? Entities::UserWithAdmin : Entities::UserBasic
users = users.preload(:identities, :u2f_registrations) if entity == Entities::UserWithAdmin
+ users, options = with_custom_attributes(users, with: entity)
- present paginate(users), with: entity
+ present paginate(users), options
end
desc 'Get a single user' do
@@ -103,12 +106,16 @@ module API
end
params do
requires :id, type: Integer, desc: 'The ID of the user'
+
+ use :with_custom_attributes
end
get ":id" do
user = User.find_by(id: params[:id])
not_found!('User') unless user && can?(current_user, :read_user, user)
opts = current_user&.admin? ? { with: Entities::UserWithAdmin } : { with: Entities::User }
+ user, opts = with_custom_attributes(user, opts)
+
present user, opts
end
diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb
index 2ccbb9da1c5..68b4d7c3982 100644
--- a/lib/api/v3/entities.rb
+++ b/lib/api/v3/entities.rb
@@ -252,8 +252,9 @@ module API
class ProjectService < Grape::Entity
expose :id, :title, :created_at, :updated_at, :active
- expose :push_events, :issues_events, :merge_requests_events
- expose :tag_push_events, :note_events, :pipeline_events
+ expose :push_events, :issues_events, :confidential_issues_events
+ expose :merge_requests_events, :tag_push_events, :note_events
+ expose :pipeline_events
expose :job_events, as: :build_events
# Expose serialized properties
expose :properties do |service, options|
@@ -262,8 +263,9 @@ module API
end
class ProjectHook < ::API::Entities::Hook
- expose :project_id, :issues_events, :merge_requests_events
- expose :note_events, :pipeline_events, :wiki_page_events
+ expose :project_id, :issues_events, :confidential_issues_events
+ expose :merge_requests_events, :note_events, :pipeline_events
+ expose :wiki_page_events
expose :job_events, as: :build_events
end
diff --git a/lib/api/v3/members.rb b/lib/api/v3/members.rb
index d7bde8ceb89..88dd598f1e9 100644
--- a/lib/api/v3/members.rb
+++ b/lib/api/v3/members.rb
@@ -124,7 +124,7 @@ module API
status(200 )
{ message: "Access revoked", id: params[:user_id].to_i }
else
- ::Members::DestroyService.new(source, current_user, declared_params).execute
+ ::Members::DestroyService.new(current_user).execute(member)
present member, with: ::API::Entities::Member
end
diff --git a/lib/api/v3/project_hooks.rb b/lib/api/v3/project_hooks.rb
index 51014591a93..631944150c7 100644
--- a/lib/api/v3/project_hooks.rb
+++ b/lib/api/v3/project_hooks.rb
@@ -11,6 +11,7 @@ module API
requires :url, type: String, desc: "The URL to send the request to"
optional :push_events, type: Boolean, desc: "Trigger hook on push events"
optional :issues_events, type: Boolean, desc: "Trigger hook on issues events"
+ optional :confidential_issues_events, type: Boolean, desc: "Trigger hook on confidential issues events"
optional :merge_requests_events, type: Boolean, desc: "Trigger hook on merge request events"
optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events"
optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events"
diff --git a/lib/api/v3/todos.rb b/lib/api/v3/todos.rb
index 2f2cf259987..3e2c61f6dbd 100644
--- a/lib/api/v3/todos.rb
+++ b/lib/api/v3/todos.rb
@@ -12,7 +12,7 @@ module API
end
delete ':id' do
TodoService.new.mark_todos_as_done_by_ids(params[:id], current_user)
- todo = Todo.find(params[:id])
+ todo = current_user.todos.find(params[:id])
present todo, with: ::API::Entities::Todo, current_user: current_user
end
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index e7e6a90b5fd..c9e3f8ce42b 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -174,7 +174,9 @@ module Banzai
title = object_link_title(object)
klass = reference_class(object_sym)
- data = data_attributes_for(link_content || match, parent, object, link: !!link_content)
+ data = data_attributes_for(link_content || match, parent, object,
+ link_content: !!link_content,
+ link_reference: link_reference)
url =
if matches.names.include?("url") && matches[:url]
@@ -194,12 +196,13 @@ module Banzai
end
end
- def data_attributes_for(text, project, object, link: false)
+ def data_attributes_for(text, project, object, link_content: false, link_reference: false)
data_attribute(
- original: text,
- link: link,
- project: project.id,
- object_sym => object.id
+ original: text,
+ link: link_content,
+ link_reference: link_reference,
+ project: project.id,
+ object_sym => object.id
)
end
diff --git a/lib/banzai/filter/html_entity_filter.rb b/lib/banzai/filter/html_entity_filter.rb
index f3bd587c28b..e008fd428b0 100644
--- a/lib/banzai/filter/html_entity_filter.rb
+++ b/lib/banzai/filter/html_entity_filter.rb
@@ -5,7 +5,7 @@ module Banzai
# Text filter that escapes these HTML entities: & " < >
class HtmlEntityFilter < HTML::Pipeline::TextFilter
def call
- ERB::Util.html_escape_once(text)
+ ERB::Util.html_escape(text)
end
end
end
diff --git a/lib/banzai/filter/issuable_state_filter.rb b/lib/banzai/filter/issuable_state_filter.rb
index 327ea9449a1..77299abe324 100644
--- a/lib/banzai/filter/issuable_state_filter.rb
+++ b/lib/banzai/filter/issuable_state_filter.rb
@@ -15,6 +15,8 @@ module Banzai
issuables = extractor.extract([doc])
issuables.each do |node, issuable|
+ next if !can_read_cross_project? && issuable.project != project
+
if VISIBLE_STATES.include?(issuable.state) && node.inner_html == issuable.reference_link_text(project)
node.content += " (#{issuable.state})"
end
@@ -25,6 +27,10 @@ module Banzai
private
+ def can_read_cross_project?
+ Ability.allowed?(current_user, :read_cross_project)
+ end
+
def current_user
context[:current_user]
end
diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb
index 2a6b0964ac5..8ec696ce5fc 100644
--- a/lib/banzai/filter/milestone_reference_filter.rb
+++ b/lib/banzai/filter/milestone_reference_filter.rb
@@ -64,7 +64,7 @@ module Banzai
finder_params[:group_ids] = [project.group.id]
end
- MilestonesFinder.new(finder_params).execute.find_by(params)
+ MilestonesFinder.new(finder_params).find_by(params)
end
def url_for_object(milestone, project)
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index a79a0154846..0ac7e231b5b 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -14,23 +14,33 @@ module Banzai
end
def highlight_node(node)
- code = node.text
css_classes = 'code highlight js-syntax-highlight'
- language = node.attr('lang')
+ lang = node.attr('lang')
+ retried = false
- if use_rouge?(language)
- lexer = lexer_for(language)
+ if use_rouge?(lang)
+ lexer = lexer_for(lang)
language = lexer.tag
+ else
+ lexer = Rouge::Lexers::PlainText.new
+ language = lang
+ end
+
+ begin
+ code = Rouge::Formatters::HTMLGitlab.format(lex(lexer, node.text), tag: language)
+ css_classes << " #{language}" if language
+ rescue
+ # Gracefully handle syntax highlighter bugs/errors to ensure users can
+ # still access an issue/comment/etc. First, retry with the plain text
+ # filter. If that fails, then just skip this entirely, but that would
+ # be a pretty bad upstream bug.
+ return if retried
- begin
- code = Rouge::Formatters::HTMLGitlab.format(lex(lexer, code), tag: language)
- css_classes << " #{language}"
- rescue
- # Gracefully handle syntax highlighter bugs/errors to ensure
- # users can still access an issue/comment/etc.
+ language = nil
+ lexer = Rouge::Lexers::PlainText.new
+ retried = true
- language = nil
- end
+ retry
end
highlighted = %(<pre class="#{css_classes}" lang="#{language}" v-pre="true"><code>#{code}</code></pre>)
diff --git a/lib/banzai/redactor.rb b/lib/banzai/redactor.rb
index de3ebe72720..fd457bebf03 100644
--- a/lib/banzai/redactor.rb
+++ b/lib/banzai/redactor.rb
@@ -19,8 +19,9 @@ module Banzai
#
# Returns the documents passed as the first argument.
def redact(documents)
- all_document_nodes = document_nodes(documents)
+ redact_cross_project_references(documents) unless can_read_cross_project?
+ all_document_nodes = document_nodes(documents)
redact_document_nodes(all_document_nodes)
end
@@ -41,16 +42,45 @@ module Banzai
next if visible.include?(node)
doc_data[:visible_reference_count] -= 1
- # The reference should be replaced by the original link's content,
- # which is not always the same as the rendered one.
- content = node.attr('data-original') || node.inner_html
- node.replace(content)
+ redacted_content = redacted_node_content(node)
+ node.replace(redacted_content)
end
end
metadata
end
+ # Return redacted content of given node as either the original link (<a> tag),
+ # the original content (text), or the inner HTML of the node.
+ #
+ def redacted_node_content(node)
+ original_content = node.attr('data-original')
+ link_reference = node.attr('data-link-reference')
+
+ # Build the raw <a> tag just with a link as href and content if
+ # it's originally a link pattern. We shouldn't return a plain text href.
+ original_link =
+ if link_reference == 'true' && href = original_content
+ %(<a href="#{href}">#{href}</a>)
+ end
+
+ # The reference should be replaced by the original link's content,
+ # which is not always the same as the rendered one.
+ original_link || original_content || node.inner_html
+ end
+
+ def redact_cross_project_references(documents)
+ extractor = Banzai::IssuableExtractor.new(project, user)
+ issuables = extractor.extract(documents)
+
+ issuables.each do |node, issuable|
+ next if issuable.project == project
+
+ node['class'] = node['class'].gsub('has-tooltip', '')
+ node['title'] = nil
+ end
+ end
+
# Returns the nodes visible to the current user.
#
# nodes - The input nodes to check.
@@ -78,5 +108,11 @@ module Banzai
{ document: document, nodes: Querying.css(document, 'a.gfm[data-reference-type]') }
end
end
+
+ private
+
+ def can_read_cross_project?
+ Ability.allowed?(user, :read_cross_project)
+ end
end
end
diff --git a/lib/banzai/reference_parser/issuable_parser.rb b/lib/banzai/reference_parser/issuable_parser.rb
index 3953867eb83..fad127d7e5b 100644
--- a/lib/banzai/reference_parser/issuable_parser.rb
+++ b/lib/banzai/reference_parser/issuable_parser.rb
@@ -18,7 +18,7 @@ module Banzai
end
def can_read_reference?(user, issuable)
- can?(user, "read_#{issuable.class.to_s.underscore}".to_sym, issuable)
+ can?(user, "read_#{issuable.class.to_s.underscore}_iid".to_sym, issuable)
end
end
end
diff --git a/lib/banzai/reference_parser/issue_parser.rb b/lib/banzai/reference_parser/issue_parser.rb
index 38d4e3f3e44..230827129b6 100644
--- a/lib/banzai/reference_parser/issue_parser.rb
+++ b/lib/banzai/reference_parser/issue_parser.rb
@@ -5,12 +5,31 @@ module Banzai
def nodes_visible_to_user(user, nodes)
issues = records_for_nodes(nodes)
+ issues_to_check = issues.values
- readable_issues = Ability
- .issues_readable_by_user(issues.values, user).to_set
+ unless can?(user, :read_cross_project)
+ issues_to_check, cross_project_issues = issues_to_check.partition do |issue|
+ issue.project == project
+ end
+ end
+
+ readable_issues = Ability.issues_readable_by_user(issues_to_check, user).to_set
nodes.select do |node|
- readable_issues.include?(issues[node])
+ issue_in_node = issues[node]
+
+ # We check the inclusion of readable issues first because it's faster.
+ #
+ # But we need to fall back to `read_issue_iid` if the user cannot read
+ # cross project, since it might be possible the user can see the IID
+ # but not the issue.
+ if readable_issues.include?(issue_in_node)
+ true
+ elsif cross_project_issues&.include?(issue_in_node)
+ can_read_reference?(user, issue_in_node)
+ else
+ false
+ end
end
end
diff --git a/lib/bitbucket/connection.rb b/lib/bitbucket/connection.rb
index b9279c33f5b..ba5a9e2f04c 100644
--- a/lib/bitbucket/connection.rb
+++ b/lib/bitbucket/connection.rb
@@ -57,7 +57,7 @@ module Bitbucket
end
def provider
- Gitlab::OAuth::Provider.config_for('bitbucket')
+ Gitlab::Auth::OAuth::Provider.config_for('bitbucket')
end
def options
diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb
index ee7f4be6b9f..62c41801d75 100644
--- a/lib/gitlab/asciidoc.rb
+++ b/lib/gitlab/asciidoc.rb
@@ -8,7 +8,8 @@ module Gitlab
module Asciidoc
DEFAULT_ADOC_ATTRS = [
'showtitle', 'idprefix=user-content-', 'idseparator=-', 'env=gitlab',
- 'env-gitlab', 'source-highlighter=html-pipeline', 'icons=font'
+ 'env-gitlab', 'source-highlighter=html-pipeline', 'icons=font',
+ 'outfilesuffix=.adoc'
].freeze
# Public: Converts the provided Asciidoc markup into HTML.
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 05932378173..86393ee254d 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -54,7 +54,7 @@ module Gitlab
# LDAP users are only authenticated via LDAP
if user.nil? || user.ldap_user?
# Second chance - try LDAP authentication
- Gitlab::LDAP::Authentication.login(login, password)
+ Gitlab::Auth::LDAP::Authentication.login(login, password)
elsif Gitlab::CurrentSettings.password_authentication_enabled_for_git?
user if user.active? && user.valid_password?(password)
end
@@ -85,7 +85,7 @@ module Gitlab
private
def authenticate_using_internal_or_ldap_password?
- Gitlab::CurrentSettings.password_authentication_enabled_for_git? || Gitlab::LDAP::Config.enabled?
+ Gitlab::CurrentSettings.password_authentication_enabled_for_git? || Gitlab::Auth::LDAP::Config.enabled?
end
def service_request_check(login, password, project)
diff --git a/lib/gitlab/auth/ldap/access.rb b/lib/gitlab/auth/ldap/access.rb
new file mode 100644
index 00000000000..77c0ddc2d48
--- /dev/null
+++ b/lib/gitlab/auth/ldap/access.rb
@@ -0,0 +1,89 @@
+# LDAP authorization model
+#
+# * Check if we are allowed access (not blocked)
+#
+module Gitlab
+ module Auth
+ module LDAP
+ class Access
+ attr_reader :provider, :user
+
+ def self.open(user, &block)
+ Gitlab::Auth::LDAP::Adapter.open(user.ldap_identity.provider) do |adapter|
+ block.call(self.new(user, adapter))
+ end
+ end
+
+ def self.allowed?(user)
+ self.open(user) do |access|
+ if access.allowed?
+ Users::UpdateService.new(user, user: user, last_credential_check_at: Time.now).execute
+
+ true
+ else
+ false
+ end
+ end
+ end
+
+ def initialize(user, adapter = nil)
+ @adapter = adapter
+ @user = user
+ @provider = user.ldap_identity.provider
+ end
+
+ def allowed?
+ if ldap_user
+ unless ldap_config.active_directory
+ unblock_user(user, 'is available again') if user.ldap_blocked?
+ return true
+ end
+
+ # Block user in GitLab if he/she was blocked in AD
+ if Gitlab::Auth::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter)
+ block_user(user, 'is disabled in Active Directory')
+ false
+ else
+ unblock_user(user, 'is not disabled anymore') if user.ldap_blocked?
+ true
+ end
+ else
+ # Block the user if they no longer exist in LDAP/AD
+ block_user(user, 'does not exist anymore')
+ false
+ end
+ end
+
+ def adapter
+ @adapter ||= Gitlab::Auth::LDAP::Adapter.new(provider)
+ end
+
+ def ldap_config
+ Gitlab::Auth::LDAP::Config.new(provider)
+ end
+
+ def ldap_user
+ @ldap_user ||= Gitlab::Auth::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter)
+ end
+
+ def block_user(user, reason)
+ user.ldap_block
+
+ Gitlab::AppLogger.info(
+ "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \
+ "blocking Gitlab user \"#{user.name}\" (#{user.email})"
+ )
+ end
+
+ def unblock_user(user, reason)
+ user.activate
+
+ Gitlab::AppLogger.info(
+ "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \
+ "unblocking Gitlab user \"#{user.name}\" (#{user.email})"
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/ldap/adapter.rb b/lib/gitlab/auth/ldap/adapter.rb
new file mode 100644
index 00000000000..caf2d18c668
--- /dev/null
+++ b/lib/gitlab/auth/ldap/adapter.rb
@@ -0,0 +1,110 @@
+module Gitlab
+ module Auth
+ module LDAP
+ class Adapter
+ attr_reader :provider, :ldap
+
+ def self.open(provider, &block)
+ Net::LDAP.open(config(provider).adapter_options) do |ldap|
+ block.call(self.new(provider, ldap))
+ end
+ end
+
+ def self.config(provider)
+ Gitlab::Auth::LDAP::Config.new(provider)
+ end
+
+ def initialize(provider, ldap = nil)
+ @provider = provider
+ @ldap = ldap || Net::LDAP.new(config.adapter_options)
+ end
+
+ def config
+ Gitlab::Auth::LDAP::Config.new(provider)
+ end
+
+ def users(fields, value, limit = nil)
+ options = user_options(Array(fields), value, limit)
+
+ entries = ldap_search(options).select do |entry|
+ entry.respond_to? config.uid
+ end
+
+ entries.map do |entry|
+ Gitlab::Auth::LDAP::Person.new(entry, provider)
+ end
+ end
+
+ def user(*args)
+ users(*args).first
+ end
+
+ def dn_matches_filter?(dn, filter)
+ ldap_search(base: dn,
+ filter: filter,
+ scope: Net::LDAP::SearchScope_BaseObject,
+ attributes: %w{dn}).any?
+ end
+
+ def ldap_search(*args)
+ # Net::LDAP's `time` argument doesn't work. Use Ruby `Timeout` instead.
+ Timeout.timeout(config.timeout) do
+ results = ldap.search(*args)
+
+ if results.nil?
+ response = ldap.get_operation_result
+
+ unless response.code.zero?
+ Rails.logger.warn("LDAP search error: #{response.message}")
+ end
+
+ []
+ else
+ results
+ end
+ end
+ rescue Net::LDAP::Error => error
+ Rails.logger.warn("LDAP search raised exception #{error.class}: #{error.message}")
+ []
+ rescue Timeout::Error
+ Rails.logger.warn("LDAP search timed out after #{config.timeout} seconds")
+ []
+ end
+
+ private
+
+ def user_options(fields, value, limit)
+ options = {
+ attributes: Gitlab::Auth::LDAP::Person.ldap_attributes(config),
+ base: config.base
+ }
+
+ options[:size] = limit if limit
+
+ if fields.include?('dn')
+ raise ArgumentError, 'It is not currently possible to search the DN and other fields at the same time.' if fields.size > 1
+
+ options[:base] = value
+ options[:scope] = Net::LDAP::SearchScope_BaseObject
+ else
+ filter = fields.map { |field| Net::LDAP::Filter.eq(field, value) }.inject(:|)
+ end
+
+ options.merge(filter: user_filter(filter))
+ end
+
+ def user_filter(filter = nil)
+ user_filter = config.constructed_user_filter if config.user_filter.present?
+
+ if user_filter && filter
+ Net::LDAP::Filter.join(filter, user_filter)
+ elsif user_filter
+ user_filter
+ else
+ filter
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/ldap/auth_hash.rb b/lib/gitlab/auth/ldap/auth_hash.rb
new file mode 100644
index 00000000000..ac5c14d374d
--- /dev/null
+++ b/lib/gitlab/auth/ldap/auth_hash.rb
@@ -0,0 +1,48 @@
+# Class to parse and transform the info provided by omniauth
+#
+module Gitlab
+ module Auth
+ module LDAP
+ class AuthHash < Gitlab::Auth::OAuth::AuthHash
+ def uid
+ @uid ||= Gitlab::Auth::LDAP::Person.normalize_dn(super)
+ end
+
+ def username
+ super.tap do |username|
+ username.downcase! if ldap_config.lowercase_usernames
+ end
+ end
+
+ private
+
+ def get_info(key)
+ attributes = ldap_config.attributes[key.to_s]
+ return super unless attributes
+
+ attributes = Array(attributes)
+
+ value = nil
+ attributes.each do |attribute|
+ value = get_raw(attribute)
+ value = value.first if value
+ break if value.present?
+ end
+
+ return super unless value
+
+ Gitlab::Utils.force_utf8(value)
+ value
+ end
+
+ def get_raw(key)
+ auth_hash.extra[:raw_info][key] if auth_hash.extra
+ end
+
+ def ldap_config
+ @ldap_config ||= Gitlab::Auth::LDAP::Config.new(self.provider)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/ldap/authentication.rb b/lib/gitlab/auth/ldap/authentication.rb
new file mode 100644
index 00000000000..cbb9cf4bb9c
--- /dev/null
+++ b/lib/gitlab/auth/ldap/authentication.rb
@@ -0,0 +1,72 @@
+# These calls help to authenticate to LDAP by providing username and password
+#
+# Since multiple LDAP servers are supported, it will loop through all of them
+# until a valid bind is found
+#
+
+module Gitlab
+ module Auth
+ module LDAP
+ class Authentication
+ def self.login(login, password)
+ return unless Gitlab::Auth::LDAP::Config.enabled?
+ return unless login.present? && password.present?
+
+ auth = nil
+ # loop through providers until valid bind
+ providers.find do |provider|
+ auth = new(provider)
+ 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 :provider, :ldap_user
+
+ def initialize(provider)
+ @provider = provider
+ end
+
+ def login(login, password)
+ @ldap_user = adapter.bind_as(
+ filter: user_filter(login),
+ size: 1,
+ password: password
+ )
+ end
+
+ def adapter
+ OmniAuth::LDAP::Adaptor.new(config.omniauth_options)
+ end
+
+ def config
+ Gitlab::Auth::LDAP::Config.new(provider)
+ end
+
+ def user_filter(login)
+ filter = Net::LDAP::Filter.equals(config.uid, login)
+
+ # Apply LDAP user filter if present
+ if config.user_filter.present?
+ filter = Net::LDAP::Filter.join(filter, config.constructed_user_filter)
+ end
+
+ filter
+ end
+
+ def user
+ return nil unless ldap_user
+
+ Gitlab::Auth::LDAP::User.find_by_uid_and_provider(ldap_user.dn, provider)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/ldap/config.rb b/lib/gitlab/auth/ldap/config.rb
new file mode 100644
index 00000000000..77185f52ced
--- /dev/null
+++ b/lib/gitlab/auth/ldap/config.rb
@@ -0,0 +1,237 @@
+# Load a specific server configuration
+module Gitlab
+ module Auth
+ module LDAP
+ class Config
+ NET_LDAP_ENCRYPTION_METHOD = {
+ simple_tls: :simple_tls,
+ start_tls: :start_tls,
+ plain: nil
+ }.freeze
+
+ attr_accessor :provider, :options
+
+ def self.enabled?
+ Gitlab.config.ldap.enabled
+ end
+
+ def self.servers
+ Gitlab.config.ldap['servers']&.values || []
+ end
+
+ def self.available_servers
+ return [] unless enabled?
+
+ Array.wrap(servers.first)
+ end
+
+ def self.providers
+ servers.map { |server| server['provider_name'] }
+ end
+
+ def self.valid_provider?(provider)
+ providers.include?(provider)
+ end
+
+ def self.invalid_provider(provider)
+ raise "Unknown provider (#{provider}). Available providers: #{providers}"
+ end
+
+ def initialize(provider)
+ if self.class.valid_provider?(provider)
+ @provider = provider
+ else
+ self.class.invalid_provider(provider)
+ end
+
+ @options = config_for(@provider) # Use @provider, not provider
+ end
+
+ def enabled?
+ base_config.enabled
+ end
+
+ def adapter_options
+ opts = base_options.merge(
+ encryption: encryption_options
+ )
+
+ opts.merge!(auth_options) if has_auth?
+
+ opts
+ end
+
+ def omniauth_options
+ opts = base_options.merge(
+ base: base,
+ encryption: options['encryption'],
+ filter: omniauth_user_filter,
+ name_proc: name_proc,
+ disable_verify_certificates: !options['verify_certificates']
+ )
+
+ if has_auth?
+ opts.merge!(
+ bind_dn: options['bind_dn'],
+ password: options['password']
+ )
+ end
+
+ opts[:ca_file] = options['ca_file'] if options['ca_file'].present?
+ opts[:ssl_version] = options['ssl_version'] if options['ssl_version'].present?
+
+ opts
+ end
+
+ def base
+ options['base']
+ end
+
+ def uid
+ options['uid']
+ end
+
+ def sync_ssh_keys?
+ sync_ssh_keys.present?
+ end
+
+ # The LDAP attribute in which the ssh keys are stored
+ def sync_ssh_keys
+ options['sync_ssh_keys']
+ end
+
+ def user_filter
+ options['user_filter']
+ end
+
+ def constructed_user_filter
+ @constructed_user_filter ||= Net::LDAP::Filter.construct(user_filter)
+ end
+
+ def group_base
+ options['group_base']
+ end
+
+ def admin_group
+ options['admin_group']
+ end
+
+ def active_directory
+ options['active_directory']
+ end
+
+ def block_auto_created_users
+ options['block_auto_created_users']
+ end
+
+ def attributes
+ default_attributes.merge(options['attributes'])
+ end
+
+ def timeout
+ options['timeout'].to_i
+ end
+
+ def has_auth?
+ options['password'] || options['bind_dn']
+ end
+
+ def allow_username_or_email_login
+ options['allow_username_or_email_login']
+ end
+
+ def lowercase_usernames
+ options['lowercase_usernames']
+ end
+
+ def name_proc
+ if allow_username_or_email_login
+ proc { |name| name.gsub(/@.*\z/, '') }
+ else
+ proc { |name| name }
+ end
+ end
+
+ def default_attributes
+ {
+ 'username' => %w(uid sAMAccountName userid),
+ 'email' => %w(mail email userPrincipalName),
+ 'name' => 'cn',
+ 'first_name' => 'givenName',
+ 'last_name' => 'sn'
+ }
+ end
+
+ protected
+
+ def base_options
+ {
+ host: options['host'],
+ port: options['port']
+ }
+ end
+
+ def base_config
+ Gitlab.config.ldap
+ end
+
+ def config_for(provider)
+ base_config.servers.values.find { |server| server['provider_name'] == provider }
+ end
+
+ def encryption_options
+ method = translate_method(options['encryption'])
+ return nil unless method
+
+ {
+ method: method,
+ tls_options: tls_options(method)
+ }
+ end
+
+ def translate_method(method_from_config)
+ NET_LDAP_ENCRYPTION_METHOD[method_from_config.to_sym]
+ end
+
+ def tls_options(method)
+ return { verify_mode: OpenSSL::SSL::VERIFY_NONE } unless method
+
+ opts = if options['verify_certificates']
+ OpenSSL::SSL::SSLContext::DEFAULT_PARAMS
+ else
+ # It is important to explicitly set verify_mode for two reasons:
+ # 1. The behavior of OpenSSL is undefined when verify_mode is not set.
+ # 2. The net-ldap gem implementation verifies the certificate hostname
+ # unless verify_mode is set to VERIFY_NONE.
+ { verify_mode: OpenSSL::SSL::VERIFY_NONE }
+ end
+
+ opts[:ca_file] = options['ca_file'] if options['ca_file'].present?
+ opts[:ssl_version] = options['ssl_version'] if options['ssl_version'].present?
+
+ opts
+ end
+
+ def auth_options
+ {
+ auth: {
+ method: :simple,
+ username: options['bind_dn'],
+ password: options['password']
+ }
+ }
+ end
+
+ def omniauth_user_filter
+ uid_filter = Net::LDAP::Filter.eq(uid, '%{username}')
+
+ if user_filter.present?
+ Net::LDAP::Filter.join(uid_filter, constructed_user_filter).to_s
+ else
+ uid_filter.to_s
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/ldap/dn.rb b/lib/gitlab/auth/ldap/dn.rb
new file mode 100644
index 00000000000..1fa5338f5a6
--- /dev/null
+++ b/lib/gitlab/auth/ldap/dn.rb
@@ -0,0 +1,303 @@
+# -*- ruby encoding: utf-8 -*-
+
+# Based on the `ruby-net-ldap` gem's `Net::LDAP::DN`
+#
+# For our purposes, this class is used to normalize DNs in order to allow proper
+# comparison.
+#
+# E.g. DNs should be compared case-insensitively (in basically all LDAP
+# implementations or setups), therefore we downcase every DN.
+
+##
+# Objects of this class represent an LDAP DN ("Distinguished Name"). A DN
+# ("Distinguished Name") is a unique identifier for an entry within an LDAP
+# directory. It is made up of a number of other attributes strung together,
+# to identify the entry in the tree.
+#
+# Each attribute that makes up a DN needs to have its value escaped so that
+# the DN is valid. This class helps take care of that.
+#
+# A fully escaped DN needs to be unescaped when analysing its contents. This
+# class also helps take care of that.
+module Gitlab
+ module Auth
+ module LDAP
+ class DN
+ FormatError = Class.new(StandardError)
+ MalformedError = Class.new(FormatError)
+ UnsupportedError = Class.new(FormatError)
+
+ def self.normalize_value(given_value)
+ dummy_dn = "placeholder=#{given_value}"
+ normalized_dn = new(*dummy_dn).to_normalized_s
+ normalized_dn.sub(/\Aplaceholder=/, '')
+ end
+
+ ##
+ # Initialize a DN, escaping as required. Pass in attributes in name/value
+ # pairs. If there is a left over argument, it will be appended to the dn
+ # without escaping (useful for a base string).
+ #
+ # Most uses of this class will be to escape a DN, rather than to parse it,
+ # so storing the dn as an escaped String and parsing parts as required
+ # with a state machine seems sensible.
+ def initialize(*args)
+ if args.length > 1
+ initialize_array(args)
+ else
+ initialize_string(args[0])
+ end
+ end
+
+ ##
+ # Parse a DN into key value pairs using ASN from
+ # http://tools.ietf.org/html/rfc2253 section 3.
+ # rubocop:disable Metrics/AbcSize
+ # rubocop:disable Metrics/CyclomaticComplexity
+ # rubocop:disable Metrics/PerceivedComplexity
+ def each_pair
+ state = :key
+ key = StringIO.new
+ value = StringIO.new
+ hex_buffer = ""
+
+ @dn.each_char.with_index do |char, dn_index|
+ case state
+ when :key then
+ case char
+ when 'a'..'z', 'A'..'Z' then
+ state = :key_normal
+ key << char
+ when '0'..'9' then
+ state = :key_oid
+ key << char
+ when ' ' then state = :key
+ else raise(MalformedError, "Unrecognized first character of an RDN attribute type name \"#{char}\"")
+ end
+ when :key_normal then
+ case char
+ when '=' then state = :value
+ when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char
+ else raise(MalformedError, "Unrecognized RDN attribute type name character \"#{char}\"")
+ end
+ when :key_oid then
+ case char
+ when '=' then state = :value
+ when '0'..'9', '.', ' ' then key << char
+ else raise(MalformedError, "Unrecognized RDN OID attribute type name character \"#{char}\"")
+ end
+ when :value then
+ case char
+ when '\\' then state = :value_normal_escape
+ when '"' then state = :value_quoted
+ when ' ' then state = :value
+ when '#' then
+ state = :value_hexstring
+ value << char
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else
+ state = :value_normal
+ value << char
+ end
+ when :value_normal then
+ case char
+ when '\\' then state = :value_normal_escape
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ when '+' then raise(UnsupportedError, "Multivalued RDNs are not supported")
+ else value << char
+ end
+ when :value_normal_escape then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_normal_escape_hex
+ hex_buffer = char
+ else
+ state = :value_normal
+ value << char
+ end
+ when :value_normal_escape_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_normal
+ value << "#{hex_buffer}#{char}".to_i(16).chr
+ else raise(MalformedError, "Invalid escaped hex code \"\\#{hex_buffer}#{char}\"")
+ end
+ when :value_quoted then
+ case char
+ when '\\' then state = :value_quoted_escape
+ when '"' then state = :value_end
+ else value << char
+ end
+ when :value_quoted_escape then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_quoted_escape_hex
+ hex_buffer = char
+ else
+ state = :value_quoted
+ value << char
+ end
+ when :value_quoted_escape_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_quoted
+ value << "#{hex_buffer}#{char}".to_i(16).chr
+ else raise(MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"#{char}\"")
+ end
+ when :value_hexstring then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_hexstring_hex
+ value << char
+ when ' ' then state = :value_end
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else raise(MalformedError, "Expected the first character of a hex pair, but got \"#{char}\"")
+ end
+ when :value_hexstring_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_hexstring
+ value << char
+ else raise(MalformedError, "Expected the second character of a hex pair, but got \"#{char}\"")
+ end
+ when :value_end then
+ case char
+ when ' ' then state = :value_end
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else raise(MalformedError, "Expected the end of an attribute value, but got \"#{char}\"")
+ end
+ else raise "Fell out of state machine"
+ end
+ end
+
+ # Last pair
+ raise(MalformedError, 'DN string ended unexpectedly') unless
+ [:value, :value_normal, :value_hexstring, :value_end].include? state
+
+ yield key.string.strip, rstrip_except_escaped(value.string, @dn.length)
+ end
+
+ def rstrip_except_escaped(str, dn_index)
+ str_ends_with_whitespace = str.match(/\s\z/)
+
+ if str_ends_with_whitespace
+ dn_part_ends_with_escaped_whitespace = @dn[0, dn_index].match(/\\(\s+)\z/)
+
+ if dn_part_ends_with_escaped_whitespace
+ dn_part_rwhitespace = dn_part_ends_with_escaped_whitespace[1]
+ num_chars_to_remove = dn_part_rwhitespace.length - 1
+ str = str[0, str.length - num_chars_to_remove]
+ else
+ str.rstrip!
+ end
+ end
+
+ str
+ end
+
+ ##
+ # Returns the DN as an array in the form expected by the constructor.
+ def to_a
+ a = []
+ self.each_pair { |key, value| a << key << value } unless @dn.empty?
+ a
+ end
+
+ ##
+ # Return the DN as an escaped string.
+ def to_s
+ @dn
+ end
+
+ ##
+ # Return the DN as an escaped and normalized string.
+ def to_normalized_s
+ self.class.new(*to_a).to_s.downcase
+ end
+
+ # https://tools.ietf.org/html/rfc4514 section 2.4 lists these exceptions
+ # for DN values. All of the following must be escaped in any normal string
+ # using a single backslash ('\') as escape. The space character is left
+ # out here because in a "normalized" string, spaces should only be escaped
+ # if necessary (i.e. leading or trailing space).
+ NORMAL_ESCAPES = [',', '+', '"', '\\', '<', '>', ';', '='].freeze
+
+ # The following must be represented as escaped hex
+ HEX_ESCAPES = {
+ "\n" => '\0a',
+ "\r" => '\0d'
+ }.freeze
+
+ # Compiled character class regexp using the keys from the above hash, and
+ # checking for a space or # at the start, or space at the end, of the
+ # string.
+ ESCAPE_RE = Regexp.new("(^ |^#| $|[" +
+ NORMAL_ESCAPES.map { |e| Regexp.escape(e) }.join +
+ "])")
+
+ HEX_ESCAPE_RE = Regexp.new("([" +
+ HEX_ESCAPES.keys.map { |e| Regexp.escape(e) }.join +
+ "])")
+
+ ##
+ # Escape a string for use in a DN value
+ def self.escape(string)
+ escaped = string.gsub(ESCAPE_RE) { |char| "\\" + char }
+ escaped.gsub(HEX_ESCAPE_RE) { |char| HEX_ESCAPES[char] }
+ end
+
+ private
+
+ def initialize_array(args)
+ buffer = StringIO.new
+
+ args.each_with_index do |arg, index|
+ if index.even? # key
+ buffer << "," if index > 0
+ buffer << arg
+ else # value
+ buffer << "="
+ buffer << self.class.escape(arg)
+ end
+ end
+
+ @dn = buffer.string
+ end
+
+ def initialize_string(arg)
+ @dn = arg.to_s
+ end
+
+ ##
+ # Proxy all other requests to the string object, because a DN is mainly
+ # used within the library as a string
+ # rubocop:disable GitlabSecurity/PublicSend
+ def method_missing(method, *args, &block)
+ @dn.send(method, *args, &block)
+ end
+
+ ##
+ # Redefined to be consistent with redefined `method_missing` behavior
+ def respond_to?(sym, include_private = false)
+ @dn.respond_to?(sym, include_private)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/ldap/person.rb b/lib/gitlab/auth/ldap/person.rb
new file mode 100644
index 00000000000..8dfae3ee541
--- /dev/null
+++ b/lib/gitlab/auth/ldap/person.rb
@@ -0,0 +1,122 @@
+module Gitlab
+ module Auth
+ module LDAP
+ class Person
+ # Active Directory-specific LDAP filter that checks if bit 2 of the
+ # userAccountControl attribute is set.
+ # Source: http://ctogonewild.com/2009/09/03/bitmask-searches-in-ldap/
+ AD_USER_DISABLED = Net::LDAP::Filter.ex("userAccountControl:1.2.840.113556.1.4.803", "2")
+
+ InvalidEntryError = Class.new(StandardError)
+
+ attr_accessor :entry, :provider
+
+ def self.find_by_uid(uid, adapter)
+ uid = Net::LDAP::Filter.escape(uid)
+ adapter.user(adapter.config.uid, uid)
+ end
+
+ def self.find_by_dn(dn, adapter)
+ adapter.user('dn', dn)
+ end
+
+ def self.find_by_email(email, adapter)
+ email_fields = adapter.config.attributes['email']
+
+ adapter.user(email_fields, email)
+ end
+
+ def self.disabled_via_active_directory?(dn, adapter)
+ adapter.dn_matches_filter?(dn, AD_USER_DISABLED)
+ end
+
+ def self.ldap_attributes(config)
+ [
+ 'dn',
+ config.uid,
+ *config.attributes['name'],
+ *config.attributes['email'],
+ *config.attributes['username']
+ ].compact.uniq
+ end
+
+ def self.normalize_dn(dn)
+ ::Gitlab::Auth::LDAP::DN.new(dn).to_normalized_s
+ rescue ::Gitlab::Auth::LDAP::DN::FormatError => e
+ Rails.logger.info("Returning original DN \"#{dn}\" due to error during normalization attempt: #{e.message}")
+
+ dn
+ end
+
+ # Returns the UID in a normalized form.
+ #
+ # 1. Excess spaces are stripped
+ # 2. The string is downcased (for case-insensitivity)
+ def self.normalize_uid(uid)
+ ::Gitlab::Auth::LDAP::DN.normalize_value(uid)
+ rescue ::Gitlab::Auth::LDAP::DN::FormatError => e
+ Rails.logger.info("Returning original UID \"#{uid}\" due to error during normalization attempt: #{e.message}")
+
+ uid
+ end
+
+ def initialize(entry, provider)
+ Rails.logger.debug { "Instantiating #{self.class.name} with LDIF:\n#{entry.to_ldif}" }
+ @entry = entry
+ @provider = provider
+ end
+
+ def name
+ attribute_value(:name).first
+ end
+
+ def uid
+ entry.public_send(config.uid).first # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def username
+ username = attribute_value(:username)
+
+ # Depending on the attribute, multiple values may
+ # be returned. We need only one for username.
+ # Ex. `uid` returns only one value but `mail` may
+ # return an array of multiple email addresses.
+ [username].flatten.first.tap do |username|
+ username.downcase! if config.lowercase_usernames
+ end
+ end
+
+ def email
+ attribute_value(:email)
+ end
+
+ def dn
+ self.class.normalize_dn(entry.dn)
+ end
+
+ private
+
+ def entry
+ @entry
+ end
+
+ def config
+ @config ||= Gitlab::Auth::LDAP::Config.new(provider)
+ end
+
+ # Using the LDAP attributes configuration, find and return the first
+ # attribute with a value. For example, by default, when given 'email',
+ # this method looks for 'mail', 'email' and 'userPrincipalName' and
+ # returns the first with a value.
+ def attribute_value(attribute)
+ attributes = Array(config.attributes[attribute.to_s])
+ selected_attr = attributes.find { |attr| entry.respond_to?(attr) }
+
+ return nil unless selected_attr
+
+ entry.public_send(selected_attr) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/ldap/user.rb b/lib/gitlab/auth/ldap/user.rb
new file mode 100644
index 00000000000..068212d9a21
--- /dev/null
+++ b/lib/gitlab/auth/ldap/user.rb
@@ -0,0 +1,54 @@
+# LDAP extension for User model
+#
+# * Find or create user from omniauth.auth data
+# * Links LDAP account with existing user
+# * Auth LDAP user with login and password
+#
+module Gitlab
+ module Auth
+ module LDAP
+ class User < Gitlab::Auth::OAuth::User
+ class << self
+ def find_by_uid_and_provider(uid, provider)
+ identity = ::Identity.with_extern_uid(provider, uid).take
+
+ identity && identity.user
+ end
+ end
+
+ def save
+ super('LDAP')
+ end
+
+ # instance methods
+ def find_user
+ find_by_uid_and_provider || find_by_email || build_new_user
+ end
+
+ def find_by_uid_and_provider
+ self.class.find_by_uid_and_provider(auth_hash.uid, auth_hash.provider)
+ end
+
+ def changed?
+ gl_user.changed? || gl_user.identities.any?(&:changed?)
+ end
+
+ def block_after_signup?
+ ldap_config.block_auto_created_users
+ end
+
+ def allowed?
+ Gitlab::Auth::LDAP::Access.allowed?(gl_user)
+ end
+
+ def ldap_config
+ Gitlab::Auth::LDAP::Config.new(auth_hash.provider)
+ end
+
+ def auth_hash=(auth_hash)
+ @auth_hash = Gitlab::Auth::LDAP::AuthHash.new(auth_hash)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/o_auth/auth_hash.rb b/lib/gitlab/auth/o_auth/auth_hash.rb
new file mode 100644
index 00000000000..ed8fba94305
--- /dev/null
+++ b/lib/gitlab/auth/o_auth/auth_hash.rb
@@ -0,0 +1,92 @@
+# Class to parse and transform the info provided by omniauth
+#
+module Gitlab
+ module Auth
+ module OAuth
+ class AuthHash
+ attr_reader :auth_hash
+ def initialize(auth_hash)
+ @auth_hash = auth_hash
+ end
+
+ def uid
+ @uid ||= Gitlab::Utils.force_utf8(auth_hash.uid.to_s)
+ end
+
+ def provider
+ @provider ||= auth_hash.provider.to_s
+ end
+
+ def name
+ @name ||= get_info(:name) || "#{get_info(:first_name)} #{get_info(:last_name)}"
+ end
+
+ def username
+ @username ||= username_and_email[:username].to_s
+ end
+
+ def email
+ @email ||= username_and_email[:email].to_s
+ end
+
+ def password
+ @password ||= Gitlab::Utils.force_utf8(Devise.friendly_token[0, 8].downcase)
+ end
+
+ def location
+ location = get_info(:address)
+ if location.is_a?(Hash)
+ [location.locality.presence, location.country.presence].compact.join(', ')
+ else
+ location
+ end
+ end
+
+ def has_attribute?(attribute)
+ if attribute == :location
+ get_info(:address).present?
+ else
+ get_info(attribute).present?
+ end
+ end
+
+ private
+
+ def info
+ auth_hash.info
+ end
+
+ def get_info(key)
+ value = info[key]
+ Gitlab::Utils.force_utf8(value) if value
+ value
+ end
+
+ def username_and_email
+ @username_and_email ||= begin
+ username = get_info(:username).presence || get_info(:nickname).presence
+ email = get_info(:email).presence
+
+ username ||= generate_username(email) if email
+ email ||= generate_temporarily_email(username) if username
+
+ {
+ username: username,
+ email: email
+ }
+ end
+ end
+
+ # Get the first part of the email address (before @)
+ # In addtion in removes illegal characters
+ def generate_username(email)
+ email.match(/^[^@]*/)[0].mb_chars.normalize(:kd).gsub(/[^\x00-\x7F]/, '').to_s
+ end
+
+ def generate_temporarily_email(username)
+ "temp-email-for-oauth-#{username}@gitlab.localhost"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/o_auth/provider.rb b/lib/gitlab/auth/o_auth/provider.rb
new file mode 100644
index 00000000000..f8ab8ee1388
--- /dev/null
+++ b/lib/gitlab/auth/o_auth/provider.rb
@@ -0,0 +1,56 @@
+module Gitlab
+ module Auth
+ module OAuth
+ class Provider
+ LABELS = {
+ "github" => "GitHub",
+ "gitlab" => "GitLab.com",
+ "google_oauth2" => "Google"
+ }.freeze
+
+ def self.providers
+ Devise.omniauth_providers
+ end
+
+ def self.enabled?(name)
+ providers.include?(name.to_sym)
+ end
+
+ def self.ldap_provider?(name)
+ name.to_s.start_with?('ldap')
+ end
+
+ def self.sync_profile_from_provider?(provider)
+ return true if ldap_provider?(provider)
+
+ providers = Gitlab.config.omniauth.sync_profile_from_provider
+
+ if providers.is_a?(Array)
+ providers.include?(provider)
+ else
+ providers
+ end
+ end
+
+ def self.config_for(name)
+ name = name.to_s
+ if ldap_provider?(name)
+ if Gitlab::Auth::LDAP::Config.valid_provider?(name)
+ Gitlab::Auth::LDAP::Config.new(name).options
+ else
+ nil
+ end
+ else
+ Gitlab.config.omniauth.providers.find { |provider| provider.name == name }
+ end
+ end
+
+ def self.label_for(name)
+ name = name.to_s
+ config = config_for(name)
+ (config && config['label']) || LABELS[name] || name.titleize
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/o_auth/session.rb b/lib/gitlab/auth/o_auth/session.rb
new file mode 100644
index 00000000000..8f2b4d58552
--- /dev/null
+++ b/lib/gitlab/auth/o_auth/session.rb
@@ -0,0 +1,21 @@
+# :nocov:
+module Gitlab
+ module Auth
+ module OAuth
+ module Session
+ def self.create(provider, ticket)
+ Rails.cache.write("gitlab:#{provider}:#{ticket}", ticket, expires_in: Gitlab.config.omniauth.cas3.session_duration)
+ end
+
+ def self.destroy(provider, ticket)
+ Rails.cache.delete("gitlab:#{provider}:#{ticket}")
+ end
+
+ def self.valid?(provider, ticket)
+ Rails.cache.read("gitlab:#{provider}:#{ticket}").present?
+ end
+ end
+ end
+ end
+end
+# :nocov:
diff --git a/lib/gitlab/auth/o_auth/user.rb b/lib/gitlab/auth/o_auth/user.rb
new file mode 100644
index 00000000000..acd785bb02d
--- /dev/null
+++ b/lib/gitlab/auth/o_auth/user.rb
@@ -0,0 +1,246 @@
+# OAuth extension for User model
+#
+# * Find GitLab user based on omniauth uid and provider
+# * Create new user from omniauth data
+#
+module Gitlab
+ module Auth
+ module OAuth
+ class User
+ SignupDisabledError = Class.new(StandardError)
+ SigninDisabledForProviderError = Class.new(StandardError)
+
+ attr_accessor :auth_hash, :gl_user
+
+ def initialize(auth_hash)
+ self.auth_hash = auth_hash
+ update_profile
+ add_or_update_user_identities
+ end
+
+ def persisted?
+ gl_user.try(:persisted?)
+ end
+
+ def new?
+ !persisted?
+ end
+
+ def valid?
+ gl_user.try(:valid?)
+ end
+
+ def save(provider = 'OAuth')
+ raise SigninDisabledForProviderError if oauth_provider_disabled?
+ raise SignupDisabledError unless gl_user
+
+ block_after_save = needs_blocking?
+
+ Users::UpdateService.new(gl_user, user: gl_user).execute!
+
+ gl_user.block if block_after_save
+
+ log.info "(#{provider}) saving user #{auth_hash.email} from login with extern_uid => #{auth_hash.uid}"
+ gl_user
+ rescue ActiveRecord::RecordInvalid => e
+ log.info "(#{provider}) Error saving user #{auth_hash.uid} (#{auth_hash.email}): #{gl_user.errors.full_messages}"
+ return self, e.record.errors
+ end
+
+ def gl_user
+ return @gl_user if defined?(@gl_user)
+
+ @gl_user = find_user
+ end
+
+ def find_user
+ user = find_by_uid_and_provider
+
+ user ||= find_or_build_ldap_user if auto_link_ldap_user?
+ user ||= build_new_user if signup_enabled?
+
+ user.external = true if external_provider? && user&.new_record?
+
+ user
+ end
+
+ protected
+
+ def add_or_update_user_identities
+ return unless gl_user
+
+ # find_or_initialize_by doesn't update `gl_user.identities`, and isn't autosaved.
+ identity = gl_user.identities.find { |identity| identity.provider == auth_hash.provider }
+
+ identity ||= gl_user.identities.build(provider: auth_hash.provider)
+ identity.extern_uid = auth_hash.uid
+
+ if auto_link_ldap_user? && !gl_user.ldap_user? && ldap_person
+ log.info "Correct LDAP account has been found. identity to user: #{gl_user.username}."
+ gl_user.identities.build(provider: ldap_person.provider, extern_uid: ldap_person.dn)
+ end
+ end
+
+ def find_or_build_ldap_user
+ return unless ldap_person
+
+ user = Gitlab::Auth::LDAP::User.find_by_uid_and_provider(ldap_person.dn, ldap_person.provider)
+ if user
+ log.info "LDAP account found for user #{user.username}. Building new #{auth_hash.provider} identity."
+ return user
+ end
+
+ log.info "No user found using #{auth_hash.provider} provider. Creating a new one."
+ build_new_user
+ end
+
+ def find_by_email
+ return unless auth_hash.has_attribute?(:email)
+
+ ::User.find_by(email: auth_hash.email.downcase)
+ end
+
+ def auto_link_ldap_user?
+ Gitlab.config.omniauth.auto_link_ldap_user
+ end
+
+ def creating_linked_ldap_user?
+ auto_link_ldap_user? && ldap_person
+ end
+
+ def ldap_person
+ return @ldap_person if defined?(@ldap_person)
+
+ # Look for a corresponding person with same uid in any of the configured LDAP providers
+ Gitlab::Auth::LDAP::Config.providers.each do |provider|
+ adapter = Gitlab::Auth::LDAP::Adapter.new(provider)
+ @ldap_person = find_ldap_person(auth_hash, adapter)
+ break if @ldap_person
+ end
+ @ldap_person
+ end
+
+ def find_ldap_person(auth_hash, adapter)
+ Gitlab::Auth::LDAP::Person.find_by_uid(auth_hash.uid, adapter) ||
+ Gitlab::Auth::LDAP::Person.find_by_email(auth_hash.uid, adapter) ||
+ Gitlab::Auth::LDAP::Person.find_by_dn(auth_hash.uid, adapter)
+ end
+
+ def ldap_config
+ Gitlab::Auth::LDAP::Config.new(ldap_person.provider) if ldap_person
+ end
+
+ def needs_blocking?
+ new? && block_after_signup?
+ end
+
+ def signup_enabled?
+ providers = Gitlab.config.omniauth.allow_single_sign_on
+ if providers.is_a?(Array)
+ providers.include?(auth_hash.provider)
+ else
+ providers
+ end
+ end
+
+ def external_provider?
+ Gitlab.config.omniauth.external_providers.include?(auth_hash.provider)
+ end
+
+ def block_after_signup?
+ if creating_linked_ldap_user?
+ ldap_config.block_auto_created_users
+ else
+ Gitlab.config.omniauth.block_auto_created_users
+ end
+ end
+
+ def auth_hash=(auth_hash)
+ @auth_hash = AuthHash.new(auth_hash)
+ end
+
+ def find_by_uid_and_provider
+ identity = Identity.with_extern_uid(auth_hash.provider, auth_hash.uid).take
+ identity && identity.user
+ end
+
+ def build_new_user
+ user_params = user_attributes.merge(skip_confirmation: true)
+ Users::BuildService.new(nil, user_params).execute(skip_authorization: true)
+ end
+
+ def user_attributes
+ # Give preference to LDAP for sensitive information when creating a linked account
+ if creating_linked_ldap_user?
+ username = ldap_person.username.presence
+ email = ldap_person.email.first.presence
+ end
+
+ username ||= auth_hash.username
+ email ||= auth_hash.email
+
+ valid_username = ::Namespace.clean_path(username)
+
+ uniquify = Uniquify.new
+ valid_username = uniquify.string(valid_username) { |s| !NamespacePathValidator.valid_path?(s) }
+
+ name = auth_hash.name
+ name = valid_username if name.strip.empty?
+
+ {
+ name: name,
+ username: valid_username,
+ email: email,
+ password: auth_hash.password,
+ password_confirmation: auth_hash.password,
+ password_automatically_set: true
+ }
+ end
+
+ def sync_profile_from_provider?
+ Gitlab::Auth::OAuth::Provider.sync_profile_from_provider?(auth_hash.provider)
+ end
+
+ def update_profile
+ clear_user_synced_attributes_metadata
+
+ return unless sync_profile_from_provider? || creating_linked_ldap_user?
+
+ metadata = gl_user.build_user_synced_attributes_metadata
+
+ if sync_profile_from_provider?
+ UserSyncedAttributesMetadata::SYNCABLE_ATTRIBUTES.each do |key|
+ if auth_hash.has_attribute?(key) && gl_user.sync_attribute?(key)
+ gl_user[key] = auth_hash.public_send(key) # rubocop:disable GitlabSecurity/PublicSend
+ metadata.set_attribute_synced(key, true)
+ else
+ metadata.set_attribute_synced(key, false)
+ end
+ end
+
+ metadata.provider = auth_hash.provider
+ end
+
+ if creating_linked_ldap_user? && gl_user.email == ldap_person.email.first
+ metadata.set_attribute_synced(:email, true)
+ metadata.provider = ldap_person.provider
+ end
+ end
+
+ def clear_user_synced_attributes_metadata
+ gl_user&.user_synced_attributes_metadata&.destroy
+ end
+
+ def log
+ Gitlab::AppLogger
+ end
+
+ def oauth_provider_disabled?
+ Gitlab::CurrentSettings.current_application_settings
+ .disabled_oauth_sign_in_sources
+ .include?(auth_hash.provider)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/request_authenticator.rb b/lib/gitlab/auth/request_authenticator.rb
index 46ec040ce92..a0b5cd868c3 100644
--- a/lib/gitlab/auth/request_authenticator.rb
+++ b/lib/gitlab/auth/request_authenticator.rb
@@ -20,6 +20,14 @@ module Gitlab
rescue Gitlab::Auth::AuthenticationError
nil
end
+
+ def valid_access_token?(scopes: [])
+ validate_access_token!(scopes: scopes)
+
+ true
+ rescue Gitlab::Auth::AuthenticationError
+ false
+ end
end
end
end
diff --git a/lib/gitlab/auth/saml/auth_hash.rb b/lib/gitlab/auth/saml/auth_hash.rb
new file mode 100644
index 00000000000..c345a7e3f6c
--- /dev/null
+++ b/lib/gitlab/auth/saml/auth_hash.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Auth
+ module Saml
+ class AuthHash < Gitlab::Auth::OAuth::AuthHash
+ def groups
+ Array.wrap(get_raw(Gitlab::Auth::Saml::Config.groups))
+ end
+
+ private
+
+ def get_raw(key)
+ # Needs to call `all` because of https://git.io/vVo4u
+ # otherwise just the first value is returned
+ auth_hash.extra[:raw_info].all[key]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/saml/config.rb b/lib/gitlab/auth/saml/config.rb
new file mode 100644
index 00000000000..e654e7fe438
--- /dev/null
+++ b/lib/gitlab/auth/saml/config.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module Auth
+ module Saml
+ class Config
+ class << self
+ def options
+ Gitlab.config.omniauth.providers.find { |provider| provider.name == 'saml' }
+ end
+
+ def groups
+ options[:groups_attribute]
+ end
+
+ def external_groups
+ options[:external_groups]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/saml/user.rb b/lib/gitlab/auth/saml/user.rb
new file mode 100644
index 00000000000..d4024e9ec39
--- /dev/null
+++ b/lib/gitlab/auth/saml/user.rb
@@ -0,0 +1,52 @@
+# SAML extension for User model
+#
+# * Find GitLab user based on SAML uid and provider
+# * Create new user from SAML data
+#
+module Gitlab
+ module Auth
+ module Saml
+ class User < Gitlab::Auth::OAuth::User
+ def save
+ super('SAML')
+ end
+
+ def find_user
+ user = find_by_uid_and_provider
+
+ user ||= find_by_email if auto_link_saml_user?
+ user ||= find_or_build_ldap_user if auto_link_ldap_user?
+ user ||= build_new_user if signup_enabled?
+
+ if external_users_enabled? && user
+ # Check if there is overlap between the user's groups and the external groups
+ # setting then set user as external or internal.
+ user.external = !(auth_hash.groups & Gitlab::Auth::Saml::Config.external_groups).empty?
+ end
+
+ user
+ end
+
+ def changed?
+ return true unless gl_user
+
+ gl_user.changed? || gl_user.identities.any?(&:changed?)
+ end
+
+ protected
+
+ def auto_link_saml_user?
+ Gitlab.config.omniauth.auto_link_saml_user
+ end
+
+ def external_users_enabled?
+ !Gitlab::Auth::Saml::Config.external_groups.nil?
+ end
+
+ def auth_hash=(auth_hash)
+ @auth_hash = Gitlab::Auth::Saml::AuthHash.new(auth_hash)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/create_fork_network_memberships_range.rb b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb
index 03b17b319fa..1b4a9e8a194 100644
--- a/lib/gitlab/background_migration/create_fork_network_memberships_range.rb
+++ b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb
@@ -14,6 +14,14 @@ module Gitlab
def perform(start_id, end_id)
log("Creating memberships for forks: #{start_id} - #{end_id}")
+ insert_members(start_id, end_id)
+
+ if missing_members?(start_id, end_id)
+ BackgroundMigrationWorker.perform_in(RESCHEDULE_DELAY, "CreateForkNetworkMembershipsRange", [start_id, end_id])
+ end
+ end
+
+ def insert_members(start_id, end_id)
ActiveRecord::Base.connection.execute <<~INSERT_MEMBERS
INSERT INTO fork_network_members (fork_network_id, project_id, forked_from_project_id)
@@ -33,10 +41,9 @@ module Gitlab
WHERE existing_members.project_id = forked_project_links.forked_to_project_id
)
INSERT_MEMBERS
-
- if missing_members?(start_id, end_id)
- BackgroundMigrationWorker.perform_in(RESCHEDULE_DELAY, "CreateForkNetworkMembershipsRange", [start_id, end_id])
- end
+ rescue ActiveRecord::RecordNotUnique => e
+ # `fork_network_member` was created concurrently in another migration
+ log(e.message)
end
def missing_members?(start_id, end_id)
diff --git a/lib/gitlab/background_migration/migrate_build_stage.rb b/lib/gitlab/background_migration/migrate_build_stage.rb
new file mode 100644
index 00000000000..8fe4f1a2289
--- /dev/null
+++ b/lib/gitlab/background_migration/migrate_build_stage.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+# rubocop:disable Metrics/AbcSize
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class MigrateBuildStage
+ module Migratable
+ class Stage < ActiveRecord::Base
+ self.table_name = 'ci_stages'
+ end
+
+ class Build < ActiveRecord::Base
+ self.table_name = 'ci_builds'
+
+ def ensure_stage!(attempts: 2)
+ find_stage || create_stage!
+ rescue ActiveRecord::RecordNotUnique
+ retry if (attempts -= 1) > 0
+ raise
+ end
+
+ def find_stage
+ Stage.find_by(name: self.stage || 'test',
+ pipeline_id: self.commit_id,
+ project_id: self.project_id)
+ end
+
+ def create_stage!
+ Stage.create!(name: self.stage || 'test',
+ pipeline_id: self.commit_id,
+ project_id: self.project_id)
+ end
+ end
+ end
+
+ def perform(start_id, stop_id)
+ stages = Migratable::Build.where('stage_id IS NULL')
+ .where('id BETWEEN ? AND ?', start_id, stop_id)
+ .map { |build| build.ensure_stage! }
+ .compact.map(&:id)
+
+ MigrateBuildStageIdReference.new.perform(start_id, stop_id)
+ MigrateStageStatus.new.perform(stages.min, stages.max)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb b/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
index 85749366bfd..d9d3d2e667b 100644
--- a/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
+++ b/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
@@ -16,281 +16,283 @@ module Gitlab
# And if the normalize behavior is changed in the future, it must be
# accompanied by another migration.
module Gitlab
- module LDAP
- class DN
- FormatError = Class.new(StandardError)
- MalformedError = Class.new(FormatError)
- UnsupportedError = Class.new(FormatError)
+ module Auth
+ module LDAP
+ class DN
+ FormatError = Class.new(StandardError)
+ MalformedError = Class.new(FormatError)
+ UnsupportedError = Class.new(FormatError)
- def self.normalize_value(given_value)
- dummy_dn = "placeholder=#{given_value}"
- normalized_dn = new(*dummy_dn).to_normalized_s
- normalized_dn.sub(/\Aplaceholder=/, '')
- end
+ def self.normalize_value(given_value)
+ dummy_dn = "placeholder=#{given_value}"
+ normalized_dn = new(*dummy_dn).to_normalized_s
+ normalized_dn.sub(/\Aplaceholder=/, '')
+ end
- ##
- # Initialize a DN, escaping as required. Pass in attributes in name/value
- # pairs. If there is a left over argument, it will be appended to the dn
- # without escaping (useful for a base string).
- #
- # Most uses of this class will be to escape a DN, rather than to parse it,
- # so storing the dn as an escaped String and parsing parts as required
- # with a state machine seems sensible.
- def initialize(*args)
- if args.length > 1
- initialize_array(args)
- else
- initialize_string(args[0])
+ ##
+ # Initialize a DN, escaping as required. Pass in attributes in name/value
+ # pairs. If there is a left over argument, it will be appended to the dn
+ # without escaping (useful for a base string).
+ #
+ # Most uses of this class will be to escape a DN, rather than to parse it,
+ # so storing the dn as an escaped String and parsing parts as required
+ # with a state machine seems sensible.
+ def initialize(*args)
+ if args.length > 1
+ initialize_array(args)
+ else
+ initialize_string(args[0])
+ end
end
- end
- ##
- # Parse a DN into key value pairs using ASN from
- # http://tools.ietf.org/html/rfc2253 section 3.
- # rubocop:disable Metrics/AbcSize
- # rubocop:disable Metrics/CyclomaticComplexity
- # rubocop:disable Metrics/PerceivedComplexity
- def each_pair
- state = :key
- key = StringIO.new
- value = StringIO.new
- hex_buffer = ""
+ ##
+ # Parse a DN into key value pairs using ASN from
+ # http://tools.ietf.org/html/rfc2253 section 3.
+ # rubocop:disable Metrics/AbcSize
+ # rubocop:disable Metrics/CyclomaticComplexity
+ # rubocop:disable Metrics/PerceivedComplexity
+ def each_pair
+ state = :key
+ key = StringIO.new
+ value = StringIO.new
+ hex_buffer = ""
- @dn.each_char.with_index do |char, dn_index|
- case state
- when :key then
- case char
- when 'a'..'z', 'A'..'Z' then
- state = :key_normal
- key << char
- when '0'..'9' then
- state = :key_oid
- key << char
- when ' ' then state = :key
- else raise(MalformedError, "Unrecognized first character of an RDN attribute type name \"#{char}\"")
- end
- when :key_normal then
- case char
- when '=' then state = :value
- when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char
- else raise(MalformedError, "Unrecognized RDN attribute type name character \"#{char}\"")
- end
- when :key_oid then
- case char
- when '=' then state = :value
- when '0'..'9', '.', ' ' then key << char
- else raise(MalformedError, "Unrecognized RDN OID attribute type name character \"#{char}\"")
- end
- when :value then
- case char
- when '\\' then state = :value_normal_escape
- when '"' then state = :value_quoted
- when ' ' then state = :value
- when '#' then
- state = :value_hexstring
- value << char
- when ',' then
- state = :key
- yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
- key = StringIO.new
- value = StringIO.new
- else
- state = :value_normal
- value << char
- end
- when :value_normal then
- case char
- when '\\' then state = :value_normal_escape
- when ',' then
- state = :key
- yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
- key = StringIO.new
- value = StringIO.new
- when '+' then raise(UnsupportedError, "Multivalued RDNs are not supported")
- else value << char
- end
- when :value_normal_escape then
- case char
- when '0'..'9', 'a'..'f', 'A'..'F' then
- state = :value_normal_escape_hex
- hex_buffer = char
- else
- state = :value_normal
- value << char
+ @dn.each_char.with_index do |char, dn_index|
+ case state
+ when :key then
+ case char
+ when 'a'..'z', 'A'..'Z' then
+ state = :key_normal
+ key << char
+ when '0'..'9' then
+ state = :key_oid
+ key << char
+ when ' ' then state = :key
+ else raise(MalformedError, "Unrecognized first character of an RDN attribute type name \"#{char}\"")
+ end
+ when :key_normal then
+ case char
+ when '=' then state = :value
+ when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char
+ else raise(MalformedError, "Unrecognized RDN attribute type name character \"#{char}\"")
+ end
+ when :key_oid then
+ case char
+ when '=' then state = :value
+ when '0'..'9', '.', ' ' then key << char
+ else raise(MalformedError, "Unrecognized RDN OID attribute type name character \"#{char}\"")
+ end
+ when :value then
+ case char
+ when '\\' then state = :value_normal_escape
+ when '"' then state = :value_quoted
+ when ' ' then state = :value
+ when '#' then
+ state = :value_hexstring
+ value << char
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else
+ state = :value_normal
+ value << char
+ end
+ when :value_normal then
+ case char
+ when '\\' then state = :value_normal_escape
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ when '+' then raise(UnsupportedError, "Multivalued RDNs are not supported")
+ else value << char
+ end
+ when :value_normal_escape then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_normal_escape_hex
+ hex_buffer = char
+ else
+ state = :value_normal
+ value << char
+ end
+ when :value_normal_escape_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_normal
+ value << "#{hex_buffer}#{char}".to_i(16).chr
+ else raise(MalformedError, "Invalid escaped hex code \"\\#{hex_buffer}#{char}\"")
+ end
+ when :value_quoted then
+ case char
+ when '\\' then state = :value_quoted_escape
+ when '"' then state = :value_end
+ else value << char
+ end
+ when :value_quoted_escape then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_quoted_escape_hex
+ hex_buffer = char
+ else
+ state = :value_quoted
+ value << char
+ end
+ when :value_quoted_escape_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_quoted
+ value << "#{hex_buffer}#{char}".to_i(16).chr
+ else raise(MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"#{char}\"")
+ end
+ when :value_hexstring then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_hexstring_hex
+ value << char
+ when ' ' then state = :value_end
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else raise(MalformedError, "Expected the first character of a hex pair, but got \"#{char}\"")
+ end
+ when :value_hexstring_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_hexstring
+ value << char
+ else raise(MalformedError, "Expected the second character of a hex pair, but got \"#{char}\"")
+ end
+ when :value_end then
+ case char
+ when ' ' then state = :value_end
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else raise(MalformedError, "Expected the end of an attribute value, but got \"#{char}\"")
+ end
+ else raise "Fell out of state machine"
end
- when :value_normal_escape_hex then
- case char
- when '0'..'9', 'a'..'f', 'A'..'F' then
- state = :value_normal
- value << "#{hex_buffer}#{char}".to_i(16).chr
- else raise(MalformedError, "Invalid escaped hex code \"\\#{hex_buffer}#{char}\"")
- end
- when :value_quoted then
- case char
- when '\\' then state = :value_quoted_escape
- when '"' then state = :value_end
- else value << char
- end
- when :value_quoted_escape then
- case char
- when '0'..'9', 'a'..'f', 'A'..'F' then
- state = :value_quoted_escape_hex
- hex_buffer = char
- else
- state = :value_quoted
- value << char
- end
- when :value_quoted_escape_hex then
- case char
- when '0'..'9', 'a'..'f', 'A'..'F' then
- state = :value_quoted
- value << "#{hex_buffer}#{char}".to_i(16).chr
- else raise(MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"#{char}\"")
- end
- when :value_hexstring then
- case char
- when '0'..'9', 'a'..'f', 'A'..'F' then
- state = :value_hexstring_hex
- value << char
- when ' ' then state = :value_end
- when ',' then
- state = :key
- yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
- key = StringIO.new
- value = StringIO.new
- else raise(MalformedError, "Expected the first character of a hex pair, but got \"#{char}\"")
- end
- when :value_hexstring_hex then
- case char
- when '0'..'9', 'a'..'f', 'A'..'F' then
- state = :value_hexstring
- value << char
- else raise(MalformedError, "Expected the second character of a hex pair, but got \"#{char}\"")
- end
- when :value_end then
- case char
- when ' ' then state = :value_end
- when ',' then
- state = :key
- yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
- key = StringIO.new
- value = StringIO.new
- else raise(MalformedError, "Expected the end of an attribute value, but got \"#{char}\"")
- end
- else raise "Fell out of state machine"
end
- end
- # Last pair
- raise(MalformedError, 'DN string ended unexpectedly') unless
- [:value, :value_normal, :value_hexstring, :value_end].include? state
+ # Last pair
+ raise(MalformedError, 'DN string ended unexpectedly') unless
+ [:value, :value_normal, :value_hexstring, :value_end].include? state
- yield key.string.strip, rstrip_except_escaped(value.string, @dn.length)
- end
+ yield key.string.strip, rstrip_except_escaped(value.string, @dn.length)
+ end
- def rstrip_except_escaped(str, dn_index)
- str_ends_with_whitespace = str.match(/\s\z/)
+ def rstrip_except_escaped(str, dn_index)
+ str_ends_with_whitespace = str.match(/\s\z/)
- if str_ends_with_whitespace
- dn_part_ends_with_escaped_whitespace = @dn[0, dn_index].match(/\\(\s+)\z/)
+ if str_ends_with_whitespace
+ dn_part_ends_with_escaped_whitespace = @dn[0, dn_index].match(/\\(\s+)\z/)
- if dn_part_ends_with_escaped_whitespace
- dn_part_rwhitespace = dn_part_ends_with_escaped_whitespace[1]
- num_chars_to_remove = dn_part_rwhitespace.length - 1
- str = str[0, str.length - num_chars_to_remove]
- else
- str.rstrip!
+ if dn_part_ends_with_escaped_whitespace
+ dn_part_rwhitespace = dn_part_ends_with_escaped_whitespace[1]
+ num_chars_to_remove = dn_part_rwhitespace.length - 1
+ str = str[0, str.length - num_chars_to_remove]
+ else
+ str.rstrip!
+ end
end
- end
- str
- end
+ str
+ end
- ##
- # Returns the DN as an array in the form expected by the constructor.
- def to_a
- a = []
- self.each_pair { |key, value| a << key << value } unless @dn.empty?
- a
- end
+ ##
+ # Returns the DN as an array in the form expected by the constructor.
+ def to_a
+ a = []
+ self.each_pair { |key, value| a << key << value } unless @dn.empty?
+ a
+ end
- ##
- # Return the DN as an escaped string.
- def to_s
- @dn
- end
+ ##
+ # Return the DN as an escaped string.
+ def to_s
+ @dn
+ end
- ##
- # Return the DN as an escaped and normalized string.
- def to_normalized_s
- self.class.new(*to_a).to_s.downcase
- end
+ ##
+ # Return the DN as an escaped and normalized string.
+ def to_normalized_s
+ self.class.new(*to_a).to_s.downcase
+ end
- # https://tools.ietf.org/html/rfc4514 section 2.4 lists these exceptions
- # for DN values. All of the following must be escaped in any normal string
- # using a single backslash ('\') as escape. The space character is left
- # out here because in a "normalized" string, spaces should only be escaped
- # if necessary (i.e. leading or trailing space).
- NORMAL_ESCAPES = [',', '+', '"', '\\', '<', '>', ';', '='].freeze
+ # https://tools.ietf.org/html/rfc4514 section 2.4 lists these exceptions
+ # for DN values. All of the following must be escaped in any normal string
+ # using a single backslash ('\') as escape. The space character is left
+ # out here because in a "normalized" string, spaces should only be escaped
+ # if necessary (i.e. leading or trailing space).
+ NORMAL_ESCAPES = [',', '+', '"', '\\', '<', '>', ';', '='].freeze
- # The following must be represented as escaped hex
- HEX_ESCAPES = {
- "\n" => '\0a',
- "\r" => '\0d'
- }.freeze
+ # The following must be represented as escaped hex
+ HEX_ESCAPES = {
+ "\n" => '\0a',
+ "\r" => '\0d'
+ }.freeze
- # Compiled character class regexp using the keys from the above hash, and
- # checking for a space or # at the start, or space at the end, of the
- # string.
- ESCAPE_RE = Regexp.new("(^ |^#| $|[" +
- NORMAL_ESCAPES.map { |e| Regexp.escape(e) }.join +
- "])")
+ # Compiled character class regexp using the keys from the above hash, and
+ # checking for a space or # at the start, or space at the end, of the
+ # string.
+ ESCAPE_RE = Regexp.new("(^ |^#| $|[" +
+ NORMAL_ESCAPES.map { |e| Regexp.escape(e) }.join +
+ "])")
- HEX_ESCAPE_RE = Regexp.new("([" +
- HEX_ESCAPES.keys.map { |e| Regexp.escape(e) }.join +
- "])")
+ HEX_ESCAPE_RE = Regexp.new("([" +
+ HEX_ESCAPES.keys.map { |e| Regexp.escape(e) }.join +
+ "])")
- ##
- # Escape a string for use in a DN value
- def self.escape(string)
- escaped = string.gsub(ESCAPE_RE) { |char| "\\" + char }
- escaped.gsub(HEX_ESCAPE_RE) { |char| HEX_ESCAPES[char] }
- end
+ ##
+ # Escape a string for use in a DN value
+ def self.escape(string)
+ escaped = string.gsub(ESCAPE_RE) { |char| "\\" + char }
+ escaped.gsub(HEX_ESCAPE_RE) { |char| HEX_ESCAPES[char] }
+ end
- private
+ private
- def initialize_array(args)
- buffer = StringIO.new
+ def initialize_array(args)
+ buffer = StringIO.new
- args.each_with_index do |arg, index|
- if index.even? # key
- buffer << "," if index > 0
- buffer << arg
- else # value
- buffer << "="
- buffer << self.class.escape(arg)
+ args.each_with_index do |arg, index|
+ if index.even? # key
+ buffer << "," if index > 0
+ buffer << arg
+ else # value
+ buffer << "="
+ buffer << self.class.escape(arg)
+ end
end
- end
- @dn = buffer.string
- end
+ @dn = buffer.string
+ end
- def initialize_string(arg)
- @dn = arg.to_s
- end
+ def initialize_string(arg)
+ @dn = arg.to_s
+ end
- ##
- # Proxy all other requests to the string object, because a DN is mainly
- # used within the library as a string
- # rubocop:disable GitlabSecurity/PublicSend
- def method_missing(method, *args, &block)
- @dn.send(method, *args, &block)
- end
+ ##
+ # Proxy all other requests to the string object, because a DN is mainly
+ # used within the library as a string
+ # rubocop:disable GitlabSecurity/PublicSend
+ def method_missing(method, *args, &block)
+ @dn.send(method, *args, &block)
+ end
- ##
- # Redefined to be consistent with redefined `method_missing` behavior
- def respond_to?(sym, include_private = false)
- @dn.respond_to?(sym, include_private)
+ ##
+ # Redefined to be consistent with redefined `method_missing` behavior
+ def respond_to?(sym, include_private = false)
+ @dn.respond_to?(sym, include_private)
+ end
end
end
end
@@ -302,11 +304,11 @@ module Gitlab
ldap_identities = Identity.where("provider like 'ldap%'").where(id: start_id..end_id)
ldap_identities.each do |identity|
begin
- identity.extern_uid = Gitlab::LDAP::DN.new(identity.extern_uid).to_normalized_s
+ identity.extern_uid = Gitlab::Auth::LDAP::DN.new(identity.extern_uid).to_normalized_s
unless identity.save
Rails.logger.info "Unable to normalize \"#{identity.extern_uid}\". Skipping."
end
- rescue Gitlab::LDAP::DN::FormatError => e
+ rescue Gitlab::Auth::LDAP::DN::FormatError => e
Rails.logger.info "Unable to normalize \"#{identity.extern_uid}\" due to \"#{e.message}\". Skipping."
end
end
diff --git a/lib/gitlab/background_migration/populate_untracked_uploads.rb b/lib/gitlab/background_migration/populate_untracked_uploads.rb
index 8a8e770940e..9232f20a063 100644
--- a/lib/gitlab/background_migration/populate_untracked_uploads.rb
+++ b/lib/gitlab/background_migration/populate_untracked_uploads.rb
@@ -5,157 +5,10 @@ module Gitlab
# This class processes a batch of rows in `untracked_files_for_uploads` by
# adding each file to the `uploads` table if it does not exist.
class PopulateUntrackedUploads # rubocop:disable Metrics/ClassLength
- # This class is responsible for producing the attributes necessary to
- # track an uploaded file in the `uploads` table.
- class UntrackedFile < ActiveRecord::Base # rubocop:disable Metrics/ClassLength, Metrics/LineLength
- self.table_name = 'untracked_files_for_uploads'
-
- # Ends with /:random_hex/:filename
- FILE_UPLOADER_PATH = %r{/\h+/[^/]+\z}
- FULL_PATH_CAPTURE = /\A(.+)#{FILE_UPLOADER_PATH}/
-
- # These regex patterns are tested against a relative path, relative to
- # the upload directory.
- # For convenience, if there exists a capture group in the pattern, then
- # it indicates the model_id.
- PATH_PATTERNS = [
- {
- pattern: %r{\A-/system/appearance/logo/(\d+)/},
- uploader: 'AttachmentUploader',
- model_type: 'Appearance'
- },
- {
- pattern: %r{\A-/system/appearance/header_logo/(\d+)/},
- uploader: 'AttachmentUploader',
- model_type: 'Appearance'
- },
- {
- pattern: %r{\A-/system/note/attachment/(\d+)/},
- uploader: 'AttachmentUploader',
- model_type: 'Note'
- },
- {
- pattern: %r{\A-/system/user/avatar/(\d+)/},
- uploader: 'AvatarUploader',
- model_type: 'User'
- },
- {
- pattern: %r{\A-/system/group/avatar/(\d+)/},
- uploader: 'AvatarUploader',
- model_type: 'Namespace'
- },
- {
- pattern: %r{\A-/system/project/avatar/(\d+)/},
- uploader: 'AvatarUploader',
- model_type: 'Project'
- },
- {
- pattern: FILE_UPLOADER_PATH,
- uploader: 'FileUploader',
- model_type: 'Project'
- }
- ].freeze
-
- def to_h
- @upload_hash ||= {
- path: upload_path,
- uploader: uploader,
- model_type: model_type,
- model_id: model_id,
- size: file_size,
- checksum: checksum
- }
- end
-
- def upload_path
- # UntrackedFile#path is absolute, but Upload#path depends on uploader
- @upload_path ||=
- if uploader == 'FileUploader'
- # Path relative to project directory in uploads
- matchd = path_relative_to_upload_dir.match(FILE_UPLOADER_PATH)
- matchd[0].sub(%r{\A/}, '') # remove leading slash
- else
- path
- end
- end
-
- def uploader
- matching_pattern_map[:uploader]
- end
-
- def model_type
- matching_pattern_map[:model_type]
- end
-
- def model_id
- return @model_id if defined?(@model_id)
-
- pattern = matching_pattern_map[:pattern]
- matchd = path_relative_to_upload_dir.match(pattern)
-
- # If something is captured (matchd[1] is not nil), it is a model_id
- # Only the FileUploader pattern will not match an ID
- @model_id = matchd[1] ? matchd[1].to_i : file_uploader_model_id
- end
-
- def file_size
- File.size(absolute_path)
- end
-
- def checksum
- Digest::SHA256.file(absolute_path).hexdigest
- end
-
- private
-
- def matching_pattern_map
- @matching_pattern_map ||= PATH_PATTERNS.find do |path_pattern_map|
- path_relative_to_upload_dir.match(path_pattern_map[:pattern])
- end
-
- unless @matching_pattern_map
- raise "Unknown upload path pattern \"#{path}\""
- end
-
- @matching_pattern_map
- end
-
- def file_uploader_model_id
- matchd = path_relative_to_upload_dir.match(FULL_PATH_CAPTURE)
- not_found_msg = <<~MSG
- Could not capture project full_path from a FileUploader path:
- "#{path_relative_to_upload_dir}"
- MSG
- raise not_found_msg unless matchd
-
- full_path = matchd[1]
- project = Project.find_by_full_path(full_path)
- return nil unless project
-
- project.id
- end
-
- # Not including a leading slash
- def path_relative_to_upload_dir
- upload_dir = Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR # rubocop:disable Metrics/LineLength
- base = %r{\A#{Regexp.escape(upload_dir)}/}
- @path_relative_to_upload_dir ||= path.sub(base, '')
- end
-
- def absolute_path
- File.join(Gitlab.config.uploads.storage_path, path)
- end
- end
-
- # This class is used to query the `uploads` table.
- class Upload < ActiveRecord::Base
- self.table_name = 'uploads'
- end
-
def perform(start_id, end_id)
return unless migrate?
- files = UntrackedFile.where(id: start_id..end_id)
+ files = Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::UntrackedFile.where(id: start_id..end_id)
processed_files = insert_uploads_if_needed(files)
processed_files.delete_all
@@ -165,7 +18,8 @@ module Gitlab
private
def migrate?
- UntrackedFile.table_exists? && Upload.table_exists?
+ Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::UntrackedFile.table_exists? &&
+ Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::Upload.table_exists?
end
def insert_uploads_if_needed(files)
@@ -197,7 +51,7 @@ module Gitlab
def filter_existing_uploads(files)
paths = files.map(&:upload_path)
- existing_paths = Upload.where(path: paths).pluck(:path).to_set
+ existing_paths = Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::Upload.where(path: paths).pluck(:path).to_set
files.reject do |file|
existing_paths.include?(file.upload_path)
@@ -229,7 +83,7 @@ module Gitlab
end
ids.each do |model_type, model_ids|
- model_class = Object.const_get(model_type)
+ model_class = "Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::#{model_type}".constantize
found_ids = model_class.where(id: model_ids.uniq).pluck(:id)
deleted_ids = ids[model_type] - found_ids
ids[model_type] = deleted_ids
@@ -249,8 +103,8 @@ module Gitlab
end
def drop_temp_table_if_finished
- if UntrackedFile.all.empty?
- UntrackedFile.connection.drop_table(:untracked_files_for_uploads,
+ if Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::UntrackedFile.all.empty? && !Rails.env.test? # Dropping a table intermittently breaks test cleanup
+ Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::UntrackedFile.connection.drop_table(:untracked_files_for_uploads,
if_exists: true)
end
end
diff --git a/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb b/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb
new file mode 100644
index 00000000000..a2c5acbde71
--- /dev/null
+++ b/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb
@@ -0,0 +1,201 @@
+# frozen_string_literal: true
+module Gitlab
+ module BackgroundMigration
+ module PopulateUntrackedUploadsDependencies
+ # This class is responsible for producing the attributes necessary to
+ # track an uploaded file in the `uploads` table.
+ class UntrackedFile < ActiveRecord::Base # rubocop:disable Metrics/ClassLength, Metrics/LineLength
+ self.table_name = 'untracked_files_for_uploads'
+
+ # Ends with /:random_hex/:filename
+ FILE_UPLOADER_PATH = %r{/\h+/[^/]+\z}
+ FULL_PATH_CAPTURE = /\A(.+)#{FILE_UPLOADER_PATH}/
+
+ # These regex patterns are tested against a relative path, relative to
+ # the upload directory.
+ # For convenience, if there exists a capture group in the pattern, then
+ # it indicates the model_id.
+ PATH_PATTERNS = [
+ {
+ pattern: %r{\A-/system/appearance/logo/(\d+)/},
+ uploader: 'AttachmentUploader',
+ model_type: 'Appearance'
+ },
+ {
+ pattern: %r{\A-/system/appearance/header_logo/(\d+)/},
+ uploader: 'AttachmentUploader',
+ model_type: 'Appearance'
+ },
+ {
+ pattern: %r{\A-/system/note/attachment/(\d+)/},
+ uploader: 'AttachmentUploader',
+ model_type: 'Note'
+ },
+ {
+ pattern: %r{\A-/system/user/avatar/(\d+)/},
+ uploader: 'AvatarUploader',
+ model_type: 'User'
+ },
+ {
+ pattern: %r{\A-/system/group/avatar/(\d+)/},
+ uploader: 'AvatarUploader',
+ model_type: 'Namespace'
+ },
+ {
+ pattern: %r{\A-/system/project/avatar/(\d+)/},
+ uploader: 'AvatarUploader',
+ model_type: 'Project'
+ },
+ {
+ pattern: FILE_UPLOADER_PATH,
+ uploader: 'FileUploader',
+ model_type: 'Project'
+ }
+ ].freeze
+
+ def to_h
+ @upload_hash ||= {
+ path: upload_path,
+ uploader: uploader,
+ model_type: model_type,
+ model_id: model_id,
+ size: file_size,
+ checksum: checksum
+ }
+ end
+
+ def upload_path
+ # UntrackedFile#path is absolute, but Upload#path depends on uploader
+ @upload_path ||=
+ if uploader == 'FileUploader'
+ # Path relative to project directory in uploads
+ matchd = path_relative_to_upload_dir.match(FILE_UPLOADER_PATH)
+ matchd[0].sub(%r{\A/}, '') # remove leading slash
+ else
+ path
+ end
+ end
+
+ def uploader
+ matching_pattern_map[:uploader]
+ end
+
+ def model_type
+ matching_pattern_map[:model_type]
+ end
+
+ def model_id
+ return @model_id if defined?(@model_id)
+
+ pattern = matching_pattern_map[:pattern]
+ matchd = path_relative_to_upload_dir.match(pattern)
+
+ # If something is captured (matchd[1] is not nil), it is a model_id
+ # Only the FileUploader pattern will not match an ID
+ @model_id = matchd[1] ? matchd[1].to_i : file_uploader_model_id
+ end
+
+ def file_size
+ File.size(absolute_path)
+ end
+
+ def checksum
+ Digest::SHA256.file(absolute_path).hexdigest
+ end
+
+ private
+
+ def matching_pattern_map
+ @matching_pattern_map ||= PATH_PATTERNS.find do |path_pattern_map|
+ path_relative_to_upload_dir.match(path_pattern_map[:pattern])
+ end
+
+ unless @matching_pattern_map
+ raise "Unknown upload path pattern \"#{path}\""
+ end
+
+ @matching_pattern_map
+ end
+
+ def file_uploader_model_id
+ matchd = path_relative_to_upload_dir.match(FULL_PATH_CAPTURE)
+ not_found_msg = <<~MSG
+ Could not capture project full_path from a FileUploader path:
+ "#{path_relative_to_upload_dir}"
+ MSG
+ raise not_found_msg unless matchd
+
+ full_path = matchd[1]
+ project = Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::Project.find_by_full_path(full_path)
+ return nil unless project
+
+ project.id
+ end
+
+ # Not including a leading slash
+ def path_relative_to_upload_dir
+ upload_dir = Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR # rubocop:disable Metrics/LineLength
+ base = %r{\A#{Regexp.escape(upload_dir)}/}
+ @path_relative_to_upload_dir ||= path.sub(base, '')
+ end
+
+ def absolute_path
+ File.join(Gitlab.config.uploads.storage_path, path)
+ end
+ end
+
+ # Avoid using application code
+ class Upload < ActiveRecord::Base
+ self.table_name = 'uploads'
+ end
+
+ # Avoid using application code
+ class Appearance < ActiveRecord::Base
+ self.table_name = 'appearances'
+ end
+
+ # Avoid using application code
+ class Namespace < ActiveRecord::Base
+ self.table_name = 'namespaces'
+ end
+
+ # Avoid using application code
+ class Note < ActiveRecord::Base
+ self.table_name = 'notes'
+ end
+
+ # Avoid using application code
+ class User < ActiveRecord::Base
+ self.table_name = 'users'
+ end
+
+ # Since project Markdown upload paths don't contain the project ID, we have to find the
+ # project by its full_path. Due to MySQL/PostgreSQL differences, and historical reasons,
+ # the logic is somewhat complex, so I've mostly copied it in here.
+ class Project < ActiveRecord::Base
+ self.table_name = 'projects'
+
+ def self.find_by_full_path(path)
+ binary = Gitlab::Database.mysql? ? 'BINARY' : ''
+ order_sql = "(CASE WHEN #{binary} routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)"
+ where_full_path_in(path).reorder(order_sql).take
+ end
+
+ def self.where_full_path_in(path)
+ cast_lower = Gitlab::Database.postgresql?
+
+ path = connection.quote(path)
+
+ where =
+ if cast_lower
+ "(LOWER(routes.path) = LOWER(#{path}))"
+ else
+ "(routes.path = #{path})"
+ end
+
+ joins("INNER JOIN routes ON routes.source_id = projects.id AND routes.source_type = 'Project'").where(where)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/prepare_untracked_uploads.rb b/lib/gitlab/background_migration/prepare_untracked_uploads.rb
index a7a1bbe1752..914a9e48a2f 100644
--- a/lib/gitlab/background_migration/prepare_untracked_uploads.rb
+++ b/lib/gitlab/background_migration/prepare_untracked_uploads.rb
@@ -43,7 +43,11 @@ module Gitlab
store_untracked_file_paths
- schedule_populate_untracked_uploads_jobs
+ if UntrackedFile.all.empty?
+ drop_temp_table
+ else
+ schedule_populate_untracked_uploads_jobs
+ end
end
private
@@ -92,7 +96,7 @@ module Gitlab
end
end
- yield(paths)
+ yield(paths) if paths.any?
end
def build_find_command(search_dir)
@@ -165,6 +169,13 @@ module Gitlab
bulk_queue_background_migration_jobs_by_range(
UntrackedFile, FOLLOW_UP_MIGRATION)
end
+
+ def drop_temp_table
+ unless Rails.env.test? # Dropping a table intermittently breaks test cleanup
+ UntrackedFile.connection.drop_table(:untracked_files_for_uploads,
+ if_exists: true)
+ end
+ end
end
end
end
diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb
index 945d70e7a24..3ce5f807989 100644
--- a/lib/gitlab/checks/change_access.rb
+++ b/lib/gitlab/checks/change_access.rb
@@ -16,11 +16,11 @@ module Gitlab
lfs_objects_missing: 'LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".'
}.freeze
- attr_reader :user_access, :project, :skip_authorization, :protocol, :oldrev, :newrev, :ref, :branch_name, :tag_name
+ attr_reader :user_access, :project, :skip_authorization, :skip_lfs_integrity_check, :protocol, :oldrev, :newrev, :ref, :branch_name, :tag_name
def initialize(
change, user_access:, project:, skip_authorization: false,
- protocol:
+ skip_lfs_integrity_check: false, protocol:
)
@oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
@branch_name = Gitlab::Git.branch_name(@ref)
@@ -28,16 +28,18 @@ module Gitlab
@user_access = user_access
@project = project
@skip_authorization = skip_authorization
+ @skip_lfs_integrity_check = skip_lfs_integrity_check
@protocol = protocol
end
- def exec
+ def exec(skip_commits_check: false)
return true if skip_authorization
push_checks
branch_checks
tag_checks
- lfs_objects_exist_check
+ lfs_objects_exist_check unless skip_lfs_integrity_check
+ commits_check unless skip_commits_check
true
end
@@ -117,8 +119,31 @@ module Gitlab
end
end
+ def commits_check
+ return if deletion? || newrev.nil?
+ return unless should_run_commit_validations?
+
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ee/issues/3593
+ ::Gitlab::GitalyClient.allow_n_plus_1_calls do
+ commits.each do |commit|
+ commit_check.validate(commit, validations_for_commit(commit))
+ end
+ end
+
+ commit_check.validate_file_paths
+ end
+
+ # Method overwritten in EE to inject custom validations
+ def validations_for_commit(_)
+ []
+ end
+
private
+ def should_run_commit_validations?
+ commit_check.validate_lfs_file_locks?
+ end
+
def updated_from_web?
protocol == 'web'
end
@@ -150,6 +175,14 @@ module Gitlab
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:lfs_objects_missing]
end
end
+
+ def commit_check
+ @commit_check ||= Gitlab::Checks::CommitCheck.new(project, user_access.user, newrev, oldrev)
+ end
+
+ def commits
+ @commits ||= project.repository.new_commits(newrev)
+ end
end
end
end
diff --git a/lib/gitlab/checks/commit_check.rb b/lib/gitlab/checks/commit_check.rb
new file mode 100644
index 00000000000..43a52b493bb
--- /dev/null
+++ b/lib/gitlab/checks/commit_check.rb
@@ -0,0 +1,61 @@
+module Gitlab
+ module Checks
+ class CommitCheck
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :project, :user, :newrev, :oldrev
+
+ def initialize(project, user, newrev, oldrev)
+ @project = project
+ @user = user
+ @newrev = user
+ @oldrev = user
+ @file_paths = []
+ end
+
+ def validate(commit, validations)
+ return if validations.empty? && path_validations.empty?
+
+ commit.raw_deltas.each do |diff|
+ @file_paths << (diff.new_path || diff.old_path)
+
+ validations.each do |validation|
+ if error = validation.call(diff)
+ raise ::Gitlab::GitAccess::UnauthorizedError, error
+ end
+ end
+ end
+ end
+
+ def validate_file_paths
+ path_validations.each do |validation|
+ if error = validation.call(@file_paths)
+ raise ::Gitlab::GitAccess::UnauthorizedError, error
+ end
+ end
+ end
+
+ def validate_lfs_file_locks?
+ strong_memoize(:validate_lfs_file_locks) do
+ project.lfs_enabled? && project.lfs_file_locks.any? && newrev && oldrev
+ end
+ end
+
+ private
+
+ def lfs_file_locks_validation
+ lambda do |paths|
+ lfs_lock = project.lfs_file_locks.where(path: paths).where.not(user_id: user.id).first
+
+ if lfs_lock
+ return "The path '#{lfs_lock.path}' is locked in Git LFS by #{lfs_lock.user.name}"
+ end
+ end
+ end
+
+ def path_validations
+ validate_lfs_file_locks? ? [lfs_file_locks_validation] : []
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/base.rb b/lib/gitlab/ci/pipeline/expression/lexeme/base.rb
new file mode 100644
index 00000000000..047ab66e9b3
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/base.rb
@@ -0,0 +1,25 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ module Lexeme
+ class Base
+ def evaluate(**variables)
+ raise NotImplementedError
+ end
+
+ def self.build(token)
+ raise NotImplementedError
+ end
+
+ def self.scan(scanner)
+ if scanner.scan(self::PATTERN)
+ Expression::Token.new(scanner.matched, self)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/equals.rb b/lib/gitlab/ci/pipeline/expression/lexeme/equals.rb
new file mode 100644
index 00000000000..3a2f0c6924e
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/equals.rb
@@ -0,0 +1,26 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ module Lexeme
+ class Equals < Lexeme::Operator
+ PATTERN = /==/.freeze
+
+ def initialize(left, right)
+ @left = left
+ @right = right
+ end
+
+ def evaluate(variables = {})
+ @left.evaluate(variables) == @right.evaluate(variables)
+ end
+
+ def self.build(_value, behind, ahead)
+ new(behind, ahead)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/null.rb b/lib/gitlab/ci/pipeline/expression/lexeme/null.rb
new file mode 100644
index 00000000000..a2778716924
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/null.rb
@@ -0,0 +1,25 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ module Lexeme
+ class Null < Lexeme::Value
+ PATTERN = /null/.freeze
+
+ def initialize(value = nil)
+ @value = nil
+ end
+
+ def evaluate(variables = {})
+ nil
+ end
+
+ def self.build(_value)
+ self.new
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/operator.rb b/lib/gitlab/ci/pipeline/expression/lexeme/operator.rb
new file mode 100644
index 00000000000..f640d0b5855
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/operator.rb
@@ -0,0 +1,15 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ module Lexeme
+ class Operator < Lexeme::Base
+ def self.type
+ :operator
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/string.rb b/lib/gitlab/ci/pipeline/expression/lexeme/string.rb
new file mode 100644
index 00000000000..48bde213d44
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/string.rb
@@ -0,0 +1,25 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ module Lexeme
+ class String < Lexeme::Value
+ PATTERN = /("(?<string>.+?)")|('(?<string>.+?)')/.freeze
+
+ def initialize(value)
+ @value = value
+ end
+
+ def evaluate(variables = {})
+ @value.to_s
+ end
+
+ def self.build(string)
+ new(string.match(PATTERN)[:string])
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/value.rb b/lib/gitlab/ci/pipeline/expression/lexeme/value.rb
new file mode 100644
index 00000000000..f2611d65faf
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/value.rb
@@ -0,0 +1,15 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ module Lexeme
+ class Value < Lexeme::Base
+ def self.type
+ :value
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/variable.rb b/lib/gitlab/ci/pipeline/expression/lexeme/variable.rb
new file mode 100644
index 00000000000..b781c15fd67
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/variable.rb
@@ -0,0 +1,25 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ module Lexeme
+ class Variable < Lexeme::Value
+ PATTERN = /\$(?<name>\w+)/.freeze
+
+ def initialize(name)
+ @name = name
+ end
+
+ def evaluate(variables = {})
+ HashWithIndifferentAccess.new(variables).fetch(@name, nil)
+ end
+
+ def self.build(string)
+ new(string.match(PATTERN)[:name])
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexer.rb b/lib/gitlab/ci/pipeline/expression/lexer.rb
new file mode 100644
index 00000000000..e1c68b7c3c2
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/lexer.rb
@@ -0,0 +1,59 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ class Lexer
+ include ::Gitlab::Utils::StrongMemoize
+
+ LEXEMES = [
+ Expression::Lexeme::Variable,
+ Expression::Lexeme::String,
+ Expression::Lexeme::Null,
+ Expression::Lexeme::Equals
+ ].freeze
+
+ SyntaxError = Class.new(Statement::StatementError)
+
+ MAX_TOKENS = 100
+
+ def initialize(statement, max_tokens: MAX_TOKENS)
+ @scanner = StringScanner.new(statement)
+ @max_tokens = max_tokens
+ end
+
+ def tokens
+ strong_memoize(:tokens) { tokenize }
+ end
+
+ def lexemes
+ tokens.map(&:to_lexeme)
+ end
+
+ private
+
+ def tokenize
+ tokens = []
+
+ @max_tokens.times do
+ @scanner.skip(/\s+/) # ignore whitespace
+
+ return tokens if @scanner.eos?
+
+ lexeme = LEXEMES.find do |type|
+ type.scan(@scanner).tap do |token|
+ tokens.push(token) if token.present?
+ end
+ end
+
+ unless lexeme.present?
+ raise Lexer::SyntaxError, 'Unknown lexeme found!'
+ end
+ end
+
+ raise Lexer::SyntaxError, 'Too many tokens!'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/parser.rb b/lib/gitlab/ci/pipeline/expression/parser.rb
new file mode 100644
index 00000000000..90f94d0b763
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/parser.rb
@@ -0,0 +1,40 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ class Parser
+ def initialize(tokens)
+ @tokens = tokens.to_enum
+ @nodes = []
+ end
+
+ ##
+ # This produces a reverse descent parse tree.
+ #
+ # It currently does not support precedence of operators.
+ #
+ def tree
+ while token = @tokens.next
+ case token.type
+ when :operator
+ token.build(@nodes.pop, tree).tap do |node|
+ @nodes.push(node)
+ end
+ when :value
+ token.build.tap do |leaf|
+ @nodes.push(leaf)
+ end
+ end
+ end
+ rescue StopIteration
+ @nodes.last || Lexeme::Null.new
+ end
+
+ def self.seed(statement)
+ new(Expression::Lexer.new(statement).tokens)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/statement.rb b/lib/gitlab/ci/pipeline/expression/statement.rb
new file mode 100644
index 00000000000..4f0e101b730
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/statement.rb
@@ -0,0 +1,42 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ class Statement
+ StatementError = Class.new(StandardError)
+
+ GRAMMAR = [
+ %w[variable equals string],
+ %w[variable equals variable],
+ %w[variable equals null],
+ %w[string equals variable],
+ %w[null equals variable],
+ %w[variable]
+ ].freeze
+
+ def initialize(statement, pipeline)
+ @lexer = Expression::Lexer.new(statement)
+
+ @variables = pipeline.variables.map do |variable|
+ [variable.key, variable.value]
+ end
+ end
+
+ def parse_tree
+ raise StatementError if @lexer.lexemes.empty?
+
+ unless GRAMMAR.find { |syntax| syntax == @lexer.lexemes }
+ raise StatementError, 'Unknown pipeline expression!'
+ end
+
+ Expression::Parser.new(@lexer.tokens).tree
+ end
+
+ def evaluate
+ parse_tree.evaluate(@variables.to_h)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/token.rb b/lib/gitlab/ci/pipeline/expression/token.rb
new file mode 100644
index 00000000000..58211800b88
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/token.rb
@@ -0,0 +1,28 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ class Token
+ attr_reader :value, :lexeme
+
+ def initialize(value, lexeme)
+ @value = value
+ @lexeme = lexeme
+ end
+
+ def build(*args)
+ @lexeme.build(@value, *args)
+ end
+
+ def type
+ @lexeme.type
+ end
+
+ def to_lexeme
+ @lexeme.name.demodulize.downcase
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb
index 0735243e021..9576d5a3fd8 100644
--- a/lib/gitlab/contributions_calendar.rb
+++ b/lib/gitlab/contributions_calendar.rb
@@ -34,6 +34,8 @@ module Gitlab
end
def events_by_date(date)
+ return Event.none unless can_read_cross_project?
+
events = Event.contributions.where(author_id: contributor.id)
.where(created_at: date.beginning_of_day..date.end_of_day)
.where(project_id: projects)
@@ -53,6 +55,10 @@ module Gitlab
private
+ def can_read_cross_project?
+ Ability.allowed?(current_user, :read_cross_project)
+ end
+
def event_counts(date_from, feature)
t = Event.arel_table
diff --git a/lib/gitlab/cross_project_access.rb b/lib/gitlab/cross_project_access.rb
new file mode 100644
index 00000000000..6eaed51b64c
--- /dev/null
+++ b/lib/gitlab/cross_project_access.rb
@@ -0,0 +1,67 @@
+module Gitlab
+ class CrossProjectAccess
+ class << self
+ delegate :add_check, :find_check, :checks,
+ to: :instance
+ end
+
+ def self.instance
+ @instance ||= new
+ end
+
+ attr_reader :checks
+
+ def initialize
+ @checks = {}
+ end
+
+ def add_check(
+ klass,
+ actions: {},
+ positive_condition: nil,
+ negative_condition: nil,
+ skip: false)
+
+ new_check = CheckInfo.new(actions,
+ positive_condition,
+ negative_condition,
+ skip
+ )
+
+ @checks[klass] ||= Gitlab::CrossProjectAccess::CheckCollection.new
+ @checks[klass].add_check(new_check)
+ recalculate_checks_for_class(klass)
+
+ @checks[klass]
+ end
+
+ def find_check(object)
+ @cached_checks ||= Hash.new do |cache, new_class|
+ parent_classes = @checks.keys.select { |existing_class| new_class <= existing_class }
+ closest_class = closest_parent(parent_classes, new_class)
+ cache[new_class] = @checks[closest_class]
+ end
+
+ @cached_checks[object.class]
+ end
+
+ private
+
+ def recalculate_checks_for_class(klass)
+ new_collection = @checks[klass]
+
+ @checks.each do |existing_class, existing_check_collection|
+ if existing_class < klass
+ existing_check_collection.add_collection(new_collection)
+ elsif klass < existing_class
+ new_collection.add_collection(existing_check_collection)
+ end
+ end
+ end
+
+ def closest_parent(classes, subject)
+ relevant_ancestors = subject.ancestors & classes
+ relevant_ancestors.first
+ end
+ end
+end
diff --git a/lib/gitlab/cross_project_access/check_collection.rb b/lib/gitlab/cross_project_access/check_collection.rb
new file mode 100644
index 00000000000..88376232065
--- /dev/null
+++ b/lib/gitlab/cross_project_access/check_collection.rb
@@ -0,0 +1,47 @@
+module Gitlab
+ class CrossProjectAccess
+ class CheckCollection
+ attr_reader :checks
+
+ def initialize
+ @checks = []
+ end
+
+ def add_collection(collection)
+ @checks |= collection.checks
+ end
+
+ def add_check(check)
+ @checks << check
+ end
+
+ def should_run?(object)
+ skips, runs = arranged_checks
+
+ # If one rule tells us to skip, we skip the cross project check
+ return false if skips.any? { |check| check.should_skip?(object) }
+
+ # If the rule isn't skipped, we run it if any of the checks says we
+ # should run
+ runs.any? { |check| check.should_run?(object) }
+ end
+
+ def arranged_checks
+ return [@skips, @runs] if @skips && @runs
+
+ @skips = []
+ @runs = []
+
+ @checks.each do |check|
+ if check.skip
+ @skips << check
+ else
+ @runs << check
+ end
+ end
+
+ [@skips, @runs]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cross_project_access/check_info.rb b/lib/gitlab/cross_project_access/check_info.rb
new file mode 100644
index 00000000000..e8a845c7f1e
--- /dev/null
+++ b/lib/gitlab/cross_project_access/check_info.rb
@@ -0,0 +1,66 @@
+module Gitlab
+ class CrossProjectAccess
+ class CheckInfo
+ attr_accessor :actions, :positive_condition, :negative_condition, :skip
+
+ def initialize(actions, positive_condition, negative_condition, skip)
+ @actions = actions
+ @positive_condition = positive_condition
+ @negative_condition = negative_condition
+ @skip = skip
+ end
+
+ def should_skip?(object)
+ return !should_run?(object) unless @skip
+
+ skip_for_action = @actions[current_action(object)]
+ skip_for_action = false if @actions[current_action(object)].nil?
+
+ # We need to do the opposite of what was defined in the following cases:
+ # - skip_cross_project_access_check index: true, if: -> { false }
+ # - skip_cross_project_access_check index: true, unless: -> { true }
+ if positive_condition_is_false?(object)
+ skip_for_action = !skip_for_action
+ end
+
+ if negative_condition_is_true?(object)
+ skip_for_action = !skip_for_action
+ end
+
+ skip_for_action
+ end
+
+ def should_run?(object)
+ return !should_skip?(object) if @skip
+
+ run_for_action = @actions[current_action(object)]
+ run_for_action = true if @actions[current_action(object)].nil?
+
+ # We need to do the opposite of what was defined in the following cases:
+ # - requires_cross_project_access index: true, if: -> { false }
+ # - requires_cross_project_access index: true, unless: -> { true }
+ if positive_condition_is_false?(object)
+ run_for_action = !run_for_action
+ end
+
+ if negative_condition_is_true?(object)
+ run_for_action = !run_for_action
+ end
+
+ run_for_action
+ end
+
+ def positive_condition_is_false?(object)
+ @positive_condition && !object.instance_exec(&@positive_condition)
+ end
+
+ def negative_condition_is_true?(object)
+ @negative_condition && object.instance_exec(&@negative_condition)
+ end
+
+ def current_action(object)
+ object.respond_to?(:action_name) ? object.action_name.to_sym : nil
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cross_project_access/class_methods.rb b/lib/gitlab/cross_project_access/class_methods.rb
new file mode 100644
index 00000000000..90eac94800c
--- /dev/null
+++ b/lib/gitlab/cross_project_access/class_methods.rb
@@ -0,0 +1,48 @@
+module Gitlab
+ class CrossProjectAccess
+ module ClassMethods
+ def requires_cross_project_access(*args)
+ positive_condition, negative_condition, actions = extract_params(args)
+
+ Gitlab::CrossProjectAccess.add_check(
+ self,
+ actions: actions,
+ positive_condition: positive_condition,
+ negative_condition: negative_condition
+ )
+ end
+
+ def skip_cross_project_access_check(*args)
+ positive_condition, negative_condition, actions = extract_params(args)
+
+ Gitlab::CrossProjectAccess.add_check(
+ self,
+ actions: actions,
+ positive_condition: positive_condition,
+ negative_condition: negative_condition,
+ skip: true
+ )
+ end
+
+ private
+
+ def extract_params(args)
+ actions = {}
+ positive_condition = nil
+ negative_condition = nil
+
+ args.each do |argument|
+ if argument.is_a?(Hash)
+ positive_condition = argument.delete(:if)
+ negative_condition = argument.delete(:unless)
+ actions.merge!(argument)
+ else
+ actions[argument] = true
+ end
+ end
+
+ [positive_condition, negative_condition, actions]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/base_query.rb b/lib/gitlab/cycle_analytics/base_query.rb
index 8b3bc3e440d..86d708be0d6 100644
--- a/lib/gitlab/cycle_analytics/base_query.rb
+++ b/lib/gitlab/cycle_analytics/base_query.rb
@@ -8,13 +8,14 @@ module Gitlab
private
def base_query
- @base_query ||= stage_query
+ @base_query ||= stage_query(@project.id) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
- def stage_query
+ def stage_query(project_ids)
query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id]))
.join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id]))
- .where(issue_table[:project_id].eq(@project.id)) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ .project(issue_table[:project_id].as("project_id"))
+ .where(issue_table[:project_id].in(project_ids))
.where(issue_table[:created_at].gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables
# Load merge_requests
diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb
index cac31ea8cff..038d5a19bc4 100644
--- a/lib/gitlab/cycle_analytics/base_stage.rb
+++ b/lib/gitlab/cycle_analytics/base_stage.rb
@@ -21,17 +21,28 @@ module Gitlab
end
def median
- cte_table = Arel::Table.new("cte_table_for_#{name}")
+ BatchLoader.for(@project.id).batch(key: name) do |project_ids, loader|
+ cte_table = Arel::Table.new("cte_table_for_#{name}")
- # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
- # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
- # We compute the (end_time - start_time) interval, and give it an alias based on the current
- # cycle analytics stage.
- interval_query = Arel::Nodes::As.new(
- cte_table,
- subtract_datetimes(base_query.dup, start_time_attrs, end_time_attrs, name.to_s))
+ # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
+ # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
+ # We compute the (end_time - start_time) interval, and give it an alias based on the current
+ # cycle analytics stage.
+ interval_query = Arel::Nodes::As.new(cte_table,
+ subtract_datetimes(stage_query(project_ids), start_time_attrs, end_time_attrs, name.to_s))
- median_datetime(cte_table, interval_query, name)
+ if project_ids.one?
+ loader.call(@project.id, median_datetime(cte_table, interval_query, name))
+ else
+ begin
+ median_datetimes(cte_table, interval_query, name, :project_id)&.each do |project_id, median|
+ loader.call(project_id, median)
+ end
+ rescue NotSupportedError
+ {}
+ end
+ end
+ end
end
def name
diff --git a/lib/gitlab/cycle_analytics/production_helper.rb b/lib/gitlab/cycle_analytics/production_helper.rb
index 7a889b3877f..d0ca62e46e4 100644
--- a/lib/gitlab/cycle_analytics/production_helper.rb
+++ b/lib/gitlab/cycle_analytics/production_helper.rb
@@ -1,8 +1,8 @@
module Gitlab
module CycleAnalytics
module ProductionHelper
- def stage_query
- super
+ def stage_query(project_ids)
+ super(project_ids)
.where(mr_metrics_table[:first_deployed_to_production_at]
.gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
diff --git a/lib/gitlab/cycle_analytics/test_stage.rb b/lib/gitlab/cycle_analytics/test_stage.rb
index 2b5f72bef89..0e9d235ca79 100644
--- a/lib/gitlab/cycle_analytics/test_stage.rb
+++ b/lib/gitlab/cycle_analytics/test_stage.rb
@@ -25,11 +25,11 @@ module Gitlab
_("Total test time for all commits/merges")
end
- def stage_query
+ def stage_query(project_ids)
if @options[:branch]
- super.where(build_table[:ref].eq(@options[:branch]))
+ super(project_ids).where(build_table[:ref].eq(@options[:branch]))
else
- super
+ super(project_ids)
end
end
end
diff --git a/lib/gitlab/cycle_analytics/usage_data.rb b/lib/gitlab/cycle_analytics/usage_data.rb
new file mode 100644
index 00000000000..5122e3417ca
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/usage_data.rb
@@ -0,0 +1,72 @@
+module Gitlab
+ module CycleAnalytics
+ class UsageData
+ PROJECTS_LIMIT = 10
+
+ attr_reader :projects, :options
+
+ def initialize
+ @projects = Project.sorted_by_activity.limit(PROJECTS_LIMIT)
+ @options = { from: 7.days.ago }
+ end
+
+ def to_json
+ total = 0
+
+ values =
+ medians_per_stage.each_with_object({}) do |(stage_name, medians), hsh|
+ calculations = stage_values(medians)
+
+ total += calculations.values.compact.sum
+ hsh[stage_name] = calculations
+ end
+
+ values[:total] = total
+
+ { avg_cycle_analytics: values }
+ end
+
+ private
+
+ def medians_per_stage
+ projects.each_with_object({}) do |project, hsh|
+ ::CycleAnalytics.new(project, options).all_medians_per_stage.each do |stage_name, median|
+ hsh[stage_name] ||= []
+ hsh[stage_name] << median
+ end
+ end
+ end
+
+ def stage_values(medians)
+ medians = medians.map(&:presence).compact
+ average = calc_average(medians)
+
+ {
+ average: average,
+ sd: standard_deviation(medians, average),
+ missing: projects.length - medians.length
+ }
+ end
+
+ def calc_average(values)
+ return if values.empty?
+
+ (values.sum / values.length).to_i
+ end
+
+ def standard_deviation(values, average)
+ Math.sqrt(sample_variance(values, average)).to_i
+ end
+
+ def sample_variance(values, average)
+ return 0 if values.length <= 1
+
+ sum = values.inject(0) do |acc, val|
+ acc + (val - average)**2
+ end
+
+ sum / (values.length - 1)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/median.rb b/lib/gitlab/database/median.rb
index 059054ac9ff..74fed447289 100644
--- a/lib/gitlab/database/median.rb
+++ b/lib/gitlab/database/median.rb
@@ -2,18 +2,14 @@
module Gitlab
module Database
module Median
+ NotSupportedError = Class.new(StandardError)
+
def median_datetime(arel_table, query_so_far, column_sym)
- median_queries =
- if Gitlab::Database.postgresql?
- pg_median_datetime_sql(arel_table, query_so_far, column_sym)
- elsif Gitlab::Database.mysql?
- mysql_median_datetime_sql(arel_table, query_so_far, column_sym)
- end
-
- results = Array.wrap(median_queries).map do |query|
- ActiveRecord::Base.connection.execute(query)
- end
- extract_median(results).presence
+ extract_median(execute_queries(arel_table, query_so_far, column_sym)).presence
+ end
+
+ def median_datetimes(arel_table, query_so_far, column_sym, partition_column)
+ extract_medians(execute_queries(arel_table, query_so_far, column_sym, partition_column)).presence
end
def extract_median(results)
@@ -21,13 +17,21 @@ module Gitlab
if Gitlab::Database.postgresql?
result = result.first.presence
- median = result['median'] if result
- median.to_f if median
+
+ result['median']&.to_f if result
elsif Gitlab::Database.mysql?
result.to_a.flatten.first
end
end
+ def extract_medians(results)
+ median_values = results.compact.first.values
+
+ median_values.each_with_object({}) do |(id, median), hash|
+ hash[id.to_i] = median&.to_f
+ end
+ end
+
def mysql_median_datetime_sql(arel_table, query_so_far, column_sym)
query = arel_table
.from(arel_table.project(Arel.sql('*')).order(arel_table[column_sym]).as(arel_table.table_name))
@@ -53,7 +57,7 @@ module Gitlab
]
end
- def pg_median_datetime_sql(arel_table, query_so_far, column_sym)
+ def pg_median_datetime_sql(arel_table, query_so_far, column_sym, partition_column = nil)
# Create a CTE with the column we're operating on, row number (after sorting by the column
# we're operating on), and count of the table we're operating on (duplicated across) all rows
# of the CTE. For example, if we're looking to find the median of the `projects.star_count`
@@ -64,41 +68,107 @@ module Gitlab
# 5 | 1 | 3
# 9 | 2 | 3
# 15 | 3 | 3
+ #
+ # If a partition column is used we will do the same operation but for separate partitions,
+ # when that happens the CTE might look like this:
+ #
+ # project_id | star_count | row_id | ct
+ # ------------+------------+--------+----
+ # 1 | 5 | 1 | 2
+ # 1 | 9 | 2 | 2
+ # 2 | 10 | 1 | 3
+ # 2 | 15 | 2 | 3
+ # 2 | 20 | 3 | 3
cte_table = Arel::Table.new("ordered_records")
+
cte = Arel::Nodes::As.new(
cte_table,
- arel_table
- .project(
- arel_table[column_sym].as(column_sym.to_s),
- Arel::Nodes::Over.new(Arel::Nodes::NamedFunction.new("row_number", []),
- Arel::Nodes::Window.new.order(arel_table[column_sym])).as('row_id'),
- arel_table.project("COUNT(1)").as('ct')).
+ arel_table.project(*rank_rows(arel_table, column_sym, partition_column)).
# Disallow negative values
where(arel_table[column_sym].gteq(zero_interval)))
# From the CTE, select either the middle row or the middle two rows (this is accomplished
# by 'where cte.row_id between cte.ct / 2.0 AND cte.ct / 2.0 + 1'). Find the average of the
# selected rows, and this is the median value.
- cte_table.project(average([extract_epoch(cte_table[column_sym])], "median"))
- .where(
- Arel::Nodes::Between.new(
- cte_table[:row_id],
- Arel::Nodes::And.new(
- [(cte_table[:ct] / Arel.sql('2.0')),
- (cte_table[:ct] / Arel.sql('2.0') + 1)]
+ result =
+ cte_table
+ .project(*median_projections(cte_table, column_sym, partition_column))
+ .where(
+ Arel::Nodes::Between.new(
+ cte_table[:row_id],
+ Arel::Nodes::And.new(
+ [(cte_table[:ct] / Arel.sql('2.0')),
+ (cte_table[:ct] / Arel.sql('2.0') + 1)]
+ )
)
)
- )
- .with(query_so_far, cte)
- .to_sql
+ .with(query_so_far, cte)
+
+ result.group(cte_table[partition_column]).order(cte_table[partition_column]) if partition_column
+
+ result.to_sql
end
private
+ def median_queries(arel_table, query_so_far, column_sym, partition_column = nil)
+ if Gitlab::Database.postgresql?
+ pg_median_datetime_sql(arel_table, query_so_far, column_sym, partition_column)
+ elsif Gitlab::Database.mysql?
+ raise NotSupportedError, "partition_column is not supported for MySQL" if partition_column
+
+ mysql_median_datetime_sql(arel_table, query_so_far, column_sym)
+ end
+ end
+
+ def execute_queries(arel_table, query_so_far, column_sym, partition_column = nil)
+ queries = median_queries(arel_table, query_so_far, column_sym, partition_column)
+
+ Array.wrap(queries).map { |query| ActiveRecord::Base.connection.execute(query) }
+ end
+
def average(args, as)
Arel::Nodes::NamedFunction.new("AVG", args, as)
end
+ def rank_rows(arel_table, column_sym, partition_column)
+ column_row = arel_table[column_sym].as(column_sym.to_s)
+
+ if partition_column
+ partition_row = arel_table[partition_column]
+ row_id =
+ Arel::Nodes::Over.new(
+ Arel::Nodes::NamedFunction.new('rank', []),
+ Arel::Nodes::Window.new.partition(arel_table[partition_column])
+ .order(arel_table[column_sym])
+ ).as('row_id')
+
+ count = arel_table.from(arel_table.alias)
+ .project('COUNT(*)')
+ .where(arel_table[partition_column].eq(arel_table.alias[partition_column]))
+ .as('ct')
+
+ [partition_row, column_row, row_id, count]
+ else
+ row_id =
+ Arel::Nodes::Over.new(
+ Arel::Nodes::NamedFunction.new('row_number', []),
+ Arel::Nodes::Window.new.order(arel_table[column_sym])
+ ).as('row_id')
+
+ count = arel_table.project("COUNT(1)").as('ct')
+
+ [column_row, row_id, count]
+ end
+ end
+
+ def median_projections(table, column_sym, partition_column)
+ projections = []
+ projections << table[partition_column] if partition_column
+ projections << average([extract_epoch(table[column_sym])], "median")
+ projections
+ end
+
def extract_epoch(arel_attribute)
Arel.sql(%Q{EXTRACT(EPOCH FROM "#{arel_attribute.relation.name}"."#{arel_attribute.name}")})
end
diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb
index 0f897e6316c..269016daac2 100644
--- a/lib/gitlab/diff/highlight.rb
+++ b/lib/gitlab/diff/highlight.rb
@@ -27,7 +27,17 @@ module Gitlab
rich_line = highlight_line(diff_line) || diff_line.text
if line_inline_diffs = inline_diffs[i]
- rich_line = InlineDiffMarker.new(diff_line.text, rich_line).mark(line_inline_diffs)
+ begin
+ rich_line = InlineDiffMarker.new(diff_line.text, rich_line).mark(line_inline_diffs)
+ # This should only happen when the encoding of the diff doesn't
+ # match the blob, which is a bug. But we shouldn't fail to render
+ # completely in that case, even though we want to report the error.
+ rescue RangeError => e
+ if Gitlab::Sentry.enabled?
+ Gitlab::Sentry.context
+ Raven.capture_exception(e)
+ end
+ end
end
diff_line.text = rich_line
diff --git a/lib/gitlab/encoding_helper.rb b/lib/gitlab/encoding_helper.rb
index c0edcabc6fd..6659efa0961 100644
--- a/lib/gitlab/encoding_helper.rb
+++ b/lib/gitlab/encoding_helper.rb
@@ -28,9 +28,9 @@ module Gitlab
# encode and clean the bad chars
message.replace clean(message)
- rescue ArgumentError
- return nil
- rescue
+ rescue ArgumentError => e
+ return unless e.message.include?('unknown encoding name')
+
encoding = detect ? detect[:encoding] : "unknown"
"--broken encoding: #{encoding}"
end
diff --git a/lib/gitlab/file_finder.rb b/lib/gitlab/file_finder.rb
index 10ffc345bd5..8c082c0c336 100644
--- a/lib/gitlab/file_finder.rb
+++ b/lib/gitlab/file_finder.rb
@@ -28,7 +28,7 @@ module Gitlab
def find_by_content(query)
results = repository.search_files_by_content(query, ref).first(BATCH_SIZE)
- results.map { |result| Gitlab::ProjectSearchResults.parse_search_result(result) }
+ results.map { |result| Gitlab::ProjectSearchResults.parse_search_result(result, project) }
end
def find_by_filename(query, except: [])
@@ -45,7 +45,8 @@ module Gitlab
basename: File.basename(blob.path),
ref: ref,
startline: 1,
- data: blob.data
+ data: blob.data,
+ project: project
)
end
end
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index 4828301dbb9..b2fca2c16de 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -53,11 +53,7 @@ module Gitlab
def batch(repository, blob_references, blob_size_limit: MAX_DATA_DISPLAY_SIZE)
Gitlab::GitalyClient.migrate(:list_blobs_by_sha_path) do |is_enabled|
if is_enabled
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- blob_references.map do |sha, path|
- find_by_gitaly(repository, sha, path, limit: blob_size_limit)
- end
- end
+ repository.gitaly_blob_client.get_blobs(blob_references, blob_size_limit).to_a
else
blob_references.map do |sha, path|
find_by_rugged(repository, sha, path, limit: blob_size_limit)
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 768617e2cae..594b6a9cbc5 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -250,6 +250,45 @@ module Gitlab
end
end
+ def extract_signature_lazily(repository, commit_id)
+ BatchLoader.for({ repository: repository, commit_id: commit_id }).batch do |items, loader|
+ items_by_repo = items.group_by { |i| i[:repository] }
+
+ items_by_repo.each do |repo, items|
+ commit_ids = items.map { |i| i[:commit_id] }
+
+ signatures = batch_signature_extraction(repository, commit_ids)
+
+ signatures.each do |commit_sha, signature_data|
+ loader.call({ repository: repository, commit_id: commit_sha }, signature_data)
+ end
+ end
+ end
+ end
+
+ def batch_signature_extraction(repository, commit_ids)
+ repository.gitaly_migrate(:extract_commit_signature_in_batch) do |is_enabled|
+ if is_enabled
+ gitaly_batch_signature_extraction(repository, commit_ids)
+ else
+ rugged_batch_signature_extraction(repository, commit_ids)
+ end
+ end
+ end
+
+ def gitaly_batch_signature_extraction(repository, commit_ids)
+ repository.gitaly_commit_client.get_commit_signatures(commit_ids)
+ end
+
+ def rugged_batch_signature_extraction(repository, commit_ids)
+ commit_ids.each_with_object({}) do |commit_id, signatures|
+ signature_data = rugged_extract_signature(repository, commit_id)
+ next unless signature_data
+
+ signatures[commit_id] = signature_data
+ end
+ end
+
def rugged_extract_signature(repository, commit_id)
begin
Rugged::Commit.extract_signature(repository.rugged, commit_id)
@@ -402,15 +441,6 @@ module Gitlab
end
end
- # Get a collection of Rugged::Reference objects for this commit.
- #
- # Ex.
- # commit.ref(repo)
- #
- def refs(repo)
- repo.refs_hash[id]
- end
-
# Get ref names collection
#
# Ex.
@@ -418,7 +448,7 @@ module Gitlab
#
def ref_names(repo)
refs(repo).map do |ref|
- ref.name.sub(%r{^refs/(heads|remotes|tags)/}, "")
+ ref.sub(%r{^refs/(heads|remotes|tags)/}, "")
end
end
@@ -517,7 +547,7 @@ module Gitlab
@committed_date = Time.at(commit.committer.date.seconds).utc
@committer_name = commit.committer.name.dup
@committer_email = commit.committer.email.dup
- @parent_ids = commit.parent_ids
+ @parent_ids = Array(commit.parent_ids)
end
def serialize_keys
@@ -553,6 +583,15 @@ module Gitlab
date: Google::Protobuf::Timestamp.new(seconds: author_or_committer[:time].to_i)
)
end
+
+ # Get a collection of Gitlab::Git::Ref objects for this commit.
+ #
+ # Ex.
+ # commit.ref(repo)
+ #
+ def refs(repo)
+ repo.refs_hash[id]
+ end
end
end
end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index d7510061def..d7c373ccd6f 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -467,7 +467,8 @@ module Gitlab
follow: false,
skip_merges: false,
after: nil,
- before: nil
+ before: nil,
+ all: false
}
options = default_options.merge(options)
@@ -478,8 +479,9 @@ module Gitlab
raise ArgumentError.new("invalid Repository#log limit: #{limit.inspect}")
end
+ # TODO support options[:all] in Gitaly https://gitlab.com/gitlab-org/gitaly/issues/1049
gitaly_migrate(:find_commits) do |is_enabled|
- if is_enabled
+ if is_enabled && !options[:all]
gitaly_commit_client.find_commits(options)
else
raw_log(options).map { |c| Commit.decorate(self, c) }
@@ -489,13 +491,16 @@ module Gitlab
# Used in gitaly-ruby
def raw_log(options)
- actual_ref = options[:ref] || root_ref
- begin
- sha = sha_from_ref(actual_ref)
- rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError
- # Return an empty array if the ref wasn't found
- return []
- end
+ sha =
+ unless options[:all]
+ actual_ref = options[:ref] || root_ref
+ begin
+ sha_from_ref(actual_ref)
+ rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError
+ # Return an empty array if the ref wasn't found
+ return []
+ end
+ end
log_by_shell(sha, options)
end
@@ -503,8 +508,9 @@ module Gitlab
def count_commits(options)
count_commits_options = process_count_commits_options(options)
+ # TODO add support for options[:all] in Gitaly https://gitlab.com/gitlab-org/gitaly/issues/1050
gitaly_migrate(:count_commits) do |is_enabled|
- if is_enabled
+ if is_enabled && !options[:all]
count_commits_by_gitaly(count_commits_options)
else
count_commits_by_shelling_out(count_commits_options)
@@ -631,21 +637,18 @@ module Gitlab
end
end
- # Get refs hash which key is SHA1
- # and value is a Rugged::Reference
+ # Get refs hash which key is is the commit id
+ # and value is a Gitlab::Git::Tag or Gitlab::Git::Branch
+ # Note that both inherit from Gitlab::Git::Ref
def refs_hash
- # Initialize only when first call
- if @refs_hash.nil?
- @refs_hash = Hash.new { |h, k| h[k] = [] }
-
- rugged.references.each do |r|
- # Symbolic/remote references may not have an OID; skip over them
- target_oid = r.target.try(:oid)
- if target_oid
- sha = rev_parse_target(target_oid).oid
- @refs_hash[sha] << r
- end
- end
+ return @refs_hash if @refs_hash
+
+ @refs_hash = Hash.new { |h, k| h[k] = [] }
+
+ (tags + branches).each do |ref|
+ next unless ref.target && ref.name
+
+ @refs_hash[ref.dereferenced_target.id] << ref.name
end
@refs_hash
@@ -1035,6 +1038,21 @@ module Gitlab
end
end
+ def license_short_name
+ gitaly_migrate(:license_short_name) do |is_enabled|
+ if is_enabled
+ gitaly_repository_client.license_short_name
+ else
+ begin
+ # The licensee gem creates a Rugged object from the path:
+ # https://github.com/benbalter/licensee/blob/v8.7.0/lib/licensee/projects/git_project.rb
+ Licensee.license(path).try(:key)
+ rescue Rugged::Error
+ end
+ end
+ end
+ end
+
def with_repo_branch_commit(start_repository, start_branch_name)
Gitlab::Git.check_namespace!(start_repository)
start_repository = RemoteRepository.new(start_repository) unless start_repository.is_a?(RemoteRepository)
@@ -1237,7 +1255,13 @@ module Gitlab
end
def squash_in_progress?(squash_id)
- fresh_worktree?(worktree_path(SQUASH_WORKTREE_PREFIX, squash_id))
+ gitaly_migrate(:squash_in_progress) do |is_enabled|
+ if is_enabled
+ gitaly_repository_client.squash_in_progress?(squash_id)
+ else
+ fresh_worktree?(worktree_path(SQUASH_WORKTREE_PREFIX, squash_id))
+ end
+ end
end
def push_remote_branches(remote_name, branch_names, forced: true)
@@ -1352,7 +1376,7 @@ module Gitlab
if is_enabled
gitaly_ref_client.branch_names_contains_sha(sha)
else
- refs_contains_sha(:branch, sha)
+ refs_contains_sha('refs/heads/', sha)
end
end
end
@@ -1362,7 +1386,7 @@ module Gitlab
if is_enabled
gitaly_ref_client.tag_names_contains_sha(sha)
else
- refs_contains_sha(:tag, sha)
+ refs_contains_sha('refs/tags/', sha)
end
end
end
@@ -1461,19 +1485,25 @@ module Gitlab
end
end
- def refs_contains_sha(ref_type, sha)
- args = %W(#{ref_type} --contains #{sha})
- names = run_git(args).first
+ def refs_contains_sha(refs_prefix, sha)
+ refs_prefix << "/" unless refs_prefix.ends_with?('/')
+
+ # By forcing the output to %(refname) each line wiht a ref will start with
+ # the ref prefix. All other lines can be discarded.
+ args = %W(for-each-ref --contains=#{sha} --format=%(refname) #{refs_prefix})
+ names, code = run_git(args)
- return [] unless names.respond_to?(:split)
+ return [] unless code.zero?
- names = names.split("\n").map(&:strip)
+ refs = []
+ left_slice_count = refs_prefix.length
+ names.lines.each do |line|
+ next unless line.start_with?(refs_prefix)
- names.each do |name|
- name.slice! '* '
+ refs << line.rstrip[left_slice_count..-1]
end
- names
+ refs
end
def rugged_write_config(full_path:)
@@ -1617,17 +1647,14 @@ module Gitlab
# Gitaly note: JV: Trying to get rid of the 'filter' option so we can implement this with 'git'.
def branches_filter(filter: nil, sort_by: nil)
- # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37464
- branches = Gitlab::GitalyClient.allow_n_plus_1_calls do
- rugged.branches.each(filter).map do |rugged_ref|
- begin
- target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target)
- Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit)
- rescue Rugged::ReferenceError
- # Omit invalid branch
- end
- end.compact
- end
+ branches = rugged.branches.each(filter).map do |rugged_ref|
+ begin
+ target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target)
+ Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit)
+ rescue Rugged::ReferenceError
+ # Omit invalid branch
+ end
+ end.compact
sort_branches(branches, sort_by)
end
@@ -1695,7 +1722,12 @@ module Gitlab
cmd << '--no-merges' if options[:skip_merges]
cmd << "--after=#{options[:after].iso8601}" if options[:after]
cmd << "--before=#{options[:before].iso8601}" if options[:before]
- cmd << sha
+
+ if options[:all]
+ cmd += %w[--all --reverse]
+ else
+ cmd << sha
+ end
# :path can be a string or an array of strings
if options[:path].present?
@@ -1912,7 +1944,16 @@ module Gitlab
cmd << "--before=#{options[:before].iso8601}" if options[:before]
cmd << "--max-count=#{options[:max_count]}" if options[:max_count]
cmd << "--left-right" if options[:left_right]
- cmd += %W[--count #{options[:ref]}]
+ cmd << '--count'
+
+ cmd << if options[:all]
+ '--all'
+ elsif options[:ref]
+ options[:ref]
+ else
+ raise ArgumentError, "Please specify a valid ref or set the 'all' attribute to true"
+ end
+
cmd += %W[-- #{options[:path]}] if options[:path].present?
cmd
end
@@ -2194,13 +2235,14 @@ module Gitlab
)
diff_range = "#{start_sha}...#{end_sha}"
diff_files = run_git!(
- %W(diff --name-only --diff-filter=a --binary #{diff_range})
+ %W(diff --name-only --diff-filter=ar --binary #{diff_range})
).chomp
with_worktree(squash_path, branch, sparse_checkout_files: diff_files, env: env) do
# Apply diff of the `diff_range` to the worktree
diff = run_git!(%W(diff --binary #{diff_range}))
- run_git!(%w(apply --index), chdir: squash_path, env: env) do |stdin|
+ run_git!(%w(apply --index --whitespace=nowarn), chdir: squash_path, env: env) do |stdin|
+ stdin.binmode
stdin.write(diff)
end
diff --git a/lib/gitlab/git/tree.rb b/lib/gitlab/git/tree.rb
index ba6058fd3c9..b6ceb542dd1 100644
--- a/lib/gitlab/git/tree.rb
+++ b/lib/gitlab/git/tree.rb
@@ -14,14 +14,14 @@ module Gitlab
# Uses rugged for raw objects
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/320
- def where(repository, sha, path = nil)
+ def where(repository, sha, path = nil, recursive = false)
path = nil if path == '' || path == '/'
Gitlab::GitalyClient.migrate(:tree_entries) do |is_enabled|
if is_enabled
- repository.gitaly_commit_client.tree_entries(repository, sha, path)
+ repository.gitaly_commit_client.tree_entries(repository, sha, path, recursive)
else
- tree_entries_from_rugged(repository, sha, path)
+ tree_entries_from_rugged(repository, sha, path, recursive)
end
end
end
@@ -57,7 +57,22 @@ module Gitlab
end
end
- def tree_entries_from_rugged(repository, sha, path)
+ def tree_entries_from_rugged(repository, sha, path, recursive)
+ current_path_entries = get_tree_entries_from_rugged(repository, sha, path)
+ ordered_entries = []
+
+ current_path_entries.each do |entry|
+ ordered_entries << entry
+
+ if recursive && entry.dir?
+ ordered_entries.concat(tree_entries_from_rugged(repository, sha, entry.path, true))
+ end
+ end
+
+ ordered_entries
+ end
+
+ def get_tree_entries_from_rugged(repository, sha, path)
commit = repository.lookup(sha)
root_tree = commit.tree
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
index 39040d56971..52b44b9b3c5 100644
--- a/lib/gitlab/git/wiki.rb
+++ b/lib/gitlab/git/wiki.rb
@@ -25,9 +25,8 @@ module Gitlab
@repository.exists?
end
- # Disabled because of https://gitlab.com/gitlab-org/gitaly/merge_requests/539
def write_page(name, format, content, commit_details)
- @repository.gitaly_migrate(:wiki_write_page, status: Gitlab::GitalyClient::MigrationStatus::DISABLED) do |is_enabled|
+ @repository.gitaly_migrate(:wiki_write_page) do |is_enabled|
if is_enabled
gitaly_write_page(name, format, content, commit_details)
gollum_wiki.clear_cache
@@ -48,9 +47,8 @@ module Gitlab
end
end
- # Disable because of https://gitlab.com/gitlab-org/gitlab-ce/issues/42094
def update_page(page_path, title, format, content, commit_details)
- @repository.gitaly_migrate(:wiki_update_page, status: Gitlab::GitalyClient::MigrationStatus::DISABLED) do |is_enabled|
+ @repository.gitaly_migrate(:wiki_update_page) do |is_enabled|
if is_enabled
gitaly_update_page(page_path, title, format, content, commit_details)
gollum_wiki.clear_cache
@@ -61,7 +59,7 @@ module Gitlab
end
def pages(limit: nil)
- @repository.gitaly_migrate(:wiki_get_all_pages, status: Gitlab::GitalyClient::MigrationStatus::DISABLED) do |is_enabled|
+ @repository.gitaly_migrate(:wiki_get_all_pages) do |is_enabled|
if is_enabled
gitaly_get_all_pages
else
@@ -70,9 +68,8 @@ module Gitlab
end
end
- # Disable because of https://gitlab.com/gitlab-org/gitlab-ce/issues/42039
def page(title:, version: nil, dir: nil)
- @repository.gitaly_migrate(:wiki_find_page, status: Gitlab::GitalyClient::MigrationStatus::DISABLED) do |is_enabled|
+ @repository.gitaly_migrate(:wiki_find_page) do |is_enabled|
if is_enabled
gitaly_find_page(title: title, version: version, dir: dir)
else
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 8ec3386184a..6400089a22f 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -198,8 +198,8 @@ module Gitlab
end
def check_repository_existence!
- unless project.repository.exists?
- raise UnauthorizedError, ERROR_MESSAGES[:no_repo]
+ unless repository.exists?
+ raise NotFoundError, ERROR_MESSAGES[:no_repo]
end
end
@@ -238,19 +238,22 @@ module Gitlab
changes_list = Gitlab::ChangesList.new(changes)
# Iterate over all changes to find if user allowed all of them to be applied
- changes_list.each do |change|
+ changes_list.each.with_index do |change, index|
+ first_change = index == 0
+
# If user does not have access to make at least one change, cancel all
# push by allowing the exception to bubble up
- check_single_change_access(change)
+ check_single_change_access(change, skip_lfs_integrity_check: !first_change)
end
end
- def check_single_change_access(change)
+ def check_single_change_access(change, skip_lfs_integrity_check: false)
Checks::ChangeAccess.new(
change,
user_access: user_access,
project: project,
skip_authorization: deploy_key?,
+ skip_lfs_integrity_check: skip_lfs_integrity_check,
protocol: protocol
).exec
end
@@ -324,5 +327,9 @@ module Gitlab
def push_to_read_only_message
ERROR_MESSAGES[:cannot_push_to_read_only]
end
+
+ def repository
+ project.repository
+ end
end
end
diff --git a/lib/gitlab/git_access_wiki.rb b/lib/gitlab/git_access_wiki.rb
index 1c9477e84b2..a5b3902ebf4 100644
--- a/lib/gitlab/git_access_wiki.rb
+++ b/lib/gitlab/git_access_wiki.rb
@@ -13,7 +13,7 @@ module Gitlab
authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_wiki_code)
end
- def check_single_change_access(change)
+ def check_single_change_access(change, _options = {})
unless user_access.can_do_action?(:create_wiki)
raise UnauthorizedError, ERROR_MESSAGES[:write_to_wiki]
end
@@ -28,5 +28,11 @@ module Gitlab
def push_to_read_only_message
ERROR_MESSAGES[:read_only]
end
+
+ private
+
+ def repository
+ project.wiki.repository
+ end
end
end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index c5d3e944f7d..9cd76630484 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -125,6 +125,8 @@ module Gitlab
kwargs = yield(kwargs) if block_given?
stub(service, storage).__send__(rpc, request, kwargs) # rubocop:disable GitlabSecurity/PublicSend
+ rescue GRPC::Unavailable => ex
+ handle_grpc_unavailable!(ex)
ensure
duration = Gitlab::Metrics::System.monotonic_time - start
@@ -135,6 +137,27 @@ module Gitlab
duration)
end
+ def self.handle_grpc_unavailable!(ex)
+ status = ex.to_status
+ raise ex unless status.details == 'Endpoint read failed'
+
+ # There is a bug in grpc 1.8.x that causes a client process to get stuck
+ # always raising '14:Endpoint read failed'. The only thing that we can
+ # do to recover is to restart the process.
+ #
+ # See https://gitlab.com/gitlab-org/gitaly/issues/1029
+
+ if Sidekiq.server?
+ raise Gitlab::SidekiqMiddleware::Shutdown::WantShutdown.new(ex.to_s)
+ else
+ # SIGQUIT requests a Unicorn worker to shut down gracefully after the current request.
+ Process.kill('QUIT', Process.pid)
+ end
+
+ raise ex
+ end
+ private_class_method :handle_grpc_unavailable!
+
def self.current_transaction_labels
Gitlab::Metrics::Transaction.current&.labels || {}
end
diff --git a/lib/gitlab/gitaly_client/blob_service.rb b/lib/gitlab/gitaly_client/blob_service.rb
index d70a1a7665e..dfa0fa43b0f 100644
--- a/lib/gitlab/gitaly_client/blob_service.rb
+++ b/lib/gitlab/gitaly_client/blob_service.rb
@@ -1,6 +1,8 @@
module Gitlab
module GitalyClient
class BlobService
+ include Gitlab::EncodingHelper
+
def initialize(repository)
@gitaly_repo = repository.gitaly_repository
end
@@ -54,6 +56,30 @@ module Gitlab
end
end
end
+
+ def get_blobs(revision_paths, limit = -1)
+ return [] if revision_paths.empty?
+
+ revision_paths.map! do |rev, path|
+ Gitaly::GetBlobsRequest::RevisionPath.new(revision: rev, path: encode_binary(path))
+ end
+
+ request = Gitaly::GetBlobsRequest.new(
+ repository: @gitaly_repo,
+ revision_paths: revision_paths,
+ limit: limit
+ )
+
+ response = GitalyClient.call(
+ @gitaly_repo.storage_name,
+ :blob_service,
+ :get_blobs,
+ request,
+ timeout: GitalyClient.default_timeout
+ )
+
+ GitalyClient::BlobsStitcher.new(response)
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/blobs_stitcher.rb b/lib/gitlab/gitaly_client/blobs_stitcher.rb
new file mode 100644
index 00000000000..5ca592ff812
--- /dev/null
+++ b/lib/gitlab/gitaly_client/blobs_stitcher.rb
@@ -0,0 +1,47 @@
+module Gitlab
+ module GitalyClient
+ class BlobsStitcher
+ include Enumerable
+
+ def initialize(rpc_response)
+ @rpc_response = rpc_response
+ end
+
+ def each
+ current_blob_data = nil
+
+ @rpc_response.each do |msg|
+ begin
+ if msg.oid.blank? && msg.data.blank?
+ next
+ elsif msg.oid.present?
+ yield new_blob(current_blob_data) if current_blob_data
+
+ current_blob_data = msg.to_h.slice(:oid, :path, :size, :revision, :mode)
+ current_blob_data[:data] = msg.data.dup
+ else
+ current_blob_data[:data] << msg.data
+ end
+ end
+ end
+
+ yield new_blob(current_blob_data) if current_blob_data
+ end
+
+ private
+
+ def new_blob(blob_data)
+ Gitlab::Git::Blob.new(
+ id: blob_data[:oid],
+ mode: blob_data[:mode].to_s(8),
+ name: File.basename(blob_data[:path]),
+ path: blob_data[:path],
+ size: blob_data[:size],
+ commit_id: blob_data[:revision],
+ data: blob_data[:data],
+ binary: Gitlab::Git::Blob.binary?(blob_data[:data])
+ )
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 5767f06b0ce..1ad0bf1d060 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -105,11 +105,12 @@ module Gitlab
entry unless entry.oid.blank?
end
- def tree_entries(repository, revision, path)
+ def tree_entries(repository, revision, path, recursive)
request = Gitaly::GetTreeEntriesRequest.new(
repository: @gitaly_repo,
revision: encode_binary(revision),
- path: path.present? ? encode_binary(path) : '.'
+ path: path.present? ? encode_binary(path) : '.',
+ recursive: recursive
)
response = GitalyClient.call(@repository.storage, :commit_service, :get_tree_entries, request, timeout: GitalyClient.medium_timeout)
@@ -222,14 +223,25 @@ module Gitlab
end
def find_commit(revision)
- request = Gitaly::FindCommitRequest.new(
- repository: @gitaly_repo,
- revision: encode_binary(revision)
- )
-
- response = GitalyClient.call(@repository.storage, :commit_service, :find_commit, request, timeout: GitalyClient.medium_timeout)
-
- response.commit
+ if RequestStore.active?
+ # We don't use RequeStstore.fetch(key) { ... } directly because `revision`
+ # can be a branch name, so we can't use it as a key as it could point
+ # to another commit later on (happens a lot in tests).
+ key = {
+ storage: @gitaly_repo.storage_name,
+ relative_path: @gitaly_repo.relative_path,
+ commit_id: revision
+ }
+ return RequestStore[key] if RequestStore.exist?(key)
+
+ commit = call_find_commit(revision)
+ return unless commit
+
+ key[:commit_id] = commit.id
+ RequestStore[key] = commit
+ else
+ call_find_commit(revision)
+ end
end
def patch(revision)
@@ -307,6 +319,23 @@ module Gitlab
[signature, signed_text]
end
+ def get_commit_signatures(commit_ids)
+ request = Gitaly::GetCommitSignaturesRequest.new(repository: @gitaly_repo, commit_ids: commit_ids)
+ response = GitalyClient.call(@repository.storage, :commit_service, :get_commit_signatures, request)
+
+ signatures = Hash.new { |h, k| h[k] = [''.b, ''.b] }
+ current_commit_id = nil
+
+ response.each do |message|
+ current_commit_id = message.commit_id if message.commit_id.present?
+
+ signatures[current_commit_id].first << message.signature
+ signatures[current_commit_id].last << message.signed_text
+ end
+
+ signatures
+ end
+
private
def call_commit_diff(request_params, options = {})
@@ -346,6 +375,17 @@ module Gitlab
def encode_repeated(a)
Google::Protobuf::RepeatedField.new(:bytes, a.map { |s| encode_binary(s) } )
end
+
+ def call_find_commit(revision)
+ request = Gitaly::FindCommitRequest.new(
+ repository: @gitaly_repo,
+ revision: encode_binary(revision)
+ )
+
+ response = GitalyClient.call(@repository.storage, :commit_service, :find_commit, request, timeout: GitalyClient.medium_timeout)
+
+ response.commit
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index 60706b4f0d8..fdb3247cf4d 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -41,7 +41,7 @@ module Gitlab
end
def apply_gitattributes(revision)
- request = Gitaly::ApplyGitattributesRequest.new(repository: @gitaly_repo, revision: revision)
+ request = Gitaly::ApplyGitattributesRequest.new(repository: @gitaly_repo, revision: encode_binary(revision))
GitalyClient.call(@storage, :repository_service, :apply_gitattributes, request)
end
@@ -134,6 +134,23 @@ module Gitlab
response.in_progress
end
+ def squash_in_progress?(squash_id)
+ request = Gitaly::IsSquashInProgressRequest.new(
+ repository: @gitaly_repo,
+ squash_id: squash_id.to_s
+ )
+
+ response = GitalyClient.call(
+ @storage,
+ :repository_service,
+ :is_squash_in_progress,
+ request,
+ timeout: GitalyClient.default_timeout
+ )
+
+ response.in_progress
+ end
+
def fetch_source_branch(source_repository, source_branch, local_ref)
request = Gitaly::FetchSourceBranchRequest.new(
repository: @gitaly_repo,
@@ -232,6 +249,14 @@ module Gitlab
raise Gitlab::Git::OSError.new(response.error) unless response.error.empty?
end
+
+ def license_short_name
+ request = Gitaly::FindLicenseRequest.new(repository: @gitaly_repo)
+
+ response = GitalyClient.call(@storage, :repository_service, :find_license, request, timeout: GitalyClient.fast_timeout)
+
+ response.license_short_name.presence
+ end
end
end
end
diff --git a/lib/gitlab/github_import/importer/repository_importer.rb b/lib/gitlab/github_import/importer/repository_importer.rb
index 7dd68a0d1cd..ab0b751fe24 100644
--- a/lib/gitlab/github_import/importer/repository_importer.rb
+++ b/lib/gitlab/github_import/importer/repository_importer.rb
@@ -63,6 +63,7 @@ module Gitlab
true
rescue Gitlab::Shell::Error => e
if e.message !~ /repository not exported/
+ project.create_wiki
fail_import("Failed to import the wiki: #{e.message}")
else
true
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 86a90d57d9c..92f0e0402a8 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -13,14 +13,13 @@ module Gitlab
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
gon.shortcuts_path = help_page_path('shortcuts')
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
- gon.katex_css_url = ActionController::Base.helpers.asset_path('katex.css')
- gon.katex_js_url = ActionController::Base.helpers.asset_path('katex.js')
gon.sentry_dsn = Gitlab::CurrentSettings.clientside_sentry_dsn if Gitlab::CurrentSettings.clientside_sentry_enabled
gon.gitlab_url = Gitlab.config.gitlab.url
gon.revision = Gitlab::REVISION
gon.gitlab_logo = ActionController::Base.helpers.asset_path('gitlab_logo.png')
gon.sprite_icons = IconsHelper.sprite_icon_path
gon.sprite_file_icons = IconsHelper.sprite_file_icons_path
+ gon.test_env = Rails.env.test?
if current_user
gon.current_user_id = current_user.id
diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb
index 672b5579dfd..6d2278d0876 100644
--- a/lib/gitlab/gpg/commit.rb
+++ b/lib/gitlab/gpg/commit.rb
@@ -1,15 +1,29 @@
module Gitlab
module Gpg
class Commit
+ include Gitlab::Utils::StrongMemoize
+
def initialize(commit)
@commit = commit
repo = commit.project.repository.raw_repository
- @signature_text, @signed_text = Gitlab::Git::Commit.extract_signature(repo, commit.sha)
+ @signature_data = Gitlab::Git::Commit.extract_signature_lazily(repo, commit.sha || commit.id)
+ end
+
+ def signature_text
+ strong_memoize(:signature_text) do
+ @signature_data&.itself && @signature_data[0]
+ end
+ end
+
+ def signed_text
+ strong_memoize(:signed_text) do
+ @signature_data&.itself && @signature_data[1]
+ end
end
def has_signature?
- !!(@signature_text && @signed_text)
+ !!(signature_text && signed_text)
end
def signature
@@ -53,14 +67,16 @@ module Gitlab
end
def verified_signature
- @verified_signature ||= GPGME::Crypto.new.verify(@signature_text, signed_text: @signed_text) do |verified_signature|
+ @verified_signature ||= GPGME::Crypto.new.verify(signature_text, signed_text: signed_text) do |verified_signature|
break verified_signature
end
end
def create_cached_signature!
using_keychain do |gpg_key|
- GpgSignature.create!(attributes(gpg_key))
+ signature = GpgSignature.new(attributes(gpg_key))
+ signature.save! unless Gitlab::Database.read_only?
+ signature
end
end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 2daed10f678..9f404003125 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -27,6 +27,8 @@ project_tree:
- :releases
- project_members:
- :user
+ - lfs_file_locks:
+ - :user
- merge_requests:
- notes:
- :author
diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb
index c14646b0611..a00795f553e 100644
--- a/lib/gitlab/import_export/importer.rb
+++ b/lib/gitlab/import_export/importer.rb
@@ -50,9 +50,10 @@ module Gitlab
end
def wiki_restorer
- Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: wiki_repo_path,
+ Gitlab::ImportExport::WikiRestorer.new(path_to_bundle: wiki_repo_path,
shared: @shared,
- project: ProjectWiki.new(project_tree.restored_project))
+ project: ProjectWiki.new(project_tree.restored_project),
+ wiki_enabled: @project.wiki_enabled?)
end
def uploads_restorer
diff --git a/lib/gitlab/import_export/project_creator.rb b/lib/gitlab/import_export/project_creator.rb
deleted file mode 100644
index 77bb3ca6581..00000000000
--- a/lib/gitlab/import_export/project_creator.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-module Gitlab
- module ImportExport
- class ProjectCreator
- def initialize(namespace_id, current_user, file, project_path)
- @namespace_id = namespace_id
- @current_user = current_user
- @file = file
- @project_path = project_path
- end
-
- def execute
- ::Projects::CreateService.new(
- @current_user,
- name: @project_path,
- path: @project_path,
- namespace_id: @namespace_id,
- import_type: "gitlab_project",
- import_source: @file
- ).execute
- end
- end
- end
-end
diff --git a/lib/gitlab/import_export/wiki_restorer.rb b/lib/gitlab/import_export/wiki_restorer.rb
new file mode 100644
index 00000000000..f33bfb332ab
--- /dev/null
+++ b/lib/gitlab/import_export/wiki_restorer.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module ImportExport
+ class WikiRestorer < RepoRestorer
+ def initialize(project:, shared:, path_to_bundle:, wiki_enabled:)
+ super(project: project, shared: shared, path_to_bundle: path_to_bundle)
+
+ @wiki_enabled = wiki_enabled
+ end
+
+ def restore
+ @project.wiki if create_empty_wiki?
+
+ super
+ end
+
+ private
+
+ def create_empty_wiki?
+ !File.exist?(@path_to_bundle) && @wiki_enabled
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/job_waiter.rb b/lib/gitlab/job_waiter.rb
index f654508c391..f7a8eae0be4 100644
--- a/lib/gitlab/job_waiter.rb
+++ b/lib/gitlab/job_waiter.rb
@@ -15,16 +15,22 @@ module Gitlab
# push to that array when done. Once the waiter has popped `count` items, it
# knows all the jobs are done.
class JobWaiter
+ KEY_PREFIX = "gitlab:job_waiter".freeze
+
def self.notify(key, jid)
Gitlab::Redis::SharedState.with { |redis| redis.lpush(key, jid) }
end
+ def self.key?(key)
+ key.is_a?(String) && key =~ /\A#{KEY_PREFIX}:\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/
+ end
+
attr_reader :key, :finished
attr_accessor :jobs_remaining
# jobs_remaining - the number of jobs left to wait for
# key - The key of this waiter.
- def initialize(jobs_remaining = 0, key = "gitlab:job_waiter:#{SecureRandom.uuid}")
+ def initialize(jobs_remaining = 0, key = "#{KEY_PREFIX}:#{SecureRandom.uuid}")
@key = key
@jobs_remaining = jobs_remaining
@finished = []
diff --git a/lib/gitlab/kubernetes/config_map.rb b/lib/gitlab/kubernetes/config_map.rb
new file mode 100644
index 00000000000..95e1054919d
--- /dev/null
+++ b/lib/gitlab/kubernetes/config_map.rb
@@ -0,0 +1,37 @@
+module Gitlab
+ module Kubernetes
+ class ConfigMap
+ def initialize(name, values)
+ @name = name
+ @values = values
+ end
+
+ def generate
+ resource = ::Kubeclient::Resource.new
+ resource.metadata = metadata
+ resource.data = { values: values }
+ resource
+ end
+
+ private
+
+ attr_reader :name, :values
+
+ def metadata
+ {
+ name: config_map_name,
+ namespace: namespace,
+ labels: { name: config_map_name }
+ }
+ end
+
+ def config_map_name
+ "values-content-configuration-#{name}"
+ end
+
+ def namespace
+ Gitlab::Kubernetes::Helm::NAMESPACE
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/kubernetes/helm/api.rb b/lib/gitlab/kubernetes/helm/api.rb
index 737081ddc5b..2edd34109ba 100644
--- a/lib/gitlab/kubernetes/helm/api.rb
+++ b/lib/gitlab/kubernetes/helm/api.rb
@@ -9,7 +9,8 @@ module Gitlab
def install(command)
@namespace.ensure_exists!
- @kubeclient.create_pod(pod_resource(command))
+ create_config_map(command) if command.config_map?
+ @kubeclient.create_pod(command.pod_resource)
end
##
@@ -33,8 +34,10 @@ module Gitlab
private
- def pod_resource(command)
- Gitlab::Kubernetes::Helm::Pod.new(command, @namespace.name, @kubeclient).generate
+ def create_config_map(command)
+ command.config_map_resource.tap do |config_map_resource|
+ @kubeclient.create_config_map(config_map_resource)
+ end
end
end
end
diff --git a/lib/gitlab/kubernetes/helm/base_command.rb b/lib/gitlab/kubernetes/helm/base_command.rb
new file mode 100644
index 00000000000..6e4df05aa7e
--- /dev/null
+++ b/lib/gitlab/kubernetes/helm/base_command.rb
@@ -0,0 +1,40 @@
+module Gitlab
+ module Kubernetes
+ module Helm
+ class BaseCommand
+ attr_reader :name
+
+ def initialize(name)
+ @name = name
+ end
+
+ def pod_resource
+ Gitlab::Kubernetes::Helm::Pod.new(self, namespace).generate
+ end
+
+ def generate_script
+ <<~HEREDOC
+ set -eo pipefail
+ apk add -U ca-certificates openssl >/dev/null
+ wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v#{Gitlab::Kubernetes::Helm::HELM_VERSION}-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
+ mv /tmp/linux-amd64/helm /usr/bin/
+ HEREDOC
+ end
+
+ def config_map?
+ false
+ end
+
+ def pod_name
+ "install-#{name}"
+ end
+
+ private
+
+ def namespace
+ Gitlab::Kubernetes::Helm::NAMESPACE
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/kubernetes/helm/init_command.rb b/lib/gitlab/kubernetes/helm/init_command.rb
new file mode 100644
index 00000000000..a02e64561f6
--- /dev/null
+++ b/lib/gitlab/kubernetes/helm/init_command.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Kubernetes
+ module Helm
+ class InitCommand < BaseCommand
+ def generate_script
+ super + [
+ init_helm_command
+ ].join("\n")
+ end
+
+ private
+
+ def init_helm_command
+ "helm init >/dev/null"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/kubernetes/helm/install_command.rb b/lib/gitlab/kubernetes/helm/install_command.rb
index bf6981035f4..30af3e97b4a 100644
--- a/lib/gitlab/kubernetes/helm/install_command.rb
+++ b/lib/gitlab/kubernetes/helm/install_command.rb
@@ -1,54 +1,45 @@
module Gitlab
module Kubernetes
module Helm
- class InstallCommand
- attr_reader :name, :install_helm, :chart, :chart_values_file
+ class InstallCommand < BaseCommand
+ attr_reader :name, :chart, :repository, :values
- def initialize(name, install_helm: false, chart: false, chart_values_file: false)
+ def initialize(name, chart:, values:, repository: nil)
@name = name
- @install_helm = install_helm
@chart = chart
- @chart_values_file = chart_values_file
+ @values = values
+ @repository = repository
end
- def pod_name
- "install-#{name}"
+ def generate_script
+ super + [
+ init_command,
+ repository_command,
+ script_command
+ ].compact.join("\n")
end
- def generate_script(namespace_name)
- [
- install_dps_command,
- init_command,
- complete_command(namespace_name)
- ].join("\n")
+ def config_map?
+ true
+ end
+
+ def config_map_resource
+ Gitlab::Kubernetes::ConfigMap.new(name, values).generate
end
private
def init_command
- if install_helm
- 'helm init >/dev/null'
- else
- 'helm init --client-only >/dev/null'
- end
+ 'helm init --client-only >/dev/null'
end
- def complete_command(namespace_name)
- return unless chart
-
- if chart_values_file
- "helm install #{chart} --name #{name} --namespace #{namespace_name} -f /data/helm/#{name}/config/values.yaml >/dev/null"
- else
- "helm install #{chart} --name #{name} --namespace #{namespace_name} >/dev/null"
- end
+ def repository_command
+ "helm repo add #{name} #{repository}" if repository
end
- def install_dps_command
+ def script_command
<<~HEREDOC
- set -eo pipefail
- apk add -U ca-certificates openssl >/dev/null
- wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v#{Gitlab::Kubernetes::Helm::HELM_VERSION}-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
- mv /tmp/linux-amd64/helm /usr/bin/
+ helm install #{chart} --name #{name} --namespace #{Gitlab::Kubernetes::Helm::NAMESPACE} -f /data/helm/#{name}/config/values.yaml >/dev/null
HEREDOC
end
end
diff --git a/lib/gitlab/kubernetes/helm/pod.rb b/lib/gitlab/kubernetes/helm/pod.rb
index ca5e06009fa..1e12299eefd 100644
--- a/lib/gitlab/kubernetes/helm/pod.rb
+++ b/lib/gitlab/kubernetes/helm/pod.rb
@@ -2,18 +2,17 @@ module Gitlab
module Kubernetes
module Helm
class Pod
- def initialize(command, namespace_name, kubeclient)
+ def initialize(command, namespace_name)
@command = command
@namespace_name = namespace_name
- @kubeclient = kubeclient
end
def generate
spec = { containers: [container_specification], restartPolicy: 'Never' }
- if command.chart_values_file
- create_config_map
+ if command.config_map?
spec[:volumes] = volumes_specification
+ spec[:containers][0][:volumeMounts] = volume_mounts_specification
end
::Kubeclient::Resource.new(metadata: metadata, spec: spec)
@@ -21,18 +20,16 @@ module Gitlab
private
- attr_reader :command, :namespace_name, :kubeclient
+ attr_reader :command, :namespace_name, :kubeclient, :config_map
def container_specification
- container = {
+ {
name: 'helm',
image: 'alpine:3.6',
env: generate_pod_env(command),
command: %w(/bin/sh),
args: %w(-c $(COMMAND_SCRIPT))
}
- container[:volumeMounts] = volume_mounts_specification if command.chart_values_file
- container
end
def labels
@@ -50,13 +47,12 @@ module Gitlab
}
end
- def volume_mounts_specification
- [
- {
- name: 'configuration-volume',
- mountPath: "/data/helm/#{command.name}/config"
- }
- ]
+ def generate_pod_env(command)
+ {
+ HELM_VERSION: Gitlab::Kubernetes::Helm::HELM_VERSION,
+ TILLER_NAMESPACE: namespace_name,
+ COMMAND_SCRIPT: command.generate_script
+ }.map { |key, value| { name: key, value: value } }
end
def volumes_specification
@@ -71,23 +67,13 @@ module Gitlab
]
end
- def generate_pod_env(command)
- {
- HELM_VERSION: Gitlab::Kubernetes::Helm::HELM_VERSION,
- TILLER_NAMESPACE: namespace_name,
- COMMAND_SCRIPT: command.generate_script(namespace_name)
- }.map { |key, value| { name: key, value: value } }
- end
-
- def create_config_map
- resource = ::Kubeclient::Resource.new
- resource.metadata = {
- name: "values-content-configuration-#{command.name}",
- namespace: namespace_name,
- labels: { name: "values-content-configuration-#{command.name}" }
- }
- resource.data = { values: File.read(command.chart_values_file) }
- kubeclient.create_config_map(resource)
+ def volume_mounts_specification
+ [
+ {
+ name: 'configuration-volume',
+ mountPath: "/data/helm/#{command.name}/config"
+ }
+ ]
end
end
end
diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb
deleted file mode 100644
index e60ceba27c8..00000000000
--- a/lib/gitlab/ldap/access.rb
+++ /dev/null
@@ -1,87 +0,0 @@
-# LDAP authorization model
-#
-# * Check if we are allowed access (not blocked)
-#
-module Gitlab
- module LDAP
- class Access
- attr_reader :provider, :user
-
- def self.open(user, &block)
- Gitlab::LDAP::Adapter.open(user.ldap_identity.provider) do |adapter|
- block.call(self.new(user, adapter))
- end
- end
-
- def self.allowed?(user)
- self.open(user) do |access|
- if access.allowed?
- Users::UpdateService.new(user, user: user, last_credential_check_at: Time.now).execute
-
- true
- else
- false
- end
- end
- end
-
- def initialize(user, adapter = nil)
- @adapter = adapter
- @user = user
- @provider = user.ldap_identity.provider
- end
-
- def allowed?
- if ldap_user
- unless ldap_config.active_directory
- unblock_user(user, 'is available again') if user.ldap_blocked?
- return true
- end
-
- # Block user in GitLab if he/she was blocked in AD
- if Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter)
- block_user(user, 'is disabled in Active Directory')
- false
- else
- unblock_user(user, 'is not disabled anymore') if user.ldap_blocked?
- true
- end
- else
- # Block the user if they no longer exist in LDAP/AD
- block_user(user, 'does not exist anymore')
- false
- end
- end
-
- def adapter
- @adapter ||= Gitlab::LDAP::Adapter.new(provider)
- end
-
- def ldap_config
- Gitlab::LDAP::Config.new(provider)
- end
-
- def ldap_user
- @ldap_user ||= Gitlab::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter)
- end
-
- def block_user(user, reason)
- user.ldap_block
-
- Gitlab::AppLogger.info(
- "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \
- "blocking Gitlab user \"#{user.name}\" (#{user.email})"
- )
- end
-
- def unblock_user(user, reason)
- user.activate
-
- Gitlab::AppLogger.info(
- "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \
- "unblocking Gitlab user \"#{user.name}\" (#{user.email})"
- )
- end
- end
- end
-end
diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb
deleted file mode 100644
index 76863e77dc3..00000000000
--- a/lib/gitlab/ldap/adapter.rb
+++ /dev/null
@@ -1,108 +0,0 @@
-module Gitlab
- module LDAP
- class Adapter
- attr_reader :provider, :ldap
-
- def self.open(provider, &block)
- Net::LDAP.open(config(provider).adapter_options) do |ldap|
- block.call(self.new(provider, ldap))
- end
- end
-
- def self.config(provider)
- Gitlab::LDAP::Config.new(provider)
- end
-
- def initialize(provider, ldap = nil)
- @provider = provider
- @ldap = ldap || Net::LDAP.new(config.adapter_options)
- end
-
- def config
- Gitlab::LDAP::Config.new(provider)
- end
-
- def users(fields, value, limit = nil)
- options = user_options(Array(fields), value, limit)
-
- entries = ldap_search(options).select do |entry|
- entry.respond_to? config.uid
- end
-
- entries.map do |entry|
- Gitlab::LDAP::Person.new(entry, provider)
- end
- end
-
- def user(*args)
- users(*args).first
- end
-
- def dn_matches_filter?(dn, filter)
- ldap_search(base: dn,
- filter: filter,
- scope: Net::LDAP::SearchScope_BaseObject,
- attributes: %w{dn}).any?
- end
-
- def ldap_search(*args)
- # Net::LDAP's `time` argument doesn't work. Use Ruby `Timeout` instead.
- Timeout.timeout(config.timeout) do
- results = ldap.search(*args)
-
- if results.nil?
- response = ldap.get_operation_result
-
- unless response.code.zero?
- Rails.logger.warn("LDAP search error: #{response.message}")
- end
-
- []
- else
- results
- end
- end
- rescue Net::LDAP::Error => error
- Rails.logger.warn("LDAP search raised exception #{error.class}: #{error.message}")
- []
- rescue Timeout::Error
- Rails.logger.warn("LDAP search timed out after #{config.timeout} seconds")
- []
- end
-
- private
-
- def user_options(fields, value, limit)
- options = {
- attributes: Gitlab::LDAP::Person.ldap_attributes(config),
- base: config.base
- }
-
- options[:size] = limit if limit
-
- if fields.include?('dn')
- raise ArgumentError, 'It is not currently possible to search the DN and other fields at the same time.' if fields.size > 1
-
- options[:base] = value
- options[:scope] = Net::LDAP::SearchScope_BaseObject
- else
- filter = fields.map { |field| Net::LDAP::Filter.eq(field, value) }.inject(:|)
- end
-
- options.merge(filter: user_filter(filter))
- end
-
- def user_filter(filter = nil)
- user_filter = config.constructed_user_filter if config.user_filter.present?
-
- if user_filter && filter
- Net::LDAP::Filter.join(filter, user_filter)
- elsif user_filter
- user_filter
- else
- filter
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ldap/auth_hash.rb b/lib/gitlab/ldap/auth_hash.rb
deleted file mode 100644
index 96171dc26c4..00000000000
--- a/lib/gitlab/ldap/auth_hash.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# Class to parse and transform the info provided by omniauth
-#
-module Gitlab
- module LDAP
- class AuthHash < Gitlab::OAuth::AuthHash
- def uid
- @uid ||= Gitlab::LDAP::Person.normalize_dn(super)
- end
-
- def username
- super.tap do |username|
- username.downcase! if ldap_config.lowercase_usernames
- end
- end
-
- private
-
- def get_info(key)
- attributes = ldap_config.attributes[key.to_s]
- return super unless attributes
-
- attributes = Array(attributes)
-
- value = nil
- attributes.each do |attribute|
- value = get_raw(attribute)
- value = value.first if value
- break if value.present?
- end
-
- return super unless value
-
- Gitlab::Utils.force_utf8(value)
- value
- end
-
- def get_raw(key)
- auth_hash.extra[:raw_info][key] if auth_hash.extra
- end
-
- def ldap_config
- @ldap_config ||= Gitlab::LDAP::Config.new(self.provider)
- end
- end
- end
-end
diff --git a/lib/gitlab/ldap/authentication.rb b/lib/gitlab/ldap/authentication.rb
deleted file mode 100644
index 7274d1c3b43..00000000000
--- a/lib/gitlab/ldap/authentication.rb
+++ /dev/null
@@ -1,70 +0,0 @@
-# These calls help to authenticate to LDAP by providing username and password
-#
-# Since multiple LDAP servers are supported, it will loop through all of them
-# until a valid bind is found
-#
-
-module Gitlab
- module LDAP
- class Authentication
- def self.login(login, password)
- return unless Gitlab::LDAP::Config.enabled?
- return unless login.present? && password.present?
-
- auth = nil
- # loop through providers until valid bind
- providers.find do |provider|
- auth = new(provider)
- 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::LDAP::Authentication instance we tried.
- auth.user
- end
-
- def self.providers
- Gitlab::LDAP::Config.providers
- end
-
- attr_accessor :provider, :ldap_user
-
- def initialize(provider)
- @provider = provider
- end
-
- def login(login, password)
- @ldap_user = adapter.bind_as(
- filter: user_filter(login),
- size: 1,
- password: password
- )
- end
-
- def adapter
- OmniAuth::LDAP::Adaptor.new(config.omniauth_options)
- end
-
- def config
- Gitlab::LDAP::Config.new(provider)
- end
-
- def user_filter(login)
- filter = Net::LDAP::Filter.equals(config.uid, login)
-
- # Apply LDAP user filter if present
- if config.user_filter.present?
- filter = Net::LDAP::Filter.join(filter, config.constructed_user_filter)
- end
-
- filter
- end
-
- def user
- return nil unless ldap_user
-
- Gitlab::LDAP::User.find_by_uid_and_provider(ldap_user.dn, provider)
- end
- end
- end
-end
diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb
deleted file mode 100644
index 47b3fce3e7a..00000000000
--- a/lib/gitlab/ldap/config.rb
+++ /dev/null
@@ -1,235 +0,0 @@
-# Load a specific server configuration
-module Gitlab
- module LDAP
- class Config
- NET_LDAP_ENCRYPTION_METHOD = {
- simple_tls: :simple_tls,
- start_tls: :start_tls,
- plain: nil
- }.freeze
-
- attr_accessor :provider, :options
-
- def self.enabled?
- Gitlab.config.ldap.enabled
- end
-
- def self.servers
- Gitlab.config.ldap.servers.values
- end
-
- def self.available_servers
- return [] unless enabled?
-
- Array.wrap(servers.first)
- end
-
- def self.providers
- servers.map { |server| server['provider_name'] }
- end
-
- def self.valid_provider?(provider)
- providers.include?(provider)
- end
-
- def self.invalid_provider(provider)
- raise "Unknown provider (#{provider}). Available providers: #{providers}"
- end
-
- def initialize(provider)
- if self.class.valid_provider?(provider)
- @provider = provider
- else
- self.class.invalid_provider(provider)
- end
-
- @options = config_for(@provider) # Use @provider, not provider
- end
-
- def enabled?
- base_config.enabled
- end
-
- def adapter_options
- opts = base_options.merge(
- encryption: encryption_options
- )
-
- opts.merge!(auth_options) if has_auth?
-
- opts
- end
-
- def omniauth_options
- opts = base_options.merge(
- base: base,
- encryption: options['encryption'],
- filter: omniauth_user_filter,
- name_proc: name_proc,
- disable_verify_certificates: !options['verify_certificates']
- )
-
- if has_auth?
- opts.merge!(
- bind_dn: options['bind_dn'],
- password: options['password']
- )
- end
-
- opts[:ca_file] = options['ca_file'] if options['ca_file'].present?
- opts[:ssl_version] = options['ssl_version'] if options['ssl_version'].present?
-
- opts
- end
-
- def base
- options['base']
- end
-
- def uid
- options['uid']
- end
-
- def sync_ssh_keys?
- sync_ssh_keys.present?
- end
-
- # The LDAP attribute in which the ssh keys are stored
- def sync_ssh_keys
- options['sync_ssh_keys']
- end
-
- def user_filter
- options['user_filter']
- end
-
- def constructed_user_filter
- @constructed_user_filter ||= Net::LDAP::Filter.construct(user_filter)
- end
-
- def group_base
- options['group_base']
- end
-
- def admin_group
- options['admin_group']
- end
-
- def active_directory
- options['active_directory']
- end
-
- def block_auto_created_users
- options['block_auto_created_users']
- end
-
- def attributes
- default_attributes.merge(options['attributes'])
- end
-
- def timeout
- options['timeout'].to_i
- end
-
- def has_auth?
- options['password'] || options['bind_dn']
- end
-
- def allow_username_or_email_login
- options['allow_username_or_email_login']
- end
-
- def lowercase_usernames
- options['lowercase_usernames']
- end
-
- def name_proc
- if allow_username_or_email_login
- proc { |name| name.gsub(/@.*\z/, '') }
- else
- proc { |name| name }
- end
- end
-
- def default_attributes
- {
- 'username' => %w(uid sAMAccountName userid),
- 'email' => %w(mail email userPrincipalName),
- 'name' => 'cn',
- 'first_name' => 'givenName',
- 'last_name' => 'sn'
- }
- end
-
- protected
-
- def base_options
- {
- host: options['host'],
- port: options['port']
- }
- end
-
- def base_config
- Gitlab.config.ldap
- end
-
- def config_for(provider)
- base_config.servers.values.find { |server| server['provider_name'] == provider }
- end
-
- def encryption_options
- method = translate_method(options['encryption'])
- return nil unless method
-
- {
- method: method,
- tls_options: tls_options(method)
- }
- end
-
- def translate_method(method_from_config)
- NET_LDAP_ENCRYPTION_METHOD[method_from_config.to_sym]
- end
-
- def tls_options(method)
- return { verify_mode: OpenSSL::SSL::VERIFY_NONE } unless method
-
- opts = if options['verify_certificates']
- OpenSSL::SSL::SSLContext::DEFAULT_PARAMS
- else
- # It is important to explicitly set verify_mode for two reasons:
- # 1. The behavior of OpenSSL is undefined when verify_mode is not set.
- # 2. The net-ldap gem implementation verifies the certificate hostname
- # unless verify_mode is set to VERIFY_NONE.
- { verify_mode: OpenSSL::SSL::VERIFY_NONE }
- end
-
- opts[:ca_file] = options['ca_file'] if options['ca_file'].present?
- opts[:ssl_version] = options['ssl_version'] if options['ssl_version'].present?
-
- opts
- end
-
- def auth_options
- {
- auth: {
- method: :simple,
- username: options['bind_dn'],
- password: options['password']
- }
- }
- end
-
- def omniauth_user_filter
- uid_filter = Net::LDAP::Filter.eq(uid, '%{username}')
-
- if user_filter.present?
- Net::LDAP::Filter.join(uid_filter, constructed_user_filter).to_s
- else
- uid_filter.to_s
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ldap/dn.rb b/lib/gitlab/ldap/dn.rb
deleted file mode 100644
index d6142dc6549..00000000000
--- a/lib/gitlab/ldap/dn.rb
+++ /dev/null
@@ -1,301 +0,0 @@
-# -*- ruby encoding: utf-8 -*-
-
-# Based on the `ruby-net-ldap` gem's `Net::LDAP::DN`
-#
-# For our purposes, this class is used to normalize DNs in order to allow proper
-# comparison.
-#
-# E.g. DNs should be compared case-insensitively (in basically all LDAP
-# implementations or setups), therefore we downcase every DN.
-
-##
-# Objects of this class represent an LDAP DN ("Distinguished Name"). A DN
-# ("Distinguished Name") is a unique identifier for an entry within an LDAP
-# directory. It is made up of a number of other attributes strung together,
-# to identify the entry in the tree.
-#
-# Each attribute that makes up a DN needs to have its value escaped so that
-# the DN is valid. This class helps take care of that.
-#
-# A fully escaped DN needs to be unescaped when analysing its contents. This
-# class also helps take care of that.
-module Gitlab
- module LDAP
- class DN
- FormatError = Class.new(StandardError)
- MalformedError = Class.new(FormatError)
- UnsupportedError = Class.new(FormatError)
-
- def self.normalize_value(given_value)
- dummy_dn = "placeholder=#{given_value}"
- normalized_dn = new(*dummy_dn).to_normalized_s
- normalized_dn.sub(/\Aplaceholder=/, '')
- end
-
- ##
- # Initialize a DN, escaping as required. Pass in attributes in name/value
- # pairs. If there is a left over argument, it will be appended to the dn
- # without escaping (useful for a base string).
- #
- # Most uses of this class will be to escape a DN, rather than to parse it,
- # so storing the dn as an escaped String and parsing parts as required
- # with a state machine seems sensible.
- def initialize(*args)
- if args.length > 1
- initialize_array(args)
- else
- initialize_string(args[0])
- end
- end
-
- ##
- # Parse a DN into key value pairs using ASN from
- # http://tools.ietf.org/html/rfc2253 section 3.
- # rubocop:disable Metrics/AbcSize
- # rubocop:disable Metrics/CyclomaticComplexity
- # rubocop:disable Metrics/PerceivedComplexity
- def each_pair
- state = :key
- key = StringIO.new
- value = StringIO.new
- hex_buffer = ""
-
- @dn.each_char.with_index do |char, dn_index|
- case state
- when :key then
- case char
- when 'a'..'z', 'A'..'Z' then
- state = :key_normal
- key << char
- when '0'..'9' then
- state = :key_oid
- key << char
- when ' ' then state = :key
- else raise(MalformedError, "Unrecognized first character of an RDN attribute type name \"#{char}\"")
- end
- when :key_normal then
- case char
- when '=' then state = :value
- when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char
- else raise(MalformedError, "Unrecognized RDN attribute type name character \"#{char}\"")
- end
- when :key_oid then
- case char
- when '=' then state = :value
- when '0'..'9', '.', ' ' then key << char
- else raise(MalformedError, "Unrecognized RDN OID attribute type name character \"#{char}\"")
- end
- when :value then
- case char
- when '\\' then state = :value_normal_escape
- when '"' then state = :value_quoted
- when ' ' then state = :value
- when '#' then
- state = :value_hexstring
- value << char
- when ',' then
- state = :key
- yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
- key = StringIO.new
- value = StringIO.new
- else
- state = :value_normal
- value << char
- end
- when :value_normal then
- case char
- when '\\' then state = :value_normal_escape
- when ',' then
- state = :key
- yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
- key = StringIO.new
- value = StringIO.new
- when '+' then raise(UnsupportedError, "Multivalued RDNs are not supported")
- else value << char
- end
- when :value_normal_escape then
- case char
- when '0'..'9', 'a'..'f', 'A'..'F' then
- state = :value_normal_escape_hex
- hex_buffer = char
- else
- state = :value_normal
- value << char
- end
- when :value_normal_escape_hex then
- case char
- when '0'..'9', 'a'..'f', 'A'..'F' then
- state = :value_normal
- value << "#{hex_buffer}#{char}".to_i(16).chr
- else raise(MalformedError, "Invalid escaped hex code \"\\#{hex_buffer}#{char}\"")
- end
- when :value_quoted then
- case char
- when '\\' then state = :value_quoted_escape
- when '"' then state = :value_end
- else value << char
- end
- when :value_quoted_escape then
- case char
- when '0'..'9', 'a'..'f', 'A'..'F' then
- state = :value_quoted_escape_hex
- hex_buffer = char
- else
- state = :value_quoted
- value << char
- end
- when :value_quoted_escape_hex then
- case char
- when '0'..'9', 'a'..'f', 'A'..'F' then
- state = :value_quoted
- value << "#{hex_buffer}#{char}".to_i(16).chr
- else raise(MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"#{char}\"")
- end
- when :value_hexstring then
- case char
- when '0'..'9', 'a'..'f', 'A'..'F' then
- state = :value_hexstring_hex
- value << char
- when ' ' then state = :value_end
- when ',' then
- state = :key
- yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
- key = StringIO.new
- value = StringIO.new
- else raise(MalformedError, "Expected the first character of a hex pair, but got \"#{char}\"")
- end
- when :value_hexstring_hex then
- case char
- when '0'..'9', 'a'..'f', 'A'..'F' then
- state = :value_hexstring
- value << char
- else raise(MalformedError, "Expected the second character of a hex pair, but got \"#{char}\"")
- end
- when :value_end then
- case char
- when ' ' then state = :value_end
- when ',' then
- state = :key
- yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
- key = StringIO.new
- value = StringIO.new
- else raise(MalformedError, "Expected the end of an attribute value, but got \"#{char}\"")
- end
- else raise "Fell out of state machine"
- end
- end
-
- # Last pair
- raise(MalformedError, 'DN string ended unexpectedly') unless
- [:value, :value_normal, :value_hexstring, :value_end].include? state
-
- yield key.string.strip, rstrip_except_escaped(value.string, @dn.length)
- end
-
- def rstrip_except_escaped(str, dn_index)
- str_ends_with_whitespace = str.match(/\s\z/)
-
- if str_ends_with_whitespace
- dn_part_ends_with_escaped_whitespace = @dn[0, dn_index].match(/\\(\s+)\z/)
-
- if dn_part_ends_with_escaped_whitespace
- dn_part_rwhitespace = dn_part_ends_with_escaped_whitespace[1]
- num_chars_to_remove = dn_part_rwhitespace.length - 1
- str = str[0, str.length - num_chars_to_remove]
- else
- str.rstrip!
- end
- end
-
- str
- end
-
- ##
- # Returns the DN as an array in the form expected by the constructor.
- def to_a
- a = []
- self.each_pair { |key, value| a << key << value } unless @dn.empty?
- a
- end
-
- ##
- # Return the DN as an escaped string.
- def to_s
- @dn
- end
-
- ##
- # Return the DN as an escaped and normalized string.
- def to_normalized_s
- self.class.new(*to_a).to_s.downcase
- end
-
- # https://tools.ietf.org/html/rfc4514 section 2.4 lists these exceptions
- # for DN values. All of the following must be escaped in any normal string
- # using a single backslash ('\') as escape. The space character is left
- # out here because in a "normalized" string, spaces should only be escaped
- # if necessary (i.e. leading or trailing space).
- NORMAL_ESCAPES = [',', '+', '"', '\\', '<', '>', ';', '='].freeze
-
- # The following must be represented as escaped hex
- HEX_ESCAPES = {
- "\n" => '\0a',
- "\r" => '\0d'
- }.freeze
-
- # Compiled character class regexp using the keys from the above hash, and
- # checking for a space or # at the start, or space at the end, of the
- # string.
- ESCAPE_RE = Regexp.new("(^ |^#| $|[" +
- NORMAL_ESCAPES.map { |e| Regexp.escape(e) }.join +
- "])")
-
- HEX_ESCAPE_RE = Regexp.new("([" +
- HEX_ESCAPES.keys.map { |e| Regexp.escape(e) }.join +
- "])")
-
- ##
- # Escape a string for use in a DN value
- def self.escape(string)
- escaped = string.gsub(ESCAPE_RE) { |char| "\\" + char }
- escaped.gsub(HEX_ESCAPE_RE) { |char| HEX_ESCAPES[char] }
- end
-
- private
-
- def initialize_array(args)
- buffer = StringIO.new
-
- args.each_with_index do |arg, index|
- if index.even? # key
- buffer << "," if index > 0
- buffer << arg
- else # value
- buffer << "="
- buffer << self.class.escape(arg)
- end
- end
-
- @dn = buffer.string
- end
-
- def initialize_string(arg)
- @dn = arg.to_s
- end
-
- ##
- # Proxy all other requests to the string object, because a DN is mainly
- # used within the library as a string
- # rubocop:disable GitlabSecurity/PublicSend
- def method_missing(method, *args, &block)
- @dn.send(method, *args, &block)
- end
-
- ##
- # Redefined to be consistent with redefined `method_missing` behavior
- def respond_to?(sym, include_private = false)
- @dn.respond_to?(sym, include_private)
- end
- end
- end
-end
diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb
deleted file mode 100644
index b91757c2a4b..00000000000
--- a/lib/gitlab/ldap/person.rb
+++ /dev/null
@@ -1,135 +0,0 @@
-module Gitlab
- module LDAP
- class Person
- # Active Directory-specific LDAP filter that checks if bit 2 of the
- # userAccountControl attribute is set.
- # Source: http://ctogonewild.com/2009/09/03/bitmask-searches-in-ldap/
- AD_USER_DISABLED = Net::LDAP::Filter.ex("userAccountControl:1.2.840.113556.1.4.803", "2")
-
- InvalidEntryError = Class.new(StandardError)
-
- attr_accessor :entry, :provider
-
- def self.find_by_uid(uid, adapter)
- uid = Net::LDAP::Filter.escape(uid)
- adapter.user(adapter.config.uid, uid)
- end
-
- def self.find_by_dn(dn, adapter)
- adapter.user('dn', dn)
- end
-
- def self.find_by_email(email, adapter)
- email_fields = adapter.config.attributes['email']
-
- adapter.user(email_fields, email)
- end
-
- def self.disabled_via_active_directory?(dn, adapter)
- adapter.dn_matches_filter?(dn, AD_USER_DISABLED)
- end
-
- def self.ldap_attributes(config)
- [
- 'dn',
- config.uid,
- *config.attributes['name'],
- *config.attributes['email'],
- *config.attributes['username']
- ].compact.uniq
- end
-
- def self.normalize_dn(dn)
- ::Gitlab::LDAP::DN.new(dn).to_normalized_s
- rescue ::Gitlab::LDAP::DN::FormatError => e
- Rails.logger.info("Returning original DN \"#{dn}\" due to error during normalization attempt: #{e.message}")
-
- dn
- end
-
- # Returns the UID in a normalized form.
- #
- # 1. Excess spaces are stripped
- # 2. The string is downcased (for case-insensitivity)
- def self.normalize_uid(uid)
- ::Gitlab::LDAP::DN.normalize_value(uid)
- rescue ::Gitlab::LDAP::DN::FormatError => e
- Rails.logger.info("Returning original UID \"#{uid}\" due to error during normalization attempt: #{e.message}")
-
- uid
- end
-
- def initialize(entry, provider)
- Rails.logger.debug { "Instantiating #{self.class.name} with LDIF:\n#{entry.to_ldif}" }
- @entry = entry
- @provider = provider
-
- validate_entry
- end
-
- def name
- attribute_value(:name).first
- end
-
- def uid
- entry.public_send(config.uid).first # rubocop:disable GitlabSecurity/PublicSend
- end
-
- def username
- username = attribute_value(:username)
-
- # Depending on the attribute, multiple values may
- # be returned. We need only one for username.
- # Ex. `uid` returns only one value but `mail` may
- # return an array of multiple email addresses.
- [username].flatten.first.tap do |username|
- username.downcase! if config.lowercase_usernames
- end
- end
-
- def email
- attribute_value(:email)
- end
-
- def dn
- self.class.normalize_dn(entry.dn)
- end
-
- private
-
- def entry
- @entry
- end
-
- def config
- @config ||= Gitlab::LDAP::Config.new(provider)
- end
-
- # Using the LDAP attributes configuration, find and return the first
- # attribute with a value. For example, by default, when given 'email',
- # this method looks for 'mail', 'email' and 'userPrincipalName' and
- # returns the first with a value.
- def attribute_value(attribute)
- attributes = Array(config.attributes[attribute.to_s])
- selected_attr = attributes.find { |attr| entry.respond_to?(attr) }
-
- return nil unless selected_attr
-
- entry.public_send(selected_attr) # rubocop:disable GitlabSecurity/PublicSend
- end
-
- def validate_entry
- allowed_attrs = self.class.ldap_attributes(config).map(&:downcase)
-
- # Net::LDAP::Entry transforms keys to symbols. Change to strings to compare.
- entry_attrs = entry.attribute_names.map { |n| n.to_s.downcase }
- invalid_attrs = entry_attrs - allowed_attrs
-
- if invalid_attrs.any?
- raise InvalidEntryError,
- "#{self.class.name} initialized with Net::LDAP::Entry containing invalid attributes(s): #{invalid_attrs}"
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb
deleted file mode 100644
index 84ee94e38e4..00000000000
--- a/lib/gitlab/ldap/user.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-# LDAP extension for User model
-#
-# * Find or create user from omniauth.auth data
-# * Links LDAP account with existing user
-# * Auth LDAP user with login and password
-#
-module Gitlab
- module LDAP
- class User < Gitlab::OAuth::User
- class << self
- def find_by_uid_and_provider(uid, provider)
- identity = ::Identity.with_extern_uid(provider, uid).take
-
- identity && identity.user
- end
- end
-
- def save
- super('LDAP')
- end
-
- # instance methods
- def find_user
- find_by_uid_and_provider || find_by_email || build_new_user
- end
-
- def find_by_uid_and_provider
- self.class.find_by_uid_and_provider(auth_hash.uid, auth_hash.provider)
- end
-
- def changed?
- gl_user.changed? || gl_user.identities.any?(&:changed?)
- end
-
- def block_after_signup?
- ldap_config.block_auto_created_users
- end
-
- def allowed?
- Gitlab::LDAP::Access.allowed?(gl_user)
- end
-
- def ldap_config
- Gitlab::LDAP::Config.new(auth_hash.provider)
- end
-
- def auth_hash=(auth_hash)
- @auth_hash = Gitlab::LDAP::AuthHash.new(auth_hash)
- end
- end
- end
-end
diff --git a/lib/gitlab/metrics/sidekiq_metrics_exporter.rb b/lib/gitlab/metrics/sidekiq_metrics_exporter.rb
index 5980a4ded2b..db8bdde74b2 100644
--- a/lib/gitlab/metrics/sidekiq_metrics_exporter.rb
+++ b/lib/gitlab/metrics/sidekiq_metrics_exporter.rb
@@ -23,7 +23,7 @@ module Gitlab
end
def stop_working
- server.shutdown
+ server.shutdown if server
@server = nil
end
diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb
index 45b9e14ba55..f3e48083c19 100644
--- a/lib/gitlab/metrics/transaction.rb
+++ b/lib/gitlab/metrics/transaction.rb
@@ -73,7 +73,7 @@ module Gitlab
# event_name - The name of the event (e.g. "git_push").
# tags - A set of tags to attach to the event.
def add_event(event_name, tags = {})
- self.class.transaction_metric(event_name, :counter, prefix: 'event_', tags: tags).increment(tags.merge(labels))
+ self.class.transaction_metric(event_name, :counter, prefix: 'event_', use_feature_flag: true, tags: tags).increment(tags.merge(labels))
@metrics << Metric.new(EVENT_SERIES, { count: 1 }, tags.merge(event: event_name), :event)
end
@@ -150,11 +150,12 @@ module Gitlab
with_feature :prometheus_metrics_transaction_allocated_memory
end
- def self.transaction_metric(name, type, prefix: nil, tags: {})
+ def self.transaction_metric(name, type, prefix: nil, use_feature_flag: false, tags: {})
metric_name = "gitlab_transaction_#{prefix}#{name}_total".to_sym
fetch_metric(type, metric_name) do
docstring "Transaction #{prefix}#{name} #{type}"
base_labels tags.merge(BASE_LABELS)
+ with_feature "prometheus_transaction_#{prefix}#{name}_total".to_sym if use_feature_flag
if type == :gauge
multiprocess_mode :livesum
diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb
index 1a570f480c6..1fd8f147b44 100644
--- a/lib/gitlab/middleware/go.rb
+++ b/lib/gitlab/middleware/go.rb
@@ -114,7 +114,15 @@ module Gitlab
end
def current_user(request)
- request.env['warden']&.authenticate
+ authenticator = Gitlab::Auth::RequestAuthenticator.new(request)
+ user = authenticator.find_user_from_access_token || authenticator.find_user_from_warden
+
+ return unless user&.can?(:access_api)
+
+ # Right now, the `api` scope is the only one that should be able to determine private project existence.
+ return unless authenticator.valid_access_token?(scopes: [:api])
+
+ user
end
end
end
diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb
index cc1e92480be..d4c54049b74 100644
--- a/lib/gitlab/middleware/multipart.rb
+++ b/lib/gitlab/middleware/multipart.rb
@@ -42,7 +42,7 @@ module Gitlab
key, value = parsed_field.first
if value.nil?
- value = open_file(tmp_path)
+ value = open_file(tmp_path, @request.params["#{key}.name"])
@open_files << value
else
value = decorate_params_value(value, @request.params[key], tmp_path)
@@ -70,7 +70,7 @@ module Gitlab
case path_value
when nil
- value_hash[path_key] = open_file(tmp_path)
+ value_hash[path_key] = open_file(tmp_path, value_hash.dig(path_key, '.name'))
@open_files << value_hash[path_key]
value_hash
when Hash
@@ -81,8 +81,8 @@ module Gitlab
end
end
- def open_file(path)
- ::UploadedFile.new(path, File.basename(path), 'application/octet-stream')
+ def open_file(path, name)
+ ::UploadedFile.new(path, name || File.basename(path), 'application/octet-stream')
end
end
diff --git a/lib/gitlab/o_auth.rb b/lib/gitlab/o_auth.rb
deleted file mode 100644
index 5ad8d83bd6e..00000000000
--- a/lib/gitlab/o_auth.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-module Gitlab
- module OAuth
- SignupDisabledError = Class.new(StandardError)
- SigninDisabledForProviderError = Class.new(StandardError)
- end
-end
diff --git a/lib/gitlab/o_auth/auth_hash.rb b/lib/gitlab/o_auth/auth_hash.rb
deleted file mode 100644
index 5b5ed449f94..00000000000
--- a/lib/gitlab/o_auth/auth_hash.rb
+++ /dev/null
@@ -1,90 +0,0 @@
-# Class to parse and transform the info provided by omniauth
-#
-module Gitlab
- module OAuth
- class AuthHash
- attr_reader :auth_hash
- def initialize(auth_hash)
- @auth_hash = auth_hash
- end
-
- def uid
- @uid ||= Gitlab::Utils.force_utf8(auth_hash.uid.to_s)
- end
-
- def provider
- @provider ||= auth_hash.provider.to_s
- end
-
- def name
- @name ||= get_info(:name) || "#{get_info(:first_name)} #{get_info(:last_name)}"
- end
-
- def username
- @username ||= username_and_email[:username].to_s
- end
-
- def email
- @email ||= username_and_email[:email].to_s
- end
-
- def password
- @password ||= Gitlab::Utils.force_utf8(Devise.friendly_token[0, 8].downcase)
- end
-
- def location
- location = get_info(:address)
- if location.is_a?(Hash)
- [location.locality.presence, location.country.presence].compact.join(', ')
- else
- location
- end
- end
-
- def has_attribute?(attribute)
- if attribute == :location
- get_info(:address).present?
- else
- get_info(attribute).present?
- end
- end
-
- private
-
- def info
- auth_hash.info
- end
-
- def get_info(key)
- value = info[key]
- Gitlab::Utils.force_utf8(value) if value
- value
- end
-
- def username_and_email
- @username_and_email ||= begin
- username = get_info(:username).presence || get_info(:nickname).presence
- email = get_info(:email).presence
-
- username ||= generate_username(email) if email
- email ||= generate_temporarily_email(username) if username
-
- {
- username: username,
- email: email
- }
- end
- end
-
- # Get the first part of the email address (before @)
- # In addtion in removes illegal characters
- def generate_username(email)
- email.match(/^[^@]*/)[0].mb_chars.normalize(:kd).gsub(/[^\x00-\x7F]/, '').to_s
- end
-
- def generate_temporarily_email(username)
- "temp-email-for-oauth-#{username}@gitlab.localhost"
- end
- end
- end
-end
diff --git a/lib/gitlab/o_auth/provider.rb b/lib/gitlab/o_auth/provider.rb
deleted file mode 100644
index 657db29c85a..00000000000
--- a/lib/gitlab/o_auth/provider.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-module Gitlab
- module OAuth
- class Provider
- LABELS = {
- "github" => "GitHub",
- "gitlab" => "GitLab.com",
- "google_oauth2" => "Google"
- }.freeze
-
- def self.providers
- Devise.omniauth_providers
- end
-
- def self.enabled?(name)
- providers.include?(name.to_sym)
- end
-
- def self.ldap_provider?(name)
- name.to_s.start_with?('ldap')
- end
-
- def self.sync_profile_from_provider?(provider)
- return true if ldap_provider?(provider)
-
- providers = Gitlab.config.omniauth.sync_profile_from_provider
-
- if providers.is_a?(Array)
- providers.include?(provider)
- else
- providers
- end
- end
-
- def self.config_for(name)
- name = name.to_s
- if ldap_provider?(name)
- if Gitlab::LDAP::Config.valid_provider?(name)
- Gitlab::LDAP::Config.new(name).options
- else
- nil
- end
- else
- Gitlab.config.omniauth.providers.find { |provider| provider.name == name }
- end
- end
-
- def self.label_for(name)
- name = name.to_s
- config = config_for(name)
- (config && config['label']) || LABELS[name] || name.titleize
- end
- end
- end
-end
diff --git a/lib/gitlab/o_auth/session.rb b/lib/gitlab/o_auth/session.rb
deleted file mode 100644
index 30739f2a2c5..00000000000
--- a/lib/gitlab/o_auth/session.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# :nocov:
-module Gitlab
- module OAuth
- module Session
- def self.create(provider, ticket)
- Rails.cache.write("gitlab:#{provider}:#{ticket}", ticket, expires_in: Gitlab.config.omniauth.cas3.session_duration)
- end
-
- def self.destroy(provider, ticket)
- Rails.cache.delete("gitlab:#{provider}:#{ticket}")
- end
-
- def self.valid?(provider, ticket)
- Rails.cache.read("gitlab:#{provider}:#{ticket}").present?
- end
- end
- end
-end
-# :nocov:
diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb
deleted file mode 100644
index a3e1c66c19f..00000000000
--- a/lib/gitlab/o_auth/user.rb
+++ /dev/null
@@ -1,235 +0,0 @@
-# OAuth extension for User model
-#
-# * Find GitLab user based on omniauth uid and provider
-# * Create new user from omniauth data
-#
-module Gitlab
- module OAuth
- class User
- attr_accessor :auth_hash, :gl_user
-
- def initialize(auth_hash)
- self.auth_hash = auth_hash
- update_profile
- add_or_update_user_identities
- end
-
- def persisted?
- gl_user.try(:persisted?)
- end
-
- def new?
- !persisted?
- end
-
- def valid?
- gl_user.try(:valid?)
- end
-
- def save(provider = 'OAuth')
- raise SigninDisabledForProviderError if oauth_provider_disabled?
- raise SignupDisabledError unless gl_user
-
- block_after_save = needs_blocking?
-
- Users::UpdateService.new(gl_user, user: gl_user).execute!
-
- gl_user.block if block_after_save
-
- log.info "(#{provider}) saving user #{auth_hash.email} from login with extern_uid => #{auth_hash.uid}"
- gl_user
- rescue ActiveRecord::RecordInvalid => e
- log.info "(#{provider}) Error saving user #{auth_hash.uid} (#{auth_hash.email}): #{gl_user.errors.full_messages}"
- return self, e.record.errors
- end
-
- def gl_user
- return @gl_user if defined?(@gl_user)
-
- @gl_user = find_user
- end
-
- def find_user
- user = find_by_uid_and_provider
-
- user ||= find_or_build_ldap_user if auto_link_ldap_user?
- user ||= build_new_user if signup_enabled?
-
- user.external = true if external_provider? && user&.new_record?
-
- user
- end
-
- protected
-
- def add_or_update_user_identities
- return unless gl_user
-
- # find_or_initialize_by doesn't update `gl_user.identities`, and isn't autosaved.
- identity = gl_user.identities.find { |identity| identity.provider == auth_hash.provider }
-
- identity ||= gl_user.identities.build(provider: auth_hash.provider)
- identity.extern_uid = auth_hash.uid
-
- if auto_link_ldap_user? && !gl_user.ldap_user? && ldap_person
- log.info "Correct LDAP account has been found. identity to user: #{gl_user.username}."
- gl_user.identities.build(provider: ldap_person.provider, extern_uid: ldap_person.dn)
- end
- end
-
- def find_or_build_ldap_user
- return unless ldap_person
-
- user = Gitlab::LDAP::User.find_by_uid_and_provider(ldap_person.dn, ldap_person.provider)
- if user
- log.info "LDAP account found for user #{user.username}. Building new #{auth_hash.provider} identity."
- return user
- end
-
- log.info "No user found using #{auth_hash.provider} provider. Creating a new one."
- build_new_user
- end
-
- def find_by_email
- return unless auth_hash.has_attribute?(:email)
-
- ::User.find_by(email: auth_hash.email.downcase)
- end
-
- def auto_link_ldap_user?
- Gitlab.config.omniauth.auto_link_ldap_user
- end
-
- def creating_linked_ldap_user?
- auto_link_ldap_user? && ldap_person
- end
-
- def ldap_person
- return @ldap_person if defined?(@ldap_person)
-
- # Look for a corresponding person with same uid in any of the configured LDAP providers
- Gitlab::LDAP::Config.providers.each do |provider|
- adapter = Gitlab::LDAP::Adapter.new(provider)
- @ldap_person = find_ldap_person(auth_hash, adapter)
- break if @ldap_person
- end
- @ldap_person
- end
-
- def find_ldap_person(auth_hash, adapter)
- Gitlab::LDAP::Person.find_by_uid(auth_hash.uid, adapter) ||
- Gitlab::LDAP::Person.find_by_email(auth_hash.uid, adapter) ||
- Gitlab::LDAP::Person.find_by_dn(auth_hash.uid, adapter)
- end
-
- def ldap_config
- Gitlab::LDAP::Config.new(ldap_person.provider) if ldap_person
- end
-
- def needs_blocking?
- new? && block_after_signup?
- end
-
- def signup_enabled?
- providers = Gitlab.config.omniauth.allow_single_sign_on
- if providers.is_a?(Array)
- providers.include?(auth_hash.provider)
- else
- providers
- end
- end
-
- def external_provider?
- Gitlab.config.omniauth.external_providers.include?(auth_hash.provider)
- end
-
- def block_after_signup?
- if creating_linked_ldap_user?
- ldap_config.block_auto_created_users
- else
- Gitlab.config.omniauth.block_auto_created_users
- end
- end
-
- def auth_hash=(auth_hash)
- @auth_hash = AuthHash.new(auth_hash)
- end
-
- def find_by_uid_and_provider
- identity = Identity.with_extern_uid(auth_hash.provider, auth_hash.uid).take
- identity && identity.user
- end
-
- def build_new_user
- user_params = user_attributes.merge(skip_confirmation: true)
- Users::BuildService.new(nil, user_params).execute(skip_authorization: true)
- end
-
- def user_attributes
- # Give preference to LDAP for sensitive information when creating a linked account
- if creating_linked_ldap_user?
- username = ldap_person.username.presence
- email = ldap_person.email.first.presence
- end
-
- username ||= auth_hash.username
- email ||= auth_hash.email
-
- valid_username = ::Namespace.clean_path(username)
-
- uniquify = Uniquify.new
- valid_username = uniquify.string(valid_username) { |s| !NamespacePathValidator.valid_path?(s) }
-
- name = auth_hash.name
- name = valid_username if name.strip.empty?
-
- {
- name: name,
- username: valid_username,
- email: email,
- password: auth_hash.password,
- password_confirmation: auth_hash.password,
- password_automatically_set: true
- }
- end
-
- def sync_profile_from_provider?
- Gitlab::OAuth::Provider.sync_profile_from_provider?(auth_hash.provider)
- end
-
- def update_profile
- return unless sync_profile_from_provider? || creating_linked_ldap_user?
-
- metadata = gl_user.user_synced_attributes_metadata || gl_user.build_user_synced_attributes_metadata
-
- if sync_profile_from_provider?
- UserSyncedAttributesMetadata::SYNCABLE_ATTRIBUTES.each do |key|
- if auth_hash.has_attribute?(key) && gl_user.sync_attribute?(key)
- gl_user[key] = auth_hash.public_send(key) # rubocop:disable GitlabSecurity/PublicSend
- metadata.set_attribute_synced(key, true)
- else
- metadata.set_attribute_synced(key, false)
- end
- end
-
- metadata.provider = auth_hash.provider
- end
-
- if creating_linked_ldap_user? && gl_user.email == ldap_person.email.first
- metadata.set_attribute_synced(:email, true)
- metadata.provider = ldap_person.provider
- end
- end
-
- def log
- Gitlab::AppLogger
- end
-
- def oauth_provider_disabled?
- Gitlab::CurrentSettings.current_application_settings
- .disabled_oauth_sign_in_sources
- .include?(auth_hash.provider)
- end
- end
- end
-end
diff --git a/lib/gitlab/plugin.rb b/lib/gitlab/plugin.rb
new file mode 100644
index 00000000000..0d1cb16b378
--- /dev/null
+++ b/lib/gitlab/plugin.rb
@@ -0,0 +1,26 @@
+module Gitlab
+ module Plugin
+ def self.files
+ Dir.glob(Rails.root.join('plugins/*')).select do |entry|
+ File.file?(entry)
+ end
+ end
+
+ def self.execute_all_async(data)
+ args = files.map { |file| [file, data] }
+
+ PluginWorker.bulk_perform_async(args)
+ end
+
+ def self.execute(file, data)
+ result = Gitlab::Popen.popen_with_detail([file]) do |stdin|
+ stdin.write(data.to_json)
+ end
+
+ exit_status = result.status&.exitstatus
+ [exit_status.zero?, result.stderr]
+ rescue => e
+ [false, e.message]
+ end
+ end
+end
diff --git a/lib/gitlab/plugin_logger.rb b/lib/gitlab/plugin_logger.rb
new file mode 100644
index 00000000000..c4f6ec3e21d
--- /dev/null
+++ b/lib/gitlab/plugin_logger.rb
@@ -0,0 +1,7 @@
+module Gitlab
+ class PluginLogger < Gitlab::Logger
+ def self.file_name_noext
+ 'plugin'
+ end
+ end
+end
diff --git a/lib/gitlab/profiler.rb b/lib/gitlab/profiler.rb
index 95d94b3cc68..98a168b43bb 100644
--- a/lib/gitlab/profiler.rb
+++ b/lib/gitlab/profiler.rb
@@ -45,6 +45,7 @@ module Gitlab
if user
private_token ||= user.personal_access_tokens.active.pluck(:token).first
+ raise 'Your user must have a personal_access_token' unless private_token
end
headers['Private-Token'] = private_token if private_token
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 9e2fa07a205..cf0935dbd9a 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -41,7 +41,7 @@ module Gitlab
@commits_count ||= commits.count
end
- def self.parse_search_result(result)
+ def self.parse_search_result(result, project = nil)
ref = nil
filename = nil
basename = nil
@@ -66,7 +66,8 @@ module Gitlab
basename: basename,
ref: ref,
startline: startline,
- data: data
+ data: data,
+ project_id: project ? project.id : nil
)
end
diff --git a/lib/gitlab/prometheus/metric_group.rb b/lib/gitlab/prometheus/metric_group.rb
index 729fef34b35..e91c6fb2e27 100644
--- a/lib/gitlab/prometheus/metric_group.rb
+++ b/lib/gitlab/prometheus/metric_group.rb
@@ -6,9 +6,14 @@ module Gitlab
attr_accessor :name, :priority, :metrics
validates :name, :priority, :metrics, presence: true
- def self.all
+ def self.common_metrics
AdditionalMetricsParser.load_groups_from_yaml
end
+
+ # EE only
+ def self.for_project(_)
+ common_metrics
+ end
end
end
end
diff --git a/lib/gitlab/prometheus/queries/additional_metrics_deployment_query.rb b/lib/gitlab/prometheus/queries/additional_metrics_deployment_query.rb
index 69d055c901c..972ab75d1d5 100644
--- a/lib/gitlab/prometheus/queries/additional_metrics_deployment_query.rb
+++ b/lib/gitlab/prometheus/queries/additional_metrics_deployment_query.rb
@@ -4,9 +4,10 @@ module Gitlab
class AdditionalMetricsDeploymentQuery < BaseQuery
include QueryAdditionalMetrics
- def query(deployment_id)
+ def query(environment_id, deployment_id)
Deployment.find_by(id: deployment_id).try do |deployment|
query_metrics(
+ deployment.project,
common_query_context(
deployment.environment,
timeframe_start: (deployment.created_at - 30.minutes).to_f,
diff --git a/lib/gitlab/prometheus/queries/additional_metrics_environment_query.rb b/lib/gitlab/prometheus/queries/additional_metrics_environment_query.rb
index 32fe8201a8d..9273e69e158 100644
--- a/lib/gitlab/prometheus/queries/additional_metrics_environment_query.rb
+++ b/lib/gitlab/prometheus/queries/additional_metrics_environment_query.rb
@@ -7,6 +7,7 @@ module Gitlab
def query(environment_id)
::Environment.find_by(id: environment_id).try do |environment|
query_metrics(
+ environment.project,
common_query_context(environment, timeframe_start: 8.hours.ago.to_f, timeframe_end: Time.now.to_f)
)
end
diff --git a/lib/gitlab/prometheus/queries/deployment_query.rb b/lib/gitlab/prometheus/queries/deployment_query.rb
index 170f483540e..6e6da593178 100644
--- a/lib/gitlab/prometheus/queries/deployment_query.rb
+++ b/lib/gitlab/prometheus/queries/deployment_query.rb
@@ -2,7 +2,7 @@ module Gitlab
module Prometheus
module Queries
class DeploymentQuery < BaseQuery
- def query(deployment_id)
+ def query(environment_id, deployment_id)
Deployment.find_by(id: deployment_id).try do |deployment|
environment_slug = deployment.environment.slug
diff --git a/lib/gitlab/prometheus/queries/matched_metrics_query.rb b/lib/gitlab/prometheus/queries/matched_metrics_query.rb
index 4c3edccc71a..5710ad47c1a 100644
--- a/lib/gitlab/prometheus/queries/matched_metrics_query.rb
+++ b/lib/gitlab/prometheus/queries/matched_metrics_query.rb
@@ -18,7 +18,7 @@ module Gitlab
private
def groups_data
- metrics_groups = groups_with_active_metrics(Gitlab::Prometheus::MetricGroup.all)
+ metrics_groups = groups_with_active_metrics(Gitlab::Prometheus::MetricGroup.common_metrics)
lookup = active_series_lookup(metrics_groups)
groups = {}
diff --git a/lib/gitlab/prometheus/queries/query_additional_metrics.rb b/lib/gitlab/prometheus/queries/query_additional_metrics.rb
index 5cddc96a643..0c280dc9a3c 100644
--- a/lib/gitlab/prometheus/queries/query_additional_metrics.rb
+++ b/lib/gitlab/prometheus/queries/query_additional_metrics.rb
@@ -2,10 +2,10 @@ module Gitlab
module Prometheus
module Queries
module QueryAdditionalMetrics
- def query_metrics(query_context)
+ def query_metrics(project, query_context)
query_processor = method(:process_query).curry[query_context]
- groups = matched_metrics.map do |group|
+ groups = matched_metrics(project).map do |group|
metrics = group.metrics.map do |metric|
{
title: metric.title,
@@ -60,8 +60,8 @@ module Gitlab
@available_metrics ||= client_label_values || []
end
- def matched_metrics
- result = Gitlab::Prometheus::MetricGroup.all.map do |group|
+ def matched_metrics(project)
+ result = Gitlab::Prometheus::MetricGroup.for_project(project).map do |group|
group.metrics.select! do |metric|
metric.required_metrics.all?(&available_metrics.method(:include?))
end
diff --git a/lib/gitlab/prometheus_client.rb b/lib/gitlab/prometheus_client.rb
index aa94614bf18..659021c9ac9 100644
--- a/lib/gitlab/prometheus_client.rb
+++ b/lib/gitlab/prometheus_client.rb
@@ -1,12 +1,13 @@
module Gitlab
- PrometheusError = Class.new(StandardError)
-
# Helper methods to interact with Prometheus network services & resources
class PrometheusClient
- attr_reader :api_url
+ Error = Class.new(StandardError)
+ QueryError = Class.new(Gitlab::PrometheusClient::Error)
+
+ attr_reader :rest_client, :headers
- def initialize(api_url:)
- @api_url = api_url
+ def initialize(rest_client)
+ @rest_client = rest_client
end
def ping
@@ -22,10 +23,10 @@ module Gitlab
def query_range(query, start: 8.hours.ago, stop: Time.now)
get_result('matrix') do
json_api_get('query_range',
- query: query,
- start: start.to_f,
- end: stop.to_f,
- step: 1.minute.to_i)
+ query: query,
+ start: start.to_f,
+ end: stop.to_f,
+ step: 1.minute.to_i)
end
end
@@ -40,39 +41,44 @@ module Gitlab
private
def json_api_get(type, args = {})
- get(join_api_url(type, args))
+ path = ['api', 'v1', type].join('/')
+ get(path, args)
+ rescue JSON::ParserError
+ raise PrometheusClient::Error, 'Parsing response failed'
rescue Errno::ECONNREFUSED
- raise PrometheusError, 'Connection refused'
- end
-
- def join_api_url(type, args = {})
- url = URI.parse(api_url)
- rescue URI::Error
- raise PrometheusError, "Invalid API URL: #{api_url}"
- else
- url.path = [url.path.sub(%r{/+\z}, ''), 'api', 'v1', type].join('/')
- url.query = args.to_query
-
- url.to_s
+ raise PrometheusClient::Error, 'Connection refused'
end
- def get(url)
- handle_response(HTTParty.get(url))
+ def get(path, args)
+ response = rest_client[path].get(params: args)
+ handle_response(response)
rescue SocketError
- raise PrometheusError, "Can't connect to #{url}"
+ raise PrometheusClient::Error, "Can't connect to #{rest_client.url}"
rescue OpenSSL::SSL::SSLError
- raise PrometheusError, "#{url} contains invalid SSL data"
- rescue HTTParty::Error
- raise PrometheusError, "Network connection error"
+ raise PrometheusClient::Error, "#{rest_client.url} contains invalid SSL data"
+ rescue RestClient::ExceptionWithResponse => ex
+ handle_exception_response(ex.response)
+ rescue RestClient::Exception
+ raise PrometheusClient::Error, "Network connection error"
end
def handle_response(response)
+ json_data = JSON.parse(response.body)
+ if response.code == 200 && json_data['status'] == 'success'
+ json_data['data'] || {}
+ else
+ raise PrometheusClient::Error, "#{response.code} - #{response.body}"
+ end
+ end
+
+ def handle_exception_response(response)
if response.code == 200 && response['status'] == 'success'
response['data'] || {}
elsif response.code == 400
- raise PrometheusError, response['error'] || 'Bad data received'
+ json_data = JSON.parse(response.body)
+ raise PrometheusClient::QueryError, json_data['error'] || 'Bad data received'
else
- raise PrometheusError, "#{response.code} - #{response.body}"
+ raise PrometheusClient::Error, "#{response.code} - #{response.body}"
end
end
diff --git a/lib/gitlab/query_limiting.rb b/lib/gitlab/query_limiting.rb
index f64f1757144..9f69a9e4a39 100644
--- a/lib/gitlab/query_limiting.rb
+++ b/lib/gitlab/query_limiting.rb
@@ -6,7 +6,7 @@ module Gitlab
# This ensures we don't produce any errors that users can't do anything
# about themselves.
def self.enable?
- Gitlab.com? || Rails.env.development? || Rails.env.test?
+ Rails.env.development? || Rails.env.test?
end
# Allows the current request to execute any number of SQL queries.
diff --git a/lib/gitlab/query_limiting/active_support_subscriber.rb b/lib/gitlab/query_limiting/active_support_subscriber.rb
index 66049c94ec6..4c83581c4b1 100644
--- a/lib/gitlab/query_limiting/active_support_subscriber.rb
+++ b/lib/gitlab/query_limiting/active_support_subscriber.rb
@@ -3,8 +3,10 @@ module Gitlab
class ActiveSupportSubscriber < ActiveSupport::Subscriber
attach_to :active_record
- def sql(*)
- Transaction.current&.increment
+ def sql(event)
+ unless event.payload[:name] == 'CACHE'
+ Transaction.current&.increment
+ end
end
end
end
diff --git a/lib/gitlab/query_limiting/transaction.rb b/lib/gitlab/query_limiting/transaction.rb
index 3cbafadb0d0..66d7d9275cf 100644
--- a/lib/gitlab/query_limiting/transaction.rb
+++ b/lib/gitlab/query_limiting/transaction.rb
@@ -51,13 +51,7 @@ module Gitlab
error = ThresholdExceededError.new(error_message)
- if raise_error?
- raise(error)
- else
- # Raven automatically logs to the Rails log if disabled, thus we don't
- # need to manually log anything in case Sentry support is not enabled.
- Raven.capture_exception(error)
- end
+ raise(error) if raise_error?
end
def increment
diff --git a/lib/gitlab/quick_actions/command_definition.rb b/lib/gitlab/quick_actions/command_definition.rb
index 3937d9c153a..96415271316 100644
--- a/lib/gitlab/quick_actions/command_definition.rb
+++ b/lib/gitlab/quick_actions/command_definition.rb
@@ -24,15 +24,14 @@ module Gitlab
action_block.nil?
end
- def available?(opts)
+ def available?(context)
return true unless condition_block
- context = OpenStruct.new(opts)
context.instance_exec(&condition_block)
end
- def explain(context, opts, arg)
- return unless available?(opts)
+ def explain(context, arg)
+ return unless available?(context)
if explanation.respond_to?(:call)
execute_block(explanation, context, arg)
@@ -41,15 +40,13 @@ module Gitlab
end
end
- def execute(context, opts, arg)
- return if noop? || !available?(opts)
+ def execute(context, arg)
+ return if noop? || !available?(context)
execute_block(action_block, context, arg)
end
- def to_h(opts)
- context = OpenStruct.new(opts)
-
+ def to_h(context)
desc = description
if desc.respond_to?(:call)
desc = context.instance_exec(&desc) rescue ''
diff --git a/lib/gitlab/quick_actions/dsl.rb b/lib/gitlab/quick_actions/dsl.rb
index 536765305e1..d82dccd0db5 100644
--- a/lib/gitlab/quick_actions/dsl.rb
+++ b/lib/gitlab/quick_actions/dsl.rb
@@ -62,9 +62,8 @@ module Gitlab
# Allows to define conditions that must be met in order for the command
# to be returned by `.command_names` & `.command_definitions`.
- # It accepts a block that will be evaluated with the context given to
- # `CommandDefintion#to_h`.
- #
+ # It accepts a block that will be evaluated with the context
+ # of a QuickActions::InterpretService instance
# Example:
#
# condition do
diff --git a/lib/gitlab/quick_actions/extractor.rb b/lib/gitlab/quick_actions/extractor.rb
index c0878a34fb1..075ff91700c 100644
--- a/lib/gitlab/quick_actions/extractor.rb
+++ b/lib/gitlab/quick_actions/extractor.rb
@@ -29,7 +29,7 @@ module Gitlab
# commands = extractor.extract_commands(msg) #=> [['labels', '~foo ~"bar baz"']]
# msg #=> "hello\nworld"
# ```
- def extract_commands(content, opts = {})
+ def extract_commands(content)
return [content, []] unless content
content = content.dup
@@ -37,7 +37,7 @@ module Gitlab
commands = []
content.delete!("\r")
- content.gsub!(commands_regex(opts)) do
+ content.gsub!(commands_regex) do
if $~[:cmd]
commands << [$~[:cmd], $~[:arg]].reject(&:blank?)
''
@@ -60,8 +60,8 @@ module Gitlab
# It looks something like:
#
# /^\/(?<cmd>close|reopen|...)(?:( |$))(?<arg>[^\/\n]*)(?:\n|$)/
- def commands_regex(opts)
- names = command_names(opts).map(&:to_s)
+ def commands_regex
+ names = command_names.map(&:to_s)
@commands_regex ||= %r{
(?<code>
@@ -133,7 +133,7 @@ module Gitlab
[content, commands]
end
- def command_names(opts)
+ def command_names
command_definitions.flat_map do |command|
next if command.noop?
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 7ab85e1c35c..ac3de2a8f71 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -40,12 +40,16 @@ module Gitlab
'a-zA-Z0-9_/\\$\\{\\}\\. \\-'
end
+ def environment_name_regex_chars_without_slash
+ 'a-zA-Z0-9_\\$\\{\\}\\. -'
+ end
+
def environment_name_regex
- @environment_name_regex ||= /\A[#{environment_name_regex_chars}]+\z/.freeze
+ @environment_name_regex ||= /\A[#{environment_name_regex_chars_without_slash}]([#{environment_name_regex_chars}]*[#{environment_name_regex_chars_without_slash}])?\z/.freeze
end
def environment_name_regex_message
- "can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.', and spaces"
+ "can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.', and spaces, but it cannot start or end with '/'"
end
def kubernetes_namespace_regex
diff --git a/lib/gitlab/saml/auth_hash.rb b/lib/gitlab/saml/auth_hash.rb
deleted file mode 100644
index 33d19373098..00000000000
--- a/lib/gitlab/saml/auth_hash.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-module Gitlab
- module Saml
- class AuthHash < Gitlab::OAuth::AuthHash
- def groups
- Array.wrap(get_raw(Gitlab::Saml::Config.groups))
- end
-
- private
-
- def get_raw(key)
- # Needs to call `all` because of https://git.io/vVo4u
- # otherwise just the first value is returned
- auth_hash.extra[:raw_info].all[key]
- end
- end
- end
-end
diff --git a/lib/gitlab/saml/config.rb b/lib/gitlab/saml/config.rb
deleted file mode 100644
index 574c3a4b28c..00000000000
--- a/lib/gitlab/saml/config.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module Gitlab
- module Saml
- class Config
- class << self
- def options
- Gitlab.config.omniauth.providers.find { |provider| provider.name == 'saml' }
- end
-
- def groups
- options[:groups_attribute]
- end
-
- def external_groups
- options[:external_groups]
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/saml/user.rb b/lib/gitlab/saml/user.rb
deleted file mode 100644
index d8faf7aad8c..00000000000
--- a/lib/gitlab/saml/user.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-# SAML extension for User model
-#
-# * Find GitLab user based on SAML uid and provider
-# * Create new user from SAML data
-#
-module Gitlab
- module Saml
- class User < Gitlab::OAuth::User
- def save
- super('SAML')
- end
-
- def find_user
- user = find_by_uid_and_provider
-
- user ||= find_by_email if auto_link_saml_user?
- user ||= find_or_build_ldap_user if auto_link_ldap_user?
- user ||= build_new_user if signup_enabled?
-
- if external_users_enabled? && user
- # Check if there is overlap between the user's groups and the external groups
- # setting then set user as external or internal.
- user.external = !(auth_hash.groups & Gitlab::Saml::Config.external_groups).empty?
- end
-
- user
- end
-
- def changed?
- return true unless gl_user
-
- gl_user.changed? || gl_user.identities.any?(&:changed?)
- end
-
- protected
-
- def auto_link_saml_user?
- Gitlab.config.omniauth.auto_link_saml_user
- end
-
- def external_users_enabled?
- !Gitlab::Saml::Config.external_groups.nil?
- end
-
- def auth_hash=(auth_hash)
- @auth_hash = Gitlab::Saml::AuthHash.new(auth_hash)
- end
- end
- end
-end
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index 5ad219179f3..781783f4d97 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -1,7 +1,9 @@
module Gitlab
class SearchResults
class FoundBlob
- attr_reader :id, :filename, :basename, :ref, :startline, :data
+ include EncodingHelper
+
+ attr_reader :id, :filename, :basename, :ref, :startline, :data, :project_id
def initialize(opts = {})
@id = opts.fetch(:id, nil)
@@ -9,8 +11,9 @@ module Gitlab
@basename = opts.fetch(:basename, nil)
@ref = opts.fetch(:ref, nil)
@startline = opts.fetch(:startline, nil)
- @data = opts.fetch(:data, nil)
+ @data = encode_utf8(opts.fetch(:data, nil))
@per_page = opts.fetch(:per_page, 20)
+ @project_id = opts.fetch(:project_id, nil)
end
def path
diff --git a/lib/gitlab/setup_helper.rb b/lib/gitlab/setup_helper.rb
index e90a90508a2..07d7c91cb5d 100644
--- a/lib/gitlab/setup_helper.rb
+++ b/lib/gitlab/setup_helper.rb
@@ -37,7 +37,7 @@ module Gitlab
config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path }
config[:bin_dir] = Gitlab.config.gitaly.client_path
- TOML.dump(config)
+ TomlRB.dump(config)
end
# rubocop:disable Rails/Output
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index f4a41dc3eda..4ba44e0feef 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -294,7 +294,8 @@ module Gitlab
# add_namespace("/path/to/storage", "gitlab")
#
def add_namespace(storage, name)
- Gitlab::GitalyClient.migrate(:add_namespace) do |enabled|
+ Gitlab::GitalyClient.migrate(:add_namespace,
+ status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled
gitaly_namespace_client(storage).add(name)
else
@@ -315,7 +316,8 @@ module Gitlab
# rm_namespace("/path/to/storage", "gitlab")
#
def rm_namespace(storage, name)
- Gitlab::GitalyClient.migrate(:remove_namespace) do |enabled|
+ Gitlab::GitalyClient.migrate(:remove_namespace,
+ status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled
gitaly_namespace_client(storage).remove(name)
else
@@ -333,7 +335,8 @@ module Gitlab
# mv_namespace("/path/to/storage", "gitlab", "gitlabhq")
#
def mv_namespace(storage, old_name, new_name)
- Gitlab::GitalyClient.migrate(:rename_namespace) do |enabled|
+ Gitlab::GitalyClient.migrate(:rename_namespace,
+ status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled
gitaly_namespace_client(storage).rename(old_name, new_name)
else
@@ -368,7 +371,8 @@ module Gitlab
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385
def exists?(storage, dir_name)
- Gitlab::GitalyClient.migrate(:namespace_exists) do |enabled|
+ Gitlab::GitalyClient.migrate(:namespace_exists,
+ status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled
gitaly_namespace_client(storage).exists?(dir_name)
else
diff --git a/lib/gitlab/sidekiq_middleware/memory_killer.rb b/lib/gitlab/sidekiq_middleware/memory_killer.rb
deleted file mode 100644
index b89ae2505c9..00000000000
--- a/lib/gitlab/sidekiq_middleware/memory_killer.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-module Gitlab
- module SidekiqMiddleware
- class MemoryKiller
- # Default the RSS limit to 0, meaning the MemoryKiller is disabled
- MAX_RSS = (ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS'] || 0).to_s.to_i
- # Give Sidekiq 15 minutes of grace time after exceeding the RSS limit
- GRACE_TIME = (ENV['SIDEKIQ_MEMORY_KILLER_GRACE_TIME'] || 15 * 60).to_s.to_i
- # Wait 30 seconds for running jobs to finish during graceful shutdown
- SHUTDOWN_WAIT = (ENV['SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT'] || 30).to_s.to_i
-
- # Create a mutex used to ensure there will be only one thread waiting to
- # shut Sidekiq down
- MUTEX = Mutex.new
-
- def call(worker, job, queue)
- yield
-
- current_rss = get_rss
-
- return unless MAX_RSS > 0 && current_rss > MAX_RSS
-
- Thread.new do
- # Return if another thread is already waiting to shut Sidekiq down
- return unless MUTEX.try_lock
-
- Sidekiq.logger.warn "Sidekiq worker PID-#{pid} current RSS #{current_rss}"\
- " exceeds maximum RSS #{MAX_RSS} after finishing job #{worker.class} JID-#{job['jid']}"
- Sidekiq.logger.warn "Sidekiq worker PID-#{pid} will stop fetching new jobs in #{GRACE_TIME} seconds, and will be shut down #{SHUTDOWN_WAIT} seconds later"
-
- # Wait `GRACE_TIME` to give the memory intensive job time to finish.
- # Then, tell Sidekiq to stop fetching new jobs.
- wait_and_signal(GRACE_TIME, 'SIGSTP', 'stop fetching new jobs')
-
- # Wait `SHUTDOWN_WAIT` to give already fetched jobs time to finish.
- # Then, tell Sidekiq to gracefully shut down by giving jobs a few more
- # moments to finish, killing and requeuing them if they didn't, and
- # then terminating itself.
- wait_and_signal(SHUTDOWN_WAIT, 'SIGTERM', 'gracefully shut down')
-
- # Wait for Sidekiq to shutdown gracefully, and kill it if it didn't.
- wait_and_signal(Sidekiq.options[:timeout] + 2, 'SIGKILL', 'die')
- end
- end
-
- private
-
- def get_rss
- output, status = Gitlab::Popen.popen(%W(ps -o rss= -p #{pid}), Rails.root.to_s)
- return 0 unless status.zero?
-
- output.to_i
- end
-
- def wait_and_signal(time, signal, explanation)
- Sidekiq.logger.warn "waiting #{time} seconds before sending Sidekiq worker PID-#{pid} #{signal} (#{explanation})"
- sleep(time)
-
- Sidekiq.logger.warn "sending Sidekiq worker PID-#{pid} #{signal} (#{explanation})"
- Process.kill(signal, pid)
- end
-
- def pid
- Process.pid
- end
- end
- end
-end
diff --git a/lib/gitlab/sidekiq_middleware/shutdown.rb b/lib/gitlab/sidekiq_middleware/shutdown.rb
new file mode 100644
index 00000000000..c2b8d6de66e
--- /dev/null
+++ b/lib/gitlab/sidekiq_middleware/shutdown.rb
@@ -0,0 +1,133 @@
+require 'mutex_m'
+
+module Gitlab
+ module SidekiqMiddleware
+ class Shutdown
+ extend Mutex_m
+
+ # Default the RSS limit to 0, meaning the MemoryKiller is disabled
+ MAX_RSS = (ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS'] || 0).to_s.to_i
+ # Give Sidekiq 15 minutes of grace time after exceeding the RSS limit
+ GRACE_TIME = (ENV['SIDEKIQ_MEMORY_KILLER_GRACE_TIME'] || 15 * 60).to_s.to_i
+ # Wait 30 seconds for running jobs to finish during graceful shutdown
+ SHUTDOWN_WAIT = (ENV['SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT'] || 30).to_s.to_i
+
+ # This exception can be used to request that the middleware start shutting down Sidekiq
+ WantShutdown = Class.new(StandardError)
+
+ ShutdownWithoutRaise = Class.new(WantShutdown)
+ private_constant :ShutdownWithoutRaise
+
+ # For testing only, to avoid race conditions (?) in Rspec mocks.
+ attr_reader :trace
+
+ # We store the shutdown thread in a class variable to ensure that there
+ # can be only one shutdown thread in the process.
+ def self.create_shutdown_thread
+ mu_synchronize do
+ return unless @shutdown_thread.nil?
+
+ @shutdown_thread = Thread.new { yield }
+ end
+ end
+
+ # For testing only: so we can wait for the shutdown thread to finish.
+ def self.shutdown_thread
+ mu_synchronize { @shutdown_thread }
+ end
+
+ # For testing only: so that we can reset the global state before each test.
+ def self.clear_shutdown_thread
+ mu_synchronize { @shutdown_thread = nil }
+ end
+
+ def initialize
+ @trace = Queue.new if Rails.env.test?
+ end
+
+ def call(worker, job, queue)
+ shutdown_exception = nil
+
+ begin
+ yield
+ check_rss!
+ rescue WantShutdown => ex
+ shutdown_exception = ex
+ end
+
+ return unless shutdown_exception
+
+ self.class.create_shutdown_thread do
+ do_shutdown(worker, job, shutdown_exception)
+ end
+
+ raise shutdown_exception unless shutdown_exception.is_a?(ShutdownWithoutRaise)
+ end
+
+ private
+
+ def do_shutdown(worker, job, shutdown_exception)
+ Sidekiq.logger.warn "Sidekiq worker PID-#{pid} shutting down because of #{shutdown_exception} after job "\
+ "#{worker.class} JID-#{job['jid']}"
+ Sidekiq.logger.warn "Sidekiq worker PID-#{pid} will stop fetching new jobs in #{GRACE_TIME} seconds, and will be shut down #{SHUTDOWN_WAIT} seconds later"
+
+ # Wait `GRACE_TIME` to give the memory intensive job time to finish.
+ # Then, tell Sidekiq to stop fetching new jobs.
+ wait_and_signal(GRACE_TIME, 'SIGTSTP', 'stop fetching new jobs')
+
+ # Wait `SHUTDOWN_WAIT` to give already fetched jobs time to finish.
+ # Then, tell Sidekiq to gracefully shut down by giving jobs a few more
+ # moments to finish, killing and requeuing them if they didn't, and
+ # then terminating itself.
+ wait_and_signal(SHUTDOWN_WAIT, 'SIGTERM', 'gracefully shut down')
+
+ # Wait for Sidekiq to shutdown gracefully, and kill it if it didn't.
+ wait_and_signal(Sidekiq.options[:timeout] + 2, 'SIGKILL', 'die')
+ end
+
+ def check_rss!
+ return unless MAX_RSS > 0
+
+ current_rss = get_rss
+ return unless current_rss > MAX_RSS
+
+ raise ShutdownWithoutRaise.new("current RSS #{current_rss} exceeds maximum RSS #{MAX_RSS}")
+ end
+
+ def get_rss
+ output, status = Gitlab::Popen.popen(%W(ps -o rss= -p #{pid}), Rails.root.to_s)
+ return 0 unless status.zero?
+
+ output.to_i
+ end
+
+ def wait_and_signal(time, signal, explanation)
+ Sidekiq.logger.warn "waiting #{time} seconds before sending Sidekiq worker PID-#{pid} #{signal} (#{explanation})"
+ sleep(time)
+
+ Sidekiq.logger.warn "sending Sidekiq worker PID-#{pid} #{signal} (#{explanation})"
+ kill(signal, pid)
+ end
+
+ def pid
+ Process.pid
+ end
+
+ def sleep(time)
+ if Rails.env.test?
+ @trace << [:sleep, time]
+ else
+ Kernel.sleep(time)
+ end
+ end
+
+ def kill(signal, pid)
+ if Rails.env.test?
+ @trace << [:kill, signal, pid]
+ else
+ Process.kill(signal, pid)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/base_command.rb b/lib/gitlab/slash_commands/base_command.rb
index cc3c9a50555..466554e398c 100644
--- a/lib/gitlab/slash_commands/base_command.rb
+++ b/lib/gitlab/slash_commands/base_command.rb
@@ -31,10 +31,11 @@ module Gitlab
raise NotImplementedError
end
- attr_accessor :project, :current_user, :params
+ attr_accessor :project, :current_user, :params, :chat_name
- def initialize(project, user, params = {})
- @project, @current_user, @params = project, user, params.dup
+ def initialize(project, chat_name, params = {})
+ @project, @current_user, @params = project, chat_name.user, params.dup
+ @chat_name = chat_name
end
private
diff --git a/lib/gitlab/slash_commands/command.rb b/lib/gitlab/slash_commands/command.rb
index a78408b0519..85aaa6b0eba 100644
--- a/lib/gitlab/slash_commands/command.rb
+++ b/lib/gitlab/slash_commands/command.rb
@@ -13,12 +13,13 @@ module Gitlab
if command
if command.allowed?(project, current_user)
- command.new(project, current_user, params).execute(match)
+ command.new(project, chat_name, params).execute(match)
else
Gitlab::SlashCommands::Presenters::Access.new.access_denied
end
else
- Gitlab::SlashCommands::Help.new(project, current_user, params).execute(available_commands, params[:text])
+ Gitlab::SlashCommands::Help.new(project, chat_name, params)
+ .execute(available_commands, params[:text])
end
end
diff --git a/lib/gitlab/sql/pattern.rb b/lib/gitlab/sql/pattern.rb
index 5f0c98cb5a4..53744bad1f4 100644
--- a/lib/gitlab/sql/pattern.rb
+++ b/lib/gitlab/sql/pattern.rb
@@ -25,7 +25,11 @@ module Gitlab
query.length >= MIN_CHARS_FOR_PARTIAL_MATCHING
end
- def fuzzy_arel_match(column, query)
+ # column - The column name to search in.
+ # query - The text to search for.
+ # lower_exact_match - When set to `true` we'll fall back to using
+ # `LOWER(column) = query` instead of using `ILIKE`.
+ def fuzzy_arel_match(column, query, lower_exact_match: false)
query = query.squish
return nil unless query.present?
@@ -36,7 +40,13 @@ module Gitlab
else
# No words of at least 3 chars, but we can search for an exact
# case insensitive match with the query as a whole
- arel_table[column].matches(sanitize_sql_like(query))
+ if lower_exact_match
+ Arel::Nodes::NamedFunction
+ .new('LOWER', [arel_table[column]])
+ .eq(query)
+ else
+ arel_table[column].matches(sanitize_sql_like(query))
+ end
end
end
diff --git a/lib/gitlab/ssh_public_key.rb b/lib/gitlab/ssh_public_key.rb
index 545e7c74f7e..6f63ea91ae8 100644
--- a/lib/gitlab/ssh_public_key.rb
+++ b/lib/gitlab/ssh_public_key.rb
@@ -53,7 +53,7 @@ module Gitlab
end
def valid?
- key.present? && bits && technology.supported_sizes.include?(bits)
+ SSHKey.valid_ssh_public_key?(key_text)
end
def type
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 9d13d1d781f..37d3512990e 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -9,6 +9,7 @@ module Gitlab
license_usage_data.merge(system_usage_data)
.merge(features_usage_data)
.merge(components_usage_data)
+ .merge(cycle_analytics_usage_data)
end
def to_json(force_refresh: false)
@@ -71,6 +72,10 @@ module Gitlab
}
end
+ def cycle_analytics_usage_data
+ Gitlab::CycleAnalytics::UsageData.new.to_json
+ end
+
def features_usage_data
features_usage_data_ce
end
diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb
index 15eb1c41213..91b8bb2a83f 100644
--- a/lib/gitlab/user_access.rb
+++ b/lib/gitlab/user_access.rb
@@ -31,7 +31,7 @@ module Gitlab
return false unless can_access_git?
if user.requires_ldap_check? && user.try_obtain_ldap_lease
- return false unless Gitlab::LDAP::Access.allowed?(user)
+ return false unless Gitlab::Auth::LDAP::Access.allowed?(user)
end
true
@@ -65,7 +65,7 @@ module Gitlab
return false unless can_access_git?
if protected?(ProtectedBranch, project, ref)
- return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user)
+ return true if project.user_can_push_to_empty_repo?(user)
protected_branch_accessible_to?(ref, action: :push)
else
diff --git a/lib/google_api/cloud_platform/client.rb b/lib/google_api/cloud_platform/client.rb
index ff638c07755..f30dd995695 100644
--- a/lib/google_api/cloud_platform/client.rb
+++ b/lib/google_api/cloud_platform/client.rb
@@ -76,9 +76,13 @@ module GoogleApi
"initial_node_count": cluster_size,
"node_config": {
"machine_type": machine_type
+ },
+ "legacy_abac": {
+ "enabled": true
}
}
- } )
+ }
+ )
service.create_cluster(project_id, zone, request_body, options: user_agent_header)
end
diff --git a/lib/haml_lint/inline_javascript.rb b/lib/haml_lint/inline_javascript.rb
index f5485eb89fa..4f776330e80 100644
--- a/lib/haml_lint/inline_javascript.rb
+++ b/lib/haml_lint/inline_javascript.rb
@@ -12,6 +12,12 @@ unless Rails.env.production?
record_lint(node, 'Inline JavaScript is discouraged (https://docs.gitlab.com/ee/development/gotchas.html#do-not-use-inline-javascript-in-views)')
end
+
+ def visit_tag(node)
+ return unless node.tag_name == 'script'
+
+ record_lint(node, 'Inline JavaScript is discouraged (https://docs.gitlab.com/ee/development/gotchas.html#do-not-use-inline-javascript-in-views)')
+ end
end
end
end
diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake
index c2d3a6b6950..c6942d22926 100644
--- a/lib/tasks/gemojione.rake
+++ b/lib/tasks/gemojione.rake
@@ -115,7 +115,7 @@ namespace :gemojione do
end
end
- style_path = Rails.root.join(*%w(app assets stylesheets framework emoji-sprites.scss))
+ style_path = Rails.root.join(*%w(app assets stylesheets framework emoji_sprites.scss))
# Combine the resized assets into a packed sprite and re-generate the SCSS
SpriteFactory.cssurl = "image-url('$IMAGE')"
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index e05a3aad824..2403f57f05a 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -336,7 +336,7 @@ namespace :gitlab do
warn_user_is_not_gitlab
start_checking "LDAP"
- if Gitlab::LDAP::Config.enabled?
+ if Gitlab::Auth::LDAP::Config.enabled?
check_ldap(args.limit)
else
puts 'LDAP is disabled in config/gitlab.yml'
@@ -346,13 +346,13 @@ namespace :gitlab do
end
def check_ldap(limit)
- servers = Gitlab::LDAP::Config.providers
+ servers = Gitlab::Auth::LDAP::Config.providers
servers.each do |server|
puts "Server: #{server}"
begin
- Gitlab::LDAP::Adapter.open(server) do |adapter|
+ Gitlab::Auth::LDAP::Adapter.open(server) do |adapter|
check_ldap_auth(adapter)
puts "LDAP users with access to your GitLab server (only showing the first #{limit} results)"
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index 5a53eac0897..2453079911d 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -87,7 +87,7 @@ namespace :gitlab do
print "#{user.name} (#{user.ldap_identity.extern_uid}) ..."
- if Gitlab::LDAP::Access.allowed?(user)
+ if Gitlab::Auth::LDAP::Access.allowed?(user)
puts " [OK]".color(:green)
else
if block_flag
diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake
index 107ff1d8aeb..e9ca6404fe8 100644
--- a/lib/tasks/gitlab/gitaly.rake
+++ b/lib/tasks/gitlab/gitaly.rake
@@ -2,7 +2,7 @@ namespace :gitlab do
namespace :gitaly do
desc "GitLab | Install or upgrade gitaly"
task :install, [:dir, :repo] => :gitlab_environment do |t, args|
- require 'toml'
+ require 'toml-rb'
warn_user_is_not_gitlab
@@ -38,7 +38,7 @@ namespace :gitlab do
desc "GitLab | Print storage configuration in TOML format"
task storage_config: :environment do
- require 'toml'
+ require 'toml-rb'
puts "# Gitaly storage configuration generated from #{Gitlab.config.source} on #{Time.current.to_s(:long)}"
puts "# This is in TOML format suitable for use in Gitaly's config.toml file."
diff --git a/lib/tasks/lint.rake b/lib/tasks/lint.rake
index 3ab406eff2c..fe5032cae18 100644
--- a/lib/tasks/lint.rake
+++ b/lib/tasks/lint.rake
@@ -16,5 +16,54 @@ unless Rails.env.production?
task :javascript do
Rake::Task['eslint'].invoke
end
+
+ desc "GitLab | lint | Run several lint checks"
+ task :all do
+ status = 0
+
+ %w[
+ config_lint
+ haml_lint
+ scss_lint
+ flay
+ gettext:lint
+ lint:static_verification
+ ].each do |task|
+ pid = Process.fork do
+ rd, wr = IO.pipe
+ stdout = $stdout.dup
+ stderr = $stderr.dup
+ $stdout.reopen(wr)
+ $stderr.reopen(wr)
+
+ begin
+ begin
+ Rake::Task[task].invoke
+ rescue RuntimeError # The haml_lint tasks raise a RuntimeError
+ exit(1)
+ end
+ rescue SystemExit => ex
+ msg = "*** Rake task #{task} failed with the following error(s):"
+ raise ex
+ ensure
+ $stdout.reopen(stdout)
+ $stderr.reopen(stderr)
+ wr.close
+
+ if msg
+ warn "\n#{msg}\n\n"
+ IO.copy_stream(rd, $stderr)
+ else
+ IO.copy_stream(rd, $stdout)
+ end
+ end
+ end
+
+ Process.waitpid(pid)
+ status += $?.exitstatus
+ end
+
+ exit(status)
+ end
end
end
diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake
index 31cbd651edb..1c7a8a90f5c 100644
--- a/lib/tasks/migrate/setup_postgresql.rake
+++ b/lib/tasks/migrate/setup_postgresql.rake
@@ -8,6 +8,7 @@ task setup_postgresql: :environment do
require Rails.root.join('db/migrate/20170503185032_index_redirect_routes_path_for_like')
require Rails.root.join('db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb')
require Rails.root.join('db/migrate/20180113220114_rework_redirect_routes_indexes.rb')
+ require Rails.root.join('db/migrate/20180215181245_users_name_lower_index.rb')
NamespacesProjectsPathLowerIndexes.new.up
AddUsersLowerUsernameEmailIndexes.new.up
@@ -17,4 +18,5 @@ task setup_postgresql: :environment do
IndexRedirectRoutesPathForLike.new.up
AddIndexOnNamespacesLowerName.new.up
ReworkRedirectRoutesIndexes.new.up
+ UsersNameLowerIndex.new.up
end
diff --git a/lib/tasks/plugins.rake b/lib/tasks/plugins.rake
new file mode 100644
index 00000000000..e73dd7e68df
--- /dev/null
+++ b/lib/tasks/plugins.rake
@@ -0,0 +1,16 @@
+namespace :plugins do
+ desc 'Validate existing plugins'
+ task validate: :environment do
+ puts 'Validating plugins from /plugins directory'
+
+ Gitlab::Plugin.files.each do |file|
+ success, message = Gitlab::Plugin.execute(file, Gitlab::DataBuilder::Push::SAMPLE_DATA)
+
+ if success
+ puts "* #{file} succeed (zero exit code)."
+ else
+ puts "* #{file} failure (non-zero exit code). #{message}"
+ end
+ end
+ end
+end