general: remove ozwillo extension (#53229)

This commit is contained in:
Frédéric Péters 2021-04-18 20:33:35 +02:00
parent fb3627eaf8
commit 5e865ad9b3
21 changed files with 1 additions and 1209 deletions

View File

@ -13,9 +13,6 @@ recursive-include hobo/locale *.po *.mo
recursive-include hobo/environment/locale *.po *.mo
recursive-include hobo/agent/authentic2/locale *.po *.mo
recursive-include tests *.py *.json
recursive-include hobo/contrib/ozwillo/scripts *.py
recursive-include hobo/contrib/ozwillo/examples *.json
include hobo/contrib/ozwillo/README.rst
include hobo/multitenant/README
include MANIFEST.in
include COPYING

View File

@ -1,105 +0,0 @@
Ozwillo contrib app for SICTIAM
===============================
Install on Debian
-----------------
1. add `hobo.contrib.ozwillo` to INSTALLED_APPS
2. copy files from examples/ into /etc/hobo/ozwillo/ (must be readable by all)
3. copy following line in /etc/sudoers.d/sictiam::
hobo ALL=(ALL:ALL) NOPASSWD: ALL
4. set the following variables in `/etc/hobo/settings.d/10_ozwillo.py`:
- 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)
- OZWILLO_SERVICES (use only for the destruction, explained down)
Exemple::
OZWILLO_DESTRUCTION_SECRET = "mysecret"
OZWILLO_DESTRUCTION_URI = "https://hobo-sve.test-demarches.sictiam.fr/ozwillo/delete-publik-instance/"
OZWILLO_ENABLED = True,
OZWILLO_ENV_DOMAIN = "test-demarches.sictiam.fr"
OZWILLO_PLATEFORM = "https://accounts.ozwillo-preprod.eu/"
OZWILLO_SECRET = "myothersecret"
OZWILLO_SERVICES = {
"authentic-multitenant": [
"connexion-",
"authentic2-multitenant-manage"
],
"combo_agent": [
"agents-",
"combo-manage"
],
"combo_usager": [
"",
"combo-manage"
],
"fargo": [
"porte-documents-",
"fargo-manage"
],
"hobo": [
"hobo-",
"hobo-manage"
],
"passerelle": [
"passerelle-",
"passerelle-manage"
],
"wcs": [
"demarches-",
"wcsctl"
]
}
Design
------
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 acknowledgement response, the script sends a 'services'
dictionnary for ozillo to set some links and parameters in its backoffice about
the app deployed).
OZWILLO_SERVICES is a dict following this pattern: 'service_user':
['service_prefix', 'service_command_manager']. E.G::
{
"authentic-multitenant": ["connexion-", "authentic2-multitenant-manage"],
"combo_agent": ["agents-", "combo-manage"],
"combo_usager": ["", "combo-manage"],
"fargo": ["porte-documents-", "fargo-manage"],
"hobo": ["hobo-", "hobo-manage"],
"passerelle": ["passerelle-", "passerelle-manage"],
"wcs": ["demarches-", "wcsctl"]
}
Destruction
===========
For the complete destruction of w.c.s. instances it's necessary that in the
default skeleton (`/var/lib/wcs/skeletons/export.zip`) the `config.pck` file
contains a key `postgresql.createdb-connection-params`.

View File

@ -1,10 +0,0 @@
from django.contrib import admin
from .models import OzwilloInstance
class OzwilloInstanceAdmin(admin.ModelAdmin):
list_display = ['id', 'domain_slug', 'external_ozwillo_id', 'state', 'created', 'modified']
list_filter = ['state', 'created', 'modified']
admin.site.register(OzwilloInstance, OzwilloInstanceAdmin)

View File

