kstor 0.4.2 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|