Browse Source

Integrations with existing sites (#1269)

Great! Let's merge those and work out the quirks in next PRs!
Wojciech Zając 5 years ago
parent
commit
e16b69def2

+ 6 - 0
devproject/settings.py

@@ -193,6 +193,7 @@ INSTALLED_APPS = [
     "misago.socialauth",
     "misago.socialauth",
     "misago.graphql",
     "misago.graphql",
     "misago.faker",
     "misago.faker",
+    "misago.sso",
 ]
 ]
 
 
 INTERNAL_IPS = ["127.0.0.1"]
 INTERNAL_IPS = ["127.0.0.1"]
@@ -416,3 +417,8 @@ EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
 DEBUG_TOOLBAR_CONFIG = {
 DEBUG_TOOLBAR_CONFIG = {
     "SHOW_TOOLBAR_CALLBACK": "misago.conf.debugtoolbar.enable_debug_toolbar"
     "SHOW_TOOLBAR_CALLBACK": "misago.conf.debugtoolbar.enable_debug_toolbar"
 }
 }
+
+# SECURITY WARNING: keep the private key used in production secret!
+SSO_PRIVATE_KEY = None  # This key should be secret (max 64 chars)
+SSO_PUBLIC_KEY = None  # This key may be public (max 64 chars)
+SSO_SERVER = "http://www.example.com/server/"

+ 8 - 0
devproject/test_settings.py

@@ -52,6 +52,9 @@ MISAGO_POST_VALIDATORS = ["misago.core.testproject.validators.test_post_validato
 # Register test post search filter
 # Register test post search filter
 MISAGO_POST_SEARCH_FILTERS = ["misago.core.testproject.searchfilters.test_filter"]
 MISAGO_POST_SEARCH_FILTERS = ["misago.core.testproject.searchfilters.test_filter"]
 
 
+# Default test name
+TEST_NAME = "miasago_test"
+
 # Additional overrides for Travis-CI
 # Additional overrides for Travis-CI
 if os.environ.get("TRAVIS"):
 if os.environ.get("TRAVIS"):
     DATABASES = {
     DATABASES = {
@@ -66,3 +69,8 @@ if os.environ.get("TRAVIS"):
     }
     }
 
 
     TEST_NAME = "travis_ci_test"
     TEST_NAME = "travis_ci_test"
+
+# for testing misago.sso module (single sign on)
+SSO_PRIVATE_KEY = "priv1"
+SSO_PUBLIC_KEY = "fakeSsoPublicKey"
+SSO_SERVER = "http://example.com/server/"

+ 2 - 0
devproject/urls.py

@@ -43,6 +43,8 @@ urlpatterns = [
     ),
     ),
     # Uncomment next line if you plan to use Django admin for 3rd party apps
     # Uncomment next line if you plan to use Django admin for 3rd party apps
     url(r"^django-admin/", admin.site.urls),
     url(r"^django-admin/", admin.site.urls),
+    # django-simple-sso doesn't have namespaces, we can't use namespace here
+    url(r"^sso/", include("misago.sso.urls")),
 ]
 ]
 
 
 
 

+ 56 - 0
misago/sso/README.rst

@@ -0,0 +1,56 @@
+=====================
+Misago Single Sign ON
+=====================
+
+Client
+======
+
+In Misago instance set settings::
+
+    SSO_PRIVATE_KEY = 'MySecretKey'
+    SSO_PUBLIC_KEY = 'MyPublicKey'
+    SSO_SERVER = "http://www.example.com/server/"
+
+Server
+======
+
+Example django server instance config:
+
+1. Install django-simple-sso: ``pip install django-simple-sso``
+2. Run migrations: ``./manage.py migrate``
+3. Create ``Consumer`` object in shell: ``./manage.py shell``::
+
+    > from simple_sso.sso_server.models import Consumer
+    > Consumer.objects.create(public_key='MyPublicKey',
+    ... private_key='MySecretKey', name='MyAppName')
+
+4. Add ``'simple_sso.sso_server'`` to ``INSTALLED_APPS``
+5. Initialize server and add urls to ``urls.py``::
+
+    from simple_sso.sso_server.server import Server
+    my_server = Server()
+    urlpatterns += [
+        url(r'^server/', include(my_server.get_urls())),
+    ]
+
+How to test
+===========
+
+In your web browser Go to site ``http://localhost:8000/sso/client/``. You should be redirected to
+login on your ``server`` site. After logging you will back to Misago and you will be logged as a
+user from ``server`` site.
+
+External docs
+=============
+
+Soruce code
+-----------
+
+* https://github.com/divio/django-simple-sso
+
+Docs sites
+----------
+
+* https://micropyramid.com/blog/django-single-sign-on-sso-to-multiple-applications/
+* https://medium.com/@MicroPyramid/django-single-sign-on-sso-to-multiple-applications-64637da015f4
+

