conjur-cli 4.25.2 → 4.26.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -22,15 +22,15 @@ class Conjur::Command::Variables < Conjur::Command
22
22
  desc "Manage variables"
23
23
  command :variable do |var|
24
24
  var.desc "Create and store a variable"
25
- var.arg_name "id [value]"
25
+ var.arg_name "NAME VALUE"
26
26
  var.command :create do |c|
27
- c.arg_name "mime_type"
27
+ c.arg_name "MIME-TYPE"
28
28
  c.flag [:m, :"mime-type"], default_value: 'text/plain'
29
29
 
30
- c.arg_name "kind"
30
+ c.arg_name "KIND"
31
31
  c.flag [:k, :"kind"], default_value: 'secret'
32
32
 
33
- c.arg_name "value"
33
+ c.arg_name "VALUE"
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
 
@@ -91,21 +91,21 @@ class Conjur::Command::Variables < Conjur::Command
91
91
  end
92
92
 
93
93
  var.desc "Show a variable"
94
- var.arg_name "id"
94
+ var.arg_name "VARIABLE"
95
95
  var.command :show do |c|
96
96
  c.action do |global_options,options,args|
97
- id = require_arg(args, 'id')
97
+ id = require_arg(args, 'VARIABLE')
98
98
  display(api.variable(id), options)
99
99
  end
100
100
  end
101
101
 
102
102
  var.desc "Decommission a variable"
103
- var.arg_name "id"
103
+ var.arg_name "VARIABLE"
104
104
  var.command :retire do |c|
105
105
  retire_options c
106
106
 
107
107
  c.action do |global_options,options,args|
108
- id = require_arg(args, 'id')
108
+ id = require_arg(args, 'VARIABLE')
109
109
 
110
110
  variable = api.variable(id)
111
111
 
@@ -130,10 +130,10 @@ class Conjur::Command::Variables < Conjur::Command
130
130
  var.desc "Access variable values"
131
131
  var.command :values do |values|
132
132
  values.desc "Add a value"
133
- values.arg_name "variable ( value | STDIN )"
133
+ values.arg_name "VARIABLE VALUE"
134
134
  values.command :add do |c|
135
135
  c.action do |global_options,options,args|
136
- id = require_arg(args, 'variable')
136
+ id = require_arg(args, 'VARIABLE')
137
137
  value = args.shift || STDIN.read
138
138
 
139
139
  api.variable(id).add_value(value)
@@ -143,13 +143,13 @@ class Conjur::Command::Variables < Conjur::Command
143
143
  end
144
144
 
145
145
  var.desc "Get a value"
146
- var.arg_name "variable"
146
+ var.arg_name "VARIABLE"
147
147
  var.command :value do |c|
148
148
  c.desc "Version number"
149
149
  c.flag [:v, :version]
150
150
 
151
151
  c.action do |global_options,options,args|
152
- id = require_arg(args, 'variable')
152
+ id = require_arg(args, 'VARIABLE')
153
153
  $stdout.write api.variable(id).value(options[:version])
154
154
  end
155
155
  end
