diff options
Diffstat (limited to 'app/controllers')
74 files changed, 1081 insertions, 272 deletions
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 3a5b8b2862e..73f71f7ad55 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -32,7 +32,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController end def integrations - @integrations = Service.find_or_initialize_instances.sort_by(&:title) + @integrations = Service.find_or_initialize_all(Service.for_instance).sort_by(&:title) end def update @@ -170,6 +170,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController def set_application_setting @application_setting = ApplicationSetting.current_without_cache + @plans = Plan.all end def whitelist_query_limiting diff --git a/app/controllers/instance_statistics/cohorts_controller.rb b/app/controllers/admin/cohorts_controller.rb index 0de62a56b01..e3df98b7917 100644 --- a/app/controllers/instance_statistics/cohorts_controller.rb +++ b/app/controllers/admin/cohorts_controller.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true -class InstanceStatistics::CohortsController < InstanceStatistics::ApplicationController +class Admin::CohortsController < Admin::ApplicationController include Analytics::UniqueVisitsHelper - before_action :authenticate_usage_ping_enabled_or_admin! - track_unique_visits :index, target_id: 'i_analytics_cohorts' def index @@ -16,8 +14,4 @@ class InstanceStatistics::CohortsController < InstanceStatistics::ApplicationCon @cohorts = CohortsSerializer.new.represent(cohorts_results) end end - - def authenticate_usage_ping_enabled_or_admin! - render_404 unless Gitlab::CurrentSettings.usage_ping_enabled || current_user.admin? - end end diff --git a/app/controllers/admin/concerns/authenticates_2fa_for_admin_mode.rb b/app/controllers/admin/concerns/authenticates_2fa_for_admin_mode.rb index 6014ed0dd13..03783cd75a3 100644 --- a/app/controllers/admin/concerns/authenticates_2fa_for_admin_mode.rb +++ b/app/controllers/admin/concerns/authenticates_2fa_for_admin_mode.rb @@ -11,7 +11,13 @@ module Authenticates2FAForAdminMode return handle_locked_user(user) unless user.can?(:log_in) session[:otp_user_id] = user.id - setup_u2f_authentication(user) + push_frontend_feature_flag(:webauthn) + + if user.two_factor_webauthn_enabled? + setup_webauthn_authentication(user) + else + setup_u2f_authentication(user) + end render 'admin/sessions/two_factor', layout: 'application' end @@ -24,7 +30,11 @@ module Authenticates2FAForAdminMode if user_params[:otp_attempt].present? && session[:otp_user_id] admin_mode_authenticate_with_two_factor_via_otp(user) elsif user_params[:device_response].present? && session[:otp_user_id] - admin_mode_authenticate_with_two_factor_via_u2f(user) + if user.two_factor_webauthn_enabled? + admin_mode_authenticate_with_two_factor_via_webauthn(user) + else + admin_mode_authenticate_with_two_factor_via_u2f(user) + end elsif user && user.valid_password?(user_params[:password]) admin_mode_prompt_for_two_factor(user) else @@ -52,18 +62,17 @@ module Authenticates2FAForAdminMode def admin_mode_authenticate_with_two_factor_via_u2f(user) if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenge]) - # Remove any lingering user data from login - session.delete(:otp_user_id) - session.delete(:challenge) - - # The admin user has successfully passed 2fa, enable admin mode ignoring password - enable_admin_mode + admin_handle_two_factor_success else - user.increment_failed_attempts! - Gitlab::AppLogger.info("Failed Admin Mode Login: user=#{user.username} ip=#{request.remote_ip} method=U2F") - flash.now[:alert] = _('Authentication via U2F device failed.') + admin_handle_two_factor_failure(user, 'U2F') + end + end - admin_mode_prompt_for_two_factor(user) + def admin_mode_authenticate_with_two_factor_via_webauthn(user) + if Webauthn::AuthenticateService.new(user, user_params[:device_response], session[:challenge]).execute + admin_handle_two_factor_success + else + admin_handle_two_factor_failure(user, 'WebAuthn') end end @@ -81,4 +90,21 @@ module Authenticates2FAForAdminMode flash.now[:alert] = _('Invalid login or password') render :new end + + def admin_handle_two_factor_success + # Remove any lingering user data from login + session.delete(:otp_user_id) + session.delete(:challenge) + + # The admin user has successfully passed 2fa, enable admin mode ignoring password + enable_admin_mode + end + + def admin_handle_two_factor_failure(user, method) + user.increment_failed_attempts! + Gitlab::AppLogger.info("Failed Admin Mode Login: user=#{user.username} ip=#{request.remote_ip} method=#{method}") + flash.now[:alert] = _('Authentication via %{method} device failed.') % { method: method } + + admin_mode_prompt_for_two_factor(user) + end end diff --git a/app/controllers/admin/dev_ops_report_controller.rb b/app/controllers/admin/dev_ops_report_controller.rb new file mode 100644 index 00000000000..bed0d51c331 --- /dev/null +++ b/app/controllers/admin/dev_ops_report_controller.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class Admin::DevOpsReportController < Admin::ApplicationController + include Analytics::UniqueVisitsHelper + + track_unique_visits :show, target_id: 'i_analytics_dev_ops_score' + + # rubocop: disable CodeReuse/ActiveRecord + def show + @metric = DevOpsReport::Metric.order(:created_at).last&.present + end + # rubocop: enable CodeReuse/ActiveRecord +end diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index 0245c00aacb..6414792dd43 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -19,7 +19,7 @@ class Admin::GroupsController < Admin::ApplicationController # the Group with statistics). @group = Group.with_statistics.find(group&.id) @members = present_members( - @group.members.order("access_level DESC").page(params[:members_page])) + group_members.order("access_level DESC").page(params[:members_page])) @requesters = present_members( AccessRequestsFinder.new(@group).execute(current_user)) @projects = @group.projects.with_statistics.page(params[:projects_page]) @@ -82,6 +82,10 @@ class Admin::GroupsController < Admin::ApplicationController @group ||= Group.find_by_full_path(params[:id]) end + def group_members + @group.members + end + def group_params params.require(:group).permit(allowed_group_params) end diff --git a/app/controllers/admin/instance_statistics_controller.rb b/app/controllers/admin/instance_statistics_controller.rb new file mode 100644 index 00000000000..3aee26b97a2 --- /dev/null +++ b/app/controllers/admin/instance_statistics_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class Admin::InstanceStatisticsController < Admin::ApplicationController + include Analytics::UniqueVisitsHelper + + before_action :check_feature_flag + + track_unique_visits :index, target_id: 'i_analytics_instance_statistics' + + def index + end + + def check_feature_flag + render_404 unless Feature.enabled?(:instance_statistics) + end +end diff --git a/app/controllers/admin/integrations_controller.rb b/app/controllers/admin/integrations_controller.rb index b2d5a2d130c..1e2a99f7078 100644 --- a/app/controllers/admin/integrations_controller.rb +++ b/app/controllers/admin/integrations_controller.rb @@ -6,9 +6,7 @@ class Admin::IntegrationsController < Admin::ApplicationController private def find_or_initialize_integration(name) - if name.in?(Service.available_services_names) - "#{name}_service".camelize.constantize.find_or_initialize_by(instance: true) # rubocop:disable CodeReuse/ActiveRecord - end + Service.find_or_initialize_integration(name, instance: true) end def integrations_enabled? diff --git a/app/controllers/admin/plan_limits_controller.rb b/app/controllers/admin/plan_limits_controller.rb new file mode 100644 index 00000000000..2620db8aec5 --- /dev/null +++ b/app/controllers/admin/plan_limits_controller.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class Admin::PlanLimitsController < Admin::ApplicationController + include InternalRedirect + + before_action :set_plan_limits + + def create + redirect_path = referer_path(request) || general_admin_application_settings_path + + respond_to do |format| + if @plan_limits.update(plan_limits_params) + format.json { head :ok } + format.html { redirect_to redirect_path, notice: _('Application limits saved successfully') } + else + format.json { head :bad_request } + format.html { render_update_error } + end + end + end + + private + + def set_plan_limits + @plan_limits = Plan.find(plan_limits_params[:plan_id]).actual_limits + end + + def plan_limits_params + params.require(:plan_limits).permit(%i[ + plan_id + conan_max_file_size + maven_max_file_size + npm_max_file_size + nuget_max_file_size + pypi_max_file_size + generic_packages_max_file_size + ]) + end +end diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb index 2449fa3128c..7a377a33d41 100644 --- a/app/controllers/admin/runners_controller.rb +++ b/app/controllers/admin/runners_controller.rb @@ -17,7 +17,6 @@ class Admin::RunnersController < Admin::ApplicationController def update if Ci::UpdateRunnerService.new(@runner).update(runner_params) respond_to do |format| - format.js format.html { redirect_to admin_runner_path(@runner) } end else diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb index 1bc82e98ab8..1f4250639c4 100644 --- a/app/controllers/admin/services_controller.rb +++ b/app/controllers/admin/services_controller.rb @@ -8,7 +8,7 @@ class Admin::ServicesController < Admin::ApplicationController def index @services = Service.find_or_create_templates.sort_by(&:title) - @existing_instance_types = Service.instances.pluck(:type) # rubocop: disable CodeReuse/ActiveRecord + @existing_instance_types = Service.for_instance.pluck(:type) # rubocop: disable CodeReuse/ActiveRecord end def edit diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index fc0acd8f99a..050f83edacb 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -111,10 +111,14 @@ class Admin::UsersController < Admin::ApplicationController end def disable_two_factor - update_user { |user| user.disable_two_factor! } + result = TwoFactor::DestroyService.new(current_user, user: user).execute - redirect_to admin_user_path(user), - notice: _('Two-factor Authentication has been disabled for this user') + if result[:status] == :success + redirect_to admin_user_path(user), + notice: _('Two-factor authentication has been disabled for this user') + else + redirect_to admin_user_path(user), alert: result[:message] + end end def create @@ -145,7 +149,7 @@ class Admin::UsersController < Admin::ApplicationController password_confirmation: params[:user][:password_confirmation] } - password_params[:password_expires_at] = Time.current unless changing_own_password? + password_params[:password_expires_at] = Time.current if admin_making_changes_for_another_user? user_params_with_pass.merge!(password_params) end @@ -153,6 +157,7 @@ class Admin::UsersController < Admin::ApplicationController respond_to do |format| result = Users::UpdateService.new(current_user, user_params_with_pass.merge(user: user)).execute do |user| user.skip_reconfirmation! + user.send_only_admin_changed_your_password_notification! if admin_making_changes_for_another_user? end if result[:status] == :success @@ -193,8 +198,8 @@ class Admin::UsersController < Admin::ApplicationController protected - def changing_own_password? - user == current_user + def admin_making_changes_for_another_user? + user != current_user end def user diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 2595b646964..5f05337e59e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -551,13 +551,9 @@ class ApplicationController < ActionController::Base "#{self.class.name}##{action_name}" end - # A user requires a role and have the setup_for_company attribute set when they are part of the experimental signup - # flow (executed by the Growth team). Users are redirected to the welcome page when their role is required and the - # experiment is enabled for the current user. def required_signup_info return unless current_user return unless current_user.role_required? - return unless experiment_enabled?(:signup_flow) store_location_for :user, request.fullpath diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb index cae9d098799..7006c23321c 100644 --- a/app/controllers/clusters/clusters_controller.rb +++ b/app/controllers/clusters/clusters_controller.rb @@ -237,6 +237,7 @@ class Clusters::ClustersController < Clusters::BaseController :environment_scope, :managed, provider_aws_attributes: [ + :kubernetes_version, :key_name, :role_arn, :region, diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb index b93c98a4790..9ff97f398f5 100644 --- a/app/controllers/concerns/authenticates_with_two_factor.rb +++ b/app/controllers/concerns/authenticates_with_two_factor.rb @@ -22,9 +22,15 @@ module AuthenticatesWithTwoFactor return handle_locked_user(user) unless user.can?(:log_in) session[:otp_user_id] = user.id - session[:user_updated_at] = user.updated_at + session[:user_password_hash] = Digest::SHA256.hexdigest(user.encrypted_password) + push_frontend_feature_flag(:webauthn) + + if user.two_factor_webauthn_enabled? + setup_webauthn_authentication(user) + else + setup_u2f_authentication(user) + end - setup_u2f_authentication(user) render 'devise/sessions/two_factor' end @@ -41,12 +47,16 @@ module AuthenticatesWithTwoFactor def authenticate_with_two_factor user = self.resource = find_user return handle_locked_user(user) unless user.can?(:log_in) - return handle_changed_user(user) if user_changed?(user) + return handle_changed_user(user) if user_password_changed?(user) if user_params[:otp_attempt].present? && session[:otp_user_id] authenticate_with_two_factor_via_otp(user) elsif user_params[:device_response].present? && session[:otp_user_id] - authenticate_with_two_factor_via_u2f(user) + if user.two_factor_webauthn_enabled? + authenticate_with_two_factor_via_webauthn(user) + else + authenticate_with_two_factor_via_u2f(user) + end elsif user && user.valid_password?(user_params[:password]) prompt_for_two_factor(user) end @@ -66,7 +76,7 @@ module AuthenticatesWithTwoFactor def clear_two_factor_attempt! session.delete(:otp_user_id) - session.delete(:user_updated_at) + session.delete(:user_password_hash) session.delete(:challenge) end @@ -89,16 +99,17 @@ module AuthenticatesWithTwoFactor # Authenticate using the response from a U2F (universal 2nd factor) device def authenticate_with_two_factor_via_u2f(user) if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenge]) - # Remove any lingering user data from login - clear_two_factor_attempt! + handle_two_factor_success(user) + else + handle_two_factor_failure(user, 'U2F') + end + end - remember_me(user) if user_params[:remember_me] == '1' - sign_in(user, message: :two_factor_authenticated, event: :authentication) + def authenticate_with_two_factor_via_webauthn(user) + if Webauthn::AuthenticateService.new(user, user_params[:device_response], session[:challenge]).execute + handle_two_factor_success(user) else - user.increment_failed_attempts! - Gitlab::AppLogger.info("Failed Login: user=#{user.username} ip=#{request.remote_ip} method=U2F") - flash.now[:alert] = _('Authentication via U2F device failed.') - prompt_for_two_factor(user) + handle_two_factor_failure(user, 'WebAuthn') end end @@ -116,8 +127,38 @@ module AuthenticatesWithTwoFactor sign_requests: sign_requests }) end end + + def setup_webauthn_authentication(user) + if user.webauthn_registrations.present? + + webauthn_registration_ids = user.webauthn_registrations.pluck(:credential_xid) + + get_options = WebAuthn::Credential.options_for_get(allow: webauthn_registration_ids, + user_verification: 'discouraged', + extensions: { appid: WebAuthn.configuration.origin }) + + session[:credentialRequestOptions] = get_options + session[:challenge] = get_options.challenge + gon.push(webauthn: { options: get_options.to_json }) + end + end # rubocop: enable CodeReuse/ActiveRecord + def handle_two_factor_success(user) + # Remove any lingering user data from login + clear_two_factor_attempt! + + remember_me(user) if user_params[:remember_me] == '1' + sign_in(user, message: :two_factor_authenticated, event: :authentication) + end + + def handle_two_factor_failure(user, method) + user.increment_failed_attempts! + Gitlab::AppLogger.info("Failed Login: user=#{user.username} ip=#{request.remote_ip} method=#{method}") + flash.now[:alert] = _('Authentication via %{method} device failed.') % { method: method } + prompt_for_two_factor(user) + end + def handle_changed_user(user) clear_two_factor_attempt! @@ -126,13 +167,9 @@ module AuthenticatesWithTwoFactor # If user has been updated since we validated the password, # the password might have changed. - def user_changed?(user) - return false unless session[:user_updated_at] - - # See: https://gitlab.com/gitlab-org/gitlab/-/issues/244638 - # Rounding errors happen when the user is updated, as the Rails ActiveRecord - # object has higher precision than what is stored in the database, therefore - # using .to_i to force truncation to the timestamp - user.updated_at.to_i != session[:user_updated_at].to_i + def user_password_changed?(user) + return false unless session[:user_password_hash] + + Digest::SHA256.hexdigest(user.encrypted_password) != session[:user_password_hash] end end diff --git a/app/controllers/concerns/integrations_actions.rb b/app/controllers/concerns/integrations_actions.rb index 9a8e5d14123..6060dc729af 100644 --- a/app/controllers/concerns/integrations_actions.rb +++ b/app/controllers/concerns/integrations_actions.rb @@ -16,12 +16,11 @@ module IntegrationsActions def update saved = integration.update(service_params[:service]) - overwrite = Gitlab::Utils.to_boolean(params[:overwrite]) respond_to do |format| format.html do if saved - PropagateIntegrationWorker.perform_async(integration.id, overwrite) + PropagateIntegrationWorker.perform_async(integration.id, false) redirect_to scoped_edit_integration_path(integration), notice: success_message else render 'shared/integrations/edit' @@ -57,9 +56,11 @@ module IntegrationsActions end def success_message - message = integration.active? ? _('activated') : _('settings saved, but not activated') - - _('%{service_title} %{message}.') % { service_title: integration.title, message: message } + if integration.active? + s_('Integrations|%{integration} settings saved and active.') % { integration: integration.title } + else + s_('Integrations|%{integration} settings saved, but not active.') % { integration: integration.title } + end end def serialize_as_json diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index c4dbce00593..a1a2740cde2 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -9,7 +9,7 @@ module IssuableActions before_action :check_destroy_confirmation!, only: :destroy before_action :authorize_admin_issuable!, only: :bulk_update before_action only: :show do - push_frontend_feature_flag(:scoped_labels, default_enabled: true) + push_frontend_feature_flag(:scoped_labels, type: :licensed, default_enabled: true) end before_action do push_frontend_feature_flag(:not_issuable_queries, @project, default_enabled: true) diff --git a/app/controllers/concerns/issuable_links.rb b/app/controllers/concerns/issuable_links.rb new file mode 100644 index 00000000000..2bdb190f1d5 --- /dev/null +++ b/app/controllers/concerns/issuable_links.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module IssuableLinks + def index + render json: issuables + end + + def create + result = create_service.execute + + render json: { message: result[:message], issuables: issuables }, status: result[:http_status] + end + + def destroy + result = destroy_service.execute + + render json: { issuables: issuables }, status: result[:http_status] + end + + private + + def issuables + list_service.execute + end + + def list_service + raise NotImplementedError + end + + def create_params + params.permit(issuable_references: []) + end + + def create_service + raise NotImplementedError + end + + def destroy_service + raise NotImplementedError + end +end diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index f4fc7decb60..7a5b470f366 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -100,7 +100,7 @@ module NotesActions # the finder. Here, we select between returning all notes since then, or a # page's worth of notes. def gather_notes - if Feature.enabled?(:paginated_notes, project) + if Feature.enabled?(:paginated_notes, noteable.try(:resource_parent)) gather_some_notes else gather_all_notes diff --git a/app/controllers/concerns/redis_tracking.rb b/app/controllers/concerns/redis_tracking.rb new file mode 100644 index 00000000000..fa5eef981d1 --- /dev/null +++ b/app/controllers/concerns/redis_tracking.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +# Example: +# +# # In controller include module +# # Track event for index action +# +# include RedisTracking +# +# track_redis_hll_event :index, :show, name: 'i_analytics_dev_ops_score', feature: :my_feature +# +# if the feature flag is enabled by default you should use +# track_redis_hll_event :index, :show, name: 'i_analytics_dev_ops_score', feature: :my_feature, feature_default_enabled: true +module RedisTracking + extend ActiveSupport::Concern + + class_methods do + def track_redis_hll_event(*controller_actions, name:, feature:, feature_default_enabled: false) + after_action only: controller_actions, if: -> { request.format.html? && request.headers['DNT'] != '1' } do + track_unique_redis_hll_event(name, feature, feature_default_enabled) + end + end + end + + private + + def track_unique_redis_hll_event(event_name, feature, feature_default_enabled) + return unless metric_feature_enabled?(feature, feature_default_enabled) + return unless Gitlab::CurrentSettings.usage_ping_enabled? + return unless visitor_id + + Gitlab::UsageDataCounters::HLLRedisCounter.track_event(visitor_id, event_name) + end + + def metric_feature_enabled?(feature, default_enabled) + Feature.enabled?(feature, default_enabled: default_enabled) + end + + def visitor_id + return cookies[:visitor_id] if cookies[:visitor_id].present? + return unless current_user + + uuid = SecureRandom.uuid + cookies[:visitor_id] = { value: uuid, expires: 24.months } + uuid + end +end diff --git a/app/controllers/concerns/renders_notes.rb b/app/controllers/concerns/renders_notes.rb index 18015b1de88..f8e3717acee 100644 --- a/app/controllers/concerns/renders_notes.rb +++ b/app/controllers/concerns/renders_notes.rb @@ -5,7 +5,6 @@ module RendersNotes def prepare_notes_for_rendering(notes, noteable = nil) preload_noteable_for_regular_notes(notes) preload_max_access_for_authors(notes, @project) - preload_first_time_contribution_for_authors(noteable, notes) preload_author_status(notes) Notes::RenderService.new(current_user).execute(notes) @@ -19,7 +18,8 @@ module RendersNotes return unless project user_ids = notes.map(&:author_id) - project.team.max_member_access_for_user_ids(user_ids) + access = project.team.max_member_access_for_user_ids(user_ids).select { |k, v| v == Gitlab::Access::NO_ACCESS }.keys + project.team.contribution_check_for_user_ids(access) end # rubocop: disable CodeReuse/ActiveRecord @@ -28,12 +28,6 @@ module RendersNotes end # rubocop: enable CodeReuse/ActiveRecord - def preload_first_time_contribution_for_authors(noteable, notes) - return unless noteable.is_a?(Issuable) && noteable.first_contribution? - - notes.each {|n| n.specialize_for_first_contribution!(noteable)} - end - # rubocop: disable CodeReuse/ActiveRecord def preload_author_status(notes) ActiveRecord::Associations::Preloader.new.preload(notes, { author: :status }) diff --git a/app/controllers/concerns/send_file_upload.rb b/app/controllers/concerns/send_file_upload.rb index 7cb19fc7e58..2f06cd84ee5 100644 --- a/app/controllers/concerns/send_file_upload.rb +++ b/app/controllers/concerns/send_file_upload.rb @@ -2,6 +2,8 @@ module SendFileUpload def send_upload(file_upload, send_params: {}, redirect_params: {}, attachment: nil, proxy: false, disposition: 'attachment') + content_type = content_type_for(attachment) + if attachment response_disposition = ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: attachment) @@ -9,7 +11,7 @@ module SendFileUpload # Google Cloud Storage, so the metadata needs to be cleared on GCS for # this to work. However, this override works with AWS. redirect_params[:query] = { "response-content-disposition" => response_disposition, - "response-content-type" => guess_content_type(attachment) } + "response-content-type" => content_type } # By default, Rails will send uploads with an extension of .js with a # content-type of text/javascript, which will trigger Rails' # cross-origin JavaScript protection. @@ -20,7 +22,7 @@ module SendFileUpload if image_scaling_request?(file_upload) location = file_upload.file_storage? ? file_upload.path : file_upload.url - headers.store(*Gitlab::Workhorse.send_scaled_image(location, params[:width].to_i)) + headers.store(*Gitlab::Workhorse.send_scaled_image(location, params[:width].to_i, content_type)) head :ok elsif file_upload.file_storage? send_file file_upload.path, send_params @@ -32,6 +34,12 @@ module SendFileUpload end end + def content_type_for(attachment) + return '' unless attachment + + guess_content_type(attachment) + end + def guess_content_type(filename) types = MIME::Types.type_for(filename) @@ -45,15 +53,33 @@ module SendFileUpload private def image_scaling_request?(file_upload) - avatar_image_upload?(file_upload) && valid_image_scaling_width? && current_user && - Feature.enabled?(:dynamic_image_resizing, current_user) + avatar_safe_for_scaling?(file_upload) && + scaling_allowed_by_feature_flags?(file_upload) && + valid_image_scaling_width? end - def avatar_image_upload?(file_upload) - file_upload.try(:image?) && file_upload.try(:mounted_as)&.to_sym == :avatar + def avatar_safe_for_scaling?(file_upload) + file_upload.try(:image_safe_for_scaling?) && mounted_as_avatar?(file_upload) + end + + def mounted_as_avatar?(file_upload) + file_upload.try(:mounted_as)&.to_sym == :avatar end def valid_image_scaling_width? Avatarable::ALLOWED_IMAGE_SCALER_WIDTHS.include?(params[:width]&.to_i) end + + # We use two separate feature gates to allow image resizing. + # The first, `:dynamic_image_resizing_requester`, based on the content requester. + # Enabling it for the user would allow that user to send resizing requests for any avatar. + # The second, `:dynamic_image_resizing_owner`, based on the content owner. + # Enabling it for the user would allow anyone to send resizing requests against the mentioned user avatar only. + # This flag allows us to operate on trusted data only, more in https://gitlab.com/gitlab-org/gitlab/-/issues/241533. + # Because of this, you need to enable BOTH to serve resized image, + # as you would need at least one allowed requester and at least one allowed avatar. + def scaling_allowed_by_feature_flags?(file_upload) + Feature.enabled?(:dynamic_image_resizing_requester, current_user) && + Feature.enabled?(:dynamic_image_resizing_owner, file_upload.model) + end end diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb index 5552fd663f7..4548595d968 100644 --- a/app/controllers/concerns/snippets_actions.rb +++ b/app/controllers/concerns/snippets_actions.rb @@ -14,8 +14,6 @@ module SnippetsActions skip_before_action :verify_authenticity_token, if: -> { action_name == 'show' && js_request? } - before_action :redirect_if_binary, only: [:edit, :update] - respond_to :html end @@ -134,10 +132,4 @@ module SnippetsActions recaptcha_check_with_fallback(errors.empty?) { render action } end - - def redirect_if_binary - return if Feature.enabled?(:snippets_binary_blob) - - redirect_to gitlab_snippet_path(snippet) if blob&.binary? - end end diff --git a/app/controllers/concerns/wiki_actions.rb b/app/controllers/concerns/wiki_actions.rb index 5b953fe37d6..5a5b634da40 100644 --- a/app/controllers/concerns/wiki_actions.rb +++ b/app/controllers/concerns/wiki_actions.rb @@ -93,9 +93,10 @@ module WikiActions def update return render('shared/wikis/empty') unless can?(current_user, :create_wiki, container) - @page = WikiPages::UpdateService.new(container: container, current_user: current_user, params: wiki_params).execute(page) + response = WikiPages::UpdateService.new(container: container, current_user: current_user, params: wiki_params).execute(page) + @page = response.payload[:page] - if page.valid? + if response.success? redirect_to( wiki_page_path(wiki, page), notice: _('Wiki was successfully updated.') @@ -103,7 +104,7 @@ module WikiActions else render 'shared/wikis/edit' end - rescue WikiPage::PageChangedError, WikiPage::PageRenameError, Gitlab::Git::Wiki::OperationError => e + rescue WikiPage::PageChangedError, WikiPage::PageRenameError => e @error = e render 'shared/wikis/edit' end @@ -120,13 +121,8 @@ module WikiActions notice: _('Wiki was successfully updated.') ) else - flash[:alert] = response.message render 'shared/wikis/edit' end - rescue Gitlab::Git::Wiki::OperationError => e - @page = build_page(wiki_params) - @error = e - render 'shared/wikis/edit' end # rubocop:enable Gitlab/ModuleWithInstanceVariables @@ -162,14 +158,18 @@ module WikiActions # rubocop:disable Gitlab/ModuleWithInstanceVariables def destroy - WikiPages::DestroyService.new(container: container, current_user: current_user).execute(page) + return render_404 unless page - redirect_to wiki_path(wiki), - status: :found, - notice: _("Page was successfully deleted") - rescue Gitlab::Git::Wiki::OperationError => e - @error = e - render 'shared/wikis/edit' + response = WikiPages::DestroyService.new(container: container, current_user: current_user).execute(page) + + if response.success? + redirect_to wiki_path(wiki), + status: :found, + notice: _("Page was successfully deleted") + else + @error = response + render 'shared/wikis/edit' + end end # rubocop:enable Gitlab/ModuleWithInstanceVariables diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index 91704f030cd..2bd6fd85381 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -63,10 +63,11 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController end def load_projects(finder_params) - @total_user_projects_count = ProjectsFinder.new(params: { non_public: true }, current_user: current_user).execute - @total_starred_projects_count = ProjectsFinder.new(params: { starred: true }, current_user: current_user).execute + @total_user_projects_count = ProjectsFinder.new(params: { non_public: true, without_deleted: true }, current_user: current_user).execute + @total_starred_projects_count = ProjectsFinder.new(params: { starred: true, without_deleted: true }, current_user: current_user).execute finder_params[:use_cte] = true if use_cte_for_finder? + finder_params[:without_deleted] = true projects = ProjectsFinder.new(params: finder_params, current_user: current_user).execute @@ -89,7 +90,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController def load_events projects = ProjectsFinder - .new(params: params.merge(non_public: true), current_user: current_user) + .new(params: params.merge(non_public: true, without_deleted: true), current_user: current_user) .execute @events = EventCollection diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index a1348e4d858..123102bf793 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -81,7 +81,7 @@ class GraphqlController < ApplicationController end def context - @context ||= { current_user: current_user } + @context ||= { current_user: current_user, is_sessionless_user: !!sessionless_user? } end def build_variables(variable_info) @@ -107,4 +107,12 @@ class GraphqlController < ApplicationController render json: error, status: status end + + def append_info_to_payload(payload) + super + + # Merging to :metadata will ensure these are logged as top level keys + payload[:metadata] ||= {} + payload[:metadata].merge!(graphql: { operation_name: params[:operationName] }) + end end diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb index 23d4f0d24e9..ea7e83a2caf 100644 --- a/app/controllers/groups/boards_controller.rb +++ b/app/controllers/groups/boards_controller.rb @@ -8,6 +8,7 @@ class Groups::BoardsController < Groups::ApplicationController before_action :assign_endpoint_vars before_action do push_frontend_feature_flag(:multi_select_board, default_enabled: true) + push_frontend_feature_flag(:graphql_board_lists, group, default_enabled: false) push_frontend_feature_flag(:boards_with_swimlanes, group, default_enabled: false) end diff --git a/app/controllers/groups/settings/integrations_controller.rb b/app/controllers/groups/settings/integrations_controller.rb index adfbe9bfa17..e8551a7f270 100644 --- a/app/controllers/groups/settings/integrations_controller.rb +++ b/app/controllers/groups/settings/integrations_controller.rb @@ -8,15 +8,19 @@ module Groups before_action :authorize_admin_group! def index - @integrations = [] + @integrations = Service.find_or_initialize_all(Service.for_group(group)).sort_by(&:title) + end + + def edit + @default_integration = Service.default_integration(integration.type, group) + + super end private - # TODO: Make this compatible with group-level integration - # https://gitlab.com/groups/gitlab-org/-/epics/2543 def find_or_initialize_integration(name) - Project.first.find_or_initialize_service(name) + Service.find_or_initialize_integration(name, group_id: group.id) end def integrations_enabled? diff --git a/app/controllers/instance_statistics/application_controller.rb b/app/controllers/instance_statistics/application_controller.rb deleted file mode 100644 index a273dde105c..00000000000 --- a/app/controllers/instance_statistics/application_controller.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -class InstanceStatistics::ApplicationController < ApplicationController - before_action :authorize_read_instance_statistics! - layout 'instance_statistics' - - def authorize_read_instance_statistics! - render_404 unless can?(current_user, :read_instance_statistics) - end -end diff --git a/app/controllers/instance_statistics/dev_ops_score_controller.rb b/app/controllers/instance_statistics/dev_ops_score_controller.rb deleted file mode 100644 index b98a1bf7f99..00000000000 --- a/app/controllers/instance_statistics/dev_ops_score_controller.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -class InstanceStatistics::DevOpsScoreController < InstanceStatistics::ApplicationController - include Analytics::UniqueVisitsHelper - - track_unique_visits :index, target_id: 'i_analytics_dev_ops_score' - - # rubocop: disable CodeReuse/ActiveRecord - def index - @metric = DevOpsScore::Metric.order(:created_at).last&.present - end - # rubocop: enable CodeReuse/ActiveRecord -end diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 29cafbbbdb6..aa9c7d01ba3 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -12,11 +12,13 @@ class InvitesController < ApplicationController respond_to :html def show + track_experiment('opened') accept if skip_invitation_prompt? end def accept if member.accept_invite!(current_user) + track_experiment('accepted') redirect_to invite_details[:path], notice: _("You have been granted %{member_human_access} access to %{title} %{name}.") % { member_human_access: member.human_access, title: invite_details[:title], name: invite_details[:name] } else @@ -74,8 +76,14 @@ class InvitesController < ApplicationController notice << "or create an account" if Gitlab::CurrentSettings.allow_signup? notice = notice.join(' ') + "." + # this is temporary finder instead of using member method due to render_404 possibility + # will be resolved via https://gitlab.com/gitlab-org/gitlab/-/issues/245325 + initial_member = Member.find_by_invite_token(params[:id]) + redirect_params = initial_member ? { invite_email: initial_member.invite_email } : {} + store_location_for :user, request.fullpath - redirect_to new_user_session_path(invite_email: member.invite_email), notice: notice + + redirect_to new_user_session_path(redirect_params), notice: notice end def invite_details @@ -96,4 +104,17 @@ class InvitesController < ApplicationController } end end + + def track_experiment(action) + return unless params[:new_user_invite] + + property = params[:new_user_invite] == 'experiment' ? 'experiment_group' : 'control_group' + + Gitlab::Tracking.event( + Gitlab::Experimentation::EXPERIMENTS[:invite_email][:tracking_category], + action, + property: property, + label: Digest::MD5.hexdigest(member.to_global_id.to_s) + ) + end end diff --git a/app/controllers/jira_connect/app_descriptor_controller.rb b/app/controllers/jira_connect/app_descriptor_controller.rb new file mode 100644 index 00000000000..bf53c61601b --- /dev/null +++ b/app/controllers/jira_connect/app_descriptor_controller.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +# This returns an app descriptor for use with Jira in development mode +# For the Atlassian Marketplace, a static copy of this JSON is uploaded to the marketplace +# https://developer.atlassian.com/cloud/jira/platform/app-descriptor/ + +class JiraConnect::AppDescriptorController < JiraConnect::ApplicationController + skip_before_action :verify_atlassian_jwt! + + def show + render json: { + name: Atlassian::JiraConnect.app_name, + description: 'Integrate commits, branches and merge requests from GitLab into Jira', + key: Atlassian::JiraConnect.app_key, + baseUrl: jira_connect_base_url(protocol: 'https'), + lifecycle: { + installed: relative_to_base_path(jira_connect_events_installed_path), + uninstalled: relative_to_base_path(jira_connect_events_uninstalled_path) + }, + vendor: { + name: 'GitLab', + url: 'https://gitlab.com' + }, + links: { + documentation: help_page_url('integration/jira_development_panel', anchor: 'gitlabcom-1') + }, + authentication: { + type: 'jwt' + }, + scopes: %w(READ WRITE DELETE), + apiVersion: 1, + modules: { + jiraDevelopmentTool: { + key: 'gitlab-development-tool', + application: { + value: 'GitLab' + }, + name: { + value: 'GitLab' + }, + url: 'https://gitlab.com', + logoUrl: view_context.image_url('gitlab_logo.png'), + capabilities: %w(branch commit pull_request) + }, + postInstallPage: { + key: 'gitlab-configuration', + name: { + value: 'GitLab Configuration' + }, + url: relative_to_base_path(jira_connect_subscriptions_path) + } + }, + apiMigrations: { + gdpr: true + } + } + end + + private + + def relative_to_base_path(full_path) + full_path.sub(/^#{jira_connect_base_path}/, '') + end +end diff --git a/app/controllers/jira_connect/application_controller.rb b/app/controllers/jira_connect/application_controller.rb new file mode 100644 index 00000000000..a84f25998a6 --- /dev/null +++ b/app/controllers/jira_connect/application_controller.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +class JiraConnect::ApplicationController < ApplicationController + include Gitlab::Utils::StrongMemoize + + skip_before_action :authenticate_user! + skip_before_action :verify_authenticity_token + before_action :verify_atlassian_jwt! + + attr_reader :current_jira_installation + + private + + def verify_atlassian_jwt! + return render_403 unless atlassian_jwt_valid? + + @current_jira_installation = installation_from_jwt + end + + def verify_qsh_claim! + payload, _ = decode_auth_token! + + # Make sure `qsh` claim matches the current request + render_403 unless payload['qsh'] == Atlassian::Jwt.create_query_string_hash(request.url, request.method, jira_connect_base_url) + rescue + render_403 + end + + def atlassian_jwt_valid? + return false unless installation_from_jwt + + # Verify JWT signature with our stored `shared_secret` + decode_auth_token! + rescue JWT::DecodeError + false + end + + def installation_from_jwt + return unless auth_token + + strong_memoize(:installation_from_jwt) do + # Decode without verification to get `client_key` in `iss` + payload, _ = Atlassian::Jwt.decode(auth_token, nil, false) + JiraConnectInstallation.find_by_client_key(payload['iss']) + end + end + + def decode_auth_token! + Atlassian::Jwt.decode(auth_token, installation_from_jwt.shared_secret) + end + + def auth_token + strong_memoize(:auth_token) do + params[:jwt] || request.headers['Authorization']&.split(' ', 2)&.last + end + end +end diff --git a/app/controllers/jira_connect/events_controller.rb b/app/controllers/jira_connect/events_controller.rb new file mode 100644 index 00000000000..8f79c82d847 --- /dev/null +++ b/app/controllers/jira_connect/events_controller.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class JiraConnect::EventsController < JiraConnect::ApplicationController + skip_before_action :verify_atlassian_jwt!, only: :installed + before_action :verify_qsh_claim!, only: :uninstalled + + def installed + installation = JiraConnectInstallation.new(install_params) + + if installation.save + head :ok + else + head :unprocessable_entity + end + end + + def uninstalled + if current_jira_installation.destroy + head :ok + else + head :unprocessable_entity + end + end + + private + + def install_params + params.permit(:clientKey, :sharedSecret, :baseUrl).transform_keys(&:underscore) + end +end diff --git a/app/controllers/jira_connect/subscriptions_controller.rb b/app/controllers/jira_connect/subscriptions_controller.rb new file mode 100644 index 00000000000..3ff12f29f10 --- /dev/null +++ b/app/controllers/jira_connect/subscriptions_controller.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +class JiraConnect::SubscriptionsController < JiraConnect::ApplicationController + layout 'jira_connect' + + content_security_policy do |p| + next if p.directives.blank? + + # rubocop: disable Lint/PercentStringArray + script_src_values = Array.wrap(p.directives['script-src']) | %w('self' https://connect-cdn.atl-paas.net https://unpkg.com/jquery@3.3.1/) + style_src_values = Array.wrap(p.directives['style-src']) | %w('self' 'unsafe-inline' https://unpkg.com/@atlaskit/) + # rubocop: enable Lint/PercentStringArray + + p.frame_ancestors :self, 'https://*.atlassian.net' + p.script_src(*script_src_values) + p.style_src(*style_src_values) + end + + before_action :allow_rendering_in_iframe, only: :index + before_action :verify_qsh_claim!, only: :index + before_action :authenticate_user!, only: :create + + def index + @subscriptions = current_jira_installation.subscriptions.preload_namespace_route + end + + def create + result = create_service.execute + + if result[:status] == :success + render json: { success: true } + else + render json: { error: result[:message] }, status: result[:http_status] + end + end + + def destroy + subscription = current_jira_installation.subscriptions.find(params[:id]) + + if subscription.destroy + render json: { success: true } + else + render json: { error: subscription.errors.full_messages.join(', ') }, status: :unprocessable_entity + end + end + + private + + def create_service + JiraConnectSubscriptions::CreateService.new(current_jira_installation, current_user, namespace_path: params['namespace_path']) + end + + def allow_rendering_in_iframe + response.headers.delete('X-Frame-Options') + end +end diff --git a/app/controllers/oauth/jira/authorizations_controller.rb b/app/controllers/oauth/jira/authorizations_controller.rb new file mode 100644 index 00000000000..f552b0dc10c --- /dev/null +++ b/app/controllers/oauth/jira/authorizations_controller.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +# This controller's role is to mimic and rewire the GitLab OAuth +# flow routes for Jira DVCS integration. +# See https://gitlab.com/gitlab-org/gitlab/issues/2381 +# +class Oauth::Jira::AuthorizationsController < ApplicationController + skip_before_action :authenticate_user! + skip_before_action :verify_authenticity_token + + # 1. Rewire Jira OAuth initial request to our stablished OAuth authorization URL. + def new + session[:redirect_uri] = params['redirect_uri'] + + redirect_to oauth_authorization_path(client_id: params['client_id'], + response_type: 'code', + scope: params['scope'], + redirect_uri: oauth_jira_callback_url) + end + + # 2. Handle the callback call as we were a Github Enterprise instance client. + def callback + # Handling URI query params concatenation. + redirect_uri = URI.parse(session['redirect_uri']) + new_query = URI.decode_www_form(String(redirect_uri.query)) << ['code', params[:code]] + redirect_uri.query = URI.encode_www_form(new_query) + + redirect_to redirect_uri.to_s + end + + # 3. Rewire and adjust access_token request accordingly. + def access_token + # We have to modify request.parameters because Doorkeeper::Server reads params from there + request.parameters[:redirect_uri] = oauth_jira_callback_url + + strategy = Doorkeeper::Server.new(self).token_request('authorization_code') + response = strategy.authorize + + if response.status == :ok + access_token, scope, token_type = response.body.values_at('access_token', 'scope', 'token_type') + + render body: "access_token=#{access_token}&scope=#{scope}&token_type=#{token_type}" + else + render status: response.status, body: response.body + end + rescue Doorkeeper::Errors::DoorkeeperError => e + render status: :unauthorized, body: e.type + end +end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index a558b01f0c6..b798d6680bc 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -83,6 +83,10 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController end end + def atlassian_oauth2 + omniauth_flow(Gitlab::Auth::Atlassian) + end + private def log_failed_login(user, provider) diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index af860297358..c27226c3f3f 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -31,8 +31,10 @@ class PasswordsController < Devise::PasswordsController def update super do |resource| - if resource.valid? && resource.password_automatically_set? - resource.update_attribute(:password_automatically_set, false) + if resource.valid? + resource.password_automatically_set = false + resource.password_expires_at = nil + resource.save(validate: false) if resource.changed? end end end diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb index 95e055a44db..b19285e98bb 100644 --- a/app/controllers/profiles/accounts_controller.rb +++ b/app/controllers/profiles/accounts_controller.rb @@ -7,10 +7,9 @@ class Profiles::AccountsController < Profiles::ApplicationController render(locals: show_view_variables) end - # rubocop: disable CodeReuse/ActiveRecord def unlink provider = params[:provider] - identity = current_user.identities.find_by(provider: provider) + identity = find_identity(provider) return render_404 unless identity @@ -22,13 +21,18 @@ class Profiles::AccountsController < Profiles::ApplicationController redirect_to profile_account_path end - # rubocop: enable CodeReuse/ActiveRecord private def show_view_variables {} end + + def find_identity(provider) + return current_user.atlassian_identity if provider == 'atlassian_oauth2' + + current_user.identities.find_by(provider: provider) # rubocop: disable CodeReuse/ActiveRecord + end end Profiles::AccountsController.prepend_if_ee('EE::Profiles::AccountsController') diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb index 99e1b9027fa..965493955ac 100644 --- a/app/controllers/profiles/keys_controller.rb +++ b/app/controllers/profiles/keys_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Profiles::KeysController < Profiles::ApplicationController + skip_before_action :authenticate_user!, only: [:get_keys] + def index @keys = current_user.keys.order_id_desc @key = Key.new @@ -31,6 +33,25 @@ class Profiles::KeysController < Profiles::ApplicationController end end + # Get all keys of a user(params[:username]) in a text format + # Helpful for sysadmins to put in respective servers + def get_keys + if params[:username].present? + begin + user = UserFinder.new(params[:username]).find_by_username + if user.present? + render plain: user.all_ssh_keys.join("\n") + else + render_404 + end + rescue => e + render html: e.message + end + else + render_404 + end + end + private def key_params diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb index 064b2a2cc12..bc51830c119 100644 --- a/app/controllers/profiles/notifications_controller.rb +++ b/app/controllers/profiles/notifications_controller.rb @@ -4,12 +4,9 @@ class Profiles::NotificationsController < Profiles::ApplicationController # rubocop: disable CodeReuse/ActiveRecord def show @user = current_user - @group_notifications = current_user.notification_settings.preload_source_route.for_groups.order(:id) - @group_notifications += GroupsFinder.new( - current_user, - all_available: false, - exclude_group_ids: @group_notifications.select(:source_id) - ).execute.map { |group| current_user.notification_settings_for(group, inherit: true) } + @user_groups = user_groups + @group_notifications = UserGroupNotificationSettingsFinder.new(current_user, user_groups).execute + @project_notifications = current_user.notification_settings.for_projects.order(:id) .preload_source_route .select { |notification| current_user.can?(:read_project, notification.source) } @@ -32,4 +29,10 @@ class Profiles::NotificationsController < Profiles::ApplicationController def user_params params.require(:user).permit(:notification_email, :notified_of_own_activity) end + + private + + def user_groups + GroupsFinder.new(current_user, all_available: false).execute.order_name_asc.page(params[:page]) + end end diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb index 8653fe3b6ed..ea4d3e861be 100644 --- a/app/controllers/profiles/preferences_controller.rb +++ b/app/controllers/profiles/preferences_controller.rb @@ -51,6 +51,7 @@ class Profiles::PreferencesController < Profiles::ApplicationController :view_diffs_file_by_file, :tab_width, :sourcegraph_enabled, + :gitpod_enabled, :render_whitespace_in_code ] end diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index 50fbf8146e5..5de6d84fdd9 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -2,6 +2,9 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController skip_before_action :check_two_factor_requirement + before_action do + push_frontend_feature_flag(:webauthn) + end def show unless current_user.two_factor_enabled? @@ -33,7 +36,12 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController @qr_code = build_qr_code @account_string = account_string - setup_u2f_registration + + if Feature.enabled?(:webauthn) + setup_webauthn_registration + else + setup_u2f_registration + end end def create @@ -48,7 +56,13 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController else @error = _('Invalid pin code') @qr_code = build_qr_code - setup_u2f_registration + + if Feature.enabled?(:webauthn) + setup_webauthn_registration + else + setup_u2f_registration + end + render 'show' end end @@ -56,7 +70,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController # A U2F (universal 2nd factor) device's information is stored after successful # registration, which is then used while 2FA authentication is taking place. def create_u2f - @u2f_registration = U2fRegistration.register(current_user, u2f_app_id, u2f_registration_params, session[:challenges]) + @u2f_registration = U2fRegistration.register(current_user, u2f_app_id, device_registration_params, session[:challenges]) if @u2f_registration.persisted? session.delete(:challenges) @@ -68,6 +82,21 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController end end + def create_webauthn + @webauthn_registration = Webauthn::RegisterService.new(current_user, device_registration_params, session[:challenge]).execute + if @webauthn_registration.persisted? + session.delete(:challenge) + + redirect_to profile_two_factor_auth_path, notice: s_("Your WebAuthn device was registered!") + else + @qr_code = build_qr_code + + setup_webauthn_registration + + render :show + end + end + def codes Users::UpdateService.new(current_user, user: current_user).execute! do |user| @codes = user.generate_otp_backup_codes! @@ -75,9 +104,13 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController end def destroy - current_user.disable_two_factor! + result = TwoFactor::DestroyService.new(current_user, user: current_user).execute - redirect_to profile_account_path, status: :found + if result[:status] == :success + redirect_to profile_account_path, status: :found, notice: s_('Two-factor authentication has been disabled successfully!') + else + redirect_to profile_account_path, status: :found, alert: result[:message] + end end def skip @@ -108,11 +141,11 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController # Actual communication is performed using a Javascript API def setup_u2f_registration @u2f_registration ||= U2fRegistration.new - @u2f_registrations = current_user.u2f_registrations + @registrations = u2f_registrations u2f = U2F::U2F.new(u2f_app_id) registration_requests = u2f.registration_requests - sign_requests = u2f.authentication_requests(@u2f_registrations.map(&:key_handle)) + sign_requests = u2f.authentication_requests(current_user.u2f_registrations.map(&:key_handle)) session[:challenges] = registration_requests.map(&:challenge) gon.push(u2f: { challenges: session[:challenges], app_id: u2f_app_id, @@ -120,8 +153,53 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController sign_requests: sign_requests }) end - def u2f_registration_params - params.require(:u2f_registration).permit(:device_response, :name) + def device_registration_params + params.require(:device_registration).permit(:device_response, :name) + end + + def setup_webauthn_registration + @registrations = webauthn_registrations + @webauthn_registration ||= WebauthnRegistration.new + + unless current_user.webauthn_xid + current_user.user_detail.update!(webauthn_xid: WebAuthn.generate_user_id) + end + + options = webauthn_options + session[:challenge] = options.challenge + + gon.push(webauthn: { options: options, app_id: u2f_app_id }) + end + + # Adds delete path to u2f registrations + # to reduce logic in view template + def u2f_registrations + current_user.u2f_registrations.map do |u2f_registration| + { + name: u2f_registration.name, + created_at: u2f_registration.created_at, + delete_path: profile_u2f_registration_path(u2f_registration) + } + end + end + + def webauthn_registrations + current_user.webauthn_registrations.map do |webauthn_registration| + { + name: webauthn_registration.name, + created_at: webauthn_registration.created_at, + delete_path: profile_webauthn_registration_path(webauthn_registration) + } + end + end + + def webauthn_options + WebAuthn::Credential.options_for_create( + user: { id: current_user.webauthn_xid, name: current_user.username }, + exclude: current_user.webauthn_registrations.map { |c| c.credential_xid }, + authenticator_selection: { user_verification: 'discouraged' }, + rp: { name: 'GitLab' } + ) end def groups_notification(groups) @@ -129,6 +207,6 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController leave_group_links = groups.map { |group| view_context.link_to (s_("leave %{group_name}") % { group_name: group.full_name }), leave_group_members_path(group), remote: false, method: :delete}.to_sentence s_(%{The group settings for %{group_links} require you to enable Two-Factor Authentication for your account. You can %{leave_group_links}.}) - .html_safe % { group_links: group_links.html_safe, leave_group_links: leave_group_links.html_safe } + .html_safe % { group_links: group_links.html_safe, leave_group_links: leave_group_links.html_safe } end end diff --git a/app/controllers/profiles/webauthn_registrations_controller.rb b/app/controllers/profiles/webauthn_registrations_controller.rb new file mode 100644 index 00000000000..81b1dd6f710 --- /dev/null +++ b/app/controllers/profiles/webauthn_registrations_controller.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class Profiles::WebauthnRegistrationsController < Profiles::ApplicationController + def destroy + webauthn_registration = current_user.webauthn_registrations.find(params[:id]) + webauthn_registration.destroy + + redirect_to profile_two_factor_auth_path, status: :found, notice: _("Successfully deleted WebAuthn device.") + end +end diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index c9f46eb72c5..248d5755d92 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -6,6 +6,9 @@ class ProfilesController < Profiles::ApplicationController before_action :user before_action :authorize_change_username!, only: :update_username skip_before_action :require_email, only: [:show, :update] + before_action do + push_frontend_feature_flag(:webauthn) + end def show end @@ -101,6 +104,7 @@ class ProfilesController < Profiles::ApplicationController :bio, :email, :role, + :gitpod_enabled, :hide_no_password, :hide_no_ssh_key, :hide_project_limit, diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 518d414be1b..ca2692438e8 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -58,9 +58,9 @@ class Projects::ApplicationController < ApplicationController def method_missing(method_sym, *arguments, &block) case method_sym.to_s when /\Aauthorize_(.*)!\z/ - authorize_action!($1.to_sym) + authorize_action!(Regexp.last_match(1).to_sym) when /\Acheck_(.*)_available!\z/ - check_project_feature_available!($1.to_sym) + check_project_feature_available!(Regexp.last_match(1).to_sym) else super end diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb index 59a7dff680c..eb47fec2b7e 100644 --- a/app/controllers/projects/badges_controller.rb +++ b/app/controllers/projects/badges_controller.rb @@ -9,6 +9,7 @@ class Projects::BadgesController < Projects::ApplicationController def pipeline pipeline_status = Gitlab::Badge::Pipeline::Status .new(project, params[:ref], opts: { + ignore_skipped: params[:ignore_skipped], key_text: params[:key_text], key_width: params[:key_width] }) diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index d969e7bf771..1568d9966dd 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -10,6 +10,8 @@ class Projects::BlobController < Projects::ApplicationController include RedirectsForMissingPathOnTree include SourcegraphDecorator include DiffHelper + include RedisTracking + extend ::Gitlab::Utils::Override prepend_before_action :authenticate_user!, only: [:edit] @@ -33,8 +35,11 @@ class Projects::BlobController < Projects::ApplicationController before_action only: :show do push_frontend_feature_flag(:code_navigation, @project, default_enabled: true) push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline) + push_frontend_feature_flag(:gitlab_ci_yml_preview, @project, default_enabled: false) end + track_redis_hll_event :create, :update, name: 'g_edit_by_sfe', feature: :track_editor_edit_actions, feature_default_enabled: true + def new commit unless @repository.empty? end @@ -99,8 +104,6 @@ class Projects::BlobController < Projects::ApplicationController end def diff - apply_diff_view_cookie! - @form = Blobs::UnfoldPresenter.new(blob, diff_params) # keep only json rendering when @@ -256,4 +259,9 @@ class Projects::BlobController < Projects::ApplicationController def diff_params params.permit(:full, :since, :to, :bottom, :unfold, :offset, :indent) end + + override :visitor_id + def visitor_id + current_user&.id + end end diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index db05da0bb7f..5ed35094476 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -9,6 +9,7 @@ class Projects::BoardsController < Projects::ApplicationController before_action :assign_endpoint_vars before_action do push_frontend_feature_flag(:multi_select_board, default_enabled: true) + push_frontend_feature_flag(:boards_with_swimlanes, project, default_enabled: false) end private diff --git a/app/controllers/projects/ci/lints_controller.rb b/app/controllers/projects/ci/lints_controller.rb index c13baaea8c6..813a0a9ddd5 100644 --- a/app/controllers/projects/ci/lints_controller.rb +++ b/app/controllers/projects/ci/lints_controller.rb @@ -2,6 +2,9 @@ class Projects::Ci::LintsController < Projects::ApplicationController before_action :authorize_create_pipeline! + before_action do + push_frontend_feature_flag(:ci_lint_vue, project) + end def show end @@ -10,40 +13,15 @@ class Projects::Ci::LintsController < Projects::ApplicationController @content = params[:content] @dry_run = params[:dry_run] - if @dry_run && Gitlab::Ci::Features.lint_creates_pipeline_with_dry_run?(@project) - pipeline = Ci::CreatePipelineService - .new(@project, current_user, ref: @project.default_branch) - .execute(:push, dry_run: true, content: @content) - - @status = pipeline.error_messages.empty? - @stages = pipeline.stages - @errors = pipeline.error_messages.map(&:content) - @warnings = pipeline.warning_messages.map(&:content) - else - result = Gitlab::Ci::YamlProcessor.new_with_validation_errors(@content, yaml_processor_options) + @result = Gitlab::Ci::Lint + .new(project: @project, current_user: current_user) + .validate(@content, dry_run: @dry_run) - @status = result.valid? - @errors = result.errors - @warnings = result.warnings - - if result.valid? - @config_processor = result.config - @stages = @config_processor.stages - @builds = @config_processor.builds - @jobs = @config_processor.jobs + respond_to do |format| + format.html { render :show } + format.json do + render json: ::Ci::Lint::ResultSerializer.new.represent(@result) end end - - render :show - end - - private - - def yaml_processor_options - { - project: @project, - user: current_user, - sha: project.repository.commit.sha - } end end diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb index 41631aea620..c6b6b825bb7 100644 --- a/app/controllers/projects/forks_controller.rb +++ b/app/controllers/projects/forks_controller.rb @@ -43,9 +43,10 @@ class Projects::ForksController < Projects::ApplicationController end format.json do - namespaces = fork_service.valid_fork_targets - [current_user.namespace, project.namespace] + namespaces = load_namespaces_with_associations - [project.namespace] + render json: { - namespaces: ForkNamespaceSerializer.new.represent(namespaces, project: project, current_user: current_user) + namespaces: ForkNamespaceSerializer.new.represent(namespaces, project: project, current_user: current_user, memberships: memberships_hash) } end end @@ -100,6 +101,14 @@ class Projects::ForksController < Projects::ApplicationController def whitelist_query_limiting Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42335') end + + def load_namespaces_with_associations + @load_namespaces_with_associations ||= fork_service.valid_fork_targets(only_groups: true).preload(:route) + end + + def memberships_hash + current_user.members.where(source: load_namespaces_with_associations).index_by(&:source_id) + end end Projects::ForksController.prepend_if_ee('EE::Projects::ForksController') diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb index deba71c9dd3..9bed12fd151 100644 --- a/app/controllers/projects/imports_controller.rb +++ b/app/controllers/projects/imports_controller.rb @@ -5,10 +5,10 @@ class Projects::ImportsController < Projects::ApplicationController include ImportUrlParams # Authorize - before_action :authorize_admin_project!, only: [:new, :create] + before_action :authorize_admin_project!, except: :show before_action :require_namespace_project_creation_permission, only: :show - before_action :require_no_repo, only: [:new, :create] - before_action :redirect_if_progress, only: [:new, :create] + before_action :require_no_repo, except: :show + before_action :redirect_if_progress, except: :show before_action :redirect_if_no_import, only: :show def new diff --git a/app/controllers/projects/issue_links_controller.rb b/app/controllers/projects/issue_links_controller.rb new file mode 100644 index 00000000000..2f7489373ed --- /dev/null +++ b/app/controllers/projects/issue_links_controller.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Projects + class IssueLinksController < Projects::ApplicationController + include IssuableLinks + + before_action :authorize_admin_issue_link!, only: [:create, :destroy] + before_action :authorize_issue_link_association!, only: :destroy + + private + + def authorize_admin_issue_link! + render_403 unless can?(current_user, :admin_issue_link, @project) + end + + def authorize_issue_link_association! + render_404 if link.target != issue && link.source != issue + end + + # rubocop: disable CodeReuse/ActiveRecord + def issue + @issue ||= + IssuesFinder.new(current_user, project_id: @project.id) + .find_by!(iid: params[:issue_id]) + end + # rubocop: enable CodeReuse/ActiveRecord + + def list_service + IssueLinks::ListService.new(issue, current_user) + end + + def create_service + IssueLinks::CreateService.new(issue, current_user, create_params) + end + + def destroy_service + IssueLinks::DestroyService.new(link, current_user) + end + + def link + @link ||= IssueLink.find(params[:id]) + end + + def create_params + params.permit(:link_type, issuable_references: []) + end + end +end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 2200860a184..7f0d23b79ce 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -10,13 +10,8 @@ class Projects::IssuesController < Projects::ApplicationController include SpammableActions include RecordUserLastActivity - def issue_except_actions - %i[index calendar new create bulk_update import_csv export_csv service_desk] - end - - def set_issuables_index_only_actions - %i[index calendar service_desk] - end + ISSUES_EXCEPT_ACTIONS = %i[index calendar new create bulk_update import_csv export_csv service_desk].freeze + SET_ISSUEABLES_INDEX_ONLY_ACTIONS = %i[index calendar service_desk].freeze prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) } prepend_before_action(only: [:calendar]) { authenticate_sessionless_user!(:ics) } @@ -25,9 +20,10 @@ class Projects::IssuesController < Projects::ApplicationController before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update] before_action :check_issues_available! - before_action :issue, unless: ->(c) { c.issue_except_actions.include?(c.action_name.to_sym) } + before_action :issue, unless: ->(c) { ISSUES_EXCEPT_ACTIONS.include?(c.action_name.to_sym) } + after_action :log_issue_show, unless: ->(c) { ISSUES_EXCEPT_ACTIONS.include?(c.action_name.to_sym) } - before_action :set_issuables_index, if: ->(c) { c.set_issuables_index_only_actions.include?(c.action_name.to_sym) } + before_action :set_issuables_index, if: ->(c) { SET_ISSUEABLES_INDEX_ONLY_ACTIONS.include?(c.action_name.to_sym) } # Allow write(create) issue before_action :authorize_create_issue!, only: [:new, :create] @@ -48,6 +44,8 @@ class Projects::IssuesController < Projects::ApplicationController push_frontend_feature_flag(:vue_issuable_sidebar, project.group) push_frontend_feature_flag(:tribute_autocomplete, @project) push_frontend_feature_flag(:vue_issuables_list, project) + push_frontend_feature_flag(:design_management_todo_button, project, default_enabled: true) + push_frontend_feature_flag(:vue_sidebar_labels, @project) end before_action only: :show do @@ -95,7 +93,7 @@ class Projects::IssuesController < Projects::ApplicationController discussion_to_resolve: params[:discussion_to_resolve], confidential: !!Gitlab::Utils.to_boolean(params[:issue][:confidential]) ) - service = Issues::BuildService.new(project, current_user, build_params) + service = ::Issues::BuildService.new(project, current_user, build_params) @issue = @noteable = service.execute @@ -115,7 +113,7 @@ class Projects::IssuesController < Projects::ApplicationController discussion_to_resolve: params[:discussion_to_resolve] ) - service = Issues::CreateService.new(project, current_user, create_params) + service = ::Issues::CreateService.new(project, current_user, create_params) @issue = service.execute if service.discussions_to_resolve.count(&:resolved?) > 0 @@ -143,7 +141,7 @@ class Projects::IssuesController < Projects::ApplicationController new_project = Project.find(params[:move_to_project_id]) return render_404 unless issue.can_move?(current_user, new_project) - @issue = Issues::UpdateService.new(project, current_user, target_project: new_project).execute(issue) + @issue = ::Issues::UpdateService.new(project, current_user, target_project: new_project).execute(issue) end respond_to do |format| @@ -157,7 +155,7 @@ class Projects::IssuesController < Projects::ApplicationController end def reorder - service = Issues::ReorderService.new(project, current_user, reorder_params) + service = ::Issues::ReorderService.new(project, current_user, reorder_params) if service.execute(issue) head :ok @@ -167,7 +165,7 @@ class Projects::IssuesController < Projects::ApplicationController end def related_branches - @related_branches = Issues::RelatedBranchesService + @related_branches = ::Issues::RelatedBranchesService .new(project, current_user) .execute(issue) .map { |branch| branch.merge(link: branch_link(branch)) } @@ -249,6 +247,13 @@ class Projects::IssuesController < Projects::ApplicationController @issue end # rubocop: enable CodeReuse/ActiveRecord + + def log_issue_show + return unless current_user && @issue + + ::Gitlab::Search::RecentIssues.new(user: current_user).log_view(@issue) + end + alias_method :subscribable_resource, :issue alias_method :issuable, :issue alias_method :awardable, :issue @@ -319,7 +324,7 @@ class Projects::IssuesController < Projects::ApplicationController def update_service update_params = issue_params.merge(spammable_params) - Issues::UpdateService.new(project, current_user, update_params) + ::Issues::UpdateService.new(project, current_user, update_params) end def finder_type @@ -340,10 +345,12 @@ class Projects::IssuesController < Projects::ApplicationController def finder_options options = super - return options unless service_desk? + options[:issue_types] = Issue::TYPES_FOR_LIST - options.reject! { |key| key == 'author_username' || key == 'author_id' } - options[:author_id] = User.support_bot + if service_desk? + options.reject! { |key| key == 'author_username' || key == 'author_id' } + options[:author_id] = User.support_bot + end options end diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb index 0bb4e0fb5ee..921da788ad2 100644 --- a/app/controllers/projects/merge_requests/application_controller.rb +++ b/app/controllers/projects/merge_requests/application_controller.rb @@ -43,6 +43,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont :discussion_locked, label_ids: [], assignee_ids: [], + reviewer_ids: [], update_task: [:index, :checked, :line_number, :line_source] ] end diff --git a/app/controllers/projects/merge_requests/content_controller.rb b/app/controllers/projects/merge_requests/content_controller.rb index eec5c1a4355..399745151b1 100644 --- a/app/controllers/projects/merge_requests/content_controller.rb +++ b/app/controllers/projects/merge_requests/content_controller.rb @@ -10,6 +10,9 @@ class Projects::MergeRequests::ContentController < Projects::MergeRequests::Appl before_action :set_polling_header around_action :allow_gitaly_ref_name_caching + FAST_POLLING_INTERVAL = 10.seconds.in_milliseconds + SLOW_POLLING_INTERVAL = 5.minutes.in_milliseconds + def widget respond_to do |format| format.json do @@ -29,7 +32,8 @@ class Projects::MergeRequests::ContentController < Projects::MergeRequests::Appl private def set_polling_header - Gitlab::PollingInterval.set_header(response, interval: 10_000) + interval = merge_request.open? ? FAST_POLLING_INTERVAL : SLOW_POLLING_INTERVAL + Gitlab::PollingInterval.set_header(response, interval: interval) end def serializer(entity) diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb index bceccc7063b..8aacfdce094 100644 --- a/app/controllers/projects/merge_requests/diffs_controller.rb +++ b/app/controllers/projects/merge_requests/diffs_controller.rb @@ -4,7 +4,6 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic include DiffHelper include RendersNotes - before_action :apply_diff_view_cookie! before_action :commit before_action :define_diff_vars before_action :define_diff_comment_vars, except: [:diffs_batch, :diffs_metadata] @@ -21,15 +20,15 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic end def diffs_batch - return render_404 unless Feature.enabled?(:diffs_batch_load, @merge_request.project, default_enabled: true) - diffs = @compare.diffs_in_batch(params[:page], params[:per_page], diff_options: diff_options) positions = @merge_request.note_positions_for_paths(diffs.diff_file_paths, current_user) + environment = @merge_request.environments_for(current_user, latest: true).last diffs.unfold_diff_files(positions.unfoldable) diffs.write_cache options = { + environment: environment, merge_request: @merge_request, diff_view: diff_view, pagination_data: diffs.pagination_data @@ -65,7 +64,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic render: ->(partial, locals) { view_to_html_string(partial, locals) } } - options = additional_attributes.merge(diff_view: diff_view) + options = additional_attributes.merge(diff_view: Feature.enabled?(:unified_diff_lines, @merge_request.project) ? "inline" : diff_view) if @merge_request.project.context_commits_enabled? options[:context_commits] = @merge_request.recent_context_commits diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index e77d2f0f5ee..92785540172 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -10,8 +10,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo include IssuableCollections include RecordUserLastActivity include SourcegraphDecorator + include DiffHelper skip_before_action :merge_request, only: [:index, :bulk_update] + before_action :apply_diff_view_cookie!, only: [:show] before_action :whitelist_query_limiting, only: [:assign_related_issues, :update] before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort] before_action :authorize_read_actual_head_pipeline!, only: [ @@ -25,9 +27,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo before_action :authenticate_user!, only: [:assign_related_issues] before_action :check_user_can_push_to_source_branch!, only: [:rebase] before_action only: [:show] do - push_frontend_feature_flag(:diffs_batch_load, @project, default_enabled: true) push_frontend_feature_flag(:deploy_from_footer, @project, default_enabled: true) - push_frontend_feature_flag(:single_mr_diff_view, @project, default_enabled: true) push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline) push_frontend_feature_flag(:code_navigation, @project, default_enabled: true) push_frontend_feature_flag(:widget_visibility_polling, @project, default_enabled: true) @@ -36,10 +36,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo push_frontend_feature_flag(:multiline_comments, @project, default_enabled: true) push_frontend_feature_flag(:file_identifier_hash) push_frontend_feature_flag(:batch_suggestions, @project, default_enabled: true) - push_frontend_feature_flag(:auto_expand_collapsed_diffs, @project, default_enabled: true) push_frontend_feature_flag(:approvals_commented_by, @project, default_enabled: true) push_frontend_feature_flag(:hide_jump_to_next_unresolved_in_threads, default_enabled: true) push_frontend_feature_flag(:merge_request_widget_graphql, @project) + push_frontend_feature_flag(:unified_diff_lines, @project) + push_frontend_feature_flag(:highlight_current_diff_row, @project) + push_frontend_feature_flag(:default_merge_ref_for_diffs, @project) end before_action do @@ -48,6 +50,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions] + after_action :log_merge_request_show, only: [:show] + feature_category :source_code_management, unless: -> (action) { action.ends_with?("_reports") } feature_category :code_testing, @@ -427,7 +431,13 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42438') end - def reports_response(report_comparison) + def reports_response(report_comparison, pipeline = nil) + if pipeline&.active? + ::Gitlab::PollingInterval.set_header(response, interval: 3000) + + render json: '', status: :no_content && return + end + case report_comparison[:status] when :parsing ::Gitlab::PollingInterval.set_header(response, interval: 3000) @@ -442,6 +452,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo end end + def log_merge_request_show + return unless current_user && @merge_request + + ::Gitlab::Search::RecentMergeRequests.new(user: current_user).log_view(@merge_request) + end + def authorize_read_actual_head_pipeline! return render_404 unless can?(current_user, :read_build, merge_request.actual_head_pipeline) end @@ -450,6 +466,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo params = request.query_parameters params[:view] = cookies[:diff_view] if params[:view].blank? && cookies[:diff_view].present? + if Feature.enabled?(:default_merge_ref_for_diffs, project) + params = params.merge(diff_head: true) + end + diffs_metadata_project_json_merge_request_path(project, merge_request, 'json', params) end end diff --git a/app/controllers/projects/metrics_dashboard_controller.rb b/app/controllers/projects/metrics_dashboard_controller.rb index 51307c3665c..bc0a701b9fd 100644 --- a/app/controllers/projects/metrics_dashboard_controller.rb +++ b/app/controllers/projects/metrics_dashboard_controller.rb @@ -6,6 +6,8 @@ module Projects # app/controllers/projects/environments_controller.rb # See https://gitlab.com/gitlab-org/gitlab/-/issues/226002 for more details. + include Gitlab::Utils::StrongMemoize + before_action :authorize_metrics_dashboard! before_action do push_frontend_feature_flag(:prometheus_computed_alerts) @@ -15,6 +17,15 @@ module Projects def show if environment render 'projects/environments/metrics' + elsif default_environment + redirect_to project_metrics_dashboard_path( + project, + # Reverse merge the query parameters so that a query parameter named dashboard_path doesn't + # override the dashboard_path path parameter. + **permitted_params.to_h.symbolize_keys + .merge(environment: default_environment.id) + .reverse_merge(request.query_parameters.symbolize_keys) + ) else render 'projects/environments/empty_metrics' end @@ -22,13 +33,21 @@ module Projects private + def permitted_params + @permitted_params ||= params.permit(:dashboard_path, :environment, :page) + end + def environment - @environment ||= - if params[:environment] - project.environments.find(params[:environment]) - else - project.default_environment - end + strong_memoize(:environment) do + env = permitted_params[:environment] + project.environments.find(env) if env + end + end + + def default_environment + strong_memoize(:default_environment) do + project.default_environment + end end end end diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb index 2a8bc823931..9ad6bf4095a 100644 --- a/app/controllers/projects/pages_controller.rb +++ b/app/controllers/projects/pages_controller.rb @@ -21,7 +21,7 @@ class Projects::PagesController < Projects::ApplicationController format.html do redirect_to project_pages_path(@project), status: :found, - notice: 'Pages were removed' + notice: 'Pages were scheduled for removal' end end end diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index bfe23eb1035..c1734d2cd8a 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -63,10 +63,27 @@ class Projects::PipelinesController < Projects::ApplicationController .new(project, current_user, create_params) .execute(:web, ignore_skip_ci: true, save_on_errors: false) - if @pipeline.created_successfully? - redirect_to project_pipeline_path(project, @pipeline) - else - render 'new', status: :bad_request + respond_to do |format| + format.html do + if @pipeline.created_successfully? + redirect_to project_pipeline_path(project, @pipeline) + else + render 'new', status: :bad_request + end + end + format.json do + if @pipeline.created_successfully? + render json: PipelineSerializer + .new(project: project, current_user: current_user) + .represent(@pipeline), + status: :created + else + render json: { errors: @pipeline.error_messages.map(&:content), + warnings: @pipeline.warning_messages(limit: ::Gitlab::Ci::Warnings::MAX_LIMIT).map(&:content), + total_warnings: @pipeline.warning_messages.length }, + status: :bad_request + end + end end end @@ -247,7 +264,7 @@ class Projects::PipelinesController < Projects::ApplicationController end def latest_pipeline - @project.latest_pipeline_for_ref(params['ref']) + @project.latest_pipeline(params['ref']) &.present(current_user: current_user) end diff --git a/app/controllers/projects/product_analytics_controller.rb b/app/controllers/projects/product_analytics_controller.rb index badd7671dcf..c019dc191d6 100644 --- a/app/controllers/projects/product_analytics_controller.rb +++ b/app/controllers/projects/product_analytics_controller.rb @@ -27,6 +27,10 @@ class Projects::ProductAnalyticsController < Projects::ApplicationController .new(project, { graph: graph, timerange: @timerange }) .execute end + + @activity_graph = ProductAnalytics::BuildActivityGraphService + .new(project, { timerange: @timerange }) + .execute end private diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb index c48d573edbf..bd24aae980c 100644 --- a/app/controllers/projects/releases_controller.rb +++ b/app/controllers/projects/releases_controller.rb @@ -11,6 +11,9 @@ class Projects::ReleasesController < Projects::ApplicationController push_frontend_feature_flag(:release_show_page, project, default_enabled: true) push_frontend_feature_flag(:release_asset_link_editing, project, default_enabled: true) push_frontend_feature_flag(:release_asset_link_type, project, default_enabled: true) + push_frontend_feature_flag(:graphql_release_data, project, default_enabled: true) + push_frontend_feature_flag(:graphql_milestone_stats, project, default_enabled: true) + push_frontend_feature_flag(:graphql_releases_page, project, default_enabled: false) end before_action :authorize_update_release!, only: %i[edit update] before_action :authorize_create_release!, only: :new diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index ca2a19e67b0..9a69ef991dd 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -20,7 +20,7 @@ class Projects::ServicesController < Projects::ApplicationController layout "project_settings" def edit - @admin_integration = Service.instance_for(service.type) + @default_integration = Service.default_integration(service.type, project) end def update @@ -65,18 +65,20 @@ class Projects::ServicesController < Projects::ApplicationController result = ::Integrations::Test::ProjectService.new(@service, current_user, params[:event]).execute unless result[:success] - return { error: true, message: _('Test failed.'), service_response: result[:message].to_s, test_failed: true } + return { error: true, message: s_('Integrations|Connection failed. Please check your settings.'), service_response: result[:message].to_s, test_failed: true } end {} rescue Gitlab::HTTP::BlockedUrlError => e - { error: true, message: _('Test failed.'), service_response: e.message, test_failed: true } + { error: true, message: s_('Integrations|Connection failed. Please check your settings.'), service_response: e.message, test_failed: true } end def success_message - message = @service.active? ? _('activated') : _('settings saved, but not activated') - - _('%{service_title} %{message}.') % { service_title: @service.title, message: message } + if @service.active? + s_('Integrations|%{integration} settings saved and active.') % { integration: @service.title } + else + s_('Integrations|%{integration} settings saved, but not active.') % { integration: @service.title } + end end def service diff --git a/app/controllers/projects/static_site_editor_controller.rb b/app/controllers/projects/static_site_editor_controller.rb index 9ec50ff8196..e97a8db0b79 100644 --- a/app/controllers/projects/static_site_editor_controller.rb +++ b/app/controllers/projects/static_site_editor_controller.rb @@ -14,13 +14,27 @@ class Projects::StaticSiteEditorController < Projects::ApplicationController end def show - @config = Gitlab::StaticSiteEditor::Config.new(@repository, @ref, @path, params[:return_url]) + service_response = ::StaticSiteEditor::ConfigService.new( + container: project, + current_user: current_user, + params: { + ref: @ref, + path: @path, + return_url: params[:return_url] + } + ).execute + + if service_response.success? + @data = service_response.payload + else + respond_422 + end end private def assign_ref_and_path - @ref, @path = extract_ref(params[:id]) + @ref, @path = extract_ref(params.fetch(:id)) render_404 if @ref.blank? || @path.blank? end diff --git a/app/controllers/projects/todos_controller.rb b/app/controllers/projects/todos_controller.rb index 0b11ee9edc0..33205b93317 100644 --- a/app/controllers/projects/todos_controller.rb +++ b/app/controllers/projects/todos_controller.rb @@ -15,6 +15,9 @@ class Projects::TodosController < Projects::ApplicationController IssuesFinder.new(current_user, project_id: @project.id).find(params[:issuable_id]) when "merge_request" MergeRequestsFinder.new(current_user, project_id: @project.id).find(params[:issuable_id]) + when "design" + issue = IssuesFinder.new(current_user, project_id: @project.id).find(params[:issue_id]) + DesignManagement::DesignsFinder.new(issue, current_user).find(params[:issuable_id]) end end end diff --git a/app/controllers/projects/web_ide_schemas_controller.rb b/app/controllers/projects/web_ide_schemas_controller.rb new file mode 100644 index 00000000000..3d16a6fafd4 --- /dev/null +++ b/app/controllers/projects/web_ide_schemas_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class Projects::WebIdeSchemasController < Projects::ApplicationController + before_action :authenticate_user! + + def show + return respond_422 unless branch_sha + + result = ::Ide::SchemasConfigService.new(project, current_user, sha: branch_sha, filename: params[:filename]).execute + + if result[:status] == :success + render json: result[:schema] + else + render json: result, status: :unprocessable_entity + end + end + + private + + def branch_sha + return unless params[:branch].present? + + project.commit(params[:branch])&.id + end +end diff --git a/app/controllers/projects/web_ide_terminals_controller.rb b/app/controllers/projects/web_ide_terminals_controller.rb index 08ea5c4bca8..76bcaa9e80c 100644 --- a/app/controllers/projects/web_ide_terminals_controller.rb +++ b/app/controllers/projects/web_ide_terminals_controller.rb @@ -11,7 +11,7 @@ class Projects::WebIdeTerminalsController < Projects::ApplicationController def check_config return respond_422 unless branch_sha - result = ::Ci::WebIdeConfigService.new(project, current_user, sha: branch_sha).execute + result = ::Ide::TerminalConfigService.new(project, current_user, sha: branch_sha).execute if result[:status] == :success head :ok diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index ba21fbddde1..848625ff6b5 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -42,10 +42,7 @@ class ProjectsController < Projects::ApplicationController before_action only: [:edit] do push_frontend_feature_flag(:service_desk_custom_address, @project) - end - - before_action only: [:edit] do - push_frontend_feature_flag(:approval_suggestions, @project) + push_frontend_feature_flag(:approval_suggestions, @project, default_enabled: true) end layout :determine_layout @@ -98,6 +95,7 @@ class ProjectsController < Projects::ApplicationController end else flash.now[:alert] = result[:message] + @project.reset format.html { render_edit } end diff --git a/app/controllers/registrations/experience_levels_controller.rb b/app/controllers/registrations/experience_levels_controller.rb index 5bb039bd9ba..38cffff91eb 100644 --- a/app/controllers/registrations/experience_levels_controller.rb +++ b/app/controllers/registrations/experience_levels_controller.rb @@ -12,7 +12,6 @@ module Registrations if current_user.save hide_advanced_issues - flash[:message] = I18n.t('devise.registrations.signed_up') redirect_to group_path(params[:namespace_path]) else render :show diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 2a865aac767..a1252c68403 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -26,18 +26,18 @@ class RegistrationsController < Devise::RegistrationsController end def create - track_experiment_event(:terms_opt_in, 'end') accept_pending_invitations super do |new_user| persist_accepted_terms_if_required(new_user) set_role_required(new_user) + track_terms_experiment(new_user) yield new_user if block_given? end - # Do not show the signed_up notice message when the signup_flow experiment is enabled. - # Instead, show it after successfully updating the role. - flash[:notice] = nil if experiment_enabled?(:signup_flow) + # Devise sets a flash message on `create` for a successful signup, + # which we don't want to show. + flash[:notice] = nil rescue Gitlab::Access::AccessDeniedError redirect_to(new_user_session_path) end @@ -69,7 +69,6 @@ class RegistrationsController < Devise::RegistrationsController return redirect_to new_users_sign_up_group_path if experiment_enabled?(:onboarding_issues) && show_onboarding_issues_experiment? - set_flash_message! :notice, :signed_up redirect_to path_for_signed_in_user(current_user) else render :welcome @@ -89,7 +88,7 @@ class RegistrationsController < Devise::RegistrationsController end def set_role_required(new_user) - new_user.set_role_required! if new_user.persisted? && experiment_enabled?(:signup_flow) + new_user.set_role_required! if new_user.persisted? end def destroy_confirmation_valid? @@ -115,9 +114,7 @@ class RegistrationsController < Devise::RegistrationsController def after_sign_up_path_for(user) Gitlab::AppLogger.info(user_created_message(confirmed: user.confirmed?)) - return users_sign_up_welcome_path if experiment_enabled?(:signup_flow) - - path_for_signed_in_user(user) + users_sign_up_welcome_path end def after_inactive_sign_up_path_for(resource) @@ -154,7 +151,7 @@ class RegistrationsController < Devise::RegistrationsController end def sign_up_params - params.require(:user).permit(:username, :email, :email_confirmation, :name, :first_name, :last_name, :password) + params.require(:user).permit(:username, :email, :name, :first_name, :last_name, :password) end def resource_name @@ -201,6 +198,13 @@ class RegistrationsController < Devise::RegistrationsController true end + def track_terms_experiment(new_user) + return unless new_user.persisted? + + track_experiment_event(:terms_opt_in, 'end') + record_experiment_user(:terms_opt_in) + end + def load_recaptcha Gitlab::Recaptcha.load_configurations! end @@ -208,7 +212,7 @@ class RegistrationsController < Devise::RegistrationsController # Part of an experiment to build a new sign up flow. Will be resolved # with https://gitlab.com/gitlab-org/growth/engineering/issues/64 def choose_layout - if experiment_enabled?(:signup_flow) + if %w(welcome update_registration).include?(action_name) || experiment_enabled?(:signup_flow) 'devise_experimental_separate_sign_up_flow' else 'devise' @@ -216,7 +220,10 @@ class RegistrationsController < Devise::RegistrationsController end def show_onboarding_issues_experiment? - !helpers.in_subscription_flow? && !helpers.in_invitation_flow? && !helpers.in_oauth_flow? + !helpers.in_subscription_flow? && + !helpers.in_invitation_flow? && + !helpers.in_oauth_flow? && + !helpers.in_trial_flow? end end diff --git a/app/controllers/repositories/lfs_api_controller.rb b/app/controllers/repositories/lfs_api_controller.rb index f93038f455e..35751a2578f 100644 --- a/app/controllers/repositories/lfs_api_controller.rb +++ b/app/controllers/repositories/lfs_api_controller.rb @@ -46,7 +46,7 @@ module Repositories end def download_objects! - existing_oids = project.all_lfs_objects_oids(oids: objects_oids) + existing_oids = project.lfs_objects_oids(oids: objects_oids) objects.each do |object| if existing_oids.include?(object[:oid]) diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 56b6a5201e7..dedaf0c903a 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -4,14 +4,18 @@ class SearchController < ApplicationController include ControllerWithCrossProjectAccessCheck include SearchHelper include RendersCommits + include RedisTracking SCOPE_PRELOAD_METHOD = { projects: :with_web_entity_associations, issues: :with_web_entity_associations }.freeze + track_redis_hll_event :show, name: 'i_search_total', feature: :search_track_unique_users, feature_default_enabled: true + around_action :allow_gitaly_ref_name_caching + before_action :block_anonymous_global_searches skip_before_action :authenticate_user! requires_cross_project_access if: -> do search_term_present = params[:search].present? || params[:term].present? @@ -119,10 +123,22 @@ class SearchController < ApplicationController super # Merging to :metadata will ensure these are logged as top level keys - payload[:metadata] || {} + payload[:metadata] ||= {} payload[:metadata]['meta.search.group_id'] = params[:group_id] payload[:metadata]['meta.search.project_id'] = params[:project_id] payload[:metadata]['meta.search.search'] = params[:search] payload[:metadata]['meta.search.scope'] = params[:scope] end + + def block_anonymous_global_searches + return if params[:project_id].present? || params[:group_id].present? + return if current_user + return unless ::Feature.enabled?(:block_anonymous_global_searches) + + store_location_for(:user, request.fullpath) + + redirect_to new_user_session_path, alert: _('You must be logged in to search across all of GitLab') + end end + +SearchController.prepend_if_ee('EE::SearchController') diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 9435b9887e9..318553b5e0a 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -11,6 +11,8 @@ class SessionsController < Devise::SessionsController include Gitlab::Utils::StrongMemoize skip_before_action :check_two_factor_requirement, only: [:destroy] + skip_before_action :check_password_expiration, only: [:destroy] + # replaced with :require_no_authentication_without_flash skip_before_action :require_no_authentication, only: [:new, :create] @@ -27,6 +29,9 @@ class SessionsController < Devise::SessionsController before_action :save_failed_login, if: :action_new_and_failed_login? before_action :load_recaptcha before_action :set_invite_params, only: [:new] + before_action do + push_frontend_feature_flag(:webauthn) + end after_action :log_failed_login, if: :action_new_and_failed_login? after_action :verify_known_sign_in, only: [:create] @@ -157,13 +162,13 @@ class SessionsController < Devise::SessionsController (options = request.env["warden.options"]) && options[:action] == "unauthenticated" end - # storing sessions per IP lets us check if there are associated multiple + # counting sessions per IP lets us check if there are associated multiple # anonymous sessions with one IP and prevent situations when there are # multiple attempts of logging in def store_unauthenticated_sessions return if current_user - Gitlab::AnonymousSession.new(request.remote_ip, session_id: request.session.id).store_session_id_per_ip + Gitlab::AnonymousSession.new(request.remote_ip).count_session_ip end # Handle an "initial setup" state, where there's only one user, it's an admin, @@ -285,13 +290,15 @@ class SessionsController < Devise::SessionsController end def exceeded_anonymous_sessions? - Gitlab::AnonymousSession.new(request.remote_ip).stored_sessions >= MAX_FAILED_LOGIN_ATTEMPTS + Gitlab::AnonymousSession.new(request.remote_ip).session_count >= MAX_FAILED_LOGIN_ATTEMPTS end def authentication_method if user_params[:otp_attempt] "two-factor" - elsif user_params[:device_response] + elsif user_params[:device_response] && Feature.enabled?(:webauthn) + "two-factor-via-webauthn-device" + elsif user_params[:device_response] && !Feature.enabled?(:webauthn) "two-factor-via-u2f-device" else "standard" diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 95ea31fa977..75a861423ed 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -37,12 +37,6 @@ class UsersController < ApplicationController end end - # Get all keys of a user(params[:username]) in a text format - # Helpful for sysadmins to put in respective servers - def ssh_keys - render plain: user.all_ssh_keys.join("\n") - end - def activity respond_to do |format| format.html { render 'show' } |