Browse Source

Add access control on the isp edit view

An email will be sent to the owner containing a link with a signed token
allowing to edit the ISP for one hour.
Gu1 11 years ago
parent
commit
80aa975827

+ 3 - 0
config.py

@@ -7,3 +7,6 @@ DEBUG = True
 CRAWLER_MIN_CACHE_TIME=60*60 # 1 hour
 CRAWLER_MAX_CACHE_TIME=60*60*24*14 # 2 week
 CRAWLER_DEFAULT_CACHE_TIME=60*60*12 # 12 hours
+EMAIL_SENDER='FFDN DB <no-reply@db.ffdn.org>'
+#MAIL_SERVER=''
+#SERVER_NAME = 'db.ffdn.org'

+ 19 - 6
ffdnispdb/forms.py

@@ -159,7 +159,8 @@ class ProjectForm(Form):
         return json
 
     @classmethod
-    def edit_json(cls, json):
+    def edit_json(cls, isp):
+        json=isp.json
         obj=type('abject', (object,), {})()
         def set_attr(attr, itemk=None, d=json):
             if itemk is None:
@@ -186,12 +187,18 @@ class ProjectForm(Form):
         if 'otherWebsites' in json:
             setattr(obj, 'other_websites', [{'name': n, 'url': w} for n, w in json['otherWebsites'].iteritems()])
         set_attr('covered_areas', 'coveredAreas')
+        obj.tech_email=isp.tech_email
         return cls(obj=obj)
 
 
 class URLField(TextField):
     def _value(self):
-        return urlparse.urlunsplit(self.data) if self.data is not None else ''
+        if isinstance(self.data, basestring):
+            return self.data
+        elif self.data is None:
+            return ''
+        else:
+            return urlparse.urlunsplit(self.data)
 
     def process_formdata(self, valuelist):
         if valuelist:
@@ -215,18 +222,24 @@ def is_url_unique(url):
     return True
 
 class ProjectJSONForm(Form):
-    url = URLField(_(u'base url'), description=['E.g. https://isp.com/', 'A ressource implementing our '+\
-                                                'JSON-Schema specification must exist at path /isp.json'])
+    json_url = URLField(_(u'base url'), description=[_(u'E.g. https://isp.com/'),
+                            _(u'A ressource implementing our JSON-Schema specification '+
+                               'must exist at path /isp.json')])
     tech_email = TextField(_(u'Email'), validators=[Email()], description=[None,
                            _(u'Technical contact, in case of problems')])
 
-    def validate_url(self, field):
+    def validate_json_url(self, field):
         if not field.data.netloc:
             raise ValidationError(_(u'Invalid URL'))
 
         if field.data.scheme not in ('http', 'https'):
             raise ValidationError(_(u'Invalid URL (must be HTTP(s))'))
 
-        if not is_url_unique(field.data):
+        if not field.object_data and not is_url_unique(field.data):
             raise ValidationError(_(u'This URL is already in our database'))
 
+
+class RequestEditToken(Form):
+    tech_email = TextField(_(u'Tech Email'), validators=[Email()], description=[None,
+                           _(u'The Technical contact you provided while registering')])
+

+ 4 - 0
ffdnispdb/models.py

@@ -73,6 +73,10 @@ class ISP(db.Model):
         return [c['name'] for c in self.json.get('coveredAreas', [])]
 
     @property
+    def is_local(self):
+        return self.json_url is None
+
+    @property
     def complete_name(self):
         if 'shortname' in self.json:
             return u'%s (%s)'%(self.json['shortname'], self.json['name'])

+ 2 - 0
ffdnispdb/templates/edit_project_form.html

@@ -0,0 +1,2 @@
+{% set page_title = _("Edit your project") %}
+{% include 'project_form_generic.html' with context %}

+ 2 - 0
ffdnispdb/templates/edit_project_json_form.html

@@ -0,0 +1,2 @@
+{% set page_title = _("Edit your project") %}
+{% include 'project_json_form_generic.html' with context %}

+ 21 - 0
ffdnispdb/templates/gen_edit_token.html

@@ -0,0 +1,21 @@
+{% extends "layout.html" %}
+{% import "form_macros.html" as fm %}
+{% block container %}
+<div class="row">
+  <div class="span11 well">
+    <form method="post" class="form-horizontal">
+      {{ form.csrf_token }}
+      <fieldset>
+        <legend>{{ _("Edit your ISP") }}</legend>
+        <p>To edit your ISP, you need to give us the email provided while registering and we will send you a link to edit it.</p>
+        {{ fm.render_field(form.tech_email) }}
+        <div class="form-actions">
+          <input type="submit" class="btn btn-primary" value="{{ _("Submit") }}" />
+          <input type="reset" class="btn" value="{{ _("Cancel") }}" />
+        </div>
+      </fieldset>
+    </form>
+  </div>
+</div>
+
+{% endblock %}

+ 87 - 13
ffdnispdb/views.py

