kstor 0.4.2 → 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.
- checksums.yaml +4 -4
- data/README.md +6 -2
- data/bin/kstor +48 -40
- data/bin/kstor-srv +2 -2
- data/lib/kstor/config.rb +2 -1
- data/lib/kstor/controller/authentication.rb +25 -4
- data/lib/kstor/controller/base.rb +61 -0
- data/lib/kstor/controller/request_handler.rb +84 -16
- data/lib/kstor/controller/secret.rb +63 -64
- data/lib/kstor/controller/users.rb +11 -22
- data/lib/kstor/controller.rb +3 -3
- data/lib/kstor/crypto/armored_value.rb +82 -0
- data/lib/kstor/crypto/keys.rb +9 -41
- data/lib/kstor/crypto.rb +0 -1
- data/lib/kstor/error.rb +36 -3
- data/lib/kstor/log/simple_logger.rb +83 -0
- data/lib/kstor/log/systemd_logger.rb +22 -0
- data/lib/kstor/log.rb +53 -4
- data/lib/kstor/message/base.rb +223 -0
- data/lib/kstor/message/error.rb +15 -0
- data/lib/kstor/message/group_create.rb +14 -0
- data/lib/kstor/message/group_created.rb +16 -0
- data/lib/kstor/message/ping.rb +14 -0
- data/lib/kstor/message/pong.rb +14 -0
- data/lib/kstor/message/secret_create.rb +16 -0
- data/lib/kstor/message/secret_created.rb +14 -0
- data/lib/kstor/message/secret_delete.rb +14 -0
- data/lib/kstor/message/secret_deleted.rb +14 -0
- data/lib/kstor/message/secret_list.rb +14 -0
- data/lib/kstor/message/secret_search.rb +14 -0
- data/lib/kstor/message/secret_unlock.rb +14 -0
- data/lib/kstor/message/secret_update_meta.rb +15 -0
- data/lib/kstor/message/secret_update_value.rb +15 -0
- data/lib/kstor/message/secret_updated.rb +14 -0
- data/lib/kstor/message/secret_value.rb +35 -0
- data/lib/kstor/message.rb +26 -113
- data/lib/kstor/model.rb +42 -25
- data/lib/kstor/server.rb +15 -3
- data/lib/kstor/session.rb +34 -2
- data/lib/kstor/socket_server.rb +20 -1
- data/lib/kstor/sql_connection.rb +16 -1
- data/lib/kstor/store.rb +182 -72
- data/lib/kstor/systemd.rb +5 -0
- data/lib/kstor/version.rb +2 -1
- data/lib/kstor.rb +1 -1
- metadata +25 -3
data/lib/kstor/store.rb
CHANGED
@@ -1,23 +1,74 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'mutex_m'
|
4
|
+
|
3
5
|
require 'kstor/sql_connection'
|
4
6
|
require 'kstor/model'
|
5
7
|
require 'kstor/log'
|
6
8
|
|
7
9
|
module KStor
|
10
|
+
# Simplistic cache for list of users and groups.
|
11
|
+
class StoreCache
|
12
|
+
# Create new cache.
|
13
|
+
def initialize
|
14
|
+
@cache = {}
|
15
|
+
@cache.extend(Mutex_m)
|
16
|
+
end
|
17
|
+
|
18
|
+
class << self
|
19
|
+
# @!macro [new] dsl_storecache_property
|
20
|
+
# @!attribute $1
|
21
|
+
# @return returns cached list of $1
|
22
|
+
|
23
|
+
# Declare a cached list of values.
|
24
|
+
#
|
25
|
+
# @param name [Symbol] name of list
|
26
|
+
def property(name)
|
27
|
+
define_method(name) do |&block|
|
28
|
+
@cache.synchronize do
|
29
|
+
return @cache[name] if @cache.key?(name)
|
30
|
+
|
31
|
+
@cache[name] = block.call
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
define_method("#{name}=".to_sym) do |list|
|
36
|
+
@cache.synchronize { @cache[name] = list }
|
37
|
+
end
|
38
|
+
|
39
|
+
define_method("forget_#{name}") do
|
40
|
+
@cache.synchronize { @cache.delete(name) }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# @!macro dsl_storecache_property
|
46
|
+
property :users
|
47
|
+
# @!macro dsl_storecache_property
|
48
|
+
property :groups
|
49
|
+
end
|
50
|
+
|
8
51
|
# Store and fetch objects in an SQLite database.
|
9
52
|
# rubocop:disable Metrics/MethodLength
|
10
53
|
class Store
|
54
|
+
# Create a new store backed by the given SQLite database file.
|
55
|
+
#
|
56
|
+
# @param file_path [String] path to SQLite database file
|
57
|
+
# @return [KStor::Store] a data store
|
11
58
|
def initialize(file_path)
|
12
59
|
@file_path = file_path
|
13
60
|
@db = SQLConnection.new(file_path)
|
14
|
-
@cache =
|
61
|
+
@cache = StoreCache.new
|
15
62
|
end
|
16
63
|
|
64
|
+
# Execute the given block in a database transaction.
|
17
65
|
def transaction(&)
|
18
66
|
@db.transaction(&)
|
19
67
|
end
|
20
68
|
|
69
|
+
# True if database contains any users.
|
70
|
+
#
|
71
|
+
# @return [Boolean] false if user table is empty
|
21
72
|
def users?
|
22
73
|
rows = @db.execute('SELECT count(*) AS n FROM users')
|
23
74
|
count = Integer(rows.first['n'])
|
@@ -26,25 +77,33 @@ module KStor
|
|
26
77
|
count.positive?
|
27
78
|
end
|
28
79
|
|
80
|
+
# Create a new user in database.
|
81
|
+
#
|
82
|
+
# @param user [KStor::Model::User] the user to create
|
83
|
+
# @return [KStor::Model::User] the same user with a brand-new ID
|
29
84
|
def user_create(user)
|
30
85
|
@db.execute(<<-EOSQL, user.login, user.name, 'new')
|
31
86
|
INSERT INTO users (login, name, status)
|
32
87
|
VALUES (?, ?, ?)
|
33
88
|
EOSQL
|
34
|
-
|
89
|
+
user_id = @db.last_insert_row_id
|
35
90
|
Log.debug("store: stored new user #{user.login}")
|
36
91
|
params = [user.kdf_params, user.pubk, user.encrypted_privk].map(&:to_s)
|
37
|
-
return
|
92
|
+
return user_id if params.any?(&:nil?)
|
38
93
|
|
39
94
|
@db.execute(<<-EOSQL, user.id, *params)
|
40
95
|
INSERT INTO users_crypto_data (user_id, kdf_params, pubk, encrypted_privk)
|
41
96
|
VALUES (?, ?, ?, ?)
|
42
97
|
EOSQL
|
43
98
|
Log.debug("store: stored user crypto data for #{user.login}")
|
99
|
+
@cache.forget_users
|
44
100
|
|
45
|
-
|
101
|
+
user_id
|
46
102
|
end
|
47
103
|
|
104
|
+
# Update user name, status and keychain.
|
105
|
+
#
|
106
|
+
# @param user [KStor::Model::User] user to modify.
|
48
107
|
def user_update(user)
|
49
108
|
@db.execute(<<-EOSQL, user.name, user.status, user.id)
|
50
109
|
UPDATE users SET name = ?, status = ?
|
@@ -60,6 +119,12 @@ module KStor
|
|
60
119
|
EOSQL
|
61
120
|
end
|
62
121
|
|
122
|
+
# Add a group private key to a user keychain.
|
123
|
+
#
|
124
|
+
# @param user_id [Integer] ID of an existing user
|
125
|
+
# @param group_id [Integer] ID of an existing group
|
126
|
+
# @param encrypted_privk [KStor::Crypto::ArmoredValue] group private key
|
127
|
+
# encrypted with user public key
|
63
128
|
def keychain_item_create(user_id, group_id, encrypted_privk)
|
64
129
|
@db.execute(<<-EOSQL, user_id, group_id, encrypted_privk.to_s)
|
65
130
|
INSERT INTO group_members (user_id, group_id, encrypted_privk)
|
@@ -67,64 +132,79 @@ module KStor
|
|
67
132
|
EOSQL
|
68
133
|
end
|
69
134
|
|
135
|
+
# Create a new group.
|
136
|
+
#
|
137
|
+
# Note that it doesn't store the group private key, as it must only exist
|
138
|
+
# in users keychains.
|
139
|
+
#
|
140
|
+
# @param name [String] Name of the new group (must be unique in database)
|
141
|
+
# @param pubk [KStor::Crypto::PublicKey] group public key
|
142
|
+
# @return [Integer] ID of the new group
|
70
143
|
def group_create(name, pubk)
|
71
144
|
@db.execute(<<-EOSQL, name, pubk.to_s)
|
72
145
|
INSERT INTO groups (name, pubk)
|
73
146
|
VALUES (?, ?)
|
74
147
|
EOSQL
|
148
|
+
@cache.forget_groups
|
75
149
|
@db.last_insert_row_id
|
76
150
|
end
|
77
151
|
|
152
|
+
# List all groups.
|
153
|
+
#
|
154
|
+
# Note that this list is cached in memory, so calling this method multiple
|
155
|
+
# times should be cheap.
|
156
|
+
#
|
157
|
+
# @return [Array[KStor::Model::Group]] a list of all groups in database
|
78
158
|
def groups
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
159
|
+
@cache.groups do
|
160
|
+
Log.debug('store: loading groups')
|
161
|
+
rows = @db.execute(<<-EOSQL)
|
162
|
+
SELECT id,
|
163
|
+
name,
|
164
|
+
pubk
|
165
|
+
FROM groups
|
166
|
+
ORDER BY name
|
167
|
+
EOSQL
|
168
|
+
rows.to_h do |r|
|
169
|
+
a = []
|
170
|
+
a << r['id']
|
171
|
+
a << Model::Group.new(
|
172
|
+
id: r['id'], name: r['name'], pubk: Crypto::PublicKey.new(r['pubk'])
|
173
|
+
)
|
174
|
+
a
|
175
|
+
end
|
96
176
|
end
|
97
177
|
end
|
98
178
|
|
179
|
+
# List all users.
|
180
|
+
#
|
181
|
+
# Note that this list is cached in memory, so calling this method multiple
|
182
|
+
# times should be cheap.
|
183
|
+
#
|
184
|
+
# @return [Array[KStor::Model::User]] a list of all users in database
|
99
185
|
def users
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
EOSQL
|
186
|
+
@cache.users do
|
187
|
+
Log.debug('store: loading users')
|
188
|
+
rows = @db.execute(<<-EOSQL)
|
189
|
+
SELECT u.id,
|
190
|
+
u.login,
|
191
|
+
u.name,
|
192
|
+
u.status,
|
193
|
+
c.pubk
|
194
|
+
FROM users u
|
195
|
+
LEFT JOIN users_crypto_data c ON (c.user_id = u.id)
|
196
|
+
ORDER BY u.login
|
197
|
+
EOSQL
|
113
198
|
|
114
|
-
|
199
|
+
users_from_resultset(rows)
|
200
|
+
end
|
115
201
|
end
|
116
202
|
|
117
|
-
#
|
118
|
-
#
|
119
|
-
#
|
120
|
-
#
|
121
|
-
#
|
122
|
-
# - public key
|
123
|
-
# - key derivation function parameters
|
124
|
-
# - encrypted private key
|
125
|
-
# - keychain: hash of:
|
126
|
-
# - group ID
|
127
|
-
# - encrypted group private key
|
203
|
+
# Lookup user by login.
|
204
|
+
#
|
205
|
+
# @param login [String] User login
|
206
|
+
# @return [KStor::Model::User, nil] a user object instance with encrypted
|
207
|
+
# private data, or nil if login was not found in database.
|
128
208
|
def user_by_login(login)
|
129
209
|
Log.debug("store: loading user by login #{login.inspect}")
|
130
210
|
rows = @db.execute(<<-EOSQL, login)
|
@@ -142,14 +222,11 @@ module KStor
|
|
142
222
|
user_from_resultset(rows, include_crypto_data: true)
|
143
223
|
end
|
144
224
|
|
145
|
-
#
|
146
|
-
#
|
147
|
-
#
|
148
|
-
#
|
149
|
-
#
|
150
|
-
# - public key
|
151
|
-
# - key derivation function parameters
|
152
|
-
# - encrypted private key
|
225
|
+
# Lookup user by ID.
|
226
|
+
#
|
227
|
+
# @param user_id [Integer] User ID
|
228
|
+
# @return [KStor::Model::User, nil] a user object instance with encrypted
|
229
|
+
# private data, or nil if login was not found in database.
|
153
230
|
def user_by_id(user_id)
|
154
231
|
Log.debug("store: loading user by ID ##{user_id}")
|
155
232
|
rows = @db.execute(<<-EOSQL, user_id)
|
@@ -167,12 +244,11 @@ module KStor
|
|
167
244
|
user_from_resultset(rows, include_crypto_data: true)
|
168
245
|
end
|
169
246
|
|
170
|
-
#
|
171
|
-
#
|
172
|
-
#
|
173
|
-
#
|
174
|
-
#
|
175
|
-
# - secret value and metadata author IDs
|
247
|
+
# List of all secrets that should be readable by a user.
|
248
|
+
#
|
249
|
+
# @param user_id [Integer] ID of user that will read the secrets
|
250
|
+
# @return [Array[KStor::Model::Secret]] A list of secrets, that may be
|
251
|
+
# empty.
|
176
252
|
def secrets_for_user(user_id)
|
177
253
|
Log.debug("store: loading secrets for user ##{user_id}")
|
178
254
|
rows = @db.execute(<<-EOSQL, user_id)
|
@@ -195,8 +271,12 @@ module KStor
|
|
195
271
|
rows.map { |r| secret_from_row(r) }
|
196
272
|
end
|
197
273
|
|
198
|
-
#
|
199
|
-
#
|
274
|
+
# Fetch one secret by it's ID.
|
275
|
+
#
|
276
|
+
# @param secret_id [Integer] ID of secret
|
277
|
+
# @param user_id [Integer] ID of secret reader
|
278
|
+
# @return [KStor::Model::Secret, nil] A secret, or nil if secret_id was not
|
279
|
+
# found or user_id can't read it.
|
200
280
|
def secret_fetch(secret_id, user_id)
|
201
281
|
Log.debug(
|
202
282
|
"store: loading secret value ##{secret_id} for user ##{user_id}"
|
@@ -221,14 +301,16 @@ module KStor
|
|
221
301
|
secret_from_row(rows.first)
|
222
302
|
end
|
223
303
|
|
224
|
-
#
|
225
|
-
#
|
226
|
-
#
|
227
|
-
#
|
228
|
-
#
|
229
|
-
#
|
230
|
-
#
|
231
|
-
#
|
304
|
+
# Create a new secret.
|
305
|
+
#
|
306
|
+
# Encrypted data should be a map of group_id to encrypted data for this
|
307
|
+
# group's key pair as a two-value array, first metadata and then value.
|
308
|
+
#
|
309
|
+
# @param author_id [Integer] ID of user that creates the new secret
|
310
|
+
# @param encrypted_data
|
311
|
+
# [Array[Hash[Integer, Array[KStor::Crypto::ArmoredValue]]]] see above
|
312
|
+
# description for shape of value.
|
313
|
+
# @return [Integer] ID of new secret
|
232
314
|
def secret_create(author_id, encrypted_data)
|
233
315
|
Log.debug("store: creating secret for user #{author_id}")
|
234
316
|
@db.execute(<<-EOSQL, author_id, author_id)
|
@@ -242,6 +324,10 @@ module KStor
|
|
242
324
|
secret_id
|
243
325
|
end
|
244
326
|
|
327
|
+
# List of group IDs that can read this secret.
|
328
|
+
#
|
329
|
+
# @param secret_id [Integer] ID of secret
|
330
|
+
# @return [Array[KStor::Model::Group]] list of group ids
|
245
331
|
def groups_for_secret(secret_id)
|
246
332
|
Log.debug("store: loading group IDs for secret #{secret_id}")
|
247
333
|
rows = @db.execute(<<-EOSQL, secret_id)
|
@@ -252,8 +338,13 @@ module KStor
|
|
252
338
|
rows.map { |r| r['group_id'] }
|
253
339
|
end
|
254
340
|
|
255
|
-
#
|
256
|
-
#
|
341
|
+
# Overwrite secret metadata.
|
342
|
+
#
|
343
|
+
# @param secret_id [Integer] ID of secret to update
|
344
|
+
# @param user_id [Integer] ID of user that changes metadata
|
345
|
+
# @param group_encrypted_metadata
|
346
|
+
# [Array[Hash[Integer, KStor::Crypt::ArmoredValue]]] map of group IDs to
|
347
|
+
# encrypted metadata.
|
257
348
|
def secret_setmeta(secret_id, user_id, group_encrypted_metadata)
|
258
349
|
Log.debug("store: set metadata for secret ##{secret_id}")
|
259
350
|
@db.execute(<<-EOSQL, user_id, secret_id)
|
@@ -269,6 +360,13 @@ module KStor
|
|
269
360
|
end
|
270
361
|
end
|
271
362
|
|
363
|
+
# Overwrite secret value.
|
364
|
+
#
|
365
|
+
# @param secret_id [Integer] ID of secret to update
|
366
|
+
# @param user_id [Integer] ID of user that changes the value
|
367
|
+
# @param group_ciphertexts
|
368
|
+
# [Array[Hash[Integer, KStor::Crypt::ArmoredValue]]] map of group IDs to
|
369
|
+
# encrypted values.
|
272
370
|
def secret_setvalue(secret_id, user_id, group_ciphertexts)
|
273
371
|
Log.debug("store: set value for secret ##{secret_id}")
|
274
372
|
@db.execute(<<-EOSQL, user_id, secret_id)
|
@@ -284,6 +382,9 @@ module KStor
|
|
284
382
|
end
|
285
383
|
end
|
286
384
|
|
385
|
+
# Delete a secrete from database.
|
386
|
+
#
|
387
|
+
# @param secret_id [Integer] ID of secret
|
287
388
|
def secret_delete(secret_id)
|
288
389
|
Log.debug("store: delete secret ##{secret_id}")
|
289
390
|
# Will cascade to secret_values:
|
@@ -318,6 +419,15 @@ module KStor
|
|
318
419
|
users.to_h { |uu| [uu.id, uu] }
|
319
420
|
end
|
320
421
|
|
422
|
+
# Create a new instance of {KStor::Model::User} from a query result rows.
|
423
|
+
#
|
424
|
+
# This will shift (consume) one row from provided array.
|
425
|
+
#
|
426
|
+
# @param rows [Array[Hash[String, Any]]] Array of query result rows
|
427
|
+
# @param include_crypto_data [Boolean] true if new user object should have
|
428
|
+
# encrypted private key data and keychain
|
429
|
+
# @return [Array[KStor::Model], nil] A new user object, or nil if there
|
430
|
+
# were no more rows
|
321
431
|
def user_from_resultset(rows, include_crypto_data: true)
|
322
432
|
return nil if rows.empty?
|
323
433
|
|
data/lib/kstor/systemd.rb
CHANGED
@@ -6,6 +6,9 @@ module KStor
|
|
6
6
|
# Collection of methods for systemd integration.
|
7
7
|
module Systemd
|
8
8
|
class << self
|
9
|
+
# Get main socket from systemd
|
10
|
+
#
|
11
|
+
# @return [nil,Socket] The socket or nil if systemd didn't provide
|
9
12
|
def socket
|
10
13
|
listen_pid = ENV['LISTEN_PID'].to_i
|
11
14
|
return nil unless Process.pid == listen_pid
|
@@ -13,10 +16,12 @@ module KStor
|
|
13
16
|
Socket.for_fd(3)
|
14
17
|
end
|
15
18
|
|
19
|
+
# Notify systemd that we're ready to serve clients.
|
16
20
|
def service_ready
|
17
21
|
SdNotify.ready
|
18
22
|
end
|
19
23
|
|
24
|
+
# Notify systemd that we're stopping.
|
20
25
|
def service_stopping
|
21
26
|
SdNotify.stopping
|
22
27
|
end
|
data/lib/kstor/version.rb
CHANGED
data/lib/kstor.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'kstor/log'
|
3
4
|
require 'kstor/config'
|
4
5
|
require 'kstor/store'
|
5
6
|
require 'kstor/controller'
|
6
7
|
require 'kstor/server'
|
7
|
-
require 'kstor/log'
|
8
8
|
require 'kstor/session'
|
9
9
|
require 'kstor/message'
|
10
10
|
require 'kstor/version'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kstor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jérémie Pierson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-11-
|
11
|
+
date: 2022-11-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: journald-logger
|
@@ -100,15 +100,36 @@ files:
|
|
100
100
|
- lib/kstor/config.rb
|
101
101
|
- lib/kstor/controller.rb
|
102
102
|
- lib/kstor/controller/authentication.rb
|
103
|
+
- lib/kstor/controller/base.rb
|
103
104
|
- lib/kstor/controller/request_handler.rb
|
104
105
|
- lib/kstor/controller/secret.rb
|
105
106
|
- lib/kstor/controller/users.rb
|
106
107
|
- lib/kstor/crypto.rb
|
108
|
+
- lib/kstor/crypto/armored_value.rb
|
107
109
|
- lib/kstor/crypto/ascii_armor.rb
|
108
110
|
- lib/kstor/crypto/keys.rb
|
109
111
|
- lib/kstor/error.rb
|
110
112
|
- lib/kstor/log.rb
|
113
|
+
- lib/kstor/log/simple_logger.rb
|
114
|
+
- lib/kstor/log/systemd_logger.rb
|
111
115
|
- lib/kstor/message.rb
|
116
|
+
- lib/kstor/message/base.rb
|
117
|
+
- lib/kstor/message/error.rb
|
118
|
+
- lib/kstor/message/group_create.rb
|
119
|
+
- lib/kstor/message/group_created.rb
|
120
|
+
- lib/kstor/message/ping.rb
|
121
|
+
- lib/kstor/message/pong.rb
|
122
|
+
- lib/kstor/message/secret_create.rb
|
123
|
+
- lib/kstor/message/secret_created.rb
|
124
|
+
- lib/kstor/message/secret_delete.rb
|
125
|
+
- lib/kstor/message/secret_deleted.rb
|
126
|
+
- lib/kstor/message/secret_list.rb
|
127
|
+
- lib/kstor/message/secret_search.rb
|
128
|
+
- lib/kstor/message/secret_unlock.rb
|
129
|
+
- lib/kstor/message/secret_update_meta.rb
|
130
|
+
- lib/kstor/message/secret_update_value.rb
|
131
|
+
- lib/kstor/message/secret_updated.rb
|
132
|
+
- lib/kstor/message/secret_value.rb
|
112
133
|
- lib/kstor/model.rb
|
113
134
|
- lib/kstor/server.rb
|
114
135
|
- lib/kstor/session.rb
|
@@ -117,11 +138,12 @@ files:
|
|
117
138
|
- lib/kstor/store.rb
|
118
139
|
- lib/kstor/systemd.rb
|
119
140
|
- lib/kstor/version.rb
|
120
|
-
homepage: https://
|
141
|
+
homepage: https://git.arlol.net/jpi/kstor
|
121
142
|
licenses:
|
122
143
|
- GPL-3.0-or-later
|
123
144
|
metadata:
|
124
145
|
rubygems_mfa_required: 'true'
|
146
|
+
allowed_push_host: https://rubygems.org/
|
125
147
|
post_install_message:
|
126
148
|
rdoc_options: []
|
127
149
|
require_paths:
|