MuranoCLI 3.1.0 → 3.2.0.beta.1
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/.rubocop.yml +2 -0
- data/.trustme.sh +6 -2
- data/lib/MrMurano/Account.rb +8 -262
- data/lib/MrMurano/AccountBase.rb +214 -0
- data/lib/MrMurano/Business.rb +3 -19
- data/lib/MrMurano/Config-Migrate.rb +0 -4
- data/lib/MrMurano/Config.rb +159 -70
- data/lib/MrMurano/Content.rb +3 -2
- data/lib/MrMurano/Exchange.rb +3 -1
- data/lib/MrMurano/Gateway.rb +3 -2
- data/lib/MrMurano/HttpAuthed.rb +287 -0
- data/lib/MrMurano/Passwords.rb +82 -35
- data/lib/MrMurano/ReCommander.rb +2 -0
- data/lib/MrMurano/Solution.rb +3 -2
- data/lib/MrMurano/SyncRoot.rb +2 -0
- data/lib/MrMurano/Webservice.rb +3 -2
- data/lib/MrMurano/commands/business.rb +2 -2
- data/lib/MrMurano/commands/globals.rb +41 -0
- data/lib/MrMurano/commands/init.rb +1 -1
- data/lib/MrMurano/commands/login.rb +30 -6
- data/lib/MrMurano/commands/logs.rb +1 -6
- data/lib/MrMurano/commands/password.rb +15 -11
- data/lib/MrMurano/commands/show.rb +1 -1
- data/lib/MrMurano/commands/token.rb +209 -0
- data/lib/MrMurano/commands.rb +1 -0
- data/lib/MrMurano/http.rb +13 -24
- data/lib/MrMurano/version.rb +1 -1
- data/lib/MrMurano.rb +1 -0
- data/spec/Account_spec.rb +8 -8
- data/spec/Http_spec.rb +0 -1
- data/spec/cmd_token_spec.rb +56 -0
- data/spec/fixtures/websocket/README.rst +2 -2
- metadata +8 -4
data/lib/MrMurano/Config.rb
CHANGED
@@ -79,29 +79,44 @@ module MrMurano
|
|
79
79
|
|
80
80
|
CFG_SOLUTION_ID_KEYS = %w[application.id product.id].freeze
|
81
81
|
|
82
|
+
CFG_NET_PROTOCOLS = %w[https http].freeze
|
83
|
+
|
84
|
+
# If token not cached, set a very short timeout, to avoid database pollution.
|
85
|
+
CFG_AUTH_DEFAULT_TTL = 10
|
86
|
+
|
82
87
|
def migrate_old_env
|
83
88
|
return if ENV[CFG_OLD_ENV_NAME].nil?
|
84
|
-
warning %(
|
89
|
+
warning %(
|
90
|
+
ENV "#{CFG_OLD_ENV_NAME}" is no longer supported. Rename it to "#{CFG_ENV_NAME}"
|
91
|
+
).strip
|
85
92
|
unless ENV[CFG_ENV_NAME].nil?
|
86
|
-
warning %(
|
93
|
+
warning %(
|
94
|
+
Both "#{CFG_ENV_NAME}" and "#{CFG_OLD_ENV_NAME}" defined, please remove "#{CFG_OLD_ENV_NAME}".
|
95
|
+
).strip
|
87
96
|
end
|
88
97
|
ENV[CFG_ENV_NAME] = ENV[CFG_OLD_ENV_NAME]
|
89
98
|
end
|
90
99
|
|
91
100
|
def migrate_old_config(where)
|
101
|
+
migrate_warn_old_dir(where)
|
102
|
+
migrate_warn_old_cfg(where)
|
103
|
+
end
|
104
|
+
|
105
|
+
def migrate_warn_old_dir(where)
|
92
106
|
# Check for dir.
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
107
|
+
return unless (where + CFG_OLD_DIR_NAME).exist?
|
108
|
+
warning %(Moving old directory "#{CFG_OLD_DIR_NAME}" to "#{CFG_DIR_NAME}" in "#{where}")
|
109
|
+
(where + CFG_OLD_DIR_NAME).rename(where + CFG_DIR_NAME)
|
110
|
+
end
|
97
111
|
|
112
|
+
def migrate_warn_old_cfg(where)
|
98
113
|
# Check for cfg.
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
114
|
+
return unless (where + CFG_OLD_FILE_NAME).exist?
|
115
|
+
warning %(
|
116
|
+
Moving old config "#{CFG_OLD_FILE_NAME}" to "#{CFG_FILE_NAME}" in "#{where}"
|
117
|
+
).strip
|
118
|
+
(where + CFG_DIR_NAME).mkpath
|
119
|
+
(where + CFG_OLD_FILE_NAME).rename(where + CFG_FILE_NAME)
|
105
120
|
end
|
106
121
|
|
107
122
|
def initialize(cmd_runner=nil)
|
@@ -138,6 +153,10 @@ module MrMurano
|
|
138
153
|
# The user can exclude certain scopes.
|
139
154
|
@exclude_scopes = []
|
140
155
|
|
156
|
+
# Poor man's input validator, for string 'choice's.
|
157
|
+
@value_options = {}
|
158
|
+
@value_errors = 0
|
159
|
+
|
141
160
|
set_defaults
|
142
161
|
end
|
143
162
|
|
@@ -154,6 +173,15 @@ module MrMurano
|
|
154
173
|
|
155
174
|
set('net.host', CFG_HOST_BIZAPI, :defaults)
|
156
175
|
set('net.protocol', 'https', :defaults)
|
176
|
+
@value_options['net.protocol'] = CFG_NET_PROTOCOLS
|
177
|
+
|
178
|
+
set('auth.ttl', nil, :defaults)
|
179
|
+
# See global options --[no-]token and --[no-]basic.
|
180
|
+
set('auth.scheme-token', nil, :defaults)
|
181
|
+
set('auth.scheme-basic', nil, :defaults)
|
182
|
+
# See global options --[no-]login and --[no-]cache.
|
183
|
+
set('auth.persist-token', nil, :defaults)
|
184
|
+
set('auth.persist-basic', nil, :defaults)
|
157
185
|
|
158
186
|
set('location.base', @project_dir, :defaults) unless @project_dir.nil?
|
159
187
|
set('location.files', 'files', :defaults)
|
@@ -163,7 +191,9 @@ module MrMurano
|
|
163
191
|
set('location.resources', 'specs/resources.yaml', :defaults)
|
164
192
|
set('location.cors', 'cors.yaml', :defaults)
|
165
193
|
|
166
|
-
|
194
|
+
if defined? SyncRoot
|
195
|
+
set('sync.bydefault', SyncRoot.instance.bydefault.join(' '), :defaults)
|
196
|
+
end
|
167
197
|
|
168
198
|
set('files.default_page', 'index.html', :defaults)
|
169
199
|
set('files.searchFor', '**/*', :defaults)
|
@@ -382,6 +412,8 @@ module MrMurano
|
|
382
412
|
init_curl_file
|
383
413
|
# If user doesn't want paging, disable it.
|
384
414
|
program :help_paging, !$cfg['tool.no-page'] unless $cfg['tool.no-page'].nil?
|
415
|
+
# Check for errors.
|
416
|
+
must_be_valid_values!
|
385
417
|
end
|
386
418
|
|
387
419
|
## Load specified file into the config stack
|
@@ -480,6 +512,7 @@ module MrMurano
|
|
480
512
|
# If key isn't dotted, then assume the tool section.
|
481
513
|
ikey = section
|
482
514
|
section = 'tool'
|
515
|
+
key = "#{section}.#{ikey}"
|
483
516
|
end
|
484
517
|
|
485
518
|
paths = @paths.select { |p| scope == p.kind }
|
@@ -493,8 +526,11 @@ module MrMurano
|
|
493
526
|
cfg = paths.first
|
494
527
|
data = cfg.data
|
495
528
|
tomod = data[section]
|
496
|
-
|
497
|
-
|
529
|
+
if !value.nil?
|
530
|
+
change_value(key, tomod, ikey, value)
|
531
|
+
else
|
532
|
+
tomod.delete(ikey)
|
533
|
+
end
|
498
534
|
data[section] = tomod
|
499
535
|
# Remove empty sections to make test results more predictable.
|
500
536
|
# Interesting: IniFile.each only returns sections with key-vals,
|
@@ -509,6 +545,24 @@ module MrMurano
|
|
509
545
|
cfg.write
|
510
546
|
end
|
511
547
|
|
548
|
+
def change_value(key, section, ikey, value)
|
549
|
+
choices = @value_options[key]
|
550
|
+
if !choices.nil? && !choices.include?(value)
|
551
|
+
warning %(Invalid value #{fancy_ticks(value)} for key #{fancy_ticks(key)})
|
552
|
+
warning %( Choose from: #{choices.join(', ')})
|
553
|
+
@value_errors += 1
|
554
|
+
else
|
555
|
+
section[ikey] = value
|
556
|
+
end
|
557
|
+
end
|
558
|
+
|
559
|
+
def must_be_valid_values!
|
560
|
+
return unless @value_errors > 0
|
561
|
+
error('You must correct the errors in your config (listed above) to continue.')
|
562
|
+
@runner.command_exit = 1
|
563
|
+
exit 1
|
564
|
+
end
|
565
|
+
|
512
566
|
# key is <section>.<key>
|
513
567
|
def [](key)
|
514
568
|
get(key)
|
@@ -537,74 +591,101 @@ module MrMurano
|
|
537
591
|
CFG_SCOPES.each do |scope|
|
538
592
|
locats += "\n" unless first
|
539
593
|
first = false
|
594
|
+
locats += location_of_scope(scope)
|
595
|
+
end
|
596
|
+
locats
|
597
|
+
end
|
540
598
|
|
541
|
-
|
599
|
+
def location_of_scope(scope)
|
600
|
+
locat = ''
|
542
601
|
|
543
|
-
|
544
|
-
|
545
|
-
|
602
|
+
scope_q = fancy_ticks(scope)
|
603
|
+
scope_l = "Scope: #{scope_l}\n\n"
|
604
|
+
locat += Rainbow(scope_l).bright.underline
|
546
605
|
|
547
|
-
|
548
|
-
|
606
|
+
cfg_paths = @paths.select { |p| p.kind == scope }
|
607
|
+
if !cfg_paths.empty?
|
608
|
+
cfg = cfg_paths.first
|
609
|
+
locat += location_of_cfg(scope, cfg)
|
610
|
+
else
|
611
|
+
locat += location_not_found(scope, scope_q)
|
612
|
+
end
|
613
|
+
locat
|
614
|
+
end
|
549
615
|
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
616
|
+
def location_of_cfg(scope, cfg)
|
617
|
+
locat = ''
|
618
|
+
|
619
|
+
locat += location_path(scope, cfg)
|
620
|
+
locat += "\n"
|
621
|
+
|
622
|
+
locat_env, skip_content = location_env_and_skip?(scope, cfg)
|
623
|
+
locat += locat_env
|
624
|
+
return '' if skip_content
|
625
|
+
locat += "\n" if scope == :env
|
626
|
+
|
627
|
+
base = IniFile.new
|
628
|
+
base.merge! cfg.data
|
629
|
+
content = base.to_s
|
630
|
+
if !content.empty?
|
631
|
+
locat += "Config:\n\n"
|
632
|
+
base.to_s.split("\n").each do |line|
|
633
|
+
locat += ' ' + line + "\n"
|
634
|
+
end
|
635
|
+
else
|
636
|
+
config_l = "Config: Empty INI file.\n"
|
637
|
+
locat += config_l
|
638
|
+
end
|
639
|
+
|
640
|
+
locat
|
641
|
+
end
|
642
|
+
|
643
|
+
def location_path(scope, cfg)
|
644
|
+
scope_q = fancy_ticks(scope)
|
645
|
+
if !cfg.path.nil? && cfg.path.exist?
|
646
|
+
"Path: #{cfg.path}\n"
|
647
|
+
elsif %i[internal defaults].include? cfg.kind
|
648
|
+
# cfg.path is nil.
|
649
|
+
"Path: #{scope_q} config is not saved.\n"
|
650
|
+
else
|
651
|
+
"Path: #{scope_q} config does not exist.\n"
|
652
|
+
end
|
653
|
+
end
|
654
|
+
|
655
|
+
def location_env_and_skip?(scope, cfg)
|
656
|
+
locat_env = ''
|
657
|
+
skip_content = false
|
658
|
+
if scope == :env
|
659
|
+
locat_env += "Use the environment variable, MURANO_CONFIGFILE, to specify this config file.\n"
|
660
|
+
locat_env += "\n"
|
661
|
+
if ENV['MURANO_PASSWORD'].to_s.empty?
|
662
|
+
locat_env += "The MURANO_PASSWORD environ is not set.\n"
|
590
663
|
else
|
591
|
-
|
592
|
-
if scope != :specified
|
593
|
-
locats += Rainbow(msg).red.bright
|
594
|
-
else
|
595
|
-
locats += "Path: #{scope_q} config does not exist.\n\n"
|
596
|
-
locats += "Use --configfile to specify this config file.\n"
|
597
|
-
end
|
664
|
+
locat_env += "The MURANO_PASSWORD environ is set and will be used.\n"
|
598
665
|
end
|
666
|
+
skip_content = !cfg.path.exist?
|
599
667
|
end
|
600
|
-
|
668
|
+
[locat_env, skip_content]
|
669
|
+
end
|
670
|
+
|
671
|
+
def location_not_found(scope, scope_q)
|
672
|
+
locat = ''
|
673
|
+
config_l = "No config found for #{scope_q}.\n"
|
674
|
+
if scope != :specified
|
675
|
+
locat += Rainbow(config_l).red.bright
|
676
|
+
else
|
677
|
+
locat += "Path: #{scope_q} config does not exist.\n\n"
|
678
|
+
locat += "Use --configfile to specify this config file.\n"
|
679
|
+
end
|
680
|
+
locat
|
601
681
|
end
|
602
682
|
|
603
683
|
# To capture curl calls when running rspec, write to a file.
|
604
684
|
def init_curl_file
|
605
685
|
if self['tool.curldebug'] && !self['tool.curlfile'].to_s.strip.empty?
|
606
686
|
if @curlfile_f.nil?
|
607
|
-
|
687
|
+
open_curl_file
|
688
|
+
return if @curlfile_f.nil?
|
608
689
|
# MEH: Call @curlfile_f.close() at some point? Or let Ruby do on exit.
|
609
690
|
@curlfile_f << Time.now.to_s + "\n"
|
610
691
|
@curlfile_f << "murano #{ARGV.join(' ')}\n"
|
@@ -616,6 +697,14 @@ module MrMurano
|
|
616
697
|
end
|
617
698
|
end
|
618
699
|
|
700
|
+
def open_curl_file
|
701
|
+
@curlfile_f = File.open(self['tool.curlfile'], 'a')
|
702
|
+
rescue Errno::ENOENT => err
|
703
|
+
@curlfile_f = nil
|
704
|
+
warning("Unable to create curlfile at: #{self['tool.curlfile']}")
|
705
|
+
error(err.message)
|
706
|
+
end
|
707
|
+
|
619
708
|
# To set a different protocol://host, i.e., for local developing.
|
620
709
|
def set_net_host(url_or_host, scope=:internal)
|
621
710
|
return if url_or_host.nil?
|
data/lib/MrMurano/Content.rb
CHANGED
@@ -12,8 +12,8 @@ require 'mime/types'
|
|
12
12
|
require 'digest'
|
13
13
|
require 'http/form_data'
|
14
14
|
require 'MrMurano/Config'
|
15
|
-
require 'MrMurano/http'
|
16
15
|
require 'MrMurano/verbosing'
|
16
|
+
require 'MrMurano/AccountBase'
|
17
17
|
require 'MrMurano/SolutionId'
|
18
18
|
require 'MrMurano/SyncAllowed'
|
19
19
|
require 'MrMurano/SyncUpDown'
|
@@ -22,12 +22,13 @@ module MrMurano
|
|
22
22
|
## The details of talking to the Content service.
|
23
23
|
module Content
|
24
24
|
class Base
|
25
|
-
include
|
25
|
+
include AccountBase
|
26
26
|
include Verbose
|
27
27
|
include SolutionId
|
28
28
|
include SyncAllowed
|
29
29
|
|
30
30
|
def initialize
|
31
|
+
super
|
31
32
|
@solntype = 'product.id'
|
32
33
|
@uriparts_apidex = 1
|
33
34
|
init_api_id!
|
data/lib/MrMurano/Exchange.rb
CHANGED
@@ -5,12 +5,14 @@
|
|
5
5
|
# vim:tw=0:ts=2:sw=2:et:ai
|
6
6
|
# Unauthorized copying of this file is strictly prohibited.
|
7
7
|
|
8
|
+
require 'MrMurano/AccountBase'
|
8
9
|
require 'MrMurano/Exchange-Element'
|
10
|
+
require 'MrMurano/verbosing'
|
9
11
|
|
10
12
|
module MrMurano
|
11
13
|
# The Exchange class represents an end user's Murano IoT Exchange Elements.
|
12
14
|
class Exchange < Business
|
13
|
-
include
|
15
|
+
include AccountBase
|
14
16
|
include Verbose
|
15
17
|
|
16
18
|
def get(path='', query=nil, &block)
|
data/lib/MrMurano/Gateway.rb
CHANGED
@@ -11,8 +11,8 @@ require 'net/http'
|
|
11
11
|
require 'pathname'
|
12
12
|
require 'uri'
|
13
13
|
require 'MrMurano/hash'
|
14
|
-
require 'MrMurano/http'
|
15
14
|
require 'MrMurano/verbosing'
|
15
|
+
require 'MrMurano/AccountBase'
|
16
16
|
require 'MrMurano/Config'
|
17
17
|
require 'MrMurano/SolutionId'
|
18
18
|
require 'MrMurano/SyncRoot'
|
@@ -23,12 +23,13 @@ module MrMurano
|
|
23
23
|
# This is where interfacing to real hardware happens.
|
24
24
|
module Gateway
|
25
25
|
class GweBase
|
26
|
-
include
|
26
|
+
include AccountBase
|
27
27
|
include Verbose
|
28
28
|
include SolutionId
|
29
29
|
include SyncAllowed
|
30
30
|
|
31
31
|
def initialize
|
32
|
+
super
|
32
33
|
@solntype = 'product.id'
|
33
34
|
@uriparts_apidex = 1
|
34
35
|
init_api_id!
|
@@ -0,0 +1,287 @@
|
|
1
|
+
# Copyright © 2016-2017 Exosite LLC. All Rights Reserved
|
2
|
+
# License: PROPRIETARY. See LICENSE.txt.
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
# vim:tw=0:ts=2:sw=2:et:ai
|
6
|
+
# Unauthorized copying of this file is strictly prohibited.
|
7
|
+
|
8
|
+
require 'base64'
|
9
|
+
require 'singleton'
|
10
|
+
require 'MrMurano/http'
|
11
|
+
require 'MrMurano/verbosing'
|
12
|
+
require 'MrMurano/Config'
|
13
|
+
require 'MrMurano/Passwords'
|
14
|
+
|
15
|
+
module MrMurano
|
16
|
+
class HttpAuthed
|
17
|
+
# The tool only works for a single user. To avoid fetching the
|
18
|
+
# token multiple times (and to avoid having to pass an Account
|
19
|
+
# object around), we use a singleton [i.e., a nasty global!].
|
20
|
+
include Singleton
|
21
|
+
|
22
|
+
include Http
|
23
|
+
include Verbose
|
24
|
+
|
25
|
+
attr_accessor :login_info
|
26
|
+
attr_reader :token_biz
|
27
|
+
|
28
|
+
def initialize
|
29
|
+
@login_info = nil
|
30
|
+
@pass_file = nil
|
31
|
+
@token_file = nil
|
32
|
+
token_reset
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_headers(request)
|
36
|
+
# Note that calling super (Http.add_headers) is redundant,
|
37
|
+
# as AccountBase will have done it before calling us here.
|
38
|
+
super
|
39
|
+
return request if @logging_on
|
40
|
+
if $cfg['auth.scheme-basic']
|
41
|
+
ensure_password!
|
42
|
+
# encode64 adds newlines every 60 encoded characters.
|
43
|
+
encoded = Base64.encode64("#{user}:#{password}").delete("\n")
|
44
|
+
request['Authorization'] = "basic #{encoded}"
|
45
|
+
elsif $cfg['auth.scheme-token']
|
46
|
+
ensure_token!
|
47
|
+
request['Authorization'] = 'token ' + token unless token.to_s.empty?
|
48
|
+
else
|
49
|
+
# DEVS: If this happens, you may need to use 'authless'.
|
50
|
+
raise 'No authentication specified'
|
51
|
+
end
|
52
|
+
request
|
53
|
+
end
|
54
|
+
|
55
|
+
# ---------------------------------------------------------------------
|
56
|
+
|
57
|
+
def pass_file
|
58
|
+
return @pass_file unless @pass_file.nil?
|
59
|
+
pwd_path = $cfg.file_at(MrMurano::Passwords::FILENAME, :user)
|
60
|
+
@pass_file = MrMurano::Passwords.new(pwd_path)
|
61
|
+
@pass_file.load
|
62
|
+
@pass_file
|
63
|
+
end
|
64
|
+
|
65
|
+
def password_save(net_host, user_name, user_pass)
|
66
|
+
@password_user = user_pass
|
67
|
+
return unless $cfg['auth.persist-basic']
|
68
|
+
pass_file.set(net_host, user_name, user_pass)
|
69
|
+
pass_file.save
|
70
|
+
end
|
71
|
+
|
72
|
+
def password_get_or_ask(net_host, user_name)
|
73
|
+
user_pass = password(net_host, user_name)
|
74
|
+
if user_pass.nil?
|
75
|
+
user_pass = yield
|
76
|
+
password_save(net_host, user_name, user_pass)
|
77
|
+
end
|
78
|
+
user_pass
|
79
|
+
end
|
80
|
+
|
81
|
+
def password_remove(net_host, user_name)
|
82
|
+
pass_file.remove(net_host, user_name)
|
83
|
+
pass_file.save
|
84
|
+
end
|
85
|
+
|
86
|
+
def token_file
|
87
|
+
return @token_file unless @token_file.nil?
|
88
|
+
tok_path = $cfg.file_at(MrMurano::Tokens::FILENAME, :user)
|
89
|
+
@token_file = MrMurano::Tokens.new(tok_path)
|
90
|
+
@token_file.load
|
91
|
+
@token_file
|
92
|
+
end
|
93
|
+
|
94
|
+
# ---------------------------------------------------------------------
|
95
|
+
|
96
|
+
def get_host_user_pair(net_host, user_name)
|
97
|
+
net_host = host if net_host.to_s.empty?
|
98
|
+
user_name = user if user_name.to_s.empty?
|
99
|
+
[net_host, user_name]
|
100
|
+
end
|
101
|
+
|
102
|
+
def password(net_host=nil, user_name=nil)
|
103
|
+
return @password_user unless @password_user.to_s.empty?
|
104
|
+
net_host, user_name = get_host_user_pair(net_host, user_name)
|
105
|
+
@password_user = pass_file.get(net_host, user_name)
|
106
|
+
end
|
107
|
+
|
108
|
+
def ensure_password!
|
109
|
+
pwd = password(host, user)
|
110
|
+
return pwd unless pwd.to_s.empty?
|
111
|
+
error 'Missing password!'
|
112
|
+
exit 1
|
113
|
+
end
|
114
|
+
|
115
|
+
def token
|
116
|
+
return '' if defined?(@logging_on) && @logging_on
|
117
|
+
token_fetch_biz if @token_biz.to_s.empty?
|
118
|
+
@token_biz
|
119
|
+
end
|
120
|
+
|
121
|
+
def token_reset
|
122
|
+
@token_biz = ''
|
123
|
+
end
|
124
|
+
|
125
|
+
def ensure_token!
|
126
|
+
return token unless token.to_s.empty?
|
127
|
+
error 'Not logged in!'
|
128
|
+
exit 1
|
129
|
+
end
|
130
|
+
|
131
|
+
# ---------------------------------------------------------------------
|
132
|
+
|
133
|
+
def token_lookup
|
134
|
+
return @token_biz unless @token_biz.to_s.empty?
|
135
|
+
@token_biz = token_from_file
|
136
|
+
end
|
137
|
+
|
138
|
+
def token_from_file(net_host=nil, user_name=nil)
|
139
|
+
net_host, user_name = get_host_user_pair(net_host, user_name)
|
140
|
+
acc_token = token_file.lookup(net_host, user_name)
|
141
|
+
if acc_token.to_s.empty?
|
142
|
+
nil
|
143
|
+
elsif acc_token !~ /^[a-fA-F0-9]+$/
|
144
|
+
warning "Malformed '#{net_host}:#{user_name}' token: #{acc_token}"
|
145
|
+
nil
|
146
|
+
else
|
147
|
+
acc_token
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def token_save(net_host=nil, user_name=nil)
|
152
|
+
net_host, user_name = get_host_user_pair(net_host, user_name)
|
153
|
+
return unless $cfg['auth.persist-token']
|
154
|
+
token_file.set(net_host, user_name, @token_biz)
|
155
|
+
token_file.save
|
156
|
+
end
|
157
|
+
|
158
|
+
def token_remove(net_host=nil, user_name=nil)
|
159
|
+
net_host, user_name = get_host_user_pair(net_host, user_name)
|
160
|
+
token_file.remove(net_host, user_name)
|
161
|
+
token_file.save
|
162
|
+
end
|
163
|
+
|
164
|
+
def token_forget(net_host=nil, user_name=nil, keep_password: false)
|
165
|
+
net_host, user_name = get_host_user_pair(net_host, user_name)
|
166
|
+
password_remove(net_host, user_name) unless keep_password
|
167
|
+
token_remove(net_host, user_name)
|
168
|
+
end
|
169
|
+
|
170
|
+
def token_resolve
|
171
|
+
token_old = @token_biz
|
172
|
+
token_old = ENV['MURANO_TOKEN'] if token_old.to_s.empty?
|
173
|
+
token_old = token_from_file if token_old.to_s.empty?
|
174
|
+
token_old
|
175
|
+
end
|
176
|
+
|
177
|
+
# ---------------------------------------------------------------------
|
178
|
+
|
179
|
+
def token_fetch_biz
|
180
|
+
@logging_on = true
|
181
|
+
|
182
|
+
token_old = token_resolve
|
183
|
+
token_reset
|
184
|
+
|
185
|
+
# If token found, verify it works.
|
186
|
+
unless token_old.to_s.empty?
|
187
|
+
get('token/' + token_old) do |request, http|
|
188
|
+
http.request(request) do |response|
|
189
|
+
if response.is_a?(Net::HTTPSuccess)
|
190
|
+
# response.body is, e.g., "{\"email\":\"xxx@yyy.zzz\",\"ttl\":172800}"
|
191
|
+
@token_biz = token_old
|
192
|
+
elsif response.is_a?(Net::HTTPNotFound)
|
193
|
+
# Token expired!
|
194
|
+
token_filed = token_from_file
|
195
|
+
token_remove if token_filed == token_old
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
unless @token_biz.to_s.empty?
|
200
|
+
# Token should already be saved.
|
201
|
+
@logging_on = false
|
202
|
+
return
|
203
|
+
end
|
204
|
+
# else, token was rejected.
|
205
|
+
end
|
206
|
+
|
207
|
+
creds = @login_info
|
208
|
+
if @login_info.nil?
|
209
|
+
if $cfg['auth.scheme-token']
|
210
|
+
# Using token auth, but no creds, so user probably needs to logon.
|
211
|
+
@logging_on = false
|
212
|
+
return
|
213
|
+
end
|
214
|
+
# This would be a developer error.
|
215
|
+
raise 'No @login_info'
|
216
|
+
end
|
217
|
+
|
218
|
+
MrMurano::Verbose.whirly_start('Logging in...')
|
219
|
+
post('token/', creds) do |request, http|
|
220
|
+
http.request(request) do |response|
|
221
|
+
reply = JSON.parse(response.body, json_opts)
|
222
|
+
if response.is_a?(Net::HTTPSuccess)
|
223
|
+
@token_biz = reply[:token]
|
224
|
+
elsif response.is_a?(Net::HTTPConflict) && reply[:message] == 'twofactor'
|
225
|
+
MrMurano::Verbose.whirly_interject do
|
226
|
+
# Prompt user for emailed code.
|
227
|
+
token_fetch_2fa(creds)
|
228
|
+
end
|
229
|
+
else
|
230
|
+
showHttpError(request, response)
|
231
|
+
error 'Check to see if username and password are correct.'
|
232
|
+
unless ENV['MURANO_PASSWORD'].to_s.empty?
|
233
|
+
pwd_path = $cfg.file_at('passwords', :user)
|
234
|
+
warning %(
|
235
|
+
NOTE: MURANO_PASSWORD specifies password; it was not read from #{pwd_path}
|
236
|
+
).strip
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
if @token_biz.to_s.empty?
|
243
|
+
keep_password = true
|
244
|
+
token_forget(keep_password: keep_password)
|
245
|
+
else
|
246
|
+
token_save
|
247
|
+
end
|
248
|
+
MrMurano::Verbose.whirly_stop
|
249
|
+
@logging_on = false
|
250
|
+
end
|
251
|
+
|
252
|
+
def token_fetch_2fa(creds)
|
253
|
+
error 'Two-factor Authentication'
|
254
|
+
warning 'A verification code has been sent to your email.'
|
255
|
+
code = ask('Please enter the code here to continue: ').strip
|
256
|
+
unless code =~ /^[a-fA-F0-9]+$/
|
257
|
+
error 'Expected token to contain only numbers and hexadecimal letters.'
|
258
|
+
exit 1
|
259
|
+
end
|
260
|
+
MrMurano::Verbose.whirly_start('Verifying code...')
|
261
|
+
|
262
|
+
path = 'key/' + code
|
263
|
+
|
264
|
+
response = get(path)
|
265
|
+
# Response is, e.g., {
|
266
|
+
# purpose: "twofactor",
|
267
|
+
# status: "exists",
|
268
|
+
# email: "xxx@yyy.zzz",
|
269
|
+
# bizid: null,
|
270
|
+
# businessName: null, }
|
271
|
+
return if response.nil?
|
272
|
+
|
273
|
+
response = post(path, password: creds[:password])
|
274
|
+
# Response is, e.g., { "token": "..." }
|
275
|
+
return if response.nil?
|
276
|
+
|
277
|
+
@token_biz = response[:token]
|
278
|
+
|
279
|
+
MrMurano::Verbose.whirly_stop
|
280
|
+
|
281
|
+
warning %(
|
282
|
+
When finished, run `murano token delete` or `murano logout` to clear your token.
|
283
|
+
).strip
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|