145 lines
5.6 KiB
Python
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)
|