conjur-cli 4.28.2 → 4.29.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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.dockerignore +8 -0
  3. data/.gitignore +2 -0
  4. data/.overcommit.yml +10 -0
  5. data/.rubocop.yml +14 -0
  6. data/CHANGELOG.md +16 -0
  7. data/Dockerfile +10 -0
  8. data/Gemfile +2 -0
  9. data/Rakefile +1 -1
  10. data/acceptance-features/audit/audit_event_send.feature +46 -43
  11. data/acceptance-features/audit/send.feature +0 -19
  12. data/acceptance-features/authentication/login.feature +0 -2
  13. data/acceptance-features/authentication/logout.feature +0 -3
  14. data/acceptance-features/authorization/resource/check.feature +6 -4
  15. data/acceptance-features/authorization/resource/create.feature +4 -2
  16. data/acceptance-features/authorization/resource/exists.feature +8 -6
  17. data/acceptance-features/authorization/resource/give.feature +3 -1
  18. data/acceptance-features/authorization/resource/show.feature +3 -1
  19. data/acceptance-features/authorization/role/graph.feature +0 -1
  20. data/acceptance-features/conjurenv/check.feature +3 -10
  21. data/acceptance-features/conjurenv/run.feature +3 -3
  22. data/acceptance-features/conjurenv/template.feature +1 -1
  23. data/acceptance-features/directory/hostfactory/create.feature +13 -0
  24. data/acceptance-features/directory/hostfactory/tokens.feature +16 -0
  25. data/acceptance-features/directory/layer/retire.feature +43 -0
  26. data/acceptance-features/directory/user/update_password.feature +0 -1
  27. data/acceptance-features/directory/variable/value.feature +3 -2
  28. data/acceptance-features/dsl/policy_owner.feature +21 -7
  29. data/acceptance-features/dsl/resource_owner.feature +4 -4
  30. data/acceptance-features/pubkeys/add.feature +4 -2
  31. data/acceptance-features/pubkeys/names.feature +6 -3
  32. data/acceptance-features/pubkeys/show.feature +4 -2
  33. data/acceptance-features/step_definitions/{cli.rb → cli_steps.rb} +18 -4
  34. data/acceptance-features/step_definitions/user_steps.rb +13 -12
  35. data/acceptance-features/support/env.rb +0 -1
  36. data/acceptance-features/support/hooks.rb +11 -14
  37. data/acceptance-features/support/world.rb +16 -18
  38. data/build-deb.sh +19 -0
  39. data/ci/test.sh +19 -0
  40. data/conjur.gemspec +9 -12
  41. data/debify.sh +4 -0
  42. data/distrib/bin/_conjur +3 -0
  43. data/distrib/bin/conjur +3 -0
  44. data/distrib/bin/conjurize +3 -0
  45. data/distrib/bin/jsonfield +3 -0
  46. data/features/conjurize.feature +25 -25
  47. data/features/support/env.rb +5 -1
  48. data/features/support/hooks.rb +0 -1
  49. data/jenkins.sh +29 -1
  50. data/lib/conjur/cli.rb +27 -4
  51. data/lib/conjur/command.rb +36 -0
  52. data/lib/conjur/command/audit.rb +12 -0
  53. data/lib/conjur/command/bootstrap.rb +5 -9
  54. data/lib/conjur/command/host_factories.rb +187 -0
  55. data/lib/conjur/command/hosts.rb +82 -2
  56. data/lib/conjur/command/layers.rb +28 -0
  57. data/lib/conjur/command/resources.rb +1 -0
  58. data/lib/conjur/command/rspec/mock_services.rb +1 -1
  59. data/lib/conjur/command/server.rb +67 -0
  60. data/lib/conjur/command/users.rb +67 -12
  61. data/lib/conjur/command/variables.rb +101 -14
  62. data/lib/conjur/conjurize.rb +25 -69
  63. data/lib/conjur/conjurize/script.rb +133 -0
  64. data/lib/conjur/version.rb +1 -1
  65. data/publish.sh +6 -0
  66. data/spec/command/elevate_spec.rb +1 -1
  67. data/spec/command/host_factories_spec.rb +38 -0
  68. data/spec/command/hosts_spec.rb +86 -22
  69. data/spec/command/users_spec.rb +51 -3
  70. data/spec/command/variable_expiration_spec.rb +174 -0
  71. data/spec/command/variables_spec.rb +1 -1
  72. data/spec/conjurize_spec.rb +70 -0
  73. metadata +61 -64
