confidant 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 20203303d07ffce5c33fb5fcdd92f479eef85e0a
4
+ data.tar.gz: 72ba74d4b41018265a3ad9d535ec3f479802d5ce
5
+ SHA512:
6
+ metadata.gz: ff62a6debd26773bdddce93ab885a4b4ad0cd2ed0c3ff5bbf4000e07eef0cbfc1291851fc24fde7e2968b1ce1148095cd6de7364d7b59fccf3fb698be4ab6f91
7
+ data.tar.gz: 5dd2f41cc9336023c6188dac971a3bf69578dc75cb7f77c277c8bbdbb78f8a55f901a2cbcb16d7fa9258dbbd287e751ed1746b8aa00ab075427789623c60c0bb
@@ -0,0 +1,14 @@
1
+ # Change Log
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](http://keepachangelog.com/)
5
+ and this project adheres to [Semantic Versioning](http://semver.org/).
6
+
7
+ ## [Unreleased]
8
+ ### Added
9
+
10
+ ### Changed
11
+
12
+ ## [0.1.0] - 2016-12-03
13
+ ### Added
14
+ - Initial public release of Confidant, with Confidant::Client library and CLI
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Matt Greensmith
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,211 @@
1
+ # Confidant
2
+
3
+ This is a client for [Confidant](https://lyft.github.io/confidant), an open source secret management service.
4
+
5
+ [![Gem](https://img.shields.io/gem/v/confidant.svg)](https://rubygems.org/gems/confidant)
6
+ [![Travis](https://img.shields.io/mgreensmith/confidant-client-ruby.svg)](https://travis-ci.org/mgreensmith/confidant-client-ruby)
7
+
8
+ ## Installation
9
+
10
+ $ gem install confidant
11
+
12
+ ## Configuration
13
+
14
+ This client is compatible with the config file format of the [official Python client](https://lyft.github.io/confidant/basics/client/); it should be a drop-in replacement.
15
+
16
+ The client will automatically look in `~/.confidant` and `/etc/confidant/config` for its configuration. Alternate config files can be specified via the `--config-files` (Ruby `:config_files`) option. Config files can be YAML or JSON format.
17
+
18
+ The configuration file supports profiles, which let you specify multiple environments in the same file. The default profile is `default`, an alternate profile can be specified with the `--profile` (Ruby: `:profile`) option.
19
+
20
+ The client does not merge config from multiple files; it expects to find a configuration block for the specified profile in the first file it finds.
21
+
22
+ The following configuration is supported, with the listed defaults. Some defaults differ from the official Python client, namely `user_type` and `region`. Additionally, this client supports per-command configuration: options provided within config keys named for a CLI command (or `Confidant::Client` method) will be used as to configure that command.
23
+
24
+ ```yaml
25
+ default:
26
+
27
+ # URL of the confidant server.
28
+ url: nil
29
+
30
+ # The KMS auth key to use. i.e. alias/authnz-production
31
+ auth_key: nil
32
+
33
+ # Note: unlike the official Python client, this client configures
34
+ # encryption-context-related options in the top-level config.
35
+
36
+ # The IAM role or user to authenticate with. i.e. myservice-production or myuser
37
+ from: nil
38
+
39
+ # The IAM role name of confidant. i.e. confidant-production
40
+ to: nil
41
+
42
+ # The confidant user-type to authenticate as. i.e. user or service
43
+ user_type: service
44
+
45
+ # Provided for compatibility with the official Python client.
46
+ # Any configured options in auth_context will be flattened into
47
+ # the top-level config, and will override provided top-level values.
48
+ auth_context:
49
+ from: nil
50
+ to: nil
51
+ user_type: service
52
+
53
+ # The token lifetime, in minutes.
54
+ token_lifetime: 10
55
+
56
+ # The version of the KMS auth token.
57
+ token_version: 2
58
+
59
+ # Use the specified AWS region for authentication.
60
+ region: us-east-1
61
+
62
+ # Example of per-command configuration for the get_service command
63
+ # get_service:
64
+ # service: my-service
65
+
66
+ # Not yet implemented in this client, will be ignored if provided:
67
+ # retries: 0
68
+ # backoff: 1
69
+ # token_cache_file: '/run/confidant/confidant_token'
70
+ # assume_role: nil
71
+ ```
72
+
73
+ When using the CLI, CLI-provided option flags are merged with config file options, with CLI options taking precedence.
74
+
75
+ When using the client as a Ruby library, options passed as parameters to `Confidant.configure` are merged with config file options, with parameter options taking precedence.
76
+
77
+ ## CLI Usage
78
+
79
+ ```
80
+ NAME
81
+ confidant - Client for Confidant, an open source secret management system
82
+
83
+ SYNOPSIS
84
+ confidant [global options] command [command options] [arguments...]
85
+
86
+ VERSION
87
+ 0.1.0
88
+
89
+ GLOBAL OPTIONS
90
+ --config-files=arg - Comma separated list of configuration files to use (default: ~/.confidant,/etc/confidant/config)
91
+ --from=arg - The IAM role or user to authenticate with. i.e. myservice-production or myuser (default: none)
92
+ --help - Show this message
93
+ -k, --auth-key=arg - The KMS auth key to use. i.e. alias/authnz-production (default: none)
94
+ -l, --token-lifetime=arg - The token lifetime, in minutes. (default: none)
95
+ --log-level=arg - Logging verbosity. (default: info)
96
+ --profile=arg - Configuration profile to use. (default: default)
97
+ --region=arg - Use the specified region for authentication. (default: none)
98
+ --to=arg - The IAM role name of confidant. i.e. confidant-production (default: none)
99
+ --token-version=arg - The version of the KMS auth token. (default: none)
100
+ -u, --url=arg - URL of the confidant server. (default: none)
101
+ --user-type=arg - The confidant user-type to authenticate as. i.e. user or service (default: none)
102
+ --version - Display the program version
103
+
104
+ COMMANDS
105
+ get_service - Get credentials for a service
106
+ help - Shows a list of commands or help for one command
107
+ show_config - Show the current config
108
+ ```
109
+
110
+ The CLI returns JSON to `STDOUT`, for drop-in compatibility with the official Python client.
111
+
112
+ Logs are written to `STDERR` by default. TO write logs to a file, redirect `STDERR` output:
113
+
114
+ ```
115
+ confidant 2>/some/log/file
116
+ ```
117
+
118
+ ## Library Usage
119
+
120
+ Require the client.
121
+
122
+ ```ruby
123
+ require 'confidant'
124
+ ```
125
+
126
+ ### Configuration
127
+
128
+ Configure the library via `Confidant.configure`.
129
+
130
+ An insufficiently-specified config, or any errors during configuration, will raise `Confidant::ConfigurationError`
131
+
132
+ ```ruby
133
+ # Configure Confidant using options from config file only:
134
+ Confidant.configure
135
+
136
+ # Or provide options as parameters, which will be merged onto
137
+ # the options from the config file, with parameter options
138
+ # taking precedence:
139
+ Confidant.configure(
140
+ auth_key: 'alias/authnz-production',
141
+ from: 'myservice-production',
142
+ to: 'confidant-production',
143
+ get_service: {
144
+ service: 'myservice-production'
145
+ }
146
+ )
147
+ ```
148
+
149
+ ### Get Service
150
+
151
+ Get credentials for a service from the Confidant server via `Confidant.get_service`
152
+
153
+ JSON responses from the server are returned as Ruby `Hash`es.
154
+
155
+ ```ruby
156
+ # If a service name was provided in config,
157
+ # i.e. `get_service: { service: 'my-service' }`,
158
+ # `get_service` will get that service's credentials:
159
+ Confidant.get_service
160
+ => {"account"=>nil,
161
+ "blind_credentials"=>[],
162
+ "credentials"=>
163
+ [{"credential_pairs"=>{"my_fancy_secret"=>"I love cats!", "something_is"=>"A super secret!"},
164
+ <SNIP>
165
+
166
+ # Or, provide a service name via parameter:
167
+ Confidant.get_service('my-service')
168
+ ```
169
+
170
+ ### Multiple Clients
171
+
172
+ Use multiple client instances with different configurations simultaneously by instantiating `Confidant::Configurator` and `Confidant::Client` directly:
173
+
174
+ ```ruby
175
+ default_config = Confidant::Configurator.new
176
+ default_client = Confidant::Client.new(default_config)
177
+ default_client.get_service('my-service')
178
+
179
+ production_config = Confidant::Configurator.new(profile: 'production')
180
+ production_client = Confidant::Client.new(production_config)
181
+ production_client.get_service('my-service-production')
182
+ ```
183
+
184
+ ## WARNING
185
+
186
+ This client is pre-alpha, and does not have feature parity with the official Python client!
187
+
188
+ #### Things that work
189
+
190
+ - The `get_service` CLI command (`Client.get_service`) can fetch service credentials from a Confidant server using a v2 KMS authentication token.
191
+ - All currently-available CLI options are correctly configurable.
192
+
193
+ #### Things that have not been implemented yet
194
+
195
+ - Any other CLI command, notably everything to do with server-blinded credentials
196
+ - API retries/backoff
197
+ - The `confidant-format` formatter
198
+ - KMS v1 auth tokens
199
+ - Token caching
200
+ - Assuming an IAM role
201
+ - MFA tokens
202
+
203
+ ## Contributing
204
+
205
+ Bug reports and pull requests are welcome on GitHub at https://github.com/mgreensmith/confidant-client-ruby.
206
+
207
+
208
+ ## License
209
+
210
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
211
+
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'confidant'
4
+ require 'confidant/cli'
5
+
6
+ exit Confidant::CLI.run(ARGV)
@@ -0,0 +1,40 @@
1
+ require 'logging'
2
+
3
+ include Logging.globally(:log)
4
+
5
+ require 'confidant/version'
6
+
7
+ # This is a set of client libs for Confidant
8
+ module Confidant
9
+ Logging.logger.root.appenders = Logging.appenders.stderr
10
+ Logging.logger.root.level = :info
11
+
12
+ # An invalid configuration was provided
13
+ class ConfigurationError < StandardError
14
+ end
15
+
16
+ require 'confidant/configurator'
17
+ require 'confidant/client'
18
+
19
+ module_function
20
+
21
+ ### Wrap common workflow into module methods for end-user simplicity.
22
+
23
+ def configure(config = {})
24
+ @configurator = Configurator.new(config)
25
+ end
26
+
27
+ def get_service(service = nil)
28
+ unless @configurator
29
+ raise ConfigurationError, 'Not configured, run Confidant.configure'
30
+ end
31
+ Client.new(@configurator).get_service(service)
32
+ end
33
+
34
+ def log_exception(klass, ex)
35
+ klass.log.error("#{ex.class} : #{ex.message}")
36
+ ex.backtrace.each do |frame|
37
+ klass.log.debug("\t#{frame}")
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,135 @@
1
+ require 'yaml'
2
+ require 'gli'
3
+
4
+ module Confidant
5
+ # Creates a CLI that fronts the Confidant client
6
+ class CLI
7
+ extend GLI::App
8
+
9
+ program_desc 'Client for Confidant, an open source secret management system'
10
+ version Confidant::VERSION
11
+
12
+ ### Global configuration options
13
+
14
+ # 'flag' options take params
15
+
16
+ desc 'Comma separated list of configuration files to use'
17
+ flag 'config-files', default_value:
18
+ Confidant::Configurator::DEFAULT_OPTS[:config_files].join(',')
19
+
20
+ desc 'Configuration profile to use.'
21
+ flag 'profile', default_value:
22
+ Confidant::Configurator::DEFAULT_OPTS[:profile]
23
+
24
+ desc 'Logging verbosity.'
25
+ flag 'log-level', default_value:
26
+ Confidant::Configurator::DEFAULT_OPTS[:log_level]
27
+
28
+ desc 'URL of the confidant server.'
29
+ flag %w(u url)
30
+
31
+ desc 'The KMS auth key to use. i.e. alias/authnz-production'
32
+ flag ['k', 'auth-key']
33
+
34
+ desc 'The token lifetime, in minutes.'
35
+ flag ['l', 'token-lifetime']
36
+
37
+ desc 'The version of the KMS auth token.'
38
+ flag 'token-version'
39
+
40
+ desc 'The IAM role or user to authenticate with. ' \
41
+ 'i.e. myservice-production or myuser'
42
+ flag 'from'
43
+
44
+ desc 'The IAM role name of confidant. i.e. confidant-production'
45
+ flag 'to'
46
+
47
+ desc 'The confidant user-type to authenticate as. i.e. user or service'
48
+ flag 'user-type'
49
+
50
+ desc 'Use the specified region for authentication.'
51
+ flag 'region'
52
+
53
+ # TODO: Implement support for these.
54
+
55
+ # desc 'Assume the specified IAM role.'
56
+ # flag 'assume-role'
57
+
58
+ # desc 'Number of retries that should be attempted on ' \
59
+ # 'confidant server errors.'
60
+ # flag 'retries'
61
+
62
+ # 'switch' options are booleans
63
+
64
+ # desc 'Prompt for an MFA token.'
65
+ # switch 'mfa'
66
+
67
+ ### Commands
68
+
69
+ desc 'Get credentials for a service'
70
+ command :get_service do |c|
71
+ c.desc 'The service to get.'
72
+ c.flag 'service'
73
+
74
+ c.action do |_global_options, _options, _|
75
+ log.debug 'Running get_service command'
76
+ client = Confidant::Client.new(@configurator)
77
+ client.suppress_errors
78
+ puts JSON.pretty_generate(client.get_service)
79
+ end
80
+ end
81
+
82
+ desc 'Show the current config'
83
+ command :show_config do |c|
84
+ c.action do |_global_options, _options, _|
85
+ puts @configurator.config.to_yaml
86
+ end
87
+ end
88
+
89
+ ### Hooks
90
+
91
+ pre do |global_options, command, options, _|
92
+ Logging.logger.root.level = global_options['log-level'].to_sym
93
+
94
+ opts = clean_opts(global_options)
95
+ opts[command.name] = clean_opts(options) if options
96
+
97
+ log.debug "Parsed CLI options: #{opts}"
98
+ @configurator = Confidant::Configurator.new(opts, command.name)
99
+ end
100
+
101
+ on_error do |ex|
102
+ Confidant.log_exception(self, ex)
103
+ false # return false to suppress standard message
104
+ end
105
+
106
+ ### Helper methods
107
+
108
+ # Try and clean up GLI's output into something useable.
109
+ def self::clean_opts(gli_opts)
110
+ # GLI provides String and Symbol keys for each flag/switch.
111
+ # We want the String keys (because some of our flags have dashes,
112
+ # and GLI makes :"dash-keys" symbols which need extra work to clean)
113
+ string_opts = gli_opts.select { |k, _| k.is_a?(String) }
114
+
115
+ # Convert the dashes in key names to underscores and symbolize the keys.
116
+ opts = {}
117
+ string_opts.each_pair { |k, v| opts[k.tr('-', '_').to_sym] = v }
118
+
119
+ # Convert :config_files into an array.
120
+ if opts[:config_files]
121
+ opts[:config_files] = opts[:config_files].split(',')
122
+ end
123
+
124
+ # Remove unneeded hash pairs:
125
+ # - nils: GLI returns 'nil' default for non-specified flag-type opts
126
+ # - false: GLI returns 'false' default for non-specified switch-type opts
127
+ # Removing false values also removes GLI's :help and :version keys
128
+ # - single-letter keys: these opts all have longer-form doppelgangers
129
+ opts.delete_if { |k, v| v.nil? || v == false || k.length == 1 }
130
+
131
+ # Now, only defaulted and explicitly-specified options remain.
132
+ opts
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,137 @@
1
+ require 'json'
2
+ require 'base64'
3
+ require 'aws-sdk-core'
4
+ require 'rest-client'
5
+
6
+ require 'confidant/configurator'
7
+
8
+ module Confidant
9
+ # The Confidant Client implementation
10
+ class Client
11
+ TOKEN_SKEW_SECONDS = 3 * 60
12
+ TIME_FORMAT = '%Y%m%dT%H%M%SZ'.freeze
13
+
14
+ attr_accessor :config
15
+
16
+ # Initialize with a +configurator+, which is
17
+ # an instance of Confidant::Configurator
18
+ def initialize(configurator)
19
+ @config = configurator.config
20
+ @kms = Aws::KMS::Client.new(region: config[:region])
21
+ @suppress_errors = false
22
+ end
23
+
24
+ # Return a Hash of credentials from the confidant API
25
+ # for +service+, either explicitly-provided, or from config.
26
+ def get_service(service = nil)
27
+ log.debug "Requesting #{api_service_url(service_name(service))} " \
28
+ "as user #{api_user}"
29
+ response = RestClient::Request.execute(
30
+ method: :get,
31
+ url: api_service_url(service_name(service)),
32
+ user: api_user,
33
+ password: generate_token,
34
+ headers: {
35
+ user_agent: RestClient::Platform.default_user_agent.prepend(
36
+ "confidant-client/#{Confidant::VERSION} "
37
+ )
38
+ }
39
+ )
40
+
41
+ JSON.parse(response.body)
42
+ rescue => ex
43
+ Confidant.log_exception(self, ex)
44
+ @suppress_errors ? api_error_response : raise
45
+ end
46
+
47
+ # The Python client suppresses API errors,
48
+ # returning { result: false } instead.
49
+ # Mimic this behavior based on the truthiness of +enable+.
50
+ # This is generally only called from Confidant::CLI
51
+ def suppress_errors(enable = true)
52
+ @suppress_errors = enable
53
+ true
54
+ end
55
+
56
+ private
57
+
58
+ # Return the name of the service for which we
59
+ # should fetch credentials from the confidant API.
60
+ # Returns +service+ if provided, or the config
61
+ # value at @config[:get_service][:service]
62
+ # Raises +ConfigurationError+ if no service was
63
+ # provided or configured.
64
+ def service_name(service = nil)
65
+ return service unless service.nil?
66
+ if @config[:get_service] && @config[:get_service][:service]
67
+ return @config[:get_service][:service]
68
+ end
69
+ raise ConfigurationError,
70
+ 'Service name must be specified, or provided in config as ' \
71
+ '{get_service: { service: \'my-service\' }'
72
+ end
73
+
74
+ # Return the name of the user that will connect to the confidant API
75
+ # TODO(v1-auth-support): Support v1-style user names.
76
+ def api_user
77
+ format(
78
+ '%s/%s/%s',
79
+ @config[:token_version],
80
+ @config[:user_type],
81
+ @config[:from]
82
+ )
83
+ end
84
+
85
+ # The URL to get credentials for +service+ from the Confidant server.
86
+ def api_service_url(service)
87
+ format('%s/v1/services/%s', @config[:url], service)
88
+ end
89
+
90
+ # The falsey response to return when
91
+ # @suppress_errors is true,
92
+ # rather than raising exceptions.
93
+ def api_error_response
94
+ { 'result' => 'false' }
95
+ end
96
+
97
+ # The content of a confidant auth token payload,
98
+ # to be encrypted by KMS.
99
+ def token_payload
100
+ now = Time.now.utc
101
+
102
+ start_time = (now - TOKEN_SKEW_SECONDS)
103
+
104
+ end_time = (
105
+ now - TOKEN_SKEW_SECONDS +
106
+ (@config[:token_lifetime].to_i * 60)
107
+ )
108
+
109
+ { not_before: start_time.strftime(TIME_FORMAT),
110
+ not_after: end_time.strftime(TIME_FORMAT) }.to_json
111
+ end
112
+
113
+ # Return an auth token for the confidant service,
114
+ # encrypted via KMS.
115
+ def generate_token
116
+ # TODO(v1-auth-support): Handle the different encryption_context
117
+ if @config[:token_version].to_i != 2
118
+ raise ConfigurationError,
119
+ 'This client only supports KMS v2 auth tokens.'
120
+ end
121
+
122
+ encrypt_params = {
123
+ key_id: @config[:auth_key],
124
+ plaintext: token_payload,
125
+ encryption_context: {
126
+ to: @config[:to],
127
+ from: @config[:from],
128
+ user_type: @config[:user_type]
129
+ }
130
+ }
131
+
132
+ log.debug "Asking KMS to encrypt: #{encrypt_params}"
133
+ resp = @kms.encrypt(encrypt_params)
134
+ Base64.strict_encode64(resp.ciphertext_blob)
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,148 @@
1
+ require 'yaml'
2
+ require 'active_support/hash_with_indifferent_access'
3
+
4
+ module Confidant
5
+ # Builds configuration for the Confidant client
6
+ class Configurator
7
+ # Default configration options for the Confidant module
8
+ # and this Configurator class, and not for the Client.
9
+ # Pass these through to the CLI for use in the `pre` hook,
10
+ # and strip them out of the final config hash used by the Client.
11
+ DEFAULT_OPTS = {
12
+ config_files: %w(~/.confidant /etc/confidant/config),
13
+ profile: 'default',
14
+ log_level: 'info'
15
+ }.freeze
16
+
17
+ # Default configuration options for the Client.
18
+ DEFAULTS = {
19
+ token_lifetime: 10,
20
+ token_version: 2,
21
+ user_type: 'service',
22
+ region: 'us-east-1'
23
+ }.freeze
24
+
25
+ # Keys that must exist in the final config in order for
26
+ # the Client to be able to function.
27
+ MANDATORY_CONFIG_KEYS = {
28
+ global: [:url, :auth_key, :from, :to],
29
+ get_service: [:service]
30
+ }.freeze
31
+
32
+ attr_reader :config
33
+
34
+ # Instantiate with a hash of configuration +opts+,and optionally
35
+ # the name of a +command+ that may have mandatory config options
36
+ # that should be validated along with the global config.
37
+ def initialize(opts = {}, command = nil)
38
+ configure(opts, command)
39
+ end
40
+
41
+ # Given a hash of configuration +opts+, and optionally the name
42
+ # of a +command+ that may have mandatory config options
43
+ # that should be validated along with the global config,
44
+ # load configuration from files, merge config keys together,
45
+ # and validate the presence of sufficient top-level config keys
46
+ # and command-specific config keys to be able to use the client.
47
+ #
48
+ # Saves and returns the final merged config.
49
+ def configure(opts, command = nil)
50
+ # Merge 'opts' onto DEFAULT_OPTS so that we can self-configure.
51
+ # This is a noop if we were called from CLI,
52
+ # as those keys are defaults in GLI and guaranteed to exist in 'opts',
53
+ # but this is necessary if we were invoked as a lib.
54
+ config = DEFAULT_OPTS.dup.merge(opts)
55
+ log.debug "Local config: #{config}"
56
+
57
+ # Merge local config over the profile config from file.
58
+ config = profile_config(
59
+ config[:config_files],
60
+ config[:profile]
61
+ ).dup.merge(config)
62
+
63
+ # We don't need any of the internal DEFAULT_OPTS any longer
64
+ DEFAULT_OPTS.keys.each { |k| config.delete(k) }
65
+
66
+ # Merge config onto local DEFAULTS
67
+ # to backfill any keys that are needed for KMS.
68
+ config = DEFAULTS.dup.merge(config)
69
+
70
+ @config = config
71
+ validate_config(command)
72
+ log.debug "Authoritative config: #{config}"
73
+ @config
74
+ end
75
+
76
+ # Validate the instance's @config for the presence of
77
+ # all global mandatory config keys. If +command+ is provided,
78
+ # validate the presence of all mandatory config keys specific
79
+ # to that command, otherwise validate that mandatory config keys
80
+ # exist for any command keys that exist in the top-level hash.
81
+ # Raises +ConfigurationError+ if mandatory config options are missing.
82
+ def validate_config(command = nil)
83
+ missing_keys = MANDATORY_CONFIG_KEYS[:global] - @config.keys
84
+
85
+ commands_to_verify = if command
86
+ [command.to_sym]
87
+ else
88
+ (MANDATORY_CONFIG_KEYS.keys & @config.keys)
89
+ end
90
+
91
+ commands_to_verify.each do |cmd|
92
+ missing = missing_keys_for_command(cmd)
93
+ next if missing.empty?
94
+ missing_keys << "#{cmd}[#{missing.join(',')}]"
95
+ end
96
+
97
+ return true if missing_keys.empty?
98
+ raise ConfigurationError,
99
+ "Missing required config keys: #{missing_keys.join(', ')}"
100
+ end
101
+
102
+ private
103
+
104
+ # Return a hash of the config for the provided +profile+ from
105
+ # the first-existing file in the provided array of +config_files+.
106
+ def profile_config(config_files, profile)
107
+ config = nil
108
+ config_files.each do |config_file|
109
+ next unless File.exist?(File.expand_path(config_file))
110
+ log.debug "found config file: #{config_file}"
111
+ config = profile_from_file(config_file, profile)
112
+ break
113
+ end
114
+ log.debug "Profile config: #{config}"
115
+ config || {}
116
+ end
117
+
118
+ # Given the pathname of a YAML or JSON +config_file+ and the name
119
+ # of a config +profile+ within that file, load the file and
120
+ # return a +Hash+ of the contents of that profile key.
121
+ def profile_from_file(config_file, profile)
122
+ content = YAML.load_file(File.expand_path(config_file))
123
+
124
+ # Fetch options from file for the specified profile
125
+ unless content.key?(profile)
126
+ raise ConfigurationError,
127
+ "Profile '#{profile}' not found in '#{config_file}"
128
+ end
129
+ profile_config = content[profile].symbolize_keys!
130
+
131
+ # Merge the :auth_context keys into the top-level hash.
132
+ profile_config.merge!(profile_config[:auth_context].symbolize_keys!)
133
+ profile_config.delete_if { |k, _| k == :auth_context }
134
+ profile_config
135
+ end
136
+
137
+ # Given a +command+, return an +array+ of
138
+ # the key names from MANDATORY_CONFIG_KEYS for that command,
139
+ # that are not present in the current @config
140
+ def missing_keys_for_command(command)
141
+ mandatory_keys = MANDATORY_CONFIG_KEYS[command] || []
142
+ return [] if mandatory_keys.empty?
143
+ return mandatory_keys unless @config[command] &&
144
+ @config[command].is_a?(Hash)
145
+ mandatory_keys - @config[command].keys
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,3 @@
1
+ module Confidant
2
+ VERSION = '0.1.0'.freeze
3
+ end
@@ -0,0 +1,92 @@
1
+ require 'spec_helper'
2
+
3
+ describe Confidant::Client do
4
+ before do
5
+ Logging.logger.root.appenders = nil
6
+
7
+ config = {
8
+ user_type: 'philosopher',
9
+ from: 'hypatia',
10
+ to: 'aedesia',
11
+ region: 'alexandria',
12
+ auth_key: 'astrolabe',
13
+ url: 'athens',
14
+ token_version: 2
15
+ }
16
+
17
+ @client = Confidant::Client.new(Confidant::Configurator.new(config))
18
+
19
+ allow(RestClient::Platform)
20
+ .to receive(:default_user_agent).and_return('sextant')
21
+
22
+ stub_const('Confidant::VERSION', '999')
23
+ end
24
+
25
+ context '#get_service' do
26
+ before do
27
+ allow_any_instance_of(Confidant::Client)
28
+ .to receive(:generate_token).and_return('tympan')
29
+ end
30
+
31
+ it 'can get credentials for a service from the API' do
32
+ allow_any_instance_of(Confidant::Client)
33
+ .to receive(:generate_token).and_return('tympan')
34
+
35
+ api_response = double
36
+ expect(api_response).to receive(:body).and_return('{ "hello": "world" }')
37
+
38
+ expect(RestClient::Request).to receive(:execute).with(
39
+ method: :get,
40
+ url: 'athens/v1/services/oracle',
41
+ user: '2/philosopher/hypatia',
42
+ password: 'tympan',
43
+ headers: { user_agent: 'confidant-client/999 sextant' }
44
+ ).and_return(api_response)
45
+
46
+ expect(@client.get_service('oracle')).to eq('hello' => 'world')
47
+ end
48
+
49
+ it 'does not suppress errors by default' do
50
+ expect(RestClient::Request).to receive(:execute).and_raise(StandardError)
51
+ expect { @client.get_service('oracle') }.to raise_error(StandardError)
52
+ end
53
+
54
+ it 'suppresses errors if @suppress_errors is true' do
55
+ expect(RestClient::Request).to receive(:execute).and_raise(StandardError)
56
+ @client.suppress_errors
57
+ expect(@client.get_service('oracle')).to eq('result' => 'false')
58
+ end
59
+ end
60
+
61
+ context '#generate_token' do
62
+ it 'can generate a v2 auth token' do
63
+ kms_response = double
64
+ expect(kms_response).to receive(:ciphertext_blob).and_return('12345')
65
+
66
+ allow_any_instance_of(Aws::KMS::Client)
67
+ .to receive(:encrypt).and_return(kms_response)
68
+
69
+ # Expect Bas64-encoded :ciphertext_blob
70
+ expect(@client.send(:generate_token)).to eq('MTIzNDU=')
71
+ end
72
+
73
+ it 'raises if a v1 token is requested' do
74
+ @client.config[:token_version] = 1
75
+ expect { @client.send(:generate_token) }
76
+ .to raise_error(Confidant::ConfigurationError)
77
+ end
78
+ end
79
+
80
+ context '#service_name' do
81
+ it 'returns a provided service name from parameter' do
82
+ @client.config[:get_service] = { service: 'aristotle' }
83
+ expect(@client.send(:service_name, 'plato')).to eq('plato')
84
+ expect(@client.send(:service_name)).to eq('aristotle')
85
+ end
86
+
87
+ it 'raises if a service name was not provided or configured' do
88
+ expect { @client.send(:service_name) }
89
+ .to raise_error(Confidant::ConfigurationError)
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ describe Confidant do
4
+ it 'has a version number' do
5
+ expect(Confidant::VERSION).not_to be nil
6
+ end
7
+
8
+ context 'module workflow' do
9
+ it 'exposes #configure' do
10
+ expect(Confidant).to respond_to(:configure).with(0..1).arguments
11
+ end
12
+
13
+ it 'exposes #get_service' do
14
+ expect(Confidant).to respond_to(:configure).with(0..1).arguments
15
+ end
16
+ end
17
+
18
+ it 'has a #log_exception helper' do
19
+ expect(Confidant).to respond_to(:log_exception).with(2).arguments
20
+ end
21
+ end
22
+
23
+ describe Confidant::ConfigurationError do
24
+ it 'subclasses StandardError' do
25
+ expect(Confidant::ConfigurationError).to be_kind_of(StandardError.class)
26
+ end
27
+ end
@@ -0,0 +1,9 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+
3
+ require 'simplecov'
4
+ SimpleCov.start do
5
+ add_filter '/spec/'
6
+ end
7
+
8
+ require 'confidant'
9
+ require 'confidant/cli'
metadata ADDED
@@ -0,0 +1,225 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: confidant
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matt Greensmith
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-12-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: gli
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.14'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.14'
27
+ - !ruby/object:Gem::Dependency
28
+ name: logging
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activesupport
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: aws-sdk-core
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.6'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.6'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rest-client
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: bundler
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '10.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '10.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '3.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '3.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: simplecov
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '0.12'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '0.12'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rubocop
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '0.46'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '0.46'
153
+ - !ruby/object:Gem::Dependency
154
+ name: pry
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '0.10'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '0.10'
167
+ - !ruby/object:Gem::Dependency
168
+ name: gem-release
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '0.7'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '0.7'
181
+ description:
182
+ email:
183
+ - matt@mattgreensmith.net
184
+ executables:
185
+ - confidant
186
+ extensions: []
187
+ extra_rdoc_files: []
188
+ files:
189
+ - CHANGELOG.md
190
+ - LICENSE.txt
191
+ - README.md
192
+ - bin/confidant
193
+ - lib/confidant.rb
194
+ - lib/confidant/cli.rb
195
+ - lib/confidant/client.rb
196
+ - lib/confidant/configurator.rb
197
+ - lib/confidant/version.rb
198
+ - spec/confidant/client_spec.rb
199
+ - spec/confidant_spec.rb
200
+ - spec/spec_helper.rb
201
+ homepage: https://github.com/mgreensmith/confidant-client-ruby
202
+ licenses:
203
+ - MIT
204
+ metadata: {}
205
+ post_install_message:
206
+ rdoc_options: []
207
+ require_paths:
208
+ - lib
209
+ required_ruby_version: !ruby/object:Gem::Requirement
210
+ requirements:
211
+ - - ">="
212
+ - !ruby/object:Gem::Version
213
+ version: '0'
214
+ required_rubygems_version: !ruby/object:Gem::Requirement
215
+ requirements:
216
+ - - ">="
217
+ - !ruby/object:Gem::Version
218
+ version: '0'
219
+ requirements: []
220
+ rubyforge_project:
221
+ rubygems_version: 2.4.5.1
222
+ signing_key:
223
+ specification_version: 4
224
+ summary: A CLI and client library for the Confidant secret management service.
225
+ test_files: []