+ 1 - 0
misago/sso/__init__.py

@@ -0,0 +1 @@
+default_app_config = "misago.sso.apps.MisagoSsoConfig"

+ 11 - 0
misago/sso/apps.py

@@ -0,0 +1,11 @@
+from django.apps import AppConfig
+
+
+class MisagoSsoConfig(AppConfig):
+    """
+    Using https://github.com/divio/django-simple-sso
+    """
+
+    name = "misago.sso"
+    label = "misago_sso"
+    verbose_name = "Misago Single Sign On"

+ 39 - 0
misago/sso/client.py

@@ -0,0 +1,39 @@
+from django.contrib.auth import get_user_model
+from django.contrib.auth.hashers import make_password
+from misago.conf import settings
+from misago.conf.shortcuts import get_dynamic_settings
+from misago.users.authbackends import MisagoBackend
+from misago.users.setupnewuser import setup_new_user
+
+from simple_sso.sso_client.client import Client
+
+User = get_user_model()
+
+
+class ClientMisago(Client):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.backend = "%s.%s" % (MisagoBackend.__module__, MisagoBackend.__name__)
+
+    def build_user(self, user_data):
+        try:
+            user = User.objects.get(username=user_data["username"])
+        except User.DoesNotExist:
+
+            user = User.objects.create_user(
+                user_data["username"],
+                user_data["email"],
+                make_password(make_password("ItDoesMatter")),
+            )
+
+            user.update_acl_key()
+
+            user_settings = get_dynamic_settings()
+            setup_new_user(user_settings, user)
+
+        return user
+
+
+client = ClientMisago(
+    settings.SSO_SERVER, settings.SSO_PUBLIC_KEY, settings.SSO_PRIVATE_KEY
+)

+ 104 - 0
misago/sso/tests.py

@@ -0,0 +1,104 @@
+from itsdangerous.timed import TimestampSigner
+from requests import Response
+from requests.sessions import Session
+from urllib.parse import urlparse
+
+from django.contrib.auth import get_user_model
+from django.shortcuts import reverse
+from django.test import override_settings, TestCase
+from django.utils.timezone import now
+
+User = get_user_model()
+
+
+class ConnectionMock:
+    def __init__(self):
+        self.Session = Session
+
+    def __enter__(self):
+        self.origin_post = Session.post
+
+        def mocked_post(*args, **kwargs):
+            mocked_response = Response()
+            requested_url = args[1]
+            if "/server/request-token/" == urlparse(requested_url).path:
+                # token generated for private key settings.SSO_PRIVATE_KEY = 'priv1'
+                mocked_response._content = (
+                    b'{"request_token": "XcHtuemqcjnIT6J2WHTFswLQP0W07nI96XfxqGkm6b1zFToF0YGEoIYu3'
+                    b'7QOajkc"}.XTd9sA.quRsXFxqMk-ufwSc79q-_YLDNzg'
+                )
+            elif "/server/verify/" == urlparse(requested_url).path:
+                mocked_response._content = (
+                    b'{"username": "jkowalski", "email": "jkowalski@example.com", "first_name": '
+                    b'"Jan", "last_name": "Kowalski", "is_staff": false, "is_superuser": false, '
+                    b'"is_active": true}.XTg4IQ._cANZR5jHvtwhNzcnNYDfE1nLHE'
+                )
+
+            mocked_response.status_code = 200
+            return mocked_response
+
+        setattr(self.Session, "post", mocked_post)
+        return self.Session
+
+    def __exit__(self, type, value, traceback):
+        setattr(self.Session, "post", self.origin_post)
+
+
+class TimestampSignerMock:
+    def __init__(self):
+        self.TimestampSigner = TimestampSigner
+
+    def __enter__(self):
+        self.origin_unsign = TimestampSigner.unsign
+
+        def mocked_unsign(*args, **kwargs):
+            s = args[1]
+            if b'"username": "jkowalski"' in s:
+                value = s[:166]  # {...}
+                timestamp_to_datetime = now()
+                return value, timestamp_to_datetime
+            else:
+                return self.origin_unsign(*args, **kwargs)
+
+        setattr(self.TimestampSigner, "unsign", mocked_unsign)
+        return self.TimestampSigner
+
+    def __exit__(self, type, value, traceback):
+        setattr(self.TimestampSigner, "unsign", self.origin_unsign)
+
+
+class SsoModuleTestCase(TestCase):
+    def test_sso_client(self):
+        url_to_external_logging = reverse("simple-sso-login")
+        self.assertEqual("/sso/client/", url_to_external_logging)
+
+        with ConnectionMock():
+            response = self.client.get(url_to_external_logging)
+
+        self.assertEqual(302, response.status_code)
+
+        url_parsed = urlparse(response.url)
+        self.assertEqual("/server/authorize/", url_parsed.path)
+        self.assertEqual(
+            "token=XcHtuemqcjnIT6J2WHTFswLQP0W07nI96XfxqGkm6b1zFToF0YGEoIYu37QOajkc",
+            url_parsed.query,
+        )
+
+    def test_sso_client_authenticate(self):
+        url_to_authenticate = reverse("simple-sso-authenticate")
+        self.assertEqual("/sso/client/authenticate/", url_to_authenticate)
+        query = (
+            "next=%2F&access_token=InBBMjllMlNla2ZWdDdJMnR0c3R3QWIxcjQwRzV6TmphZDRSaEprbjlMbnR0TnF"
+            "Ka3Q2d1dNR1lVYkhzVThvZU0i.XTeRVQ.3XiIMg0AFcJKDFCekse6s43uNLI"
+        )
+        url_to_authenticate += "?" + query
+
+        with ConnectionMock():
+            with TimestampSignerMock():
+                response = self.client.get(url_to_authenticate)
+
+        self.assertEqual(302, response.status_code)
+        self.assertEqual("/", response.url)
+
+        u = User.objects.first()
+        self.assertEqual("jkowalski", u.username)

