From d3cca385a5fe39181eedb32efc625aeabb6db4e0 Mon Sep 17 00:00:00 2001 From: Othman Moumni Abdou Date: Fri, 22 Oct 2021 16:52:58 +0200 Subject: [PATCH] Feat #1 Add CRUD on topic-permissions * Black * Fix tests as in https://github.com/ambitioninc/rabbitmq-admin/pull/7 * Add env variables for tests --- .travis.yml | 5 + CONTRIBUTORS.txt | 1 + rabbitmq_admin/api.py | 321 ++++++++++++++--------- rabbitmq_admin/tests/api_tests.py | 420 ++++++++++++++---------------- setup.py | 69 ++--- 5 files changed, 431 insertions(+), 385 deletions(-) diff --git a/.travis.yml b/.travis.yml index 849b165..bb5639e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,10 +11,15 @@ before_install: - sudo iptables -N DOCKER || true - "docker run -d -h rabbit1 -p 5673:5672 -p 15673:15672 -e RABBITMQ_DEFAULT_USER=guest -e RABBITMQ_DEFAULT_PASS=guest --name rabbit1 rabbitmq:3-management" - docker ps -a +env: + - RABBITMQ_HOST=127.0.0.1 + - RABBITMQ_AMQP_PORT=5673 + - RABBITMQ_ADMIN_PORT=15673 install: - pip install flake8 nose>=1.3.0 coverage coveralls - pip install -e .[all] script: + - black . - flake8 . - python setup.py nosetests - coverage report diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index bab5ff2..df8f215 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -1 +1,2 @@ Micah Hausler (micah.hausler@ambition.com) +Othman Moumni Abdou (othman.moumniabdou@inuse.eu) diff --git a/rabbitmq_admin/api.py b/rabbitmq_admin/api.py index 26152a3..e96b679 100644 --- a/rabbitmq_admin/api.py +++ b/rabbitmq_admin/api.py @@ -11,23 +11,21 @@ def overview(self): """ Various random bits of information that describe the whole system """ - return self._api_get('/api/overview') + return self._api_get("/api/overview") def get_cluster_name(self): """ Name identifying this RabbitMQ cluster. """ return self._get( - url=self.url + '/api/cluster-name', - headers=self.headers, - auth=self.auth + url=self.url + "/api/cluster-name", headers=self.headers, auth=self.auth ) def list_nodes(self): """ A list of nodes in the RabbitMQ cluster. """ - return self._api_get('/api/nodes') + return self._api_get("/api/nodes") def get_node(self, name, memory=False, binary=False): """ @@ -37,18 +35,15 @@ def get_node(self, name, memory=False, binary=False): system). """ return self._api_get( - url='/api/nodes/{0}'.format(name), - params=dict( - binary=binary, - memory=memory, - ), + url="/api/nodes/{0}".format(name), + params=dict(binary=binary, memory=memory,), ) def list_extensions(self): """ A list of extensions to the management plugin. """ - return self._api_get('/api/extensions') + return self._api_get("/api/extensions") def get_definitions(self): """ @@ -58,7 +53,7 @@ def get_definitions(self): This method can be used for backing up the configuration of a server or cluster. """ - return self._api_get('/api/definitions') + return self._api_get("/api/definitions") def post_definitions(self, data): """ @@ -82,13 +77,13 @@ def post_definitions(self, data): :param data: The definitions for a RabbitMQ server :type data: dict """ - self._api_post('/api/definitions', data=data) + self._api_post("/api/definitions", data=data) def list_connections(self): """ A list of all open connections. """ - return self._api_get('/api/connections') + return self._api_get("/api/connections") def get_connection(self, name): """ @@ -97,9 +92,7 @@ def get_connection(self, name): :param name: The connection name :type name: str """ - return self._api_get('/api/connections/{0}'.format( - urllib.parse.quote_plus(name) - )) + return self._api_get("/api/connections/{0}".format(urllib.parse.quote(name))) def delete_connection(self, name, reason=None): """ @@ -111,13 +104,10 @@ def delete_connection(self, name, reason=None): :param reason: An option reason why the connection was deleted :type reason: str """ - headers = {'X-Reason': reason} if reason else {} + headers = {"X-Reason": reason} if reason else {} self._api_delete( - '/api/connections/{0}'.format( - urllib.parse.quote_plus(name) - ), - headers=headers, + "/api/connections/{0}".format(urllib.parse.quote(name)), headers=headers, ) def list_connection_channels(self, name): @@ -127,15 +117,15 @@ def list_connection_channels(self, name): :param name: The connection name :type name: str """ - return self._api_get('/api/connections/{0}/channels'.format( - urllib.parse.quote_plus(name) - )) + return self._api_get( + "/api/connections/{0}/channels".format(urllib.parse.quote(name)) + ) def list_channels(self): """ A list of all open channels. """ - return self._api_get('/api/channels') + return self._api_get("/api/channels") def get_channel(self, name): """ @@ -144,15 +134,13 @@ def get_channel(self, name): :param name: The channel name :type name: str """ - return self._api_get('/api/channels/{0}'.format( - urllib.parse.quote_plus(name) - )) + return self._api_get("/api/channels/{0}".format(urllib.parse.quote(name))) def list_consumers(self): """ A list of all consumers. """ - return self._api_get('/api/consumers') + return self._api_get("/api/consumers") def list_consumers_for_vhost(self, vhost): """ @@ -161,15 +149,15 @@ def list_consumers_for_vhost(self, vhost): :param vhost: The vhost name :type vhost: str """ - return self._api_get('/api/consumers/{0}'.format( - urllib.parse.quote_plus(vhost) - )) + return self._api_get( + "/api/consumers/{0}".format(urllib.parse.quote_plus(vhost)) + ) def list_exchanges(self): """ A list of all exchanges. """ - return self._api_get('/api/exchanges') + return self._api_get("/api/exchanges") def list_exchanges_for_vhost(self, vhost): """ @@ -178,9 +166,9 @@ def list_exchanges_for_vhost(self, vhost): :param vhost: The vhost name :type vhost: str """ - return self._api_get('/api/exchanges/{0}'.format( - urllib.parse.quote_plus(vhost) - )) + return self._api_get( + "/api/exchanges/{0}".format(urllib.parse.quote_plus(vhost)) + ) def get_exchange_for_vhost(self, exchange, vhost): """ @@ -192,10 +180,11 @@ def get_exchange_for_vhost(self, exchange, vhost): :param vhost: The vhost name :type vhost: str """ - return self._api_get('/api/exchanges/{0}/{1}'.format( - urllib.parse.quote_plus(vhost), - urllib.parse.quote_plus(exchange) - )) + return self._api_get( + "/api/exchanges/{0}/{1}".format( + urllib.parse.quote_plus(vhost), urllib.parse.quote_plus(exchange) + ) + ) def create_exchange_for_vhost(self, exchange, vhost, body): """ @@ -223,10 +212,10 @@ def create_exchange_for_vhost(self, exchange, vhost, body): :type body: dict """ self._api_put( - '/api/exchanges/{0}/{1}'.format( - urllib.parse.quote_plus(vhost), - urllib.parse.quote_plus(exchange)), - data=body + "/api/exchanges/{0}/{1}".format( + urllib.parse.quote_plus(vhost), urllib.parse.quote_plus(exchange) + ), + data=body, ) def delete_exchange_for_vhost(self, exchange, vhost, if_unused=False): @@ -245,19 +234,17 @@ def delete_exchange_for_vhost(self, exchange, vhost, if_unused=False): :type if_unused: bool """ self._api_delete( - '/api/exchanges/{0}/{1}'.format( - urllib.parse.quote_plus(vhost), - urllib.parse.quote_plus(exchange)), - params={ - 'if-unused': if_unused - }, + "/api/exchanges/{0}/{1}".format( + urllib.parse.quote_plus(vhost), urllib.parse.quote_plus(exchange) + ), + params={"if-unused": if_unused}, ) def list_bindings(self): """ A list of all bindings. """ - return self._api_get('/api/bindings') + return self._api_get("/api/bindings") def list_bindings_for_vhost(self, vhost): """ @@ -266,15 +253,13 @@ def list_bindings_for_vhost(self, vhost): :param vhost: The vhost name :type vhost: str """ - return self._api_get('/api/bindings/{}'.format( - urllib.parse.quote_plus(vhost) - )) + return self._api_get("/api/bindings/{}".format(urllib.parse.quote_plus(vhost))) def list_vhosts(self): """ A list of all vhosts. """ - return self._api_get('/api/vhosts') + return self._api_get("/api/vhosts") def get_vhost(self, name): """ @@ -283,9 +268,7 @@ def get_vhost(self, name): :param name: The vhost name :type name: str """ - return self._api_get('/api/vhosts/{0}'.format( - urllib.parse.quote_plus(name) - )) + return self._api_get("/api/vhosts/{0}".format(urllib.parse.quote_plus(name))) def delete_vhost(self, name): """ @@ -294,9 +277,7 @@ def delete_vhost(self, name): :param name: The vhost name :type name: str """ - self._api_delete('/api/vhosts/{0}'.format( - urllib.parse.quote_plus(name) - )) + self._api_delete("/api/vhosts/{0}".format(urllib.parse.quote_plus(name))) def create_vhost(self, name, tracing=False): """ @@ -308,17 +289,16 @@ def create_vhost(self, name, tracing=False): :param tracing: Set to ``True`` to enable tracing :type tracing: bool """ - data = {'tracing': True} if tracing else {} + data = {"tracing": True} if tracing else {} self._api_put( - '/api/vhosts/{0}'.format(urllib.parse.quote_plus(name)), - data=data, + "/api/vhosts/{0}".format(urllib.parse.quote_plus(name)), data=data, ) def list_users(self): """ A list of all users. """ - return self._api_get('/api/users') + return self._api_get("/api/users") def get_user(self, name): """ @@ -327,9 +307,7 @@ def get_user(self, name): :param name: The user's name :type name: str """ - return self._api_get('/api/users/{0}'.format( - urllib.parse.quote_plus(name) - )) + return self._api_get("/api/users/{0}".format(urllib.parse.quote_plus(name))) def delete_user(self, name): """ @@ -338,9 +316,7 @@ def delete_user(self, name): :param name: The user's name :type name: str """ - self._api_delete('/api/users/{0}'.format( - urllib.parse.quote_plus(name) - )) + self._api_delete("/api/users/{0}".format(urllib.parse.quote_plus(name))) def create_user(self, name, password, password_hash=None, tags=None): """ @@ -358,19 +334,16 @@ def create_user(self, name, password, password_hash=None, tags=None): supplied, the user will have no permissions. :type tags: list of str """ - data = { - 'tags': ', '.join(tags or []) - } + data = {"tags": ", ".join(tags or [])} if password: - data['password'] = password + data["password"] = password elif password_hash: - data['password_hash'] = password_hash + data["password_hash"] = password_hash else: - data['password_hash'] = "" + data["password_hash"] = "" self._api_put( - '/api/users/{0}'.format(urllib.parse.quote_plus(name)), - data=data, + "/api/users/{0}".format(urllib.parse.quote_plus(name)), data=data, ) def list_user_permissions(self, name): @@ -380,21 +353,21 @@ def list_user_permissions(self, name): :param name: The user's name :type name: str """ - return self._api_get('/api/users/{0}/permissions'.format( - urllib.parse.quote_plus(name) - )) + return self._api_get( + "/api/users/{0}/permissions".format(urllib.parse.quote_plus(name)) + ) def whoami(self): """ Details of the currently authenticated user. """ - return self._api_get('/api/whoami') + return self._api_get("/api/whoami") def list_permissions(self): """ A list of all permissions for all users. """ - return self._api_get('/api/permissions') + return self._api_get("/api/permissions") def get_user_permission(self, vhost, name): """ @@ -406,10 +379,11 @@ def get_user_permission(self, vhost, name): :param name: The user's name :type name: str """ - return self._api_get('/api/permissions/{0}/{1}'.format( - urllib.parse.quote_plus(vhost), - urllib.parse.quote_plus(name) - )) + return self._api_get( + "/api/permissions/{0}/{1}".format( + urllib.parse.quote_plus(vhost), urllib.parse.quote_plus(name) + ) + ) def delete_user_permission(self, name, vhost): """ @@ -421,17 +395,15 @@ def delete_user_permission(self, name, vhost): :param vhost: The vhost name :type vhost: str """ - self._api_delete('/api/permissions/{0}/{1}'.format( - urllib.parse.quote_plus(vhost), - urllib.parse.quote_plus(name) - )) + self._api_delete( + "/api/permissions/{0}/{1}".format( + urllib.parse.quote_plus(vhost), urllib.parse.quote_plus(name) + ) + ) - def create_user_permission(self, - name, - vhost, - configure=None, - write=None, - read=None): + def create_user_permission( + self, name, vhost, configure=None, write=None, read=None + ): """ Create a user permission :param name: The user's name @@ -447,31 +419,28 @@ def create_user_permission(self, :type read: str """ data = { - 'configure': configure or '.*', - 'write': write or '.*', - 'read': read or '.*', + "configure": configure or ".*", + "write": write or ".*", + "read": read or ".*", } self._api_put( - '/api/permissions/{0}/{1}'.format( - urllib.parse.quote_plus(vhost), - urllib.parse.quote_plus(name) + "/api/permissions/{0}/{1}".format( + urllib.parse.quote_plus(vhost), urllib.parse.quote_plus(name) ), - data=data + data=data, ) def list_policies(self): """ A list of all policies """ - return self._api_get('/api/policies') + return self._api_get("/api/policies") def list_policies_for_vhost(self, vhost): """ A list of all policies for a vhost. """ - return self._api_get('/api/policies/{0}'.format( - urllib.parse.quote_plus(vhost) - )) + return self._api_get("/api/policies/{0}".format(urllib.parse.quote_plus(vhost))) def get_policy_for_vhost(self, vhost, name): """ @@ -482,17 +451,15 @@ def get_policy_for_vhost(self, vhost, name): :param name: The name of the policy :type name: str """ - return self._api_get('/api/policies/{0}/{1}'.format( - urllib.parse.quote_plus(vhost), - urllib.parse.quote_plus(name), - )) + return self._api_get( + "/api/policies/{0}/{1}".format( + urllib.parse.quote_plus(vhost), urllib.parse.quote_plus(name), + ) + ) def create_policy_for_vhost( - self, vhost, name, - definition, - pattern=None, - priority=0, - apply_to='all'): + self, vhost, name, definition, pattern=None, priority=0, apply_to="all" + ): """ Create a policy for a vhost. @@ -526,12 +493,11 @@ def create_policy_for_vhost( "pattern": pattern, "definition": definition, "priority": priority, - "apply-to": apply_to + "apply-to": apply_to, } self._api_put( - '/api/policies/{0}/{1}'.format( - urllib.parse.quote_plus(vhost), - urllib.parse.quote_plus(name), + "/api/policies/{0}/{1}".format( + urllib.parse.quote_plus(vhost), urllib.parse.quote_plus(name), ), data=data, ) @@ -545,10 +511,11 @@ def delete_policy_for_vhost(self, vhost, name): :param name: The name of the policy :type name: str """ - self._api_delete('/api/policies/{0}/{1}/'.format( - urllib.parse.quote_plus(vhost), - urllib.parse.quote_plus(name), - )) + self._api_delete( + "/api/policies/{0}/{1}/".format( + urllib.parse.quote_plus(vhost), urllib.parse.quote_plus(name), + ) + ) def is_vhost_alive(self, vhost): """ @@ -558,6 +525,100 @@ def is_vhost_alive(self, vhost): :param vhost: The vhost name to check :type vhost: str """ - return self._api_get('/api/aliveness-test/{0}'.format( - urllib.parse.quote_plus(vhost) - )) + return self._api_get( + "/api/aliveness-test/{0}".format(urllib.parse.quote_plus(vhost)) + ) + + def list_topic_permissions(self): + """ + A list of all topic permissions for all users. + """ + return self._api_get("/api/topic-permissions") + + def list_vhost_topic_permissions(self, vhost): + """ + A list of all topic permissions for a given virtual host. + + :param vhost: The vhost name + :type vhost: str + """ + return self._api_get( + "/api/vhosts/{0}/topic-permissions".format(urllib.parse.quote_plus(vhost)) + ) + + def list_user_topic_permissions(self, user): + """ + A list of all topic permissions for a given user. + + :param user: The user's name + :type user: str + """ + return self._api_get( + "/api/users/{0}/topic-permissions".format(urllib.parse.quote_plus(user)) + ) + + def list_vhost_user_topic_permissions(self, vhost, user): + """ + A set of topic permission of a user and virtual host. + + :param vhost: The vhost name + :type vhost: str + + :param user: The user's name + :type user: str + """ + return self._api_get( + "/api/topic-permissions/{0}/{1}".format( + urllib.parse.quote_plus(vhost), urllib.parse.quote_plus(user) + ) + ) + + def create_topic_permission(self, user, vhost, exchange, write="", read=""): + """ + Create a topic permission. For the ``read`` and ``write`` topic permissions, + ``''`` is a synonym for ``'^$'`` and restricts permissions in the exact same way. + + :param user: The user's name + :type user: str + :param vhost: The vhost to assign the permission to + :type vhost: str + :param exchange: The exchange name + :type exchange: str + :param write: A regex for the write permission. Default is '' + :type write: str + :param read: A regex for the read permission. Default is '' + :type read: str + """ + + data = { + "exchange": exchange, + "read": read, + "write": write, + } + return self._api_put( + "/api/topic-permissions/{0}/{1}".format( + urllib.parse.quote_plus(vhost), urllib.parse.quote_plus(user) + ), + data=data, + ) + + def delete_topic_permission(self, user, vhost, exchange): + """ + Delete an individual topic permission of a user, virtual host and exchange. + + :param user: The user's name + :type user: str + + :param vhost: The vhost name + :type vhost: str + + :param exchange: The exchange name + :type exchange: str + """ + self._api_delete( + "/api/topic-permissions/{0}/{1}/{2}".format( + urllib.parse.quote_plus(vhost), + urllib.parse.quote_plus(user), + urllib.parse.quote_plus(exchange), + ), + ) diff --git a/rabbitmq_admin/tests/api_tests.py b/rabbitmq_admin/tests/api_tests.py index 8f3e009..1bc858e 100644 --- a/rabbitmq_admin/tests/api_tests.py +++ b/rabbitmq_admin/tests/api_tests.py @@ -1,4 +1,5 @@ import os +import time from unittest import TestCase import pika @@ -15,11 +16,11 @@ class AdminAPITests(TestCase): docker run -d \ -h rabbit1 \ - -p 5672:5672 \ - -p 15672:15672 \ + -p 5673:5672 \ + -p 15673:15672 \ -e RABBITMQ_DEFAULT_USER=guest \ -e RABBITMQ_DEFAULT_PASS=guest \ - -name rabbit1 \ + --name rabbit_test \ rabbitmq:3-management """ @@ -33,29 +34,26 @@ def setUpClass(cls): TravisCI sometimes turns on RabbitMQ when we don't want it, so we use alternative ports 5673 and 15673 """ - if os.environ.get('TRAVIS'): # pragma: no cover - cls.host = '127.0.0.1' - cls.amqp_port = 5673 - cls.admin_port = 15673 - else: # pragma: no cover - cls.host = os.environ.get('RABBITMQ_HOST', '192.168.99.100') - cls.amqp_port = 5672 - cls.admin_port = 15672 - - credentials = pika.PlainCredentials('guest', 'guest') + + cls.host = os.environ.get("RABBITMQ_HOST", "127.0.0.1") + cls.amqp_port = int(os.environ.get("RABBITMQ_AMQP_PORT", "5673")) + cls.admin_port = int(os.environ.get("RABBITMQ_ADMIN_PORT", "15673")) + + credentials = pika.PlainCredentials("guest", "guest") cls.connection = pika.BlockingConnection( pika.ConnectionParameters( - cls.host, - port=cls.amqp_port, - credentials=credentials + cls.host, port=cls.amqp_port, credentials=credentials ), ) channel = cls.connection.channel() - channel.queue_declare(queue='test_queue') + channel.queue_declare(queue="test_queue") channel.basic_publish( - exchange='', - routing_key='test_queue', - body='Test Message') + exchange="", routing_key="test_queue", body="Test Message" + ) + # It takes a while for rabbitmq-mgmt to detect new connection. + # Waiting a few seconds should be enough. + # See also test_list_connections code - it needed 3 seconds on local workstation. + time.sleep(5) @classmethod def tearDownClass(cls): @@ -64,324 +62,262 @@ def tearDownClass(cls): def setUp(self): super(AdminAPITests, self).setUp() - url = 'http://{host}:{port}'.format(host=self.host, port=self.admin_port) + url = "http://{host}:{port}".format(host=self.host, port=self.admin_port) - self.api = AdminAPI(url, auth=('guest', 'guest')) - self.node_name = 'rabbit@rabbit1' + self.api = AdminAPI(url, auth=("guest", "guest")) + self.node_name = "rabbit@rabbit1" def test_overview(self): response = self.api.overview() self.assertIsInstance(response, dict) def test_get_cluster_name(self): - self.assertDictEqual( - self.api.get_cluster_name(), - {'name': 'rabbit@rabbit1'} - ) + self.assertDictEqual(self.api.get_cluster_name(), {"name": "rabbit@rabbit1"}) def test_list_nodes(self): - self.assertEqual( - len(self.api.list_nodes()), - 1 - ) + self.assertEqual(len(self.api.list_nodes()), 1) def test_get_node(self): response = self.api.get_node(self.node_name) self.assertIsInstance(response, dict) - self.assertEqual(response['name'], self.node_name) + self.assertEqual(response["name"], self.node_name) def test_list_extensions(self): - self.assertEqual( - self.api.list_extensions(), - [{'javascript': 'dispatcher.js'}] - ) + self.assertEqual(self.api.list_extensions(), [{"javascript": "dispatcher.js"}]) def test_get_definitions(self): response = self.api.get_definitions() - self.assertEqual(len(response['users']), 1) - self.assertEqual(len(response['vhosts']), 1) + self.assertEqual(len(response["users"]), 1) + self.assertEqual(len(response["vhosts"]), 1) def test_post_definitions(self): response = self.api.get_definitions() self.api.post_definitions(response) def test_list_connections(self): - self.assertEqual( - len(self.api.list_connections()), - 1 - ) + self.assertEqual(len(self.api.list_connections()), 1) def test_get_connection(self): - cname = self.api.list_connections()[0].get('name') - self.assertIsInstance( - self.api.get_connection(cname), - dict - ) + cname = self.api.list_connections()[0].get("name") + self.assertIsInstance(self.api.get_connection(cname), dict) def test_delete_connection(self): with self.assertRaises(requests.HTTPError): - self.api.delete_connection('not-a-connection') + self.api.delete_connection("not-a-connection") def test_delete_connection_with_reson(self): with self.assertRaises(requests.HTTPError): - self.api.delete_connection('not-a-connection', 'I don\'t like you') + self.api.delete_connection("not-a-connection", "I don't like you") def test_list_connection_channels(self): - cname = self.api.list_connections()[0].get('name') + cname = self.api.list_connections()[0].get("name") response = self.api.list_connection_channels(cname) - self.assertEqual( - response[0].get('name'), - cname + ' (1)' - ) + self.assertEqual(response[0].get("name"), cname + " (1)") def test_list_channels(self): - self.assertEqual( - len(self.api.list_channels()), - 1 - ) + self.assertEqual(len(self.api.list_channels()), 1) def test_get_channel(self): - cname = self.api.list_channels()[0].get('name') - self.assertIsInstance( - self.api.get_channel(cname), - dict - ) + cname = self.api.list_channels()[0].get("name") + self.assertIsInstance(self.api.get_channel(cname), dict) def test_list_consumers(self): - self.assertEqual( - self.api.list_consumers(), - [] - ) + self.assertEqual(self.api.list_consumers(), []) def test_list_consumers_for_vhost(self): - self.assertEqual( - self.api.list_consumers_for_vhost('/'), - [] - ) + self.assertEqual(self.api.list_consumers_for_vhost("/"), []) def test_list_exchanges(self): - self.assertEqual( - len(self.api.list_exchanges()), - 8 - ) + self.assertEqual(len(self.api.list_exchanges()), 7) def test_list_exchanges_for_vhost(self): - self.assertEqual( - len(self.api.list_exchanges_for_vhost('/')), - 8 - ) + self.assertEqual(len(self.api.list_exchanges_for_vhost("/")), 7) def test_get_create_delete_exchange_for_vhost(self): - name = 'myexchange' + name = "myexchange" body = { "type": "direct", "auto_delete": False, "durable": False, "internal": False, - "arguments": {} + "arguments": {}, } - self.api.create_exchange_for_vhost(name, '/', body) - self.assertEqual( - len(self.api.list_exchanges_for_vhost('/')), - 9 - ) - self.assertEqual( - self.api.get_exchange_for_vhost(name, '/').get('name'), - name - ) + self.api.create_exchange_for_vhost(name, "/", body) + self.assertEqual(len(self.api.list_exchanges_for_vhost("/")), 8) + self.assertEqual(self.api.get_exchange_for_vhost(name, "/").get("name"), name) - self.api.delete_exchange_for_vhost(name, '/') - self.assertEqual( - len(self.api.list_exchanges_for_vhost('/')), - 8 - ) + self.api.delete_exchange_for_vhost(name, "/") + self.assertEqual(len(self.api.list_exchanges_for_vhost("/")), 7) def test_list_bindings(self): self.assertEqual( self.api.list_bindings(), - [{'arguments': {}, - 'destination': 'aliveness-test', - 'destination_type': 'queue', - 'properties_key': 'aliveness-test', - 'routing_key': 'aliveness-test', - 'source': '', - 'vhost': '/'}, - {'arguments': {}, - 'destination': 'test_queue', - 'destination_type': 'queue', - 'properties_key': 'test_queue', - 'routing_key': 'test_queue', - 'source': '', - 'vhost': '/'}] + [ + { + "arguments": {}, + "destination": "aliveness-test", + "destination_type": "queue", + "properties_key": "aliveness-test", + "routing_key": "aliveness-test", + "source": "", + "vhost": "/", + }, + { + "arguments": {}, + "destination": "test_queue", + "destination_type": "queue", + "properties_key": "test_queue", + "routing_key": "test_queue", + "source": "", + "vhost": "/", + }, + ], ) def test_list_bindings_for_vhost(self): self.assertEqual( - self.api.list_bindings_for_vhost('/'), - [{'arguments': {}, - 'destination': 'aliveness-test', - 'destination_type': 'queue', - 'properties_key': 'aliveness-test', - 'routing_key': 'aliveness-test', - 'source': '', - 'vhost': '/'}, - {'arguments': {}, - 'destination': 'test_queue', - 'destination_type': 'queue', - 'properties_key': 'test_queue', - 'routing_key': 'test_queue', - 'source': '', - 'vhost': '/'}] + self.api.list_bindings_for_vhost("/"), + [ + { + "arguments": {}, + "destination": "aliveness-test", + "destination_type": "queue", + "properties_key": "aliveness-test", + "routing_key": "aliveness-test", + "source": "", + "vhost": "/", + }, + { + "arguments": {}, + "destination": "test_queue", + "destination_type": "queue", + "properties_key": "test_queue", + "routing_key": "test_queue", + "source": "", + "vhost": "/", + }, + ], ) def test_list_vhosts(self): response = self.api.list_vhosts() - self.assertEqual( - len(response), - 1 - ) - self.assertEqual(response[0].get('name'), '/') + self.assertEqual(len(response), 1) + self.assertEqual(response[0].get("name"), "/") def test_get_vhosts(self): - response = self.api.get_vhost('/') - self.assertEqual(response.get('name'), '/') + response = self.api.get_vhost("/") + self.assertEqual(response.get("name"), "/") def test_create_delete_vhost(self): - name = 'vhost2' + name = "vhost2" self.api.create_vhost(name) - self.assertEqual( - len(self.api.list_vhosts()), - 2 - ) + self.assertEqual(len(self.api.list_vhosts()), 2) self.api.delete_vhost(name) - self.assertEqual( - len(self.api.list_vhosts()), - 1 - ) + self.assertEqual(len(self.api.list_vhosts()), 1) def test_create_delete_vhost_tracing(self): - name = 'vhost2' + name = "vhost2" self.api.create_vhost(name, tracing=True) - self.assertEqual( - len(self.api.list_vhosts()), - 2 - ) + self.assertEqual(len(self.api.list_vhosts()), 2) self.api.delete_vhost(name) - self.assertEqual( - len(self.api.list_vhosts()), - 1 - ) + self.assertEqual(len(self.api.list_vhosts()), 1) def test_list_users(self): - self.assertEqual( - len(self.api.list_users()), - 1 - ) + self.assertEqual(len(self.api.list_users()), 1) def test_get_user(self): - response = self.api.get_user('guest') - self.assertEqual(response.get('name'), 'guest') - self.assertEqual(response.get('tags'), 'administrator') + response = self.api.get_user("guest") + self.assertEqual(response.get("name"), "guest") + self.assertEqual(response.get("tags"), ["administrator"]) def test_create_delete_user(self): - name = 'user2' - password_hash = '5f4dcc3b5aa765d61d8327deb882cf99' # md5 of 'password' + name = "user2" + password_hash = "5f4dcc3b5aa765d61d8327deb882cf99" # md5 of 'password' - self.api.create_user(name, password='', password_hash=password_hash) - self.assertEqual( - len(self.api.list_users()), - 2 - ) + self.api.create_user(name, password="", password_hash=password_hash) + self.assertEqual(len(self.api.list_users()), 2) self.api.delete_user(name) - self.assertEqual( - len(self.api.list_users()), - 1 - ) + self.assertEqual(len(self.api.list_users()), 1) def test_create_delete_user_password(self): - name = 'user2' - password = 'password' + name = "user2" + password = "password" self.api.create_user(name, password=password) - self.assertEqual( - len(self.api.list_users()), - 2 - ) + self.assertEqual(len(self.api.list_users()), 2) self.api.delete_user(name) - self.assertEqual( - len(self.api.list_users()), - 1 - ) + self.assertEqual(len(self.api.list_users()), 1) def test_create_delete_user_no_password(self): - name = 'user2' - password = '' + name = "user2" + password = "" self.api.create_user(name, password=password) - self.assertEqual( - len(self.api.list_users()), - 2 - ) + self.assertEqual(len(self.api.list_users()), 2) self.api.delete_user(name) - self.assertEqual( - len(self.api.list_users()), - 1 - ) + self.assertEqual(len(self.api.list_users()), 1) def test_list_user_permissions(self): self.assertEqual( - self.api.list_user_permissions('guest'), - [{'configure': '.*', - 'read': '.*', - 'user': 'guest', - 'vhost': '/', - 'write': '.*'}] + self.api.list_user_permissions("guest"), + [ + { + "configure": ".*", + "read": ".*", + "user": "guest", + "vhost": "/", + "write": ".*", + } + ], ) def test_whoami(self): self.assertEqual( - self.api.whoami(), - {'name': 'guest', 'tags': 'administrator'} + self.api.whoami(), {"name": "guest", "tags": ["administrator"]} ) def test_list_permissions(self): self.assertEqual( self.api.list_permissions(), - [{'configure': '.*', - 'read': '.*', - 'user': 'guest', - 'vhost': '/', - 'write': '.*'}] + [ + { + "configure": ".*", + "read": ".*", + "user": "guest", + "vhost": "/", + "write": ".*", + } + ], ) def test_get_user_permission(self): self.assertEqual( - self.api.get_user_permission('/', 'guest'), + self.api.get_user_permission("/", "guest"), { - 'configure': '.*', - 'read': '.*', - 'user': 'guest', - 'vhost': '/', - 'write': '.*' - } + "configure": ".*", + "read": ".*", + "user": "guest", + "vhost": "/", + "write": ".*", + }, ) def test_create_delete_user_permission(self): - uname = 'test_user' - vname = 'test_vhost' - password_hash = '5f4dcc3b5aa765d61d8327deb882cf99' # md5 of 'password' + uname = "test_user" + vname = "test_vhost" + password_hash = "5f4dcc3b5aa765d61d8327deb882cf99" # md5 of 'password' # Create test user/vhost - self.api.create_user(uname, password='', password_hash=password_hash) + self.api.create_user(uname, password="", password_hash=password_hash) self.api.create_vhost(vname) self.assertEqual(len(self.api.list_user_permissions(uname)), 0) @@ -419,19 +355,61 @@ def test_policies(self): self.api.get_policy_for_vhost("/", "not-a-policy") get_response = self.api.get_policy_for_vhost("/", "ha-all") - self.assertEqual( - get_response, - list_all_response[0] - ) + self.assertEqual(get_response, list_all_response[0]) self.api.delete_policy_for_vhost("/", "ha-all") + self.assertEqual(len(self.api.list_policies()), 0) + + def test_is_vhost_alive(self): + self.assertDictEqual(self.api.is_vhost_alive("/"), {"status": "ok"}) + + def test_list_topic_permissions(self): self.assertEqual( - len(self.api.list_policies()), - 0 + self.api.list_topic_permissions(), [], ) - def test_is_vhost_alive(self): - self.assertDictEqual( - self.api.is_vhost_alive('/'), - {'status': 'ok'} + def test_get_topic_permission(self): + with self.assertRaises(HTTPError): + self.api.list_vhost_user_topic_permissions("/", "guest") + + def test_create_delete_topic_permission(self): + uname = "test_user" + vname = "test_vhost" + password_hash = "5f4dcc3b5aa765d61d8327deb882cf99" # md5 of 'password' + + # Create test user/vhost + self.api.create_user(uname, password="", password_hash=password_hash) + self.api.create_vhost(vname) + + self.assertEqual(len(self.api.list_user_topic_permissions(uname)), 0) + + # Create the permission + self.api.create_topic_permission(uname, vname, "amq.topic") + + self.assertEqual(len(self.api.list_user_topic_permissions(uname)), 1) + self.assertEqual(len(self.api.list_vhost_topic_permissions(vname)), 1) + self.assertEqual(len(self.api.list_topic_permissions()), 1) + + # Get the topic permission created + self.assertEqual( + self.api.list_vhost_user_topic_permissions(vname, uname), + [ + { + "user": uname, + "vhost": vname, + "exchange": "amq.topic", + "write": "", + "read": "", + } + ], ) + + # Delete the permission + self.api.delete_topic_permission(uname, vname, "amq.topic") + + self.assertEqual(len(self.api.list_user_topic_permissions(uname)), 0) + self.assertEqual(len(self.api.list_vhost_topic_permissions(vname)), 0) + self.assertEqual(len(self.api.list_topic_permissions()), 0) + # delete test user/vhost + self.api.delete_user(uname) + self.api.delete_vhost(vname) diff --git a/setup.py b/setup.py index 4b60e88..9f29126 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ # import multiprocessing to avoid this bug (http://bugs.python.org/issue15881#msg170215) import multiprocessing + assert multiprocessing import re from setuptools import setup, find_packages @@ -9,59 +10,59 @@ def get_version(): """ Extracts the version number from the version.py file. """ - VERSION_FILE = 'rabbitmq_admin/version.py' - mo = re.search(r'^__version__ = [\'"]([^\'"]*)[\'"]', open(VERSION_FILE, 'rt').read(), re.M) + VERSION_FILE = "rabbitmq_admin/version.py" + mo = re.search( + r'^__version__ = [\'"]([^\'"]*)[\'"]', open(VERSION_FILE, "rt").read(), re.M + ) if mo: return mo.group(1) else: - raise RuntimeError('Unable to find version string in {0}.'.format(VERSION_FILE)) + raise RuntimeError("Unable to find version string in {0}.".format(VERSION_FILE)) -install_requires = [ - 'requests>=2.7.0', - 'six>=1.8.0' -] + +install_requires = ["requests>=2.7.0", "six>=1.8.0"] tests_require = [ - 'coverage>=4.0', - 'flake8>=2.2.0', - 'pika>=0.10.0', - 'mock>=1.0.1', - 'nose>=1.3.0'] -docs_require = [ - 'Sphinx>=1.2.2', - 'sphinx_rtd_theme'] + "black>=21.9b0", + "coverage>=4.0", + "flake8>=2.2.0", + "pika>=0.10.0", + "mock>=1.0.1", + "nose>=1.3.0", +] +docs_require = ["Sphinx>=1.2.2", "sphinx_rtd_theme"] extras_require = { - 'test': tests_require, - 'packaging': ['wheel'], - 'docs': docs_require, + "test": tests_require, + "packaging": ["wheel"], + "docs": docs_require, } everything = set(install_requires) for deps in extras_require.values(): everything.update(deps) -extras_require['all'] = list(everything) +extras_require["all"] = list(everything) setup( - name='rabbitmq-admin', + name="rabbitmq-admin", version=get_version(), - description='A python interface for the RabbitMQ Admin HTTP API', - long_description=open('README.rst').read(), - url='https://github.com/ambitioninc/rabbitmq-admin', - author='Micah Hausler', - author_email='opensource@ambition.com', - keywords='RabbitMQ, AMQP, admin', + description="A python interface for the RabbitMQ Admin HTTP API", + long_description=open("README.rst").read(), + url="https://github.com/ambitioninc/rabbitmq-admin", + author="Micah Hausler", + author_email="opensource@ambition.com", + keywords="RabbitMQ, AMQP, admin", packages=find_packages(), classifiers=[ - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", ], - license='MIT', + license="MIT", include_package_data=True, - test_suite='nose.collector', + test_suite="nose.collector", install_requires=install_requires, tests_require=tests_require, extras_require=extras_require,