diff --git a/oslo_messaging/_drivers/amqp1_driver/controller.py b/oslo_messaging/_drivers/amqp1_driver/controller.py index f0d073b8d..1bd37772d 100644 --- a/oslo_messaging/_drivers/amqp1_driver/controller.py +++ b/oslo_messaging/_drivers/amqp1_driver/controller.py @@ -763,13 +763,14 @@ class NotificationServer(Server): class Hosts(object): - """An order list of TransportHost addresses. Connection failover - progresses from one host to the next. username and password come from the - configuration and are used only if no username/password was given in the - URL. + """An order list of TransportHost addresses. Connection failover progresses + from one host to the next. username, password , and realm come from the + configuration and are used only if no username/password/realm is present in + the URL. """ def __init__(self, entries=None, default_username=None, - default_password=None): + default_password=None, + default_realm=None): if entries: self._entries = entries[:] else: @@ -779,6 +780,8 @@ class Hosts(object): entry.port = entry.port or 5672 entry.username = entry.username or default_username entry.password = entry.password or default_password + if default_realm and entry.username and '@' not in entry.username: + entry.username = entry.username + '@' + default_realm self._current = random.randint(0, len(self._entries) - 1) @property @@ -840,7 +843,8 @@ class Controller(pyngus.ConnectionEventHandler): self.sasl_config_dir = config.oslo_messaging_amqp.sasl_config_dir self.sasl_config_name = config.oslo_messaging_amqp.sasl_config_name self.hosts = Hosts(hosts, config.oslo_messaging_amqp.username, - config.oslo_messaging_amqp.password) + config.oslo_messaging_amqp.password, + config.oslo_messaging_amqp.sasl_default_realm) self.conn_retry_interval = \ config.oslo_messaging_amqp.connection_retry_interval self.conn_retry_backoff = \ diff --git a/oslo_messaging/_drivers/amqp1_driver/opts.py b/oslo_messaging/_drivers/amqp1_driver/opts.py index ad53f49d2..20860306b 100644 --- a/oslo_messaging/_drivers/amqp1_driver/opts.py +++ b/oslo_messaging/_drivers/amqp1_driver/opts.py @@ -83,6 +83,10 @@ amqp1_opts = [ deprecated_group='amqp1', help='Name of configuration file (without .conf suffix)'), + cfg.StrOpt('sasl_default_realm', + default='', + help='SASL realm to use if no realm present in username'), + cfg.StrOpt('username', default='', deprecated_group='amqp1', diff --git a/oslo_messaging/tests/drivers/test_amqp_driver.py b/oslo_messaging/tests/drivers/test_amqp_driver.py index 51b67a102..9f7b2a5f0 100644 --- a/oslo_messaging/tests/drivers/test_amqp_driver.py +++ b/oslo_messaging/tests/drivers/test_amqp_driver.py @@ -711,7 +711,7 @@ class TestCyrusAuthentication(test_utils.BaseTestCase): # the temp dir after the first test is run os.makedirs(cls._conf_dir) db = os.path.join(cls._conf_dir, 'openstack.sasldb') - _t = "echo secret | saslpasswd2 -c -p -f ${db} joe" + _t = "echo secret | saslpasswd2 -c -p -f ${db} -u myrealm joe" cmd = Template(_t).substitute(db=db) try: subprocess.check_call(args=cmd, shell=True) @@ -744,7 +744,7 @@ mech_list: ${mechs} _dir = TestCyrusAuthentication._conf_dir self._broker = FakeBroker(self.conf.oslo_messaging_amqp, sasl_mechanisms=_mechs, - user_credentials=["\0joe\0secret"], + user_credentials=["\0joe@myrealm\0secret"], sasl_config_dir=_dir, sasl_config_name="openstack") self._broker.start() @@ -757,45 +757,44 @@ mech_list: ${mechs} self._broker = None super(TestCyrusAuthentication, self).tearDown() - def test_authentication_ok(self): - """Verify that username and password given in TransportHost are - accepted by the broker. - """ - addr = "amqp://joe:secret@%s:%d" % (self._broker.host, - self._broker.port) + def _authentication_test(self, addr, retry=None): url = oslo_messaging.TransportURL.parse(self.conf, addr) driver = amqp_driver.ProtonDriver(self.conf, url) target = oslo_messaging.Target(topic="test-topic") listener = _ListenerThread( driver.listen(target, None, None)._poll_style_listener, 1) - rc = driver.send(target, {"context": True}, - {"method": "echo"}, wait_for_reply=True) - self.assertIsNotNone(rc) - listener.join(timeout=30) - self.assertFalse(listener.isAlive()) - driver.cleanup() + try: + rc = driver.send(target, {"context": True}, + {"method": "echo"}, wait_for_reply=True, + retry=retry) + self.assertIsNotNone(rc) + listener.join(timeout=30) + self.assertFalse(listener.isAlive()) + finally: + driver.cleanup() + + def test_authentication_ok(self): + """Verify that username and password given in TransportHost are + accepted by the broker. + """ + addr = "amqp://joe@myrealm:secret@%s:%d" % (self._broker.host, + self._broker.port) + self._authentication_test(addr) def test_authentication_failure(self): """Verify that a bad password given in TransportHost is rejected by the broker. """ - addr = "amqp://joe:badpass@%s:%d" % (self._broker.host, - self._broker.port) - url = oslo_messaging.TransportURL.parse(self.conf, addr) - driver = amqp_driver.ProtonDriver(self.conf, url) - target = oslo_messaging.Target(topic="test-topic") - _ListenerThread( - driver.listen(target, None, None)._poll_style_listener, 1) + addr = "amqp://joe@myrealm:badpass@%s:%d" % (self._broker.host, + self._broker.port) try: - driver.send(target, {"context": True}, {"method": "echo"}, - wait_for_reply=True, retry=2) + self._authentication_test(addr, retry=2) except oslo_messaging.MessageDeliveryFailure as e: # verify the exception indicates the failure was an authentication # error self.assertTrue('amqp:unauthorized-access' in str(e)) else: self.assertIsNone("Expected authentication failure") - driver.cleanup() def test_authentication_bad_mechs(self): """Verify that the connection fails if the client's SASL mechanisms do @@ -803,40 +802,41 @@ mech_list: ${mechs} """ self.config(sasl_mechanisms="EXTERNAL ANONYMOUS", group="oslo_messaging_amqp") - addr = "amqp://joe:secret@%s:%d" % (self._broker.host, - self._broker.port) - url = oslo_messaging.TransportURL.parse(self.conf, addr) - driver = amqp_driver.ProtonDriver(self.conf, url) - target = oslo_messaging.Target(topic="test-topic") - _ListenerThread( - driver.listen(target, None, None)._poll_style_listener, 1) + addr = "amqp://joe@myrealm:secret@%s:%d" % (self._broker.host, + self._broker.port) self.assertRaises(oslo_messaging.MessageDeliveryFailure, - driver.send, - target, {"context": True}, - {"method": "echo"}, - wait_for_reply=True, + self._authentication_test, + addr, retry=0) - driver.cleanup() def test_authentication_default_username(self): """Verify that a configured username/password is used if none appears in the URL. + Deprecated: username password deprecated in favor of transport_url """ addr = "amqp://%s:%d" % (self._broker.host, self._broker.port) - self.config(username="joe", + self.config(username="joe@myrealm", password="secret", group="oslo_messaging_amqp") - url = oslo_messaging.TransportURL.parse(self.conf, addr) - driver = amqp_driver.ProtonDriver(self.conf, url) - target = oslo_messaging.Target(topic="test-topic") - listener = _ListenerThread( - driver.listen(target, None, None)._poll_style_listener, 1) - rc = driver.send(target, {"context": True}, - {"method": "echo"}, wait_for_reply=True) - self.assertIsNotNone(rc) - listener.join(timeout=30) - self.assertFalse(listener.isAlive()) - driver.cleanup() + self._authentication_test(addr) + + def test_authentication_default_realm(self): + """Verify that default realm is used if none present in username""" + addr = "amqp://joe:secret@%s:%d" % (self._broker.host, + self._broker.port) + self.config(sasl_default_realm="myrealm", + group="oslo_messaging_amqp") + self._authentication_test(addr) + + def test_authentication_ignore_default_realm(self): + """Verify that default realm is not used if realm present in + username + """ + addr = "amqp://joe@myrealm:secret@%s:%d" % (self._broker.host, + self._broker.port) + self.config(sasl_default_realm="bad-realm", + group="oslo_messaging_amqp") + self._authentication_test(addr) @testtools.skipUnless(pyngus, "proton modules not present")