Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-04-22 09:09:45 +00:00
parent d17c58402b
commit b81fd57f3d
62 changed files with 695 additions and 332 deletions

View File

@ -1546,9 +1546,6 @@ Gitlab/NamespacedClass:
- 'app/finders/context_commits_finder.rb'
- 'app/finders/contributed_projects_finder.rb'
- 'app/finders/deployments_finder.rb'
- 'app/finders/environment_names_finder.rb'
- 'app/finders/environments_finder.rb'
- 'app/finders/environments_by_deployments_finder.rb'
- 'app/finders/events_finder.rb'
- 'app/finders/feature_flags_finder.rb'
- 'app/finders/feature_flags_user_lists_finder.rb'

View File

@ -1 +1 @@
d0d2f4a763c2be059e4e2353f2e5affaff83305d
a7bc2f86b507daaaf9f18e0ea189b062d6149720

View File

@ -197,7 +197,7 @@ class GroupsController < Groups::ApplicationController
def unfoldered_environment_names
respond_to do |format|
format.json do
render json: EnvironmentNamesFinder.new(@group, current_user).execute
render json: Environments::EnvironmentNamesFinder.new(@group, current_user).execute
end
end
end

View File

@ -26,8 +26,7 @@ class InvitesController < ApplicationController
experiment('members/invite_email', actor: member).track(:accepted) if initial_invite_email?
session.delete(:invite_type)
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] }
redirect_to invite_details[:path], notice: helpers.invite_accepted_notice(member)
else
redirect_back_or_default(options: { alert: _("The invitation could not be accepted.") })
end
@ -91,7 +90,7 @@ class InvitesController < ApplicationController
def authenticate_user!
return if current_user
store_location_for :user, request.fullpath
store_location_for(:user, invite_landing_url) if member
if user_sign_up?
redirect_to new_user_registration_path(invite_email: member.invite_email), notice: _("To accept this invitation, create an account or sign in.")
@ -116,6 +115,10 @@ class InvitesController < ApplicationController
end
end
def invite_landing_url
root_url + invite_details[:path]
end
def invite_details
@invite_details ||= case member.source
when Project
@ -123,14 +126,14 @@ class InvitesController < ApplicationController
name: member.source.full_name,
url: project_url(member.source),
title: _("project"),
path: project_path(member.source)
path: activity_project_path(member.source)
}
when Group
{
name: member.source.name,
url: group_url(member.source),
title: _("group"),
path: group_path(member.source)
path: activity_group_path(member.source)
}
end
end

View File

@ -20,7 +20,7 @@ class Projects::BlameController < Projects::ApplicationController
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
environment_params[:find_latest] = true
@environment = EnvironmentsByDeploymentsFinder.new(@project, current_user, environment_params).execute.last
@environment = Environments::EnvironmentsByDeploymentsFinder.new(@project, current_user, environment_params).execute.last
@blame = Gitlab::Blame.new(@blob, @commit)
@blame = Gitlab::View::Presenter::Factory.new(@blame, project: @project, path: @path).fabricate!

View File

@ -214,7 +214,7 @@ class Projects::BlobController < Projects::ApplicationController
def show_html
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
environment_params[:find_latest] = true
@environment = EnvironmentsByDeploymentsFinder.new(@project, current_user, environment_params).execute.last
@environment = Environments::EnvironmentsByDeploymentsFinder.new(@project, current_user, environment_params).execute.last
@last_commit = @repository.last_commit_for_path(@commit.id, @blob.path, literal_pathspec: true)
@code_navigation_path = Gitlab::CodeNavigationPath.new(@project, @blob.commit_id).full_json_path_for(@blob.path)

View File

@ -167,7 +167,7 @@ class Projects::CommitController < Projects::ApplicationController
@diffs = commit.diffs(opts)
@notes_count = commit.notes.count
@environment = EnvironmentsByDeploymentsFinder.new(@project, current_user, commit: @commit, find_latest: true).execute.last
@environment = Environments::EnvironmentsByDeploymentsFinder.new(@project, current_user, commit: @commit, find_latest: true).execute.last
end
# rubocop: disable CodeReuse/ActiveRecord

View File

@ -136,7 +136,7 @@ class Projects::CompareController < Projects::ApplicationController
if compare
environment_params = source_project.repository.branch_exists?(head_ref) ? { ref: head_ref } : { commit: compare.commit }
environment_params[:find_latest] = true
@environment = EnvironmentsByDeploymentsFinder.new(source_project, current_user, environment_params).execute.last
@environment = Environments::EnvironmentsByDeploymentsFinder.new(source_project, current_user, environment_params).execute.last
end
end

View File

@ -58,7 +58,7 @@ module Projects
def environment
strong_memoize(:environment) do
if cluster_params.key?(:environment_name)
EnvironmentsFinder.new(project, current_user, name: cluster_params[:environment_name]).execute.first
::Environments::EnvironmentsFinder.new(project, current_user, name: cluster_params[:environment_name]).execute.first
else
project.default_environment
end

View File

@ -311,7 +311,7 @@ class ProjectsController < Projects::ApplicationController
def unfoldered_environment_names
respond_to do |format|
format.json do
render json: EnvironmentNamesFinder.new(@project, current_user).execute
render json: Environments::EnvironmentNamesFinder.new(@project, current_user).execute
end
end
end

View File

@ -18,7 +18,11 @@ module Registrations
if result[:status] == :success
return redirect_to new_users_sign_up_group_path if show_signup_onboarding?
redirect_to path_for_signed_in_user(current_user)
if current_user.members.count == 1
redirect_to path_for_signed_in_user(current_user), notice: helpers.invite_accepted_notice(current_user.members.last)
else
redirect_to path_for_signed_in_user(current_user)
end
else
render :show
end
@ -48,7 +52,20 @@ module Registrations
def path_for_signed_in_user(user)
return users_almost_there_path if requires_confirmation?(user)
stored_location_for(user) || dashboard_projects_path
stored_location_for(user) || members_activity_path(user)
end
def members_activity_path(user)
return dashboard_projects_path unless user.members.count >= 1
case user.members.last.source
when Project
activity_project_path(user.members.last.source)
when Group
activity_group_path(user.members.last.source)
else
dashboard_projects_path
end
end
def show_signup_onboarding?

