Skip to content

Commit

Permalink
Add ability to restrict by geoip
Browse files Browse the repository at this point in the history
  - City
  - Country
  - Continent
  • Loading branch information
dnfd committed Sep 5, 2019
1 parent 689f179 commit 3425c90
Show file tree
Hide file tree
Showing 16 changed files with 200 additions and 17 deletions.
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ gem 'uglifier', '>= 1.3.0'
# See https://github.com/rails/execjs#readme for more supported runtimes
gem 'mini_racer', platforms: :ruby

gem 'maxmind-db', '~> 1.0'

gem 'kaminari'

gem 'peatio', '~> 0.4.4'
Expand Down
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ GEM
mini_mime (>= 0.1.1)
marcel (0.3.3)
mimemagic (~> 0.3.2)
maxmind-db (1.0.0)
memoist (0.16.0)
method_source (0.9.2)
mime-types (3.2.2)
Expand Down Expand Up @@ -391,6 +392,7 @@ DEPENDENCIES
jwt-multisig (~> 1.0)
kaminari
listen (>= 3.0.5, < 3.2)
maxmind-db (~> 1.0)
memoist (~> 0.16)
mini_racer
mysql2 (>= 0.4.4, < 0.6.0)
Expand Down
4 changes: 3 additions & 1 deletion app/models/restriction.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# frozen_string_literal: true

class Restriction < ApplicationRecord
SCOPES = %w[continent country city ip ip_subnet]
NET_ADDR_SCOPES = %w[ip ip_subnet]
LOCATION_SCOPES = %w[continent country city]
SCOPES = NET_ADDR_SCOPES | LOCATION_SCOPES
STATES = %w[enabled disabled]
SUBNET_REGEX = /\A([0-9]{1,3}\.){3}[0-9]{1,3}\/([0-9]|[1-2][0-9]|3[0-2])\z/

Expand Down
6 changes: 5 additions & 1 deletion config/initializers/barong.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,12 @@
config.set(:app_name, 'Barong')
config.set(:barong_domain, 'barong.io')
config.set(:barong_uid_prefix, 'ID', regex: /^[A-z]{2,6}$/)
config.set(:barong_config, 'barong.yml')
config.set(:barong_config, 'config/barong.yml', type: :path)
config.set(:barong_maxminddb_path, '', type: :path)
config.set(:barong_geoip_lang, 'en', values: %w[en de es fr ja ru])
end

Barong::GeoIP.lang = Barong::App.config.barong_geoip_lang

Rails.application.config.x.keystore = kstore
Barong::App.config.keystore = kstore
6 changes: 1 addition & 5 deletions config/initializers/barong_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@ def list
private

def read_from_yaml
conf = YAML.safe_load(
ERB.new(
File.read(Rails.root.join('config', Barong::App.config.barong_config))
).result
)
conf = YAML.load_file(Barong::App.config.barong_config)
conf['activation_requirements'] = {'email' => 'verified'} unless conf['activation_requirements']
conf
end
Expand Down
3 changes: 0 additions & 3 deletions config/initializers/sessions_store.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
# frozen_string_literal:true

require 'barong/app'

# Store sessions in cookies

Rails.application.config.session_store :cookie_store, key: '_barong_session'
Barong::App.set(:session_expire_time, '1800', type: :integer)
9 changes: 8 additions & 1 deletion lib/barong/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,17 @@ def type!(key, value, options)
when :bool
values!(key, value, %w(true false))
return value == 'true'

when :integer
regex!(key, value, /^\d+$/)
return value
when :path
return Rails.root.join(value).tap { |p| path!(key, p) }
end
end

def path!(key, path)
unless File.exists?(path)
raise Error.new("#{key.to_s.upcase} path is invalid #{path.to_s}")
end
end

Expand Down
25 changes: 19 additions & 6 deletions lib/barong/authorize.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,28 @@ def api_key_owner
end

def validate_restrictions!
restrictions = Rails.cache.fetch('restrictions', expires_in: 5.minutes) do
Restriction.where(state: 'enabled').to_a
restrictions = Rails.cache.fetch('restrictions', expires_in: 5.minutes) { fetch_restrictions }

request_ip = @request.remote_ip

restrict! if restrictions[:ip].include?(request_ip)
restrict! if restrictions[:ip_subnet].any? { |r| IPAddr.new(r).include?(request_ip) }

scopes = Restriction::LOCATION_SCOPES.map(&:to_sym)
values = Barong::GeoIP.info(request_ip, *scopes)

Hash[scopes.zip values].each do |scope, value|
restrict! if value && restrictions[scope].any? { |r| r.casecmp?(value) }
end
end

ips = restrictions.select { |r| r.scope == 'ip' }.map { |r| r.value }
restrict! if ips.include? @request.remote_ip
def fetch_restrictions
enabled = Restriction.where(state: 'enabled').to_a

ip_ranges = restrictions.select { |r| r.scope == 'ip_subnet' }.map { |r| r.value }
restrict! if ip_ranges.any? { |r| IPAddr.new(r).include? @request.remote_ip }
Restriction::SCOPES.inject(Hash.new) do |table, scope|
scope_restrictions = enabled.select { |r| r.scope == scope }.map!(&:value)
table.tap { |t| t[scope.to_sym] = scope_restrictions }
end
end

def restrict!
Expand Down
57 changes: 57 additions & 0 deletions lib/barong/geo_ip.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# frozen_string_literal: true

module Barong
# MaxmindDB reader adapter
module GeoIP
class << self
attr_accessor :lang

# Usage: country, city = Barong::GeoIP.info(request_ip, :country, :city)
def info(ip, *keys)
record = reader.get(ip)
keys.map { |key| fetch(record, key) }
end

