kstor 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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'