major security overhaul for #420 and #423
authorBryan <btbonval@gmail.com>
Thu, 12 Mar 2015 06:31:15 +0000 (02:31 -0400)
committerBryan <btbonval@gmail.com>
Thu, 12 Mar 2015 06:32:19 +0000 (02:32 -0400)
cleaned a lot of code, centralized security definitions into models, moved security deployment from templates to views, enhanced template consistency for buttons, improved ajax handling for tags, removed redundant and divergent tag operations.

karmaworld/apps/notes/models.py
karmaworld/apps/notes/templatetags/notes.py
karmaworld/apps/notes/views.py
karmaworld/apps/users/models.py
karmaworld/assets/js/note-detail.js
karmaworld/templates/notes/note_base.html

index 9bb0a1cb8b6a6d9ec983449db187ab20f3c92a1d..fe91c3b186c6368bdfefe5535ec99b4176f87ff9 100644 (file)
@@ -6,10 +6,13 @@
     Models for the notes django app.
     Contains only the minimum for handling files and their representation
 """
+import os
+import time
+import urllib
+import logging
 import datetime
 import traceback
-import logging
-from allauth.account.signals import user_logged_in
+
 from django.contrib.auth.models import User
 from django.contrib.sites.models import Site
 from django.core.urlresolvers import reverse
@@ -19,25 +22,28 @@ from django.core.files.storage import default_storage
 from django.db.models import SET_NULL
 from django.db.models.signals import post_save, post_delete, pre_save
 from django.dispatch import receiver
-from karmaworld.apps.users.models import NoteKarmaEvent, GenericKarmaEvent
-from karmaworld.utils.filepicker import encode_fp_policy, sign_fp_policy
-import os
-import time
-import urllib
-
 from django.conf import settings
 from django.core.files import File
 from django.core.files.storage import FileSystemStorage
 from django.db import models
 from django.utils.text import slugify
+
+import markdown
 import django_filepicker
 from taggit.managers import TaggableManager
-import markdown
+from allauth.account.signals import user_logged_in
 
 from karmaworld.apps.notes import sanitizer
+from karmaworld.apps.users.models import UserProfile
+from karmaworld.apps.users.models import NoteKarmaEvent
+from karmaworld.apps.users.models import GenericKarmaEvent
+from karmaworld.apps.notes.search import SearchIndex
 from karmaworld.apps.courses.models import Course
 from karmaworld.apps.licenses.models import License
-from karmaworld.apps.notes.search import SearchIndex
+
+from karmaworld.utils.filepicker import sign_fp_policy
+from karmaworld.utils.filepicker import encode_fp_policy
+
 from karmaworld.settings.manual_unique_together import auto_add_check_unique_together
 
 FILEPICKER_API_KEY = os.environ['FILEPICKER_API_KEY']
@@ -366,6 +372,65 @@ class Note(Document):
         return self.category in Note.EDITABLE_CATEGORIES
 
 
+    def allows_edit_by(self, user, cache={}):
+        """
+        Security policy for editing notes. Hand rolled for now.
+        cache is a dictionary to return some calculated info pass-by-ref.
+          (does nothing, maintained for consistency)
+        Returns True if the user can edit the note.
+        Checks if the user can tag and the note is editable.
+        """
+        # Of course the note must be editable in the first place.
+        # If so, user must be able to tag note to edit it.
+        return self.is_editable() and self.allows_tags_by(user)
+
+    def allows_tags_by(self, user, cache={}):
+        """
+        Security policy for tagging notes. Hand rolled for now.
+        cache is a dictionary to return some calculated info pass-by-ref.
+        Returns True if the user can edit the note.
+        Checks if the user can delete and there are enough points to tag.
+        """
+        # If the user cannot delete the note, the user cannot add tags.
+        if not self.allows_delete_by(user, cache):
+            return False
+        # Staff can edit, no worries about points.
+        if cache.has_key('staff') and cache['staff']:
+            return True
+        # Users must have 20 points to edit tags.
+        if cache.has_key('profile') and not cache.has_key('points'):
+            cache['points'] = cache['profile'].get_points()
+        return cache.has_key('points') and cache['points'] >= 20
+
+    def allows_delete_by(self, user, cache={}):
+        """
+        Security policy for deleting notes. Hand rolled for now.
+        cache is a dictionary to return some calculated info pass-by-ref
+        Returns True if the user can delete the note.
+        Checks if user is admin or owns the note.
+        """
+        # do not allow unauthenticated users access
+        if hasattr(user, 'is_authenticated') and not user.is_authenticated():
+            return False
+        cache['user'] = user
+        # ensure user is a UserProfile by contract.
+        if not hasattr(user, 'has_staff_status'):
+            # assume it's a regular User and try to resolve it
+            user = UserProfile.objects.get(user_id=user.id)
+        cache['profile'] = user
+        # if the user is staff, the user can delete.
+        cache['staff'] = False
+        if user.has_staff_status():
+            cache['staff'] = True
+            return True
+        # apparently some notes are un-owned. so, user is not the owner.
+        if self.user is None:
+            return False
+        # pull user id for this note
+        owner_id = self.user.id
+        # Note owner may edit, others may not.
+        return owner_id == user.get_id()
+
 class NoteMarkdown(models.Model):
     note     = models.OneToOneField(Note, primary_key=True)
     markdown = models.TextField(blank=True, null=True)
index cba5016218b9dbb9789373bed43e47640afe35ea..6d376cd16a6d19e8d9a366c148c4a17c169776ad 100644 (file)
@@ -22,3 +22,17 @@ def ordinal_letter(value):
 def keyvalue(dict, key):
     return dict[key]
 
+
+@register.filter()
+def can_edit(user,note):
+    return note.allows_edit_by(user)
+
+
+@register.filter()
+def can_tag(user,note):
+    return note.allows_tags_by(user)
+
+
+@register.filter()
+def can_del(user,note):
+    return note.allows_delete_by(user)
index 0e380248825c143dad028a0869ff40cce81fc779..4efd0de5219dbed0c499b182e3aa3f61afeec826 100644 (file)
@@ -2,31 +2,42 @@
 # -*- coding:utf8 -*-
 # Copyright (C) 2012  FinalsClub Foundation
 
-import traceback
+import json
 import logging
+import traceback
+
 from django.contrib import messages
 
 from django.core import serializers
 from django.core.exceptions import ValidationError
 from django.forms.formsets import formset_factory
-from karmaworld.apps.courses.forms import CourseForm
-from karmaworld.apps.courses.models import Course
-from karmaworld.apps.notes.search import SearchIndex
-from karmaworld.apps.quizzes.create_quiz import quiz_from_keywords
-from karmaworld.apps.quizzes.forms import KeywordForm
-from karmaworld.apps.quizzes.models import Keyword
-from karmaworld.apps.quizzes.tasks import submit_extract_keywords_hit, get_extract_keywords_results
-from karmaworld.apps.users.models import NoteKarmaEvent
-from karmaworld.utils.ajax_utils import *
 
-from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
+from django.http import HttpResponse
+from django.http import HttpResponseRedirect
+from django.http import HttpResponseForbidden
+from django.http import HttpResponseBadRequest
 from django.views.generic import DetailView, ListView, TemplateView
 from django.views.generic import UpdateView, FormView
 from django.views.generic import View
 from django.views.generic.detail import SingleObjectMixin
 
-from karmaworld.apps.notes.models import Note, NoteMarkdown, KEYWORD_MTURK_THRESHOLD
-from karmaworld.apps.notes.forms import NoteForm, NoteDeleteForm
+from karmaworld.apps.notes.forms import NoteForm
+from karmaworld.apps.notes.forms import NoteDeleteForm
+from karmaworld.apps.notes.models import Note
+from karmaworld.apps.notes.models import NoteMarkdown
+from karmaworld.apps.notes.models import KEYWORD_MTURK_THRESHOLD
+from karmaworld.apps.notes.search import SearchIndex
+from karmaworld.apps.users.models import NoteKarmaEvent
+from karmaworld.apps.courses.forms import CourseForm
+from karmaworld.apps.quizzes.forms import KeywordForm
+from karmaworld.apps.quizzes.tasks import submit_extract_keywords_hit
+from karmaworld.apps.quizzes.tasks import get_extract_keywords_results
+from karmaworld.apps.courses.models import Course
+from karmaworld.apps.quizzes.models import Keyword
+from karmaworld.apps.quizzes.create_quiz import quiz_from_keywords
+
+from karmaworld.utils.ajax_utils import ajax_pk_base
+from karmaworld.utils.ajax_utils import ajax_increment
 
 
 logger = logging.getLogger(__name__)
@@ -39,6 +50,10 @@ USER_PROFILE_FLAGS_FIELD = 'flagged_notes'
 def note_page_context_helper(note, request, context):
 
     if request.method == 'POST':
+        if not note.allows_edit_by(request.user):
+            # This user is Balrog. It. Shall. Not. Pass.
+            return HttpResponseForbidden()
+        # Only save tags if not forbidden above.
         context['note_edit_form'] = NoteForm(request.POST)
     else:
         tags_string = ','.join([str(tag) for tag in note.tags.all()])
@@ -76,6 +91,15 @@ class NoteView(UpdateView):
     def get_success_url(self):
         return self.object.get_absolute_url()
 
+    def form_valid(self, form):
+        self.note = self.object
+        # Ensure that the requesting user has permission to edit.
+        if self.note.allows_edit_by(self.request.user):
+            return super(NoteView, self).form_valid(form)
+        else:
+            messages.error(self.request, 'Permission denied.')
+            return HttpResponseRedirect(self.get_success_url())
+
     def get_context_data(self, **kwargs):
         context = super(NoteView, self).get_context_data(**kwargs)
         context['show_note_container'] = True
@@ -108,9 +132,8 @@ class NoteDeleteView(FormView):
 
     def form_valid(self, form):
         self.note = Note.objects.get(id=form.cleaned_data['note'])
-        u = self.request.user
         # Ensure that the requesting user has permission to delete.
-        if (u.is_authenticated() and u.id == self.note.user_id) or u.is_staff:
+        if self.note.allows_delete_by(self.request.user):
             self.note.is_hidden = True
             self.note.save()
             messages.success(self.request, 'The note "{0}" was deleted successfully.'.format(self.note.name))
@@ -158,7 +181,10 @@ class NoteKeywordsView(FormView, SingleObjectMixin):
         kwargs['keywords'] = Keyword.objects.filter(note=self.get_object())
         kwargs['show_keywords'] = True
 
-        note_page_context_helper(self.get_object(), self.request, kwargs)
+        ret = note_page_context_helper(self.get_object(), self.request, kwargs)
+        # check for errors returned by the helper.
+        if ret:
+            return ret
 
         return super(NoteKeywordsView, self).get_context_data(**kwargs)
 
@@ -202,7 +228,10 @@ class NoteQuizView(TemplateView):
     def get_context_data(self, **kwargs):
         note = Note.objects.get(slug=self.kwargs['slug'])
 
-        note_page_context_helper(note, self.request, kwargs)
+        ret = note_page_context_helper(note, self.request, kwargs)
+        # check for errors returned by the helper.
+        if ret:
+            return ret
 
         kwargs['note'] = note
         kwargs['questions'] = quiz_from_keywords(note)
@@ -328,8 +357,8 @@ def edit_note_tags(request, pk):
     """
     Saves the posted string of tags
     """
-    if request.method == "POST" and request.is_ajax() and request.user.is_authenticated() and request.user.get_profile().can_edit_items():
-        note = Note.objects.get(pk=pk)
+    note = Note.objects.get(pk=pk)
+    if request.method == "POST" and request.is_ajax() and note.allows_tags_by(request.user):
         note.tags.set(request.body)
 
         note_json = serializers.serialize('json', [note,])
@@ -337,7 +366,10 @@ def edit_note_tags(request, pk):
         resp['fields']['tags'] = list(note.tags.names())
 
         return HttpResponse(json.dumps(resp), mimetype="application/json")
-    else:
+    if request.method != "POST" or not request.is_ajax():
         return HttpResponseBadRequest(json.dumps({'status': 'fail', 'message': 'Invalid request'}),
                                       mimetype="application/json")
+    else:
+        return HttpResponseForbidden(json.dumps({'status': 'fail', 'message': 'Not permitted'}),
+                                      mimetype="application/json")
 
index 1caea61731bfe4d0959e94bf89d0bcc9c77331a4..c34917582d5074a4cf0fee51884ac3e5fd18d377 100644 (file)
@@ -40,11 +40,11 @@ class UserProfile(models.Model):
 
         return sum
 
-    def can_edit_items(self):
-        if self.user.is_staff:
-            return True
-        else:
-            return (self.get_points() >= 20)
+    def get_id(self):
+        return self.user.id
+
+    def has_staff_status(self):
+        return self.user.is_staff
 
     NO_BADGE = 0
     PROSPECT = 1
index 0ea09a2a3468be3a11c6d4580c3ce24f04fd395a..58626217747c8eb90d30e600e993da963554d91c 100644 (file)
@@ -123,6 +123,11 @@ function initNoteContentPage() {
           $('.tags').append($('<span>', { class: 'tag-span', text: tag }));
         });
         $('#note-tag-dialog').foundation('reveal', 'close');
+      },
+      error: function(data) {
+        $('#note_tags_form').slideUp();
+        $('#note-tag-dialog').foundation('reveal', 'close');
+        confirm('Could not add tags: ' + data.responseJSON.message);
       }
     });
   });
index beab74581a5371addec0b0b8fee5097169d84d78..46221713d0014c5fb0847ecf94169576cabad072 100644 (file)
@@ -1,6 +1,7 @@
 {% extends "base.html" %}
 {% load url from future %}
 {% load compress %}
+{% load notes %}
 
 {% block title %}
   {{ note.name }}
@@ -57,7 +58,7 @@
       });
     {% endif %}
   </script>
-  {% if note.is_editable %}
+  {% if user|can_edit:note %}
     <script type="text/javascript">
       // wysihtml5 doesn't init correctly in a hidden div.  So we remove it every
       // time before showing it in the modal.
           <span class="header-title">{{ note.name }} </span>
           <span style="display: inline;">
             <span class="show-for-large-up">
+              {% if user|can_del:note %}
+                <form method="POST" action="{% url 'note_delete' %}">
+                  {% csrf_token %}
+                  {{ note_delete_form }}
+                  <button id="delete-note-button" type="submit" class="scary"><i class="fa fa-trash-o"></i> Delete Note</button>
+                </form>
+              {% else %}
+                {# just to keep the formatting consistent #}
+                <form></form>
+              {% endif %}
               {% if user.is_authenticated %}
                 {% if already_thanked %}
                   <button id="thank-button-disabled" class="modify-button disabled opentip"
                   <i class="fa fa-download"></i> Download Note</button>
               {% endif %}
 
-              {% if user.get_profile.can_edit_items and note.user != user %}
+              {% if user|can_tag:note %}
                 <button id="edit-note-tags" class="modify-button" data-reveal-id="note-tag-dialog">
-                  <i class="fa fa-pencil-square-o"></i> Edit Tags
-                </button>
+                  <i class="fa fa-pencil-square-o"></i> Edit Tags</button>
+              {% else %}
+                <button id="edit-note-tags-disabled" class="modify-button disabled opentip"
+                          data-ot="Sorry, you are not allowed to edit tags on this note"
+                           {% include 'partial/opentip_settings.html' %}>
+                  <i class="fa fa-pencil-square-o"></i> Edit Tags</button>
               {% endif %}
 
-              {% if note.user == user or user.is_staff %}
-                <button id="edit-button" data-reveal-id="note-edit-dialog" class="modify-button"> <i class="fa fa-edit"></i> Edit This Note</button>&nbsp;&nbsp;
+              {% if user|can_edit:note %}
+                <button id="edit-button" class="modify-button" data-reveal-id="note-edit-dialog">
+                  <i class="fa fa-edit"></i> Edit This Note</button>
+              {% else %}
+                <button id="edit-button-disabled" class="modify-button disabled opentip"
+                          data-ot="Sorry, you are not allowed to edit the contents of this note"
+                           {% include 'partial/opentip_settings.html' %}>
+                  <i class="fa fa-edit"></i> Edit This Note</button>
               {% endif %}
 
               {% if note.license %}
           <div class="small-12 columns">
             <h3>Edit this note's tags</h3>
             <input id="note_tags_input" type="text" value="{% for tag in note.tags.all %}{{ tag.name }}{% if not forloop.last %}, {% endif %}{% endfor %}">
+            <p>{{ note_edit_form.tags.help_text }}</p>
             <button id="save_note_tags" type="submit" value="tags-form"><i class="fa fa-save"></i> Save</button>
           </div>
         </div>
           <div class="small-8 columns">
             <h3>Edit Your Note</h3>
           </div>
-          <div class="small-4 columns text-right">
-            <form method="POST" action="{% url 'note_delete' %}">
-              {% csrf_token %}
-              {{ note_delete_form }}
-              <button id="delete-note-button" type="submit" class="scary"><i class="fa fa-trash-o"></i> Delete Note</button>
-            </form>
-          </div>
         </div>
         <div class="row">
           <form method="POST" action="{{ note.get_absolute_url }}">
                 <p>{{ field.help_text }}</p>
               {% endwith %}
             </div>
-            <div class="small-12 large-6 columns">
-              {% with note_edit_form.tags as field %}
+            <div class="small-12 columns">
+              {% with note_edit_form.html as field %}
                 {{ field.errors|safe }}
                 <label for="{{ field.id_for_label }}">{{ field.label }}:</label>
                 {{ field }}
                 <p>{{ field.help_text }}</p>
               {% endwith %}
             </div>
-            {% if note.is_editable %}
-              <div class="small-12 columns">
-                {% with note_edit_form.html as field %}
-                  {{ field.errors|safe }}
-                  <label for="{{ field.id_for_label }}">{{ field.label }}:</label>
-                  {{ field }}
-                  <p>{{ field.help_text }}</p>
-                {% endwith %}
-              </div>
-            {% endif %}
             <div class="small-12 columns text-center">
               <button type="submit"><i class="fa fa-save"></i> Save</button>
             </div>