ravendb-manager 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/ravendb-manager.rb +348 -0
  3. metadata +44 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 23e22ef63e24ebe6f613b1fb8f7ee1602d84a161
4
+ data.tar.gz: 51c247984693b2bc4c319e7c0bda3b99bdac3d02
5
+ SHA512:
6
+ metadata.gz: a39fcd7615ac584cac75301a627de77a88c15316b4a0d97f1fd9bb5efb9cc2c82685f118f36228fa19d3230029e4ee533f7585855f14b313851aa1d5419fa911
7
+ data.tar.gz: a69fd21bcb89ce9294f24d31630543a02e27c7e30aba318cc93d0e23dec5c51d2fda0ce6c7be103704e1684553afda438501867ed4f203f0597f443f7545e64e
@@ -0,0 +1,348 @@
1
+ require 'net/http'
2
+ require 'digest/sha1'
3
+ require 'base64'
4
+ require 'securerandom'
5
+ require 'json'
6
+
7
+ class RavenDBManager
8
+ @hostname
9
+ @port
10
+ @api_key_name
11
+ @api_key_secret
12
+ @bearer_token
13
+
14
+ @uri
15
+ @oauth_endpoint
16
+
17
+ def self.create(server_url, api_key)
18
+ uri = URI.parse(server_url)
19
+ api_key_parts = api_key.split('/')
20
+
21
+ return RavenDBManager.new(uri.host, uri.port, api_key_parts[0], api_key_parts[1])
22
+ end
23
+
24
+ def initialize(hostname, port, api_key_name, api_key_secret)
25
+ raise 'hostname is empty' unless !hostname.to_s.empty?
26
+ raise 'port is invalid' unless port.to_i != 0
27
+ raise 'api_key_name empty' unless !api_key_name.to_s.empty?
28
+ raise 'api_key_secret empty' unless !api_key_secret.to_s.empty?
29
+
30
+ @hostname = hostname.to_s
31
+ @port = port.to_i
32
+ @api_key_name = api_key_name.to_s
33
+ @api_key_secret = api_key_secret.to_s
34
+
35
+ @uri = "http://#{@hostname}:#{port}"
36
+ @oauth_endpoint = URI.parse("#{@uri}/OAuth/API-Key")
37
+
38
+ @bearer_token = get_bearer_token
39
+ end
40
+
41
+ def bearer_token
42
+ @bearer_token
43
+ end
44
+
45
+ def list_databases
46
+ start = 0
47
+ databases = []
48
+
49
+ results = []
50
+ loop do
51
+ response = http_get("/databases/?pageSize=1024&start=#{start}")
52
+ raise get_error_string(response) unless is_success?(response)
53
+ results = JSON.parse(response.body)
54
+ databases = databases + results
55
+ start += 1024
56
+ break if results.length < 1024
57
+ end
58
+
59
+ return databases
60
+ end
61
+
62
+ def list_filesystems
63
+ start = 0
64
+ filesystems = []
65
+
66
+ results = []
67
+ loop do
68
+ response = http_get("/fs/?pageSize=1024&start=#{start}")
69
+ raise get_error_string(response) unless is_success?(response)
70
+ results = JSON.parse(response.body)
71
+ filesystems = filesystems + results
72
+ start += 1024
73
+ break if results.length < 1024
74
+ end
75
+
76
+ return filesystems
77
+ end
78
+
79
+ def version
80
+ response = http_get('/databases/<system>/build/version')
81
+ raise get_error_string(response) unless is_success?(response)
82
+ return JSON.parse(response.body)
83
+ end
84
+
85
+ def alerts(database = '<system>')
86
+ raise "Invalid database name #{database}" unless is_valid_string?(database)
87
+
88
+ doc = get_document(database, 'Raven/Alerts')
89
+ return doc.nil? ? [] : doc['Alerts']
90
+ end
91
+
92
+ def create_database(name, useVoron = false)
93
+ raise "Invalid database name #{name}" unless is_valid_string?(name)
94
+ raise "Invalid parameter useVoron" unless !!useVoron == useVoron
95
+
96
+ db_document = {
97
+ 'Id' => name,
98
+ 'Settings' => {
99
+ 'Raven/ActiveBundles' => "",
100
+ 'Raven/DataDir' => "~/#{name}",
101
+ 'Raven/StorageTypeName' => useVoron ? 'voron' : 'esent'
102
+ },
103
+ 'SecuredSettings'=> {},
104
+ 'Disabled' => false}
105
+ .to_json
106
+
107
+ response = http_put("/admin/databases/#{name}", db_document)
108
+ raise get_error_string(response) unless is_success?(response)
109
+ return nil
110
+ end
111
+
112
+ def create_filesystem(name)
113
+ raise "Invalid filesystem name #{name}" unless is_valid_string?(name)
114
+
115
+ fs_document = {
116
+ 'Id' => name,
117
+ 'Settings' => {
118
+ 'Raven/ActiveBundles' => "",
119
+ 'Raven/FileSystem/DataDir' => "~/#{name}"
120
+ },
121
+ 'SecuredSettings'=> {},
122
+ 'Disabled' => false}
123
+ .to_json
124
+
125
+ response = http_put("/admin/fs/#{name}", fs_document)
126
+ raise get_error_string(response) unless is_success?(response)
127
+ return nil
128
+ end
129
+
130
+ def get_document(database, key)
131
+ raise "Invalid database name #{database}" unless is_valid_string?(database)
132
+ raise "Invalid key #{key}" unless key.to_s.length > 0
133
+
134
+ response = http_get("/databases/#{database}/docs?id=#{key}")
135
+ if is_success?(response)
136
+ return JSON.parse(response.body)
137
+ elsif response.code == '404'
138
+ return nil
139
+ else
140
+ raise get_error_string(response)
141
+ end
142
+ end
143
+
144
+ def put_document(database, key, document)
145
+ raise "Invalid database name #{database}" unless is_valid_string?(database)
146
+ raise "Invalid key #{key}" unless key.to_s.length > 0
147
+ raise "Invalid document" if document.nil?
148
+
149
+ response = http_put("/databases/#{database}/docs/#{key}", document.to_json)
150
+ raise get_error_string(response) unless is_success?(response)
151
+ return JSON.parse(response.body)['Key']
152
+ end
153
+
154
+ def delete_document(database, key)
155
+ raise "Invalid database name #{database}" unless is_valid_string?(database)
156
+ raise "Invalid key #{key}" unless key.to_s.length > 0
157
+
158
+ response = http_delete("/databases/#{database}/docs/#{key}")
159
+ raise get_error_string(response) unless is_success?(response)
160
+ return nil
161
+ end
162
+
163
+ def get_indexing_status(database)
164
+ raise "Invalid database name #{database}" unless is_valid_string?(database)
165
+
166
+ response = http_get("/databases/#{database}/admin/IndexingStatus")
167
+ raise get_error_string(response) unless is_success?(response)
168
+ return JSON.parse(response.body)['IndexingStatus']
169
+ end
170
+
171
+ def compact_database(database)
172
+ raise "Invalid database name #{database}" unless is_valid_string?(database)
173
+
174
+ response = http_post("/admin/compact?database=#{database}")
175
+ raise get_error_string(response) unless is_success?(response)
176
+ return JSON.parse(response.body)['OperationId']
177
+ end
178
+
179
+ def get_operation_status(operation_id)
180
+ raise "Invalid OperationId #{operation_id}" if operation_id.nil?
181
+
182
+ response = http_get("/operation/status?id=#{operation_id}")
183
+ return is_success?(response) ? JSON.parse(response.body) : nil
184
+ end
185
+
186
+ def database_statistics(database)
187
+ raise "Invalid database name #{database}" unless is_valid_string?(database)
188
+
189
+ response = http_get("/databases/#{database}/stats")
190
+ raise get_error_string(response) unless is_success?(response)
191
+ return JSON.parse(response.body)
192
+ end
193
+
194
+ def server_statistics
195
+ response = http_get("/admin/stats")
196
+ raise get_error_string(response) unless is_success?(response)
197
+ return JSON.parse(response.body)
198
+ end
199
+
200
+ # Backup ...
201
+
202
+ def create_api_key(name)
203
+ raise "Invalid ApiKey name #{name}" unless is_valid_string?(name)
204
+
205
+ document = {
206
+ 'Enabled' => true,
207
+ 'Name' => name,
208
+ 'Secret' => generate_secret,
209
+ 'Databases': []
210
+ }
211
+ key = "Raven/ApiKeys/#{name}"
212
+ put_document('<system>', key, document)
213
+ return "#{name}/#{document['Secret']}"
214
+ end
215
+
216
+ def add_db_to_api_key(api_key_name, db_name, admin = false, readonly = false)
217
+ raise "Invalid ApiKey name #{api_key_name}" unless is_valid_string?(api_key_name)
218
+ raise "Invalid database name #{db_name}" unless is_valid_string?(db_name)
219
+ raise "Invalid parameter admin" unless !!admin == admin
220
+ raise "Invalid parameter readonly" unless !!readonly == readonly
221
+
222
+ key = "Raven/ApiKeys/#{api_key_name}"
223
+ document = get_document('<system>', key)
224
+ raise "API-Key #{api_key_name} doesn't exist" if document.nil?
225
+ document['Databases'] = document['Databases'].select {|db| db['TenantId'] != db_name}
226
+ document['Databases'].push({'TenantId' => db_name, 'admin' => admin, 'readonly' => readonly})
227
+ put_document('<system>', key, document)
228
+ return nil
229
+ end
230
+
231
+ private
232
+
233
+ PASSWORD_CHARS = [*('a'..'z'),*('A'..'Z'),*(0..9)]
234
+ NAME_PATTERN = /^[a-zA-Z0-9.-<>]+$/
235
+
236
+ def generate_secret
237
+ return (16 + SecureRandom.random_number(4)).times.map { PASSWORD_CHARS[SecureRandom.random_number(PASSWORD_CHARS.length)] }.join
238
+ end
239
+
240
+ def is_valid_string?(str)
241
+ return !NAME_PATTERN.match(str).nil?
242
+ end
243
+
244
+ def is_success?(http_response)
245
+ code = http_response.code.to_i
246
+ return code >= 200 && code <= 299
247
+ end
248
+
249
+ def get_error_string(http_error_response)
250
+ if http_error_response.methods.include?(:body) and !http_error_response.body.to_s.empty?
251
+ return http_error_response.body
252
+ else
253
+ return "#{http_error_response.code} #{http_error_response.message}"
254
+ end
255
+ end
256
+
257
+ def http_get(url)
258
+ http = Net::HTTP.new(@hostname, @port)
259
+ request = Net::HTTP::Get.new(URI.escape(url))
260
+ request['Authorization'] = "Bearer #{@bearer_token}"
261
+ return http.request(request)
262
+ end
263
+
264
+ def http_delete(url)
265
+ http = Net::HTTP.new(@hostname, @port)
266
+ request = Net::HTTP::Delete.new(URI.escape(url))
267
+ request['Authorization'] = "Bearer #{@bearer_token}"
268
+ return http.request(request)
269
+ end
270
+
271
+ def http_put(url, document)
272
+ http = Net::HTTP.new(@hostname, @port)
273
+ request = Net::HTTP::Put.new(URI.escape(url))
274
+ request['Authorization'] = "Bearer #{@bearer_token}"
275
+ request.body = document
276
+ return http.request(request)
277
+ end
278
+
279
+ def http_post(url, document = nil)
280
+ http = Net::HTTP.new(@hostname, @port)
281
+ request = Net::HTTP::Post.new(URI.escape(url))
282
+ request['Authorization'] = "Bearer #{@bearer_token}"
283
+ request.body = document unless document.nil?
284
+ return http.request(request)
285
+ end
286
+
287
+ def get_bearer_token
288
+ request = get_auth_request_hash
289
+ response = prepare_response(request['challenge'])
290
+ public_key = create_rsa_key(request['modulus'], request['exponent'])
291
+
292
+ data = "api key name=#{@api_key_name},challenge=#{request['challenge']},response=#{response}"
293
+ response_data = "exponent=#{request['exponent']},modulus=#{request['modulus']},data=#{encrypt(public_key, data)}"
294
+ get_auth_token(response_data)
295
+ end
296
+
297
+ def get_auth_request_hash
298
+ http = Net::HTTP.new(@server_url, 8088)
299
+ res = Net::HTTP.post_form(@oauth_endpoint, {})
300
+
301
+ wwwauth_header = res.header['www-authenticate']
302
+ wwwauth_header[7, wwwauth_header.length - 7] # Remove 'Raven '
303
+ .split(',')
304
+ .map{|str|
305
+ idx = str.index '='
306
+ [str[0, idx].strip.downcase, str[idx+1, str.length]]
307
+ }
308
+ .to_h
309
+ end
310
+
311
+ def get_auth_token (response_data)
312
+ req = Net::HTTP::Post.new(@oauth_endpoint.request_uri)
313
+ req.add_field('grant-type', 'client_credentials')
314
+ req.body = response_data
315
+
316
+ res = Net::HTTP.new(@oauth_endpoint.host, @oauth_endpoint.port).request(req)
317
+ res.body.to_s
318
+ end
319
+
320
+ def prepare_response (challenge)
321
+ input = challenge + ";" + @api_key_secret
322
+ Digest::SHA1.base64digest input
323
+ end
324
+
325
+ def create_rsa_key (modulus_base64, exponent_base64)
326
+ key = OpenSSL::PKey::RSA.new
327
+ exponent = OpenSSL::BN.new(Base64.decode64(exponent_base64).unpack("H*").first, 16)
328
+ modulus = OpenSSL::BN.new(Base64.decode64(modulus_base64).unpack("H*").first, 16)
329
+ key.e = exponent
330
+ key.n = modulus
331
+ key
332
+ end
333
+
334
+ def encrypt(public_key, data)
335
+ key = SecureRandom.random_bytes(32)
336
+ iv = SecureRandom.random_bytes(16)
337
+
338
+ key_and_iv_encrypted = public_key.public_encrypt(key + iv, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
339
+
340
+ cipher = OpenSSL::Cipher::AES256.new(:CBC)
341
+ cipher.encrypt
342
+ cipher.key = key
343
+ cipher.iv = iv
344
+ encrypted = cipher.update(data) + cipher.final
345
+
346
+ Base64.encode64 (key_and_iv_encrypted + encrypted)
347
+ end
348
+ end
metadata ADDED
@@ -0,0 +1,44 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ravendb-manager
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Christoph Enzmann
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-08-31 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Allows to manage and monitor your RavenDB Document Database
14
+ email: christoph.enzmann@oakten.ch
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/ravendb-manager.rb
20
+ homepage: https://github.com/oakten-software-engineering/ravendb-manager
21
+ licenses:
22
+ - GPL-3.0
23
+ metadata: {}
24
+ post_install_message:
25
+ rdoc_options: []
26
+ require_paths:
27
+ - lib
28
+ required_ruby_version: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ required_rubygems_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ requirements: []
39
+ rubyforge_project:
40
+ rubygems_version: 2.6.7
41
+ signing_key:
42
+ specification_version: 4
43
+ summary: Manage your RavenDB Document Database
44
+ test_files: []