kstor 0.4.3 → 0.5.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -2
  3. data/bin/kstor +48 -40
  4. data/bin/kstor-srv +2 -2
  5. data/lib/kstor/config.rb +2 -1
  6. data/lib/kstor/controller/authentication.rb +25 -4
  7. data/lib/kstor/controller/base.rb +61 -0
  8. data/lib/kstor/controller/request_handler.rb +84 -16
  9. data/lib/kstor/controller/secret.rb +63 -64
  10. data/lib/kstor/controller/users.rb +11 -22
  11. data/lib/kstor/controller.rb +3 -3
  12. data/lib/kstor/crypto/armored_value.rb +82 -0
  13. data/lib/kstor/crypto/keys.rb +9 -41
  14. data/lib/kstor/crypto.rb +0 -1
  15. data/lib/kstor/error.rb +36 -3
  16. data/lib/kstor/log/simple_logger.rb +83 -0
  17. data/lib/kstor/log/systemd_logger.rb +22 -0
  18. data/lib/kstor/log.rb +53 -4
  19. data/lib/kstor/message/base.rb +223 -0
  20. data/lib/kstor/message/error.rb +15 -0
  21. data/lib/kstor/message/group_create.rb +14 -0
  22. data/lib/kstor/message/group_created.rb +16 -0
  23. data/lib/kstor/message/ping.rb +14 -0
  24. data/lib/kstor/message/pong.rb +14 -0
  25. data/lib/kstor/message/secret_create.rb +16 -0
  26. data/lib/kstor/message/secret_created.rb +14 -0
  27. data/lib/kstor/message/secret_delete.rb +14 -0
  28. data/lib/kstor/message/secret_deleted.rb +14 -0
  29. data/lib/kstor/message/secret_list.rb +14 -0
  30. data/lib/kstor/message/secret_search.rb +14 -0
  31. data/lib/kstor/message/secret_unlock.rb +14 -0
  32. data/lib/kstor/message/secret_update_meta.rb +15 -0
  33. data/lib/kstor/message/secret_update_value.rb +15 -0
  34. data/lib/kstor/message/secret_updated.rb +14 -0
  35. data/lib/kstor/message/secret_value.rb +35 -0
  36. data/lib/kstor/message.rb +26 -113
  37. data/lib/kstor/model.rb +42 -25
  38. data/lib/kstor/server.rb +15 -3
  39. data/lib/kstor/session.rb +34 -2
  40. data/lib/kstor/socket_server.rb +20 -1
  41. data/lib/kstor/sql_connection.rb +16 -1
  42. data/lib/kstor/store.rb +182 -72
  43. data/lib/kstor/systemd.rb +5 -0
  44. data/lib/kstor/version.rb +2 -1
  45. data/lib/kstor.rb +1 -1
  46. metadata +25 -3
data/lib/kstor/message.rb CHANGED
@@ -3,130 +3,43 @@
3
3
  require 'json'
4
4
 
5
5
  module KStor
6
- # Internal exception when a request is received with neither a session ID nor
7
- # a login/password pair.
8
- #
9
- # We can't use a KStor::Error here because kstor/error.rb require()s
10
- # kstor/message.rb .
11
- class RequestMissesAuthData < RuntimeError
12
- end
13
-
14
- class UnparsableResponse < RuntimeError
15
- end
16
-
17
- # A user request or response.
18
- class Message
19
- attr_reader :type
20
- attr_reader :args
21
-
22
- def initialize(type, args)
23
- @type = type
24
- @args = args
25
- end
26
-
27
- def to_h
28
- { 'type' => @type, 'args' => @args }
29
- end
30
-
31
- def serialize
32
- to_h.to_json
33
- end
34
-
35
- # Parse a user request, freshly arrived from the network.
6
+ module Message
7
+ # Internal exception when a request is received with neither a session ID
8
+ # nor a login/password pair.
36
9
  #
