Mercurial > kallithea
changeset 8142:8f468d08f463
Merge stable
author | Mads Kiilerich <mads@kiilerich.com> |
---|---|
date | Wed, 22 Jan 2020 23:46:12 +0100 |
parents | ed67d1df7125 (current diff) 28fa94f56370 (diff) |
children | 42ef4ea26efa |
files | kallithea/__init__.py kallithea/alembic/env.py kallithea/bin/kallithea_cli_base.py kallithea/bin/kallithea_cli_ssh.py kallithea/config/app_cfg.py kallithea/controllers/admin/users.py kallithea/lib/auth.py kallithea/lib/hooks.py kallithea/lib/ssh.py kallithea/lib/utils2.py kallithea/model/db.py kallithea/model/ssh_key.py kallithea/tests/functional/test_admin_users.py kallithea/tests/functional/test_login.py kallithea/tests/functional/test_my_account.py |
diffstat | 35 files changed, 204 insertions(+), 93 deletions(-) [+] |
line wrap: on
line diff
--- a/.hgtags Sat Jan 04 00:30:21 2020 +0100 +++ b/.hgtags Wed Jan 22 23:46:12 2020 +0100 @@ -74,3 +74,4 @@ 19086c5de05f4984d7a90cd31624c45dd893f6bb 0.4.0 da65398a62fff50f3d241796cbf17acdea2092ef 0.4.1 bfa0b0a814644f0af3f492d17a9ed169cc3b89fe 0.5.0 +d01a8e92936dbd62c76505432f60efba432e9397 0.5.1
--- a/CONTRIBUTORS Sat Jan 04 00:30:21 2020 +0100 +++ b/CONTRIBUTORS Wed Jan 22 23:46:12 2020 +0100 @@ -1,11 +1,12 @@ List of contributors to Kallithea project: + Thomas De Schampheleire <thomas.de_schampheleire@nokia.com> 2014-2020 + Mads Kiilerich <mads@kiilerich.com> 2016-2020 Andrej Shadura <andrew@shadura.me> 2012 2014-2017 2019 - Thomas De Schampheleire <thomas.de_schampheleire@nokia.com> 2014-2019 Étienne Gilli <etienne.gilli@gmail.com> 2015-2017 2019 - Mads Kiilerich <mads@kiilerich.com> 2016-2019 Allan Nordhøy <epost@anotheragency.no> 2017-2019 ssantos <ssantos@web.de> 2018-2019 + Adi Kriegisch <adi@cg.tuwien.ac.at> 2019 Danni Randeris <danniranderis@gmail.com> 2019 Edmund Wong <ewong@crazy-cat.org> 2019 Elizabeth Sherrock <lizzyd710@gmail.com> 2019 @@ -15,6 +16,7 @@ Mateusz Mendel <mendelm9@gmail.com> 2019 Nathan <bonnemainsnathan@gmail.com> 2019 Oleksandr Shtalinberg <o.shtalinberg@gmail.com> 2019 + Private <adamantine.sword@gmail.com> 2019 THANOS SIOURDAKIS <siourdakisthanos@gmail.com> 2019 Wolfgang Scherer <wolfgang.scherer@gmx.de> 2019 Христо Станев <hstanev@gmail.com> 2019
--- a/development.ini Sat Jan 04 00:30:21 2020 +0100 +++ b/development.ini Wed Jan 22 23:46:12 2020 +0100 @@ -90,10 +90,12 @@ static_files = true ## Internationalization (see setup documentation for details) -## By default, the language requested by the browser is used if available. -#i18n.enabled = false -## Fallback language, empty for English (valid values are the names of subdirectories in kallithea/i18n): -i18n.lang = +## By default, the languages requested by the browser are used if available, with English as default. +## Set i18n.enabled=false to disable automatic language choice. +#i18n.enabled = true +## To Force a language, set i18n.enabled=false and specify the language in i18n.lang. +## Valid values are the names of subdirectories in kallithea/i18n with a LC_MESSAGES/kallithea.mo +#i18n.lang = en cache_dir = %(here)s/data index_dir = %(here)s/data/index
--- a/docs/conf.py Sat Jan 04 00:30:21 2020 +0100 +++ b/docs/conf.py Wed Jan 22 23:46:12 2020 +0100 @@ -47,7 +47,7 @@ # General information about the project. project = u'Kallithea' -copyright = u'2010-2019 by various authors, licensed as GPLv3.' +copyright = u'2010-2020 by various authors, licensed as GPLv3.' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the
--- a/docs/setup.rst Sat Jan 04 00:30:21 2020 +0100 +++ b/docs/setup.rst Wed Jan 22 23:46:12 2020 +0100 @@ -80,13 +80,12 @@ language, as indicated by the browser. Thus, different users may see the application in different languages. If the requested language is not available (because the translation file for that language does not yet exist or is -incomplete), the language specified in setting ``i18n.lang`` in the Kallithea -configuration file is used as fallback. If no fallback language is explicitly -specified, English is used. +incomplete), English is used. If you want to disable automatic language detection and instead configure a fixed language regardless of user preference, set ``i18n.enabled = false`` and -set ``i18n.lang`` to the desired language (or leave empty for English). +specify another language by setting ``i18n.lang`` in the Kallithea +configuration file. Using Kallithea with SSH @@ -562,7 +561,7 @@ ini = '/srv/kallithea/my.ini' from logging.config import fileConfig - fileConfig(ini) + fileConfig(ini, {'__file__': ini, 'here': '/srv/kallithea'}) from paste.deploy import loadapp application = loadapp('config:' + ini) @@ -578,7 +577,7 @@ ini = '/srv/kallithea/kallithea.ini' from logging.config import fileConfig - fileConfig(ini) + fileConfig(ini, {'__file__': ini, 'here': '/srv/kallithea'}) from paste.deploy import loadapp application = loadapp('config:' + ini)
--- a/kallithea/alembic/env.py Sat Jan 04 00:30:21 2020 +0100 +++ b/kallithea/alembic/env.py Wed Jan 22 23:46:12 2020 +0100 @@ -15,6 +15,7 @@ # Alembic migration environment (configuration). import logging +import os from logging.config import fileConfig from alembic import context @@ -43,7 +44,9 @@ # stamping during "kallithea-cli db-create"), config_file_name is not available, # and loggers are assumed to already have been configured. if config.config_file_name: - fileConfig(config.config_file_name, disable_existing_loggers=False) + fileConfig(config.config_file_name, + {'__file__': config.config_file_name, 'here': os.path.dirname(config.config_file_name)}, + disable_existing_loggers=False) def include_in_autogeneration(object, name, type, reflected, compare_to):
--- a/kallithea/alembic/versions/4851d15bc437_db_migration_step_after_95c01895c006_.py Sat Jan 04 00:30:21 2020 +0100 +++ b/kallithea/alembic/versions/4851d15bc437_db_migration_step_after_95c01895c006_.py Wed Jan 22 23:46:12 2020 +0100 @@ -31,14 +31,20 @@ def upgrade(): - meta = sa.MetaData() - meta.reflect(bind=op.get_bind()) + pass + # The following upgrade step turned out to be a bad idea. A later step + # "d7ec25b66e47_ssh_drop_usk_public_key_idx_again" will remove the index + # again if it exists ... but we shouldn't even try to create it. - if not any(i.name == 'usk_public_key_idx' for i in meta.tables['user_ssh_keys'].indexes): - with op.batch_alter_table('user_ssh_keys', schema=None) as batch_op: - batch_op.create_index('usk_public_key_idx', ['public_key'], unique=False) + #meta = sa.MetaData() + #meta.reflect(bind=op.get_bind()) + + #if not any(i.name == 'usk_public_key_idx' for i in meta.tables['user_ssh_keys'].indexes): + # with op.batch_alter_table('user_ssh_keys', schema=None) as batch_op: + # batch_op.create_index('usk_public_key_idx', ['public_key'], unique=False) def downgrade(): - with op.batch_alter_table('user_ssh_keys', schema=None) as batch_op: - batch_op.drop_index('usk_public_key_idx') + if any(i.name == 'usk_public_key_idx' for i in meta.tables['user_ssh_keys'].indexes): + with op.batch_alter_table('user_ssh_keys', schema=None) as batch_op: + batch_op.drop_index('usk_public_key_idx')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/kallithea/alembic/versions/d7ec25b66e47_ssh_drop_usk_public_key_idx_again.py Wed Jan 22 23:46:12 2020 +0100 @@ -0,0 +1,43 @@ +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +"""ssh: drop usk_public_key_idx again + +Revision ID: d7ec25b66e47 +Revises: 4851d15bc437 +Create Date: 2019-12-29 15:33:10.982003 + +""" + +# The following opaque hexadecimal identifiers ("revisions") are used +# by Alembic to track this migration script and its relations to others. +revision = 'd7ec25b66e47' +down_revision = '4851d15bc437' +branch_labels = None +depends_on = None + +import sqlalchemy as sa +from alembic import op + + +def upgrade(): + meta = sa.MetaData() + meta.reflect(bind=op.get_bind()) + + if any(i.name == 'usk_public_key_idx' for i in meta.tables['user_ssh_keys'].indexes): + with op.batch_alter_table('user_ssh_keys', schema=None) as batch_op: + batch_op.drop_index('usk_public_key_idx') + + +def downgrade(): + pass
--- a/kallithea/bin/kallithea_cli_base.py Sat Jan 04 00:30:21 2020 +0100 +++ b/kallithea/bin/kallithea_cli_base.py Wed Jan 22 23:46:12 2020 +0100 @@ -72,7 +72,8 @@ path_to_ini_file = os.path.realpath(config_file) kallithea.CONFIG = paste.deploy.appconfig('config:' + path_to_ini_file) config_string = read_config(path_to_ini_file, strip_section_prefix=annotated.__name__) - logging.config.fileConfig(io.StringIO(config_string)) + logging.config.fileConfig(io.StringIO(config_string), + {'__file__': path_to_ini_file, 'here': os.path.dirname(path_to_ini_file)}) if config_file_initialize_app: kallithea.config.middleware.make_app_without_logging(kallithea.CONFIG.global_conf, **kallithea.CONFIG.local_conf) return annotated(*args, **kwargs)
--- a/kallithea/bin/kallithea_cli_iis.py Sat Jan 04 00:30:21 2020 +0100 +++ b/kallithea/bin/kallithea_cli_iis.py Wed Jan 22 23:46:12 2020 +0100 @@ -33,7 +33,8 @@ def __ExtensionFactory__(): from paste.deploy import loadapp from logging.config import fileConfig - fileConfig('%(inifile)s') + fileConfig('%(inifile)s', {'__file__': '%(inifile)s', 'here': '%(inifiledir)s'}) + application = loadapp('config:%(inifile)s') def app(environ, start_response): @@ -75,6 +76,7 @@ with open(dispatchfile, 'w') as f: f.write(dispath_py_template % { 'inifile': config_file_abs.replace('\\', '\\\\'), + 'inifiledir': os.path.dirname(config_file_abs).replace('\\', '\\\\'), 'virtualdir': virtualdir, })
--- a/kallithea/bin/kallithea_cli_ssh.py Sat Jan 04 00:30:21 2020 +0100 +++ b/kallithea/bin/kallithea_cli_ssh.py Wed Jan 22 23:46:12 2020 +0100 @@ -24,7 +24,7 @@ from kallithea.lib.utils2 import str2bool from kallithea.lib.vcs.backends.git.ssh import GitSshHandler from kallithea.lib.vcs.backends.hg.ssh import MercurialSshHandler -from kallithea.model.ssh_key import SshKeyModel +from kallithea.model.ssh_key import SshKeyModel, SshKeyModelException log = logging.getLogger(__name__) @@ -82,5 +82,8 @@ The file is usually maintained automatically, but this command will also re-write it. """ - - SshKeyModel().write_authorized_keys() + try: + SshKeyModel().write_authorized_keys() + except SshKeyModelException as e: + sys.stderr.write("%s\n" % e) + sys.exit(1)
--- a/kallithea/config/app_cfg.py Sat Jan 04 00:30:21 2020 +0100 +++ b/kallithea/config/app_cfg.py Wed Jan 22 23:46:12 2020 +0100 @@ -98,6 +98,11 @@ # Disable transaction manager -- currently Kallithea takes care of transactions itself self['tm.enabled'] = False + # Set the i18n source language so TG doesn't search beyond 'en' in Accept-Language. + # Don't force the default here if configuration force something else. + if not self.get('i18n.lang'): + self['i18n.lang'] = 'en' + base_config = KallitheaAppConfig()
--- a/kallithea/config/middleware.py Sat Jan 04 00:30:21 2020 +0100 +++ b/kallithea/config/middleware.py Wed Jan 22 23:46:12 2020 +0100 @@ -13,8 +13,6 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. """WSGI middleware initialization for the Kallithea application.""" -import logging.config - from kallithea.config.app_cfg import base_config from kallithea.config.environment import load_environment @@ -49,5 +47,4 @@ ``app_conf`` contains all the application-specific settings (those defined under ``[app:main]``. """ - logging.config.fileConfig(global_conf['__file__']) return make_app_without_logging(global_conf, full_stack=full_stack, **app_conf)
--- a/kallithea/controllers/admin/my_account.py Sat Jan 04 00:30:21 2020 +0100 +++ b/kallithea/controllers/admin/my_account.py Wed Jan 22 23:46:12 2020 +0100 @@ -285,9 +285,9 @@ @IfSshEnabled def my_account_ssh_keys_delete(self): - public_key = request.POST.get('del_public_key') + fingerprint = request.POST.get('del_public_key_fingerprint') try: - SshKeyModel().delete(public_key, request.authuser.user_id) + SshKeyModel().delete(fingerprint, request.authuser.user_id) Session().commit() SshKeyModel().write_authorized_keys() h.flash(_("SSH key successfully deleted"), category='success')
--- a/kallithea/controllers/admin/users.py Sat Jan 04 00:30:21 2020 +0100 +++ b/kallithea/controllers/admin/users.py Wed Jan 22 23:46:12 2020 +0100 @@ -460,9 +460,9 @@ def ssh_keys_delete(self, id): c.user = self._get_user_or_raise_if_default(id) - public_key = request.POST.get('del_public_key') + fingerprint = request.POST.get('del_public_key_fingerprint') try: - SshKeyModel().delete(public_key, c.user.user_id) + SshKeyModel().delete(fingerprint, c.user.user_id) Session().commit() SshKeyModel().write_authorized_keys() h.flash(_("SSH key successfully deleted"), category='success')
--- a/kallithea/controllers/login.py Sat Jan 04 00:30:21 2020 +0100 +++ b/kallithea/controllers/login.py Wed Jan 22 23:46:12 2020 +0100 @@ -210,12 +210,10 @@ # The template needs the email address outside of the form. c.email = request.params.get('email') - + c.timestamp = request.params.get('timestamp') or '' + c.token = request.params.get('token') or '' if not request.POST: - return htmlfill.render( - render('/password_reset_confirmation.html'), - defaults=dict(request.params), - encoding='UTF-8') + return render('/password_reset_confirmation.html') form = PasswordResetConfirmationForm()() try:
--- a/kallithea/lib/auth.py Sat Jan 04 00:30:21 2020 +0100 +++ b/kallithea/lib/auth.py Wed Jan 22 23:46:12 2020 +0100 @@ -28,6 +28,7 @@ import itertools import logging import os +import string import ipaddr from decorator import decorator @@ -109,8 +110,9 @@ :param password: password :param hashed: password in hashed form """ - - if is_windows: + # sha256 hashes will always be 64 hex chars + # bcrypt hashes will always contain $ (and be shorter) + if is_windows or len(hashed) == 64 and all(x in string.hexdigits for x in hashed): return hashlib.sha256(safe_bytes(password)).hexdigest() == hashed elif is_unix: import bcrypt
--- a/kallithea/lib/hooks.py Sat Jan 04 00:30:21 2020 +0100 +++ b/kallithea/lib/hooks.py Wed Jan 22 23:46:12 2020 +0100 @@ -26,6 +26,7 @@ """ import os +import sys import time import mercurial.scmutil @@ -33,7 +34,7 @@ from kallithea.lib import helpers as h from kallithea.lib.exceptions import UserCreationError from kallithea.lib.utils import action_logger, make_ui -from kallithea.lib.utils2 import ascii_str, get_hook_environment, safe_bytes, safe_str, safe_unicode +from kallithea.lib.utils2 import HookEnvironmentError, ascii_str, get_hook_environment, safe_bytes, safe_str, safe_unicode from kallithea.lib.vcs.backends.base import EmptyChangeset from kallithea.model.db import Repository, User @@ -333,7 +334,11 @@ def handle_git_post_receive(repo_path, git_stdin_lines): """Called from Git post-receive hook""" - baseui, repo = _hook_environment(repo_path) + try: + baseui, repo = _hook_environment(repo_path) + except HookEnvironmentError as e: + sys.stderr.write("Skipping Kallithea Git post-recieve hook %r.\nGit was apparently not invoked by Kallithea: %s\n" % (sys.argv[0], e)) + return 0 # the post push hook should never use the cached instance scm_repo = repo.scm_instance_no_cache()
--- a/kallithea/lib/paster_commands/template.ini.mako Sat Jan 04 00:30:21 2020 +0100 +++ b/kallithea/lib/paster_commands/template.ini.mako Wed Jan 22 23:46:12 2020 +0100 @@ -185,10 +185,12 @@ static_files = true <%text>## Internationalization (see setup documentation for details)</%text> -<%text>## By default, the language requested by the browser is used if available.</%text> -#i18n.enabled = false -<%text>## Fallback language, empty for English (valid values are the names of subdirectories in kallithea/i18n):</%text> -i18n.lang = +<%text>## By default, the languages requested by the browser are used if available, with English as default.</%text> +<%text>## Set i18n.enabled=false to disable automatic language choice.</%text> +#i18n.enabled = true +<%text>## To Force a language, set i18n.enabled=false and specify the language in i18n.lang.</%text> +<%text>## Valid values are the names of subdirectories in kallithea/i18n with a LC_MESSAGES/kallithea.mo</%text> +#i18n.lang = en cache_dir = %(here)s/data index_dir = %(here)s/data/index
--- a/kallithea/lib/ssh.py Sat Jan 04 00:30:21 2020 +0100 +++ b/kallithea/lib/ssh.py Wed Jan 22 23:46:12 2020 +0100 @@ -48,7 +48,7 @@ >>> parse_pub_key('''AAAAB3NzaC1yc2EAAAALVGhpcyBpcyBmYWtlIQ''') Traceback (most recent call last): ... - SshKeyParseError: Incorrect SSH key - it must have both a key type and a base64 part + SshKeyParseError: Incorrect SSH key - it must have both a key type and a base64 part, like 'ssh-rsa ASRNeaZu4FA...xlJp=' >>> parse_pub_key('''abc AAAAB3NzaC1yc2EAAAALVGhpcyBpcyBmYWtlIQ''') Traceback (most recent call last): ... @@ -76,7 +76,7 @@ parts = ssh_key.split(None, 2) if len(parts) < 2: - raise SshKeyParseError(_("Incorrect SSH key - it must have both a key type and a base64 part")) + raise SshKeyParseError(_("Incorrect SSH key - it must have both a key type and a base64 part, like 'ssh-rsa ASRNeaZu4FA...xlJp='")) keytype, keyvalue, comment = (parts + [''])[:3] if keytype not in ('ssh-rsa', 'ssh-dss', 'ssh-ed25519'): @@ -99,6 +99,18 @@ SSH_OPTIONS = 'no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding' +def _safe_check(s, rec = re.compile('^[a-zA-Z0-9+/]+={0,2}$')): + """Return true if s really has the right content for base64 encoding and only contains safe characters + >>> _safe_check('asdf') + True + >>> _safe_check('as df') + False + >>> _safe_check('AAAAB3NzaC1yc2EAAAALVGhpcyBpcyBmYWtlIQ==') + True + """ + return rec.match(s) is not None + + def authorized_keys_line(kallithea_cli_path, config_file, key): """ Return a line as it would appear in .authorized_keys @@ -116,6 +128,8 @@ return '# Invalid Kallithea SSH key: %s %s\n' % (key.user.user_id, key.user_ssh_key_id) base64_key = ascii_str(base64.b64encode(key_bytes)) assert '\n' not in base64_key + if not _safe_check(base64_key): + return '# Invalid Kallithea SSH key - bad base64 encoding: %s %s\n' % (key.user.user_id, key.user_ssh_key_id) return '%s,command="%s ssh-serve -c %s %s %s" %s %s\n' % ( SSH_OPTIONS, kallithea_cli_path, config_file, key.user.user_id, key.user_ssh_key_id,
--- a/kallithea/lib/utils2.py Sat Jan 04 00:30:21 2020 +0100 +++ b/kallithea/lib/utils2.py Wed Jan 22 23:46:12 2020 +0100 @@ -430,6 +430,9 @@ return str(_url) +class HookEnvironmentError(Exception): pass + + def get_hook_environment(): """ Get hook context by deserializing the global KALLITHEA_EXTRAS environment @@ -441,15 +444,16 @@ """ try: - extras = json.loads(os.environ['KALLITHEA_EXTRAS']) + kallithea_extras = os.environ['KALLITHEA_EXTRAS'] except KeyError: - raise Exception("Environment variable KALLITHEA_EXTRAS not found") + raise HookEnvironmentError("Environment variable KALLITHEA_EXTRAS not found") + extras = json.loads(kallithea_extras) try: - for k in ['username', 'repository', 'scm', 'action', 'ip']: + for k in ['username', 'repository', 'scm', 'action', 'ip', 'config']: extras[k] except KeyError: - raise Exception('Missing key %s in KALLITHEA_EXTRAS %s' % (k, extras)) + raise HookEnvironmentError('Missing key %s in KALLITHEA_EXTRAS %s' % (k, extras)) return AttributeDict(extras)
--- a/kallithea/model/db.py Sat Jan 04 00:30:21 2020 +0100 +++ b/kallithea/model/db.py Wed Jan 22 23:46:12 2020 +0100 @@ -2523,7 +2523,6 @@ class UserSshKeys(Base, BaseDbModel): __tablename__ = 'user_ssh_keys' __table_args__ = ( - Index('usk_public_key_idx', 'public_key'), Index('usk_fingerprint_idx', 'fingerprint'), UniqueConstraint('fingerprint'), _table_args_default_dict
--- a/kallithea/model/ssh_key.py Sat Jan 04 00:30:21 2020 +0100 +++ b/kallithea/model/ssh_key.py Wed Jan 22 23:46:12 2020 +0100 @@ -30,6 +30,7 @@ from kallithea.lib import ssh from kallithea.lib.utils2 import str2bool +from kallithea.lib.vcs.exceptions import RepositoryError from kallithea.model.db import User, UserSshKeys from kallithea.model.meta import Session @@ -37,7 +38,7 @@ log = logging.getLogger(__name__) -class SshKeyModelException(Exception): +class SshKeyModelException(RepositoryError): """Exception raised by SshKeyModel methods to report errors""" @@ -72,21 +73,19 @@ return new_ssh_key - def delete(self, public_key, user=None): + def delete(self, fingerprint, user): """ - Deletes given public_key, if user is set it also filters the object for - deletion by given user. + Deletes ssh key with given fingerprint for the given user. Will raise SshKeyModelException on errors """ - ssh_key = UserSshKeys.query().filter(UserSshKeys._public_key == public_key) + ssh_key = UserSshKeys.query().filter(UserSshKeys.fingerprint == fingerprint) - if user: - user = User.guess_instance(user) - ssh_key = ssh_key.filter(UserSshKeys.user_id == user.user_id) + user = User.guess_instance(user) + ssh_key = ssh_key.filter(UserSshKeys.user_id == user.user_id) ssh_key = ssh_key.scalar() if ssh_key is None: - raise SshKeyModelException(_('SSH key %r not found') % public_key) + raise SshKeyModelException(_('SSH key with fingerprint %r found') % fingerprint) Session().delete(ssh_key) def get_ssh_keys(self, user): @@ -116,7 +115,7 @@ # Now, test that the directory is or was created in a readable way by previous. if not (os.path.isdir(authorized_keys_dir) and os.access(authorized_keys_dir, os.W_OK)): - raise Exception("Directory of authorized_keys cannot be written to so authorized_keys file %s cannot be written" % (authorized_keys)) + raise SshKeyModelException("Directory of authorized_keys cannot be written to so authorized_keys file %s cannot be written" % (authorized_keys)) # Make sure we don't overwrite a key file with important content if os.path.exists(authorized_keys): @@ -127,10 +126,11 @@ elif ssh.SSH_OPTIONS in l and ' ssh-serve ' in l: pass # Kallithea entries are ok to overwrite else: - raise Exception("Safety check failed, found %r in %s - please review and remove it" % (l.strip(), authorized_keys)) + raise SshKeyModelException("Safety check failed, found %r line in %s - please remove it if Kallithea should manage the file" % (l.strip(), authorized_keys)) fh, tmp_authorized_keys = tempfile.mkstemp('.authorized_keys', dir=os.path.dirname(authorized_keys)) with os.fdopen(fh, 'w') as f: + f.write("# WARNING: This .ssh/authorized_keys file is managed by Kallithea. Manual editing or adding new entries will make Kallithea back off.\n") for key in UserSshKeys.query().join(UserSshKeys.user).filter(User.active == True): f.write(ssh.authorized_keys_line(kallithea_cli_path, config['__file__'], key)) os.chmod(tmp_authorized_keys, stat.S_IRUSR | stat.S_IWUSR)
--- a/kallithea/templates/about.html Sat Jan 04 00:30:21 2020 +0100 +++ b/kallithea/templates/about.html Wed Jan 22 23:46:12 2020 +0100 @@ -24,12 +24,13 @@ necessarily limited to the following:</p> <ul> - <li>Copyright © 2012–2019, Mads Kiilerich</li> + <li>Copyright © 2012–2020, Mads Kiilerich</li> + <li>Copyright © 2014–2020, Thomas De Schampheleire</li> <li>Copyright © 2012, 2014–2017, 2019, Andrej Shadura</li> - <li>Copyright © 2014–2019, Thomas De Schampheleire</li> <li>Copyright © 2015–2017, 2019, Étienne Gilli</li> <li>Copyright © 2017–2019, Allan Nordhøy</li> <li>Copyright © 2018–2019, ssantos</li> + <li>Copyright © 2019, Adi Kriegisch</li> <li>Copyright © 2019, Danni Randeris</li> <li>Copyright © 2019, Edmund Wong</li> <li>Copyright © 2019, Elizabeth Sherrock</li> @@ -39,6 +40,7 @@ <li>Copyright © 2019, Mateusz Mendel</li> <li>Copyright © 2019, Nathan</li> <li>Copyright © 2019, Oleksandr Shtalinberg</li> + <li>Copyright © 2019, Private</li> <li>Copyright © 2019, THANOS SIOURDAKIS</li> <li>Copyright © 2019, Wolfgang Scherer</li> <li>Copyright © 2019, Христо Станев</li>
--- a/kallithea/templates/admin/my_account/my_account_ssh_keys.html Sat Jan 04 00:30:21 2020 +0100 +++ b/kallithea/templates/admin/my_account/my_account_ssh_keys.html Wed Jan 22 23:46:12 2020 +0100 @@ -23,7 +23,7 @@ </td> <td> ${h.form(url('my_account_ssh_keys_delete'))} - ${h.hidden('del_public_key', ssh_key.public_key)} + ${h.hidden('del_public_key_fingerprint', ssh_key.fingerprint)} <button class="btn btn-danger btn-xs" type="submit" onclick="return confirm('${_('Confirm to remove this SSH key: %s') % ssh_key.fingerprint}');"> <i class="icon-trashcan"></i>
--- a/kallithea/templates/admin/users/user_edit_ssh_keys.html Sat Jan 04 00:30:21 2020 +0100 +++ b/kallithea/templates/admin/users/user_edit_ssh_keys.html Wed Jan 22 23:46:12 2020 +0100 @@ -23,7 +23,7 @@ </td> <td> ${h.form(url('edit_user_ssh_keys_delete', id=c.user.user_id))} - ${h.hidden('del_public_key', ssh_key.public_key)} + ${h.hidden('del_public_key_fingerprint', ssh_key.fingerprint)} <button class="btn btn-danger btn-xs" type="submit" onclick="return confirm('${_('Confirm to remove this SSH key: %s') % ssh_key.fingerprint}');"> <i class="icon-trashcan"></i>
--- a/kallithea/templates/base/base.html Sat Jan 04 00:30:21 2020 +0100 +++ b/kallithea/templates/base/base.html Wed Jan 22 23:46:12 2020 +0100 @@ -23,7 +23,7 @@ <a class="navbar-link" href="${h.url('kallithea_project_url')}" target="_blank">Kallithea</a>, %endif which is - <a class="navbar-link" href="${h.canonical_url('about')}#copyright">© 2010–2019 by various authors & licensed under GPLv3</a>. + <a class="navbar-link" href="${h.canonical_url('about')}#copyright">© 2010–2020 by various authors & licensed under GPLv3</a>. %if c.issues_url: – <a class="navbar-link" href="${c.issues_url}" target="_blank">${_('Support')}</a> %endif
--- a/kallithea/templates/password_reset_confirmation.html Sat Jan 04 00:30:21 2020 +0100 +++ b/kallithea/templates/password_reset_confirmation.html Wed Jan 22 23:46:12 2020 +0100 @@ -22,13 +22,13 @@ ${h.form(h.url('reset_password_confirmation'), method='post')} <p>${_('You are about to set a new password for the email address %s.') % c.email}</p> <p>${_('Note that you must use the same browser session for this as the one used to request the password reset.')}</p> - ${h.hidden('email')} - ${h.hidden('timestamp')} + ${h.hidden('email', value=c.email)} + ${h.hidden('timestamp', value=c.timestamp)} <div class="form"> <div class="form-group"> <label class="control-label" for="token">${_('Code you received in the email')}:</label> <div> - ${h.text('token', class_='form-control')} + ${h.text('token', value=c.token, class_='form-control')} </div> </div>
--- a/kallithea/tests/functional/test_admin_users.py Sat Jan 04 00:30:21 2020 +0100 +++ b/kallithea/tests/functional/test_admin_users.py Wed Jan 22 23:46:12 2020 +0100 @@ -556,7 +556,7 @@ assert ssh_key.description == u'me@localhost' response = self.app.post(base.url('edit_user_ssh_keys_delete', id=user_id), - {'del_public_key': ssh_key.public_key, + {'del_public_key_fingerprint': ssh_key.fingerprint, '_session_csrf_secret_token': self.session_csrf_secret_token()}) self.checkSessionFlash(response, 'SSH key successfully deleted') keys = UserSshKeys.query().all()
--- a/kallithea/tests/functional/test_login.py Sat Jan 04 00:30:21 2020 +0100 +++ b/kallithea/tests/functional/test_login.py Wed Jan 22 23:46:12 2020 +0100 @@ -3,8 +3,10 @@ import time import urlparse +import mock from tg.util.webtest import test_context +import kallithea.lib.celerylib.tasks from kallithea.lib import helpers as h from kallithea.lib.auth import check_password from kallithea.lib.utils2 import generate_api_key @@ -404,18 +406,38 @@ Session().add(new) Session().commit() - response = self.app.post(base.url(controller='login', - action='password_reset'), - {'email': email, - '_session_csrf_secret_token': self.session_csrf_secret_token()}) + token = UserModel().get_reset_password_token( + User.get_by_username(username), timestamp, self.session_csrf_secret_token()) + + collected = [] + def mock_send_email(recipients, subject, body='', html_body='', headers=None, author=None): + collected.append((recipients, subject, body, html_body)) + + with mock.patch.object(kallithea.lib.celerylib.tasks, 'send_email', mock_send_email): + response = self.app.post(base.url(controller='login', + action='password_reset'), + {'email': email, + '_session_csrf_secret_token': self.session_csrf_secret_token()}) self.checkSessionFlash(response, 'A password reset confirmation code has been sent') + ((recipients, subject, body, html_body),) = collected + assert recipients == ['username@example.com'] + assert subject == 'Password reset link' + assert '\n%s\n' % token in body + (confirmation_url,) = (line for line in body.splitlines() if line.startswith('http://')) + assert ' href="%s"' % confirmation_url.replace('&', '&').replace('@', '%40') in html_body + + d = urlparse.parse_qs(urlparse.urlparse(confirmation_url).query) + assert d['token'] == [token] + assert d['timestamp'] == [str(timestamp)] + assert d['email'] == [email] + response = response.follow() # BAD TOKEN - token = "bad" + bad_token = "bad" response = self.app.post(base.url(controller='login', action='password_reset_confirmation'), @@ -423,7 +445,7 @@ 'timestamp': timestamp, 'password': "p@ssw0rd", 'password_confirm': "p@ssw0rd", - 'token': token, + 'token': bad_token, '_session_csrf_secret_token': self.session_csrf_secret_token(), }) assert response.status == '200 OK' @@ -431,20 +453,16 @@ # GOOD TOKEN - # TODO: The token should ideally be taken from the mail sent - # above, instead of being recalculated. - - token = UserModel().get_reset_password_token( - User.get_by_username(username), timestamp, self.session_csrf_secret_token()) - - response = self.app.get(base.url(controller='login', - action='password_reset_confirmation', - email=email, - timestamp=timestamp, - token=token)) + response = self.app.get(confirmation_url) assert response.status == '200 OK' response.mustcontain("You are about to set a new password for the email address %s" % email) + response.mustcontain('<form action="%s" method="post">' % base.url(controller='login', action='password_reset_confirmation')) + response.mustcontain('value="%s"' % self.session_csrf_secret_token()) + response.mustcontain('value="%s"' % token) + response.mustcontain('value="%s"' % timestamp) + response.mustcontain('value="username@example.com"') + # fake a submit of that form response = self.app.post(base.url(controller='login', action='password_reset_confirmation'), {'email': email,
--- a/kallithea/tests/functional/test_my_account.py Sat Jan 04 00:30:21 2020 +0100 +++ b/kallithea/tests/functional/test_my_account.py Wed Jan 22 23:46:12 2020 +0100 @@ -289,7 +289,7 @@ assert ssh_key.description == u'me@localhost' response = self.app.post(base.url('my_account_ssh_keys_delete'), - {'del_public_key': ssh_key.public_key, + {'del_public_key_fingerprint': ssh_key.fingerprint, '_session_csrf_secret_token': self.session_csrf_secret_token()}) self.checkSessionFlash(response, 'SSH key successfully deleted') keys = UserSshKeys.query().all()
--- a/scripts/make-release Sat Jan 04 00:30:21 2020 +0100 +++ b/scripts/make-release Wed Jan 22 23:46:12 2020 +0100 @@ -46,7 +46,7 @@ echo "Releasing Kallithea $version in directory $namerel" echo "Verify dist file content" -diff -u <((hg mani | grep -v '^\.hg') | LANG=C sort) <(tar tf dist/Kallithea-$version.tar.gz | sed "s|^$namerel/||" | grep . | grep -v '^kallithea/i18n/.*/LC_MESSAGES/kallithea.mo$\|^Kallithea.egg-info/\|^PKG-INFO$\|/$' | LANG=C sort) +diff -u <((hg mani | grep -v '^\.hg\|^kallithea/i18n/en/LC_MESSAGES/kallithea.mo$') | LANG=C sort) <(tar tf dist/Kallithea-$version.tar.gz | sed "s|^$namerel/||" | grep . | grep -v '^kallithea/i18n/.*/LC_MESSAGES/kallithea.mo$\|^Kallithea.egg-info/\|^PKG-INFO$\|/$' | LANG=C sort) echo "Verify docs build" python2 setup.py build_sphinx # the results are not actually used, but we want to make sure it builds
--- a/scripts/update-copyrights.py Sat Jan 04 00:30:21 2020 +0100 +++ b/scripts/update-copyrights.py Wed Jan 22 23:46:12 2020 +0100 @@ -100,6 +100,9 @@ for year, name in all_entries: if name in no_entries or (name, year) in no_entries: continue + parts = name.split(' <', 1) + if len(parts) == 2: + name = parts[0] + ' <' + parts[1].lower() domain = name.split('@', 1)[-1].rstrip('>') if domain in domain_extra: name_years[domain_extra[domain]].add(year)
--- a/scripts/validate-minimum-dependency-versions Sat Jan 04 00:30:21 2020 +0100 +++ b/scripts/validate-minimum-dependency-versions Wed Jan 22 23:46:12 2020 +0100 @@ -34,7 +34,7 @@ pip install -e . -r "$min_requirements" python-ldap python-pam 2> >(tee "$log" >&2) # Strip out the known Python 2.7 deprecation message. -sed -i '/DEPRECATION: Python 2\.7 will reach the end of its life/d' "$log" +sed -i '/DEPRECATION: Python 2\.7 /d' "$log" # Treat any message on stderr as a problem, for the caller to interpret. if [ -s "$log" ]; then