class Morpheus::Cli::CypherCommand

Public Class Methods

new() click to toggle source
some appropriate aliases

register_subcommands :read => :get, register_subcommands :write => :put register_subcommands :add => :put register_subcommands :delete => :remove

register_subcommands :destroy => :destroy
# File lib/morpheus/cli/commands/cypher_command.rb, line 16
def initialize()
  # @appliance_name, @appliance_url = Morpheus::Cli::Remote.active_appliance
end

Public Instance Methods

connect(opts) click to toggle source
# File lib/morpheus/cli/commands/cypher_command.rb, line 20
def connect(opts)
  @api_client = establish_remote_appliance_connection(opts)
  # @cypher_interface = @api_client.cypher
  @cypher_interface = @api_client.cypher
end
get(args) click to toggle source
# File lib/morpheus/cli/commands/cypher_command.rb, line 114
def get(args)
  options = {}
  params = {}
  value_only = false
  do_decrypt = false
  ttl = nil
  optparse = Morpheus::Cli::OptionParser.new do |opts|
    opts.banner = subcommand_usage("[key]")
    # opts.on(nil, '--decrypt', 'Display the decrypted value') do
    #   do_decrypt = true
    # end
    # opts.on(nil, '--metadata', 'Display metadata about the key, such as versions.') do
    #   display_versions = true
    # end
    opts.on('-v', '--value', 'Print only the decrypted value.') do
      value_only = true
    end
    opts.on( '-t', '--ttl SECONDS', "Time to live, the lease duration before this key expires. Use if creating new key." ) do |val|
      ttl = val
      if val.to_s.empty? || val.to_s == '0'
        ttl = 0
      else
        ttl = val
      end
    end
    build_common_options(opts, options, [:query, :json, :yaml, :csv, :fields, :outfile, :dry_run, :quiet, :remote])
    opts.footer = "Read a cypher item and display the decrypted value." + "\n" +
                  "[key] is required. This is the cypher key to read." + "\n" +
                  "Use --ttl to specify a ttl if expecting cypher engine to automatically create the key."
  end
  optparse.parse!(args)
  if args.count != 1
    print_error Morpheus::Terminal.angry_prompt
    puts_error  "wrong number of arguments, expected 1 and got #{args.count}\n#{optparse}"
    return 1
  end
  connect(options)
  begin
    item_key = args[0]
    if ttl
      params["ttl"] = ttl
    end
    @cypher_interface.setopts(options)
    if options[:dry_run]
      print_dry_run @cypher_interface.dry.get(item_key, params)
      return 0
    end
    params.merge!(parse_list_options(options))
    json_response = @cypher_interface.get(item_key, params)
    render_result = render_with_format(json_response, options)
    if render_result
      return 0
    end
    
    cypher_item = json_response['cypher']
    decrypted_value = json_response["data"]
    data_type = decrypted_value.is_a?(String) ? 'string' : 'object'

    if value_only
      print cyan
      if decrypted_value.is_a?(Hash)
        puts as_json(decrypted_value)
      else
        puts decrypted_value.to_s
      end
      print reset
      return 0
    end

    print_h1 "Cypher Key", [], options
    print cyan
    # This response does contain cypher too though.
    
    if cypher_item.empty?
      puts "Cypher item not found in response"
    else
      description_cols = {
        #"ID" => 'id',
        "Key" => lambda {|it| it["itemKey"] },
        "TTL" => lambda {|it| 
          format_expiration_ttl(it["expireDate"])
        },
        # "Type" => lambda {|it|
        #   data_type
        # },
        "Expiration" => lambda {|it| 
          format_expiration_date(it["expireDate"])
        },
        # "Date Created" => lambda {|it| format_local_dt(it["dateCreated"]) },
        "Last Updated" => lambda {|it| format_local_dt(it["lastUpdated"]) },
        "Last Accessed" => lambda {|it| format_local_dt(it["lastAccessed"]) }
      }
      if cypher_item["expireDate"].nil?
        description_cols.delete("Expires")
      end
      print_description_list(description_cols, cypher_item)
    end
    # print_h2 "Value", options
    # print_h2 "Data", options
    print_h2 "Data (#{data_type})", options
    
    if decrypted_value
      print cyan
      if decrypted_value.is_a?(String)
        # attempt to parse and render as_json
        if decrypted_value.to_s[0] == '{' && decrypted_value.to_s[-1] == '}'
          begin
            json_value = JSON.parse(decrypted_value)
            puts as_json(json_value)
          rescue => ex
            Morpheus::Logging::DarkPrinter.puts "Failed to parse cypher value '#{decrypted_value}' as JSON. Error: #{ex}" if Morpheus::Logging.debug?
            puts decrypted_value
          end
        else
          puts decrypted_value
        end
      else
        puts as_json(decrypted_value)
      end
    else
      puts "No data found."
    end
    
    print reset, "\n"

    return 0
  rescue RestClient::Exception => e
    print_rest_exception(e, options)
    return 1
  end
