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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 72e1dae661e2cb26fb40e0ac19f066ae35b9eb09605123915f0119a7ed98100c
4
- data.tar.gz: 9a10808b3a5e5c6b65babc84281984b5c3aed4974227808db992652eab7542e5
3
+ metadata.gz: 74582d56a367754e14553d134666e9fe82322f5a61d2e87905b846e539f5a395
4
+ data.tar.gz: 36bc6b27ea95681d1214f2c2194773114cee637b4f039a56571b0fdf606174a0
5
5
  SHA512:
6
- metadata.gz: ed9c26317bb1765ca766abbc1531b24eb0cf33b6e44820b6c37dee62ea6b1c063bd34cd8c15d0f562dddb7766e7f2d841a87b30d1a8c094c882937790d29819b
7
- data.tar.gz: c2c3d2f28793d0a55b62491b1a3bc5950e3b80784e7bb3ba72f996530d6a104d9d6a74d88ab55756d4193cae486dfb5addbe362c40b60bbbf29966fa7f943b62
6
+ metadata.gz: 18522832bdcd1af680938d8f89857ce74c535763d4912782ea40b303374ce3bbd8fe9d6f8432773b19c86b44459ded250e3048a897678086b6b72c8cc863cdc6
7
+ data.tar.gz: 76798cadbb665be06ca2292c5365603fc242b3f841efc2411d9e93f32091e05366bc82ba06dc9d0ed87e178f0bdd552cf1021deb129a33ea1f8511b2abba9c96
data/README.md CHANGED
@@ -28,7 +28,7 @@ group key pairs. Group private keys are used to decrypt secrets. Pfew!
28
28
  4. systemctl --user start kstor.socket
29
29
  5. bundle exec kstor --help
30
30
 
31
- ### Available request types
31
+ ## Available request types
32
32
 
33
33
  So far I've implemented:
34
34
  * group-create
@@ -39,7 +39,7 @@ So far I've implemented:
39
39
  * secret-update-value
40
40
  * secret-delete
41
41
 
42
- ### Notes
42
+ ## Notes
43
43
 
44
44
  On first access, it will create your user in database (login defaults to your
45
45
  login). Passwords are asked interactively.
@@ -47,3 +47,7 @@ login). Passwords are asked interactively.
47
47
  It will store session ID in XDG_RUNTIME_DIR/kstor/session-id .
48
48
 
49
49
  Each request can be authentified either with login/password or with session ID.
