-
Notifications
You must be signed in to change notification settings - Fork 650
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
Sync DO API keys between clouds #16205
Changes from all commits
60fb07f
5cea5f1
382fb30
c0768cd
273d240
6e9777f
040ca63
56098cc
6a60e94
9ed36fc
eec8ce1
13a9f39
c0e5c3e
88575d0
4feee1a
88b0a70
0090942
b144e76
4f3da6a
103f8e5
60e3683
2051e3e
62bdb3a
0e2796b
305de8e
6cd437d
c57531c
f428951
29df9dc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
module RemoteDoApiKeyCommands | ||
class Create < ::CartoCommand | ||
|
||
private | ||
|
||
def run_command | ||
api_key = Carto::RemoteDoApiKey.new(params) | ||
api_key.save! | ||
end | ||
|
||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
module RemoteDoApiKeyCommands | ||
class Destroy < ::CartoCommand | ||
|
||
private | ||
|
||
def run_command | ||
api_key = Carto::RemoteDoApiKey.new(params) | ||
api_key.destroy! | ||
end | ||
|
||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
module MessageBrokerHelper | ||
|
||
def message_broker | ||
Carto::Common::MessageBroker.new(logger: Rails.logger) | ||
end | ||
|
||
def cartodb_central_topic | ||
message_broker.get_topic(:cartodb_central) | ||
end | ||
|
||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,9 @@ module Carto | |
class ApiKey < ActiveRecord::Base | ||
|
||
include Carto::AuthTokenGenerator | ||
include ::MessageBrokerHelper | ||
|
||
REDIS_KEY_PREFIX = 'api_keys:'.freeze | ||
|
||
TYPE_REGULAR = 'regular'.freeze | ||
TYPE_MASTER = 'master'.freeze | ||
|
@@ -65,15 +68,19 @@ class ApiKey < ActiveRecord::Base | |
validate :valid_default_public_key, if: :default_public? | ||
|
||
after_create :setup_db_role, if: ->(k) { k.needs_setup? && !k.skip_role_setup } | ||
after_create :create_remote_do_api_key, if: ->(api_key) { api_key.master? } | ||
|
||
after_save { remove_from_redis(redis_key(token_was)) if token_changed? } | ||
after_save { invalidate_cache if token_changed? } | ||
after_save :add_to_redis, if: :valid_user? | ||
after_save :save_cdb_conf_info, unless: :skip_cdb_conf_info? | ||
after_save :regenerate_remote_do_api_key, if: ->(k) { k.master? && k.token_changed? } | ||
|
||
after_destroy :reassign_owner, :drop_db_role, if: ->(k) { k.needs_setup? && !k.skip_role_setup } | ||
after_destroy :remove_from_redis | ||
after_destroy :invalidate_cache | ||
after_destroy :remove_cdb_conf_info, unless: :skip_cdb_conf_info? | ||
after_destroy :destroy_remote_do_api_key, if: ->(api_key) { api_key.master? } | ||
|
||
scope :master, -> { where(type: TYPE_MASTER) } | ||
scope :default_public, -> { where(type: TYPE_DEFAULT_PUBLIC) } | ||
|
@@ -150,6 +157,35 @@ def self.new_from_hash(api_key_hash) | |
) | ||
end | ||
|
||
def create_remote_do_api_key | ||
cartodb_central_topic.publish( | ||
:create_remote_do_api_key, | ||
{ type: type, token: token, user_id: user_id, username: user.username } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note to myself: sending both |
||
) | ||
end | ||
|
||
def regenerate_remote_do_api_key | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, but we need a specific message here: there's no guarantee on the order of the messages, and the likelihood of the "create" to overtake the "destroy" is pretty high in this case. I think it is safer to have a specific message/command There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After our chat and for the record: it is OK as worst case scenario is having an old api key in redis for a short while, as old and new have different redis keys. OTOH that triggered the need for this: https://github.com/CartoDB/data-observatory/pull/1010 |
||
original_token, saved_token = token_change | ||
|
||
# Order of execution is not guaranteed, but since the token is different there's no danger | ||
# of race conditions | ||
cartodb_central_topic.publish( | ||
:destroy_remote_do_api_key, | ||
{ token: original_token, user_id: user_id, username: user.username } | ||
) | ||
cartodb_central_topic.publish( | ||
:create_remote_do_api_key, | ||
{ type: type, token: saved_token, user_id: user_id, username: user.username } | ||
) | ||
end | ||
|
||
def destroy_remote_do_api_key | ||
cartodb_central_topic.publish( | ||
:destroy_remote_do_api_key, | ||
{ token: token, user_id: user_id, username: user.username } | ||
) | ||
end | ||
|
||
def revoke_permissions(table, revoked_permissions) | ||
Carto::TableAndFriends.apply(db_connection, table.database_schema, table.name) do |s, t, qualified_name| | ||
query = %{ | ||
|
@@ -379,8 +415,6 @@ def setup_table_permissions | |
|
||
PASSWORD_LENGTH = 40 | ||
|
||
REDIS_KEY_PREFIX = 'api_keys:'.freeze | ||
|
||
def raise_unprocessable_entity_error(error) | ||
raise Carto::UnprocesableEntityError.new(/PG::Error: ERROR: (.+)/ =~ error.message && $1 || 'Unexpected error') | ||
end | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
module Carto | ||
class RemoteDoApiKey | ||
|
||
attr_accessor :token, :type, :username | ||
attr_reader :redis_client | ||
|
||
def initialize(attributes = {}) | ||
attributes = attributes.with_indifferent_access | ||
@token = attributes[:token] | ||
@username = attributes[:username] | ||
@type = attributes[:type] | ||
@redis_client = $users_metadata | ||
end | ||
|
||
def save! | ||
redis_client.hmset(redis_key, redis_hash_as_array) | ||
end | ||
|
||
def destroy! | ||
redis_client.del(redis_key) | ||
end | ||
|
||
private | ||
|
||
def redis_key | ||
"#{Carto::ApiKey::REDIS_KEY_PREFIX}#{username}:#{token}" | ||
end | ||
|
||
def redis_hash_as_array | ||
['user', username, 'type', type] | ||
end | ||
|
||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
require 'spec_helper' | ||
|
||
describe RemoteDoApiKeyCommands::Create do | ||
let(:command) { described_class.new(params) } | ||
let(:params) do | ||
{ | ||
token: '1234-abcd-5678', | ||
username: 'perico', | ||
type: Carto::ApiKey::TYPE_MASTER | ||
} | ||
end | ||
|
||
describe '#run' do | ||
it 'runs OK' do | ||
command.run | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
require 'spec_helper' | ||
|
||
describe RemoteDoApiKeyCommands::Destroy do | ||
let(:command) { described_class.new(token: '1234-abcd-5678', username: 'perico') } | ||
|
||
describe '#run' do | ||
it 'runs OK' do | ||
command.run | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
require 'spec_helper_min' | ||
|
||
describe Carto::RemoteDoApiKey do | ||
let(:username) { 'random-user' } | ||
let(:type) { Carto::ApiKey::TYPE_MASTER } | ||
let(:token) { '1234-abcd-5678' } | ||
let(:redis_key) { 'api_keys:random-user:1234-abcd-5678' } | ||
let(:api_key) { described_class.new(token: token, username: username, type: type) } | ||
|
||
after { clean_redis_databases } | ||
|
||
describe '#initialize' do | ||
it 'correctly initializes the record' do | ||
expect(api_key.token).to eq(token) | ||
expect(api_key.username).to eq(username) | ||
expect(api_key.type).to eq(type) | ||
expect(api_key.redis_client).to be_present | ||
end | ||
end | ||
|
||
describe '#save!' do | ||
it 'saves the API key to redis' do | ||
api_key.save! | ||
|
||
expect($users_metadata.hgetall(redis_key)).to( | ||
eq('user' => 'random-user', 'type' => 'master') | ||
) | ||
end | ||
end | ||
|
||
describe '#destroy!' do | ||
before { api_key.save! } | ||
|
||
it 'destroys the API key from redis' do | ||
api_key.destroy! | ||
|
||
expect($users_metadata.hgetall(redis_key)).to eq({}) | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just an opinion: I like more the syntax we use in Central
(I understand you do need a block or a new function predicate for the check below (regenerate callback))
(as an opinion and likes and dislikes you're entitled to completely ignore it)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the suggestion 👍 . I agree with you and will try to remember this for the next time, but for (lazyness? 😅 ) I'm going to deploy it like it's now to avoid having to wait for the build again and potentially having to do a retry