hobo/hobo/agent/common/management/commands/hobo_notify.py

206 lines
8.4 KiB
Python

# hobo - portal to configure and deploy applications
# Copyright (C) 2015 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 os
import sys
import random
import logging
from django.core.management.base import BaseCommand
from django.db.transaction import atomic
from django.db import IntegrityError
from tenant_schemas.utils import tenant_context
from hobo.multitenant.middleware import TenantMiddleware
from hobo.multitenant.utils import provision_user_groups
from hobo.agent.common.models import Role
class TryAgain(Exception):
pass
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('notification', metavar='NOTIFICATION', type=str)
@classmethod
def load_notification(cls, notification):
if notification == '-':
# get environment definition from stdin
return json.load(sys.stdin)
else:
return json.load(open(notification))
def handle(self, notification, **kwargs):
notification = self.load_notification(notification)
for tenant in TenantMiddleware.get_tenants():
if not os.path.exists(os.path.join(tenant.get_directory(), 'hobo.json')):
continue
with tenant_context(tenant):
self.process_notification(tenant, notification)
@classmethod
def check_valid_notification(cls, notification):
return isinstance(notification, dict) \
and '@type' in notification \
and notification['@type'] in ['provision', 'deprovision'] \
and 'objects' in notification \
and 'audience' in notification \
and isinstance(notification['audience'], list) \
and isinstance(notification['objects'], dict)
@classmethod
def check_valid_role(cls, o):
return 'uuid' in o \
and 'name' in o \
and 'description' in o
@classmethod
def check_valid_user(cls, o):
return 'uuid' in o \
and 'is_superuser' in o \
and 'email' in o \
and 'first_name' in o \
and 'last_name' in o \
and 'roles' in o
@classmethod
def provision_user(cls, issuer, action, data, full=False):
from django.contrib.auth import get_user_model
from mellon.models import UserSAMLIdentifier
User = get_user_model()
assert not full # provisionning all users is dangerous, we prefer deprovision
uuids = set()
for o in data:
try:
with atomic():
if action == 'provision':
assert cls.check_valid_user(o)
try:
mellon_user = UserSAMLIdentifier.objects.get(
issuer=issuer, name_id=o['uuid'])
user = mellon_user.user
except UserSAMLIdentifier.DoesNotExist:
try:
user = User.objects.get(username=o['uuid'][:30])
except User.DoesNotExist:
# temp user object
random_uid = str(random.randint(1,10000000000000))
user = User.objects.create(
username=random_uid)
mellon_user = UserSAMLIdentifier.objects.create(
user=user, issuer=issuer, name_id=o['uuid'])
user.first_name = o['first_name'][:30]
user.last_name = o['last_name'][:30]
user.email = o['email'][:75]
user.username = o['uuid'][:30]
user.is_superuser = o['is_superuser']
user.is_staff = o['is_superuser']
user.save()
role_uuids = [role['uuid'] for role in o.get('roles', [])]
provision_user_groups(user, role_uuids)
elif action == 'deprovision':
assert 'uuid' in o
uuids.add(o['uuid'])
except IntegrityError:
raise TryAgain
if full and action == 'provision':
for usi in UserSAMLIdentifier.objects.exclude(name_id__in=uuids):
usi.user.delete()
elif action == 'deprovision':
for user in User.objects.filter(saml_identifiers__name_id__in=uuids):
user.delete()
@classmethod
def provision_role(cls, issuer, action, data, full=False):
logger = logging.getLogger(__name__)
uuids = set()
for o in data:
assert 'uuid' in o
uuids.add(o['uuid'])
if action == 'provision':
assert cls.check_valid_role(o)
role_name = o['name']
if len(role_name) > 70:
role_name = role_name[:70] + '(...)'
try:
role = Role.objects.get(uuid=o['uuid'])
created = False
except Role.DoesNotExist:
try:
with atomic():
role, created = Role.objects.get_or_create(
name=role_name, defaults={
'uuid': o['uuid'],
'description': o['description']})
except IntegrityError:
# Can happen if uuid and name already exist
logger.error(u'cannot provision role "%s" (%s)', o['name'], o['uuid'])
continue
if not created:
save = False
if role.name != role_name:
role.name = role_name
save = True
if role.uuid != o['uuid']:
role.uuid = o['uuid']
save = True
if role.description != o['description']:
role.description = o['description']
save = True
if role.details != o.get('details', u''):
role.details = o.get('details', u'')
save = True
if save:
try:
with atomic():
role.save()
except IntegrityError:
# Can happen if uuid and name already exist
logger.error(u'cannot provision role "%s" (%s)', o['name'], o['uuid'])
continue
if full and action == 'provision':
for role in Role.objects.exclude(uuid__in=uuids):
role.delete()
elif action == 'deprovision':
for role in Role.objects.filter(uuid__in=uuids):
role.delete()
@classmethod
def process_notification(cls, tenant, notification):
assert cls.check_valid_notification(notification), \
'invalid notification'
service = tenant.get_service()
action = notification['@type']
audience = notification['audience']
full = notification['full'] if 'full' in notification else False
entity_id = service.get('saml-sp-metadata-url')
issuer = notification.get('issuer')
assert entity_id, 'service has no saml-sp-metadat-url field'
if entity_id not in audience:
return
uuids = set()
object_type = notification['objects']['@type']
for i in range(20):
try:
getattr(cls, 'provision_' + object_type)(issuer, action, notification['objects']['data'], full=full)
except TryAgain:
continue
break