Skip to content

Commit

Permalink
Feat/OFAC list updater (#4374)
Browse files Browse the repository at this point in the history
* Add a job to update the Ofac list

* Add stored address api endpoint

* Add validator for ensuring banned addresses can't connect

* Update OFAC script to use new token

* Lint
  • Loading branch information
tsmartt authored Apr 22, 2024
1 parent 722e2df commit afbd4e7
Show file tree
Hide file tree
Showing 18 changed files with 85 additions and 7 deletions.
4 changes: 4 additions & 0 deletions app/controllers/api/v3/public/ofac_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

class Api::V3::Public::OfacController < Api::V3::Public::BaseController
def banned_lists
render(json: {addresses: OfacAddress.pluck(:address)}.to_json, status: 200)
end

def banned_lists_fresh
render(json: ParseOfacListService.perform.to_json, status: 200)
end
end
15 changes: 15 additions & 0 deletions app/jobs/update_ofac_list_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# typed: ignore

class UpdateOfacListJob < ApplicationJob
queue_as :scheduler

def perform
new_ofac_list = ParseOfacListService.perform[:addresses]
raise "Empty list" unless new_ofac_list.present?
list = new_ofac_list.map { |addr| OfacAddress.new(address: addr) }
ActiveRecord::Base.transaction do
ActiveRecord::Base.connection.truncate_tables(:ofac_addresses)
OfacAddress.import list
end
end
end
1 change: 1 addition & 0 deletions app/models/bitflyer_connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class BitflyerConnection < Oauth2::AuthorizationCodeBase

validates :recipient_id, uniqueness: true, allow_blank: true
validates :default_currency, inclusion: {in: SUPPORTED_CURRENCIES}, allow_nil: true
validates :recipient_id, banned_address: true

# FIXME: This should be reused
scope :with_expired_tokens, -> {
Expand Down
2 changes: 2 additions & 0 deletions app/models/gemini_connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class CapabilityError < WalletCreationError; end
belongs_to :publisher
has_many :gemini_connection_for_channels, dependent: :destroy

validates :recipient_id, banned_address: true

encrypts :access_token
encrypts :refresh_token
# GeminiConnections do not have a default currency field, it is always assumed to be BAT
Expand Down
1 change: 1 addition & 0 deletions app/models/gemini_connection_for_channel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ class GeminiConnectionForChannel < ApplicationRecord
belongs_to :channel

validates :channel_identifier, uniqueness: {scope: [:gemini_connection_id, :channel_identifier]}
validates :recipient_id, banned_address: true
end
2 changes: 2 additions & 0 deletions app/models/ofac_address.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class OfacAddress < ApplicationRecord
end
1 change: 1 addition & 0 deletions app/models/uphold_connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class UpholdAccountState
JAPAN = "JP"

validates :default_currency, inclusion: {in: SUPPORTED_CURRENCIES}, allow_nil: true, if: :default_currency_changed?
validates :address, banned_address: true

#################
# Associations
Expand Down
1 change: 1 addition & 0 deletions app/models/uphold_connection_for_channel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ class UpholdConnectionForChannel < ApplicationRecord
NETWORK = "anonymous"

validates :channel_identifier, uniqueness: {scope: [:uphold_connection_id, :channel_identifier, :currency, :uphold_id]}
validates :address, banned_address: true
end
6 changes: 3 additions & 3 deletions app/services/parse_ofac_list_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ def self.fetch_github_file_content(repo_owner:, repo_name:, branch:, file_path:,
https.use_ssl = true

request = Net::HTTP::Get.new(uri)
request.add_field("Authorization", github_headers["Authorization"])
request.add_field("User-Agent", github_headers["User-Agent"])
request.add_field("Authorization", github_headers[:Authorization])
request.add_field("Accept", github_headers[:Accept])

response = https.request(request)
content_data = JSON.parse(response.body)
Expand All @@ -30,7 +30,7 @@ def self.fetch_github_file_content(repo_owner:, repo_name:, branch:, file_path:,
end

def self.fetch_github_repo_top_level_files(repo_owner:, repo_name:, branch:)
github_token = Rails.configuration.pub_secrets[:api_auth_token_github]
github_token = Rails.configuration.pub_secrets[:github_ofac_token]
github_headers = {
Accept: "application/vnd.github.v3+json",
Authorization: "token #{github_token}"
Expand Down
11 changes: 11 additions & 0 deletions app/validators/banned_address_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class BannedAddressValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
banned_addresses = OfacAddress.pluck(:address)

banned_addresses.each do |banned_address|
if !banned_address.blank? && value.to_s.strip.downcase.include?(banned_address.to_s.strip.downcase)
record.errors.add attribute.to_sym, "can't be a banned address"
end
end
end
end
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@
end
namespace :ofac, defaults: {format: :json} do
get "banned_lists"
get "banned_lists_fresh"
end
end
namespace :channels, defaults: {format: :json} do
Expand Down
1 change: 1 addition & 0 deletions config/secrets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ default: &default
slack_webhook_url: <%= ENV["SLACK_WEBHOOK_URL"] %>
# For access to Github API
api_auth_token_github: <%= ENV["API_AUTH_TOKEN_GITHUB"] %>
github_ofac_token: <%= ENV["API_AUTH_TOKEN_GITHUB_OFAC"] %>
# e.g. publishers.brave.com
url_host: <%= ENV["URL_HOST"] %>
uphold_authorization_endpoint: <%= ENV["UPHOLD_AUTHORIZATION_ENDPOINT"] %>
Expand Down
6 changes: 5 additions & 1 deletion config/sidekiq.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@
decription: "For Site Channels created within the past week, enqueue jobs to verify the domain of each unique brave_publisher_id."
queue: scheduler
CleanStaleUpholdDataJob:
cron: "0 4 * * *"
cron: "0 42 * * *"
description: "For Publishers who have stale uphold codes or access parameters, clean out uphold codes or access parameter."
queue: scheduler
UpdateOfacListJob:
cron: "0 2 * * *"
description: "Update the Ofac list"
queue: scheduler
CleanAbandonedSiteChannelsJob:
cron: "0 3 * * *"
description: "Remove abandoned site channels."
Expand Down
7 changes: 7 additions & 0 deletions db/migrate/20240301140330_add_ofac_list.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class AddOfacList < ActiveRecord::Migration[7.1]
def change
create_table :ofac_addresses, id: :uuid do |t|
t.string :address, null: false
end
end
end
6 changes: 5 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.1].define(version: 2024_01_23_163948) do
ActiveRecord::Schema[7.1].define(version: 2024_03_01_140330) do
# These are extensions that must be enabled in order to support this database
enable_extension "btree_gin"
enable_extension "citext"
Expand Down Expand Up @@ -394,6 +394,10 @@
t.index ["user_id"], name: "index_memberships_on_user_id"
end

create_table "ofac_addresses", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "address", null: false
end

create_table "organization_permissions", id: :uuid, default: -> { "uuid_generate_v4()" }, force: :cascade do |t|
t.uuid "organization_id"
t.boolean "uphold_wallet", default: false, null: false
Expand Down
12 changes: 10 additions & 2 deletions test/controllers/api/v3/public/ofac_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,23 @@ class Api::V3::Public::OfacControllerTest < ActionDispatch::IntegrationTest
stub_rewards_parameters
end

test "/api/v3/public/channels/total_verified returns json count of verified channels" do
test "Ofac list from stored" do
ret_json = {"addresses" => OfacAddress.pluck(:address)}

get api_v3_public_ofac_banned_lists_path, headers: {"HTTP_AUTHORIZATION" => "Token token=fake_api_auth_token"}
assert_equal(200, response.status)
assert_equal(ret_json, JSON.parse(response.body))
end

test "Ofac list fresh" do
ret_json = {"addresses" =>
["0x4f47bc496083c727c5fbe3ce9cdf2b0f6496270c",
"18M8bJWMzWHDBMxoLqjHHAffdRy4SrzkfB",
"qpf2cphc5dkuclkqur7lhj2yuqq9pk3hmukle77vhq",
"qpusmp64rajses77x95g9ah825mtyyv74smwwkxhx3"]}
ParseOfacListService.expects(:fetch_github_repo_top_level_files).returns(ret_json)

get api_v3_public_ofac_banned_lists_path, headers: {"HTTP_AUTHORIZATION" => "Token token=fake_api_auth_token"}
get api_v3_public_ofac_banned_lists_fresh_path, headers: {"HTTP_AUTHORIZATION" => "Token token=fake_api_auth_token"}

assert_equal(200, response.status)
assert_equal(ret_json, JSON.parse(response.body))
Expand Down
8 changes: 8 additions & 0 deletions test/fixtures/ofac_addresses.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
one:
address: '9VoNhsLUhXVTzmqTE4iRPTTmACREYrKR11HG4J4BKyfp'

two:
address: 'HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH'

three:
address: 'BwG1C3hDdNq2rTDHvcMFwuokefdQvUQUXRQgwjJL86XM'
7 changes: 7 additions & 0 deletions test/models/gemini_connection_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,13 @@ class GeminiConnectionTest < SidekiqTestCase
it "is valid" do
assert gemini_connection.valid?
end

it "is invalid when using a restricted address" do
gemini_connection.recipient_id = OfacAddress.last.address
gemini_connection.save
refute gemini_connection.valid?
assert gemini_connection.errors.full_messages.first == "Recipient can't be a banned address"
end
end
end

Expand Down

0 comments on commit afbd4e7

Please sign in to comment.