choria-mcorpc-support 2.23.0 → 2.23.1

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.
@@ -86,14 +86,6 @@ module MCollective
86
86
  end
87
87
  end
88
88
 
89
- # Which port to provide stats over HTTP on
90
- #
91
- # @return [Integer,nil]
92
- # @raise [StandardError] when not numeric
93
- def stats_port
94
- Integer(get_option("choria.stats_port", "")) if has_option?("choria.stats_port")
95
- end
96
-
97
89
  # Determines if there are any federations configured
98
90
  #
99
91
  # @return [Boolean]
@@ -888,15 +880,6 @@ module MCollective
888
880
  File.exist?(csr_path)
889
881
  end
890
882
 
891
- # The formatted string representation of the CSR fingerprint
892
- #
893
- # @return [String]
894
- def csr_fingerprint
895
- require "puppet"
896
- csr = OpenSSL::X509::Request.new(File.read(csr_path))
897
- Puppet::SSL::Digest.new(nil, csr.to_der)
898
- end
899
-
900
883
  # Searches the PATH for an executable command
901
884
  #
902
885
  # @param command [String] a command to search for
@@ -927,146 +910,6 @@ module MCollective
927
910
  which("facter")
928
911
  end
929
912
 
930
- # Creates any missing SSL directories
931
- #
932
- # This prepares a Puppet like SSL tree in case Puppet
933
- # has not been initialized yet
934
- #
935
- # @return [void]
936
- def make_ssl_dirs
937
- return if file_security?
938
-
939
- FileUtils.mkdir_p(ssl_dir, :mode => 0o0771)
940
-
941
- ["certificate_requests", "certs", "public_keys"].each do |dir|
942
- FileUtils.mkdir_p(File.join(ssl_dir, dir), :mode => 0o0755)
943
- end
944
-
945
- ["private_keys", "private"].each do |dir|
946
- FileUtils.mkdir_p(File.join(ssl_dir, dir), :mode => 0o0750)
947
- end
948
- end
949
-
950
- # Creates a RSA key of a certain strenth
951
- #
952
- # @return [OpenSSL::PKey::RSA]
953
- def create_rsa_key(bits)
954
- OpenSSL::PKey::RSA.new(bits)
955
- end
956
-
957
- # Writes a new 4096 bit key in the puppet default locatioj
958
- #
959
- # @return [OpenSSL::PKey::RSA]
960
- # @raise [StandardError] when the key already exist
961
- def write_key
962
- raise("Refusing to overwrite existing key in %s" % client_private_key) if has_client_private_key?
963
-
964
- key = create_rsa_key(4096)
965
- File.open(client_private_key, "w", 0o0640) {|f| f.write(key.to_pem)}
966
-
967
- key
968
- end
969
-
970
- # Creates a basic CSR
971
- #
972
- # @return [OpenSSL::X509::Request] signed CSR
973
- def create_csr(comonname, orgunit, key)
974
- csr = OpenSSL::X509::Request.new
975
- csr.version = 0
976
- csr.public_key = key.public_key
977
- csr.subject = OpenSSL::X509::Name.new(
978
- [
979
- ["CN", comonname, OpenSSL::ASN1::UTF8STRING],
980
- ["OU", orgunit, OpenSSL::ASN1::UTF8STRING]
981
- ]
982
- )
983
- csr.sign(key, OpenSSL::Digest.new("SHA1"))
984
-
985
- csr
986
- end
987
-
988
- # Creates a new CSR signed by the given key
989
- #
990
- # @param key [OpenSSL::PKey::RSA]
991
- # @return [String] PEM encoded CSR
992
- def write_csr(key)
993
- raise("Refusing to overwrite existing CSR in %s" % csr_path) if has_csr?
994
-
995
- csr = create_csr(certname, "mcollective", key)
996
-
997
- File.open(csr_path, "w", 0o0644) {|f| f.write(csr.to_pem)}
998
-
999
- csr.to_pem
1000
- end
1001
-
1002
- # Fetch and save the CA from Puppet
1003
- #
1004
- # @return [Boolean]
1005
- def fetch_ca
1006
- return true if has_ca?
1007
-
1008
- server = puppetca_server
1009
-
1010
- req = http_get("/puppet-ca/v1/certificate/ca?environment=production", "Accept" => "text/plain")
1011
- resp, _ = https(server).request(req)
1012
-
1013
- if resp.code == "200"
1014
- File.open(ca_path, "w", 0o0644) {|f| f.write(resp.body)}
1015
- else
1016
- raise(UserError, "Failed to fetch CA from %s:%s: %s: %s" % [server[:target], server[:port], resp.code, resp.message])
1017
- end
1018
-
1019
- has_ca?
1020
- end
1021
-
1022
- # Requests a certificate from the Puppet CA
1023
- #
1024
- # This will attempt to create a new key, write a CSR and
1025
- # then sends it to the CA for signing
1026
- #
1027
- # @return [Boolean]
1028
- # @raise [UserError] when requesting the cert fails
1029
- def request_cert
1030
- key = write_key
1031
- csr = write_csr(key)
1032
-
1033
- server = puppetca_server
1034
-
1035
- req = Net::HTTP::Put.new("/puppet-ca/v1/certificate_request/%s?environment=production" % certname, "Content-Type" => "text/plain")
1036
- req.body = csr
1037
- resp, _ = https(server).request(req)
1038
-
1039
- if resp.code == "200"
1040
- true
1041
- else
1042
- raise(UserError, "Failed to request certificate from %s:%s: %s: %s: %s" % [server[:target], server[:port], resp.code, resp.message, resp.body])
1043
- end
1044
- end
1045
-
1046
- # Attempts to fetch a cert from the CA
1047
- #
1048
- # @return [Boolean]
1049
- def attempt_fetch_cert
1050
- return true if has_client_public_cert?
1051
-
1052
- req = http_get("/puppet-ca/v1/certificate/%s?environment=production" % certname, "Accept" => "text/plain")
1053
- resp, _ = https(puppetca_server).request(req)
1054
-
1055
- if resp.code == "200"
1056
- File.open(client_public_cert, "w", 0o0644) {|f| f.write(resp.body)}
1057
- true
1058
- else
1059
- false
1060
- end
1061
- end
1062
-
1063
- # Determines if a CSR has been sent but not yet retrieved
1064
- #
1065
- # @return [Boolean]
1066
- def waiting_for_cert?
1067
- !has_client_public_cert? && has_client_private_key?
1068
- end
1069
-
1070
913
  # Gets a config option