View File

@ -1,57 +0,0 @@
# frozen_string_literal: true
# Finder for obtaining the unique environment names of a project or group.
#
# This finder exists so that the merge requests "environments" filter can be
# populated with a unique list of environment names. If we retrieve _just_ the
# environments, duplicates may be present (e.g. multiple projects in a group
# having a "staging" environment).
#
# In addition, this finder only produces unfoldered environments. We do this
# because when searching for environments we want to exclude review app
# environments.
class EnvironmentNamesFinder
attr_reader :project_or_group, :current_user
def initialize(project_or_group, current_user = nil)
@project_or_group = project_or_group
@current_user = current_user
end
def execute
all_environments.unfoldered.order_by_name.pluck_unique_names
end
def all_environments
if project_or_group.is_a?(Namespace)
namespace_environments
else
project_environments
end
end
def namespace_environments
# We assume reporter access is needed for the :read_environment permission
# here. This expection is also present in
# IssuableFinder::Params#min_access_level, which is used for filtering out
# merge requests that don't have the right permissions.
#
# We use this approach so we don't need to load every project into memory
# just to verify if we can see their environments. Doing so would not be
# efficient, and possibly mess up pagination if certain projects are not
# meant to be visible.
projects = project_or_group
.all_projects
.public_or_visible_to_user(current_user, Gitlab::Access::REPORTER)
Environment.for_project(projects)
end
def project_environments
if Ability.allowed?(current_user, :read_environment, project_or_group)
project_or_group.environments
else
Environment.none
end
end
end

View File

@ -0,0 +1,59 @@
# frozen_string_literal: true
module Environments
# Finder for obtaining the unique environment names of a project or group.
#
# This finder exists so that the merge requests "environments" filter can be
# populated with a unique list of environment names. If we retrieve _just_ the
# environments, duplicates may be present (e.g. multiple projects in a group
# having a "staging" environment).
#
# In addition, this finder only produces unfoldered environments. We do this
# because when searching for environments we want to exclude review app
# environments.
class EnvironmentNamesFinder
attr_reader :project_or_group, :current_user
def initialize(project_or_group, current_user = nil)
@project_or_group = project_or_group
@current_user = current_user
end
def execute
all_environments.unfoldered.order_by_name.pluck_unique_names
end
def all_environments
if project_or_group.is_a?(Namespace)
namespace_environments
else
project_environments
end
end
def namespace_environments
# We assume reporter access is needed for the :read_environment permission
# here. This expection is also present in
# IssuableFinder::Params#min_access_level, which is used for filtering out
# merge requests that don't have the right permissions.
#
# We use this approach so we don't need to load every project into memory
# just to verify if we can see their environments. Doing so would not be
# efficient, and possibly mess up pagination if certain projects are not
# meant to be visible.
projects = project_or_group
.all_projects
.public_or_visible_to_user(current_user, Gitlab::Access::REPORTER)
Environment.for_project(projects)
end
def project_environments
if Ability.allowed?(current_user, :read_environment, project_or_group)
project_or_group.environments
else
Environment.none
end
end
end
end

View File

@ -0,0 +1,69 @@
# frozen_string_literal: true
module Environments
class EnvironmentsByDeploymentsFinder
attr_reader :project, :current_user, :params
def initialize(project, current_user, params = {})
@project = project
@current_user = current_user
@params = params
end
# rubocop: disable CodeReuse/ActiveRecord
def execute
deployments = project.deployments
deployments =
if ref
deployments_query = params[:with_tags] ? 'ref = :ref OR tag IS TRUE' : 'ref = :ref'
deployments.where(deployments_query, ref: ref.to_s)
elsif commit
deployments.where(sha: commit.sha)
else
deployments.none
end
environment_ids = deployments
.group(:environment_id)
.select(:environment_id)
environments = project.environments.available
.where(id: environment_ids)
if params[:find_latest]
find_one(environments.order_by_last_deployed_at_desc)
else
find_all(environments.order_by_last_deployed_at.to_a)
end
end
# rubocop: enable CodeReuse/ActiveRecord
private
def find_one(environments)
[environments.find { |environment| valid_environment?(environment) }].compact
end
def find_all(environments)
environments.select { |environment| valid_environment?(environment) }
end
def valid_environment?(environment)
# Go in order of cost: SQL calls are cheaper than Gitaly calls
return false unless Ability.allowed?(current_user, :read_environment, environment)
return false if ref && params[:recently_updated] && !environment.recently_updated_on_branch?(ref)
return false if ref && commit && !environment.includes_commit?(commit)
true
end
def ref
params[:ref].try(:to_s)
end
def commit
params[:commit]
end
end
end

View File

@ -0,0 +1,65 @@
# frozen_string_literal: true
module Environments
class EnvironmentsFinder
attr_reader :project, :current_user, :params
InvalidStatesError = Class.new(StandardError)
def initialize(project, current_user, params = {})
@project = project
@current_user = current_user
@params = params
end
def execute
environments = project.environments
environments = by_name(environments)
environments = by_search(environments)
# Raises InvalidStatesError if params[:states] contains invalid states.
by_states(environments)
end
private
def by_name(environments)
if params[:name].present?
environments.for_name(params[:name])
else
environments
end
end
def by_search(environments)
if params[:search].present?
environments.for_name_like(params[:search], limit: nil)
else
environments
end
end
def by_states(environments)
if params[:states].present?
environments_with_states(environments)
else
environments
end
end
def environments_with_states(environments)
# Convert to array of strings
states = Array(params[:states]).map(&:to_s)
raise InvalidStatesError, _('Requested states are invalid') unless valid_states?(states)
environments.with_states(states)
end
def valid_states?(states)
valid_states = Environment.valid_states.map(&:to_s)
(states - valid_states).empty?
end
end
end