+ 4 - 0
misago/sso/urls.py

@@ -0,0 +1,4 @@
+from django.conf.urls import url, include
+from .client import client
+
+urlpatterns = [url(r"^client/", include(client.get_urls()))]

+ 1 - 0
requirements.in

@@ -7,6 +7,7 @@ djangorestframework<3.10
 django-debug-toolbar<1.12
 django-debug-toolbar<1.12
 django-htmlmin<0.12
 django-htmlmin<0.12
 django-mptt
 django-mptt
+django-simple-sso
 Faker<1.1
 Faker<1.1
 html5lib<1.1
 html5lib<1.1
 markdown<3
 markdown<3

+ 3 - 0
requirements.txt

@@ -19,6 +19,7 @@ django-debug-toolbar==1.11
 django-htmlmin==0.11.0
 django-htmlmin==0.11.0
 django-js-asset==1.2.2    # via django-mptt
 django-js-asset==1.2.2    # via django-mptt
 django-mptt==0.10.0
 django-mptt==0.10.0
+django-simple-sso==0.14.0
 django==2.2.3
 django==2.2.3
 djangorestframework==3.9.4
 djangorestframework==3.9.4
 faker==1.0.7
 faker==1.0.7
@@ -26,6 +27,7 @@ graphql-core-next==1.0.5  # via ariadne
 html5lib==1.0.1
 html5lib==1.0.1
 idna==2.8                 # via requests
 idna==2.8                 # via requests
 importlib-metadata==0.18  # via pluggy, pytest
 importlib-metadata==0.18  # via pluggy, pytest
+itsdangerous==1.1.0       # via django-simple-sso, webservices
 kombu==4.6.3              # via celery
 kombu==4.6.3              # via celery
 markdown==2.6.11
 markdown==2.6.11
 more-itertools==7.1.0     # via pytest
 more-itertools==7.1.0     # via pytest
@@ -63,4 +65,5 @@ urllib3==1.25.3           # via requests
 vine==1.3.0               # via amqp, celery
 vine==1.3.0               # via amqp, celery
 wcwidth==0.1.7            # via pytest
 wcwidth==0.1.7            # via pytest
 webencodings==0.5.1       # via bleach, html5lib
 webencodings==0.5.1       # via bleach, html5lib
+webservices[django]==0.7  # via django-simple-sso
 zipp==0.5.2               # via importlib-metadata
 zipp==0.5.2               # via importlib-metadata