changeset 193:215684074923

Implement Atom feed security in a way compatible with feed readers.
author Daniele Nicolodi <daniele@grinta.net>
date Tue, 08 Nov 2011 23:05:48 +0100
parents 83c2d2f07df4
children 53c58b8bac9f
files src/ltpdarepo/__init__.py src/ltpdarepo/templates/database.html src/ltpdarepo/views/feed.py
diffstat 3 files changed, 62 insertions(+), 32 deletions(-) [+]
line wrap: on
line diff
--- a/src/ltpdarepo/__init__.py	Sun Aug 14 12:23:55 2011 +0200
+++ b/src/ltpdarepo/__init__.py	Tue Nov 08 23:05:48 2011 +0100
@@ -174,8 +174,9 @@
 from .views.browse import module
 app.register_blueprint(module, url_prefix='/browse')
 
-from .views.feed import module
+from .views.feed import module, url_for_atom_feed
 app.register_blueprint(module, url_prefix='/browse')
+app.jinja_env.globals['url_for_atom_feed'] = url_for_atom_feed
 
 from .views.profile import module
 app.register_blueprint(module, url_prefix='/user')
--- a/src/ltpdarepo/templates/database.html	Sun Aug 14 12:23:55 2011 +0200
+++ b/src/ltpdarepo/templates/database.html	Tue Nov 08 23:05:48 2011 +0100
@@ -1,6 +1,6 @@
 {% extends "layout.html" %}
 {% block head %}
-<link href="{{ url_for('feed.atom', database=database.id) }}" rel="alternate" title="Recent Submissions" type="application/atom+xml">
+<link href="{{ url_for_atom_feed(database.id) }}" rel="alternate" title="Recent Submissions" type="application/atom+xml">
 {% endblock %}
 {% block title %}{{ database.id }}{% endblock %}
 {% block body %}
@@ -29,7 +29,7 @@
 {% endif %}
 <h2>Feed</h2>
 <p class="discrete">Atom feed with the latest submisions to the database:</p>
-<a class="feed" href="{{ url_for('feed.atom', database=database.id) }}">
+<a class="feed" href="{{ url_for_atom_feed(database.id) }}">
   <img src="{{ url_for('static', filename='feed.png') }}"></img>
 </a>
 {% endblock %}
--- a/src/ltpdarepo/views/feed.py	Sun Aug 14 12:23:55 2011 +0200
+++ b/src/ltpdarepo/views/feed.py	Tue Nov 08 23:05:48 2011 +0100
@@ -1,43 +1,72 @@
-from flask import Blueprint, request, url_for, g
+from flask import Blueprint, request, url_for, g, current_app, abort
+from itsdangerous import BadSignature, Signer as BaseSigner
 from werkzeug.contrib.atom import AtomFeed
 
 from .browse import Objs
 
 from ltpdarepo.database import Database
-from ltpdarepo.security import require, view
 
 app = Blueprint('feed', __name__)
 
 
-@app.route('/<database>/atom.xml')
-@require('user')
-def atom(database):
-    with view('database', database):
-        db = Database.load(id=database)
-        if db is None:
-            # not found
-            abort(404)
+class Signer(BaseSigner):
+    def __init__(self, secret=None, salt=None):
+        if secret is None:
+            secret = current_app.config['SECRET_KEY']
+        if salt is None:
+            salt = request.host
+        super(Signer, self).__init__(secret, salt, sep='/')
 
-        feed = AtomFeed(
-            title=db.id, subtitle=db.description,
-            url=url_for('browse.database', database=database, _external=True),
-            feed_url=request.url,
-            generator=('LTPDA Repository', None, g.version))
+    def unsign(self, token):
+        # translate bad signature exceptions into unauthorized http errors
+        try:
+            return super(Signer, self).unsign(token)
+        except BadSignature:
+            # unauthorized
+            abort(403)
+
+
+def url_for_atom_feed(database, **kwargs):
+    signer = Signer()
+    token = signer.sign(database)
+    return url_for('feed.atom', token=token, **kwargs)
+
+
+@app.route('/<path:token>/atom.xml')
+def atom(token):
+    # this route cannot require authentication in the usual way
+    # because most feed readers do not support authentication. the
+    # trick is therefore to encode an authorization token into the url
 
-        # first n objects ordered per descending submission time
-        objs = Objs(database=database).orderby('submitted', 1).limit(64)
-        
-        for obj in objs.all():
-            obj['url'] = url_for('browse.obj', database=database,
-                                 objid=obj['id'], _external=True)
-            feed.add(title='%s: %s' % (obj['name'], obj['title']),
-                     content='%s %s' % (obj['description'], obj['analysis']),
-                     content_type='text',
-                     author=obj['author'],
-                     url=obj['url'],
-                     updated=obj['submitted'])
+    # check the token
+    signer = Signer()
+    database = signer.unsign(token)
+
+    db = Database.load(id=database)
+    if db is None:
+        # not found
+        abort(404)
+
+    feed = AtomFeed(
+        title=db.id, subtitle=db.description,
+        url=url_for('browse.database', database=database, _external=True),
+        feed_url=request.url,
+        generator=('LTPDA Repository', None, g.version))
 
-        # return with the correct mime type
-        return feed.get_response()
+    # first n objects ordered per descending submission time
+    objs = Objs(database=database).orderby('submitted', 1).limit(64)
+
+    for obj in objs.all():
+        obj['url'] = url_for('browse.obj', database=database,
+                             objid=obj['id'], _external=True)
+        feed.add(title='%s: %s' % (obj['name'], obj['title']),
+                 content='%s %s' % (obj['description'], obj['analysis']),
+                 content_type='text',
+                 author=obj['author'],
+                 url=obj['url'],
+                 updated=obj['submitted'])
+
+    # return with the correct mime type
+    return feed.get_response()
 
 module = app