Repository: auth0/auth0-python
Pull request #202 (2019-06-27 22:45:41)

Add Roles and Permissions endpoints
### Changes

Adds all missing endpoints for **Roles** and User/Role **Permissions**.

### Testing

- [x] This change adds unit test coverage
- [x] This change adds integration test coverage
- [x] This change has been tested on the latest version of the platform/language or why not

### Checklist

- [x] I have read the [Auth0 general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md)
- [x] I have read the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md)
- [x] All existing and new tests complete without errors


lbalmaceda: @damieng will need a force merge

Patch:
diff --git a/auth0/v3/management/auth0.py b/auth0/v3/management/auth0.py
index 6d43bc5..f51ef69 100644
--- a/auth0/v3/management/auth0.py
+++ b/auth0/v3/management/auth0.py
@@ -11,6 +11,7 @@ from .guardian import Guardian
 from .jobs import Jobs
 from .logs import Logs
 from .resource_servers import ResourceServers
+from .roles import Roles
 from .rules import Rules
 from .rules_configs import RulesConfigs
 from .stats import Stats
@@ -44,6 +45,7 @@ class Auth0(object):
         self.jobs = Jobs(domain, token)
         self.logs = Logs(domain, token)
         self.resource_servers = ResourceServers(domain, token)
+        self.roles = Roles(domain, token)
         self.rules = Rules(domain, token)
         self.rules_configs = RulesConfigs(domain, token)
         self.stats = Stats(domain, token)
diff --git a/auth0/v3/management/rest.py b/auth0/v3/management/rest.py
index 44d9342..013a1d6 100644
--- a/auth0/v3/management/rest.py
+++ b/auth0/v3/management/rest.py
@@ -68,10 +68,10 @@ class RestClient(object):
         response = requests.put(url, json=data, headers=headers)
         return self._process_response(response)
 
-    def delete(self, url, params=None):
+    def delete(self, url, params=None, data=None):
         headers = self.base_headers.copy()
 
-        response = requests.delete(url, headers=headers, params=params or {})
+        response = requests.delete(url, headers=headers, params=params or {}, json=data)
         return self._process_response(response)
 
     def _process_response(self, response):