# Usage: city = Barong::GeoIP.get(ip: ip, key: :city)
def get(ip:, key:)
fetch(reader.get(ip), key)
end

private
def reader
@reader ||= MaxMind::DB.new(Barong::App.config.barong_maxminddb_path, mode: MaxMind::DB::MODE_MEMORY)
end

def fetch(record, key)
return unless record

case key.to_sym
when :country
return country(record)
when :continent
return continent(record)
when :city
return city(record)
end
end

def country(record)
record['country']['names'][lang] if record['country']
end

def continent(record)
record['continent']['names'][lang] if record['continent']
end

def city(record)
if record['city']
record['city']['names'][lang]
elsif record['subdivisions']
record['subdivisions'].first['names'][lang]
else
nil
end
end
end
end
end
2 changes: 2 additions & 0 deletions spec/api/v2/auth/audit_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
require 'spec_helper'

describe '/api/v2/auth functionality test' do
include_context 'geoip mock'

before do
create :permission, role: 'admin', action: 'AUDIT', verb: 'put', path: 'api/v2/admin/users'
create :permission, role: 'member'
Expand Down
2 changes: 2 additions & 0 deletions spec/api/v2/auth/auth_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
require 'spec_helper'

describe '/api/v2/auth functionality test' do
include_context 'geoip mock'

let(:uri) { '/api/v2/identity/sessions' }
let!(:create_permissions) do
create :permission, role: 'admin'
Expand Down
3 changes: 3 additions & 0 deletions spec/api/v2/auth/rbac_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# frozen_string_literal: true

require 'spec_helper'

describe '/api/v2/auth functionality test' do
include_context 'geoip mock'

let(:do_protected_request) { get '/api/v2/auth/api/v2/resource/users/me' }

describe 'testing rbac workability' do
Expand Down
59 changes: 59 additions & 0 deletions spec/api/v2/auth/restriction_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
require 'spec_helper'

describe '/api/v2/auth functionality test' do
include_context 'geoip mock'

let(:uri) { '/api/v2/identity/sessions' }
let!(:create_permissions) do
create :permission, role: 'member', action: 'ACCEPT', verb: 'all', path: 'tasty_endpoint'
Expand Down Expand Up @@ -58,6 +60,7 @@

expect(response.status).to eq(401)
expect(response.headers['Authorization']).to be_nil
expect(response.body).to eq("{\"errors\":[\"authz.access_restricted\"]}")
end

it 'request with non-restricted ip' do
Expand All @@ -67,5 +70,61 @@
expect(response.status).to eq(200)
end
end

context 'geoip' do
context 'restricts with city' do
let!(:restriction) { create(:restriction, value: 'London', scope: 'city') }

it 'with restricted ip' do
allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return(london_ip)
get auth_request
expect(response.status).to eq(401)
expect(response.headers['Authorization']).to be_nil
expect(response.body).to eq("{\"errors\":[\"authz.access_restricted\"]}")
end

it 'with non-restricted ip' do
allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return(tokyo_ip)
get auth_request
expect(response.status).to eq(200)
end
end

context 'restricts with country' do
let!(:restriction) { create(:restriction, value: 'japan', scope: 'country') }

it 'with restricted ip' do
allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return(tokyo_ip)
get auth_request
expect(response.status).to eq(401)
expect(response.headers['Authorization']).to be_nil
expect(response.body).to eq("{\"errors\":[\"authz.access_restricted\"]}")
end

it 'with non-restricted ip' do
allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return(london_ip)
get auth_request
expect(response.status).to eq(200)
end
end

context 'restricts with continent' do
let!(:restriction) { create(:restriction, value: 'EUROPE', scope: 'continent') }

it 'with restricted ip' do
allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return(london_ip)
get auth_request
expect(response.status).to eq(401)
expect(response.headers['Authorization']).to be_nil
expect(response.body).to eq("{\"errors\":[\"authz.access_restricted\"]}")
end

it 'with non-restricted ip' do
allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return(tokyo_ip)
get auth_request
expect(response.status).to eq(200)
end
end
end
end
end
2 changes: 2 additions & 0 deletions spec/api/v2/identity/sessions_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# frozen_string_literal: true

describe API::V2::Identity::Sessions do
include_context 'geoip mock'

include ActiveSupport::Testing::TimeHelpers
let!(:create_member_permission) do
create :permission,
Expand Down
2 changes: 2 additions & 0 deletions spec/api/v2/identity/users_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
require 'spec_helper'

describe API::V2::Identity::Users do
include_context 'geoip mock'

let!(:create_member_permission) do
create :permission,
role: 'member',
Expand Down
33 changes: 33 additions & 0 deletions spec/support/maxmin_context.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

shared_context 'geoip mock' do
let(:london_ip) { '196.245.163.202' }
let(:tokyo_ip) { '140.227.60.114' }

before do
class DummyReader
def get(ip)
case ip
when '196.245.163.202'
{
'country' => { 'names' => { 'en' => 'United Kingdom' } },
'city' => { 'names' => { 'en' => 'London' } },
'continent' => { 'names' => { 'en' => 'Europe' } }
}
when '140.227.60.114'
{
'country' => { 'names' => { 'en' => 'Japan' } },
'city' => { 'names' => { 'en' => 'Tokyo' } },
'continent' => { 'names' => { 'en' => 'Asia' } }
}
else
{}
end
end
end

reader = DummyReader.new

allow(Barong::GeoIP).to receive(:reader).and_return(reader)
end
end

0 comments on commit 3425c90

Please sign in to comment.