View File

@ -1,67 +0,0 @@
# frozen_string_literal: true
class EnvironmentsByDeploymentsFinder
attr_reader :project, :current_user, :params
def initialize(project, current_user, params = {})
@project = project
@current_user = current_user
@params = params
end
# rubocop: disable CodeReuse/ActiveRecord
def execute
deployments = project.deployments
deployments =
if ref
deployments_query = params[:with_tags] ? 'ref = :ref OR tag IS TRUE' : 'ref = :ref'
deployments.where(deployments_query, ref: ref.to_s)
elsif commit
deployments.where(sha: commit.sha)
else
deployments.none
end
environment_ids = deployments
.group(:environment_id)
.select(:environment_id)
environments = project.environments.available
.where(id: environment_ids)
if params[:find_latest]
find_one(environments.order_by_last_deployed_at_desc)
else
find_all(environments.order_by_last_deployed_at.to_a)
end
end
# rubocop: enable CodeReuse/ActiveRecord
private
def find_one(environments)
[environments.find { |environment| valid_environment?(environment) }].compact
end
def find_all(environments)
environments.select { |environment| valid_environment?(environment) }
end
def valid_environment?(environment)
# Go in order of cost: SQL calls are cheaper than Gitaly calls
return false unless Ability.allowed?(current_user, :read_environment, environment)
return false if ref && params[:recently_updated] && !environment.recently_updated_on_branch?(ref)
return false if ref && commit && !environment.includes_commit?(commit)
true
end
def ref
params[:ref].try(:to_s)
end
def commit
params[:commit]
end
end

View File

@ -1,63 +0,0 @@
# frozen_string_literal: true
class EnvironmentsFinder
attr_reader :project, :current_user, :params
InvalidStatesError = Class.new(StandardError)
def initialize(project, current_user, params = {})
@project = project
@current_user = current_user
@params = params
end
def execute
environments = project.environments
environments = by_name(environments)
environments = by_search(environments)
# Raises InvalidStatesError if params[:states] contains invalid states.
by_states(environments)
end
private
def by_name(environments)
if params[:name].present?
environments.for_name(params[:name])
else
environments
end
end
def by_search(environments)
if params[:search].present?
environments.for_name_like(params[:search], limit: nil)
else
environments
end
end
def by_states(environments)
if params[:states].present?
environments_with_states(environments)
else
environments
end
end
def environments_with_states(environments)
# Convert to array of strings
states = Array(params[:states]).map(&:to_s)
raise InvalidStatesError, _('Requested states are invalid') unless valid_states?(states)
environments.with_states(states)
end
def valid_states?(states)
valid_states = Environment.valid_states.map(&:to_s)
(states - valid_states).empty?
end
end

View File

@ -21,8 +21,8 @@ module Resolvers
def resolve(**args)
return unless project.present?
EnvironmentsFinder.new(project, context[:current_user], args).execute
rescue EnvironmentsFinder::InvalidStatesError => exception
Environments::EnvironmentsFinder.new(project, context[:current_user], args).execute
rescue Environments::EnvironmentsFinder::InvalidStatesError => exception
raise Gitlab::Graphql::Errors::ArgumentError, exception.message
end
end

View File

@ -46,6 +46,17 @@ module InviteMembersHelper
end
end
def invite_accepted_notice(member)
case member.source
when Project
_("You have been granted %{member_human_access} access to project %{name}.") %
{ member_human_access: member.human_access, name: member.source.name }
when Group
_("You have been granted %{member_human_access} access to group %{name}.") %
{ member_human_access: member.human_access, name: member.source.name }
end
end
private
def invite_members_url(form_model)

View File

@ -106,6 +106,14 @@ class BroadcastMessage < ApplicationRecord
return false if current_path.blank? && target_path.present?
return true if current_path.blank? || target_path.blank?
# Ensure paths are consistent across callers.
# This fixes a mismatch between requests in the GUI and CLI
#
# This has to be reassigned due to frozen strings being provided.
unless current_path.start_with?("/")
current_path = "/#{current_path}"
end
escaped = Regexp.escape(target_path).gsub('\\*', '.*')
regexp = Regexp.new "^#{escaped}$", Regexp::IGNORECASE

View File

