conjur-cli 4.24.0 → 4.25.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.
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