diff --git a/auth0/v3/management/roles.py b/auth0/v3/management/roles.py
new file mode 100644
index 0000000..0503d8e
--- /dev/null
+++ b/auth0/v3/management/roles.py
@@ -0,0 +1,178 @@
+from .rest import RestClient
+
+
+class Roles(object):
+
+    """Auth0 roles endpoints
+
+    Args:
+        domain (str): Your Auth0 domain, e.g: 'username.auth0.com'
+
+        token (str): Management API v2 Token
+
+        telemetry (bool, optional): Enable or disable Telemetry
+            (defaults to True)
+    """
+
+    def __init__(self, domain, token, telemetry=True):
+        self.domain = domain
+        self.client = RestClient(jwt=token, telemetry=telemetry)
+
+    def _url(self, id=None):
+        url = 'https://{}/api/v2/roles'.format(self.domain)
+        if id is not None:
+            return '{}/{}'.format(url, id)
+        return url
+
+    def list(self, page=0, per_page=25, include_totals=True, name_filter=None):
+        """List or search roles.
+
+        Args:
+            page (int, optional): The result's page number (zero based).
+
+            per_page (int, optional): The amount of entries per page.
+
+            include_totals (bool, optional): True if the query summary is
+                to be included in the result, False otherwise.
+
+            name_filter (str, optional): A case-insensitive filter to apply
+                to search for roles by name            
+
+        See: https://auth0.com/docs/api/management/v2#!/Roles/get_roles
+        """
+        params = {
+            'per_page': per_page,
+            'page': page,
+            'include_totals': str(include_totals).lower(),
+            'name_filter': name_filter
+        }
+        return self.client.get(self._url(), params=params)
+
+    def create(self, body):
+        """Creates a new role.
+
+        Args:
+            body (dict): Please see: https://auth0.com/docs/api/v2#!/Roles/post_roles
+        """
+        return self.client.post(self._url(), data=body)
+
+    def get(self, id):
+        """Get a role.
+
+        Args:
+            id (str): The id of the role to retrieve.
+
+        See: https://auth0.com/docs/api/management/v2#!/Roles/get_roles_by_id
+        """
+
+        return self.client.get(self._url(id))
+
+    def delete(self, id):
+        """Delete a role.
+
+        Args:
+            id (str): The id of the role to delete.
+
+        See: https://auth0.com/docs/api/management/v2#!/Roles/delete_roles_by_id
+        """
+        return self.client.delete(self._url(id))
+
+    def update(self, id, body):
+        """Update a role with the attributes passed in 'body'
+
+        Args:
+            id (str): The id of the role to update.
+
+            body (dict): Please see: https://auth0.com/docs/api/management/v2#!/Roles/patch_roles_by_id
+        """
+        return self.client.patch(self._url(id), data=body)
+
+    def list_users(self, id, page=0, per_page=25, include_totals=True):
+        """List the users that have been associated with a given role.
+
+        Args:
+            id (str): The role's id.
+
+            page (int, optional): The result's page number (zero based). 
+  
+            per_page (int, optional): The amount of entries per page. 
+
+            include_totals (bool, optional): True if the query summary is
+                to be included in the result, False otherwise.
+
+        See https://auth0.com/docs/api/management/v2#!/Roles/get_role_user
+        """
+
+        params = {
+            'per_page': per_page,
+            'page': page,
+            'include_totals': str(include_totals).lower()
+        }
+        url = self._url('{}/users'.format(id))
+        return self.client.get(url, params=params)
+
+    def add_users(self, id, users):
+        """Assign users to a role.
+
+        Args:
+            id (str): The role's id.
+
+            users (list of str): A list of users ids to add to this role
+
+        See https://auth0.com/docs/api/management/v2#!/Roles/post_role_users
+        """
+        url = self._url('{}/users'.format(id))
+        body = {'users': users}
+        return self.client.post(url, data=body)
+
+    def list_permissions(self, id, page=0, per_page=25, include_totals=True):
+        """List the permissions associated to a role.
+
+        Args:
+            id (str): The role's id.
+
+            page (int, optional): The result's page number (zero based). 
+  
+            per_page (int, optional): The amount of entries per page. 
+
+            include_totals (bool, optional): True if the query summary is
+                to be included in the result, False otherwise.
+
+        See https://auth0.com/docs/api/management/v2#!/Roles/get_role_permission
+        """
+        params = {
+            'per_page': per_page,
+            'page': page,
+            'include_totals': str(include_totals).lower()
+        }
+        url = self._url('{}/permissions'.format(id))
+        return self.client.get(url, params=params)
+
+    def remove_permissions(self, id, permissions):
+        """Unassociates permissions from a role.
+
+        Args:
+            id (str): The role's id.
+
+            permissions (list of str): A list of permission ids to unassociate from the role.
+
+        See https://auth0.com/docs/api/management/v2#!/Roles/delete_role_permission_assignment
+        """
+        url = self._url('{}/permissions'.format(id))
+        body = {'permissions': permissions}
+        return self.client.delete(url, data=body)
+
+
+    def add_permissions(self, id, permissions):
+        """Associates permissions with a role.
+
+        Args:
+            id (str): The role's id.
+
+            permissions (list of str): A list of permission ids to associate to the role.
+
+        See https://auth0.com/docs/api/management/v2#!/Roles/post_role_permission_assignment
+        """
+        url = self._url('{}/permissions'.format(id))
+        body = {'permissions': permissions}
+        return self.client.post(url, data=body)
\ No newline at end of file
diff --git a/auth0/v3/management/users.py b/auth0/v3/management/users.py
index 71fb4a9..79514e0 100644
--- a/auth0/v3/management/users.py
+++ b/auth0/v3/management/users.py
@@ -47,6 +47,9 @@ class Users(object):
                 when querying for users. Will default to the latest version available.
                 See: https://auth0.com/docs/users/search
 
