conjur-cli 4.24.0 → 4.25.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f4a996c0914a820ab371451e111785dc061c89a7
4
- data.tar.gz: 3469ca2580a5df324247818f181515e530d4bc03
3
+ metadata.gz: 9327368d238b90717af3151f2fc1a2091ff4f051
4
+ data.tar.gz: a90f6d8e898919557b9b20f54435a5b07d508f61
5
5
  SHA512:
6
- metadata.gz: c73f490d57c0dbd9cfc0c69aab11d6832442be416c29cc4b147d048669ca7c0914bdeda8ed6f82f8bde7d171ca955680b201af0685f879ba85334de850e0438b
7
- data.tar.gz: b4a2f624525b425cfbfbb69eb03f2b04fd84cbe02772f47ac4dbe14371860aa764dfaa69151e8cc0a021108388ed7c491420cdd16a6944bf539b673c13f03dee
6
+ metadata.gz: d3b6e29c9c849478d5a67e50d0d59b6e5973dced0d58010d00fbdd1c2dc5287911a9f308c5b9ba9bc6320ea6fedfcb9e098b53582227b1231e95b3693d2a6bb1
7
+ data.tar.gz: a7a9a8fb315d6fd1dd089e6e0e3cd2330ea98f8ccb37f555eea056d09a5d258660b9b6f793f475951390d2de0c597e01ec04f339dd2e4a670d14c78e93b526be
data/CHANGELOG.md CHANGED
@@ -1,4 +1,13 @@
1
- # Unreleased
1
+ # 4.25.0
2
+
3
+ * A record can be retired to a specific role, in addition to the default behavior of retiring to the `attic` user.
4
+ * Variable can be created with the id only, without becoming interactive
5
+ * Run `conjur variable create -i -a` to create interactively with annotations
6
+ * Interactive annotation can be performed on bare resources with `conjur resource annotate -i`.
7
+ * Don't require 'admin' user to bootstrap, prompt to create a new security admin during bootstrap
8
+ * Check if user privileges are sufficient before running `retire`
9
+ * Don't revoke a user's access to a record in the middle of retire, because doing so leads to 403 errors later on.
10
+ * Interactive mode of user, group and pubkey creation
2
11
 
3
12
  # 4.24.0
4
13
 