@ -1367,11 +1367,11 @@ class MergeRequest < ApplicationRecord
def environments_for(current_user, latest: false)
return [] unless diff_head_commit
envs = EnvironmentsByDeploymentsFinder.new(target_project, current_user,
envs = Environments::EnvironmentsByDeploymentsFinder.new(target_project, current_user,
ref: target_branch, commit: diff_head_commit, with_tags: true, find_latest: latest).execute
if source_project
envs.concat EnvironmentsByDeploymentsFinder.new(source_project, current_user,
envs.concat Environments::EnvironmentsByDeploymentsFinder.new(source_project, current_user,
ref: source_branch, commit: diff_head_commit, find_latest: latest).execute
end

View File

@ -241,7 +241,6 @@ class Service < ApplicationRecord
service.project_id = project_id
service.group_id = group_id
service.inherit_from_id = integration.id if integration.instance? || integration.group
service.active = false if service.invalid?
service
end

View File

@ -35,7 +35,7 @@ module Ci
private
def environments
@environments ||= EnvironmentsByDeploymentsFinder
@environments ||= Environments::EnvironmentsByDeploymentsFinder
.new(project, current_user, ref: @ref, recently_updated: true)
.execute
end

View File

@ -17,11 +17,16 @@ module Lfs
success
rescue => err
Gitlab::ErrorTracking.log_exception(err, extra_context)
error(err.message)
end
private
def extra_context
{ project_id: project.id, user_id: current_user&.id }.compact
end
# Currently we only set repository_type for design repository objects, so
# push mirroring must send objects with a `nil` repository type - but if the
# wiki repository uses LFS, its objects will also be sent. This will be

View File

@ -84,7 +84,7 @@ module Prometheus
def environment
strong_memoize(:environment) do
EnvironmentsFinder.new(project, nil, name: 'production').execute.first ||
Environments::EnvironmentsFinder.new(project, nil, name: 'production').execute.first ||
project.environments.first
end
end

View File

@ -34,7 +34,6 @@ class BuildFinishedWorker # rubocop:disable Scalability/IdempotentWorker
# We execute these async as these are independent operations.
BuildHooksWorker.perform_async(build.id)
ExpirePipelineCacheWorker.perform_async(build.pipeline_id)
ChatNotificationWorker.perform_async(build.id) if build.pipeline.chat?
if build.failed?

View File

@ -0,0 +1,5 @@
---
title: Redirect to activity page when accepting invitation
merge_request: 56695
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Remove unnecessary validation avoiding N+1 queries when building integrations
merge_request: 59635
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Deprecate Alerts service metric
merge_request: 59899
author:
type: deprecated

View File

@ -0,0 +1,5 @@
---
title: Fix for shell announcement banners
merge_request: 59482
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Update Metrics Definitions for Runner
merge_request: 59824
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Log exceptions in Lfs::PushService
merge_request: 59960
author:
type: changed

View File

@ -0,0 +1,8 @@
---
name: new_graphql_keyset_pagination
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56751
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323730
milestone: '13.10'
type: development
group: group::optimize
default_enabled: false

View File

@ -2,7 +2,7 @@
key_path: counts.clusters_applications_runner
description: Total GitLab Managed clusters with GitLab Managed App:Runner installed
product_section: ops
product_stage:
product_stage: configure
product_group: group::configure
product_category: kubernetes_management
value_type: number
@ -16,4 +16,3 @@ tier:
- free
- premium
- ultimate
skip_validation: true

View File

@ -6,7 +6,7 @@ product_stage: monitor
product_group: group::health
product_category: incident_management
value_type: number
status: data_available
status: deprecated
time_frame: all
data_source: database
distribution:

View File

@ -1,19 +1,18 @@
---
key_path: usage_activity_by_stage.verify.clusters_applications_runner
description: Total GitLab Managed clusters with Runner enabled
description: Count of users creating managed clusters with Runner enabled
product_section: ops
product_stage: verify
product_group: group::runner
product_category: runner
product_stage: configure
product_group: group::configure
product_category: kubernetes_management
value_type: number
status: data_available
time_frame: all
data_source:
data_source: database
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
skip_validation: true
- ultimate

View File

@ -1,16 +1,18 @@
---
key_path: gitlab_shared_runners_enabled
description: Whether shared runners is enabled
product_section: growth
product_stage: growth
product_group: group::product intelligence
product_category: collection
product_section: ops
product_stage: verify
product_group: group::runner
product_category: runner
value_type: boolean
status: data_available
time_frame: none
data_source:
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -4756,7 +4756,7 @@ Count of projects that have enabled the Alerts service
Group: `group::health`
Status: `data_available`
Status: `deprecated`
Tiers: `free`, `premium`, `ultimate`
@ -6506,11 +6506,11 @@ Whether shared runners is enabled
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/settings/20210204124902_gitlab_shared_runners_enabled.yml)
Group: `group::product intelligence`
Group: `group::runner`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `gitpod_enabled`
@ -16224,11 +16224,11 @@ Tiers: `free`
### `usage_activity_by_stage.verify.clusters_applications_runner`
Total GitLab Managed clusters with Runner enabled
Count of users creating managed clusters with Runner enabled
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216181949_clusters_applications_runner.yml)
Group: `group::runner`
Group: `group::configure`
Status: `data_available`

View File

@ -56,14 +56,7 @@ The instance then notifies the user.
## Review existing GPG keys
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/282429) in GitLab 13.10.
> - [Deployed behind a feature flag](../feature_flags.md), disabled by default.
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/292961) in GitLab 13.11.
> - Enabled on GitLab.com.
> - Recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-the-gpg-keys-view).
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/292961) in GitLab 13.12.
You can view all existing GPG in your GitLab instance by navigating to the
credentials inventory GPG Keys tab, as well as the following properties:
@ -73,22 +66,3 @@ credentials inventory GPG Keys tab, as well as the following properties:
- Whether the GPG key is [verified or unverified](../project/repository/gpg_signed_commits/index.md)
![Credentials inventory page - GPG keys](img/credentials_inventory_gpg_keys_v13_10.png)
### Enable or disable the GPG keys view
Enabling or disabling the GPG keys view is under development but ready for production use.
It is deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can opt to disable it.
To enable it:
```ruby
Feature.enable(:credential_inventory_gpg_keys)
```
To disable it:
```ruby
Feature.disable(:credential_inventory_gpg_keys)
```

View File