1071
914
  #
1072
915
  # @param opt [String] config option to look up
@@ -250,8 +250,9 @@ module MCollective
250
250
  # @param environment [Hash] environment to run with
251
251
  # @param stdin [String] stdin to send to the command
252
252
  # @param spooldir [String] path to the spool for this specific request
253
+ # @param run_as [String] name of the user who will run the command
253
254
  # @return [Integer] the pid that was spawned
254
- def spawn_command(command, environment, stdin, spooldir)
255
+ def spawn_command(command, environment, stdin, spooldir, run_as)
255
256
  wrapper_input = File.join(spooldir, "wrapper_stdin")
256
257
  wrapper_stdout = File.join(spooldir, "wrapper_stdout")
257
258
  wrapper_stderr = File.join(spooldir, "wrapper_stderr")
@@ -269,7 +270,24 @@ module MCollective
269
270
  options[:in] = wrapper_input
270
271
  end
271
272
 
272
- pid = Process.spawn(environment, command, options)
273
+ if run_as
274
+ raise("System does not allow forking. run_as not usable.") unless Process.respond_to?(:fork)
275
+
276
+ require "etc"
277
+
278
+ u = Etc.getpwnam(run_as)
279
+
280
+ FileUtils.chown_R(u.uid, u.gid, spooldir)
281
+
282
+ pid = Process.fork
283
+ if pid.nil?
284
+ Process.gid = Process.egid = u.gid
285
+ Process.uid = Process.euid = u.uid
286
+ Process.exec(environment, command, options)
287
+ end
288
+ else
289
+ pid = Process.spawn(environment, command, options)
290
+ end
273
291
 
274
292
  sleep 0.1 until File.exist?(wrapper_stdout)
275
293
 
@@ -353,7 +371,7 @@ module MCollective
353
371
  meta.print(data.to_json)
354
372
  end
355
373
 