end
handle(args) click to toggle source
# File lib/morpheus/cli/commands/cypher_command.rb, line 26
def handle(args)
  handle_subcommand(args)
end
list(args) click to toggle source
# File lib/morpheus/cli/commands/cypher_command.rb, line 30
def list(args)
  options = {}
  params = {}
  optparse = Morpheus::Cli::OptionParser.new do |opts|
    opts.banner = subcommand_usage("[key]")
    # opts.on('--details', '--details', "Show more details." ) do
    #   options[:details] = true
    # end
    build_common_options(opts, options, [:list, :query, :json, :yaml, :csv, :fields, :json, :dry_run, :remote])
    opts.footer = "List cypher keys." + "\n" +
                  "[key] is optional. This is the cypher key or path to search for."
  end
  optparse.parse!(args)
  connect(options)
  if args.count > 1
    print_error Morpheus::Terminal.angry_prompt
    puts_error  "wrong number of arguments, expected 0-1 and got #{args.count}\n#{optparse}"
    return 1
  end
  item_key = args[0]
  begin
    params.merge!(parse_list_options(options))
    @cypher_interface.setopts(options)
    if options[:dry_run]
      print_dry_run @cypher_interface.dry.list(item_key, params)
      return 0
    end
    json_response = @cypher_interface.list(item_key, params)
    if options[:json]
      puts as_json(json_response, options)
      return 0
    elsif options[:yaml]
      puts as_yaml(json_response, options)
      return 0
    elsif options[:csv]
      puts records_as_csv([json_response], options)
      return 0
    end
    cypher_items = json_response["cypherItems"] || json_response["cyphers"]
    cypher_data = json_response["data"]
    title = "Morpheus Cypher Key List"
    subtitles = []
    subtitles += parse_list_subtitles(options)
    if item_key
      subtitles << "Key: #{item_key}"
    end
    print_h1 title, subtitles, options

    cypher_keys = json_response["data"] ? json_response["data"]["keys"] : []
    if cypher_keys.nil? || cypher_keys.empty?
      if item_key
        print cyan,"No cypher items found for '#{item_key}'.",reset,"\n"
      end
    else

      cypher_columns = {
        "KEY" => lambda {|it| it["itemKey"] },
        # "LEASE REMAINING" => lambda {|it|
        #   format_lease_remaining(it["expireDate"])
        # },
        "TTL" => lambda {|it| 
          format_expiration_ttl(it["expireDate"])
        },
        "EXPIRATION" => lambda {|it| 
          format_expiration_date(it["expireDate"])
        },
        # "DATE CREATED" => lambda {|it| format_local_dt(it["dateCreated"]) },
        "LAST UPDATED" => lambda {|it| format_local_dt(it["lastUpdated"]) },
        "LAST ACCESSED" => lambda {|it| format_local_dt(it["lastAccessed"]) }
      }
      print cyan
      print as_pretty_table(cypher_items, cypher_columns, options)
      print reset
      # print_results_pagination({size:cypher_keys.size,total:cypher_keys.size.to_i})
      print_results_pagination(json_response)
    end
    print reset,"\n"
    return 0
  rescue RestClient::Exception => e
    print_rest_exception(e, options)
    exit 1
  end
end
put(args) click to toggle source
# File lib/morpheus/cli/commands/cypher_command.rb, line 246
  def put(args)
        usage = <<-EOT
Usage: morpheus #{command_name} put [key] [value] [options] to store a string.
       morpheus #{command_name} put [key] [k=v] [k=v] [options] to store an object.
