diff --git a/README.md b/README.md index d00760cf33b6695fa16ecacaea1c64db78dcbcb5..a823e51c4f53583db38e51e216109a723822bbc2 100644 --- a/README.md +++ b/README.md @@ -16,31 +16,29 @@ #### Development setup -This is what you normally want during the development. + +- If you don't need a fully functional SSO, you don't need to configure anything at all. Use the dev view: `localhost:8000/dev/` (might change depending on your urlconf and settings). + +- If you need a fully functional SSO during development, here's an example: ```python """ settings/dev.py """ +from django import urls import socket import os -DO_YOU_WANT_SSO = False +IDP_META_URL = "https://idp-test.it.hs-hannover.de/idp/shibboleth" -if DO_YOU_WANT_SSO: - SP_HOST = "localhost" - SP_PORT = 8000 - SP_SSL = False - SP_FORCE_ENTITY_ID = "dev-auto-id-{0}-{1}".format(socket.gethostname(), os.path.dirname(os.path.dirname(__file__))) - IDP_META_URL = "https://idp-test.it.hs-hannover.de/idp/shibboleth" # development -else: - SSO_DISABLED = True -``` +SP_KEY = "{project_settings}/cert/sp.key" +SP_CERT = "{project_settings}/cert/sp.pem" +SP_FORCE_ENTITY_ID = "dev-auto-id-{0}-{1}".format(socket.gethostname(), os.path.dirname(os.path.dirname(__file__))) -Use `localhost:8000/dev/` to acces the development view. - -The code snippet above disables the actual SSO. If you need it: - - change `DO_YOU_WANT_SSO` to True - - see the SSO configuration section +SP_HOST = "localhost" +SP_PORT = 8000 +SP_SSL = False +LOGIN_URL = urls.reverse_lazy("sso-dev") +``` #### Groups To receive groups over SSO you need a mapping. You can manage group mapping with `group_mapping` management command. Example: @@ -54,10 +52,12 @@ To receive groups over SSO you need a mapping. You can manage group mapping with ```python """ settings/prod.py """ +from django import urls SP_HOST = "141.71.foo.bar" # your SP host or IP address IDP_META_URL = "https://idp.hs-hannover.de/idp/shibboleth" # production +LOGIN_URL = urls.reverse_lazy("sso-login") ``` You will also need to configure the SSO diff --git a/ssoauth/app_settings/__init__.py b/ssoauth/app_settings/__init__.py index 61836c1abc1104f828f0f0c0c305626858a8ac06..b043f659d4f779d78d617a99463d60c8818f7793 100644 --- a/ssoauth/app_settings/__init__.py +++ b/ssoauth/app_settings/__init__.py @@ -1,7 +1,8 @@ -from onelogin.saml2 import settings as onelogin_settings -from .defaults import * from django import conf from datetime import datetime, timedelta +from onelogin.saml2 import settings as onelogin_settings +import sys +from .defaults import * # merge defaults with customized user settings @@ -15,43 +16,11 @@ for setting_name in [k for k in globals().keys() if k.isupper()]: pass # not set -# checks - -SSO_DISABLED = SSO_DISABLED or getattr(conf.settings, "IDP_IGNORE", False) # legacy config -if SSO_DISABLED: - assert conf.settings.DEBUG, "Not ignoring IDP on production." -else: - assert SP_HOST and SP_PORT, "Need SP_HOST and SP_PORT configured in settings." - assert not SP_HOST.lower().startswith(("http:", "https:",)), "Need host name without protocol and port." - -# helpers - -_SET_ON_RUNTIME = None and "doesn't make sense to change it" - - -def get_project_settings_path(): - module = os.environ.get(conf.ENVIRONMENT_VARIABLE) - path = os.path.abspath(os.path.join(*module.split("."))) - if not os.path.isdir(path): - # module, not a package - path = os.path.dirname(path) - return path - - -def read_key(path): - path = path.format(project_settings=get_project_settings_path()) - try: - with open(path, "r") as f: - return f.read() - except FileNotFoundError: - if SSO_DISABLED: - return None - else: - raise FileNotFoundError("SSO requires a key pair. Missing: {path}".format(path=path)) - # template for OneLogin toolkit settings +_SET_ON_RUNTIME = None and "will be set on runtime" + ONELOGIN_SETTINGS_TEMPLATE = { # Don't change it (you cannot in fact) # If you really need to adjust something here, please use ONELOGIN_OVERRIDES instead @@ -61,9 +30,14 @@ ONELOGIN_SETTINGS_TEMPLATE = { "entityId": _SET_ON_RUNTIME or str(), "assertionConsumerService": { "url": _SET_ON_RUNTIME or str(), + "binding": onelogin_settings.OneLogin_Saml2_Constants.BINDING_HTTP_POST, + }, + "singleLogoutService": { + "url": _SET_ON_RUNTIME or str(), + "binding": onelogin_settings.OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT, }, - "x509cert": read_key(SP_CERT), - "privateKey": read_key(SP_KEY), + "x509cert": _SET_ON_RUNTIME, + "privateKey": _SET_ON_RUNTIME, "NameIDFormat": onelogin_settings.OneLogin_Saml2_Constants.NAMEID_PERSISTENT, # otherwise Shibboleth shows warnings }, "idp": { @@ -102,4 +76,14 @@ ONELOGIN_SETTINGS_TEMPLATE = { ONELOGIN_SETTINGS_TEMPLATE.update(ONELOGIN_OVERRIDES) -ONELOGIN_SETTINGS = _SET_ON_RUNTIME +ONELOGIN_SETTINGS_OBJECT = _SET_ON_RUNTIME + + +# helpers + +SSO_REQUIRED = any([ + SSO_REQUIRED_IN_DEBUG and conf.settings.DEBUG, + SSO_REQUIRED_IN_PRODUCTION and not conf.settings.DEBUG, + SSO_REQUIRED_OUTSIDE_MANAGE_PY and not any("manage.py" in arg.lower() for arg in sys.argv), +]) + diff --git a/ssoauth/app_settings/defaults.py b/ssoauth/app_settings/defaults.py index b9055a8229150163f124fa7f6c8dd36f00b88aed..5207242d75c1645941e26cc07d6f0436607ec981 100644 --- a/ssoauth/app_settings/defaults.py +++ b/ssoauth/app_settings/defaults.py @@ -13,22 +13,24 @@ Settings you may want to change: """ # host and port, not what Django thinks, but what nginx serves -SP_HOST = None # e.g. "141.71.foo.bar", for development can use "localhost" and set FORCE_ENTITY_ID +SP_HOST = "localhost" # e.g. "141.71.foo.bar", for development can use "localhost" and set FORCE_ENTITY_ID SP_PORT = 443 SP_SSL = True -IDP_META_URL = "https://idp.hs-hannover.de/idp/shibboleth" # test is "https://idp-test.it.hs-hannover.de/idp/shibboleth" +IDP_META_URL = None # e.g. "https://idp-test.hs-hannover.de/idp/shibboleth" SP_KEY = "{project_settings}/cert/sp.key" SP_CERT = "{project_settings}/cert/sp.pem" +SSO_REQUIRED_IN_DEBUG = False +SSO_REQUIRED_IN_PRODUCTION = False # disabled because of e.g. collectstatic on the static server +SSO_REQUIRED_OUTSIDE_MANAGE_PY = True # enabled to ensure that production (that uses WSGI) has SSO + """ Settings you might want to change on development (don't change them for production): """ -# development helpers -SSO_DISABLED = False SP_FORCE_ENTITY_ID = None # do NOT set for production, set to some unique string on development @@ -36,11 +38,11 @@ SP_FORCE_ENTITY_ID = None # do NOT set for production, set to some unique strin Settings you DON'T want to change (in fact, you want to avoid even thinking about them): """ -SP_METADATA_LIFETIME_DAYS = 365 * 20 - # if you really really need to add/modify something in OneLogin settings, add it to ONELOGIN_OVERRIDES ONELOGIN_OVERRIDES = {} # e.g.: ONELOGIN_OVERRIDES = { "strict": False } +SP_METADATA_LIFETIME_DAYS = 365 * 20 + PROJECT_NAME = os.environ.get('DJANGO_SETTINGS_MODULE').split('.')[0] SU_GROUP_NAME = "{0}_superusers".format(PROJECT_NAME) @@ -50,7 +52,7 @@ STAFF_PERM_NAME = "staff" """ -Not settings... just... Eww. +Not really settings... """ SP_CONTACTS = { diff --git a/ssoauth/apps.py b/ssoauth/apps.py index d78bff087ce0a5d10cec4a776dcddbfdf83f15dd..29d6e6ad3d334a547080ce170ae79dcf59705c6d 100644 --- a/ssoauth/apps.py +++ b/ssoauth/apps.py @@ -2,6 +2,7 @@ from django.apps import AppConfig from django.core import management from django.db.models.signals import post_migrate from django import conf +import sys from . import logger from . import sso_utils from . import app_settings @@ -13,14 +14,14 @@ class SSOAuthConfig(AppConfig): def ready(self, *args, **kwargs): super().ready(*args, **kwargs) # OneLogin settings stuff - if app_settings.SSO_DISABLED: - assert conf.settings.DEBUG - logger.debug("SSO is disabled.") - else: - try: - app_settings.ONELOGIN_SETTINGS = sso_utils.create_onelogin_settings(app_settings.ONELOGIN_SETTINGS_TEMPLATE) - except Exception as e: - raise RuntimeError("SSO failed to start. {ec}: {e}".format(ec=e.__class__.__name__, e=str(e),)) + try: + app_settings.ONELOGIN_SETTINGS_OBJECT = sso_utils.create_onelogin_settings() + except Exception as e: + msg = "SSO failed to start. {ec}: {e}".format(ec=e.__class__.__name__, e=str(e),) + if app_settings.SSO_REQUIRED: + raise RuntimeError(msg) + else: + logger.warning(msg) # default groups post_migrate.connect(self.post_migrate_callback, sender=self) diff --git a/ssoauth/auth_utils.py b/ssoauth/auth_utils.py index c6bd5c89ae103961ee82d538f3e8102ebb4d4d74..98b4f7920e01c7733f207a51f93491f663469a67 100644 --- a/ssoauth/auth_utils.py +++ b/ssoauth/auth_utils.py @@ -7,7 +7,6 @@ from . import models from . import logger from . import app_settings import functools -import re def _validate_username(username): @@ -39,7 +38,7 @@ def get_or_create_user(uuid, username): user = get_user(uuid=uuid) if user.username != username: # have to do it because users might be renamed - logger.warning("Usernames mismatch. Changing from {0} to {1}.".format(user.username, username)) + logger.warning("Username has changed. Renaming locally from {0} to {1}.".format(user.username, username)) user.username = username user.save() return user @@ -125,11 +124,11 @@ def set_user_compat_flags(user): group_su = Group.objects.get(name=app_settings.SU_GROUP_NAME) group_staff = Group.objects.get(name=app_settings.STAFF_GROUP_NAME) if group_su in user.groups.all(): - logger.info("User {user} is superuser.".format(**locals())) + logger.info("User {user} is superuser".format(**locals())) user.is_staff = True user.is_superuser = True if group_staff in user.groups.all(): - logger.info("User {user} has admin access.".format(**locals())) + logger.info("User {user} is staff (has admin access)".format(**locals())) user.is_staff = True except Group.DoesNotExist: logger.error("No compat groups. Migrate or run management command create_compat_groups.") diff --git a/ssoauth/checks.py b/ssoauth/checks.py index d7155dee6fe7b006844964a1b339a909cb929b46..991d90c5e08026b39ab7e2db908507ac97213db4 100644 --- a/ssoauth/checks.py +++ b/ssoauth/checks.py @@ -50,9 +50,39 @@ def compatible_user_model(app_configs, **kwargs): return errors +@register(Tags.compatibility) +def host_without_protocol(app_configs, **kwargs): + errors = list() + if app_settings.SP_HOST is not None: + if app_settings.SP_HOST.lower().startswith(("http:", "https:",)): + errors.append(Error("Bad setting SP_HOST. Need host name without protocol and port.")) + return errors + + +@register(Tags.compatibility) +def old_settings(app_configs, **kwargs): + def settings_have(key): + try: + getattr(conf.settings, key) + return True + except AttributeError: + return False + errors = list() + if settings_have("SSO_DISABLED") or settings_have("SSOAUTH_SSO_DISABLED"): + errors.append(Warning("SSO_DISABLED is deprecated.")) + if settings_have("IDP_IGNORE") or settings_have("SSOAUTH_IDP_IGNORE"): + errors.append(Warning("IDP_IGNORE is deprecated.")) + if errors: + errors.append(Warning("Please check the README. Alternatively simply remove *all* ssoauth settings from your project settings, " + "you don't need them anymore as long as you don't need a working SSO.")) + return errors + + @register(Tags.urls) def auth_urls_configured(app_configs, **kwargs): errors = list() + if not app_settings.SSO_REQUIRED: + return errors def _url_from_setting(setting_name): # get url based on django setting name, handles most of cases of something going wrong @@ -61,7 +91,7 @@ def auth_urls_configured(app_configs, **kwargs): assert urls.resolve(setting_value) # page exists return setting_value except Exception as e: - errors.append(Warning("{ec}: {e}".format(ec=e.__class__.__name__, e=str(e)), obj=conf.settings,)) + 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 # login diff --git a/ssoauth/management/commands/group_mapping.py b/ssoauth/management/commands/group_mapping.py index 5dcb6b014520cfb2aad773f74e44e152d0cfafed..42375fae9cd71f5ce74e57e4939c61be8a7ef83e 100644 --- a/ssoauth/management/commands/group_mapping.py +++ b/ssoauth/management/commands/group_mapping.py @@ -94,8 +94,9 @@ class Command(BaseCommand): return None - # raise CommandError("Could not ensure that compatibility groups and permissions exist. {0}".format(str(e))) - + ## seems like the following code can create groups and permissions as we had it + ## in the old hshauth, based on the project settings + # # @staticmethod # def ensure_group_exists(group_name, permission_names=list()): # """ diff --git a/ssoauth/sso_utils.py b/ssoauth/sso_utils.py index 767947754fb138f9a136fa24d6bf095fe89d048b..d8786b7861a2441ff1197e6844b4bb4e2c2d4cf9 100644 --- a/ssoauth/sso_utils.py +++ b/ssoauth/sso_utils.py @@ -5,6 +5,26 @@ from . import app_settings from django import urls from onelogin.saml2.settings import OneLogin_Saml2_Settings from copy import copy +import os +from django import conf + + +def get_project_settings_path(): + module = os.environ.get(conf.ENVIRONMENT_VARIABLE) + path = os.path.abspath(os.path.join(*module.split("."))) + if not os.path.isdir(path): + # module, not a package + path = os.path.dirname(path) + return path + + +def read_key(path): + path = path.format(project_settings=get_project_settings_path()) + try: + with open(path, "r") as f: + return f.read() + except FileNotFoundError: + raise FileNotFoundError("Could not read the key: {0}".format(path)) def get_idp_runtime_info(meta_url): @@ -53,7 +73,7 @@ def get_idp_runtime_info(meta_url): } -def create_onelogin_settings(template): +def create_onelogin_settings(template=app_settings.ONELOGIN_SETTINGS_TEMPLATE): """ This function is intended to be run only once, on app startup. Raises exceptions. """ # get the template template = copy(template) @@ -62,15 +82,19 @@ def create_onelogin_settings(template): port_suffix = "" if app_settings.SP_PORT in [80, 443] else ":{0}".format(app_settings.SP_PORT) host = app_settings.SP_HOST host_full = "{protocol}://{host}{port_suffix}".format(**locals()) - # SP settings - template["sp"]["entityId"] = app_settings.SP_FORCE_ENTITY_ID or (host_full + urls.reverse("sso-saml2-meta")) - template["sp"]["assertionConsumerService"]["url"] = host_full + urls.reverse("sso-saml2-acs") # IDP settings + assert app_settings.IDP_META_URL, "IDP_META_URL is not specified" # before get_idp_runtime_info starts logging errors idp_info = get_idp_runtime_info(app_settings.IDP_META_URL) template["idp"]["x509certMulti"]["signing"] = idp_info["certificates"]["signing"] template["idp"]["x509certMulti"]["encryption"] = idp_info["certificates"]["encryption"] template["idp"]["singleSignOnService"]["url"] = idp_info["bindings"]["sso_redirect"] template["idp"]["singleLogoutService"]["url"] = idp_info["bindings"]["slo_redirect"] + # SP settings + template["sp"]["entityId"] = app_settings.SP_FORCE_ENTITY_ID or (host_full + urls.reverse("sso-saml2-meta")) + template["sp"]["assertionConsumerService"]["url"] = host_full + urls.reverse("sso-saml2-acs") + template["sp"]["singleLogoutService"]["url"] = host_full + urls.reverse("sso-saml2-sls") + template["sp"]["x509cert"] = read_key(app_settings.SP_CERT) + template["sp"]["privateKey"] = read_key(app_settings.SP_KEY) # done return OneLogin_Saml2_Settings(settings=template, sp_validation_only=True) diff --git a/ssoauth/templates/ssoauth/dev.html b/ssoauth/templates/ssoauth/dev.html index bbf893791b113517e9d2466579c805a3662d2435..cddbd57d82b7999382060e54963d9cf10f4c1c66 100644 --- a/ssoauth/templates/ssoauth/dev.html +++ b/ssoauth/templates/ssoauth/dev.html @@ -8,6 +8,9 @@ <style> .hidden {display: none !important;} .wrapper>.container { padding-bottom: 2rem; padding-top: 2rem; } + .disabled {opacity: 0.60; filter: grayscale(100%); pointer-events: none !important;} + .button-narrow {padding-left: 1em; padding-right: 1em;} + .message {padding: 0.2em 0.5em; border: 1px solid rgba(127,127,127,0.5); border-radius: 4px; background-color: rgba(127,127,127,0.05); display: inline-block;} </style> </head> <body> @@ -17,6 +20,8 @@ <h2 class="title">Actions</h2> <form action="" method="post"> {% csrf_token %} + <input type="submit" class="hidden"/> {# as chromium actually works #} + <button type="submit" class="hidden"></button> {# according to html5 specs #} <div class="row"> <div class="column"> <h4 class="title">Log In</h4> @@ -25,7 +30,7 @@ </div> <div class="column"> <h4 class="title">Toggle Groups</h4> - <select name="toggle_group" class="disabled" id="id_toggle_group" onchange="this.form.submit()"> + <select name="toggle_group" id="id_toggle_group" onchange="this.form.submit()"> {% for value, name in form.fields.toggle_group.choices %} <option value="{{ value }}">{% if value %}{{ name }}{% endif %}</option> {% endfor %} @@ -33,14 +38,21 @@ <p><i>Add or remove groups for the logged in user</i></p> </div> <div class="column"> + <h4 class="title">Extras</h4> + <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 %}"> <h4 class="title">Production Actions</h4> - <a class="button button-outline button-black" href="{% url "sso-login" %}?next={% url "sso-dev" %}">SSO Log in</a> - <a class="button button-outline button-black" href="{% url "sso-logout" %}?next={% url "sso-dev" %}">Log out</a> - <p><i>These actions are used in production</i></p> + <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" %}?next={% url "sso-dev" %}">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> </div> </div> - <input class="hidden" type="submit"> </form> + {% if next_url %}<p class="message">You will be redirected to: <strong>{{ next_url }}</strong></p>{% endif %} + {% if user.is_superuser %}<p class="message">You are a superuser</p>{% endif %} </section> {% for table_name, table_contents in tables %} diff --git a/ssoauth/urls.py b/ssoauth/urls.py index f63ebf522051768d3a59e94a9e3662644d6fecad..9048579403cd3bebca9135b4b7b8fca6d4826a27 100644 --- a/ssoauth/urls.py +++ b/ssoauth/urls.py @@ -5,7 +5,8 @@ urlpatterns = ( url(r"^login/?$", views.LogInView.as_view(), name="sso-login"), url(r"^logout/?$", views.LogOutView.as_view(), name="sso-logout"), url(r"^saml2/acs/?$", views.ACSAuthNView.as_view(), name="sso-saml2-acs"), - url(r"^saml2/meta/?$", views.MetadataView.as_view(), name="sso-saml2-meta"), + url(r"^saml2/sls/?$", views.SLSView.as_view(), name="sso-saml2-sls"), + url(r"^saml2/meta(?:data)?/?$", views.MetadataView.as_view(), name="sso-saml2-meta"), url(r"^dev/?$", views.DevView.as_view(), name="sso-dev"), ) diff --git a/ssoauth/views.py b/ssoauth/views.py index c1006318c1de6f372cd9740273bf2c64e15cb365..07e8adc3d9af36553a4bbf09465b18467bd6f045 100644 --- a/ssoauth/views.py +++ b/ssoauth/views.py @@ -32,7 +32,7 @@ class SAMLMixin: """ Merges Django runtime info and settings for OneLogin toolkit """ def __init__(self, *args, **kwargs): - if not app_settings.ONELOGIN_SETTINGS: + if not app_settings.ONELOGIN_SETTINGS_OBJECT: raise exceptions.ImproperlyConfigured("SSO is not configured.") super().__init__(*args, **kwargs) @@ -52,7 +52,7 @@ class SAMLMixin: def get_onelogin_auth(cls, request): return OneLogin_Saml2_Auth( request_data=cls.get_onelogin_request_data(request), - old_settings=app_settings.ONELOGIN_SETTINGS + old_settings=app_settings.ONELOGIN_SETTINGS_OBJECT ) @@ -148,11 +148,22 @@ class ACSAuthNView(SAMLMixin, View): logger.debug("Logged in {user}".format(**locals())) +@method_decorator(never_cache, "dispatch") +@method_decorator(csrf_exempt, "dispatch") +class SLSView(SAMLMixin, View): + """ + SLS (Single Logout Service) binding. + When this view is opened by a user (usually inside of a frame) this user must be unconditionally logged off. + """ + def get(self): + return None + + class MetadataView(SAMLMixin, View): def get(self, request, *args, **kwargs): - meta = app_settings.ONELOGIN_SETTINGS.get_sp_metadata() - errors = app_settings.ONELOGIN_SETTINGS.validate_metadata(meta) + meta = app_settings.ONELOGIN_SETTINGS_OBJECT.get_sp_metadata() + errors = app_settings.ONELOGIN_SETTINGS_OBJECT.validate_metadata(meta) if errors: for e in errors: logger.error(e) @@ -178,34 +189,51 @@ class DevView(FormView): else: super().__init__(*args, **kwargs) + @property + def next_url(self): + return self.request.GET.get(REDIRECT_FIELD_NAME, None) + def get_context_data(self, **kwargs): assert conf.settings.DEBUG context = super().get_context_data(**kwargs) user = self.request.user groups = list(user.groups.all()) if user.is_authenticated else list() + permissions = list(user.get_all_permissions()) context["tables"] =[ ["User", OrderedDict([ ["user", "{0} ({1})".format(self.request.user.username, self.request.user.__class__.__name__)], ["groups", ", ".join(str(g) for g in groups)], + ["permissions", ", ".join(str(p) for p in permissions)], ])], ["SAML2 Attributes", self.request.session.get("DEBUG_SAML2_ATTRS")], ["Session", dict(self.request.session)], ] + context.update(dict( + sso_configured=bool(app_settings.ONELOGIN_SETTINGS_OBJECT), + next_url=self.next_url, + )) return context def form_valid(self, form): assert conf.settings.DEBUG - username = form.cleaned_data["username"] + log_in_as_username = form.cleaned_data["username"] toggle_group = form.cleaned_data["toggle_group"] - if username: + local_logout = bool(self.request.POST.get("local-logout", None)) + if local_logout: + if log_in_as_username or toggle_group: + log_in_as_username, toggle_group = None, None # single page / single form for everything can cause weird effects + # perform an action + if log_in_as_username: + logger.info("Logging in as {0}".format(log_in_as_username)) try: - user = auth_utils.get_user(username=username) + user = auth_utils.get_user(username=log_in_as_username) except exceptions.ObjectDoesNotExist: import uuid - user = auth_utils.get_or_create_user(username=username, uuid=uuid.uuid4()) + user = auth_utils.get_or_create_user(username=log_in_as_username, uuid=uuid.uuid4()) self.request.user = user contrib_auth.login(request=self.request, user=user) elif toggle_group: + logger.info("Toggling group: {0}".format(toggle_group)) if self.request.user.is_authenticated: if toggle_group in self.request.user.groups.all(): self.request.user.groups.remove(toggle_group) @@ -213,6 +241,12 @@ class DevView(FormView): self.request.user.groups.add(toggle_group) else: logger.warning("Too anonymous to join groups.") - auth_utils.set_user_compat_flags(self.request.user) - return http.HttpResponseRedirect(urls.reverse("sso-dev")) + elif local_logout: + logger.info("Logging out") + contrib_auth.logout(self.request) + # update the compat flags, might be needed when user or their groups change + if self.request.user.is_authenticated: + auth_utils.set_user_compat_flags(self.request.user) + # redirect + return http.HttpResponseRedirect(self.next_url or urls.reverse("sso-dev"))