@@ -0,0 +1,263 @@
1
+ #
2
+ # Copyright (C) 2015 Conjur Inc.
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person
5
+ # obtaining a copy of this software and associated documentation files
6
+ # (the "Software"), to deal in the Software without restriction,
7
+ # including without limitation the rights to use, copy, modify, merge,
8
+ # publish, distribute, sublicense, and/or sell copies of the Software,
9
+ # and to permit persons to whom the Software is furnished to do so,
10
+ # subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
19
+ # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
20
+ # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21
+ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ # SOFTWARE.
23
+ #
24
+ require 'conjur/cli'
25
+ require 'shellwords'
26
+
27
+ # Class for generating `conjur` bash completions
28
+ class Conjur::CLI::Complete
29
+ attr_reader :line, :words, :current_word_index, :commands,
30
+ :switch_words, :flag_words, :arg_words, :command_words
31
+ def initialize line, point=nil
32
+ @line = line
33
+ @words = tokenize_cmd @line
34
+ point ||= @line.length
35
+ @current_word_index=(tokenize_cmd @line.slice(0,point)).length-1
36
+ # fix arrays for empty "current word"
37
+ # ie "conjur group list "
38
+ if @line.match(/[ =]$/)
39
+ @words << ''
40
+ @current_word_index += 1
41
+ end
42
+ @commands,
43
+ @switch_words,
44
+ @flag_words,
45
+ @arg_words = parse_command @words, @current_word_index
46
+ @command_words = @commands
47
+ .drop(1)
48
+ .map(&:name)
49
+ .map(&:to_s)
50
+ .unshift('conjur')
51
+ end
52
+
53
+ def current_word offset=0
54
+ @words[@current_word_index + offset]
55
+ end
56
+
57
+ # Generate array of subcommands for which documentation is not hidden
58
+ #
59
+ # @param cmd [Conjur::CLI::Command] the command to search
60
+ # @return [Array] the subcommands
61
+ def subcommands cmd
62
+ cmd.commands.select do |_, c|
63
+ c.nodoc.nil?
64
+ end
65
+ end
66
+
67
+ # Generate array of symbols representing switches for +cmd+ and
68
+ # their aliases
69
+ #
70
+ # @param cmd [Conjur::CLI::Command] the command to search
71
+ # @return [Array] the symbols representing switches and their aliases
72
+ def switches cmd
73
+ cmd.switches.map { |_,switch|
74
+ [switch.name] + (switch.aliases or [])
75
+ }.flatten
76
+ end
77
+
78
+ # Split line according on spaces and after '='
79
+ #
80
+ # @param line [String] to split
81
+ # @return [Array] the substrings
82
+ def tokenize_cmd line
83
+ line.split(/ |(?<==)/)
84
+ end
85
+
86
+ def flag_to_sym flag
87
+ flag.match(/--?([^=]+)=?/)[1].to_sym
88
+ end
89
+
90
+ def parse_command words, current_word_index
91
+ command = Conjur::CLI
92
+ commands = [command]
93
+ switches = []
94
+ flags = []
95
+ arguments = []
96
+ index = 1
97
+ until index >= current_word_index do
98
+ word = words[index]
99
+ case classify_word word, command
100
+ when :switch
101
+ switches.push word
102
+ when :flag
103
+ flags.push [word, words[index+1]]
104
+ index += 1
105
+ when :subcommand
106
+ command = command.commands[word.to_sym]
107
+ commands.push command
108
+ when :argument
109
+ arguments.push word
110
+ end
111
+ index += 1
112
+ end
113
+ return commands, switches, flags, arguments
114
+ end
115
+
116
+ def classify_word word, command
117
+ if word.start_with? '-'
118
+ sym = flag_to_sym word
119
+ if switches(command).member? sym
120
+ :switch
121
+ else
122
+ :flag
123
+ end
124
+ else
125
+ if subcommands(command).has_key? word.to_sym
126
+ :subcommand
127
+ else
128
+ :argument
129
+ end
130
+ end
131
+ end
132
+
133
+ def complete kind
134
+ kind = kind.to_s.downcase.gsub(/[^a-z]/, '')
135
+ case kind
136
+ when 'resource'
137
+ complete_resource
138
+ when 'role'
139
+ complete_role
140
+ when 'file'
141
+ complete_file current_word
142
+ when 'hostname'
143
+ complete_hostname
144
+ else
145
+ complete_resource kind if [
146
+ 'group',
147
+ 'user',
148
+ 'variable',
149
+ 'host',
150
+ 'layer',
151
+ ].member? kind
152
+ end or []
153
+ end
154
+
155
+ # generate completions for the switches and flags of a Conjur::CLI::Command
156
+ #
157
+ # @param cmd [Conjur::CLI::Command] command for which to search for flags
158
+ # and switches
159
+ # @return [Array] completion words
160
+ def complete_flags cmd
161
+ cmd.flags.values.map do |flag|
162
+ candidates = [flag.name]
163
+ candidates += flag.aliases if flag.aliases
164
+ candidates.map do |c|
165
+ "-#{'-' if c.length > 1}#{c}#{'=' if c.length > 1}"
166
+ end
167
+ end + cmd.switches.values.map do |switch|
168
+ candidates = [switch.name]
169
+ candidates += switch.aliases if switch.aliases
170
+ candidates.map do |c|
171
+ "-#{'-' if c.length > 1}#{c}"
172
+ end
173
+ end
174
+ end
175
+
176
+ def complete_args cmd, prev, num_args
177
+ kind=nil
178
+ if prev.start_with? '-'
179
+ flag_name=flag_to_sym prev
180
+ flag = cmd.flags[flag_name]
181
+ desc = flag.argument_name if defined? flag.argument_name
182
+ kind = desc.to_s.downcase
183
+ else
184
+ desc = cmd.arguments_description if defined? cmd.arguments_description
185
+ kind = desc.to_s.downcase.split[num_args-1]
186
+ end
187
+ complete kind
188
+ end
189
+
190
+ def complete_resource resource_kind=nil
191
+ Conjur::Command.api.resources({kind: resource_kind})
192
+ .map do |r|
193
+ res = Resource.new r.attributes['id']
194
+ if resource_kind
195
+ res.name
196
+ else
197
+ res.to_s
198
+ end
199
+ end
200
+ end
201
+
202
+ def complete_role
203
+ Conjur::Command.api.current_role.all
204
+ .map { |r| Resource.new(r.roleid) }
205
+ .reject { |r| r.kind.start_with? '@' }
206
+ .map(&:to_s)
207
+ end
208
+
209
+ def complete_file word
210
+ # use Bash's file completion for compatibility
211
+ `bash -c "compgen -f #{word}"`.shellsplit
212
+ end
213
+
214
+ def complete_hostname
215
+ `bash -c "compgen -A hostname"`.shellsplit
216
+ end
217
+
218
+ def completions
219
+ prev = current_word(-1)
220
+ if current_word.start_with? '-'
221
+ complete_flags @commands.last
222
+ else
223
+ (subcommands @commands.last).keys.map(&:to_s) +
224
+ (complete_args @commands.last, prev, @arg_words.length)
225
+ end.flatten
226
+ .select do |candidate|
227
+ candidate.start_with? current_word.sub('\:',':') end
228
+ .map do |candidate|
229
+ # if the current word is colon separated, strip its complete tokens
230
+ # eg. for --as-role=user:ryanprior, we're actually only completing 'ryanprior'
231
+ # because bash treats 'user' as a separate word
232
+ non_escaped_colon_regex = /(?<!\\):/
233
+ num_tokens = current_word.split(non_escaped_colon_regex).length
234
+ if num_tokens > 1
235
+ candidate = candidate
236
+ .split(non_escaped_colon_regex)
237
+ .drop(num_tokens-1)
238
+ .join(':')
239
+ end
240
+ "#{candidate}#{' ' if not candidate.end_with? '='}" end
241
+ end
242
+ public :current_word, :completions
243
+ end
244
+
245
+ class Conjur::CLI::Complete::Resource
246
+ attr_reader :account, :kind, :name
247
+ attr_accessor :include_account
248
+ def initialize resource_string, include_account=false
249
+ @include_account = include_account
250
+ fields = resource_string.split ':'
251
+ raise ArgumentError.new "too many fields (#{resource_string})" if fields.length > 3
252
+ fields.unshift nil while fields.length < 3
253
+ @account, @kind, @name = fields
254
+ end
255
+
256
+ def to_ary
257
+ [(@account if @include_account), @kind, @name].reject { |a| a.nil? }
258
+ end
259
+
260
+ def to_s
261
+ to_ary.join ':'
262
+ end
263
+ 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.25.2"
22
+ VERSION = "4.26.0"
23
23
  ::Version=VERSION