37
- # @param str [String] serialized request
38
- # @return [LoginRequest,SessionRequest] a request
39
- # @raise [KStor::RequestMissesAuthData]
40
- def self.parse_request(str)
41
- data = JSON.parse(str)
42
- if data.key?('login') && data.key?('password')
43
- LoginRequest.new(
44
- data['login'], data['password'],
45
- data['type'], data['args']
46
- )
47
- elsif data.key?('session_id')
48
- SessionRequest.new(
49
- data['session_id'], data['type'], data['args']
50
- )
51
- else
52
- raise RequestMissesAuthData
53
- end
54
- end
55
- end
56
-
57
- # A user authentication request.
58
- class LoginRequest < Message
59
- attr_reader :login
60
- attr_reader :password
61
-
62
- def initialize(login, password, type, args)
63
- @login = login
64
- @password = password
65
- super(type, args)
66
- end
67
-
68
- def inspect
69
- fmt = [
70
- '#<KStor::LoginRequest:%<id>x',
71
- '@login=%<login>s',
72
- '@password="******"',
73
- '@args=%<args>s>'
74
- ].join(' ')
75
- format(fmt, id: object_id, login: @login.inspect, args: @args.inspect)
10
+ # We can't use a KStor::Error here because kstor/error.rb require()s
11
+ # kstor/message.rb .
12
+ class RequestMissesAuthData < RuntimeError
76
13
  end
77
14
 
78
- def to_h
79
- super.merge('login' => @login, 'password' => @password)
15
+ # Response data was invalid JSON.
16
+ class UnparsableResponse < RuntimeError
80
17
  end
81
18
  end
19
+ end
82
20
 
83
- # A user request with a session ID.
84
- class SessionRequest < Message
85
- attr_reader :session_id
21
+ require 'kstor/message/error'
86
22
 
87
- def initialize(session_id, type, args)
88
- @session_id = session_id
89
- super(type, args)
90
- end
23
+ require 'kstor/message/ping'
24
+ require 'kstor/message/pong'
91
25
 
92
- def inspect
93
- fmt = [
94
- '#<KStor::SessionRequest:%<id>x',
95
- '@session_id=******',
96
- '@args=%<args>s>'
97
- ].join(' ')
98
- format(fmt, id: object_id, args: @args.inspect)
99
- end
26
+ require 'kstor/message/group_create'
27
+ require 'kstor/message/group_created'
100
28
 
101
- def to_h
102
- super.merge('session_id' => @session_id)
103
- end
104
- end
29
+ require 'kstor/message/secret_create'
30
+ require 'kstor/message/secret_created'
105
31
 
106
- # Response to a user request.
107
- class Response < Message
108
- attr_accessor :session_id
32
+ require 'kstor/message/secret_delete'
33
+ require 'kstor/message/secret_deleted'
109
34
 
110
- def initialize(type, args)
111
- @session_id = nil
112
- super
113
- end
35
+ require 'kstor/message/secret_search'
36
+ require 'kstor/message/secret_list'
114
37
 
115
- def self.parse(str)
116
- data = JSON.parse(str)
117
- resp = new(data['type'], data['args'])
118
- resp.session_id = data['session_id']
119
- resp
120
- rescue JSON::ParserError => e
121
- raise UnparsableResponse, e.message
122
- end
38
+ require 'kstor/message/secret_unlock'
39
+ require 'kstor/message/secret_value'
123
40
 
124
- def error?
125
- @type == 'error'
126
- end
41
+ require 'kstor/message/secret_update_meta'
42
+ require 'kstor/message/secret_update_value'
43
+ require 'kstor/message/secret_updated'
127
44
 
128
- def to_h
129
- super.merge('session_id' => @session_id)
130
- end
131
- end
132
- end
45
+ KStor::Message.register_all_message_types
data/lib/kstor/model.rb CHANGED
@@ -86,10 +86,7 @@ module KStor
86
86
 
87
87
  # Dump properties except pubk.
88
88
  def to_h
89
- h = super
90
- h.delete('pubk')
91
-
92
- h
89
+ super.except('pubk')
93
90
  end
94
91
  end
95
92
 
@@ -154,9 +151,7 @@ module KStor
154
151
 
155
152
  # Dump properties except {#encrypted_privk}.
156
153
  def to_h
157
- h = super
158
- h.delete('encrypted_privk')
159
- h
154
+ super.except('encrypted_privk')
160
155
  end
161
156
  end
162
157
 
@@ -263,9 +258,6 @@ module KStor
263
258
  self.kdf_params = secret_key.kdf_params
264
259
  encrypt(secret_key)
265
260
  self.keychain = {}
266
- # FIXME: delete keychain items from database!
267
- # they won't be useable (decryption key is lost) but will provoke
268
- # errors.
269
261
  end
270
262
 
271
263
  # Re-encrypt private key and keychain with a new secret key derived from