50
+
51
+ ## Badges
52
+
53
+ Gem version on Rubygems.org: [![Gem Version](https://badge.fury.io/rb/kstor.svg)](https://badge.fury.io/rb/kstor)
data/bin/kstor CHANGED
@@ -13,14 +13,17 @@ module KStor
13
13
  # Manage KStor client configuration and state on disk.
14
14
  module ClientState
15
15
  class << self
16
+ # Default client config.
16
17
  DEFAULT_CONFIG = {
17
- 'socket' => '/home/jpi/code/kstor/testworkdir/kstor.socket'
18
+ 'socket' => '/run/kstor.socket'
18
19
  }.freeze
19
20
 
21
+ # Load client config from disk.
20
22
  def load_config(progr)
21
23
  DEFAULT_CONFIG.merge(load_config_file(progr))
22
24
  end
23
25
 
26
+ # Path to session ID file on disk.
24
27
  def session_id_file(progr)
25
28
  dir = File.join(xdg_runtime, progr)
26
29
  FileUtils.mkdir_p(dir)
@@ -30,6 +33,7 @@ module KStor
30
33
  file
31
34
  end
32
35
 
36
+ # Load session ID from disk.
33
37
  def load_session_id(progr)
34
38
  sid = nil
35
39
  File.open(session_id_file(progr)) { |f| sid = f.read.chomp }
@@ -59,12 +63,11 @@ module KStor
59
63
  def config_file(progr)
60
64
  dir = File.join(xdg_config, progr)
61
65
  FileUtils.mkdir_p(dir)
62
- File.join(dir, 'session-id')
66
+ File.join(dir, 'config.yaml')
63
67
  end
64
68
 
65
69
  def load_config_file(progr)
66
- data = File.read(config_file(progr))
67
- YAML.parse(data)
70
+ YAML.load_file(config_file(progr))
68
71
  rescue Errno::ENOENT
69
72
  {}
70
73
  end
@@ -73,14 +76,16 @@ module KStor
73
76
 
74
77
  # Sub-commands that can be invoked from the command-line.
75
78
  module ClientSubCommands
79
+ # Create a user group.
76
80
  def group_create
77
- request('group-create') do |o|
81
+ request('group_create') do |o|
78
82
  o.string('-n', '--name', 'Group name')
79
83
  end
80
84
  end
81
85
 
86
+ # Create a secret.
82
87
  def secret_create
83
- req = request('secret-create') do |o|
88
+ request_with_meta('secret_create') do |o|
84
89
  o.string('-p', '--plaintext', 'Value of the secret')
85
90
  o.array('-g', '--group_ids', 'Groups that can unlock the secret')
86
91
  o.string('-a', '--app', 'application of this secret')
@@ -89,11 +94,11 @@ module KStor
89
94
  o.string('-S', '--server', 'server of this secret')
90
95
  o.string('-u', '--url', 'url for this secret')
91
96
  end
92
- reorganize_secret_meta_args(req)
93
97
  end
94
98
 
99
+ # Return a list of matching secrets.
95
100
  def secret_search
96
- request('secret-search') do |o|
101
+ request_with_meta('secret_search') do |o|
97
102
  o.string('-a', '--app', 'secrets for this application')
98
103
  o.string('-d', '--database', 'secrets for this database')
99
104
  o.string('-l', '--login', 'secrets for this login')
@@ -102,14 +107,16 @@ module KStor
102
107
  end
103
108
  end
104
109
 
110
+ # Decrypt secret value and metadata
105
111
  def secret_unlock
106
- request('secret-unlock') do |o|
112
+ request('secret_unlock') do |o|
107
113
  o.string('-s', '--secret-id', 'secret ID to unlock')
108
114
  end
109
115
  end
110
116
 
117
+ # Update secret metadata
111
118
  def secret_update_meta
112
- req = request('secret-update-meta') do |o|
119
+ request_with_meta('secret_update_meta') do |o|
113
120
  o.string('-s', '--secret-id', 'secret ID to modify')
114
121
  o.string('-a', '--app', 'new application of this secret')
115
122
  o.string('-d', '--database', 'new database of this secret')
@@ -117,18 +124,19 @@ module KStor
117
124
  o.string('-S', '--server', 'new server of this secret')
118
125
  o.string('-u', '--url', 'new url for this secret')
119
126
  end
120
- reorganize_secret_meta_args(req)
121
127
  end
122
128
 
129
+ # Update secret value
123
130
  def secret_update_value
124
- request('secret-update-value') do |o|
131
+ request('secret_update_value') do |o|
125
132
  o.string('-s', '--secret-id', 'secret ID to modify')
126
133
  o.string('-p', '--plaintext', 'new plaintext value')
127
134
  end
128
135
  end
129
136
 
137
+ # Delete secret
130
138
  def secret_delete
131
- request('secret-delete') do |o|
139
+ request('secret_delete') do |o|
132
140
  o.string('-s', '--secret-id', 'secret ID to delete')
133
141
  end
134
142
  end
@@ -138,14 +146,16 @@ module KStor
138
146
  class Client
139
147
  include ClientSubCommands
140
148
 
149
+ # Create new command-line client.
141
150
  def initialize
142
151
  @progr = File.basename($PROGRAM_NAME)
143
152
  @config = ClientState.load_config(@progr)
144
153
  @user = user_from_argv
145
154
  end
146
155
 
156
+ # Read command-line args, send request to server and display results..
147
157
  def run
148
- request_type = ARGV.shift
158
+ request_type = ARGV.shift.to_sym
149
159
  resp = send_request(request_type)
150
160
  handle_error!(resp) if resp.error?
151
161
 
@@ -178,7 +188,7 @@ module KStor
178
188
  end
179
189
 
180
190
  def handle_error!(resp)
181
- if resp.args['code'] == 'AUTH/BADSESSION'
191
+ if resp.code == 'AUTH/BADSESSION'
182
192
  FileUtils.rm(ClientState.session_id_file(@progr))
183
193
  warn('session expired')
184
194
  else
@@ -195,34 +205,23 @@ module KStor
195
205
  socket.send(req.serialize, 0)
196
206
 
197
207
  data, = socket.recvfrom(4096)
198
- Response.parse(data)
199
- rescue UnparsableResponse
200
- warn('server speaks funny; look at logs!')
208
+ Message::Base.parse(data)
209
+ rescue Message::UnparsableResponse
210
+ warn('Invalid response from server; look at logs!')
201
211
  exit(1)
202
212
  end
203
213
 
204
- REQUEST_METHODS = {
205
- 'group-create' => :group_create,
206
- 'secret-create' => :secret_create,
207
- 'secret-search' => :secret_search,
208
- 'secret-unlock' => :secret_unlock,
209
- 'secret-update-meta' => :secret_update_meta,
210
- 'secret-update-value' => :secret_update_value,
211
- 'secret-delete' => :secret_delete
212
- }.freeze
213
-
214
214
  def method_name(request_type)
215
215
  if request_type.nil?
216
216
  base_usage
217
217
  exit 0
218
218
  end
219
- meth = REQUEST_METHODS[request_type]
220
- if meth.nil?
219
+ unless KStor::Message::Base.type?(request_type)
221
220
  warn("Unknown request type #{request_type.inspect}")
222
221
  base_usage
223
222
  exit 1
224
223
  end
225
- meth
224
+ request_type
226
225
  end
227
226
 
228
227
  def user_from_argv
@@ -250,19 +249,28 @@ module KStor
250
249
  session_id = ClientState.load_session_id(@progr)
251
250
 
252
251
  if session_id
253
- { 'session_id' => session_id }
252
+ { session_id: }
254
253
  else
255
- { 'login' => @user, 'password' => ask_password }
254
+ { login: @user, password: ask_password }
256
255
  end
257
256
  end
258
257
 
258
+ def request_with_meta(type, &block)
259
+ args = parse_opts(type) { |o| block.call(o) }
260
+ args['meta'] = {
261
+ 'app' => args.delete('app'),
262
+ 'database' => args.delete('database'),
263
+ 'login' => args.delete('login'),
264
+ 'server' => args.delete('server'),
265
+ 'url' => args.delete('url')
266
+ }
267
+ args['meta'].compact!
268
+ KStor::Message::Base.for_type(type, args, **auth)
269
+ end
270
+
259
271
  def request(type, &block)
260
- hreq = {}
261
- hreq['type'] = type
262
- hreq['args'] = parse_opts(type) do |o|
263
- block.call(o)
264
- end
265
- KStor::Message.parse_request(hreq.merge(auth).to_json)
272
+ args = parse_opts(type) { |o| block.call(o) }
273
+ KStor::Message::Base.for_type(type, args, **auth)
266
274
  end
267
275
 
268
276
  def parse_opts(request_type)
@@ -277,7 +285,7 @@ module KStor
277
285
  o.separator('')
278
286
  yield o
279
287
  end
280
- opts.to_hash.compact
288
+ opts.to_hash.compact.transform_keys(&:to_s)
281
289
  end
282
290
  end
283
291
  end
data/bin/kstor-srv CHANGED
@@ -3,10 +3,10 @@
3
3
 
4
4
  require 'kstor'
5
5
 
6
- KStor::Log.reporting_level = Journald::LOG_DEBUG
7
-
8
6
  config = KStor::Config.load(ARGV.shift)
9
7
 
8
+ KStor::Log.reporting_level = config.log_level
9
+
10
10
  store = KStor::Store.new(config.database)
11
11
  session_store = KStor::SessionStore.new(
12
12
  config.session_idle_timeout,
data/lib/kstor/config.rb CHANGED
@@ -29,7 +29,8 @@ module KStor
29
29
  'socket' => '/run/kstor-server.socket',
30
30
  'nworkers' => 5,
31
31
  'session_idle_timeout' => 15 * 60,
32
- 'session_life_timeout' => 4 * 60 * 60
32
+ 'session_life_timeout' => 4 * 60 * 60,
33
+ 'log_level' => 'warn'
33
34
  }.freeze
34
35
 
35
36
  class << self
@@ -8,13 +8,28 @@ require 'kstor/model'
8
8
 
9
9
  module KStor
10
10
  module Controller
11
- # Handle user authentication and sessions.
11
+ # Specialized controller for user authentication and sessions.
12
12
  class Authentication
13
+ # Create new auth controller.
14
+ #
15
+ # @param store [KStor::Store] data store where users are
16
+ # @param session_store [KStor::SessionStore] where user sessions are
17
+ # @return [KStor::Controller::Authentication] new auth controller
13
18
  def initialize(store, session_store)
14
19
  @store = store
15
20
  @sessions = session_store
16
21
  end
17
22
 
23
+ # Authenticate request user.
24
+ #
25
+ # Request may either contain a login/password, or a session ID.
26
+ #
27
+ # @param req [KStor::Message::Base] client request
28
+ # @return [KStor::Model::User] client user
29
+ # @raise [KStor::InvalidSession] if session ID is invalid
30
+ # @raise [KStor::UserNotAllowed] if user is not allowed
31
+ # @raise [KStor::MissingLoginPassword] if database is empty and request
32
+ # only contains a session ID
18
33
  def authenticate(req)
19
34
  if @store.users?
20
35
  unlock_user(req)
@@ -23,13 +38,19 @@ module KStor
23
38
  end
24
39
  end
25
40
 
26
- # return true if login is allowed to access the database.
41
+ # Check if user is allowed to access the application.
42
+ #
43
+ # @param user [KStor::Model::User] client user
44
+ # @return [Boolean] true if login is allowed to access application data.
27
45
  def allowed?(user)
28
46
  user.status == 'new' || user.status == 'active'
29
47
  end
30
48
 
49
+ private
50
+
51
+ # Load user from database and decrypt private key and keychain.
31
52
  def unlock_user(req)
32
- if req.respond_to?(:session_id)
53
+ if req.session_request?
33
54
  session_id = req.session_id
34
55
  user, secret_key = load_session(session_id)
35
56
  else
@@ -72,7 +93,7 @@ module KStor
72
93
  )
73
94
  secret_key = user.secret_key(req.password)
74
95
  user.unlock(secret_key)
75
- @store.user_create(user)
96
+ user.id = @store.user_create(user)
76
97
  Log.info("user #{user.login} created")
77
98
 
78
99
  session = Session.create(user, secret_key)
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KStor
4
+ # Various controllers that participate in serving client.
5
+ module Controller
6
+ # Common code for all controllers (except RequestHandler).
7
+ #
8
+ # @abstract
9
+ class Base
10
+ class << self
11
+ attr_accessor :request_types
12
+ attr_accessor :response_types
13
+
14
+ # Declare that this controller handles this type of request.
15
+ def request_type(klass)
16
+ @request_types ||= []
17
+ @request_types << klass
18
+ end
19
+
20
+ # Declare that this controller produces this type of response.
21
+ def response_type(klass)
22
+ @response_types ||= []
23
+ @response_types << klass
24
+ end
25
+
26
+ # True if sub-controller handles these requests.
27
+ #
28
+ # @param type [String] request type
29
+ # @return [Boolean] true if request type may be handled
30
+ def handles?(req)
31
+ @request_types.include?(req.class)
32
+ end
33
+ end
34
+
35
+ # Create sub-controller with access to data store.
36
+ #
37
+ # @param store [KStor::Store] data store
38
+ # @return [KStor::Controller::Base] a new sub-controller
39
+ def initialize(store)
40
+ @store = store
41
+ @request_handlers = self.class.request_types.to_h do |klass|
42
+ meth = "handle_#{klass.type}".to_sym
43
+ [klass, meth]
44
+ end
45
+ end
46
+
47
+ # Handle client request.
48
+ #
49
+ # @param user [KStor::Model::User] user making this request
50
+ # @param req [KStor::Message::Base] client request
51
+ def handle_request(user, sid, req)
52
+ unless @request_handlers.key?(req.class)
53
+ raise Error.for_code('REQ/UNKNOWN', req.type)
54
+ end
55
+
56
+ klass, args = __send__(@request_handlers[req.class], user, req)
57
+ klass.new(args, { session_id: sid })
58
+ end
59
+ end
60
+ end
61
+ end
@@ -9,41 +9,109 @@ require 'kstor/controller/users'
9
9
 
10
10
  module KStor
11
11
  module Controller
12
- # Request handler.
12
+ # Undeclared response type.
13
+ class UnknownResponseType < RuntimeError
14
+ end
15
+
16
+ # Top-level request handler.
17
+ #
18
+ # Dispatches requests to specialized sub-controller.
13
19
  class RequestHandler
20
+ class << self
21
+ # List of sub-controllers.
22
+ attr_accessor :controllers
23
+
24
+ # Request types handled by all sub-controllers.
25
+ #
26
+ # @return [Array[String]] list of all handled request types
27
+ def request_types
28
+ @controllers.map(&:request_types).inject([], &:+)
29
+ end
30
+
31
+ # Response types that sub-controllers can produce.
32
+ #
33
+ # @return [Array[String]] list of all response types that this handler
34
+ # can produce
35
+ def response_types
36
+ [Message::Error] + @controllers.map(&:response_types).inject([], &:+)
37
+ end
38
+
39
+ # All message types handled and produced by this controller.
40
+ #
41
+ # @return [Array[String]] List of all message types
42
+ def message_types
43
+ request_types + response_types
44
+ end
45
+
46
+ # True if this controller can handle this request type.
47
+ #
48
+ # @param type [Class] request type
49
+ # @return [Boolean] true if handled
50
+ def handles?(type)
51
+ request_types.include?(type)
52
+ end
53
+
54
+ # True if this controller can respond to a client with this type of
55
+ # reponse.
56
+ #
57
+ # @param type [String] response type
58
+ # @return [Boolean] true if can be produced.
59
+ def responds?(type)
60
+ response_types.include?(type)
61
+ end
62
+ end
63
+
64
+ self.controllers = [Controller::User, Controller::Secret]
65
+
66
+ # Create new request handler controller from data store and session store.
67
+ #
68
+ # @param store [KStor::Store] data store
69
+ # @param session_store [KStor::SessionStore] session store
70
+ # @return [KStor::Controller::RequestHandler] new top-level controller.
14
71
  def initialize(store, session_store)
15
72
  @auth = Controller::Authentication.new(store, session_store)
16
- @secret = Controller::Secret.new(store)
17
- @user = Controller::User.new(store)
18
73
  @store = store
74
+ @controllers = self.class.controllers.map do |klass|
75
+ klass.new(store)
76
+ end
19
77
  end
20
78
 
79
+ # Serve a client.
80
+ #
81
+ # @param req [KStor::Message::Base] client request
82
+ # @return [KStor::Message::Base] server response
21
83
  def handle_request(req)
22
84
  user, sid = @auth.authenticate(req)
23
85
  controller = controller_from_request_type(req)
24
- resp = @store.transaction { controller.handle_request(user, req) }
86
+ resp = @store.transaction { controller.handle_request(user, sid, req) }
25
87
  user.lock
26
- resp.session_id = sid
27
- resp
88
+ finish_response(resp)
28
89
  rescue RbNaClError => e
29
90
  Log.exception(e)
30
- Error.for_code('CRYPTO/UNSPECIFIED').response
31
- rescue Error => e
91
+ Error.for_code('CRYPTO/UNSPECIFIED').response(sid)
92
+ rescue KStor::MissingMessageArgument => e
93
+ raise e
94
+ rescue KStor::Error => e
32
95
  Log.info(e.message)
33
- e.response
96
+ e.response(sid)
34
97
  end
35
98
 
36
99
  private
37
100
 
101
+ def finish_response(resp)
102
+ unless self.class.responds?(resp.class)
103
+ raise UnknownResponseType, 'Unknown response type ' \
104
+ "#{resp.type.inspect}"
105
+ end
106
+ resp
107
+ end
108
+
38
109
  def controller_from_request_type(req)
39
- case req.type
40
- when /^secret-(create|delete|search|unlock|update-(meta|value)?)$/
41
- @secret
42
- when /^group-create$/
43
- @user
44
- else
45
- raise Error.for_code('REQ/UNKNOWN', req.type)
110
+ @controllers.each do |ctrl|
111
+ return ctrl if ctrl.class.handles?(req)
46
112
  end
113
+
114
+ raise Error.for_code('REQ/UNKNOWN', req.type)
47
115
  end
48
116
  end
49
117
  end