diff --git a/pandora/app/oidc.py b/pandora/app/oidc.py new file mode 100644 index 00000000..7739e1ba --- /dev/null +++ b/pandora/app/oidc.py @@ -0,0 +1,34 @@ +import unicodedata + +from django.contrib.auth import get_user_model + +import mozilla_django_oidc.auth + +from user.utils import prepare_user + +User = get_user_model() + + +class OIDCAuthenticationBackend(mozilla_django_oidc.auth.OIDCAuthenticationBackend): + def create_user(self, claims): + user = super(OIDCAuthenticationBackend, self).create_user(claims) + username = claims.get("preferred_username") + n = 1 + if username and username != user.username: + uname = username + while User.objects.filter(username=uname).exclude(id=user.id).exists(): + n += 1 + uname = '%s (%s)' % (username, n) + user.username = uname + user.save() + prepare_user(user) + return user + + def update_user(self, user, claims): + print("update user", user, claims) + #user.save() + return user + + +def generate_username(email): + return unicodedata.normalize('NFKC', email)[:150] diff --git a/pandora/app/views.py b/pandora/app/views.py index 2abd1f99..734a5305 100644 --- a/pandora/app/views.py +++ b/pandora/app/views.py @@ -184,6 +184,7 @@ def init(request, data): except: pass + config['site']['oidc'] = bool(getattr(settings, 'OIDC_RP_CLIENT_ID', False)) response['data']['site'] = config response['data']['user'] = init_user(request.user, request) request.session['last_init'] = str(datetime.now()) diff --git a/pandora/settings.py b/pandora/settings.py index 084ce8b3..5045c6c9 100644 --- a/pandora/settings.py +++ b/pandora/settings.py @@ -111,6 +111,7 @@ ROOT_URLCONF = 'urls' INSTALLED_APPS = ( 'django.contrib.auth', + 'mozilla_django_oidc', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', @@ -158,6 +159,27 @@ INSTALLED_APPS = ( ) AUTH_USER_MODEL = 'system.User' +AUTH_PROFILE_MODULE = 'user.UserProfile' +AUTH_CHECK_USERNAME = True + +AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', +) + +# OpenID Connect login support +LOGIN_REDIRECT_URL = "/grid" +LOGOUT_REDIRECT_URL = "/grid" +OIDC_USERNAME_ALGO = "app.oidc.generate_username" +OIDC_RP_CLIENT_ID = None + +# define those in local_settings to enable OCID based login +#OIDC_RP_CLIENT_ID = '' +#OIDC_RP_CLIENT_SECRET = '' +#OIDC_RP_SIGN_ALGO = "RS256" +#OIDC_OP_JWKS_ENDPOINT = "" +#OIDC_OP_AUTHORIZATION_ENDPOINT = "" +#OIDC_OP_TOKEN_ENDPOINT = "" +#OIDC_OP_USER_ENDPOINT = "" # Log errors into db LOGGING = { @@ -193,8 +215,6 @@ CACHES = { DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" -AUTH_PROFILE_MODULE = 'user.UserProfile' -AUTH_CHECK_USERNAME = True FFMPEG = 'ffmpeg' FFPROBE = 'ffprobe' USE_VP9 = True @@ -323,3 +343,7 @@ except NameError: INSTALLED_APPS = tuple(list(INSTALLED_APPS) + LOCAL_APPS) +if OIDC_RP_CLIENT_ID: + AUTHENTICATION_BACKENDS = list(AUTHENTICATION_BACKENDS) + [ + 'app.oidc.OIDCAuthenticationBackend' + ] diff --git a/pandora/urls.py b/pandora/urls.py index 36af1aa0..36e9e8f3 100644 --- a/pandora/urls.py +++ b/pandora/urls.py @@ -2,7 +2,7 @@ import os import importlib -from django.urls import path, re_path +from django.urls import path, re_path, include from oxdjango.http import HttpFileResponse from django.conf import settings @@ -36,6 +36,8 @@ def serve_static_file(path, location, content_type): urlpatterns = [ #path('admin/', admin.site.urls), + path('oidc/', include('mozilla_django_oidc.urls')), + re_path(r'^api/locale.(?P.*).json$', translation.views.locale_json), re_path(r'^api/upload/text/?$', text.views.upload), re_path(r'^api/upload/document/?$', document.views.upload), diff --git a/requirements.txt b/requirements.txt index b1ef09f9..3eabf484 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,3 +20,4 @@ future pytz pypdfium2 Pillow>=10 +mozilla-django-oidc==4.0.1 diff --git a/static/js/account.js b/static/js/account.js index e213330f..05b05321 100644 --- a/static/js/account.js +++ b/static/js/account.js @@ -337,7 +337,11 @@ pandora.ui.accountSignoutDialog = function() { that.close(); pandora.UI.set({page: ''}); pandora.api.signout({}, function(result) { - pandora.signout(result.data); + if (pandora.site.site.oidc) { + pandora.oidcLogout(); + } else { + pandora.signout(result.data); + } }); } }) diff --git a/static/js/appPanel.js b/static/js/appPanel.js index 5ca52e00..a2442d43 100644 --- a/static/js/appPanel.js +++ b/static/js/appPanel.js @@ -100,6 +100,10 @@ pandora.ui.appPanel = function() { pandora.$ui.siteDialog = pandora.ui.siteDialog(page).open(); } } else if (['signup', 'signin'].indexOf(page) > -1) { + if (pandora.site.site.oidc) { + pandora.oidcLogin() + return + } if (pandora.user.level == 'guest') { if (pandora.$ui.accountDialog && pandora.$ui.accountDialog.is(':visible')) { pandora.$ui.accountDialog.options(pandora.ui.accountDialogOptions(page)); diff --git a/static/js/folders.js b/static/js/folders.js index c23feedf..3d54aee9 100644 --- a/static/js/folders.js +++ b/static/js/folders.js @@ -424,26 +424,35 @@ pandora.ui.folders = function(section) { }).bindEvent({ click: function() { var $dialog = pandora.ui.iconDialog({ - buttons: title != Ox._('Featured ' + folderItems) ? [ - Ox.Button({title: Ox._('Sign Up...')}).bindEvent({ - click: function() { - $dialog.close(); - pandora.$ui.accountDialog = pandora.ui.accountDialog('signup').open(); - } - }), - Ox.Button({title: Ox._('Sign In...')}).bindEvent({ - click: function() { - $dialog.close(); - pandora.$ui.accountDialog = pandora.ui.accountDialog('signin').open(); - } - }), - {}, - Ox.Button({title: Ox._('Not Now')}).bindEvent({ - click: function() { - $dialog.close(); - } - }) - ] : [ + buttons: title != Ox._('Featured ' + folderItems) ? [].concat( + pandora.site.site.oidc ? [] + : [ + Ox.Button({title: Ox._('Sign Up...')}).bindEvent({ + click: function() { + $dialog.close(); + pandora.$ui.accountDialog = pandora.ui.accountDialog('signup').open(); + } + }) + ], + [ + Ox.Button({title: Ox._('Sign In...')}).bindEvent({ + click: function() { + $dialog.close(); + if (pandora.site.site.oidc) { + pandora.oidcLogin() + } else { + pandora.$ui.accountDialog = pandora.ui.accountDialog('signin').open(); + } + } + }), + {}, + Ox.Button({title: Ox._('Not Now')}).bindEvent({ + click: function() { + $dialog.close(); + } + }) + ] + ): [ Ox.Button({title: Ox._('Close')}).bindEvent({ click: function() { $dialog.close(); diff --git a/static/js/home.js b/static/js/home.js index 76162e7a..56649cf9 100644 --- a/static/js/home.js +++ b/static/js/home.js @@ -171,13 +171,13 @@ pandora.ui.home = function() { }), $signinButton = Ox.Button({ title: Ox._('Sign In'), - width: 74 + width: pandora.site.site.oidc ? 156 : 74 }) .css({ position: 'absolute', left: 0, top: '112px', - right: '82px', + right: pandora.site.site.oidc ? '164px' : '82px', bottom: 0, margin: 'auto', opacity: 0 @@ -248,7 +248,13 @@ pandora.ui.home = function() { adjustRatio(); if (pandora.user.level == 'guest') { - $signupButton.appendTo(that); + if (pandora.site.site.oidc) { + $signinButton.options({ + width: 156 + }) + } else { + $signupButton.appendTo(that); + } $signinButton.appendTo(that); } else { $preferencesButton.appendTo(that); diff --git a/static/js/mainMenu.js b/static/js/mainMenu.js index 2efc217f..d3fdf493 100644 --- a/static/js/mainMenu.js +++ b/static/js/mainMenu.js @@ -46,7 +46,9 @@ pandora.ui.mainMenu = function() { { id: 'tasks', title: Ox._('Tasks...'), disabled: isGuest }, { id: 'archives', title: Ox._('Archives...'), disabled: /*isGuest*/ true }, {}, - { id: 'signup', title: Ox._('Sign Up...'), disabled: !isGuest }, + !pandora.site.site.oidc + ? { id: 'signup', title: Ox._('Sign Up...'), disabled: !isGuest } + : [], isGuest ? { id: 'signin', title: Ox._('Sign In...')} : { id: 'signout', title: Ox._('Sign Out...')} ] }, diff --git a/static/js/utils.js b/static/js/utils.js index 7bf3fa5e..874e5d35 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -2650,6 +2650,20 @@ pandora.logEvent = function(data, event, element) { } }; +pandora.oidcLogin = function() { + Ox.LoadingScreen().css({zIndex: 100}).addClass('OxScreen').appendTo(document.body).show().start() + document.location.href = '/oidc/authenticate/'; +}; + +pandora.oidcLogout = function() { + Ox.LoadingScreen().css({zIndex: 100}).addClass('OxScreen').appendTo(document.body).show().start() + const form = document.createElement("form"); + form.setAttribute("method", "post"); + form.setAttribute("action", "/oidc/logout/"); + document.body.appendChild(form); + form.submit(); +}; + pandora.openLicenseDialog = function() { if (!Ox.Focus.focusedElementIsInput() && !pandora.hasDialogOrScreen()) { pandora.ui.licenseDialog().open().bindEvent({ diff --git a/update.py b/update.py index f6b514b5..d414b97b 100755 --- a/update.py +++ b/update.py @@ -302,6 +302,8 @@ if __name__ == "__main__": if old <= 6581: run('./bin/pip', 'install', '-U', 'pip') run('./bin/pip', 'install', '-r', 'requirements.txt') + if old <= 6659: + run('./bin/pip', 'install', '-r', 'requirements.txt') else: if len(sys.argv) == 1: branch = get_branch()