@@ -283,9 +275,7 @@ module KStor
283
275
 
284
276
  # Dump properties except {#encrypted_privk} and {#pubk}.
285
277
  def to_h
286
- h = super
287
- h.delete('encrypted_privk')
288
- h.delete('pubk')
278
+ h = super.except('encrypted_privk', 'pubk')
289
279
  h['keychain'] = keychain.transform_values(&:to_h) if keychain
290
280
 
291
281
  h
@@ -320,6 +310,13 @@ module KStor
320
310
  # Secret should be used at this URL
321
311
  attr_accessor :url
322
312
 
313
+ # Create new metadata for a secret.
314
+ #
315
+ # Hash param can contains keys for "app", "database", "login", "server"
316
+ # and "url". Any other key is ignored.
317
+ #
318
+ # @param values [Hash, KStor::Crypto::ArmoredHash] metadata
319
+ # @return [KStor::Model::SecretMeta] secret metadata
323
320
  def initialize(values)
324
321
  @app = values['app']
325
322
  @database = values['database']
@@ -328,30 +325,50 @@ module KStor
328
325
  @url = values['url']
329
326
  end
330
327
 
328
+ # Convert this metadata to a Hash.
329
+ #
330
+ # Empty values will not be included.
331
+ #
332
+ # @return [Hash[String, String]] metadata as a Hash
331
333
  def to_h
332
334
  { 'app' => @app, 'database' => @database, 'login' => @login,
333
335
  'server' => @server, 'url' => @url }.compact
334
336
  end
335
337
 
338
+ # Prepare metadata to be written to disk or database.
339
+ #
340
+ # @return [KStor::Crypto::ArmoredHash] serialized metadata
336
341
  def serialize
337
342
  Crypto::ArmoredHash.from_hash(to_h)
338
343
  end
339
344
 
345
+ # Merge metadata.
346
+ #
347
+ # @param other [KStor::Model::SecretMeta] other metadata that will
348
+ # override this object's values.
340
349
  def merge(other)
341
350
  values = to_h.merge(other.to_h)
342
351
  values.reject! { |_, v| v.empty? }
343
352
  self.class.new(values)
344
353
  end
345
354
 
346
- def self.load(armored_hash)
347
- new(armored_hash.to_hash)
348
- end
349
-
355
+ # Match against wildcards.
356
+ #
357
+ # Metadata will be matched against another metadata object with wildcard
358
+ # values. This uses roughly the same rules that shell wildcards (e.g.
359
+ # fnmatch(3) C function).
360
+ #
361
+ # @see File.fnmatch?
362
+ #
363
+ # @param meta [KStor::Model::SecretMeta] wildcard metadata
364
+ # @return [Boolean] true if matched
365
+ # rubocop:disable Metrics/CyclomaticComplexity
350
366
  def match?(meta)
351
367
  self_h = to_h
352
368
  other_h = meta.to_h
353
369
  other_h.each do |k, wildcard|
354
370
  val = self_h[k]
371
+ return false if val.nil? && !wildcard.nil? && wildcard != '*'
355
372
  next if val.nil?
356
373
  next if wildcard.nil?
357
374
 
@@ -362,6 +379,7 @@ module KStor
362
379
  end
363
380
  true
364
381
  end
382
+ # rubocop:enable Metrics/CyclomaticComplexity
365
383
  end
366
384
 
367
385
  # A secret, with metadata and a value that are kept encrypted on disk.
@@ -383,8 +401,11 @@ module KStor
383
401
  # @!macro dsl_model_properties_rw
384
402
  property :metadata, read_only: true
385
403
 
404
+ # Set metadata (or unset if nil).
405
+ #
406
+ # @param armored_hash [KStor::Crypt::ArmoredHash] metadata to load
386
407
  def metadata=(armored_hash)
387
- @data[:metadata] = armored_hash ? SecretMeta.load(armored_hash) : nil
408
+ @data[:metadata] = armored_hash ? SecretMeta.new(armored_hash) : nil
388
409
  end
389
410
 
390
411
  # Decrypt secret value.
@@ -424,13 +445,9 @@ module KStor
424
445
  # Dump properties except {#ciphertext}, {#encrypted_metadata},
425
446
  # {#value_author_id} and {#meta_author_id}.
426
447
  def to_h
