Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-03-09 12:10:10 +00:00
parent 4889025ac7
commit fcb8c4e13d
27 changed files with 329 additions and 48 deletions

View File

@ -1560,7 +1560,6 @@ RSpec/FeatureCategory:
- 'spec/graphql/types/customer_relations/organization_state_counts_type_spec.rb'
- 'spec/graphql/types/customer_relations/organization_type_spec.rb'
- 'spec/graphql/types/dependency_proxy/blob_type_spec.rb'
- 'spec/graphql/types/dependency_proxy/group_setting_type_spec.rb'
- 'spec/graphql/types/dependency_proxy/manifest_type_spec.rb'
- 'spec/graphql/types/deployment_tier_enum_spec.rb'
- 'spec/graphql/types/design_management/design_collection_copy_state_enum_spec.rb'
@ -3243,7 +3242,6 @@ RSpec/FeatureCategory:
- 'spec/models/customer_relations/contact_state_counts_spec.rb'
- 'spec/models/cycle_analytics/project_level_stage_adapter_spec.rb'
- 'spec/models/dependency_proxy/blob_spec.rb'
- 'spec/models/dependency_proxy/group_setting_spec.rb'
- 'spec/models/dependency_proxy/image_ttl_group_policy_spec.rb'
- 'spec/models/dependency_proxy/manifest_spec.rb'
- 'spec/models/dependency_proxy/registry_spec.rb'

View File

@ -8,8 +8,8 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
include Gitlab::Utils::StrongMemoize
before_action :ensure_group
before_action :ensure_token_granted!, only: [:blob, :manifest]
before_action :ensure_feature_enabled!
before_action :ensure_token_granted!, only: [:blob, :manifest]
before_action :verify_workhorse_api!,
only: [:authorize_upload_blob, :upload_blob, :authorize_upload_manifest, :upload_manifest]
@ -154,8 +154,8 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
event_name
end
def dependency_proxy
@dependency_proxy ||= group.dependency_proxy_setting
def dependency_proxy_setting
@dependency_proxy_setting ||= group.dependency_proxy_setting
end
def ensure_group
@ -163,11 +163,11 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
end
def ensure_feature_enabled!
render_404 unless dependency_proxy.enabled
render_404 unless dependency_proxy_setting.enabled
end
def ensure_token_granted!
result = DependencyProxy::RequestTokenService.new(image).execute
result = DependencyProxy::RequestTokenService.new(image:, dependency_proxy_setting:).execute
if result[:status] == :success
@token = result[:token]

View File

@ -22,6 +22,18 @@ module Mutations
required: false,
description: copy_field_description(Types::DependencyProxy::ImageTtlGroupPolicyType, :enabled)
argument :identity, GraphQL::Types::String, required: false,
experiment: { milestone: '17.10' },
description: 'Identity credential used to authenticate with Docker Hub when pulling images. ' \
'Can be a username (for password or PAT) or organization name (for OAT). ' \
'Ignored if `dependency_proxy_containers_docker_hub_credentials` is disabled.'
argument :secret, GraphQL::Types::String, required: false,
experiment: { milestone: '17.10' },
description: 'Secret credential used to authenticate with Docker Hub when pulling images. ' \
'Can be a password, Personal Access Token (PAT), or Organization Access Token (OAT). ' \
'Ignored if `dependency_proxy_containers_docker_hub_credentials` is disabled.'
field :dependency_proxy_setting,
Types::DependencyProxy::GroupSettingType,
null: true,
@ -30,6 +42,11 @@ module Mutations
def resolve(group_path:, **args)
group = authorized_find!(group_path: group_path)
unless Feature.enabled?(:dependency_proxy_containers_docker_hub_credentials, group)
args.delete(:identity)
args.delete(:secret)
end
result = ::DependencyProxy::GroupSettings::UpdateService
.new(container: group, current_user: current_user, params: args)
.execute

View File