@ -73,18 +73,31 @@ CSV file containing details of the resources scanned.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/235558) in GitLab 13.6.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/285476) in GitLab 13.10, options to zoom in on a date range, and download the vulnerabilities chart.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/285477) in GitLab 13.11, date range slider to visualise data between given dates.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/285477) in GitLab 13.11, date range slider to visualize data between given dates.
At the project level, the Security Dashboard displays a chart with the number of vulnerabilities over time.
Access it by navigating to **Security & Compliance > Security Dashboard**. We display historical
data up to 365 days. The chart's data is updated daily.
A project's Security Dashboard displays a chart with the total number of vulnerabilities
over time. It updates daily with up to 365 days of historical data. By default,
it shows statistics for all vulnerability severities.
To access the dashboard, from your project's home page go to **Security & Compliance > Security Dashboard**.
![Project Security Dashboard](img/project_security_dashboard_chart_v13_11.png)
Filter the historical data by clicking on the corresponding legend name. The image above, for example, shows
only the graph for vulnerabilities with **high** severity.
### Filter the vulnerabilities chart
To zoom in, select the left-most icon, then select the desired rangeby dragging across the chart. Select **Remove Selection** (**{redo}**) to reset to the original date range.
To filter the chart by vulnerability severity, select the corresponding legend name.
In the previous example, the chart shows statistics only for vulnerabilities of medium or unknown severity.
### Customize vulnerabilities chart display
To customize the view of the vulnerability chart, you can select:
- A specific time frame by using the time range handles (**{scroll-handle}**).
- A specific area of the chart by using the left-most icon (**{marquee-selection}**) then drag
across the chart. To reset to the original range, select **Remove Selection** (**{redo}**).
### Download a copy of the vulnerabilities chart
To download an SVG image of the chart, select **Save chart to an image** (**{download}**).

View File

@ -26,7 +26,7 @@ module API
get ':id/environments' do
authorize! :read_environment, user_project
environments = ::EnvironmentsFinder.new(user_project, current_user, params).execute
environments = ::Environments::EnvironmentsFinder.new(user_project, current_user, params).execute
present paginate(environments), with: Entities::Environment, current_user: current_user
end

View File

@ -130,7 +130,7 @@ module Gitlab
strong_memoize(:environment) do
next unless environment_name
EnvironmentsFinder
::Environments::EnvironmentsFinder
.new(project, nil, { name: environment_name })
.execute
.first

View File

@ -10,6 +10,8 @@ module Gitlab
extend ActiveSupport::Concern
def ordered_items
raise ArgumentError, 'Relation must have a primary key' unless items.primary_key.present?
return super unless Gitlab::Pagination::Keyset::Order.keyset_aware?(items)
items
@ -40,6 +42,17 @@ module Gitlab
sliced = slice_nodes(sliced, after, :after) if after.present?
sliced
end
def items
original_items = super
return original_items if Gitlab::Pagination::Keyset::Order.keyset_aware?(original_items) || Feature.disabled?(:new_graphql_keyset_pagination)
strong_memoize(:generic_keyset_pagination_items) do
rebuilt_items_with_keyset_order, success = Gitlab::Pagination::Keyset::SimpleOrderBuilder.build(original_items)
success ? rebuilt_items_with_keyset_order : original_items
end
end
end
end
end

View File

@ -170,6 +170,8 @@ module Gitlab
self.class.build(column_definitions.map(&:reverse))
end
alias_method :to_sql, :to_s
private
# Adds extra columns to the SELECT clause

View File

@ -0,0 +1,137 @@
# frozen_string_literal: true
module Gitlab
module Pagination
module Keyset
# This class transforms the `order()` values from an Activerecord scope into a
# Gitlab::Pagination::Keyset::Order instance so the query later can be used in
# keyset pagination.
#
# Return values:
# [transformed_scope, true] # true indicates that the new scope was successfully built
# [orginal_scope, false] # false indicates that the order values are not supported in this class
class SimpleOrderBuilder
def self.build(scope)
new(scope: scope).build
end
def initialize(scope:)
@scope = scope
@order_values = scope.order_values
@model_class = scope.model
@arel_table = @model_class.arel_table
@primary_key = @model_class.primary_key
end
def build
order = if order_values.empty?
primary_key_descending_order
elsif ordered_by_primary_key?
primary_key_order
elsif ordered_by_other_column?
column_with_tie_breaker_order
elsif ordered_by_other_column_with_tie_breaker?
tie_breaker_attribute = order_values.second
tie_breaker_column_order = Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: model_class.primary_key,
order_expression: tie_breaker_attribute
)
column_with_tie_breaker_order(tie_breaker_column_order)
end
order ? [scope.reorder!(order), true] : [scope, false] # [scope, success]
end
private
attr_reader :scope, :order_values, :model_class, :arel_table, :primary_key
def primary_key_descending_order
Gitlab::Pagination::Keyset::Order.build([
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: model_class.primary_key,
order_expression: arel_table[primary_key].desc
)
])
end
def primary_key_order
Gitlab::Pagination::Keyset::Order.build([
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: model_class.primary_key,
order_expression: order_values.first
)
])
end
def column_with_tie_breaker_order(tie_breaker_column_order = default_tie_breaker_column_order)
order_expression = order_values.first
attribute_name = order_expression.expr.name
column_nullable = model_class.columns.find { |column| column.name == attribute_name }.null
nullable = if column_nullable && order_expression.is_a?(Arel::Nodes::Ascending)
:nulls_last
elsif column_nullable && order_expression.is_a?(Arel::Nodes::Descending)
:nulls_first
else
:not_nullable
end
Gitlab::Pagination::Keyset::Order.build([
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: attribute_name,
order_expression: order_expression,
nullable: nullable,
distinct: false
),
tie_breaker_column_order
])
end
def ordered_by_primary_key?
return unless order_values.one?
attribute = order_values.first.try(:expr)
return unless attribute
arel_table[primary_key].to_s == attribute.to_s
end
def ordered_by_other_column?
return unless order_values.one?
attribute = order_values.first.try(:expr)
return unless attribute
return unless attribute.try(:name)
model_class.column_names.include?(attribute.name.to_s)
end
def ordered_by_other_column_with_tie_breaker?
return unless order_values.size == 2
attribute = order_values.first.try(:expr)
tie_breaker_attribute = order_values.second.try(:expr)
return unless attribute
return unless tie_breaker_attribute
model_class.column_names.include?(attribute.name.to_s) &&
arel_table[primary_key].to_s == tie_breaker_attribute.to_s
end
def default_tie_breaker_column_order
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: model_class.primary_key,
order_expression: arel_table[primary_key].desc
)
end
end
end
end
end

