Repository: uclapi/uclapi

Issue:
Add verbosity to oauth callback URL error message
We should tell users whats wrong with their url, for example not starting with https:// etc...


Discussion:

Resolving patch:
diff --git a/backend/uclapi/dashboard/api_applications.py b/backend/uclapi/dashboard/api_applications.py
--- a/backend/uclapi/dashboard/api_applications.py
+++ b/backend/uclapi/dashboard/api_applications.py
@@ -4,7 +4,8 @@
 from oauth.scoping import Scopes
 from common.helpers import PrettyJsonResponse
 
-from .app_helpers import is_url_safe
+from .app_helpers import (is_url_unsafe, NOT_HTTPS,
+                          NOT_VALID, URL_BLACKLISTED, NOT_PUBLIC)
 from .models import App, User
 
 
@@ -251,12 +252,20 @@ def set_callback_url(request):
         })
         response.status_code = 400
         return response
-
-    if not is_url_safe(new_callback_url):
+    url_not_safe_saved = is_url_unsafe(new_callback_url)
+    if url_not_safe_saved:
+        if url_not_safe_saved == NOT_HTTPS:
+            message = "The requested callback URL does not "\
+                      "start with 'https://'."
+        elif url_not_safe_saved == NOT_VALID:
+            message = "The requested callback URL is not valid."
+        elif url_not_safe_saved == URL_BLACKLISTED:
+            message = "The requested callback URL is forbidden."
+        elif url_not_safe_saved == NOT_PUBLIC:
+            message = "The requested callback URL is not publicly available."
         response = PrettyJsonResponse({
             "success": False,
-            "message": ("The requested callback URL"
-                        " is not valid.")
+            "message": message
         })
         response.status_code = 400
         return response
diff --git a/backend/uclapi/dashboard/app_helpers.py b/backend/uclapi/dashboard/app_helpers.py
--- a/backend/uclapi/dashboard/app_helpers.py
+++ b/backend/uclapi/dashboard/app_helpers.py
@@ -13,6 +13,10 @@
 import textwrap
 import validators
 
+NOT_HTTPS = 1
+NOT_VALID = 2
+URL_BLACKLISTED = 3
+NOT_PUBLIC = 4
 
 def get_articles():
     r = redis.Redis(host=REDIS_UCLAPI_HOST)
@@ -73,23 +77,25 @@ def generate_app_client_secret():
     return client_secret
 
 
-def is_url_safe(url):
+def is_url_unsafe(url):
     if not url.startswith("https://"):
-        return False
+        return NOT_HTTPS
 
     if not validators.url(url, public=True):
-        return False
+        if validators.url(url, public=False):
+            return NOT_PUBLIC
+        return NOT_VALID
 
     whitelist_urls = os.environ["WHITELISTED_CALLBACK_URLS"].split(';')
     if url in whitelist_urls:
-        return True
+        return 0
 
     forbidden_urls = os.environ["FORBIDDEN_CALLBACK_URLS"].split(';')
     for furl in forbidden_urls:
         if furl in url:
-            return False
+            return URL_BLACKLISTED
 
-    return True
+    return 0
 
 
 def generate_secret():
diff --git a/backend/uclapi/dashboard/webhook_views.py b/backend/uclapi/dashboard/webhook_views.py
--- a/backend/uclapi/dashboard/webhook_views.py
+++ b/backend/uclapi/dashboard/webhook_views.py
@@ -2,7 +2,7 @@
 from .models import App, User
 from dashboard.tasks import keen_add_event_task as keen_add_event
 import requests
-from .app_helpers import generate_secret, is_url_safe
+from .app_helpers import generate_secret, is_url_unsafe
 
 
 def user_owns_app(user_id, app_id):
@@ -84,7 +84,7 @@ def edit_webhook(request):
     webhook = app.webhook
 
     if url != webhook.url:
