hobo/hobo/contrib/ozwillo/views.py

145 lines
5.6 KiB
Python

# 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)