diff --git a/.gitlab/ci/docs.gitlab-ci.yml b/.gitlab/ci/docs.gitlab-ci.yml index c776e546f11..4acc3c7d1fe 100644 --- a/.gitlab/ci/docs.gitlab-ci.yml +++ b/.gitlab/ci/docs.gitlab-ci.yml @@ -50,7 +50,7 @@ docs lint: - .default-retry - .default-only - .only:changes-docs - image: "registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-docs-lint" + image: "registry.gitlab.com/gitlab-org/gitlab-docs:docs-lint" stage: test dependencies: [] script: diff --git a/Gemfile b/Gemfile index 5f3721e7663..0c4f8d83782 100644 --- a/Gemfile +++ b/Gemfile @@ -327,7 +327,7 @@ group :metrics do gem 'influxdb', '~> 0.2', require: false # Prometheus - gem 'prometheus-client-mmap', '~> 0.9.10' + gem 'prometheus-client-mmap', '~> 0.10.0' gem 'raindrops', '~> 0.18' end diff --git a/Gemfile.lock b/Gemfile.lock index c5f3dc3e5a4..6a19d35774d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -749,7 +749,7 @@ GEM parser unparser procto (0.0.3) - prometheus-client-mmap (0.9.10) + prometheus-client-mmap (0.10.0) pry (0.11.3) coderay (~> 1.1.0) method_source (~> 0.9.0) @@ -1292,7 +1292,7 @@ DEPENDENCIES pg (~> 1.1) png_quantizator (~> 0.2.1) premailer-rails (~> 1.10.3) - prometheus-client-mmap (~> 0.9.10) + prometheus-client-mmap (~> 0.10.0) pry-byebug (~> 3.5.1) pry-rails (~> 0.3.4) rack (~> 2.0.7) diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index c4c1bb0bcc1..31e87d1a7cf 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -386,22 +386,19 @@ margin: 5px; } -.issue-boards-sidebar { +.right-sidebar.issue-boards-sidebar { .gutter-toggle { bottom: 15px; width: 22px; - color: $gray-darkest; + padding-left: $gl-padding-32; svg { position: absolute; top: 50%; + right: 0; margin-top: (-11px / 2); - } - - &:hover { - path { - fill: $gray-darkest; - } + height: $gl-font-size-12; + width: $gl-font-size-12; } } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 11d5f4cd374..43636f65eb8 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -173,6 +173,20 @@ margin-top: 7px; } + .gutter-toggle { + margin-left: 20px; + padding-left: 10px; + + &:hover { + color: $gl-text-color; + } + + &:hover, + &:focus { + text-decoration: none; + } + } + .block { @include clearfix; padding: $gl-padding 0; @@ -195,20 +209,6 @@ margin-top: 0; } - .gutter-toggle { - margin-left: 20px; - padding-left: 10px; - - &:hover { - color: $gl-text-color; - } - - &:hover, - &:focus { - text-decoration: none; - } - } - &.assignee { .author-link { display: block; diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 953a6d5b18a..dc06cd8c166 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -23,6 +23,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController def index @environments = project.environments .with_state(params[:scope] || :available) + @project = ProjectPresenter.new(project, current_user: current_user) respond_to do |format| format.html @@ -31,6 +32,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController render json: { environments: serialize_environments(request, response, params[:nested]), + review_app: serialize_review_app, available_count: project.environments.available.count, stopped_count: project.environments.stopped.count } @@ -242,6 +244,10 @@ class Projects::EnvironmentsController < Projects::ApplicationController .represent(@environments) end + def serialize_review_app + ReviewAppSetupSerializer.new(current_user: @current_user).represent(@project) + end + def authorize_stop_environment! access_denied! unless can?(current_user, :stop_environment, environment) end diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb index 81018398d5d..45f4668112b 100644 --- a/app/presenters/project_presenter.rb +++ b/app/presenters/project_presenter.rb @@ -276,8 +276,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated end def kubernetes_cluster_anchor_data - if current_user && can?(current_user, :create_cluster, project) - + if can_instantiate_cluster? if clusters.empty? AnchorData.new(false, statistic_icon + _('Add Kubernetes cluster'), @@ -294,7 +293,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated end def gitlab_ci_anchor_data - if current_user && can_current_user_push_code? && repository.gitlab_ci_yml.blank? && !auto_devops_enabled? + if cicd_missing? AnchorData.new(false, statistic_icon + _('Set up CI/CD'), add_ci_yml_path) @@ -326,8 +325,28 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated count_of_extra_topics_not_shown > 0 end + def can_setup_review_app? + strong_memoize(:can_setup_review_app) do + (can_instantiate_cluster? && all_clusters_empty?) || cicd_missing? + end + end + + def all_clusters_empty? + strong_memoize(:all_clusters_empty) do + project.all_clusters.empty? + end + end + private + def cicd_missing? + current_user && can_current_user_push_code? && repository.gitlab_ci_yml.blank? && !auto_devops_enabled? + end + + def can_instantiate_cluster? + current_user && can?(current_user, :create_cluster, project) + end + def filename_path(filename) if blob = repository.public_send(filename) # rubocop:disable GitlabSecurity/PublicSend project_blob_path( diff --git a/app/presenters/release_presenter.rb b/app/presenters/release_presenter.rb index b38bbc8d96c..099ac9b09cd 100644 --- a/app/presenters/release_presenter.rb +++ b/app/presenters/release_presenter.rb @@ -40,7 +40,7 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated def evidence_file_path return unless release.evidence.present? - evidence_project_release_url(project, tag, format: :json) + evidence_project_release_url(project, release.to_param, format: :json) end private diff --git a/app/serializers/review_app_setup_entity.rb b/app/serializers/review_app_setup_entity.rb new file mode 100644 index 00000000000..3a21fe24d9e --- /dev/null +++ b/app/serializers/review_app_setup_entity.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class ReviewAppSetupEntity < Grape::Entity + include RequestAwareEntity + + expose :can_setup_review_app?, as: :can_setup_review_app + + expose :all_clusters_empty?, as: :all_clusters_empty, if: -> (_, _) { project.can_setup_review_app? } do |project| + project.all_clusters_empty? + end + + expose :review_snippet, if: -> (_, _) { project.can_setup_review_app? } do |_| + YAML.safe_load(File.read(Rails.root.join('lib', 'gitlab', 'ci', 'snippets', 'review_app_default.yml'))).to_s + end + + private + + def current_user + request.current_user + end + + def project + object + end +end diff --git a/app/serializers/review_app_setup_serializer.rb b/app/serializers/review_app_setup_serializer.rb new file mode 100644 index 00000000000..4baec7679b0 --- /dev/null +++ b/app/serializers/review_app_setup_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ReviewAppSetupSerializer < BaseSerializer + entity ReviewAppSetupEntity +end diff --git a/app/services/prometheus/proxy_service.rb b/app/services/prometheus/proxy_service.rb index a62eb76b8ce..3585c90fc8f 100644 --- a/app/services/prometheus/proxy_service.rb +++ b/app/services/prometheus/proxy_service.rb @@ -5,9 +5,17 @@ module Prometheus include ReactiveCaching include Gitlab::Utils::StrongMemoize - self.reactive_cache_key = ->(service) { service.cache_key } + self.reactive_cache_key = ->(service) { [] } self.reactive_cache_lease_timeout = 30.seconds - self.reactive_cache_refresh_interval = 30.seconds + + # reactive_cache_refresh_interval should be set to a value higher than + # reactive_cache_lifetime. If the refresh_interval is less than lifetime + # then the ReactiveCachingWorker will re-query prometheus for this + # PromQL query even though it's (probably) already been picked up by + # the frontend + # refresh_interval should be set less than lifetime only if this data + # is expected to change *and* be fetched again by the frontend + self.reactive_cache_refresh_interval = 90.seconds self.reactive_cache_lifetime = 1.minute self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) } diff --git a/app/workers/concerns/cluster_queue.rb b/app/workers/concerns/cluster_queue.rb index 180b86b0124..60ba8785347 100644 --- a/app/workers/concerns/cluster_queue.rb +++ b/app/workers/concerns/cluster_queue.rb @@ -8,6 +8,6 @@ module ClusterQueue included do queue_namespace :gcp_cluster - feature_category :kubernetes_configuration + feature_category :kubernetes_management end end diff --git a/app/workers/group_destroy_worker.rb b/app/workers/group_destroy_worker.rb index 553fd359baf..fc751f8b612 100644 --- a/app/workers/group_destroy_worker.rb +++ b/app/workers/group_destroy_worker.rb @@ -4,7 +4,7 @@ class GroupDestroyWorker include ApplicationWorker include ExceptionBacktrace - feature_category :groups + feature_category :subgroups def perform(group_id, user_id) begin diff --git a/changelogs/unreleased/194144-speed-up-url-helpers.yml b/changelogs/unreleased/194144-speed-up-url-helpers.yml new file mode 100644 index 00000000000..271dacefe35 --- /dev/null +++ b/changelogs/unreleased/194144-speed-up-url-helpers.yml @@ -0,0 +1,5 @@ +--- +title: Improve link generation performance +merge_request: 22426 +author: +type: performance diff --git a/changelogs/unreleased/22465-rack-attack-authenticate-job-token-requests.yml b/changelogs/unreleased/22465-rack-attack-authenticate-job-token-requests.yml new file mode 100644 index 00000000000..19cc7b83385 --- /dev/null +++ b/changelogs/unreleased/22465-rack-attack-authenticate-job-token-requests.yml @@ -0,0 +1,5 @@ +--- +title: Authenticate API requests with job tokens for Rack::Attack +merge_request: 21412 +author: +type: fixed diff --git a/changelogs/unreleased/27427-boards-sidebar-icon.yml b/changelogs/unreleased/27427-boards-sidebar-icon.yml new file mode 100644 index 00000000000..de8a5966d77 --- /dev/null +++ b/changelogs/unreleased/27427-boards-sidebar-icon.yml @@ -0,0 +1,5 @@ +--- +title: Increase size of issue boards sidebar collapse button +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/39119-actioncontroller-urlgenerationerror-no-route-matches-action-evidenc.yml b/changelogs/unreleased/39119-actioncontroller-urlgenerationerror-no-route-matches-action-evidenc.yml new file mode 100644 index 00000000000..25d8ef6651b --- /dev/null +++ b/changelogs/unreleased/39119-actioncontroller-urlgenerationerror-no-route-matches-action-evidenc.yml @@ -0,0 +1,5 @@ +--- +title: Fix releases page when tag contains a slash +merge_request: 22527 +author: +type: fixed diff --git a/changelogs/unreleased/sh-disable-prom-metrics-on-failure.yml b/changelogs/unreleased/sh-disable-prom-metrics-on-failure.yml new file mode 100644 index 00000000000..d9db2847d2e --- /dev/null +++ b/changelogs/unreleased/sh-disable-prom-metrics-on-failure.yml @@ -0,0 +1,5 @@ +--- +title: Disable Prometheus metrics if initialization fails +merge_request: 22355 +author: +type: fixed diff --git a/changelogs/unreleased/sidekiq-cluster-terminate-hung-workers.yml b/changelogs/unreleased/sidekiq-cluster-terminate-hung-workers.yml new file mode 100644 index 00000000000..294c739839f --- /dev/null +++ b/changelogs/unreleased/sidekiq-cluster-terminate-hung-workers.yml @@ -0,0 +1,5 @@ +--- +title: When sidekiq-cluster is asked to shutdown, actively terminate any sidekiq processes that don't finish cleanly in short order +merge_request: 21796 +author: +type: fixed diff --git a/changelogs/unreleased/smaller-prom-redis-keys.yml b/changelogs/unreleased/smaller-prom-redis-keys.yml new file mode 100644 index 00000000000..a1a9e40d0c3 --- /dev/null +++ b/changelogs/unreleased/smaller-prom-redis-keys.yml @@ -0,0 +1,5 @@ +--- +title: Reduce redis key size for the Prometheus proxy and the amount of queries by half +merge_request: 20006 +author: +type: performance diff --git a/config/feature_categories.yml b/config/feature_categories.yml index 59752a81f60..50776d92a30 100644 --- a/config/feature_categories.yml +++ b/config/feature_categories.yml @@ -8,10 +8,10 @@ # --- - accessibility_testing -- account-management -- agile_portfolio_management - analysis -- audit_management +- attack_emulation +- audit_events +- audit_reports - authentication_and_authorization - auto_devops - backup_restore @@ -25,25 +25,29 @@ - code_quality - code_review - collection +- compliance_controls +- compliance_frameworks - container_network_security - container_registry - container_scanning - continuous_delivery - continuous_integration - data_loss_prevention +- ddos_protection - dependency_proxy - dependency_scanning - design_management - devops_score - disaster_recovery - dynamic_application_security_testing +- epics - error_tracking - feature_flags - fuzzing - geo_replication - gitaly +- gitlab_handbook - gitter -- groups - helm_chart_registry - importers - incident_management @@ -55,12 +59,13 @@ - internationalization - issue_tracking - kanban_boards -- kubernetes_configuration +- kubernetes_management - language_specific - license_compliance - live_coding - load_testing - logging +- malware_scanning - metrics - omnibus_package - package_registry @@ -69,7 +74,9 @@ - release_governance - release_orchestration - requirements_management +- responsible_disclosure - review_apps +- roadmaps - runbooks - runner - runtime_application_self_protection @@ -82,8 +89,9 @@ - snippets - source_code_management - static_application_security_testing +- static_site_editor - status_page -- storage_security +- subgroups - synthetic_monitoring - system_testing - templates @@ -100,4 +108,3 @@ - web_ide - web_performance - wiki -- workflow_policies diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb index 22bb5f1764d..aa2601ea650 100644 --- a/config/initializers/7_prometheus_metrics.rb +++ b/config/initializers/7_prometheus_metrics.rb @@ -43,6 +43,9 @@ if !Rails.env.test? && Gitlab::Metrics.prometheus_metrics_enabled? defined?(::Prometheus::Client.reinitialize_on_pid_change) && Prometheus::Client.reinitialize_on_pid_change Gitlab::Metrics::Samplers::RubySampler.initialize_instance(Settings.monitoring.ruby_sampler_interval).start + rescue IOError => e + Gitlab::ErrorTracking.track_exception(e) + Gitlab::Metrics.error_detected! end Gitlab::Cluster::LifecycleEvents.on_master_start do @@ -55,6 +58,9 @@ if !Rails.env.test? && Gitlab::Metrics.prometheus_metrics_enabled? end Gitlab::Metrics::RequestsRackMiddleware.initialize_http_request_duration_seconds + rescue IOError => e + Gitlab::ErrorTracking.track_exception(e) + Gitlab::Metrics.error_detected! end end diff --git a/config/initializers/action_dispatch_journey_formatter.rb b/config/initializers/action_dispatch_journey_formatter.rb new file mode 100644 index 00000000000..93cf407c73c --- /dev/null +++ b/config/initializers/action_dispatch_journey_formatter.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# TODO: Eliminate this file when https://github.com/rails/rails/pull/38184 is released. +# Cleanup issue: https://gitlab.com/gitlab-org/gitlab/issues/195841 +ActionDispatch::Journey::Formatter.prepend(Gitlab::Patch::ActionDispatchJourneyFormatter) + +module ActionDispatch + module Journey + module Path + class Pattern + def requirements_for_missing_keys_check + @requirements_for_missing_keys_check ||= requirements.each_with_object({}) do |(key, regex), hash| + hash[key] = /\A#{regex}\Z/ + end + end + end + end + end +end diff --git a/doc/administration/packages/container_registry.md b/doc/administration/packages/container_registry.md index e573699e856..6ef1a3ec607 100644 --- a/doc/administration/packages/container_registry.md +++ b/doc/administration/packages/container_registry.md @@ -98,7 +98,7 @@ There are two ways you can configure the Registry's external domain. Either: for that domain. Since the container Registry requires a TLS certificate, in the end it all boils -down to how easy or pricey is to get a new one. +down to how easy or pricey it is to get a new one. Please take this into consideration before configuring the Container Registry for the first time. diff --git a/doc/security/asset_proxy.md b/doc/security/asset_proxy.md index 6e615028e8b..5522a41ff01 100644 --- a/doc/security/asset_proxy.md +++ b/doc/security/asset_proxy.md @@ -30,22 +30,22 @@ To install a Camo server as an asset proxy: 1. Make sure your instance of GitLab is running, and that you have created a private API token. Using the API, configure the asset proxy settings on your GitLab instance. For example: - ```sh - curl --request "PUT" "https://gitlab.example.com/api/v4/application/settings?\ - asset_proxy_enabled=true&\ - asset_proxy_url=https://proxy.gitlab.example.com&\ - asset_proxy_secret_key=" \ - --header 'PRIVATE-TOKEN: ' - ``` + ```sh + curl --request "PUT" "https://gitlab.example.com/api/v4/application/settings?\ + asset_proxy_enabled=true&\ + asset_proxy_url=https://proxy.gitlab.example.com&\ + asset_proxy_secret_key=" \ + --header 'PRIVATE-TOKEN: ' + ``` - The following settings are supported: + The following settings are supported: - | Attribute | Description | - |:-------------------------|:-------------------------------------------------------------------------------------------------------------------------------------| - | `asset_proxy_enabled` | Enable proxying of assets. If enabled, requires: `asset_proxy_url`). | - | `asset_proxy_secret_key` | Shared secret with the asset proxy server. | - | `asset_proxy_url` | URL of the asset proxy server. | - | `asset_proxy_whitelist` | Assets that match these domain(s) will NOT be proxied. Wildcards allowed. Your GitLab installation URL is automatically whitelisted. | + | Attribute | Description | + |:-------------------------|:-------------------------------------------------------------------------------------------------------------------------------------| + | `asset_proxy_enabled` | Enable proxying of assets. If enabled, requires: `asset_proxy_url`). | + | `asset_proxy_secret_key` | Shared secret with the asset proxy server. | + | `asset_proxy_url` | URL of the asset proxy server. | + | `asset_proxy_whitelist` | Assets that match these domain(s) will NOT be proxied. Wildcards allowed. Your GitLab installation URL is automatically whitelisted. | 1. Restart the server for the changes to take effect. Each time you change any values for the asset proxy, you need to restart the server. diff --git a/lib/gitlab/auth/auth_finders.rb b/lib/gitlab/auth/auth_finders.rb index 33cbb070c2f..fe61d9fe8ca 100644 --- a/lib/gitlab/auth/auth_finders.rb +++ b/lib/gitlab/auth/auth_finders.rb @@ -25,9 +25,10 @@ module Gitlab PRIVATE_TOKEN_HEADER = 'HTTP_PRIVATE_TOKEN' PRIVATE_TOKEN_PARAM = :private_token - JOB_TOKEN_HEADER = "HTTP_JOB_TOKEN".freeze + JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN'.freeze JOB_TOKEN_PARAM = :job_token RUNNER_TOKEN_PARAM = :token + RUNNER_JOB_TOKEN_PARAM = :token # Check the Rails session for valid authentication details def find_user_from_warden @@ -57,11 +58,13 @@ module Gitlab def find_user_from_job_token return unless route_authentication_setting[:job_token_allowed] - token = (params[JOB_TOKEN_PARAM] || env[JOB_TOKEN_HEADER]).to_s - return unless token.present? + token = current_request.params[JOB_TOKEN_PARAM].presence || + current_request.params[RUNNER_JOB_TOKEN_PARAM].presence || + current_request.env[JOB_TOKEN_HEADER].presence + return unless token job = ::Ci::Build.find_by_token(token) - raise ::Gitlab::Auth::UnauthorizedError unless job + raise UnauthorizedError unless job @current_authenticated_job = job # rubocop:disable Gitlab/ModuleWithInstanceVariables diff --git a/lib/gitlab/auth/request_authenticator.rb b/lib/gitlab/auth/request_authenticator.rb index 34ccff588f4..c6216fa9cad 100644 --- a/lib/gitlab/auth/request_authenticator.rb +++ b/lib/gitlab/auth/request_authenticator.rb @@ -33,7 +33,8 @@ module Gitlab find_user_from_web_access_token(request_format) || find_user_from_feed_token(request_format) || find_user_from_static_object_token(request_format) || - find_user_from_basic_auth_job + find_user_from_basic_auth_job || + find_user_from_job_token rescue Gitlab::Auth::AuthenticationError nil end @@ -45,6 +46,14 @@ module Gitlab rescue Gitlab::Auth::AuthenticationError false end + + private + + def route_authentication_setting + @route_authentication_setting ||= { + job_token_allowed: api_request? + } + end end end end diff --git a/lib/gitlab/ci/snippets/review_app_default.yml b/lib/gitlab/ci/snippets/review_app_default.yml new file mode 100644 index 00000000000..b6db08ef537 --- /dev/null +++ b/lib/gitlab/ci/snippets/review_app_default.yml @@ -0,0 +1,9 @@ +deploy_review: + stage: deploy + script: + - echo "Deploy a review app" + environment: + name: review/$CI_COMMIT_REF_NAME + url: https://$CI_ENVIRONMENT_SLUG.example.com + only: + - branches diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb index 61ed20ad623..d759ae24051 100644 --- a/lib/gitlab/metrics.rb +++ b/lib/gitlab/metrics.rb @@ -5,8 +5,14 @@ module Gitlab include Gitlab::Metrics::InfluxDb include Gitlab::Metrics::Prometheus + @error = false + def self.enabled? influx_metrics_enabled? || prometheus_metrics_enabled? end + + def self.error? + @error + end end end diff --git a/lib/gitlab/metrics/prometheus.rb b/lib/gitlab/metrics/prometheus.rb index cab1edab48f..f7480a8789e 100644 --- a/lib/gitlab/metrics/prometheus.rb +++ b/lib/gitlab/metrics/prometheus.rb @@ -61,6 +61,14 @@ module Gitlab safe_provide_metric(:histogram, name, docstring, base_labels, buckets) end + def error_detected! + clear_memoization(:prometheus_metrics_enabled) + + PROVIDER_MUTEX.synchronize do + @error = true + end + end + private def safe_provide_metric(method, name, *args) @@ -81,7 +89,7 @@ module Gitlab end def prometheus_metrics_enabled_unmemoized - metrics_folder_present? && Gitlab::CurrentSettings.prometheus_metrics_enabled || false + !error? && metrics_folder_present? && Gitlab::CurrentSettings.prometheus_metrics_enabled || false end end end diff --git a/lib/gitlab/patch/action_dispatch_journey_formatter.rb b/lib/gitlab/patch/action_dispatch_journey_formatter.rb new file mode 100644 index 00000000000..2d3b7bb9923 --- /dev/null +++ b/lib/gitlab/patch/action_dispatch_journey_formatter.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Gitlab + module Patch + module ActionDispatchJourneyFormatter + def self.prepended(mod) + mod.alias_method(:old_missing_keys, :missing_keys) + mod.remove_method(:missing_keys) + end + + private + + def missing_keys(route, parts) + missing_keys = nil + tests = route.path.requirements_for_missing_keys_check + route.required_parts.each do |key| + case tests[key] + when nil + unless parts[key] + missing_keys ||= [] + missing_keys << key + end + else + unless tests[key].match?(parts[key]) + missing_keys ||= [] + missing_keys << key + end + end + end + missing_keys + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ef8e20ec830..8bee019d867 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1350,6 +1350,9 @@ msgstr "" msgid "AdminUsers|External" msgstr "" +msgid "AdminUsers|Is using seat" +msgstr "" + msgid "AdminUsers|It's you!" msgstr "" diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index 2b923df40c5..9143db16b87 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -318,6 +318,7 @@ describe 'Issue Boards', :js do wait_for_requests click_link bug.title + within('.dropdown-menu-labels') { expect(page).to have_selector('.is-active', count: 3) } click_link regression.title wait_for_requests diff --git a/spec/features/projects/releases/user_views_releases_spec.rb b/spec/features/projects/releases/user_views_releases_spec.rb index a9b8ff9dc4d..4507d90576b 100644 --- a/spec/features/projects/releases/user_views_releases_spec.rb +++ b/spec/features/projects/releases/user_views_releases_spec.rb @@ -57,4 +57,14 @@ describe 'User views releases', :js do expect(page).to have_content('Upcoming Release') end end + + context 'with a tag containing a slash' do + it 'sees the release' do + release = create :release, :with_evidence, project: project, tag: 'debian/2.4.0-1' + visit project_releases_path(project) + + expect(page).to have_content(release.name) + expect(page).to have_content(release.tag) + end + end end diff --git a/spec/lib/gitlab/auth/auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb index 82ff8e7f76c..bffaaef4ed4 100644 --- a/spec/lib/gitlab/auth/auth_finders_spec.rb +++ b/spec/lib/gitlab/auth/auth_finders_spec.rb @@ -446,6 +446,93 @@ describe Gitlab::Auth::AuthFinders do end end + describe '#find_user_from_job_token' do + let(:job) { create(:ci_build, user: user) } + let(:route_authentication_setting) { { job_token_allowed: true } } + + subject { find_user_from_job_token } + + context 'when the job token is in the headers' do + it 'returns the user if valid job token' do + env[described_class::JOB_TOKEN_HEADER] = job.token + + is_expected.to eq(user) + expect(@current_authenticated_job).to eq(job) + end + + it 'returns nil without job token' do + env[described_class::JOB_TOKEN_HEADER] = '' + + is_expected.to be_nil + end + + it 'returns exception if invalid job token' do + env[described_class::JOB_TOKEN_HEADER] = 'invalid token' + + expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError) + end + + context 'when route is not allowed to be authenticated' do + let(:route_authentication_setting) { { job_token_allowed: false } } + + it 'sets current_user to nil' do + env[described_class::JOB_TOKEN_HEADER] = job.token + + allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(true) + + is_expected.to be_nil + end + end + end + + context 'when the job token is in the params' do + shared_examples 'job token params' do |token_key_name| + before do + set_param(token_key_name, token) + end + + context 'with valid job token' do + let(:token) { job.token } + + it 'returns the user' do + is_expected.to eq(user) + expect(@current_authenticated_job).to eq(job) + end + end + + context 'with empty job token' do + let(:token) { '' } + + it 'returns nil' do + is_expected.to be_nil + end + end + + context 'with invalid job token' do + let(:token) { 'invalid token' } + + it 'returns exception' do + expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError) + end + end + + context 'when route is not allowed to be authenticated' do + let(:route_authentication_setting) { { job_token_allowed: false } } + let(:token) { job.token } + + it 'sets current_user to nil' do + allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(true) + + is_expected.to be_nil + end + end + end + + it_behaves_like 'job token params', described_class::JOB_TOKEN_PARAM + it_behaves_like 'job token params', described_class::RUNNER_JOB_TOKEN_PARAM + end + end + describe '#find_runner_from_token' do let(:runner) { create(:ci_runner) } diff --git a/spec/lib/gitlab/auth/request_authenticator_spec.rb b/spec/lib/gitlab/auth/request_authenticator_spec.rb index 4dbcd0df302..87c96803c3a 100644 --- a/spec/lib/gitlab/auth/request_authenticator_spec.rb +++ b/spec/lib/gitlab/auth/request_authenticator_spec.rb @@ -42,6 +42,8 @@ describe Gitlab::Auth::RequestAuthenticator do describe '#find_sessionless_user' do let!(:access_token_user) { build(:user) } let!(:feed_token_user) { build(:user) } + let!(:static_object_token_user) { build(:user) } + let!(:job_token_user) { build(:user) } it 'returns access_token user first' do allow_any_instance_of(described_class).to receive(:find_user_from_web_access_token).and_return(access_token_user) @@ -56,6 +58,22 @@ describe Gitlab::Auth::RequestAuthenticator do expect(subject.find_sessionless_user([:api])).to eq feed_token_user end + it 'returns static_object_token user if no feed_token user found' do + allow_any_instance_of(described_class) + .to receive(:find_user_from_static_object_token) + .and_return(static_object_token_user) + + expect(subject.find_sessionless_user([:api])).to eq static_object_token_user + end + + it 'returns job_token user if no static_object_token user found' do + allow_any_instance_of(described_class) + .to receive(:find_user_from_job_token) + .and_return(job_token_user) + + expect(subject.find_sessionless_user([:api])).to eq job_token_user + end + it 'returns nil if no user found' do expect(subject.find_sessionless_user([:api])).to be_blank end @@ -67,6 +85,39 @@ describe Gitlab::Auth::RequestAuthenticator do end end + describe '#find_user_from_job_token' do + let!(:user) { build(:user) } + let!(:job) { build(:ci_build, user: user) } + + before do + env[Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER] = 'token' + end + + context 'with API requests' do + before do + env['SCRIPT_NAME'] = '/api/endpoint' + end + + it 'tries to find the user' do + expect(::Ci::Build).to receive(:find_by_token).and_return(job) + + expect(subject.find_sessionless_user([:api])).to eq user + end + end + + context 'without API requests' do + before do + env['SCRIPT_NAME'] = '/web/endpoint' + end + + it 'does not search for job users' do + expect(::Ci::Build).not_to receive(:find_by_token) + + expect(subject.find_sessionless_user([:api])).to be_nil + end + end + end + describe '#runner' do let!(:runner) { build(:ci_runner) } diff --git a/spec/lib/gitlab/metrics/prometheus_spec.rb b/spec/lib/gitlab/metrics/prometheus_spec.rb index b37624982e2..d4aa96a5b20 100644 --- a/spec/lib/gitlab/metrics/prometheus_spec.rb +++ b/spec/lib/gitlab/metrics/prometheus_spec.rb @@ -17,4 +17,21 @@ describe Gitlab::Metrics::Prometheus, :prometheus do expect(all_metrics.registry.metrics.count).to eq(0) end end + + describe '#error_detected!' do + before do + allow(all_metrics).to receive(:metrics_folder_present?).and_return(true) + stub_application_setting(prometheus_metrics_enabled: true) + end + + it 'disables Prometheus metrics' do + expect(all_metrics.error?).to be_falsey + expect(all_metrics.prometheus_metrics_enabled?).to be_truthy + + all_metrics.error_detected! + + expect(all_metrics.prometheus_metrics_enabled?).to be_falsey + expect(all_metrics.error?).to be_truthy + end + end end diff --git a/spec/lib/gitlab/patch/action_dispatch_journey_formatter_spec.rb b/spec/lib/gitlab/patch/action_dispatch_journey_formatter_spec.rb new file mode 100644 index 00000000000..5f0e1f40231 --- /dev/null +++ b/spec/lib/gitlab/patch/action_dispatch_journey_formatter_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Patch::ActionDispatchJourneyFormatter do + let(:group) { create(:group) } + let(:project) { create(:project, namespace: group) } + let(:pipeline) { create(:ci_empty_pipeline, project: project) } + let(:url) { Gitlab::Routing.url_helpers.project_pipeline_url(project, pipeline) } + let(:expected_path) { "#{project.full_path}/pipelines/#{pipeline.id}" } + + context 'custom implementation of #missing_keys' do + before do + expect_any_instance_of(Gitlab::Patch::ActionDispatchJourneyFormatter).to receive(:missing_keys) + end + + it 'generates correct url' do + expect(url).to end_with(expected_path) + end + end + + context 'original implementation of #missing_keys' do + before do + allow_any_instance_of(Gitlab::Patch::ActionDispatchJourneyFormatter).to receive(:missing_keys) do |instance, route, parts| + instance.send(:old_missing_keys, route, parts) # test the old implementation + end + end + + it 'generates correct url' do + expect(url).to end_with(expected_path) + end + end +end diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb index 318024bacd6..09ca106c54c 100644 --- a/spec/presenters/project_presenter_spec.rb +++ b/spec/presenters/project_presenter_spec.rb @@ -4,11 +4,10 @@ require 'spec_helper' describe ProjectPresenter do let(:user) { create(:user) } + let(:project) { create(:project) } + let(:presenter) { described_class.new(project, current_user: user) } describe '#license_short_name' do - let(:project) { create(:project) } - let(:presenter) { described_class.new(project, current_user: user) } - context 'when project.repository has a license_key' do it 'returns the nickname of the license if present' do allow(project.repository).to receive(:license_key).and_return('agpl-3.0') @@ -33,8 +32,6 @@ describe ProjectPresenter do end describe '#default_view' do - let(:presenter) { described_class.new(project, current_user: user) } - context 'user not signed in' do let(:user) { nil } @@ -125,7 +122,6 @@ describe ProjectPresenter do describe '#can_current_user_push_code?' do let(:project) { create(:project, :repository) } - let(:presenter) { described_class.new(project, current_user: user) } context 'empty repo' do let(:project) { create(:project) } @@ -163,7 +159,6 @@ describe ProjectPresenter do context 'statistics anchors (empty repo)' do let(:project) { create(:project, :empty_repo) } - let(:presenter) { described_class.new(project, current_user: user) } describe '#files_anchor_data' do it 'returns files data' do @@ -200,7 +195,6 @@ describe ProjectPresenter do context 'statistics anchors' do let(:project) { create(:project, :repository) } - let(:presenter) { described_class.new(project, current_user: user) } describe '#files_anchor_data' do it 'returns files data' do @@ -416,7 +410,6 @@ describe ProjectPresenter do describe '#statistics_buttons' do let(:project) { build(:project) } - let(:presenter) { described_class.new(project, current_user: user) } it 'orders the items correctly' do allow(project.repository).to receive(:readme).and_return(double(name: 'readme')) @@ -435,8 +428,6 @@ describe ProjectPresenter do end describe '#repo_statistics_buttons' do - let(:presenter) { described_class.new(project, current_user: user) } - subject(:empty_repo_statistics_buttons) { presenter.empty_repo_statistics_buttons } before do @@ -485,4 +476,73 @@ describe ProjectPresenter do end end end + + describe '#can_setup_review_app?' do + subject { presenter.can_setup_review_app? } + + context 'when the ci/cd file is missing' do + before do + allow(presenter).to receive(:cicd_missing?).and_return(true) + end + + it { is_expected.to be_truthy } + end + + context 'when the ci/cd file is not missing' do + before do + allow(presenter).to receive(:cicd_missing?).and_return(false) + end + + context 'and the user can create a cluster' do + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(user, :create_cluster, project).and_return(true) + end + + context 'and there is no cluster associated to this project' do + let(:project) { create(:project, clusters: []) } + + it { is_expected.to be_truthy } + end + + context 'and there is already a cluster associated to this project' do + let(:project) { create(:project, clusters: [build(:cluster, :providing_by_gcp)]) } + + it { is_expected.to be_falsey } + end + + context 'when a group cluster is instantiated' do + let_it_be(:cluster) { create(:cluster, :group) } + let_it_be(:group) { cluster.group } + + context 'and the project belongs to this group' do + let!(:project) { create(:project, group: group) } + + it { is_expected.to be_falsey } + end + + context 'and the project does not belong to this group' do + it { is_expected.to be_truthy } + end + end + + context 'and there is already an instance cluster' do + it 'is false' do + create(:cluster, :instance) + + is_expected.to be_falsey + end + end + end + + context 'and the user cannot create a cluster' do + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(user, :create_cluster, project).and_return(false) + end + + it { is_expected.to be_falsey } + end + end + end end diff --git a/spec/presenters/release_presenter_spec.rb b/spec/presenters/release_presenter_spec.rb index 2f978b0a036..4c6142f2edb 100644 --- a/spec/presenters/release_presenter_spec.rb +++ b/spec/presenters/release_presenter_spec.rb @@ -96,4 +96,28 @@ describe ReleasePresenter do it { is_expected.to be_nil } end end + + describe '#evidence_file_path' do + subject { presenter.evidence_file_path } + + context 'without evidence' do + it { is_expected.to be_falsy } + end + + context 'with evidence' do + let(:release) { create :release, :with_evidence, project: project } + + specify do + is_expected.to match /#{evidence_project_release_url(project, release.tag, format: :json)}/ + end + end + + context 'when a tag contains a slash' do + let(:release) { create :release, :with_evidence, project: project, tag: 'debian/2.4.0-1' } + + specify do + is_expected.to match /#{evidence_project_release_url(project, CGI.escape(release.tag), format: :json)}/ + end + end + end end diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb index 233f0497b7f..d3fe4c22b1d 100644 --- a/spec/requests/api/releases_spec.rb +++ b/spec/requests/api/releases_spec.rb @@ -115,6 +115,16 @@ describe API::Releases do end end + context 'when tag contains a slash' do + let!(:release) { create(:release, project: project, tag: 'debian/2.4.0-1', description: "debian/2.4.0-1") } + + it 'returns 200 HTTP status' do + get api("/projects/#{project.id}/releases", maintainer) + + expect(response).to have_gitlab_http_status(:ok) + end + end + context 'when user is a guest' do let!(:release) do create(:release, diff --git a/spec/serializers/review_app_setup_entity_spec.rb b/spec/serializers/review_app_setup_entity_spec.rb new file mode 100644 index 00000000000..19949fa9282 --- /dev/null +++ b/spec/serializers/review_app_setup_entity_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ReviewAppSetupEntity do + let_it_be(:user) { create(:admin) } + let(:project) { create(:project) } + let(:presenter) { ProjectPresenter.new(project, current_user: user) } + let(:entity) { described_class.new(presenter) } + let(:request) { double('request') } + + before do + allow(request).to receive(:current_user).and_return(user) + allow(request).to receive(:project).and_return(project) + end + + subject { entity.as_json } + + describe '#as_json' do + it 'contains can_setup_review_app' do + expect(subject).to include(:can_setup_review_app) + end + + context 'when the user can setup a review app' do + before do + allow(presenter).to receive(:can_setup_review_app?).and_return(true) + end + + it 'contains relevant fields' do + expect(subject.keys).to include(:all_clusters_empty, :review_snippet) + end + + it 'exposes the relevant review snippet' do + review_app_snippet = YAML.safe_load(File.read(Rails.root.join('lib', 'gitlab', 'ci', 'snippets', 'review_app_default.yml'))).to_s + + expect(subject[:review_snippet]).to eq(review_app_snippet) + end + + it 'exposes whether the project has associated clusters' do + expect(subject[:all_clusters_empty]).to be_truthy + end + end + + context 'when the user cannot setup a review app' do + before do + allow(presenter).to receive(:can_setup_review_app?).and_return(false) + end + + it 'does not expose certain fields' do + expect(subject.keys).not_to include(:all_clusters_empty, :review_snippet) + end + end + end +end diff --git a/spec/services/prometheus/proxy_service_spec.rb b/spec/services/prometheus/proxy_service_spec.rb index 03bda94e9c6..0ce08252425 100644 --- a/spec/services/prometheus/proxy_service_spec.rb +++ b/spec/services/prometheus/proxy_service_spec.rb @@ -8,6 +8,12 @@ describe Prometheus::ProxyService do set(:project) { create(:project) } set(:environment) { create(:environment, project: project) } + describe 'configuration' do + it 'ReactiveCaching refresh is not needed' do + expect(described_class.reactive_cache_refresh_interval).to be > described_class.reactive_cache_lifetime + end + end + describe '#initialize' do let(:params) { ActionController::Parameters.new(query: '1').permit! }