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/oxdjango/api/views.py b/pandora/oxdjango/api/views.py index 3a4a2e89..ce7df892 100644 --- a/pandora/oxdjango/api/views.py +++ b/pandora/oxdjango/api/views.py @@ -34,8 +34,15 @@ def api(request): return response if request.META.get('CONTENT_TYPE') == 'application/json': r = json.loads(request.body.decode('utf-8')) - action = r['action'] - data = r.get('data', {}) + if 'action' not in r: + logger.error("invalid api request: %s", r) + response = render_to_json_response(json_response(status=400, + text='Invalid request')) + response['Access-Control-Allow-Origin'] = '*' + return response + else: + action = r['action'] + data = r.get('data', {}) else: action = request.POST['action'] data = json.loads(request.POST.get('data', '{}')) 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/pandora/user/utils.py b/pandora/user/utils.py index db557dfe..9abecaaa 100644 --- a/pandora/user/utils.py +++ b/pandora/user/utils.py @@ -3,6 +3,38 @@ from django.contrib.gis.geoip2 import GeoIP2, GeoIP2Exception import ox +def prepare_user(user): + from django.contrib.auth import get_user_model + from django.conf import settings + from itemlist.models import List, Position + from django.db.models import Max + + User = get_user_model() + + first_user_qs = User.objects.all() + if user.id: + first_user_qs = first_user_qs.exclude(id=user.id) + if first_user_qs.count() == 0: + user.is_superuser = True + user.is_staff = True + user.save() + + for l in settings.CONFIG['personalLists']: + list = List(name=l['title'], user=user) + for key in ('query', 'public', 'featured'): + if key in l: + setattr(list, key, l[key]) + if key == 'query': + for c in list.query['conditions']: + if c['key'] == 'user': + c['value'] = c['value'].format(username=user.username) + list.save() + pos = Position(list=list, section='personal', user=user) + qs = Position.objects.filter(user=user, section='personal') + pos.position = (qs.aggregate(Max('position'))['position__max'] or 0) + 1 + pos.save() + + def get_ip(request): if 'HTTP_X_FORWARDED_FOR' in request.META: ip = request.META['HTTP_X_FORWARDED_FOR'].split(',')[0] diff --git a/pandora/user/views.py b/pandora/user/views.py index a2a678f1..86e31537 100644 --- a/pandora/user/views.py +++ b/pandora/user/views.py @@ -28,7 +28,7 @@ from user.models import Group from . import models from .decorators import capability_required_json -from .utils import rename_user +from .utils import rename_user, prepare_user User = get_user_model() @@ -177,28 +177,10 @@ def signup(request, data): } }) else: - first_user = User.objects.count() == 0 user = User(username=data['username'], email=data['email']) user.set_password(data['password']) - #make first user admin - user.is_superuser = first_user - user.is_staff = first_user user.save() - #create default user lists: - for l in settings.CONFIG['personalLists']: - list = List(name=l['title'], user=user) - for key in ('query', 'public', 'featured'): - if key in l: - setattr(list, key, l[key]) - if key == 'query': - for c in list.query['conditions']: - if c['key'] == 'user': - c['value'] = c['value'].format(username=user.username) - list.save() - pos = Position(list=list, section='personal', user=user) - qs = Position.objects.filter(user=user, section='personal') - pos.position = (qs.aggregate(Max('position'))['position__max'] or 0) + 1 - pos.save() + prepare_user(user) if request.session.session_key: models.SessionData.objects.filter(session_key=request.session.session_key).update(user=user) ui = json.loads(request.session.get('ui', 'null')) 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()