"Fossies" - the Fresh Open Source Software Archive 
Member "salt-3002.2/tests/integration/netapi/test_client.py" (18 Nov 2020, 18792 Bytes) of package /linux/misc/salt-3002.2.tar.gz:
As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style:
standard) with prefixed line numbers.
Alternatively you can here
view or
download the uninterpreted source code file.
See also the latest
Fossies "Diffs" side-by-side code changes report for "test_client.py":
3002.1_vs_3002.2.
1 import copy
2 import logging
3 import os
4 import time
5
6 import pytest
7 import salt.config
8 import salt.netapi
9 import salt.utils.files
10 import salt.utils.platform
11 import salt.utils.pycrypto
12 from salt.exceptions import EauthAuthenticationError
13 from tests.support.case import ModuleCase, SSHCase
14 from tests.support.helpers import (
15 SKIP_IF_NOT_RUNNING_PYTEST,
16 SaveRequestsPostHandler,
17 Webserver,
18 slowTest,
19 )
20 from tests.support.mixins import AdaptedConfigurationTestCaseMixin
21 from tests.support.mock import patch
22 from tests.support.runtests import RUNTIME_VARS
23 from tests.support.unit import TestCase, skipIf
24
25 log = logging.getLogger(__name__)
26
27
28 @pytest.mark.usefixtures("salt_master", "salt_sub_minion")
29 class NetapiClientTest(TestCase):
30 eauth_creds = {
31 "username": "saltdev_auto",
32 "password": "saltdev",
33 "eauth": "auto",
34 }
35
36 def setUp(self):
37 """
38 Set up a NetapiClient instance
39 """
40 opts = AdaptedConfigurationTestCaseMixin.get_config("client_config").copy()
41 self.netapi = salt.netapi.NetapiClient(opts)
42
43 def tearDown(self):
44 del self.netapi
45
46 @slowTest
47 def test_local(self):
48 low = {"client": "local", "tgt": "*", "fun": "test.ping", "timeout": 300}
49 low.update(self.eauth_creds)
50
51 ret = self.netapi.run(low)
52 # If --proxy is set, it will cause an extra minion_id to be in the
53 # response. Since there's not a great way to know if the test
54 # runner's proxy minion is running, and we're not testing proxy
55 # minions here anyway, just remove it from the response.
56 ret.pop("proxytest", None)
57 self.assertEqual(ret, {"minion": True, "sub_minion": True})
58
59 @slowTest
60 def test_local_batch(self):
61 low = {"client": "local_batch", "tgt": "*", "fun": "test.ping", "timeout": 300}
62 low.update(self.eauth_creds)
63
64 ret = self.netapi.run(low)
65 rets = []
66 for _ret in ret:
67 rets.append(_ret)
68 self.assertIn({"sub_minion": True}, rets)
69 self.assertIn({"minion": True}, rets)
70
71 def test_local_async(self):
72 low = {"client": "local_async", "tgt": "*", "fun": "test.ping"}
73 low.update(self.eauth_creds)
74
75 ret = self.netapi.run(low)
76
77 # Remove all the volatile values before doing the compare.
78 self.assertIn("jid", ret)
79 ret.pop("jid", None)
80 ret["minions"] = sorted(ret["minions"])
81 try:
82 # If --proxy is set, it will cause an extra minion_id to be in the
83 # response. Since there's not a great way to know if the test
84 # runner's proxy minion is running, and we're not testing proxy
85 # minions here anyway, just remove it from the response.
86 ret["minions"].remove("proxytest")
87 except ValueError:
88 pass
89 self.assertEqual(ret, {"minions": sorted(["minion", "sub_minion"])})
90
91 def test_local_unauthenticated(self):
92 low = {"client": "local", "tgt": "*", "fun": "test.ping"}
93
94 with self.assertRaises(EauthAuthenticationError) as excinfo:
95 ret = self.netapi.run(low)
96
97 @slowTest
98 def test_wheel(self):
99 low = {"client": "wheel", "fun": "key.list_all"}
100 low.update(self.eauth_creds)
101
102 ret = self.netapi.run(low)
103
104 # Remove all the volatile values before doing the compare.
105 self.assertIn("tag", ret)
106 ret.pop("tag")
107
108 data = ret.get("data", {})
109 self.assertIn("jid", data)
110 data.pop("jid", None)
111
112 self.assertIn("tag", data)
113 data.pop("tag", None)
114
115 ret.pop("_stamp", None)
116 data.pop("_stamp", None)
117
118 self.maxDiff = None
119 self.assertTrue(
120 {"master.pem", "master.pub"}.issubset(set(ret["data"]["return"]["local"]))
121 )
122
123 @slowTest
124 def test_wheel_async(self):
125 # Give this test a little breathing room
126 time.sleep(3)
127 low = {"client": "wheel_async", "fun": "key.list_all"}
128 low.update(self.eauth_creds)
129
130 ret = self.netapi.run(low)
131 self.assertIn("jid", ret)
132 self.assertIn("tag", ret)
133
134 def test_wheel_unauthenticated(self):
135 low = {"client": "wheel", "tgt": "*", "fun": "test.ping"}
136
137 with self.assertRaises(EauthAuthenticationError) as excinfo:
138 ret = self.netapi.run(low)
139
140 @skipIf(True, "This is not testing anything. Skipping for now.")
141 def test_runner(self):
142 # TODO: fix race condition in init of event-- right now the event class
143 # will finish init even if the underlying zmq socket hasn't connected yet
144 # this is problematic for the runnerclient's master_call method if the
145 # runner is quick
146 # low = {'client': 'runner', 'fun': 'cache.grains'}
147 low = {"client": "runner", "fun": "test.sleep", "arg": [2]}
148 low.update(self.eauth_creds)
149
150 ret = self.netapi.run(low)
151
152 @skipIf(True, "This is not testing anything. Skipping for now.")
153 def test_runner_async(self):
154 low = {"client": "runner", "fun": "cache.grains"}
155 low.update(self.eauth_creds)
156
157 ret = self.netapi.run(low)
158
159 def test_runner_unauthenticated(self):
160 low = {"client": "runner", "tgt": "*", "fun": "test.ping"}
161
162 with self.assertRaises(EauthAuthenticationError) as excinfo:
163 ret = self.netapi.run(low)
164
165
166 @SKIP_IF_NOT_RUNNING_PYTEST
167 @pytest.mark.requires_sshd_server
168 class NetapiSSHClientTest(SSHCase):
169 eauth_creds = {
170 "username": "saltdev_auto",
171 "password": "saltdev",
172 "eauth": "auto",
173 }
174
175 def setUp(self):
176 """
177 Set up a NetapiClient instance
178 """
179 opts = AdaptedConfigurationTestCaseMixin.get_config("client_config").copy()
180 self.netapi = salt.netapi.NetapiClient(opts)
181 self.priv_file = os.path.join(RUNTIME_VARS.TMP_SSH_CONF_DIR, "client_key")
182 self.rosters = os.path.join(RUNTIME_VARS.TMP_CONF_DIR)
183 self.roster_file = os.path.join(self.rosters, "roster")
184
185 def tearDown(self):
186 del self.netapi
187
188 @classmethod
189 def setUpClass(cls):
190 cls.post_webserver = Webserver(handler=SaveRequestsPostHandler)
191 cls.post_webserver.start()
192 cls.post_web_root = cls.post_webserver.web_root
193 cls.post_web_handler = cls.post_webserver.handler
194
195 @classmethod
196 def tearDownClass(cls):
197 cls.post_webserver.stop()
198 del cls.post_webserver
199
200 @slowTest
201 def test_ssh(self):
202 low = {
203 "client": "ssh",
204 "tgt": "localhost",
205 "fun": "test.ping",
206 "ignore_host_keys": True,
207 "roster_file": self.roster_file,
208 "rosters": [self.rosters],
209 "ssh_priv": self.priv_file,
210 }
211
212 low.update(self.eauth_creds)
213
214 ret = self.netapi.run(low)
215
216 self.assertIn("localhost", ret)
217 self.assertIn("return", ret["localhost"])
218 self.assertEqual(ret["localhost"]["return"], True)
219 self.assertEqual(ret["localhost"]["id"], "localhost")
220 self.assertEqual(ret["localhost"]["fun"], "test.ping")
221
222 @slowTest
223 def test_ssh_unauthenticated(self):
224 low = {"client": "ssh", "tgt": "localhost", "fun": "test.ping"}
225
226 with self.assertRaises(EauthAuthenticationError) as excinfo:
227 ret = self.netapi.run(low)
228
229 @slowTest
230 def test_ssh_unauthenticated_raw_shell_curl(self):
231
232 fun = "-o ProxyCommand curl {}".format(self.post_web_root)
233 low = {"client": "ssh", "tgt": "localhost", "fun": fun, "raw_shell": True}
234
235 ret = None
236 with self.assertRaises(EauthAuthenticationError) as excinfo:
237 ret = self.netapi.run(low)
238
239 self.assertEqual(self.post_web_handler.received_requests, [])
240 self.assertEqual(ret, None)
241
242 @slowTest
243 def test_ssh_unauthenticated_raw_shell_touch(self):
244
245 badfile = os.path.join(RUNTIME_VARS.TMP, "badfile.txt")
246 fun = "-o ProxyCommand touch {}".format(badfile)
247 low = {"client": "ssh", "tgt": "localhost", "fun": fun, "raw_shell": True}
248
249 ret = None
250 with self.assertRaises(EauthAuthenticationError) as excinfo:
251 ret = self.netapi.run(low)
252
253 self.assertEqual(ret, None)
254 self.assertFalse(os.path.exists("badfile.txt"))
255
256 @slowTest
257 def test_ssh_authenticated_raw_shell_disabled(self):
258
259 badfile = os.path.join(RUNTIME_VARS.TMP, "badfile.txt")
260 fun = "-o ProxyCommand touch {}".format(badfile)
261 low = {"client": "ssh", "tgt": "localhost", "fun": fun, "raw_shell": True}
262
263 low.update(self.eauth_creds)
264
265 ret = None
266 with patch.dict(self.netapi.opts, {"netapi_allow_raw_shell": False}):
267 with self.assertRaises(EauthAuthenticationError) as excinfo:
268 ret = self.netapi.run(low)
269
270 self.assertEqual(ret, None)
271 self.assertFalse(os.path.exists("badfile.txt"))
272
273 @staticmethod
274 def cleanup_file(path):
275 try:
276 os.remove(path)
277 except OSError:
278 pass
279
280 @staticmethod
281 def cleanup_dir(path):
282 try:
283 salt.utils.files.rm_rf(path)
284 except OSError:
285 pass
286
287 @slowTest
288 def test_shell_inject_ssh_priv(self):
289 """
290 Verify CVE-2020-16846 for ssh_priv variable
291 """
292 # ZDI-CAN-11143
293 path = "/tmp/test-11143"
294 self.addCleanup(self.cleanup_file, path)
295 self.addCleanup(self.cleanup_file, "aaa")
296 self.addCleanup(self.cleanup_file, "aaa.pub")
297 self.addCleanup(self.cleanup_dir, "aaa|id>")
298 tgt = "www.zerodayinitiative.com"
299 low = {
300 "roster": "cache",
301 "client": "ssh",
302 "tgt": tgt,
303 "ssh_priv": "aaa|id>{} #".format(path),
304 "fun": "test.ping",
305 "eauth": "auto",
306 "username": "saltdev_auto",
307 "password": "saltdev",
308 "roster_file": self.roster_file,
309 "rosters": self.rosters,
310 }
311 ret = self.netapi.run(low)
312 self.assertFalse(ret[tgt]["stdout"])
313 self.assertTrue(ret[tgt]["stderr"])
314 self.assertFalse(os.path.exists(path))
315
316 @slowTest
317 def test_shell_inject_tgt(self):
318 """
319 Verify CVE-2020-16846 for tgt variable
320 """
321 # ZDI-CAN-11167
322 path = "/tmp/test-11167"
323 self.addCleanup(self.cleanup_file, path)
324 low = {
325 "roster": "cache",
326 "client": "ssh",
327 "tgt": "root|id>{} #@127.0.0.1".format(path),
328 "roster_file": self.roster_file,
329 "rosters": "/",
330 "fun": "test.ping",
331 "eauth": "auto",
332 "username": "saltdev_auto",
333 "password": "saltdev",
334 "ignore_host_keys": True,
335 }
336 ret = self.netapi.run(low)
337 self.assertFalse(ret["127.0.0.1"]["stdout"])
338 self.assertTrue(ret["127.0.0.1"]["stderr"])
339 self.assertFalse(os.path.exists(path))
340
341 @slowTest
342 def test_shell_inject_ssh_options(self):
343 """
344 Verify CVE-2020-16846 for ssh_options
345 """
346 # ZDI-CAN-11169
347 path = "/tmp/test-11169"
348 self.addCleanup(self.cleanup_file, path)
349 low = {
350 "roster": "cache",
351 "client": "ssh",
352 "tgt": "127.0.0.1",
353 "renderer": "jinja|yaml",
354 "fun": "test.ping",
355 "eauth": "auto",
356 "username": "saltdev_auto",
357 "password": "saltdev",
358 "roster_file": self.roster_file,
359 "rosters": "/",
360 "ssh_options": ["|id>{} #".format(path), "lol"],
361 }
362 ret = self.netapi.run(low)
363 self.assertFalse(ret["127.0.0.1"]["stdout"])
364 self.assertTrue(ret["127.0.0.1"]["stderr"])
365 self.assertFalse(os.path.exists(path))
366
367 @slowTest
368 def test_shell_inject_ssh_port(self):
369 """
370 Verify CVE-2020-16846 for ssh_port variable
371 """
372 # ZDI-CAN-11172
373 path = "/tmp/test-11172"
374 self.addCleanup(self.cleanup_file, path)
375 low = {
376 "roster": "cache",
377 "client": "ssh",
378 "tgt": "127.0.0.1",
379 "renderer": "jinja|yaml",
380 "fun": "test.ping",
381 "eauth": "auto",
382 "username": "saltdev_auto",
383 "password": "saltdev",
384 "roster_file": self.roster_file,
385 "rosters": "/",
386 "ssh_port": "hhhhh|id>{} #".format(path),
387 "ignore_host_keys": True,
388 }
389 ret = self.netapi.run(low)
390 self.assertFalse(ret["127.0.0.1"]["stdout"])
391 self.assertTrue(ret["127.0.0.1"]["stderr"])
392 self.assertFalse(os.path.exists(path))
393
394 @slowTest
395 def test_shell_inject_remote_port_forwards(self):
396 """
397 Verify CVE-2020-16846 for remote_port_forwards variable
398 """
399 # ZDI-CAN-11173
400 path = "/tmp/test-1173"
401 self.addCleanup(self.cleanup_file, path)
402 low = {
403 "roster": "cache",
404 "client": "ssh",
405 "tgt": "127.0.0.1",
406 "renderer": "jinja|yaml",
407 "fun": "test.ping",
408 "roster_file": self.roster_file,
409 "rosters": "/",
410 "ssh_remote_port_forwards": "hhhhh|id>{} #, lol".format(path),
411 "eauth": "auto",
412 "username": "saltdev_auto",
413 "password": "saltdev",
414 "ignore_host_keys": True,
415 }
416 ret = self.netapi.run(low)
417 self.assertFalse(ret["127.0.0.1"]["stdout"])
418 self.assertTrue(ret["127.0.0.1"]["stderr"])
419 self.assertFalse(os.path.exists(path))
420
421
422 @pytest.mark.requires_sshd_server
423 class NetapiSSHClientAuthTest(SSHCase):
424
425 USERA = "saltdev-auth"
426 USERA_PWD = "saltdev"
427
428 def setUp(self):
429 """
430 Set up a NetapiClient instance
431 """
432 opts = salt.config.client_config(
433 os.path.join(RUNTIME_VARS.TMP_CONF_DIR, "master")
434 )
435 naopts = copy.deepcopy(opts)
436 naopts["ignore_host_keys"] = True
437 self.netapi = salt.netapi.NetapiClient(naopts)
438
439 self.priv_file = os.path.join(RUNTIME_VARS.TMP_SSH_CONF_DIR, "client_key")
440 self.rosters = os.path.join(RUNTIME_VARS.TMP_CONF_DIR)
441 self.roster_file = os.path.join(self.rosters, "roster")
442 # Initialize salt-ssh
443 self.run_function("test.ping")
444 self.mod_case = ModuleCase()
445 try:
446 add_user = self.mod_case.run_function(
447 "user.add", [self.USERA], createhome=False
448 )
449 self.assertTrue(add_user)
450 if salt.utils.platform.is_darwin():
451 hashed_password = self.USERA_PWD
452 else:
453 hashed_password = salt.utils.pycrypto.gen_hash(password=self.USERA_PWD)
454 add_pwd = self.mod_case.run_function(
455 "shadow.set_password", [self.USERA, hashed_password],
456 )
457 self.assertTrue(add_pwd)
458 except AssertionError:
459 self.mod_case.run_function("user.delete", [self.USERA], remove=True)
460 self.skipTest("Could not add user or password, skipping test")
461
462 def tearDown(self):
463 del self.netapi
464 self.mod_case.run_function("user.delete", [self.USERA], remove=True)
465
466 @classmethod
467 def setUpClass(cls):
468 cls.post_webserver = Webserver(handler=SaveRequestsPostHandler)
469 cls.post_webserver.start()
470 cls.post_web_root = cls.post_webserver.web_root
471 cls.post_web_handler = cls.post_webserver.handler
472
473 @classmethod
474 def tearDownClass(cls):
475 cls.post_webserver.stop()
476 del cls.post_webserver
477
478 @slowTest
479 def test_ssh_auth_bypass(self):
480 """
481 CVE-2020-25592 - Bogus eauth raises exception.
482 """
483 low = {
484 "roster": "cache",
485 "client": "ssh",
486 "tgt": "127.0.0.1",
487 "renderer": "jinja|yaml",
488 "fun": "test.ping",
489 "roster_file": self.roster_file,
490 "rosters": "/",
491 "eauth": "xx",
492 "ignore_host_keys": True,
493 }
494 with self.assertRaises(salt.exceptions.EauthAuthenticationError):
495 ret = self.netapi.run(low)
496
497 @slowTest
498 def test_ssh_auth_valid(self):
499 """
500 CVE-2020-25592 - Valid eauth works as expected.
501 """
502 low = {
503 "client": "ssh",
504 "tgt": "localhost",
505 "fun": "test.ping",
506 "roster_file": "roster",
507 "rosters": [self.rosters],
508 "ssh_priv": self.priv_file,
509 "eauth": "pam",
510 "username": self.USERA,
511 "password": self.USERA_PWD,
512 }
513 ret = self.netapi.run(low)
514 assert "localhost" in ret
515 assert ret["localhost"]["return"] is True
516
517 @slowTest
518 def test_ssh_auth_invalid(self):
519 """
520 CVE-2020-25592 - Wrong password raises exception.
521 """
522 low = {
523 "client": "ssh",
524 "tgt": "localhost",
525 "fun": "test.ping",
526 "roster_file": "roster",
527 "rosters": [self.rosters],
528 "ssh_priv": self.priv_file,
529 "eauth": "pam",
530 "username": self.USERA,
531 "password": "notvalidpassword",
532 }
533 with self.assertRaises(salt.exceptions.EauthAuthenticationError):
534 ret = self.netapi.run(low)
535
536 @slowTest
537 def test_ssh_auth_invalid_acl(self):
538 """
539 CVE-2020-25592 - Eauth ACL enforced.
540 """
541 low = {
542 "client": "ssh",
543 "tgt": "localhost",
544 "fun": "at.at",
545 "args": ["12:05am", "echo foo"],
546 "roster_file": "roster",
547 "rosters": [self.rosters],
548 "ssh_priv": self.priv_file,
549 "eauth": "pam",
550 "username": self.USERA,
551 "password": "notvalidpassword",
552 }
553 with self.assertRaises(salt.exceptions.EauthAuthenticationError):
554 ret = self.netapi.run(low)
555
556 @slowTest
557 def test_ssh_auth_token(self):
558 """
559 CVE-2020-25592 - Eauth tokens work as expected.
560 """
561 low = {
562 "eauth": "pam",
563 "username": self.USERA,
564 "password": self.USERA_PWD,
565 }
566 ret = self.netapi.loadauth.mk_token(low)
567 assert "token" in ret and ret["token"]
568 low = {
569 "client": "ssh",
570 "tgt": "localhost",
571 "fun": "test.ping",
572 "roster_file": "roster",
573 "rosters": [self.rosters],
574 "ssh_priv": self.priv_file,
575 "token": ret["token"],
576 }
577 ret = self.netapi.run(low)
578 assert "localhost" in ret
579 assert ret["localhost"]["return"] is True