"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