kstor 0.4.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.
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kstor/log'
4
+ require 'kstor/systemd'
5
+
6
+ require 'socket'
7
+ require 'timeout'
8
+
9
+ module KStor
10
+ # Serve clients on UNIX sockets.
11
+ class SocketServer
12
+ GRACEFUL_TIMEOUT = 10
13
+
14
+ def initialize(socket_path:, nworkers:)
15
+ @path = socket_path
16
+ @nworkers = nworkers
17
+ @client_queue = Queue.new
18
+ @workers = []
19
+ end
20
+
21
+ def start
22
+ start_workers
23
+ server = Systemd.socket
24
+ Systemd.service_ready
25
+ loop do
26
+ maintain_workers
27
+ @client_queue.enq(server.accept.first)
28
+ end
29
+ rescue Interrupt
30
+ stop(server)
31
+ Log.info('socket_server: stopped.')
32
+ end
33
+
34
+ def work(client)
35
+ # Abstract method.
36
+ client.close
37
+ end
38
+
39
+ private
40
+
41
+ def worker_run
42
+ while (client = @client_queue.deq)
43
+ Log.debug("socket_server: #{Thread.current.name} serving one client")
44
+ work(client)
45
+ Log.debug("socket_server: #{Thread.current.name} done serving client")
46
+ end
47
+ end
48
+
49
+ def stop(server)
50
+ Systemd.service_stopping
51
+ Log.debug('socket_server: closing UNIXServer')
52
+ server.close
53
+ Log.debug('socket_server: closing client queue')
54
+ @client_queue.close
55
+ Log.debug("socket_server: waiting #{GRACEFUL_TIMEOUT} seconds " \
56
+ 'for workers to finish')
57
+ Timeout.timeout(GRACEFUL_TIMEOUT) { @workers.each(&:join) }
58
+ rescue Timeout::Error
59
+ Log.warn('socket_server: graceful timeout reached, killing workers')
60
+ immediate_stop(server)
61
+ end
62
+
63
+ def immediate_stop
64
+ @workers.each { |w| w.raise(Interrupt.new('abort')) }
65
+ @workers.each(&:join)
66
+ end
67
+
68
+ def start_workers
69
+ @nworkers.times do |i|
70
+ @workers << start_worker("worker-#{i}")
71
+ Log.debug("socket_server: started #{@workers.last.name}")
72
+ end
73
+ end
74
+
75
+ def start_worker(name)
76
+ thr = Thread.new { worker_run }
77
+ thr.name = name
78
+
79
+ thr
80
+ end
81
+
82
+ def maintain_workers
83
+ collect_dead_workers.each do |i, w|
84
+ name = w.name
85
+ Log.error("socket_server: #{name} died!")
86
+ rescue_worker_exception(w)
87
+ Log.info("socket_server: performing resurrection on #{name}")
88
+ @workers[i] = start_worker(name)
89
+ Log.debug("socket_server: welcome back, comrade #{name}")
90
+ end
91
+ end
92
+
93
+ def collect_dead_workers
94
+ deads = {}
95
+ @workers.each_with_index do |w, i|
96
+ next if %w[sleep run].include?(w.status)
97
+
98
+ Log.debug("socket_server: #{w.name} status is #{w.status.inspect}")
99
+ deads[i] = w
100
+ end
101
+
102
+ deads
103
+ end
104
+
105
+ # rubocop:disable Lint/RescueException
106
+ def rescue_worker_exception(worker)
107
+ worker.join
108
+ rescue Exception => e
109
+ Log.exception(e)
110
+ end
111
+ # rubocop:enable Lint/RescueException
112
+ end
113
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kstor/log'
4
+ require 'kstor/error'
5
+
6
+ require 'sqlite3'
7
+
8
+ module KStor
9
+ # Error: can't open database file.
10
+ class CantOpenDatabase < Error
11
+ error_code 'SQL/CANTOPEN'
12
+ error_message "Can't open database file at %s"
13
+ end
14
+
15
+ # Execute SQL commands in a per-thread SQLite connection.
16
+ class SQLConnection
17
+ def initialize(file_path)
18
+ @file_path = file_path
19
+ end
20
+
21
+ def execute(sql, *params, &)
22
+ database.execute(sql, *params, &)
23
+ end
24
+
25
+ def last_insert_row_id
26
+ database.last_insert_row_id
27
+ end
28
+
29
+ def transaction(&block)
30
+ result = nil
31
+ database.transaction do |db|
32
+ result = block.call(db)
33
+ end
34
+
35
+ result
36
+ end
37
+
38
+ private
39
+
40
+ def database
41
+ key = :kstor_db_connection
42
+ setup_thread_connection(key)
43
+ db = Thread.current[key]
44
+ return db unless db.closed?
45
+
46
+ Log.warn('sqlite: bad connection, will re-connect')
47
+ db.close
48
+ Thread.current[k] = nil
49
+ setup_thread_connection(key)
50
+ end
51
+
52
+ def setup_thread_connection(key)
53
+ return if Thread.current[key]
54
+
55
+ Log.info(
56
+ "sqlite: initializing connection in thread #{Thread.current.name}"
57
+ )
58
+ Thread.current[key] = connect(@file_path)
59
+ Log.debug("sqlite: opened #{@file_path}")
60
+
61
+ Thread.current[key]
62
+ end
63
+
64
+ def connect(file_path)
65
+ db = SQLite3::Database.new(file_path)
66
+ db.results_as_hash = true
67
+ db.type_translation = SQLite3::Translator.new
68
+
69
+ db
70
+ rescue SQLite3::CantOpenException
71
+ raise Error.for_code('SQL/CANTOPEN', file_path)
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,383 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kstor/sql_connection'
4
+ require 'kstor/model'
5
+ require 'kstor/log'
6
+
7
+ module KStor
8
+ # Store and fetch objects in an SQLite database.
9
+ # rubocop:disable Metrics/MethodLength
10
+ class Store
11
+ def initialize(file_path)
12
+ @file_path = file_path
13
+ @db = SQLConnection.new(file_path)
14
+ @cache = {}
15
+ end
16
+
17
+ def transaction(&)
18
+ @db.transaction(&)
19
+ end
20
+
21
+ def users?
22
+ rows = @db.execute('SELECT count(*) AS n FROM users')
23
+ count = Integer(rows.first['n'])
24
+ Log.debug("store: count of users is #{count}")
25
+
26
+ count.positive?
27
+ end
28
+
29
+ def user_create(user)
30
+ @db.execute(<<-EOSQL, user.login, user.name, 'new')
31
+ INSERT INTO users (login, name, status)
32
+ VALUES (?, ?, ?)
33
+ EOSQL
34
+ user.id = @db.last_insert_row_id
35
+ Log.debug("store: stored new user #{user.login}")
36
+ params = [user.kdf_params, user.pubk, user.encrypted_privk].map(&:to_s)
37
+ return user if params.any?(&:nil?)
38
+
39
+ @db.execute(<<-EOSQL, user.id, *params)
40
+ INSERT INTO users_crypto_data (user_id, kdf_params, pubk, encrypted_privk)
41
+ VALUES (?, ?, ?, ?)
42
+ EOSQL
43
+ Log.debug("store: stored user crypto data for #{user.login}")
44
+
45
+ user
46
+ end
47
+
48
+ def user_update(user)
49
+ @db.execute(<<-EOSQL, user.name, user.status, user.id)
50
+ UPDATE users SET name = ?, status = ?
51
+ WHERE id = ?
52
+ EOSQL
53
+ params = [user.kdf_params, user.pubk, user.encrypted_privk, user.id]
54
+ @db.execute(<<-EOSQL, *params)
55
+ UPDATE users_crypto_data SET
56
+ kdf_params = ?,
57
+ pubk = ?
58
+ encrypted_params = ?
59
+ WHERE user_id = ?
60
+ EOSQL
61
+ end
62
+
63
+ def keychain_item_create(user_id, group_id, encrypted_privk)
64
+ @db.execute(<<-EOSQL, user_id, group_id, encrypted_privk.to_s)
65
+ INSERT INTO group_members (user_id, group_id, encrypted_privk)
66
+ VALUES (?, ?, ?)
67
+ EOSQL
68
+ end
69
+
70
+ def group_create(name, pubk)
71
+ @db.execute(<<-EOSQL, name, pubk.to_s)
72
+ INSERT INTO groups (name, pubk)
73
+ VALUES (?, ?)
74
+ EOSQL
75
+ @db.last_insert_row_id
76
+ end
77
+
78
+ def groups
79
+ return @cache[:groups] if @cache.key?(:groups)
80
+
81
+ Log.debug('store: loading groups')
82
+ rows = @db.execute(<<-EOSQL)
83
+ SELECT id,
84
+ name,
85
+ pubk
86
+ FROM groups
87
+ ORDER BY name
88
+ EOSQL
89
+ @cache[:groups] = rows.to_h do |r|
90
+ a = []
91
+ a << r['id']
92
+ a << Model::Group.new(
93
+ id: r['id'], name: r['name'], pubk: Crypto::PublicKey.new(r['pubk'])
94
+ )
95
+ a
96
+ end
97
+ end
98
+
99
+ def users
100
+ return @cache[:users] if @cache.key?(:users)
101
+
102
+ Log.debug('store: loading users')
103
+ rows = @db.execute(<<-EOSQL)
104
+ SELECT u.id,
105
+ u.login,
106
+ u.name,
107
+ u.status,
108
+ c.pubk
109
+ FROM users u
110
+ LEFT JOIN users_crypto_data c ON (c.user_id = u.id)
111
+ ORDER BY u.login
112
+ EOSQL
113
+
114
+ @cache[:users] = users_from_resultset(rows)
115
+ end
116
+
117
+ # in: login
118
+ # out:
119
+ # - ID
120
+ # - name
121
+ # - status
122
+ # - public key
123
+ # - key derivation function parameters
124
+ # - encrypted private key
125
+ # - keychain: hash of:
126
+ # - group ID
127
+ # - encrypted group private key
128
+ def user_by_login(login)
129
+ Log.debug("store: loading user by login #{login.inspect}")
130
+ rows = @db.execute(<<-EOSQL, login)
131
+ SELECT u.id,
132
+ u.login,
133
+ u.name,
134
+ u.status,
135
+ c.kdf_params,
136
+ c.pubk,
137
+ c.encrypted_privk
138
+ FROM users u
139
+ LEFT JOIN users_crypto_data c ON (c.user_id = u.id)
140
+ WHERE u.login = ?
141
+ EOSQL
142
+ user_from_resultset(rows, include_crypto_data: true)
143
+ end
144
+
145
+ # in: user ID
146
+ # out:
147
+ # - ID
148
+ # - name
149
+ # - status
150
+ # - public key
151
+ # - key derivation function parameters
152
+ # - encrypted private key
153
+ def user_by_id(user_id)
154
+ Log.debug("store: loading user by ID ##{user_id}")
155
+ rows = @db.execute(<<-EOSQL, user_id)
156
+ SELECT u.id,
157
+ u.login,
158
+ u.name,
159
+ u.status,
160
+ c.kdf_params,
161
+ c.pubk,
162
+ c.encrypted_privk,
163
+ FROM users u
164
+ LEFT JOIN users_crypto_data c ON (c.user_id = u.id)
165
+ WHERE u.id = ?
166
+ EOSQL
167
+ user_from_resultset(rows, include_crypto_data: true)
168
+ end
169
+
170
+ # in: user ID
171
+ # out: array of:
172
+ # - secret ID
173
+ # - group ID common between user and secret
174
+ # - secret encrypted metadata
175
+ # - secret value and metadata author IDs
176
+ def secrets_for_user(user_id)
177
+ Log.debug("store: loading secrets for user ##{user_id}")
178
+ rows = @db.execute(<<-EOSQL, user_id)
179
+ SELECT s.id,
180
+ s.value_author_id,
181
+ s.meta_author_id,
182
+ sv.group_id,
183
+ sv.ciphertext,
184
+ sv.encrypted_metadata
185
+ FROM secrets s,
186
+ secret_values sv,
187
+ group_members gm
188
+ WHERE gm.user_id = ?
189
+ AND gm.group_id = sv.group_id
190
+ AND sv.secret_id = s.id
191
+ GROUP BY s.id
192
+ ORDER BY s.id, sv.group_id
193
+ EOSQL
194
+
195
+ rows.map { |r| secret_from_row(r) }
196
+ end
197
+
198
+ # in: secret ID, user ID
199
+ # out: encrypted value
200
+ def secret_fetch(secret_id, user_id)
201
+ Log.debug(
202
+ "store: loading secret value ##{secret_id} for user ##{user_id}"
203
+ )
204
+ rows = @db.execute(<<-EOSQL, user_id, secret_id)
205
+ SELECT s.id,
206
+ s.value_author_id,
207
+ s.meta_author_id,
208
+ sv.group_id,
209
+ sv.ciphertext,
210
+ sv.encrypted_metadata
211
+ FROM secrets s,
212
+ secret_values sv,
213
+ group_members gm
214
+ WHERE gm.user_id = ?
215
+ AND gm.group_id = sv.group_id
216
+ AND sv.secret_id = ?
217
+ AND s.id = sv.secret_id
218
+ EOSQL
219
+ return nil if rows.empty?
220
+
221
+ secret_from_row(rows.first)
222
+ end
223
+
224
+ # in:
225
+ # - user ID
226
+ # - hash of:
227
+ # - group ID
228
+ # - array of:
229
+ # - ciphertext
230
+ # - encrypted metadata
231
+ # out: secret ID
232
+ def secret_create(author_id, encrypted_data)
233
+ Log.debug("store: creating secret for user #{author_id}")
234
+ @db.execute(<<-EOSQL, author_id, author_id)
235
+ INSERT INTO secrets (value_author_id, meta_author_id) VALUES (?, ?)
236
+ EOSQL
237
+ secret_id = @db.last_insert_row_id
238
+ encrypted_data.each do |group_id, (ciphertext, encrypted_metadata)|
239
+ secret_value_create(secret_id, group_id, ciphertext, encrypted_metadata)
240
+ end
241
+
242
+ secret_id
243
+ end
244
+
245
+ def groups_for_secret(secret_id)
246
+ Log.debug("store: loading group IDs for secret #{secret_id}")
247
+ rows = @db.execute(<<-EOSQL, secret_id)
248
+ SELECT group_id
249
+ FROM secret_values
250
+ WHERE secret_id = ?
251
+ EOSQL
252
+ rows.map { |r| r['group_id'] }
253
+ end
254
+
255
+ # in: secret ID, author ID, array of [group ID, encrypted_metadata]
256
+ # out: nil
257
+ def secret_setmeta(secret_id, user_id, group_encrypted_metadata)
258
+ Log.debug("store: set metadata for secret ##{secret_id}")
259
+ @db.execute(<<-EOSQL, user_id, secret_id)
260
+ UPDATE secrets SET meta_author_id = ? WHERE id = ?
261
+ EOSQL
262
+ group_encrypted_metadata.each do |group_id, encrypted_metadata|
263
+ @db.execute(<<-EOSQL, encrypted_metadata.to_s, secret_id, group_id)
264
+ UPDATE secret_values
265
+ SET encrypted_metadata = ?
266
+ WHERE secret_id = ?
267
+ AND group_id = ?
268
+ EOSQL
269
+ end
270
+ end
271
+
272
+ def secret_setvalue(secret_id, user_id, group_ciphertexts)
273
+ Log.debug("store: set value for secret ##{secret_id}")
274
+ @db.execute(<<-EOSQL, user_id, secret_id)
275
+ UPDATE secrets SET value_author_id = ? WHERE id = ?
276
+ EOSQL
277
+ group_ciphertexts.each do |group_id, ciphertext|
278
+ @db.execute(<<-EOSQL, ciphertext.to_s, secret_id, group_id)
279
+ UPDATE secret_values
280
+ SET ciphertext = ?
281
+ WHERE secret_id = ?
282
+ AND group_id = ?
283
+ EOSQL
284
+ end
285
+ end
286
+
287
+ def secret_delete(secret_id)
288
+ Log.debug("store: delete secret ##{secret_id}")
289
+ # Will cascade to secret_values:
290
+ @db.execute(<<-EOSQL, secret_id)
291
+ DELETE FROM secrets WHERE id = ?
292
+ EOSQL
293
+ end
294
+
295
+ private
296
+
297
+ # in: secret ID, group ID, encrypted metadata and value
298
+ # out: nil
299
+ def secret_value_create(secret_id, group_id, ciphertext, encrypted_metadata)
300
+ params = [ciphertext.to_s, encrypted_metadata.to_s]
301
+ @db.execute(<<-EOSQL, secret_id, group_id, *params)
302
+ INSERT INTO secret_values (
303
+ secret_id, group_id,
304
+ ciphertext, encrypted_metadata
305
+ ) VALUES (
306
+ ?, ?,
307
+ ?, ?
308
+ )
309
+ EOSQL
310
+ end
311
+
312
+ def users_from_resultset(rows)
313
+ users = []
314
+ while (u = user_from_resultset(rows, include_crypto_data: false))
315
+ users << u
316
+ end
317
+
318
+ users.to_h { |uu| [uu.id, uu] }
319
+ end
320
+
321
+ def user_from_resultset(rows, include_crypto_data: true)
322
+ return nil if rows.empty?
323
+
324
+ row = rows.shift
325
+ user_data = {
326
+ id: row['id'],
327
+ login: row['login'],
328
+ name: row['name'],
329
+ status: row['status'],
330
+ pubk: Crypto::PublicKey.new(row['pubk'])
331
+ }
332
+ include_crypto_data && user_crypto_data_from_resultset(user_data, row)
333
+ Model::User.new(**user_data)
334
+ end
335
+
336
+ def user_crypto_data_from_resultset(user_data, row)
337
+ user_data.merge!(
338
+ kdf_params: Crypto::KDFParams.new(row['kdf_params']),
339
+ encrypted_privk: Crypto::ArmoredValue.new(row['encrypted_privk']),
340
+ keychain: keychain_fetch(row['id'])
341
+ )
342
+ end
343
+
344
+ # in: user ID
345
+ # out: hash of:
346
+ # - group ID
347
+ # - encrypted_private key
348
+ def keychain_fetch(user_id)
349
+ rows = @db.execute(<<-EOSQL, user_id)
350
+ SELECT g.id,
351
+ g.pubk,
352
+ gm.encrypted_privk
353
+ FROM groups g,
354
+ group_members gm
355
+ WHERE gm.user_id = ?
356
+ AND gm.group_id = g.id
357
+ ORDER BY g.name
358
+ EOSQL
359
+ rows.to_h do |r|
360
+ [
361
+ r['id'],
362
+ Model::KeychainItem.new(
363
+ group_id: r['id'],
364
+ group_pubk: Crypto::PublicKey.new(r['pubk']),
365
+ encrypted_privk: Crypto::ArmoredValue.new(r['encrypted_privk'])
366
+ )
367
+ ]
368
+ end
369
+ end
370
+
371
+ def secret_from_row(row)
372
+ Model::Secret.new(
373
+ id: row['id'],
374
+ value_author_id: row['value_author_id'],
375
+ meta_author_id: row['meta_author_id'],
376
+ group_id: row['group_id'],
377
+ ciphertext: Crypto::ArmoredValue.new(row['ciphertext']),
378
+ encrypted_metadata: Crypto::ArmoredValue.new(row['encrypted_metadata'])
379
+ )
380
+ end
381
+ end
382
+ # rubocop:enable Metrics/MethodLength
383
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sd_notify'
4
+
5
+ module KStor
6
+ # Collection of methods for systemd integration.
7
+ module Systemd
8
+ class << self
9
+ def socket
10
+ listen_pid = ENV['LISTEN_PID'].to_i
11
+ return nil unless Process.pid == listen_pid
12
+
13
+ Socket.for_fd(3)
14
+ end
15
+
16
+ def service_ready
17
+ SdNotify.ready
18
+ end
19
+
20
+ def service_stopping
21
+ SdNotify.stopping
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KStor
4
+ VERSION = '0.4.0'
5
+ end
data/lib/kstor.rb ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kstor/config'
4
+ require 'kstor/store'
5
+ require 'kstor/controller'
6
+ require 'kstor/server'
7
+ require 'kstor/log'
8
+ require 'kstor/session'
9
+ require 'kstor/message'
10
+ require 'kstor/version'