View File

@ -164,7 +164,7 @@ module Gitlab
projects_with_repositories_enabled: count(ProjectFeature.where('repository_access_level > ?', ProjectFeature::DISABLED)),
projects_with_tracing_enabled: count(ProjectTracingSetting),
projects_with_error_tracking_enabled: count(::ErrorTracking::ProjectErrorTrackingSetting.where(enabled: true)),
projects_with_alerts_service_enabled: count(Service.active.where(type: 'AlertsService')),
projects_with_alerts_service_enabled: DEPRECATED_VALUE,
projects_with_alerts_created: distinct_count(::AlertManagement::Alert, :project_id),
projects_with_enabled_alert_integrations: distinct_count(::AlertManagement::HttpIntegration.active, :project_id),
projects_with_prometheus_alerts: distinct_count(PrometheusAlert, :project_id),

View File

@ -691,6 +691,9 @@ msgstr ""
msgid "%{milliseconds}ms"
msgstr ""
msgid "%{model_name} not found"
msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
@ -5786,6 +5789,9 @@ msgstr ""
msgid "Cannot create the abuse report. This user has been blocked."
msgstr ""
msgid "Cannot delete %{profile_name} referenced in security policy"
msgstr ""
msgid "Cannot enable shared runners because parent group does not allow it"
msgstr ""
@ -5807,6 +5813,9 @@ msgstr ""
msgid "Cannot merge"
msgstr ""
msgid "Cannot modify %{profile_name} referenced in security policy"
msgstr ""
msgid "Cannot modify managed Kubernetes cluster"
msgstr ""
@ -10115,6 +10124,9 @@ msgstr ""
msgid "DastProfiles|Request headers"
msgstr ""
msgid "DastProfiles|Rest API"
msgstr ""
msgid "DastProfiles|Run scan"
msgstr ""
@ -10160,6 +10172,9 @@ msgstr ""
msgid "DastProfiles|Site name"
msgstr ""
msgid "DastProfiles|Site type"
msgstr ""
msgid "DastProfiles|Spider timeout"
msgstr ""
@ -10208,6 +10223,9 @@ msgstr ""
msgid "DastProfiles|Validation status"
msgstr ""
msgid "DastProfiles|Website"
msgstr ""
msgid "DastSiteValidation|Copy HTTP header to clipboard"
msgstr ""
@ -17185,6 +17203,9 @@ msgstr ""
msgid "Instance overview"
msgstr ""
msgid "Insufficient permissions"
msgstr ""
msgid "Integration"
msgstr ""
@ -27661,6 +27682,12 @@ msgstr ""
msgid "Scanner"
msgstr ""
msgid "Scanner profile failed to delete"
msgstr ""
msgid "Scanner profile not found for given parameters"
msgstr ""
msgid "Schedule a new pipeline"
msgstr ""
@ -29310,6 +29337,12 @@ msgstr ""
msgid "Single or combined queries"
msgstr ""
msgid "Site profile failed to delete"
msgstr ""
msgid "Site profile not found for given parameters"
msgstr ""
msgid "Size"
msgstr ""
@ -36162,7 +36195,10 @@ msgstr ""
msgid "You have been granted %{access_level} access to the %{source_name} %{source_type}."
msgstr ""
msgid "You have been granted %{member_human_access} access to %{title} %{name}."
msgid "You have been granted %{member_human_access} access to group %{name}."
msgstr ""
msgid "You have been granted %{member_human_access} access to project %{name}."
msgstr ""
msgid "You have been invited"

View File