427
- h = super
428
- h.delete('ciphertext')
429
- h.delete('encrypted_metadata')
430
- h.delete('value_author_id')
431
- h.delete('meta_author_id')
432
-
433
- h
448
+ super.except(
449
+ *%w[ciphertext encrypted_metadata value_author_id meta_author_id]
450
+ )
434
451
  end
435
452
  end
436
453
  end
data/lib/kstor/server.rb CHANGED
@@ -15,11 +15,24 @@ module KStor
15
15
 
16
16
  # Listen for clients and respond to their requests.
17
17
  class Server < SocketServer
18
+ # Create a new server object.
19
+ #
20
+ # @param controller [KStor::Controller::RequestHandler] client request
21
+ # handler
22
+ # @param args [Hash] parameters to apss to parent class
23
+ # {KStor::SocketServer}
24
+ # @return [KStor::Server] new KStor server.
18
25
  def initialize(controller:, **args)
19
26
  @controller = controller
20
27
  super(**args)
21
28
  end
22
29
 
30
+ # Implement {KStor::SocketServer#work} to actually serve clients.
31
+ #
32
+ # This method must read client request, write a response and close the
33
+ # socket.
34
+ #
35
+ # @param client [Socket] channel of communication to a client
23
36
  def work(client)
24
37
  client_data, = client.recvfrom(4096)
25
38
  Log.debug("server: read #{client_data.bytesize} bytes from client")
@@ -36,15 +49,14 @@ module KStor
36
49
  private
37
50
 
38
51
  def handle_client_data(data)
39
- req = Message.parse_request(data)
52
+ req = Message::Base.parse(data)
40
53
  resp = @controller.handle_request(req)
41
- Log.debug("server: response = #{resp.inspect}")
42
54
  resp.serialize
43
55
  rescue JSON::ParserError => e
44
56
  err = Error.for_code('MSG/INVALID', e.message)
45
57
  Log.info("server: #{err}")
46
58
  err.response.serialize
47
- rescue RequestMissesAuthData
59
+ rescue Message::RequestMissesAuthData
48
60
  raise Error.for_code('MSG/INVALID', 'Missing authentication data')
49
61
  end
50
62
  end
data/lib/kstor/session.rb CHANGED
@@ -11,6 +11,13 @@ module KStor
11
11
  attr_reader :created_at
12
12
  attr_reader :updated_at
13
13
 
14
+ # Create a new user session.
15
+ #
16
+ # @param sid [String] Session ID
17
+ # @param user [KStor::Model::User] user owning the session
18
+ # @param secret_key [KStor::Crypto::SecretKey] user secret key, derived
19
+ # from password
20
+ # @return [KStor::Session] new session
14
21
  def initialize(sid, user, secret_key)
15
22
  @id = sid
16
23
  @user = user
@@ -19,21 +26,37 @@ module KStor
19
26
  @updated_at = Time.now
20
27
  end
21
28
 
29
+ # Update access time, for idle sessions weeding.
30
+ #
31
+ # @return [KStor::Session] updated session
22
32
  def update
23
33
  @updated_at = Time.now
24
34
  self
25
35
  end
26
36
 
37
+ # Create a new session for a user.
38
+ #
39
+ # @param user [KStor::Model::User] user owning the session
40
+ # @param secret_key [KStor::Crypto::SecretKey] user secret key, derived
41
+ # from password
42
+ # @return [KStor::Session] new session with a random SID
27
43
  def self.create(user, secret_key)
28
44
  sid = SecureRandom.urlsafe_base64(16)
29
45
  new(sid, user, secret_key)
30
46
  end
31
47
  end
32
48
 
33
- # Collection of user sessions (in memory)
49
+ # Collection of user sessions (in memory).
34
50
  #
35
- # FIXME make it thread safe!
51
+ # Concurrent accesses are synchronized on a mutex.
36
52
  class SessionStore
53
+ # Create new session store.
54
+ #
55
+ # @param idle_timeout [Integer] sessions that aren't updated for this
56
+ # number of seconds are considered invalid
57
+ # @param life_timeout [Integer] sessions that are older than this number of
58
+ # seconds are considered invalid
59
+ # @return [KStor::SessionStore] a new session store.
37
60
  def initialize(idle_timeout, life_timeout)
38
61
  @idle_timeout = idle_timeout
39
62
  @life_timeout = life_timeout
@@ -41,12 +64,20 @@ module KStor
41
64
  @sessions.extend(Mutex_m)
