Skip to content

Commit

Permalink
Merge pull request #15567 from CartoDB/dbdirect
Browse files Browse the repository at this point in the history
DB Direct Certificates Management
  • Loading branch information
jgoizueta authored Apr 23, 2020
2 parents 1b91cf3 + cd88ebb commit c02c80b
Show file tree
Hide file tree
Showing 26 changed files with 1,928 additions and 33 deletions.
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ gem 'redis', '~> 3.3.5'

gem 'nokogiri', '~> 1.10'
gem 'statsd-client', '0.0.7', require: 'statsd'
gem 'aws-sdk-acmpca', '~> 1'
gem 'aws-sdk-s3', '~> 1'
gem 'ruby-prof', '0.15.1'
gem 'request_store', '1.1.0'
Expand Down Expand Up @@ -81,6 +82,7 @@ gem 'resque-metrics', '0.1.1'

gem 'net-telnet'

gem 'rubyzip', '>= 2.0.0'

# This is weird. In ruby 2 test-unit is required. We don't know why for sure
gem 'test-unit'
Expand Down
30 changes: 19 additions & 11 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -70,19 +70,25 @@ GEM
argon2 (2.0.2)
ffi (~> 1.9)
ffi-compiler (>= 0.1)
aws-partitions (1.77.0)
aws-sdk-core (3.19.0)
aws-partitions (~> 1.0)
aws-sigv4 (~> 1.0)
aws-eventstream (1.0.3)
aws-partitions (1.286.0)
aws-sdk-acmpca (1.22.0)
aws-sdk-core (~> 3, >= 3.71.0)
aws-sigv4 (~> 1.1)
aws-sdk-core (3.92.0)
aws-eventstream (~> 1.0, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.5.0)
aws-sdk-core (~> 3)
aws-sigv4 (~> 1.0)
aws-sdk-s3 (1.9.0)
aws-sdk-core (~> 3)
aws-sdk-kms (1.30.0)
aws-sdk-core (~> 3, >= 3.71.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.61.1)
aws-sdk-core (~> 3, >= 3.83.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.0)
aws-sigv4 (1.0.2)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.1.1)
aws-eventstream (~> 1.0, >= 1.0.2)
axiom-types (0.1.1)
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
Expand Down Expand Up @@ -474,6 +480,7 @@ DEPENDENCIES
activerecord-postgresql-adapter
addressable (~> 2.5)
analytics-ruby (~> 2.0.0)
aws-sdk-acmpca (~> 1)
aws-sdk-s3 (~> 1)
byebug
capybara (= 2.18.0)
Expand Down Expand Up @@ -531,6 +538,7 @@ DEPENDENCIES
rspec-rails (= 2.12.0)
ruby-prof (= 0.15.1)
ruby-saml (= 1.4.1)
rubyzip (>= 2.0.0)
selenium-webdriver (>= 2.5.0)
sequel (~> 4.45.0)
sequel-rails (~> 1.0.1)
Expand Down
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,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))
- Add Kepler.gl maps to Recent content section in the Dashboard ([#15486](https://github.com/CartoDB/cartodb/issues/15486))
Expand Down
21 changes: 21 additions & 0 deletions app/controllers/carto/api/dbdirect_certificate_presenter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
require 'json'

module Carto
module Api
class DbdirectCertificatePresenter
def initialize(dbdirect_certificate)
@dbdirect_certificate = dbdirect_certificate
end

def to_poro
return {} unless @dbdirect_certificate

{
id: @dbdirect_certificate.id,
name: @dbdirect_certificate.name,
expiration: @dbdirect_certificate.expiration.to_datetime.rfc3339
}
end
end
end
end
131 changes: 131 additions & 0 deletions app/controllers/carto/api/dbdirect_certificates_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
require 'in_mem_zipper'

module Carto
module Api
class DbdirectCertificatesController < ::Api::ApplicationController
include Carto::ControllerHelper
extend Carto::DefaultRescueFroms

skip_before_filter :verify_authenticity_token, only: [:create], if: :zip_formatted_request?

ssl_required :list, :show, :create, :destroy

before_action :load_user
before_action :check_permissions

setup_default_rescues

def index
dbdirect_certificates = @user.dbdirect_certificates
certificates_info = dbdirect_certificates.map do |certificate|
Carto::Api::DbdirectCertificatePresenter.new(certificate).to_poro
end
render_jsonp(certificates_info, 200)
end

def show
dbdirect_certificate = Carto::DbdirectCertificate.find(params[:id])
check_permissions_for_certificate(dbdirect_certificate)
render_jsonp(Carto::Api::DbdirectCertificatePresenter.new(dbdirect_certificate).to_poro, 200)
end

def create
validity_days = params[:validity].blank? ? Carto::DbdirectCertificate.default_validity : params[:validity].to_i
data, cert = Carto::DbdirectCertificate.generate(
user: @user,
name: params[:name],
passphrase: params[:pass],
validity_days: validity_days,
server_ca: params[:server_ca]
)
result = {
id: cert.id,
name: cert.name, # must include name since we may have changed or generated it
client_key: data[:client_key],
client_crt: data[:client_crt],
server_ca: data[:server_ca]
}

respond_to do |format|
format.json do
render_jsonp(result, 201)
end
format.zip do
zip_filename, zip_data = zip_certificates(result)
send_data(
zip_data,
type: "application/zip; charset=binary; header=present",
disposition: "attachment; filename=#{zip_filename}",
status: 201
)
end
end
end

def destroy
dbdirect_certificate = Carto::DbdirectCertificate.find(params[:id])
check_permissions_for_certificate(dbdirect_certificate)
dbdirect_certificate.destroy!
head :no_content
end

private

def zip_certificates(result)
username = @user.username
dbproxy_host = Cartodb.get_config(:dbdirect, 'pgproxy', 'host')
dbproxy_port = Cartodb.get_config(:dbdirect, 'pgproxy', 'port')
certificate_id = result[:id]
certificate_name = result[:name]
client_key = result[:client_key]
client_crt = result[:client_crt]
server_ca = result[:server_ca]

readme = generate_readme(
certificate_id: certificate_id,
certificate_name: certificate_name,
username: username,
dbproxy_host: dbproxy_host,
dbproxy_port: dbproxy_port
)
filename = "#{certificate_name}.zip"

zip_data = InMemZipper.zip(
'README.txt' => readme,
'client.key' => client_key,
'client.crt' => client_crt,
'server_ca.pem' => server_ca
)
[ filename, zip_data ]
end

def generate_readme(readme_params)
if params[:readme].present?
readme = view_context.render(inline: params[:readme], :locals => readme_params)
else
readme = view_context.render(template: 'carto/api/dbdirect_certificates/README.txt.erb', :locals => readme_params)
end
end

def load_user
@user = Carto::User.find(current_viewer.id)
end

def check_permissions
# TODO: should the user be an organization owner?
api_key = Carto::ApiKey.find_by_token(params["api_key"])
if api_key.present?
raise UnauthorizedError unless api_key.master?
raise UnauthorizedError unless api_key.user_id == @user.id
end
unless @user.has_feature_flag?('dbdirect')
raise UnauthorizedError.new("DBDirect not enabled for user #{@user.username}")
end
end

def check_permissions_for_certificate(dbdirect_certificate)
raise UnauthorizedError unless dbdirect_certificate.user_id == @user.id
end
end
end
end
50 changes: 50 additions & 0 deletions app/controllers/carto/api/dbdirect_ips_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
module Carto
module Api
class DbdirectIpsController < ::Api::ApplicationController
include Carto::ControllerHelper
extend Carto::DefaultRescueFroms

ssl_required :show, :update, :destroy

before_action :load_user
before_action :check_permissions

setup_default_rescues

def show
ips = @user.dbdirect_effective_ips
render_jsonp({ ips: ips }, 200)
end

def update
@user.dbdirect_effective_ips = params[:ips]
@user.save!
render_jsonp({ ips: @user.reload.dbdirect_effective_ips }, 201)
end

def destroy
@user.dbdirect_effective_ips = nil
@user.save!
head :no_content
end

private

def load_user
@user = Carto::User.find(current_viewer.id)
end

def check_permissions
# TODO: should the user be an organization owner?
api_key = Carto::ApiKey.find_by_token(params["api_key"])
if api_key.present?
raise UnauthorizedError unless api_key.master?
raise UnauthorizedError unless api_key.user_id == @user.id
end
unless @user.has_feature_flag?('dbdirect')
raise UnauthorizedError.new("DBDirect not enabled for user #{@user.username}")
end
end
end
end
end
1 change: 1 addition & 0 deletions app/controllers/carto/controller_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def rescue_from_standard_error(error)
respond_to do |format|
format.html { render text: message, status: 500 }
format.json { render json: { errors: message }, status: 500 }
format.zip { render text: message, status: 500 }
end
end

Expand Down
76 changes: 76 additions & 0 deletions app/models/carto/dbdirect_certificate.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
require 'carto/dbdirect/certificate_manager'

module Carto
class DbdirectCertificate < ActiveRecord::Base
belongs_to :user, inverse_of: :dbdirect_certificates, foreign_key: :user_id
validates_uniqueness_of :name, scope: :user_id

scope :expired, -> { where('expiration <=', Time.current) }
scope :valid, -> { where('expiration >', Time.current) }

before_destroy :revoke

def self.generate(user:, name:, passphrase: nil, validity_days: nil, server_ca: true)
validity_days ||= config['maximum_validity_days']
name = valid_name(user, name)

certificates, arn = certificate_manager.generate_certificate(
username: user.username,
passphrase: passphrase,
validity_days: validity_days,
server_ca: server_ca
)

new_record = create(
user_id: user.id,
name: name,
arn: arn,
expiration: DateTime.now + validity_days # TODO: extract from cert.?
)

return certificates, new_record
end

def self.default_validity
config[:maximum_validity_days]
end

def self.certificate_manager
certificate_manager_class.new(config)
end

def self.certificate_manager_class
Carto::Dbdirect::CertificateManager
end

private

def revoke
self.class.certificate_manager.revoke_certificate(arn: arn)
end

class <<self
private

def config
Cartodb.get_config(:dbdirect, 'certificates')
end

def certificate_names(user)
Carto::User.find(user.id).dbdirect_certificates.map(&:name)
end

def valid_name(user, name)
name = user.username if name.blank?
names = certificate_names(user)
return name unless name.in?(names)

max_suffix = names.map do |existing_name|
match = /\A#{Regexp.escape name}_(\d+)\Z/.match(existing_name)
match ? match[1].to_i : 0
end.max
"#{name}_#{max_suffix + 1}"
end
end
end
end
Loading

0 comments on commit c02c80b

Please sign in to comment.