@@ -2,14 +2,7 @@ require 'methadone'
2
2
  require 'json'
3
3
  require 'open-uri'
4
4
  require 'conjur/version.rb'
5
-
6
- def latest_conjur_release
7
- url = 'https://api.github.com/repos/conjur-cookbooks/conjur/releases'
8
- resp = open(url)
9
- json = JSON.parse(resp.read)
10
- latest = json[0]['assets'].select {|asset| asset['name'] =~ /conjur-v\d.\d.\d.tar.gz/}[0]
11
- latest['browser_download_url']
12
- end
5
+ require "conjur/conjurize/script"
13
6
 
14
7
  module Conjur
15
8
  class Conjurize
@@ -31,77 +24,37 @@ DESC
31
24
  else
32
25
  STDIN.read
33
26
  end
34
- host = JSON.parse input
35
27
 
36
- login = host['id'] or raise "No 'id' field in host JSON"
37
- api_key = host['api_key'] or raise "No 'api_key' field in host JSON"
28
+ puts generate JSON.parse input
29
+ end
30
+
31
+ def self.generate host
32
+ config = configuration host
38
33
 
39
- require 'conjur/cli'
34
+ if options[:json]
35
+ JSON.dump config
36
+ else
37
+ Script.generate config, options
38
+ end
39
+ end
40
+
41
+ def self.apply_client_config
42
+ require "conjur/cli"
40
43
  if conjur_config = options[:c]
41
44
  Conjur::Config.load [ conjur_config ]
42
45
  else
43
46
  Conjur::Config.load
44
47
  end
45
48
  Conjur::Config.apply
49
+ end
46
50
 
