def test_08_ingest_configs(self): """ With a strong file, check our imports """ 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') config.add_section('duo-behavior') config.set('duo-behavior', 'fail_open', 'True') config.set('duo-behavior', 'duo-timeout', '120') config.add_section('duo-openvpn') config.set('duo-openvpn', 'syslog-events-send', 'True') config.set('duo-openvpn', 'syslog-events-facility', 'local5') with open(self.testing_conffile, 'w') as configfile: config.write(configfile) with mock.patch.object(DuoOpenVPN, 'CONFIG_FILE_LOCATIONS', new=[self.testing_conffile]): self.library = DuoOpenVPN() self.assertIn('ikey', self.library.duo_client_args) self.assertIn('skey', self.library.duo_client_args) self.assertIn('host', self.library.duo_client_args) self.assertTrue(self.library.failopen) self.assertEqual(self.library.duo_timeout, 120) self.assertTrue(self.library.event_send) self.assertEqual(self.library.event_facility, syslog.LOG_LOCAL5)
def test_bogus_user(self): """ A bogus user is denied """ os.environ['common_name'] = 'user-who-does-not-exist' os.environ['password'] = '******' library = DuoOpenVPN() res = library.main_authentication() self.assertFalse(res, 'invalid users must be denied')
def test_bogus_user(self): """ A bogus user is denied """ os.environ['common_name'] = 'user-who-does-not-exist' os.environ['password'] = '******' library = DuoOpenVPN() library.log_to_stdout = False res = library.main_authentication() self.assertFalse(res, 'invalid users must be denied')
def test_1fa_user_bad_pw(self): """ A 1FA user with a bad password fails """ if not self.one_fa_user: # pragma: no cover return self.skipTest('No testing/one_fa_user defined') os.environ['common_name'] = self.one_fa_user os.environ['password'] = '******' library = DuoOpenVPN() res = library.main_authentication() self.assertFalse(res, '1fa user with bad password must be denied')
def test_2fa_user_good(self): """ A 2FA user with a bad push fails PLEASE ALLOW """ if not self.deep_test_main: # pragma: no cover return self.skipTest('because of .deep_testing preference') os.environ['common_name'] = self.normal_user os.environ['password'] = '******' library = DuoOpenVPN() library.log_to_stdout = False res = library.main_authentication() self.assertTrue(res, '2fa user with an allow must be True')
def test_2fa_user_bad(self): """ A 2FA user with a bad push fails PLEASE DENY """ if not self.deep_test_main: return self.skipTest('because of .deep_testing preference') os.environ['common_name'] = self.normal_user os.environ['password'] = '******' library = DuoOpenVPN() library.log_to_stdout = False res = library.main_authentication() self.assertFalse(res, '2fa user with a deny must be False')
def test_1fa_user_good_pw(self): """ A 1FA user with a good password works """ if not self.one_fa_user: # pragma: no cover return self.skipTest('No testing/one_fa_user defined') if not self.one_fa_pass: # pragma: no cover return self.skipTest('No testing/one_fa_pass defined') os.environ['common_name'] = self.one_fa_user os.environ['password'] = self.one_fa_pass library = DuoOpenVPN() res = library.main_authentication() self.assertTrue(res, '1fa user with good password gets accepted')
def test_1fa_user_attempts_2fa(self): """ A 1FA user trying to 2FA fails """ # This is a weird test that stems from a 1FA user pretending to # have a Duo. if not self.one_fa_user: # pragma: no cover return self.skipTest('No testing/one_fa_user defined') os.environ['common_name'] = self.one_fa_user os.environ['password'] = '******' library = DuoOpenVPN() res = library.main_authentication() self.assertFalse(res, '1fa user attempting to 2fa must be denied')
def test_1fa_user_bad_pw(self): """ A 1FA user with a bad password fails """ try: one_fa_user = self.main_object.configfile.get('testing', 'one_fa_user') except (NoOptionError, NoSectionError): # pragma: no cover return self.skipTest('No testing/one_fa_user defined') os.environ['common_name'] = one_fa_user os.environ['password'] = '******' library = DuoOpenVPN() library.log_to_stdout = False res = library.main_authentication() self.assertFalse(res, '1fa user with bad password must be denied')
def test_1fa_user_bad_pw(self): """ A 1FA user with a bad password fails """ try: one_fa_user = self.main_object.configfile.get( 'testing', 'one_fa_user') except (NoOptionError, NoSectionError): # pragma: no cover return self.skipTest('No testing/one_fa_user defined') os.environ['common_name'] = one_fa_user os.environ['password'] = '******' library = DuoOpenVPN() library.log_to_stdout = False res = library.main_authentication() self.assertFalse(res, '1fa user with bad password must be denied')
def test_1fa_user_bad_pw(self): """ A 1FA user with a bad password fails """ if not self.main_object.configfile.has_section('testing'): return self.skipTest('No testing section defined') if not self.main_object.configfile.has_option('testing', 'one_fa_user'): return self.skipTest('No testing/one_fa_user defined') one_fa_user = self.main_object.configfile.get('testing', 'one_fa_user') os.environ['common_name'] = one_fa_user os.environ['password'] = '******' library = DuoOpenVPN() library.log_to_stdout = False res = library.main_authentication() self.assertFalse(res, '1fa user with bad password must be denied')
def main(): """ The main function. Handles file-writing back to openvpn. All the good stuff is in class DuoOpenVPN """ # # 'auth_control_file' is passed to this script via an # environmental variable. It's an ephemeral file. # If we allow a user, put a 1 in the file. # If we deny a user, put a 0 in the file. # control_file_path = os.environ.get('auth_control_file') if control_file_path is None: # Not having this set is bad: we have no way to tell openvpn # what's happened. Or, you're running the script by hand. # Either way, there's no point in continuing. Get out. # We print to STDOUT because this is likely a human, instead # of an actual run. It's possible that we should 'log' this. print('No auth_control_file env variable provided.') sys.exit(1) # There are many more environmental variables needed for this # whole process to work. They are captured/realized farther down in # the stack, so, if this looks sad and short, it's intentional. # The env-variable work is in OpenVPNCredentials auth_object = DuoOpenVPN() try: if auth_object.duo_timeout: signal.signal(signal.SIGALRM, duo_timeout_handler) signal.alarm(auth_object.duo_timeout) should_allow_in = auth_object.main_authentication() if auth_object.duo_timeout: signal.alarm(0) except DuoTimeoutError: should_allow_in = auth_object.failopen if should_allow_in: writeout_value = str(1) else: writeout_value = str(0) try: with open(control_file_path, 'w') as filehandle: filehandle.write(writeout_value) except IOError: # I couldn't write to the file, so we can't tell openvpn what # happened. There's nothing to do but error out. sys.exit(1) # we wrote out in the try, so we're done. sys.exit(0)
def test_1fa_user_attempts_2fa(self): """ A 1FA user trying to 2FA fails """ # This is a weird test that stems from a 1FA user pretending to # have a Duo. try: one_fa_user = self.main_object.configfile.get('testing', 'one_fa_user') except (NoOptionError, NoSectionError): # pragma: no cover return self.skipTest('No testing/one_fa_user defined') os.environ['common_name'] = one_fa_user os.environ['password'] = '******' library = DuoOpenVPN() library.log_to_stdout = False res = library.main_authentication() self.assertFalse(res, '1fa user attempting to 2fa must be denied')
def test_1fa_user_attempts_2fa(self): """ A 1FA user trying to 2FA fails """ # This is a weird test that stems from a 1FA user pretending to # have a Duo. try: one_fa_user = self.main_object.configfile.get( 'testing', 'one_fa_user') except (NoOptionError, NoSectionError): # pragma: no cover return self.skipTest('No testing/one_fa_user defined') os.environ['common_name'] = one_fa_user os.environ['password'] = '******' library = DuoOpenVPN() library.log_to_stdout = False res = library.main_authentication() self.assertFalse(res, '1fa user attempting to 2fa must be denied')
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. self.main_object = DuoOpenVPN() self.main_object.log_to_stdout = False self.normal_user = self.main_object.configfile.get( 'testing', 'normal_user') self.deep_test_rawauth = self.main_object.configfile.getboolean( 'testing', 'deep_testing_rawauth') self.deep_test_mfa = self.main_object.configfile.getboolean( 'testing', 'deep_testing_mfa') self.deep_test_main = self.main_object.configfile.getboolean( 'testing', 'deep_testing_mainauth') # os.environ['untrusted_ip'] = 'testing-ip-Unknown-is-OK' os.environ['common_name'] = self.normal_user 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 # self.library = DuoAPIAuth(**self.main_object.duo_client_args)
def setUp(self): """ Preparing test rig """ # Our test cases depend on the setup of an object that reads # in the environment at the time we create an object of our # test class. As such, we don't have a good setup here. # Each test will have to do a lot of situational setup. # That said, we make a garbage object just to get our library read: self.main_object = DuoOpenVPN() self.normal_user = self.main_object.configfile.get( 'testing', 'normal_user') try: self.deep_test_main = self.main_object.configfile.getboolean( 'testing', 'deep_testing_mainauth') except (NoOptionError, NoSectionError): # pragma: no cover self.deep_test_main = False # os.environ['untrusted_ip'] = 'testing-ip-Unknown-is-OK'
def test_1fa_user_good_pw(self): """ A 1FA user with a good password works """ try: one_fa_user = self.main_object.configfile.get( 'testing', 'one_fa_user') except (NoOptionError, NoSectionError): # pragma: no cover return self.skipTest('No testing/one_fa_user defined') try: one_fa_pass = self.main_object.configfile.get( 'testing', 'one_fa_pass') except (NoOptionError, NoSectionError): # pragma: no cover return self.skipTest('No testing/one_fa_pass defined') os.environ['common_name'] = one_fa_user os.environ['password'] = one_fa_pass library = DuoOpenVPN() library.log_to_stdout = False res = library.main_authentication() self.assertTrue(res, '1fa user with good password gets accepted')
def test_1fa_user_good_pw(self): """ A 1FA user with a good password works """ if not self.main_object.configfile.has_section('testing'): return self.skipTest('No testing section defined') if not self.main_object.configfile.has_option('testing', 'one_fa_user'): return self.skipTest('No testing/one_fa_user defined') one_fa_user = self.main_object.configfile.get('testing', 'one_fa_user') if not self.main_object.configfile.has_option('testing', 'one_fa_pass'): return self.skipTest('No testing/one_fa_pass defined') one_fa_pass = self.main_object.configfile.get('testing', 'one_fa_pass') os.environ['common_name'] = one_fa_user os.environ['password'] = one_fa_pass library = DuoOpenVPN() library.log_to_stdout = False res = library.main_authentication() self.assertTrue(res, '1fa user with good password gets accepted')
def test_1fa_user_good_pw(self): """ A 1FA user with a good password works """ try: one_fa_user = self.main_object.configfile.get('testing', 'one_fa_user') except (NoOptionError, NoSectionError): # pragma: no cover return self.skipTest('No testing/one_fa_user defined') try: one_fa_pass = self.main_object.configfile.get('testing', 'one_fa_pass') except (NoOptionError, NoSectionError): # pragma: no cover return self.skipTest('No testing/one_fa_pass defined') os.environ['common_name'] = one_fa_user os.environ['password'] = one_fa_pass library = DuoOpenVPN() library.log_to_stdout = False res = library.main_authentication() self.assertTrue(res, '1fa user with good password gets accepted')
def setUp(self): """ Preparing test rig """ # Our test cases depend on the setup of an object that reads # in the environment at the time we create an object of our # test class. As such, we don't have a good setup here. # Each test will have to do a lot of situational setup. # That said, we make a garbage object just to get our library read: 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=[self.testing_conffile]): self.library = DuoOpenVPN()
def setUp(self): """ Preparing test rig """ # Our test cases depend on the setup of an object that reads # in the environment at the time we create an object of our # test class. As such, we don't have a good setup here. # Each test will have to do a lot of situational setup. # That said, we make a garbage object just to get our library read: 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.main_object = DuoOpenVPN() try: self.normal_user = self.main_object.configfile.get( 'testing', 'normal_user') except (configparser.NoOptionError, configparser.NoSectionError): # pragma: no cover self.normal_user = None try: self.deep_test_main = self.main_object.configfile.getboolean( 'testing', 'deep_testing_mainauth') except (configparser.NoOptionError, configparser.NoSectionError): # pragma: no cover self.deep_test_main = False try: self.one_fa_user = self.main_object.configfile.get( 'testing', 'one_fa_user') except (configparser.NoOptionError, configparser.NoSectionError): # pragma: no cover self.one_fa_user = None try: self.one_fa_pass = self.main_object.configfile.get( 'testing', 'one_fa_pass') except (configparser.NoOptionError, configparser.NoSectionError): # pragma: no cover self.one_fa_pass = None # os.environ['untrusted_ip'] = 'testing-ip-Unknown-is-OK'
def test_09_ingest_stupidity(self): """ With a terrible file, check our imports """ 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') config.add_section('duo-behavior') config.set('duo-behavior', 'duo-timeout', '-5') config.add_section('duo-openvpn') config.set('duo-openvpn', 'syslog-events-facility', 'junk') with open(self.testing_conffile, 'w') as configfile: config.write(configfile) with mock.patch.object(DuoOpenVPN, 'CONFIG_FILE_LOCATIONS', new=[self.testing_conffile]): self.library = DuoOpenVPN() self.assertEqual(self.library.duo_timeout, 300) self.assertEqual(self.library.event_facility, syslog.LOG_AUTH)
def main(): """ The main function. Handles file-writing back to openvpn. All the good stuff is in class DuoOpenVPN """ # # 'auth_control_file' is passed to this script via an # environmental variable. It's an ephemeral file. # If we allow a user, put a 1 in the file. # If we deny a user, put a 0 in the file. # control_file_path = os.environ.get('auth_control_file') if control_file_path is None: # Not having this set is bad: we have no way to tell openvpn # what's happened. Or, you're running the script by hand. # Either way, there's no point in continuing. Get out. # We print to STDOUT because this is likely a human, instead # of an actual run. It's possible that we should 'log' this. print('No auth_control_file env variable provided.') sys.exit(1) # There are many more environmental variables needed for this # whole process to work. They are captured/realized farther down in # the stack, so, if this looks sad and short, it's intentional. # The env-variable work is in OpenVPNCredentials auth_object = DuoOpenVPN() if auth_object.main_authentication(): writeout_value = str(1) else: writeout_value = str(0) try: with open(control_file_path, 'w') as filehandle: filehandle.write(writeout_value) except IOError: # I couldn't write to the file, so we can't tell openvpn what # happened. There's nothing to do but error out. sys.exit(1) # we wrote out in the try, so we're done. sys.exit(0)
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. 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.main_object = DuoOpenVPN() try: self.normal_user = self.main_object.configfile.get( 'testing', 'normal_user') except (configparser.NoOptionError, configparser.NoSectionError): # pragma: no cover return self.skipTest('No testing/normal_user defined') try: self.deep_test_rawauth = self.main_object.configfile.getboolean( 'testing', 'deep_testing_rawauth') except (configparser.NoOptionError, configparser.NoSectionError): # pragma: no cover self.deep_test_rawauth = False try: self.deep_test_mfa = self.main_object.configfile.getboolean( 'testing', 'deep_testing_mfa') except (configparser.NoOptionError, configparser.NoSectionError): # pragma: no cover self.deep_test_mfa = False try: self.deep_test_main = self.main_object.configfile.getboolean( 'testing', 'deep_testing_mainauth') except (configparser.NoOptionError, configparser.NoSectionError): # pragma: no cover self.deep_test_main = False # os.environ['untrusted_ip'] = 'testing-ip-Unknown-is-OK' os.environ['common_name'] = self.normal_user 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 # self.library = DuoAPIAuth(**self.main_object.duo_client_args)
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)
class TestDuoOpenVPNUnit(unittest.TestCase): """ These are intended to exercise internal functions of the library's DuoOpenVPN class without going out to Duo. """ testing_conffile = '/tmp/TestDuoOpenVPNUnit.txt' def setUp(self): """ Preparing test rig """ # Our test cases depend on the setup of an object that reads # in the environment at the time we create an object of our # test class. As such, we don't have a good setup here. # Each test will have to do a lot of situational setup. # That said, we make a garbage object just to get our library read: 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=[self.testing_conffile]): self.library = DuoOpenVPN() def tearDown(self): """ Clear the env so we don't impact other tests """ 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_03_ingest_no_config_files(self): """ With no config files, get an exception """ with mock.patch.object(DuoOpenVPN, 'CONFIG_FILE_LOCATIONS', new=[]): with self.assertRaises(IOError): self.library._ingest_config_from_file() def test_04_ingest_no_config_file(self): """ With all missing config files, get an exception """ with mock.patch.object(DuoOpenVPN, 'CONFIG_FILE_LOCATIONS', new=['/tmp/no-such-file.txt']): with self.assertRaises(IOError): self.library._ingest_config_from_file() def test_05_ingest_bad_config_file(self): """ With a bad config file, get an exception """ with mock.patch.object(DuoOpenVPN, 'CONFIG_FILE_LOCATIONS', new=['test/context.py']): with self.assertRaises(IOError): self.library._ingest_config_from_file() def test_06_ingest_config_from_file(self): """ With an actual config file, get a populated ConfigParser """ test_reading_file = '/tmp/test-reader.txt' with open(test_reading_file, 'w') as filepointer: filepointer.write('[aa]\nbb = cc\n') filepointer.close() with mock.patch.object( DuoOpenVPN, 'CONFIG_FILE_LOCATIONS', new=['/tmp/no-such-file.txt', test_reading_file]): result = self.library._ingest_config_from_file() os.remove(test_reading_file) self.assertIsInstance(result, configparser.ConfigParser, 'Did not create a config object') self.assertEqual(result.sections(), ['aa'], 'Should have found one configfile section.') self.assertEqual(result.options('aa'), ['bb'], 'Should have found one option.') self.assertEqual(result.get('aa', 'bb'), 'cc', 'Should have read a correct value.') def test_07_ingest_defaults(self): """ With a weak file, check our defaults """ self.assertIn('ikey', self.library.duo_client_args) self.assertIn('skey', self.library.duo_client_args) self.assertIn('host', self.library.duo_client_args) self.assertFalse(self.library.failopen) self.assertFalse(self.library.event_send) self.assertEqual(self.library.event_facility, syslog.LOG_AUTH) self.assertEqual(self.library.duo_timeout, 300) def test_08_ingest_configs(self): """ With a strong file, check our imports """ 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') config.add_section('duo-behavior') config.set('duo-behavior', 'fail_open', 'True') config.set('duo-behavior', 'duo-timeout', '120') config.add_section('duo-openvpn') config.set('duo-openvpn', 'syslog-events-send', 'True') config.set('duo-openvpn', 'syslog-events-facility', 'local5') with open(self.testing_conffile, 'w') as configfile: config.write(configfile) with mock.patch.object(DuoOpenVPN, 'CONFIG_FILE_LOCATIONS', new=[self.testing_conffile]): self.library = DuoOpenVPN() self.assertIn('ikey', self.library.duo_client_args) self.assertIn('skey', self.library.duo_client_args) self.assertIn('host', self.library.duo_client_args) self.assertTrue(self.library.failopen) self.assertEqual(self.library.duo_timeout, 120) self.assertTrue(self.library.event_send) self.assertEqual(self.library.event_facility, syslog.LOG_LOCAL5) def test_09_ingest_stupidity(self): """ With a terrible file, check our imports """ 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') config.add_section('duo-behavior') config.set('duo-behavior', 'duo-timeout', '-5') config.add_section('duo-openvpn') config.set('duo-openvpn', 'syslog-events-facility', 'junk') with open(self.testing_conffile, 'w') as configfile: config.write(configfile) with mock.patch.object(DuoOpenVPN, 'CONFIG_FILE_LOCATIONS', new=[self.testing_conffile]): self.library = DuoOpenVPN() self.assertEqual(self.library.duo_timeout, 300) self.assertEqual(self.library.event_facility, syslog.LOG_AUTH) def test_10_log_nosend(self): ''' Test the log method failing to send ''' self.library.event_send = False with mock.patch('syslog.openlog') as mock_openlog, \ mock.patch('syslog.syslog') as mock_syslog: self.library.log('some message', {'foo': 5}, 'CRITICAL') mock_openlog.assert_not_called() mock_syslog.assert_not_called() def test_11_log_send(self): ''' Test the log method tries to send ''' datetime_mock = mock.Mock(wraps=datetime.datetime) datetime_mock.utcnow.return_value = datetime.datetime( 2020, 12, 25, 13, 14, 15, 123456) self.library.event_send = True self.library.event_facility = syslog.LOG_LOCAL1 with mock.patch('syslog.openlog') as mock_openlog, \ mock.patch('syslog.syslog') as mock_syslog, \ mock.patch('datetime.datetime', new=datetime_mock), \ mock.patch('os.getpid', return_value=12345), \ mock.patch('socket.getfqdn', return_value='my.host.name'): self.library.log('some message', {'foo': 5}, 'CRITICAL') mock_openlog.assert_called_once_with(facility=syslog.LOG_LOCAL1) mock_syslog.assert_called_once() arg_passed_in = mock_syslog.call_args_list[0][0][0] json_sent = json.loads(arg_passed_in) details = json_sent['details'] self.assertEqual(json_sent['category'], 'authentication') self.assertEqual(json_sent['processid'], 12345) self.assertEqual(json_sent['severity'], 'CRITICAL') self.assertIn('processname', json_sent) self.assertEqual(json_sent['timestamp'], '2020-12-25T13:14:15.123456+00:00') self.assertEqual(json_sent['hostname'], 'my.host.name') self.assertEqual(json_sent['summary'], 'some message') self.assertEqual(json_sent['source'], 'openvpn') self.assertEqual(json_sent['tags'], ['vpn', 'duosecurity']) self.assertEqual(details, {'foo': 5}) def test_20_auth_bogus_user(self): """ A bogus user is denied """ with mock.patch.object(DuoOpenVPN, 'log') as mock_log: with mock.patch.object(OpenVPNCredentials, 'load_variables_from_environment', side_effect=ValueError), \ mock.patch('sys.stderr', new=StringIO()) as fake_out: res = self.library.main_authentication() self.assertFalse(res, 'invalid environment must be denied access') # 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') self.assertIn('Traceback', fake_out.getvalue()) def test_21_auth_bad_iam(self): """ A user is denied when IAM is unreachable """ os.environ['untrusted_ip'] = 'testing-ip-Unknown-is-OK' os.environ['common_name'] = 'bob' with mock.patch.object(DuoOpenVPN, 'log') as mock_log: with mock.patch('iamvpnlibrary.IAMVPNLibrary', side_effect=RuntimeError): res = self.library.main_authentication() self.assertFalse(res, 'Disconnected IAM must be denied access') # Check the call_args - [1] is the kwargs. self.assertEqual(mock_log.call_args[1]['details']['success'], 'false') def test_22_auth_disabled_user(self): """ A disabled user is denied """ os.environ['untrusted_ip'] = 'testing-ip-Unknown-is-OK' os.environ['common_name'] = 'bob' with mock.patch.object(DuoOpenVPN, 'log') as mock_log: with mock.patch('iamvpnlibrary.IAMVPNLibrary') as mock_iam: iam_instance = mock_iam.return_value with mock.patch.object(iam_instance, 'user_allowed_to_vpn', return_value=False): res = self.library.main_authentication() self.assertFalse(res, 'Disallowed user must be denied access') # Check the call_args - [1] is the kwargs. self.assertEqual(mock_log.call_args[1]['details']['success'], 'false') def test_23_auth_1fa_garbage_pw(self): """ 1fa user with a 2fa / bonkers 'password' """ os.environ['untrusted_ip'] = 'testing-ip-Unknown-is-OK' os.environ['common_name'] = 'bob' os.environ['password'] = '******' with mock.patch.object(DuoOpenVPN, 'log') as mock_log: with mock.patch('iamvpnlibrary.IAMVPNLibrary') as mock_iam, \ mock.patch.object(mock_iam.return_value, 'user_allowed_to_vpn', return_value=True): with mock.patch.object(mock_iam.return_value, 'does_user_require_vpn_mfa', return_value=False): res = self.library.main_authentication() self.assertFalse( res, '1fa user with stupid colliding passwords must be denied access') # Check the call_args - [1] is the kwargs. self.assertEqual(mock_log.call_args[1]['details']['success'], 'false') def test_24_auth_1fa_bad_pw(self): """ 1fa user with a bad password """ os.environ['untrusted_ip'] = 'testing-ip-Unknown-is-OK' os.environ['common_name'] = 'bob' os.environ['password'] = '******' with mock.patch.object(DuoOpenVPN, 'log') as mock_log: with mock.patch('iamvpnlibrary.IAMVPNLibrary') as mock_iam, \ mock.patch.object(mock_iam.return_value, 'user_allowed_to_vpn', return_value=True): with mock.patch.object(mock_iam.return_value, 'does_user_require_vpn_mfa', return_value=False), \ mock.patch.object(mock_iam.return_value, 'non_mfa_vpn_authentication', return_value=False): res = self.library.main_authentication() self.assertFalse( res, '1fa user with a wrong password must be denied access') # Check the call_args - [1] is the kwargs. self.assertEqual(mock_log.call_args[1]['details']['success'], 'false') def test_25_auth_1fa_good_pw(self): """ 1fa user with a good password """ os.environ['untrusted_ip'] = 'testing-ip-Unknown-is-OK' os.environ['common_name'] = 'bob' os.environ['password'] = '******' with mock.patch.object(DuoOpenVPN, 'log') as mock_log: with mock.patch('iamvpnlibrary.IAMVPNLibrary') as mock_iam, \ mock.patch.object(mock_iam.return_value, 'user_allowed_to_vpn', return_value=True): with mock.patch.object(mock_iam.return_value, 'does_user_require_vpn_mfa', return_value=False), \ mock.patch.object(mock_iam.return_value, 'non_mfa_vpn_authentication', return_value=True): res = self.library.main_authentication() self.assertTrue(res, '1fa user with the right password can get in') # Check the call_args - [1] is the kwargs. self.assertEqual(mock_log.call_args[1]['details']['success'], 'true') def test_26_auth_2fa_no_load(self): """ 2fa user who we can't load into Duo """ os.environ['untrusted_ip'] = 'testing-ip-Unknown-is-OK' os.environ['common_name'] = 'bob' os.environ['password'] = '******' with mock.patch.object(DuoOpenVPN, 'log') as mock_log: with mock.patch('iamvpnlibrary.IAMVPNLibrary') as mock_iam, \ mock.patch.object(mock_iam.return_value, 'user_allowed_to_vpn', return_value=True), \ mock.patch.object(mock_iam.return_value, 'does_user_require_vpn_mfa', return_value=True): with mock.patch('duo_auth.DuoAPIAuth'), \ mock.patch.object(DuoAPIAuth, 'load_user_to_verify', return_value=False): res = self.library.main_authentication() self.assertFalse( res, '2fa user is denied when we cannot load up our Duo search') # Check the call_args - [1] is the kwargs. self.assertEqual(mock_log.call_args[1]['details']['success'], 'false') def test_27_auth_2fa_fail_to_auth(self): """ 2fa user who we can't load into Duo """ os.environ['untrusted_ip'] = 'testing-ip-Unknown-is-OK' os.environ['common_name'] = 'bob' os.environ['password'] = '******' with mock.patch.object(DuoOpenVPN, 'log') as mock_log: with mock.patch('iamvpnlibrary.IAMVPNLibrary') as mock_iam, \ mock.patch.object(mock_iam.return_value, 'user_allowed_to_vpn', return_value=True), \ mock.patch.object(mock_iam.return_value, 'does_user_require_vpn_mfa', return_value=True): with mock.patch('duo_auth.DuoAPIAuth'), \ mock.patch.object(DuoAPIAuth, 'load_user_to_verify', return_value=True), \ mock.patch.object(DuoAPIAuth, 'main_auth', side_effect=IOError), \ mock.patch('sys.stderr', new=StringIO()) as fake_out: res = self.library.main_authentication() self.assertFalse(res, '2fa user is denied when Duo errors out on us') # 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') self.assertIn('Traceback', fake_out.getvalue()) def test_27_auth_2fa_duo_deny(self): """ 2fa user who is denied by Duo """ os.environ['untrusted_ip'] = 'testing-ip-Unknown-is-OK' os.environ['common_name'] = 'bob' os.environ['password'] = '******' with mock.patch('iamvpnlibrary.IAMVPNLibrary') as mock_iam, \ mock.patch.object(mock_iam.return_value, 'user_allowed_to_vpn', return_value=True), \ mock.patch.object(mock_iam.return_value, 'does_user_require_vpn_mfa', return_value=True): with mock.patch('duo_auth.DuoAPIAuth'), \ mock.patch.object(DuoAPIAuth, 'load_user_to_verify', return_value=True), \ mock.patch.object(DuoAPIAuth, 'main_auth', return_value=False): res = self.library.main_authentication() self.assertFalse(res, '2fa user is denied when Duo denies them') def test_27_auth_2fa_duo_allow(self): """ 2fa user who is allowed by Duo """ os.environ['untrusted_ip'] = 'testing-ip-Unknown-is-OK' os.environ['common_name'] = 'bob' os.environ['password'] = '******' with mock.patch('iamvpnlibrary.IAMVPNLibrary') as mock_iam, \ mock.patch.object(mock_iam.return_value, 'user_allowed_to_vpn', return_value=True), \ mock.patch.object(mock_iam.return_value, 'does_user_require_vpn_mfa', return_value=True): with mock.patch('duo_auth.DuoAPIAuth'), \ mock.patch.object(DuoAPIAuth, 'load_user_to_verify', return_value=True), \ mock.patch.object(DuoAPIAuth, 'main_auth', return_value=True): res = self.library.main_authentication() self.assertTrue(res, '2fa user is allowed when Duo allows them')
class TestDuoOpenVPN(unittest.TestCase): """ These are intended to exercise internal functions of the library's DuoOpenVPN class. """ def setUp(self): """ Preparing test rig """ # Our test cases depend on the setup of an object that reads # in the environment at the time we create an object of our # test class. As such, we don't have a good setup here. # Each test will have to do a lot of situational setup. # That said, we make a garbage object just to get our library read: self.main_object = DuoOpenVPN() self.normal_user = self.main_object.configfile.get( 'testing', 'normal_user') try: self.deep_test_main = self.main_object.configfile.getboolean( 'testing', 'deep_testing_mainauth') except (NoOptionError, NoSectionError): # pragma: no cover self.deep_test_main = False # os.environ['untrusted_ip'] = 'testing-ip-Unknown-is-OK' 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] def test_init(self): """ Verify init does the right thing """ self.assertIsNotNone(self.main_object.configfile, 'DuoOpenVPN must have a valid configfile') self.assertIsInstance(self.main_object.failopen, bool, '_fail_open must return a bool') self.assertIsInstance(self.main_object.duo_client_args, dict, 'duo_client_args must be a dict') self.assertIsInstance(self.main_object.log_to_stdout, bool, 'log_to_stdout must be a bool') def test_bogus_user(self): """ A bogus user is denied """ os.environ['common_name'] = 'user-who-does-not-exist' os.environ['password'] = '******' library = DuoOpenVPN() library.log_to_stdout = False res = library.main_authentication() self.assertFalse(res, 'invalid users must be denied') def test_1fa_user_attempts_2fa(self): """ A 1FA user trying to 2FA fails """ # This is a weird test that stems from a 1FA user pretending to # have a Duo. try: one_fa_user = self.main_object.configfile.get('testing', 'one_fa_user') except (NoOptionError, NoSectionError): # pragma: no cover return self.skipTest('No testing/one_fa_user defined') os.environ['common_name'] = one_fa_user os.environ['password'] = '******' library = DuoOpenVPN() library.log_to_stdout = False res = library.main_authentication() self.assertFalse(res, '1fa user attempting to 2fa must be denied') def test_1fa_user_bad_pw(self): """ A 1FA user with a bad password fails """ try: one_fa_user = self.main_object.configfile.get('testing', 'one_fa_user') except (NoOptionError, NoSectionError): # pragma: no cover return self.skipTest('No testing/one_fa_user defined') os.environ['common_name'] = one_fa_user os.environ['password'] = '******' library = DuoOpenVPN() library.log_to_stdout = False res = library.main_authentication() self.assertFalse(res, '1fa user with bad password must be denied') def test_1fa_user_good_pw(self): """ A 1FA user with a good password works """ try: one_fa_user = self.main_object.configfile.get('testing', 'one_fa_user') except (NoOptionError, NoSectionError): # pragma: no cover return self.skipTest('No testing/one_fa_user defined') try: one_fa_pass = self.main_object.configfile.get('testing', 'one_fa_pass') except (NoOptionError, NoSectionError): # pragma: no cover return self.skipTest('No testing/one_fa_pass defined') os.environ['common_name'] = one_fa_user os.environ['password'] = one_fa_pass library = DuoOpenVPN() library.log_to_stdout = False res = library.main_authentication() self.assertTrue(res, '1fa user with good password gets accepted') def test_2fa_user_bad(self): """ A 2FA user with a bad push fails PLEASE DENY """ if not self.deep_test_main: # pragma: no cover return self.skipTest('because of .deep_testing preference') os.environ['common_name'] = self.normal_user os.environ['password'] = '******' library = DuoOpenVPN() library.log_to_stdout = False res = library.main_authentication() self.assertFalse(res, '2fa user with a deny must be False') def test_2fa_user_good(self): """ A 2FA user with a bad push fails PLEASE ALLOW """ if not self.deep_test_main: # pragma: no cover return self.skipTest('because of .deep_testing preference') os.environ['common_name'] = self.normal_user os.environ['password'] = '******' library = DuoOpenVPN() library.log_to_stdout = False res = library.main_authentication() self.assertTrue(res, '2fa user with an allow must be True') def test_log_good(self): """ Test sending a log message to mozdef - all good """ # There is no raise or return. We're just poking at # the function and making sure it doesn't raise. self.main_object.log_to_stdout = False self.main_object.log(summary='TEST message', severity='DEBUG',) def test_log_bad1(self): """ Test sending a log message to mozdef - bad severity """ # There is no raise or return. We're just poking at # the function and making sure it doesn't raise. # This has a garbage severity, function should correct it. self.main_object.log_to_stdout = False self.main_object.log(summary='TEST message', severity='blerp',)
class TestDuoOpenVPN(unittest.TestCase): """ These are intended to exercise internal functions of the library's DuoOpenVPN class. """ def setUp(self): """ Preparing test rig """ # Our test cases depend on the setup of an object that reads # in the environment at the time we create an object of our # test class. As such, we don't have a good setup here. # Each test will have to do a lot of situational setup. # That said, we make a garbage object just to get our library read: self.main_object = DuoOpenVPN() self.normal_user = self.main_object.configfile.get( 'testing', 'normal_user') try: self.deep_test_main = self.main_object.configfile.getboolean( 'testing', 'deep_testing_mainauth') except (NoOptionError, NoSectionError): # pragma: no cover self.deep_test_main = False # os.environ['untrusted_ip'] = 'testing-ip-Unknown-is-OK' 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] def test_init(self): """ Verify init does the right thing """ self.assertIsNotNone(self.main_object.configfile, 'DuoOpenVPN must have a valid configfile') self.assertIsInstance(self.main_object.failopen, bool, '_fail_open must return a bool') self.assertIsInstance(self.main_object.duo_client_args, dict, 'duo_client_args must be a dict') self.assertIsInstance(self.main_object.log_to_stdout, bool, 'log_to_stdout must be a bool') def test_bogus_user(self): """ A bogus user is denied """ os.environ['common_name'] = 'user-who-does-not-exist' os.environ['password'] = '******' library = DuoOpenVPN() library.log_to_stdout = False res = library.main_authentication() self.assertFalse(res, 'invalid users must be denied') def test_1fa_user_attempts_2fa(self): """ A 1FA user trying to 2FA fails """ # This is a weird test that stems from a 1FA user pretending to # have a Duo. try: one_fa_user = self.main_object.configfile.get( 'testing', 'one_fa_user') except (NoOptionError, NoSectionError): # pragma: no cover return self.skipTest('No testing/one_fa_user defined') os.environ['common_name'] = one_fa_user os.environ['password'] = '******' library = DuoOpenVPN() library.log_to_stdout = False res = library.main_authentication() self.assertFalse(res, '1fa user attempting to 2fa must be denied') def test_1fa_user_bad_pw(self): """ A 1FA user with a bad password fails """ try: one_fa_user = self.main_object.configfile.get( 'testing', 'one_fa_user') except (NoOptionError, NoSectionError): # pragma: no cover return self.skipTest('No testing/one_fa_user defined') os.environ['common_name'] = one_fa_user os.environ['password'] = '******' library = DuoOpenVPN() library.log_to_stdout = False res = library.main_authentication() self.assertFalse(res, '1fa user with bad password must be denied') def test_1fa_user_good_pw(self): """ A 1FA user with a good password works """ try: one_fa_user = self.main_object.configfile.get( 'testing', 'one_fa_user') except (NoOptionError, NoSectionError): # pragma: no cover return self.skipTest('No testing/one_fa_user defined') try: one_fa_pass = self.main_object.configfile.get( 'testing', 'one_fa_pass') except (NoOptionError, NoSectionError): # pragma: no cover return self.skipTest('No testing/one_fa_pass defined') os.environ['common_name'] = one_fa_user os.environ['password'] = one_fa_pass library = DuoOpenVPN() library.log_to_stdout = False res = library.main_authentication() self.assertTrue(res, '1fa user with good password gets accepted') def test_2fa_user_bad(self): """ A 2FA user with a bad push fails PLEASE DENY """ if not self.deep_test_main: # pragma: no cover return self.skipTest('because of .deep_testing preference') os.environ['common_name'] = self.normal_user os.environ['password'] = '******' library = DuoOpenVPN() library.log_to_stdout = False res = library.main_authentication() self.assertFalse(res, '2fa user with a deny must be False') def test_2fa_user_good(self): """ A 2FA user with a bad push fails PLEASE ALLOW """ if not self.deep_test_main: # pragma: no cover return self.skipTest('because of .deep_testing preference') os.environ['common_name'] = self.normal_user os.environ['password'] = '******' library = DuoOpenVPN() library.log_to_stdout = False res = library.main_authentication() self.assertTrue(res, '2fa user with an allow must be True') def test_log_good(self): """ Test sending a log message to mozdef - all good """ # There is no raise or return. We're just poking at # the function and making sure it doesn't raise. self.main_object.log_to_stdout = False self.main_object.log( summary='TEST message', severity='DEBUG', ) def test_log_bad1(self): """ Test sending a log message to mozdef - bad severity """ # There is no raise or return. We're just poking at # the function and making sure it doesn't raise. # This has a garbage severity, function should correct it. self.main_object.log_to_stdout = False self.main_object.log( summary='TEST message', severity='blerp', )