From 363d843822de779d2542335917a0bac2c6f979a9 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Jaillet Date: Thu, 9 Feb 2017 15:54:53 +0100 Subject: [PATCH] ozwillo: create ozwillo app in contrib (#14935) --- hobo/contrib/__init__.py | 0 hobo/contrib/ozwillo/README.txt | 22 +++ hobo/contrib/ozwillo/__init__.py | 0 .../ozwillo/examples/import-site-agents.json | 30 ++++ .../ozwillo/examples/import-site-template.json | 169 +++++++++++++++++++++ hobo/contrib/ozwillo/examples/template_recipe.json | 72 +++++++++ .../contrib/ozwillo/scripts/create_user_ozwillo.py | 47 ++++++ hobo/contrib/ozwillo/urls.py | 26 ++++ hobo/contrib/ozwillo/views.py | 162 ++++++++++++++++++++ hobo/urls.py | 4 + 10 files changed, 532 insertions(+) create mode 100644 hobo/contrib/__init__.py create mode 100644 hobo/contrib/ozwillo/README.txt create mode 100644 hobo/contrib/ozwillo/__init__.py create mode 100644 hobo/contrib/ozwillo/examples/import-site-agents.json create mode 100644 hobo/contrib/ozwillo/examples/import-site-template.json create mode 100644 hobo/contrib/ozwillo/examples/template_recipe.json create mode 100644 hobo/contrib/ozwillo/scripts/create_user_ozwillo.py create mode 100644 hobo/contrib/ozwillo/urls.py create mode 100644 hobo/contrib/ozwillo/views.py diff --git a/hobo/contrib/__init__.py b/hobo/contrib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hobo/contrib/ozwillo/README.txt b/hobo/contrib/ozwillo/README.txt new file mode 100644 index 0000000..bc8b910 --- /dev/null +++ b/hobo/contrib/ozwillo/README.txt @@ -0,0 +1,22 @@ +To run this plugin well, you have to set some files in /etc/hobo/ozwillo : +- the files are in the folder examples. +- it's a common recipe for your publik, and two extracts of user and agent combo. +- hobo has to be in the sudoers + +You have to set several var in a settings.json too : +-OZWILLO_SECRET +-OZWILLO_ENV_DOMAIN (e.g: sictiam.dev.entrouvert.org) +-OZWILLO_DESTRUCTION_URI +-OZWILLO_DESTRUCTION_SECRET +-OZWILLO_PLATEFORM (https://dev.entrouvert.org/projects/sictiam/wiki/Raccordement_OpenID_Connect_%C3%A0_Ozwillo for the values) + +And finally you have to enable in INSTALLED_APPS hobo.contrib.ozwillo. + +The views create-publik-instance receive an ozwillo request with some clients informations (secret and id), the ozwillo user sending the request, the organization name (which is the collectivity's name to deploy) and the registration uri (where you're supposed to POST when the job's done). +The script modify a template_recipe by replacing every 'instance_name' by the actual organization name, and same for the combo user extract (rewritting all the url_redirect fields). +The script then launch a cook and three commands : +- the import of the combo user with the modified extract +- the import of the combo agent +- a runscript creating a role (same as the one in wcs linked to the form sve 'agents sve'), a provider (the details are in the page linked for the parameter OZWILLO_PLATEFORM) and an admin User in Authentic who is the ozwillo user sending the request. +In the final acknolegment response, the script sends a 'services' dictionnary for ozillo to set some links and parameters in its backoffice about the app deployed). + diff --git a/hobo/contrib/ozwillo/__init__.py b/hobo/contrib/ozwillo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hobo/contrib/ozwillo/examples/import-site-agents.json b/hobo/contrib/ozwillo/examples/import-site-agents.json new file mode 100644 index 0000000..866c2e6 --- /dev/null +++ b/hobo/contrib/ozwillo/examples/import-site-agents.json @@ -0,0 +1,30 @@ +[ + { + "cells": [ + { + "fields": { + "extra_css_class": "", + "groups": [], + "order": 1, + "placeholder": "content", + "public": true, + "restricted_to_unlogged": false, + "slug": "services", + "text": "

Bienvenue

\r\n" + }, + "model": "data.textcell" + } + ], + "fields": { + "exclude_from_navigation": false, + "groups": [], + "order": 1, + "parent": null, + "public": false, + "redirect_url": "", + "slug": "index", + "template_name": "standard", + "title": "Accueil" + } + } +] diff --git a/hobo/contrib/ozwillo/examples/import-site-template.json b/hobo/contrib/ozwillo/examples/import-site-template.json new file mode 100644 index 0000000..8cbb2ed --- /dev/null +++ b/hobo/contrib/ozwillo/examples/import-site-template.json @@ -0,0 +1,169 @@ +[ + { + "cells": [ + { + "fields": { + "extra_css_class": "", + "groups": [], + "order": 0, + "placeholder": "content", + "public": true, + "restricted_to_unlogged": true, + "slug": "", + "text": "

Bienvenue

\r\n\r\n

Bienvenue sur votre compte usager.

\r\n\r\n

Pour suivre vos démarches en cours, créez votre compte personnel ou identifiez-vous depuis la page de connexion.

\r\n" + }, + "model": "data.textcell" + }, + { + "fields": { + "extra_css_class": "", + "groups": [], + "order": 1, + "placeholder": "content", + "public": false, + "restricted_to_unlogged": false, + "slug": "", + "text": "

Bienvenue

\r\n\r\n

Bienvenue sur votre compte usager.

\r\n" + }, + "model": "data.textcell" + }, + { + "fields": { + "extra_css_class": "", + "groups": [], + "order": 2, + "placeholder": "right", + "public": true, + "restricted_to_unlogged": false, + "slug": "", + "wcs_site": "" + }, + "model": "wcs.trackingcodeinputcell" + }, + { + "fields": { + "extra_css_class": "", + "groups": [], + "order": 3, + "placeholder": "right", + "public": true, + "restricted_to_unlogged": true, + "slug": "", + "text": "

Demandes en cours

\r\n\r\n

Connectez-vous pour voir vos demandes en cours.

\r\n" + }, + "model": "data.textcell" + }, + { + "fields": { + "current_forms": true, + "done_forms": false, + "extra_css_class": "", + "groups": [], + "order": 4, + "placeholder": "right", + "public": false, + "restricted_to_unlogged": false, + "slug": "", + "wcs_site": "" + }, + "model": "wcs.wcscurrentformscell" + }, + { + "fields": { + "extra_css_class": "", + "groups": [], + "order": 5, + "placeholder": "footer", + "public": true, + "restricted_to_unlogged": false, + "slug": "", + "text": "

Ce service est proposé par le SICTIAM.

\r\n" + }, + "model": "data.textcell" + }, + { + "fields": { + "category_reference": "eservices:nous-contacter", + "extra_css_class": "", + "groups": [], + "limit": null, + "manual_order": { + "data": [] + }, + "order": 6, + "ordering": "popularity", + "placeholder": "content", + "public": true, + "restricted_to_unlogged": false, + "slug": "" + }, + "model": "wcs.wcsformsofcategorycell" + } + ], + "fields": { + "exclude_from_navigation": false, + "groups": [], + "order": 1, + "parent": null, + "public": true, + "redirect_url": "", + "slug": "index", + "template_name": "two-columns", + "title": "Accueil" + } + }, + { + "cells": [ + { + "fields": { + "extra_css_class": "", + "groups": [], + "order": 0, + "placeholder": "footer", + "public": true, + "restricted_to_unlogged": false, + "slug": "" + }, + "model": "data.parentcontentcell" + } + ], + "fields": { + "exclude_from_navigation": false, + "groups": [], + "order": 2, + "parent": null, + "public": true, + "redirect_url": "https://connexion-instance_name.sictiam.dev.entrouvert.org/accounts/", + "slug": "mon-compte", + "template_name": "standard", + "title": "Mon compte" + } + }, + { + "cells": [ + { + "fields": { + "extra_css_class": "", + "groups": [], + "order": 0, + "placeholder": "footer", + "public": true, + "restricted_to_unlogged": false, + "slug": "" + }, + "model": "data.parentcontentcell" + } + ], + "fields": { + "exclude_from_navigation": false, + "groups": [], + "order": 3, + "parent": null, + "public": true, + "redirect_url": "https://demarches-instance_name.sictiam.dev.entrouvert.org/saisine-par-voie-electronique/tryauth", + "slug": "nous-contacter", + "template_name": "standard", + "title": "Nous contacter" + } + } +] diff --git a/hobo/contrib/ozwillo/examples/template_recipe.json b/hobo/contrib/ozwillo/examples/template_recipe.json new file mode 100644 index 0000000..aac57ae --- /dev/null +++ b/hobo/contrib/ozwillo/examples/template_recipe.json @@ -0,0 +1,72 @@ +{ + "steps" : [ + { + "create-hobo" : { + "url" : "https://${hobo}/" + } + }, + { + "create-authentic" : { + "title" : "Connexion", + "url" : "https://${authentic}/" + } + }, + { + "set-idp" : {} + }, + { + "create-superuser" : { + "username" : "admin", + "email" : "admin@example.net" + } + }, + { + "create-combo" : { + "template_name" : "portal-user", + "title" : "Compte citoyen", + "url" : "https://${combo}/" + } + }, + { + "create-combo" : { + "template_name" : "portal-agent", + "title" : "Portail agent", + "url" : "https://${combo_agent}/", + "slug" : "portal-agent" + } + }, + { + "create-wcs" : { + "url" : "https://${wcs}/", + "title" : "Démarches", + "template_name": "export.wcs" + } + }, + { + "create-fargo" : { + "url" : "https://${fargo}/", + "title" : "Porte documents" + } + }, + { + "create-passerelle" : { + "url" : "https://${passerelle}/", + "title" : "Passerelle" + } + }, + { + "set-theme" : { + "theme" : "publik" + } + } + ], + "variables" : { + "authentic" : "connexion-instance_name.sictiam.dev.entrouvert.org", + "combo" : "instance_name.sictiam.dev.entrouvert.org", + "combo_agent" : "agents-instance_name.sictiam.dev.entrouvert.org", + "hobo" : "hobo-instance_name.sictiam.dev.entrouvert.org", + "wcs" : "demarches-instance_name.sictiam.dev.entrouvert.org", + "passerelle": "passerelle-instance_name.sictiam.dev.entrouvert.org", + "fargo": "porte-documents-instance_name.sictiam.dev.entrouvert.org" + } +} diff --git a/hobo/contrib/ozwillo/scripts/create_user_ozwillo.py b/hobo/contrib/ozwillo/scripts/create_user_ozwillo.py new file mode 100644 index 0000000..318513f --- /dev/null +++ b/hobo/contrib/ozwillo/scripts/create_user_ozwillo.py @@ -0,0 +1,47 @@ +import sys +import logging + +from django.core.management import BaseCommand +from django.contrib.auth import get_user_model + +from authentic2_auth_oidc.models import OIDCAccount +from authentic2_auth_oidc.models import OIDCProvider +from authentic2.a2_rbac.models import Role, OrganizationalUnit + +args = sys.argv + +try: + email_address = args[1] + username = args[2] + user_id = args[3] +except IndexError: + pass + +#create agent_sve role +ou = OrganizationalUnit.objects.get(slug=u'default') +Role.objects.create(name=u'Agents SVE', ou=ou) + +#create admin user from ozwillo in authentic +provider = OIDCProvider.objects.get(name='Ozwillo') + +User = get_user_model() +new_user = User.objects.create(is_staff=True, is_superuser=True, username=username) + +logging.info('Provisionning email %s with sub %s', email_address, user_id) +oidc_account, created = OIDCAccount.objects.select_related().get_or_create(provider=provider, sub=user_id, defaults={'user': new_user}) + +if created: + if OIDCAccount.objects.filter(provider=provider, sub=user_id).count() > 1: + oidc_account.delete() + new_user.delete() + logging.info('provisionned with uuid %s', new_user.msguuid) + else: + new_user.delete() + new_user = oidc_account.user +if new_user.email != email_address: + new_user.email = email_address + new_user.save() +if new_user.ou != provider.ou: + new_user.ou = provider.ou + new_user.save() + diff --git a/hobo/contrib/ozwillo/urls.py b/hobo/contrib/ozwillo/urls.py new file mode 100644 index 0000000..5015764 --- /dev/null +++ b/hobo/contrib/ozwillo/urls.py @@ -0,0 +1,26 @@ +# Ozwillo plugin to deploy +# Copyright (C) 2017 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.conf.urls import patterns, url, include + +from . import views + + +urlpatterns = patterns('', + url(r'create-publik-instance', views.create_publik_instance, name='ozwillo-create-publik-instance'), + url(r'delete-publik-instance', views.delete_publik_instance, name='ozwillo-delete-publik-instance'), +) + diff --git a/hobo/contrib/ozwillo/views.py b/hobo/contrib/ozwillo/views.py new file mode 100644 index 0000000..9e82a68 --- /dev/null +++ b/hobo/contrib/ozwillo/views.py @@ -0,0 +1,162 @@ +# Ozwillo plugin to deploy Publik +# Copyright (C) 2017 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +import os +import logging +import requests +import io +import json +import subprocess +import hmac +import tempfile +from hashlib import sha1 + +from django.views.decorators.csrf import csrf_exempt +from django.conf import settings +from django.http import HttpResponseForbidden, HttpResponseBadRequest, HttpResponseNotFound +from django.core.management import call_command + + +logger = logging.getLogger(__name__) + +def valid_signature_required(func): + signature_header_name = 'HTTP_X_HUB_SIGNATURE' + api_secret = settings.OZWILLO_SECRET + def wrapper(request): + if signature_header_name in request.META: + if request.META[signature_header_name].startswith('sha1='): + algo, received_hmac = request.META[signature_header_name].rsplit('=') + computed_hmac = hmac.new(api_secret, request.body, sha1).hexdigest() + # the received hmac is uppercase according to + # http://doc.ozwillo.com/#ref-3-2-1 + if received_hmac.lower() != computed_hmac: + logger.error('Invalid HMAC') + return HttpResponseForbidden('Invalid HMAC') + else: + logger.error('Invalid HMAC algo') + return HttpResponseForbidden('Invalid HMAC algo') + else: + logger.error('No HMAC in the header') + return HttpResponseForbidden('No HMAC in the header') + return func(request) + return wrapper + +def is_ozwillo_enabled(func): + def wrapper(request): + if not os.path.exists('/etc/hobo/ozwillo'): + return HttpResponseNotFound('Owillo providing is not active here.') + return func(request) + return wrapper + +@csrf_exempt +@is_ozwillo_enabled +@valid_signature_required +def create_publik_instance(request): + data = json.loads(request.body) + + logger.debug(data) + + client_id = data.pop('client_id') + client_secret = data.pop('client_secret') + instance_id = data.pop('instance_id') + instance_name = data.pop('organization_name', None) + + if not instance_name: + return HttpResponseBadRequest('Missing parameter "instance_name"') + + instance_name = instance_name.lower() + registration_uri = data.pop('instance_registration_uri') + organization = data['organization'] + user = data['user'] + + services = {'services': [{ + 'local_id': 'publik', + 'name': 'Publik - %s' % (instance_name), + 'service_uri':'https://connexion-%s.%s/accounts/oidc/login' % (instance_name, settings.OZWILLO_ENV_DOMAIN), + 'description': 'Gestion de la relation usagers', + 'tos_uri': 'https://publik.entrouvert.com/', + 'policy_uri': 'https://publik.entrouvert.com/', + 'icon': 'https://publik.entrouvert.com/static/img/logo-publik.png', + 'payment_option': 'FREE', + 'target_audience': ['PUBLIC_BODIES', + 'CITIZENS', + 'COMPANIES'], + 'contacts': ['https://publik.entrouvert.com/'], + 'redirect_uris':['https://connexion-%s.%s/accounts/oidc/callback' % (instance_name, settings.OZWILLO_ENV_DOMAIN)], + }], + 'instance_id': instance_id, + 'destruction_uri': settings.OZWILLO_DESTRUCTION_URI, + 'destruction_secret': settings.OZWILLO_DESTRUCTION_SECRET, + 'needed_scopes': [] + } + + logger.debug(services) + + template_recipe = json.load(open('/etc/hobo/ozwillo/template_recipe.json', 'rb')) + var = template_recipe['variables'] + for key, value in var.items(): + var[key] = value.replace('instance_name', instance_name) + + template_recipe['variables'] = var + domain = var['combo'] + domain_agent = var['combo_agent'] + imp_site_template = json.load(open('/etc/hobo/ozwillo/import-site-template.json', 'r')) + + for row in imp_site_template: + row['fields']['redirect_url'] = row['fields']['redirect_url'].replace('instance_name', instance_name) + + combo_file_handle, combo_file_path = tempfile.mkstemp() + recipe_file_handle, recipe_file_path = tempfile.mkstemp() + + #set read rights for others as mkstemp create 600 file + os.fchmod(combo_file_handle, 0644) + + with io.open(combo_file_path, 'w', encoding='utf-8') as f: + f.write(unicode(json.dumps(imp_site_template, ensure_ascii=False))) + + with io.open(recipe_file_path, 'w', encoding='utf-8') as f: + f.write(unicode(json.dumps(template_recipe, ensure_ascii=False))) + + call_command('cook', recipe_file_path) + subprocess.call(['sudo', '-u', 'combo', 'combo-manage', + 'tenant_command', 'import_site', + combo_file_path, '-d', domain]) + subprocess.call(['sudo', '-u', 'combo', 'combo-manage', + 'tenant_command', 'import_site', + '/etc/hobo/ozwillo/import-site-agents.json', '-d', domain_agent]) + + subprocess.call(['sudo', '-u', 'authentic-multitenant', 'authentic2-multitenant-manage', 'tenant_command', + 'oidc-register-issuer', '-d', 'connexion-%s.%s' % (instance_name, settings.OZWILLO_ENV_DOMAIN), + '--scope', 'profile', '--scope', 'email', '--issuer', settings.OZWILLO_PLATEFORM, '--client-id', client_id, + '--client-secret', client_secret, '--claim-mapping', '"given_name first_name always_verified"', + '--claim-mapping', '"family_name last_name always_verified"', '--ou-slug', 'default', '--claim-mapping', + '"email email required"', 'Ozwillo']) + subprocess.call(['sudo', '-u', 'authentic-multitenant', + 'authentic2-multitenant-manage', 'tenant_command','runscript', '-d', + 'connexion-%s.%s' % (instance_name, settings.OZWILLO_ENV_DOMAIN), + '/usr/lib/python2.7/dist-packages/hobo/contrib/ozwillo/scripts/create_user_ozwillo.py', + user['email_address'], user['name'], user['id']]) + os.remove(combo_file_path) + os.remove(recipe_file_path) + + headers = {'Content-type': 'application/json', 'Accept': 'application/json'} + requests.post(registration_uri, data=json.dumps(services), auth=(client_id, client_secret), headers=headers) + +@csrf_exempt +@is_ozwillo_enabled +@valid_signature_required +def delete_publik_instance(request): + pass + diff --git a/hobo/urls.py b/hobo/urls.py index e7dbc24..63a0413 100644 --- a/hobo/urls.py +++ b/hobo/urls.py @@ -32,3 +32,7 @@ urlpatterns += patterns('', url(r'^login/local/$', login_local), # to be used as backup, in case of idp down url(r'^accounts/mellon/', include('mellon.urls')), ) +if 'hobo.contrib.ozwillo' in settings.INSTALLED_APPS: + urlpatterns += patterns('', + url(r'ozwillo/', include('hobo.contrib.ozwillo.urls')), + ) -- 2.11.0