From c96f0a78593a43a06884519e2aa6ee5b41bb300a Mon Sep 17 00:00:00 2001 From: Seth Call Date: Wed, 20 May 2015 09:24:17 -0500 Subject: [PATCH] * VRFS-3260 - fraud check code and admin helpers complete --- admin/app/admin/fraud_alert.rb | 69 +++++++++++ admin/app/admin/machine_extra.rb | 8 ++ admin/app/admin/machine_fingerprint.rb | 22 ++++ admin/app/admin/music_sessions_comment.rb | 13 +++ admin/app/admin/session_info_comment.rb | 8 ++ db/manifest | 3 +- db/up/optimized_redemption_warn_mode.sql | 42 +++++++ ruby/lib/jam_ruby.rb | 3 + .../jam_ruby/models/fingerprint_whitelist.rb | 17 +++ ruby/lib/jam_ruby/models/fraud_alert.rb | 26 +++++ ruby/lib/jam_ruby/models/jam_track_right.rb | 91 +++++++++++---- ruby/lib/jam_ruby/models/machine_extra.rb | 35 ++++++ .../jam_ruby/models/machine_fingerprint.rb | 20 +++- .../jam_ruby/models/jam_track_right_spec.rb | 110 +++++++++++++----- ruby/spec/support/utilities.rb | 8 ++ web/app/assets/javascripts/jam_rest.js | 5 +- web/app/controllers/users_controller.rb | 8 +- web/config/application.rb | 4 +- .../api_jam_tracks_controller_spec.rb | 17 ++- web/spec/support/app_config.rb | 8 ++ web/spec/support/remote_ip_monkey_patch.rb | 17 +++ web/spec/support/utilities.rb | 2 +- 22 files changed, 471 insertions(+), 65 deletions(-) create mode 100644 admin/app/admin/fraud_alert.rb create mode 100644 admin/app/admin/machine_extra.rb create mode 100644 admin/app/admin/machine_fingerprint.rb create mode 100644 admin/app/admin/music_sessions_comment.rb create mode 100644 admin/app/admin/session_info_comment.rb create mode 100644 db/up/optimized_redemption_warn_mode.sql create mode 100644 ruby/lib/jam_ruby/models/fingerprint_whitelist.rb create mode 100644 ruby/lib/jam_ruby/models/fraud_alert.rb create mode 100644 ruby/lib/jam_ruby/models/machine_extra.rb create mode 100644 web/spec/support/remote_ip_monkey_patch.rb diff --git a/admin/app/admin/fraud_alert.rb b/admin/app/admin/fraud_alert.rb new file mode 100644 index 000000000..7c4754a83 --- /dev/null +++ b/admin/app/admin/fraud_alert.rb @@ -0,0 +1,69 @@ +ActiveAdmin.register JamRuby::FraudAlert, :as => 'Fraud Alerts' do + + menu :label => 'Fraud Alerts', :parent => 'JamTracks' + + config.sort_order = 'created_at desc' + config.batch_actions = false + + + scope("Not Whitelisted", default:true) { |scope| + scope.joins('INNER JOIN "machine_fingerprints" ON "machine_fingerprints"."id" = "fraud_alerts"."machine_fingerprint_id" LEFT OUTER JOIN "fingerprint_whitelists" ON "fingerprint_whitelists"."fingerprint" = "machine_fingerprints"."fingerprint"').where('fingerprint_whitelists IS NULL')} + + index do + default_actions + + column :machine_fingerprint + column :user + column :created_at + column :resolved + + column "" do |alert| + link_to 'Matching MAC', "fraud_alerts/#{alert.id}/same_fingerprints" + end + column "" do |alert| + link_to 'Matching MAC and IP Address', "fraud_alerts/#{alert.id}/same_fingerprints_and_ip" + end + column "" do |alert| + link_to 'Matching IP Address', "fraud_alerts/#{alert.id}/same_ip" + end + column "" do |alert| + link_to 'Resolve', "fraud_alerts/#{alert.id}/resolve" + end + column "" do |alert| + link_to 'Whitelist Similar', "fraud_alerts/#{alert.id}/whitelist" + end + end + + member_action :same_fingerprints, :method => :get do + alert = FraudAlert.find(params[:id]) + + redirect_to admin_machine_fingerprints_path("q[fingerprint_equals]" => alert.machine_fingerprint.fingerprint, commit: 'Filter', order: 'created_at_desc') + end + + member_action :same_fingerprints_and_ip, :method => :get do + alert = FraudAlert.find(params[:id]) + + redirect_to admin_machine_fingerprints_path("q[fingerprint_equals]" => alert.machine_fingerprint.fingerprint, "q[remote_ip_equals]" => alert.machine_fingerprint.remote_ip, commit: 'Filter', order: 'created_at_desc') + end + + + member_action :resolve, :method => :get do + alert = FraudAlert.find(params[:id]) + alert.resolved = true + alert.save! + + redirect_to admin_fraud_alerts_path, notice: "That fraud alert has been marked as resolved" + end + + member_action :whitelist, :method => :get do + alert = FraudAlert.find(params[:id]) + + wl = FingerprintWhitelist.new + wl.fingerprint = alert.machine_fingerprint.fingerprint + success = wl.save + + redirect_to admin_fraud_alerts_path, notice: success ? "Added #{alert.machine_fingerprint.fingerprint} to whitelist" : "Could not add #{alert.machine_fingerprint.fingerprint} to whiteliste" + end + + +end \ No newline at end of file diff --git a/admin/app/admin/machine_extra.rb b/admin/app/admin/machine_extra.rb new file mode 100644 index 000000000..c1c5b8738 --- /dev/null +++ b/admin/app/admin/machine_extra.rb @@ -0,0 +1,8 @@ +ActiveAdmin.register JamRuby::MachineExtra, :as => 'Machine Extra' do + + menu :label => 'Machine Extra', :parent => 'JamTracks' + + config.sort_order = 'created_at desc' + config.batch_actions = false + +end \ No newline at end of file diff --git a/admin/app/admin/machine_fingerprint.rb b/admin/app/admin/machine_fingerprint.rb new file mode 100644 index 000000000..4ea9c3c7a --- /dev/null +++ b/admin/app/admin/machine_fingerprint.rb @@ -0,0 +1,22 @@ +ActiveAdmin.register JamRuby::MachineFingerprint, :as => 'Machine Fingerprints' do + + menu :label => 'Machine Fingerprints', :parent => 'JamTracks' + + config.sort_order = 'created_at desc' + config.batch_actions = false + + index do + column :user + column 'Hash' do |fp| + fp.fingerprint + end + column :remote_ip + column 'Detail' do |fp| + detail = fp.detail + if detail + detail.to_s + end + end + column :created_at + end +end \ No newline at end of file diff --git a/admin/app/admin/music_sessions_comment.rb b/admin/app/admin/music_sessions_comment.rb new file mode 100644 index 000000000..5b651a11f --- /dev/null +++ b/admin/app/admin/music_sessions_comment.rb @@ -0,0 +1,13 @@ +ActiveAdmin.register JamRuby::MusicSessionComment, :as => 'Ratings' do + + config.per_page = 150 + config.clear_action_items! + config.sort_order = 'created_at_desc' + menu :parent => 'Sessions', :label => 'Ratings' + + index do + column :comment + column :user + column :created_at + end +end diff --git a/admin/app/admin/session_info_comment.rb b/admin/app/admin/session_info_comment.rb new file mode 100644 index 000000000..cf8fb3c0c --- /dev/null +++ b/admin/app/admin/session_info_comment.rb @@ -0,0 +1,8 @@ +ActiveAdmin.register JamRuby::SessionInfoComment, :as => 'Comments' do + + config.per_page = 50 + config.clear_action_items! + config.sort_order = 'created_at_desc' + menu :parent => 'Sessions', :label => 'Comments' + +end diff --git a/db/manifest b/db/manifest index 48ba477b9..f2cfbbc71 100755 --- a/db/manifest +++ b/db/manifest @@ -283,4 +283,5 @@ payment_history.sql jam_track_right_private_key.sql first_downloaded_jamtrack_at.sql signing.sql -optimized_redeemption.sql \ No newline at end of file +optimized_redeemption.sql +optimized_redemption_warn_mode.sql \ No newline at end of file diff --git a/db/up/optimized_redemption_warn_mode.sql b/db/up/optimized_redemption_warn_mode.sql new file mode 100644 index 000000000..faf2ad984 --- /dev/null +++ b/db/up/optimized_redemption_warn_mode.sql @@ -0,0 +1,42 @@ +DROP TABLE machine_fingerprints; + +CREATE TABLE machine_fingerprints ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + fingerprint VARCHAR(20000) NOT NULL, + when_taken VARCHAR NOT NULL, + print_type VARCHAR NOT NULL, + remote_ip VARCHAR(1000) NOT NULL, + jam_track_right_id BIGINT REFERENCES jam_track_rights(id) ON DELETE SET NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE machine_extras ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + machine_fingerprint_id VARCHAR(64) NOT NULL REFERENCES machine_fingerprints(id) ON DELETE CASCADE, + mac_address VARCHAR(100), + mac_name VARCHAR(255), + upstate BOOLEAN, + ipaddr_0 VARCHAR(200), + ipaddr_1 VARCHAR(200), + ipaddr_2 VARCHAR(200), + ipaddr_3 VARCHAR(200), + ipaddr_4 VARCHAR(200), + ipaddr_5 VARCHAR(200), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE fraud_alerts ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + machine_fingerprint_id VARCHAR(64) NOT NULL REFERENCES machine_fingerprints(id) ON DELETE CASCADE, + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + resolved BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE fingerprint_whitelists ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + fingerprint VARCHAR(20000) UNIQUE NOT NULL +); + +CREATE INDEX machine_fingerprints_index1 ON machine_fingerprints USING btree (fingerprint, user_id, remote_ip, created_at); diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 09b125e3d..58ebc614a 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -104,6 +104,9 @@ require "jam_ruby/models/user" require "jam_ruby/models/anonymous_user" require "jam_ruby/models/signup_hint" require "jam_ruby/models/machine_fingerprint" +require "jam_ruby/models/machine_extra" +require "jam_ruby/models/fraud_alert" +require "jam_ruby/models/fingerprint_whitelist" require "jam_ruby/models/rsvp_request" require "jam_ruby/models/rsvp_slot" require "jam_ruby/models/rsvp_request_rsvp_slot" diff --git a/ruby/lib/jam_ruby/models/fingerprint_whitelist.rb b/ruby/lib/jam_ruby/models/fingerprint_whitelist.rb new file mode 100644 index 000000000..88f015aa3 --- /dev/null +++ b/ruby/lib/jam_ruby/models/fingerprint_whitelist.rb @@ -0,0 +1,17 @@ +module JamRuby + class FingerprintWhitelist < ActiveRecord::Base + + @@log = Logging.logger[FingerprintWhitelist] + + validates :fingerprint, presence: true, uniqueness: true + has_many :machine_fingerprint, class_name: 'JamRuby::MachineFingerprint', foreign_key: :fingerprint + + def admin_url + APP_CONFIG.admin_root_url + "/admin/fingerprint_whitelists/" + id + end + + def to_s + "#{fingerprint}" + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/fraud_alert.rb b/ruby/lib/jam_ruby/models/fraud_alert.rb new file mode 100644 index 000000000..344eb7ddc --- /dev/null +++ b/ruby/lib/jam_ruby/models/fraud_alert.rb @@ -0,0 +1,26 @@ +module JamRuby + class FraudAlert < ActiveRecord::Base + + @@log = Logging.logger[MachineExtra] + + belongs_to :machine_fingerprint, :class_name => "JamRuby::MachineFingerprint" + belongs_to :user, :class_name => "JamRuby::User" + + + def self.create(machine_fingerprint, user) + fraud = FraudAlert.new + fraud.machine_fingerprint = machine_fingerprint + fraud.user = user + fraud.save + + unless fraud.save + @@log.error("unable to create fraud: #{fraud.errors.inspect}") + end + fraud + end + + def admin_url + APP_CONFIG.admin_root_url + "/admin/fraud_alerts/" + id + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/jam_track_right.rb b/ruby/lib/jam_ruby/models/jam_track_right.rb index fda1ecb97..c77da5ffe 100644 --- a/ruby/lib/jam_ruby/models/jam_track_right.rb +++ b/ruby/lib/jam_ruby/models/jam_track_right.rb @@ -229,8 +229,8 @@ module JamRuby return "no fingerprint specified" end - all_fingerprint = fingerprint[:all] - running_fingerprint = fingerprint[:running] + all_fingerprint = fingerprint.delete(:all) + running_fingerprint = fingerprint.delete(:running) if all_fingerprint.blank? return "no all fingerprint specified" @@ -240,6 +240,9 @@ module JamRuby return "no running fingerprint specified" end + all_fingerprint_extra = fingerprint[all_fingerprint] + running_fingerprint_extra = fingerprint[running_fingerprint] + if redeemed && !redeemed_and_fingerprinted # if this is a free JamTrack, we need to check for fraud or accidental misuse @@ -250,41 +253,80 @@ module JamRuby return "already redeemed another" end - # can we find a jam track that belongs to someone else with the same fingerprint - match = MachineFingerprint.find_by_fingerprint(all_fingerprint) + if FingerprintWhitelist.select('id').find_by_fingerprint(all_fingerprint) + # we can short circuit out of the rest of the check, since this is a known bad fingerprint + @@log.debug("ignoring 'all' hash found in whitelist") + else + # can we find a jam track that belongs to someone else with the same fingerprint + conflict = MachineFingerprint.select('count(id) as count').where('user_id != ?', current_user.id).where(fingerprint: all_fingerprint).where(remote_ip: remote_ip).where('created_at > ?', APP_CONFIG.expire_fingerprint_days.days.ago).first + conflict_count = conflict['count'].to_i - if match && match.user != current_user - AdminMailer.alerts(subject: "'All' fingerprint collision by #{current_user.name}", - body: "MachineFingerprint #{match.inspect}\n\nCurrent User: #{current_user.admin_url}").deliver + if conflict_count >= APP_CONFIG.found_conflict_count + mf = MachineFingerprint.create(all_fingerprint, current_user, MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT, MachineFingerprint::PRINT_TYPE_ACTIVE, remote_ip, all_fingerprint_extra, self) + + # record the alert + fraud = FraudAlert.create(mf, current_user) if mf.valid? + fraud_admin_url = fraud.admin_url if fraud + + + AdminMailer.alerts(subject: "'All' fingerprint collision by #{current_user.name}", + body: "Current User: #{current_user.admin_url}\n\n Fraud Alert: #{fraud_admin_url}").deliver + + # try to record the other fingerprint + mf = MachineFingerprint.create(running_fingerprint, current_user, MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT, MachineFingerprint::PRINT_TYPE_ACTIVE, remote_ip, running_fingerprint_extra, self) + + if APP_CONFIG.error_on_fraud + return "other user has 'all' fingerprint" + else + self.redeemed_and_fingerprinted = true + save! + return nil + end - # try to record the other fingerprint - MachineFingerprint.create(running_fingerprint, current_user, MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT, MachineFingerprint::PRINT_TYPE_ACTIVE, remote_ip, self) - if APP_CONFIG.error_on_fraud - return "other user has 'all' fingerprint" end + end - if all_fingerprint != running_fingerprint - match = MachineFingerprint.find_by_fingerprint(running_fingerprint) - if match && match.user != current_user - AdminMailer.alerts(subject: "'Running' fingerprint collision by #{current_user.name}", - body: "MachineFingerprint #{match.inspect}\n\nCurrent User: #{current_user.admin_url}").deliver - # try to record the other fingerprint - MachineFingerprint.create(all_fingerprint, current_user, MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT, MachineFingerprint::PRINT_TYPE_ALL, remote_ip, self) - if APP_CONFIG.error_on_fraud - return "other user has 'running' fingerprint" + if all_fingerprint != running_fingerprint + if FingerprintWhitelist.select('id').find_by_fingerprint(running_fingerprint) + # we can short circuit out of the rest of the check, since this is a known bad fingerprint + @@log.debug("ignoring 'running' hash found in whitelist") + else + + conflict = MachineFingerprint.select('count(id) as count').where('user_id != ?', current_user.id).where(fingerprint: running_fingerprint).where(remote_ip: remote_ip).where('created_at > ?', APP_CONFIG.expire_fingerprint_days.days.ago).first + conflict_count = conflict['count'].to_i + if conflict_count >= APP_CONFIG.found_conflict_count + mf = MachineFingerprint.create(running_fingerprint, current_user, MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT, MachineFingerprint::PRINT_TYPE_ACTIVE, remote_ip, running_fingerprint_extra, self) + + # record the alert + fraud = FraudAlert.create(mf, current_user) if mf.valid? + fraud_admin_url = fraud.admin_url if fraud + AdminMailer.alerts(subject: "'Running' fingerprint collision by #{current_user.name}", + body: "Current User: #{current_user.admin_url}\n\nFraud Alert: #{fraud_admin_url}").deliver\ + + # try to record the other fingerprint + mf = MachineFingerprint.create(all_fingerprint, current_user, MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT, MachineFingerprint::PRINT_TYPE_ALL, remote_ip, all_fingerprint_extra, self) + + + if APP_CONFIG.error_on_fraud + return "other user has 'running' fingerprint" + else + self.redeemed_and_fingerprinted = true + save! + return nil + end end end + end # we made it past all checks; let's slap on the redeemed_fingerprint self.redeemed_and_fingerprinted = true - - MachineFingerprint.create(all_fingerprint, current_user, MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD, MachineFingerprint::PRINT_TYPE_ALL, remote_ip, self) + MachineFingerprint.create(all_fingerprint, current_user, MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD, MachineFingerprint::PRINT_TYPE_ALL, remote_ip, all_fingerprint_extra, self) if all_fingerprint != running_fingerprint - MachineFingerprint.create(running_fingerprint, current_user, MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD, MachineFingerprint::PRINT_TYPE_ACTIVE, remote_ip, self) + MachineFingerprint.create(running_fingerprint, current_user, MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD, MachineFingerprint::PRINT_TYPE_ACTIVE, remote_ip, running_fingerprint_extra, self) end save! @@ -297,11 +339,12 @@ module JamRuby def self.stats stats = {} - result = JamTrackRight.select('count(id) as total, count(CASE WHEN signing_44 THEN 1 ELSE NULL END) + count(CASE WHEN signing_48 THEN 1 ELSE NULL END) as signing_count, count(CASE WHEN redeemed THEN 1 ELSE NULL END) as redeem_count').where(is_test_purchase: false).first + result = JamTrackRight.select('count(id) as total, count(CASE WHEN signing_44 THEN 1 ELSE NULL END) + count(CASE WHEN signing_48 THEN 1 ELSE NULL END) as signing_count, count(CASE WHEN redeemed THEN 1 ELSE NULL END) as redeem_count, count(CASE WHEN redeemed_and_fingerprinted THEN 1 ELSE NULL END) as redeemed_and_dl_count').where(is_test_purchase: false).first stats['count'] = result['total'].to_i stats['signing_count'] = result['signing_count'].to_i stats['redeemed_count'] = result['redeem_count'].to_i + stats['redeemed_and_dl_count'] = result['redeem_and_dl_count'].to_i stats['purchased_count'] = stats['count'] - stats['redeemed_count'] stats end diff --git a/ruby/lib/jam_ruby/models/machine_extra.rb b/ruby/lib/jam_ruby/models/machine_extra.rb new file mode 100644 index 000000000..74b1402d0 --- /dev/null +++ b/ruby/lib/jam_ruby/models/machine_extra.rb @@ -0,0 +1,35 @@ +module JamRuby + class MachineExtra < ActiveRecord::Base + + @@log = Logging.logger[MachineExtra] + + belongs_to :machine_fingerprint, :class_name => "JamRuby::MachineFingerprint" + + def self.create(machine_fingerprint, data) + me = MachineExtra.new + me.machine_fingerprint = machine_fingerprint + me.mac_address = data[:mac] + me.mac_name = data[:name] + me.upstate = data[:upstate] + me.ipaddr_0 = data[:ipaddr_0] + me.ipaddr_1 = data[:ipaddr_1] + me.ipaddr_2 = data[:ipaddr_2] + me.ipaddr_3 = data[:ipaddr_3] + me.ipaddr_4 = data[:ipaddr_4] + me.ipaddr_5 = data[:ipaddr_5] + me.save + + unless me.save + @@log.error("unable to create machine extra: #{me.errors.inspect}") + end + end + + def admin_url + APP_CONFIG.admin_root_url + "/admin/machine_extras/" + id + end + + def to_s + "#{mac_address} #{mac_name} #{upstate ? 'UP' : 'DOWN'} #{ipaddr_0} #{ipaddr_1} #{ipaddr_2} #{ipaddr_3} #{ipaddr_4} #{ipaddr_5}" + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/machine_fingerprint.rb b/ruby/lib/jam_ruby/models/machine_fingerprint.rb index 7d0076e50..d4d668052 100644 --- a/ruby/lib/jam_ruby/models/machine_fingerprint.rb +++ b/ruby/lib/jam_ruby/models/machine_fingerprint.rb @@ -5,6 +5,8 @@ module JamRuby belongs_to :user, :class_name => "JamRuby::User" belongs_to :jam_track_right, :class_name => "JamRuby::JamTrackRight" + has_one :detail, :class_name => "JamRuby::MachineExtra" + belongs_to :fingerprint_whitelist, class_name: 'JamRuby::FingerprintWhitelist', foreign_key: :fingerprint TAKEN_ON_SUCCESSFUL_DOWNLOAD = 'dl' TAKEN_ON_FRAUD_CONFLICT = 'fc' @@ -15,11 +17,11 @@ module JamRuby validates :user, presence:true validates :when_taken, :inclusion => {:in => [TAKEN_ON_SUCCESSFUL_DOWNLOAD, TAKEN_ON_FRAUD_CONFLICT]} - validates :fingerprint, presence: true, uniqueness:true + validates :fingerprint, presence: true validates :print_type, presence: true, :inclusion => {:in =>[PRINT_TYPE_ALL, PRINT_TYPE_ACTIVE]} validates :remote_ip, presence: true - def self.create(fingerprint, user, when_taken, print_type, remote_ip, jam_track_right = nil) + def self.create(fingerprint, user, when_taken, print_type, remote_ip, extra, jam_track_right = nil) mf = MachineFingerprint.new mf.fingerprint = fingerprint mf.user = user @@ -27,9 +29,21 @@ module JamRuby mf.print_type = print_type mf.remote_ip = remote_ip mf.jam_track_right = jam_track_right - unless mf.save + if mf.save + MachineExtra.create(mf, extra) if extra + else + @@log.error("unable to create machine fingerprint: #{mf.errors.inspect}") end + mf + end + + def admin_url + APP_CONFIG.admin_root_url + "/admin/machine_fingerprints/" + id + end + + def to_s + "#{fingerprint} #{remote_ip} #{user} #{detail}" end end end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/jam_track_right_spec.rb b/ruby/spec/jam_ruby/models/jam_track_right_spec.rb index 1adef7d18..9119bdfd9 100644 --- a/ruby/spec/jam_ruby/models/jam_track_right_spec.rb +++ b/ruby/spec/jam_ruby/models/jam_track_right_spec.rb @@ -168,7 +168,8 @@ describe JamTrackRight do JamTrackRight.stats.should eq('count' => 1, 'signing_count' => 0, 'redeemed_count' => 0, - 'purchased_count' => 1) + 'purchased_count' => 1, + 'redeemed_and_dl_count' => 0) end it "two" do @@ -178,7 +179,8 @@ describe JamTrackRight do JamTrackRight.stats.should eq('count' => 2, 'signing_count' => 0, 'redeemed_count' => 0, - 'purchased_count' => 2) + 'purchased_count' => 2, + 'redeemed_and_dl_count' => 0) right1.signing_44 = true right1.save! @@ -188,7 +190,8 @@ describe JamTrackRight do JamTrackRight.stats.should eq('count' => 2, 'signing_count' => 2, 'redeemed_count' => 0, - 'purchased_count' => 2) + 'purchased_count' => 2, + 'redeemed_and_dl_count' => 0) right1.redeemed = true right1.save! @@ -196,7 +199,8 @@ describe JamTrackRight do JamTrackRight.stats.should eq('count' => 2, 'signing_count' => 2, 'redeemed_count' => 1, - 'purchased_count' => 1) + 'purchased_count' => 1, + 'redeemed_and_dl_count' => 0) right2.is_test_purchase = true right2.save! @@ -204,7 +208,8 @@ describe JamTrackRight do JamTrackRight.stats.should eq('count' => 1, 'signing_count' => 1, 'redeemed_count' => 1, - 'purchased_count' => 0) + 'purchased_count' => 0, + 'redeemed_and_dl_count' => 0) end end @@ -213,7 +218,9 @@ describe JamTrackRight do let(:other) {FactoryGirl.create(:user)} let(:first_fingerprint) { {all: 'all', running: 'running' } } let(:new_fingerprint) { {all: 'all_2', running: 'running' } } + let(:full_fingerprint) { {all: :all_3, running: :running_3, all_3: { mac: "72:00:02:4C:1E:61", name: "en2", upstate: true }, running_3: { mac: "72:00:02:4C:1E:62", name: "en3", upstate: false } } } let(:remote_ip) {'1.1.1.1'} + let(:remote_ip2) {'2.2.2.2'} let(:jam_track_right) { FactoryGirl.create(:jam_track_right, user: user, redeemed: true, redeemed_and_fingerprinted: false) } let(:jam_track_right_purchased) { FactoryGirl.create(:jam_track_right, user: user, redeemed: false, redeemed_and_fingerprinted: false) } let(:jam_track_right_other) { FactoryGirl.create(:jam_track_right, user: other, redeemed: true, redeemed_and_fingerprinted: false) } @@ -233,16 +240,16 @@ describe JamTrackRight do jam_track_right.redeemed_and_fingerprinted.should be_true - mf = MachineFingerprint.find_by_fingerprint(first_fingerprint[:all]) + mf = MachineFingerprint.find_by_fingerprint('all') mf.user.should eq(user) - mf.fingerprint.should eq(first_fingerprint[:all]) + mf.fingerprint.should eq('all') mf.when_taken.should eq(MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD) mf.print_type.should eq(MachineFingerprint::PRINT_TYPE_ALL) mf.jam_track_right.should eq(jam_track_right) - mf = MachineFingerprint.find_by_fingerprint(first_fingerprint[:running]) + mf = MachineFingerprint.find_by_fingerprint('running') mf.user.should eq(user) - mf.fingerprint.should eq(first_fingerprint[:running]) + mf.fingerprint.should eq('running') mf.when_taken.should eq(MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD) mf.print_type.should eq(MachineFingerprint::PRINT_TYPE_ACTIVE) mf.jam_track_right.should eq(jam_track_right) @@ -274,40 +281,80 @@ describe JamTrackRight do end it "protects against re-using fingerprint across users (conflicts on all fp)" do + first_fingerprint2 = first_fingerprint.clone jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip).should be_nil MachineFingerprint.count.should eq(2) - first_fingerprint[:running] = 'running_2' - jam_track_right_other.guard_against_fraud(other, first_fingerprint, remote_ip).should eq("other user has 'all' fingerprint") + first_fingerprint2[:running] = 'running_2' + jam_track_right_other.guard_against_fraud(other, first_fingerprint2, remote_ip).should eq("other user has 'all' fingerprint") - mf = MachineFingerprint.find_by_fingerprint(first_fingerprint[:running]) - mf.user.should eq(other) - mf.fingerprint.should eq(first_fingerprint[:running]) - mf.when_taken.should eq(MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT) + mf = MachineFingerprint.find_by_fingerprint('running') + mf.user.should eq(user) + mf.fingerprint.should eq('running') + mf.when_taken.should eq(MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD) mf.print_type.should eq(MachineFingerprint::PRINT_TYPE_ACTIVE) - mf.jam_track_right.should eq(jam_track_right_other) - MachineFingerprint.count.should eq(3) + mf.jam_track_right.should eq(jam_track_right) + MachineFingerprint.count.should eq(4) end it "protects against re-using fingerprint across users (conflicts on running fp)" do + first_fingerprint2 = first_fingerprint.clone jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip).should be_nil MachineFingerprint.count.should eq(2) - first_fingerprint[:all] = 'all_2' - jam_track_right_other.guard_against_fraud(other, first_fingerprint, remote_ip).should eq("other user has 'running' fingerprint") + first_fingerprint2[:all] = 'all_2' + jam_track_right_other.guard_against_fraud(other, first_fingerprint2, remote_ip).should eq("other user has 'running' fingerprint") - mf = MachineFingerprint.find_by_fingerprint(first_fingerprint[:all]) - mf.user.should eq(other) - mf.fingerprint.should eq(first_fingerprint[:all]) - mf.when_taken.should eq(MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT) + mf = MachineFingerprint.find_by_fingerprint('all') + mf.user.should eq(user) + mf.fingerprint.should eq('all') + mf.when_taken.should eq(MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD) mf.print_type.should eq(MachineFingerprint::PRINT_TYPE_ALL) - mf.jam_track_right.should eq(jam_track_right_other) - MachineFingerprint.count.should eq(3) + mf.jam_track_right.should eq(jam_track_right) + MachineFingerprint.count.should eq(4) + + FraudAlert.count.should eq(1) + fraud = FraudAlert.first + fraud.user.should eq(other) + fraud.machine_fingerprint.should eq(MachineFingerprint.where(fingerprint:'running').where(user_id:other.id).first) + end + + it "ignores whitelisted fingerprint" do + whitelist = FingerprintWhitelist.new + whitelist.fingerprint = first_fingerprint[:running] + whitelist.save! + + first_fingerprint2 = first_fingerprint.clone + jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip).should be_nil + MachineFingerprint.count.should eq(2) + first_fingerprint2[:all] = 'all_2' + jam_track_right_other.guard_against_fraud(other, first_fingerprint2, remote_ip).should be_nil + + FraudAlert.count.should eq(0) + end + + it "does not conflict if same mac, but different IP address" do + first_fingerprint2 = first_fingerprint.clone + jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip).should be_nil + MachineFingerprint.count.should eq(2) + first_fingerprint2[:all] = 'all_2' + jam_track_right_other.guard_against_fraud(other, first_fingerprint2, remote_ip2).should eq(nil) + + mf = MachineFingerprint.find_by_fingerprint('all') + mf.user.should eq(user) + mf.fingerprint.should eq('all') + mf.when_taken.should eq(MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD) + mf.print_type.should eq(MachineFingerprint::PRINT_TYPE_ALL) + mf.jam_track_right.should eq(jam_track_right) + MachineFingerprint.count.should eq(4) + + FraudAlert.count.should eq(0) end # if you try to buy a regular jamtrack with a fingerprint belonging to another user? so what. you paid for it it "allows re-use of fingerprint if jamtrack is a normal purchase" do + first_fingerprint2 = first_fingerprint.clone jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip).should be_nil MachineFingerprint.count.should eq(2) - jam_track_right_other_purchased.guard_against_fraud(other, first_fingerprint, remote_ip).should be_nil + jam_track_right_other_purchased.guard_against_fraud(other, first_fingerprint2, remote_ip).should be_nil MachineFingerprint.count.should eq(2) end @@ -319,13 +366,22 @@ describe JamTrackRight do it "let's you download a free jamtrack if you have a second but undownloaded free one" do right1 = FactoryGirl.create(:jam_track_right, user: user, redeemed: true, redeemed_and_fingerprinted: false) + first_fingerprint2 = first_fingerprint.clone jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip).should be_nil MachineFingerprint.count.should eq(2) - right1.guard_against_fraud(user, first_fingerprint, remote_ip).should eq('already redeemed another') + right1.guard_against_fraud(user, first_fingerprint2, remote_ip).should eq('already redeemed another') MachineFingerprint.count.should eq(2) end + it "creates metadata" do + right1 = FactoryGirl.create(:jam_track_right, user: user, redeemed: true, redeemed_and_fingerprinted: false) + + jam_track_right.guard_against_fraud(user, full_fingerprint, remote_ip).should be_nil + MachineFingerprint.count.should eq(2) + MachineExtra.count.should eq(2) + + end end end diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb index 5e9380eac..8d16dffbb 100644 --- a/ruby/spec/support/utilities.rb +++ b/ruby/spec/support/utilities.rb @@ -198,6 +198,14 @@ def app_config true end + def expire_fingerprint_days + 14 + end + + def found_conflict_count + 1 + end + private def audiomixer_workspace_path diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index b8fcf54f6..ffccd50d4 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -1478,12 +1478,13 @@ function enqueueJamTrack(options) { var jamTrackId = options['id']; + delete options['id'] return $.ajax({ type: "POST", - url: '/api/jamtracks/enqueue/' + jamTrackId + '?' + $.param(options), + url: '/api/jamtracks/enqueue/' + jamTrackId, dataType: "json", - contentType: 'applications/json' + data: options }); } diff --git a/web/app/controllers/users_controller.rb b/web/app/controllers/users_controller.rb index 47675f9ce..7c5a06768 100644 --- a/web/app/controllers/users_controller.rb +++ b/web/app/controllers/users_controller.rb @@ -196,9 +196,14 @@ class UsersController < ApplicationController render :layout => "web" end - # DO NOT USE CURRENT_USER IN THIS ROUTINE. IT'S CACHED FOR THE WHOLE SITE + # DO NOT USE CURRENT_USER IN THIS ROUTINE UNLESS REDIRECTING. IT'S CACHED FOR THE WHOLE SITE def home + if current_user + redirect_to "/client#/home" + return + end + @no_user_dropdown = false @promo_buzz = PromoBuzz.active @@ -209,7 +214,6 @@ class UsersController < ApplicationController end - gon.signed_in = !current_user.nil? render :layout => "web" end diff --git a/web/config/application.rb b/web/config/application.rb index 4de4e6d8c..44b5ce2ff 100644 --- a/web/config/application.rb +++ b/web/config/application.rb @@ -341,7 +341,9 @@ if defined?(Bundler) config.remove_whitespace_credit_card = false config.estimate_taxes = true config.ad_sense_enabled = false - config.guard_against_fraud = false + config.guard_against_fraud = true config.error_on_fraud = false + config.expire_fingerprint_days = 14 + config.found_conflict_count = 1 end end diff --git a/web/spec/controllers/api_jam_tracks_controller_spec.rb b/web/spec/controllers/api_jam_tracks_controller_spec.rb index fc4685eb9..74bc1da94 100644 --- a/web/spec/controllers/api_jam_tracks_controller_spec.rb +++ b/web/spec/controllers/api_jam_tracks_controller_spec.rb @@ -15,6 +15,8 @@ describe ApiJamTracksController do end before(:each) do + FingerprintWhitelist.destroy_all + FraudAlert.destroy_all MachineFingerprint.destroy_all JamTrackRight.destroy_all JamTrack.destroy_all @@ -299,6 +301,9 @@ describe ApiJamTracksController do describe "guard fraud" do + after(:each) do + ENV['RAILS_TEST_IP_ADDRESS'] = nil + end it "stops second user from downloading using same machine" do right = JamTrackRight.create(:user=>@user, :jam_track=>@jam_track) @@ -306,6 +311,7 @@ describe ApiJamTracksController do right.save! get :download, :id=>@jam_track.id, sample_rate: 48, all_fp: 'all', running_fp: 'running' response.status.should == 202 + MachineFingerprint.count.should eq(2) user2 = FactoryGirl.create(:user) @@ -315,8 +321,10 @@ describe ApiJamTracksController do right.redeemed = true right.save! get :download, :id=>@jam_track.id, sample_rate: 48, all_fp: 'all', running_fp: 'running' - response.status.should == 403 - JSON.parse(response.body)['message'].should eq("other user has 'all' fingerprint") + response.status.should == 202 + + # no error for the user... but we should have a FraudAlert + FraudAlert.count.should eq(1) end it "stops second user from enqueuing using same machine" do @@ -336,9 +344,10 @@ describe ApiJamTracksController do right.save! right.signing_queued_at.should be_nil post :enqueue, {:format=>'json', :id=>right.jam_track.id, fingerprint: {all: 'all', running: 'running'} } + response.status.should == 200 get :download, :id=>@jam_track.id, sample_rate: 48, all_fp: 'all', running_fp: 'running' - response.status.should == 403 - JSON.parse(response.body)['message'].should eq("other user has 'all' fingerprint") + response.status.should == 202 + FraudAlert.count.should eq(1) end end end diff --git a/web/spec/support/app_config.rb b/web/spec/support/app_config.rb index b7cc08d89..6ee586981 100644 --- a/web/spec/support/app_config.rb +++ b/web/spec/support/app_config.rb @@ -94,6 +94,14 @@ def web_config def secret_token 'ced345e01611593c1b783bae98e4e56dbaee787747e92a141565f7c61d0ab2c6f98f7396fb4b178258301e2713816e158461af58c14b695901692f91e72b6200' end + + def expire_fingerprint_days + 14 + end + + def found_conflict_count + 1 + end end klass.new end diff --git a/web/spec/support/remote_ip_monkey_patch.rb b/web/spec/support/remote_ip_monkey_patch.rb new file mode 100644 index 000000000..7d725d072 --- /dev/null +++ b/web/spec/support/remote_ip_monkey_patch.rb @@ -0,0 +1,17 @@ +module ActionDispatch + class Request + + def remote_ip_with_mocking + test_ip = ENV['RAILS_TEST_IP_ADDRESS'] + + unless test_ip.nil? or test_ip.empty? + return test_ip + else + return remote_ip_without_mocking + end + end + + alias_method_chain :remote_ip, :mocking + + end +end \ No newline at end of file diff --git a/web/spec/support/utilities.rb b/web/spec/support/utilities.rb index 8c06407da..39b109ee9 100644 --- a/web/spec/support/utilities.rb +++ b/web/spec/support/utilities.rb @@ -225,7 +225,7 @@ def should_be_at_root(options={signed_in:nil}) find('h1', text: 'Live music platform & social network for musicians') end when Capybara::RackTest::Driver - signed_in = false # actually, the user may be signed in, but, we only redirect to /client in javascript, so RackTest won't do that + signed_in = !cookie_jar['remember_token'].nil? if signed_in find('h2', text: 'create session') else