Exemple #1
0
 def test_log_good(self):
     """ Test sending a log message - all good """
     # There is no raise or return.  We're just poking at
     # the function and making sure it doesn't raise.
     tmplibrary = DuoAPIAuth(log_func=self.main_object.log,
                             **self.main_object.duo_client_args)
     tmplibrary.log(
         summary='TEST message',
         severity='DEBUG',
     )
class TestDuoAPIAuthUnit(unittest.TestCase):
    """
        These are intended to exercise internal functions of the library's
        DuoAPIAuth class without going out to Duo.
    """

    testing_conffile = '/tmp/TestDuoAPIAuthUnit.txt'

    def setUp(self):
        """ Preparing test rig """
        # To get a decent test, we're going to need items from the config
        # file in order to test.
        config = configparser.ConfigParser()
        config.add_section('duo-credentials')
        config.set('duo-credentials', 'IKEY', 'DI9QQ99X9MK4H99RJ9FF')
        config.set('duo-credentials', 'SKEY',
                   '2md9rw5xeyxt8c648dgkmdrg3zpvnhj5b596mgku')
        config.set('duo-credentials', 'HOST', 'api-9f134ff9.duosekurity.com')
        with open(self.testing_conffile, 'w') as configfile:
            config.write(configfile)

        with mock.patch.object(DuoOpenVPN,
                               'CONFIG_FILE_LOCATIONS',
                               new=[
                                   'duo_openvpn.conf',
                                   '/usr/local/etc/duo_openvpn.conf',
                                   '/etc/openvpn/duo_openvpn.conf',
                                   '/etc/duo_openvpn.conf',
                                   self.testing_conffile
                               ]):
            self.main_object = DuoOpenVPN()
        os.environ['untrusted_ip'] = 'testing-ip-Unknown-is-OK'
        os.environ['common_name'] = 'bob'
        user_creds = dict()
        for varname in OpenVPNCredentials.DUO_RESERVED_WORDS:
            os.environ['password'] = varname
            res = OpenVPNCredentials()
            res.load_variables_from_environment()
            user_creds[varname] = res
        self.user_data = user_creds
        #
        with mock.patch.object(DuoOpenVPN,
                               'CONFIG_FILE_LOCATIONS',
                               new=[
                                   'duo_openvpn.conf',
                                   '/usr/local/etc/duo_openvpn.conf',
                                   '/etc/openvpn/duo_openvpn.conf',
                                   '/etc/duo_openvpn.conf',
                                   self.testing_conffile
                               ]):
            self.library = DuoAPIAuth(**self.main_object.duo_client_args)

    def tearDown(self):
        """ Clear the env so we don't impact other tests """
        for varname in ['common_name', 'password', 'username', 'untrusted_ip']:
            if varname in os.environ:
                del os.environ[varname]
        try:
            os.unlink(self.testing_conffile)
        except OSError:  # pragma: no cover
            # ... else, there was nothing there (likely) ...
            if os.path.exists(self.testing_conffile):
                # ... but if there is, we couldn't delete it, so complain.
                raise

    def test_init(self):
        """ Verify init does the right thing """
        self.assertIsInstance(
            self.library, duo_client.Auth,
            'our DuoAPIAuth must be a descendant '
            'of duo_client.Auth')
        self.assertIsInstance(self.library.hostname, str,
                              'hostname must be set and a string')
        self.assertIsInstance(self.library._fail_open, bool,
                              '_fail_open must return a bool')
        self.assertIsNone(self.library.user_config,
                          'user_config must be empty on a plain init')
        self.assertIsNone(self.library.log_func,
                          'log_func must default to None')

    def test_fail_open_00(self):
        """ _fail_open performs as expected """
        res = self.library._fail_open
        self.assertIsInstance(res, bool, '_fail_open must return a bool')
        self.assertFalse(res, '_fail_open defaults to False')

    def test_fail_open_01(self):
        """ _fail_open performs as expected on a True """
        tmplibrary = DuoAPIAuth(fail_open=True,
                                **self.main_object.duo_client_args)
        res = tmplibrary._fail_open
        self.assertIsInstance(res, bool, '_fail_open must return a bool')
        self.assertTrue(res, '_fail_open must return an expected True')

    def test_fail_open_02(self):
        """ _fail_open performs as expected on a False """
        tmplibrary = DuoAPIAuth(fail_open=False,
                                **self.main_object.duo_client_args)
        res = tmplibrary._fail_open
        self.assertIsInstance(res, bool, '_fail_open must return a bool')
        self.assertFalse(res, '_fail_open must return an expected False')

    def test_load_user_to_verify_00(self):
        """ load_user_to_verify can fail for garbage """
        res = self.library.load_user_to_verify({})
        self.assertFalse(res, 'load_user_to_verify must be False '
                         'for junk input')

    def test_load_user_to_verify_01(self):
        """ load_user_to_verify can fail for a bad username """
        setattr(self.user_data['push'], 'username', 0)
        res = self.library.load_user_to_verify(self.user_data['push'])
        self.assertFalse(
            res, 'load_user_to_verify must be False '
            'for a bad username')

    def test_load_user_to_verify_02(self):
        """ load_user_to_verify can fail for a bad factor """
        setattr(self.user_data['push'], 'factor', 0)
        res = self.library.load_user_to_verify(self.user_data['push'])
        self.assertFalse(
            res, 'load_user_to_verify must be False '
            'for a bad factor')

    def test_load_user_to_verify_03(self):
        """ load_user_to_verify can succeed and return True """
        res = self.library.load_user_to_verify(self.user_data['push'])
        self.assertTrue(res, 'load_user_to_verify must be True')

    def test_log(self):
        """ Test the weird logging function """
        with mock.patch.object(self.library, 'log_func') as mock_dummy:
            self.library.log('foo', bar='quux')
        mock_dummy.assert_called_once_with('foo', bar='quux')

    def test_10_preflight(self):
        """ Test various levels of preflight checks """
        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   'ping',
                                   side_effect=socket.error):
                res = self.library._preflight()
            self.assertFalse(res, "Broken ping must return False")
            # Check the call_args - [1] is the kwargs.
            #self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            #self.assertEqual(mock_log.call_args[1]['details']['success'], 'false')

        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library, 'ping', return_value=None):
                with mock.patch.object(self.library,
                                       'check',
                                       side_effect=socket.error):
                    res = self.library._preflight()
            self.assertFalse(res, "Broken check must return False")
            # Check the call_args - [1] is the kwargs.
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library, 'ping', return_value=None):
                with mock.patch.object(self.library,
                                       'check',
                                       side_effect=RuntimeError):
                    res = self.library._preflight()
            self.assertFalse(res, "Broken check must return False")
            # Check the call_args - [1] is the kwargs.
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        with mock.patch.object(self.library, 'ping', return_value=None):
            with mock.patch.object(self.library, 'check', return_value=None):
                res = self.library._preflight()
            self.assertTrue(res, "Good preflight check must return True")

    def test_20_preauth(self):
        """ Test various levels of preauth checks """
        # a _preauth without users configured must fail hard:
        with self.assertRaises(Exception):
            self.library._preauth()

        self.library.user_config = self.user_data['push']
        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   'preauth',
                                   side_effect=socket.error):
                res = self.library._preauth()
            self.assertFalse(res, "Broken check must return False")
            # Check the call_args - [1] is the kwargs.
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   'preauth',
                                   side_effect=RuntimeError):
                res = self.library._preauth()
            self.assertFalse(res, "Broken check must return False")
            # Check the call_args - [1] is the kwargs.
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   'preauth',
                                   side_effect=httplib.BadStatusLine('')):
                res = self.library._preauth()
            self.assertFalse(res, "Broken check must return False")
            # Check the call_args - [1] is the kwargs.
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        upstream_return = ['some', 14, 'thing']
        with mock.patch.object(self.library,
                               'preauth',
                               return_value=upstream_return):
            res = self.library._preauth()
        self.assertEqual(res, upstream_return,
                         "Good preauth returns whatever upstream sent")

    def test_30_auth_fails(self):
        """ Test various failure levels of auth checks """
        # a _auth without users configured must fail hard:
        with self.assertRaises(Exception):
            self.library._auth()

        # SMS can't auth:
        with mock.patch.object(self.library, 'log') as mock_log:
            self.library.user_config = self.user_data['sms']
            res = self.library._auth()
            self.assertIsNone(res, "sms _auth must return None")
            # Check the call_args - [1] is the kwargs.
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        # garbage factors can't auth:
        with mock.patch.object(self.library, 'log') as mock_log:
            self.library.user_config = self.user_data['push']
            self.library.user_config.factor = 'nonsense'
            res = self.library._auth()
            self.assertIsNone(res, "nonsense factor _auth must return None")
            # Check the call_args - [1] is the kwargs.
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

    def test_32_auth_phone(self):
        """ Test phone on auth checks. """
        # Remember that 'phone' is a garbage code path.  If you break here,
        # feel free to attack this problem some other way.
        self.library.user_config = self.user_data['phone']
        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   'auth',
                                   side_effect=socket.error):
                res = self.library._auth()
            self.assertIsNone(res, "Failed _auth must return None")
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   'auth',
                                   side_effect=RuntimeError):
                res = self.library._auth()
            self.assertIsNone(res, "Failed _auth must return None")
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        upstream_return = ['some', 32, 'thing']
        with mock.patch.object(self.library,
                               'auth',
                               return_value=upstream_return) as mock_auth:
            res = self.library._auth()
        self.assertEqual(res, upstream_return,
                         "Good auth returns whatever upstream sent")
        self.assertEqual(mock_auth.call_args[1]['device'], 'auto')

    def test_33_auth_auto(self):
        """ Test auto on auth checks. """
        # Remember that 'auto' is a garbage code path.  If you break here,
        # feel free to attack this problem some other way.
        self.library.user_config = self.user_data['auto']
        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   'auth',
                                   side_effect=socket.error):
                res = self.library._auth()
            self.assertIsNone(res, "Failed _auth must return None")
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   'auth',
                                   side_effect=RuntimeError):
                res = self.library._auth()
            self.assertIsNone(res, "Failed _auth must return None")
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        upstream_return = ['some', 33, 'thing']
        with mock.patch.object(self.library,
                               'auth',
                               return_value=upstream_return) as mock_auth:
            res = self.library._auth()
        self.assertEqual(res, upstream_return,
                         "Good auth returns whatever upstream sent")
        self.assertEqual(mock_auth.call_args[1]['device'], 'auto')

    def test_34_auth_passcode(self):
        """ Test passcode on auth checks. """
        os.environ['password'] = '******'
        creds = OpenVPNCredentials()
        creds.load_variables_from_environment()
        self.library.user_config = creds
        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   'auth',
                                   side_effect=socket.error):
                res = self.library._auth()
            self.assertIsNone(res, "Failed _auth must return None")
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   'auth',
                                   side_effect=RuntimeError):
                res = self.library._auth()
            self.assertIsNone(res, "Failed _auth must return None")
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        upstream_return = ['some', 34, 'thing']
        with mock.patch.object(self.library,
                               'auth',
                               return_value=upstream_return) as mock_auth:
            res = self.library._auth()
        self.assertEqual(res, upstream_return,
                         "Good auth returns whatever upstream sent")
        self.assertEqual(mock_auth.call_args[1]['passcode'],
                         self.library.user_config.passcode)

    def test_35_auth_push(self):
        """ Test push on auth checks. """
        self.library.user_config = self.user_data['push']
        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   'auth',
                                   side_effect=socket.error):
                res = self.library._auth()
            self.assertIsNone(res, "Failed _auth must return None")
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   'auth',
                                   side_effect=RuntimeError):
                res = self.library._auth()
            self.assertIsNone(res, "Failed _auth must return None")
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        upstream_return = ['some', 35, 'thing']
        with mock.patch.object(self.library,
                               'auth',
                               return_value=upstream_return) as mock_auth:
            res = self.library._auth()
        self.assertEqual(res, upstream_return,
                         "Good auth returns whatever upstream sent")
        self.assertEqual(mock_auth.call_args[1]['device'], 'auto')

    def test_40_do_mfa(self):
        """ Unit test for _do_mfa_for_user """
        # It doesn't particularly matter what kind of user we have, so picked 'push'  *shrug*
        self.library.user_config = self.user_data['push']
        with mock.patch.object(self.library, '_auth', return_value=None):
            res = self.library._do_mfa_for_user()
        self.assertFalse(
            res, "Failed _auth must cause _do_mfa_for_user to be False")

        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   '_auth',
                                   return_value='weirdness'):
                res = self.library._do_mfa_for_user()
            self.assertFalse(
                res, "Garbage _auth must cause _do_mfa_for_user to be False")
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   '_auth',
                                   return_value={'result': 'allow'}):
                res = self.library._do_mfa_for_user()
            self.assertTrue(
                res, "Allowed _auth must cause _do_mfa_for_user to be True")
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'true')

        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   '_auth',
                                   return_value={
                                       'result': 'deny',
                                       'status_msg': 'Nope'
                                   }):
                res = self.library._do_mfa_for_user()
            self.assertFalse(
                res, "Denied _auth must cause _do_mfa_for_user to be False")
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

    def test_50_main_auth_awful1(self):
        """ Test main_auth that fail preflight """
        # It doesn't particularly matter what kind of user we have, so picked 'push'  *shrug*
        self.library.user_config = self.user_data['push']
        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   '_preflight',
                                   return_value=False):
                self.library._fail_open = True
                #with mock.patch.object(self.library, '_fail_open', return_value=True):
                res = self.library.main_auth()
        self.assertTrue(res,
                        "main_auth follows fail_open when preflight fails")
        self.assertEqual(mock_log.call_args[1]['details']['success'], 'true')

        with mock.patch.object(self.library, 'log') as mock_log:
            with mock.patch.object(self.library,
                                   '_preflight',
                                   return_value=False):
                self.library._fail_open = False
                res = self.library.main_auth()
        self.assertFalse(res,
                         "main_auth follows fail_open when preflight fails")
        mock_log.assert_not_called()

    def test_51_main_auth_awful2(self):
        """ Test main_auth that fail preauth """
        # It doesn't particularly matter what kind of user we have, so picked 'push'  *shrug*
        self.library.user_config = self.user_data['push']
        with mock.patch.object(self.library, '_preflight', return_value=True):
            with mock.patch.object(self.library, 'log') as mock_log:
                with mock.patch.object(self.library,
                                       '_preauth',
                                       return_value=None):
                    res = self.library.main_auth()
            self.assertFalse(res, "main_auth should fail when preauth fails")
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

            with mock.patch.object(self.library, 'log') as mock_log:
                with mock.patch.object(self.library,
                                       '_preauth',
                                       return_value='junk'):
                    res = self.library.main_auth()
            self.assertFalse(
                res, "main_auth should fail when preauth has an API failure")
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

    def test_52_main_auth_allow(self):
        """ Test main_auth when it gets an 'allow' response """
        # It doesn't particularly matter what kind of user we have, so picked 'push'  *shrug*
        self.library.user_config = self.user_data['push']
        with mock.patch.object(self.library, '_preflight', return_value=True):
            with mock.patch.object(self.library, 'log') as mock_log:
                with mock.patch.object(self.library,
                                       '_preauth',
                                       return_value={'result': 'allow'}):
                    res = self.library.main_auth()
            self.assertTrue(
                res, "main_auth should allow when preauth gets an allow")
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'true')

    def test_53_main_auth_enroll(self):
        """ Test main_auth when it gets an 'enroll' response """
        # It doesn't particularly matter what kind of user we have, so picked 'push'  *shrug*
        self.library.user_config = self.user_data['push']
        with mock.patch.object(self.library, '_preflight', return_value=True):
            with mock.patch.object(self.library, 'log') as mock_log:
                with mock.patch.object(self.library,
                                       '_preauth',
                                       return_value={'result': 'enroll'}):
                    res = self.library.main_auth()
            self.assertFalse(
                res, "main_auth should deny when preauth gets an enroll")
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

    def test_54_main_auth_deny(self):
        """ Test main_auth when it gets a 'deny' response """
        # It doesn't particularly matter what kind of user we have, so picked 'push'  *shrug*
        self.library.user_config = self.user_data['push']
        with mock.patch.object(self.library, '_preflight', return_value=True):
            with mock.patch.object(self.library, 'log') as mock_log:
                with mock.patch.object(self.library,
                                       '_preauth',
                                       return_value={'result': 'deny'}):
                    res = self.library.main_auth()
            self.assertFalse(res,
                             "main_auth should deny when preauth gets a deny")
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')

    def test_55_main_auth_auth(self):
        """ Test main_auth when it gets an 'auth' response """
        # It doesn't particularly matter what kind of user we have, so picked 'push'  *shrug*
        self.library.user_config = self.user_data['push']
        with mock.patch.object(self.library, '_preflight', return_value=True):
            with mock.patch.object(self.library, 'log') as mock_log:
                with mock.patch.object(self.library,
                                       '_preauth',
                                       return_value={'result': 'auth'}):
                    upstream_return = ['some', 55, 'thing']
                    with mock.patch.object(self.library,
                                           '_do_mfa_for_user',
                                           return_value=upstream_return):
                        res = self.library.main_auth()
            self.assertEqual(res, upstream_return,
                             "main_auth should deny when preauth gets a deny")
            mock_log.assert_not_called()

    def test_59_main_auth_nonsense(self):
        """ Test main_auth when it gets an unexpected response """
        # It doesn't particularly matter what kind of user we have, so picked 'push'  *shrug*
        self.library.user_config = self.user_data['push']
        with mock.patch.object(self.library, '_preflight', return_value=True):
            with mock.patch.object(self.library, 'log') as mock_log:
                with mock.patch.object(
                        self.library,
                        '_preauth',
                        return_value={'result': 'never_seen_this'}):
                    res = self.library.main_auth()
            self.assertFalse(
                res,
                "main_auth should deny when preauth gets something unexpected")
            self.assertEqual(mock_log.call_args[1]['details']['error'], 'true')
            self.assertEqual(mock_log.call_args[1]['details']['success'],
                             'false')