Changeset - a545d2274120
[Not reviewed]
default
0 7 0
Mads Kiilerich - 6 years ago 2019-07-22 03:29:45
mads@kiilerich.com
helpers: rename internal names of authentication_token to clarify that secure_form is about session CSRF secrets - not authentication
7 files changed with 16 insertions and 15 deletions:
0 comments (0 inline, 0 general)
kallithea/controllers/login.py
Show inline comments
 
@@ -234,25 +234,25 @@ class LoginController(BaseController):
 
            form_result['token'],
 
        ):
 
            return htmlfill.render(
 
                render('/password_reset_confirmation.html'),
 
                defaults=form_result,
 
                errors={'token': _('Invalid password reset token')},
 
                prefix_error=False,
 
                encoding='UTF-8')
 

	
 
        UserModel().reset_password(form_result['email'], form_result['password'])
 
        h.flash(_('Successfully updated password'), category='success')
 
        raise HTTPFound(location=url('login_home'))
 

	
 
    def logout(self):
 
        session.delete()
 
        log.info('Logging out and deleting session for user')
 
        raise HTTPFound(location=url('home'))
 

	
 
    def authentication_token(self):
 
        """Return the CSRF protection token for the session - just like it
 
        could have been screen scraped from a page with a form.
 
        Only intended for testing but might also be useful for other kinds
 
        of automation.
 
        """
 
        return h.authentication_token()
 
        return h.session_csrf_secret_token()
kallithea/lib/base.py
Show inline comments
 
@@ -345,50 +345,50 @@ class BaseVCSController(object):
 
                raise webob.exc.HTTPInternalServerError()
 

	
 
        except webob.exc.HTTPException as e:
 
            return e(environ, start_response)
 
        finally:
 
            log_ = logging.getLogger('kallithea.' + self.__class__.__name__)
 
            log_.debug('Request time: %.3fs', time.time() - start)
 
            meta.Session.remove()
 

	
 

	
 
