kstor 0.4.3 → 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
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