pg-aws_rds_iam 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/pull-request.yml +90 -0
- data/.gitignore +14 -0
- data/.rubocop.yml +40 -0
- data/.ruby-version +1 -0
- data/.yardopts +2 -0
- data/CHANGELOG.md +17 -0
- data/CODE_OF_CONDUCT.md +87 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +95 -0
- data/LICENSE.txt +21 -0
- data/README.md +127 -0
- data/Rakefile +39 -0
- data/bin/bundle +105 -0
- data/bin/console +8 -0
- data/bin/install-bundler +6 -0
- data/bin/rake +29 -0
- data/bin/setup +6 -0
- data/bin/yard +29 -0
- data/lib/pg/aws_rds_iam.rb +30 -0
- data/lib/pg/aws_rds_iam/auth_token_generator.rb +29 -0
- data/lib/pg/aws_rds_iam/auth_token_generator_registry.rb +68 -0
- data/lib/pg/aws_rds_iam/auth_token_injector.rb +48 -0
- data/lib/pg/aws_rds_iam/connection.rb +25 -0
- data/lib/pg/aws_rds_iam/connection_info.rb +21 -0
- data/lib/pg/aws_rds_iam/connection_info/keyword_value_string.rb +114 -0
- data/lib/pg/aws_rds_iam/connection_info/parse_error.rb +9 -0
- data/lib/pg/aws_rds_iam/connection_info/uri.rb +44 -0
- data/lib/pg/aws_rds_iam/version.rb +8 -0
- data/pg-aws_rds_iam.gemspec +39 -0
- metadata +256 -0
data/Rakefile
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rake/testtask"
|
5
|
+
require "rubocop/rake_task"
|
6
|
+
require "yard"
|
7
|
+
|
8
|
+
namespace :test do
|
9
|
+
Rake::TestTask.new :acceptance do |t|
|
10
|
+
t.description = "Run acceptance tests"
|
11
|
+
t.libs << "test"
|
12
|
+
t.test_files = ["test/acceptance/test.rb"]
|
13
|
+
end
|
14
|
+
|
15
|
+
Rake::TestTask.new :unit do |t|
|
16
|
+
t.description = "Run unit tests"
|
17
|
+
t.libs << "test"
|
18
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
desc "Run all tests"
|
23
|
+
task :test => ["test:unit", "test:acceptance"]
|
24
|
+
|
25
|
+
RuboCop::RakeTask.new do |t|
|
26
|
+
t.formatters = ["fuubar"]
|
27
|
+
end
|
28
|
+
|
29
|
+
desc "Generate documentation"
|
30
|
+
YARD::Rake::YardocTask.new
|
31
|
+
|
32
|
+
namespace :yard do
|
33
|
+
desc "Run documentation server"
|
34
|
+
task :server do
|
35
|
+
exec "bin/yard", "server", "--reload"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
task :default => ["test:unit", :rubocop]
|
data/bin/bundle
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'bundle' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "rubygems"
|
12
|
+
|
13
|
+
m = Module.new do
|
14
|
+
module_function
|
15
|
+
|
16
|
+
def invoked_as_script?
|
17
|
+
File.expand_path($0) == File.expand_path(__FILE__)
|
18
|
+
end
|
19
|
+
|
20
|
+
def env_var_version
|
21
|
+
ENV["BUNDLER_VERSION"]
|
22
|
+
end
|
23
|
+
|
24
|
+
def cli_arg_version
|
25
|
+
return unless invoked_as_script? # don't want to hijack other binstubs
|
26
|
+
return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update`
|
27
|
+
bundler_version = nil
|
28
|
+
update_index = nil
|
29
|
+
ARGV.each_with_index do |a, i|
|
30
|
+
if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN
|
31
|
+
bundler_version = a
|
32
|
+
end
|
33
|
+
next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
|
34
|
+
bundler_version = $1 || ">= 0.a"
|
35
|
+
update_index = i
|
36
|
+
end
|
37
|
+
bundler_version
|
38
|
+
end
|
39
|
+
|
40
|
+
def gemfile
|
41
|
+
gemfile = ENV["BUNDLE_GEMFILE"]
|
42
|
+
return gemfile if gemfile && !gemfile.empty?
|
43
|
+
|
44
|
+
File.expand_path("../../Gemfile", __FILE__)
|
45
|
+
end
|
46
|
+
|
47
|
+
def lockfile
|
48
|
+
lockfile =
|
49
|
+
case File.basename(gemfile)
|
50
|
+
when "gems.rb" then gemfile.sub(/\.rb$/, gemfile)
|
51
|
+
else "#{gemfile}.lock"
|
52
|
+
end
|
53
|
+
File.expand_path(lockfile)
|
54
|
+
end
|
55
|
+
|
56
|
+
def lockfile_version
|
57
|
+
return unless File.file?(lockfile)
|
58
|
+
lockfile_contents = File.read(lockfile)
|
59
|
+
return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
|
60
|
+
Regexp.last_match(1)
|
61
|
+
end
|
62
|
+
|
63
|
+
def bundler_version
|
64
|
+
@bundler_version ||= begin
|
65
|
+
env_var_version || cli_arg_version ||
|
66
|
+
lockfile_version || "#{Gem::Requirement.default}.a"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def load_bundler!
|
71
|
+
ENV["BUNDLE_GEMFILE"] ||= gemfile
|
72
|
+
|
73
|
+
# must dup string for RG < 1.8 compatibility
|
74
|
+
activate_bundler(bundler_version.dup)
|
75
|
+
end
|
76
|
+
|
77
|
+
def activate_bundler(bundler_version)
|
78
|
+
if Gem::Version.correct?(bundler_version) && Gem::Version.new(bundler_version).release < Gem::Version.new("2.0")
|
79
|
+
bundler_version = "< 2"
|
80
|
+
end
|
81
|
+
gem_error = activation_error_handling do
|
82
|
+
gem "bundler", bundler_version
|
83
|
+
end
|
84
|
+
return if gem_error.nil?
|
85
|
+
require_error = activation_error_handling do
|
86
|
+
require "bundler/version"
|
87
|
+
end
|
88
|
+
return if require_error.nil? && Gem::Requirement.new(bundler_version).satisfied_by?(Gem::Version.new(Bundler::VERSION))
|
89
|
+
warn "Activating bundler (#{bundler_version}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_version}'`"
|
90
|
+
exit 42
|
91
|
+
end
|
92
|
+
|
93
|
+
def activation_error_handling
|
94
|
+
yield
|
95
|
+
nil
|
96
|
+
rescue StandardError, LoadError => e
|
97
|
+
e
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
m.load_bundler!
|
102
|
+
|
103
|
+
if m.invoked_as_script?
|
104
|
+
load Gem.bin_path("bundler", "bundle")
|
105
|
+
end
|
data/bin/console
ADDED
data/bin/install-bundler
ADDED
data/bin/rake
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rake' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("rake", "rake")
|
data/bin/setup
ADDED
data/bin/yard
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'yard' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("yard", "yard")
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "aws-sdk-rds"
|
4
|
+
require "pg"
|
5
|
+
require "strscan"
|
6
|
+
require "uri"
|
7
|
+
|
8
|
+
require_relative "aws_rds_iam/auth_token_generator"
|
9
|
+
require_relative "aws_rds_iam/auth_token_generator_registry"
|
10
|
+
require_relative "aws_rds_iam/auth_token_injector"
|
11
|
+
require_relative "aws_rds_iam/connection"
|
12
|
+
require_relative "aws_rds_iam/connection_info"
|
13
|
+
require_relative "aws_rds_iam/version"
|
14
|
+
|
15
|
+
# The top-level [PG](https://deveiate.org/code/pg/PG.html) namespace.
|
16
|
+
module PG
|
17
|
+
# The top-level AWS RDS IAM plugin namespace.
|
18
|
+
module AWS_RDS_IAM
|
19
|
+
@auth_token_generators = AuthTokenGeneratorRegistry.new
|
20
|
+
|
21
|
+
# Registry of available {AuthTokenGenerator}s.
|
22
|
+
#
|
23
|
+
# @return [AuthTokenGeneratorRegistry]
|
24
|
+
def self.auth_token_generators
|
25
|
+
@auth_token_generators
|
26
|
+
end
|
27
|
+
|
28
|
+
PG::Connection.singleton_class.prepend Connection
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PG
|
4
|
+
module AWS_RDS_IAM
|
5
|
+
# Generates short-lived authentication tokens for connecting to Amazon RDS instances.
|
6
|
+
#
|
7
|
+
# @see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html
|
8
|
+
class AuthTokenGenerator
|
9
|
+
# Creates a new authentication token generator.
|
10
|
+
#
|
11
|
+
# @param credentials [Aws::CredentialProvider] the IAM credentials with which to sign the token
|
12
|
+
# @param region [String] the AWS region in which the RDS instances are running
|
13
|
+
def initialize(credentials:, region:)
|
14
|
+
@generator = Aws::RDS::AuthTokenGenerator.new(credentials: credentials)
|
15
|
+
@region = region
|
16
|
+
end
|
17
|
+
|
18
|
+
# Generates an authentication token for connecting to an Amazon RDS instance.
|
19
|
+
#
|
20
|
+
# @param host [String] the host name of the RDS instance that you want to access
|
21
|
+
# @param port [String] the port number used for connecting to your RDS instance
|
22
|
+
# @param user [String] the database account that you want to access
|
23
|
+
# @return [String] the generated authentication token
|
24
|
+
def call(host:, port:, user:)
|
25
|
+
@generator.auth_token(region: @region, endpoint: "#{host}:#{port}", user_name: user)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PG
|
4
|
+
module AWS_RDS_IAM
|
5
|
+
# Registers {AuthTokenGenerator}s to be used to generate authentication tokens for `PG::Connection`s that have the
|
6
|
+
# `aws_rds_iam_auth_token_generator` connection parameter set to the registered name.
|
7
|
+
class AuthTokenGeneratorRegistry
|
8
|
+
# Creates a new `AuthTokenRegistry`.
|
9
|
+
#
|
10
|
+
# @param default_auth_token_generator_class [Class] the class to register as the default {AuthTokenGenerator}
|
11
|
+
def initialize(default_auth_token_generator_class: AuthTokenGenerator)
|
12
|
+
@default_auth_token_generator_class = default_auth_token_generator_class
|
13
|
+
reset
|
14
|
+
end
|
15
|
+
|
16
|
+
# Registers an {AuthTokenGenerator}.
|
17
|
+
#
|
18
|
+
# @param name [String, Symbol]
|
19
|
+
# @return [void]
|
20
|
+
# @yieldreturn [AuthTokenGenerator]
|
21
|
+
def add(name, &block)
|
22
|
+
@registry[name.to_s] = Memoizer.new(&block)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Looks up an {AuthTokenGenerator} by name.
|
26
|
+
#
|
27
|
+
# @param name [String, Symbol]
|
28
|
+
# @return [AuthTokenGenerator]
|
29
|
+
def fetch(name)
|
30
|
+
@registry.fetch(name.to_s).call
|
31
|
+
end
|
32
|
+
|
33
|
+
# Unregisters an {AuthTokenGenerator}.
|
34
|
+
#
|
35
|
+
# @param name [String, Symbol]
|
36
|
+
# @return [void]
|
37
|
+
def remove(name)
|
38
|
+
@registry.delete name.to_s
|
39
|
+
end
|
40
|
+
|
41
|
+
# Unregisters all {AuthTokenGenerator}s and re-registers the default {AuthTokenGenerator}.
|
42
|
+
#
|
43
|
+
# @return [void]
|
44
|
+
def reset
|
45
|
+
@registry = {}
|
46
|
+
|
47
|
+
add :default do
|
48
|
+
config = Aws::RDS::Client.new.config
|
49
|
+
@default_auth_token_generator_class.new(credentials: config.credentials, region: config.region)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class Memoizer # rubocop:disable Style/Documentation
|
54
|
+
def initialize(&block)
|
55
|
+
@block = block
|
56
|
+
end
|
57
|
+
|
58
|
+
def call
|
59
|
+
return @auth_token_generator if defined?(@auth_token_generator)
|
60
|
+
|
61
|
+
@auth_token_generator = @block.call
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private_constant :Memoizer
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PG
|
4
|
+
module AWS_RDS_IAM
|
5
|
+
class AuthTokenInjector
|
6
|
+
def self.call(connection_string, auth_token_generators: AWS_RDS_IAM.auth_token_generators)
|
7
|
+
new(connection_string, auth_token_generators: auth_token_generators).call
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(connection_string, auth_token_generators:)
|
11
|
+
@connection_string = connection_string
|
12
|
+
@connection_info = ConnectionInfo.new(connection_string)
|
13
|
+
@connection_defaults = PG::Connection.conndefaults_hash
|
14
|
+
@auth_token_generators = auth_token_generators
|
15
|
+
end
|
16
|
+
|
17
|
+
def call
|
18
|
+
return @connection_string unless generate_auth_token?
|
19
|
+
|
20
|
+
@connection_info.password = generate_auth_token
|
21
|
+
|
22
|
+
@connection_info.to_s
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def generate_auth_token?
|
28
|
+
@connection_info.auth_token_generator_name
|
29
|
+
end
|
30
|
+
|
31
|
+
def generate_auth_token
|
32
|
+
@auth_token_generators
|
33
|
+
.fetch(@connection_info.auth_token_generator_name)
|
34
|
+
.call(
|
35
|
+
user: @connection_info.user || default(:user),
|
36
|
+
host: @connection_info.host || default(:host),
|
37
|
+
port: @connection_info.port || default(:port)
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
def default(key)
|
42
|
+
@connection_defaults.fetch(key)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private_constant :AuthTokenInjector
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PG
|
4
|
+
module AWS_RDS_IAM
|
5
|
+
module Connection
|
6
|
+
def conndefaults
|
7
|
+
super + [{
|
8
|
+
keyword: "aws_rds_iam_auth_token_generator",
|
9
|
+
envvar: nil,
|
10
|
+
compiled: nil,
|
11
|
+
val: nil,
|
12
|
+
label: "AWS-RDS-IAM-auth-token-generator",
|
13
|
+
dispchar: "",
|
14
|
+
dispsize: 64
|
15
|
+
}]
|
16
|
+
end
|
17
|
+
|
18
|
+
def parse_connect_args(*)
|
19
|
+
AuthTokenInjector.call(super)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private_constant :Connection
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "connection_info/keyword_value_string"
|
4
|
+
require_relative "connection_info/parse_error"
|
5
|
+
require_relative "connection_info/uri"
|
6
|
+
|
7
|
+
module PG
|
8
|
+
module AWS_RDS_IAM
|
9
|
+
module ConnectionInfo
|
10
|
+
def self.new(connection_string)
|
11
|
+
if URI.match?(connection_string)
|
12
|
+
URI.new(connection_string)
|
13
|
+
else
|
14
|
+
KeywordValueString.new(connection_string)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private_constant :ConnectionInfo
|
20
|
+
end
|
21
|
+
end
|