data/PUBLISH.md ADDED
@@ -0,0 +1,26 @@
1
+ # Publishing the CLI
2
+
3
+ We distribute the Conjur CLI as a package for Ubuntu, Centos, OSX and also as a rubygem.
4
+
5
+ Steps to publish a new version of the CLI:
6
+
7
+ 1. Update `VERSION` in [lib/conjur/version.rb](lib/conjur/version.rb)
8
+ 2. Update the [CHANGELOG.md](CHANGELOG.md) with any changes for this version
9
+ 3. Commit these changes with the message `"v#{VERSION}"`, where `VERSION` = the new version
10
+ 4. Go to the specific build page for the commit [in Jenkins](https://jenkins.conjur.net/job/cli-ruby/)
11
+ 5. In the left sidebar, open `Promotion Status`
12
+ 6. Click `Approve` for the "rubygems" promotion and wait for it to finish
13
+ 7. Click `Approve` for the "packages" promotion, this will kick off the [omnibus-conjur](https://jenkins.conjur.net/job/omnibus-conjur/) build flow [1](#ref1).
14
+ 8. Download the [deb](https://jenkins.conjur.net/job/omnibus-conjur-ubuntu/), [rpm](https://jenkins.conjur.net/job/omnibus-conjur-centos/) and [pkg](https://jenkins.conjur.net/job/omnibus-conjur-osx/) packages from their build pages in Jenkins.
15
+ 9. Move the downloaded files to the `pkg` folder in your local [omnibus-conjur](https://github.com/conjurinc/omnibus-conjur) project.
16
+ 10. In the `omnibus-conjur` project, upload each file to S3 with `./publish pkg/<filename>`.
17
+ 11. Update the links on the [CLI page](https://github.com/conjurinc/developer-www/blob/master/app/views/pages/cli/index.html.haml) for the devsite.
18
+ 12. Promote the devsite to production [in Jenkins](https://jenkins.conjur.net/job/developer-www/) [2](#ref2).
19
+
20
+ ---
21
+
22
+ <a id="ref1">1</a>:
23
+ The packages promotion depends on the new gem version being published.
24
+
25
+ <a id="ref2">2</a>
26
+ After deploy it will take a few minutes for the cache to update.
data/lib/conjur/authn.rb CHANGED
@@ -91,6 +91,8 @@ module Conjur::Authn
91
91
  write_credentials
92
92
  end
93
93
 
94
+ alias save_credentials fetch_credentials
95
+
94
96
  def write_credentials
95
97
  netrc[host] = @credentials
96
98
  netrc.save
data/lib/conjur/cli.rb CHANGED
@@ -113,12 +113,12 @@ module Conjur
113
113
  group = Conjur::Command.api.group(as_group)
114
114
  role = Conjur::Command.api.role(group.roleid)
115
115
  exit_now!("Group '#{as_group}' doesn't exist, or you don't have permission to use it") unless role.exists?
116
- options[:"ownerid"] = group.roleid
116
+ options[:ownerid] = group.roleid
117
117
  end
118
118
  if as_role = options.delete(:"as-role")
119
119
  role = Conjur::Command.api.role(as_role)
120
120
  exit_now!("Role '#{as_role}' does not exist, or you don't have permission to use it") unless role.exists?
121
- options[:"ownerid"] = role.roleid
121
+ options[:ownerid] = role.roleid
122
122
  end
123
123
 
124
124
  true
@@ -18,6 +18,8 @@
18
18
  # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19
19
  # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
20
  #
21
+ require 'base64'
22
+
21
23
  module Conjur
22
24
  class Command
23
25
  extend Conjur::IdentifierManipulation
@@ -42,13 +44,23 @@ module Conjur
42
44
  def api
43
45
  @@api ||= Conjur::Authn.connect
44
46
  end
47
+
48
+ def current_user
49
+ username = api.username
50
+ kind, id = username.split('/')
51
+ unless kind && id
52
+ id = kind
53
+ kind = 'user'
54
+ end
55
+ api.send(kind, username)
56
+ end
45
57
 
46
58
  # Prevent a deprecated command from being displayed in the help output
47
59
  def hide_docs(command)
48
60
  def command.nodoc; true end
49
61
  end
50
62
 
51
- def acting_as_option(command)
63
+ def acting_as_option command
52
64
  return if command.flags.member?(:"as-group") # avoid duplicate flags
53
65
  command.arg_name 'Perform all actions as the specified Group'
54
66
  command.flag [:"as-group"]
@@ -57,6 +69,45 @@ module Conjur
57
69
  command.flag [:"as-role"]
58
70
  end
59
71
 
72
+ def interactive_option command
73
+ command.arg_name 'interactive'
74
+ command.desc 'Create variable interactively'
75
+ command.switch [:i, :'interactive']
76
+ end
77
+
78
+ def annotate_option command
79
+ command.arg_name 'annotate'
80
+ command.desc 'Add variable annotations interactively'
81
+ command.switch [:a, :annotate]
82
+ end
83
+
84
+ def prompt_for_annotations
85
+ highline.say('Add annotations (a name and value for each one):')
86
+ {}.tap do |annotations|
87
+ until (name = highline.ask(' annotation name (press enter to quit annotations): ')).empty?
88
+ annotations[name] = read_till_eof(' annotation value (^D on its own line to finish):')
89
+ end
90
+ end
91
+ end
92
+
93
+ def highline
94
+ require 'highline'
95
+ @highline ||= HighLine.new($stdin,$stderr)
96
+ end
97
+
98
+ def read_till_eof(prompt = nil)
99
+ highline.say(prompt) if prompt
100
+ [].tap do |lines|
101
+ loop do
102
+ begin
103
+ lines << highline.ask('')
104
+ rescue EOFError
105
+ break
106
+ end
107
+ end
108
+ end.join("\n")
109
+ end
110
+
60
111
  def command_options_for_list(c)
61
112
  return if c.flags.member?(:role) # avoid duplicate flags
62
113
  c.desc "Role to act as. By default, the current logged-in role is used."
@@ -100,6 +151,53 @@ module Conjur
100
151
  end
101
152
  end
102
153
 
154
+ def validate_privileges message, &block
155
+ valid = begin
156
+ yield
157
+ rescue RestClient::Forbidden
158
+ false
159
+ end
160
+ exit_now! message unless valid
161
+ end
162
+
163
+ def retire_options command
164
+ command.arg_name 'role'
165
+ command.desc "Specify a role to give the retired record to (default: the 'attic' user)"
166
+ command.long_desc %Q(When retired, all a record's roles and permissions are revoked.
167
+
168
+ As a final step, the record is 'given' (e.g. 'conjur resource give') to a destination role.
169
+ The default role to receive the record is the user 'attic'. This option can be used to specify
170
+ an alternative destination role.)
171
+ command.flag [:d, :"destination-role"]
172
+ end
173
+
174
+ def destination_role options
175
+ destination = options[:"destination-role"]
176
+ if destination
177
+ api.role(destination)
178
+ else
179
+ api.user('attic')
180
+ end
181
+ end
182
+
183
+ def validate_retire_privileges record, options
184
+ if record.respond_to?(:role)
185
+ memberships = current_user.role.memberships.map(&:roleid)
186
+ validate_privileges "You can't administer this record" do
187
+ # The current user has a role which is admin of the record's role
188
+ record.role.members.find{|m| memberships.member?(m.member.roleid) && m.admin_option}
189
+ end
190
+ end
191
+
192
+ validate_privileges "You don't own the record" do
193
+ # The current user has the role which owns the record's resource
194
+ current_user.role.member_of?(record.resource.ownerid)
195
+ end
196
+
197
+ role = destination_role(options)
198
+ exit_now! "Destination role '#{role.roleid}' doesn't exist" unless role.exists?
199
+ end
200
+
103
201
  def retire_resource obj
104
202
  obj.resource.attributes['permissions'].each do |p|
105
203
  role = api.role(p['role'])
@@ -111,13 +209,36 @@ module Conjur
111
209
  end
112
210
 
113
211
  def retire_role obj
114
- obj.role.members.each do |r|
212
+ members = obj.role.members
213
+ # Move the invoking role to the end of the roles list, so that it doesn't
214
+ # lose its permissions in the middle of this operation.
215
+ # I'm sure there's a cleaner way to do this.
216
+ self_member = members.select{|m| m.member.roleid == current_user.role.roleid}
217
+ self_member.each do |m|
218
+ members.delete m
219
+ end
220
+ members.concat self_member if self_member
221
+ members.each do |r|
115
222
  member = api.role(r.member)
116
223
  puts "Revoking from role #{member.roleid}"
117
224
  obj.role.revoke_from member
118
225
  end
119
226
  end
120
227
 
228
+ def give_away_resource obj, options
229
+ destination = options[:"destination-role"]
230
+ destination_role = if destination
231
+ api.role(destination)
232
+ else
233
+ api.user('attic')
234
+ end
235
+
236
+ exit_now! "Role #{destination_role.roleid} doesn't exist" unless destination_role.exists?
237
+
238
+ puts "Giving ownership to '#{destination_role.roleid}'"
239
+ obj.resource.give_to destination_role
240
+ end
241
+
121
242
  def display_members(members, options)
122
243
  result = if options[:V]
123
244
  members.collect {|member|
@@ -148,6 +269,106 @@ module Conjur
148
269
  puts str
149
270
  end
150
271
 
272
+ def prompt_to_confirm kind, properties
273
+ puts
274
+ puts "A new #{kind} will be created with the following properties:"
275
+ puts
276
+ properties.select{|k,v| !v.blank?}.each do |k,v|
277
+ printf "%-10s: %s\n", k, v
278
+ end
279
+ puts
280
+
281
+ exit(0) unless %w(yes y).member?(highline.ask("Proceed? (yes/no): ").strip.downcase)
282
+ end
283
+
284
+ def integer? v
285
+ Integer(v, 10) rescue false
286
+ end
287
+
288
+ def prompt_for_id kind, label = 'id'
289
+ highline.ask("Enter the #{label}: ") do |q|
290
+ q.readline = true
291
+ q.validate = lambda{|id|
292
+ !id.blank? && !api.send(kind, id).exists?
293
+ }
294
+ q.responses[:not_valid] = "<% if @answer.blank? %>"\
295
+ "#{label} cannot be blank<% else %>"\
296
+ "A #{kind} called '<%= @answer %>' already exists<% end %>"
297
+ end
298
+ end
299
+
300
+ def prompt_for_public_key
301
+ public_key = highline.ask("Enter the public key (press enter to skip): ") do |q|
302
+ q.validate = lambda{|key|
303
+ if key.blank?
304
+ true
305
+ else
306
+ validate_public_key key
307
+ end
308
+ }
309
+ q.responses[:not_valid] = "Public key format is invalid; please try again"
310
+ end
311
+ public_key.blank? ? nil : public_key.strip
312
+ end
313
+
314
+ # http://serverfault.com/questions/453296/how-do-i-validate-a-rsa-ssh-public-key-file-id-rsa-pub
315
+ def validate_public_key key
316
+ if system('which ssh-keygen 2>&1 > /dev/null')
317
+ Conjur.log.debug "Using ssh-keygen to verify the public key\n" if Conjur.log
318
+ require 'tempfile'
319
+ tempfile = Tempfile.new 'public_key'
320
+ tempfile.write(key)
321
+ tempfile.close
322
+ `ssh-keygen -l -f #{tempfile.path}`
323
+ $? == 0
324
+ else
325
+ Conjur.log.debug "ssh-keygen is not available; falling back to simple string testing\n" if Conjur.log
326
+ # Should be a line with at least 2 components,
327
+ # first one being the algo id and second a base64 string.
328
+ # In principle this means:
329
+ # Base64.strict_decode64 key.strip[/\Assh-\w+ (\S+).*/, 1]
330
+
331
+ # Since the pubkeys service is more strict: needs a name and
332
+ # rejects ones with a space, instead reproduce its algorithm here.
333
+ begin
334
+ components = key.strip.split ' '
335
+ Base64.strict_decode64 components[1]
336
+ components.length == 3
337
+ rescue NoMethodError, ArgumentError
338
+ false
339
+ end
340
+ end
341
+ end
342
+
343
+ def prompt_for_group options = {}
344
+ options[:hint] ||= "press enter to own the record yourself"
345
+ group_ids = api.groups.map(&:id)
346
+
347
+ highline.ask("Enter the group which will own the record (#{options[:hint]}): ", [ "" ] + group_ids) do |q|
348
+ require 'readline'
349
+ Readline.completion_append_character = ""
350
+ Readline.completer_word_break_characters = ""
351
+
352
+ q.readline = true
353
+ q.validate = lambda{|id|
354
+ @group = nil
355
+ id.empty? || (@group = api.group(id)).exists?
356
+ }
357
+ q.responses[:not_valid] = "Group '<%= @answer %>' doesn't exist, or you don't have permission to use it"
358
+ end
359
+ @group ? @group.roleid : nil
360
+ end
361
+
362
+ def prompt_for_idnumber label
363
+ result = highline.ask("Enter a #{label}: ") do |q|
364
+ q.validate = lambda{|id|
365
+ id.blank? || integer?(id)
366
+ }
367
+ q.responses[:not_valid] = "The #{label} must be an integer"
368
+ end
369
+ result.blank? ? nil : result.to_i
370
+ end
371
+
151
372
  def prompt_for_password
152
373
  require 'highline'
153
374
  # use stderr to allow output redirection, e.g.
@@ -155,6 +376,14 @@ module Conjur
155
376
  hl = HighLine.new($stdin, $stderr)
156
377
 
157
378
  password = hl.ask("Enter the password (it will not be echoed): "){ |q| q.echo = false }
379
+ if password.blank?
380
+ if hl.agree "No password (y/n)?"
381
+ return nil
382
+ else
383
+ return prompt_for_password
384
+ end
385
+ end
386
+
158
387
  confirmation = hl.ask("Confirm the password: "){ |q| q.echo = false }
159
388
 
160
389
  raise "Password does not match confirmation" unless password == confirmation
@@ -21,12 +21,61 @@
21
21
 
22
22
  class Conjur::Command::Bootstrap < Conjur::Command
23
23
  desc "Create initial users, groups, and permissions"
24
+ long_desc %Q(When you launch a new Conjur master server, it contains only one login: the "admin" user.
25
+ The bootstrap command will finish the setup of a new Conjur system by creating other essential records.
26
+
27
+ Actions performed by "bootstrap" include:
28
+
29
+ * Creation of a group called "security_admin".
30
+
31
+ * Giving the "security_admin" the power to manage public keys.
32
+
33
+ * Creation of a user called "attic", which will be the owner of retired records.
34
+
35
+ * Storing the "attic" user's API key in a variable called "conjur/users/attic/api-key".
36
+
37
+ * (optional) Create a new user who will be made a member and admin of the "security_admin" group.
38
+
39
+ * (optional) If a new user was created, login as that user.
40
+ )
41
+
42
+ # Determines whether the current logged-in user is sufficiently powerful to perform bootstrap.
43
+ # This is currently determined by detecting whether the logged-in role:
44
+ #
45
+ # * Is a user
46
+ # * Has admin privilege on the security_admin group role
47
+ # * Is an owner of the security_admin group resource
48
+ #
49
+ # The admin user will always satisfy these conditions, unless they are revoked for some reason.
50
+ # Other users created by the bootstrap command will (typically) also have these powers.
51
+ def self.security_admin_manager? api
52
+ username = api.username
53
+ user = if username.index('/')
54
+ nil
55
+ else
56
+ api.user(username)
57
+ end
58
+ security_admin = api.group("security_admin")
59
+ memberships = user.role.memberships.map(&:roleid) if user
60
+ begin
61
+ # The user exists
62
+ # The security_admin group exists
63
+ # The user has a role which is admin of the security_admin role
64
+ # The user has the role which owns the security_admin resource
65
+ user &&
66
+ security_admin.exists? &&
67
+ security_admin.role.members.find{|m| memberships.member?(m.member.roleid) && m.admin_option} &&
68
+ memberships.member?(security_admin.resource.ownerid)
69
+ rescue RestClient::Forbidden
70
+ false
71
+ end
72
+ end
24
73
 
25
74
  Conjur::CLI.command :bootstrap do |c|
26
75
  c.action do |global_options,options,args|
27
76
  require 'highline/import'
28
77
 
29
- exit_now! "You must be logged in as 'admin' to bootstrap Conjur" unless api.username == "admin"
78
+ exit_now! "You must be an administrator to bootstrap Conjur" unless security_admin_manager?(api)
30
79
 
31
80
  if (security_admin = api.group("security_admin")).exists?
32
81
  puts "Group 'security_admin' exists"
@@ -35,16 +84,22 @@ class Conjur::Command::Bootstrap < Conjur::Command
35
84
  security_admin = api.create_group("security_admin")
36
85
  end
37
86
 
38
- puts "Permitting group 'security_admin' to manage public keys"
39
- api.group("pubkeys-1.0/key-managers").add_member security_admin, admin_option: true
87
+ security_admin.resource.give_to(security_admin) unless security_admin.resource.ownerid == security_admin.role.roleid
88
+
89
+ key_managers = api.group("pubkeys-1.0/key-managers")
90
+ unless security_admin.role.memberships.map(&:roleid).member?(key_managers.role.roleid)
91
+ puts "Permitting group 'security_admin' to manage public keys"
92
+ key_managers.add_member security_admin, admin_option: true
93
+ end
40
94
 
41
95
  security_administrators = security_admin.role.members.select{|m| m.member.roleid.split(':')[1..-1] != [ 'user', 'admin'] }
42
96
  puts "Current 'security_admin' members are : #{security_administrators.map{|m| m.member.roleid.split(':')[-1]}.join(', ')}" unless security_administrators.blank?
97
+ created_user = nil
43
98
  if security_administrators.empty? || agree("Create a new security_admin? (answer 'y' or 'yes'):")
44
99
  username = ask("Enter #{security_administrators.empty? ? 'your' : 'the'} username:")
45
100
  password = prompt_for_password
46
101
  puts "Creating user '#{username}'"
47
- user = api.create_user(username, password: password)
102
+ created_user = user = api.create_user(username, password: password)
48
103
  Conjur::API.new_from_key(user.login, password).user(user.login).resource.give_to security_admin
49
104
  puts "User created"
50
105
  puts "Making '#{username}' a member and admin of group 'security_admin'"
@@ -53,11 +108,22 @@ class Conjur::Command::Bootstrap < Conjur::Command
53
108
  puts "Adminship granted"
54
109
  end
55
110
 
56
- if (attic = api.user("attic")).exists?
57
- puts "User 'attic' exists"
111
+ attic_user_name = "attic"
112
+ if (attic = api.user(attic_user_name)).exists?
113
+ puts "User '#{attic_user_name}' already exists"
58
114
  else
59
- puts "Creating user 'attic'"
60
- attic = api.create_user("attic")
115
+ puts "Creating user '#{attic_user_name}' to own retired records"
116
+ attic = api.create_user(attic_user_name)
117
+ api.create_variable "text/plain",
118
+ "conjur-api-key",
119
+ id: "conjur/users/#{attic_user_name}/api-key",
120
+ value: attic.api_key,
121
+ ownerid: security_admin.role.roleid
122
+ end
123
+
124
+ if created_user && agree("Login as user '#{created_user.login}'? (answer 'y' or 'yes'):")
125
+ Conjur::Authn.fetch_credentials(username: created_user.login, password: created_user.api_key)
126
+ puts "Logged in as '#{created_user.login}'"
61
127
  end
62
128
  end
63
129
  end
@@ -37,12 +37,34 @@ class Conjur::Command::Groups < Conjur::Command
37
37
 
38
38
  acting_as_option(c)
39
39
 
40
- c.action do |global_options,options,args|
41
- id = require_arg(args, 'id')
40
+ interactive_option c
42
41
 
43
- options[:gidnumber] = Integer(options[:gidnumber]) if options[:gidnumber]
44
-
45
- group = api.create_group(id, options)
42
+ c.action do |global_options,options,args|
43
+ id = args.shift
44
+
45
+ interactive = options[:interactive] || id.blank?
46
+
47
+ groupid = options[:ownerid]
48
+ gidnumber = options[:gidnumber]
49
+
50
+ if interactive
51
+ id ||= prompt_for_id :group
52
+
53
+ groupid ||= prompt_for_group
54
+ gidnumber ||= prompt_for_gidnumber
55
+
56
+ prompt_to_confirm :group, {
57
+ "Id" => id,
58
+ "Owner" => groupid,
59
+ "Gidnumber" => gidnumber
60
+ }
61
+ end
62
+
63
+ group_options = { }
64
+ group_options[:ownerid] = groupid if groupid
65
+ group_options[:gidnumber] = gidnumber.to_i unless gidnumber.blank?
66
+
67
+ group = api.create_group(id, group_options)
46
68
  display(group, options)
47
69
  end
48
70
  end
@@ -92,16 +114,18 @@ class Conjur::Command::Groups < Conjur::Command
92
114
  group.desc "Decommission a group"
93
115
  group.arg_name "id"
94
116
  group.command :retire do |c|
117
+ retire_options c
118
+
95
119
  c.action do |global_options,options,args|
96
120
  id = require_arg(args, 'id')
97
121
 
98
122
  group = api.group(id)
99
123
 
124
+ validate_retire_privileges group, options
125
+
100
126
  retire_resource group
101
127
  retire_role group
102
-
103
- puts "Giving ownership to 'attic'"
104
- group.resource.give_to api.user('attic')
128
+ give_away_resource group, options
105
129
 
106
130
  puts "Group retired"
107
131
  end
@@ -168,7 +192,9 @@ class Conjur::Command::Groups < Conjur::Command
168
192
  end
169
193
 
170
194
  end
171
-
195
+ end
196
+
197
+ def self.prompt_for_gidnumber
198
+ prompt_for_idnumber "gid number"
172
199
  end
173
200
  end
174
-
@@ -58,10 +58,14 @@ class Conjur::Command::Hosts < Conjur::Command
58
58
  hosts.desc "Decommission a host"
59
59
  hosts.arg_name "id"
60
60
  hosts.command :retire do |c|
61
+ retire_options c
62
+
61
63
  c.action do |global_options,options,args|
62
64
  id = require_arg(args, 'id')
63
65
 
64
66
  host = api.host(id)
67
+
68
+ validate_retire_privileges host, options
65
69
 
66
70
  host_layer_roles(host).each do |layer|
67
71
  puts "Removing from layer #{layer.id}"
@@ -70,9 +74,7 @@ class Conjur::Command::Hosts < Conjur::Command
70
74
 
71
75
  retire_resource host
72
76
  retire_role host
73
-
74
- puts "Giving ownership to 'attic'"
75
- host.resource.give_to api.user('attic')
77
+ give_away_resource host, options
76
78
 
77
79
  puts "Host retired"
78
80
  end
@@ -47,17 +47,39 @@ class Conjur::Command::Pubkeys < Conjur::Command
47
47
  end
48
48
 
49
49
  pubkeys.desc "Add a public key for a user"
50
- pubkeys.arg_name "username key"
50
+ pubkeys.long_desc %Q(Adds a public key for a user. The username is a required argument of this method.
51
+
52
+ The public key itself may be provided in several ways.
53
+
54
+ 1. After the username argument, the public key can be provided as a literal (quoted) string.
55
+
56
+ 2. After the username argument, the path to the public key file can be provided with a leading @ character.
57
+
58
+ 3. If the only argument to this command is the username, the key will be read from stdin.
59
+
60
+ 4. If you provide the -i (interactive) command option, you'll be prompted for the public key
61
+ )
62
+ pubkeys.arg_name "username key?"
51
63
  pubkeys.command :add do |c|
64
+ interactive_option c
65
+
52
66
  c.action do |global_options, options, args|
67
+ options[:interactive] = $stdin.isatty if options[:interactive].nil?
53
68
  username = require_arg args, "username"
54
69
  if key = args.shift
55
70
  if /^@(.+)$/ =~ key
56
71
  key = File.read(File.expand_path($1))
57
72
  end
58
73
  else
59
- key = STDIN.read.strip
74
+ key = if options[:interactive]
75
+ prompt_for_public_key
76
+ else
77
+ STDIN.read.strip.tap do |k|
78
+ exit_now! "Invalid public key format" unless validate_public_key(k)
79
+ end
80
+ end
60
81
  end
82
+ fail "Cancelled by the user" if key.blank?
61
83
  api.add_public_key username, key
62
84
  puts "Public key '#{key.split(' ').last}' added"
63
85
  end
@@ -74,4 +96,4 @@ class Conjur::Command::Pubkeys < Conjur::Command
74
96
  end
75
97
  end
76
98
  end
77
- end
99
+ end
@@ -138,12 +138,22 @@ class Conjur::Command::Resources < Conjur::Command
138
138
  resource.desc "Set an annotation on a resource"
139
139
  resource.arg_name "resource-id name value"
140
140
  resource.command :annotate do |c|
141
+ interactive_option c
142
+
141
143
  c.action do |global_options, options, args|
142
144
  id = full_resource_id require_arg(args, 'resource-id')
143
- name = require_arg args, 'name'
144
- value = require_arg args, 'value'
145
- api.resource(id).annotations[name] = value
146
- puts "Set annotation '#{name}' to '#{value}' for resource '#{id}'"
145
+
146
+ annotations = if options[:interactive]
147
+ prompt_for_annotations
148
+ else
149
+ name = require_arg args, 'name'
150
+ value = require_arg args, 'value'
151
+ { name => value }
152
+ end
153
+ unless annotations.blank?
154
+ api.resource(id).annotations.merge!(annotations)
155
+ puts "Set annotations #{annotations.keys} for resource '#{id}'"
156
+ end
147
157
  end
148
158
  end
149
159
 
@@ -37,4 +37,7 @@ end
37
37
 
38
38
  shared_context "when not logged in", logged_in: false do
39
39
  include_context "with mock authn"
40
+ before do
41
+ Conjur::Authn.instance_variable_set :@credentials, nil
42
+ end
40
43
  end
@@ -35,20 +35,54 @@ class Conjur::Command::Users < Conjur::Command
35
35
 
36
36
  acting_as_option(c)
37
37
 
38
- c.action do |global_options,options,args|
39
- login = require_arg(args, 'login')
38
+ interactive_option c
40
39
 
41
- opts = options.slice(:ownerid, :uidnumber)
42
- if opts[:uidnumber]
43
- raise "uidnumber should be integer" unless /\d+/ =~ opts[:uidnumber]
44
- opts[:uidnumber] = opts[:uidnumber].to_i
40
+ c.action do |global_options,options,args|
41
+ login = args.shift
42
+
43
+ interactive = options[:interactive] || login.blank?
44
+
45
+ groupid = options[:ownerid]
46
+ uidnumber = options[:uidnumber]
47
+ password = nil
48
+ exit_now! "uidnumber should be integer" unless uidnumber.blank? || /\d+/ =~ uidnumber
49
+
50
+ if interactive
51
+ login ||= prompt_for_id :user, "login name"
52
+
53
+ groupid ||= prompt_for_group hint: "press enter to have the user own their own record"
54
+ uidnumber ||= prompt_for_uidnumber
55
+ password = prompt_for_password unless options[:"no-password"]
56
+
57
+ attributes = {
58
+ "Login" => login,
59
+ "Owner" => groupid,
60
+ "UID Number" => uidnumber
61
+ }
62
+ attributes["Password"] = "********" unless password.blank?
63
+ prompt_to_confirm :user, attributes
45
64
  end
46
-
47
- if options[:p]
48
- opts[:password] = prompt_for_password
65
+
66
+ if options[:p] && password.blank?
67
+ password = prompt_for_password
49
68
  end
50
69
 
51
- display api.create_user(login, opts)
70
+ user_options = { }
71
+ user_options[:ownerid] = groupid if groupid
72
+ user_options[:uidnumber] = uidnumber.to_i if uidnumber
73
+ user_options[:password] = password if password
74
+ user = api.create_user(login, user_options)
75
+
76
+ puts "User created"
77
+ display user
78
+
79
+ if interactive
80
+ public_key = prompt_for_public_key
81
+ if public_key
82
+ api.add_public_key user.login, public_key
83
+ puts "Public key added"
84
+ end
85
+ end
52
86
  end
53
87
  end
54
88
 
@@ -64,16 +98,18 @@ class Conjur::Command::Users < Conjur::Command
64
98
  user.desc "Decommission a user"
65
99
  user.arg_name "id"
66
100
  user.command :retire do |c|
101
+ retire_options c
102
+
67
103
  c.action do |global_options,options,args|
68
104
  id = require_arg(args, 'id')
69
105
 
70
106
  user = api.user(id)
71
107
 
108
+ validate_retire_privileges user, options
109
+
72
110
  retire_resource user
73
111
  retire_role user
74
-
75
- puts "Giving ownership to 'attic'"
76
- user.resource.give_to api.user('attic')
112
+ give_away_resource user, options
77
113
 
78
114
  puts "User retired"
79
115
  end
@@ -125,7 +161,9 @@ class Conjur::Command::Users < Conjur::Command
125
161
  display api.find_users(uidnumber: uidnumber)
126
162
  end
127
163
  end
128
-
129
164
  end
130
-
165
+
166
+ def self.prompt_for_uidnumber
167
+ prompt_for_idnumber "uid number"
168
+ end
131
169
  end
@@ -34,55 +34,57 @@ class Conjur::Command::Variables < Conjur::Command
34
34
  c.desc "Initial value, which may also be specified as the second command argument after the variable id"
35
35
  c.flag [:v, :"value"]
36
36
 
37
- acting_as_option(c)
38
-
39
- c.arg_name 'interactive'
40
- c.desc 'Create variable interactively'
41
- c.switch [:i, :'interactive']
37
+ acting_as_option c
38
+
39
+ annotate_option c
42
40
 
41
+ interactive_option c
42
+
43
43
  c.action do |global_options,options, args|
44
44
  @default_mime_type = c.flags[:m].default_value
45
45
  @default_kind = c.flags[:k].default_value
46
46
 
47
47
  id = args.shift unless args.empty?
48
-
49
48
  value = args.shift unless args.empty?
50
49
 
51
- raise "Received conflicting value arguments" if value && options[:value]
52
-
53
- groupid = options[:'ownerid']
54
- mime_type = options.delete(:m)
55
- kind = options.delete(:k)
56
- value ||= options.delete(:v)
57
-
58
- options.delete(:'interactive')
59
- options.delete(:"mime-type")
60
- options.delete(:"kind")
61
- options.delete(:'value')
62
-
50
+ exit_now! "Received conflicting value arguments" if value && options[:value]
51
+
52
+ groupid = options[:ownerid]
53
+ mime_type = options[:m]
54
+ kind = options[:k]
55
+ value ||= options[:v]
56
+ interactive = options[:interactive] || id.blank?
57
+ annotate = options[:annotate]
58
+
59
+ exit_now! "Received --annotate option without --interactive" if annotate && !interactive
60
+
63
61
  annotations = {}
64
-
65
- # If the user asked for interactive mode, or he didn't specify
66
- # both an id and a value, prompt for any missing options.
67
- if options.delete(:i) || !(id && value)
68
- id ||= prompt_for_id
69
-
62
+ # If the user asked for interactive mode, or he didn't specify and id
63
+ # prompt for any missing options.
64
+ if interactive
65
+ id ||= prompt_for_id :variable
66
+
70
67
  groupid ||= prompt_for_group
71
68
 
72
69
  kind = prompt_for_kind if !kind || kind == @default_kind
70
+
71
+ mime_type = prompt_for_mime_type if mime_type.blank? || mime_type == @default_mime_type
73
72
 
74
- mime_type = prompt_for_mime_type if !mime_type || mime_type == @default_mime_type
75
-
76
- annotations = prompt_for_annotations
73
+ annotations = prompt_for_annotations if annotate
77
74
 
78
75
  value ||= prompt_for_value
76
+
77
+ prompt_to_confirm :variable, "Id" => id,
78
+ "Kind" => kind,
79
+ "MIME type" => mime_type,
80
+ "Owner" => groupid,
81
+ "Value" => value
79
82
  end
80
83
 
81
- options[:id] = id
82
- options[:value] = value
83
- options[:'ownerid'] = groupid if groupid
84
-
85
- var = api.create_variable(mime_type, kind, options)
84
+ variable_options = { id: id }
85
+ variable_options[:ownerid] = groupid if groupid
86
+ variable_options[:value] = value unless value.blank?
87
+ var = api.create_variable(mime_type, kind, variable_options)
86
88
  api.resource(var).annotations.merge!(annotations) if annotations && !annotations.empty?
87
89
  display(var, options)
88
90
  end
@@ -100,15 +102,17 @@ class Conjur::Command::Variables < Conjur::Command
100
102
  var.desc "Decommission a variable"
101
103
  var.arg_name "id"
102
104
  var.command :retire do |c|
105
+ retire_options c
106
+
103
107
  c.action do |global_options,options,args|
104
108
  id = require_arg(args, 'id')
105
109
 
106
110
  variable = api.variable(id)
111
+
112
+ validate_retire_privileges variable, options
107
113
 
108
114
  retire_resource variable
109
-
110
- puts "Giving ownership to 'attic'"
111
- variable.resource.give_to api.user('attic')
115
+ give_away_resource variable, options
112
116
 
113
117
  puts "Variable retired"
114
118
  end
@@ -149,58 +153,24 @@ class Conjur::Command::Variables < Conjur::Command
149
153
  $stdout.write api.variable(id).value(options[:version])
150
154
  end
151
155
  end
152
-
153
156
  end
154
-
155
- def self.prompt_for_id
156
- highline.ask('Enter the id: ')
157
- end
158
-
159
- def self.prompt_for_group
160
- highline.ask('Enter the group: ', ->(name) { @group && @group.roleid } ) do |q|
161
- q.validate = ->(name) do
162
- name.empty? || (@group = api.group(name)).exists?
163
- end
164
- q.responses[:not_valid] = "Group '<%= @answer %>' doesn't exist, or you don't have permission to use it"
165
- end
166
- end
167
-
157
+
168
158
  def self.prompt_for_kind
169
159
  highline.ask('Enter the kind: ') {|q| q.default = @default_kind }
170
160
  end
171
161
 
172
162
  def self.prompt_for_mime_type
173
- highline.ask('Enter the MIME type: ') {|q| q.default = @default_mime_type }
174
- end
175
-
176
- def self.prompt_for_annotations
177
- highline.say('Add annotations (press enter to finish):')
178
- {}.tap do |annotations|
179
- until (name = highline.ask('annotation name: ')).empty?
180
- annotations[name] = read_till_eof('annotation value (^D to finish):')
163
+ highline.choose do |menu|
164
+ menu.prompt = 'Enter the MIME type: '
165
+ menu.choice @default_mime_type
166
+ menu.choices *%w(application/json application/xml application/x-yaml application/x-pem-file)
167
+ menu.choice "other", nil do |c|
168
+ @highline.ask('Enter a custom mime type: ')
181
169
  end
182
170
  end
183
171
  end
184
172
 
185
173
  def self.prompt_for_value
186
- read_till_eof('Enter the value (^D on its own line to finish):')
187
- end
188
-
189
- def self.highline
190
- require 'highline'
191
- @highline ||= HighLine.new($stdin,$stderr)
192
- end
193
-
194
- def self.read_till_eof(prompt = nil)
195
- highline.say(prompt) if prompt
196
- [].tap do |lines|
197
- loop do
198
- begin
199
- lines << highline.ask('')
200
- rescue EOFError
201
- break
202
- end
203
- end
204
- end.join("\n")
174
+ read_till_eof('Enter the secret value (^D on its own line to finish):')
205
175
  end
206
176
  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.24.0"
22
+ VERSION = "4.25.0"
23
23
  ::Version=VERSION
24
24
  end
@@ -61,6 +61,8 @@ describe Conjur::Command::Pubkeys, logged_in: true do
61
61
  let(:stdin_contents){ "ssh-rsa blahblah keyname" }
62
62
  it "calls api.add_public_key('alice', stdin) and prints the key name" do
63
63
  expect(STDIN).to receive(:read).and_return(stdin_contents)
64
+ allow(STDIN).to receive(:isatty).and_return(false)
65
+ expect(described_class).to receive(:validate_public_key).and_return(true)
64
66
  expect(described_class.api).to receive(:add_public_key).with('alice', stdin_contents)
65
67
  expect{ invoke }.to write("Public key 'keyname' added")
66
68
  end
@@ -149,4 +149,14 @@ describe Conjur::Command::Resources, logged_in: true do
149
149
  expect(JSON.parse( expect { invoke }.to write )).to eq(roles_list)
150
150
  end
151
151
  end
152
+
153
+ context "interactivity" do
154
+ subject { Conjur::Command::Resources }
155
+ describe_command 'resource:annotate -i #{KIND}:#{ID}' do
156
+ it {
157
+ is_expected.to receive(:prompt_for_annotations)
158
+ invoke_silently
159
+ }
160
+ end
161
+ end
152
162
  end
@@ -12,10 +12,10 @@ describe Conjur::Command::Variables, logged_in: true do
12
12
  end
13
13
  let(:id) { 'the-id' }
14
14
  let(:variable) { post_response(id) }
15
- let (:group) { nil }
16
- let (:annotation) { {} }
17
- let (:value) { 'the-value' }
18
- let (:full_payload) { base_payload }
15
+ let(:group) { nil }
16
+ let(:annotation) { {} }
17
+ let(:value) { 'the-value' }
18
+ let(:full_payload) { base_payload }
19
19
 
20
20
  context 'when there are command-line errors' do
21
21
  describe_command "variable:create -v the-value-1 the-id the-value-2" do
@@ -25,16 +25,34 @@ describe Conjur::Command::Variables, logged_in: true do
25
25
  end
26
26
  end
27
27
 
28
+ context "-a without -i" do
29
+ describe_command 'variable:create -a the-id' do
30
+ it "is an error" do
31
+ expect { invoke }.to raise_error("Received --annotate option without --interactive")
32
+ end
33
+ end
34
+ end
35
+
36
+ context 'non-interactive' do
37
+ describe_command "variable:create the-id" do
38
+ it "is non-interactive" do
39
+ allow(Conjur::Command::Variables).to receive(:prompt_for_id).and_raise("Unexpected prompt for id")
40
+ expect(RestClient::Request).to receive(:execute).and_return(variable)
41
+ invoke
42
+ end
43
+ end
44
+ end
45
+
28
46
  context 'when there are no command-line errors' do
29
-
30
47
  before do
48
+ allow(Conjur::Command::Variables).to receive(:prompt_to_confirm) { "yes"}
31
49
  allow(Conjur::Command::Variables).to receive(:prompt_for_id) { id }
32
50
  allow(Conjur::Command::Variables).to receive(:prompt_for_group) { group }
33
51
  allow(Conjur::Command::Variables).to receive(:prompt_for_kind) { kind }
34
52
  allow(Conjur::Command::Variables).to receive(:prompt_for_mime_type) { mime_type }
35
53
  allow(Conjur::Command::Variables).to receive(:prompt_for_annotations) { annotation }
36
54
  allow(Conjur::Command::Variables).to receive(:prompt_for_value) { value }
37
-
55
+
38
56
  expect(RestClient::Request).to receive(:execute).with({
39
57
  method: :post,
40
58
  url: collection_url,
@@ -43,29 +61,42 @@ describe Conjur::Command::Variables, logged_in: true do
43
61
  }.merge(cert_store_options)).and_return(variable)
44
62
  end
45
63
 
46
- describe_command "variable:create the-different-id" do
47
- let (:id) { 'the-different-id' }
64
+ describe_command "variable:create the-id the-different-value" do
65
+ let (:value) { 'the-different-value' }
48
66
  it "propagates the user-assigned id" do
49
- expect { invoke }.to write({ id: 'the-different-id' }).to(:stdout)
67
+ expect { invoke }.to write({ id: 'the-id' }).to(:stdout)
50
68
  end
51
69
  end
52
70
 
53
- describe_command "variable:create the-id the-different-value" do
54
- let (:value) { 'the-different-value' }
55
- it "propagates the user-assigned id" do
71
+ describe_command "variable:create the-id" do
72
+ let(:value) { "" }
73
+ let(:full_payload) {
74
+ base_payload.dup.tap do |m|
75
+ m.delete_if{|k,_| k == :value}
76
+ end
77
+ }
78
+ it "will propagate the user-assigned id without a value" do
56
79
  expect { invoke }.to write({ id: 'the-id' }).to(:stdout)
57
80
  end
58
81
  end
59
82
 
83
+ let(:base_payload) do
84
+ { id: id, value: value, mime_type: mime_type, kind: kind }.tap do |t|
85
+ group && t.merge(ownerid: group)
86
+ end
87
+ end
88
+
60
89
  describe_command "variable:create -m application/json" do
61
- let (:mime_type) { 'application/json' }
90
+ let(:mime_type) { 'application/json' }
91
+ let(:payload) { valueless_payload }
62
92
  it "propagates the user-assigned MIME type" do
63
93
  expect { invoke }.to write({ id: 'the-id' }).to(:stdout)
64
94
  end
65
95
  end
66
96
 
67
97
  describe_command "variable:create -k password" do
68
- let (:kind) { 'password' }
98
+ let(:kind) { 'password' }
99
+ let(:payload) { valueless_payload }
69
100
  it "propagates the user-assigned kind" do
70
101
  expect { invoke }.to write({ id: 'the-id' }).to(:stdout)
71
102
  end
@@ -84,25 +115,22 @@ describe Conjur::Command::Variables, logged_in: true do
84
115
  it { is_expected.to receive(:prompt_for_group) }
85
116
  it { is_expected.to receive(:prompt_for_kind) }
86
117
  it { is_expected.to receive(:prompt_for_mime_type) }
87
- it { is_expected.to receive(:prompt_for_annotations) }
118
+ it { is_expected.not_to receive(:prompt_for_annotations) }
88
119
  it { is_expected.to receive(:prompt_for_value) }
89
120
  end
90
121
 
91
- describe_command 'variable:create the-id' do
92
- it { is_expected.not_to receive(:prompt_for_id) }
93
- end
94
-
95
122
  describe_command 'variable:create the-id the-value' do
123
+ it { is_expected.not_to receive(:prompt_for_id) }
96
124
  it { is_expected.not_to receive(:prompt_for_value) }
97
125
  end
98
126
 
99
127
  describe_command 'variable:create -m application/json' do
100
- let (:mime_type) { 'application/json' }
128
+ let(:mime_type) { 'application/json' }
101
129
  it { is_expected.not_to receive(:prompt_for_mime_type) }
102
130
  end
103
131
 
104
132
  describe_command 'variable:create -k password' do
105
- let (:kind) { 'password' }
133
+ let(:kind) { 'password' }
106
134
  it { is_expected.not_to receive(:prompt_for_kind) }
107
135
  end
108
136
 
@@ -119,7 +147,7 @@ describe Conjur::Command::Variables, logged_in: true do
119
147
  }.merge(cert_store_options)).and_return(OpenStruct.new(headers: {}, body: '{}'))
120
148
  end
121
149
 
122
- let (:full_payload) { base_payload.merge(ownerid: 'the-account:group:the-group') }
150
+ let(:full_payload) { base_payload.merge(ownerid: 'the-account:group:the-group') }
123
151
 
124
152
  it { is_expected.not_to receive(:prompt_for_group) }
125
153
  end
@@ -133,25 +161,32 @@ describe Conjur::Command::Variables, logged_in: true do
133
161
  }.merge(cert_store_options)).and_return(OpenStruct.new(headers: {}, body: '{}'))
134
162
  end
135
163
 
136
- let (:full_payload) { base_payload.merge(ownerid: 'the-account:group:the-group') }
164
+ let(:full_payload) { base_payload.merge(ownerid: 'the-account:group:the-group') }
137
165
 
138
166
  it { is_expected.not_to receive(:prompt_for_group) }
139
167
  end
140
168
 
141
169
  end
142
170
 
143
- context "when -i is provided" do
171
+ context "explicit interactivity" do
144
172
  describe_command 'variable:create -i the-id the-value' do
145
173
  it { is_expected.not_to receive(:prompt_for_id) }
146
174
  it { is_expected.not_to receive(:prompt_for_value) }
147
175
  it { is_expected.to receive(:prompt_for_group) }
148
176
  it { is_expected.to receive(:prompt_for_mime_type) }
149
177
  it { is_expected.to receive(:prompt_for_kind) }
150
- it { is_expected.to receive(:prompt_for_annotations) }
178
+ it { is_expected.not_to receive(:prompt_for_annotations) }
151
179
  end
152
180
  end
153
181
 
182
+ context "interactive annotations" do
183
+ describe_command 'variable:create -a' do
184
+ it { is_expected.to receive(:prompt_for_annotations) }
185
+ end
186
+ describe_command 'variable:create -ia the-id' do
187
+ it { is_expected.to receive(:prompt_for_annotations) }
188
+ end
189
+ end
154
190
  end
155
-
156
191
  end
157
192
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: conjur-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.24.0
4
+ version: 4.25.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rafal Rzepecki
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-05-06 00:00:00.000000000 Z
12
+ date: 2015-05-30 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -269,6 +269,7 @@ files:
269
269
  - CHANGELOG.md
270
270
  - Gemfile
271
271
  - LICENSE
272
+ - PUBLISH.md
272
273
  - README.md
273
274
  - Rakefile
274
275
  - bin/_conjur_completions