356
- pid = spawn_command(wrapper_path, task_environment(task, requestid, callerid), wrapper_input.to_json, spool)
374
+ pid = spawn_command(wrapper_path, task_environment(task, requestid, callerid), wrapper_input.to_json, spool, task["run_as"])
357
375
 
358
376
  Log.info("Spawned task %s in spool %s with pid %s" % [task["task"], spool, pid])
359
377
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: choria-mcorpc-support
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.23.0
4
+ version: 2.23.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - R.I.Pienaar
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-29 00:00:00.000000000 Z
11
+ date: 2021-01-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: systemu
@@ -79,7 +79,6 @@ files:
79
79
  - lib/mcollective/application.rb
80
80
  - lib/mcollective/application/choria.rb
81
81
  - lib/mcollective/application/completion.rb
82
- - lib/mcollective/application/describe_filter.rb
83
82
  - lib/mcollective/application/facts.rb
84
83
  - lib/mcollective/application/federation.rb
85
84
  - lib/mcollective/application/find.rb
@@ -145,9 +144,6 @@ files:
145
144
  - lib/mcollective/logger/console_logger.rb
146
145
  - lib/mcollective/logger/file_logger.rb
147
146
  - lib/mcollective/logger/syslog_logger.rb
148
- - lib/mcollective/matcher.rb
149
- - lib/mcollective/matcher/parser.rb
150
- - lib/mcollective/matcher/scanner.rb
151
147
  - lib/mcollective/message.rb
152
148
  - lib/mcollective/monkey_patches.rb
153
149
  - lib/mcollective/optionparser.rb