@@ -3,6 +3,7 @@
 from flask import request, g, redirect, url_for, abort, \
     render_template, flash, json, session, Response, Markup
 from flask.ext.babel import gettext as _
+from flask.ext.mail import Message
 import itsdangerous
 import docutils.core
 import ispformat.specs
@@ -16,7 +17,7 @@ import os.path
 
 from . import forms
 from .constants import *
-from . import app, db, cache
+from . import app, db, cache, mail
 from .models import ISP, ISPWhoosh
 from .crawler import WebValidator, PrettyValidator
 
@@ -67,20 +68,93 @@ def project(projectid):
 
 @app.route('/isp/<projectid>/edit', methods=['GET', 'POST'])
 def edit_project(projectid):
-    isp=ISP.query.filter_by(id=projectid, is_disabled=False).first()
-    if not isp:
-        abort(404)
-    form = forms.ProjectForm.edit_json(isp.json)
-    if form.validate_on_submit():
-        isp.name = form.name.data
-        isp.shortname = form.shortname.data or None
-        isp.json=form.to_json(isp.json)
+    MAX_TOKEN_AGE=3600
+    isp=ISP.query.filter_by(id=projectid, is_disabled=False).first_or_404()
+    sess_token=session.get('edit_tokens', {}).get(isp.id)
 
-        db.session.add(isp)
-        db.session.commit()
-        flash(_(u'Project modified'), 'info')
+    if 'token' in request.args:
+        print session
+        s = itsdangerous.URLSafeTimedSerializer(app.secret_key, salt='edit')
+        try:
+            r = s.loads(request.args['token'], max_age=MAX_TOKEN_AGE,
+                        return_timestamp=True)
+        except:
+            abort(403)
+
+        if r[0] != isp.id:
+            abort(403)
+
+        tokens = session.setdefault('edit_tokens', {})
+        tokens[r[0]] = r[1]
+        # refresh page, without the token in the url
+        return redirect(url_for('edit_project', projectid=r[0]))
+    elif (sess_token is None or (datetime.utcnow()-sess_token).total_seconds() > MAX_TOKEN_AGE):
+        return redirect(url_for('gen_edit_token', projectid=isp.id))
+
+    if isp.is_local:
+        form = forms.ProjectForm.edit_json(isp)
+        if form.validate_on_submit():
+            isp.name = form.name.data
+            isp.shortname = form.shortname.data or None
+            isp.json = form.to_json(isp.json)
+            isp.tech_email = form.tech_email.data
+
+            db.session.add(isp)
+            db.session.commit()
+            flash(_(u'Project modified'), 'info')
+            return redirect(url_for('project', projectid=isp.id))
+        return render_template('edit_project_form.html', form=form)
+    else:
+        form = forms.ProjectJSONForm(obj=isp)
+        if form.validate_on_submit():
+            isp.tech_email = form.tech_email.data
+            u = list(form.json_url.data)
+            u[2]='/isp.json' # new path
+            url=urlunsplit(u)
+            isp.json_url = url
+
+            db.session.add(isp)
+            db.session.commit()
+            flash(_(u'Project modified'), 'info')
+            return redirect(url_for('project', projectid=isp.id))
+        return render_template('edit_project_json_form.html', form=form)
+
+
+@app.route('/isp/<projectid>/gen_edit_token', methods=['GET', 'POST'])
+def gen_edit_token(projectid):
+    isp=ISP.query.filter_by(id=projectid, is_disabled=False).first_or_404()
+    form = forms.RequestEditToken()
+    if form.validate_on_submit(): # validated
+        if form.tech_email.data == isp.tech_email:
+            s = itsdangerous.URLSafeTimedSerializer(app.secret_key, salt='edit')
+            token = s.dumps(isp.id)
+            msg = Message("Edit request of your ISP", sender=app.config['EMAIL_SENDER'])
+            msg.body="""
+Hello,
+You are receiving this message because your are listed as technical contact for "%s" on the FFDN ISP database.
+
+Someone asked to edit your ISP's data in our database. If it's not you, please ignore this message.
+
+To proceed to the editing form, please click on the following link:
+%s?token=%s
+
+Note: the link is only valid for one hour from the moment we send you this email.
+
+Thanks,
+The FFDN ISP Database team
+https://db.ffdn.org
+            """.strip() % (isp.complete_name,
+                           url_for('edit_project', projectid=isp.id, _external=True),
+                           token)
+            msg.add_recipient(isp.tech_email)
+            mail.send(msg)
+
+        # if the email provided is not the correct one, we still redirect
+        flash(_(u'If you provided the correct email adress, '
+                 'you must will receive a message shortly (check your spam folder)'), 'info')
         return redirect(url_for('project', projectid=isp.id))
-    return render_template('project_form.html', form=form, project=isp)
+
+    return render_template('gen_edit_token.html', form=form)
 
 
 @app.route('/add-a-project', methods=['GET'])