42
65
  end
43
66
 
67
+ # Add a session to the store.
68
+ #
69
+ # @param session [KStor::Session] session to store
44
70
  def <<(session)
45
71
  @sessions.synchronize do
46
72
  @sessions[session.id] = session
47
73
  end
48
74
  end
49
75
 
76
+ # Fetch a session from it's ID.
77
+ #
78
+ # @param sid [String] session ID to lookup
79
+ # @return [KStor::Session, nil] session or nil if session ID was not found
80
+ # or session has expired
50
81
  def [](sid)
51
82
  @sessions.synchronize do
52
83
  s = @sessions[sid.to_s]
@@ -61,6 +92,7 @@ module KStor
61
92
  end
62
93
  end
63
94
 
95
+ # Delete expired sessions.
64
96
  def purge
65
97
  now = Time.now
66
98
  @sessions.synchronize do
@@ -9,8 +9,14 @@ require 'timeout'
9
9
  module KStor
10
10
  # Serve clients on UNIX sockets.
11
11
  class SocketServer
12
+ # Wait this number of seconds for worker threads to terminate before
13
+ # killing them without mercy.
12
14
  GRACEFUL_TIMEOUT = 10
13
15
 
16
+ # Create a new server.
17
+ #
18
+ # @param socket_path [String] path to listening socket
19
+ # @param nworkers [Integer] number of worker threads
14
20
  def initialize(socket_path:, nworkers:)
15
21
  @path = socket_path
16
22
  @nworkers = nworkers
@@ -18,10 +24,14 @@ module KStor
18
24
  @workers = []
19
25
  end
20
26
 
27
+ # Start serving clients.
28
+ #
29
+ # Send interrupt signal to cleanly stop.
21
30
  def start
22
31
  start_workers
23
- server = Systemd.socket
32
+ server = server_socket
24
33
  Systemd.service_ready
34
+ Log.info('socket_server: started')
25
35
  loop do
26
36
  maintain_workers
27
37
  @client_queue.enq(server.accept.first)
@@ -31,6 +41,8 @@ module KStor
31
41
  Log.info('socket_server: stopped.')
32
42
  end
33
43
 
44
+ # Do some work for a client and write a response.
45
+ # @abstract
34
46
  def work(client)
35
47
  # Abstract method.
36
48
  client.close
@@ -38,6 +50,13 @@ module KStor
38
50
 
39
51
  private
40
52
 
53
+ def server_socket
54
+ s = Systemd.socket
55
+ return s if s
56
+
57
+ UNIXServer.new(@path)
58
+ end
59
+
41
60
  def worker_run
42
61
  while (client = @client_queue.deq)
43
62
  Log.debug("socket_server: #{Thread.current.name} serving one client")
@@ -14,18 +14,33 @@ module KStor
14
14
 
15
15
  # Execute SQL commands in a per-thread SQLite connection.
16
16
  class SQLConnection
17
+ # Create a new SQL connection.
18
+ #
19
+ # @param file_path [String] path to SQLite database
20
+ # @return [KStor::SQLConnection] the new connection
17
21
  def initialize(file_path)
18
22
  @file_path = file_path
19
23
  end
20
24
 
25
+ # Execute SQL statement.
26
+ #
27
+ # @param sql [String] SQL statement
28
+ # @param params [Array] parameters to fill placeholders in the statement
29
+ # @return [Any] Whatever SQLite returns
21
30
  def execute(sql, *params, &)
22
31
  database.execute(sql, *params, &)
23
32
  end
24
33
 
34
+ # Last generated ID (from an INSERT statement).
35
+ #
36
+ # @return [Integer] generated ID from last insert statement.
25
37
  def last_insert_row_id
26
38
  database.last_insert_row_id
27
39
  end
28
40
 
41
+ # Execute given block of code in a database transaction.
42
+ #
43
+ # @return [Any] Whatever the block returns
29
44
  def transaction(&block)
30
45
  result = nil
31
46
  database.transaction do |db|
@@ -52,7 +67,7 @@ module KStor
52
67
  def setup_thread_connection(key)
53
68
  return if Thread.current[key]
54
69
 
55
- Log.info(
70
+ Log.debug(
56
71
  "sqlite: initializing connection in thread #{Thread.current.name}"
57
72
  )
58
73
  Thread.current[key] = connect(@file_path)