Commit 634bd522 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 5cf2def4 6c1626e0
d12fb69a841d91d843f392a124865f6d47d3bc22
747d61c17a51f361cc883c9d4700e174219088a5
......@@ -93,12 +93,12 @@ export default {
return {
name,
list,
title: this.getAwardListTitle(list),
title: this.getAwardListTitle(list, name),
classes: this.getAwardClassBindings(list),
html: glEmojiTag(name),
};
},
getAwardListTitle(awardsList) {
getAwardListTitle(awardsList, name) {
if (!awardsList.length) {
return '';
}
......@@ -128,7 +128,7 @@ export default {
// We have 10+ awarded user, join them with comma and add `and x more`.
if (remainingAwardList.length) {
title = sprintf(
__(`%{listToShow}, and %{awardsListLength} more.`),
__(`%{listToShow}, and %{awardsListLength} more`),
{
listToShow: namesToShow.join(', '),
awardsListLength: remainingAwardList.length,
......@@ -146,7 +146,7 @@ export default {
title = namesToShow.join(__(' and '));
}
return title;
return title + sprintf(__(' reacted with :%{name}:'), { name });
},
handleAward(awardName) {
if (!this.canAwardEmoji) {
......
......@@ -16,6 +16,7 @@ module Environments
environments = project.environments
environments = by_name(environments)
environments = by_search(environments)
environments = by_ids(environments)
# Raises InvalidStatesError if params[:states] contains invalid states.
by_states(environments)
......@@ -47,6 +48,14 @@ module Environments
end
end
def by_ids(environments)
if params[:environment_ids].present?
environments.for_id(params[:environment_ids])
else
environments
end
end
def environments_with_states(environments)
# Convert to array of strings
states = Array(params[:states]).map(&:to_s)
......
......@@ -9,6 +9,8 @@ class FinalizePushEventPayloadsBigintConversion < ActiveRecord::Migration[6.1]
INDEX_NAME = 'index_push_event_payloads_on_event_id_convert_to_bigint'
def up
return unless should_run?
ensure_batched_background_migration_is_finished(
job_class_name: 'CopyColumnUsingBackgroundMigrationJob',
table_name: TABLE_NAME,
......@@ -20,11 +22,17 @@ class FinalizePushEventPayloadsBigintConversion < ActiveRecord::Migration[6.1]
end
def down
return unless should_run?
swap_columns
end
private
def should_run?
Gitlab.dev_or_test_env? || Gitlab.com?
end
def swap_columns
add_concurrent_index TABLE_NAME, :event_id_convert_to_bigint, unique: true, name: INDEX_NAME
......
# frozen_string_literal: true
require Rails.root.join('db', 'post_migrate', '20210622041846_finalize_push_event_payloads_bigint_conversion')
class MigratePushEventPayloadsEventIdBackToIntegerForGitlabCom < ActiveRecord::Migration[6.1]
disable_ddl_transaction!
def up
FinalizePushEventPayloadsBigintConversion.new.down
end
def down
FinalizePushEventPayloadsBigintConversion.new.up
end
end
71ad8c8f2419721f8fdf6c6bbd1265c4a7ca277972c59319e155bc6dfc46aa48
\ No newline at end of file
......@@ -17376,7 +17376,7 @@ ALTER SEQUENCE protected_tags_id_seq OWNED BY protected_tags.id;
CREATE TABLE push_event_payloads (
commit_count bigint NOT NULL,
event_id_convert_to_bigint integer DEFAULT 0 NOT NULL,
event_id integer NOT NULL,
action smallint NOT NULL,
ref_type smallint NOT NULL,
commit_from bytea,
......@@ -17384,7 +17384,7 @@ CREATE TABLE push_event_payloads (
ref text,
commit_title character varying(70),
ref_count integer,
event_id bigint NOT NULL
event_id_convert_to_bigint bigint DEFAULT 0 NOT NULL
);
CREATE TABLE push_rules (
......@@ -10949,6 +10949,7 @@ Represents the network policy.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="networkpolicyenabled"></a>`enabled` | [`Boolean!`](#boolean) | Indicates whether this policy is enabled. |
| <a id="networkpolicyenvironments"></a>`environments` | [`EnvironmentConnection`](#environmentconnection) | Environments where this policy is applied. (see [Connections](#connections)) |
| <a id="networkpolicyfromautodevops"></a>`fromAutoDevops` | [`Boolean!`](#boolean) | Indicates whether this policy is created from AutoDevops. |
| <a id="networkpolicyname"></a>`name` | [`String!`](#string) | Name of the policy. |
| <a id="networkpolicynamespace"></a>`namespace` | [`String!`](#string) | Namespace of the policy. |
......
......@@ -29,7 +29,9 @@ module Resolvers
updated_at: Time.iso8601(policy_json[:creation_timestamp]),
yaml: policy_json[:manifest],
from_auto_devops: policy_json[:is_autodevops],
enabled: policy_json[:is_enabled]
enabled: policy_json[:is_enabled],
environment_ids: policy_json[:environment_ids],
project: project
}
end
end
......
......@@ -35,5 +35,21 @@ module Types
Types::TimeType,
null: false,
description: 'Timestamp of when the policy YAML was last updated.'
field :environments,
Types::EnvironmentType.connection_type,
null: true,
description: 'Environments where this policy is applied.'
def environments
BatchLoader::GraphQL.for(object[:environment_ids]).batch do |policy_environment_ids, loader|
finder = ::Environments::EnvironmentsFinder.new(object[:project], context[:current_user], environment_ids: policy_environment_ids.flatten.uniq)
environments_by_id = finder.execute.index_by(&:id)
policy_environment_ids.each do |ids|
loader.call(ids, environments_by_id.values_at(*ids))
end
end
end
end
end
......@@ -17,8 +17,8 @@ module NetworkPolicies
policies = []
errors = []
@kubeclient_info.each do |platform, namespace|
policies_per_environment, error_per_environment = execute_per_environment(platform, namespace)
@kubeclient_info.each do |platform, (namespace, environment_ids)|
policies_per_environment, error_per_environment = execute_per_environment(platform, namespace, environment_ids)
policies += policies_per_environment if policies_per_environment
errors << error_per_environment if error_per_environment
end
......@@ -33,13 +33,13 @@ module NetworkPolicies
track_usage_event(:clusters_using_network_policies_ui, platform.cluster_id)
end
def execute_per_environment(platform, namespace)
def execute_per_environment(platform, namespace, environment_ids)
policies = platform.kubeclient
.get_network_policies(namespace: namespace)
.map { |resource| Gitlab::Kubernetes::NetworkPolicy.from_resource(resource) }
.map { |resource| Gitlab::Kubernetes::NetworkPolicy.from_resource(resource, environment_ids) }
policies += platform.kubeclient
.get_cilium_network_policies(namespace: namespace)
.map { |resource| Gitlab::Kubernetes::CiliumNetworkPolicy.from_resource(resource) }
.map { |resource| Gitlab::Kubernetes::CiliumNetworkPolicy.from_resource(resource, environment_ids) }
track_usage_data_for_cluster(platform, policies)
[policies, nil]
rescue Kubeclient::HttpError => e
......@@ -47,7 +47,7 @@ module NetworkPolicies
end
def has_deployment_platform?(kubeclient_info)
kubeclient_info.any? { |platform, namespace| platform.present? }
kubeclient_info.any? { |platform, _| platform.present? }
end
# rubocop: disable CodeReuse/ActiveRecord
......@@ -63,7 +63,7 @@ module NetworkPolicies
.order(updated_at: :desc)
.preload(:platform_kubernetes)
.group_by(&:namespace)
.map { |namespace, kubernetes_namespaces| [kubernetes_namespaces.first.platform_kubernetes, namespace] }
.map { |namespace, kubernetes_namespaces| [kubernetes_namespaces.first.platform_kubernetes, [namespace, kubernetes_namespaces.map(&:environment_id)]] }
end
# rubocop: enable CodeReuse/ActiveRecord
end
......
......@@ -16,7 +16,8 @@ RSpec.describe Resolvers::NetworkPolicyResolver do
namespace: 'another',
creation_timestamp: time_now.iso8601,
selector: { matchLabels: { role: 'db' } },
ingress: [{ from: [{ namespaceSelector: { matchLabels: { project: 'myproject' } } }] }]
ingress: [{ from: [{ namespaceSelector: { matchLabels: { project: 'myproject' } } }] }],
environment_ids: [1, 2]
)
end
......@@ -27,7 +28,8 @@ RSpec.describe Resolvers::NetworkPolicyResolver do
creation_timestamp: time_now.iso8601,
resource_version: '102',
selector: { matchLabels: { role: 'db' } },
ingress: [{ endpointFrom: [{ matchLabels: { project: 'myproject' } }] }]
ingress: [{ endpointFrom: [{ matchLabels: { project: 'myproject' } }] }],
environment_ids: [3, 4]
)
end
......@@ -97,7 +99,9 @@ RSpec.describe Resolvers::NetworkPolicyResolver do
enabled: true,
yaml: policy.as_json[:manifest],
updated_at: time_now,
from_auto_devops: false
from_auto_devops: false,
environment_ids: [1, 2],
project: project
},
{
name: 'cilium_policy',
......@@ -105,7 +109,9 @@ RSpec.describe Resolvers::NetworkPolicyResolver do
enabled: true,
yaml: cilium_policy.as_json[:manifest],
updated_at: time_now,
from_auto_devops: false
from_auto_devops: false,
environment_ids: [3, 4],
project: project
}
]
expect(resolve_network_policies).to eq(expected_resolved)
......
......@@ -12,7 +12,54 @@ RSpec.describe GitlabSchema.types['NetworkPolicy'] do
:enabled,
:from_auto_devops,
:yaml,
:updated_at
:updated_at,
:environments
)
end
describe '#environments' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
let_it_be(:environment_1) { create(:environment, project: project) }
let_it_be(:environment_2) { create(:environment, project: project) }
let_it_be(:environment_3) { create(:environment, project: project) }
let(:policy_1) { double(as_json: { creation_timestamp: Time.current.iso8601, project: project, environment_ids: [environment_1.id] }) }
let(:policy_2) { double(as_json: { creation_timestamp: Time.current.iso8601, project: project, environment_ids: [environment_1.id, environment_2.id] }) }
let(:service_response_single_environment) { double(success?: true, payload: [policy_1]) }
let(:service_response_multiple_environments) { double(success?: true, payload: [policy_1, policy_2]) }
let(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
networkPolicies {
nodes {
environments {
nodes {
id
name
}
}
}
}
}
}
)
end
it 'avoids N+1 database queries' do
allow_next_instance_of(NetworkPolicies::ResourcesService) do |service|
allow(service).to receive(:execute).and_return(service_response_single_environment)
end
control_count = ActiveRecord::QueryRecorder.new { GitlabSchema.execute(query, context: { current_user: user }) }.count
allow_next_instance_of(NetworkPolicies::ResourcesService) do |service|
allow(service).to receive(:execute).and_return(service_response_multiple_environments)
end
expect { GitlabSchema.execute(query, context: { current_user: user }) }.not_to exceed_query_limit(control_count)
end
end
end
......@@ -16,7 +16,8 @@ RSpec.describe NetworkPolicies::ResourcesService do
name: 'policy',
namespace: 'another',
selector: { matchLabels: { role: 'db' } },
ingress: [{ from: [{ namespaceSelector: { matchLabels: { project: 'myproject' } } }] }]
ingress: [{ from: [{ namespaceSelector: { matchLabels: { project: 'myproject' } } }] }],
environment_ids: [environment.id]
)
end
......@@ -26,7 +27,8 @@ RSpec.describe NetworkPolicies::ResourcesService do
namespace: 'another',
resource_version: '102',
selector: { matchLabels: { role: 'db' } },
ingress: [{ endpointFrom: [{ matchLabels: { project: 'myproject' } }] }]
ingress: [{ endpointFrom: [{ matchLabels: { project: 'myproject' } }] }],
environment_ids: [environment.id]
)
end
......@@ -109,7 +111,8 @@ RSpec.describe NetworkPolicies::ResourcesService do
name: 'policy_2',
namespace: 'another_2',
selector: { matchLabels: { role: 'db' } },
ingress: [{ from: [{ namespaceSelector: { matchLabels: { project: 'myproject' } } }] }]
ingress: [{ from: [{ namespaceSelector: { matchLabels: { project: 'myproject' } } }] }],
environment_ids: [environment.id]
)
end
......
......@@ -12,7 +12,7 @@ module Gitlab
# We are modeling existing kubernetes resource and don't have
# control over amount of parameters.
# rubocop:disable Metrics/ParameterLists
def initialize(name:, namespace:, selector:, ingress:, resource_version: nil, description: nil, labels: nil, creation_timestamp: nil, egress: nil, annotations: nil)
def initialize(name:, namespace:, selector:, ingress:, resource_version: nil, description: nil, labels: nil, creation_timestamp: nil, egress: nil, annotations: nil, environment_ids: [])
@name = name
@description = description
@namespace = namespace
......@@ -23,6 +23,7 @@ module Gitlab
@ingress = ingress
@egress = egress
@annotations = annotations
@environment_ids = environment_ids
end
# rubocop:enable Metrics/ParameterLists
......@@ -49,7 +50,7 @@ module Gitlab
nil
end
def self.from_resource(resource)
def self.from_resource(resource, environment_ids = [])
return unless resource
return if !resource[:metadata] || !resource[:spec]
......@@ -65,7 +66,8 @@ module Gitlab
creation_timestamp: metadata[:creationTimestamp],
selector: spec[:endpointSelector],
ingress: spec[:ingress],
egress: spec[:egress]
egress: spec[:egress],
environment_ids: environment_ids
)
end
......@@ -83,7 +85,7 @@ module Gitlab
private
attr_reader :name, :description, :namespace, :labels, :creation_timestamp, :resource_version, :ingress, :egress, :annotations
attr_reader :name, :description, :namespace, :labels, :creation_timestamp, :resource_version, :ingress, :egress, :annotations, :environment_ids
def selector
@selector ||= {}
......
......@@ -8,7 +8,8 @@ module Gitlab
KIND = 'NetworkPolicy'
def initialize(name:, namespace:, selector:, ingress:, labels: nil, creation_timestamp: nil, policy_types: ["Ingress"], egress: nil)
# rubocop:disable Metrics/ParameterLists
def initialize(name:, namespace:, selector:, ingress:, labels: nil, creation_timestamp: nil, policy_types: ["Ingress"], egress: nil, environment_ids: [])
@name = name
@namespace = namespace
@labels = labels
......@@ -17,7 +18,9 @@ module Gitlab
@policy_types = policy_types
@ingress = ingress
@egress = egress
@environment_ids = environment_ids
end
# rubocop:enable Metrics/ParameterLists
def self.from_yaml(manifest)
return unless manifest
......@@ -40,7 +43,7 @@ module Gitlab
nil
end
def self.from_resource(resource)
def self.from_resource(resource, environment_ids = [])
return unless resource
return if !resource[:metadata] || !resource[:spec]
......@@ -54,7 +57,8 @@ module Gitlab
selector: spec[:podSelector],
policy_types: spec[:policyTypes],
ingress: spec[:ingress],
egress: spec[:egress]
egress: spec[:egress],
environment_ids: environment_ids
)
end
......@@ -69,7 +73,7 @@ module Gitlab
private
attr_reader :name, :namespace, :labels, :creation_timestamp, :policy_types, :ingress, :egress
attr_reader :name, :namespace, :labels, :creation_timestamp, :policy_types, :ingress, :egress, :environment_ids
def selector
@selector ||= {}
......
......@@ -16,7 +16,8 @@ module Gitlab
creation_timestamp: creation_timestamp,
manifest: manifest,
is_autodevops: autodevops?,
is_enabled: enabled?
is_enabled: enabled?,
environment_ids: environment_ids
}
end
......
......@@ -177,7 +177,7 @@ module Gitlab
end
def repository_route_regex
@repository_route_regex ||= /#{full_namespace_route_regex}|#{personal_snippet_repository_path_regex}/.freeze
@repository_route_regex ||= /(#{full_namespace_route_regex}|#{personal_snippet_repository_path_regex})\.*/.freeze
end
def repository_git_route_regex
......@@ -185,7 +185,7 @@ module Gitlab
end
def repository_wiki_git_route_regex
@repository_wiki_git_route_regex ||= /#{full_namespace_route_regex}\.wiki\.git/.freeze
@repository_wiki_git_route_regex ||= /#{full_namespace_route_regex}\.*\.wiki\.git/.freeze
end
def full_namespace_path_regex
......
......@@ -77,6 +77,9 @@ msgstr ""
msgid " or references (e.g. path/to/project!merge_request_id)"
msgstr ""
msgid " reacted with :%{name}:"
msgstr ""
msgid "\"%{path}\" did not exist on \"%{ref}\""
msgstr ""
......@@ -687,7 +690,7 @@ msgstr ""
msgid "%{link_start}Start the title with %{draft_snippet}%{link_end} to prevent a merge request that is a work in progress from being merged before it's ready."
msgstr ""
msgid "%{listToShow}, and %{awardsListLength} more."
msgid "%{listToShow}, and %{awardsListLength} more"
msgstr ""
msgid "%{location} is missing required keys: %{keys}"
......
......@@ -63,7 +63,7 @@ RSpec.describe 'User interacts with awards' do
page.within('.awards') do
expect(page).to have_selector('[data-testid="award-button"]')
expect(page.find('[data-testid="award-button"].selected .js-counter')).to have_content('1')
expect(page).to have_css('[data-testid="award-button"].selected[title="You"]')
expect(page).to have_css('[data-testid="award-button"].selected[title="You reacted with :8ball:"]')
expect do
page.find('[data-testid="award-button"].selected').click
......
......@@ -3,9 +3,11 @@
require 'spec_helper'
RSpec.describe Environments::EnvironmentsFinder do
let(:project) { create(:project, :repository) }
let(:user) { project.creator }
let(:environment) { create(:environment, :available, project: project) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.creator }
let_it_be(:environment) { create(:environment, :available, project: project) }
let_it_be(:environment_stopped) { create(:environment, :stopped, name: 'test2', project: project) }
let_it_be(:environment_available) { create(:environment, :available, name: 'test3', project: project) }
before do
project.add_maintainer(user)
......@@ -13,18 +15,18 @@ RSpec.describe Environments::EnvironmentsFinder do
describe '#execute' do
context 'with states parameter' do
let(:stopped_environment) { create(:environment, :stopped, project: project) }
let_it_be(:stopped_environment) { create(:environment, :stopped, project: project) }
it 'returns environments with the requested state' do
result = described_class.new(project, user, states: 'available').execute
expect(result).to contain_exactly(environment)
expect(result).to contain_exactly(environment, environment_available)
end
it 'returns environments with any of the requested states' do
result = described_class.new(project, user, states: %w(available stopped)).execute
expect(result).to contain_exactly(environment, stopped_environment)
expect(result).to contain_exactly(environment, environment_stopped, environment_available, stopped_environment)
end
it 'raises exception when requested state is invalid' do
......@@ -37,25 +39,30 @@ RSpec.describe Environments::EnvironmentsFinder do
it 'returns environments with the requested state' do
result = described_class.new(project, user, states: :available).execute
expect(result).to contain_exactly(environment)
expect(result).to contain_exactly(environment, environment_available)
end
it 'returns environments with any of the requested states' do
result = described_class.new(project, user, states: [:available, :stopped]).execute
expect(result).to contain_exactly(environment, stopped_environment)
expect(result).to contain_exactly(environment, environment_stopped, environment_available, stopped_environment)
end
end
end
context 'with search and states' do
let(:environment2) { create(:environment, :stopped, name: 'test2', project: project) }
let(:environment3) { create(:environment, :available, name: 'test3', project: project) }
it 'searches environments by name and state' do
result = described_class.new(project, user, search: 'test', states: :available).execute
expect(result).to contain_exactly(environment3)
expect(result).to contain_exactly(environment_available)
end
end
context 'with id' do
it 'searches environments by name and state' do
result = described_class.new(project, user, search: 'test', environment_ids: [environment_available.id]).execute
expect(result).to contain_exactly(environment_available)
end
end
end
......
......@@ -7,7 +7,7 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<button
class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button"
data-testid="award-button"
title="Ada, Leonardo, and Marie"
title="Ada, Leonardo, and Marie reacted with :thumbsup:"
type="button"
>
<!---->
......@@ -37,7 +37,7 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<button
class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button selected"
data-testid="award-button"
title="You, Ada, and Marie"
title="You, Ada, and Marie reacted with :thumbsdown:"
type="button"
>
<!---->
......@@ -67,7 +67,7 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<button
class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button"
data-testid="award-button"
title="Ada and Jane"
title="Ada and Jane reacted with :smile:"
type="button"
>
<!---->
......@@ -97,7 +97,7 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<button
class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button selected"
data-testid="award-button"
title="You, Ada, Jane, and Leonardo"
title="You, Ada, Jane, and Leonardo reacted with :ok_hand:"
type="button"
>
<!---->
......@@ -127,7 +127,7 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<button
class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button selected"
data-testid="award-button"
title="You"
title="You reacted with :cactus:"
type="button"
>
<!---->
......@@ -157,7 +157,7 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<button
class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button"
data-testid="award-button"
title="Marie"
title="Marie reacted with :a:"
type="button"
>
<!---->
......@@ -187,7 +187,7 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<button
class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button selected"
data-testid="award-button"
title="You"
title="You reacted with :b:"
type="button"
>
<!---->
......
......@@ -98,43 +98,43 @@ describe('vue_shared/components/awards_list', () => {
classes: REACTION_CONTROL_CLASSES,
count: 3,
html: matchingEmojiTag(EMOJI_THUMBSUP),
title: 'Ada, Leonardo, and Marie',
title: `Ada, Leonardo, and Marie reacted with :${EMOJI_THUMBSUP}:`,
},
{
classes: [...REACTION_CONTROL_CLASSES, 'selected'],
count: 3,
html: matchingEmojiTag(EMOJI_THUMBSDOWN),
title: 'You, Ada, and Marie',
title: `You, Ada, and Marie reacted with :${EMOJI_THUMBSDOWN}:`,
},
{
classes: REACTION_CONTROL_CLASSES,
count: 2,
html: matchingEmojiTag(EMOJI_SMILE),
title: 'Ada and Jane',
title: `Ada and Jane reacted with :${EMOJI_SMILE}:`,
},
{
classes: [...REACTION_CONTROL_CLASSES, 'selected'],
count: 4,
html: matchingEmojiTag(EMOJI_OK),
title: 'You, Ada, Jane, and Leonardo',
title: `You, Ada, Jane, and Leonardo reacted with :${EMOJI_OK}:`,
},
{
classes: [...REACTION_CONTROL_CLASSES, 'selected'],
count: 1,
html: matchingEmojiTag(EMOJI_CACTUS),
title: 'You',
title: `You reacted with :${EMOJI_CACTUS}:`,
},
{
classes: REACTION_CONTROL_CLASSES,
count: 1,
html: matchingEmojiTag(EMOJI_A),
title: 'Marie',
title: `Marie reacted with :${EMOJI_A}:`,
},
{
classes: [...REACTION_CONTROL_CLASSES, 'selected'],
count: 1,
html: matchingEmojiTag(EMOJI_B),
title: 'You',
title: `You reacted with :${EMOJI_B}:`,
},
]);
});
......@@ -246,13 +246,13 @@ describe('vue_shared/components/awards_list', () => {
classes: REACTION_CONTROL_CLASSES,
count: 1,
html: matchingEmojiTag(EMOJI_100),
title: 'Marie',
title: `Marie reacted with :${EMOJI_100}:`,
},
{
classes: REACTION_CONTROL_CLASSES,
count: 1,
html: matchingEmojiTag(EMOJI_SMILE),
title: 'Marie',
title: `Marie reacted with :${EMOJI_SMILE}:`,
},
]);
});
......
......@@ -206,6 +206,14 @@ RSpec.describe Gitlab::Kubernetes::CiliumNetworkPolicy do
it { is_expected.to be_nil }
end
context 'with environment_ids' do
subject { Gitlab::Kubernetes::CiliumNetworkPolicy.from_resource(resource, [1, 2, 3]) }
it 'includes environment_ids in as_json result' do
expect(subject.as_json).to include(environment_ids: [1, 2, 3])
end
end
end
describe '#resource' do
......
......@@ -196,6 +196,14 @@ RSpec.describe Gitlab::Kubernetes::NetworkPolicy do
it { is_expected.to be_nil }
end
context 'with environment_ids' do
subject { Gitlab::Kubernetes::NetworkPolicy.from_resource(resource, [1, 2, 3]) }
it 'includes environment_ids in as_json result' do
expect(subject.as_json).to include(environment_ids: [1, 2, 3])
end
end
end
describe '#resource' do
......
......@@ -445,6 +445,8 @@ RSpec.describe Gitlab::PathRegex do
[
'gitlab-org',
'gitlab-org/gitlab-test',
'gitlab-org/foo.',
'gitlab-org/bar..',
'gitlab-org/gitlab-test/snippets/1',
'gitlab-org/gitlab-test/snippets/foo', # ambiguous, we allow creating a sub-group called 'snippets'
'snippets/1'
......
......@@ -966,6 +966,722 @@ RSpec.describe 'Git HTTP requests' do
end
end
end
context "when the project path ends with a dot" do
let(:path) { "#{project.full_path}.git" }
context "when the project is public" do
let(:project) { create(:project, :repository, :public, path: 'foo.') }
it_behaves_like 'pushes require Basic HTTP Authentication'
context 'when not authenticated' do
let(:env) { {} }
it_behaves_like 'pulls are allowed'
end
context "when authenticated" do
let(:env) { { user: user.username, password: user.password } }
context 'as a developer on the team' do
before do
project.add_developer(user)
end
it_behaves_like 'pulls are allowed'
it_behaves_like 'pushes are allowed'
context 'but git-receive-pack over HTTP is disabled in config' do
before do
allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false)
end
it 'rejects pushes with 403 Forbidden' do
upload(path, **env) do |response|
expect(response).to have_gitlab_http_status(:forbidden)
expect(response.body).to eq(git_access_error(:receive_pack_disabled_over_http))
end
end
end
context 'but git-upload-pack over HTTP is disabled in config' do
it "rejects pushes with 403 Forbidden" do
allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false)
download(path, **env) do |response|
expect(response).to have_gitlab_http_status(:forbidden)
expect(response.body).to eq(git_access_error(:upload_pack_disabled_over_http))
end
end
end
context 'but the service parameter is missing' do
it 'rejects clones with 403 Forbidden' do
get("/#{path}/info/refs", headers: auth_env(*env.values_at(:user, :password), nil))
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
context 'and not a member of the team' do
it_behaves_like 'pulls are allowed'
it 'rejects pushes with 403 Forbidden' do
upload(path, **env) do |response|
expect(response).to have_gitlab_http_status(:forbidden)
expect(response.body).to eq('You are not allowed to push code to this project.')
end
end
context 'when merge requests are open that allow maintainer access' do
let(:canonical_project) { create(:project, :public, :repository) }
let(:project) { fork_project(canonical_project, nil, repository: true) }
before do
canonical_project.add_maintainer(user)
create(:merge_request,
source_project: project,
target_project: canonical_project,
source_branch: 'fixes',
allow_collaboration: true)
end
it_behaves_like 'pushes are allowed'
end
context 'but the service parameter is missing' do
it 'rejects clones with 401 Unauthorized' do
get("/#{path}/info/refs")
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
end
context 'when the request is not from gitlab-workhorse' do
it 'raises an exception' do
expect do
get("/#{project.full_path}.git/info/refs?service=git-upload-pack")
end.to raise_error(JWT::DecodeError)
end
end
context 'when the repo is public' do
context 'but the repo is disabled' do
let(:project) { create(:project, :public, :repository, :repository_disabled) }
let(:path) { "#{project.full_path}.git" }
let(:env) { {} }
it_behaves_like 'pulls require Basic HTTP Authentication'
it_behaves_like 'pushes require Basic HTTP Authentication'
it_behaves_like 'operations are not allowed with expired password'
end
context 'but the repo is enabled' do
let(:project) { create(:project, :public, :repository, :repository_enabled) }
let(:path) { "#{project.full_path}.git" }
let(:env) { {} }
it_behaves_like 'pulls are allowed'
end
context 'but only project members are allowed' do
let(:project) { create(:project, :public, :repository, :repository_private) }
it_behaves_like 'pulls require Basic HTTP Authentication'
it_behaves_like 'pushes require Basic HTTP Authentication'
it_behaves_like 'operations are not allowed with expired password'
end
end
context 'and the user requests a redirected path' do
let!(:redirect) { project.route.create_redirect('foo/bar') }
let(:path) { "#{redirect.path}.git" }
it 'downloads get status 200 for redirects' do
clone_get(path)
expect(response).to have_gitlab_http_status(:ok)
end
end
end
context "when the project is private" do
let(:project) { create(:project, :repository, :private, path: 'foo.') }
it_behaves_like 'pulls require Basic HTTP Authentication'
it_behaves_like 'pushes require Basic HTTP Authentication'
it_behaves_like 'operations are not allowed with expired password'
context "when username and password are provided" do
let(:env) { { user: user.username, password: 'nope' } }
context "when authentication fails" do
context "when the user is IP banned" do
before do
stub_rack_attack_setting(enabled: true, ip_whitelist: [])
end
it "responds with status 403" do
expect(Rack::Attack::Allow2Ban).to receive(:banned?).and_return(true)
expect(Gitlab::AuthLogger).to receive(:error).with({
message: 'Rack_Attack',
env: :blocklist,
remote_ip: '127.0.0.1',
request_method: 'GET',
path: "/#{path}/info/refs?service=git-upload-pack"
})
clone_get(path, **env)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
context "when authentication succeeds" do
let(:env) { { user: user.username, password: user.password } }
context "when the user has access to the project" do
before do
project.add_maintainer(user)
end
context "when the user is blocked" do
it "rejects pulls with 401 Unauthorized" do
user.block
project.add_maintainer(user)
download(path, **env) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
it "rejects pulls with 401 Unauthorized for unknown projects (no project existence information leak)" do
user.block
download('doesnt/exist.git', **env) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
context "when the user isn't blocked" do
before do
stub_rack_attack_setting(enabled: true, bantime: 1.minute, findtime: 5.minutes, maxretry: 2, ip_whitelist: [])
end
it "resets the IP in Rack Attack on download" do
expect(Rack::Attack::Allow2Ban).to receive(:reset).twice
download(path, **env) do
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
end
it "resets the IP in Rack Attack on upload" do
expect(Rack::Attack::Allow2Ban).to receive(:reset).twice
upload(path, **env) do
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
end
it 'updates the user last activity', :clean_gitlab_redis_shared_state do
expect(user.last_activity_on).to be_nil
download(path, **env) do |response|
expect(user.reload.last_activity_on).to eql(Date.today)
end
end
end
context "when an oauth token is provided" do
before do
application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
@token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api")
end
let(:path) { "#{project.full_path}.git" }
let(:env) { { user: 'oauth2', password: @token.token } }
it_behaves_like 'pulls are allowed'
it_behaves_like 'pushes are allowed'
context "when password is expired" do
it "responds to downloads with status 401 unauthorized" do
user.update!(password_expires_at: 2.days.ago)
download(path, **env) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
end
context 'when user has 2FA enabled' do
let(:user) { create(:user, :two_factor) }
let(:access_token) { create(:personal_access_token, user: user) }
let(:path) { "#{project.full_path}.git" }
before do
project.add_maintainer(user)
end
context 'when username and password are provided' do
it 'rejects pulls with personal access token error message' do
download(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
end
end
it 'rejects the push attempt with personal access token error message' do
upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
end
end
end
context 'when username and personal access token are provided' do
let(:env) { { user: user.username, password: access_token.token } }
it_behaves_like 'pulls are allowed'
it_behaves_like 'pushes are allowed'
it 'rejects the push attempt for read_repository scope' do
read_access_token = create(:personal_access_token, user: user, scopes: [:read_repository])
upload(path, user: user.username, password: read_access_token.token) do |response|
expect(response).to have_gitlab_http_status(:forbidden)
expect(response.body).to include('You are not allowed to upload code')
end
end
it 'accepts the push attempt for write_repository scope' do
write_access_token = create(:personal_access_token, user: user, scopes: [:write_repository])
upload(path, user: user.username, password: write_access_token.token) do |response|
expect(response).to have_gitlab_http_status(:ok)
end
end
it 'accepts the pull attempt for read_repository scope' do
read_access_token = create(:personal_access_token, user: user, scopes: [:read_repository])
download(path, user: user.username, password: read_access_token.token) do |response|
expect(response).to have_gitlab_http_status(:ok)
end
end
it 'accepts the pull attempt for api scope' do
read_access_token = create(:personal_access_token, user: user, scopes: [:api])
download(path, user: user.username, password: read_access_token.token) do |response|
expect(response).to have_gitlab_http_status(:ok)
end
end
it 'accepts the push attempt for api scope' do
write_access_token = create(:personal_access_token, user: user, scopes: [:api])
upload(path, user: user.username, password: write_access_token.token) do |response|
expect(response).to have_gitlab_http_status(:ok)
end
end
context "when password is expired" do
it "responds to uploads with status 401 unauthorized" do
user.update!(password_expires_at: 2.days.ago)
write_access_token = create(:personal_access_token, user: user, scopes: [:write_repository])
upload(path, user: user.username, password: write_access_token.token) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
end
end
context 'when internal auth is disabled' do
before do
allow_any_instance_of(ApplicationSetting).to receive(:password_authentication_enabled_for_git?) { false }
end
it 'rejects pulls with personal access token error message' do
download(path, user: 'foo', password: 'bar') do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
end
end
it 'rejects pushes with personal access token error message' do
upload(path, user: 'foo', password: 'bar') do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
end
end
context 'when LDAP is configured' do
before do
allow(Gitlab::Auth::Ldap::Config).to receive(:enabled?).and_return(true)
allow_any_instance_of(Gitlab::Auth::Ldap::Authentication)
.to receive(:login).and_return(nil)
end
it 'does not display the personal access token error message' do
upload(path, user: 'foo', password: 'bar') do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.body).not_to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
end
end
end
end
context "when blank password attempts follow a valid login" do
def attempt_login(include_password)
password = include_password ? user.password : ""
clone_get path, user: user.username, password: password
response.status
end
include_context 'rack attack cache store'
it "repeated attempts followed by successful attempt" do
options = Gitlab.config.rack_attack.git_basic_auth
maxretry = options[:maxretry]
ip = '1.2.3.4'
allow_any_instance_of(ActionDispatch::Request).to receive(:ip).and_return(ip)
Rack::Attack::Allow2Ban.reset(ip, options)
maxretry.times.each do
expect(attempt_login(false)).to eq(401)
end
expect(attempt_login(true)).to eq(200)
expect(Rack::Attack::Allow2Ban.banned?(ip)).to be_falsey
end
end
context 'and the user requests a redirected path' do
let!(:redirect) { project.route.create_redirect('foo/bar') }
let(:path) { "#{redirect.path}.git" }
let(:project_moved_message) do
<<-MSG.strip_heredoc
Project '#{redirect.path}' was moved to '#{project.full_path}'.
Please update your Git remote:
git remote set-url origin #{project.http_url_to_repo}.
MSG
end
it 'downloads get status 200' do
clone_get(path, **env)
expect(response).to have_gitlab_http_status(:ok)
end
it 'uploads get status 404 with "project was moved" message' do
upload(path, **env) do |response|
expect(response).to have_gitlab_http_status(:ok)
end
end
end
end
context "when the user doesn't have access to the project" do
it "pulls get status 404" do
download(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:not_found)
end
end
it "uploads get status 404" do
upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
end
context "when a gitlab ci token is provided" do
let(:project) { create(:project, :repository) }
let(:build) { create(:ci_build, :running) }
let(:other_project) { create(:project, :repository) }
before do
build.update!(project: project) # can't associate it on factory create
end
context 'when build created by system is authenticated' do
let(:path) { "#{project.full_path}.git" }
let(:env) { { user: 'gitlab-ci-token', password: build.token } }
it_behaves_like 'pulls are allowed'
# A non-401 here is not an information leak since the system is
# "authenticated" as CI using the correct token. It does not have
# push access, so pushes should be rejected as forbidden, and giving
# a reason is fine.
#
# We know for sure it is not an information leak since pulls using
# the build token must be allowed.
it "rejects pushes with 403 Forbidden" do
push_get(path, **env)
expect(response).to have_gitlab_http_status(:forbidden)
expect(response.body).to eq(git_access_error(:auth_upload))
end
# We are "authenticated" as CI using a valid token here. But we are
# not authorized to see any other project, so return "not found".
it "rejects pulls for other project with 404 Not Found" do
clone_get("#{other_project.full_path}.git", **env)
expect(response).to have_gitlab_http_status(:not_found)
expect(response.body).to eq(git_access_error(:project_not_found))
end
end
context 'and build created by' do
before do
build.update!(user: user)
project.add_reporter(user)
end
shared_examples 'can download code only' do
let(:path) { "#{project.full_path}.git" }
let(:env) { { user: 'gitlab-ci-token', password: build.token } }
it_behaves_like 'pulls are allowed'
context 'when the repo does not exist' do
let(:project) { create(:project) }
it 'rejects pulls with 404 Not Found' do
clone_get(path, **env)
expect(response).to have_gitlab_http_status(:not_found)
expect(response.body).to eq(git_access_error(:no_repo))
end
end
it 'rejects pushes with 403 Forbidden' do
push_get(path, **env)
expect(response).to have_gitlab_http_status(:forbidden)
expect(response.body).to eq(git_access_error(:auth_upload))
end
end
context 'administrator' do
let(:user) { create(:admin) }
context 'when admin mode is enabled', :enable_admin_mode do
it_behaves_like 'can download code only'
it 'downloads from other project get status 404' do
clone_get "#{other_project.full_path}.git", user: 'gitlab-ci-token', password: build.token
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when admin mode is disabled' do
it_behaves_like 'can download code only'
it 'downloads from other project get status 404' do
clone_get "#{other_project.full_path}.git", user: 'gitlab-ci-token', password: build.token
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'regular user' do
let(:user) { create(:user) }
it_behaves_like 'can download code only'
it 'downloads from other project get status 404' do
clone_get "#{other_project.full_path}.git", user: 'gitlab-ci-token', password: build.token
expect(response).to have_gitlab_http_status(:not_found)
end
context 'when users password is expired' do
it 'rejects pulls with 401 unauthorized' do
user.update!(password_expires_at: 2.days.ago)
download(path, user: 'gitlab-ci-token', password: build.token) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
end
end
end
end
it_behaves_like 'project path without .git suffix' do
let(:repository_path) { create(:project, :repository, :public, path: 'project.').full_path }
end
context "retrieving an info/refs file" do
let(:project) { create(:project, :repository, :public, path: 'project.') }
context "when the file exists" do
before do
# Provide a dummy file in its place
allow_any_instance_of(Repository).to receive(:blob_at).and_call_original
allow_any_instance_of(Repository).to receive(:blob_at).with('b83d6e391c22777fca1ed3012fce84f633d7fed0', 'info/refs') do
Blob.decorate(Gitlab::Git::Blob.find(project.repository, 'master', 'bar/branch-test.txt'), project)
end
get "/#{project.full_path}/-/blob/master/info/refs"
end
it "returns the file" do
expect(response).to have_gitlab_http_status(:ok)
end
end
context "when the file does not exist" do
before do
get "/#{project.full_path}/-/blob/master/info/refs"
end
it "redirects" do
expect(response).to have_gitlab_http_status(:found)
end
end
end
end
context "when the Wiki path ends with a dot" do
let(:wiki) { ProjectWiki.new(project) }
let(:path) { "/#{wiki.repository.full_path}.git" }
context "when the project is public" do
let(:project) { create(:project, :wiki_repo, :public, :wiki_enabled, path: 'foo.') }
it_behaves_like 'pushes require Basic HTTP Authentication'
context 'when unauthenticated' do
let(:env) { {} }
it_behaves_like 'pulls are allowed'
it "responds to pulls with the wiki's repo" do
download(path) do |response|
json_body = ActiveSupport::JSON.decode(response.body)
expect(json_body['Repository']['relative_path']).to eq(wiki.repository.relative_path)
end
end
end
context 'when authenticated' do
let(:env) { { user: user.username, password: user.password } }
context 'and as a developer on the team' do
before do
project.add_developer(user)
end
context 'but the repo is disabled' do
let(:project) { create(:project, :wiki_repo, :public, :repository_disabled, :wiki_enabled, path: 'foo.') }
it_behaves_like 'pulls are allowed'
it_behaves_like 'pushes are allowed'
end
end
context 'and not on the team' do
it_behaves_like 'pulls are allowed'
it 'rejects pushes with 403 Forbidden' do
upload(path, **env) do |response|
expect(response).to have_gitlab_http_status(:forbidden)
expect(response.body).to eq(git_access_wiki_error(:write_to_wiki))
end
end
end
end
end
context "when the project is private" do
let(:project) { create(:project, :wiki_repo, :private, :wiki_enabled, path: 'foo.') }
it_behaves_like 'pulls require Basic HTTP Authentication'
it_behaves_like 'pushes require Basic HTTP Authentication'
it_behaves_like 'operations are not allowed with expired password'
context 'when authenticated' do
context 'and as a developer on the team' do
before do
project.add_developer(user)
end
context 'when user is using credentials with special characters' do
context 'with password with special characters' do
before do
user.update!(password: 'RKszEwéC5kFnû∆f243fycGu§Gh9ftDj!U')
end
it 'allows clones' do
download(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:ok)
end
end
end
end
context 'but the repo is disabled' do
let(:project) { create(:project, :wiki_repo, :private, :repository_disabled, :wiki_enabled, path: 'foo.') }
it 'allows clones' do
download(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:ok)
end
end
it 'pushes are allowed' do
upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:ok)
end
end
end
end
context 'and not on the team' do
it 'rejects clones with 404 Not Found' do
download(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:not_found)
expect(response.body).to eq(git_access_wiki_error(:not_found))
end
end
it 'rejects pushes with 404 Not Found' do
upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:not_found)
expect(response.body).to eq(git_access_wiki_error(:not_found))
end
end
end
end
end
end
end
describe "User with LDAP identity" do
......
......@@ -19,7 +19,8 @@ RSpec.shared_examples 'network policy common specs' do
creation_timestamp: nil,
manifest: YAML.dump(policy.resource.deep_stringify_keys),
is_autodevops: false,
is_enabled: true
is_enabled: true,
environment_ids: []
}
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment