Skip to content
Snippets Groups Projects
Verified Commit f2f292d1 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas :palm_tree: Committed by GitLab
Browse files

Merge branch 'require-identity-verification-to-run-pipelines' into 'master'

Add pipeline validator to check identity verification

Closes gitlab-org/modelops/anti-abuse/team-tasks#669

See merge request !151834



Merged-by: Jose Ivan Vargas's avatarJose Ivan Vargas <jvargas@gitlab.com>
Approved-by: default avatarRudy Crespo <rcrespo@gitlab.com>
Approved-by: Aboobacker MK's avatarAboobacker MK <akarakath@gitlab.com>
Approved-by: Fabio Pitino's avatarFabio Pitino <fpitino@gitlab.com>
Approved-by: Jose Ivan Vargas's avatarJose Ivan Vargas <jvargas@gitlab.com>
Reviewed-by: Fabio Pitino's avatarFabio Pitino <fpitino@gitlab.com>
Reviewed-by: default avatarIan Anderson <ianderson@gitlab.com>
Co-authored-by: default avatarimand3r <ianderson@gitlab.com>
parents bb77b885 3582f4ee
No related branches found
No related tags found
2 merge requests!162233Draft: Script to update Topology Service Gem,!151834Add pipeline validator to check identity verification
Pipeline #1295356433 passed
Showing
with 352 additions and 230 deletions
......@@ -2181,7 +2181,6 @@ Layout/LineLength:
- 'ee/spec/support/shared_examples/requests/api/project_approval_rules_api_shared_examples.rb'
- 'ee/spec/support/shared_examples/services/boards/base_service_shared_examples.rb'
- 'ee/spec/support/shared_examples/services/build_execute_shared_examples.rb'
- 'ee/spec/support/shared_examples/services/ci/play_job_service_shared_examples.rb'
- 'ee/spec/support/shared_examples/services/dast_on_demand_scans_shared_examples.rb'
- 'ee/spec/support/shared_examples/services/geo/geo_request_service_shared_examples.rb'
- 'ee/spec/support/shared_examples/services/group_saml/saml_provider/base_service_shared_examples.rb'
......
......@@ -10,5 +10,5 @@ export const TAG_REF_TYPE = 'tag';
// must match pipeline/chain/validate/after_config.rb
export const CC_VALIDATION_REQUIRED_ERROR = __(
'Credit card required to be on file in order to create a pipeline',
'Credit card required to be on file in order to run CI jobs',
);
......@@ -18,6 +18,7 @@ module IdentityVerifiable
MEDIUM_RISK_USER_METHODS = %w[email phone].freeze
LOW_RISK_USER_METHODS = %w[email].freeze
ACTIVE_USER_METHODS = %w[phone].freeze
IDENTITY_VERIFICATION_RELEASE_DATE = Date.new(2024, 6, 3)
def signup_identity_verification_enabled?
return false unless ::Gitlab::Saas.feature_available?(:identity_verification)
......@@ -66,8 +67,9 @@ def identity_verification_enabled?
end
def identity_verified?
return false unless active_user?
return true unless identity_verification_enabled?
return false unless active_user?
return true if created_at < IDENTITY_VERIFICATION_RELEASE_DATE
# Allow an existing credit card validation to override the identity verification state if
# credit_card is not a required verification method.
......
......@@ -11,15 +11,10 @@ module PlayBridgeService
def check_access!(bridge)
super
if current_user && !current_user.has_required_credit_card_to_run_pipelines?(project)
::Gitlab::AppLogger.info(
message: 'Credit card required to be on file in order to play a job',
project_path: project.full_path,
user_id: current_user.id,
plan: project.root_namespace.actual_plan_name
)
raise ::Gitlab::Access::AccessDeniedError, 'Credit card required to be on file in order to play a job'
begin
::Users::IdentityVerification::AuthorizeCi.new(user: current_user, project: project).authorize_run_jobs!
rescue ::Users::IdentityVerification::Error => e
raise ::Gitlab::Access::AccessDeniedError, e
end
end
end
......
......@@ -11,15 +11,10 @@ module PlayBuildService
def check_access!(build, job_variables_attributes)
super
if current_user && !current_user.has_required_credit_card_to_run_pipelines?(project)
::Gitlab::AppLogger.info(
message: 'Credit card required to be on file in order to play a job',
project_path: project.full_path,
user_id: current_user.id,
plan: project.root_namespace.actual_plan_name
)
raise ::Gitlab::Access::AccessDeniedError, 'Credit card required to be on file in order to play a job'
begin
::Users::IdentityVerification::AuthorizeCi.new(user: current_user, project: project).authorize_run_jobs!
rescue ::Users::IdentityVerification::Error => e
raise ::Gitlab::Access::AccessDeniedError, e
end
end
end
......
......@@ -12,15 +12,10 @@ module RetryJobService
def check_access!(build)
super
if current_user && !current_user.has_required_credit_card_to_run_pipelines?(project)
::Gitlab::AppLogger.info(
message: 'Credit card required to be on file in order to retry build',
project_path: project.full_path,
user_id: current_user.id,
plan: project.root_namespace.actual_plan_name
)
raise ::Gitlab::Access::AccessDeniedError, 'Credit card required to be on file in order to retry a build'
begin
::Users::IdentityVerification::AuthorizeCi.new(user: current_user, project: project).authorize_run_jobs!
rescue ::Users::IdentityVerification::Error => e
raise ::Gitlab::Access::AccessDeniedError, e
end
end
......
......@@ -7,11 +7,13 @@ module RetryPipelineService
override :check_access
def check_access(pipeline)
if current_user && !current_user.has_required_credit_card_to_run_pipelines?(project)
ServiceResponse.error(message: 'Credit card required to be on file in order to retry a pipeline', http_status: :forbidden)
else
super
begin
::Users::IdentityVerification::AuthorizeCi.new(user: current_user, project: project).authorize_run_jobs!
rescue ::Users::IdentityVerification::Error => e
return ServiceResponse.error(message: e.message, http_status: :forbidden)
end
super
end
private
......
---
name: ci_requires_identity_verification_on_free_plan
feature_issue_url: https://gitlab.com/gitlab-org/modelops/anti-abuse/team-tasks/-/issues/669
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/151834
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/460157
milestone: '17.0'
group: group::anti-abuse
type: gitlab_com_derisk
default_enabled: false
......@@ -11,15 +11,11 @@ module AfterConfig
override :perform!
def perform!
if current_user && !current_user.has_required_credit_card_to_run_pipelines?(project)
::Gitlab::AppLogger.info(
message: 'Credit card required to be on file in order to create a pipeline',
project_path: project.full_path,
user_id: current_user.id,
plan: project.root_namespace.actual_plan_name
)
return error('Credit card required to be on file in order to create a pipeline', drop_reason: :user_not_verified)
begin
::Users::IdentityVerification::AuthorizeCi.new(user: current_user, project: project)
.authorize_run_jobs!
rescue ::Users::IdentityVerification::Error => e
return error(e.message, drop_reason: :user_not_verified)
end
super
......
# frozen_string_literal: true
module Users
module IdentityVerification
Error = Class.new(StandardError)
class AuthorizeCi
attr_reader :user, :project
def initialize(user:, project:)
@user = user
@project = project
end
def authorize_run_jobs!
return unless user
authorize_credit_card!
authorize_identity_verification!
end
private
def authorize_credit_card!
return if user.has_required_credit_card_to_run_pipelines?(project)
ci_access_denied!('Credit card required to be on file in order to run CI jobs')
end
def authorize_identity_verification!
return unless identity_verification_enabled_for_ci?
return if user.identity_verified?
return unless project_requires_identity_verification_to_run_pipelines?
ci_access_denied!('Identity verification is required in order to run CI jobs')
end
def identity_verification_enabled_for_ci?
::Feature.enabled?(:ci_requires_identity_verification_on_free_plan, project, type: :gitlab_com_derisk)
end
def project_requires_identity_verification_to_run_pipelines?
return false unless project.shared_runners_enabled
root_namespace = project.root_namespace
return false if root_namespace.actual_plan.paid_excluding_trials?
ci_usage = root_namespace.ci_minutes_usage
return false if ci_usage.quota_enabled? && ci_usage.quota.any_purchased?
true
end
def ci_access_denied!(message)
log_ci_access_denied(message)
raise ::Users::IdentityVerification::Error, message
end
def log_ci_access_denied(message)
::Gitlab::AppLogger.info(
message: message,
class: self.class.name,
project_path: project.full_path,
user_id: user.id,
plan: project.root_namespace.actual_plan_name
)
end
end
end
end
......@@ -6,7 +6,9 @@
include IdentityVerificationHelpers
include ListboxHelpers
let_it_be_with_reload(:user) { create(:user) }
let_it_be_with_reload(:user) do
create(:user, created_at: IdentityVerifiable::IDENTITY_VERIFICATION_RELEASE_DATE + 1.day)
end
before do
stub_saas_features(identity_verification: true)
......@@ -39,6 +41,16 @@
expect_to_see_dashboard_page
end
context 'when the user was created before the feature relase date' do
let_it_be(:user) do
create(:user, created_at: IdentityVerifiable::IDENTITY_VERIFICATION_RELEASE_DATE - 1.day)
end
it 'does not verify the user' do
expect_to_see_dashboard_page
end
end
context 'when the user requests a phone verification exemption' do
it 'verifies the user' do
expect_to_see_identity_verification_page
......
......@@ -3,8 +3,8 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::AfterConfig, feature_category: :continuous_integration do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, developers: user) }
let(:pipeline) do
build(:ci_pipeline, project: project)
......@@ -19,57 +19,36 @@
let(:ref) { 'master' }
describe '#perform!' do
before do
project.add_developer(user)
end
describe 'credit card requirement', :saas do
context 'when user does not have credit card for pipelines in project' do
before do
allow(user)
.to receive(:has_required_credit_card_to_run_pipelines?)
.with(project)
.and_return(false)
end
it 'breaks the chain with an error' do
step.perform!
expect(step.break?).to be_truthy
expect(pipeline.errors.to_a)
.to include('Credit card required to be on file in order to create a pipeline')
expect(pipeline.failure_reason).to eq('user_not_verified')
expect(pipeline).to be_persisted # when passing a failure reason the pipeline is persisted
context 'when the user is not authorized' do
before do
allow_next_instance_of(::Users::IdentityVerification::AuthorizeCi) do |instance|
allow(instance).to receive(:authorize_run_jobs!).and_raise(
::Users::IdentityVerification::Error, 'authorization error')
end
end
it 'logs the event' do
allow(Gitlab).to receive(:com?).and_return(true).at_least(:once)
allow(Gitlab::AppLogger).to receive(:info)
expect(Gitlab::AppLogger).to receive(:info).with(
message: 'Credit card required to be on file in order to create a pipeline',
project_path: project.full_path,
user_id: user.id,
plan: 'free')
it 'breaks the chain with an error' do
step.perform!
step.perform!
end
expect(step.break?).to be_truthy
expect(pipeline.errors.to_a).to include('authorization error')
expect(pipeline.failure_reason).to eq('user_not_verified')
expect(pipeline).to be_persisted # when passing a failure reason the pipeline is persisted
end
end
context 'when user has credit card for pipelines in project' do
before do
allow(user)
.to receive(:has_required_credit_card_to_run_pipelines?)
.with(project)
.and_return(true)
context 'when the user is authorized' do
before do
allow_next_instance_of(::Users::IdentityVerification::AuthorizeCi) do |instance|
allow(instance).to receive(:authorize_run_jobs!)
end
end
it 'succeeds the step' do
step.perform!
it 'succeeds the step' do
step.perform!
expect(step.break?).to be_falsey
expect(pipeline.errors).to be_empty
end
expect(step.break?).to be_falsey
expect(pipeline.errors).to be_empty
end
end
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Users::IdentityVerification::AuthorizeCi, :saas, feature_category: :instance_resiliency do
let_it_be(:user) { create(:user, :with_sign_ins) }
let_it_be_with_reload(:project) { create(:project) }
def stub_verifications(credit_card:, identity_verification:)
allow_next_instance_of(described_class) do |instance|
allow(instance).to receive(:authorize_credit_card!) unless credit_card
allow(instance).to receive(:authorize_identity_verification!) unless identity_verification
end
end
describe '#authorize_run_jobs!' do
subject(:authorize) { described_class.new(user: user, project: project).authorize_run_jobs! }
shared_examples 'logs the failure and raises an exception' do
before do
allow(::Gitlab::AppLogger).to receive(:info)
end
specify :aggregate_failures do
expect(::Gitlab::AppLogger)
.to receive(:info)
.with(
message: error_message,
class: described_class.name,
project_path: project.full_path,
user_id: user.id,
plan: 'free')
expect { authorize }.to raise_error(::Users::IdentityVerification::Error, error_message)
end
end
shared_examples 'credit card verification' do
let(:error_message) { 'Credit card required to be on file in order to run CI jobs' }
context 'when the user has validated a credit card' do
before do
build(:credit_card_validation, user: user)
end
it { expect { authorize }.not_to raise_error }
end
context 'when the user has not validated a credit card' do
before do
allow(user).to receive(:has_required_credit_card_to_run_pipelines?).with(project).and_return(false)
end
it_behaves_like 'logs the failure and raises an exception'
end
end
context 'when the user is nil' do
let(:user) { nil }
before do
allow(described_class).to receive(:authorize_credit_card!).and_raise(::Users::IdentityVerification::Error)
allow(described_class).to receive(:authorize_identity_verification!).and_raise(
::Users::IdentityVerification::Error)
end
it { expect { authorize }.not_to raise_error }
end
context 'when credit card verification is required' do
before do
stub_verifications(credit_card: true, identity_verification: false)
end
it_behaves_like 'credit card verification'
end
context 'when credit card and identity verification are required' do
before do
stub_verifications(credit_card: true, identity_verification: true)
end
it_behaves_like 'credit card verification'
end
context 'when identity verification is required' do
before do
stub_verifications(credit_card: false, identity_verification: true)
end
context 'when the feature flag is disabled' do
before do
stub_feature_flags(ci_requires_identity_verification_on_free_plan: false)
end
it { expect { authorize }.not_to raise_error }
end
context 'when user identity is verified' do
before do
allow(user).to receive(:identity_verified?).and_return(true)
end
it { expect { authorize }.not_to raise_error }
end
context 'when user identity is not verified' do
before do
allow(user).to receive(:identity_verified?).and_return(false)
end
it_behaves_like 'logs the failure and raises an exception' do
let(:error_message) { 'Identity verification is required in order to run CI jobs' }
end
context 'when project shared runners are disabled' do
before do
allow(project).to receive(:shared_runners_enabled).and_return(false)
end
it { expect { authorize }.not_to raise_error }
end
context 'when root namespace has a paid plan' do
let_it_be(:ultimate_group) { create(:group_with_plan, :public, plan: :ultimate_plan) }
let_it_be(:project) { create(:project, group: ultimate_group) }
it { expect { authorize }.not_to raise_error }
end
context 'when root namespace has purchased CI minutes' do
before do
project.namespace.update!(extra_shared_runners_minutes_limit: 100)
project.namespace.clear_memoization(:ci_minutes_usage)
end
it { expect { authorize }.not_to raise_error }
end
end
end
end
end
......@@ -84,7 +84,9 @@ def assume_high_risk(user)
end
describe('#identity_verified?') do
let_it_be(:user) { create(:user, :with_sign_ins) }
let_it_be(:user) do
create(:user, :with_sign_ins, created_at: described_class::IDENTITY_VERIFICATION_RELEASE_DATE + 1.day)
end
subject(:identity_verified?) { user.identity_verified? }
......@@ -155,6 +157,14 @@ def assume_high_risk(user)
it { is_expected.to eq(result) }
end
end
context 'when the user was created before the release date' do
let_it_be(:user) do
create(:user, :with_sign_ins, created_at: described_class::IDENTITY_VERIFICATION_RELEASE_DATE - 1.day)
end
it { is_expected.to eq true }
end
end
describe('#active_for_authentication?') do
......
......@@ -160,69 +160,6 @@
end
end
describe 'credit card requirement' do
shared_examples 'creates a successful pipeline' do
it 'creates a successful pipeline', :aggregate_failures do
response, pipeline = create_pipeline!
expect(response).to be_success
expect(pipeline).to be_created_successfully
end
end
context 'when credit card is required' do
context 'when project is on free plan' do
before do
allow(::Gitlab).to receive(:com?).and_return(true)
namespace.gitlab_subscription.update!(hosted_plan: create(:free_plan))
user.created_at = ::Users::CreditCardValidation::RELEASE_DAY
end
context 'when user has credit card' do
before do
allow(user).to receive(:credit_card_validated_at).and_return(Time.current)
end
it_behaves_like 'creates a successful pipeline'
end
context 'when user does not have credit card' do
it 'creates a pipeline with errors', :aggregate_failures do
_, pipeline = create_pipeline!
expect(pipeline).not_to be_created_successfully
expect(pipeline.failure_reason).to eq('user_not_verified')
end
context 'when config is blank' do
before do
stub_ci_pipeline_yaml_file(nil)
end
it 'does not create a pipeline', :aggregate_failures do
response, pipeline = create_pipeline!
expect(response).to be_error
expect(pipeline).not_to be_persisted
end
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(ci_require_credit_card_on_free_plan: false)
end
it_behaves_like 'creates a successful pipeline'
end
end
end
end
context 'when credit card is not required' do
it_behaves_like 'creates a successful pipeline'
end
end
def create_pipeline!
response = service.execute(:push)
......
......@@ -3,13 +3,13 @@
require 'spec_helper'
RSpec.describe Ci::PlayBridgeService, '#execute', feature_category: :continuous_integration do
it_behaves_like 'prevents playing job when credit card is required' do
let(:user) { create(:user, maintainer_of: [project, downstream_project]) }
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:downstream_project) { create(:project) }
let(:job) { create(:ci_bridge, :playable, pipeline: pipeline, downstream: downstream_project) }
subject { described_class.new(project, user).execute(job) }
end
let_it_be(:project) { create(:project) }
let_it_be(:downstream_project) { create(:project) }
let_it_be(:user) { create(:user, maintainer_of: [project, downstream_project]) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let_it_be_with_reload(:job) { create(:ci_bridge, :playable, pipeline: pipeline, downstream: downstream_project) }
subject { described_class.new(project, user).execute(job) }
it_behaves_like 'authorizing CI jobs'
end
......@@ -7,12 +7,12 @@
subject { service.execute(build) }
end
it_behaves_like 'prevents playing job when credit card is required' do
let(:user) { create(:user, maintainer_of: project) }
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:job) { create(:ci_build, :manual, pipeline: pipeline) }
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user, maintainer_of: project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let_it_be(:job) { create(:ci_build, :manual, pipeline: pipeline) }
subject { described_class.new(project, user).execute(job) }
end
subject { described_class.new(project, user).execute(job) }
it_behaves_like 'authorizing CI jobs'
end
......@@ -69,61 +69,8 @@
end
end
describe 'credit card requirement' do
shared_examples 'creates a retried build' do
it 'creates a retried build' do
build
expect { new_build }.to change { Ci::Build.count }.by(1)
expect(new_build.name).to eq build.name
expect(new_build).to be_latest
expect(build).to be_retried
expect(build).to be_processed
end
end
context 'when credit card is required', :saas do
let_it_be(:ultimate_plan) { create(:ultimate_plan) }
let_it_be(:plan_limits) { create(:plan_limits, plan: ultimate_plan) }
before do
create(:gitlab_subscription, namespace: namespace, hosted_plan: ultimate_plan)
end
context 'when project is on free plan' do
before do
namespace.gitlab_subscription.update!(hosted_plan: create(:free_plan))
user.created_at = ::Users::CreditCardValidation::RELEASE_DAY
end
context 'when user has credit card' do
before do
allow(user).to receive(:credit_card_validated_at).and_return(Time.current)
end
it_behaves_like 'creates a retried build'
end
context 'when user does not have credit card' do
it 'raises an exception', :aggregate_failures do
expect { new_build }.to raise_error Gitlab::Access::AccessDeniedError
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(ci_require_credit_card_on_free_plan: false)
end
it_behaves_like 'creates a retried build'
end
end
end
end
context 'when credit card is not required' do
it_behaves_like 'creates a retried build'
end
it_behaves_like 'authorizing CI jobs' do
subject { new_build }
end
end
end
......
......@@ -37,17 +37,19 @@
end
end
context 'when user is not allowed to retry pipeline because of missing credit card' do
it 'returns an error' do
allow(user)
.to receive(:has_required_credit_card_to_run_pipelines?)
.with(project)
.and_return(false)
context 'when the user is not authorized to run jobs' do
before do
allow_next_instance_of(::Users::IdentityVerification::AuthorizeCi) do |instance|
allow(instance).to receive(:authorize_run_jobs!)
.and_raise(::Users::IdentityVerification::Error, 'authorization error')
end
end
it 'returns an error' do
response = service.execute(pipeline)
expect(response.http_status).to eq(:forbidden)
expect(response.errors).to include('Credit card required to be on file in order to retry a pipeline')
expect(response.errors).to include('authorization error')
expect(pipeline.reload).not_to be_running
end
end
......
# frozen_string_literal: true
RSpec.shared_examples 'authorizing CI jobs' do
context 'when the user is not authorized to run jobs' do
before do
allow_next_instance_of(::Users::IdentityVerification::AuthorizeCi) do |instance|
allow(instance).to receive(:authorize_run_jobs!).and_raise(::Users::IdentityVerification::Error)
end
end
it 'raises an exception' do
expect { subject }.to raise_error do |error|
expect(error.cause).to be_a(::Users::IdentityVerification::Error)
end
end
end
context 'when the user is authorized to run jobs' do
before do
allow_next_instance_of(::Users::IdentityVerification::AuthorizeCi) do |instance|
allow(instance).to receive(:authorize_run_jobs!)
end
end
it 'does not raise an exception' do
expect { subject }.not_to raise_error
end
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment