From 69eb0e128612ac7be56674f54950e8a86097fd83 Mon Sep 17 00:00:00 2001 From: Art Lukyanchyk <artiom.lukyanchyk@hs-hannover.de> Date: Thu, 25 Apr 2019 18:25:41 +0200 Subject: [PATCH] Untangle some spaghetti and remove unnecessary config --- README.md | 24 +++++------ ssoauth/app_settings/__init__.py | 4 ++ ssoauth/app_settings/defaults.py | 2 +- ssoauth/checks.py | 43 ------------------- ssoauth/templates/ssoauth/dev.html | 4 +- ssoauth/urls.py | 3 +- ssoauth/views.py | 67 ++++++++++++++++-------------- 7 files changed, 56 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index 4fb7d06..c95faf4 100644 --- a/README.md +++ b/README.md @@ -13,20 +13,21 @@ - Binary dependencies: `sudo apt install libxml2-dev libxslt1-dev xmlsec1 libxmlsec1-dev pkg-config` - Python dependencies: see `requirements.txt` or `setup.py` - Add the app into `INSTALLED_APPS` -- Include the app's `urls.py` into the project `urls.py` `urlpatterns`, preferably without a prefix +- Include the `ssoauth` `urls.py` into the project `urls.py` `urlpatterns`: + - Without a path/prefix: youre done. + - With a path/prefix: + - Reconsider it. It's highly recommended to include `ssoauth` **without** a prefix/path to avoid issues with apps like `contrib.admin` and `wagtail` that provide their own log in pages. + - If you really need to use a path/prefix, make sure to set a setting `LOGIN_URL = urls.reverse_lazy("sso-login")` #### Development Setup -- Simply use `localhost:8000/dev/` _(might change depending on your urlconf)_ for all the logging-in-out-users-groups stuff -- To make your life easier, add the following to your project settings: -```python -LOGIN_URL = urls.reverse_lazy("sso-dev") -``` -- If you want to debug `ssoauth` or need a fully functional SSO during development for some other reason, an example is below. For additional info see the production setup section and `ssoauth.app_settings.defaults`. If you also want a working SLO during development you will need SSL for your localhost, `nginx` will be your best friend. +- Should work with zero additional configuration. +- You can use `localhost:8000/dev/` _(might change depending on your configuration)_ to access the dev view with all its helper features. #### Advanced development setup -If you are not sure whether you need it: you don't need it. +If you are not sure whether you need it: you don't need it. +Use this only if you want an actual SSO with SAML2. For extra details see the default settings in `ssoauth.app_settings.defaults`. You might also need SSL, try using `nginx` for it. ```python """ settings/dev.py """ @@ -44,8 +45,6 @@ SP_PORT = 8000 SP_SSL = False SP_FORCE_ENTITY_ID = "dev-id-{0}-{1}".format(socket.gethostname(), os.path.dirname(os.path.dirname(__file__))) # too many localhosts around - -LOGIN_URL = urls.reverse_lazy("sso-dev") # it's "sso-login" for prod ``` #### Overriding Log In Pages of Other Apps @@ -128,8 +127,8 @@ openssl req -newkey rsa:16384 -x509 -days 3650 -nodes -out sp.pem -keyout sp.key #### Add an SP to an IdP -- Ask somebody who knows more. Seriously. -- Try on the test IdP before you brick the production! +- Ask for assistance. Seriously. +- Please try on the test IdP before you brick the production! - Grab your meta - Run your project. - Find meta of your SP (relative path `/saml2/meta`) @@ -142,7 +141,6 @@ openssl req -newkey rsa:16384 -x509 -days 3650 -nodes -out sp.pem -keyout sp.key - `id` should be unique - `metadataFile` should point on your new metadata - edit `./conf/attribute-filter.xml` - - use your head - `systemctl restart tomcat8` - make sure that IDP works - if IDP does not work, rollback your changes and restart the IDP again diff --git a/ssoauth/app_settings/__init__.py b/ssoauth/app_settings/__init__.py index 6de60b5..f6c7bdc 100644 --- a/ssoauth/app_settings/__init__.py +++ b/ssoauth/app_settings/__init__.py @@ -82,6 +82,10 @@ ONELOGIN_SETTINGS_OBJECT = _SET_ON_RUNTIME # helpers +def sso_is_configured(): + return bool(ONELOGIN_SETTINGS_OBJECT) + + SSO_REQUIRED = any([ SSO_REQUIRED_IN_DEBUG and conf.settings.DEBUG, SSO_REQUIRED_IN_PRODUCTION and not conf.settings.DEBUG, diff --git a/ssoauth/app_settings/defaults.py b/ssoauth/app_settings/defaults.py index 12aad16..cdcb34b 100644 --- a/ssoauth/app_settings/defaults.py +++ b/ssoauth/app_settings/defaults.py @@ -10,7 +10,7 @@ If you want to change something: """ -Settings you may want to change: +Settings you might want to change: """ # host and port, not what Django thinks, but what nginx serves diff --git a/ssoauth/checks.py b/ssoauth/checks.py index 846eab9..87b38dd 100644 --- a/ssoauth/checks.py +++ b/ssoauth/checks.py @@ -109,49 +109,6 @@ def sp_host_is_not_localhost(app_configs, **kwargs): return errors -@register(Tags.compatibility) -def pretend_backend(app_configs, **kwargs): - errors = list() - pretend_expected = "django.contrib.auth.backends.ModelBackend" - if app_settings.PRETEND_AUTH_BACKEND != pretend_expected: - errors.append(Warning("Please make sure the first element of AUTHENTICATION_BACKENDS is django.contrib.auth.backends.ModelBackend; " - "if you are not using this backend you need to ensure the first backend in the list knows how to get user by id/natural key")) - return errors - - -@register(Tags.urls) -def auth_urls_configured(app_configs, **kwargs): - errors = list() - - def _url_from_setting(setting_name): - # get url based on django setting name, handles most of cases of something going wrong - try: - setting_value = getattr(conf.settings, setting_name, None) # setting exists - assert urls.resolve(setting_value) # page exists - return setting_value - except Exception as e: - errors.append(Warning("Failed to resolve an URL for {s}. {ec}: {e}".format(ec=e.__class__.__name__, e=str(e),s=setting_name), obj=conf.settings)) - return None - - # # default landing and logout pages - # # probably this part is not longer required because how the logout process works - # for setting_name in ("LOGIN_REDIRECT_URL", "LOGOUT_REDIRECT_URL",): - # if not _url_from_setting(setting_name): - # errors.append(Warning("{setting_name} is not found or invalid.".format(**locals()), obj=conf.settings,)) - - if app_settings.SSO_REQUIRED: - # login - login_url_needed = urls.reverse("sso-login") - login_url_current = _url_from_setting("LOGIN_URL") - if login_url_current != login_url_needed: - errors.append(Warning( - "LOGIN_URL must point on {login_url_needed}, not on {login_url_current}. Add the following to your project settings: " - "LOGIN_URL = urls.reverse_lazy(\"sso-login\")".format(**locals()), - obj=conf.settings, - )) - return errors - - @register(Tags.security) def session_lifetime(app_configs, **kwargs): errors = list() diff --git a/ssoauth/templates/ssoauth/dev.html b/ssoauth/templates/ssoauth/dev.html index 4409f03..c9d3ff9 100644 --- a/ssoauth/templates/ssoauth/dev.html +++ b/ssoauth/templates/ssoauth/dev.html @@ -43,12 +43,12 @@ <button type="submit" name="local-logout" value="local-logout" class="button button-outline button-narrow">Log Out</button> <p><i>Extra actions for development and debugging</i></p> </div> - <div class="column {% if not sso_configured %}disabled{% endif %}"> + <div class="column {% if not sso_is_configured %}disabled{% endif %}"> <h4 class="title">Production Actions</h4> <a class="button button-outline button-narrow" href="{% url "sso-login" %}?next={% url "sso-dev" %}">Log In</a> <a class="button button-outline button-narrow" href="{% url "sso-logout" %}">Log Out</a> <a class="button button-outline button-narrow" href="{% url "sso-saml2-meta" %}" target="_blank">Meta</a> - <p><i>{% if sso_configured %}These actions are used in production{% else %}Your SSO settings are a potato{% endif %}</i></p> + <p><i>{% if sso_is_configured %}These actions are used in production{% else %}SSO is not configured{% endif %}</i></p> </div> </div> </form> diff --git a/ssoauth/urls.py b/ssoauth/urls.py index 5970e76..550dc82 100644 --- a/ssoauth/urls.py +++ b/ssoauth/urls.py @@ -3,7 +3,7 @@ from django import conf from . import views urlpatterns = ( - url(r"^login/?$", views.LogInView.as_view(), name="sso-login"), + url(r"^(?:.*/)?login/?$", views.LogInView.as_view(), name="sso-login"), # aggressive login pattern helps against apps that provide own login pages and forms url(r"^logout/?$", views.LogOutView.as_view(), name="sso-logout"), url(r"^logout/message/?$", views.LoggedOutLocallyView.as_view(), name="sso-logged-out-locally"), url(r"^logout/idp/?$", views.IdpLogoutRedirectView.as_view(), name="sso-logout-idp"), @@ -13,6 +13,7 @@ urlpatterns = ( ) if conf.settings.DEBUG: + # add the dev view urlpatterns += ( url(r"^dev/?$", views.DevView.as_view(), name="sso-dev"), ) diff --git a/ssoauth/views.py b/ssoauth/views.py index d61f747..b71db1c 100644 --- a/ssoauth/views.py +++ b/ssoauth/views.py @@ -22,15 +22,19 @@ from datetime import timedelta class SAMLMixin: - """ Merges Django runtime info and settings for OneLogin toolkit """ - def __init__(self, *args, **kwargs): - if not app_settings.ONELOGIN_SETTINGS_OBJECT: + @classmethod + def get_onelogin_auth(cls, request): + """ :return: that weird auth object used by the onelogin toolkit """ + if not app_settings.sso_is_configured(): raise exceptions.ImproperlyConfigured("SSO is not configured.") - super().__init__(*args, **kwargs) + return OneLogin_Saml2_Auth( + request_data=cls._get_onelogin_request_data(request), + old_settings=app_settings.ONELOGIN_SETTINGS_OBJECT + ) - @staticmethod - def get_onelogin_request_data(request): + @classmethod + def _get_onelogin_request_data(cls, request): return { # since nginx is in the way, the settings cannot be fetched from the request "https": "on" if app_settings.SP_SSL else "off", @@ -41,16 +45,9 @@ class SAMLMixin: "post_data": request.POST.copy(), } - @classmethod - def get_onelogin_auth(cls, request): - return OneLogin_Saml2_Auth( - request_data=cls.get_onelogin_request_data(request), - old_settings=app_settings.ONELOGIN_SETTINGS_OBJECT - ) - @method_decorator(never_cache, "dispatch") -class LogInView(SAMLMixin, View): +class LogInView(SAMLMixin, RedirectView): """ This view initiates SSO log in. To change behavior for already logged in users, you can use: @@ -58,26 +55,24 @@ class LogInView(SAMLMixin, View): - LogInView.as_view(already_authenticated_redirect="some-url-name") """ - already_authenticated_403 = False # e.g. admin redirects to login when insufficient permissions - already_authenticated_redirect = None + already_authenticated_403 = False # raise 403 if the user is already authenticated - def get(self, request, *args, **kwargs): - if request.user.is_authenticated: - logger.warning("{u} is already authenticated and attempts to log in again.".format(u=request.user)) + def get_redirect_url(self, *args, **kwargs): + if self.request.user.is_authenticated: + logger.info("{u} is already authenticated and attempts to log in again.".format(u=self.request.user)) if self.already_authenticated_403: - logger.warning("Assuming 403 (you configured the view this way).") - raise exceptions.PermissionDenied() - if self.already_authenticated_redirect: - logger.warning("Redirecting (you configured the view this way).") - return http.HttpResponseRedirect(urls.reverse(self.already_authenticated_redirect)) - if request.user.last_login > timezone.now() - timedelta(seconds=20): - # possible redirect loop (e.g. django.contrib.admin causes them) - logger.error("{u} is logging in too often (probably an endless redirect loop).".format(u=request.user)) + raise exceptions.PermissionDenied("Already authenticated") + if self.request.user.last_login > timezone.now() - timedelta(seconds=60): + # most likely a redirect loop (django.contrib.admin loves to cause these) + logger.warning("{u} has already logged in recently. Probably a redirect loop, happens in django due to insufficient permissions to display some page.".format(u=self.request.user)) raise exceptions.PermissionDenied() + if conf.settings.DEBUG and not app_settings.sso_is_configured(): + logger.info("SSO is not configured, redirecting to the dev view instead.") + return self._get_redirect_to_dev() # logging in - auth = self.get_onelogin_auth(request) - login = auth.login(return_to=self.get_next_url(request)) - return http.HttpResponseRedirect(login) + auth = self.get_onelogin_auth(self.request) + idp_login_url = auth.login(return_to=self.get_next_url(self.request)) + return idp_login_url @staticmethod def get_next_url(request): @@ -85,6 +80,16 @@ class LogInView(SAMLMixin, View): logger.debug("Will ask IDP to redirect after login to: {}".format(next_url)) return str(next_url) + def _get_redirect_to_dev(self): + redirect_to = urls.reverse("sso-dev") + if REDIRECT_FIELD_NAME in self.request.GET: # preserve the next url + redirect_to = "{0}?{1}={2}".format( + redirect_to, + REDIRECT_FIELD_NAME, + self.request.GET.get(REDIRECT_FIELD_NAME) + ) + return redirect_to + @method_decorator(never_cache, "dispatch") class LogOutView(RedirectView): @@ -311,7 +316,7 @@ class DevView(FormView): ["Session", dict(self.request.session)], ] context.update(dict( - sso_configured=bool(app_settings.ONELOGIN_SETTINGS_OBJECT), + sso_is_configured=bool(app_settings.sso_is_configured()), next_url=self.next_url, )) return context -- GitLab