class BaseController(TGController):
 

	
 
    def _before(self, *args, **kwargs):
 
        """
 
        _before is called before controller methods and after __call__
 
        """
 
        if request.needs_csrf_check:
 
            # CSRF protection: Whenever a request has ambient authority (whether
 
            # through a session cookie or its origin IP address), it must include
 
            # the correct token, unless the HTTP method is GET or HEAD (and thus
 
            # guaranteed to be side effect free. In practice, the only situation
 
            # where we allow side effects without ambient authority is when the
 
            # authority comes from an API key; and that is handled above.
 
            from kallithea.lib import helpers as h
 
            token = request.POST.get(h.token_key)
 
            if not token or token != h.authentication_token():
 
            token = request.POST.get(h.session_csrf_secret_name)
 
            if not token or token != h.session_csrf_secret_token():
 
                log.error('CSRF check failed')
 
                raise webob.exc.HTTPForbidden()
 

	
 
        c.kallithea_version = __version__
 
        rc_config = Setting.get_app_settings()
 

	
 
        # Visual options
 
        c.visual = AttributeDict({})
 

	
 
        ## DB stored
 
        c.visual.show_public_icon = str2bool(rc_config.get('show_public_icon'))
 
        c.visual.show_private_icon = str2bool(rc_config.get('show_private_icon'))
 
        c.visual.stylify_metalabels = str2bool(rc_config.get('stylify_metalabels'))
 
        c.visual.page_size = safe_int(rc_config.get('dashboard_items', 100))
 
        c.visual.admin_grid_items = safe_int(rc_config.get('admin_grid_items', 100))
 
        c.visual.repository_fields = str2bool(rc_config.get('repository_fields'))
 
        c.visual.show_version = str2bool(rc_config.get('show_version'))
 
        c.visual.use_gravatar = str2bool(rc_config.get('use_gravatar'))
 
        c.visual.gravatar_url = rc_config.get('gravatar_url')
 

	
 
        c.ga_code = rc_config.get('ga_code')
 
        # TODO: replace undocumented backwards compatibility hack with db upgrade and rename ga_code
 
        if c.ga_code and '<' not in c.ga_code:
 
            c.ga_code = '''<script type="text/javascript">
 
@@ -458,51 +458,51 @@ class BaseController(TGController):
 

	
 
        # User is default user (if active) or anonymous
 
        default_user = User.get_default_user(cache=True)
 
        authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr)
 
        if authuser is None: # fall back to anonymous
 
            authuser = AuthUser(dbuser=default_user) # TODO: somehow use .make?
 
        return authuser
 

	
 
    @staticmethod
 
    def _basic_security_checks():
 
        """Perform basic security/sanity checks before processing the request."""
 

	
 
        # Only allow the following HTTP request methods.
 
        if request.method not in ['GET', 'HEAD', 'POST']:
 
            raise webob.exc.HTTPMethodNotAllowed()
 

	
 
        # Also verify the _method override - no longer allowed.
 
        if request.params.get('_method') is None:
 
            pass # no override, no problem
 
        else:
 
            raise webob.exc.HTTPMethodNotAllowed()
 

	
 
        # Make sure CSRF token never appears in the URL. If so, invalidate it.
 
        from kallithea.lib import helpers as h
 
        if h.token_key in request.GET:
 
        if h.session_csrf_secret_name in request.GET:
 
            log.error('CSRF key leak detected')
 
            session.pop(h.token_key, None)
 
            session.pop(h.session_csrf_secret_name, None)
 
            session.save()
 
            h.flash(_('CSRF token leak has been detected - all form tokens have been expired'),
 
                    category='error')
 

	
 
        # WebOb already ignores request payload parameters for anything other
 
        # than POST/PUT, but double-check since other Kallithea code relies on
 
        # this assumption.
 
        if request.method not in ['POST', 'PUT'] and request.POST:
 
            log.error('%r request with payload parameters; WebOb should have stopped this', request.method)
 
            raise webob.exc.HTTPBadRequest()
 

	
 
    def __call__(self, environ, context):
 
        try:
 
            ip_addr = _get_ip_addr(environ)
 
            self._basic_security_checks()
 

	
 
            api_key = request.GET.get('api_key')
 
            try:
 
                # Request.authorization may raise ValueError on invalid input
 
                type, params = request.authorization
 
            except (ValueError, TypeError):
 
                pass
 
            else:
 
                if type.lower() == 'bearer':
kallithea/lib/helpers.py
Show inline comments
 
@@ -14,49 +14,49 @@
 
"""
 
Helper functions
 

	
 
Consists of functions to typically be used within templates, but also
 
available to Controllers. This module is available to both as 'h'.
 
"""
 
import hashlib
 
import json
 
import StringIO
 
import logging
 
import re
 
import urlparse
 
import textwrap
 

	
 
from beaker.cache import cache_region
 
from pygments.formatters.html import HtmlFormatter
 
from pygments import highlight as code_highlight
 
from tg.i18n import ugettext as _
 

	
 
from webhelpers.html import literal, HTML, escape
 
from webhelpers.html.tags import checkbox, end_form, hidden, link_to, \
 
    select, submit, text, password, textarea, radio, form as insecure_form
 
from webhelpers.number import format_byte_size
 
from webhelpers.pylonslib import Flash as _Flash
 
from webhelpers.pylonslib.secure_form import secure_form, authentication_token, token_key
 
from webhelpers.pylonslib.secure_form import secure_form, authentication_token as session_csrf_secret_token, token_key as session_csrf_secret_name
 
from webhelpers.text import chop_at, truncate, wrap_paragraphs
 
from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
 
    convert_boolean_attrs, NotGiven, _make_safe_id_component
 

	
 
from kallithea.config.routing import url
 
from kallithea.lib.annotate import annotate_highlight
 
from kallithea.lib.pygmentsutils import get_custom_lexer
 
from kallithea.lib.utils2 import str2bool, safe_unicode, safe_str, \
 
    time_to_datetime, AttributeDict, safe_int, MENTIONS_REGEX
 
from kallithea.lib.markup_renderer import url_re
 
from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError
 
from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
def canonical_url(*args, **kargs):
 
    '''Like url(x, qualified=True), but returns url that not only is qualified
 
    but also canonical, as configured in canonical_url'''
 
    from kallithea import CONFIG
 
    try:
 
        parts = CONFIG.get('canonical_url', '').split('://', 1)
 
        kargs['host'] = parts[1]
 
        kargs['protocol'] = parts[0]
 
@@ -1254,30 +1254,31 @@ def journal_filter_help():
 
            "repository:vcs*" - search everything starting with 'vcs'
 
            "repository:*vcs*" - search for repository containing 'vcs'
 

	
 
        Optional AND / OR operators in queries
 
            "repository:vcs OR repository:test"
 
            "username:test AND repository:test*"
 
    '''))
 

	
 

	
 
