class StoreTests(unittest.TestCase): def setUp(self): self.mock_filewatcher = mock.Mock(spec=FileWatcher) self.store = SecretsStore("/whatever") self.store._filewatcher = self.mock_filewatcher def test_file_not_found(self): self.mock_filewatcher.get_data.side_effect = WatchedFileNotAvailableError("path", None) with self.assertRaises(SecretsNotAvailableError): self.store.get_raw("test") def test_vault_info(self): self.mock_filewatcher.get_data.return_value = { "secrets": {}, "vault": {"token": "test", "url": "http://vault.example.com:8200/"}, } self.assertEqual(self.store.get_vault_token(), "test") self.assertEqual(self.store.get_vault_url(), "http://vault.example.com:8200/") def test_raw_secrets(self): self.mock_filewatcher.get_data.return_value = { "secrets": {"test": {"something": "exists"}}, "vault": {"token": "test", "url": "http://vault.example.com:8200/"}, } self.assertEqual(self.store.get_raw("test"), {"something": "exists"}) with self.assertRaises(SecretNotFoundError): self.store.get_raw("test_missing") def test_simple_secrets(self): self.mock_filewatcher.get_data.return_value = { "secrets": { "test": {"type": "simple", "value": "easy"}, "test_base64": {"type": "simple", "value": "aHVudGVyMg==", "encoding": "base64"}, "test_unknown_encoding": { "type": "simple", "value": "sdlfkj", "encoding": "mystery", }, "test_not_simple": {"something": "else"}, "test_no_value": {"type": "simple"}, "test_bad_base64": {"type": "simple", "value": "aHVudGVyMg", "encoding": "base64"}, }, "vault": {"token": "test", "url": "http://vault.example.com:8200/"}, } self.assertEqual(self.store.get_simple("test"), b"easy") self.assertEqual(self.store.get_simple("test_base64"), b"hunter2") with self.assertRaises(CorruptSecretError): self.store.get_simple("test_unknown_encoding") with self.assertRaises(CorruptSecretError): self.store.get_simple("test_not_simple") with self.assertRaises(CorruptSecretError): self.store.get_simple("test_no_value") with self.assertRaises(CorruptSecretError): self.store.get_simple("test_bad_base64") def test_versioned_secrets(self): self.mock_filewatcher.get_data.return_value = { "secrets": { "test": {"type": "versioned", "current": "easy"}, "test_base64": { "type": "versioned", "previous": "aHVudGVyMQ==", "current": "aHVudGVyMg==", "next": "aHVudGVyMw==", "encoding": "base64", }, "test_unknown_encoding": { "type": "versioned", "current": "sdlfkj", "encoding": "mystery", }, "test_not_versioned": {"something": "else"}, "test_no_value": {"type": "versioned"}, "test_bad_base64": {"type": "simple", "value": "aHVudGVyMg", "encoding": "base64"}, }, "vault": {"token": "test", "url": "http://vault.example.com:8200/"}, } simple = self.store.get_versioned("test") self.assertEqual(simple.current, b"easy") self.assertEqual(list(simple.all_versions), [b"easy"]) encoded = self.store.get_versioned("test_base64") self.assertEqual(encoded.previous, b"hunter1") self.assertEqual(encoded.current, b"hunter2") self.assertEqual(encoded.next, b"hunter3") self.assertEqual(list(encoded.all_versions), [b"hunter2", b"hunter1", b"hunter3"]) with self.assertRaises(CorruptSecretError): self.store.get_versioned("test_unknown_encoding") with self.assertRaises(CorruptSecretError): self.store.get_versioned("test_not_versioned") with self.assertRaises(CorruptSecretError): self.store.get_versioned("test_no_value") with self.assertRaises(CorruptSecretError): self.store.get_versioned("test_bad_base64") def test_credential_secrets(self): self.mock_filewatcher.get_data.return_value = { "secrets": { "test": {"type": "credential", "username": "******", "password": "******"}, "test_identity": { "type": "credential", "username": "******", "password": "******", "encoding": "identity", }, "test_base64": { "type": "credential", "username": "******", "password": "******", "encoding": "base64", }, "test_unknown_encoding": { "type": "credential", "username": "******", "password": "******", "encoding": "something", }, "test_not_credentials": {"type": "versioned", "current": "easy"}, "test_no_values": {"type": "credential"}, "test_no_username": {"type": "credential", "password": "******"}, "test_no_password": {"type": "credential", "username": "******"}, "test_bad_type": {"type": "credential", "username": "******", "password": 100}, }, "vault": {"token": "test", "url": "http://vault.example.com:8200/"}, } self.assertEqual(self.store.get_credentials("test"), CredentialSecret("user", "password")) self.assertEqual( self.store.get_credentials("test_identity"), CredentialSecret("spez", "hunter2") ) with self.assertRaises(CorruptSecretError): self.store.get_credentials("test_base64") with self.assertRaises(CorruptSecretError): self.store.get_credentials("test_unknown_encoding") with self.assertRaises(CorruptSecretError): self.store.get_credentials("test_not_credentials") with self.assertRaises(CorruptSecretError): self.store.get_credentials("test_no_values") with self.assertRaises(CorruptSecretError): self.store.get_credentials("test_no_username") with self.assertRaises(CorruptSecretError): self.store.get_credentials("test_no_password")
def zookeeper_client_from_config( secrets: SecretsStore, app_config: config.RawConfig, read_only: Optional[bool] = None) -> KazooClient: """Configure and return a ZooKeeper client. There are several configuration options: ``zookeeper.hosts`` A comma-delimited list of hosts with optional ``chroot`` at the end. For example ``zk01:2181,zk02:2181`` or ``zk01:2181,zk02:2181/some/root``. ``zookeeper.credentials`` (Optional) A comma-delimited list of paths to secrets in the secrets store that contain ZooKeeper authentication credentials. Secrets should be of the "simple" type and contain ``username:password``. ``zookeeper.timeout`` (Optional) A time span of how long to wait for each connection attempt. The client will attempt forever to reconnect on connection loss. :param secrets: A secrets store object :param raw_config: The application configuration which should have settings for the ZooKeeper client. :param read_only: Whether or not to allow connections to read-only ZooKeeper servers. """ full_cfg = config.parse_config( app_config, { "zookeeper": { "hosts": config.String, "credentials": config.Optional(config.TupleOf(config.String), default=[]), "timeout": config.Optional(config.Timespan, default=config.Timespan("5 seconds")), } }, ) # pylint: disable=maybe-no-member cfg = full_cfg.zookeeper auth_data = [] for path in cfg.credentials: credentials = secrets.get_simple(path) auth_data.append(("digest", credentials.decode("utf8"))) return KazooClient( cfg.hosts, timeout=cfg.timeout.total_seconds(), auth_data=auth_data, read_only=read_only, # this retry policy tells Kazoo how often it should attempt connections # to ZooKeeper from its worker thread/greenlet. when the connection is # lost during normal operation (i.e. after it was first established) # Kazoo will do retries quietly in the background while the application # continues forward. because of this, we want it to retry forever so # that it doesn't just give up at some point. the application can still # decide if it wants to exit after being disconnected for an amount of # time by polling the KazooClient.connected property. # # note: KazooClient.start() has a timeout parameter which defaults to # 15 seconds and controls the maximum amount of time start() will block # waiting for the background thread to confirm it has established a # connection. so even though we do infinite retries here, users of this # function can configure the amount of time they are willing to wait # for initial connection. connection_retry=dict( max_tries=-1, # keep reconnecting forever delay=0.1, # initial delay backoff=2, # exponential backoff max_jitter=1, # maximum amount to jitter sleeptimes max_delay=60, # never wait longer than this ), )