@ -3,11 +3,14 @@
function retrieve_tests_metadata() {
mkdir -p knapsack/ rspec_flaky/ rspec_profiling/
# ${CI_DEFAULT_BRANCH} might not be master in other forks but we want to
# always target the canonical project here, so the branch must be hardcoded
local project_path="gitlab-org/gitlab"
local artifact_branch="master"
local test_metadata_job_id
# Ruby
test_metadata_job_id=$(scripts/api/get_job_id.rb --project "${project_path}" -q "status=success" -q "ref=${CI_DEFAULT_BRANCH}" -q "username=gitlab-bot" -Q "scope=success" --job-name "update-tests-metadata")
test_metadata_job_id=$(scripts/api/get_job_id.rb --project "${project_path}" -q "status=success" -q "ref=${artifact_branch}" -q "username=gitlab-bot" -Q "scope=success" --job-name "update-tests-metadata")
if [[ ! -f "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" ]]; then
scripts/api/download_job_artifact.rb --project "${project_path}" --job-id "${test_metadata_job_id}" --artifact-path "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" || echo "{}" > "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}"

View File

@ -32,8 +32,6 @@ FactoryBot.define do
create(:service, project: projects[2], type: 'CustomIssueTrackerService', active: true)
create(:project_error_tracking_setting, project: projects[0])
create(:project_error_tracking_setting, project: projects[1], enabled: false)
create(:service, project: projects[0], type: 'AlertsService', active: true)
create(:service, project: projects[1], type: 'AlertsService', active: false)
alert_bot_issues = create_list(:incident, 2, project: projects[0], author: User.alert_bot)
create_list(:incident, 2, project: projects[1], author: User.alert_bot)
issues = create_list(:issue, 4, project: projects[0])

View File

@ -64,13 +64,12 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do
expect(find_field('Email').value).to eq(group_invite.invite_email)
end
it 'sign in, grants access and redirects to group page' do
it 'sign in, grants access and redirects to group activity page' do
click_link 'Sign in'
fill_in_sign_in_form(user)
expect(current_path).to eq(group_path(group))
expect(page).to have_content('You have been granted Developer access to group Owned.')
expect(current_path).to eq(activity_group_path(group))
end
end
@ -117,26 +116,28 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do
context 'email confirmation disabled' do
let(:send_email_confirmation) { false }
it 'signs up and redirects to the dashboard page with all the projects/groups invitations automatically accepted' do
it 'signs up and redirects to the most recent membership activity page with all the projects/groups invitations automatically accepted' do
fill_in_sign_up_form(new_user)
fill_in_welcome_form
expect(current_path).to eq(dashboard_projects_path)
expect(page).to have_content(project.full_name)
expect(current_path).to eq(activity_group_path(group))
expect(page).to have_content('You have been granted Owner access to group Owned.')
visit group_path(group)
expect(page).to have_content(group.full_name)
visit project_path(project)
expect(page).to have_content(project.name)
end
context 'the user sign-up using a different email address' do
let(:invite_email) { build_stubbed(:user).email }
it 'signs up and redirects to the invitation page' do
it 'signs up and redirects to the activity page' do
fill_in_sign_up_form(new_user)
fill_in_welcome_form
expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
expect(current_path).to eq(activity_group_path(group))
end
end
end
@ -207,7 +208,7 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do
fill_in_sign_in_form(new_user)
fill_in_welcome_form
expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
expect(current_path).to eq(activity_group_path(group))
end
end
@ -221,7 +222,7 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do
fill_in_sign_up_form(new_user)
fill_in_welcome_form
expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
expect(current_path).to eq(activity_group_path(group))
end
end
end
@ -273,7 +274,7 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do
end
end
context 'when accepting the invitation' do
context 'when accepting the invitation as an existing user' do
let(:send_email_confirmation) { true }
before do
@ -286,7 +287,7 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do
page.click_link 'Accept invitation'
expect(current_path).to eq(group_path(group))
expect(current_path).to eq(activity_group_path(group))
expect(page).to have_content('You have been granted Owner access to group Owned.')
expect(group.users.include?(user)).to be true
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe EnvironmentNamesFinder do
RSpec.describe Environments::EnvironmentNamesFinder do
describe '#execute' do
let!(:group) { create(:group) }
let!(:public_project) { create(:project, :public, namespace: group) }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe EnvironmentsByDeploymentsFinder do
RSpec.describe Environments::EnvironmentsByDeploymentsFinder do
let(:project) { create(:project, :repository) }
let(:user) { project.creator }
let(:environment) { create(:environment, :available, project: project) }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe EnvironmentsFinder do
RSpec.describe Environments::EnvironmentsFinder do
let(:project) { create(:project, :repository) }
let(:user) { project.creator }
let(:environment) { create(:environment, :available, project: project) }

View File

@ -357,9 +357,10 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do
it 'is added to end' do
sliced = subject.sliced_nodes
last_order_name = sliced.order_values.last.expr.name
expect(last_order_name).to eq sliced.primary_key
order_sql = sliced.order_values.last.to_sql
expect(order_sql).to end_with(Project.arel_table[:id].desc.to_sql)
end
end

View File

@ -0,0 +1,91 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Pagination::Keyset::SimpleOrderBuilder do
let(:ordered_scope) { described_class.build(scope).first }
let(:order_object) { Gitlab::Pagination::Keyset::Order.extract_keyset_order_object(ordered_scope) }
subject(:sql_with_order) { ordered_scope.to_sql }
context 'when no order present' do
let(:scope) { Project.where(id: [1, 2, 3]) }
it 'orders by primary key' do
expect(sql_with_order).to end_with('ORDER BY "projects"."id" DESC')
end
it 'sets the column definition distinct and not nullable' do
column_definition = order_object.column_definitions.first
expect(column_definition).to be_not_nullable
expect(column_definition).to be_distinct
end
end
context 'when primary key order present' do
let(:scope) { Project.where(id: [1, 2, 3]).order(id: :asc) }
it 'orders by primary key without altering the direction' do
expect(sql_with_order).to end_with('ORDER BY "projects"."id" ASC')
end
end
context 'when ordered by other column' do
let(:scope) { Project.where(id: [1, 2, 3]).order(created_at: :asc) }
it 'adds extra primary key order as tie-breaker' do
expect(sql_with_order).to end_with('ORDER BY "projects"."created_at" ASC, "projects"."id" DESC')
end
it 'sets the column definition for created_at non-distinct and nullable' do
column_definition = order_object.column_definitions.first
expect(column_definition.attribute_name).to eq('created_at')
expect(column_definition.nullable?).to eq(true) # be_nullable calls non_null? method for some reason
expect(column_definition).not_to be_distinct
end
end
context 'when ordered by two columns where the last one is the tie breaker' do
let(:scope) { Project.where(id: [1, 2, 3]).order(created_at: :asc, id: :asc) }
it 'preserves the order' do
expect(sql_with_order).to end_with('ORDER BY "projects"."created_at" ASC, "projects"."id" ASC')
end
end
context 'when non-nullable column is given' do
let(:scope) { Project.where(id: [1, 2, 3]).order(namespace_id: :asc, id: :asc) }
it 'sets the column definition for namespace_id non-distinct and non-nullable' do
column_definition = order_object.column_definitions.first
expect(column_definition.attribute_name).to eq('namespace_id')
expect(column_definition).to be_not_nullable
expect(column_definition).not_to be_distinct
end
end
context 'return :unable_to_order symbol when order cannot be built' do
subject(:success) { described_class.build(scope).last }
context 'when raw SQL order is given' do
let(:scope) { Project.order('id DESC') }
it { is_expected.to eq(false) }
end
context 'when NULLS LAST order is given' do
let(:scope) { Project.order(::Gitlab::Database.nulls_last_order('created_at', 'ASC')) }
it { is_expected.to eq(false) }
end
context 'when more than 2 columns are given for the order' do
let(:scope) { Project.order(created_at: :asc, updated_at: :desc, id: :asc) }
it { is_expected.to eq(false) }
end
end
end

View File

@ -571,7 +571,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(count_data[:projects_with_repositories_enabled]).to eq(3)
expect(count_data[:projects_with_error_tracking_enabled]).to eq(1)
expect(count_data[:projects_with_tracing_enabled]).to eq(1)
expect(count_data[:projects_with_alerts_service_enabled]).to eq(1)
expect(count_data[:projects_with_alerts_service_enabled]).to eq(Gitlab::UsageData::DEPRECATED_VALUE)
expect(count_data[:projects_with_enabled_alert_integrations]).to eq(1)
expect(count_data[:projects_with_prometheus_alerts]).to eq(2)
expect(count_data[:projects_with_terraform_reports]).to eq(2)

View File

@ -120,6 +120,12 @@ RSpec.describe BroadcastMessage do
expect(subject.call('/users/name/issues').length).to eq(1)
end
it 'returns message if provided a path without a preceding slash' do
create(:broadcast_message, target_path: "/users/*/issues", broadcast_type: broadcast_type)
expect(subject.call('users/name/issues').length).to eq(1)
end
it 'returns the message for empty target path' do
create(:broadcast_message, target_path: "", broadcast_type: broadcast_type)

View File

@ -283,25 +283,50 @@ RSpec.describe 'GraphQL' do
)
end
it 'paginates datetimes correctly when they have millisecond data' do
# let's make sure we're actually querying a timestamp, just in case
expect(Gitlab::Graphql::Pagination::Keyset::QueryBuilder)
.to receive(:new).with(anything, anything, hash_including('created_at'), anything).and_call_original
context 'when new_graphql_keyset_pagination feature flag is off' do
before do
stub_feature_flags(new_graphql_keyset_pagination: false)
end
execute_query
first_page = graphql_data
edges = first_page.dig(*issues_edges)
cursor = first_page.dig(*end_cursor)
it 'paginates datetimes correctly when they have millisecond data' do
# let's make sure we're actually querying a timestamp, just in case
expect(Gitlab::Graphql::Pagination::Keyset::QueryBuilder)
.to receive(:new).with(anything, anything, hash_including('created_at'), anything).and_call_original
expect(edges.count).to eq(6)
expect(edges.last['node']['iid']).to eq(issues[4].iid.to_s)
execute_query
first_page = graphql_data
edges = first_page.dig(*issues_edges)
cursor = first_page.dig(*end_cursor)
execute_query(after: cursor)
second_page = graphql_data
edges = second_page.dig(*issues_edges)
expect(edges.count).to eq(6)
expect(edges.last['node']['iid']).to eq(issues[4].iid.to_s)
expect(edges.count).to eq(4)
expect(edges.last['node']['iid']).to eq(issues[0].iid.to_s)
execute_query(after: cursor)
second_page = graphql_data
edges = second_page.dig(*issues_edges)
expect(edges.count).to eq(4)
expect(edges.last['node']['iid']).to eq(issues[0].iid.to_s)
end
end
context 'when new_graphql_keyset_pagination feature flag is on' do
it 'paginates datetimes correctly when they have millisecond data' do
execute_query
first_page = graphql_data
edges = first_page.dig(*issues_edges)
cursor = first_page.dig(*end_cursor)
expect(edges.count).to eq(6)
expect(edges.last['node']['iid']).to eq(issues[4].iid.to_s)
execute_query(after: cursor)
second_page = graphql_data
edges = second_page.dig(*issues_edges)
expect(edges.count).to eq(4)
expect(edges.last['node']['iid']).to eq(issues[0].iid.to_s)
end
end
end
end

