keeper_secrets_manager 17.0.3
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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +49 -0
- data/Gemfile +13 -0
- data/LICENSE +21 -0
- data/README.md +305 -0
- data/Rakefile +30 -0
- data/examples/basic_usage.rb +139 -0
- data/examples/config_string_example.rb +99 -0
- data/examples/debug_secrets.rb +84 -0
- data/examples/demo_list_secrets.rb +182 -0
- data/examples/download_files.rb +100 -0
- data/examples/flexible_records_example.rb +94 -0
- data/examples/folder_hierarchy_demo.rb +109 -0
- data/examples/full_demo.rb +176 -0
- data/examples/my_test_standalone.rb +176 -0
- data/examples/simple_test.rb +162 -0
- data/examples/storage_examples.rb +126 -0
- data/lib/keeper_secrets_manager/config_keys.rb +27 -0
- data/lib/keeper_secrets_manager/core.rb +1231 -0
- data/lib/keeper_secrets_manager/crypto.rb +348 -0
- data/lib/keeper_secrets_manager/dto/payload.rb +152 -0
- data/lib/keeper_secrets_manager/dto.rb +221 -0
- data/lib/keeper_secrets_manager/errors.rb +79 -0
- data/lib/keeper_secrets_manager/field_types.rb +152 -0
- data/lib/keeper_secrets_manager/folder_manager.rb +114 -0
- data/lib/keeper_secrets_manager/keeper_globals.rb +59 -0
- data/lib/keeper_secrets_manager/notation.rb +354 -0
- data/lib/keeper_secrets_manager/notation_enhancements.rb +67 -0
- data/lib/keeper_secrets_manager/storage.rb +254 -0
- data/lib/keeper_secrets_manager/totp.rb +140 -0
- data/lib/keeper_secrets_manager/utils.rb +196 -0
- data/lib/keeper_secrets_manager/version.rb +3 -0
- data/lib/keeper_secrets_manager.rb +38 -0
- metadata +82 -0
@@ -0,0 +1,221 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'ostruct'
|
3
|
+
require_relative 'dto/payload'
|
4
|
+
|
5
|
+
module KeeperSecretsManager
|
6
|
+
module Dto
|
7
|
+
# Base class for dynamic record handling
|
8
|
+
class KeeperRecord
|
9
|
+
attr_accessor :uid, :title, :type, :fields, :custom, :notes, :folder_uid, :data, :revision, :files
|
10
|
+
|
11
|
+
def initialize(attrs = {})
|
12
|
+
if attrs.is_a?(Hash)
|
13
|
+
# Support both raw API response and user-friendly creation
|
14
|
+
@uid = attrs['recordUid'] || attrs['uid'] || attrs[:uid]
|
15
|
+
@folder_uid = attrs['folderUid'] || attrs['folder_uid'] || attrs[:folder_uid]
|
16
|
+
@revision = attrs['revision'] || attrs[:revision] || 0
|
17
|
+
|
18
|
+
# Handle encrypted data or direct attributes
|
19
|
+
if attrs['data']
|
20
|
+
data = attrs['data'].is_a?(String) ? JSON.parse(attrs['data']) : attrs['data']
|
21
|
+
@title = data['title'] || ''
|
22
|
+
@type = data['type'] || 'login'
|
23
|
+
@fields = data['fields'] || []
|
24
|
+
@custom = data['custom'] || []
|
25
|
+
@notes = data['notes'] || ''
|
26
|
+
else
|
27
|
+
@title = attrs['title'] || attrs[:title] || ''
|
28
|
+
@type = attrs['type'] || attrs[:type] || 'login'
|
29
|
+
@fields = attrs['fields'] || attrs[:fields] || []
|
30
|
+
@custom = attrs['custom'] || attrs[:custom] || []
|
31
|
+
@notes = attrs['notes'] || attrs[:notes] || ''
|
32
|
+
end
|
33
|
+
|
34
|
+
@files = attrs['files'] || attrs[:files] || []
|
35
|
+
@data = attrs
|
36
|
+
end
|
37
|
+
|
38
|
+
# Ensure fields are always arrays of hashes
|
39
|
+
normalize_fields!
|
40
|
+
end
|
41
|
+
|
42
|
+
# Convert to hash for API submission
|
43
|
+
def to_h
|
44
|
+
{
|
45
|
+
'uid' => uid,
|
46
|
+
'title' => title,
|
47
|
+
'type' => type,
|
48
|
+
'fields' => fields,
|
49
|
+
'custom' => custom,
|
50
|
+
'notes' => notes,
|
51
|
+
'folder_uid' => folder_uid
|
52
|
+
}.compact
|
53
|
+
end
|
54
|
+
|
55
|
+
# Find field by type or label
|
56
|
+
def get_field(type_or_label, custom_field = false)
|
57
|
+
field_array = custom_field ? custom : fields
|
58
|
+
field_array.find { |f| f['type'] == type_or_label || f['label'] == type_or_label }
|
59
|
+
end
|
60
|
+
|
61
|
+
# Get field value (always returns array)
|
62
|
+
def get_field_value(type_or_label, custom_field = false)
|
63
|
+
field = get_field(type_or_label, custom_field)
|
64
|
+
field ? field['value'] || [] : []
|
65
|
+
end
|
66
|
+
|
67
|
+
# Get single field value (first element)
|
68
|
+
def get_field_value_single(type_or_label, custom_field = false)
|
69
|
+
values = get_field_value(type_or_label, custom_field)
|
70
|
+
values.first
|
71
|
+
end
|
72
|
+
|
73
|
+
# Add or update field
|
74
|
+
def set_field(type, value, label = nil, custom_field = false)
|
75
|
+
field_array = custom_field ? @custom : @fields
|
76
|
+
|
77
|
+
# Ensure value is an array
|
78
|
+
value = [value] unless value.is_a?(Array)
|
79
|
+
|
80
|
+
# Find existing field
|
81
|
+
existing = field_array.find { |f| f['type'] == type || (label && f['label'] == label) }
|
82
|
+
|
83
|
+
if existing
|
84
|
+
existing['value'] = value
|
85
|
+
existing['label'] = label if label
|
86
|
+
else
|
87
|
+
new_field = { 'type' => type, 'value' => value }
|
88
|
+
new_field['label'] = label if label
|
89
|
+
field_array << new_field
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Dynamic field access methods
|
94
|
+
def method_missing(method, *args, &block)
|
95
|
+
method_name = method.to_s
|
96
|
+
|
97
|
+
# Handle setters
|
98
|
+
if method_name.end_with?('=')
|
99
|
+
field_name = method_name.chomp('=')
|
100
|
+
set_field(field_name, args.first)
|
101
|
+
# Handle getters
|
102
|
+
elsif common_field_types.include?(method_name)
|
103
|
+
get_field_value_single(method_name)
|
104
|
+
else
|
105
|
+
super
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def respond_to_missing?(method, include_private = false)
|
110
|
+
method_name = method.to_s.chomp('=')
|
111
|
+
common_field_types.include?(method_name) || super
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def normalize_fields!
|
117
|
+
@fields = normalize_field_array(@fields)
|
118
|
+
@custom = normalize_field_array(@custom)
|
119
|
+
end
|
120
|
+
|
121
|
+
def normalize_field_array(fields)
|
122
|
+
return [] unless fields.is_a?(Array)
|
123
|
+
|
124
|
+
fields.map do |field|
|
125
|
+
next field if field.is_a?(Hash)
|
126
|
+
|
127
|
+
# Convert to hash if needed
|
128
|
+
field.to_h
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def common_field_types
|
133
|
+
%w[login password url fileRef oneTimeCode name phone email address
|
134
|
+
paymentCard bankAccount birthDate secureNote sshKey host
|
135
|
+
databaseType script passkey]
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Folder representation
|
140
|
+
class KeeperFolder
|
141
|
+
attr_accessor :uid, :name, :parent_uid, :folder_type, :folder_key, :records
|
142
|
+
|
143
|
+
def initialize(attrs = {})
|
144
|
+
@uid = attrs['folderUid'] || attrs['uid'] || attrs[:uid]
|
145
|
+
@name = attrs['name'] || attrs[:name]
|
146
|
+
@parent_uid = attrs['parentUid'] || attrs['parent_uid'] || attrs[:parent_uid] || attrs['parent']
|
147
|
+
@folder_type = attrs['folderType'] || attrs['folder_type'] || attrs[:folder_type] || 'user_folder'
|
148
|
+
@folder_key = attrs['folderKey'] || attrs['folder_key'] || attrs[:folder_key]
|
149
|
+
@records = attrs['records'] || attrs[:records] || []
|
150
|
+
end
|
151
|
+
|
152
|
+
def to_h
|
153
|
+
{
|
154
|
+
'folderUid' => uid,
|
155
|
+
'name' => name,
|
156
|
+
'parentUid' => parent_uid,
|
157
|
+
'folderType' => folder_type
|
158
|
+
}.compact
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# File attachment representation
|
163
|
+
class KeeperFile
|
164
|
+
attr_accessor :uid, :name, :title, :mime_type, :size, :data, :url
|
165
|
+
|
166
|
+
def initialize(attrs = {})
|
167
|
+
@uid = attrs['fileUid'] || attrs['uid'] || attrs[:uid]
|
168
|
+
@name = attrs['name'] || attrs[:name]
|
169
|
+
@title = attrs['title'] || attrs[:title] || @name
|
170
|
+
@mime_type = attrs['mimeType'] || attrs['mime_type'] || attrs[:mime_type]
|
171
|
+
@size = attrs['size'] || attrs[:size]
|
172
|
+
@data = attrs['data'] || attrs[:data]
|
173
|
+
@url = attrs['url'] || attrs[:url]
|
174
|
+
end
|
175
|
+
|
176
|
+
def to_h
|
177
|
+
{
|
178
|
+
'fileUid' => uid,
|
179
|
+
'name' => name,
|
180
|
+
'title' => title,
|
181
|
+
'mimeType' => mime_type,
|
182
|
+
'size' => size
|
183
|
+
}.compact
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Response wrapper
|
188
|
+
class SecretsManagerResponse
|
189
|
+
attr_accessor :records, :folders, :app_data, :warnings, :errors, :just_bound
|
190
|
+
|
191
|
+
def initialize(attrs = {})
|
192
|
+
@records = attrs[:records] || []
|
193
|
+
@folders = attrs[:folders] || []
|
194
|
+
@app_data = attrs[:app_data] || {}
|
195
|
+
@warnings = attrs[:warnings] || []
|
196
|
+
@errors = attrs[:errors] || []
|
197
|
+
@just_bound = attrs[:just_bound] || false
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Query options
|
202
|
+
class QueryOptions
|
203
|
+
attr_accessor :records_filter, :folders_filter
|
204
|
+
|
205
|
+
def initialize(records: nil, folders: nil)
|
206
|
+
@records_filter = records
|
207
|
+
@folders_filter = folders
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# Create options
|
212
|
+
class CreateOptions
|
213
|
+
attr_accessor :folder_uid, :subfolder_uid
|
214
|
+
|
215
|
+
def initialize(folder_uid: nil, subfolder_uid: nil)
|
216
|
+
@folder_uid = folder_uid
|
217
|
+
@subfolder_uid = subfolder_uid
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module KeeperSecretsManager
|
2
|
+
# Base error class for all KSM errors
|
3
|
+
class Error < StandardError; end
|
4
|
+
|
5
|
+
# Configuration errors
|
6
|
+
class ConfigurationError < Error; end
|
7
|
+
|
8
|
+
# Authentication/authorization errors
|
9
|
+
class AuthenticationError < Error; end
|
10
|
+
class AccessDeniedError < AuthenticationError; end
|
11
|
+
|
12
|
+
# API/network errors
|
13
|
+
class NetworkError < Error
|
14
|
+
attr_reader :status_code, :response_body
|
15
|
+
|
16
|
+
def initialize(message, status_code: nil, response_body: nil)
|
17
|
+
super(message)
|
18
|
+
@status_code = status_code
|
19
|
+
@response_body = response_body
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Crypto errors
|
24
|
+
class CryptoError < Error; end
|
25
|
+
class DecryptionError < CryptoError; end
|
26
|
+
class EncryptionError < CryptoError; end
|
27
|
+
|
28
|
+
# Notation errors
|
29
|
+
class NotationError < Error; end
|
30
|
+
|
31
|
+
# Record errors
|
32
|
+
class RecordError < Error; end
|
33
|
+
class RecordNotFoundError < RecordError; end
|
34
|
+
class RecordValidationError < RecordError; end
|
35
|
+
|
36
|
+
# Server errors
|
37
|
+
class ServerError < Error
|
38
|
+
attr_reader :result_code, :message
|
39
|
+
|
40
|
+
def initialize(result_code, message = nil)
|
41
|
+
@result_code = result_code
|
42
|
+
@message = message || "Server error: #{result_code}"
|
43
|
+
super(@message)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Specific server error types
|
48
|
+
class InvalidClientVersionError < ServerError; end
|
49
|
+
class InvalidTokenError < ServerError; end
|
50
|
+
class BadRequestError < ServerError; end
|
51
|
+
class RecordUidNotFoundError < ServerError; end
|
52
|
+
class FolderUidNotFoundError < ServerError; end
|
53
|
+
class AccessViolationError < ServerError; end
|
54
|
+
class ThrottledError < ServerError; end
|
55
|
+
|
56
|
+
# Error factory
|
57
|
+
class ErrorFactory
|
58
|
+
def self.from_server_response(result_code, message = nil)
|
59
|
+
case result_code
|
60
|
+
when 'invalid_client_version'
|
61
|
+
InvalidClientVersionError.new(result_code, message)
|
62
|
+
when 'invalid_client', 'invalid_token'
|
63
|
+
InvalidTokenError.new(result_code, message)
|
64
|
+
when 'bad_request'
|
65
|
+
BadRequestError.new(result_code, message)
|
66
|
+
when 'record_uid_not_found'
|
67
|
+
RecordUidNotFoundError.new(result_code, message)
|
68
|
+
when 'folder_uid_not_found'
|
69
|
+
FolderUidNotFoundError.new(result_code, message)
|
70
|
+
when 'access_violation'
|
71
|
+
AccessViolationError.new(result_code, message)
|
72
|
+
when 'throttled'
|
73
|
+
ThrottledError.new(result_code, message)
|
74
|
+
else
|
75
|
+
ServerError.new(result_code, message)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
module KeeperSecretsManager
|
2
|
+
module FieldTypes
|
3
|
+
# Base field helper
|
4
|
+
class Field
|
5
|
+
attr_accessor :type, :label, :value, :required, :privacy_screen
|
6
|
+
|
7
|
+
def initialize(type:, value:, label: nil, required: false, privacy_screen: false)
|
8
|
+
@type = type
|
9
|
+
@label = label
|
10
|
+
@required = required
|
11
|
+
@privacy_screen = privacy_screen
|
12
|
+
|
13
|
+
# Ensure value is always an array
|
14
|
+
@value = value.is_a?(Array) ? value : [value]
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_h
|
18
|
+
h = { 'type' => type, 'value' => value }
|
19
|
+
h['label'] = label if label
|
20
|
+
h['required'] = required if required
|
21
|
+
h['privacyScreen'] = privacy_screen if privacy_screen
|
22
|
+
h
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Helper methods for creating common fields
|
27
|
+
module Helpers
|
28
|
+
def self.login(value, label: nil)
|
29
|
+
Field.new(type: 'login', value: value, label: label)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.password(value, label: nil)
|
33
|
+
Field.new(type: 'password', value: value, label: label)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.url(value, label: nil)
|
37
|
+
Field.new(type: 'url', value: value, label: label)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.file_ref(value, label: nil)
|
41
|
+
Field.new(type: 'fileRef', value: value, label: label)
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.one_time_code(value, label: nil)
|
45
|
+
Field.new(type: 'oneTimeCode', value: value, label: label)
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.name(first:, last:, middle: nil, label: nil)
|
49
|
+
value = { 'first' => first, 'last' => last }
|
50
|
+
value['middle'] = middle if middle
|
51
|
+
Field.new(type: 'name', value: value, label: label)
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.phone(number:, region: 'US', type: nil, ext: nil, label: nil)
|
55
|
+
value = { 'region' => region, 'number' => number }
|
56
|
+
value['type'] = type if type
|
57
|
+
value['ext'] = ext if ext
|
58
|
+
Field.new(type: 'phone', value: value, label: label)
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.email(email, label: nil)
|
62
|
+
Field.new(type: 'email', value: email, label: label)
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.address(street1:, city:, state:, zip:, country: 'US', street2: nil, label: nil)
|
66
|
+
value = {
|
67
|
+
'street1' => street1,
|
68
|
+
'city' => city,
|
69
|
+
'state' => state,
|
70
|
+
'zip' => zip,
|
71
|
+
'country' => country
|
72
|
+
}
|
73
|
+
value['street2'] = street2 if street2
|
74
|
+
Field.new(type: 'address', value: value, label: label)
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.payment_card(number:, expiration_date:, security_code:, cardholder_name: nil, label: nil)
|
78
|
+
value = {
|
79
|
+
'cardNumber' => number,
|
80
|
+
'cardExpirationDate' => expiration_date,
|
81
|
+
'cardSecurityCode' => security_code
|
82
|
+
}
|
83
|
+
value['cardholderName'] = cardholder_name if cardholder_name
|
84
|
+
Field.new(type: 'paymentCard', value: value, label: label)
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.bank_account(account_type:, routing_number:, account_number:, label: nil)
|
88
|
+
value = {
|
89
|
+
'accountType' => account_type,
|
90
|
+
'routingNumber' => routing_number,
|
91
|
+
'accountNumber' => account_number
|
92
|
+
}
|
93
|
+
Field.new(type: 'bankAccount', value: value, label: label)
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.birth_date(date, label: nil)
|
97
|
+
# Date should be in unix timestamp (milliseconds)
|
98
|
+
timestamp = case date
|
99
|
+
when Date, Time, DateTime
|
100
|
+
(date.to_time.to_f * 1000).to_i
|
101
|
+
when Integer
|
102
|
+
date
|
103
|
+
when String
|
104
|
+
(Date.parse(date).to_time.to_f * 1000).to_i
|
105
|
+
else
|
106
|
+
raise ArgumentError, "Invalid date format"
|
107
|
+
end
|
108
|
+
Field.new(type: 'birthDate', value: timestamp, label: label)
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.secure_note(note, label: nil)
|
112
|
+
Field.new(type: 'secureNote', value: note, label: label)
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.ssh_key(private_key:, public_key: nil, label: nil)
|
116
|
+
value = { 'privateKey' => private_key }
|
117
|
+
value['publicKey'] = public_key if public_key
|
118
|
+
Field.new(type: 'sshKey', value: value, label: label)
|
119
|
+
end
|
120
|
+
|
121
|
+
def self.host(hostname:, port: nil, label: nil)
|
122
|
+
value = { 'hostName' => hostname }
|
123
|
+
value['port'] = port.to_s if port
|
124
|
+
Field.new(type: 'host', value: value, label: label)
|
125
|
+
end
|
126
|
+
|
127
|
+
def self.database_type(type, label: nil)
|
128
|
+
Field.new(type: 'databaseType', value: type, label: label)
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.script(script, label: nil)
|
132
|
+
Field.new(type: 'script', value: script, label: label)
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.passkey(private_key:, credential_id:, rp_id:, user_id:, username:, label: nil)
|
136
|
+
value = {
|
137
|
+
'privateKey' => private_key,
|
138
|
+
'credentialId' => credential_id,
|
139
|
+
'relyingParty' => rp_id,
|
140
|
+
'userId' => user_id,
|
141
|
+
'username' => username
|
142
|
+
}
|
143
|
+
Field.new(type: 'passkey', value: value, label: label)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Generic field for any type
|
147
|
+
def self.custom(type:, value:, label: nil, required: false)
|
148
|
+
Field.new(type: type, value: value, label: label, required: required)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module KeeperSecretsManager
|
2
|
+
class FolderManager
|
3
|
+
def initialize(folders)
|
4
|
+
@folders = folders
|
5
|
+
end
|
6
|
+
|
7
|
+
# Build a hierarchical tree structure from flat folder list
|
8
|
+
def build_folder_tree
|
9
|
+
# Create a hash for quick lookup
|
10
|
+
folder_map = {}
|
11
|
+
@folders.each { |f| folder_map[f.uid] = f }
|
12
|
+
|
13
|
+
# Find root folders (no parent) and build tree
|
14
|
+
root_folders = []
|
15
|
+
@folders.each do |folder|
|
16
|
+
if folder.parent_uid.nil? || folder.parent_uid.empty?
|
17
|
+
root_folders << build_node(folder, folder_map)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
root_folders
|
22
|
+
end
|
23
|
+
|
24
|
+
# Get folder path from root to given folder
|
25
|
+
def get_folder_path(folder_uid)
|
26
|
+
folder = @folders.find { |f| f.uid == folder_uid }
|
27
|
+
return nil unless folder
|
28
|
+
|
29
|
+
path = []
|
30
|
+
current = folder
|
31
|
+
|
32
|
+
# Walk up the tree
|
33
|
+
while current
|
34
|
+
path.unshift(current.name)
|
35
|
+
current = @folders.find { |f| f.uid == current.parent_uid }
|
36
|
+
end
|
37
|
+
|
38
|
+
path.join('/')
|
39
|
+
end
|
40
|
+
|
41
|
+
# Get all ancestors of a folder (parent, grandparent, etc.)
|
42
|
+
def get_ancestors(folder_uid)
|
43
|
+
ancestors = []
|
44
|
+
folder = @folders.find { |f| f.uid == folder_uid }
|
45
|
+
return ancestors unless folder
|
46
|
+
|
47
|
+
current_parent_uid = folder.parent_uid
|
48
|
+
while current_parent_uid && !current_parent_uid.empty?
|
49
|
+
parent = @folders.find { |f| f.uid == current_parent_uid }
|
50
|
+
break unless parent
|
51
|
+
|
52
|
+
ancestors << parent
|
53
|
+
current_parent_uid = parent.parent_uid
|
54
|
+
end
|
55
|
+
|
56
|
+
ancestors
|
57
|
+
end
|
58
|
+
|
59
|
+
# Get all descendants of a folder (children, grandchildren, etc.)
|
60
|
+
def get_descendants(folder_uid)
|
61
|
+
descendants = []
|
62
|
+
children = @folders.select { |f| f.parent_uid == folder_uid }
|
63
|
+
|
64
|
+
children.each do |child|
|
65
|
+
descendants << child
|
66
|
+
descendants.concat(get_descendants(child.uid))
|
67
|
+
end
|
68
|
+
|
69
|
+
descendants
|
70
|
+
end
|
71
|
+
|
72
|
+
# Find folder by name (optionally within a parent)
|
73
|
+
def find_folder_by_name(name, parent_uid: nil)
|
74
|
+
if parent_uid
|
75
|
+
@folders.find { |f| f.name == name && f.parent_uid == parent_uid }
|
76
|
+
else
|
77
|
+
@folders.find { |f| f.name == name }
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Print folder tree to console
|
82
|
+
def print_tree(folders = nil, indent = 0)
|
83
|
+
folders ||= build_folder_tree
|
84
|
+
|
85
|
+
folders.each do |node|
|
86
|
+
puts "#{' ' * indent}├── #{node[:folder].name} (#{node[:folder].uid})"
|
87
|
+
if node[:folder].records && !node[:folder].records.empty?
|
88
|
+
node[:folder].records.each do |record|
|
89
|
+
puts "#{' ' * (indent + 4)}└─ #{record.title} (#{record.uid})"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
print_tree(node[:children], indent + 4) if node[:children]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def build_node(folder, folder_map)
|
99
|
+
node = {
|
100
|
+
folder: folder,
|
101
|
+
children: []
|
102
|
+
}
|
103
|
+
|
104
|
+
# Find children
|
105
|
+
@folders.each do |f|
|
106
|
+
if f.parent_uid == folder.uid
|
107
|
+
node[:children] << build_node(f, folder_map)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
node
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require_relative 'version'
|
2
|
+
|
3
|
+
module KeeperSecretsManager
|
4
|
+
module KeeperGlobals
|
5
|
+
# Client version prefix
|
6
|
+
# **NOTE: 'mb' client version is NOT YET REGISTERED with Keeper servers!**
|
7
|
+
# **TODO: Register 'mb' (Ruby) client with Keeper before production use**
|
8
|
+
# **Currently using 'mr' which is already registered (likely Rust SDK)**
|
9
|
+
CLIENT_VERSION_PREFIX = 'mr'.freeze # Should be 'mb' for Ruby, but using 'mr' temporarily
|
10
|
+
|
11
|
+
# Get client version
|
12
|
+
def self.client_version
|
13
|
+
# Use standard version format matching other SDKs
|
14
|
+
# Java: mj17.0.0, Python: mp16.x.x, JavaScript: ms16.x.x, Go: mg16.x.x
|
15
|
+
# Ruby should be: mb17.0.0 (but not registered yet)
|
16
|
+
"#{CLIENT_VERSION_PREFIX}17.0.0"
|
17
|
+
end
|
18
|
+
|
19
|
+
# Keeper public keys by ID
|
20
|
+
KEEPER_PUBLIC_KEYS = {
|
21
|
+
'1' => 'BK9w6TZFxE6nFNbMfIpULCup2a8xc6w2tUTABjxny7yFmxW0dAEojwC6j6zb5nTlmb1dAx8nwo3qF7RPYGmloRM',
|
22
|
+
'2' => 'BK9w6TZFxE6nFNbMfIpULCup2a8xc6w2tUTABjxny7yFmxW0dAEojwC6j6zb5nTlmb1dAx8nwo3qF7RPYGmloRM',
|
23
|
+
'3' => 'BK9w6TZFxE6nFNbMfIpULCup2a8xc6w2tUTABjxny7yFmxW0dAEojwC6j6zb5nTlmb1dAx8nwo3qF7RPYGmloRM',
|
24
|
+
'4' => 'BK9w6TZFxE6nFNbMfIpULCup2a8xc6w2tUTABjxny7yFmxW0dAEojwC6j6zb5nTlmb1dAx8nwo3qF7RPYGmloRM',
|
25
|
+
'5' => 'BK9w6TZFxE6nFNbMfIpULCup2a8xc6w2tUTABjxny7yFmxW0dAEojwC6j6zb5nTlmb1dAx8nwo3qF7RPYGmloRM',
|
26
|
+
'6' => 'BK9w6TZFxE6nFNbMfIpULCup2a8xc6w2tUTABjxny7yFmxW0dAEojwC6j6zb5nTlmb1dAx8nwo3qF7RPYGmloRM',
|
27
|
+
'7' => 'BK9w6TZFxE6nFNbMfIpULCup2a8xc6w2tUTABjxny7yFmxW0dAEojwC6j6zb5nTlmb1dAx8nwo3qF7RPYGmloRM',
|
28
|
+
'8' => 'BKnhy0obglZJK-igwthNLdknoSXRrGB-mvFRzyb_L-DKKefWjYdFD2888qN1ROczz4n3keYSfKz9Koj90Z6w_tQ',
|
29
|
+
'9' => 'BAsPQdCpLIGXdWNLdAwx-3J5lNqUtKbaOMV56hUj8VzxE2USLHuHHuKDeno0ymJt-acxWV1xPlBfNUShhRTR77g',
|
30
|
+
'10' => 'BNYIh_Sv03nRZUUJveE8d2mxKLIDXv654UbshaItHrCJhd6cT7pdZ_XwbdyxAOCWMkBb9AZ4t1XRCsM8-wkEBRg',
|
31
|
+
'11' => 'BA6uNfeYSvqagwu4TOY6wFK4JyU5C200vJna0lH4PJ-SzGVXej8l9dElyQ58_ljfPs5Rq6zVVXpdDe8A7Y3WRhk',
|
32
|
+
'12' => 'BMjTIlXfohI8TDymsHxo0DqYysCy7yZGJ80WhgOBR4QUd6LBDA6-_318a-jCGW96zxXKMm8clDTKpE8w75KG-FY',
|
33
|
+
'13' => 'BJBDU1P1H21IwIdT2brKkPqbQR0Zl0TIHf7Bz_OO9jaNgIwydMkxt4GpBmkYoprZ_DHUGOrno2faB7pmTR7HhuI',
|
34
|
+
'14' => 'BJFF8j-dH7pDEw_U347w2CBM6xYM8Dk5fPPAktjib-opOqzvvbsER-WDHM4ONCSBf9O_obAHzCyygxmtpktDuiE',
|
35
|
+
'15' => 'BDKyWBvLbyZ-jMueORl3JwJnnEpCiZdN7yUvT0vOyjwpPBCDf6zfL4RWzvSkhAAFnwOni_1tQSl8dfXHbXqXsQ8',
|
36
|
+
'16' => 'BDXyZZnrl0tc2jdC5I61JjwkjK2kr7uet9tZjt8StTiJTAQQmnVOYBgbtP08PWDbecxnHghx3kJ8QXq1XE68y8c',
|
37
|
+
'17' => 'BFX68cb97m9_sweGdOVavFM3j5ot6gveg6xT4BtGahfGhKib-zdZyO9pwvv1cBda9ahkSzo1BQ4NVXp9qRyqVGU'
|
38
|
+
}.freeze
|
39
|
+
|
40
|
+
# Keeper servers by region
|
41
|
+
KEEPER_SERVERS = {
|
42
|
+
'US' => 'keepersecurity.com',
|
43
|
+
'EU' => 'keepersecurity.eu',
|
44
|
+
'AU' => 'keepersecurity.com.au',
|
45
|
+
'GOV' => 'govcloud.keepersecurity.us',
|
46
|
+
'JP' => 'keepersecurity.jp',
|
47
|
+
'CA' => 'keepersecurity.ca'
|
48
|
+
}.freeze
|
49
|
+
|
50
|
+
# Default server (US)
|
51
|
+
DEFAULT_SERVER = KEEPER_SERVERS['US'].freeze
|
52
|
+
|
53
|
+
# Default public key ID
|
54
|
+
DEFAULT_KEY_ID = '7'.freeze
|
55
|
+
|
56
|
+
# Logger name
|
57
|
+
LOGGER_NAME = 'keeper_secrets_manager'.freeze
|
58
|
+
end
|
59
|
+
end
|