24
24
  end
@@ -312,6 +312,25 @@ describe Conjur::Command::Audit, logged_in: true do
312
312
  expect { invoke }.to write(" created role super:user")
313
313
  end
314
314
  end
315
+
316
+ describe 'audit of ssh:sudo' do
317
+ let(:ssh_event) { default_audit_event.merge('kind' => 'audit', 'facility' => 'ssh', 'action' => 'sudo', 'command' => '/bin/ls', 'system_user' => 'test_user', 'target_user' => 'root') }
318
+ context 'when sudo successful' do
319
+ let(:test_event) { ssh_event.merge('allowed' => true) }
320
+ it 'prints <user> ran <command>' do
321
+ expect { invoke }.to write(" test_user ran '/bin/ls' as root")
322
+ end
323
+ end
324
+
325
+ context 'when sudo fails' do
326
+ let(:test_event) { ssh_event.merge('allowed' => false) }
327
+
328
+ it 'prints <user> attempted to run <command>' do
329
+ expect { invoke }.to write(" test_user attempted to run '/bin/ls' as root")
330
+ end
331
+ end
332
+ end
333
+
315
334
  end
316
335
  end
317
336
 
@@ -0,0 +1,265 @@
1
+ require 'spec_helper'
2
+
3
+ describe Conjur::CLI::Complete do
4
+ def expects_completions_for string, point=nil
5
+ expect(described_class.new("conjur #{string}",point)
6
+ .completions
7
+ .map { |c| c.chomp ' ' })
8
+ end
9
+
10
+ describe 'conjur bash completion' do
11
+ describe 'for conjur subcommands beginning' do
12
+
13
+ before(:each) { expect(Conjur::Command).not_to receive :api }
14
+
15
+ context 'with "conjur gr"' do
16
+ it { expects_completions_for('gr').to include 'group' }
17
+ end
18
+
19
+ context 'with "conjur group"' do
20
+ it { expects_completions_for('group').to contain_exactly 'group' }
21
+ end
22
+
23
+ context 'with "conjur p"' do
24
+ it { expects_completions_for('p').to include 'plugin',
25
+ 'policy',
26
+ 'pubkeys' }
27
+ end
28
+
29
+ context 'with "conjur host l"' do
30
+ it { expects_completions_for('host l').to include 'layers',
31
+ 'list' }
32
+ end
33
+
34
+ context 'with "conjur policy"' do
35
+ it { expects_completions_for('policy ').to include 'load' }
36
+ end
37
+ end
38
+
39
+ describe 'for deprecated subcommands such as `conjur field`' do
40
+ it { expects_completions_for('fi').not_to include 'field' }
41
+ end
42
+
43
+ describe 'for command flags beginning' do
44
+
45
+ before(:each) { expect(Conjur::Command).not_to receive :api }
46
+
47
+ context 'conjur -' do
48
+ it { expects_completions_for('-').to include '--version' }
49
+ end
50
+
51
+ context 'conjur role memberships -' do
52
+ it { expects_completions_for('role memberships -')
53
+ .to include '-s', '--system'}
54
+ end
55
+
56
+ context 'conjur audit all -' do
57
+ it { expects_completions_for('audit all -s -')
58
+ .to include '-f', '--follow', '-l', '--limit=',
59
+ '-o', '--offset=', '-s', '--short' }
60
+ end
61
+
62
+ context 'conjur layer create --as-' do
63
+ it { expects_completions_for('layer create --as-')
64
+ .to include '--as-role=' }
65
+ end
66
+
67
+ context 'conjur group create --as-role' do
68
+ it { expects_completions_for('layer create --as-role')
69
+ .to contain_exactly '--as-role=' }
70
+ end
71
+ end
72
+
73
+ describe 'for arguments' do
74
+
75
+ let (:api) { double('api') }
76
+ before(:each) {
77
+ expect(Conjur::Command).to receive(:api).at_least(:once).and_return api
78
+ }
79
+
80
+ describe 'expecting a resource' do
81
+
82
+ let (:users) { ['Tweedledum', 'Tweedledee'] }
83
+ let (:groups) { ['sharks', 'jets'] }
84
+ let (:layers) { ['limbo', 'lust', 'gluttony', 'greed',
85
+ 'anger', 'heresy', 'violence', 'fraud',
86
+ 'treachery'] }
87
+ let (:variables) { ['id/superman', 'id/batman', 'id/spiderman'] }
88
+ let (:hosts) { ['skynet', 'hal9000', 'deep-thought'] }
89
+
90
+
91
+ def mock_resources
92
+ fake_results = yield.map { |result|
93
+ double('resource', :attributes => { 'id' => result })
94
+ }
95
+ expect(Conjur::Command.api).to receive(:resources)
96
+ .once.and_return fake_results
97
+ end
98
+
99
+ context 'with kind "user"' do
100
+ before(:each) { mock_resources { users.map { |user| "user:#{user}" }}}
101
+ it { expects_completions_for('user show ')
102
+ .to contain_exactly(*users) }
103
+ end
104
+
105
+ context 'with kind "group"' do
106
+ before(:each) {
107
+ mock_resources { groups.map { |group| "group:#{group}" }}
108
+ }
109
+ context 'for a command' do
110
+ it { expects_completions_for('group show ')
111
+ .to contain_exactly(*groups) }
112
+ end
113
+ context 'for a flag' do
114
+ it { expects_completions_for('group create --as-group=')
115
+ .to contain_exactly(*groups) }
116
+ end
117
+ end
118
+
119
+ context 'with kind "layer"' do
120
+ before(:each) {
121
+ mock_resources { layers.map { |layer| "layer:#{layer}" }}
122
+ }
123
+ it { expects_completions_for('layer show ')
124
+ .to contain_exactly(*layers) }
125
+ end
126
+
127
+ context 'with kind "variable"' do
128
+ before(:each) {
129
+ mock_resources { variables.map { |variable| "variable:#{variable}" }}
130
+ }
131
+ it { expects_completions_for('variable show ')
132
+ .to contain_exactly(*variables) }
133
+ end
134
+
135
+ context 'with kind "host"' do
136
+ before(:each) {
137
+ mock_resources { hosts.map { |host| "host:#{host}" }}
138
+ }
139
+ it { expects_completions_for('host show ')
140
+ .to contain_exactly(*hosts)
141
+ }
142
+ end
143
+
144
+ context 'without kind specified' do
145
+ let (:resources) { (users+groups+layers+variables+hosts)
146
+ .map { |res| "arbitrarykind:#{res}" }}
147
+ before(:each) { mock_resources { resources }}
148
+ it 'should show all resources with their reported kinds' do
149
+ expects_completions_for('resource show ')
150
+ .to contain_exactly(*resources)
151
+ end
152
+ end
153
+ end
154
+
155
+ describe 'expecting a role' do
156
+ let (:roles) { ['layer:population/tire',
157
+ 'host:bubs-4k',
158
+ 'user:strongbad',
159
+ 'user:strongsad',
160
+ 'user:strongmad']}
161
+ before(:each) {
162
+ role_doubles = roles.map { |role| double('role', :roleid => role) }
163
+ expect(api).to receive(:current_role).once
164
+ .and_return double('current_role', :all => role_doubles)
165
+ }
166
+ it { expects_completions_for('role memberships ')
167
+ .to contain_exactly(*roles) }
168
+ it 'completes colon separated values per-token' do
169
+ expects_completions_for('layer list --role=host:b')
170
+ .to contain_exactly 'bubs-4k'
171
+ end
172
+ it 'recognizes shell-escaped colons' do
173
+ expects_completions_for('role members layer\:pop')
174
+ .to contain_exactly 'layer:population/tire'
175
+ end
176
+ end
177
+ end
178
+
179
+ describe 'completes mid-line' do
180
+ it 'completes a subcommand not at the end of a line' do
181
+ expect(described_class.new('conjur gr create dwarves/7', 9).completions)
182
+ .to include 'group '
183
+ end
184
+ it 'tolerates garbage flags and arguments' do
185
+ expect(described_class.new('conjur omg --lol wat pu').completions)
186
+ .to include 'pubkeys '
187
+ end
188
+ end
189
+ end
190
+ end
191
+
192
+ describe Conjur::CLI::Complete::Resource do
193
+ describe 'splits resource ids' do
194
+ describe '#initialize(resource_string)' do
195
+ describe 'accepts long or brief ids' do
196
+ context 'gratuitous id (4+ tokens)' do
197
+ it 'raises an ArgumentError' do
198
+ expect {
199
+ described_class.new '1:2:3:4'
200
+ }.to raise_error ArgumentError
201
+ end
202
+ end
203
+ context 'long id (3 tokens)' do
204
+ it 'stores all 3 tokens' do
205
+ dummy_string = 'acct:kind:name'
206
+ dummy = described_class.new dummy_string
207
+ expect(dummy.account).to eq 'acct'
208
+ expect(dummy.kind).to eq 'kind'
209
+ expect(dummy.name).to eq 'name'
210
+ end
211
+ end
212
+ context 'brief id (2 tokens)' do
213
+ it 'stores tokens in kind and name' do
214
+ dummy = described_class.new 'kind:name'
215
+ expect(dummy.account).to eq nil
216
+ expect(dummy.kind).to eq 'kind'
217
+ expect(dummy.name).to eq 'name'
218
+ end
219
+ end
220
+ context 'tiny id (1 token)' do
221
+ it 'stores token in name' do
222
+ dummy = described_class.new 'name'
223
+ expect(dummy.account).to eq nil
224
+ expect(dummy.kind).to eq nil
225
+ expect(dummy.name).to eq 'name'
226
+ end
227
+ end
228
+ end
229
+ it 'hides the account by default' do
230
+ expect(described_class.new('a:b:c').include_account).to eq false
231
+ expect(described_class.new('a:b:c', true).include_account).to eq true
232
+ end
233
+ end
234
+
235
+ describe '#to_ary' do
236
+ context 'account not important' do
237
+ it 'hides account when converting to array' do
238
+ dummy = described_class.new 'a:b:c'
239
+ expect(dummy.to_ary).to eq ['b','c']
240
+ end
241
+ end
242
+ context 'account is important' do
243
+ it 'includes account when converting to array' do
244
+ dummy = described_class.new 'a:b:c', true
245
+ expect(dummy.to_ary).to eq ['a','b','c']
246
+ end
247
+ end
248
+ end
249
+
250
+ describe '#to_s' do
251
+ context 'account not important' do
252
+ it 'hides account when converting to string' do
253
+ dummy = described_class.new 'test:user:admin'
254
+ expect(dummy.to_s).to eq 'user:admin'
255
+ end
256
+ end
257
+ context 'account is important' do
258
+ it 'includes account when converting to string' do
259
+ dummy = described_class.new 'test:user:admin', true
260
+ expect(dummy.to_s).to eq 'test:user:admin'
261
+ end
262
+ end
263
+ end
264
+ end
265
+ end