Compare commits

..

4 commits

Author SHA1 Message Date
j
34af2b1fab add ocid based login 2024-11-08 12:29:35 +00:00
j
d83309d4cd ff 2024-11-07 16:28:18 +00:00
j
59c2045ac6 move first signup code into function for reuse 2024-11-07 15:57:16 +00:00
j
a24d96b098 log invalid api requests 2024-11-07 14:28:28 +00:00
15 changed files with 174 additions and 50 deletions

34
pandora/app/oidc.py Normal file
View file

@ -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]

View file

@ -184,6 +184,7 @@ def init(request, data):
except: except:
pass pass
config['site']['oidc'] = bool(getattr(settings, 'OIDC_RP_CLIENT_ID', False))
response['data']['site'] = config response['data']['site'] = config
response['data']['user'] = init_user(request.user, request) response['data']['user'] = init_user(request.user, request)
request.session['last_init'] = str(datetime.now()) request.session['last_init'] = str(datetime.now())

View file

@ -34,6 +34,13 @@ def api(request):
return response return response
if request.META.get('CONTENT_TYPE') == 'application/json': if request.META.get('CONTENT_TYPE') == 'application/json':
r = json.loads(request.body.decode('utf-8')) r = json.loads(request.body.decode('utf-8'))
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'] action = r['action']
data = r.get('data', {}) data = r.get('data', {})
else: else:

View file

@ -111,6 +111,7 @@ ROOT_URLCONF = 'urls'
INSTALLED_APPS = ( INSTALLED_APPS = (
'django.contrib.auth', 'django.contrib.auth',
'mozilla_django_oidc',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.sites', 'django.contrib.sites',
@ -158,6 +159,27 @@ INSTALLED_APPS = (
) )
AUTH_USER_MODEL = 'system.User' 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 = '<client id>'
#OIDC_RP_CLIENT_SECRET = '<client secret>'
#OIDC_RP_SIGN_ALGO = "RS256"
#OIDC_OP_JWKS_ENDPOINT = "<jwks endpoint>"
#OIDC_OP_AUTHORIZATION_ENDPOINT = "<authorization endpoint>"
#OIDC_OP_TOKEN_ENDPOINT = "<token endpoint>"
#OIDC_OP_USER_ENDPOINT = "<user endpoint>"
# Log errors into db # Log errors into db
LOGGING = { LOGGING = {
@ -193,8 +215,6 @@ CACHES = {
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
AUTH_PROFILE_MODULE = 'user.UserProfile'
AUTH_CHECK_USERNAME = True
FFMPEG = 'ffmpeg' FFMPEG = 'ffmpeg'
FFPROBE = 'ffprobe' FFPROBE = 'ffprobe'
USE_VP9 = True USE_VP9 = True
@ -323,3 +343,7 @@ except NameError:
INSTALLED_APPS = tuple(list(INSTALLED_APPS) + LOCAL_APPS) INSTALLED_APPS = tuple(list(INSTALLED_APPS) + LOCAL_APPS)
if OIDC_RP_CLIENT_ID:
AUTHENTICATION_BACKENDS = list(AUTHENTICATION_BACKENDS) + [
'app.oidc.OIDCAuthenticationBackend'
]

View file

@ -2,7 +2,7 @@
import os import os
import importlib import importlib
from django.urls import path, re_path from django.urls import path, re_path, include
from oxdjango.http import HttpFileResponse from oxdjango.http import HttpFileResponse
from django.conf import settings from django.conf import settings
@ -36,6 +36,8 @@ def serve_static_file(path, location, content_type):
urlpatterns = [ urlpatterns = [
#path('admin/', admin.site.urls), #path('admin/', admin.site.urls),
path('oidc/', include('mozilla_django_oidc.urls')),
re_path(r'^api/locale.(?P<lang>.*).json$', translation.views.locale_json), re_path(r'^api/locale.(?P<lang>.*).json$', translation.views.locale_json),
re_path(r'^api/upload/text/?$', text.views.upload), re_path(r'^api/upload/text/?$', text.views.upload),
re_path(r'^api/upload/document/?$', document.views.upload), re_path(r'^api/upload/document/?$', document.views.upload),

View file

@ -3,6 +3,38 @@ from django.contrib.gis.geoip2 import GeoIP2, GeoIP2Exception
import ox 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): def get_ip(request):
if 'HTTP_X_FORWARDED_FOR' in request.META: if 'HTTP_X_FORWARDED_FOR' in request.META:
ip = request.META['HTTP_X_FORWARDED_FOR'].split(',')[0] ip = request.META['HTTP_X_FORWARDED_FOR'].split(',')[0]

View file

@ -28,7 +28,7 @@ from user.models import Group
from . import models from . import models
from .decorators import capability_required_json from .decorators import capability_required_json
from .utils import rename_user from .utils import rename_user, prepare_user
User = get_user_model() User = get_user_model()
@ -177,28 +177,10 @@ def signup(request, data):
} }
}) })
else: else:
first_user = User.objects.count() == 0
user = User(username=data['username'], email=data['email']) user = User(username=data['username'], email=data['email'])
user.set_password(data['password']) user.set_password(data['password'])
#make first user admin
user.is_superuser = first_user
user.is_staff = first_user
user.save() user.save()
#create default user lists: prepare_user(user)
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()
if request.session.session_key: if request.session.session_key:
models.SessionData.objects.filter(session_key=request.session.session_key).update(user=user) models.SessionData.objects.filter(session_key=request.session.session_key).update(user=user)
ui = json.loads(request.session.get('ui', 'null')) ui = json.loads(request.session.get('ui', 'null'))