EOT
    options = {}
    params = {}
    item_key = nil
    item_value = nil
    ttl = nil
    no_overwrite = nil
    optparse = Morpheus::Cli::OptionParser.new do |opts|
      # opts.banner = subcommand_usage("[key] [value]\n\t[key] [k=v] [k=v] [k=v]")
      opts.banner = usage 
      opts.on( '--key KEY', String, "Key. This can also be passed as the first argument." ) do |val|
        item_key = val
      end
      opts.on( '-v', '--value VALUE', "Secret value. This can be used to store a string instead of an object, and can also be passed as the second argument." ) do |val|
        item_value = val
      end
      # opts.on( '--type VALUE', String, "Type, default is based on key engine, string or object" ) do |val|
      #   params['type'] = val
      # end
      opts.on( '-t', '--ttl SECONDS', "Time to live, the lease duration before this key expires." ) do |val|
        ttl = val
        if val.to_s.empty? || val.to_s == '0'
          ttl = 0
        else
          ttl = val
        end
      end
      opts.on( '--type string|object', "The type of data being stored: string or object." ) do |val|
        params['type'] = val
      end
      # opts.on( '--no-overwrite', '--no-overwrite', "Do not overwrite existing keys. Existing keys are overwritten by default." ) do
      #   params['overwrite'] = false
      # end
      build_common_options(opts, options, [:auto_confirm, :options, :payload, :query, :json, :yaml, :csv, :fields, :outfile, :dry_run, :quiet, :remote])
      opts.footer = "Create or update a cypher key." + "\n" +
                    "[key] is required. This is the key of the cypher being created or updated. The key includes the mount prefix eg. secret/hello" + "\n" +
                    "[value] is required for some cypher engines, such as secret. This is the secret value or k=v pairs being stored. Supports 1-N arguments." + "\n" +
                    "If a single [value] is passed, it is stored as type string." + "\n" +
                    "If more than one [value] is passed, the format is expected to be k=v and the value will be stored as an object." + "\n" +
                    "The --value option can be used to store a string value." + "\n" +
                    "The --payload option can be used to store an object."
    end
    optparse.parse!(args)
    # if args.count < 1
    #   print_error Morpheus::Terminal.angry_prompt
    #   puts_error  "wrong number of arguments, expected 1-N and got #{args.count}\n#{optparse}"
    #   return 1
    # end
    connect(options)
    params.merge!(parse_query_options(options))

    # parse arguments like [value] or [k=v]
    if args.count == 0
      # prompt for key and value
    elsif args.count == 1
      item_key = args[0]
      # prompt for value
    elsif args.count == 2
      item_key = args[0]
      item_value = args[1]
      # expecting [value] or [k=v]
      item_value_object = {}
      item_value_pair = item_value.split("=")
      if item_value_pair.size == 2
        item_value_object[item_value_pair[0].to_s] = item_value_pair[1]
        item_value = item_value_object
      else
        # item_value = item_value
      end
    elsif args.count > 2
      item_key = args[0]
      item_value = args[1]
      # expecting [k=v] [k=v]
      item_value_object = {}
      args[1..(args.size-1)].each do |arg|
        item_value_pair = arg.split("=")
        item_value_object[item_value_pair[0].to_s] = item_value_pair[1]
      end
      item_value = item_value_object
    end

    # this is redunant and silly, refactor soon

    # Key prompt
    if item_key
      options[:options]['key'] = item_key
    end
    if item_key.nil?
      v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'key', 'fieldLabel' => 'Key', 'type' => 'text', 'required' => true, 'description' => cypher_key_help}], options[:options])
      item_key = v_prompt['key']
    end

    payload = nil
    if options[:payload]
      payload = options[:payload]
      payload.deep_merge!(options[:options].reject {|k,v| k.is_a?(Symbol) || ['key','value'].include?(k)}) if options[:options] && options[:options].keys.size > 0
    else
      # merge -O options into normally parsed options
      params.deep_merge!(options[:options].reject {|k,v| k.is_a?(Symbol) || ['key','value'].include?(k)}) if options[:options] && options[:options].keys.size > 0
      
      # Value prompt
      value_is_required = false
      cypher_mount_type = item_key.split("/").first
      if ["secret","tfvars"].include?(cypher_mount_type)
        value_is_required = true
      end

      # todo: read value from STDIN shall we?

      # cool, we got value as arguments like foo=bar
      if args.count > 1
        # parse one and only arg as the value like password/mine mypassword123
        if args.count == 2 && args[1].split("=").size() == 1
          item_value = args[1]
        elsif args.count > 1
          # parse args as key value pairs like secret/config foo=bar thing=myvalue
          value_arguments = args[1..-1]
          value_arguments_map = {}
          value_arguments.each do |value_argument|
            value_pair = value_argument.split("=")
            value_arguments_map[value_pair[0]] = value_pair[1] ? value_pair[1..-1].join("=") : nil
          end
          item_value = value_arguments_map
        end
      else
        # Prompt for a single text value to be sent as {"value":"my secret"}
        if value_is_required
          options[:options]['value'] = item_value if item_value
          v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'value', 'fieldLabel' => 'Value', 'type' => 'text', 'required' => value_is_required, 'description' => "Secret value for this cypher"}], options[:options])
          item_value = v_prompt['value']
        end
      end

      # make sure not to pass a value for these or it will not save them.
      # if ['uuid','key','password'].include?(cypher_mount_type)
      #   item_value = nil
      # end

      # construct payload
      # payload = {
      #   'cypher' => params
      # }
      payload = {}
      # if value is valid json, then the payload IS the value
      if item_value.is_a?(String) && item_value.to_s[0] == '{' && item_value.to_s[-1] == '}'
        begin
          json_object = JSON.parse(item_value)
          item_value = json_object
        rescue => ex
          Morpheus::Logging::DarkPrinter.puts "Failed to parse cypher value '#{item_value}' as JSON. Error: #{ex}" if Morpheus::Logging.debug?
          raise_command_error "Failed to parse cypher value as JSON: #{item_value}"
          # return 1
        end
      else
        # it is just a string
        if item_value.is_a?(String)
          params['type'] = 'string'
          #params["value"] = item_value
          payload["value"] = item_value
        elsif item_value.nil?
          payload = {}
        else item_value
          # great, a Hash I hope
          payload = item_value
        end
      end
    end

    # prompt for Lease
    options[:options]['ttl'] = ttl if ttl
    v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'ttl', 'fieldLabel' => 'Lease (TTL in seconds)', 'type' => 'text', 'required' => false, 'description' => cypher_ttl_help, 'defaultValue' => '0'}], options[:options])
    ttl = v_prompt['ttl']

    if ttl
      params['ttl'] = ttl
      #payload["ttl"] = ttl
    end
    @cypher_interface.setopts(options)
    if options[:dry_run]
      print_dry_run @cypher_interface.dry.create(item_key, params, payload)
      return
    end
    existing_cypher = nil
    json_response = @cypher_interface.list(item_key)
    if json_response["data"] && json_response["data"]["keys"]
      existing_cypher = json_response["data"]["keys"].find {|k| k == item_key }
    end
    if existing_cypher
      unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to overwrite the cypher key #{item_key}?")
        return 9, "aborted command"
      end
    end
    json_response = @cypher_interface.create(item_key, params, payload)
    render_result = render_with_format(json_response, options)
    if render_result
      return 0
    end
    #print_green_success "Cypher #{item_key} updated"
    # print_green_success "Wrote cypher #{item_key}"
    print_green_success "Success! Data written to: #{item_key}"
    # should print without doing get, because that can use a token.
    cypher_item = json_response['cypher']
    get_args = [item_key] + (options[:remote] ? ["-r",options[:remote]] : [])
    get(get_args)
    return 0
  end
