kstor 0.4.2 → 0.5.0

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