From d603938a077f5aa4ee10edad92587dd95cc631f1 Mon Sep 17 00:00:00 2001 From: gpetrak Date: Thu, 1 Aug 2024 12:40:13 +0300 Subject: [PATCH 1/6] First provbz_auth upgrade --- ckanext/provbzauth/controller.py | 7 +- ckanext/provbzauth/plugin.py | 18 ++- .../templates/user/edit_user_form.html | 114 +++++++++----- .../templates/user/snippets/login_form.html | 7 +- ckanext/provbzauth/views.py | 144 ++++++++++++++++++ setup.py | 16 +- 6 files changed, 252 insertions(+), 54 deletions(-) create mode 100644 ckanext/provbzauth/views.py diff --git a/ckanext/provbzauth/controller.py b/ckanext/provbzauth/controller.py index 099179c..9bcabfe 100644 --- a/ckanext/provbzauth/controller.py +++ b/ckanext/provbzauth/controller.py @@ -5,8 +5,8 @@ import logging import re -from pylons.i18n import _ -from pylons.controllers.util import redirect +from ckan.lib.i18n import _ +# from pylons.controllers.util import redirect import ckan.controllers.user as user import ckan.lib.base as base @@ -31,7 +31,7 @@ def external_login(self): # but the apache shibboleth filter should be aware of the # language path part (e.g. /it ) - return redirect(login_path) + return redirect_to(login_path) # if base.c.userobj is not None: # log.info("Repoze.who Shibboleth controller received userobj %r " % base.c.userobj) @@ -46,5 +46,6 @@ def external_login(self): def external_logout(self): logout_path = base.config.get("ckanext.provbzauth.logout_url", "/shibboleth/logout") + logout_path = 'test_logout' return base.h.redirect_to(logout_path) diff --git a/ckanext/provbzauth/plugin.py b/ckanext/provbzauth/plugin.py index 2c3a4d0..b57793d 100644 --- a/ckanext/provbzauth/plugin.py +++ b/ckanext/provbzauth/plugin.py @@ -1,12 +1,12 @@ ''' SPID authentication plugin for CKAN ''' - -import logging - import ckan.plugins as plugins import ckan.plugins.toolkit as toolkit +import ckanext.provbzauth.views as views +import logging + # from ckan.lib.plugins import DefaultTranslation # CKAN 2.5 only @@ -20,7 +20,9 @@ class ProvBzAuthPlugin(plugins.SingletonPlugin ProvBz auth plugin for CKAN ''' - plugins.implements(plugins.IRoutes, inherit=True) + # IBlueprint + plugins.implements(plugins.IBlueprint) + # plugins.implements(plugins.IRoutes, inherit=True) plugins.implements(plugins.IConfigurer) # plugins.implements(plugins.ITranslation) # CKAN 2.5 only @@ -31,6 +33,13 @@ def update_config(self, config): toolkit.add_template_directory(config, 'templates') toolkit.add_public_directory(config, 'public') + + # Implementation of IBlueprints + # ------------------------------------------------------------ + def get_blueprint(self): + return views.get_blueprints() + + ''' def before_map(self, map): """ Override IRoutes.before_map() @@ -47,3 +56,4 @@ def before_map(self, map): action='external_logout') return map + ''' diff --git a/ckanext/provbzauth/templates/user/edit_user_form.html b/ckanext/provbzauth/templates/user/edit_user_form.html index 787559b..bef4798 100644 --- a/ckanext/provbzauth/templates/user/edit_user_form.html +++ b/ckanext/provbzauth/templates/user/edit_user_form.html @@ -1,8 +1,11 @@ {% import 'macros/form.html' as form %} -
- {{ form.errors(error_summary) }} - +{% block form %} + + {{ h.csrf_input() }} + {% block form_errors %}{{ form.errors(error_summary) }}{% endblock %} + + {% block core_fields %}
{{ _('Change details') }} {{ form.input('name', label=_('Username'), id='field-username', value=data.name, error=errors.name, classes=['control-medium'], attrs={'readonly': '', 'class': 'form-control'}) }} @@ -13,44 +16,83 @@ {{ form.markdown('about', label=_('About'), id='field-about', value=data.about, error=errors.about, placeholder=_('A little information about yourself')) }} + {% set is_upload = data.image_url and not data.image_url.startswith('http') %} + {% set is_url = data.image_url and data.image_url.startswith('http') %} + + {{ form.image_upload(data, errors, is_upload_enabled=h.uploads_enabled(), is_url=is_url, is_upload=is_upload, upload_label=_('Profile picture'), url_label=_('Profile picture URL') ) }} + + {# For CKAN 2.4.6 {% if show_email_notifications %} - {% call form.checkbox('activity_streams_email_notifications', label=_('Subscribe to notification emails'), id='field-activity-streams-email-notifications', value=True, checked=g.userobj.activity_streams_email_notifications) %} - {% set helper_text = _("You will receive notification emails from {site_title}, e.g. when you have new activities on your dashboard."|string) %} - {{ form.info(helper_text.format(site_title=g.site_title), classes=['info-help-tight']) }} - {% endcall %} - {% endif %} + {% call form.checkbox('activity_streams_email_notifications', label=_('Subscribe to notification emails'), id='field-activity-streams-email-notifications', value=True, checked=g.userobj.activity_streams_email_notifications) %} + {% set helper_text = _("You will receive notification emails from {site_title}, e.g. when you have new activities on your dashboard."|string) %} + {{ form.info(helper_text.format(site_title=g.site_title), classes=['info-help-tight']) }} + {% endcall %} + {% endif %} #}
+ {% endblock %} + -
- {{ _('Change password') }} - {{ form.input('old_password', - type='password', - label=_('Sysadmin Password') if is_sysadmin else _('Old Password'), - id='field-password', - value=data.oldpassword, - error=errors.oldpassword, - classes=['control-medium'], - attrs={'autocomplete': 'off', 'class': 'form-control'} - ) }} - - {{ form.input('password1', type='password', label=_('Password'), id='field-password', value=data.password1, error=errors.password1, classes=['control-medium'], attrs={'autocomplete': 'off', 'class': 'form-control'} ) }} - - {{ form.input('password2', type='password', label=_('Confirm Password'), id='field-password-confirm', value=data.password2, error=errors.password2, classes=['control-medium'], attrs={'autocomplete': 'off', 'class': 'form-control'}) }} -
+ {#
#} + {% if is_sysadmin and current_user.name != data.name %} + {% block sysadmin_password %} +
+ {{ _('Change ' + data.name|capitalize + "'s" + ' password') }} + {{ form.input('password1', type='password', label=_('Password'), id='field-password', value=data.password1, error=errors.password1, classes=['control-medium'], attrs={'autocomplete': 'off', 'class': 'form-control'} ) }} + {{ form.input('password2', type='password', label=_('Confirm Password'), id='field-password-confirm', value=data.password2, error=errors.password2, classes=['control-medium'], attrs={'autocomplete': 'off', 'class': 'form-control'}) }} +
+ {% endblock %} -
- {% block delete_button %} - {% if h.check_access('user_delete', {'id': data.id}) %} - {% block delete_button_text %}{{ _('Delete') }}{% endblock %} - {% endif %} + {% block sysadmin_old_password %} +
+ {{ _('Sysadmin password') }} + {{ form.input('old_password', + type='password', + label=_('Sysadmin Password'), + id='field-password-old', + value=data.oldpassword, + error=errors.oldpassword, + classes=['control-medium'], + attrs={'autocomplete': 'off', 'class': 'form-control'} + ) }} + +
{% endblock %} - {% block generate_button %} - {% if h.check_access('user_generate_apikey', {'id': data.id}) %} - {% block generate_button_text %}{{ _('Regenerate API Key') }}{% endblock %} - {% endif %} + + {% else %} + {% block change_password %} +
+ {{ _('Change password') }} + {{ form.input('old_password', + type='password', + label=_('Old Password'), + id='field-password-old', + value=data.oldpassword, + error=errors.oldpassword, + classes=['control-medium'], + attrs={'autocomplete': 'off', 'class': 'form-control'} + ) }} + + {{ form.input('password1', type='password', label=_('Password'), id='field-password', value=data.password1, error=errors.password1, classes=['control-medium'], attrs={'autocomplete': 'off', 'class': 'form-control'} ) }} + + {{ form.input('password2', type='password', label=_('Confirm Password'), id='field-password-confirm', value=data.password2, error=errors.password2, classes=['control-medium'], attrs={'autocomplete': 'off', 'class': 'form-control'}) }} +
{% endblock %} - {{ form.required_message() }} - -
+ {% endif %} + +
+ {% block form_actions %} + {% set is_deleted = data.state == 'deleted' %} + {% if not is_deleted %} + {% block delete_button %} + {% if h.check_access('user_delete', {'id': data.id}) %} + {% block delete_button_text %}{{ _('Delete') }}{% endblock %} + {% endif %} + {% endblock %} + {% endif %} + {{ form.required_message() }} + + {% endblock %} +
+{% endblock %} \ No newline at end of file diff --git a/ckanext/provbzauth/templates/user/snippets/login_form.html b/ckanext/provbzauth/templates/user/snippets/login_form.html index a18ead0..c76ac63 100644 --- a/ckanext/provbzauth/templates/user/snippets/login_form.html +++ b/ckanext/provbzauth/templates/user/snippets/login_form.html @@ -27,10 +27,11 @@

{{ _('myCIVIS') }}

{#

{{ _("Authentication by using myCIVIS login") }}


#} - {% set came_from = h.get_request_param('came_from') %} - {% set login_url = h.url_for(controller='ckanext.provbzauth.controller:ProvBzAuthController', action='external_login', came_from=came_from or '') %} + {# {% set came_from = h.get_request_param('came_from') %} #} + {# {% set login_url = h.url_for('ProvBzAuthController.external_login', came_from=came_from or '') %} #} + {% set login_url = h.url_for('auth_blueprints.external_login') %} -
+
{{ _('myCIVIS') }}
diff --git a/ckanext/provbzauth/views.py b/ckanext/provbzauth/views.py new file mode 100644 index 0000000..be9b677 --- /dev/null +++ b/ckanext/provbzauth/views.py @@ -0,0 +1,144 @@ +# For CKAN 2.10 +from ckan.common import CKANConfig as config +from flask import Blueprint +from ckan.plugins.toolkit import render +from ckan.common import request, login_user +import ckan.lib.base as base +from ckan.lib.helpers import redirect_to +from ckan import model +from ckan.plugins.toolkit import render +from ckan.views.user import rotate_token +from ckan.lib.helpers import helper_functions as h +# import re +import requests +from flask import request +import json + +import logging +from ckan.views.user import next_page_or_default +from ckan.lib import authenticator + +log = logging.getLogger(__name__) + +auth_blueprints = Blueprint('auth_blueprints', __name__) + +## Define the views +@auth_blueprints.route('/redirect_external_login') +def external_login(): + + # Build the query and receive the URL for the myCivis portal + # API documentation: https://sso.civis.bz.it/swagger/index.html + resp = requests.get( + base.config.get("ckanext.provbzauth.login_url"), + params = {"targetUrl": base.config.get("targetUrl"), + "acceptedAuthTypes": base.config.get("acceptedAuthTypes"), + "authLevel": base.config.get("authLevel"), + "onlyauth": base.config.get("onlyauth"), + "forceLogin": base.config.get("forceLogin"), + "lang": base.config.get("locale") + }, + allow_redirects=False + ) + + # locale = request.environ.get('CKAN_LANG') + # login_path = re.sub('{{LANG}}', str(locale), login_path) + + log.info("REDIRECTING TO %r", redirect_to(resp.url)) + + # TODO: we whoud check if the login_path is relative or absolute. + # When relative, we should use base.h.redirect_to(login_path), + # but the apache shibboleth filter should be aware of the + # language path part (e.g. /it ) + + return redirect_to(resp.url) + + # if base.c.userobj is not None: + # log.info("Repoze.who Shibboleth controller received userobj %r " % base.c.userobj) + # return base.h.redirect_to(controller='user', + # action='read', + # id=base.c.userobj.name) + # else: + # log.error("No userobj received in Repoze.who Shibboleth controller %r " % base.c) + # base.h.flash_error(_("No user info received for login")) + # return base.h.redirect_to('/') + +# This view retrieves the token and validates it. +# The endpoint "http://localhost:5000/auth_bz" is the targetUrl parameter of the api/Auth/login from +# the API documentation: https://sso.civis.bz.it/swagger/index.html +@auth_blueprints.route('/auth_bz') +def do_authenticate(): + + token = request.args['token'] + # endpoint: /api/Auth/Validate/{token} + resp_val = requests.get( + base.config.get("validateToken") + '/{}'.format(token), + ) + + # Check authentication + if resp_val.status_code==200: + resp_prof = requests.get( + base.config.get("profile") + '/{}'.format(token) + ) + # convert the resp_prof response from string to json + profile_info = json.loads(resp_prof.text) + + # log.info("profile %r", resp_prof.text) + # Get the User profile ID + userid = profile_info["owner"]["fiscalCode"] + username = profile_info["username"] + firstname = profile_info["owner"]["firstname"] + lastname = profile_info["owner"]["lastname"] + + log.info("userid %r", userid) + name_id = username + "@" + str(userid) + + user = model.Session.query(model.user.User).autoflush(False) \ + .filter_by(name=name_id).first() + + log.info("user %r", user) + # Test if the user with the same ID exists on CKAN + if user is None: #or not user.is_active(): + log.info("do_authenticate: user not found: %s", name_id) + log.info("Provbz auth service will create an internal user with the ID: %s", userid) + # Create the new user + # example_user = model.user.User(name = "id@gpetr", fullname = "George Petrakis") + new_user = model.user.User(name = name_id, fullname = firstname + ' ' + lastname) + # Add the user to the database table + model.Session.add(new_user) + # Store it in the database + model.Session.commit() + + login_user(new_user) + rotate_token() + + return h.redirect_to(u'http://localhost:5000') + + else: + + log.info("The user {} exists".format(userid)) + login_user(user) + rotate_token() + + return h.redirect_to(u'http://localhost:5000') + + return redirect_to("http://localhost:5000/user/login") + + + + +@auth_blueprints.route('/redirect_external_logout') +def external_logout(): + + # Retrieve the logout redirected path from the configuration file + logout_resp = requests.get( + base.config.get("ckanext.provbzauth.logout_url"), + params = {"returnUrl": base.config.get("returnUrl")}, + allow_redirects=False + ) + + log.info("logout URL %r", logout_resp.url) + + return redirect_to(logout_resp.url) + +def get_blueprints(): + return [auth_blueprints] \ No newline at end of file diff --git a/setup.py b/setup.py index 303a621..defa791 100644 --- a/setup.py +++ b/setup.py @@ -33,12 +33,12 @@ ('**/templates/**.html', 'ckan', None), ], }, - entry_points={ - 'ckan.plugins': [ - 'provbz_auth=ckanext.provbzauth.plugin:ProvBzAuthPlugin', - ], - 'babel.extractors': [ - 'ckan=ckan.lib.extract:extract_ckan', - ], - }, + entry_points= + """ + [ckan.plugins] + provbz_auth=ckanext.provbzauth.plugin:ProvBzAuthPlugin + [babel.extractors] + ckan = ckan.lib.extract:extract_ckan + """, + ) From e7583cfc5a40f883127facd7eab13df820f394ad Mon Sep 17 00:00:00 2001 From: gpetrak Date: Tue, 6 Aug 2024 10:32:50 +0300 Subject: [PATCH 2/6] adding logout functionality --- ckanext/provbzauth/controller.py | 51 ------------------- ckanext/provbzauth/plugin.py | 6 +-- .../templates/user/logout_first.html | 28 ++++++++++ ckanext/provbzauth/views.py | 17 ++++--- 4 files changed, 42 insertions(+), 60 deletions(-) delete mode 100644 ckanext/provbzauth/controller.py create mode 100644 ckanext/provbzauth/templates/user/logout_first.html diff --git a/ckanext/provbzauth/controller.py b/ckanext/provbzauth/controller.py deleted file mode 100644 index 9bcabfe..0000000 --- a/ckanext/provbzauth/controller.py +++ /dev/null @@ -1,51 +0,0 @@ -''' -Repoze.who ProvBzAuthController controller -''' - -import logging -import re - -from ckan.lib.i18n import _ -# from pylons.controllers.util import redirect - -import ckan.controllers.user as user -import ckan.lib.base as base -from ckan.common import request - -log = logging.getLogger(__name__) - - -class ProvBzAuthController(user.UserController): - - def external_login(self): - - login_path = base.config.get("ckanext.provbzauth.login_url", "/shibboleth/login") - - locale = request.environ.get('CKAN_LANG') - login_path = re.sub('{{LANG}}', str(locale), login_path) - - log.debug("REDIRECTING TO " + login_path ) - - # TODO: we whoud check if the login_path is relative or absolute. - # When relative, we should use base.h.redirect_to(login_path), - # but the apache shibboleth filter should be aware of the - # language path part (e.g. /it ) - - return redirect_to(login_path) - - # if base.c.userobj is not None: - # log.info("Repoze.who Shibboleth controller received userobj %r " % base.c.userobj) - # return base.h.redirect_to(controller='user', - # action='read', - # id=base.c.userobj.name) - # else: - # log.error("No userobj received in Repoze.who Shibboleth controller %r " % base.c) - # base.h.flash_error(_("No user info received for login")) - # return base.h.redirect_to('/') - - def external_logout(self): - - logout_path = base.config.get("ckanext.provbzauth.logout_url", "/shibboleth/logout") - logout_path = 'test_logout' - - return base.h.redirect_to(logout_path) diff --git a/ckanext/provbzauth/plugin.py b/ckanext/provbzauth/plugin.py index b57793d..f509ac9 100644 --- a/ckanext/provbzauth/plugin.py +++ b/ckanext/provbzauth/plugin.py @@ -20,11 +20,11 @@ class ProvBzAuthPlugin(plugins.SingletonPlugin ProvBz auth plugin for CKAN ''' - # IBlueprint + # Interfaces plugins.implements(plugins.IBlueprint) - # plugins.implements(plugins.IRoutes, inherit=True) plugins.implements(plugins.IConfigurer) - # plugins.implements(plugins.ITranslation) # CKAN 2.5 only + plugins.implements(plugins.IAuthenticator, inherit=True) + def update_config(self, config): """ diff --git a/ckanext/provbzauth/templates/user/logout_first.html b/ckanext/provbzauth/templates/user/logout_first.html new file mode 100644 index 0000000..124b3e4 --- /dev/null +++ b/ckanext/provbzauth/templates/user/logout_first.html @@ -0,0 +1,28 @@ +{% import 'macros/form.html' as form %} +{% extends "user/login.html" %} + +{% set logout_url = h.url_for('user.logout') %} +{% set external_logout_url = h.url_for('auth_blueprints.external_logout') %} + +{% block actions %}{% endblock %} + +{% block form %} +
{{ _("You're already logged in as {user}.").format(user=current_user.name) }} {{ _('Logout') }}?
+ {{ form.input('login', label=_('Username'), id='field-login', value="", error="", classes=["control-full"], attrs={"disabled": "disabled"}) }} + {{ form.input('password', label=_('Password'), id='field-password', type="password", value="", error="", classes=["control-full"], attrs={"disabled": "disabled"}) }} + {{ form.checkbox('remember', label=_('Remember me'), id='field-remember', checked=true, attrs={"disabled": "disabled"}) }} +
+ +
+{% endblock %} + +{% block secondary_content %} +
+

{{ _("You're already logged in") }}

+
+

{{ _("You need to log out before you can log in with another account.") }}

+

{{ _("Log out now") }}

+

{{ _("Log out from SSO") }}

+
+
+{% endblock %} diff --git a/ckanext/provbzauth/views.py b/ckanext/provbzauth/views.py index be9b677..78225a9 100644 --- a/ckanext/provbzauth/views.py +++ b/ckanext/provbzauth/views.py @@ -7,7 +7,7 @@ from ckan.lib.helpers import redirect_to from ckan import model from ckan.plugins.toolkit import render -from ckan.views.user import rotate_token +from ckan.views.user import rotate_token, logout from ckan.lib.helpers import helper_functions as h # import re import requests @@ -43,13 +43,14 @@ def external_login(): # locale = request.environ.get('CKAN_LANG') # login_path = re.sub('{{LANG}}', str(locale), login_path) - log.info("REDIRECTING TO %r", redirect_to(resp.url)) - # TODO: we whoud check if the login_path is relative or absolute. # When relative, we should use base.h.redirect_to(login_path), # but the apache shibboleth filter should be aware of the # language path part (e.g. /it ) + # Logout from the CKAN account + #logout_user() + return redirect_to(resp.url) # if base.c.userobj is not None: @@ -111,7 +112,7 @@ def do_authenticate(): login_user(new_user) rotate_token() - return h.redirect_to(u'http://localhost:5000') + return h.redirect_to(u'home.index') else: @@ -119,9 +120,9 @@ def do_authenticate(): login_user(user) rotate_token() - return h.redirect_to(u'http://localhost:5000') + return h.redirect_to(u'home.index') - return redirect_to("http://localhost:5000/user/login") + return redirect_to(u'home.index') @@ -129,6 +130,7 @@ def do_authenticate(): @auth_blueprints.route('/redirect_external_logout') def external_logout(): + # Logout from the SSO # Retrieve the logout redirected path from the configuration file logout_resp = requests.get( base.config.get("ckanext.provbzauth.logout_url"), @@ -137,6 +139,9 @@ def external_logout(): ) log.info("logout URL %r", logout_resp.url) + + # CKAN logout + logout() return redirect_to(logout_resp.url) From bb9daaf3542f14971ba69b753434d0ef68c8250c Mon Sep 17 00:00:00 2001 From: gpetrak Date: Sat, 10 Aug 2024 11:58:15 +0300 Subject: [PATCH 3/6] adding SSO logout functionality --- ckanext/provbzauth/templates/footer.html | 215 ++++++++++++++++++ .../templates/user/logout_first.html | 5 + ckanext/provbzauth/views.py | 2 +- 3 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 ckanext/provbzauth/templates/footer.html diff --git a/ckanext/provbzauth/templates/footer.html b/ckanext/provbzauth/templates/footer.html new file mode 100644 index 0000000..7d7fb0a --- /dev/null +++ b/ckanext/provbzauth/templates/footer.html @@ -0,0 +1,215 @@ +{% set locale = h.get_locale() %} + + diff --git a/ckanext/provbzauth/templates/user/logout_first.html b/ckanext/provbzauth/templates/user/logout_first.html index 124b3e4..6bb6511 100644 --- a/ckanext/provbzauth/templates/user/logout_first.html +++ b/ckanext/provbzauth/templates/user/logout_first.html @@ -21,8 +21,13 @@

{{ _("You're already logged in") }}

{{ _("You need to log out before you can log in with another account.") }}

+ +

{{ _("Log out now") }}

+ + {#

{{ _("Log out now") }}

{{ _("Log out from SSO") }}

+ #}
{% endblock %} diff --git a/ckanext/provbzauth/views.py b/ckanext/provbzauth/views.py index 78225a9..4c435aa 100644 --- a/ckanext/provbzauth/views.py +++ b/ckanext/provbzauth/views.py @@ -122,7 +122,7 @@ def do_authenticate(): return h.redirect_to(u'home.index') - return redirect_to(u'home.index') + return h.redirect_to(u'home.index') From 223715e7013bafc62f2c48758cd05cf894597609 Mon Sep 17 00:00:00 2001 From: gpetrak Date: Tue, 13 Aug 2024 10:29:36 +0300 Subject: [PATCH 4/6] connecting logout buttons with external_logout view --- ckanext/provbzauth/templates/footer.html | 18 +++++++++--------- .../templates/user/logout_first.html | 2 +- ckanext/provbzauth/views.py | 2 ++ 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/ckanext/provbzauth/templates/footer.html b/ckanext/provbzauth/templates/footer.html index 7d7fb0a..1fef8e1 100644 --- a/ckanext/provbzauth/templates/footer.html +++ b/ckanext/provbzauth/templates/footer.html @@ -13,55 +13,55 @@ {# #}
  • - + {% trans %}Informazioni Dati Alto Adige{% endtrans %}
  • - + {% trans %}Domande frequenti{% endtrans %}
  • - + {% trans %}Atom Feed{% endtrans %}
  • - + {% trans %}Ringraziamenti{% endtrans %}
  • - + {% trans %}Note legali{% endtrans %}
  • - + {% trans %}Privacy{% endtrans %}
  • - + {% trans %}Contattaci{% endtrans %}
  • {% if locale == 'de' %} - + {% trans %}Cookie{% endtrans %} {% else %} - + {% trans %}Cookie{% endtrans %} {% endif %} diff --git a/ckanext/provbzauth/templates/user/logout_first.html b/ckanext/provbzauth/templates/user/logout_first.html index 6bb6511..2133220 100644 --- a/ckanext/provbzauth/templates/user/logout_first.html +++ b/ckanext/provbzauth/templates/user/logout_first.html @@ -7,7 +7,7 @@ {% block actions %}{% endblock %} {% block form %} -
    {{ _("You're already logged in as {user}.").format(user=current_user.name) }} {{ _('Logout') }}?
    +
    {{ _("You're already logged in as {user}.").format(user=current_user.name) }} {{ _('Logout') }}?
    {{ form.input('login', label=_('Username'), id='field-login', value="", error="", classes=["control-full"], attrs={"disabled": "disabled"}) }} {{ form.input('password', label=_('Password'), id='field-password', type="password", value="", error="", classes=["control-full"], attrs={"disabled": "disabled"}) }} {{ form.checkbox('remember', label=_('Remember me'), id='field-remember', checked=true, attrs={"disabled": "disabled"}) }} diff --git a/ckanext/provbzauth/views.py b/ckanext/provbzauth/views.py index 4c435aa..9e0decf 100644 --- a/ckanext/provbzauth/views.py +++ b/ckanext/provbzauth/views.py @@ -51,6 +51,8 @@ def external_login(): # Logout from the CKAN account #logout_user() + log.info("URL %r", resp.url) + return redirect_to(resp.url) # if base.c.userobj is not None: From 88d7719a6e71b4980b126708f2fe18914148e806 Mon Sep 17 00:00:00 2001 From: gpetrak Date: Tue, 13 Aug 2024 18:06:03 +0300 Subject: [PATCH 5/6] removing the old code and updating the documentation --- README.md | 73 ++--- ckanext/provbzauth/plugin.py | 5 +- ckanext/provbzauth/repoze/__init__.py | 2 - ckanext/provbzauth/repoze/auth.py | 35 --- ckanext/provbzauth/repoze/ident.py | 392 -------------------------- ckanext/provbzauth/views.py | 13 - setup.py | 5 +- 7 files changed, 26 insertions(+), 499 deletions(-) delete mode 100644 ckanext/provbzauth/repoze/__init__.py delete mode 100644 ckanext/provbzauth/repoze/auth.py delete mode 100644 ckanext/provbzauth/repoze/ident.py diff --git a/README.md b/README.md index 6136a6e..7049805 100644 --- a/README.md +++ b/README.md @@ -16,63 +16,34 @@ or Plugin configuration ==================== -production.ini configuration +ckan.ini configuration ---------------------------- Add ``provbz_auth`` the the ckan.plugins line ckan.plugins = [...] provbz_auth - -Configure external login and logout URLs: - - ckanext.provbzauth.login_url = https://test-data.civis.bz.it/Shibboleth.sso/Login?target=https%3A%2F%2Ftest-data.civis.bz.it&authnContextClassRef=SPID+CNS+PROV.BZ+SIAG.IT+GVCC.NET+lang%3a{{LANG}} - ckanext.provbzauth.logout_url = https://test-data.civis.bz.it/Shibboleth.sso/Logout - - -who.ini configuration ---------------------- - -Add the ``plugin:provbz_auth`` section, customizing the env var names: - - [plugin:provbz_auth] - use = ckanext.provbzauth.repoze.ident:make_identification_plugin - - check_auth_key = HTTP_SHIB_ORIGINAL_AUTHENTICATION_INSTANT - check_auth_op = not_empty - # check_auth_value= - - eppn = HTTP_SHIB_IDP_UID - authtype = HTTP_SHIB_AUTHTYPE - - pm_url = https://test-profilemanager.... - pm_user = ... - pm_pw = ... - - -Add ``provbz_auth`` to the list of the identifier plugins: - - [identifiers] - plugins = - provbz_auth - friendlyform;browser - auth_tkt - -Add ``ckanext.provbzauth.repoze.auth:ProvbzAuthenticator`` to the list of the authenticator plugins: - - [authenticators] - plugins = - auth_tkt - ckan.lib.authenticator:UsernamePasswordAuthenticator - ckanext.provbzauth.repoze.auth:ProvbzAuthenticator - -Add ``provbz_auth`` to the list of the challengers plugins: - - [challengers] - plugins = - provbz_auth - # friendlyform;browser - # basicauth +**Important note**: add the ``provbz_auth`` plugin before the ``provbz`` plugin in order the logout funtionality to be applied in both SSO and CKAN users and not only to CKAN users. + +This plugin was implemented using the following SSO API: https://sso.civis.bz.it/swagger/index.html +Thus, we have to define the following variables in the ckan.ini file: + + + ## ckanext-provbz-auth + ckanext.provbzauth.login_url = https://sso.civis.bz.it/api/Auth/Login + ckanext.provbzauth.logout_url = https://sso.civis.bz.it/api/Auth/Logout + validateToken = https://sso.civis.bz.it/api/Auth/Validate + profile = https://sso.civis.bz.it/api/Auth/Profile + targetUrl = http:///auth_bz + acceptedAuthTypes = SPID CNS PROV.BZ SIAG.IT GVCC.NET + serviceUID = + authLevel = 0 + onlyauth = false + locale =-it + forceLogin = false + returnUrl = http:// + +Please note that the Login service will accept redirection only from enabled/whitelisted hosts. localhost is always enabled, so you can test your local CKAN instance without any problem. External configuration ---------------------- diff --git a/ckanext/provbzauth/plugin.py b/ckanext/provbzauth/plugin.py index f509ac9..30a3c14 100644 --- a/ckanext/provbzauth/plugin.py +++ b/ckanext/provbzauth/plugin.py @@ -13,9 +13,8 @@ log = logging.getLogger(__name__) -class ProvBzAuthPlugin(plugins.SingletonPlugin - # , DefaultTranslation # CKAN 2.5 only - ): +class ProvBzAuthPlugin(plugins.SingletonPlugin): + # DefaultTranslation # CKAN 2.5 only ''' ProvBz auth plugin for CKAN ''' diff --git a/ckanext/provbzauth/repoze/__init__.py b/ckanext/provbzauth/repoze/__init__.py deleted file mode 100644 index 8d17c21..0000000 --- a/ckanext/provbzauth/repoze/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -__import__('pkg_resources').declare_namespace(__name__) - diff --git a/ckanext/provbzauth/repoze/auth.py b/ckanext/provbzauth/repoze/auth.py deleted file mode 100644 index 0a99291..0000000 --- a/ckanext/provbzauth/repoze/auth.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -''' -SPID authentication plugin for CKAN -''' - -import logging - -from zope.interface import implements -from repoze.who.interfaces import IAuthenticator - -from ckan.model import User - - -log = logging.getLogger(__name__) - - -class ProvBzAuthenticator(object): - """ - This class implements functions for repoze, and it's declared in the who.ini file. - """ - - implements(IAuthenticator) - - def authenticate(self, environ, identity): - - if 'provbz_auth' in identity: - userid = identity['provbz_auth'] - user = User.get(userid) - if user is None or not user.is_active(): - log.info("ProvBzAuthenticator: user not found: %s", userid) - return None - else: - log.info("ProvBzAuthenticator: user found %s", userid) - return user.name - return None diff --git a/ckanext/provbzauth/repoze/ident.py b/ckanext/provbzauth/repoze/ident.py deleted file mode 100644 index 383a03b..0000000 --- a/ckanext/provbzauth/repoze/ident.py +++ /dev/null @@ -1,392 +0,0 @@ -# -*- coding: utf8 -*- -''' -Repoze.who plugin for ckanext-provbz-auth -''' - -import logging -import requests -from urlparse import urlparse, urlunparse - -from requests import Response -from webob import Request, Response -from zope.interface import implements - -from repoze.who.interfaces import IIdentifier, IChallenger - -from ckan.lib.helpers import url_for -import ckan.model as model - - -log = logging.getLogger("ckanext.provbzauth") - -OP_NOT_EMPTY = 'not_empty' -OP_EQUALS = 'equals' - - -def make_identification_plugin(**kwargs): - log.info("Creating ProvBzIdentifierPlugin...") - - return ProvBzIdentifierPlugin(**kwargs) - - -class ProvBzIdentifierPlugin(object): - implements(IChallenger, IIdentifier) - - def is_active_session(self, env): - - val = env.get(self.check_auth_key, '') - - if self.check_auth_op == OP_NOT_EMPTY: - return bool(val.strip()) - elif self.check_auth_op == OP_EQUALS: - return val == self.check_auth_value - else: - return False - - - def __init__(self, eppn, authtype, **kwargs): - """ - Parameters here contain just names of the environment attributes defined - in who.ini, not their values. - """ - - log.info("Initting ProvBzIdentifierPlugin...") - - self.check_auth_key = kwargs['check_auth_key'] - self.check_auth_op = kwargs['check_auth_op'] - self.check_auth_value = kwargs['check_auth_value'] if 'check_auth_value' in kwargs else None - - self.eppn = eppn - self.auth_type = authtype - - self.pm_url = kwargs.get('pm_url') - self.pm_user = kwargs.get('pm_user') - self.pm_pw = kwargs.get('pm_pw') - - ok = True - - if self.check_auth_op not in (OP_NOT_EMPTY, OP_EQUALS): - log.warning('Check auth operator not valid. Auth will not work.') - ok = False - - if self.check_auth_key is None: - log.warning('Check auth key not set in who.ini. Auth will not work.') - ok = False - - if self.check_auth_op == OP_EQUALS and self.check_auth_value is None: - log.warning('Check auth values not set in who.ini. Auth will not work.') - ok = False - - if ok: - if self.check_auth_op == OP_EQUALS: - log.info('Authentication will be identified by %s = %s', self.check_auth_key, self.check_auth_value) - elif self.check_auth_op == OP_NOT_EMPTY: - log.info('Authentication will be identified by %s IS NOT EMPTY', self.check_auth_key) - - if self.pm_url is None or self.pm_user is None or self.pm_pw is None: - log.warning('Profile manager info are missing. New users can not be created') - - controller = 'ckanext.provbzauth.controller:ProvBzAuthController' - - self.ext_login_url = url_for(controller=controller, action='external_login') - self.ext_logout_url = url_for(controller=controller, action='external_logout') - self.int_login_url = url_for(controller='user', action='login') - self.int_logout_url = url_for(controller='user', action='logout') - - def challenge(self, environ, status, app_headers, forget_headers): - """ - repoze.who.interfaces.IChallenger.challenge. - - "Conditionally initiate a challenge to the user to provide credentials." - - "Examine the values passed in and return a WSGI application which causes a - challenge to be performed. Return None to forego performing a challenge." - - :param environ: the WSGI environment - :param status: status written into start_response by the downstream application. - :param app_headers: the headers list written into start_response by the downstream application. - :param forget_headers: - :return: - """ - - log.info("ProvBzIdentifierPlugin :: challenge") - - request = Request(environ) - - locale_default = environ.get('CKAN_LANG_IS_DEFAULT', True) - locale = environ.get('CKAN_LANG', None) - - parsed_url = list(urlparse(request.url)) - parsed_url[0] = parsed_url[1] = '' - requested_url = urlunparse(parsed_url) - - if not locale_default and locale and not requested_url.startswith('/%s/' % locale): - requested_url = "/%s%s" % (locale, requested_url) - - url = self.int_login_url + "?%s=%s" % ("came_from", requested_url) - - if not locale_default and locale: - url = "/%s%s" % (locale, url) - - response = Response() - response.status = 302 - response.location = url - - log.info("ProvBzIdentifierPlugin response: %s (%s)" % (response, response.location)) - return response - - def dumpInfo(self, env): - for key in sorted(env.iterkeys()): - log.debug(' ENV %s -> %s', key, env[key]) - - - def identify(self, environ): - """ - repoze.who.interfaces.IIdentifier.identify. - - "Extract credentials from the WSGI environment and turn them into an identity." - - This is called for every page load. - - :param environ: the WSGI environment. - :return: - """ - - request = Request(environ) - - log.debug("ProvBzIdentifierPlugin :: identify ------------------------------------------------------------") - # self.dumpInfo(environ) - - # Logout user - if request.path == self.int_logout_url: - response = Response() - - for a, v in self.forget(environ, {}): - response.headers.add(a, v) - - response.status = 302 - - # try: - # url = url_for(controller='user', action='logged_out') - # except AttributeError as e: - # # sometimes url_for fails - # log.warning('Error in url_for: %s', str(e)) - # url = '/' - - # locale = environ.get('CKAN_LANG', None) - # default_locale = environ.get('CKAN_LANG_IS_DEFAULT', True) - # if not default_locale and locale: - # url = "/%s%s" % (locale, self.shib_logout_url) - - response.location = self.ext_logout_url - environ['repoze.who.application'] = response - - log.info("ProvBzAuth user logout successful: %r" % request) - return {} - - # logout in progress - if request.path == self.ext_logout_url: - return {} - - # Login user if there are valid headers - if self.is_active_session(environ): # and request.path == self.login_url: - user = self._get_or_create_user(environ) - - if not user: - return {} - - # TODO: Fix flash message later, maybe some other place - #h.flash_success( - # _('Profile updated or restored from {idp}.').format( - # idp=environ.get('Shib-Identity-Provider', - # 'IdP not aquired'))) - response = Response() - response.status = 302 - - url = request.params.get('came_from', None) - # if not url: - # try: - # url = toolkit.url_for(controller='package', action='search') - # except AttributeError as e: - # # sometimes url_for fails - # log.warning('Error in url_for: %s', str(e)) - # url = '/' - # - # locale = environ.get('CKAN_LANG', None) - # default_locale = environ.get('CKAN_LANG_IS_DEFAULT', True) - # if not default_locale and locale: - # url = "/%s%s" % (locale, url) - - if url: - response.location = url - environ['repoze.who.application'] = response - - log.info("ProvBzAuth login successful: id:%s name:%s fullname: %s (%s)", user.id, user.name, user.fullname, response.location) - - return {'provbz_auth': user.id} - - - # User not logging in or logging out, return empty dict - return {} - - def _get_or_create_user(self, env): - - eppn = env.get(self.eppn, None) - # fullname = env.get(self.fullname, None) - # email = env.get(self.mail, None) - authtype = env.get(self.auth_type, None) - - # openid userkey - userkey = None - - if authtype in ("PROV.BZ", "SIAG.IT"): - if not eppn: - log.info('Environ does not contain user reference, user not loaded.') - return None - - # compose user id : THIS IS INTEGRATION DEPENDANT!!! - userkey = eppn + "@" + authtype - - elif authtype in ("SPID", "CNS"): - CF_KEY = "HTTP_SHIB_IDP_FISCALNUMBER" - cf = env[CF_KEY] - userkey = cf - - if not cf: - log.info('Environ key %s is empty, user not loaded.', CF_KEY) - return None - - else : - log.info('AuthType %s not allowed', authtype) - self.dumpInfo(env) - return None - - user = model.Session.query(model.User).autoflush(False) \ - .filter_by(openid=userkey).first() - - if user: - pass - # Check if user information from shibboleth has changed - # if user.fullname != fullname or user.email != email: - # log.info('User attributes modified, updating.') - # user.fullname = fullname - # user.email = email - - else: # user is None: - log.info('User does not exists, creating new one.') - self.dumpInfo(env) - - user = self._get_user_profile(userkey, env) - if not user: - log.warning("Can not retrieve user info") - return None - - # ckan allows only [0-9] [a-z] '-' '_' - basename = unicode(userkey, errors='ignore')\ - .lower()\ - .replace(' ', '_')\ - .replace('.', '_') \ - .replace('@', '_') - - username = basename - suffix = 0 - while not model.User.check_name_available(username): - suffix += 1 - username = basename + str(suffix) - - user.name = username - # Pls note that other fields are already set - # user = model.User(name=username, - # fullname=fullname, - # email=email, - # openid=userkey) - - model.Session.add(user) - model.Session.flush() - log.info('Created new user {usr}'.format(usr=user.fullname)) - - model.Session.commit() - model.Session.remove() - return user - - def _get_rememberer(self, environ): - plugins = environ.get('repoze.who.plugins', {}) - return plugins.get('auth_tkt') - - def remember(self, environ, identity): - ''' - Return a sequence of response headers which suffice to remember the given identity. - - :param environ: - :param identity: - :return: - ''' - rememberer = self._get_rememberer(environ) - return rememberer and rememberer.remember(environ, identity) - - def forget(self, environ, identity): - ''' - Return a sequence of response headers which suffice to destroy any credentials used to establish an identity. - - :param environ: - :param identity: - :return: - ''' - rememberer = self._get_rememberer(environ) - return rememberer and rememberer.forget(environ, identity) - - def _get_user_profile(self, userkey, env): - - authtype = env.get(self.auth_type, None) - - if authtype in ("PROV.BZ", "SIAG.IT"): - return self._get_user_profile_from_profilemanager(userkey, env) - - elif authtype in ("SPID"): - return self._get_user_profile_for_spid(userkey, env) - - else: - return None - - def _get_user_profile_from_profilemanager(self, userkey, env): - log.info('Creating user via profile manager, key %s', userkey) - - uid = env["HTTP_SHIB_IDP_UID"] - inst = env["HTTP_SHIB_ORIGINAL_AUTHENTICATION_INSTANT"] - idp = env["HTTP_SHIB_ORIGINAL_IDENTITY_PROVIDER"] - - headers = { - 'Content-type': 'application/json', - 'Shib-Authentication-Instant': inst, - 'Shib-Original-Identity-Provider': idp, - 'Shib-idp-uid': uid} - - r = requests.get(self.pm_url, auth=(self.pm_user, self.pm_pw), headers=headers) # type: Response - - if r.status_code != requests.codes.ok: - log.warning('Error received from the profile manager %s', r.status_code) - return None - - uj = r.json() - owner = uj.get('owner') - fullname = owner.get('firstname') + ' ' + owner.get('lastname') - deleg = uj.get('delegations')[0] - email = deleg.get('email') - - user = model.User(fullname=fullname, - email=email, - openid=userkey) - - return user - - def _get_user_profile_for_spid(self, userkey, env): - log.info('Creating user with SPID info, key %s', userkey) - - fullname = env['HTTP_SHIB_IDP_NAME'] + ' ' + env['HTTP_SHIB_IDP_FAMILYNAME'] - email = env['HTTP_SHIB_IDP_EMAIL'] - - user = model.User(fullname=fullname, - email=email, - openid=userkey) - - return user diff --git a/ckanext/provbzauth/views.py b/ckanext/provbzauth/views.py index 9e0decf..16ed01a 100644 --- a/ckanext/provbzauth/views.py +++ b/ckanext/provbzauth/views.py @@ -55,16 +55,6 @@ def external_login(): return redirect_to(resp.url) - # if base.c.userobj is not None: - # log.info("Repoze.who Shibboleth controller received userobj %r " % base.c.userobj) - # return base.h.redirect_to(controller='user', - # action='read', - # id=base.c.userobj.name) - # else: - # log.error("No userobj received in Repoze.who Shibboleth controller %r " % base.c) - # base.h.flash_error(_("No user info received for login")) - # return base.h.redirect_to('/') - # This view retrieves the token and validates it. # The endpoint "http://localhost:5000/auth_bz" is the targetUrl parameter of the api/Auth/login from # the API documentation: https://sso.civis.bz.it/swagger/index.html @@ -125,9 +115,6 @@ def do_authenticate(): return h.redirect_to(u'home.index') return h.redirect_to(u'home.index') - - - @auth_blueprints.route('/redirect_external_logout') def external_logout(): diff --git a/setup.py b/setup.py index defa791..6f981de 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages -version = '1.0.0' +version = '2.0.0' setup( name='ckanext-provbz-auth', @@ -20,8 +20,7 @@ license='AGPL', packages=find_packages(exclude=['ez_setup', 'tests']), namespace_packages=['ckanext', - 'ckanext.provbzauth', - 'ckanext.provbzauth.repoze'], + 'ckanext.provbzauth',], include_package_data=True, zip_safe=False, install_requires=[], From 8c1a49df26dacc76d4f8eb168bcaab02c8e0a495 Mon Sep 17 00:00:00 2001 From: gpetrak Date: Tue, 13 Aug 2024 18:08:16 +0300 Subject: [PATCH 6/6] update the docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7049805..3b05e8c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -SPID authentication plugin for CKAN 2.4. +SPID authentication plugin for CKAN 2.10. Install =======