+            include_totals (bool, optional): True if the query summary is
+                to be included in the result, False otherwise.
+
             fields (list of str, optional): A list of fields to include or
                 exclude from the result (depending on include_fields). Empty to
                 retrieve all fields.
@@ -126,19 +129,32 @@ class Users(object):
         """
         return self.client.patch(self._url(id), data=body)
 
-    def get_roles(self, id):
-        """Get a user's roles.
+    def list_roles(self, id, page=0, per_page=25, include_totals=True):
+        """List the roles associated with a user.
 
         Args:
             id (str): The user's id.
 
+            page (int, optional): The result's page number (zero based). 
+  
+            per_page (int, optional): The amount of entries per page. 
+
+            include_totals (bool, optional): True if the query summary is
+                to be included in the result, False otherwise.
+
         See https://auth0.com/docs/api/management/v2#!/Users/get_user_roles
         """
+        params = {
+            'per_page': per_page,
+            'page': page,
+            'include_totals': str(include_totals).lower()
+        }
+
         url = self._url('{}/roles'.format(id))
-        return self.client.get(url)
+        return self.client.get(url, params=params)
 
-    def delete_roles(self, id, roles):
-        """Removes roles from a user.
+    def remove_roles(self, id, roles):
+        """Removes an array of roles from a user.
 
         Args:
             id (str): The user's id.
@@ -151,8 +167,8 @@ class Users(object):
         body = {'roles': roles}
         return self.client.delete(url, data=body)
 
-    def assign_roles(self, id, roles):
-        """Assign roles to a user
+    def add_roles(self, id, roles):
+        """Associate an array of roles with a user.
 
         Args:
             id (str): The user's id.
@@ -165,6 +181,58 @@ class Users(object):
         body = {'roles': roles}
         return self.client.post(url, data=body)
 
