From 184b0b443c157d578e36d2235230ff6f15d13e32 Mon Sep 17 00:00:00 2001 From: Norm MacLennan Date: Sun, 1 Feb 2015 00:34:35 -0500 Subject: [PATCH] making zanzibar bin a thor app with bundler/librarian type capability --- .gitignore | 3 + .rspec | 2 +- .rubocop.yml | 5 + Gemfile | 13 - README.md | 74 ++- Rakefile | 22 +- bin/zamioculcas | 4 +- bin/zanzibar | 70 +-- lib/zanzibar.rb | 380 +++++++------- lib/zanzibar/actions.rb | 3 + lib/zanzibar/actions/base.rb | 27 + lib/zanzibar/actions/bundle.rb | 119 +++++ lib/zanzibar/actions/get.rb | 61 +++ lib/zanzibar/actions/init.rb | 40 ++ lib/zanzibar/cli.rb | 120 +++++ lib/zanzibar/defaults.rb | 14 + lib/zanzibar/error.rb | 6 + lib/zanzibar/ui.rb | 42 ++ lib/zanzibar/version.rb | 7 +- spec/files/scrt.wsdl | 629 +++++++++++++++++++++++ spec/lib/zanzibar/actions/bundle_spec.rb | 94 ++++ spec/lib/zanzibar/actions/get_spec.rb | 64 +++ spec/lib/zanzibar/actions/init_spec.rb | 53 ++ spec/lib/zanzibar/version_spec.rb | 12 + spec/{ => lib}/zanzibar_spec.rb | 239 +++++---- spec/spec/spec_helper.rb | 95 ---- spec/spec_helper.rb | 37 ++ templates/Zanzifile.erb | 11 + zanzibar.gemspec | 16 +- 29 files changed, 1747 insertions(+), 515 deletions(-) create mode 100644 .rubocop.yml mode change 100644 => 100755 bin/zamioculcas create mode 100644 lib/zanzibar/actions.rb create mode 100644 lib/zanzibar/actions/base.rb create mode 100644 lib/zanzibar/actions/bundle.rb create mode 100644 lib/zanzibar/actions/get.rb create mode 100644 lib/zanzibar/actions/init.rb create mode 100644 lib/zanzibar/cli.rb create mode 100644 lib/zanzibar/defaults.rb create mode 100644 lib/zanzibar/error.rb create mode 100644 lib/zanzibar/ui.rb create mode 100644 spec/files/scrt.wsdl create mode 100644 spec/lib/zanzibar/actions/bundle_spec.rb create mode 100644 spec/lib/zanzibar/actions/get_spec.rb create mode 100644 spec/lib/zanzibar/actions/init_spec.rb create mode 100644 spec/lib/zanzibar/version_spec.rb rename spec/{ => lib}/zanzibar_spec.rb (62%) delete mode 100644 spec/spec/spec_helper.rb create mode 100644 spec/spec_helper.rb create mode 100644 templates/Zanzifile.erb diff --git a/.gitignore b/.gitignore index ae3fdc2..b920c61 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ *.o *.a mkmf.log +secrets/ +Zanzifile +Zanzifile.resolved diff --git a/.rspec b/.rspec index 65448f4..83e16f8 100644 --- a/.rspec +++ b/.rspec @@ -1,2 +1,2 @@ --color ---require spec/spec_helper +--require spec_helper diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..c6ea525 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,5 @@ +Metrics/ClassLength: + Max: 150 + +Metrics/LineLength: + Max: 175 diff --git a/Gemfile b/Gemfile index 2376a88..fa75df1 100644 --- a/Gemfile +++ b/Gemfile @@ -1,16 +1,3 @@ source 'https://rubygems.org' -gem 'savon' - -group :test do - gem 'rake' - gem 'savon_spec' - gem 'rspec' - gem 'webmock' - gem 'codeclimate-test-reporter' - gem 'zanzibar', path: '.' - gem 'rubocop' -end - -# Specify your gem's dependencies in zanzibar.gemspec gemspec diff --git a/README.md b/README.md index 3e427cd..fd3d573 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,11 @@ Or install it yourself as: ## Usage -In your ruby project, rakefile, etc., create a new Zanzibar object. The constructor takes a hash of optional parameters for the WSDL location, the domain of the Secret Server, a hash of global variables to pass to savon (necessary for windows environments with self-signed certs) and a password for the current user (intended to be passed in through some encryption method, unless you really want a plaintext password there.). All of these parameters are optional and the user will be prompted to enter them if they are missing. +In your ruby project, rakefile, etc., create a new Zanzibar object. + +The constructor takes a hash of optional parameters for the WSDL location, the domain of the Secret Server, a hash of global variables to pass to savon (necessary for windows environments with self-signed certs) and a password for the current user (intended to be passed in through some encryption method, unless you really want a plaintext password there). + +All of these parameters are optional and the user will be prompted to enter them if they are missing. ```ruby my_object = Zanzibar::Zanzibar.new(:domain => 'my.domain.net', :wsdl => 'my.scrt.srvr.com/webservices/sswebservice.asmx?wdsl', :pwd => get_encrypted_password_from_somewhere) @@ -51,20 +55,78 @@ secrets.download_secret_file(:scrt_id => 2345, :path => 'secrets/', :type => "At ``` +### Providing Credentials + +Zanzibar has several ways of finding Secret Server credentials. It will use credentials +discovered in this order: + +* Credentials passed to the initializer + * `Zanzibar::Zanzibar.new(:username=>'auser', :password=>'itsmyPassword')` +* Credentials discovered via the environment + * If `ZANZIBAR_USER` exists, it will use that. + * If not, it will try `USER` + * If `ZANZIBAR_PASSWORD` exists, it will use that. +* Credentials entered by the user + * Zanzibar will prompt the user to enter their password on STDIN + ### Command Line -Zanzibar comes bundled with the `zanzibar` command-line utility that can be used for fetching passwords and downloading keys from outside of Ruby. +Zanzibar comes bundled with the `zanzibar` command-line utility that can be used +for fetching passwords and downloading keys from outside of Ruby scripts. -`zanzibar` supports most actions provided by Zanzibar itself. Because it operates on the command-line, it can be used as part of a pipeline or within a bash script. +`zanzibar` supports most actions provided by Zanzibar itself. Because it operates +on the command-line, it can be used as part of a pipeline or within a bash script. ```bash -# if you don't pipe in a password, you will be prompted to enter one. +# if ZANZIBAR_PASSWORD is not set, you will be prompted to enter your password. # this will download the private key from secret 1984 to the current directory -cat ./local-password | zanzibar 1984 -s server.example.com -d example.com -t privatekey +$ ZANZIBAR_PASSWORD=`gpg -d secretpasswd.txt.gpg` zanzibar get 1984 -s server.example.com -d example.com -f "Private Key" -ssh user@someremote -i ./private_key +$ ssh user@someremote -i ./private_key ``` +#### Zanzifiles + +The `zanzibar` command can also perform [bundler](http://bundler.io)-like actions. +Running `zanzibar init` will generate a `Zanzifile` in the current directory. +Information about Secret Server and the necessary secret files to be downloaded +can be added here. + +Then `zanzibar bundle` will try to download the secrets named in the file. +When it downloads a file, it gets added to `Zanzifile.resolved`. And next time +`zanzibar bundle` is run, if the file exists and the hash matches the one in the +`resolved` file, it will not attempt to re-download. `zanzibar update` will attempt +to re-download all secrets. + +Note: `zanzibar get` can fetch passwords or files, but `zanzibar bundle` can +only operate on secret files. + +Sample `Zanzifile`: + +```yaml +--- +settings: + wsdl: my.scrt.srvr.com/webservices/sswebservice.asmx?wdsl + domain: my.domain.net + secret_dir: secrets/ + ignore_ssl: true +secrets: + ssh_key: + id: 249 + label: Private Key + encryption_key: + id: 483 + label: Attachment + cert_pem: + id: 123 + label: Certificate + cert_key: + id: 986 + label: Misc Attachment +``` + +Run `zanzibar help` or `zanzibar help [command]` for more information. + ## Contributing 1. Fork it ( https://github.com/Cimpress-MCP/zanzibar/fork ) diff --git a/Rakefile b/Rakefile index 8477839..e8da831 100644 --- a/Rakefile +++ b/Rakefile @@ -1,11 +1,11 @@ -require 'bundler/gem_tasks' -require 'bundler/setup' # load up our gem environment (incl. local zanzibar) -require 'rspec/core/rake_task' -require 'zanzibar/version' -require 'rubocop/rake_task' - -task default: [:test] - -RSpec::Core::RakeTask.new(:test) - -RuboCop::RakeTask.new +require 'bundler/gem_tasks' +require 'bundler/setup' # load up our gem environment (incl. local zanzibar) +require 'rspec/core/rake_task' +require 'zanzibar/version' +require 'rubocop/rake_task' + +task default: [:test] + +RSpec::Core::RakeTask.new(:test) + +RuboCop::RakeTask.new diff --git a/bin/zamioculcas b/bin/zamioculcas old mode 100644 new mode 100755 index fc92f00..ffe437d --- a/bin/zamioculcas +++ b/bin/zamioculcas @@ -1,2 +1,4 @@ #! ruby -system("zanzibar #{ARGV.join(" ")}") +require 'zanzibar/cli' + +Zanzibar::Cli.start(ARGV) diff --git a/bin/zanzibar b/bin/zanzibar index c2231ec..ffe437d 100755 --- a/bin/zanzibar +++ b/bin/zanzibar @@ -1,70 +1,4 @@ #! ruby +require 'zanzibar/cli' -require 'zanzibar' -require 'optparse' - -options = { - :domain => 'local' -} - -OptionParser.new do |opts| - opts.banner = "Usage: zamioculcas -d domain [-w wsdl] [-k] [-p] [secret_id]" - - opts.on("-d", "--domain DOMAIN", "Specify domain") do |v| - options[:domain] = v - end - - opts.on("-w", "--wsdl WSDL", "Specify WSDL location") do |v| - options[:wsdl] = v - end - - opts.on("-s", "--server SERVER", "Secret server hostname or IP") do |v| - options[:server] = v - end - - opts.on("-k", "--no-check-certificate", "Don't run SSL certificate checks") do |v| - options[:globals] = {:ssl_verify_mode => :none} - end - - opts.on("-p", "--password PASSWORD", "Specify password") do |v| - options[:pwd] = v - end - - opts.on("-t", "--type TYPE", "Specify the type of secret") do |v| - options[:type] = v - end - - opts.on("-u", "--user USER", "Specify the username") do |v| - options[:username] = v - end - -end.parse! - -raise OptionParser::MissingArgument if options[:server].nil? -options[:type] = "password" if options[:type].nil? - -unless STDIN.tty? || options[:pwd] - options[:pwd] = $stdin.read.strip -end - -secret_id = Integer(ARGV.pop) -if(!secret_id) - fail "no secret!" -end - -unless options[:wsdl] || options[:server].nil? - options[:wsdl] = "https://#{options[:server]}/webservices/sswebservice.asmx?wsdl" -end - -scrt = Zanzibar::Zanzibar.new(options) - -case options[:type] -when "password" - $stdout.write "#{scrt.get_password(secret_id)}\n" -when "privatekey" - scrt.download_private_key(:scrt_id=>secret_id) -when "publickey" - scrt.download_public_key(:scrt_id=>secret_id) -else - $stderr.write "#{options[:type]} is not a known type." -end +Zanzibar::Cli.start(ARGV) diff --git a/lib/zanzibar.rb b/lib/zanzibar.rb index 490f813..8a2f2ae 100644 --- a/lib/zanzibar.rb +++ b/lib/zanzibar.rb @@ -1,190 +1,190 @@ -require 'zanzibar/version' -require 'savon' -require 'io/console' -require 'fileutils' - -module Zanzibar - ## - # Class for interacting with Secret Server - class Zanzibar - ## - # @param args{:domain, :wsdl, :pwd, :username, :globals{}} - - def initialize(args = {}) - if args[:username] - @@username = args[:username] - elsif ENV['ZANZIBAR_USER'] - @@username = ENV['ZANZIBAR_USER'] - else - @@username = ENV['USER'] - end - - if args[:wsdl] - @@wsdl = args[:wsdl] - else - @@wsdl = get_wsdl_location - end - - if args[:pwd] - @@password = args[:pwd] - elsif ENV['ZANZIBAR_PASSWORD'] - @@password = ENV['ZANZIBAR_PASSWORD'] - else - @@password = prompt_for_password - end - - if args[:domain] - @@domain = args[:domain] - else - @@domain = prompt_for_domain - end - args[:globals] = {} unless args[:globals] - init_client(args[:globals]) - end - - def get_client_username - @@username - end - - def get_client_password - @@password - end - - ## Initializes the Savon client class variable with the wdsl document location and optional global variables - # @param globals{}, optional - - def init_client(globals = {}) - globals = {} if globals.nil? - @@client = Savon.client(globals) do - wsdl @@wsdl - end - end - - ## Gets the user's password if none is provided in the constructor. - # @return [String] the password for the current user - - def prompt_for_password - puts "Please enter password for #{@@username}:" - STDIN.noecho(&:gets).chomp - end - - ## Gets the wsdl document location if none is provided in the constructor - # @return [String] the location of the WDSL document - - def prompt_for_wsdl_location - puts 'Enter the URL of the Secret Server WSDL:' - STDIN.gets.chomp - end - - ## Gets the domain of the Secret Server installation if none is provided in the constructor - # @return [String] the domain of the secret server installation - - def prompt_for_domain - puts 'Enter the domain of your Secret Server:' - STDIN.gets.chomp - end - - ## Get an authentication token for interacting with Secret Server. These are only good for about 10 minutes so just get a new one each time. - # Will raise an error if there is an issue with the authentication. - # @return the authentication token for the current user. - - def get_token - response = @@client.call(:authenticate, message: { username: @@username, password: @@password, organization: '', domain: @@domain }) - .hash[:envelope][:body][:authenticate_response][:authenticate_result] - fail "Error generating the authentication token for user #{@@username}: #{response[:errors][:string]}" if response[:errors] - response[:token] - rescue Savon::Error => err - raise "There was an error generating the authentiaton token for user #{@@username}: #{err}" - end - - ## Get a secret returned as a hash - # Will raise an error if there was an issue getting the secret - # @param [Integer] the secret id - # @return [Hash] the secret hash retrieved from the wsdl - - def get_secret(scrt_id, token = nil) - secret = @@client.call(:get_secret, message: { token: token || get_token, secretId: scrt_id }).hash[:envelope][:body][:get_secret_response][:get_secret_result] - fail "There was an error getting secret #{scrt_id}: #{secret[:errors][:string]}" if secret[:errors] - return secret - rescue Savon::Error => err - raise "There was an error getting the secret with id #{scrt_id}: #{err}" - end - - ## Retrieve a simple password from a secret - # Will raise an error if there are any issues - # @param [Integer] the secret id - # @return [String] the password for the given secret - - def get_password(scrt_id) - secret = get_secret(scrt_id) - secret_items = secret[:secret][:items][:secret_item] - return get_secret_item_by_field_name(secret_items, 'Password')[:value] - rescue Savon::Error => err - raise "There was an error getting the password for secret #{scrt_id}: #{err}" - end - - def write_secret_to_file(path, secret_response) - File.open(File.join(path, secret_response[:file_name]), 'wb') do |file| - file.puts Base64.decode64(secret_response[:file_attachment]) - end - end - - def get_secret_item_by_field_name(secret_items, field_name) - secret_items.each do |item| - return item if item[:field_name] == field_name - end - end - - ## Get the secret item id that relates to a key file or attachment. - # Will raise on error - # @param [Integer] the secret id - # @param [String] the type of secret item to get, one of privatekey, publickey, attachment - # @return [Integer] the secret item id - - def get_scrt_item_id(scrt_id, type, token) - secret = get_secret(scrt_id, token) - secret_items = secret[:secret][:items][:secret_item] - begin - return get_secret_item_by_field_name(secret_items, type)[:id] - rescue - raise "Unknown type, #{type}." - end - end - - ## Downloads a file for a secret and places it where Zanzibar is running, or :path if specified - # Raise on error - # @param [Hash] args, :scrt_id, :type (one of "Private Key", "Public Key", "Attachment"), :scrt_item_id - optional, :path - optional - - def download_secret_file(args = {}) - token = get_token - FileUtils.mkdir_p(args[:path]) if args[:path] - path = args[:path] ? args[:path] : '.' ## The File.join below doesn't handle nils well, so let's take that possibility away. - begin - response = @@client.call(:download_file_attachment_by_item_id, message: - { token: token, secretId: args[:scrt_id], secretItemId: args[:scrt_item_id] || get_scrt_item_id(args[:scrt_id], args[:type], token) }) - .hash[:envelope][:body][:download_file_attachment_by_item_id_response][:download_file_attachment_by_item_id_result] - fail "There was an error getting the #{args[:type]} for secret #{args[:scrt_id]}: #{response[:errors][:string]}" if response[:errors] - write_secret_to_file(path, response) - return File.join(path, response[:file_name]) - rescue Savon::Error => err - raise "There was an error getting the #{args[:type]} for secret #{args[:scrt_id]}: #{err}" - end - end - - ## Methods to maintain backwards compatibility - def download_private_key(args = {}) - args[:type] = 'Private Key' - download_secret_file(args) - end - - def download_public_key(args = {}) - args[:type] = 'Public Key' - download_secret_file(args) - end - - def download_attachment(args = {}) - args[:type] = 'Attachment' - download_secret_file(args) - end - end -end +require 'zanzibar/version' +require 'savon' +require 'io/console' +require 'fileutils' + +module Zanzibar + ## + # Class for interacting with Secret Server + class Zanzibar + ## + # @param args{:domain, :wsdl, :pwd, :username, :globals{}} + + def initialize(args = {}) + if args[:username] + @@username = args[:username] + elsif ENV['ZANZIBAR_USER'] + @@username = ENV['ZANZIBAR_USER'] + else + @@username = ENV['USER'] + end + + if args[:wsdl] + @@wsdl = args[:wsdl] + else + @@wsdl = get_wsdl_location + end + + if args[:pwd] + @@password = args[:pwd] + elsif ENV['ZANZIBAR_PASSWORD'] + @@password = ENV['ZANZIBAR_PASSWORD'] + else + @@password = prompt_for_password + end + + if args[:domain] + @@domain = args[:domain] + else + @@domain = prompt_for_domain + end + args[:globals] = {} unless args[:globals] + init_client(args[:globals]) + end + + def get_client_username + @@username + end + + def get_client_password + @@password + end + + ## Initializes the Savon client class variable with the wdsl document location and optional global variables + # @param globals{}, optional + + def init_client(globals = {}) + globals = {} if globals.nil? + @@client = Savon.client(globals) do + wsdl @@wsdl + end + end + + ## Gets the user's password if none is provided in the constructor. + # @return [String] the password for the current user + + def prompt_for_password + puts "Please enter password for #{@@username}:" + STDIN.noecho(&:gets).chomp + end + + ## Gets the wsdl document location if none is provided in the constructor + # @return [String] the location of the WDSL document + + def prompt_for_wsdl_location + puts 'Enter the URL of the Secret Server WSDL:' + STDIN.gets.chomp + end + + ## Gets the domain of the Secret Server installation if none is provided in the constructor + # @return [String] the domain of the secret server installation + + def prompt_for_domain + puts 'Enter the domain of your Secret Server:' + STDIN.gets.chomp + end + + ## Get an authentication token for interacting with Secret Server. These are only good for about 10 minutes so just get a new one each time. + # Will raise an error if there is an issue with the authentication. + # @return the authentication token for the current user. + + def get_token + response = @@client.call(:authenticate, message: { username: @@username, password: @@password, organization: '', domain: @@domain }) + .hash[:envelope][:body][:authenticate_response][:authenticate_result] + fail "Error generating the authentication token for user #{@@username}: #{response[:errors][:string]}" if response[:errors] + response[:token] + rescue Savon::Error => err + raise "There was an error generating the authentiaton token for user #{@@username}: #{err}" + end + + ## Get a secret returned as a hash + # Will raise an error if there was an issue getting the secret + # @param [Integer] the secret id + # @return [Hash] the secret hash retrieved from the wsdl + + def get_secret(scrt_id, token = nil) + secret = @@client.call(:get_secret, message: { token: token || get_token, secretId: scrt_id }).hash[:envelope][:body][:get_secret_response][:get_secret_result] + fail "There was an error getting secret #{scrt_id}: #{secret[:errors][:string]}" if secret[:errors] + return secret + rescue Savon::Error => err + raise "There was an error getting the secret with id #{scrt_id}: #{err}" + end + + ## Retrieve a simple password from a secret + # Will raise an error if there are any issues + # @param [Integer] the secret id + # @return [String] the password for the given secret + + def get_password(scrt_id) + secret = get_secret(scrt_id) + secret_items = secret[:secret][:items][:secret_item] + return get_secret_item_by_field_name(secret_items, 'Password')[:value] + rescue Savon::Error => err + raise "There was an error getting the password for secret #{scrt_id}: #{err}" + end + + def write_secret_to_file(path, secret_response) + File.open(File.join(path, secret_response[:file_name]), 'wb') do |file| + file.puts Base64.decode64(secret_response[:file_attachment]) + end + end + + def get_secret_item_by_field_name(secret_items, field_name) + secret_items.each do |item| + return item if item[:field_name] == field_name + end + end + + ## Get the secret item id that relates to a key file or attachment. + # Will raise on error + # @param [Integer] the secret id + # @param [String] the type of secret item to get, one of privatekey, publickey, attachment + # @return [Integer] the secret item id + + def get_scrt_item_id(scrt_id, type, token) + secret = get_secret(scrt_id, token) + secret_items = secret[:secret][:items][:secret_item] + begin + return get_secret_item_by_field_name(secret_items, type)[:id] + rescue + raise "Unknown type, #{type}." + end + end + + ## Downloads a file for a secret and places it where Zanzibar is running, or :path if specified + # Raise on error + # @param [Hash] args, :scrt_id, :type (one of "Private Key", "Public Key", "Attachment"), :scrt_item_id - optional, :path - optional + + def download_secret_file(args = {}) + token = get_token + FileUtils.mkdir_p(args[:path]) if args[:path] + path = args[:path] ? args[:path] : '.' ## The File.join below doesn't handle nils well, so let's take that possibility away. + begin + response = @@client.call(:download_file_attachment_by_item_id, message: + { token: token, secretId: args[:scrt_id], secretItemId: args[:scrt_item_id] || get_scrt_item_id(args[:scrt_id], args[:type], token) }) + .hash[:envelope][:body][:download_file_attachment_by_item_id_response][:download_file_attachment_by_item_id_result] + fail "There was an error getting the #{args[:type]} for secret #{args[:scrt_id]}: #{response[:errors][:string]}" if response[:errors] + write_secret_to_file(path, response) + return File.join(path, response[:file_name]) + rescue Savon::Error => err + raise "There was an error getting the #{args[:type]} for secret #{args[:scrt_id]}: #{err}" + end + end + + ## Methods to maintain backwards compatibility + def download_private_key(args = {}) + args[:type] = 'Private Key' + download_secret_file(args) + end + + def download_public_key(args = {}) + args[:type] = 'Public Key' + download_secret_file(args) + end + + def download_attachment(args = {}) + args[:type] = 'Attachment' + download_secret_file(args) + end + end +end diff --git a/lib/zanzibar/actions.rb b/lib/zanzibar/actions.rb new file mode 100644 index 0000000..04f2333 --- /dev/null +++ b/lib/zanzibar/actions.rb @@ -0,0 +1,3 @@ +require 'zanzibar/actions/init' +require 'zanzibar/actions/bundle' +require 'zanzibar/actions/get' diff --git a/lib/zanzibar/actions/base.rb b/lib/zanzibar/actions/base.rb new file mode 100644 index 0000000..e39fef2 --- /dev/null +++ b/lib/zanzibar/actions/base.rb @@ -0,0 +1,27 @@ +module Zanzibar + module Actions + # Basic plumbing for all actions + class Base + attr_accessor :options + private :options= + + attr_accessor :logger + private :logger= + + def initialize(logger, options = {}) + self.logger = logger + self.options = options + end + + private + + def debug(*args, &block) + logger.debug(*args, &block) + end + + def source_root + @source_root ||= Pathname.new(File.expand_path('../../../../', __FILE__)) + end + end + end +end diff --git a/lib/zanzibar/actions/bundle.rb b/lib/zanzibar/actions/bundle.rb new file mode 100644 index 0000000..082d897 --- /dev/null +++ b/lib/zanzibar/actions/bundle.rb @@ -0,0 +1,119 @@ +require 'zanzibar/actions/base' +require 'zanzibar/error' +require 'zanzibar' + +module Zanzibar + module Actions + # Download or verify the secrets in a Zanzifile + class Bundle < Base + attr_accessor :settings + attr_accessor :remote_secrets + attr_accessor :local_secrets + attr_accessor :update + attr_accessor :zanzibar + + def initialize(ui, options, args = {}) + super(ui, options) + @update = args[:update] + end + + def run + ensure_zanzifile + load_required_secrets + validate_environment + load_resolved_secrets if resolved_file? + validate_local_secrets unless @update + run! + end + + private + + def run! + if need_secrets? + new_secrets = download_remote_secrets + update_resolved_file new_secrets + else + debug { 'No secrets to download...' } + end + end + + def ensure_zanzifile + fail Error, NO_ZANZIFILE_ERROR unless File.exist? ZANZIFILE_NAME + debug { "#{ZANZIFILE_NAME} located..." } + end + + def resolved_file? + File.exist? RESOLVED_NAME + end + + def load_required_secrets + zanzifile = YAML.load_file(ZANZIFILE_NAME) + @settings = zanzifile['settings'] || {} + @remote_secrets = zanzifile['secrets'] || {} + @local_secrets = {} + end + + def validate_environment + return unless @settings.empty? || @remote_secrets.empty? + fail Error, INVALID_ZANZIFILE_ERROR + end + + def load_resolved_secrets + @local_secrets = YAML.load_file RESOLVED_NAME + end + + def need_secrets? + !@remote_secrets.empty? + end + + def validate_local_secrets + @local_secrets.each do |key, secret| + if File.exist?(secret[:path]) && secret[:hash] == Digest::MD5.file(secret[:path]).hexdigest + debug { "#{key} found locally, skipping download..." } + @remote_secrets.delete key + end + end + end + + def download_remote_secrets + args = @settings['ignore_ssl'] ? { ssl_verify_mode: :none } : {} + + downloaded_secrets = {} + remote_secrets.each do |key, secret| + downloaded_secrets[key] = download_one_secret(secret['id'], + secret['label'], + @settings['secret_dir'], + args) + + debug { "Downloaded secret: #{key} to #{path}..." } + end + + downloaded_secrets + end + + def download_one_secret(scrt_id, label, path, args) + path = zanzibar(args).download_secret_file(scrt_id: scrt_id, + type: label, + path: path) + + { path: path, hash: Digest::MD5.file(path).hexdigest } + end + + def update_resolved_file(new_secrets) + @local_secrets.merge! new_secrets + + File.open(RESOLVED_NAME, 'w') do |out| + YAML.dump(@local_secrets, out) + end + + debug { 'Updated resolved file...' } + end + + def zanzibar(args) + @zanzibar ||= ::Zanzibar::Zanzibar.new(wsdl: @settings['wsdl'], + domain: @settings['domain'], + globals: args) + end + end + end +end diff --git a/lib/zanzibar/actions/get.rb b/lib/zanzibar/actions/get.rb new file mode 100644 index 0000000..761bca8 --- /dev/null +++ b/lib/zanzibar/actions/get.rb @@ -0,0 +1,61 @@ +require 'zanzibar/actions/base' +require 'zanzibar/error' +require 'zanzibar' +require 'zanzibar/defaults' + +module Zanzibar + module Actions + # Fetch a single secret + class Get < Base + attr_accessor :zanibar_options + attr_accessor :scrt_id + + def initialize(ui, options, scrt_id) + super(ui, options) + @scrt_id = scrt_id + @zanzibar_options = {} + end + + def run + construct_options + ensure_options + + fetch_secret(@scrt_id, options['filelabel']) + end + + def fetch_secret(scrt_id, label = nil) + scrt = ::Zanzibar::Zanzibar.new(@zanzibar_options) + + puts @zanzibar_options + + if label + scrt.download_secret_file(scrt_id: scrt_id, + type: label) + else + scrt.get_password(scrt_id) + end + end + + def construct_options + @zanzibar_options[:wsdl] = construct_wsdl + @zanzibar_options[:globals] = { ssl_verify_mode: :none } if options['ignoressl'] + @zanzibar_options[:domain] = options['domain'] + @zanzibar_options[:username] = options['username'] unless options['username'].nil? + @zanzibar_options[:domain] = options['domain'] ? options['domain'] : 'local' + end + + def construct_wsdl + if options['wsdl'].nil? && options['server'] + DEFAULT_WSDL % options['server'] + else + options['wsdl'] + end + end + + def ensure_options + return if @zanzibar_options[:wsdl] + fail Error, NO_WSDL_ERROR + end + end + end +end diff --git a/lib/zanzibar/actions/init.rb b/lib/zanzibar/actions/init.rb new file mode 100644 index 0000000..efa6c59 --- /dev/null +++ b/lib/zanzibar/actions/init.rb @@ -0,0 +1,40 @@ +require 'zanzibar/actions/base' +require 'zanzibar/error' +require 'ostruct' +require 'erb' +require 'zanzibar/defaults' + +module Zanzibar + module Actions + # Create a new Zanzifile + class Init < Base + def run + check_for_zanzifile + write_template + end + + private + + def check_for_zanzifile + return unless File.exist?(ZANZIFILE_NAME) && !options['force'] + fail Error, ALREADY_EXISTS_ERROR + end + + def write_template + template = TemplateRenderer.new(options) + + File.open(ZANZIFILE_NAME, 'w') do |f| + f.write template.render(File.read(source_root.join(TEMPLATE_NAME))) + end + end + + # Allows us to easily feed our options hash + # to an ERB + class TemplateRenderer < OpenStruct + def render(template) + ERB.new(template).result(binding) + end + end + end + end +end diff --git a/lib/zanzibar/cli.rb b/lib/zanzibar/cli.rb new file mode 100644 index 0000000..0033778 --- /dev/null +++ b/lib/zanzibar/cli.rb @@ -0,0 +1,120 @@ +require 'thor' +require 'thor/actions' +require 'zanzibar/version' +require 'zanzibar/cli' +require 'zanzibar/ui' +require 'zanzibar/actions' +require 'zanzibar/error' +require 'zanzibar/defaults' + +module Zanzibar + # The `zanzibar` binay/thor application main class + class Cli < Thor + include Thor::Actions + + attr_accessor :ui + + def initialize(*) + super + the_shell = (options['no-color'] ? Thor::Shell::Basic.new : shell) + @ui = Shell.new(the_shell) + @ui.be_quiet! if options['quiet'] + @ui.debug! if options['verbose'] + + debug_header + end + + desc 'version', 'Display your Zanzibar verion' + def version + say "#{APPLICATION_NAME} Version: #{VERSION}" + end + + desc 'init', "Create an empty #{ZANZIFILE_NAME} in the current directory." + option 'verbose', type: :boolean, default: false, aliases: :v + option 'wsdl', type: :string, aliases: :w, + default: DEFAULT_WSDL % DEFAULT_SERVER, + desc: 'The URI of the WSDL file for your Secret Server instance' + option 'domain', type: :string, default: 'local', aliases: :d, + desc: 'The logon domain for your Secret Server account' + option 'force', type: :boolean, default: false, aliases: :f, + desc: 'Recreate the Zanzifile if one already exists.' + option 'secretdir', type: :string, default: 'secrets/', aliases: :s, + desc: 'The directory to which secrets should be downloaded.' + option 'ignoressl', type: :boolean, default: 'false', aliases: :k, + desc: 'Don\'t check the SSL certificate of Secret Server' + def init + run_action { init! } + end + + desc 'bundle', "Fetch secrets declared in your #{ZANZIFILE_NAME}" + option 'verbose', type: :boolean, default: false, aliases: :v + def bundle + run_action { bundle! } + end + + desc 'update', "Redownload all secrets in your #{ZANZIFILE_NAME}" + option 'verbose', type: :boolean, default: false, aliases: :v + def update + run_action { update! } + end + + desc 'get SECRETID', 'Fetch a single SECRETID from Secret Server' + option 'domain', type: :string, aliases: :d, + desc: 'The logon domain to use when logging in.' + option 'server', type: :string, aliases: :s, + desc: 'The Secret Server hostname or IP' + option 'wsdl', type: :string, aliases: :w, + desc: 'Full path to the Secret Server WSDL' + option 'ignoressl', type: :boolean, aliases: :k, + desc: 'Don\'t verify Secret Server\'s SSL certificate' + option 'filelabel', type: :string, aliases: :f, + desc: 'Specify a file (by label) to download' + option 'username', type: :string, aliases: :u + option 'password', type: :string, aliases: :p + def get(scrt_id) + run_action { get! scrt_id } + end + + private + + def debug_header + @ui.debug { "Running #{APPLICATION_NAME} in debug mode..." } + @ui.debug { "Ruby Version: #{RUBY_VERSION}" } + @ui.debug { "Ruby Platform: #{RUBY_PLATFORM}" } + @ui.debug { "#{APPLICATION_NAME} Version: #{VERSION}" } + end + + # Run the specified action and rescue errors we + # explicitly send back to format them + def run_action(&_block) + yield + rescue ::Zanzibar::Error => e + @ui.error e + abort "Fatal error: #{e.message}" + end + + def init! + say "Initializing a new #{ZANZIFILE_NAME} in the current directory..." + Actions::Init.new(@ui, options).run + say "Your #{ZANZIFILE_NAME} has been created!" + say 'You should check the settings and add your secrets.' + say 'Then run `zanzibar bundle` to fetch them.' + end + + def bundle! + say "Checking for secrets declared in your #{ZANZIFILE_NAME}..." + Actions::Bundle.new(@ui, options).run + say 'Finished downloading secrets!' + end + + def update! + say "Redownloading all secrets declared in your #{ZANZIFILE_NAME}..." + Actions::Bundle.new(@ui, options, update: true).run + say 'Finished downloading secrets!' + end + + def get!(scrt_id) + say Actions::Get.new(@ui, options, scrt_id).run + end + end +end diff --git a/lib/zanzibar/defaults.rb b/lib/zanzibar/defaults.rb new file mode 100644 index 0000000..78068f0 --- /dev/null +++ b/lib/zanzibar/defaults.rb @@ -0,0 +1,14 @@ +# Definitions for various strings used throughout the gem +module Zanzibar + APPLICATION_NAME = Pathname.new($PROGRAM_NAME).basename + ZANZIFILE_NAME = 'Zanzifile' + RESOLVED_NAME = 'Zanzifile.resolved' + TEMPLATE_NAME = 'templates/Zanzifile.erb' + DEFAULT_SERVER = 'secret.example.com' + DEFAULT_WSDL = 'https://%s/webservices/sswebservice.asmx?wsdl' + + ALREADY_EXISTS_ERROR = "#{ZANZIFILE_NAME} already exists! Aborting..." + NO_WSDL_ERROR = 'Could not construct WSDL URL. Please provide either --server or --wsdl' + NO_ZANZIFILE_ERROR = "You don't have a #{ZANZIFILE_NAME}! Run `#{APPLICATION_NAME} init` first!" + INVALID_ZANZIFILE_ERROR = "Unable to load your #{ZANZIFILE_NAME}. Please ensure it is valid YAML." +end diff --git a/lib/zanzibar/error.rb b/lib/zanzibar/error.rb new file mode 100644 index 0000000..db1b029 --- /dev/null +++ b/lib/zanzibar/error.rb @@ -0,0 +1,6 @@ +module Zanzibar + # A standard error with a different name + # for identifying errors internal to zanzibar + class Error < StandardError + end +end diff --git a/lib/zanzibar/ui.rb b/lib/zanzibar/ui.rb new file mode 100644 index 0000000..b8da719 --- /dev/null +++ b/lib/zanzibar/ui.rb @@ -0,0 +1,42 @@ +require 'rubygems/user_interaction' + +module Zanzibar + # Prints messages out to stdout + class Shell + attr_writer :shell + + def initialize(shell) + @shell = shell + @quiet = false + @debug = ENV['DEBUG'] + end + + def debug(message = nil) + @shell.say(message || yield) if @debug && !@quiet + end + + def info(message = nil) + @shell.say(message || yield) unless @quiet + end + + def confirm(message = nil) + @shell.say(message || yield, :green) unless @quiet + end + + def warn(message = nil) + @shell.say(message || yield, :yellow) + end + + def error(message = nil) + @shell.say(message || yield, :red) + end + + def be_quiet! + @quiet = true + end + + def debug! + @debug = true + end + end +end diff --git a/lib/zanzibar/version.rb b/lib/zanzibar/version.rb index 8cf5d72..b929520 100644 --- a/lib/zanzibar/version.rb +++ b/lib/zanzibar/version.rb @@ -1,3 +1,4 @@ -module Zanzibar - VERSION = '0.1.13' -end +# The version of the gem +module Zanzibar + VERSION = '0.1.15' +end diff --git a/spec/files/scrt.wsdl b/spec/files/scrt.wsdl new file mode 100644 index 0000000..4ee82ae --- /dev/null +++ b/spec/files/scrt.wsdl @@ -0,0 +1,629 @@ + + + Webservice for standard integration. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Webservice for standard integration. + + + + + + + + + + + + + + diff --git a/spec/lib/zanzibar/actions/bundle_spec.rb b/spec/lib/zanzibar/actions/bundle_spec.rb new file mode 100644 index 0000000..714bd81 --- /dev/null +++ b/spec/lib/zanzibar/actions/bundle_spec.rb @@ -0,0 +1,94 @@ +require 'zanzibar/cli' +require 'rspec' +require 'fakefs/spec_helpers' +require 'webmock' +require 'rspec' +require 'webmock/rspec' + +include WebMock::API + +describe Zanzibar::Cli do + include FakeFS::SpecHelpers + + describe '#bundle' do + context 'when Zanzifile already exists' do + before(:each) do + spec_root = File.join(source_root, 'spec') + response_root = File.join(spec_root, 'responses') + wsdl = File.join(spec_root, 'scrt.wsdl') + files = File.join(spec_root, 'files') + + FakeFS::FileSystem.clone response_root + FakeFS::FileSystem.clone wsdl + FakeFS::FileSystem.clone files + + stub_request(:any, 'https://www.zanzitest.net/webservices/sswebservice.asmx') + .to_return(body: AUTH_XML, status: 200).then + .to_return(body: SECRET_WITH_KEY_XML, status: 200).then + .to_return(body: PRIVATE_KEY_XML, status: 200) + + Dir.chdir File.join(source_root, 'spec', 'files') + end + + before(:all) do + ENV['ZANZIBAR_PASSWORD'] = 'password' + end + + after(:all) do + ENV.delete 'ZANZIBAR_PASSWORD' + end + + it 'should have a Zanzifile' do + expect(FakeFS::FileTest.file? File.join(source_root, 'spec', 'files', 'Zanzifile')).to be(true) + expect(File.read(File.join(source_root, 'spec', 'files', 'Zanzifile'))).to include('zanzitest') + end + + it 'should download a file' do + expect(FakeFS::FileTest.file? File.join('secrets', 'zanzi_key')).to be(false) + expect { subject.bundle }.to output(/Finished downloading secrets/).to_stdout + expect(FakeFS::FileTest.file? File.join('secrets', 'zanzi_key')).to be(true) + end + + it 'should create a resolved file' do + expect(FakeFS::FileTest.file? 'Zanzifile.resolved').to be(false) + expect { subject.bundle }.to output(/Finished downloading secrets/).to_stdout + expect(FakeFS::FileTest.file? 'Zanzifile.resolved').to be(true) + end + + it 'should not redownload files it already has' do + expect { subject.bundle }.to output(/Finished downloading secrets/).to_stdout + expect(WebMock).to have_requested(:post, 'https://www.zanzitest.net/webservices/sswebservice.asmx').times(3) + + WebMock.reset! + + expect { subject.bundle }.to output(/Finished downloading secrets/).to_stdout + expect(WebMock).not_to have_requested(:post, 'https://www.zanzitest.net/webservices/sswebservice.asmx') + end + + it 'should redownload on update action' do + expect { subject.bundle }.to output(/Finished downloading secrets/).to_stdout + expect(WebMock).to have_requested(:post, 'https://www.zanzitest.net/webservices/sswebservice.asmx').times(3) + + WebMock.reset! + stub_request(:any, 'https://www.zanzitest.net/webservices/sswebservice.asmx') + .to_return(body: AUTH_XML, status: 200).then + .to_return(body: SECRET_WITH_KEY_XML, status: 200).then + .to_return(body: PRIVATE_KEY_XML, status: 200) + + expect { subject.update }.to output(/Finished downloading secrets/).to_stdout + expect(WebMock).to have_requested(:post, 'https://www.zanzitest.net/webservices/sswebservice.asmx').times(3) + end + + it 'should reject a malformed Zanzifile' do + File.write('Zanzifile', 'broken YAML') + expect { subject.bundle }.to raise_error.with_message(/#{Zanzibar::INVALID_ZANZIFILE_ERROR}/) + end + end + + context 'when Zanzifile does not exist' do + it 'should return an error' do + expect { subject.bundle }.to raise_error.with_message(/#{Zanzibar::NO_ZANZIFILE_ERROR}/) + end + end + end +end diff --git a/spec/lib/zanzibar/actions/get_spec.rb b/spec/lib/zanzibar/actions/get_spec.rb new file mode 100644 index 0000000..7000866 --- /dev/null +++ b/spec/lib/zanzibar/actions/get_spec.rb @@ -0,0 +1,64 @@ +require 'zanzibar/cli' +require 'rspec' +require 'fakefs/spec_helpers' +require 'webmock' +require 'rspec' +require 'webmock/rspec' +require 'zanzibar/defaults' + +include WebMock::API + +describe Zanzibar::Cli do + include FakeFS::SpecHelpers + + describe '#get' do + before(:each) do + spec_root = File.join(source_root, 'spec') + response_root = File.join(spec_root, 'responses') + wsdl = File.join(spec_root, 'scrt.wsdl') + files = File.join(spec_root, 'files') + + FakeFS::FileSystem.clone response_root + FakeFS::FileSystem.clone wsdl + FakeFS::FileSystem.clone files + + stub_request(:any, 'https://www.zanzitest.net/webservices/sswebservice.asmx') + .to_return(body: AUTH_XML, status: 200).then + .to_return(body: SECRET_XML, status: 200) + + Dir.chdir File.join(source_root, 'spec', 'files') + end + + before(:all) do + ENV['ZANZIBAR_PASSWORD'] = 'password' + end + + after(:all) do + ENV.delete 'ZANZIBAR_PASSWORD' + WebMock.reset! + end + + it 'should print a password to stdout' do + subject.options = { 'domain' => 'zanzitest.net', 'wsdl' => 'scrt.wsdl' } + expect { subject.get(1234) }.to output(/zanziUserPassword/).to_stdout + end + + it 'should require a wsdl' do + expect { subject.get(1234) }.to raise_error.with_message(/#{Zanzibar::NO_WSDL_ERROR}/) + end + + it 'should be able to download files' do + WebMock.reset! + stub_request(:any, 'https://www.zanzitest.net/webservices/sswebservice.asmx') + .to_return(body: AUTH_XML, status: 200).then + .to_return(body: SECRET_WITH_KEY_XML, status: 200).then + .to_return(body: PRIVATE_KEY_XML, status: 200) + + subject.options = { 'domain' => 'zanzitest.net', 'wsdl' => 'scrt.wsdl', 'filelabel' => 'Private Key' } + + expect(FakeFS::FileTest.file? 'zanzi_key').to be(false) + expect { subject.get(2345) }.to output(/zanzi_key/).to_stdout + expect(FakeFS::FileTest.file? 'zanzi_key') + end + end +end diff --git a/spec/lib/zanzibar/actions/init_spec.rb b/spec/lib/zanzibar/actions/init_spec.rb new file mode 100644 index 0000000..e825542 --- /dev/null +++ b/spec/lib/zanzibar/actions/init_spec.rb @@ -0,0 +1,53 @@ +require 'zanzibar/cli' +require 'zanzibar/defaults' +require 'rspec' +require 'fakefs/spec_helpers' + +describe Zanzibar::Cli do + include FakeFS::SpecHelpers + + describe '#init' do + before(:each) do + templates_root = File.join(source_root, 'templates') + FakeFS::FileSystem.clone templates_root + end + + context 'when a file does not yet exist' do + it 'should create a template file' do + expect { subject.init }.to output(/has been created/).to_stdout + expect(FakeFS::FileTest.file? Zanzibar::ZANZIFILE_NAME).to be(true) + expect(File.read Zanzibar::ZANZIFILE_NAME).to match(/fill in your secrets/) + end + + it 'should accept settings as options' do + subject.options = { 'wsdl' => 'http://example.com/ss?wsdl', + 'domain' => 'example.com', + 'secretdir' => 'testfolderplzignore', + 'ignoressl' => true } + + expect { subject.init }.to output(/has been created/).to_stdout + contents = File.read Zanzibar::ZANZIFILE_NAME + expect(contents).to include('wsdl: http://example.com/ss?wsdl') + expect(contents).to include('domain: example.com') + expect(contents).to include('secret_dir: testfolderplzignore') + expect(contents).to include('ignore_ssl: true') + end + end + + context 'when a file already exists' do + before(:each) { File.write(Zanzibar::ZANZIFILE_NAME, 'test value') } + + it 'should not overwrite an existing file' do + expect { subject.init }.to raise_error.with_message(/#{Zanzibar::ALREADY_EXISTS_ERROR}/) + expect(File.read Zanzibar::ZANZIFILE_NAME).to eq('test value') + end + + it 'should obey the force flag' do + subject.options = { 'force' => true } + + expect { subject.init }.to output(/has been created/).to_stdout + expect(File.read Zanzibar::ZANZIFILE_NAME).to match('fill in your secrets') + end + end + end +end diff --git a/spec/lib/zanzibar/version_spec.rb b/spec/lib/zanzibar/version_spec.rb new file mode 100644 index 0000000..3f4b2a2 --- /dev/null +++ b/spec/lib/zanzibar/version_spec.rb @@ -0,0 +1,12 @@ +require 'zanzibar/cli' +require 'zanzibar/version' +require 'zanzibar/defaults' +require 'rspec' + +describe Zanzibar::Cli do + describe '#version' do + it 'should print the gem version' do + expect { subject.version }.to output(/#{Zanzibar::APPLICATION_NAME} Version/).to_stdout + end + end +end diff --git a/spec/zanzibar_spec.rb b/spec/lib/zanzibar_spec.rb similarity index 62% rename from spec/zanzibar_spec.rb rename to spec/lib/zanzibar_spec.rb index 6704de4..27d9bd6 100644 --- a/spec/zanzibar_spec.rb +++ b/spec/lib/zanzibar_spec.rb @@ -1,123 +1,116 @@ -require 'zanzibar' -require 'savon' -require 'webmock' -require 'rspec' -require 'webmock/rspec' - -include WebMock::API - -describe 'Zanzibar Test' do - client = Zanzibar::Zanzibar.new(domain: 'zanzitest.net', pwd: 'password', wsdl: 'spec/scrt.wsdl') - auth_xml = File.read('spec/responses/authenticate_response.xml') - secret_xml = File.read('spec/responses/get_secret_response.xml') - secret_with_key_xml = File.read('spec/responses/get_secret_with_keys_response.xml') - secret_with_attachment_xml = File.read('spec/responses/get_secret_with_attachment_response.xml') - private_key_xml = File.read('spec/responses/download_private_key_response.xml') - public_key_xml = File.read('spec/responses/download_public_key_response.xml') - attachment_xml = File.read('spec/responses/attachment_response.xml') - - it 'should return an auth token' do - stub_request(:any, 'https://www.zanzitest.net/webservices/sswebservice.asmx') - .to_return(body: auth_xml, status: 200) - - expect(client.get_token).to eq('imatoken') - end - - it 'should get a secret' do - stub_request(:any, 'https://www.zanzitest.net/webservices/sswebservice.asmx') - .to_return(body: auth_xml, status: 200).then - .to_return(body: secret_xml, status: 200) - - expect(client.get_secret(1234)[:secret][:name]).to eq('Zanzi Test Secret') - end - - it 'should get a password' do - stub_request(:any, 'https://www.zanzitest.net/webservices/sswebservice.asmx') - .to_return(body: auth_xml, status: 200).then - .to_return(body: secret_xml, status: 200) - - expect(client.get_password(1234)).to eq('zanziUserPassword') - end - - it 'should download a private key' do - stub_request(:any, 'https://www.zanzitest.net/webservices/sswebservice.asmx') - .to_return(body: auth_xml, status: 200).then - .to_return(body: secret_with_key_xml, status: 200).then - .to_return(body: private_key_xml, status: 200) - - client.download_secret_file(scrt_id: 2345, type: 'Private Key') - expect(File.exist? 'zanzi_key') - expect(File.read('zanzi_key')).to eq("-----BEGIN RSA PRIVATE KEY -----\nzanzibarTestPassword\n-----END RSA PRIVATE KEY-----\n") - File.delete('zanzi_key') - end - - it 'should download a private key legacy' do - stub_request(:any, 'https://www.zanzitest.net/webservices/sswebservice.asmx') - .to_return(body: auth_xml, status: 200).then - .to_return(body: secret_with_key_xml, status: 200).then - .to_return(body: private_key_xml, status: 200) - - client.download_private_key(scrt_id: 2345) - expect(File.exist? 'zanzi_key') - expect(File.read('zanzi_key')).to eq("-----BEGIN RSA PRIVATE KEY -----\nzanzibarTestPassword\n-----END RSA PRIVATE KEY-----\n") - File.delete('zanzi_key') - end - - it 'should download a public key' do - stub_request(:any, 'https://www.zanzitest.net/webservices/sswebservice.asmx') - .to_return(body: auth_xml, status: 200).then - .to_return(body: secret_with_key_xml, status: 200).then - .to_return(body: public_key_xml, status: 200) - - client.download_secret_file(scrt_id: 2345, type: 'Public Key') - expect(File.exist? 'zanzi_key.pub') - expect(File.read('zanzi_key.pub')).to eq("1234PublicKey5678==\n") - File.delete('zanzi_key.pub') - end - - it 'should download a public key legacy' do - stub_request(:any, 'https://www.zanzitest.net/webservices/sswebservice.asmx') - .to_return(body: auth_xml, status: 200).then - .to_return(body: secret_with_key_xml, status: 200).then - .to_return(body: public_key_xml, status: 200) - - client.download_public_key(scrt_id: 2345) - expect(File.exist? 'zanzi_key.pub') - expect(File.read('zanzi_key.pub')).to eq("1234PublicKey5678==\n") - File.delete('zanzi_key.pub') - end - - it 'should download an attachment' do - stub_request(:any, 'https://www.zanzitest.net/webservices/sswebservice.asmx') - .to_return(body: auth_xml, status: 200).then - .to_return(body: secret_with_attachment_xml, status: 200).then - .to_return(body: attachment_xml, status: 200) - - client.download_secret_file(scrt_id: 3456, type: 'Attachment') - expect(File.exist? 'attachment.txt') - expect(File.read('attachment.txt')).to eq("I am a secret attachment\n") - File.delete('attachment.txt') - end - - it 'should download an attachment legacy' do - stub_request(:any, 'https://www.zanzitest.net/webservices/sswebservice.asmx') - .to_return(body: auth_xml, status: 200).then - .to_return(body: secret_with_attachment_xml, status: 200).then - .to_return(body: attachment_xml, status: 200) - - client.download_attachment(scrt_id: 3456) - expect(File.exist? 'attachment.txt') - expect(File.read('attachment.txt')).to eq("I am a secret attachment\n") - File.delete('attachment.txt') - end - - it 'should use environment variables for credentials' do - ENV['ZANZIBAR_USER'] = "environment_user" - ENV['ZANZIBAR_PASSWORD'] = "environment_password" - client = Zanzibar::Zanzibar.new(domain: 'zanzitest.net', wsdl: 'spec/scrt.wsdl') - expect(client.get_client_username).to eq(ENV['ZANZIBAR_USER']) - expect(client.get_client_password).to eq(ENV['ZANZIBAR_PASSWORD']) - ENV.delete 'ZANZIBAR_PASSWORD' - ENV.delete 'ZANZIBAR_USER' - end -end +require 'zanzibar' +require 'savon' +require 'webmock' +require 'rspec' +require 'webmock/rspec' + +include WebMock::API + +describe 'Zanzibar Test' do + client = Zanzibar::Zanzibar.new(domain: 'zanzitest.net', pwd: 'password', wsdl: 'spec/scrt.wsdl') + + it 'should return an auth token' do + stub_request(:any, 'https://www.zanzitest.net/webservices/sswebservice.asmx') + .to_return(body: AUTH_XML, status: 200) + + expect(client.get_token).to eq('imatoken') + end + + it 'should get a secret' do + stub_request(:any, 'https://www.zanzitest.net/webservices/sswebservice.asmx') + .to_return(body: AUTH_XML, status: 200).then + .to_return(body: SECRET_XML, status: 200) + + expect(client.get_secret(1234)[:secret][:name]).to eq('Zanzi Test Secret') + end + + it 'should get a password' do + stub_request(:any, 'https://www.zanzitest.net/webservices/sswebservice.asmx') + .to_return(body: AUTH_XML, status: 200).then + .to_return(body: SECRET_XML, status: 200) + + expect(client.get_password(1234)).to eq('zanziUserPassword') + end + + it 'should download a private key' do + stub_request(:any, 'https://www.zanzitest.net/webservices/sswebservice.asmx') + .to_return(body: AUTH_XML, status: 200).then + .to_return(body: SECRET_WITH_KEY_XML, status: 200).then + .to_return(body: PRIVATE_KEY_XML, status: 200) + + client.download_secret_file(scrt_id: 2345, type: 'Private Key') + expect(File.exist? 'zanzi_key') + expect(File.read('zanzi_key')).to eq("-----BEGIN RSA PRIVATE KEY -----\nzanzibarTestPassword\n-----END RSA PRIVATE KEY-----\n") + File.delete('zanzi_key') + end + + it 'should download a private key legacy' do + stub_request(:any, 'https://www.zanzitest.net/webservices/sswebservice.asmx') + .to_return(body: AUTH_XML, status: 200).then + .to_return(body: SECRET_WITH_KEY_XML, status: 200).then + .to_return(body: PRIVATE_KEY_XML, status: 200) + + client.download_private_key(scrt_id: 2345) + expect(File.exist? 'zanzi_key') + expect(File.read('zanzi_key')).to eq("-----BEGIN RSA PRIVATE KEY -----\nzanzibarTestPassword\n-----END RSA PRIVATE KEY-----\n") + File.delete('zanzi_key') + end + + it 'should download a public key' do + stub_request(:any, 'https://www.zanzitest.net/webservices/sswebservice.asmx') + .to_return(body: AUTH_XML, status: 200).then + .to_return(body: SECRET_WITH_KEY_XML, status: 200).then + .to_return(body: PUBLIC_KEY_XML, status: 200) + + client.download_secret_file(scrt_id: 2345, type: 'Public Key') + expect(File.exist? 'zanzi_key.pub') + expect(File.read('zanzi_key.pub')).to eq("1234PublicKey5678==\n") + File.delete('zanzi_key.pub') + end + + it 'should download a public key legacy' do + stub_request(:any, 'https://www.zanzitest.net/webservices/sswebservice.asmx') + .to_return(body: AUTH_XML, status: 200).then + .to_return(body: SECRET_WITH_KEY_XML, status: 200).then + .to_return(body: PUBLIC_KEY_XML, status: 200) + + client.download_public_key(scrt_id: 2345) + expect(File.exist? 'zanzi_key.pub') + expect(File.read('zanzi_key.pub')).to eq("1234PublicKey5678==\n") + File.delete('zanzi_key.pub') + end + + it 'should download an attachment' do + stub_request(:any, 'https://www.zanzitest.net/webservices/sswebservice.asmx') + .to_return(body: AUTH_XML, status: 200).then + .to_return(body: SECRET_WITH_ATTACHMENT_XML, status: 200).then + .to_return(body: ATTACHMENT_XML, status: 200) + + client.download_secret_file(scrt_id: 3456, type: 'Attachment') + expect(File.exist? 'attachment.txt') + expect(File.read('attachment.txt')).to eq("I am a secret attachment\n") + File.delete('attachment.txt') + end + + it 'should download an attachment legacy' do + stub_request(:any, 'https://www.zanzitest.net/webservices/sswebservice.asmx') + .to_return(body: AUTH_XML, status: 200).then + .to_return(body: SECRET_WITH_ATTACHMENT_XML, status: 200).then + .to_return(body: ATTACHMENT_XML, status: 200) + + client.download_attachment(scrt_id: 3456) + expect(File.exist? 'attachment.txt') + expect(File.read('attachment.txt')).to eq("I am a secret attachment\n") + File.delete('attachment.txt') + end + + it 'should use environment variables for credentials' do + ENV['ZANZIBAR_USER'] = 'environment_user' + ENV['ZANZIBAR_PASSWORD'] = 'environment_password' + client = Zanzibar::Zanzibar.new(domain: 'zanzitest.net', wsdl: 'spec/scrt.wsdl') + expect(client.get_client_username).to eq(ENV['ZANZIBAR_USER']) + expect(client.get_client_password).to eq(ENV['ZANZIBAR_PASSWORD']) + ENV.delete 'ZANZIBAR_PASSWORD' + ENV.delete 'ZANZIBAR_USER' + end +end diff --git a/spec/spec/spec_helper.rb b/spec/spec/spec_helper.rb deleted file mode 100644 index f684b32..0000000 --- a/spec/spec/spec_helper.rb +++ /dev/null @@ -1,95 +0,0 @@ -# This file was generated by the `rspec --init` command. Conventionally, all -# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. -# The generated `.rspec` file contains `--require spec_helper` which will cause this -# file to always be loaded, without a need to explicitly require it in any files. -# -# Given that it is always loaded, you are encouraged to keep this file as -# light-weight as possible. Requiring heavyweight dependencies from this file -# will add to the boot time of your test suite on EVERY test run, even for an -# individual file that may not need all of that loaded. Instead, consider making -# a separate helper file that requires the additional dependencies and performs -# the additional setup, and require it from the spec files that actually need it. -# -# The `.rspec` file also contains a few flags that are not defaults but that -# users commonly want. -# -# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration -require 'webmock/rspec' -require 'codeclimate-test-reporter' -CodeClimate::TestReporter.start - -RSpec.configure do |config| - # rspec-expectations config goes here. You can use an alternate - # assertion/expectation library such as wrong or the stdlib/minitest - # assertions if you prefer. - config.expect_with :rspec do |expectations| - # This option will default to `true` in RSpec 4. It makes the `description` - # and `failure_message` of custom matchers include text for helper methods - # defined using `chain`, e.g.: - # be_bigger_than(2).and_smaller_than(4).description - # # => "be bigger than 2 and smaller than 4" - # ...rather than: - # # => "be bigger than 2" - expectations.include_chain_clauses_in_custom_matcher_descriptions = true - end - - # rspec-mocks config goes here. You can use an alternate test double - # library (such as bogus or mocha) by changing the `mock_with` option here. - config.mock_with :rspec do |mocks| - # Prevents you from mocking or stubbing a method that does not exist on - # a real object. This is generally recommended, and will default to - # `true` in RSpec 4. - mocks.verify_partial_doubles = true - end - - config.after(:suite) do - WebMock.disable_net_connect!(allow: 'codeclimate.com') - end - - # The settings below are suggested to provide a good initial experience - # with RSpec, but feel free to customize to your heart's content. - # # These two settings work together to allow you to limit a spec run - # # to individual examples or groups you care about by tagging them with - # # `:focus` metadata. When nothing is tagged with `:focus`, all examples - # # get run. - # config.filter_run :focus - # config.run_all_when_everything_filtered = true - # - # # Limits the available syntax to the non-monkey patched syntax that is recommended. - # # For more details, see: - # # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax - # # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ - # # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching - # config.disable_monkey_patching! - # - # # This setting enables warnings. It's recommended, but in some cases may - # # be too noisy due to issues in dependencies. - # config.warnings = true - # - # # Many RSpec users commonly either run the entire suite or an individual - # # file, and it's useful to allow more verbose output when running an - # # individual spec file. - # if config.files_to_run.one? - # # Use the documentation formatter for detailed output, - # # unless a formatter has already been configured - # # (e.g. via a command-line flag). - # config.default_formatter = 'doc' - # end - # - # # Print the 10 slowest examples and example groups at the - # # end of the spec run, to help surface which specs are running - # # particularly slow. - # config.profile_examples = 10 - # - # # Run specs in random order to surface order dependencies. If you find an - # # order dependency and want to debug it, you can fix the order by providing - # # the seed, which is printed after each run. - # # --seed 1234 - # config.order = :random - # - # # Seed global randomization in this process using the `--seed` CLI option. - # # Setting this allows you to use `--seed` to deterministically reproduce - # # test failures related to randomization by passing the same `--seed` value - # # as the one that triggered the failure. - # Kernel.srand config.seed -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..2280206 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,37 @@ +# This file was generated by the `rspec --init` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause this +# file to always be loaded, without a need to explicitly require it in any files. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +require 'webmock/rspec' +require 'codeclimate-test-reporter' +require 'simplecov' +CodeClimate::TestReporter.start +SimpleCov.start + +RSpec.configure do |config| + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end + + config.after(:suite) do + WebMock.disable_net_connect!(allow: 'codeclimate.com') + end +end + +def source_root + File.expand_path('../../', __FILE__) +end + +AUTH_XML = File.read('spec/responses/authenticate_response.xml') +SECRET_XML = File.read('spec/responses/get_secret_response.xml') +SECRET_WITH_KEY_XML = File.read('spec/responses/get_secret_with_keys_response.xml') +SECRET_WITH_ATTACHMENT_XML = File.read('spec/responses/get_secret_with_attachment_response.xml') +PRIVATE_KEY_XML = File.read('spec/responses/download_private_key_response.xml') +PUBLIC_KEY_XML = File.read('spec/responses/download_public_key_response.xml') +ATTACHMENT_XML = File.read('spec/responses/attachment_response.xml') diff --git a/templates/Zanzifile.erb b/templates/Zanzifile.erb new file mode 100644 index 0000000..5d5c497 --- /dev/null +++ b/templates/Zanzifile.erb @@ -0,0 +1,11 @@ +--- +settings: + wsdl: <%= wsdl %> + domain: <%= domain %> + secret_dir: <%= secretdir %> + ignore_ssl: <%= ignoressl %> +secrets: +# TODO fill in your secrets like so: +# ssh_key: +# id: 1 +# label: Private Key diff --git a/zanzibar.gemspec b/zanzibar.gemspec index f5082e4..198a12a 100644 --- a/zanzibar.gemspec +++ b/zanzibar.gemspec @@ -14,13 +14,21 @@ Gem::Specification.new do |spec| spec.license = 'Apache 2.0' spec.files = `git ls-files -z`.split("\x0") - spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } - spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) + spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) } + spec.test_files = spec.files.grep(/^(test|spec|features)\//) spec.require_paths = ['lib'] - spec.add_dependency 'rubyntlm', '~> 0.4.0' spec.add_development_dependency 'bundler', '~> 1.7' spec.add_development_dependency 'rake', '~> 10.0' - spec.add_development_dependency 'rubocop', '~>0.18.1' + spec.add_development_dependency 'rubocop', '~> 0.28.0' + spec.add_development_dependency 'savon_spec', '~> 0.1.6' + spec.add_development_dependency 'rspec', '~> 3.1.0' + spec.add_development_dependency 'webmock', '~> 1.20.4' + spec.add_development_dependency 'codeclimate-test-reporter' + spec.add_development_dependency 'fakefs', '~> 0.6.4' + spec.add_development_dependency 'simplecov', '~> 0.9.1' + spec.add_runtime_dependency 'savon', '~> 2.8.0' + spec.add_runtime_dependency 'rubyntlm', '~> 0.4.0' + spec.add_runtime_dependency 'thor', '~> 0.19.0' end