MuranoCLI 3.1.0 → 3.2.0.beta.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|