remove(args) click to toggle source
# File lib/morpheus/cli/commands/cypher_command.rb, line 457
def remove(args)
  options = {}
  params = {}
  optparse = Morpheus::Cli::OptionParser.new do |opts|
    opts.banner = subcommand_usage("[key]")
    build_common_options(opts, options, [:auto_confirm, :json, :dry_run, :quiet, :remote])
    opts.footer = "Delete a cypher." + "\n" +
                  "[key] is required. This is the cypher key to be deleted."
  end
  optparse.parse!(args)

  if args.count != 1
    print_error Morpheus::Terminal.angry_prompt
    puts_error  "wrong number of arguments, expected 1 and got #{args.count}\n#{optparse}"
    return 1
  end

  connect(options)
  begin
    item_key = args[0]
    cypher_item = find_cypher_by_key(item_key)
    return 1 if cypher_item.nil?
    unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete the cypher #{item_key}?")
      return 9, "aborted command"
    end
    @cypher_interface.setopts(options)
    if options[:dry_run]
      print_dry_run @cypher_interface.dry.destroy(item_key, params)
      return
    end
    json_response = @cypher_interface.destroy(item_key, params)
    if options[:json]
      puts as_json(json_response, options)
    elsif !options[:quiet]
      print_green_success "Deleted cypher #{item_key}"
      # list([])
    end
    return 0
  rescue RestClient::Exception => e
    print_rest_exception(e, options)
    return 1
  end
