changeset 368:831b22e93f94 0.6.3

Implement error and permission handling for UI views. * Views are protected by decorators. * Decorators check for permission errors and render error pages. * Global Flask error handling is for API endpoints.
author Ludovic Chabant <ludovic@chabant.com>
date Tue, 22 Sep 2015 03:17:21 -0700
parents 73e0564e4fea
children 8b55ceaf99dd
files wikked/auth.py wikked/templates/error-unauthorized.html wikked/views/__init__.py wikked/views/edit.py wikked/views/error.py wikked/views/history.py wikked/views/read.py wikked/views/special.py
diffstat 8 files changed, 106 insertions(+), 20 deletions(-) [+]
line wrap: on
line diff
--- a/wikked/auth.py	Tue Sep 22 03:15:42 2015 -0700
+++ b/wikked/auth.py	Tue Sep 22 03:17:21 2015 -0700
@@ -61,6 +61,19 @@
     def isPageWritable(self, page, username):
         return self._isAllowedForMeta(page, 'writers', username)
 
+    def hasPermission(self, meta_name, username):
+        perm = self._permissions.get(meta_name)
+        if perm is not None:
+            # Permissions are declared at the wiki level.
+            if username is None and 'anonymous' in perm:
+                return True
+            if username is not None and (
+                    '*' in perm or username in perm):
+                return True
+            return False
+
+        return True
+
     def _isAllowedForMeta(self, page, meta_name, username):
         perm = page.getMeta(meta_name)
         if perm is not None:
@@ -77,17 +90,7 @@
 
             return False
 