View file

@ -20,3 +20,4 @@ future
pytz pytz
pypdfium2 pypdfium2
Pillow>=10 Pillow>=10
mozilla-django-oidc==4.0.1

View file

@ -337,7 +337,11 @@ pandora.ui.accountSignoutDialog = function() {
that.close(); that.close();
pandora.UI.set({page: ''}); pandora.UI.set({page: ''});
pandora.api.signout({}, function(result) { pandora.api.signout({}, function(result) {
if (pandora.site.site.oidc) {
pandora.oidcLogout();
} else {
pandora.signout(result.data); pandora.signout(result.data);
}
}); });
} }
}) })

View file

@ -100,6 +100,10 @@ pandora.ui.appPanel = function() {
pandora.$ui.siteDialog = pandora.ui.siteDialog(page).open(); pandora.$ui.siteDialog = pandora.ui.siteDialog(page).open();
} }
} else if (['signup', 'signin'].indexOf(page) > -1) { } else if (['signup', 'signin'].indexOf(page) > -1) {
if (pandora.site.site.oidc) {
pandora.oidcLogin()
return
}
if (pandora.user.level == 'guest') { if (pandora.user.level == 'guest') {
if (pandora.$ui.accountDialog && pandora.$ui.accountDialog.is(':visible')) { if (pandora.$ui.accountDialog && pandora.$ui.accountDialog.is(':visible')) {
pandora.$ui.accountDialog.options(pandora.ui.accountDialogOptions(page)); pandora.$ui.accountDialog.options(pandora.ui.accountDialogOptions(page));

View file

@ -424,18 +424,26 @@ pandora.ui.folders = function(section) {
}).bindEvent({ }).bindEvent({
click: function() { click: function() {
var $dialog = pandora.ui.iconDialog({ var $dialog = pandora.ui.iconDialog({
buttons: title != Ox._('Featured ' + folderItems) ? [ buttons: title != Ox._('Featured ' + folderItems) ? [].concat(
pandora.site.site.oidc ? []
: [
Ox.Button({title: Ox._('Sign Up...')}).bindEvent({ Ox.Button({title: Ox._('Sign Up...')}).bindEvent({
click: function() { click: function() {
$dialog.close(); $dialog.close();
pandora.$ui.accountDialog = pandora.ui.accountDialog('signup').open(); pandora.$ui.accountDialog = pandora.ui.accountDialog('signup').open();
} }
}), })
],
[
Ox.Button({title: Ox._('Sign In...')}).bindEvent({ Ox.Button({title: Ox._('Sign In...')}).bindEvent({
click: function() { click: function() {
$dialog.close(); $dialog.close();
if (pandora.site.site.oidc) {
pandora.oidcLogin()
} else {
pandora.$ui.accountDialog = pandora.ui.accountDialog('signin').open(); pandora.$ui.accountDialog = pandora.ui.accountDialog('signin').open();
} }
}
}), }),
{}, {},
Ox.Button({title: Ox._('Not Now')}).bindEvent({ Ox.Button({title: Ox._('Not Now')}).bindEvent({
@ -443,7 +451,8 @@ pandora.ui.folders = function(section) {
$dialog.close(); $dialog.close();
} }
}) })
] : [ ]
): [
Ox.Button({title: Ox._('Close')}).bindEvent({ Ox.Button({title: Ox._('Close')}).bindEvent({
click: function() { click: function() {
$dialog.close(); $dialog.close();

View file

@ -171,13 +171,13 @@ pandora.ui.home = function() {
}), }),
$signinButton = Ox.Button({ $signinButton = Ox.Button({
title: Ox._('Sign In'), title: Ox._('Sign In'),
width: 74 width: pandora.site.site.oidc ? 156 : 74
}) })
.css({ .css({
position: 'absolute', position: 'absolute',
left: 0, left: 0,
top: '112px', top: '112px',
right: '82px', right: pandora.site.site.oidc ? '164px' : '82px',
bottom: 0, bottom: 0,
margin: 'auto', margin: 'auto',
opacity: 0 opacity: 0
@ -248,7 +248,13 @@ pandora.ui.home = function() {
adjustRatio(); adjustRatio();
if (pandora.user.level == 'guest') { if (pandora.user.level == 'guest') {
if (pandora.site.site.oidc) {
$signinButton.options({
width: 156
})
} else {
$signupButton.appendTo(that); $signupButton.appendTo(that);
}
$signinButton.appendTo(that); $signinButton.appendTo(that);
} else { } else {
$preferencesButton.appendTo(that); $preferencesButton.appendTo(that);

View file

@ -46,7 +46,9 @@ pandora.ui.mainMenu = function() {
{ id: 'tasks', title: Ox._('Tasks...'), disabled: isGuest }, { id: 'tasks', title: Ox._('Tasks...'), disabled: isGuest },
{ id: 'archives', title: Ox._('Archives...'), disabled: /*isGuest*/ true }, { 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...')} isGuest ? { id: 'signin', title: Ox._('Sign In...')}
: { id: 'signout', title: Ox._('Sign Out...')} : { id: 'signout', title: Ox._('Sign Out...')}
] }, ] },

View file

@ -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() { pandora.openLicenseDialog = function() {
if (!Ox.Focus.focusedElementIsInput() && !pandora.hasDialogOrScreen()) { if (!Ox.Focus.focusedElementIsInput() && !pandora.hasDialogOrScreen()) {
pandora.ui.licenseDialog().open().bindEvent({ pandora.ui.licenseDialog().open().bindEvent({

View file

@ -302,6 +302,8 @@ if __name__ == "__main__":
if old <= 6581: if old <= 6581:
run('./bin/pip', 'install', '-U', 'pip') run('./bin/pip', 'install', '-U', 'pip')
run('./bin/pip', 'install', '-r', 'requirements.txt') run('./bin/pip', 'install', '-r', 'requirements.txt')
if old <= 6659:
run('./bin/pip', 'install', '-r', 'requirements.txt')
else: else:
if len(sys.argv) == 1: if len(sys.argv) == 1:
branch = get_branch() branch = get_branch()