@ -9,5 +9,14 @@ module Types
authorize :admin_dependency_proxy
field :enabled, GraphQL::Types::Boolean, null: false, description: 'Indicates whether the dependency proxy is enabled for the group.'
field :identity, GraphQL::Types::String, null: true,
experiment: { milestone: '17.10' },
description: 'Identity credential used to authenticate with Docker Hub when pulling images. ' \
'Can be a username (for password or PAT) or organization name (for OAT). ' \
'Returns null if `dependency_proxy_containers_docker_hub_credentials` feature flag is disabled.'
def identity
object.identity if Feature.enabled?(:dependency_proxy_containers_docker_hub_credentials, object.group)
end
end
end

View File

@ -3,5 +3,20 @@
class DependencyProxy::GroupSetting < ApplicationRecord
belongs_to :group
encrypts :identity
encrypts :secret
validates :group, presence: true
validates :identity, presence: true, if: :secret?
validates :secret, presence: true, if: :identity?
validates :identity, :secret, length: { maximum: 255 }
def authorization_header
return {} unless Feature.enabled?(:dependency_proxy_containers_docker_hub_credentials, group)
return {} unless identity? && secret?
authorization = ActionController::HttpAuthentication::Basic.encode_credentials(identity, secret)
{ Authorization: authorization }
end
end

View File

@ -3,7 +3,7 @@
module DependencyProxy
module GroupSettings
class UpdateService < BaseContainerService
ALLOWED_ATTRIBUTES = %i[enabled].freeze
ALLOWED_ATTRIBUTES = %i[enabled identity secret].freeze
def execute
return ServiceResponse.error(message: 'Access Denied', http_status: 403) unless allowed?

View File

@ -2,12 +2,17 @@
module DependencyProxy
class RequestTokenService < DependencyProxy::BaseService
def initialize(image)
def initialize(image:, dependency_proxy_setting:)
@image = image
@dependency_proxy_setting = dependency_proxy_setting
end
def execute
response = Gitlab::HTTP.get(auth_url)
response = Gitlab::HTTP.get(
auth_url,
headers: @dependency_proxy_setting&.authorization_header || {},
follow_redirects: true
)
if response.success?
success(token: Gitlab::Json.parse(response.body)['token'])

View File

@ -0,0 +1,9 @@
---
name: dependency_proxy_containers_docker_hub_credentials
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/331741
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/182748
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/521312
milestone: '17.10'
group: group::container registry
type: gitlab_com_derisk
default_enabled: false

View File

@ -3,5 +3,5 @@ name: query_analyzer_gitlab_schema_metrics
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73839
milestone: '14.5'
type: ops
group: group::cells infrastructure
group: group::database frameworks
default_enabled: false

View File

@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121957
rollout_issue_url:
milestone: '16.1'
type: ops
group: group::cells infrastructure
group: group::database frameworks
default_enabled: true

View File

@ -20,12 +20,6 @@ end
OmniAuth.config.logger = Gitlab::AppLogger
omniauth_login_counter =
Gitlab::Metrics.counter(
:gitlab_omniauth_login_total,
'Counter of initiated OmniAuth login attempts')
OmniAuth.config.before_request_phase do |env|
provider = env['omniauth.strategy']&.name
omniauth_login_counter.increment(omniauth_provider: provider, status: 'initiated')
Gitlab::Auth::OAuth::BeforeRequestPhaseOauthLoginCounterIncrement.call(env)
end

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
class AddIdentitySecretToDependencyProxyGroupSettings < Gitlab::Database::Migration[2.2]
milestone '17.10'
disable_ddl_transaction!
TABLE_NAME = :dependency_proxy_group_settings
def up
with_lock_retries do
add_column TABLE_NAME, :identity, :jsonb, null: true, if_not_exists: true
add_column TABLE_NAME, :secret, :jsonb, null: true, if_not_exists: true
end
add_check_constraint TABLE_NAME,
'num_nonnulls(identity, secret) = 2 OR num_nulls(identity, secret) = 2',
check_constraint_name(TABLE_NAME, 'identity_and_secret', 'both_set_or_null')
end
def down
with_lock_retries do
remove_column(TABLE_NAME, :identity, if_exists: true)
remove_column(TABLE_NAME, :secret, if_exists: true)
end
end
end

