Class: KeeperSecretsManager::Notation::Parser

Inherits:
Object
  • Object
show all
Defined in:
lib/keeper_secrets_manager/notation.rb,
lib/keeper_secrets_manager/notation_enhancements.rb

Overview

Parse and resolve keeper:// notation URIs

Defined Under Namespace

Classes: NotationSection

Constant Summary collapse

ESCAPE_CHAR =
'\\'.freeze
ESCAPE_CHARS =

Characters that can be escaped

'/[]\\'.freeze

Instance Method Summary collapse

Constructor Details

#initialize(secrets_manager) ⇒ Parser

Returns a new instance of Parser.



10
11
12
# File 'lib/keeper_secrets_manager/notation.rb', line 10

def initialize(secrets_manager)
  @secrets_manager = secrets_manager
end

Instance Method Details

#download_file(notation) ⇒ Object

Convenience method to download file content directly



62
63
64
# File 'lib/keeper_secrets_manager/notation_enhancements.rb', line 62

def download_file(notation)
  get_value(notation, auto_process: true, auto_download: true)
end

#get_notation_results(notation) ⇒ Object

Raises:



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/keeper_secrets_manager/notation.rb', line 14

def get_notation_results(notation)
  return [] if notation.nil? || !notation.is_a?(String) || notation.empty?

  parsed = parse_notation(notation)
  raise NotationError, "Invalid notation: #{notation}" if parsed.length < 3

  record_token = parsed[1].text&.first
  selector     = parsed[2].text&.first
  raise NotationError, 'Invalid notation: missing record' unless record_token
  raise NotationError, 'Invalid notation: missing selector' unless selector

  records = @secrets_manager.get_secrets([record_token])
  if records.empty?
    all     = @secrets_manager.get_secrets
    records = all.select { |r| r.title == record_token }
  end
  records = records.uniq { |r| r.uid } if records.size > 1
  raise NotationError, "Multiple records match '#{record_token}'" if records.size > 1
  raise NotationError, "No records match '#{record_token}'"       if records.empty?

  record    = records.first
  parameter = parsed[2].parameter&.first
  index1    = parsed[2].index1&.first
  index2    = parsed[2].index2&.first

  case selector.downcase
  when 'type'
    record.type ? [record.type] : []
  when 'title'
    record.title ? [record.title] : []
  when 'notes'
    (record.notes && !record.notes.empty?) ? [record.notes] : []
  when 'file'
    notation_results_file(record, parameter, record_token)
  when 'field', 'custom_field'
    notation_results_field(record, parameter, index1, index2, parsed[2])
  else
    raise NotationError, "Invalid selector: #{selector}"
  end
end

#get_totp_code(notation) ⇒ Object

Convenience method to get TOTP code directly



57
58
59
# File 'lib/keeper_secrets_manager/notation_enhancements.rb', line 57

def get_totp_code(notation)
  get_value(notation, auto_process: true, generate_totp_code: true)
end

#get_value(notation, options = {}) ⇒ Object

Get value with enhanced functionality This method extends the basic parse method to handle special cases



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/keeper_secrets_manager/notation_enhancements.rb', line 8

def get_value(notation, options = {})
  return nil if notation.nil? || !notation.is_a?(String) || notation.empty?

  value = parse(notation)

  # Check if we should process special types
  return value unless options[:auto_process]

  # Parse the notation to understand what we're dealing with
  parsed = parse_notation(notation)
  return value if parsed.length < 3

  selector = parsed[2].text&.first
  return value unless selector

  case selector.downcase
  when 'file'
    # If it's a file and auto_download is enabled, download it
    if options[:auto_download] && value.is_a?(Hash) && value['fileUid']
      begin
        file_data = @secrets_manager.download_file(value['fileUid'])
        return file_data['data'] # Return file content
      rescue StandardError => e
        raise NotationError, "Failed to download file: #{e.message}"
      end
    end

  when 'field'
    # Check if it's a TOTP field
    parameter = parsed[2].parameter&.first
    if parameter && parameter.downcase == 'onetimecode' && value.is_a?(String) && value.start_with?('otpauth://') && (options[:generate_totp_code])
      begin
        totp_params = TOTP.parse_url(value)
        return TOTP.generate_code(
          totp_params['secret'],
          algorithm: totp_params['algorithm'],
          digits: totp_params['digits'],
          period: totp_params['period']
        )
      rescue StandardError => e
        raise NotationError, "Failed to generate TOTP code: #{e.message}"
      end
    end
  end

  value
end

#parse(notation) ⇒ Object

Parse notation and return value

Raises:



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/keeper_secrets_manager/notation.rb', line 56

def parse(notation)
  return nil if notation.nil?
  raise NotationError, 'Invalid notation format: must be a string' unless notation.is_a?(String)
  return nil if notation.empty?

  # Parse notation URI
  begin
    parsed = parse_notation(notation)
  rescue StandardError => e
    raise NotationError, "Invalid notation format: #{e.message}"
  end

  # Validate we have minimum required sections
  raise NotationError, "Invalid notation: #{notation}" if parsed.length < 3

  # Extract components
  record_token = parsed[1].text&.first
  selector = parsed[2].text&.first

  raise NotationError, 'Invalid notation: missing record' unless record_token
  raise NotationError, 'Invalid notation: missing selector' unless selector

  # Get record
  records = @secrets_manager.get_secrets([record_token])

  # If not found by UID, try by title
  if records.empty?
    all_records = @secrets_manager.get_secrets
    records = all_records.select { |r| r.title == record_token }
  end

  # Remove duplicate UIDs - shortcuts/linked records both shared to same KSM App
  records = records.uniq { |r| r.uid } if records.size > 1

  # Now check for genuine ambiguity (different records with same title)
  raise NotationError, "Multiple records match '#{record_token}'" if records.size > 1
  raise NotationError, "No records match '#{record_token}'" if records.empty?

  record = records.first

  # Extract parameters
  parameter = parsed[2].parameter&.first
  index1 = parsed[2].index1&.first
  index2 = parsed[2].index2&.first

  # Process selector
  case selector.downcase
  when 'type'
    record.type
  when 'title'
    record.title
  when 'notes'
    record.notes
  when 'file'
    handle_file_selector(record, parameter, record_token)
  when 'field', 'custom_field'
    handle_field_selector(record, selector, parameter, index1, index2, parsed[2])
  else
    raise NotationError, "Invalid selector: #{selector}"
  end
end