end

Private Instance Methods

cypher_key_help() click to toggle source
# File lib/morpheus/cli/commands/cypher_command.rb, line 517
  def cypher_key_help
    """
Keys can have different behaviors depending on the specified mountpoint.
Available Mountpoints:
password - Generates a secure password of specified character length in the key pattern (or 15) with symbols, numbers, upper case, and lower case letters (i.e. password/15/mypass generates a 15 character password).
tfvars - This is a module to store a tfvars file for terraform.
secret - This is the standard secret module that stores a key/value in encrypted form.
uuid - Returns a new UUID by key name when requested and stores the generated UUID by key name for a given lease timeout period.
key - Generates a Base 64 encoded AES Key of specified bit length in the key pattern (i.e. key/128/mykey generates a 128-bit key)"""
  end
cypher_ttl_help() click to toggle source
# File lib/morpheus/cli/commands/cypher_command.rb, line 528
  def cypher_ttl_help
    """
TTL in seconds
Quick Second Time Reference:
Hour: 3600
Day: 86400
Week: 604800
Month (30 days): 2592000
Year: 31536000
Unlimited: 0
This can be passed in abbreviated duration format. eg. 32d, 90s, 5y
The default is 0, meaning Unlimited.
"""
  end
find_cypher_by_key(key, params={}) click to toggle source
# File lib/morpheus/cli/commands/cypher_command.rb, line 503
def find_cypher_by_key(key, params={})
  begin
    json_response = @cypher_interface.get(key, params)
    return json_response
  rescue RestClient::Exception => e
    if e.response && e.response.code == 404
      print_red_alert "Cypher not found by key #{key}"
      return nil
    else
      raise e
    end
  end
end
format_expiration_date(expire_date, warning_threshold=3600, return_color=cyan) click to toggle source
# File lib/morpheus/cli/commands/cypher_command.rb, line 557
def format_expiration_date(expire_date, warning_threshold=3600, return_color=cyan)
  expire_date = parse_time(expire_date)
  if !expire_date
    # return ""
    return cyan + "Never expires" + return_color
  end
  if expire_date <= Time.now
    return red + format_local_dt(expire_date) + return_color
  else
    return cyan + format_local_dt(expire_date) + return_color
  end
end
format_expiration_ttl(expire_date, warning_threshold=3600, return_color=cyan) click to toggle source
# File lib/morpheus/cli/commands/cypher_command.rb, line 570
def format_expiration_ttl(expire_date, warning_threshold=3600, return_color=cyan)
  expire_date = parse_time(expire_date)
  if !expire_date
    return ""
    #return cyan + "Never expires" + return_color
  end
  seconds = expire_date - Time.now
  if seconds <= 0
    # return red + "Expired" + return_color
    return red + "Expired " + format_duration_seconds(seconds.abs).to_s + " ago" + return_color
  elsif seconds <= warning_threshold
    return yellow + format_duration_seconds(seconds.abs).to_s + return_color
  else
    return cyan + format_duration_seconds(seconds.abs).to_s + return_color
  end
end
format_lease_remaining(expire_date, warning_threshold=3600, return_color=cyan) click to toggle source
# File lib/morpheus/cli/commands/cypher_command.rb, line 543
def format_lease_remaining(expire_date, warning_threshold=3600, return_color=cyan)
  out = ""
  if expire_date
    out << format_expiration_date(expire_date, warning_threshold, return_color)
    out << " ("
    out << format_expiration_ttl(expire_date, warning_threshold, return_color)
    out << ")"
  else
    # out << return_color
    out << "Never expires"
  end
  return out
end