Repository: auth0/auth0-python
Pull request #303 (2022-02-16 14:50:21)

[SDK-3119] Add attack protection endpoints
### Changes

Add the new attack protection endpoints to the management client:

```
GET /api/v2/attack-protection/suspicious-ip-throttling
PATCH /api/v2/attack-protection/suspicious-ip-throttling
GET /api/v2/attack-protection/breached-password-detection
PATCH /api/v2/attack-protection/breached-password-detection
GET /api/v2/attack-protection/brute-force-protection
PATCH /api/v2/attack-protection/brute-force-protection
```

### Testing

Please describe how this can be tested by reviewers. Be specific about anything not tested and reasons why. If this library has unit and/or integration testing, tests should be added for new functionality and existing tests should complete without errors. 

- [ ] This change adds unit test coverage
- [ ] This change adds integration test coverage
- [ ] This change has been tested on the latest version of the platform/language or why not

### Checklist

- [ ] I have read the [Auth0 general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md)
- [ ] I have read the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md)
- [ ] All existing and new tests complete without errors



Patch:
diff --git a/README.rst b/README.rst
index 4c8fb8e..1989f15 100644
--- a/README.rst
+++ b/README.rst
@@ -19,7 +19,7 @@ You can install the auth0 Python SDK using the following command.
 For python3, use the following command
 
 .. code-block:: python
-    
+
     pip3 install auth0-python
 
 Python 3.2 and 3.3 have reached `EOL <https://en.wikipedia.org/wiki/CPython#Version_history>`__ and support will be removed in the near future.
@@ -86,22 +86,22 @@ For symmetric algorithms like HS256, use the ``SymmetricSignatureVerifier`` clas
 The following example demonstrates the verification of an ID token signed with the RS256 signing algorithm:
 
 .. code-block:: python
-    
+
     from auth0.v3.authentication.token_verifier import TokenVerifier, AsymmetricSignatureVerifier
-    
+
     domain = 'myaccount.auth0.com'
     client_id = 'exampleid'
-    
+
     # After authenticating
     id_token = auth_result['id_token']
-    
+
     jwks_url = 'https://{}/.well-known/jwks.json'.format(domain)
     issuer = 'https://{}/'.format(domain)
-    
+
     sv = AsymmetricSignatureVerifier(jwks_url)  # Reusable instance
     tv = TokenVerifier(signature_verifier=sv, issuer=issuer, audience=client_id)
     tv.verify(id_token)
-    
+
 If the token verification fails, a ``TokenValidationError`` will be raised. In that scenario, the ID token should be deemed invalid and its contents should not be trusted.
 
 
@@ -128,7 +128,7 @@ Log in to an organization by specifying the ``organization`` property when calli
 .. code-block:: python
 
     from auth0.v3.authentication.authorize_client import AuthorizeClient