-        perm = self._permissions.get(meta_name)
-        if perm is not None:
-            # Permissions are declared at the wiki level.
-            if username is None and 'anonymous' in perm:
-                return True
-            if username is not None and (
-                    '*' in perm or username in perm):
-                return True
-            return False
-
-        return True
+        return self.hasPermission(meta_name, username)
 
     def _updatePermissions(self, config):
         self._permissions = {
--- a/wikked/templates/error-unauthorized.html	Tue Sep 22 03:15:42 2015 -0700
+++ b/wikked/templates/error-unauthorized.html	Tue Sep 22 03:17:21 2015 -0700
@@ -2,11 +2,19 @@
 {% block content %}
 <article>
     <header>
-        <h1>You're not authorized for this</h1>
+        <h1>{{error|default("You're not authorized for this")}}</h1>
     </header>
     <section>
-        <p>The page you're trying to access is protected.
-                Please <a href="/login">log into an account</a> that has access to it.</p>
+        {%if error_details%}
+        <p>{{error_details}}</p>
+        {%else%}
+        <p>The page you're trying to access is protected.</p>
+        {%endif%}
+        {%if auth.is_logged_in%}
+        <p>User <a href="{{auth.url_profile}}">{{auth.username}}</a> is not authorized for this. Please <a href="/login">log into a different account</a> to have access to it.</p>
+        {%else%}
+        <p>Please <a href="/login">log into an account</a> that has access to it.</p>
+        {%endif%}
     </section>
 </article>
 {% endblock %}
--- a/wikked/views/__init__.py	Tue Sep 22 03:15:42 2015 -0700
+++ b/wikked/views/__init__.py	Tue Sep 22 03:17:21 2015 -0700
@@ -1,6 +1,56 @@
-from flask import request
+import functools
+from flask import request, render_template
 from flask.ext.login import current_user
-from wikked.web import app
+from wikked.web import app, get_wiki
+from wikked.webimpl import PermissionError
+
+
+def show_unauthorized_error(error=None, error_details=None, tpl_name=None):
+    if error is not None:
+        error = str(error)
+
+    data = {}
+    if error:
+        data['error'] = error
+    if error_details:
+        data['error_details'] = error_details
+
+    add_auth_data(data)
+    add_navigation_data(None, data)
+    tpl_name = tpl_name or 'error-unauthorized.html'
+    return render_template(tpl_name, **data)
+
+
+def errorhandling_ui(f):
+    @functools.wraps(f)
+    def wrapper(*args, **kwargs):
+        try:
+            return f(*args, **kwargs)
+        except PermissionError as ex:
+            return show_unauthorized_error(ex)
+    return wrapper
+
+
+def errorhandling_ui2(tpl_name):
+    def decorator(f):
+        @functools.wraps(f)
+        def wrapper(*args, **kwargs):
+            try:
+                return f(*args, **kwargs)
+            except PermissionError as ex:
+                return show_unauthorized_error(ex, tpl_name=tpl_name)
+        return wrapper
+    return decorator
+
+
+def requires_reader_auth(f):
+    @functools.wraps(f)
+    def wrapper(*args, **kwargs):
+        wiki = get_wiki()
+        if not wiki.auth.hasPermission('readers', current_user.get_id()):
+            return show_unauthorized_error()
+        return f(*args, **kwargs)
+    return wrapper
 
 
 def add_auth_data(data):
--- a/wikked/views/edit.py	Tue Sep 22 03:15:42 2015 -0700
+++ b/wikked/views/edit.py	Tue Sep 22 03:17:21 2015 -0700
@@ -1,6 +1,7 @@
 from flask import redirect, url_for, request, render_template
 from flask.ext.login import current_user
 from wikked.views import (
+        errorhandling_ui2, show_unauthorized_error,
         add_auth_data, add_navigation_data)
 from wikked.web import app, get_wiki
 from wikked.webimpl import url_from_viewarg
@@ -14,6 +15,11 @@
 
 @app.route('/create/<path:url>')
 def create_page(url):
+    wiki = get_wiki()
+    if not wiki.auth.hasPermission('writers', current_user.get_id()):
+        return show_unauthorized_error(
+                error="You're not authorized to create new pages.")
+
     data = {
             'is_new': True,
             'create_in': url.lstrip('/'),
@@ -30,12 +36,14 @@
 
 
 @app.route('/edit', methods=['POST'])
+@errorhandling_ui2('error-unauthorized-edit.html')
 def edit_new_page():
     url = request.form['title']
     return edit_page(url)
 
 
 @app.route('/edit/<path:url>', methods=['GET', 'POST'])
+@errorhandling_ui2('error-unauthorized-edit.html')
 def edit_page(url):
     wiki = get_wiki()
     user = current_user.get_id()
--- a/wikked/views/error.py	Tue Sep 22 03:15:42 2015 -0700
+++ b/wikked/views/error.py	Tue Sep 22 03:17:21 2015 -0700
@@ -23,7 +23,7 @@
     resp = {
             'error': {
                 'type': 'permission',
-                'message': error.message
+                'message': str(error)
                 }
             }
     return jsonify(resp), 403
--- a/wikked/views/history.py	Tue Sep 22 03:15:42 2015 -0700
+++ b/wikked/views/history.py	Tue Sep 22 03:17:21 2015 -0700
@@ -1,7 +1,9 @@
 import urllib.parse
 from flask import request, abort, render_template
 from flask.ext.login import current_user
-from wikked.views import add_auth_data, add_navigation_data
+from wikked.views import (
+        errorhandling_ui, requires_reader_auth,
+        add_auth_data, add_navigation_data)
 from wikked.web import app, get_wiki
 from wikked.webimpl import url_from_viewarg
 from wikked.webimpl.history import (
@@ -10,6 +12,7 @@
 
 
 @app.route('/special/history')
+@requires_reader_auth
 def site_history():
     wiki = get_wiki()
     user = current_user.get_id()
@@ -26,6 +29,7 @@
 
 
 @app.route('/hist/<path:url>')
+@errorhandling_ui
 def page_history(url):
     wiki = get_wiki()
     user = current_user.get_id()
@@ -40,6 +44,7 @@
 
 
 @app.route('/rev/<path:url>')
+@errorhandling_ui
 def page_rev(url):
     rev = request.args.get('rev')
     if rev is None:
@@ -62,6 +67,7 @@
 
 
 @app.route('/diff/<path:url>')
+@errorhandling_ui
 def diff_page(url):
     rev1 = request.args.get('rev1')
     rev2 = request.args.get('rev2')
@@ -89,6 +95,7 @@
 
 
 @app.route('/diff_rev/<rev>')
+@errorhandling_ui
 def diff_revision(rev):
     wiki = get_wiki()
     user = current_user.get_id()
--- a/wikked/views/read.py	Tue Sep 22 03:15:42 2015 -0700
+++ b/wikked/views/read.py	Tue Sep 22 03:17:21 2015 -0700
@@ -2,7 +2,7 @@
 from flask import (
         render_template, request, abort)
 from flask.ext.login import current_user
-from wikked.views import add_auth_data, add_navigation_data
+from wikked.views import add_auth_data, add_navigation_data, errorhandling_ui
 from wikked.web import app, get_wiki
 from wikked.webimpl import url_from_viewarg
 from wikked.webimpl.read import (
@@ -18,6 +18,7 @@
 
 
 @app.route('/read/<path:url>')
+@errorhandling_ui
 def read(url):
     wiki = get_wiki()
     url = url_from_viewarg(url)
@@ -33,6 +34,7 @@
 
 
 @app.route('/search')
+@errorhandling_ui
 def search():
     query = request.args.get('q')
     if query is None or query == '':
@@ -55,6 +57,7 @@
 
 
 @app.route('/inlinks/<path:url>')
+@errorhandling_ui
 def incoming_links(url):
     wiki = get_wiki()
     user = current_user.get_id()
--- a/wikked/views/special.py	Tue Sep 22 03:15:42 2015 -0700
+++ b/wikked/views/special.py	Tue Sep 22 03:17:21 2015 -0700
@@ -1,6 +1,8 @@
 from flask import render_template
 from flask.ext.login import current_user
-from wikked.views import add_auth_data, add_navigation_data
+from wikked.views import (
+        requires_reader_auth,
+        add_auth_data, add_navigation_data)
 from wikked.web import app, get_wiki
 from wikked.webimpl.special import (
         get_orphans, get_broken_redirects, get_double_redirects,
@@ -70,6 +72,7 @@
 
 
 @app.route('/special')
+@requires_reader_auth
 def special_pages_dashboard():
     data = {
             'is_special_page': True,
@@ -106,24 +109,28 @@
 
 
 @app.route('/special/list/orphans')
+@requires_reader_auth
 def special_list_orphans():
     return call_api('orphans', get_orphans,
                     raw_url='/api/orphans')
 
 
 @app.route('/special/list/broken-redirects')
+@requires_reader_auth
 def special_list_broken_redirects():
     return call_api('broken-redirects', get_broken_redirects,
                     raw_url='/api/broken-redirects')
 
 
 @app.route('/special/list/double-redirects')
+@requires_reader_auth
 def special_list_double_redirects():
     return call_api('double-redirects', get_double_redirects,
                     raw_url='/api/double-redirects')
 
 
 @app.route('/special/list/dead-ends')
+@requires_reader_auth
 def special_list_dead_ends():
     return call_api('dead-ends', get_dead_ends,
                     raw_url='/api/dead-ends')