From 7b84ecc1aac6b93553cc815a2b8853570dcd8953 Mon Sep 17 00:00:00 2001 From: amercader Date: Fri, 19 Jul 2019 11:39:26 +0200 Subject: [PATCH 01/11] [#4796] Move repoze.who plugin to own folder --- ckan/config/who.ini | 2 +- ckan/lib/{ => repoze_plugins}/auth_tkt.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename ckan/lib/{ => repoze_plugins}/auth_tkt.py (100%) diff --git a/ckan/config/who.ini b/ckan/config/who.ini index 885a497b5b4..9758bf40cb7 100644 --- a/ckan/config/who.ini +++ b/ckan/config/who.ini @@ -1,5 +1,5 @@ [plugin:auth_tkt] -use = ckan.lib.auth_tkt:make_plugin +use = ckan.lib.repoze_plugins.auth_tkt:make_plugin # If no secret key is defined here, beaker.session.secret will be used #secret = somesecret diff --git a/ckan/lib/auth_tkt.py b/ckan/lib/repoze_plugins/auth_tkt.py similarity index 100% rename from ckan/lib/auth_tkt.py rename to ckan/lib/repoze_plugins/auth_tkt.py From df84d8f1d93e96e8192a4a8fc8724c47c0e1b46c Mon Sep 17 00:00:00 2001 From: amercader Date: Fri, 19 Jul 2019 11:40:01 +0200 Subject: [PATCH 02/11] [#4796] Cleanup who.ini --- ckan/config/who.ini | 5 ----- 1 file changed, 5 deletions(-) diff --git a/ckan/config/who.ini b/ckan/config/who.ini index 9758bf40cb7..b4a909f9194 100644 --- a/ckan/config/who.ini +++ b/ckan/config/who.ini @@ -13,10 +13,6 @@ post_login_url = /user/logged_in post_logout_url = /user/logged_out charset = utf-8 -#[plugin:basicauth] -#use = repoze.who.plugins.basicauth:make_plugin -#realm = 'CKAN' - [general] request_classifier = repoze.who.classifiers:default_request_classifier challenge_decider = repoze.who.classifiers:default_challenge_decider @@ -34,4 +30,3 @@ plugins = [challengers] plugins = friendlyform;browser -# basicauth From b23fe3efcf19b8a89eb08e7941dbe5bab024814a Mon Sep 17 00:00:00 2001 From: amercader Date: Fri, 19 Jul 2019 12:57:26 +0200 Subject: [PATCH 03/11] [#4796] Include FriendlyFormPlugin source --- ckan/config/who.ini | 2 +- ckan/lib/repoze_plugins/__init__.py | 0 ckan/lib/repoze_plugins/friendly_form.py | 306 +++++++++++++++++++++++ 3 files changed, 307 insertions(+), 1 deletion(-) create mode 100644 ckan/lib/repoze_plugins/__init__.py create mode 100644 ckan/lib/repoze_plugins/friendly_form.py diff --git a/ckan/config/who.ini b/ckan/config/who.ini index b4a909f9194..eb2a5a52c80 100644 --- a/ckan/config/who.ini +++ b/ckan/config/who.ini @@ -4,7 +4,7 @@ use = ckan.lib.repoze_plugins.auth_tkt:make_plugin #secret = somesecret [plugin:friendlyform] -use = repoze.who.plugins.friendlyform:FriendlyFormPlugin +use = ckan.lib.repoze_plugins.friendly_form:FriendlyFormPlugin login_form_url= /user/login login_handler_path = /login_generic logout_handler_path = /user/logout diff --git a/ckan/lib/repoze_plugins/__init__.py b/ckan/lib/repoze_plugins/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ckan/lib/repoze_plugins/friendly_form.py b/ckan/lib/repoze_plugins/friendly_form.py new file mode 100644 index 00000000000..cf1efa6dd66 --- /dev/null +++ b/ckan/lib/repoze_plugins/friendly_form.py @@ -0,0 +1,306 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (c) 2009-2010, Gustavo Narea and contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the BSD-like license at +# http://www.repoze.org/LICENSE.txt. A copy of the license should accompany +# this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL +# EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND +# FITNESS FOR A PARTICULAR PURPOSE. +# +############################################################################## + +"""Collection of :mod:`repoze.who` friendly forms""" + +from urlparse import urlparse, urlunparse +from urllib import urlencode +try: + from urlparse import parse_qs +except ImportError:#pragma: no cover + from cgi import parse_qs + +from webob import Request +# TODO: Stop using Paste; we already started using WebOb +from webob.exc import HTTPFound, HTTPUnauthorized +from paste.request import construct_url, parse_dict_querystring, parse_formvars +from zope.interface import implements + +from repoze.who.interfaces import IChallenger, IIdentifier + +__all__ = ['FriendlyFormPlugin'] + + +class FriendlyFormPlugin(object): + """ + :class:`RedirectingFormPlugin + `-like form plugin with + more features. + + It is like ``RedirectingFormPlugin``, but provides us with the following + features: + + * Users are not challenged on logout, unless the referrer URL is a + private one (but that's up to the application). + * Developers may define post-login and/or post-logout pages. + * In the login URL, the amount of failed logins is available in the + environ. It's also increased by one on every login try. This counter + will allow developers not using a post-login page to handle logins that + fail/succeed. + + You should keep in mind that if you're using a post-login or a post-logout + page, that page will receive the referrer URL as a query string variable + whose name is "came_from". + + Forms can be submitted with any encoding (non-ASCII credentials are + supported) and ISO-8859-1 (aka "Latin-1") is the default one. + + """ + implements(IChallenger, IIdentifier) + + classifications = { + IIdentifier: ["browser"], + IChallenger: ["browser"], + } + + def __init__(self, login_form_url, login_handler_path, post_login_url, + logout_handler_path, post_logout_url, rememberer_name, + login_counter_name=None, charset="iso-8859-1"): + """ + + :param login_form_url: The URL/path where the login form is located. + :type login_form_url: str + :param login_handler_path: The URL/path where the login form is + submitted to (where it is processed by this plugin). + :type login_handler_path: str + :param post_login_url: The URL/path where the user should be redirected + to after login (even if wrong credentials were provided). + :type post_login_url: str + :param logout_handler_path: The URL/path where the user is logged out. + :type logout_handler_path: str + :param post_logout_url: The URL/path where the user should be + redirected to after logout. + :type post_logout_url: str + :param rememberer_name: The name of the repoze.who identifier which + acts as rememberer. + :type rememberer_name: str + :param login_counter_name: The name of the query string variable which + will represent the login counter. + :type login_counter_name: str + :param charset: The character encoding to be assumed when the user + agent does not submit the form with an explicit charset. + :type charset: :class:`str` + + The login counter variable's name will be set to ``__logins`` if + ``login_counter_name`` equals None. + + .. versionchanged:: 1.0.1 + Added the ``charset`` argument. + + """ + self.login_form_url = login_form_url + self.login_handler_path = login_handler_path + self.post_login_url = post_login_url + self.logout_handler_path = logout_handler_path + self.post_logout_url = post_logout_url + self.rememberer_name = rememberer_name + self.login_counter_name = login_counter_name + if not login_counter_name: + self.login_counter_name = '__logins' + self.charset = charset + + # IIdentifier + def identify(self, environ): + """ + Override the parent's identifier to introduce a login counter + (possibly along with a post-login page) and load the login counter into + the ``environ``. + + """ + request = Request(environ, charset=self.charset) + + path_info = environ['PATH_INFO'] + script_name = environ.get('SCRIPT_NAME') or '/' + query = request.GET + + if path_info == self.login_handler_path: + ## We are on the URL where repoze.who processes authentication. ## + # Let's append the login counter to the query string of the + # "came_from" URL. It will be used by the challenge below if + # authorization is denied for this request. + form = dict(request.POST) + form.update(query) + try: + login = form['login'] + password = form['password'] + except KeyError: + credentials = None + else: + if request.charset == "us-ascii": + credentials = { + 'login': str(login), + 'password': str(password), + } + else: + credentials = {'login': login,'password': password} + + try: + credentials['max_age'] = form['remember'] + except KeyError: + pass + + referer = environ.get('HTTP_REFERER', script_name) + destination = form.get('came_from', referer) + + if self.post_login_url: + # There's a post-login page, so we have to replace the + # destination with it. + destination = self._get_full_path(self.post_login_url, + environ) + if 'came_from' in query: + # There's a referrer URL defined, so we have to pass it to + # the post-login page as a GET variable. + destination = self._insert_qs_variable(destination, + 'came_from', + query['came_from']) + failed_logins = self._get_logins(environ, True) + new_dest = self._set_logins_in_url(destination, failed_logins) + environ['repoze.who.application'] = HTTPFound(location=new_dest) + return credentials + + elif path_info == self.logout_handler_path: + ## We are on the URL where repoze.who logs the user out. ## + form = parse_formvars(environ) + form.update(query) + referer = environ.get('HTTP_REFERER', script_name) + came_from = form.get('came_from', referer) + # set in environ for self.challenge() to find later + environ['came_from'] = came_from + environ['repoze.who.application'] = HTTPUnauthorized() + return None + + elif path_info == self.login_form_url or self._get_logins(environ): + ## We are on the URL that displays the from OR any other page ## + ## where the login counter is included in the query string. ## + # So let's load the counter into the environ and then hide it from + # the query string (it will cause problems in frameworks like TG2, + # where this unexpected variable would be passed to the controller) + environ['repoze.who.logins'] = self._get_logins(environ, True) + # Hiding the GET variable in the environ: + if self.login_counter_name in query: + del query[self.login_counter_name] + environ['QUERY_STRING'] = urlencode(query, doseq=True) + + # IChallenger + def challenge(self, environ, status, app_headers, forget_headers): + """ + Override the parent's challenge to avoid challenging the user on + logout, introduce a post-logout page and/or pass the login counter + to the login form. + + """ + url_parts = list(urlparse(self.login_form_url)) + query = url_parts[4] + query_elements = parse_qs(query) + came_from = environ.get('came_from', construct_url(environ)) + query_elements['came_from'] = came_from + url_parts[4] = urlencode(query_elements, doseq=True) + login_form_url = urlunparse(url_parts) + login_form_url = self._get_full_path(login_form_url, environ) + destination = login_form_url + # Configuring the headers to be set: + cookies = [(h,v) for (h,v) in app_headers if h.lower() == 'set-cookie'] + headers = forget_headers + cookies + + if environ['PATH_INFO'] == self.logout_handler_path: + # Let's log the user out without challenging. + came_from = environ.get('came_from') + if self.post_logout_url: + # Redirect to a predefined "post logout" URL. + destination = self._get_full_path(self.post_logout_url, + environ) + if came_from: + destination = self._insert_qs_variable( + destination, 'came_from', came_from) + else: + # Redirect to the referrer URL. + script_name = environ.get('SCRIPT_NAME', '') + destination = came_from or script_name or '/' + + elif 'repoze.who.logins' in environ: + # Login failed! Let's redirect to the login form and include + # the login counter in the query string + environ['repoze.who.logins'] += 1 + # Re-building the URL: + destination = self._set_logins_in_url(destination, + environ['repoze.who.logins']) + + return HTTPFound(location=destination, headers=headers) + + # IIdentifier + def remember(self, environ, identity): + rememberer = self._get_rememberer(environ) + return rememberer.remember(environ, identity) + + # IIdentifier + def forget(self, environ, identity): + rememberer = self._get_rememberer(environ) + return rememberer.forget(environ, identity) + + def _get_rememberer(self, environ): + rememberer = environ['repoze.who.plugins'][self.rememberer_name] + return rememberer + + def _get_full_path(self, path, environ): + """ + Return the full path to ``path`` by prepending the SCRIPT_NAME. + + If ``path`` is a URL, do nothing. + + """ + if path.startswith('/'): + path = environ.get('SCRIPT_NAME', '') + path + return path + + def _get_logins(self, environ, force_typecast=False): + """ + Return the login counter from the query string in the ``environ``. + + If it's not possible to convert it into an integer and + ``force_typecast`` is ``True``, it will be set to zero (int(0)). + Otherwise, it will be ``None`` or an string. + + """ + variables = parse_dict_querystring(environ) + failed_logins = variables.get(self.login_counter_name) + if force_typecast: + try: + failed_logins = int(failed_logins) + except (ValueError, TypeError): + failed_logins = 0 + return failed_logins + + def _set_logins_in_url(self, url, logins): + """ + Insert the login counter variable with the ``logins`` value into + ``url`` and return the new URL. + + """ + return self._insert_qs_variable(url, self.login_counter_name, logins) + + def _insert_qs_variable(self, url, var_name, var_value): + """ + Insert the variable ``var_name`` with value ``var_value`` in the query + string of ``url`` and return the new URL. + + """ + url_parts = list(urlparse(url)) + query_parts = parse_qs(url_parts[4]) + query_parts[var_name] = var_value + url_parts[4] = urlencode(query_parts, doseq=True) + return urlunparse(url_parts) + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, id(self)) From 0e1b68ac06351dceff2a22d61006f793737189d9 Mon Sep 17 00:00:00 2001 From: amercader Date: Fri, 19 Jul 2019 13:41:55 +0200 Subject: [PATCH 04/11] [#4796] Update friendly form plugin * Change urllib imports to six ones * Replace usage of paster methods --- ckan/lib/repoze_plugins/friendly_form.py | 27 ++++++++++++++---------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/ckan/lib/repoze_plugins/friendly_form.py b/ckan/lib/repoze_plugins/friendly_form.py index cf1efa6dd66..9d3a0df7088 100644 --- a/ckan/lib/repoze_plugins/friendly_form.py +++ b/ckan/lib/repoze_plugins/friendly_form.py @@ -1,4 +1,10 @@ # -*- coding: utf-8 -*- +''' +This is a modified version of repoze.who-friendlyform, written by +Gustavo Narea +''' + + ############################################################################## # # Copyright (c) 2009-2010, Gustavo Narea and contributors. @@ -15,17 +21,10 @@ """Collection of :mod:`repoze.who` friendly forms""" -from urlparse import urlparse, urlunparse -from urllib import urlencode -try: - from urlparse import parse_qs -except ImportError:#pragma: no cover - from cgi import parse_qs +from six.moves.urllib.parse import urlparse, urlunparse, urlencode, parse_qs -from webob import Request -# TODO: Stop using Paste; we already started using WebOb +from webob import Request, UnicodeMultiDict from webob.exc import HTTPFound, HTTPUnauthorized -from paste.request import construct_url, parse_dict_querystring, parse_formvars from zope.interface import implements from repoze.who.interfaces import IChallenger, IIdentifier @@ -33,6 +32,10 @@ __all__ = ['FriendlyFormPlugin'] +def construct_url(environ): + return Request(environ).url + + class FriendlyFormPlugin(object): """ :class:`RedirectingFormPlugin @@ -172,7 +175,9 @@ def identify(self, environ): elif path_info == self.logout_handler_path: ## We are on the URL where repoze.who logs the user out. ## - form = parse_formvars(environ) + r = Request(environ) + params = dict(list(r.GET.items()) + list(r.POST.items())) + form = UnicodeMultiDict(params) form.update(query) referer = environ.get('HTTP_REFERER', script_name) came_from = form.get('came_from', referer) @@ -273,7 +278,7 @@ def _get_logins(self, environ, force_typecast=False): Otherwise, it will be ``None`` or an string. """ - variables = parse_dict_querystring(environ) + variables = Request(environ).queryvars failed_logins = variables.get(self.login_counter_name) if force_typecast: try: From f5662fa4ee18f5865fd5b6dbb7a51214fd00441b Mon Sep 17 00:00:00 2001 From: amercader Date: Fri, 19 Jul 2019 13:50:25 +0200 Subject: [PATCH 05/11] [#4796] PEP8 --- ckan/lib/repoze_plugins/friendly_form.py | 35 ++++++++++++++---------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/ckan/lib/repoze_plugins/friendly_form.py b/ckan/lib/repoze_plugins/friendly_form.py index 9d3a0df7088..533fc5afb8e 100644 --- a/ckan/lib/repoze_plugins/friendly_form.py +++ b/ckan/lib/repoze_plugins/friendly_form.py @@ -1,13 +1,18 @@ # -*- coding: utf-8 -*- -''' -This is a modified version of repoze.who-friendlyform, written by -Gustavo Narea -''' +# +# This is a modified version of repoze.who-friendlyform, written by +# Gustavo Narea +# +# Modifications include: +# * Python 3 support +# * Replace usage of paster methods with webob ones +# ############################################################################## # -# Copyright (c) 2009-2010, Gustavo Narea and contributors. +# Copyright (c) 2009-2010, Gustavo Narea and +# contributors. # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at @@ -66,7 +71,7 @@ class FriendlyFormPlugin(object): classifications = { IIdentifier: ["browser"], IChallenger: ["browser"], - } + } def __init__(self, login_form_url, login_handler_path, post_login_url, logout_handler_path, post_logout_url, rememberer_name, @@ -129,7 +134,7 @@ def identify(self, environ): query = request.GET if path_info == self.login_handler_path: - ## We are on the URL where repoze.who processes authentication. ## + # We are on the URL where repoze.who processes authentication. # # Let's append the login counter to the query string of the # "came_from" URL. It will be used by the challenge below if # authorization is denied for this request. @@ -145,9 +150,9 @@ def identify(self, environ): credentials = { 'login': str(login), 'password': str(password), - } + } else: - credentials = {'login': login,'password': password} + credentials = {'login': login, 'password': password} try: credentials['max_age'] = form['remember'] @@ -174,7 +179,7 @@ def identify(self, environ): return credentials elif path_info == self.logout_handler_path: - ## We are on the URL where repoze.who logs the user out. ## + # We are on the URL where repoze.who logs the user out. # r = Request(environ) params = dict(list(r.GET.items()) + list(r.POST.items())) form = UnicodeMultiDict(params) @@ -187,8 +192,8 @@ def identify(self, environ): return None elif path_info == self.login_form_url or self._get_logins(environ): - ## We are on the URL that displays the from OR any other page ## - ## where the login counter is included in the query string. ## + # We are on the URL that displays the from OR any other page # + # where the login counter is included in the query string. # # So let's load the counter into the environ and then hide it from # the query string (it will cause problems in frameworks like TG2, # where this unexpected variable would be passed to the controller) @@ -216,7 +221,9 @@ def challenge(self, environ, status, app_headers, forget_headers): login_form_url = self._get_full_path(login_form_url, environ) destination = login_form_url # Configuring the headers to be set: - cookies = [(h,v) for (h,v) in app_headers if h.lower() == 'set-cookie'] + cookies = [ + (h, v) for (h, v) in app_headers if h.lower() == 'set-cookie' + ] headers = forget_headers + cookies if environ['PATH_INFO'] == self.logout_handler_path: @@ -228,7 +235,7 @@ def challenge(self, environ, status, app_headers, forget_headers): environ) if came_from: destination = self._insert_qs_variable( - destination, 'came_from', came_from) + destination, 'came_from', came_from) else: # Redirect to the referrer URL. script_name = environ.get('SCRIPT_NAME', '') From f2bb3dd78b966ec5cc1feb7b8e2685ac1aa6b8c4 Mon Sep 17 00:00:00 2001 From: Jinil Lee Date: Wed, 24 Jul 2019 23:53:31 +0900 Subject: [PATCH 06/11] add nav icon There is no nav icon because the mapper's ckan_icon attribute is not used. --- ckan/templates/group/edit_base.html | 4 ++-- ckan/templates/group/read_base.html | 6 +++--- ckan/templates/organization/edit_base.html | 6 +++--- ckan/templates/organization/read_base.html | 6 +++--- ckan/templates/package/edit_base.html | 4 ++-- ckan/templates/package/read_base.html | 6 +++--- ckan/templates/package/resource_edit_base.html | 4 ++-- ckan/templates/user/dashboard.html | 8 ++++---- ckan/templates/user/read_base.html | 4 ++-- 9 files changed, 24 insertions(+), 24 deletions(-) diff --git a/ckan/templates/group/edit_base.html b/ckan/templates/group/edit_base.html index 2c2e5ed3e75..1d7d2c59b68 100644 --- a/ckan/templates/group/edit_base.html +++ b/ckan/templates/group/edit_base.html @@ -17,8 +17,8 @@ {% endblock %} {% block content_primary_nav %} - {{ h.build_nav_icon(group_type + '.edit', _('Edit'), id=group_dict.name) }} - {{ h.build_nav_icon(group_type + '.members', _('Members'), id=group_dict.name) }} + {{ h.build_nav_icon(group_type + '.edit', _('Edit'), id=group_dict.name, icon='pencil-square-o') }} + {{ h.build_nav_icon(group_type + '.members', _('Members'), id=group_dict.name, icon='users') }} {% endblock %} {% block secondary_content %} diff --git a/ckan/templates/group/read_base.html b/ckan/templates/group/read_base.html index f98b135175b..948be560086 100644 --- a/ckan/templates/group/read_base.html +++ b/ckan/templates/group/read_base.html @@ -14,9 +14,9 @@ {% endblock %} {% block content_primary_nav %} - {{ h.build_nav_icon(group_type + '.read', _('Datasets'), id=group_dict.name) }} - {{ h.build_nav_icon(group_type + '.activity', _('Activity Stream'), id=group_dict.name, offset=0) }} - {{ h.build_nav_icon(group_type + '.about', _('About'), id=group_dict.name) }} + {{ h.build_nav_icon(group_type + '.read', _('Datasets'), id=group_dict.name, icon='sitemap') }} + {{ h.build_nav_icon(group_type + '.activity', _('Activity Stream'), id=group_dict.name, offset=0, icon='clock-o') }} + {{ h.build_nav_icon(group_type + '.about', _('About'), id=group_dict.name, icon='info-circle') }} {% endblock %} {% block secondary_content %} diff --git a/ckan/templates/organization/edit_base.html b/ckan/templates/organization/edit_base.html index eac8ab38724..0edd0894455 100644 --- a/ckan/templates/organization/edit_base.html +++ b/ckan/templates/organization/edit_base.html @@ -19,9 +19,9 @@ {% endblock %} {% block content_primary_nav %} - {{ h.build_nav_icon(group_type + '.edit', _('Edit'), id=group_dict.name) }} - {{ h.build_nav_icon(group_type + '.bulk_process', _('Datasets'), id=group_dict.name) }} - {{ h.build_nav_icon(group_type + '.members', _('Members'), id=group_dict.name) }} + {{ h.build_nav_icon(group_type + '.edit', _('Edit'), id=group_dict.name, icon='pencil-square-o') }} + {{ h.build_nav_icon(group_type + '.bulk_process', _('Datasets'), id=group_dict.name, icon='sitemap') }} + {{ h.build_nav_icon(group_type + '.members', _('Members'), id=group_dict.name, icon='users') }} {% endblock %} {% block secondary_content %} diff --git a/ckan/templates/organization/read_base.html b/ckan/templates/organization/read_base.html index 8f70ef91fdc..eff8900e09e 100644 --- a/ckan/templates/organization/read_base.html +++ b/ckan/templates/organization/read_base.html @@ -14,9 +14,9 @@ {% endblock %} {% block content_primary_nav %} - {{ h.build_nav_icon(group_type + '.read', _('Datasets'), id=group_dict.name) }} - {{ h.build_nav_icon(group_type + '.activity', _('Activity Stream'), id=group_dict.name, offset=0) }} - {{ h.build_nav_icon(group_type + '.about', _('About'), id=group_dict.name) }} + {{ h.build_nav_icon(group_type + '.read', _('Datasets'), id=group_dict.name, icon='sitemap') }} + {{ h.build_nav_icon(group_type + '.activity', _('Activity Stream'), id=group_dict.name, offset=0, icon='clock-o') }} + {{ h.build_nav_icon(group_type + '.about', _('About'), id=group_dict.name, icon='info-circle') }} {% endblock %} {% block secondary_content %} diff --git a/ckan/templates/package/edit_base.html b/ckan/templates/package/edit_base.html index 4c213c0f8e0..266b430cada 100644 --- a/ckan/templates/package/edit_base.html +++ b/ckan/templates/package/edit_base.html @@ -14,8 +14,8 @@ {% endblock %} {% block content_primary_nav %} - {{ h.build_nav_icon('dataset.edit', _('Edit metadata'), id=pkg.name) }} - {{ h.build_nav_icon('dataset.resources', _('Resources'), id=pkg.name) }} + {{ h.build_nav_icon('dataset.edit', _('Edit metadata'), id=pkg.name, icon='pencil-square-o') }} + {{ h.build_nav_icon('dataset.resources', _('Resources'), id=pkg.name, icon='bars') }} {% endblock %} {% block secondary_content %} diff --git a/ckan/templates/package/read_base.html b/ckan/templates/package/read_base.html index 0f0a458e791..adfab65eb55 100644 --- a/ckan/templates/package/read_base.html +++ b/ckan/templates/package/read_base.html @@ -18,9 +18,9 @@ {% endblock %} {% block content_primary_nav %} - {{ h.build_nav_icon('dataset.read', _('Dataset'), id=pkg.id if is_activity_archive else pkg.name) }} - {{ h.build_nav_icon('dataset.groups', _('Groups'), id=pkg.id if is_activity_archive else pkg.name) }} - {{ h.build_nav_icon('dataset.activity', _('Activity Stream'), id=pkg.id if is_activity_archive else pkg.name) }} + {{ h.build_nav_icon('dataset.read', _('Dataset'), id=pkg.id if is_activity_archive else pkg.name, icon='sitemap') }} + {{ h.build_nav_icon('dataset.groups', _('Groups'), id=pkg.id if is_activity_archive else pkg.name, icon='users') }} + {{ h.build_nav_icon('dataset.activity', _('Activity Stream'), id=pkg.id if is_activity_archive else pkg.name, icon='clock-o') }} {% endblock %} {% block secondary_content %} diff --git a/ckan/templates/package/resource_edit_base.html b/ckan/templates/package/resource_edit_base.html index 7803d1f4ea3..55211ceecd0 100644 --- a/ckan/templates/package/resource_edit_base.html +++ b/ckan/templates/package/resource_edit_base.html @@ -21,9 +21,9 @@ {% endblock %} {% block content_primary_nav %} - {{ h.build_nav_icon('resource.edit', _('Edit resource'), id=pkg.name, resource_id=res.id) }} + {{ h.build_nav_icon('resource.edit', _('Edit resource'), id=pkg.name, resource_id=res.id, icon='pencil-square-o') }} {% block inner_primary_nav %}{% endblock %} - {{ h.build_nav_icon('resource.views', _('Views'), id=pkg.name, resource_id=res.id) }} + {{ h.build_nav_icon('resource.views', _('Views'), id=pkg.name, resource_id=res.id, icon='bars') }} {% endblock %} {% block primary_content_inner %} diff --git a/ckan/templates/user/dashboard.html b/ckan/templates/user/dashboard.html index 2ff76c35f3b..cf3edcbecbc 100644 --- a/ckan/templates/user/dashboard.html +++ b/ckan/templates/user/dashboard.html @@ -16,10 +16,10 @@ {% link_for _('Edit settings'), named_route='user.edit', id=user.name, class_='btn btn-default', icon='cog' %} {% endblock %} diff --git a/ckan/templates/user/read_base.html b/ckan/templates/user/read_base.html index e5e635abd54..e78e1812a70 100644 --- a/ckan/templates/user/read_base.html +++ b/ckan/templates/user/read_base.html @@ -16,8 +16,8 @@ {% endblock %} {% block content_primary_nav %} - {{ h.build_nav_icon('user.read', _('Datasets'), id=user.name) }} - {{ h.build_nav_icon('user.activity', _('Activity Stream'), id=user.name) }} + {{ h.build_nav_icon('user.read', _('Datasets'), id=user.name, icon='sitemap') }} + {{ h.build_nav_icon('user.activity', _('Activity Stream'), id=user.name, icon='clock-o') }} {% endblock %} {% block secondary_content %} From 199024f635d32472d4fb5e25f751e4d2e4e6e118 Mon Sep 17 00:00:00 2001 From: amercader Date: Fri, 26 Jul 2019 16:52:40 +0200 Subject: [PATCH 07/11] [#4796] Mention who.ini change in the README --- CHANGELOG.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2b7c5f3a528..c694e789d57 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,24 @@ Changelog v.2.9.0 TBA ================== + * This version requires changes to the ``who.ini`` configuration file. If your + setup doesn't use the one bundled with this repo, you will have to manually + change the following lines:: + + use = ckan.lib.auth_tkt:make_plugin + + to:: + + use = ckan.lib.repoze_plugins.auth_tkt:make_plugin + + And also:: + + use = repoze.who.plugins.friendlyform:FriendlyFormPlugin + + to:: + + use = ckan.lib.repoze_plugins.friendly_form:FriendlyFormPlugin + * When upgrading from previous CKAN versions, the Activity Stream needs a migrate_package_activity.py running for displaying the history of dataset changes. This can be performed while CKAN is running or stopped (whereas the From 5957c3a539e1703a5bb8e2e2104654c7584d8df8 Mon Sep 17 00:00:00 2001 From: amercader Date: Fri, 26 Jul 2019 17:11:41 +0200 Subject: [PATCH 08/11] [#4796] [#4796] Fix tests --- ckan/lib/repoze_plugins/auth_tkt.py | 30 +++--- ckan/lib/repoze_plugins/friendly_form.py | 124 +++++++++++------------ ckan/tests/lib/test_auth_tkt.py | 2 +- 3 files changed, 78 insertions(+), 78 deletions(-) diff --git a/ckan/lib/repoze_plugins/auth_tkt.py b/ckan/lib/repoze_plugins/auth_tkt.py index 36faea5643f..8d073c1b6ec 100644 --- a/ckan/lib/repoze_plugins/auth_tkt.py +++ b/ckan/lib/repoze_plugins/auth_tkt.py @@ -19,7 +19,7 @@ def __init__(self, httponly, *args, **kwargs): self.httponly = httponly def _get_cookies(self, *args, **kwargs): - ''' + u''' Override method in superclass to ensure HttpOnly is set appropriately. ''' super_cookies = super(CkanAuthTktCookiePlugin, self). \ @@ -27,8 +27,8 @@ def _get_cookies(self, *args, **kwargs): cookies = [] for k, v in super_cookies: - replace_with = '; HttpOnly' if self.httponly else '' - v = v.replace('; HttpOnly', '') + replace_with + replace_with = u'; HttpOnly' if self.httponly else u'' + v = v.replace(u'; HttpOnly', u'') + replace_with cookies.append((k, v)) return cookies @@ -36,7 +36,7 @@ def _get_cookies(self, *args, **kwargs): def make_plugin(secret=None, secretfile=None, - cookie_name='auth_tkt', + cookie_name=u'auth_tkt', secure=False, include_ip=False, timeout=None, @@ -46,29 +46,29 @@ def make_plugin(secret=None, # ckan specifics: # Get secret from beaker setting if necessary - if secret is None or secret == 'somesecret': - secret = config['beaker.session.secret'] + if secret is None or secret == u'somesecret': + secret = config[u'beaker.session.secret'] # Session timeout and reissue time for auth cookie - if timeout is None and config.get('who.timeout'): - timeout = config.get('who.timeout') - if reissue_time is None and config.get('who.reissue_time'): - reissue_time = config.get('who.reissue_time') + if timeout is None and config.get(u'who.timeout'): + timeout = config.get(u'who.timeout') + if reissue_time is None and config.get(u'who.reissue_time'): + reissue_time = config.get(u'who.reissue_time') if timeout is not None and reissue_time is None: reissue_time = int(math.ceil(int(timeout) * 0.1)) # Set httponly based on config value. Default is True - httponly = config.get('who.httponly', True) + httponly = config.get(u'who.httponly', True) # Set secure based on config value. Default is False - secure = config.get('who.secure', False) + secure = config.get(u'who.secure', False) # back to repoze boilerplate if (secret is None and secretfile is None): - raise ValueError("One of 'secret' or 'secretfile' must not be None.") + raise ValueError(u"One of 'secret' or 'secretfile' must not be None.") if (secret is not None and secretfile is not None): - raise ValueError("Specify only one of 'secret' or 'secretfile'.") + raise ValueError(u"Specify only one of 'secret' or 'secretfile'.") if secretfile: secretfile = os.path.abspath(os.path.expanduser(secretfile)) if not os.path.exists(secretfile): - raise ValueError("No such 'secretfile': %s" % secretfile) + raise ValueError(u"No such 'secretfile': %s" % secretfile) secret = open(secretfile).read().strip() if timeout: timeout = int(timeout) diff --git a/ckan/lib/repoze_plugins/friendly_form.py b/ckan/lib/repoze_plugins/friendly_form.py index 533fc5afb8e..9f603156773 100644 --- a/ckan/lib/repoze_plugins/friendly_form.py +++ b/ckan/lib/repoze_plugins/friendly_form.py @@ -24,7 +24,7 @@ # ############################################################################## -"""Collection of :mod:`repoze.who` friendly forms""" +u'''Collection of :mod:`repoze.who` friendly forms''' from six.moves.urllib.parse import urlparse, urlunparse, urlencode, parse_qs @@ -34,7 +34,7 @@ from repoze.who.interfaces import IChallenger, IIdentifier -__all__ = ['FriendlyFormPlugin'] +__all__ = [u'FriendlyFormPlugin'] def construct_url(environ): @@ -42,7 +42,7 @@ def construct_url(environ): class FriendlyFormPlugin(object): - """ + u''' :class:`RedirectingFormPlugin `-like form plugin with more features. @@ -60,23 +60,23 @@ class FriendlyFormPlugin(object): You should keep in mind that if you're using a post-login or a post-logout page, that page will receive the referrer URL as a query string variable - whose name is "came_from". + whose name is 'came_from'. Forms can be submitted with any encoding (non-ASCII credentials are - supported) and ISO-8859-1 (aka "Latin-1") is the default one. + supported) and ISO-8859-1 (aka 'Latin-1') is the default one. - """ + ''' implements(IChallenger, IIdentifier) classifications = { - IIdentifier: ["browser"], - IChallenger: ["browser"], + IIdentifier: [u'browser'], + IChallenger: [u'browser'], } def __init__(self, login_form_url, login_handler_path, post_login_url, logout_handler_path, post_logout_url, rememberer_name, - login_counter_name=None, charset="iso-8859-1"): - """ + login_counter_name=None, charset=u'iso-8859-1'): + u''' :param login_form_url: The URL/path where the login form is located. :type login_form_url: str @@ -107,7 +107,7 @@ def __init__(self, login_form_url, login_handler_path, post_login_url, .. versionchanged:: 1.0.1 Added the ``charset`` argument. - """ + ''' self.login_form_url = login_form_url self.login_handler_path = login_handler_path self.post_login_url = post_login_url @@ -116,66 +116,66 @@ def __init__(self, login_form_url, login_handler_path, post_login_url, self.rememberer_name = rememberer_name self.login_counter_name = login_counter_name if not login_counter_name: - self.login_counter_name = '__logins' + self.login_counter_name = u'__logins' self.charset = charset # IIdentifier def identify(self, environ): - """ + u''' Override the parent's identifier to introduce a login counter (possibly along with a post-login page) and load the login counter into the ``environ``. - """ + ''' request = Request(environ, charset=self.charset) - path_info = environ['PATH_INFO'] - script_name = environ.get('SCRIPT_NAME') or '/' + path_info = environ[u'PATH_INFO'] + script_name = environ.get(u'SCRIPT_NAME') or u'/' query = request.GET if path_info == self.login_handler_path: # We are on the URL where repoze.who processes authentication. # # Let's append the login counter to the query string of the - # "came_from" URL. It will be used by the challenge below if + # 'came_from' URL. It will be used by the challenge below if # authorization is denied for this request. form = dict(request.POST) form.update(query) try: - login = form['login'] - password = form['password'] + login = form[u'login'] + password = form[u'password'] except KeyError: credentials = None else: - if request.charset == "us-ascii": + if request.charset == u'us-ascii': credentials = { - 'login': str(login), - 'password': str(password), + u'login': str(login), + u'password': str(password), } else: - credentials = {'login': login, 'password': password} + credentials = {u'login': login, u'password': password} try: - credentials['max_age'] = form['remember'] + credentials[u'max_age'] = form[u'remember'] except KeyError: pass - referer = environ.get('HTTP_REFERER', script_name) - destination = form.get('came_from', referer) + referer = environ.get(u'HTTP_REFERER', script_name) + destination = form.get(u'came_from', referer) if self.post_login_url: # There's a post-login page, so we have to replace the # destination with it. destination = self._get_full_path(self.post_login_url, environ) - if 'came_from' in query: + if u'came_from' in query: # There's a referrer URL defined, so we have to pass it to # the post-login page as a GET variable. destination = self._insert_qs_variable(destination, - 'came_from', - query['came_from']) + u'came_from', + query[u'came_from']) failed_logins = self._get_logins(environ, True) new_dest = self._set_logins_in_url(destination, failed_logins) - environ['repoze.who.application'] = HTTPFound(location=new_dest) + environ[u'repoze.who.application'] = HTTPFound(location=new_dest) return credentials elif path_info == self.logout_handler_path: @@ -184,11 +184,11 @@ def identify(self, environ): params = dict(list(r.GET.items()) + list(r.POST.items())) form = UnicodeMultiDict(params) form.update(query) - referer = environ.get('HTTP_REFERER', script_name) - came_from = form.get('came_from', referer) + referer = environ.get(u'HTTP_REFERER', script_name) + came_from = form.get(u'came_from', referer) # set in environ for self.challenge() to find later - environ['came_from'] = came_from - environ['repoze.who.application'] = HTTPUnauthorized() + environ[u'came_from'] = came_from + environ[u'repoze.who.application'] = HTTPUnauthorized() return None elif path_info == self.login_form_url or self._get_logins(environ): @@ -197,57 +197,57 @@ def identify(self, environ): # So let's load the counter into the environ and then hide it from # the query string (it will cause problems in frameworks like TG2, # where this unexpected variable would be passed to the controller) - environ['repoze.who.logins'] = self._get_logins(environ, True) + environ[u'repoze.who.logins'] = self._get_logins(environ, True) # Hiding the GET variable in the environ: if self.login_counter_name in query: del query[self.login_counter_name] - environ['QUERY_STRING'] = urlencode(query, doseq=True) + environ[u'QUERY_STRING'] = urlencode(query, doseq=True) # IChallenger def challenge(self, environ, status, app_headers, forget_headers): - """ + u''' Override the parent's challenge to avoid challenging the user on logout, introduce a post-logout page and/or pass the login counter to the login form. - """ + ''' url_parts = list(urlparse(self.login_form_url)) query = url_parts[4] query_elements = parse_qs(query) - came_from = environ.get('came_from', construct_url(environ)) - query_elements['came_from'] = came_from + came_from = environ.get(u'came_from', construct_url(environ)) + query_elements[u'came_from'] = came_from url_parts[4] = urlencode(query_elements, doseq=True) login_form_url = urlunparse(url_parts) login_form_url = self._get_full_path(login_form_url, environ) destination = login_form_url # Configuring the headers to be set: cookies = [ - (h, v) for (h, v) in app_headers if h.lower() == 'set-cookie' + (h, v) for (h, v) in app_headers if h.lower() == u'set-cookie' ] headers = forget_headers + cookies - if environ['PATH_INFO'] == self.logout_handler_path: + if environ[u'PATH_INFO'] == self.logout_handler_path: # Let's log the user out without challenging. - came_from = environ.get('came_from') + came_from = environ.get(u'came_from') if self.post_logout_url: - # Redirect to a predefined "post logout" URL. + # Redirect to a predefined u'post logout' URL. destination = self._get_full_path(self.post_logout_url, environ) if came_from: destination = self._insert_qs_variable( - destination, 'came_from', came_from) + destination, u'came_from', came_from) else: # Redirect to the referrer URL. - script_name = environ.get('SCRIPT_NAME', '') - destination = came_from or script_name or '/' + script_name = environ.get(u'SCRIPT_NAME', u'') + destination = came_from or script_name or u'/' - elif 'repoze.who.logins' in environ: + elif u'repoze.who.logins' in environ: # Login failed! Let's redirect to the login form and include # the login counter in the query string - environ['repoze.who.logins'] += 1 + environ[u'repoze.who.logins'] += 1 # Re-building the URL: destination = self._set_logins_in_url(destination, - environ['repoze.who.logins']) + environ[u'repoze.who.logins']) return HTTPFound(location=destination, headers=headers) @@ -262,29 +262,29 @@ def forget(self, environ, identity): return rememberer.forget(environ, identity) def _get_rememberer(self, environ): - rememberer = environ['repoze.who.plugins'][self.rememberer_name] + rememberer = environ[u'repoze.who.plugins'][self.rememberer_name] return rememberer def _get_full_path(self, path, environ): - """ + u''' Return the full path to ``path`` by prepending the SCRIPT_NAME. If ``path`` is a URL, do nothing. - """ - if path.startswith('/'): - path = environ.get('SCRIPT_NAME', '') + path + ''' + if path.startswith(u'/'): + path = environ.get(u'SCRIPT_NAME', u'') + path return path def _get_logins(self, environ, force_typecast=False): - """ + u''' Return the login counter from the query string in the ``environ``. If it's not possible to convert it into an integer and ``force_typecast`` is ``True``, it will be set to zero (int(0)). Otherwise, it will be ``None`` or an string. - """ + ''' variables = Request(environ).queryvars failed_logins = variables.get(self.login_counter_name) if force_typecast: @@ -295,19 +295,19 @@ def _get_logins(self, environ, force_typecast=False): return failed_logins def _set_logins_in_url(self, url, logins): - """ + u''' Insert the login counter variable with the ``logins`` value into ``url`` and return the new URL. - """ + ''' return self._insert_qs_variable(url, self.login_counter_name, logins) def _insert_qs_variable(self, url, var_name, var_value): - """ + u''' Insert the variable ``var_name`` with value ``var_value`` in the query string of ``url`` and return the new URL. - """ + ''' url_parts = list(urlparse(url)) query_parts = parse_qs(url_parts[4]) query_parts[var_name] = var_value @@ -315,4 +315,4 @@ def _insert_qs_variable(self, url, var_name, var_value): return urlunparse(url_parts) def __repr__(self): - return '<%s %s>' % (self.__class__.__name__, id(self)) + return u'<%s %s>' % (self.__class__.__name__, id(self)) diff --git a/ckan/tests/lib/test_auth_tkt.py b/ckan/tests/lib/test_auth_tkt.py index dbddcfbb0dd..d185beb4449 100644 --- a/ckan/tests/lib/test_auth_tkt.py +++ b/ckan/tests/lib/test_auth_tkt.py @@ -3,7 +3,7 @@ from nose import tools as nose_tools from ckan.tests import helpers -from ckan.lib.auth_tkt import make_plugin +from ckan.lib.repoze_plugins.auth_tkt import make_plugin class TestCkanAuthTktCookiePlugin(helpers.FunctionalTestBase): From b0e9759723754e28da5e8858f68e537695de6f8e Mon Sep 17 00:00:00 2001 From: amercader Date: Fri, 26 Jul 2019 17:33:08 +0200 Subject: [PATCH 09/11] [#4796] Don't prefix strings in auth_tkt.py as it breaks the cookie --- ckan/lib/repoze_plugins/auth_tkt.py | 6 +++--- ckan/tests/test_coding_standards.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ckan/lib/repoze_plugins/auth_tkt.py b/ckan/lib/repoze_plugins/auth_tkt.py index 8d073c1b6ec..75af0258111 100644 --- a/ckan/lib/repoze_plugins/auth_tkt.py +++ b/ckan/lib/repoze_plugins/auth_tkt.py @@ -27,8 +27,8 @@ def _get_cookies(self, *args, **kwargs): cookies = [] for k, v in super_cookies: - replace_with = u'; HttpOnly' if self.httponly else u'' - v = v.replace(u'; HttpOnly', u'') + replace_with + replace_with = '; HttpOnly' if self.httponly else '' + v = v.replace('; HttpOnly', '') + replace_with cookies.append((k, v)) return cookies @@ -36,7 +36,7 @@ def _get_cookies(self, *args, **kwargs): def make_plugin(secret=None, secretfile=None, - cookie_name=u'auth_tkt', + cookie_name='auth_tkt', secure=False, include_ip=False, timeout=None, diff --git a/ckan/tests/test_coding_standards.py b/ckan/tests/test_coding_standards.py index 3822619f3e4..944c57ca809 100644 --- a/ckan/tests/test_coding_standards.py +++ b/ckan/tests/test_coding_standards.py @@ -264,7 +264,6 @@ def find_unprefixed_string_literals(filename): u'ckan/lib/activity_streams.py', u'ckan/lib/activity_streams_session_extension.py', u'ckan/lib/app_globals.py', - u'ckan/lib/auth_tkt.py', u'ckan/lib/authenticator.py', u'ckan/lib/base.py', u'ckan/lib/captcha.py', @@ -298,6 +297,7 @@ def find_unprefixed_string_literals(filename): u'ckan/lib/search/index.py', u'ckan/lib/search/query.py', u'ckan/lib/search/sql.py', + u'ckan/lib/repoze_plugins/auth_tkt.py', u'ckan/lib/uploader.py', u'ckan/logic/__init__.py', u'ckan/logic/action/__init__.py', From ee8df9e056272601c2c2c22956f872158e24e9ad Mon Sep 17 00:00:00 2001 From: Sergey Motornyuk Date: Tue, 30 Jul 2019 11:04:27 +0300 Subject: [PATCH 10/11] Fix codestyle in CLI commands --- ckan/cli/less.py | 2 +- ckan/cli/tracking.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ckan/cli/less.py b/ckan/cli/less.py index 20945716a9e..28667f60dd4 100644 --- a/ckan/cli/less.py +++ b/ckan/cli/less.py @@ -61,7 +61,7 @@ def less(): directory = output[0].strip() if not directory: error_shout(u'Command "{}" returned nothing. Check that npm is ' - u'installed.'.format(' '.join(command))) + u'installed.'.format(u' '.join(command))) less_bin = os.path.join(directory, u'lessc') public = config.get(u'ckan.base_public_folder') diff --git a/ckan/cli/tracking.py b/ckan/cli/tracking.py index 73a1f6de653..f7a6c13000b 100644 --- a/ckan/cli/tracking.py +++ b/ckan/cli/tracking.py @@ -145,7 +145,8 @@ def update_tracking(engine, summary_date): sql = u'''UPDATE tracking_summary t SET package_id = COALESCE( (SELECT id FROM package p - WHERE p.name = regexp_replace(' ' || t.url, '^[ ]{1}(/\w{2}){0,1}' || %s, '')) + WHERE p.name = regexp_replace + (' ' || t.url, '^[ ]{1}(/\\w{2}){0,1}' || %s, '')) ,'~~not~found~~') WHERE t.package_id IS NULL AND tracking_type = 'page';''' @@ -202,8 +203,8 @@ def update_tracking_solr(engine, start_date): total = len(package_ids) not_found = 0 - click.echo('{} package index{} to be rebuilt starting from {}'.format( - total, '' if total < 2 else 'es', start_date) + click.echo(u'{} package index{} to be rebuilt starting from {}'.format( + total, u'' if total < 2 else u'es', start_date) ) from ckan.lib.search import rebuild From cd1b75d8bf8f08f6c0d07bd088a439eb96689b00 Mon Sep 17 00:00:00 2001 From: amercader Date: Fri, 2 Aug 2019 17:14:50 +0200 Subject: [PATCH 11/11] [#4796] Pep 8 --- ckan/lib/repoze_plugins/friendly_form.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ckan/lib/repoze_plugins/friendly_form.py b/ckan/lib/repoze_plugins/friendly_form.py index 9f603156773..95f89680e4d 100644 --- a/ckan/lib/repoze_plugins/friendly_form.py +++ b/ckan/lib/repoze_plugins/friendly_form.py @@ -246,8 +246,8 @@ def challenge(self, environ, status, app_headers, forget_headers): # the login counter in the query string environ[u'repoze.who.logins'] += 1 # Re-building the URL: - destination = self._set_logins_in_url(destination, - environ[u'repoze.who.logins']) + destination = self._set_logins_in_url( + destination, environ[u'repoze.who.logins']) return HTTPFound(location=destination, headers=headers)