View File

@ -0,0 +1 @@
c97190eadf5bbcb643d7b69745c435ca95ce300ee332f91dfb194ba478b2c4a2

View File

@ -12757,7 +12757,10 @@ CREATE TABLE dependency_proxy_group_settings (
group_id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
enabled boolean DEFAULT true NOT NULL
enabled boolean DEFAULT true NOT NULL,
identity jsonb,
secret jsonb,
CONSTRAINT check_7ed6c2f608 CHECK (((num_nonnulls(identity, secret) = 2) OR (num_nulls(identity, secret) = 2)))
);
CREATE SEQUENCE dependency_proxy_group_settings_id_seq

View File

@ -11239,6 +11239,8 @@ Input type: `UpdateDependencyProxySettingsInput`
| <a id="mutationupdatedependencyproxysettingsclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationupdatedependencyproxysettingsenabled"></a>`enabled` | [`Boolean`](#boolean) | Indicates whether the policy is enabled or disabled. |
| <a id="mutationupdatedependencyproxysettingsgrouppath"></a>`groupPath` | [`ID!`](#id) | Group path for the group dependency proxy. |
| <a id="mutationupdatedependencyproxysettingsidentity"></a>`identity` {{< icon name="warning-solid" >}} | [`String`](#string) | **Deprecated:** **Status**: Experiment. Introduced in GitLab 17.10. |
| <a id="mutationupdatedependencyproxysettingssecret"></a>`secret` {{< icon name="warning-solid" >}} | [`String`](#string) | **Deprecated:** **Status**: Experiment. Introduced in GitLab 17.10. |
#### Fields
@ -24418,6 +24420,7 @@ Group-level Dependency Proxy settings.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="dependencyproxysettingenabled"></a>`enabled` | [`Boolean!`](#boolean) | Indicates whether the dependency proxy is enabled for the group. |
| <a id="dependencyproxysettingidentity"></a>`identity` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 17.10. **Status**: Experiment. Identity credential used to authenticate with Docker Hub when pulling images. Can be a username (for password or PAT) or organization name (for OAT). Returns null if `dependency_proxy_containers_docker_hub_credentials` feature flag is disabled. |
### `Deployment`

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
module Gitlab
module Auth
module OAuth
module BeforeRequestPhaseOauthLoginCounterIncrement
OMNIAUTH_LOGIN_TOTAL_COUNTER =
Gitlab::Metrics.counter(:gitlab_omniauth_login_total, 'Counter of initiated OmniAuth login attempts')
def self.call(env)
provider = current_provider_name_from(env)
return unless provider
OMNIAUTH_LOGIN_TOTAL_COUNTER.increment(omniauth_provider: provider, status: 'initiated')
end
private_class_method def self.current_provider_name_from(env)
env['omniauth.strategy']&.name
end
end
end
end
end

View File

@ -5,5 +5,7 @@ FactoryBot.define do
group
enabled { true }
identity { 'username' }
secret { 'secret' }
end
end

View File

@ -9,7 +9,7 @@ RSpec.describe Mutations::DependencyProxy::GroupSettings::Update, feature_catego
let_it_be_with_reload(:group) { create(:group) }
let_it_be_with_reload(:group_settings) { create(:dependency_proxy_group_setting, group: group) }
let_it_be(:current_user) { create(:user) }
let(:params) { { group_path: group.full_path, enabled: false } }
let(:params) { { group_path: group.full_path, enabled: false, identity: 'i', secret: 's' } }
specify { expect(described_class).to require_graphql_authorizations(:admin_dependency_proxy) }
@ -18,8 +18,8 @@ RSpec.describe Mutations::DependencyProxy::GroupSettings::Update, feature_catego
shared_examples 'updating the dependency proxy group settings' do
it_behaves_like 'updating the dependency proxy group settings attributes',
from: { enabled: true },
to: { enabled: false }
from: { enabled: true, identity: 'username', secret: 'secret' },
to: { enabled: false, identity: 'i', secret: 's' }
it 'returns the dependency proxy settings no errors' do
expect(subject).to eq(
@ -27,6 +27,16 @@ RSpec.describe Mutations::DependencyProxy::GroupSettings::Update, feature_catego
errors: []
)
end
context 'with dependency_proxy_containers_docker_hub_credentials disabled' do
before do
stub_feature_flags(dependency_proxy_containers_docker_hub_credentials: false)
end
it_behaves_like 'updating the dependency proxy group settings attributes',
from: { enabled: true, identity: 'username', secret: 'secret' },
to: { enabled: false, identity: 'username', secret: 'secret' }
end
end
shared_examples 'denying access to dependency proxy group settings' do

View File

@ -2,10 +2,10 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['DependencyProxySetting'] do
RSpec.describe GitlabSchema.types['DependencyProxySetting'], feature_category: :virtual_registry do
it 'includes dependency proxy blob fields' do
expected_fields = %w[
enabled
enabled identity
]
expect(described_class).to include_graphql_fields(*expected_fields)

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Auth::OAuth::BeforeRequestPhaseOauthLoginCounterIncrement, feature_category: :system_access do
describe '.call' do
let(:env) { { 'omniauth.strategy' => omniauth_strategy } }
let(:omniauth_strategy) { instance_double(OmniAuth::Strategies::GoogleOauth2, name: 'google_oauth2') }
it 'increments Prometheus counter for the given provider' do
expect { described_class.call(env) }
.to change { gitlab_metric_omniauth_login_total_for('google_oauth2', 'initiated') }.by(1)
end
context 'when omniauth strategy is nil' do
let(:omniauth_strategy) { nil }
it 'does not increment counter' do
expect { described_class.call(env) }
.to change { gitlab_metric_omniauth_login_total_for('google_oauth2', 'initiated') }.by(0)
expect(gitlab_metric_omniauth_login_total_for(nil, 'initiated')).to eq 0
end
end
def gitlab_metric_omniauth_login_total_for(omniauth_provider, status)
Gitlab::Metrics.registry.get(:gitlab_omniauth_login_total)
&.get(omniauth_provider: omniauth_provider, status: status)
.to_f
end
end
end

View File

@ -2,7 +2,9 @@
require 'spec_helper'
RSpec.describe DependencyProxy::GroupSetting, type: :model do
RSpec.describe DependencyProxy::GroupSetting, type: :model, feature_category: :virtual_registry do
subject(:setting) { build(:dependency_proxy_group_setting) }
describe 'relationships' do
it { is_expected.to belong_to(:group) }
end
@ -14,5 +16,77 @@ RSpec.describe DependencyProxy::GroupSetting, type: :model do
describe 'validations' do
it { is_expected.to validate_presence_of(:group) }
it { is_expected.to validate_presence_of(:identity) }
it { is_expected.to validate_presence_of(:secret) }
it { is_expected.to validate_length_of(:identity).is_at_most(255) }
it { is_expected.to validate_length_of(:secret).is_at_most(255) }
context 'for identity and secret cross validation' do
using RSpec::Parameterized::TableSyntax
where(:identity, :secret, :valid) do
nil | nil | true
'i' | nil | false
nil | 's' | false
'i' | 's' | true
end
with_them do
it 'works as expected' do
setting.identity = identity
setting.secret = secret
if valid
expect(setting).to be_valid
else
expect(setting).not_to be_valid
end
end
end
end
end
describe '#authorization_header' do
let_it_be_with_reload(:dependency_proxy_setting) { create(:dependency_proxy_group_setting) }
subject { dependency_proxy_setting.authorization_header }
shared_examples 'empty authorization headers' do
it { is_expected.to eq({}) }
end
context 'with identity and secret set' do
let(:expected_headers) { { Authorization: 'Basic aTpz' } }
before do
dependency_proxy_setting.update!(identity: 'i', secret: 's')
end
it { is_expected.to eq(expected_headers) }
context 'when dependency_proxy_containers_docker_hub_credentials is disabled' do
before do
stub_feature_flags(dependency_proxy_containers_docker_hub_credentials: false)
end
it_behaves_like 'empty authorization headers'
end
end
context 'with identity and secret not set' do
before do
dependency_proxy_setting.update!(identity: nil, secret: nil)
end
it_behaves_like 'empty authorization headers'
context 'when dependency_proxy_containers_docker_hub_credentials is disabled' do
before do
stub_feature_flags(dependency_proxy_containers_docker_hub_credentials: false)
end
it_behaves_like 'empty authorization headers'
end
end
end
end

View File

@ -35,12 +35,12 @@ RSpec.describe 'getting dependency proxy settings for a group', feature_category
stub_config(dependency_proxy: { enabled: true })
end
subject { post_graphql(query, current_user: user, variables: variables) }
subject(:post_query) { post_graphql(query, current_user: user, variables: variables) }
shared_examples 'dependency proxy group setting query' do
it_behaves_like 'a working graphql query' do
before do
subject
post_query
end
end
@ -68,10 +68,13 @@ RSpec.describe 'getting dependency proxy settings for a group', feature_category
end
it 'return the proper response' do
subject
post_query
if access_granted
expect(dependency_proxy_group_setting_response).to eq('enabled' => true)
expect(dependency_proxy_group_setting_response).to eq(
'enabled' => true,
'identity' => group.dependency_proxy_setting.identity
)
else
expect(dependency_proxy_group_setting_response).to be_blank
end
@ -82,10 +85,26 @@ RSpec.describe 'getting dependency proxy settings for a group', feature_category
context 'with the settings model created' do
before do
group.create_dependency_proxy_setting!(enabled: true)
group.create_dependency_proxy_setting!(enabled: true, identity: 'i', secret: 's')
end
it_behaves_like 'dependency proxy group setting query'
context 'with dependency_proxy_containers_docker_hub_credentials disabled' do
before do
stub_feature_flags(dependency_proxy_containers_docker_hub_credentials: false)
end
it 'does not return the identity' do
group.add_owner(user)
post_query
expect(dependency_proxy_group_setting_response).to eq(
'enabled' => true,
'identity' => nil
)
end
end
end
context 'without the settings model created' do

View File

@ -5,10 +5,12 @@ require 'spec_helper'
RSpec.describe 'OmniAuth Rack middlewares', feature_category: :system_access do
describe 'OmniAuth before_request_phase callback' do
it 'increments Prometheus counter' do
post('/users/auth/google_oauth2')
counter = Gitlab::Metrics.registry.get(:gitlab_omniauth_login_total)
expect(counter.get(omniauth_provider: 'google_oauth2', status: 'initiated')).to eq(1)
expect { post('/users/auth/google_oauth2') }
.to change {
Gitlab::Metrics.registry.get(:gitlab_omniauth_login_total)
&.get(omniauth_provider: 'google_oauth2', status: 'initiated')
.to_f
}.by(1)
end
end
end

View File

@ -8,15 +8,15 @@ RSpec.describe ::DependencyProxy::GroupSettings::UpdateService, feature_category
let_it_be_with_reload(:group) { create(:group) }
let_it_be_with_reload(:group_settings) { create(:dependency_proxy_group_setting, group: group) }
let_it_be(:user) { create(:user) }
let_it_be(:params) { { enabled: false } }
let_it_be(:params) { { enabled: false, identity: 'i', secret: 's' } }
describe '#execute' do
subject { described_class.new(container: group, current_user: user, params: params).execute }
shared_examples 'updating the dependency proxy group settings' do
it_behaves_like 'updating the dependency proxy group settings attributes',
from: { enabled: true },
to: { enabled: false }
from: { enabled: true, identity: 'username', secret: 'secret' },
to: { enabled: false, identity: 'i', secret: 's' }
it 'returns a success' do
result = subject

View File

@ -4,23 +4,38 @@ require 'spec_helper'
RSpec.describe DependencyProxy::RequestTokenService, feature_category: :virtual_registry do
include DependencyProxyHelpers
let_it_be_with_reload(:dependency_proxy_setting) { create(:dependency_proxy_group_setting) }
let(:image) { 'alpine:3.9' }
let(:token) { Digest::SHA256.hexdigest('123') }
subject { described_class.new(image).execute }
subject { described_class.new(image:, dependency_proxy_setting:).execute }
context 'remote request is successful' do
before do
stub_registry_auth(image, token)
context 'with identity and secret set' do
before do
dependency_proxy_setting.update!(identity: 'i', secret: 's')
stub_registry_auth(image, token, request_headers: dependency_proxy_setting.authorization_header)
end
it { expect(subject[:status]).to eq(:success) }
it { expect(subject[:token]).to eq(token) }
end
it { expect(subject[:status]).to eq(:success) }
it { expect(subject[:token]).to eq(token) }
context 'with identity and secret are not set' do
before do
dependency_proxy_setting.update!(identity: nil, secret: nil)
stub_registry_auth(image, token, request_headers: dependency_proxy_setting.authorization_header)
end
it { expect(subject[:status]).to eq(:success) }
it { expect(subject[:token]).to eq(token) }
end
end
context 'remote request is not found' do
before do
stub_registry_auth(image, token, 404)
stub_registry_auth(image, token, status: 404, request_headers: dependency_proxy_setting.authorization_header)
end
it { expect(subject[:status]).to eq(:error) }
@ -30,7 +45,13 @@ RSpec.describe DependencyProxy::RequestTokenService, feature_category: :virtual_
context 'failed to parse response body' do
before do
stub_registry_auth(image, token, 200, 'dasd1321: wow')
stub_registry_auth(
image,
token,
status: 200,
body: 'dasd1321: wow',
request_headers: dependency_proxy_setting.authorization_header
)
end
it { expect(subject[:status]).to eq(:error) }

View File

@ -3,12 +3,13 @@
module DependencyProxyHelpers
include StubRequests
def stub_registry_auth(image, token, status = 200, body = nil)
def stub_registry_auth(image, token, status: 200, body: nil, request_headers: {})
auth_body = { 'token' => token }.to_json
auth_link = registry.auth_url(image)
stub_full_request(auth_link)
.to_return(status: status, body: body || auth_body)
stub = stub_full_request(auth_link)
stub = stub.with(headers: request_headers) unless request_headers.empty?
stub.to_return(status: status, body: body || auth_body)
end
def stub_manifest_download(image, tag, status: 200, body: nil, headers: {})

View File

@ -2,7 +2,24 @@
RSpec.shared_examples 'updating the dependency proxy group settings attributes' do |from: {}, to: {}|
it 'updates the dependency proxy settings' do
old_identity = group_settings.identity
old_secret = group_settings.secret
expect { subject }
.to change { group_settings.reload.enabled }.from(from[:enabled]).to(to[:enabled])
if from[:identity] && to[:identity]
expect(old_identity).to eq(from[:identity])
expect(group_settings.identity).to eq(to[:identity])
else
expect(group_settings.identity).to eq(old_identity)
end
if from[:secret] && to[:secret]
expect(old_secret).to eq(from[:secret])
expect(group_settings.secret).to eq(to[:secret])
else
expect(group_settings.secret).to eq(old_secret)
end
end
end