openc3 7.0.0.pre.rc2 → 7.0.0.pre.rc3
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/bin/openc3cli +13 -4
- data/bin/pipinstall +6 -7
- data/bin/pipuninstall +3 -5
- data/data/config/widgets.yaml +10 -0
- data/lib/openc3/api/cmd_api.rb +2 -0
- data/lib/openc3/api/settings_api.rb +2 -0
- data/lib/openc3/microservices/queue_microservice.rb +6 -2
- data/lib/openc3/migrations/20241208080000_no_critical_cmd.rb +1 -1
- data/lib/openc3/migrations/20250402000000_periodic_only_default.rb +1 -1
- data/lib/openc3/migrations/20251022000000_remove_unique_id.rb +1 -1
- data/lib/openc3/migrations/20260203000000_remove_store_id.rb +28 -0
- data/lib/openc3/migrations/20260204000000_remove_decom_reducer.rb +27 -1
- data/lib/openc3/models/activity_model.rb +26 -6
- data/lib/openc3/models/auth_model.rb +54 -19
- data/lib/openc3/models/cvt_model.rb +79 -97
- data/lib/openc3/models/model.rb +16 -0
- data/lib/openc3/models/plugin_model.rb +18 -12
- data/lib/openc3/models/python_package_model.rb +2 -2
- data/lib/openc3/models/queue_model.rb +5 -3
- data/lib/openc3/models/target_model.rb +43 -8
- data/lib/openc3/models/tool_config_model.rb +12 -0
- data/lib/openc3/models/widget_model.rb +1 -7
- data/lib/openc3/script/web_socket_api.rb +1 -1
- data/lib/openc3/topics/command_topic.rb +1 -0
- data/lib/openc3/utilities/authentication.rb +46 -7
- data/lib/openc3/utilities/authorization.rb +8 -1
- data/lib/openc3/utilities/aws_bucket.rb +2 -3
- data/lib/openc3/utilities/questdb_client.rb +46 -0
- data/lib/openc3/version.rb +5 -5
- data/templates/tool_angular/package.json +1 -1
- data/templates/tool_vue/package.json +1 -1
- data/templates/widget/package.json +1 -1
- metadata +4 -18
- data/lib/openc3/migrations/20251213120000_reinstall_plugins.rb +0 -45
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 920a681652723ab469d3311e6f28332ea202068300c3a506d3314c552f5c0f30
|
|
4
|
+
data.tar.gz: 2f35277fa1e25eb30646a27e8f45365cb2fd39d7d3b24758baabf004e9fe54b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1cd5cb567a743e90742e76db3b4ab8498f34d8b6ba6dc0032e351bb6e3fdfbc4f8db826cc907d500f4670ef461bd49193be74e8264648b6a0f5908d090b7e6cf
|
|
7
|
+
data.tar.gz: 9af5921a88413af21113b00c0f99dcc023e70c21b4dfc9e7f0f8840fbe226765f86d80e82a7b874458468c45cea590a3e6b0ab2d702d29613818e19e48b473be
|
data/bin/openc3cli
CHANGED
|
@@ -1348,12 +1348,21 @@ if not ARGV[0].nil? # argument(s) given
|
|
|
1348
1348
|
exit 0
|
|
1349
1349
|
end
|
|
1350
1350
|
client = OpenC3::Bucket.getClient()
|
|
1351
|
-
ENV.
|
|
1352
|
-
|
|
1353
|
-
|
|
1351
|
+
if ENV.fetch('OPENC3_CLOUD', 'local') == 'local'
|
|
1352
|
+
# In local mode we want to create all buckets to ensure they exist
|
|
1353
|
+
# Cloud deployments will have created buckets during provisioning
|
|
1354
|
+
# so we only need to ensure the correct policies and permissions are in place
|
|
1355
|
+
ENV.map do |key, value|
|
|
1356
|
+
if key.match(/^OPENC3_(.+)_BUCKET$/) && !value.empty?
|
|
1357
|
+
client.create(value)
|
|
1358
|
+
end
|
|
1354
1359
|
end
|
|
1355
1360
|
end
|
|
1356
|
-
|
|
1361
|
+
# Unless explicitly disabled, ensure the tools bucket is public
|
|
1362
|
+
unless ENV.fetch("OPENC3_NO_BUCKET_POLICY", false)
|
|
1363
|
+
client.ensure_public(ENV['OPENC3_TOOLS_BUCKET'])
|
|
1364
|
+
end
|
|
1365
|
+
# Always ensure the scriptrunner policy is in place since it is required for script execution
|
|
1357
1366
|
client.ensure_scriptrunner_policy(ENV['OPENC3_CONFIG_BUCKET'], ENV['OPENC3_LOGS_BUCKET'])
|
|
1358
1367
|
|
|
1359
1368
|
when 'runmigrations'
|
data/bin/pipinstall
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
pip3 install "$@"
|
|
2
|
+
uv venv "$PYTHONUSERBASE"
|
|
3
|
+
echo "uv pip install $@"
|
|
4
|
+
uv pip install --python "$PYTHONUSERBASE" "$@"
|
|
6
5
|
if [ $? -eq 0 ]; then
|
|
7
6
|
echo "Command succeeded"
|
|
8
7
|
else
|
|
9
8
|
echo "Command failed - retrying with --no-index"
|
|
10
|
-
|
|
11
|
-
if [ $? -
|
|
12
|
-
echo "ERROR:
|
|
9
|
+
uv pip install --python "$PYTHONUSERBASE" --no-index "$@"
|
|
10
|
+
if [ $? -ne 0 ]; then
|
|
11
|
+
echo "ERROR: uv pip install failed"
|
|
13
12
|
fi
|
|
14
13
|
fi
|
data/bin/pipuninstall
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
echo "pip3 uninstall $@"
|
|
5
|
-
pip3 uninstall "$@"
|
|
2
|
+
echo "uv pip uninstall $@"
|
|
3
|
+
uv pip uninstall --python "$PYTHONUSERBASE" "$@"
|
|
6
4
|
if [ $? -eq 0 ]; then
|
|
7
5
|
echo "Command succeeded"
|
|
8
6
|
else
|
|
9
|
-
echo "ERROR:
|
|
7
|
+
echo "ERROR: uv pip uninstall failed"
|
|
10
8
|
fi
|
data/data/config/widgets.yaml
CHANGED
|
@@ -894,6 +894,16 @@ Telemetry Widgets:
|
|
|
894
894
|
END
|
|
895
895
|
LIMITSCOLOR INST HEALTH_STATUS TEMP2 # Default is label with just item name
|
|
896
896
|
LIMITSCOLOR INST HEALTH_STATUS TEMP3 CONVERTED 20 TRUE # Full TGT/PKT/ITEM label
|
|
897
|
+
LIMITSCOLOR INST HEALTH_STATUS TEMP4
|
|
898
|
+
SETTING ASTRO TRUE
|
|
899
|
+
settings:
|
|
900
|
+
ASTRO:
|
|
901
|
+
summary: Display Astro status icons instead of a colored circle
|
|
902
|
+
description:
|
|
903
|
+
When set, the LIMITSCOLOR renders an Astro (rux-status) icon whose shape reflects
|
|
904
|
+
the severity level, improving accessibility for colorblind users.
|
|
905
|
+
Limits colors are automatically mapped to Astro statuses
|
|
906
|
+
(GREEN to normal, RED to critical, YELLOW to caution, BLUE to standby).
|
|
897
907
|
VALUELIMITSBAR:
|
|
898
908
|
summary: Displays an item VALUE followed by LIMITSBAR
|
|
899
909
|
parameters:
|
data/lib/openc3/api/cmd_api.rb
CHANGED
|
@@ -73,6 +73,8 @@ module OpenC3
|
|
|
73
73
|
authorize(permission: 'admin', manual: manual, scope: scope, token: token)
|
|
74
74
|
SettingModel.set({ name: name, data: data }, scope: scope)
|
|
75
75
|
LocalMode.save_setting(scope, name, data)
|
|
76
|
+
username = user_info(token)['username'] || 'Anonymous'
|
|
77
|
+
Logger.info("User #{username} saved setting '#{name}': #{data}", scope: scope, user: username)
|
|
76
78
|
end
|
|
77
79
|
# save_setting is DEPRECATED
|
|
78
80
|
alias save_setting set_setting
|
|
@@ -71,10 +71,14 @@ module OpenC3
|
|
|
71
71
|
else
|
|
72
72
|
cmd_params = {}
|
|
73
73
|
end
|
|
74
|
-
|
|
74
|
+
validate = command.key?('validate') ? command['validate'] : true
|
|
75
|
+
timeout = command['timeout']
|
|
76
|
+
cmd(command['target_name'], command['cmd_name'], cmd_params, queue: false, validate: validate, timeout: timeout, scope: @scope)
|
|
75
77
|
elsif command['value']
|
|
76
78
|
# Legacy format: use single string parameter for backwards compatibility
|
|
77
|
-
|
|
79
|
+
validate = command.key?('validate') ? command['validate'] : true
|
|
80
|
+
timeout = command['timeout']
|
|
81
|
+
cmd(command['value'], queue: false, validate: validate, timeout: timeout, scope: @scope)
|
|
78
82
|
else
|
|
79
83
|
@logger.error "QueueProcessor: Invalid command format, missing required fields"
|
|
80
84
|
end
|
|
@@ -13,7 +13,7 @@ module OpenC3
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def self.run
|
|
16
|
-
ScopeModel.get_all_models(scope: nil).each do |scope,
|
|
16
|
+
ScopeModel.get_all_models(scope: nil).each do |scope, _scope_model|
|
|
17
17
|
model = MicroserviceModel.get_model(name: "#{scope}__CRITICALCMD__#{scope}", scope: scope)
|
|
18
18
|
if BASE # Only remove the critical command model if we're not enterprise
|
|
19
19
|
model.destroy if model
|
|
@@ -5,7 +5,7 @@ require 'openc3/models/microservice_model'
|
|
|
5
5
|
module OpenC3
|
|
6
6
|
class PeriodicOnlyDefault < Migration
|
|
7
7
|
def self.run
|
|
8
|
-
ScopeModel.get_all_models(scope: nil).each do |scope,
|
|
8
|
+
ScopeModel.get_all_models(scope: nil).each do |scope, _scope_model|
|
|
9
9
|
next if scope == 'DEFAULT'
|
|
10
10
|
model = MicroserviceModel.get_model(name: "#{scope}__SCOPEMULTI__#{scope}", scope: scope)
|
|
11
11
|
if model
|
|
@@ -5,7 +5,7 @@ require 'openc3/models/microservice_model'
|
|
|
5
5
|
module OpenC3
|
|
6
6
|
class RemoveUniqueId < Migration
|
|
7
7
|
def self.run
|
|
8
|
-
ScopeModel.get_all_models(scope: nil).each do |scope,
|
|
8
|
+
ScopeModel.get_all_models(scope: nil).each do |scope, _scope_model|
|
|
9
9
|
target_models = TargetModel.all(scope: scope)
|
|
10
10
|
target_models.each do |name, target_model|
|
|
11
11
|
target_model.delete("cmd_unique_id_mode")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require 'openc3/utilities/migration'
|
|
2
|
+
require 'openc3/models/scope_model'
|
|
3
|
+
require 'openc3/models/plugin_model'
|
|
4
|
+
|
|
5
|
+
module OpenC3
|
|
6
|
+
# Removes the store_id property from plugin models. It got renamed to
|
|
7
|
+
# store_plugin_id in PR #2858 but that also depends on having
|
|
8
|
+
# store_version_id, which didn't exist prior to this version and can't
|
|
9
|
+
# be determined without some introspection on the plugin and querying
|
|
10
|
+
# the app store (online). When store_plugin_id is unset, COSMOS treats
|
|
11
|
+
# the plugin like it was installed from a gem file.
|
|
12
|
+
class RemoveStoreId < Migration
|
|
13
|
+
def self.run
|
|
14
|
+
ScopeModel.get_all_models(scope: nil).each do |scope, _scope_model|
|
|
15
|
+
plugin_models = PluginModel.all(scope: scope)
|
|
16
|
+
plugin_models.each do |_name, plugin_model|
|
|
17
|
+
plugin_model.delete("store_id")
|
|
18
|
+
model = PluginModel.from_json(plugin_model, scope: scope)
|
|
19
|
+
model.update()
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
unless ENV['OPENC3_NO_MIGRATE']
|
|
27
|
+
OpenC3::RemoveStoreId.run
|
|
28
|
+
end
|
|
@@ -10,14 +10,16 @@
|
|
|
10
10
|
# if purchased from OpenC3, Inc.
|
|
11
11
|
|
|
12
12
|
require 'openc3/utilities/migration'
|
|
13
|
+
require 'openc3/utilities/bucket'
|
|
13
14
|
require 'openc3/models/scope_model'
|
|
14
15
|
require 'openc3/models/target_model'
|
|
15
16
|
require 'openc3/models/microservice_model'
|
|
17
|
+
require 'openc3/models/plugin_model'
|
|
16
18
|
|
|
17
19
|
module OpenC3
|
|
18
20
|
class RemoveDecomLogSettings < Migration
|
|
19
21
|
def self.run
|
|
20
|
-
ScopeModel.get_all_models(scope: nil).each do |scope,
|
|
22
|
+
ScopeModel.get_all_models(scope: nil).each do |scope, _scope_model|
|
|
21
23
|
target_models = TargetModel.all(scope: scope)
|
|
22
24
|
target_models.each do |name, target_model|
|
|
23
25
|
# Remove deprecated decom log settings from target model
|
|
@@ -51,6 +53,30 @@ module OpenC3
|
|
|
51
53
|
end
|
|
52
54
|
end
|
|
53
55
|
end
|
|
56
|
+
|
|
57
|
+
# Reinstall all plugins to regenerate microservice configs with correct settings
|
|
58
|
+
# This must happen after removing deprecated keys above
|
|
59
|
+
client = Bucket.getClient()
|
|
60
|
+
unless client.exist?(ENV['OPENC3_CONFIG_BUCKET']) && client.exist?(ENV['OPENC3_TOOLS_BUCKET'])
|
|
61
|
+
Logger.info("Skipping plugin reinstall - buckets do not exist yet (fresh install or new storage backend)")
|
|
62
|
+
return
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
ScopeModel.get_all_models(scope: nil).each do |scope, _scope_model|
|
|
66
|
+
plugins = PluginModel.all(scope: scope)
|
|
67
|
+
plugins.each do |plugin_name, plugin_data|
|
|
68
|
+
begin
|
|
69
|
+
Logger.info("Reinstalling plugin #{plugin_name} in scope #{scope}")
|
|
70
|
+
plugin_model = PluginModel.from_json(plugin_data, scope: scope)
|
|
71
|
+
plugin_model.undeploy
|
|
72
|
+
plugin_model.restore
|
|
73
|
+
Logger.info("Successfully reinstalled plugin #{plugin_name} in scope #{scope}")
|
|
74
|
+
rescue Exception => e
|
|
75
|
+
Logger.error("Error reinstalling plugin #{plugin_name} in scope #{scope}: #{e.formatted}")
|
|
76
|
+
# Continue with other plugins even if one fails
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
54
80
|
end
|
|
55
81
|
end
|
|
56
82
|
end
|
|
@@ -261,7 +261,7 @@ module OpenC3
|
|
|
261
261
|
|
|
262
262
|
# Update the Redis hash at primary_key and set the score equal to the start Epoch time
|
|
263
263
|
# the member is set to the JSON generated via calling as_json
|
|
264
|
-
def create(overlap: true)
|
|
264
|
+
def create(overlap: true, username: nil)
|
|
265
265
|
if @recurring['end'] and @recurring['frequency'] and @recurring['span']
|
|
266
266
|
# First validate the initial recurring activity ... all others are just offsets
|
|
267
267
|
validate_input(start: @start, stop: @stop, kind: @kind, data: @data)
|
|
@@ -290,7 +290,7 @@ module OpenC3
|
|
|
290
290
|
|
|
291
291
|
# Update @updated_at and add an event assuming it all completes ok
|
|
292
292
|
@updated_at = Time.now.to_nsec_from_epoch
|
|
293
|
-
add_event(status: 'created')
|
|
293
|
+
add_event(status: 'created', username: username)
|
|
294
294
|
|
|
295
295
|
Store.multi do |multi|
|
|
296
296
|
(@start..@recurring['end']).step(recurrence).each do |start_time|
|
|
@@ -326,7 +326,7 @@ module OpenC3
|
|
|
326
326
|
end
|
|
327
327
|
end
|
|
328
328
|
@updated_at = Time.now.to_nsec_from_epoch
|
|
329
|
-
add_event(status: 'created')
|
|
329
|
+
add_event(status: 'created', username: username)
|
|
330
330
|
Store.zadd(@primary_key, @start, JSON.generate(self.as_json, allow_nan: true))
|
|
331
331
|
notify(kind: 'created')
|
|
332
332
|
end
|
|
@@ -335,7 +335,7 @@ module OpenC3
|
|
|
335
335
|
# Update the Redis hash at primary_key and remove the current activity at the current score
|
|
336
336
|
# and update the score to the new score equal to the start Epoch time this uses a multi
|
|
337
337
|
# to execute both the remove and create.
|
|
338
|
-
def update(start:, stop:, kind:, data:, overlap: true)
|
|
338
|
+
def update(start:, stop:, kind:, data:, overlap: true, username: nil)
|
|
339
339
|
array = Store.zrangebyscore(@primary_key, @start, @start)
|
|
340
340
|
if array.length == 0
|
|
341
341
|
raise ActivityError.new "failed to find activity at: #{@start}"
|
|
@@ -350,10 +350,22 @@ module OpenC3
|
|
|
350
350
|
raise ActivityOverlapError.new "failed to update #{old_start}, no activities can overlap, collision: #{collision}"
|
|
351
351
|
end
|
|
352
352
|
end
|
|
353
|
+
|
|
354
|
+
# Compute changeset for audit trail before applying changes
|
|
355
|
+
changes = {}
|
|
356
|
+
diff_field(changes, 'start', @start, start)
|
|
357
|
+
diff_field(changes, 'stop', @stop, stop)
|
|
358
|
+
diff_field(changes, 'kind', @kind, kind)
|
|
359
|
+
old_data = @data.reject { |k, _| k == 'username' }
|
|
360
|
+
new_data = data.reject { |k, _| k == 'username' }
|
|
361
|
+
(old_data.keys | new_data.keys).each do |key|
|
|
362
|
+
diff_field(changes, "data.#{key}", old_data[key], new_data[key])
|
|
363
|
+
end
|
|
364
|
+
|
|
353
365
|
set_input(start: start, stop: stop, kind: kind, data: data, events: @events)
|
|
354
366
|
@updated_at = Time.now.to_nsec_from_epoch
|
|
355
367
|
|
|
356
|
-
add_event(status: 'updated')
|
|
368
|
+
add_event(status: 'updated', username: username, changes: changes.empty? ? nil : changes)
|
|
357
369
|
json = Store.zrangebyscore(@primary_key, old_start, old_start)
|
|
358
370
|
parsed = json.map { |value| JSON.parse(value, allow_nan: true, create_additions: true) }
|
|
359
371
|
parsed.each_with_index do |value, index|
|
|
@@ -397,14 +409,22 @@ module OpenC3
|
|
|
397
409
|
|
|
398
410
|
# add_event will make an event. This will NOT save the object to the redis database
|
|
399
411
|
# @param [String] status - the event status such as "queued" or "updated" or "created"
|
|
400
|
-
|
|
412
|
+
# @param [String] username - optional username of who performed the action
|
|
413
|
+
# @param [Hash] changes - optional hash describing what fields changed (for audit)
|
|
414
|
+
def add_event(status:, username: nil, changes: nil)
|
|
401
415
|
event = {
|
|
402
416
|
'time' => Time.now.to_i,
|
|
403
417
|
'event' => status
|
|
404
418
|
}
|
|
419
|
+
event['username'] = username if username
|
|
420
|
+
event['changes'] = changes if changes
|
|
405
421
|
@events << event
|
|
406
422
|
end
|
|
407
423
|
|
|
424
|
+
def diff_field(changes, field, old_val, new_val)
|
|
425
|
+
changes[field] = {'old' => old_val, 'new' => new_val} unless old_val == new_val
|
|
426
|
+
end
|
|
427
|
+
|
|
408
428
|
# update the redis stream / timeline topic that something has changed
|
|
409
429
|
def notify(kind:, extra: nil)
|
|
410
430
|
notification = {
|
|
@@ -41,6 +41,9 @@ module OpenC3
|
|
|
41
41
|
|
|
42
42
|
MIN_PASSWORD_LENGTH = 8
|
|
43
43
|
|
|
44
|
+
SESSION_PREFIX = "ses_"
|
|
45
|
+
OTP_PREFIX = "otp_"
|
|
46
|
+
|
|
44
47
|
def self.set?(key = PRIMARY_KEY)
|
|
45
48
|
Store.exists(key) == 1
|
|
46
49
|
end
|
|
@@ -58,36 +61,49 @@ module OpenC3
|
|
|
58
61
|
|
|
59
62
|
return false if service_only
|
|
60
63
|
|
|
61
|
-
|
|
64
|
+
mode = no_password ? :token : :any
|
|
65
|
+
return verify_no_service(token, mode: mode)
|
|
62
66
|
end
|
|
63
67
|
|
|
64
68
|
# Checks whether the provided token is a valid user password or session token.
|
|
65
69
|
# @param token [String] the plaintext password or session token to check (required)
|
|
66
|
-
# @param
|
|
70
|
+
# @param mode [String] optionally restrict verification to just the password or token. Valid values: :password, :token, or :any (default :token)
|
|
67
71
|
# @return [Boolean] whether the provided password/token is valid
|
|
68
|
-
def self.verify_no_service(token,
|
|
72
|
+
def self.verify_no_service(token, mode: :token)
|
|
73
|
+
modes = [:password, :token, :any]
|
|
74
|
+
raise ArgumentError, "Invalid mode '#{mode}': must be one of #{modes}" unless modes.include?(mode)
|
|
75
|
+
|
|
69
76
|
return false if token.nil? or token.empty?
|
|
70
77
|
|
|
71
78
|
# Check cached session tokens and password hash
|
|
72
79
|
time = Time.now
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
unless mode == :password
|
|
81
|
+
if @@session_cache and (time - @@session_cache_time) < SESSION_CACHE_TIMEOUT and @@session_cache[token]
|
|
82
|
+
terminate_otp(token)
|
|
83
|
+
return true
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Check stored session tokens
|
|
87
|
+
@@session_cache = Store.hgetall(SESSIONS_KEY)
|
|
88
|
+
@@session_cache_time = time
|
|
89
|
+
if @@session_cache[token]
|
|
90
|
+
terminate_otp(token)
|
|
91
|
+
return true
|
|
92
|
+
end
|
|
76
93
|
end
|
|
77
94
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
@@session_cache_time = time
|
|
81
|
-
return true if @@session_cache[token]
|
|
95
|
+
unless mode == :token
|
|
96
|
+
return true if @@pw_hash_cache and (time - @@pw_hash_cache_time) < PW_HASH_CACHE_TIMEOUT and Argon2::Password.verify_password(token, @@pw_hash_cache)
|
|
82
97
|
|
|
83
|
-
|
|
98
|
+
# Check stored password hash
|
|
99
|
+
pw_hash = Store.get(PRIMARY_KEY)
|
|
100
|
+
raise "invalid password hash" if pw_hash.nil? || !pw_hash.start_with?("$argon2") # Catch users who didn't run the migration utility when upgrading to COSMOS 7
|
|
101
|
+
@@pw_hash_cache = pw_hash
|
|
102
|
+
@@pw_hash_cache_time = time
|
|
103
|
+
return true if Argon2::Password.verify_password(token, @@pw_hash_cache)
|
|
104
|
+
end
|
|
84
105
|
|
|
85
|
-
|
|
86
|
-
pw_hash = Store.get(PRIMARY_KEY)
|
|
87
|
-
raise "invalid password hash" unless pw_hash.start_with?("$argon2") # Catch users who didn't run the migration utility when upgrading to COSMOS 7
|
|
88
|
-
@@pw_hash_cache = pw_hash
|
|
89
|
-
@@pw_hash_cache_time = time
|
|
90
|
-
return Argon2::Password.verify_password(token, @@pw_hash_cache)
|
|
106
|
+
return false
|
|
91
107
|
end
|
|
92
108
|
|
|
93
109
|
def self.set(password, old_password, key = PRIMARY_KEY)
|
|
@@ -96,17 +112,25 @@ module OpenC3
|
|
|
96
112
|
|
|
97
113
|
if set?(key)
|
|
98
114
|
raise "old_password must not be nil or empty" if old_password.nil? or old_password.empty?
|
|
99
|
-
raise "old_password incorrect" unless verify_no_service(old_password,
|
|
115
|
+
raise "old_password incorrect" unless verify_no_service(old_password, mode: :password)
|
|
100
116
|
end
|
|
101
117
|
pw_hash = Argon2::Password.create(password, profile: ARGON2_PROFILE)
|
|
102
118
|
Store.set(key, pw_hash)
|
|
103
119
|
@@pw_hash_cache = nil
|
|
104
120
|
@@pw_hash_cache_time = nil
|
|
121
|
+
logout
|
|
105
122
|
end
|
|
106
123
|
|
|
107
124
|
# Creates a new session token. DO NOT CALL BEFORE VERIFYING.
|
|
108
|
-
|
|
125
|
+
# @param otp [Boolean] whether to create a one-time use token (default: false)
|
|
126
|
+
# @return [String] the new session token
|
|
127
|
+
def self.generate_session(otp: false)
|
|
109
128
|
token = SecureRandom.urlsafe_base64(nil, false)
|
|
129
|
+
if otp
|
|
130
|
+
token = OTP_PREFIX + token
|
|
131
|
+
else
|
|
132
|
+
token = SESSION_PREFIX + token
|
|
133
|
+
end
|
|
110
134
|
Store.hset(SESSIONS_KEY, token, Time.now.iso8601)
|
|
111
135
|
return token
|
|
112
136
|
end
|
|
@@ -117,5 +141,16 @@ module OpenC3
|
|
|
117
141
|
@@session_cache = nil
|
|
118
142
|
@@session_cache_time = nil
|
|
119
143
|
end
|
|
144
|
+
|
|
145
|
+
# Terminates the given session token.
|
|
146
|
+
def self.terminate(token)
|
|
147
|
+
Store.hdel(SESSIONS_KEY, token)
|
|
148
|
+
@@session_cache.delete(token) if @@session_cache
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Terminates the session if the token is an OTP.
|
|
152
|
+
def self.terminate_otp(token)
|
|
153
|
+
terminate(token) if token.start_with?(OTP_PREFIX)
|
|
154
|
+
end
|
|
120
155
|
end
|
|
121
156
|
end
|