Gitlab CSE Unil

Commit 8a856559 authored by Julien Furrer's avatar Julien Furrer
Browse files

Added support for Trusted Third Party authentication and authorization

parent 051d7fc1
...@@ -8,4 +8,3 @@ ...@@ -8,4 +8,3 @@
**/*.old.jpg **/*.old.jpg
screen_session_cmd screen_session_cmd
...@@ -122,7 +122,7 @@ class AnObj(models.Model): ...@@ -122,7 +122,7 @@ class AnObj(models.Model):
ordering = ["-id"] ordering = ["-id"]
def __unicode__(self): def __unicode__(self):
return u"{}".format(self.name) return "{}".format(self.name)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" """
......
# coding=utf-8 # coding=utf-8
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf import settings
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http.response import HttpResponseRedirect
from django.shortcuts import render from django.shortcuts import render
from django.core.cache import cache
import sys
from .models import AnObjMembership from .models import AnObjMembership
SHARING_MODE_NONE = 0 SHARING_MODE_NONE = 0
SHARING_MODE_MANUAL = 1 SHARING_MODE_MANUAL = 1
SHARING_MODE_REGKEY = 4 SHARING_MODE_REGKEY = 4
SHARING_MODE_AAIRULES = 8 SHARING_MODE_AAIRULES = 8
SHARING_MODE_MOODLELTI = 16 SHARING_MODE_TTP_MOODLE = 16
SHARING_MODE_TTP_TOTO = 24
SHARING_MODE_TTP_MY_TTP = 32
class PermissionClass(object): class PermissionClass(object):
...@@ -19,6 +26,7 @@ class PermissionClass(object): ...@@ -19,6 +26,7 @@ class PermissionClass(object):
and ``check_revocation``methods and ``check_revocation``methods
""" """
has_interactive_registration = False has_interactive_registration = False
ttp = False
@classmethod @classmethod
def get_interactive_registration_response(cls, request, anobj): def get_interactive_registration_response(cls, request, anobj):
...@@ -158,16 +166,111 @@ class AAIRules(PermissionClass): ...@@ -158,16 +166,111 @@ class AAIRules(PermissionClass):
pass pass
class MoodleLTI(PermissionClass): class ATTP(PermissionClass):
has_interactive_registration = True
ttp = True
ttp_id = 'moodle'
@classmethod
def set_attp_status(cls, request, anobj, status):
key = "attp_{ttp_id}{user_id}{uuid}".format(
ttp_id=cls.ttp_id, user_id=request.user.id,
uuid=anobj.uuid[:12]
)
cache.set(key, status, settings.ATTP['OPTIONS']['CACHE_TIMEOUT'])
return status
@classmethod
def get_attp_status(cls, request, anobj):
key = "attp_{ttp_id}{user_id}{uuid}".format(
ttp_id=cls.ttp_id, user_id=request.user.id,
uuid=anobj.uuid[:12]
)
status = cache.get(key)
return status
@classmethod
def clear_attp_status(cls, request, anobj):
# key = "attp_{ttp_id}{user_id}{uuid}".format(
# ttp_id=cls.ttp_id, user_id=request.user.id,
# uuid=anobj.uuid[:12]
# )
session_key = "anobj_{}".format(anobj.uuid[:12])
try:
del(request.session[session_key])
except KeyError:
pass
@classmethod
def check_registration(cls, request, anobj):
pass
@classmethod
def check_revocation(cls, request, anobj):
pass pass
@classmethod
def check_permission(cls, request, anobj):
perm_status = cls.get_attp_status(request, anobj)
if perm_status is None:
raise PermissionDenied()
elif perm_status == 'denied':
if not anobj.locked:
# Revoke only if anobj is not locked
cls._revoke_user(request.user, anobj)
cls.clear_attp_status(request, anobj)
raise PermissionDenied()
else:
if request.user not in anobj.members.all():
cls._register_user(request.user, anobj)
# Check ownership
owners = anobj.owners.all()
if request.user in owners:
if perm_status != 'owner' and len(owners) > 1:
# is owner, but shouldn't -> remove only if not last one
anobj.owners.remove(request.user)
else:
if perm_status == 'owner':
# is not owner, but should -> add
anobj.owners.add(request.user)
# cls.clear_attp_status(request, anobj)
return True
@classmethod
def get_interactive_registration_response(cls, request, anobj):
if cls.get_attp_status(request, anobj) is None:
check_url = settings.ATTP.get(cls.ttp_id, {}).get('CHECK_URL')
return HttpResponseRedirect(check_url.format(uuid=anobj.uuid))
else:
cls.clear_attp_status(request, anobj)
raise PermissionDenied()
class MoodleTTP(ATTP):
ttp_id = 'moodle'
class TotoTTP(ATTP):
ttp_id = 'toto'
class MyTTP(ATTP):
ttp_id = 'my_ttp'
PERMISSION_CLASSES = { PERMISSION_CLASSES = {
SHARING_MODE_NONE: None, SHARING_MODE_NONE: None,
SHARING_MODE_MANUAL: IsMember, SHARING_MODE_MANUAL: IsMember,
SHARING_MODE_REGKEY: RegistrationKey, SHARING_MODE_REGKEY: RegistrationKey,
SHARING_MODE_AAIRULES: AAIRules, SHARING_MODE_AAIRULES: AAIRules,
SHARING_MODE_MOODLELTI: MoodleLTI SHARING_MODE_TTP_MOODLE: MoodleTTP,
SHARING_MODE_TTP_TOTO: TotoTTP,
SHARING_MODE_TTP_MY_TTP: MyTTP
} }
...@@ -175,6 +278,18 @@ def get_permission_class(sharing_mode): ...@@ -175,6 +278,18 @@ def get_permission_class(sharing_mode):
return PERMISSION_CLASSES.get(sharing_mode) return PERMISSION_CLASSES.get(sharing_mode)
def get_ttp_sharing_mode(ttp_id=''):
"""
Return the sharing mode value for the given ttp_id
:param ttp_id:
:return:
"""
attr_name = 'SHARING_MODE_TTP_{}'.format(ttp_id.upper())
print attr_name
mode = getattr(sys.modules[__name__], attr_name, 0)
return mode
def check_anobj_permission(request, anobj): def check_anobj_permission(request, anobj):
p = get_permission_class(anobj.sharing_mode) p = get_permission_class(anobj.sharing_mode)
if p is not None: if p is not None:
......
...@@ -6,7 +6,7 @@ from rest_framework.routers import DefaultRouter ...@@ -6,7 +6,7 @@ from rest_framework.routers import DefaultRouter
from rest_framework_nested import routers from rest_framework_nested import routers
from rest_framework import urls as rest_framework_urls from rest_framework import urls as rest_framework_urls
from adim.views import AnObjViewSet, SharedAnObjViewSet, AnnotationViewSet, SharedAnnotationViewSet, UserViewSet from adim.views import AnObjViewSet, UAnObjViewSet, SharedAnObjViewSet, AnnotationViewSet, SharedAnnotationViewSet, UserViewSet
# Default router, as wee need the 'root-api' name # Default router, as wee need the 'root-api' name
...@@ -14,6 +14,7 @@ d_router = DefaultRouter() ...@@ -14,6 +14,7 @@ d_router = DefaultRouter()
router = routers.SimpleRouter() router = routers.SimpleRouter()
router.register(r'anobjs', AnObjViewSet, base_name='anobjs') router.register(r'anobjs', AnObjViewSet, base_name='anobjs')
router.register(r'uanobjs', UAnObjViewSet, base_name='uanobjs')
router.register(r'shared/anobjs', SharedAnObjViewSet, base_name='shared-anobjs') router.register(r'shared/anobjs', SharedAnObjViewSet, base_name='shared-anobjs')
router.register(r'annotations', AnnotationViewSet, base_name='annotations') router.register(r'annotations', AnnotationViewSet, base_name='annotations')
router.register(r'users', UserViewSet, base_name='users') router.register(r'users', UserViewSet, base_name='users')
......
...@@ -114,6 +114,13 @@ class AnObjViewSet(viewsets.ModelViewSet): ...@@ -114,6 +114,13 @@ class AnObjViewSet(viewsets.ModelViewSet):
return Response({'publish_mode': membership.publish_mode}) return Response({'publish_mode': membership.publish_mode})
class UAnObjViewSet(AnObjViewSet):
"""
Accessing AnObj by uuid. extents AnObj
"""
lookup_field = 'uuid'
class SharedAnObjViewSet(AnObjViewSet): class SharedAnObjViewSet(AnObjViewSet):
""" """
ViewSet for shared AnObj, requested by a user ViewSet for shared AnObj, requested by a user
......
...@@ -9,3 +9,4 @@ class UploadImageFileForm(forms.Form): ...@@ -9,3 +9,4 @@ class UploadImageFileForm(forms.Form):
for upload for upload
""" """
image_file = forms.ImageField(allow_empty_file=False) image_file = forms.ImageField(allow_empty_file=False)
name = forms.CharField(max_length=125, required=False)
\ No newline at end of file
...@@ -66,6 +66,7 @@ function($){ ...@@ -66,6 +66,7 @@ function($){
onStart: null, onStart: null,
processstart: null, processstart: null,
autoUpload: true, autoUpload: true,
sequentialUploads: false,
add: null add: null
// showAlert: function(errId) {}, // showAlert: function(errId) {},
// clearAlert: function() // clearAlert: function()
...@@ -85,7 +86,8 @@ function($){ ...@@ -85,7 +86,8 @@ function($){
dropZone: params.dropZone, dropZone: params.dropZone,
autoUpload: params.autoUpload, autoUpload: params.autoUpload,
done: function (e, data) { done: function (e, data) {
if (!data.result.error && data.result.next) { if ($fileuploadElem.fileupload('active') === 1 &&
!data.result.error && data.result.next) {
document.location.assign(data.result.next); document.location.assign(data.result.next);
} }
}, },
......
...@@ -35,7 +35,7 @@ define([ ...@@ -35,7 +35,7 @@ define([
SHARING_MODE_MANUAL = 1, SHARING_MODE_MANUAL = 1,
SHARING_MODE_REGKEY = 4, SHARING_MODE_REGKEY = 4,
SHARING_MODE_AAIRULES = 8, SHARING_MODE_AAIRULES = 8,
SHARING_MODE_MOODLELTI = 16; SHARING_MODE_MOODLETTP = 16;
return Backbone.View.extend({ return Backbone.View.extend({
...@@ -200,6 +200,14 @@ define([ ...@@ -200,6 +200,14 @@ define([
// Reset controls state // Reset controls state
this.$("input[name=aom-prop-new-member]").val(""); this.$("input[name=aom-prop-new-member]").val("");
this.$(".aom-shm-ctrl-members-remove-but").attr("disabled", "disabled"); this.$(".aom-shm-ctrl-members-remove-but").attr("disabled", "disabled");
},
'attp_label': function($el, model, opts) {
if (opts.url && opts.label) {
$el.attr("href", opts.url).text(opts.label);
} else {
$el.closest('.aom-prop-shm').addClass('hidden');
}
} }
}; };
......
...@@ -3,7 +3,11 @@ from __future__ import unicode_literals ...@@ -3,7 +3,11 @@ from __future__ import unicode_literals
import json import json
import os import os
from django.contrib.auth.views import redirect_to_login
from django.views.decorators.csrf import csrf_exempt
import ldap import ldap
import logging
from django.conf import settings from django.conf import settings
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
...@@ -11,19 +15,25 @@ from django.core.cache import cache ...@@ -11,19 +15,25 @@ from django.core.cache import cache
from django.db.models import Q from django.db.models import Q
from django.http.response import HttpResponse, HttpResponseBadRequest, HttpResponseRedirect, Http404, \ from django.http.response import HttpResponse, HttpResponseBadRequest, HttpResponseRedirect, Http404, \
HttpResponseForbidden HttpResponseForbidden
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404, resolve_url
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from django.views.decorators.cache import cache_control from django.views.decorators.cache import cache_control
from adim.models import AnObj, AnObjMembership from adim.models import AnObj, AnObjMembership
from adim.permissions import get_permission_class, has_anobj_access, SHARING_MODE_NONE from adim.permissions import get_permission_class, has_anobj_access, get_ttp_sharing_mode, SHARING_MODE_NONE
from adim_ttp.decorators import attp_login
from adim_utils.decorators import clear_function_cache
from .forms import UploadImageFileForm from .forms import UploadImageFileForm
from sendfile import sendfile from sendfile import sendfile
from .utils import add_image_border, create_image_thumbnail from .utils import add_image_border, create_image_thumbnail
logger = logging.getLogger(__name__)
def home(request): def home(request):
""" """
Home page Home page
...@@ -127,7 +137,7 @@ def annotate_new(request): ...@@ -127,7 +137,7 @@ def annotate_new(request):
return render(request, "adim/annotation_new.html", {}) return render(request, "adim/annotation_new.html", {})
@login_required # @login_required
def annotate(request, anobj_uuid=None): def annotate(request, anobj_uuid=None):
""" """
Annotation page Annotation page
...@@ -135,9 +145,9 @@ def annotate(request, anobj_uuid=None): ...@@ -135,9 +145,9 @@ def annotate(request, anobj_uuid=None):
:param anobj_uuid: :param anobj_uuid:
:return: :return:
""" """
context = { # ----- Some preliminary validations
'membership': False if anobj_uuid is None or len(anobj_uuid) < 8:
} raise Http404()
try: try:
anobj = AnObj.objects.select_related('owner').get(uuid__startswith=anobj_uuid) anobj = AnObj.objects.select_related('owner').get(uuid__startswith=anobj_uuid)
...@@ -148,44 +158,61 @@ def annotate(request, anobj_uuid=None): ...@@ -148,44 +158,61 @@ def annotate(request, anobj_uuid=None):
if len(anobj_uuid) < 32: if len(anobj_uuid) < 32:
return HttpResponseRedirect(reverse('adim_app:annotate', kwargs={'anobj_uuid': anobj.uuid})) return HttpResponseRedirect(reverse('adim_app:annotate', kwargs={'anobj_uuid': anobj.uuid}))
# ----- Login check. Not using decorator so we can delegate to Trusted Third Party if needed
permission_class = get_permission_class(anobj.sharing_mode)
if request.user.is_anonymous():
if permission_class and permission_class.ttp:
check_url = settings.ATTP.get(permission_class.ttp_id, {}).get('CHECK_URL')
return HttpResponseRedirect(check_url.format(uuid=anobj.uuid))
else:
return redirect_to_login(resolve_url('adim_app:annotate', anobj_uuid=anobj.uuid))
# ----- Build context
context = {
'membership': False
}
# is_owner = request.user == anobj.owner # is_owner = request.user == anobj.owner
# is_owner = request.user in anobj.owners.all() # is_owner = request.user in anobj.owners.all()
is_owner = anobj.is_owned(request.user.id) is_owner = anobj.is_owned(request.user.id)
context.update({'is_owner': is_owner})
# Detailed check for permissions # ----- Detailed check for permissions
membership = None membership = None
if is_owner: if is_owner and not (permission_class and permission_class.ttp):
# User is owner and anobj is not shared via Trusted Third Party
if anobj.sharing_mode != SHARING_MODE_NONE: if anobj.sharing_mode != SHARING_MODE_NONE:
membership, _ = AnObjMembership.objects.get_or_create(anobj=anobj, user=request.user) membership, _ = AnObjMembership.objects.get_or_create(anobj=anobj, user=request.user)
else: else:
# User is guest # User is guest or owner and anobj shared via TTP
perm_class = get_permission_class(anobj.sharing_mode) if permission_class is None:
if perm_class is None:
# AnObj not shared # AnObj not shared
raise Http404() raise Http404()
# raise PermissionDenied() # raise PermissionDenied()
elif not perm_class.has_permission(request, anobj): elif not permission_class.has_permission(request, anobj):
# AnObj shared but user has no permission # AnObj shared but user has no permission yet
if perm_class.has_interactive_registration: if permission_class.has_interactive_registration:
# Interactive registration exists, call it # Interactive registration exists, call it
return perm_class.get_interactive_registration_response(request, anobj) return permission_class.get_interactive_registration_response(request, anobj)
# No interactive registration for this sharing model, deny access
# No interactive registration
raise PermissionDenied() raise PermissionDenied()
else: else:
# AnObj shared, user registred, go on # AnObj shared, user authorized and registered
pass pass
# TTP permission may have changed ownership
if permission_class.ttp:
clear_function_cache(f='adim.models.annotablesis_owned', args=(anobj, request.user.id))
is_owner = anobj.is_owned(request.user.id)
membership = AnObjMembership.objects.get(anobj=anobj, user=request.user) membership = AnObjMembership.objects.get(anobj=anobj, user=request.user)
# Interactive registration may post credentials, if so redirect to current view with GET method
if request.method == 'POST': if request.method == 'POST':
return HttpResponseRedirect(reverse('adim_app:annotate', kwargs={'anobj_uuid': anobj_uuid})) return HttpResponseRedirect(reverse('adim_app:annotate', kwargs={'anobj_uuid': anobj_uuid}))
context.update({ context.update({
'is_owner': is_owner,
'membership': membership, 'membership': membership,
'anobj': anobj 'anobj': anobj
}) })
...@@ -210,14 +237,14 @@ def annotate(request, anobj_uuid=None): ...@@ -210,14 +237,14 @@ def annotate(request, anobj_uuid=None):
) )
) )
}) })
return render(request, "adim/annotation.html", context) return render(request, "adim/annotation.html", context)
@csrf_exempt
@attp_login
@login_required @login_required
def upload_file(request, anobj_uuid=None): def upload_file(request, anobj_uuid=None):
""" """
-- inspired by: https://github.com/miki725/Django-jQuery-File-Uploader-Integration-demo/blob/master/upload/views.py -- inspired by: https://github.com/miki725/Django-jQuery-File-Uploader-Integration-demo/blob/master/upload/views.py
:param request: :param request:
:return: :return:
...@@ -228,6 +255,8 @@ def upload_file(request, anobj_uuid=None): ...@@ -228,6 +255,8 @@ def upload_file(request, anobj_uuid=None):
response_type = "application/json" response_type = "application/json"
response_data = {} response_data = {}
user = request.user # if request.user.is_authenticated() else moodle_meta.get('user')
form = UploadImageFileForm(request.POST, request.FILES) form = UploadImageFileForm(request.POST, request.FILES)
if form.is_valid(): if form.is_valid():
image_file = request.FILES['image_file'] image_file = request.FILES['image_file']
...@@ -236,24 +265,43 @@ def upload_file(request, anobj_uuid=None): ...@@ -236,24 +265,43 @@ def upload_file(request, anobj_uuid=None):
'error': file_response.get('error'), 'error': file_response.get('error'),
'files': [file_response], 'files': [file_response],
}) })
anobj_name = form.cleaned_data['name']
# Create AnObj # Create AnObj
if not file_response['error']: if not file_response['error']:
anobj = None
if anobj_uuid:
try: try:
anobj = _get_anobj(request, anobj_uuid=anobj_uuid) anobj = _get_anobj(request, anobj_uuid=anobj_uuid)
except Http404:
anobj = None
if anobj:
anobj.image = image_file anobj.image = image_file
if anobj_name:
anobj.name = anobj_name
anobj.save() anobj.save()
else:
except Http404:
anobj = AnObj.objects.create( anobj = AnObj.objects.create(
owner=request.user, owner=user,
name=os.path.splitext(image_file.name)[0], name=anobj_name or os.path.splitext(image_file.name)[0],
image=image_file image=image_file
) )
if hasattr(request, 'attp_message'):
ttp_id = request.attp_message.get('attp_id')
sharing_mode = get_ttp_sharing_mode(ttp_id=ttp_id)
if sharing_mode:
anobj.sharing_mode = sharing_mode
sharing_opts = request.attp_message.get('opts')
if sharing_opts: