confidant 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.
@@ -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: []