-        if not is_url_safe(url):
+        if is_url_unsafe(url):
             response = PrettyJsonResponse({
                 "ok": False,
                 "message": (


Test patch:
diff --git a/backend/uclapi/dashboard/tests.py b/backend/uclapi/dashboard/tests.py
--- a/backend/uclapi/dashboard/tests.py
+++ b/backend/uclapi/dashboard/tests.py
@@ -6,7 +6,7 @@
 from django.conf import settings
 import redis
 
-from .app_helpers import is_url_safe, generate_api_token, \
+from .app_helpers import is_url_unsafe, generate_api_token, \
     generate_app_client_id, generate_app_client_secret, \
     generate_app_id, get_articles
 from .middleware.fake_shibboleth_middleware import FakeShibbolethMiddleWare
@@ -102,13 +102,13 @@ def test_post_agreement(self):
         self.assertContains(res, "Test testington")
 
     def test_unsafe_urls(self):
-        assert not is_url_safe("ftp://test.com")
-        assert not is_url_safe("https://uclapi.com/callback")
-        assert not is_url_safe("ssh://uclapi.com/callback")
+        assert is_url_unsafe("ftp://test.com")
+        assert is_url_unsafe("https://uclapi.com/callback")
+        assert is_url_unsafe("ssh://uclapi.com/callback")
 
     def test_safe_url(self):
-        assert is_url_safe("https://mytestapp.com/callback")
-        assert is_url_safe("https://uclapiexample.com/callback")
+        assert not is_url_unsafe("https://mytestapp.com/callback")
+        assert not is_url_unsafe("https://uclapiexample.com/callback")
 
 
 class FakeShibbolethMiddleWareTestCase(TestCase):
@@ -183,38 +183,38 @@ def test_generate_app_id(self):
 
 class URLSafetyTestCase(TestCase):
     def test_is_url_safe_full_success(self):
-        self.assertTrue(
-            is_url_safe("https://example.com")
+        self.assertFalse(
+            is_url_unsafe("https://example.com")
         )
 
     def test_is_url_safe_https_failure(self):
-        self.assertFalse(
-            is_url_safe("http://example.com")
+        self.assertTrue(
+            is_url_unsafe("http://example.com")
         )
 
     def test_is_url_safe_validators_failure(self):
-        self.assertFalse(
-            is_url_safe("https://asdasd.asd.asd.asd.1234")
+        self.assertTrue(
+            is_url_unsafe("https://asdasd.asd.asd.asd.1234")
         )
 
     def test_is_url_safe_validators_failure_private(self):
-        self.assertFalse(
-            is_url_safe("https://127.0.0.1")
+        self.assertTrue(
+            is_url_unsafe("https://127.0.0.1")
         )
 
     def test_is_url_safe_validators_failure_private2(self):
-        self.assertFalse(
-            is_url_safe("https://10.0.0.1")
+        self.assertTrue(
+            is_url_unsafe("https://10.0.0.1")
         )
 
     def test_is_url_safe_forbidden(self):
-        self.assertFalse(
-            is_url_safe("https://uclapi.com/test/test")
+        self.assertTrue(
+            is_url_unsafe("https://uclapi.com/test/test")
         )
 
     def test_is_url_safe_forbidden2(self):
-        self.assertFalse(
-            is_url_safe("https://staging.ninja/test/test")
+        self.assertTrue(
+            is_url_unsafe("https://staging.ninja/test/test")
         )
     # Testcase for whitelisted URL needed
 
@@ -311,7 +311,7 @@ def test_edit_webhook_POST_user_does_not_own_app(self):
         )
 
     @patch("dashboard.webhook_views.verify_ownership", lambda *args: False)
-    @patch("dashboard.webhook_views.is_url_safe", lambda *args: True)
+    @patch("dashboard.webhook_views.is_url_unsafe", lambda *args: False)
     def test_edit_webhook_POST_ownership_verification_fail(
         self
     ):
@@ -338,7 +338,7 @@ def test_edit_webhook_POST_ownership_verification_fail(
         )
 
     @patch("dashboard.webhook_views.verify_ownership", lambda *args: True)
-    @patch("dashboard.webhook_views.is_url_safe", lambda *args: True)
+    @patch("dashboard.webhook_views.is_url_unsafe", lambda *args: False)
     @patch("keen.add_event", lambda *args: None)
     def test_edit_webhook_POST_user_owns_app_changing_url_verification_ok(
         self
@@ -831,7 +831,7 @@ def test_change_callback_url_not_valid(self):
         self.assertEqual(response.status_code, 400)
         self.assertEqual(
             content["message"],
-            "The requested callback URL is not valid."
+            "The requested callback URL does not start with 'https://'."
         )
 
     def test_change_callback_url_success(self):