47
- conjur_cookbook_url = conjur_run_list = nil
48
-
49
- conjur_run_list = options[:"conjur-run-list"]
50
- conjur_cookbook_url = options[:"conjur-cookbook-url"]
51
- chef_executable = options[:"chef-executable"]
52
-
53
- if options[:ssh]
54
- conjur_run_list ||= "conjur"
55
- conjur_cookbook_url ||= latest_conjur_release()
56
- end
57
-
58
- sudo = lambda{|str|
59
- [ options[:sudo] ? "sudo -n" : nil, str ].compact.join(" ")
60
- }
61
-
62
- header = <<-HEADER
63
- #!/bin/sh
64
- set -e
65
-
66
- # Implementation note: 'tee' is used as a sudo-friendly 'cat' to populate a file with the contents provided below.
67
- HEADER
68
-
69
- configure_conjur = <<-CONFIGURE
70
- #{sudo.call 'tee'} /etc/conjur.conf > /dev/null << CONJUR_CONF
71
- account: #{Conjur.configuration.account}
72
- appliance_url: #{Conjur.configuration.appliance_url}
73
- cert_file: /etc/conjur-#{Conjur.configuration.account}.pem
74
- netrc_path: /etc/conjur.identity
75
- plugins: []
76
- CONJUR_CONF
77
-
78
- #{sudo.call 'tee'} /etc/conjur-#{Conjur.configuration.account}.pem > /dev/null << CONJUR_CERT
79
- #{File.read(Conjur.configuration.cert_file).strip}
80
- CONJUR_CERT
81
-
82
- #{sudo.call 'tee'} /etc/conjur.identity > /dev/null << CONJUR_IDENTITY
83
- machine #{Conjur.configuration.appliance_url}/authn
84
- login host/#{login}
85
- password #{api_key}
86
- CONJUR_IDENTITY
87
- #{sudo.call 'chmod'} 0600 /etc/conjur.identity
88
- CONFIGURE
89
-
90
- install_chef = if conjur_cookbook_url && !chef_executable
91
- %Q(curl -L https://www.opscode.com/chef/install.sh | #{sudo.call 'bash'})
92
- else
93
- nil
94
- end
95
-
96
- chef_executable ||= "chef-solo"
97
-
98
- run_chef = if conjur_cookbook_url
99
- %Q(#{sudo.call "#{chef_executable} -r #{conjur_cookbook_url} -o #{conjur_run_list}"})
100
- else
101
- nil
102
- end
51
+ def self.configuration host
52
+ apply_client_config
103
53
 
104
- puts [ header, configure_conjur, install_chef, run_chef ].compact.join("\n")
54
+ host.merge \
55
+ "account" => Conjur.configuration.account,
56
+ "appliance_url" => Conjur.configuration.appliance_url,
57
+ "certificate" => File.read(Conjur.configuration.cert_file).strip
105
58
  end
106
59
 
107
60
  on("-c CONJUR_CONFIG_FILE", "Overrides defaults (CONJURRC env var, ~/.conjurrc, /etc/conjur.conf).")
@@ -111,5 +64,8 @@ CONJUR_IDENTITY
111
64
  on("--sudo", "Indicates that all commands should be run via 'sudo'.")
112
65
  on("--conjur-cookbook-url NAME", "Overrides the default Chef cookbook URL for Conjur SSH.")
113
66
  on("--conjur-run-list RUNLIST", "Overrides the default Chef run list for Conjur SSH.")
67
+ on \
68
+ "--json",
69
+ "Don't generate the script, instead just dump the configuration as JSON"
114
70
  end
115
71
  end
@@ -0,0 +1,133 @@
1
+ require "json"
2
+ require "open-uri"
3
+
4
+ class Conjur::Conjurize
5
+ # generates a shell script to conjurize a host
6
+ class Script
7
+ COOKBOOK_RELEASES_URL =
8
+ "https://api.github.com/repos/conjur-cookbooks/conjur/releases".freeze
9
+
10
+ def self.latest_conjur_cookbook_release
11
+ json = JSON.parse open(COOKBOOK_RELEASES_URL).read
12
+ tarballs = json[0]["assets"].select do |asset|
13
+ asset["name"] =~ /conjur-v\d.\d.\d.tar.gz/
14
+ end
15
+ tarballs.first["browser_download_url"]
16
+ end
17
+
18
+ HEADER = <<-HEADER.freeze
19
+ #!/bin/sh
20
+ set -e
21
+
22
+ # Implementation note: 'tee' is used as a sudo-friendly 'cat' to populate a file with the contents provided below.
23
+ HEADER
24
+
25
+ def initialize options
26
+ @options = options
27
+ end
28
+
29
+ attr_reader :options
30
+
31
+ def sudo
32
+ @sudo ||= options["sudo"] ? ->(x) { "sudo -n #{x}" } : ->(x) { x }
33
+ end
34
+
35
+ # Generate a piece of shell to write to a file
36
+ # @param path [String] absolute path to write to
37
+ # @param content [String] contents to write
38
+ # @option options [String, Fixnum] :mode mode to apply to the file
39
+ def write_file path, content, options = {}
40
+ [
41
+ ((mode = options[:mode]) && set_mode(path, mode)),
42
+ [sudo["tee"], path, "> /dev/null << EOF"].join(" "),
43
+ content.strip,
44
+ "EOF\n"
45
+ ].compact.join("\n")
46
+ end
47
+
48
+ def set_mode path, mode
49
+ mode = mode.to_s(8) if mode.respond_to? :to_int
50
+ [
51
+ [sudo["touch"], path].join(" "),
52
+ [sudo["chmod"], mode, path].join(" ")
53
+ ].join("\n")
54
+ end
55
+
56
+ def self.generate configuration, options
57
+ new(options).generate configuration
58
+ end
59
+
60
+ def install_chef?
61
+ run_chef? && !options[:"chef-executable"]
62
+ end
63
+
64
+ def run_chef?
65
+ options.values_at(:ssh, :"conjur-run-list").any?
66
+ end
67
+
68
+ def chef_executable
69
+ options[:"chef-executable"] || "chef-solo"
70
+ end
71
+
72
+ def conjur_cookbook_url
73
+ options[:"conjur-cookbook-url"] || Script.latest_conjur_cookbook_release
74
+ end
75
+
76
+ def conjur_run_list
77
+ options[:"conjur-run-list"] || "conjur"
78
+ end
79
+
80
+ def chef_script
81
+ @chef_script ||= [
82
+ ("curl -L https://www.opscode.com/chef/install.sh | " + sudo["bash"] \
83
+ if install_chef?),
84
+ (sudo["#{chef_executable} -r #{conjur_cookbook_url} " \
85
+ "-o #{conjur_run_list}"] if run_chef?)
86
+ ].join "\n"
87
+ end
88
+
89
+ def self.rc configuration
90
+ [
91
+ "account: #{configuration['account']}",
92
+ "appliance_url: #{configuration['appliance_url']}",
93
+ "cert_file: /etc/conjur-#{configuration['account']}.pem",
94
+ "netrc_path: /etc/conjur.identity",
95
+ "plugins: []"
96
+ ].join "\n"
97
+ end
98
+
99
+ def self.identity configuration
100
+ """
101
+ machine #{configuration['appliance_url']}/authn
102
+ login host/#{configuration['id']}
103
+ password #{configuration['api_key']}
104
+ """
105
+ end
106
+
107
+ def configure_conjur configuration
108
+ [
109
+ write_file("/etc/conjur.conf", Script.rc(configuration)),
110
+ write_file(
111
+ "/etc/conjur-#{configuration['account']}.pem",
112
+ configuration["certificate"]
113
+ ),
114
+ write_file(
115
+ "/etc/conjur.identity",
116
+ Script.identity(configuration),
117
+ mode: 0600
118
+ )
119
+ ].join "\n"
120
+ end
121
+
122
+ def generate configuration
123
+ fail "No 'id' field in host JSON" unless configuration["id"]
124
+ fail "No 'api_key' field in host JSON" unless configuration["api_key"]
125
+
126
+ [
127
+ HEADER,
128
+ configure_conjur(configuration),
129
+ chef_script
130
+ ].join("\n")
131
+ end
132
+ end
133
+ end
@@ -19,6 +19,6 @@
19
19
  # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
20
  #
21
21
  module Conjur
22
- VERSION = "4.28.2"
22
+ VERSION = "4.29.0"
23
23
  ::Version=VERSION
24
24
  end
data/publish.sh ADDED
@@ -0,0 +1,6 @@
1
+ #!/bin/bash -ex
2
+
3
+ export DEBUG=true
4
+ export GLI_DEBUG=true
5
+
6
+ debify publish 4.6 cli
@@ -17,7 +17,7 @@ describe Conjur::Command::Elevate do
17
17
 
18
18
  expect(RestClient::Request).to receive(:execute).with({
19
19
  method: :get,
20
- url: "https://core.example.com/users/alice",
20
+ url: "https://core.example.com/api/users/alice",
21
21
  username: "dknuth",
22
22
  headers: {:authorization=>"Token token=\"eyJsb2dpbiI6ImRrbnV0aCJ9\"", x_conjur_privilege: "elevate"}
23
23
  }).and_return(double(:response, body: "[]"))
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+ require 'conjur/command/host_factories'
3
+
4
+ describe Conjur::Command::HostFactories, :logged_in => true do
5
+ let (:group_memberships) { double(:group_memberships, :roleid => 'the-account:group:security_admin') }
6
+ let (:group) { double(:group, :exists? => true, :memberships => [group_memberships]) }
7
+ let (:layer_members) { double(:layer_members, :member => double(:member, :roleid => 'the-account:group:security_admin'), :admin_option => true ) }
8
+ let (:layer_role) { double(:layer_role, :members => [layer_members]) }
9
+ let (:layer) { double(:layer, :exists? => true, :role => layer_role) }
10
+
11
+ before do
12
+ allow(Conjur::Command.api).to receive(:role).with("the-account:group:the-group").and_return group
13
+ allow(Conjur::Command.api).to receive(:layer).with("layer1").and_return layer
14
+ end
15
+
16
+ describe_command 'hostfactory:create --as-group the-group --layer layer1 hf1 ' do
17
+
18
+ it 'calls api.create_host_factory and prints the results' do
19
+ expect_any_instance_of(Conjur::API).to receive(:create_host_factory).and_return '{}'
20
+ expect { invoke }.to write('{}')
21
+ end
22
+ end
23
+
24
+ context 'command-line errors' do
25
+ describe_command 'hostfactory:create hf1' do
26
+ it "fails without owner" do
27
+ expect {invoke}.to raise_error('Use --as-group or --as-role to indicate the host factory role')
28
+ end
29
+ end
30
+ describe_command 'hostfactory:create --as-group the-group hf' do
31
+ it "fails without layer" do
32
+ expect {invoke}.to raise_error('Provide at least one layer')
33
+ end
34
+ end
35
+
36
+ end
37
+
38
+ end
@@ -1,30 +1,94 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Conjur::Command::Hosts, logged_in: true do
4
- let(:collection_url) { "https://core.example.com/hosts" }
5
-
6
- describe_command "host:create" do
7
- it "lets the server assign the id" do
8
- expect(RestClient::Request).to receive(:execute).with({
9
- method: :post,
10
- url: collection_url,
11
- headers: {},
12
- payload: {}
13
- }).and_return(post_response('assigned-id'))
14
-
15
- expect { invoke }.to write({ id: 'assigned-id' }).to(:stdout)
4
+ let(:collection_url) { "https://core.example.com/api/hosts" }
5
+
6
+ context "creating a host" do
7
+ let(:new_host) { double("new-host") }
8
+
9
+ describe_command "host:create" do
10
+ it "lets the server assign the id" do
11
+ expect(RestClient::Request).to receive(:execute).with({
12
+ method: :post,
13
+ url: collection_url,
14
+ headers: {},
15
+ payload: {}
16
+ }).and_return(post_response('assigned-id'))
17
+
18
+ expect { invoke }.to write({ id: 'assigned-id' }).to(:stdout)
19
+ end
20
+ end
21
+ describe_command "host:create the-id" do
22
+ it "propagates the user-assigned id" do
23
+ expect(RestClient::Request).to receive(:execute).with({
24
+ method: :post,
25
+ url: collection_url,
26
+ headers: {},
27
+ payload: { id: 'the-id' }
28
+ }).and_return(post_response('the-id'))
29
+
30
+ expect { invoke }.to write({ id: 'the-id' }).to(:stdout)
31
+ end
32
+ end
33
+ describe_command "host:create --cidr 192.168.1.1,127.0.0.0/32" do
34
+ it "Creates a host with specified CIDR" do
35
+ expect_any_instance_of(Conjur::API).to receive(:create_host).with(
36
+ { cidr: ['192.168.1.1', '127.0.0.0/32'] }
37
+ ).and_return new_host
38
+ invoke
39
+ end
40
+ end
41
+ describe_command "host:create --as-group security_admin --cidr 192.168.1.1,127.0.0.0/32" do
42
+ it "Creates a host with specified CIDR" do
43
+ expect(api).to receive(:group).with("security_admin").and_return(double(:group, roleid: "the-account:group:security_admin"))
44
+ expect(api).to receive(:role).with("the-account:group:security_admin").and_return(double(:group_role, exists?: true))
45
+ expect_any_instance_of(Conjur::API).to receive(:create_host).with(
46
+ { ownerid: "the-account:group:security_admin", cidr: ['192.168.1.1', '127.0.0.0/32'] }
47
+ ).and_return new_host
48
+ invoke
49
+ end
16
50
  end
17
51
  end
18
- describe_command "host:create the-id" do
19
- it "propagates the user-assigned id" do
20
- expect(RestClient::Request).to receive(:execute).with({
21
- method: :post,
22
- url: collection_url,
23
- headers: {},
24
- payload: { id: 'the-id' }
25
- }).and_return(post_response('the-id'))
26
-
27
- expect { invoke }.to write({ id: 'the-id' }).to(:stdout)
52
+
53
+ context "updating host attributes" do
54
+ describe_command "host update --cidr 127.0.0.0/32 the-user" do
55
+ it "updates the CIDR" do
56
+ stub_host = double()
57
+ expect_any_instance_of(Conjur::API).to receive(:host).with("the-user").and_return stub_host
58
+ expect(stub_host).to receive(:update).with(cidr: ['127.0.0.0/32']).and_return ""
59
+ expect { invoke }.to write "Host updated"
60
+ end
61
+ end
62
+
63
+ describe_command "host update --cidr all the-user" do
64
+ it "resets the CIDR restrictions" do
65
+ stub_host = double()
66
+ expect_any_instance_of(Conjur::API).to receive(:host).with("the-user").and_return stub_host
67
+ expect(stub_host).to receive(:update).with(cidr: []).and_return ""
68
+ expect { invoke }.to write "Host updated"
69
+ end
70
+ end
71
+ end
72
+
73
+ context 'rotating api key' do
74
+ describe_command 'host rotate_api_key --host redis001' do
75
+ before do
76
+ expect(RestClient::Request).to receive(:execute).with({
77
+ method: :head,
78
+ url: 'https://core.example.com/api/hosts/redis001',
79
+ headers: {}
80
+ }).and_return true
81
+ expect(RestClient::Request).to receive(:execute).with({
82
+ method: :put,
83
+ url: 'https://authn.example.com/users/api_key?id=host%2Fredis001',
84
+ headers: {},
85
+ payload: ''
86
+ }).and_return double(:response, body: 'new api key')
87
+ end
88
+
89
+ it 'puts with basic auth' do
90
+ invoke
91
+ end
28
92
  end
29
93
  end
30
94
  end
@@ -1,7 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Conjur::Command::Users, logged_in: true do
4
- let(:create_user_url) { "https://core.example.com/users" }
4
+ let(:create_user_url) { "https://core.example.com/api/users" }
5
5
  let(:update_password_url) { "https://authn.example.com/users/password" }
6
6
 
7
7
  context "creating a user" do
@@ -31,16 +31,41 @@ describe Conjur::Command::Users, logged_in: true do
31
31
  invoke
32
32
  end
33
33
  end
34
+ describe_command "#{cmd} --cidr 192.168.1.1,127.0.0.0/32 the-user" do
35
+ it "Creates a user with specified CIDR" do
36
+ expect_any_instance_of(Conjur::API).to receive(:create_user).with(
37
+ "the-user", { cidr: ['192.168.1.1', '127.0.0.0/32'] }
38
+ ).and_return new_user
39
+ invoke
40
+ end
41
+ end
34
42
  end
35
43
  end
36
44
 
37
- context "updating UID number" do
45
+ context "updating user attributes" do
38
46
  describe_command "user update --uidnumber 12345 the-user" do
39
47
  it "updates the uidnumber" do
40
48
  stub_user = double()
41
49
  expect_any_instance_of(Conjur::API).to receive(:user).with("the-user").and_return stub_user
42
50
  expect(stub_user).to receive(:update).with(uidnumber: 12345).and_return ""
43
- expect { invoke }.to write "UID set"
51
+ expect { invoke }.to write "User updated"
52
+ end
53
+ end
54
+ describe_command "user update --cidr 127.0.0.0/32 the-user" do
55
+ it "updates the CIDR" do
56
+ stub_user = double()
57
+ expect_any_instance_of(Conjur::API).to receive(:user).with("the-user").and_return stub_user
58
+ expect(stub_user).to receive(:update).with(cidr: ['127.0.0.0/32']).and_return ""
59
+ expect { invoke }.to write "User updated"
60
+ end
61
+ end
62
+
63
+ describe_command "user update --cidr all the-user" do
64
+ it "resets the CIDR restrictions" do
65
+ stub_user = double()
66
+ expect_any_instance_of(Conjur::API).to receive(:user).with("the-user").and_return stub_user
67
+ expect(stub_user).to receive(:update).with(cidr: []).and_return ""
68
+ expect { invoke }.to write "User updated"
44
69
  end
45
70
  end
46
71
  end
@@ -81,4 +106,27 @@ describe Conjur::Command::Users, logged_in: true do
81
106
  end
82
107
  end
83
108
  end
109
+
110
+ context 'rotating api key' do
111
+ describe_command 'user rotate_api_key' do
112
+ before do
113
+ expect(RestClient::Request).to receive(:execute).with({
114
+ method: :put,
115
+ url: 'https://authn.example.com/users/api_key',
116
+ user: username,
117
+ password: api_key,
118
+ headers: {},
119
+ payload: ''
120
+ }).and_return double(:response, body: 'new api key')
121
+ expect(Conjur::Authn).to receive(:save_credentials).with({
122
+ username: username,
123
+ password: 'new api key'
124
+ })
125
+ end
126
+
127
+ it 'puts with basic auth' do
128
+ invoke
129
+ end
130
+ end
131
+ end
84
132
  end