@ -1,30 +0,0 @@
[
{
"cells": [
{
"fields": {
"extra_css_class": "",
"groups": [],
"order": 1,
"placeholder": "content",
"public": true,
"restricted_to_unlogged": false,
"slug": "services",
"text": "<h2>Bienvenue</h2>\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"
}
}
]

View File

@ -1,169 +0,0 @@
[
{
"cells": [
{
"fields": {
"extra_css_class": "",
"groups": [],
"order": 0,
"placeholder": "content",
"public": true,
"restricted_to_unlogged": true,
"slug": "",
"text": "<h2>Bienvenue</h2>\r\n\r\n<p>Bienvenue sur votre compte usager.</p>\r\n\r\n<p>Pour suivre vos d&eacute;marches en cours, cr&eacute;ez votre compte personnel ou identifiez-vous depuis <a href=\"/login/\">la page de connexion</a>.</p>\r\n"
},
"model": "data.textcell"
},
{
"fields": {
"extra_css_class": "",
"groups": [],
"order": 1,
"placeholder": "content",
"public": false,
"restricted_to_unlogged": false,
"slug": "",
"text": "<h2>Bienvenue</h2>\r\n\r\n<p>Bienvenue sur votre compte usager.</p>\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": "<h2>Demandes en cours</h2>\r\n\r\n<p>Connectez-vous pour voir vos demandes en cours.</p>\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": "<p>Ce service est propos&eacute; par le <a href=\"http://www.sictiam.fr/\">SICTIAM</a>.</p>\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": "[idp_url]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": "[eservices_url]",
"slug": "nous-contacter",
"template_name": "standard",
"title": "Nous contacter"
}
}
]

View File

@ -1,67 +0,0 @@
{
"steps": [
{
"create-hobo": {
"url": "https://${hobo}/",
"primary": true
}
},
{
"create-authentic": {
"title": "Connexion",
"url": "https://${authentic}/"
}
},
{
"set-idp": {}
},
{
"create-combo": {
"template_name": "portal-user",
"title": "Compte citoyen",
"url": "https://${combo}/"
}
},
{
"create-combo": {
"slug": "portal-agent",
"template_name": "portal-agent",
"title": "Portail agent",
"url": "https://${combo_agent}/"
}
},
{
"create-wcs": {
"template_name": "export.zip",
"title": "D\u00e9marches",
"url": "https://${wcs}/"
}
},
{
"create-fargo": {
"title": "Porte documents",
"url": "https://${fargo}/"
}
},
{
"create-passerelle": {
"title": "Passerelle",
"url": "https://${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",
"fargo": "porte-documents-instance_name.sictiam.dev.entrouvert.org",
"hobo": "hobo-instance_name.sictiam.dev.entrouvert.org",
"passerelle": "passerelle-instance_name.sictiam.dev.entrouvert.org",
"wcs": "demarches-instance_name.sictiam.dev.entrouvert.org"
}
}

View File

@ -1,68 +0,0 @@
# 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 <http://www.gnu.org/licenses/>.
import logging
from hobo.contrib.ozwillo.models import OzwilloInstance
from django.db.transaction import atomic
from django.core.management.base import BaseCommand
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('args', nargs='*')
@atomic
def handle(self, *args, **options):
qs = OzwilloInstance.objects.select_for_update()
# deployment
if args:
# allow deploying manually failed instances
to_deploy = qs.fiter(
domain_slug__in=args,
state__in=[OzwilloInstance.STATE_TO_DEPLOY, OzwilloInstance.STATE_DEPLOY_ERROR])
else:
# deploy everything to be deployed
to_deploy = qs.filter(
state=OzwilloInstance.STATE_TO_DEPLOY)
for instance in to_deploy:
try:
with atomic():
instance.deploy()
except Exception:
logger.exception(u'ozwillo: deploying %s from CRON failed', instance)
instance.deploy_error()
# destruction
if args:
# allow deploying manually failed instances
to_destroy = qs.filter(
domain_slug__in=args,
state__in=[OzwilloInstance.STATE_TO_DESTROY, OzwilloInstance.STATE_DESTROY_ERROR])
else:
# destroy everything to be destroyed
to_destroy = qs.filter(state=OzwilloInstance.STATE_TO_DESTROY)
for instance in to_destroy:
try:
with atomic():
instance.destroy()
except Exception:
logger.exception(u'ozwillo: destroying %s from CRON failed', instance)
instance.destroy_error()
logger = logging.getLogger(__name__)

View File

@ -1,21 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
]
operations = [
migrations.CreateModel(
name='OzwilloInstance',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('domain_slug', models.CharField(unique=True, max_length=250)),
('external_ozwillo_id', models.CharField(max_length=450)),
],
),
]

View File

@ -1,52 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.13 on 2018-05-17 10:47
from __future__ import unicode_literals
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('ozwillo', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='ozwilloinstance',
name='created',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='ozwilloinstance',
name='deploy_data',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='ozwilloinstance',
name='modified',
field=models.DateTimeField(auto_now=True, default=django.utils.timezone.now),
),
migrations.AddField(
model_name='ozwilloinstance',
name='state',
field=models.CharField(choices=[(b'new', b'new'), (b'to_deploy', b'to_deploy'), (b'deployed', b'deployed'), (b'deploy_error', b'deploy_error'), (b'to_destroy', b'to_destroy'), (b'destroy_error', b'destroy_error'), (b'destroyed', b'destroyed')], default=b'deployed', max_length=16),
),
migrations.AlterField(
model_name='ozwilloinstance',
name='external_ozwillo_id',
field=models.CharField(max_length=450, unique=True),
),
migrations.AlterField(
model_name='ozwilloinstance',
name='modified',
field=models.DateTimeField(auto_now=True),
),
migrations.AlterField(
model_name='ozwilloinstance',
name='state',
field=models.CharField(choices=[(b'new', b'new'), (b'to_deploy', b'to_deploy'), (b'deployed', b'deployed'), (b'deploy_error', b'deploy_error'), (b'to_destroy', b'to_destroy'), (b'destroy_error', b'destroy_error'), (b'destroyed', b'destroyed')], default=b'new', max_length=16),
),
]

View File

@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.13 on 2018-05-24 08:28
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ozwillo', '0002_auto_20180517_1047'),
]
operations = [
migrations.AlterField(
model_name='ozwilloinstance',
name='domain_slug',
field=models.CharField(max_length=250),
),
]

View File

@ -1,285 +0,0 @@
# 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 <http://www.gnu.org/licenses/>.
import logging
import os
import subprocess
import tempfile
import json
import requests
from django.conf import settings
from django.db import models
from django.db.transaction import atomic
from django.utils.text import slugify
from django.core.management import call_command
from django.db import connection
from tenant_schemas.utils import tenant_context
class OzwilloInstance(models.Model):
STATE_NEW = 'new'
STATE_TO_DEPLOY = 'to_deploy'
STATE_DEPLOY_ERROR = 'deploy_error'
STATE_DEPLOYED = 'deployed'
STATE_TO_DESTROY = 'to_destroy'
STATE_DESTROY_ERROR = 'destroy_error'
STATE_DESTROYED = 'destroyed'
STATES = [
(STATE_NEW, 'new'),
(STATE_TO_DEPLOY, 'to_deploy'),
(STATE_DEPLOYED, 'deployed'),
(STATE_DEPLOY_ERROR, 'deploy_error'),
(STATE_TO_DESTROY, 'to_destroy'),
(STATE_DESTROY_ERROR, 'destroy_error'),
(STATE_DESTROYED, 'destroyed'),
]
state = models.CharField(max_length=16, default=STATE_NEW, choices=STATES)
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
domain_slug = models.CharField(max_length=250)
external_ozwillo_id = models.CharField(max_length=450, unique=True)
deploy_data = models.TextField(null=True)
def __unicode__(self):
return self.domain_slug
def __repr__(self):
return '<OzwilloInstance external_ozwillo_id: %r domain_slug: %r state: %r>' % (
self.external_ozwillo_id, self.domain_slug, self.state)
@property
def data(self):
return json.loads(self.deploy_data) if self.deploy_data else None
def to_deploy(self):
assert self.state == self.STATE_NEW
try:
with atomic():
# lock the new instance to prevent collision with the CRON job
OzwilloInstance.objects.select_for_update().filter(id=self.id)
# instance starts with the NEW state, to prevent the CRON from
# deploying instance juste created
# only instance in the state STATE_TO_DEPLOY are deployed.
self.state = self.STATE_TO_DEPLOY
self.save()
self.deploy()
except Exception:
# something failed, still make the instance to be deployed
# an reraise exception
self.state = self.STATE_TO_DEPLOY
self.save()
raise
def to_destroy(self):
assert self.state == self.STATE_DEPLOYED
self.state = self.STATE_TO_DESTROY
self.save()
def deploy_error(self):
assert self.state in [self.STATE_DEPLOY_ERROR, self.STATE_TO_DEPLOY]
if self.state == self.STATE_TO_DEPLOY:
self.state = self.STATE_DEPLOY_ERROR
self.save()
def deploy(self):
logger.info(u'ozwillo: deploy start for %s', self)
data = self.data
if not data:
logger.warning(u'ozwillo: unable to deploy, no data')
return
# Request parsing
client_id = data['client_id']
client_secret = data['client_secret']
instance_id = data['instance_id']
instance_name = data['organization_name']
instance_name = slugify(instance_name)
registration_uri = data['instance_registration_uri']
user = data['user']
# Cook new platform
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']
domain_passerelle = var['passerelle']
logger.info(u'ozwillo: cooking %s', template_recipe)
with tempfile.NamedTemporaryFile() as recipe_file:
json.dump(template_recipe, recipe_file)
recipe_file.flush()
# cook play with the tenant context, we must protect from that
with tenant_context(connection.tenant):
call_command('cook', recipe_file.name, timeout=1000, verbosity=0)
# Load user portal template
logger.info(u'ozwillo: loading combo template')
run_command([
'sudo', '-u', 'combo',
'combo-manage', 'tenant_command', 'import_site',
'/etc/hobo/ozwillo/import-site-template.json',
'-d', domain
])
# Load agent portal template
logger.info(u'ozwillo: loading combo agent template')
run_command([
'sudo', '-u', 'combo',
'combo-manage', 'tenant_command', 'import_site',
'/etc/hobo/ozwillo/import-site-agents.json',
'-d', domain_agent
])
# Configure OIDC Ozwillo authentication
logger.info(u'ozwillo: configuring OIDC ozwillo authentication')
domain_name = 'connexion-%s.%s' % (instance_name, settings.OZWILLO_ENV_DOMAIN)
if run_command([
'sudo', '-u', 'authentic-multitenant',
'authentic2-multitenant-manage', 'tenant_command', 'oidc-register-issuer',
'-d', domain_name,
'--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'
]):
# creation of the admin user depends upon the creation of provider
logger.info(u'ozwillo: creating admin user')
create_user_script = os.path.dirname(__file__) + '/scripts/create_user_ozwillo.py'
run_command([
'sudo', '-u', 'authentic-multitenant',
'authentic2-multitenant-manage', 'tenant_command', 'runscript', '-d', domain_name,
create_user_script, user['email_address'], user['id'], user['name']
])
# Load passerelle template
logger.info(u'ozwillo: loading passerelle template')
run_command([
'sudo', '-u', 'passerelle',
'passerelle-manage', 'tenant_command', 'import_site',
'/etc/hobo/ozwillo/import-site-passerelle.json',
'--import-user', '-d', domain_passerelle
])
# Sending done event to Ozwillo
services = {
'services': [{
'local_id': 'publik',
'name': 'Publik - %s' % (instance_name),
'service_uri': 'https://connexion-%s.%s/accounts/oidc/login?iss=%s'
% (instance_name, settings.OZWILLO_ENV_DOMAIN,
settings.OZWILLO_PLATEFORM),
'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-64x64.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.info(u'ozwillo: sending registration request, %r', services)
headers = {'Content-type': 'application/json', 'Accept': 'application/json'}
response = requests.post(
registration_uri,
data=json.dumps(services),
auth=(client_id, client_secret),
headers=headers)
logger.info(u'ozwillo: registration response, status=%s content=%r',
response.status_code, response.content)
self.state = self.STATE_DEPLOYED
self.save()
logger.info(u'ozwillo: deploy finished')
def destroy_error(self):
assert self.state in [self.STATE_DESTROY_ERROR, self.STATE_TO_DESTROY]
if self.state == self.STATE_TO_DESTROY:
self.state = self.STATE_DESTROY_ERROR
self.save()
def destroy(self):
logger.info(u'ozwillo: destroy start for %s', self)
instance_slug = self.domain_slug
services = settings.OZWILLO_SERVICES.copy()
# w.c.s. is handled differently
wcs = services.pop('wcs')
for s, infos in services.items():
# to get the two combo instances which have same service
service_user = s.split('_')[0]
tenant = '%s%s.%s' % (infos[0], instance_slug, settings.OZWILLO_ENV_DOMAIN)
run_command([
'sudo', '-u', service_user, infos[1],
'delete_tenant', '--force-drop', tenant
])
tenant = '%s%s.%s' % (wcs[0], instance_slug, settings.OZWILLO_ENV_DOMAIN)
run_command([
'sudo', '-u', 'wcs',
wcs[1], '-f', '/etc/wcs/wcs-au-quotidien.cfg',
'delete_tenant', '--force-drop', tenant
])
logger.info(u'ozwillo: destroy finished')
self.state = self.STATE_DESTROYED
self.save()
def run_command(args):
try:
process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except OSError as e:
logger.error('ozwillo: launching subprocess %s raised error %s', args, e)
return False
logger.info('ozwillo: launching subprocess with pid %s : %s', process.pid, args)
stdoutdata, stderrdata = process.communicate()
if process.returncode != 0:
logger.error('ozwillo: subprocess %s failed returncode=%s stdout=%r stderr=%r',
process.pid, process.returncode, stdoutdata, stderrdata)
return False
logger.info('ozwillo: subprocess terminated')
return True
logger = logging.getLogger(__name__)

View File

@ -1,55 +0,0 @@
import sys
import logging
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
def create_user(user_id, email_addressi, user_name, provider):
while True:
new_user = User.objects.create()
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()
continue
break
else:
new_user.delete()
new_user = oidc_account.user
break
new_user.email = email_address
new_user.ou = provider.ou
new_user.is_superuser = True
new_user.is_staff = True
new_user.first_name = 'admin'
new_user.last_name = user_name
new_user.save()
return new_user
# create agent_sve role
ou = OrganizationalUnit.objects.get(default=True)
role, created = Role.objects.get_or_create(uuid='3f3367d817bb4a9aa98e7ed6c83b5b09',
defaults={'name': u'Agents SVE',
'ou': ou})
# set the provider for the user creation
provider = OIDCProvider.objects.get(name='Ozwillo')
# get user info from args sent to the script
args = sys.argv
email_address = args[1]
user_id = args[2]
user_name = args[3]
# create admin user in Publik from Ozwillo
User = get_user_model()
user = create_user(user_id, email_address, user_name, provider)
logging.info('owzillo provisionning: user created with uuid: %s', user.uuid)

View File

@ -1,148 +0,0 @@
# Ozwillo plugin to deploy Publik
# Copyright (C) 2017-2019 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 <http://www.gnu.org/licenses/>.
import json
import datetime
import logging
from urllib import urlencode
import pprint
import os
import requests
from robobrowser.browser import RoboBrowser
from django.utils.six.moves.urllib import parse as urlparse
from hobo.multitenant.middleware import TenantMiddleware
from tenant_schemas.utils import tenant_context
def run_on_all_tenants(f):
for tenant in TenantMiddleware.get_tenants():
with tenant_context(tenant):
try:
f(tenant)
except:
logging.exception('unable to provision')
def provision_users(tenant):
from django.conf import settings
if not getattr(settings, 'OZWILLO_ADMIN', None):
logging.warning('No OZWILLO_ADMIN setting found')
return
logger = logging.getLogger('ozwillo_synchro')
from authentic2_auth_oidc.models import OIDCProvider
for provider in OIDCProvider.objects.all():
if 'ozwillo' in provider.issuer or 'accounts.sictiam.fr' in provider.issuer:
auth_url = provider.authorization_endpoint
token_url = provider.token_endpoint
client_id = provider.client_id
client_secret = provider.client_secret
redirect_uri = urlparse.urljoin(tenant.get_base_url(), '/accounts/oidc/callback/')
instance_users_url = urlparse.urljoin(token_url.replace('accounts', 'kernel'), '/apps/acl/instance/')
break
else:
logger.warning('No ozwillo OIDC provider found for %s', tenant.get_base_url())
return
session = requests.Session()
session.verify = False
br = RoboBrowser(user_agent='Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0',
session=session)
query = urlencode({
'client_id': client_id,
'response_type': 'code',
'scope': 'openid offline_access',
'prompt': 'consent',
'redirect_uri': redirect_uri
})
response = br.open(auth_url + '?%s' % query)
response = br._states[-1].response
if br._states[-1].response.status_code != 200:
logger.warning(u'OIDC authorization request failed: %s %r', response.status_code, response._content[:1000])
return
referer = br.response.request.url
br.session.headers['Referer'] = referer
form = br.get_form(action='/a/login')
form['u'] = settings.OZWILLO_ADMIN[0]
form['pwd'] = settings.OZWILLO_ADMIN[1]
br.submit_form(form=form)
referer = br.response.request.url
br.session.headers['Referer'] = referer
form = br.get_form()
br.submit_form(form=form, allow_redirects=False)
if 'Location' not in br.response.headers:
logger.warning(u'Not authorization form %r %r', response.status_code, response._content[:1000])
return
cb_url = urlparse.urlparse(br.response.headers['location'])
code = urlparse.parse_qs(cb_url.query)['code'][0]
logger.info('Getting token from %s', token_url)
response = requests.post(token_url, auth=(client_id, client_secret),
data={
'code': code,
'redirect_uri': redirect_uri,
'grant_type': 'authorization_code'})
logger.info('Got %s', response.json())
access_token = response.json()['access_token']
logger.info('Getting instance users from %s', instance_users_url + client_id)
response = requests.get(instance_users_url + client_id,
headers={'Authorization': 'Bearer %s' % access_token})
logger.info('Got %s', response.json())
from django.contrib.auth import get_user_model
from authentic2_auth_oidc.models import OIDCAccount
User = get_user_model()
for user in response.json():
logging.info('Provisionning email %s with sub %s', user['user_email_address'], user['user_id'])
while True:
new_user = User.objects.create()
oidc_account, created = OIDCAccount.objects.select_related().get_or_create(provider=provider, sub=user['user_id'], defaults={'user': new_user})
if created:
if OIDCAccount.objects.filter(provider=provider, sub=user['user_id']).count() > 1:
oidc_account.delete()
new_user.delete()
continue
logging.info('provisionned with uuid %s', new_user.uuid)
break
else:
new_user.delete()
new_user = oidc_account.user
break
save = False
if new_user.username != user['user_name']:
new_user.username = user['user_name']
save = True
if new_user.email != user['user_email_address']:
new_user.email = user['user_email_address']
save = True
if new_user.ou != provider.ou:
new_user.ou = provider.ou
save = True
if save:
new_user.save()
run_on_all_tenants(provision_users)

View File

@ -1,25 +0,0 @@
# 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 <http://www.gnu.org/licenses/>.
from django.conf.urls import url, include
from . import views
urlpatterns = [
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'),
]

View File

@ -1,144 +0,0 @@
# 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 <http://www.gnu.org/licenses/>.
import logging
import json
import hmac
import threading
from hashlib import sha1
from django.views.decorators.csrf import csrf_exempt
from django.conf import settings
from django.http import (HttpResponseForbidden, HttpResponseBadRequest,
HttpResponseNotFound, HttpResponse)
from django.utils.text import slugify
from django.db import DatabaseError
from django.db.transaction import atomic
from .models import OzwilloInstance
logger = logging.getLogger(__name__)
def valid_signature_required(setting):
'''Validate Ozwillo signatures'''
signature_header_name = 'HTTP_X_HUB_SIGNATURE'
def decorator(func):
def wrapper(request, *args, **kwargs):
api_secret = getattr(settings, setting)
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.content, 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(u'ozwillo: invalid HMAC')
return HttpResponseForbidden('invalid HMAC')
else:
logger.error(u'ozwillo: invalid HMAC algo')
return HttpResponseForbidden('invalid HMAC algo')
else:
logger.error(u'ozwillo: no HMAC in the header')
return HttpResponseForbidden('no HMAC in the header')
return func(request, *args, **kwargs)
return wrapper
return decorator
def is_ozwillo_enabled(func):
def wrapper(request):
if not getattr(settings, 'OZWILLO_ENABLED', False):
return HttpResponseNotFound('owillo providing is not active here.')
return func(request)
return wrapper
@csrf_exempt
@is_ozwillo_enabled
@valid_signature_required(setting='OZWILLO_SECRET')
def create_publik_instance(request):
try:
data = json.loads(request.text)
except ValueError:
logger.warning(u'ozwillo: received non JSON request')
return HttpResponseBadRequest('invalid JSON content')
logger.info(u'ozwillo: create publik instance request, %r', data)
if 'organization_name' not in data.keys():
logger.warning(u'ozwillo: missing organization_name')
return HttpResponseBadRequest('missing parameter "organization_name"')
org_name = slugify(data['organization_name'])
# forbid creation of an instance if a not destroyed previous one exists
if OzwilloInstance.objects.exclude(state=OzwilloInstance.STATE_DESTROYED).filter(domain_slug=org_name):
logger.warning(u'ozwillo: instance %s already exists', org_name)
return HttpResponseBadRequest('instance %s already exists' % org_name)
try:
instance = OzwilloInstance.objects.create(
external_ozwillo_id=data['instance_id'],
state=OzwilloInstance.STATE_NEW,
domain_slug=org_name,
# deploy_data is a TextField containing JSON
deploy_data=json.dumps(data, indent=4))
except DatabaseError as e:
logger.warning(u'ozwillo: could not create instance_id %r org_name %r: %r',
data['instance_id'], org_name, e)
return HttpResponseBadRequest(u'cannot create the instance: %r', e)
# immediate deploy in a thread
def thread_function(data):
try:
instance.to_deploy()
except Exception:
logger.exception(u'ozwillo: error occured duging initial deploy request %s', org_name)
thread = threading.Thread(target=thread_function, args=(data,))
thread.start()
return HttpResponse()
@csrf_exempt
@is_ozwillo_enabled
@valid_signature_required(setting='OZWILLO_DESTRUCTION_SECRET')
@atomic
def delete_publik_instance(request):
try:
data = json.loads(request.text)
except ValueError:
logger.warning(u'ozwillo: received non JSON request')
return HttpResponseBadRequest('invalid JSON content')
logger.info(u'ozwillo: delete publik instance request (%r)', data)
try:
instance_id = data['instance_id']
except KeyError:
logger.warning(u'ozwillo: no instance id in destroy request (%r)', data)
return HttpResponseBadRequest('no instance id')
try:
instance = OzwilloInstance.objects.select_for_update().get(
external_ozwillo_id=instance_id,
# only deployed instances can be destroyed
state=OzwilloInstance.STATE_DEPLOYED)
except OzwilloInstance.DoesNotExist:
return HttpResponseBadRequest('no instance with id %s' % data['instance_id'])
instance.to_destroy()
logger.info(u'ozwillo: destroy registered for %s (%s)', instance.domain_slug, instance.external_ozwillo_id)
return HttpResponse(status=200)

View File

@ -57,8 +57,3 @@ urlpatterns += [
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 += [
url(r'ozwillo/', include('hobo.contrib.ozwillo.urls')),
]

View File

@ -2,9 +2,8 @@ import os
LANGUAGE_CODE = 'en-us'
BROKER_URL = 'memory://'
OZWILLO_SECRET = 'secret'
INSTALLED_APPS += ('hobo.contrib.ozwillo', 'hobo.agent.common')
INSTALLED_APPS += ('hobo.agent.common', )
ALLOWED_HOSTS.append('localhost')