View File

@ -63,6 +63,7 @@ RSpec.describe Lfs::PushService do
it 'returns a failure when submitting a batch fails' do
expect(lfs_client).to receive(:batch!) { raise 'failed' }
expect(Gitlab::ErrorTracking).to receive(:log_exception).and_call_original
expect(service.execute).to eq(status: :error, message: 'failed')
end
@ -70,6 +71,7 @@ RSpec.describe Lfs::PushService do
stub_lfs_batch(lfs_object)
expect(lfs_client).to receive(:upload!) { raise 'failed' }
expect(Gitlab::ErrorTracking).to receive(:log_exception).and_call_original
expect(service.execute).to eq(status: :error, message: 'failed')
end

View File

@ -264,7 +264,7 @@ RSpec.describe PostReceiveService do
context "project path matches" do
before do
allow(project).to receive(:full_path).and_return("/company/sekrit-project")
allow(project).to receive(:full_path).and_return("company/sekrit-project")
end
it "does output the latest scoped broadcast message" do

View File

@ -273,16 +273,6 @@ RSpec.describe Projects::CreateService, '#execute' do
opts[:default_branch] = 'master'
expect(create_project(user, opts)).to eq(nil)
end
it 'sets invalid service as inactive' do
create(:service, type: 'JiraService', project: nil, template: true, active: true)
project = create_project(user, opts)
service = project.services.first
expect(project).to be_persisted
expect(service.active).to be false
end
end
context 'wiki_enabled creates repository directory' do
@ -633,17 +623,6 @@ RSpec.describe Projects::CreateService, '#execute' do
end
end
end
context 'when there is an invalid integration' do
before do
create(:service, :template, type: 'DroneCiService', active: true)
end
it 'creates an inactive service' do
expect(project).to be_persisted
expect(project.services.first.active).to be false
end
end
end
context 'when skip_disk_validation is used' do

View File

@ -22,7 +22,6 @@ RSpec.describe BuildFinishedWorker do
end
expect(BuildHooksWorker).to receive(:perform_async)
expect(ExpirePipelineCacheWorker).to receive(:perform_async)
expect(ChatNotificationWorker).not_to receive(:perform_async)
expect(ArchiveTraceWorker).to receive(:perform_in)