+    def list_permissions(self, id, page=0, per_page=25, include_totals=True):
+        """List the permissions associated to the user.
+
+        Args:
+            id (str): The user's id.
+
+            page (int, optional): The result's page number (zero based). 
+  
+            per_page (int, optional): The amount of entries per page. 
+
+            include_totals (bool, optional): True if the query summary is
+                to be included in the result, False otherwise.
+
+        See https://auth0.com/docs/api/management/v2#!/Users/get_permissions
+        """
+
+        params = {
+            'per_page': per_page,
+            'page': page,
+            'include_totals': str(include_totals).lower()
+        }
+        url = self._url('{}/permissions'.format(id))
+        return self.client.get(url, params=params)
+
+    def remove_permissions(self, id, permissions):
+        """Removes permissions from a user.
+
+        Args:
+            id (str): The user's id.
+
+            permissions (list of str): A list of permission ids to unassociate from the user.
+
+        See https://auth0.com/docs/api/management/v2#!/Users/delete_permissions
+        """
+        url = self._url('{}/permissions'.format(id))
+        body = {'permissions': permissions}
+        return self.client.delete(url, data=body)
+
+    def add_permissions(self, id, permissions):
+        """Assign permissions to a user.
+
+        Args:
+            id (str): The user's id.
+
+            permissions (list of str): A list of permission ids to associated with the user.
+
+        See https://auth0.com/docs/api/management/v2#!/Users/post_permissions
+        """
+        url = self._url('{}/permissions'.format(id))
+        body = {'permissions': permissions}
+        return self.client.post(url, data=body)
+
     def delete_multifactor(self, id, provider):
         """Delete a user's multifactor provider.
 


Test patch:
diff --git a/auth0/v3/test/management/test_auth0.py b/auth0/v3/test/management/test_auth0.py
index 2026ebe..51e2f8d 100644
--- a/auth0/v3/test/management/test_auth0.py
+++ b/auth0/v3/test/management/test_auth0.py
@@ -13,6 +13,7 @@ from ...management.guardian import Guardian
 from ...management.jobs import Jobs
 from ...management.logs import Logs
 from ...management.resource_servers import ResourceServers
+from ...management.roles import Roles
 from ...management.rules import Rules
 from ...management.rules_configs import RulesConfigs
 from ...management.stats import Stats
@@ -69,6 +70,9 @@ class TestAuth0(unittest.TestCase):
     def test_resource_servers(self):
         self.assertIsInstance(self.a0.resource_servers, ResourceServers)
 
+    def test_roles(self):
+        self.assertIsInstance(self.a0.roles, Roles)
+
     def test_rules(self):
         self.assertIsInstance(self.a0.rules, Rules)
 
diff --git a/auth0/v3/test/management/test_rest.py b/auth0/v3/test/management/test_rest.py
index e3b8619..5a77317 100644
--- a/auth0/v3/test/management/test_rest.py
+++ b/auth0/v3/test/management/test_rest.py
@@ -294,7 +294,26 @@ class TestRest(unittest.TestCase):
         mock_delete.return_value.status_code = 200
 
         response = rc.delete(url='the-url/ID')
-        mock_delete.assert_called_with('the-url/ID', headers=headers, params={})
+        mock_delete.assert_called_with('the-url/ID', headers=headers, params={}, json=None)
+
+        self.assertEqual(response, ['a', 'b'])
+
+    @mock.patch('requests.delete')
+    def test_delete_with_body_and_params(self, mock_delete):
+        rc = RestClient(jwt='a-token', telemetry=False)
+        headers = {
+            'Authorization': 'Bearer a-token',
+            'Content-Type': 'application/json',
+        }
+
+        mock_delete.return_value.text = '["a", "b"]'
+        mock_delete.return_value.status_code = 200
+
+        data = {'some': 'data'}
+        params={'A': 'param', 'B': 'param'}
+
+        response = rc.delete(url='the-url/ID', params=params, data=data)
+        mock_delete.assert_called_with('the-url/ID', headers=headers, params=params, json=data)
 
         self.assertEqual(response, ['a', 'b'])
 
diff --git a/auth0/v3/test/management/test_roles.py b/auth0/v3/test/management/test_roles.py
new file mode 100644
index 0000000..76ccec1
--- /dev/null
+++ b/auth0/v3/test/management/test_roles.py
@@ -0,0 +1,173 @@
+import unittest
+import mock
+from ...management.roles import Roles
+
+
+class TestRoles(unittest.TestCase):
+
+    @mock.patch('auth0.v3.management.roles.RestClient')
+    def test_list(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        u = Roles(domain='domain', token='jwttoken')
+        u.list()
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/roles', args[0])
+        self.assertEqual(kwargs['params'], {
+            'per_page': 25,
+            'page': 0,
+            'include_totals': 'true',
+            'name_filter': None
+        })
+
+        u.list(page=1, per_page=50, include_totals=False, name_filter='little-role')
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/roles', args[0])
+        self.assertEqual(kwargs['params'], {
+            'per_page': 50,
+            'page': 1,
+            'include_totals': 'false',
+            'name_filter': 'little-role'
+        })
+
+    @mock.patch('auth0.v3.management.roles.RestClient')
+    def test_create(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        u = Roles(domain='domain', token='jwttoken')
+        u.create({'a': 'b', 'c': 'd'})
+
+        args, kwargs = mock_instance.post.call_args
+
+        self.assertEqual('https://domain/api/v2/roles', args[0])
+        self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'})
+
+    @mock.patch('auth0.v3.management.roles.RestClient')
+    def test_get(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        u = Roles(domain='domain', token='jwttoken')
+        u.get('an-id')
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/roles/an-id', args[0])
+
+    @mock.patch('auth0.v3.management.roles.RestClient')
+    def test_delete(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        u = Roles(domain='domain', token='jwttoken')
+        u.delete('an-id')
+
+        mock_instance.delete.assert_called_with(
+            'https://domain/api/v2/roles/an-id'
+        )
+
+    @mock.patch('auth0.v3.management.roles.RestClient')
+    def test_update(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        u = Roles(domain='domain', token='jwttoken')
+        u.update('an-id', {'a': 'b', 'c': 'd'})
+
+        args, kwargs = mock_instance.patch.call_args
+
+        self.assertEqual('https://domain/api/v2/roles/an-id', args[0])
+        self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'})
+
+    @mock.patch('auth0.v3.management.roles.RestClient')
+    def test_list_users(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        u = Roles(domain='domain', token='jwttoken')
+        u.list_users('an-id')
+
+        args, kwargs = mock_instance.get.call_args
+        self.assertEqual('https://domain/api/v2/roles/an-id/users', args[0])
+        self.assertEqual(kwargs['params'], {
+            'per_page': 25,
+            'page': 0,
+            'include_totals': 'true'
+        })
+
+        u.list_users(id='an-id', page=1, per_page=50, include_totals=False)
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/roles/an-id/users', args[0])
+        self.assertEqual(kwargs['params'], {
+            'per_page': 50,
+            'page': 1,
+            'include_totals': 'false'
+        })
+
+
+    @mock.patch('auth0.v3.management.roles.RestClient')
+    def test_add_users(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        u = Roles(domain='domain', token='jwttoken')
+        u.add_users('an-id', ['a', 'b'])
+
+        args, kwargs = mock_instance.post.call_args
+
+        self.assertEqual('https://domain/api/v2/roles/an-id/users',
+                         args[0])
+        self.assertEqual(kwargs['data'], {'users': ['a', 'b']})
+
+    @mock.patch('auth0.v3.management.roles.RestClient')
+    def test_list_permissions(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        u = Roles(domain='domain', token='jwttoken')
+        u.list_permissions('an-id')
+
+        args, kwargs = mock_instance.get.call_args
+        self.assertEqual('https://domain/api/v2/roles/an-id/permissions', args[0])
+        self.assertEqual(kwargs['params'], {
+            'per_page': 25,
+            'page': 0,
+            'include_totals': 'true'
+        })
+
+        u.list_permissions(id='an-id', page=1, per_page=50, include_totals=False)
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/roles/an-id/permissions', args[0])
+        self.assertEqual(kwargs['params'], {
+            'per_page': 50,
+            'page': 1,
+            'include_totals': 'false'
+        })
+
+    @mock.patch('auth0.v3.management.roles.RestClient')
+    def test_remove_permissions(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        u = Roles(domain='domain', token='jwttoken')
+        u.remove_permissions('an-id', ['a', 'b'])
+
+        args, kwargs = mock_instance.delete.call_args
+
+        self.assertEqual('https://domain/api/v2/roles/an-id/permissions',
+                         args[0])
+        self.assertEqual(kwargs['data'], {'permissions': ['a', 'b']})
+
+    @mock.patch('auth0.v3.management.roles.RestClient')
+    def test_add_permissions(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        u = Roles(domain='domain', token='jwttoken')
+        u.add_permissions('an-id', ['a', 'b'])
+
+        args, kwargs = mock_instance.post.call_args
+
+        self.assertEqual('https://domain/api/v2/roles/an-id/permissions',
+                         args[0])
+        self.assertEqual(kwargs['data'], {'permissions': ['a', 'b']})
diff --git a/auth0/v3/test/management/test_users.py b/auth0/v3/test/management/test_users.py
index 8e33efb..b3908c1 100644
--- a/auth0/v3/test/management/test_users.py
+++ b/auth0/v3/test/management/test_users.py
@@ -114,22 +114,37 @@ class TestUsers(unittest.TestCase):
         self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'})
 
     @mock.patch('auth0.v3.management.users.RestClient')
-    def test_get_roles(self, mock_rc):
+    def test_list_roles(self, mock_rc):
         mock_instance = mock_rc.return_value
 
         u = Users(domain='domain', token='jwttoken')
-        u.get_roles('an-id')
+        u.list_roles('an-id')
 
-        mock_instance.get.assert_called_with(
-            'https://domain/api/v2/users/an-id/roles'
-        )
+        args, kwargs = mock_instance.get.call_args
+        self.assertEqual('https://domain/api/v2/users/an-id/roles', args[0])
+        self.assertEqual(kwargs['params'], {
+            'per_page': 25,
+            'page': 0,
+            'include_totals': 'true'
+        })
+
+        u.list_roles(id='an-id', page=1, per_page=50, include_totals=False)
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/users/an-id/roles', args[0])
+        self.assertEqual(kwargs['params'], {
+            'per_page': 50,
+            'page': 1,
+            'include_totals': 'false'
+        })
 
     @mock.patch('auth0.v3.management.users.RestClient')
-    def test_delete_roles(self, mock_rc):
+    def test_remove_roles(self, mock_rc):
         mock_instance = mock_rc.return_value
 
         u = Users(domain='domain', token='jwttoken')
-        u.delete_roles('an-id', ['a', 'b'])
+        u.remove_roles('an-id', ['a', 'b'])
 
         args, kwargs = mock_instance.delete.call_args
 
@@ -138,11 +153,11 @@ class TestUsers(unittest.TestCase):
         self.assertEqual(kwargs['data'], {'roles': ['a', 'b']})
 
     @mock.patch('auth0.v3.management.users.RestClient')
-    def test_assign_roles(self, mock_rc):
+    def test_add_roles(self, mock_rc):
         mock_instance = mock_rc.return_value
 
         u = Users(domain='domain', token='jwttoken')
-        u.assign_roles('an-id', ['a', 'b'])
+        u.add_roles('an-id', ['a', 'b'])
 
         args, kwargs = mock_instance.post.call_args
 
@@ -150,6 +165,58 @@ class TestUsers(unittest.TestCase):
                          args[0])
         self.assertEqual(kwargs['data'], {'roles': ['a', 'b']})
 
+    @mock.patch('auth0.v3.management.users.RestClient')
+    def test_list_permissions(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        u = Users(domain='domain', token='jwttoken')
+        u.list_permissions('an-id')
+
+        args, kwargs = mock_instance.get.call_args
+        self.assertEqual('https://domain/api/v2/users/an-id/permissions', args[0])
+        self.assertEqual(kwargs['params'], {
+            'per_page': 25,
+            'page': 0,
+            'include_totals': 'true'
+        })
+
+        u.list_permissions(id='an-id', page=1, per_page=50, include_totals=False)
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/users/an-id/permissions', args[0])
+        self.assertEqual(kwargs['params'], {
+            'per_page': 50,
+            'page': 1,
+            'include_totals': 'false'
+        })
+
+    @mock.patch('auth0.v3.management.users.RestClient')
+    def test_remove_permissions(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        u = Users(domain='domain', token='jwttoken')
+        u.remove_permissions('an-id', ['a', 'b'])
+
+        args, kwargs = mock_instance.delete.call_args
+
+        self.assertEqual('https://domain/api/v2/users/an-id/permissions',
+                         args[0])
+        self.assertEqual(kwargs['data'], {'permissions': ['a', 'b']})
+
+    @mock.patch('auth0.v3.management.users.RestClient')
+    def test_add_permissions(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        u = Users(domain='domain', token='jwttoken')
+        u.add_permissions('an-id', ['a', 'b'])
+
+        args, kwargs = mock_instance.post.call_args
+
+        self.assertEqual('https://domain/api/v2/users/an-id/permissions',
+                         args[0])
+        self.assertEqual(kwargs['data'], {'permissions': ['a', 'b']})
+
     @mock.patch('auth0.v3.management.users.RestClient')
     def test_delete_multifactor(self, mock_rc):
         mock_instance = mock_rc.return_value