-    
+
     client = AuthorizeClient('my.domain.com')
 
     client.authorize(client_id='client_id',
@@ -138,18 +138,18 @@ Log in to an organization by specifying the ``organization`` property when calli
 When logging into an organization, it is important to ensure the ``org_id`` claim of the ID Token matches the expected organization value. The ``TokenVerifier`` can be be used to ensure the ID Token contains the expected ``org_id`` claim value:
 
 .. code-block:: python
-    
+
     from auth0.v3.authentication.token_verifier import TokenVerifier, AsymmetricSignatureVerifier
-    
+
     domain = 'myaccount.auth0.com'
     client_id = 'exampleid'
-    
+
     # After authenticating
     id_token = auth_result['id_token']
-    
+
     jwks_url = 'https://{}/.well-known/jwks.json'.format(domain)
     issuer = 'https://{}/'.format(domain)
-    
+
     sv = AsymmetricSignatureVerifier(jwks_url)  # Reusable instance
     tv = TokenVerifier(signature_verifier=sv, issuer=issuer, audience=client_id)
 
@@ -210,9 +210,9 @@ The snippet below attempts to illustrate how this verification could look like u
     organization = # expected organization ID
     if data['org_id'] != organization:
         raise Exception('Organization (org_id) claim mismatch')
-    
+
     # if this line is reached, validation is successful
-  
+
 
 **************
 Management SDK
@@ -341,6 +341,7 @@ Management Endpoints
 ********************
 
 - Actions() (``Auth0().actions``)
+- AttackProtection() (``Auth0().attack_protection``)
 - Blacklists() ( ``Auth0().blacklists`` )
 - ClientGrants() ( ``Auth0().client_grants`` )
 - Clients() ( ``Auth0().clients`` )
diff --git a/auth0/v3/management/__init__.py b/auth0/v3/management/__init__.py
index e805744..65d2d87 100644
--- a/auth0/v3/management/__init__.py
+++ b/auth0/v3/management/__init__.py
@@ -1,5 +1,6 @@
 from .auth0 import Auth0
 from .actions import Actions
+from .attack_protection import AttackProtection
 from .blacklists import Blacklists
 from .client_grants import ClientGrants
 from .clients import Clients
diff --git a/auth0/v3/management/attack_protection.py b/auth0/v3/management/attack_protection.py
new file mode 100644
index 0000000..6a0d82c
--- /dev/null
+++ b/auth0/v3/management/attack_protection.py
@@ -0,0 +1,104 @@
+from .rest import RestClient
+
+
+class AttackProtection(object):
+    """Auth0 attack protection 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)
+
+        timeout (float or tuple, optional): Change the requests
+            connect and read timeout. Pass a tuple to specify
+            both values separately or a float to set both to it.
+            (defaults to 5.0 for both)
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
+    """
+
+    def __init__(self, domain, token, telemetry=True, timeout=5.0, protocol="https", rest_options=None):
+        self.domain = domain
+        self.protocol = protocol
+        self.client = RestClient(jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options)
+
+    def _url(self, component):
+        return '{}://{}/api/v2/attack-protection/{}'.format(self.protocol, self.domain, component)
+
+    def get_breached_password_detection(self):
+        """Get breached password detection settings.
+
+        Returns the breached password detection settings.
+
+        See: https://auth0.com/docs/api/management/v2#!/Attack_Protection/get_breached_password_detection
+        """
+        url = self._url('breached-password-detection')
+        return self.client.get(url)
+
+    def update_breached_password_detection(self, body):
+        """Update breached password detection settings.
+
+        Returns the breached password detection settings.
+
+        Args:
+
+           body (dict): breached password detection settings.
+
+        See: https://auth0.com/docs/api/management/v2#!/Attack_Protection/patch_breached_password_detection
+        """
+        url = self._url('breached-password-detection')
+        return self.client.patch(url, data=body)
+
+    def get_brute_force_protection(self):
+        """Get the brute force configuration.
+
+        Returns the brute force configuration.
+
+        See: https://auth0.com/docs/api/management/v2#!/Attack_Protection/get_brute_force_protection
+        """
+        url = self._url('brute-force-protection')
+        return self.client.get(url)
+
+    def update_brute_force_protection(self, body):
+        """Update the brute force configuration.
+
+        Returns the brute force configuration.
+
+        Args:
+
+           body (dict): updates of the brute force configuration.
+
+        See: https://auth0.com/docs/api/management/v2#!/Attack_Protection/patch_brute_force_protection
+        """
+        url = self._url('brute-force-protection')
+        return self.client.patch(url, data=body)
+
+    def get_suspicious_ip_throttling(self):
+        """Get the suspicious IP throttling configuration.
+
+        Returns the suspicious IP throttling configuration.
+
+        See: https://auth0.com/docs/api/management/v2#!/Attack_Protection/get_suspicious_ip_throttling
+        """
+        url = self._url('suspicious-ip-throttling')
+        return self.client.get(url)
+
+    def update_suspicious_ip_throttling(self, body):
+        """Update the suspicious IP throttling configuration.
+
+        Returns the suspicious IP throttling configuration.
+
+        Args:
+
+           body (dict): updates of the suspicious IP throttling configuration.
+
+        See: https://auth0.com/docs/api/management/v2#!/Attack_Protection/patch_suspicious_ip_throttling
+        """
+        url = self._url('suspicious-ip-throttling')
+        return self.client.patch(url, data=body)
diff --git a/auth0/v3/management/auth0.py b/auth0/v3/management/auth0.py
index 58cb9a5..55c318e 100644
--- a/auth0/v3/management/auth0.py
+++ b/auth0/v3/management/auth0.py
@@ -1,4 +1,5 @@
 from .actions import Actions
+from .attack_protection import AttackProtection
 from .blacklists import Blacklists
 from .client_grants import ClientGrants
 from .clients import Clients
@@ -43,6 +44,7 @@ class Auth0(object):
 
     def __init__(self, domain, token, rest_options=None):
         self.actions = Actions(domain=domain, token=token, rest_options=rest_options)
+        self.attack_protection = AttackProtection(domain=domain, token=token, rest_options=rest_options)
         self.blacklists = Blacklists(domain=domain, token=token, rest_options=rest_options)
         self.client_grants = ClientGrants(domain=domain, token=token, rest_options=rest_options)
         self.clients = Clients(domain=domain, token=token, rest_options=rest_options)


Test patch:
diff --git a/auth0/v3/test/management/test_atack_protection.py b/auth0/v3/test/management/test_atack_protection.py
new file mode 100644
index 0000000..a299955
--- /dev/null
+++ b/auth0/v3/test/management/test_atack_protection.py
@@ -0,0 +1,87 @@
+import unittest
+import mock
+from ...management.attack_protection import AttackProtection
+
+
+class TestAttackProtection(unittest.TestCase):
+
+    def test_init_with_optionals(self):
+        t = AttackProtection(domain='domain', token='jwttoken', telemetry=False, timeout=(10, 2))
+        self.assertEqual(t.client.options.timeout, (10, 2))
+        telemetry_header = t.client.base_headers.get('Auth0-Client', None)
+        self.assertEqual(telemetry_header, None)
+
+    @mock.patch('auth0.v3.management.attack_protection.RestClient')
+    def test_get_breached_password_detection(self, mock_rc):
+        mock_instance = mock_rc.return_value
+        mock_instance.get.return_value = {}
+
+        ap = AttackProtection(domain='domain', token='jwttoken')
+        ap.get_breached_password_detection()
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/attack-protection/breached-password-detection', args[0])
+
+    @mock.patch('auth0.v3.management.attack_protection.RestClient')
+    def test_update_breached_password_detection(self, mock_rc):
+        mock_instance = mock_rc.return_value
+        mock_instance.patch.return_value = {}
+
+        c = AttackProtection(domain='domain', token='jwttoken')
+        c.update_breached_password_detection({'a': 'b', 'c': 'd'})
+
+        mock_instance.patch.assert_called_with(
+            'https://domain/api/v2/attack-protection/breached-password-detection',
+            data={'a': 'b', 'c': 'd'}
+        )
+
+    @mock.patch('auth0.v3.management.attack_protection.RestClient')
+    def test_get_brute_force_protection(self, mock_rc):
+        mock_instance = mock_rc.return_value
+        mock_instance.get.return_value = {}
+
+        ap = AttackProtection(domain='domain', token='jwttoken')
+        ap.get_brute_force_protection()
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/attack-protection/brute-force-protection', args[0])
+
+    @mock.patch('auth0.v3.management.attack_protection.RestClient')
+    def test_update_brute_force_protection(self, mock_rc):
+        mock_instance = mock_rc.return_value
+        mock_instance.patch.return_value = {}
+
+        c = AttackProtection(domain='domain', token='jwttoken')
+        c.update_brute_force_protection({'a': 'b', 'c': 'd'})
+
+        mock_instance.patch.assert_called_with(
+            'https://domain/api/v2/attack-protection/brute-force-protection',
+            data={'a': 'b', 'c': 'd'}
+        )
+
+    @mock.patch('auth0.v3.management.attack_protection.RestClient')
+    def test_get_suspicious_ip_throttling(self, mock_rc):
+        mock_instance = mock_rc.return_value
+        mock_instance.get.return_value = {}
+
+        ap = AttackProtection(domain='domain', token='jwttoken')
+        ap.get_suspicious_ip_throttling()
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/attack-protection/suspicious-ip-throttling', args[0])
+
+    @mock.patch('auth0.v3.management.attack_protection.RestClient')
+    def test_update_suspicious_ip_throttling(self, mock_rc):
+        mock_instance = mock_rc.return_value
+        mock_instance.patch.return_value = {}
+
+        c = AttackProtection(domain='domain', token='jwttoken')
+        c.update_suspicious_ip_throttling({'a': 'b', 'c': 'd'})
+
+        mock_instance.patch.assert_called_with(
+            'https://domain/api/v2/attack-protection/suspicious-ip-throttling',
+            data={'a': 'b', 'c': 'd'}
+        )
diff --git a/auth0/v3/test/management/test_auth0.py b/auth0/v3/test/management/test_auth0.py
index 78fbc73..0822374 100644
--- a/auth0/v3/test/management/test_auth0.py
+++ b/auth0/v3/test/management/test_auth0.py
@@ -2,6 +2,7 @@ import unittest
 
 from ...management.auth0 import Auth0
 from ...management.actions import Actions
+from ...management.attack_protection import AttackProtection
 from ...management.blacklists import Blacklists
 from ...management.client_grants import ClientGrants
 from ...management.clients import Clients
@@ -36,9 +37,12 @@ class TestAuth0(unittest.TestCase):
         self.token = "a-token"
         self.a0 = Auth0(self.domain, self.token)
 
-    def test_blacklists(self):
+    def test_actions(self):
         self.assertIsInstance(self.a0.actions, Actions)
 
+    def test_attack_protection(self):
+        self.assertIsInstance(self.a0.attack_protection, AttackProtection)
+
     def test_blacklists(self):
         self.assertIsInstance(self.a0.blacklists, Blacklists)