diff --git a/app/models/users/credit_card_validation.rb b/app/models/users/credit_card_validation.rb index 998a5deb0fdd22357383667de7293e708eb3b20f..6f1cadb4344d2d4496be27e201f88f2583a532be 100644 --- a/app/models/users/credit_card_validation.rb +++ b/app/models/users/credit_card_validation.rb @@ -21,5 +21,9 @@ def similar_records network: network ).order(credit_card_validated_at: :desc).includes(:user) end + + def similar_holder_names_count + self.class.where('lower(holder_name) = :value', value: holder_name.downcase).count + end end end diff --git a/db/migrate/20220829183356_replace_index_on_credit_card_validations.rb b/db/migrate/20220829183356_replace_index_on_credit_card_validations.rb new file mode 100644 index 0000000000000000000000000000000000000000..05fa7f75feb77c000c9ba09c078cf498f5ad0377 --- /dev/null +++ b/db/migrate/20220829183356_replace_index_on_credit_card_validations.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class ReplaceIndexOnCreditCardValidations < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + OLD_INDEX_NAME = 'index_user_credit_card_validations_meta_data_full_match' + NEW_INDEX_NAME = 'index_user_credit_card_validations_meta_data_full_match_lower' + OLD_FIELDS = [:holder_name, :expiration_date, :last_digits, :credit_card_validated_at] + NEW_FIELDS = 'lower(holder_name), expiration_date, last_digits, credit_card_validated_at' + + def up + add_concurrent_index :user_credit_card_validations, NEW_FIELDS, name: NEW_INDEX_NAME + remove_concurrent_index :user_credit_card_validations, OLD_FIELDS, name: OLD_INDEX_NAME + end + + def down + add_concurrent_index :user_credit_card_validations, OLD_FIELDS, name: OLD_INDEX_NAME + remove_concurrent_index :user_credit_card_validations, NEW_FIELDS, name: NEW_INDEX_NAME + end +end diff --git a/db/schema_migrations/20220829183356 b/db/schema_migrations/20220829183356 new file mode 100644 index 0000000000000000000000000000000000000000..087a8a8ab6bf24cf12d8d6b76067c7d4b1e2cada --- /dev/null +++ b/db/schema_migrations/20220829183356 @@ -0,0 +1 @@ +4d8be5080046eff9c3736cd2494c02b2d2cb1eeea2753479617cb344bc5b1cbb \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index a5f072d7c84be1f93fe67bbd0ee26b7e7f7e0310..9a2b98050460dcab614d333d4d75d34e534418f7 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -30341,7 +30341,7 @@ CREATE UNIQUE INDEX index_user_canonical_emails_on_user_id ON user_canonical_ema CREATE UNIQUE INDEX index_user_canonical_emails_on_user_id_and_canonical_email ON user_canonical_emails USING btree (user_id, canonical_email); -CREATE INDEX index_user_credit_card_validations_meta_data_full_match ON user_credit_card_validations USING btree (holder_name, expiration_date, last_digits, credit_card_validated_at); +CREATE INDEX index_user_credit_card_validations_meta_data_full_match_lower ON user_credit_card_validations USING btree (lower(holder_name), expiration_date, last_digits, credit_card_validated_at); CREATE INDEX index_user_credit_card_validations_meta_data_partial_match ON user_credit_card_validations USING btree (expiration_date, last_digits, network, credit_card_validated_at); diff --git a/doc/administration/external_pipeline_validation.md b/doc/administration/external_pipeline_validation.md index 99876cdf503b4828d1cf3f3628aaf2498677b6ac..b9bf0cfdc5020f969263becea74daf648ab6cb42 100644 --- a/doc/administration/external_pipeline_validation.md +++ b/doc/administration/external_pipeline_validation.md @@ -44,6 +44,7 @@ required number of seconds. "required" : [ "project", "user", + "credit_card", "pipeline", "builds", "total_builds_count", @@ -85,6 +86,17 @@ required number of seconds. "sign_in_count": { "type": "integer" } } }, + "credit_card": { + "type": "object", + "required": [ + "similar_cards_count", + "similar_holder_names_count" + ], + "properties": { + "similar_cards_count": { "type": "integer" }, + "similar_holder_names_count": { "type": "integer" } + } + }, "pipeline": { "type": "object", "required": [ diff --git a/lib/gitlab/ci/pipeline/chain/validate/external.rb b/lib/gitlab/ci/pipeline/chain/validate/external.rb index ba6de77d57d8bd3b1f27557e7699b10450478bcb..f4fa9e5fe2aa499215172833a18b1d50ba9a6f14 100644 --- a/lib/gitlab/ci/pipeline/chain/validate/external.rb +++ b/lib/gitlab/ci/pipeline/chain/validate/external.rb @@ -97,6 +97,10 @@ def validation_service_payload last_sign_in_ip: current_user.last_sign_in_ip, sign_in_count: current_user.sign_in_count }, + credit_card: { + similar_cards_count: current_user.credit_card_validation&.similar_records&.count.to_i, + similar_holder_names_count: current_user.credit_card_validation&.similar_holder_names_count.to_i + }, pipeline: { sha: pipeline.sha, ref: pipeline.ref, diff --git a/spec/fixtures/api/schemas/external_validation.json b/spec/fixtures/api/schemas/external_validation.json index 4a2538a020e5055177fe194a31a9c191159e9101..411c2ed591bb60f53bec1c37dc9c533daed4b7d4 100644 --- a/spec/fixtures/api/schemas/external_validation.json +++ b/spec/fixtures/api/schemas/external_validation.json @@ -3,6 +3,7 @@ "required" : [ "project", "user", + "credit_card", "pipeline", "builds", "total_builds_count" @@ -43,6 +44,17 @@ "sign_in_count": { "type": "integer" } } }, + "credit_card": { + "type": "object", + "required": [ + "similar_cards_count", + "similar_holder_names_count" + ], + "properties": { + "similar_cards_count": { "type": "integer" }, + "similar_holder_names_count": { "type": "integer" } + } + }, "pipeline": { "type": "object", "required": [ diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb index fb1a360a4b709e99839069cc5123413ddff47874..52a00e0d501bd7bdcca47c328c6ea70f64ab1b3f 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb @@ -179,6 +179,70 @@ perform! end end + + describe 'credit_card' do + context 'with no registered credit_card' do + it 'returns the expected credit card counts' do + expect(::Gitlab::HTTP).to receive(:post) do |_url, params| + payload = Gitlab::Json.parse(params[:body]) + + expect(payload['credit_card']['similar_cards_count']).to eq(0) + expect(payload['credit_card']['similar_holder_names_count']).to eq(0) + end + + perform! + end + end + + context 'with a registered credit card' do + let!(:credit_card) { create(:credit_card_validation, last_digits: 10, holder_name: 'Alice', user: user) } + + it 'returns the expected credit card counts' do + expect(::Gitlab::HTTP).to receive(:post) do |_url, params| + payload = Gitlab::Json.parse(params[:body]) + + expect(payload['credit_card']['similar_cards_count']).to eq(1) + expect(payload['credit_card']['similar_holder_names_count']).to eq(1) + end + + perform! + end + + context 'with similar credit cards registered by other users' do + before do + create(:credit_card_validation, last_digits: 10, holder_name: 'Bob') + end + + it 'returns the expected credit card counts' do + expect(::Gitlab::HTTP).to receive(:post) do |_url, params| + payload = Gitlab::Json.parse(params[:body]) + + expect(payload['credit_card']['similar_cards_count']).to eq(2) + expect(payload['credit_card']['similar_holder_names_count']).to eq(1) + end + + perform! + end + end + + context 'with similar holder names registered by other users' do + before do + create(:credit_card_validation, last_digits: 11, holder_name: 'Alice') + end + + it 'returns the expected credit card counts' do + expect(::Gitlab::HTTP).to receive(:post) do |_url, params| + payload = Gitlab::Json.parse(params[:body]) + + expect(payload['credit_card']['similar_cards_count']).to eq(1) + expect(payload['credit_card']['similar_holder_names_count']).to eq(2) + end + + perform! + end + end + end + end end context 'when EXTERNAL_VALIDATION_SERVICE_TOKEN is set' do diff --git a/spec/models/users/credit_card_validation_spec.rb b/spec/models/users/credit_card_validation_spec.rb index 34cfd500c26fd00aca4ad3815a1ea1940e1eb21d..c003e004d298815d310bf573b86092c445243421 100644 --- a/spec/models/users/credit_card_validation_spec.rb +++ b/spec/models/users/credit_card_validation_spec.rb @@ -28,4 +28,15 @@ expect(subject.similar_records).to eq([match2, match1, subject]) end end + + describe '.similar_holder_names_count' do + subject!(:credit_card_validation) { create(:credit_card_validation, holder_name: 'ALICE M SMITH') } + + let!(:match) { create(:credit_card_validation, holder_name: 'Alice M Smith') } + let!(:non_match) { create(:credit_card_validation, holder_name: 'Bob B Brown') } + + it 'returns the count of cards with similar case insensitive holder names' do + expect(subject.similar_holder_names_count).to eq(2) + end + end end