Skip to content

Commit

Permalink
Merge pull request #15610 from CartoDB/jgoizueta/ch66432/db-direct-al…
Browse files Browse the repository at this point in the history
…lowed-ips-management

Manage Firewall rules for DB Direct allows IPs
  • Loading branch information
jgoizueta committed Apr 27, 2020
2 parents 291d25c + f102662 commit 53ced16
Show file tree
Hide file tree
Showing 7 changed files with 439 additions and 71 deletions.
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Development
- None yet

### Features
- GCP Firewall managemente for DB Direct IPs ([#15610](https://github.com/CartoDB/cartodb/pull/15610))
- Rake tasks to list DB Direct certificates ([#15625](https://github.com/CartoDB/cartodb/pull/15625))
- PKCS#8 keys support for DB-Direct certificates ([#15622](https://github.com/CartoDB/cartodb/pull/15622))

Expand All @@ -23,6 +24,7 @@ sudo make install
```

### Features

* New internal API for managing DB-Direct certificates & IPs ([#15567](https://github.com/CartoDB/cartodb/pull/15567))
* Use Dataservices API client 0.30.0
* Enable deleting Kepler.gl maps ([#15485](https://github.com/CartoDB/cartodb/issues/15485))
Expand Down
43 changes: 43 additions & 0 deletions app/models/carto/dbdirect_ip.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'ip_checker'
require 'carto/dbdirect/firewall_manager'

module Carto
class DbdirectIp < ActiveRecord::Base
Expand All @@ -12,6 +13,38 @@ class DbdirectIp < ActiveRecord::Base

validate :validate_ips

after_save do
update_firewall(*changes[:ips])
end

after_destroy do
update_firewall(ips, nil)
end


def self.firewall_manager
firewall_manager_class.new(config)
end

def self.firewall_manager_class
Carto::Dbdirect::FirewallManager
end

def firewall_rule_name
return nil unless self.class.firewall_enabled?

rule_id = user.dbdirect_bearer.organization&.name || user.dbdirect_bearer.username
self.class.config['rule_name'].gsub('{{id}}', rule_id)
end

def self.config
Cartodb.get_config(:dbdirect, 'firewall') || {}
end

def self.firewall_enabled?
!!config['enabled']
end

private

MAX_IP_MASK_HOST_BITS = 8
Expand Down Expand Up @@ -46,5 +79,15 @@ def validate_ip(ip)
exclude_loopback: true
)
end

def update_firewall(old_ips, new_ips)
return unless self.class.firewall_enabled?

old_ips ||= []
new_ips ||= []
return if old_ips.sort == new_ips.sort

self.class.firewall_manager.replace_rule(firewall_rule_name, new_ips)
end
end
end
8 changes: 6 additions & 2 deletions app/models/carto/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ def send_password_reset!
end

def dbdirect_effective_ips
dbdirect_bearer.dbdirect_ip&.ips || []
dbdirect_effective_ip&.ips || []
end

def dbdirect_effective_ips=(ips)
Expand All @@ -319,7 +319,9 @@ def dbdirect_effective_ips=(ips)
bearer.create_dbdirect_ip!(ips: ips) if ips.present?
end

private
def dbdirect_effective_ip
dbdirect_bearer.dbdirect_ip
end

def dbdirect_bearer
if organization.present? && organization.owner != self
Expand All @@ -329,6 +331,8 @@ def dbdirect_bearer
end
end

private

def set_database_host
self.database_host ||= ::SequelRails.configuration.environment_for(Rails.env)['host']
end
Expand Down
7 changes: 7 additions & 0 deletions config/app_config.yml.sample
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,13 @@ defaults: &defaults
pgproxy:
host: 'dbconn.carto.com'
port: 5432
firewall:
enabled: false
project_id: ''
rule_name: ''
network: ''
target_tag: ''
ports: 'tcp:5432'

development:
<<: *defaults
Expand Down
81 changes: 81 additions & 0 deletions lib/carto/dbdirect/firewall_manager.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
require 'google/apis/compute_v1'

module Carto
module Dbdirect
# Firewall Rule Manager
class FirewallManager
AUTH_URL = 'https://www.googleapis.com/auth/cloud-platform'.freeze
PROJECTS_URL = 'https://www.googleapis.com/compute/v1/projects'.freeze

def initialize(config)
@config = config
@service = Google::Apis::ComputeV1::ComputeService.new
@service.authorization = Google::Auth.get_application_default([AUTH_URL])
end

attr_reader :config

def replace_rule(name, ips)
delete_rule(
project_id: config['project_id'],
name: name
)

if ips.present?
create_rule(
project_id: config['project_id'],
name: name,
network: config['network'],
ips: ips,
target_tag: config['target_tag'],
ports: config['ports']
)
end
end

private

def delete_rule(project_id:, name:)
# `gcloud compute firewall-rules delete "#{name}"`

# we could check existence with @service.list_firewalls(project_id).find {|r| r.name == name}
# but since there could be race conditions anyway we'll just rescue errors
@service.delete_firewall(config['project_id'], name)

rescue Google::Apis::ClientError => error
raise unless error.message =~ /^notFound:/
end

def create_rule(project_id:, name:, network:, ips:, target_tag:, ports:)
# `gcloud -q compute firewall-rules create "#{name}" \
# --network "#{network}" \
# --direction ingress \
# --action allow \
# --source-ranges "#{ips.join(',')}" \
# --target-tags "#{target_tag}" \
# --rules "#{ports}" \
# --no-enable-logging \
# --project "#{project_id}"`

protocol, port = config['ports'].split(':')
allowed = Google::Apis::ComputeV1::Firewall::Allowed.new(
ip_protocol: protocol,
ports: [port.split(',')]
)
rule = Google::Apis::ComputeV1::Firewall.new(
name: name,
allowed: [allowed],
network: network_url(config['project_id'], config['network']),
source_ranges: ips,
target_tags: [config['target_tag']]
)

@service.insert_firewall(config['project_id'], rule)
end

def network_url(project_id, network)
"#{PROJECTS_URL}/#{project_id}/global/networks/#{network}"
end
end
end
end
2 changes: 2 additions & 0 deletions lib/tasks/dbdirect.rake
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ namespace :carto do
if ips.present?
puts "DBDirect IPs for organization #{org_id}:"
puts ips
puts "GCP Firewall Rule name: #{organization.owner.dbdirect_effective_ip.firewall_rule_name}"
else
puts "No IPs defined for organization #{org_id}"
end
Expand All @@ -115,6 +116,7 @@ namespace :carto do
if ips.present?
puts "DBDirect IPs for user #{user_id}:"
puts ips
puts "GCP Firewall Rule name: #{user.dbdirect_effective_ip.firewall_rule_name}"
else
puts "No IPs defined for user #{user_id}"
end
Expand Down
Loading

0 comments on commit 53ced16

Please sign in to comment.