Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to restrict by geoip #920

Merged
merged 3 commits into from
Sep 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ ARG RAILS_ENV=production
ARG UID=1000
ARG GID=1000

ARG MAXMINDDB_LINK
ENV MAXMINDDB_LINK=${MAXMINDDB_LINK:-https://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz}
# Devise requires secret key to be set during image build or it raises an error
# preventing from running any scripts.
# Users should override this variable by passing environment variable on container start.
Expand All @@ -36,6 +38,13 @@ RUN bundle install --jobs=$(nproc) --deployment --binstubs
# Copy the main application.
COPY --chown=app:app . $APP_HOME

# Download MaxMind Country DB
RUN wget -O ${APP_HOME}/geolite.tar.gz ${MAXMINDDB_LINK} \
&& mkdir -p ${APP_HOME}/geolite \
&& tar xzf ${APP_HOME}/geolite.tar.gz -C ${APP_HOME}/geolite --strip-components 1 \
&& rm ${APP_HOME}/geolite.tar.gz
ENV BARONG_MAXMINDDB_PATH=${APP_HOME}/geolite/GeoLite2-Country.mmdb

# Initialize application configuration & assets.
RUN ./bin/init_config \
&& bundle exec rake tmp:create
Expand Down
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
2 changes: 1 addition & 1 deletion app/models/restriction.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

class Restriction < ApplicationRecord
SCOPES = %w[continent country city ip ip_subnet]
SCOPES = %w[continent country ip ip_subnet]
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)
ec marked this conversation as resolved.
Show resolved Hide resolved
unless File.exists?(path)
raise Error.new("#{key.to_s.upcase} path is invalid #{path.to_s}")
end
end

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

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

request_ip = @request.remote_ip
country = Barong::GeoIP.info(ip: request_ip, key: :country)
continent = Barong::GeoIP.info(ip: request_ip, key: :continent)

ips = restrictions.select { |r| r.scope == 'ip' }.map { |r| r.value }
restrict! if ips.include? @request.remote_ip
restrict! if restrictions['ip'].include?(request_ip)
restrict! if restrictions['ip_subnet'].any? { |r| IPAddr.new(r).include?(request_ip) }
restrict! if restrictions['continent'].any? { |r| r.casecmp?(continent) }
restrict! if restrictions['country'].any? { |r| r.casecmp?(country) }
end

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 }
def fetch_restrictions
enabled = Restriction.where(state: 'enabled').to_a

Restriction::SCOPES.inject(Hash.new) do |table, scope|
scope_restrictions = enabled.select { |r| r.scope == scope }.map!(&:value)
table.tap { |t| t[scope] = scope_restrictions }
end
end

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

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

# Usage: city = Barong::GeoIP.get(ip: ip, key: :city)
def info(ip:, key:)
record = reader.get(ip)
return unless record

case key.to_sym
when :country
return record['country']['names'][lang] if record['country']
when :continent
return record['continent']['names'][lang] if record['continent']
end
end

private

def reader
@reader ||= MaxMind::DB.new(Barong::App.config.barong_maxminddb_path, mode: MaxMind::DB::MODE_MEMORY)
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
41 changes: 41 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,43 @@
expect(response.status).to eq(200)
end
end

context 'geoip' do
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
31 changes: 31 additions & 0 deletions spec/support/geoip_context.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# 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' } },
'continent' => { 'names' => { 'en' => 'Europe' } }
}
when '140.227.60.114'
{
'country' => { 'names' => { 'en' => 'Japan' } },
'continent' => { 'names' => { 'en' => 'Asia' } }
}
else
{}
end
end
end

reader = DummyReader.new

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