def not_mapped_error(repo_name):
 
    flash(_('%s repository is not mapped to db perhaps'
 
            ' it was created or renamed from the filesystem'
 
            ' please run the application again'
 
            ' in order to rescan repositories') % repo_name, category='error')
 

	
 

	
 
def ip_range(ip_addr):
 
    from kallithea.model.db import UserIpMap
 
    s, e = UserIpMap._get_ip_range(ip_addr)
 
    return '%s - %s' % (s, e)
 

	
 

	
 
def form(url, method="post", **attrs):
 
    """Like webhelpers.html.tags.form but automatically using secure_form with
 
    authentication_token for POST. authentication_token is thus never leaked
 
    in the URL."""
 
    session_csrf_secret_token for POST. The secret is thus never leaked in
 
    URLs.
 
    """
 
    if method.lower() == 'get':
 
        return insecure_form(url, method=method, **attrs)
 
    # webhelpers will turn everything but GET into POST
 
    return secure_form(url, method=method, **attrs)
kallithea/model/user.py
Show inline comments
 
@@ -317,49 +317,49 @@ class UserModel(object):
 
            msg=u'\0'.join([session_id, str(user.user_id), user.email, str(timestamp)]).encode('utf-8'),
 
            digestmod=hashlib.sha1,
 
        ).hexdigest()
 

	
 
    def send_reset_password_email(self, data):
 
        """
 
        Sends email with a password reset token and link to the password
 
        reset confirmation page with all information (including the token)
 
        pre-filled. Also returns URL of that page, only without the token,
 
        allowing users to copy-paste or manually enter the token from the
 
        email.
 
        """
 
        from kallithea.lib.celerylib import tasks
 
        from kallithea.model.notification import EmailNotificationModel
 
        import kallithea.lib.helpers as h
 

	
 
        user_email = data['email']
 
        user = User.get_by_email(user_email)
 
        timestamp = int(time.time())
 
        if user is not None:
 
            if self.can_change_password(user):
 
                log.debug('password reset user %s found', user)
 
                token = self.get_reset_password_token(user,
 
                                                      timestamp,
 
                                                      h.authentication_token())
 
                                                      h.session_csrf_secret_token())
 
                # URL must be fully qualified; but since the token is locked to
 
                # the current browser session, we must provide a URL with the
 
                # current scheme and hostname, rather than the canonical_url.
 
                link = h.url('reset_password_confirmation', qualified=True,
 
                             email=user_email,
 
                             timestamp=timestamp,
 
                             token=token)
 
            else:
 
                log.debug('password reset user %s found but was managed', user)
 
                token = link = None
 
            reg_type = EmailNotificationModel.TYPE_PASSWORD_RESET
 
            body = EmailNotificationModel().get_email_tmpl(
 
                reg_type, 'txt',
 
                user=user.short_contact,
 
                reset_token=token,
 
                reset_url=link)
 
            html_body = EmailNotificationModel().get_email_tmpl(
 
                reg_type, 'html',
 
                user=user.short_contact,
 
                reset_token=token,
 
                reset_url=link)
 
            log.debug('sending email')
 
            tasks.send_email([user_email], _("Password reset link"), body, html_body)
 
            log.info('send new password mail to %s', user_email)
 
@@ -370,49 +370,49 @@ class UserModel(object):
 
                     email=user_email,
 
                     timestamp=timestamp)
 

	
 
    def verify_reset_password_token(self, email, timestamp, token):
 
        from kallithea.lib.celerylib import tasks
 
        from kallithea.lib import auth
 
        import kallithea.lib.helpers as h
 
        user = User.get_by_email(email)
 
        if user is None:
 
            log.debug("user with email %s not found", email)
 
            return False
 

	
 
        token_age = int(time.time()) - int(timestamp)
 

	
 
        if token_age < 0:
 
            log.debug('timestamp is from the future')
 
            return False
 

	
 
        if token_age > UserModel.password_reset_token_lifetime:
 
            log.debug('password reset token expired')
 
            return False
 

	
 
        expected_token = self.get_reset_password_token(user,
 
                                                       timestamp,
 
                                                       h.authentication_token())
 
                                                       h.session_csrf_secret_token())
 
        log.debug('computed password reset token: %s', expected_token)
 
        log.debug('received password reset token: %s', token)
 
        return expected_token == token
 

	
 
    def reset_password(self, user_email, new_passwd):
 
        from kallithea.lib.celerylib import tasks
 
        from kallithea.lib import auth
 
        user = User.get_by_email(user_email)
 
        if user is not None:
 
            if not self.can_change_password(user):
 
                raise Exception('trying to change password for external user')
 
            user.password = auth.get_crypt_password(new_passwd)
 
            Session().commit()
 
            log.info('change password for %s', user_email)
 
        if new_passwd is None:
 
            raise Exception('unable to set new password')
 

	
 
        tasks.send_email([user_email],
 
                 _('Password reset notification'),
 
                 _('The password to your account %s has been changed using password reset form.') % (user.username,))
 
        log.info('send password reset mail to %s', user_email)
 

	
 
        return True
 

	
kallithea/public/js/base.js
Show inline comments
 
@@ -387,49 +387,49 @@ function asynchtml(url, $target, success
 
                }
 
            })
 
        .fail(function(jqXHR, textStatus, errorThrown) {
 
                if (textStatus == "abort")
 
                    return;
 
                $target.html('<span class="bg-danger">ERROR: {0}</span>'.format(textStatus));
 
                $target.css('opacity','1.0');
 
            })
 
        ;
 
};
 

	
 
var ajaxGET = function(url, success, failure) {
 
    if(failure === undefined) {
 
        failure = function(jqXHR, textStatus, errorThrown) {
 
                if (textStatus != "abort")
 
                    alert("Ajax GET error: " + textStatus);
 
            };
 
    }
 
    return $.ajax({url: url, headers: {'X-PARTIAL-XHR': '1'}, cache: false})
 
        .done(success)
 
        .fail(failure);
 
};
 

	
 
var ajaxPOST = function(url, postData, success, failure) {
 
    postData['_authentication_token'] = _authentication_token;
 
    postData['_authentication_token'] = _session_csrf_secret_token;
 
    var postData = _toQueryString(postData);
 
    if(failure === undefined) {
 
        failure = function(jqXHR, textStatus, errorThrown) {
 
                if (textStatus != "abort")
 
                    alert("Error posting to server: " + textStatus);
 
            };
 
    }
 
    return $.ajax({url: url, data: postData, type: 'POST', headers: {'X-PARTIAL-XHR': '1'}, cache: false})
 
        .done(success)
 
        .fail(failure);
 
};
 

	
 

	
 
/**
 
 * activate .show_more links
 
 * the .show_more must have an id that is the the id of an element to hide prefixed with _
 
 * the parentnode will be displayed
 
 */
 
var show_more_event = function(){
 
    $('.show_more').click(function(e){
 
        var el = e.currentTarget;
 
        $('#' + el.id.substring(1)).hide();
 
        $(el.parentNode).show();
 
    });
 
@@ -437,57 +437,57 @@ var show_more_event = function(){
 

	
 

	
 
var _onSuccessFollow = function(target){
 
    var $target = $(target);
 
    var $f_cnt = $('#current_followers_count');
 
    if ($target.hasClass('follow')) {
 
        $target.removeClass('follow').addClass('following');
 
        $target.prop('title', _TM['Stop following this repository']);
 
        if ($f_cnt.html()) {
 
            var cnt = Number($f_cnt.html())+1;
 
            $f_cnt.html(cnt);
 
        }
 
    } else {
 
        $target.removeClass('following').addClass('follow');
 
        $target.prop('title', _TM['Start following this repository']);
 
        if ($f_cnt.html()) {
 
            var cnt = Number($f_cnt.html())-1;
 
            $f_cnt.html(cnt);
 
        }
 
    }
 
}
 

	
 
var toggleFollowingRepo = function(target, follows_repository_id){
 
    var args = 'follows_repository_id=' + follows_repository_id;
 
    args += '&amp;_authentication_token=' + _authentication_token;
 
    args += '&amp;_authentication_token=' + _session_csrf_secret_token;
 
    $.post(TOGGLE_FOLLOW_URL, args, function(data){
 
            _onSuccessFollow(target);
 
        });
 
    return false;
 
};
 

	
 
var showRepoSize = function(target, repo_name){
 
    var args = '_authentication_token=' + _authentication_token;
 
    var args = '_authentication_token=' + _session_csrf_secret_token;
 

	
 
    if(!$("#" + target).hasClass('loaded')){
 
        $("#" + target).html(_TM['Loading ...']);
 
        var url = pyroutes.url('repo_size', {"repo_name":repo_name});
 
        $.post(url, args, function(data) {
 
            $("#" + target).html(data);
 
            $("#" + target).addClass('loaded');
 
        });
 
    }
 
    return false;
 
};
 

	
 
/**
 
 * load tooltips dynamically based on data attributes, used for .lazy-cs changeset links
 
 */
 
var get_changeset_tooltip = function() {
 
    var $target = $(this);
 
    var tooltip = $target.data('tooltip');
 
    if (!tooltip) {
 
        var raw_id = $target.data('raw_id');
 
        var repo_name = $target.data('repo_name');
 
        var url = pyroutes.url('changeset_info', {"repo_name": repo_name, "revision": raw_id});
 

	
 
        $.ajax(url, {
kallithea/templates/admin/gists/edit.html
Show inline comments
 
@@ -132,43 +132,43 @@
 
                        // set mode on page load
 
                        var detected_mode = CodeMirror.findModeByExtension(${h.js(file.extension)});
 

	
 
                        if (detected_mode){
 
                            setCodeMirrorMode(myCodeMirror, detected_mode);
 
                            $mimetype_select.val(detected_mode.mime);
 
                        }
 
                    });
 
                </script>
 

	
 
            %endfor
 

	
 
            <div>
 
            ${h.submit('update',_('Update Gist'),class_="btn btn-success")}
 
            <a class="btn btn-default" href="${h.url('gist', gist_id=c.gist.gist_access_id)}">${_('Cancel')}</a>
 
            </div>
 
          ${h.end_form()}
 
          <script>
 
              $('#update').on('click', function(e){
 
                  e.preventDefault();
 

	
 
                  // check for newer version.
 
                  $.ajax({
 
                    url: ${h.js(h.url('edit_gist_check_revision', gist_id=c.gist.gist_access_id))},
 
                    data: {'revision': ${h.js(c.file_changeset.raw_id)}, '_authentication_token': _authentication_token},
 
                    data: {'revision': ${h.js(c.file_changeset.raw_id)}, '_authentication_token': _session_csrf_secret_token},
 
                    dataType: 'json',
 
                    type: 'POST',
 
                    success: function(data) {
 
                      if(data.success == false){
 
                          $('#edit_error').show();
 
                      }
 
                      else{
 
                        $('#eform').submit();
 
                      }
 
                    }
 
                  });
 
              });
 
          </script>
 
        </div>
 
    </div>
 

	
 
</div>
 
</%def>
kallithea/templates/base/root.html
Show inline comments
 
@@ -44,49 +44,49 @@
 
                'Expand Diff': ${h.jshtml(_('Expand Diff'))},
 
                'No revisions': ${h.jshtml(_('No revisions'))},
 
                'Type name of user or member to grant permission': ${h.jshtml(_('Type name of user or member to grant permission'))},
 
                'Failed to revoke permission': ${h.jshtml(_('Failed to revoke permission'))},
 
                'Confirm to revoke permission for {0}: {1} ?': ${h.jshtml(_('Confirm to revoke permission for {0}: {1} ?'))},
 
                'Enabled': ${h.jshtml(_('Enabled'))},
 
                'Disabled': ${h.jshtml(_('Disabled'))},
 
                'Select changeset': ${h.jshtml(_('Select changeset'))},
 
                'Specify changeset': ${h.jshtml(_('Specify changeset'))},
 
                'MSG_SORTASC': ${h.jshtml(_('Click to sort ascending'))},
 
                'MSG_SORTDESC': ${h.jshtml(_('Click to sort descending'))},
 
                'MSG_EMPTY': ${h.jshtml(_('No records found.'))},
 
                'MSG_ERROR': ${h.jshtml(_('Data error.'))},
 
                'MSG_LOADING': ${h.jshtml(_('Loading...'))}
 
            };
 
            var _TM = TRANSLATION_MAP;
 

	
 
            var TOGGLE_FOLLOW_URL  = ${h.js(h.url('toggle_following'))};
 

	
 
            var REPO_NAME = "";
 
            %if hasattr(c, 'repo_name'):
 
                var REPO_NAME = ${h.js(c.repo_name)};
 
            %endif
 

	
 
            var _authentication_token = ${h.js(h.authentication_token())};
 
            var _session_csrf_secret_token = ${h.js(h.session_csrf_secret_token())};
 
        </script>
 
        <script type="text/javascript" src="${h.url('/js/jquery.min.js', ver=c.kallithea_version)}"></script>
 
        <script type="text/javascript" src="${h.url('/js/jquery.dataTables.js', ver=c.kallithea_version)}"></script>
 
        <script type="text/javascript" src="${h.url('/js/dataTables.bootstrap.js', ver=c.kallithea_version)}"></script>
 
        <script type="text/javascript" src="${h.url('/js/bootstrap.js', ver=c.kallithea_version)}"></script>
 
        <script type="text/javascript" src="${h.url('/js/select2.js', ver=c.kallithea_version)}"></script>
 
        <script type="text/javascript" src="${h.url('/js/jquery.caret.min.js', ver=c.kallithea_version)}"></script>
 
        <script type="text/javascript" src="${h.url('/js/jquery.atwho.min.js', ver=c.kallithea_version)}"></script>
 
        <script type="text/javascript" src="${h.url('/js/base.js', ver=c.kallithea_version)}"></script>
 
        ## EXTRA FOR JS
 
        <%block name="js_extra"/>
 
        <script type="text/javascript">
 
            $(document).ready(function(){
 
              tooltip_activate();
 
              show_more_event();
 
              // routes registration
 
              pyroutes.register('home', ${h.js(h.url('home'))}, []);
 
              pyroutes.register('new_gist', ${h.js(h.url('new_gist'))}, []);
 
              pyroutes.register('gists', ${h.js(h.url('gists'))}, []);
 
              pyroutes.register('new_repo', ${h.js(h.url('new_repo'))}, []);
 

	
 
              pyroutes.register('summary_home', ${h.js(h.url('summary_home', repo_name='%(repo_name)s'))}, ['repo_name']);
 
              pyroutes.register('changelog_home', ${h.js(h.url('changelog_home', repo_name='%(repo_name)s'))}, ['repo_name']);
 
              pyroutes.register('files_home', ${h.js(h.url('files_home', repo_name='%(repo_name)s',revision='%(revision)s',f_path='%(f_path)s'))}, ['repo_name', 'revision', 'f_path']);
0 comments (0 inline, 0 general)