@@ -1,87 +0,0 @@
1
- module MCollective
2
- class Application::Describe_filter < Application # rubocop:disable Style/ClassAndModuleChildren
3
- exclude_argument_sections "common", "rpc"
4
-
5
- description "Display human readable interpretation of filters"
6
-
7
- usage "mco describe_filter -S <filter> -F <filter> -C <filter>"
8
-
9
- def describe_s_filter(stack)
10
- indent = " "
11
- old_indent = " "
12
- puts "-S Query expands to the following instructions:"
13
- puts
14
- stack.each do |token|
15
- case token.keys[0]
16
- when "statement"
17
- if token.values[0] =~ /(<=|>=|=|=~|=)/
18
- op = $1
19
- k, v = token.values[0].split(op)
20
- puts indent + get_fact_string(k, v, op)
21
- else
22
- puts indent + get_class_string(token.values[0])
23
- end
24
- when "fstatement"
25
- v = token.values[0]
26
- result_string = indent + "Execute the Data Query '#{v['name']}'"
27
- result_string += " with parameters (#{v['params']})" if v["params"]
28
- result_string += ". "
29
- result_string += "Check if the query's '#{v['value']}' value #{v['operator']} '#{v['r_compare']}' "
30
- puts result_string
31
- when "("
32
- puts "#{indent}("
33
- old_indent = indent
34
- indent *= 2
35
- when ")"
36
- indent = old_indent
37
- puts "#{indent})"
38
- else
39
- puts indent + token.keys[0].upcase
40
- end
41
- end
42
- end
43
-
44
- def describe_f_filter(facts)
45
- puts "-F filter expands to the following fact comparisons:"
46
- puts
47
- facts.each do |f|
48
- puts " #{get_fact_string(f[:fact], f[:value], f[:operator])}"
49
- end
50
- end
51
-
52
- def describe_c_filter(classes)
53
- puts "-C filter expands to the following class checks:"
54
- puts
55
- classes.each do |c|
56
- puts " #{get_class_string(c)}"
57
- end
58
- end
59
-
60
- def main
61
- unless @options[:filter]["fact"].empty?
62
- describe_f_filter(@options[:filter]["fact"])
63
- puts
64
- end
65
-
66
- unless @options[:filter]["cf_class"].empty?
67
- describe_c_filter(@options[:filter]["cf_class"])
68
- puts
69
- end
70
-
71
- unless @options[:filter]["compound"].empty?
72
- describe_s_filter(@options[:filter]["compound"][0])
73
- puts
74
- end
75
- end
76
-
77
- private
78
-
79
- def get_fact_string(fact, value, oper)
80
- "Check if fact '#{fact}' #{oper} '#{value}'"
81
- end
82
-
83
- def get_class_string(classname)
84
- "Check if class '#{classname}' is present on the host"
85
- end
86
- end
87
- end
@@ -1,220 +0,0 @@
1
- module MCollective
2
- # A parser and scanner that creates a stack machine for a simple
3
- # fact and class matching language used on the CLI to facilitate
4
- # a rich discovery language
5
- #
6
- # Language EBNF
7
- #
8
- # compound = ["("] expression [")"] {["("] expression [")"]}
9
- # expression = [!|not]statement ["and"|"or"] [!|not] statement
10
- # char = A-Z | a-z | < | > | => | =< | _ | - |* | / { A-Z | a-z | < | > | => | =< | _ | - | * | / | }
11
- # int = 0|1|2|3|4|5|6|7|8|9{|0|1|2|3|4|5|6|7|8|9|0}
12
- module Matcher
13
- require "mcollective/matcher/parser"
14
- require "mcollective/matcher/scanner"
15
-
16
- # Helper creates a hash from a function call string
17
- def self.create_function_hash(function_call)
18
- func_hash = {}
19
- f = ""
20
- func_parts = function_call.split(/(!=|>=|<=|<|>|=)/)
21
- func_hash["r_compare"] = func_parts.pop
22
- func_hash["operator"] = func_parts.pop
23
- func = func_parts.join
24
-
25
- # Deal with dots in function parameters and functions without dot values
26
- if func =~ /^.+\(.*\)$/
27
- f = func
28
- else
29
- func_parts = func.split(".")
30
- func_hash["value"] = func_parts.pop
31
- f = func_parts.join(".")
32
- end
33
-
34
- # Deal with regular expression matches
35
- if func_hash["r_compare"] =~ /^\/.*\/$/
36
- func_hash["operator"] = "=~" if func_hash["operator"] == "="
37
- func_hash["operator"] = "!=~" if func_hash["operator"] == "!="
38
- func_hash["r_compare"] = Regexp.new(func_hash["r_compare"].gsub(/^\/|\/$/, ""))
39
- # Convert = operators to == so they can be propperly evaluated
40
- elsif func_hash["operator"] == "="
41
- func_hash["operator"] = "=="
42
- end
43
-
44
- # Grab function name and parameters from left compare string
45
- func_hash["name"], func_hash["params"] = f.split("(")
46
- if func_hash["params"] == ")"
47
- func_hash["params"] = nil
48
- else
49
-
50
- # Walk the function parameters from the front and from the
51
- # back removing the first and last instances of single of
52
- # double qoutes. We do this to handle the case where params
53
- # contain escaped qoutes.
54
- func_hash["params"] = func_hash["params"].gsub(")", "")
55
- func_quotes = func_hash["params"].split(/('|")/)
56
-
57
- func_quotes.each_with_index do |item, i|
58
- if item =~ /'|"/
59
- func_quotes.delete_at(i)
60
- break
61
- end
62
- end
63
-
64
- func_quotes.reverse.each_with_index do |item, i|
65
- if item =~ /'|"/
66
- func_quotes.delete_at(func_quotes.size - i - 1)
67
- break
68
- end
69
- end
70
-
71
- func_hash["params"] = func_quotes.join
72
- end
73
-
74
- func_hash
75
- end
76
-
77
- # Returns the result of an executed function
78
- def self.execute_function(function_hash)
79
- # In the case where a data plugin isn't present there are two ways we can handle
80
- # the raised exception. The function result can either be false or the entire
81
- # expression can fail.
82
- #
83
- # In the case where we return the result as false it opens us op to unexpected
84
- # negation behavior.
85
- #
86
- # !foo('bar').name = bar
87
- #
88
- # In this case the user would expect discovery to match on all machines where
89
- # the name value of the foo function does not equal bar. If a non existent function
90
- # returns false then it is posible to match machines where the name value of the
91
- # foo function is bar.
92
- #
93
- # Instead we raise a DDLValidationError to prevent this unexpected behavior from
94
- # happening.
95
-
96
- result = Data.send(function_hash["name"], function_hash["params"])
97
-
98
- if function_hash["value"]
99
- begin
100
- eval_result = result.send(function_hash["value"])
101
- rescue
102
- # If data field has not been set we set the comparison result to nil
103
- eval_result = nil
104
- end
105
- eval_result
106
- else
107
- result
108
- end
109
- rescue NoMethodError
110
- Log.debug("cannot execute discovery function '#{function_hash['name']}'. data plugin not found")
111
- raise DDLValidationError
112
- end
113
-
114
- # Evaluates a compound statement
115
- def self.eval_compound_statement(expression)
116
- case expression.values.first
117
- when /^\//
118
- Util.has_cf_class?(expression.values.first)
119
- when />=|<=|=|<|>/
120
- optype = expression.values.first.match(/>=|<=|=|<|>/)
121
- name, value = expression.values.first.split(optype[0])
122
- if value.split("")[0] == "/"
123
- optype = "=~"
124
- else
125
- optype[0] == "=" ? optype = "==" : optype = optype[0]
126
- end
127
-
128
- Util.has_fact?(name, value, optype).to_s
129
- else
130
- Util.has_cf_class?(expression.values.first)
131
- end
132
- end
133
-
134
- # Returns the result of an evaluated compound statement that
135
- # includes a function
136
- def self.eval_compound_fstatement(function_hash) # rubocop:disable Metrics/MethodLength
137
- l_compare = execute_function(function_hash)
138
- r_compare = function_hash["r_compare"]
139
- operator = function_hash["operator"]
140
-
141
- # Break out early and return false if the function returns nil
142
- return false if l_compare.nil?
143
-
144
- # Prevent unwanted discovery by limiting comparison operators
145
- # on Strings and Booleans
146
- if (l_compare.is_a?(String) || l_compare.is_a?(TrueClass) || l_compare.is_a?(FalseClass)) && function_hash["operator"].match(/<|>/)
147
- Log.debug("Cannot do > and < comparison on Booleans and Strings '#{l_compare} #{function_hash['operator']} #{function_hash['r_compare']}'")
148
- return false
149
- end
150
-
151
- # Prevent backticks in function parameters
152
- if function_hash["params"] =~ /`/
153
- Log.debug("Cannot use backticks in function parameters")
154
- return false
155
- end
156
-
157
- # Do a regex comparison if right compare string is a regex
158
- if operator =~ /(=~|!=~)/
159
- # Fail if left compare value isn't a string
160
- if l_compare.is_a?(String)
161
- result = l_compare.match(r_compare)
162
- # Flip return value for != operator
163
- if function_hash["operator"] == "!=~"
164
- !result
165
- else
166
- !!result
167
- end
168
- else
169
- Log.debug("Cannot do a regex check on a non string value.")
170
- false
171
- end
172
- # Otherwise do a normal comparison while taking the type into account
173
- else
174
- if l_compare.is_a? String
175
- r_compare = r_compare.to_s
176
- elsif r_compare.is_a? String
177
- case l_compare
178
- when Numeric
179
- r_compare = r_compare.strip
180
- begin
181
- r_compare = Integer(r_compare)
182
- rescue ArgumentError # rubocop:disable Metrics/BlockNesting
183
- begin
184
- r_compare = Float(r_compare)
185
- rescue ArgumentError # rubocop:disable Metrics/BlockNesting
186
- raise(ArgumentError, "invalid numeric value: #{r_compare}")
187
- end
188
- end
189
- when TrueClass, FalseClass
190
- r_compare = r_compare.strip
191
- if r_compare == true.to_s # rubocop:disable Metrics/BlockNesting
192
- r_compare = true
193
- elsif r_compare == false.to_s # rubocop:disable Metrics/BlockNesting
194
- r_compare = false
195
- else
196
- raise(ArgumentError, "invalid boolean value: #{r_compare}")
197
- end
198
- end
199
- end
200
- operator = operator.strip
201
- if operator =~ /(?:(!=|<=|>=|<|>)|==?)/
202
- operator = $1 ? $1.to_sym : :==
203
- else
204
- raise(ArgumentError, "invalid operator: #{operator}")
205
- end
206
- l_compare.send(operator, r_compare)
207
-
208
- end
209
- end
210
-
211
- # Creates a callstack to be evaluated from a compound evaluation string
212
- def self.create_compound_callstack(call_string)
213
- callstack = Matcher::Parser.new(call_string).execution_stack
214
- callstack.each_with_index do |statement, i|
215
- callstack[i]["fstatement"] = create_function_hash(statement.values.first) if statement.keys.first == "fstatement"
216
- end
217
- callstack
218
- end
219
- end
220
- end