You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by br...@apache.org on 2016/08/22 19:05:04 UTC

[1/2] allura git commit: [#8117] basic 2FA support

Repository: allura
Updated Branches:
  refs/heads/db/8117 [created] 1848bd0ed


[#8117] basic 2FA support


Project: http://git-wip-us.apache.org/repos/asf/allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/allura/commit/1848bd0e
Tree: http://git-wip-us.apache.org/repos/asf/allura/tree/1848bd0e
Diff: http://git-wip-us.apache.org/repos/asf/allura/diff/1848bd0e

Branch: refs/heads/db/8117
Commit: 1848bd0eda47125948b9b9d5731e4bbe17df3e3f
Parents: f0b0d67
Author: Dave Brondsema <da...@brondsema.net>
Authored: Tue Aug 16 11:35:26 2016 -0400
Committer: Dave Brondsema <da...@brondsema.net>
Committed: Mon Aug 22 15:04:57 2016 -0400

----------------------------------------------------------------------
 Allura/allura/controllers/auth.py       | 86 +++++++++++++++++++++++-
 Allura/allura/lib/app_globals.py        |  1 +
 Allura/allura/lib/helpers.py            | 10 ++-
 Allura/allura/lib/plugin.py             | 97 ++++++++++++++++++++++++++++
 Allura/allura/model/__init__.py         |  3 +-
 Allura/allura/model/auth.py             |  5 +-
 Allura/allura/model/multifactor.py      | 41 ++++++++++++
 Allura/allura/templates/user_prefs.html | 33 ++++++++++
 Allura/allura/templates/user_totp.html  | 48 ++++++++++++++
 Allura/development.ini                  | 12 ++++
 Allura/setup.py                         |  4 ++
 Dockerfile                              |  2 +
 requirements.txt                        | 10 +++
 13 files changed, 348 insertions(+), 4 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/1848bd0e/Allura/allura/controllers/auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/auth.py b/Allura/allura/controllers/auth.py
index 9ad0dea..931fcf5 100644
--- a/Allura/allura/controllers/auth.py
+++ b/Allura/allura/controllers/auth.py
@@ -19,6 +19,7 @@ import logging
 import os
 import datetime
 import re
+from time import time
 
 import bson
 import tg
@@ -27,8 +28,9 @@ from tg.decorators import with_trailing_slash, without_trailing_slash
 from pylons import tmpl_context as c, app_globals as g
 from pylons import request, response
 from webob import exc as wexc
-from paste.deploy.converters import asbool
+from paste.deploy.converters import asbool, asint
 from urlparse import urlparse, urljoin
+from cryptography.hazmat.primitives.twofactor import InvalidToken
 
 import allura.tasks.repo_tasks
 from allura import model as M
@@ -452,6 +454,7 @@ class PreferencesController(BaseController):
     def _check_security(self):
         require_authenticated()
 
+
     @with_trailing_slash
     @expose('jinja:allura:templates/user_prefs.html')
     def index(self, **kw):
@@ -604,6 +607,87 @@ class PreferencesController(BaseController):
         c.user.set_pref('disable_user_messages', not allow_user_messages)
         redirect(request.referer)
 
+    @expose('jinja:allura:templates/user_totp.html')
+    @without_trailing_slash
+    #@reconfirm_password
+    def totp_new(self, **kw):
+        '''
+        auth_provider = plugin.AuthenticationProvider.get(request)
+        # TODO: don't require it every single time
+        if not kw.get('password') or not auth_provider.validate_password(c.user, kw.get('password')):
+            flash('You must provide your current password to set up multifactor authentication', 'error')
+            redirect('.')
+        '''
+
+        # TODO: warn before that it will clear out previous
+
+        totp_storage = plugin.TotpStorage.get()
+        if 'totp_new_key' not in session:
+            # never been here yet
+            # get (or generate) new key
+            totp = totp_storage.get_totp(c.user)
+            # don't save to database until confirmed, just session for now
+            session['totp_new_key'] = totp.key
+            session.save()
+        else:
+            # use key from session, so we don't regenerate new keys on each page load
+            key = session['totp_new_key']
+            totp = totp_storage.Totp(key)
+
+        qr = totp_storage.get_qr_code(totp, c.user)
+        h.auditlog_user('Visited multifactor new TOTP page')
+        return dict(
+            qr=qr,
+            setup=True,
+        )
+
+
+    @expose('jinja:allura:templates/user_totp.html')
+    @without_trailing_slash
+    #@reconfirm_password
+    def totp_view(self, **kw):
+        totp_storage = plugin.TotpStorage.get()
+        totp = totp_storage.get_totp(c.user, generate_new=False)
+        qr = totp_storage.get_qr_code(totp, c.user)
+        h.auditlog_user('Viewed multifactor TOTP config page')
+        return dict(
+            qr=qr,
+            setup=False,
+        )
+
+    @expose()
+    @require_post()
+    # @reconfirm_password
+    def totp_set(self, pin, **kw):
+        key = session['totp_new_key']
+        totp_storage = plugin.TotpStorage.get()
+        totp = totp_storage.Totp(key)
+        pin = pin.replace(' ', '')  # Google authenticator puts a space in their codes
+        pin = bytes(pin)  # can't be unicode
+        try:
+            totp.verify(pin, time())
+        except InvalidToken:
+            h.auditlog_user('Failed to set up multifactor TOTP (wrong pin)')
+            tg.flash('Invalid PIN, please try again.')
+            redirect(request.referer)
+        else:
+            h.auditlog_user('Set up multifactor TOTP')
+            totp_storage.set_secret_key(c.user, key)
+            c.user.set_pref('multifactor', True)
+            del session['totp_new_key']
+            session.save()
+            tg.flash('Two factor authentication has now been set up.')
+            redirect('..')
+
+    @expose()
+    @require_post()
+    # @reconfirm_password
+    def totp_disable(self):
+        h.auditlog_user('Disabled multifactor TOTP')
+        totp_storage = plugin.TotpStorage.get()
+        totp_storage.set_secret_key(None)
+        c.user.set_pref('multifactor', False)
+
 
 class UserInfoController(BaseController):
 

http://git-wip-us.apache.org/repos/asf/allura/blob/1848bd0e/Allura/allura/lib/app_globals.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/app_globals.py b/Allura/allura/lib/app_globals.py
index 33fa123..67995bd 100644
--- a/Allura/allura/lib/app_globals.py
+++ b/Allura/allura/lib/app_globals.py
@@ -306,6 +306,7 @@ class Globals(object):
             # imported (after load, the ep itself is not used)
             macros=_cache_eps('allura.macros'),
             webhooks=_cache_eps('allura.webhooks'),
+            totp_storage=_cache_eps('allura.multifactor.totp_storage')
         )
 
         # Neighborhood cache

http://git-wip-us.apache.org/repos/asf/allura/blob/1848bd0e/Allura/allura/lib/helpers.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/helpers.py b/Allura/allura/lib/helpers.py
index 6e10208..890deb6 100644
--- a/Allura/allura/lib/helpers.py
+++ b/Allura/allura/lib/helpers.py
@@ -16,7 +16,7 @@
 #       KIND, either express or implied.  See the License for the
 #       specific language governing permissions and limitations
 #       under the License.
-
+import base64
 import sys
 import os
 import os.path
@@ -35,6 +35,7 @@ from collections import defaultdict
 import shlex
 import socket
 from functools import partial
+from cStringIO import StringIO
 
 import tg
 import genshi.template
@@ -1286,3 +1287,10 @@ def rate_limit(cfg_opt, artifact_count, start_date, exception=None):
                 artifact_count = artifact_count()
             if artifact_count >= count:
                 raise exception()
+
+
+def base64uri(image, format='PNG', mimetype='image/png'):
+    output = StringIO()
+    image.save(output, format=format)
+    data = base64.b64encode(output.getvalue())
+    return 'data:{};base64,{}'.format(mimetype, data)
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/allura/blob/1848bd0e/Allura/allura/lib/plugin.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/plugin.py b/Allura/allura/lib/plugin.py
index f74dc62..413a772 100644
--- a/Allura/allura/lib/plugin.py
+++ b/Allura/allura/lib/plugin.py
@@ -35,6 +35,8 @@ from datetime import datetime, timedelta
 import calendar
 import json
 
+import bson
+
 try:
     import ldap
     from ldap import modlist
@@ -46,6 +48,10 @@ from tg import config, request, redirect, response
 from pylons import tmpl_context as c, app_globals as g
 from webob import exc, Request
 from paste.deploy.converters import asbool, asint
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives.twofactor.totp import TOTP
+from cryptography.hazmat.primitives.hashes import SHA1
+import qrcode
 
 from ming.utils import LazyProperty
 from ming.orm import state
@@ -1489,6 +1495,97 @@ class LdapUserPreferencesProvider(UserPreferencesProvider):
             return LocalUserPreferencesProvider().set_pref(user, pref_name, pref_value)
 
 
+class TotpStorage(object):
+
+    '''
+    An interface for storing multifact auth TOTP secret keys
+
+    To use a new provider, expose an entry point in setup.py::
+
+        [allura.multifactor.totp_storage]
+        mytotp = foo.bar:MyTotpStorage
+
+    Then in your .ini file, set auth.multifactor.totp.storage=mytotp
+    '''
+
+    @classmethod
+    def get(cls):
+        method = config.get('auth.multifactor.totp.storage', 'mongodb')
+        return g.entry_points['totp_storage'][method]()
+
+    def Totp(self, key):
+        # simple constructor helper
+        totp = TOTP(key,
+                    asint(config.get('auth.multifactor.totp.length', 6)),
+                    SHA1(),
+                    asint(config.get('auth.multifactor.totp.time', 30)),
+                    backend=default_backend())
+        totp.key = key  # for convenience, else you have to use `totp._hotp._key`
+        return totp
+
+    def get_totp(self, user, generate_new=True):
+        '''
+        :param user: a :class:`User <allura.model.auth.User>`
+        :param bool generate_new: generate (but does not save) if one does not exist already
+        :return:
+        '''
+        key = self.get_secret_key(user)
+        if not key and generate_new:
+            key = os.urandom(20)  # == 160 bytes which is recommended
+        return self.Totp(key)
+
+    def get_qr_code(self, totp, user, **qrcode_params):
+        qrcode_params.setdefault('box_size', 5)
+        uri = totp.get_provisioning_uri(user.username, config['site_name'])
+        qr = qrcode.make(uri, **qrcode_params)
+        return qr
+
+    def get_secret_key(self, user):
+        '''
+        :param user: a :class:`User <allura.model.auth.User>`
+        :return: key
+        '''
+        raise NotImplementedError('get_secret_key')
+
+    def set_secret_key(self, user, key):
+        '''
+        :param user: a :class:`User <allura.model.auth.User>`
+        :param bytes key: may be `None` to clear out a key
+        '''
+        raise NotImplementedError('set_secret_key')
+
+
+class MongodbTotpStorage(TotpStorage):
+
+    def get_secret_key(self, user):
+        from allura import model as M
+        if user.is_anonymous():
+            return None
+        record = M.TotpKey.query.get(user_id=user._id)
+        if record:
+            return record.key
+
+    def set_secret_key(self, user, key):
+        from allura import model as M
+        key = bson.binary.Binary(key)
+        M.TotpKey.query.update({'user_id': user._id},
+                               {'user_id': user._id, 'key': key},
+                               upsert=True)
+
+
+class FilesystemTotpStorage(TotpStorage):
+
+    @property
+    def basedir(self):
+        return config['auth.multifactor.totp.filesystem.basedir']
+
+    def get_key(self, user):
+        pass
+
+    def set_key(self, user, key):
+        pass
+
+
 class AdminExtension(object):
 
     """

http://git-wip-us.apache.org/repos/asf/allura/blob/1848bd0e/Allura/allura/model/__init__.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/__init__.py b/Allura/allura/model/__init__.py
index a69e7c9..0b1bea2 100644
--- a/Allura/allura/model/__init__.py
+++ b/Allura/allura/model/__init__.py
@@ -36,6 +36,7 @@ from .stats import Stats
 from .oauth import OAuthToken, OAuthConsumerToken, OAuthRequestToken, OAuthAccessToken
 from .monq_model import MonQTask
 from .webhook import Webhook
+from .multifactor import TotpKey
 
 from .types import ACE, ACL, EVERYONE, ALL_PERMISSIONS, DENY_ALL, MarkdownCache
 from .session import main_doc_session, main_orm_session
@@ -60,4 +61,4 @@ __all__ = [
     'OAuthRequestToken', 'OAuthAccessToken', 'MonQTask', 'Webhook', 'ACE', 'ACL', 'EVERYONE', 'ALL_PERMISSIONS',
     'DENY_ALL', 'MarkdownCache', 'main_doc_session', 'main_orm_session', 'project_doc_session', 'project_orm_session',
     'artifact_orm_session', 'repository_orm_session', 'task_orm_session', 'ArtifactSessionExtension', 'repository',
-    'repo_refresh', 'SiteNotification']
+    'repo_refresh', 'SiteNotification', 'TotpKey']

http://git-wip-us.apache.org/repos/asf/allura/blob/1848bd0e/Allura/allura/model/auth.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/auth.py b/Allura/allura/model/auth.py
index 7f6442c..fa26353 100644
--- a/Allura/allura/model/auth.py
+++ b/Allura/allura/model/auth.py
@@ -300,7 +300,10 @@ class User(MappedClass, ActivityNode, ActivityObject, SearchIndexable):
         results_per_page=int,
         email_address=str,
         email_format=str,
-        disable_user_messages=bool))
+        disable_user_messages=bool,
+        multifactor=bool,
+        #totp=S.Binary,
+    ))
     # Additional top-level fields can/should be accessed with get/set_pref also
     # Not sure why we didn't put them within the 'preferences' dictionary :(
     display_name = FieldPropertyDisplayName(str)

http://git-wip-us.apache.org/repos/asf/allura/blob/1848bd0e/Allura/allura/model/multifactor.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/multifactor.py b/Allura/allura/model/multifactor.py
new file mode 100644
index 0000000..6006aad
--- /dev/null
+++ b/Allura/allura/model/multifactor.py
@@ -0,0 +1,41 @@
+#       Licensed to the Apache Software Foundation (ASF) under one
+#       or more contributor license agreements.  See the NOTICE file
+#       distributed with this work for additional information
+#       regarding copyright ownership.  The ASF licenses this file
+#       to you under the Apache License, Version 2.0 (the
+#       "License"); you may not use this file except in compliance
+#       with the License.  You may obtain a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#       Unless required by applicable law or agreed to in writing,
+#       software distributed under the License is distributed on an
+#       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#       KIND, either express or implied.  See the License for the
+#       specific language governing permissions and limitations
+#       under the License.
+
+import logging
+
+from ming import schema as S
+from ming.odm import FieldProperty
+from ming.odm.declarative import MappedClass
+
+from .session import main_orm_session
+
+log = logging.getLogger(__name__)
+
+
+class TotpKey(MappedClass):
+    '''
+    For use with "mongodb" storage option
+    '''
+
+    class __mongometa__:
+        session = main_orm_session
+        name = 'multifactor_totp'
+        unique_indexes = ['user_id']
+
+    _id = FieldProperty(S.ObjectId)
+    user_id = FieldProperty(S.ObjectId, required=True)
+    key = FieldProperty(str, required=True)

http://git-wip-us.apache.org/repos/asf/allura/blob/1848bd0e/Allura/allura/templates/user_prefs.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/user_prefs.html b/Allura/allura/templates/user_prefs.html
index 74d5c08..eda4c3f 100644
--- a/Allura/allura/templates/user_prefs.html
+++ b/Allura/allura/templates/user_prefs.html
@@ -101,6 +101,34 @@
       {% endif %}
     {% endblock %}
 
+    {% block multifactor %}
+      {% if h.asbool(tg.config.get('auth.multifactor.totp', False)) %}
+        <fieldset class="preferences multifactor">
+            <legend>Multifactor Authentication</legend>
+            <p>Multifactor authentication is currently
+            {% set user_multifactor = c.user.get_pref('multifactor') %}
+            {% if user_multifactor %}
+                <strong style="color:green">enabled</strong>
+            {%- else -%}
+                <strong style="color:red">disabled</strong>
+            {%- endif -%}
+            .
+            </p>
+            <p><b class="fa fa-cog"></b> <a href="totp_new">
+                {% if user_multifactor %}
+                    Regenerate multifactor key (e.g. for a new phone).
+                {% else %}
+                    Set up multifactor authentication.
+                {% endif %}
+            </a></p>
+            {% if user_multifactor %}
+                <p><b class="fa fa-qrcode"></b> <a href="totp_view">View existing configuration</a></p>
+                <p><b class="fa fa-trash"></b> <a href="totp_disable" class="post-link">Disable</a></p>
+            {% endif %}
+        </fieldset>
+      {% endif %}
+    {% endblock %}
+
     {% block upload_key_form %}
       {% if h.asbool(tg.config.get('auth.allow_upload_ssh_key', False)) %}
         <fieldset class="preferences">
@@ -139,6 +167,11 @@
     padding: 0;
     border: 0;
   }
+  .multifactor .fa {
+      font-size: 300%;
+      vertical-align: middle;
+      margin-right: 5px;
+  }
 </style>
 {% endblock %}
 

http://git-wip-us.apache.org/repos/asf/allura/blob/1848bd0e/Allura/allura/templates/user_totp.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/user_totp.html b/Allura/allura/templates/user_totp.html
new file mode 100644
index 0000000..27710ce
--- /dev/null
+++ b/Allura/allura/templates/user_totp.html
@@ -0,0 +1,48 @@
+{#-
+       Licensed to the Apache Software Foundation (ASF) under one
+       or more contributor license agreements.  See the NOTICE file
+       distributed with this work for additional information
+       regarding copyright ownership.  The ASF licenses this file
+       to you under the Apache License, Version 2.0 (the
+       "License"); you may not use this file except in compliance
+       with the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+       Unless required by applicable law or agreed to in writing,
+       software distributed under the License is distributed on an
+       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+       KIND, either express or implied.  See the License for the
+       specific language governing permissions and limitations
+       under the License.
+-#}
+{% set hide_left_bar = True %}
+{% extends "allura:templates/user_account_base.html" %}
+
+{% block title %}{{c.user.username}} / Multifactor Authentication Setup{% endblock %}
+
+{% block header %}Multifactor Authentication Setup for {{c.user.username}}{% endblock %}
+
+{% block content %}
+  {{ super() }}
+  <div class='grid-20'>
+    {% if setup %}
+    <h2>Install App</h2>
+    <p>...</p>
+    {% endif %}
+
+    <h2>Scan this</h2>
+    <img src="{{ h.base64uri(qr) }}"/>
+
+    {% if setup %}
+    <h2>Enter the code</h2>
+    <p>
+    ...
+    <form method="POST" action="totp_set">
+        <input type="text" name="pin" autofocus/>
+        {{ lib.csrf_token() }}
+    </form>
+    </p>
+    {% endif %}
+  </div>
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/allura/blob/1848bd0e/Allura/development.ini
----------------------------------------------------------------------
diff --git a/Allura/development.ini b/Allura/development.ini
index e4e0ffc..107ddd2 100644
--- a/Allura/development.ini
+++ b/Allura/development.ini
@@ -190,6 +190,18 @@ auth.upload_ssh_url = /auth/preferences/
 ; In seconds
 auth.recovery_hash_expiry_period = 600
 
+; TOTP stands for Time-based One Time Password
+; it is the most common two-factor auth protocol, used with Google Authenticator and other phone apps
+auth.multifactor.totp = true
+; Length of codes, 6 or 8 is recommended
+auth.multifactor.totp.length = 6
+; Time window codes are valid, in seconds.  30 is recommended
+auth.multifactor.totp.time = 30
+; secret key storage location.  "filesystem" is another option (compatible with TOTP PAM plugins)
+auth.multifactor.totp.storage = mongodb
+; if using filesystem storage:
+; auth.multifactor.totp.filesystem.basedir = /var/lib/allura/totp-keys
+
 user_prefs_storage.method = local
 ; user_prefs_storage.method = ldap
 ; If using ldap, you can specify which fields to use for a preference.

http://git-wip-us.apache.org/repos/asf/allura/blob/1848bd0e/Allura/setup.py
----------------------------------------------------------------------
diff --git a/Allura/setup.py b/Allura/setup.py
index a9607f8..0f69094 100644
--- a/Allura/setup.py
+++ b/Allura/setup.py
@@ -140,6 +140,10 @@ setup(
     [allura.webhooks]
     repo-push = allura.webhooks:RepoPushWebhookSender
 
+    [allura.multifactor.totp_storage]
+    mongodb = allura.lib.plugin:MongodbTotpStorage
+    filesystem = allura.lib.plugin:FilesystemTotpStorage
+
     [paste.paster_command]
     taskd = allura.command.taskd:TaskdCommand
     taskd_cleanup = allura.command.taskd_cleanup:TaskdCleanupCommand

http://git-wip-us.apache.org/repos/asf/allura/blob/1848bd0e/Dockerfile
----------------------------------------------------------------------
diff --git a/Dockerfile b/Dockerfile
index f04b37b..7ac870c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -30,6 +30,8 @@ RUN apt-get update && apt-get install -y \
     subversion \
     python-svn \
     curl
+    # build-essential libffi-dev
+# libffi-dev ?  seemed to install ok without it
 
 # up-to-date version of node & npm
 RUN curl --silent --location https://deb.nodesource.com/setup_4.x | sudo bash - && \

http://git-wip-us.apache.org/repos/asf/allura/blob/1848bd0e/requirements.txt
----------------------------------------------------------------------
diff --git a/requirements.txt b/requirements.txt
index 4a8443f..4d93621 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -37,6 +37,7 @@ python-dateutil==1.5
 python-magic==0.4.3
 python-oembed==0.2.1
 pytz==2014.10
+qrcode==5.3
 requests==2.0.0
 oauthlib==0.4.2
 requests-oauthlib==0.4.0
@@ -53,6 +54,15 @@ WebOb==1.0.8
 # part of the stdlib, but with a version number.  see http://guide.python-distribute.org/pip.html#listing-installed-packages
 wsgiref==0.1.2
 
+# dependencies for cryptography
+cryptography==1.4
+cffi==1.7.0
+pycparser==2.14
+enum34==1.1.6
+ipaddress==1.0.16
+idna==2.1
+pyasn1==0.1.9
+
 # tg2 deps (not used directly)
 Babel==1.3
 Mako==0.3.2


[2/2] allura git commit: [#8117] post-link

Posted by br...@apache.org.
[#8117] post-link


Project: http://git-wip-us.apache.org/repos/asf/allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/allura/commit/f0b0d676
Tree: http://git-wip-us.apache.org/repos/asf/allura/tree/f0b0d676
Diff: http://git-wip-us.apache.org/repos/asf/allura/diff/f0b0d676

Branch: refs/heads/db/8117
Commit: f0b0d676a3de5d5a23780bf7cf58929c5e59e2a8
Parents: 74910c7
Author: Dave Brondsema <da...@brondsema.net>
Authored: Fri Aug 19 13:06:52 2016 -0400
Committer: Dave Brondsema <da...@brondsema.net>
Committed: Mon Aug 22 15:04:57 2016 -0400

----------------------------------------------------------------------
 Allura/allura/public/nf/js/allura-base.js               | 10 ++++++++++
 ForgeTracker/forgetracker/templates/tracker/ticket.html |  7 -------
 ForgeWiki/forgewiki/templates/wiki/master.html          |  2 +-
 ForgeWiki/forgewiki/templates/wiki/page_edit.html       |  2 +-
 ForgeWiki/forgewiki/templates/wiki/page_history.html    |  2 +-
 ForgeWiki/forgewiki/templates/wiki/page_view.html       |  2 +-
 ForgeWiki/forgewiki/wiki_main.py                        |  2 +-
 7 files changed, 15 insertions(+), 12 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/f0b0d676/Allura/allura/public/nf/js/allura-base.js
----------------------------------------------------------------------
diff --git a/Allura/allura/public/nf/js/allura-base.js b/Allura/allura/public/nf/js/allura-base.js
index 7a1d731..93d269c 100644
--- a/Allura/allura/public/nf/js/allura-base.js
+++ b/Allura/allura/public/nf/js/allura-base.js
@@ -210,4 +210,14 @@ $(function(){
 
     twemoji.size = '36x36';
     twemoji.parse($('#content_base')[0]);
+
+    $('.post-link').click(function(evt) {
+        var cval = $.cookie('_session_id');
+        evt.preventDefault();
+        $.post(this.href,
+               {_session_id:cval},
+               function(val) { window.location = val.location; },
+               'json'
+        );
+    });
 });

http://git-wip-us.apache.org/repos/asf/allura/blob/f0b0d676/ForgeTracker/forgetracker/templates/tracker/ticket.html
----------------------------------------------------------------------
diff --git a/ForgeTracker/forgetracker/templates/tracker/ticket.html b/ForgeTracker/forgetracker/templates/tracker/ticket.html
index afe0253..129c00e 100644
--- a/ForgeTracker/forgetracker/templates/tracker/ticket.html
+++ b/ForgeTracker/forgetracker/templates/tracker/ticket.html
@@ -233,13 +233,6 @@
             $('a.edit_ticket').removeClass('btn_activate');
             return false;
           });
-          $('.post-link').click(function(evt) {
-                        var cval = $.cookie('_session_id');
-                        evt.preventDefault();
-                        $.post(this.href, {_session_id:cval}, function(val)
-                                { window.location = val.location; },
-                                'json');
-          });
           // delete attachments
           $('div.attachment_thumb a.delete_attachment').click(function () {
             var cval = $.cookie('_session_id');

http://git-wip-us.apache.org/repos/asf/allura/blob/f0b0d676/ForgeWiki/forgewiki/templates/wiki/master.html
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/templates/wiki/master.html b/ForgeWiki/forgewiki/templates/wiki/master.html
index ad03778..e976399 100644
--- a/ForgeWiki/forgewiki/templates/wiki/master.html
+++ b/ForgeWiki/forgewiki/templates/wiki/master.html
@@ -35,7 +35,7 @@
 
 {% block extra_js %}
     <script type="text/javascript">
-        $('.post-link').click(function () {
+        $('.post-link-confirm').click(function () {
             var dialog_text;
             var version = $(this).data("dialog-id");
             if (version) {

http://git-wip-us.apache.org/repos/asf/allura/blob/f0b0d676/ForgeWiki/forgewiki/templates/wiki/page_edit.html
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/templates/wiki/page_edit.html b/ForgeWiki/forgewiki/templates/wiki/page_edit.html
index 204b20c..935728f 100644
--- a/ForgeWiki/forgewiki/templates/wiki/page_edit.html
+++ b/ForgeWiki/forgewiki/templates/wiki/page_edit.html
@@ -34,7 +34,7 @@
 {% block actions %}
   {{ g.icons['eye'].render(href='.', title='View Page') }}
   {% if page_exists and h.has_access(page, 'delete')() %}
-    {{ g.icons['delete'].render(extra_css='post-link') }}
+    {{ g.icons['delete'].render(extra_css='post-link-confirm') }}
     <div class="confirmation_dialog" style="display:none">
         {{ g.icons['close'].render(tag='b', extra_css='close') }}
 

http://git-wip-us.apache.org/repos/asf/allura/blob/f0b0d676/ForgeWiki/forgewiki/templates/wiki/page_history.html
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/templates/wiki/page_history.html b/ForgeWiki/forgewiki/templates/wiki/page_history.html
index dc3027a..72ab9a8 100644
--- a/ForgeWiki/forgewiki/templates/wiki/page_history.html
+++ b/ForgeWiki/forgewiki/templates/wiki/page_history.html
@@ -53,7 +53,7 @@
           <td class="tright">
             {% if i != 0 and h.has_access(p, 'edit')() %}
               {{ g.icons['revert'].render(
-                  extra_css='post-link',
+                  extra_css='post-link-confirm',
                   title='Revert to version {}'.format(p.version),
                   **{'data-dialog-id': p.version}) }}
               <div class="confirmation_dialog_{{p.version}}" style="display:none">

http://git-wip-us.apache.org/repos/asf/allura/blob/f0b0d676/ForgeWiki/forgewiki/templates/wiki/page_view.html
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/templates/wiki/page_view.html b/ForgeWiki/forgewiki/templates/wiki/page_view.html
index f770ccc..bc1d18a 100644
--- a/ForgeWiki/forgewiki/templates/wiki/page_view.html
+++ b/ForgeWiki/forgewiki/templates/wiki/page_view.html
@@ -46,7 +46,7 @@
     {% endif %}
       {{ g.icons['history'].render(href='history') }}
   {% elif h.has_access(page, 'delete')() %}
-    {{ g.icons['undelete'].render(extra_css='post-link') }}
+    {{ g.icons['undelete'].render(extra_css='post-link-confirm') }}
     <div class="confirmation_dialog" style="display:none">
         {{ g.icons['close'].render(tag='b', extra_css='close') }}
         <h1>Confirm page restoration</h1>

http://git-wip-us.apache.org/repos/asf/allura/blob/f0b0d676/ForgeWiki/forgewiki/wiki_main.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/wiki_main.py b/ForgeWiki/forgewiki/wiki_main.py
index ace38b6..b6292e4 100644
--- a/ForgeWiki/forgewiki/wiki_main.py
+++ b/ForgeWiki/forgewiki/wiki_main.py
@@ -63,7 +63,7 @@ class W:
         style='linear')
     markdown_editor = ffw.MarkdownEdit()
     confirmation = ffw.Lightbox(name='confirm',
-                                trigger='a.post-link',
+                                trigger='a.post-link-confirm',
                                 options="{ modalCSS: { minHeight: 0, width: 'inherit', top: '150px'}}")
     label_edit = ffw.LabelEdit()
     attachment_add = ffw.AttachmentAdd()