Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .releaserc.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ module.exports = {
'touch $HOME/.gem/credentials '+
'chmod 0600 $HOME/.gem/credentials '+
'printf -- "---\n:rubygems_api_key: ${env.GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials '+
'( cd packages/forest_admin_agent && gem build && gem push forest_admin_agent-${nextRelease.version}.gem )' +
'( cd packages/forest_admin_datasource_toolkit && gem build && gem push forest_admin_datasource_toolkit-${nextRelease.version}.gem )' +
'( cd packages/forest_admin_datasource_toolkit && gem build && gem push forest_admin_datasource_toolkit-${nextRelease.version}.gem )' +
'( cd packages/forest_admin_rails && gem build && gem push forest_admin_rails-${nextRelease.version}.gem )' ,
'( cd packages/forest_admin_agent && gem build && touch .trigger-rubygem-release )' +
'( cd packages/forest_admin_datasource_toolkit && gem build && touch .trigger-rubygem-release )' +
'( cd packages/forest_admin_datasource_toolkit && gem build && touch .trigger-rubygem-release )' +
'( cd packages/forest_admin_rails && gem build && touch .trigger-rubygem-release )' ,
},
],
[
Expand Down
3 changes: 2 additions & 1 deletion packages/forest_admin_agent/forest_admin_agent.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ admin work on any Ruby application."
spec.require_paths = ["lib"]

spec.add_dependency "dry-container", "~> 0.11"
spec.add_dependency "lightly", "~> 0.4.0"
spec.add_dependency "ipaddress", "~> 0.8.3"
spec.add_dependency "jsonapi-serializers", "~> 1.0"
spec.add_dependency "jwt", "~> 2.7"
spec.add_dependency "lightly", "~> 0.4.0"
spec.add_dependency "mono_logger", "~> 1.1"
spec.add_dependency "openid_connect", "~> 2.2"
spec.add_dependency "rake", "~> 13.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module ForestAdminAgent
module Facades
class Whitelist
def self.check_ip(request_ip)
ip_whitelist = ForestAdminAgent::Services::IpWhitelist.new
return unless ip_whitelist.enabled?
return if ip_whitelist.ip_matches_any_rule?(request_ip)

raise Net::HTTPExceptions, "IP address rejected (#{request_ip})"
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module ForestAdminAgent
module Routes
class AbstractAuthenticatedRoute < AbstractRoute
def build(args = {})
# TODO: handle call permissions
Facades::Whitelist.check_ip(args[:headers]['action_dispatch.remote_ip'].to_s)
end
end
end
end
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module ForestAdminAgent
module Routes
module Resources
class List < AbstractRoute
class List < AbstractAuthenticatedRoute
include ForestAdminAgent::Builder
def setup_routes
add_route('forest_list', 'get', '/:collection_name', ->(args) { handle_request(args) })
Expand All @@ -12,6 +12,8 @@ def setup_routes
def handle_request(args = {})
# is_collection true for a list false for a single record
# JSONAPI::Serializer.serialize(record, is_collection: true, serializer: Serializer::ForestSerializer)
build(args)
{ name: args['collection_name'], content: args['collection_name'] }
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def setup_routes
end

def handle_authentication(args = {})
# Facades::Whitelist.check_ip(args[:headers]['action_dispatch.remote_ip'].to_s)
rendering_id = get_and_check_rendering_id args

{
Expand All @@ -36,6 +37,7 @@ def handle_authentication(args = {})
end

def handle_authentication_callback(args = {})
# Facades::Whitelist.check_ip(args[:headers]['action_dispatch.remote_ip'].to_s)
token = auth.verify_code_and_generate_token(args)
token_data = JWT.decode(
token,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
require 'ipaddress'
require 'net/http'

module ForestAdminAgent
module Services
class IpWhitelist
RULE_MATCH_IP = 0
RULE_MATCH_RANGE = 1
RULE_MATCH_SUBNET = 2

def initialize
fetch_rules
end

def use_ip_whitelist
@use_ip_whitelist ||= false
end

def rules
@rules ||= []
end

def enabled?
use_ip_whitelist && !rules.empty?
end

def ip_matches_any_rule?(ip)
rules.any? { |rule| ip_matches_rule?(ip, rule) }
end

def ip_matches_rule?(ip, rule)
case rule['type']
when RULE_MATCH_IP
ip_match_ip?(ip, rule['ip'])
when RULE_MATCH_RANGE
ip_match_range?(ip, rule['ipMinimum'], rule['ipMaximum'])
when RULE_MATCH_SUBNET
ip_match_subnet?(ip, rule['range'])
else
raise 'Invalid rule type'
end
end

def ip_match_ip?(ip1, ip2)
return both_loopback?(ip1, ip2) unless same_ip_version?(ip1, ip2)

if ip1 == ip2
true
else
both_loopback?(ip1, ip2)
end
end

def same_ip_version?(ip1, ip2)
ip_version(ip1) == ip_version(ip2)
end

def ip_version(ip)
(IPAddress ip).is_a?(IPAddress::IPv4) ? :ip_v4 : :ip_v6
end

def both_loopback?(ip1, ip2)
IPAddress(ip1).loopback? && IPAddress(ip2).loopback?
end

def ip_match_range?(ip, min, max)
return false unless same_ip_version?(ip, min)

ip_range_minimum = (IPAddress min)
ip_range_maximum = (IPAddress max)
ip_value = (IPAddress ip)

ip_value >= ip_range_minimum && ip_value <= ip_range_maximum
end

def ip_match_subnet?(ip, subnet)
return false unless same_ip_version?(ip, subnet)

IPAddress(subnet).include?(IPAddress(ip))
end

private

def fetch_rules
response = Net::HTTP.get_response(
URI("#{Facades::Container.cache(:forest_server_url)}/liana/v1/ip-whitelist-rules"),
{ 'Content-Type' => 'application/json', 'forest-secret-key' => Facades::Container.cache(:env_secret) }
)

raise Error, ForestAdminAgent::Utils::ErrorMessages::UNEXPECTED unless response.is_a?(Net::HTTPSuccess)

body = JSON.parse(response.body)
ip_whitelist_data = body['data']['attributes']

@use_ip_whitelist = ip_whitelist_data['use_ip_whitelist']
@rules = ip_whitelist_data['rules']
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ module Auth
let(:oidc_discover_resource) { instance_double(OpenIDConnect::Discovery::Provider::Config::Resource) }
let(:faraday_connection) { instance_double(Faraday::Connection) }

before do
lightly = Lightly.new(dir: "#{Facades::Container.cache(:cache_dir)}/issuer")
lightly.flush
end

context 'when then oidc is called and forest api is down' do
let(:response_bad_request) { instance_double(Faraday::Response, status: 500, body: {}) }
let(:oidc_resource) { instance_double(ForestAdminAgent::Auth::OAuth2::OidcConfig::Resource) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
require 'spec_helper'
require 'net/http'

module ForestAdminAgent
module Services
describe IpWhitelist do
subject(:ip_whitelist) { described_class.new }

let(:net_http) { class_double(Net::HTTP).as_stubbed_const }
let(:net_http_response) { Net::HTTPSuccess.new('1.1', '200', 'OK') }

context 'when there is no rule' do
before do
allow(net_http_response).to receive(:body).and_return('{
"data":{
"type":"ip-whitelist-rules",
"id":"1",
"attributes":{
"rules":[],
"use_ip_whitelist":false
}
}
}')
allow(net_http).to receive(:get_response).and_return(net_http_response)
end

it 'is not enabled' do
expect(ip_whitelist.enabled?).to be false
end

it 'returns empty rules' do
expect(ip_whitelist.rules).to eq []
end

it 'returns false on use_ip_whitelist' do
expect(ip_whitelist.use_ip_whitelist).to be false
end
end

context 'when there is bad response from the server' do
before do
allow(net_http).to receive(:get_response).and_return(Net::HTTPBadGateway)
end

it 'is not enabled' do
expect do
ip_whitelist.enabled?
end.to raise_error(ForestAdminAgent::Utils::ErrorMessages::UNEXPECTED)
end
end

context 'when there is a rule and use_ip_whitelist is true' do
before do
allow(net_http_response).to receive(:body).and_return('{
"data":{
"type":"ip-whitelist-rules",
"id":"1",
"attributes":{
"rules":[
{ "type": 0, "ip": "127.0.0.1" }
],
"use_ip_whitelist":true
}
}
}')
allow(net_http).to receive(:get_response).and_return(net_http_response)
end

it 'is enabled' do
expect(ip_whitelist.enabled?).to be true
end
end

context 'when there is a rule of type RULE_MATCH_IP and use_ip_whitelist is true' do
let(:client_ip) { '127.0.0.1' }
let(:client2_ip) { '10.10.10.1' }

before do
allow(net_http_response).to receive(:body).and_return('{
"data":{
"type":"ip-whitelist-rules",
"id":"1",
"attributes":{
"rules":[
{ "type": 0, "ip": "127.0.0.1" }
],
"use_ip_whitelist":true
}
}
}')
allow(net_http).to receive(:get_response).and_return(net_http_response)
end

it 'returns true when the client ip is in the whitelist' do
expect(ip_whitelist.ip_matches_any_rule?(client_ip)).to be true
end

it 'returns false when the client ip is not in the whitelist' do
expect(ip_whitelist.ip_matches_any_rule?(client2_ip)).to be false
end
end

context 'when there is a rule of type RULE_MATCH_RANGE and use_ip_whitelist is true' do
let(:client_ip) { '10.0.0.44' }
let(:client2_ip) { '10.0.0.200' }

before do
allow(net_http_response).to receive(:body).and_return('{
"data":{
"type":"ip-whitelist-rules",
"id":"1",
"attributes":{
"rules":[
{ "type": 1, "ipMinimum": "10.0.0.1", "ipMaximum": "10.0.0.100"}
],
"use_ip_whitelist":true
}
}
}')
allow(net_http).to receive(:get_response).and_return(net_http_response)
end

it 'returns true when the client ip is in the whitelist' do
expect(ip_whitelist.ip_matches_any_rule?(client_ip)).to be true
end

it 'returns false when the client ip is not in the whitelist' do
expect(ip_whitelist.ip_matches_any_rule?(client2_ip)).to be false
end
end

context 'when there is a rule of type RULE_MATCH_SUBNET and use_ip_whitelist is true' do
let(:client_ip) { '200.10.10.20' }
let(:client2_ip) { '200.10.20.20' }

before do
allow(net_http_response).to receive(:body).and_return('{
"data":{
"type":"ip-whitelist-rules",
"id":"1",
"attributes":{
"rules":[
{ "type": 2, "range": "200.10.10.0/24"}
],
"use_ip_whitelist":true
}
}
}')
allow(net_http).to receive(:get_response).and_return(net_http_response)
end

it 'returns true when the client ip is in the whitelist' do
expect(ip_whitelist.ip_matches_any_rule?(client_ip)).to be true
end

it 'returns false when the client ip is not in the whitelist' do
expect(ip_whitelist.ip_matches_any_rule?(client2_ip)).to be false
end
end

context 'when there is an unknown rule type' do
let(:client_ip) { '200.10.10.20' }

before do
allow(net_http_response).to receive(:body).and_return('{
"data":{
"type":"ip-whitelist-rules",
"id":"1",
"attributes":{
"rules":[
{ "type": 4}
],
"use_ip_whitelist":true
}
}
}')
allow(net_http).to receive(:get_response).and_return(net_http_response)
end

it 'raises an error' do
expect do
ip_whitelist.ip_matches_any_rule?(client_ip)
end.to raise_error('Invalid rule type')
end
end
end
end
end