hobo/hobo/agent/authentic2/management/commands/hobo_deploy.py

292 lines
13 KiB
Python

import requests
import logging
import os
from time import sleep
import xml.etree.ElementTree as ET
from authentic2 import app_settings
from authentic2.compat_lasso import lasso
from authentic2.models import Attribute
from authentic2.saml.models import LibertyProvider, SPOptionsIdPPolicy, \
SAMLAttribute, LibertyServiceProvider
from authentic2.a2_rbac.utils import get_default_ou
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext as _, activate
from django.core import serializers
from django_rbac.utils import get_role_model, get_ou_model
from django.conf import settings
from tenant_schemas.utils import tenant_context
from hobo.agent.common.management.commands import hobo_deploy
from hobo.agent.authentic2.provisionning import Provisionning
User = get_user_model()
class Command(hobo_deploy.Command):
help = 'Deploy multitenant authentic service from hobo'
backoff_factor = 5
def __init__(self, *args, **kwargs):
self.logger = logging.getLogger(__name__)
super(Command, self).__init__(*args, **kwargs)
def deploy_specifics(self, hobo_environment, tenant):
# generate SAML keys
self.generate_saml_keys(tenant)
self.configure_theme(hobo_environment, tenant)
with tenant_context(tenant):
# Activate default translation
activate(settings.LANGUAGE_CODE)
for user_dict in hobo_environment.get('users'):
# create hobo users in authentic to bootstrap
# (don't update them, hobo is not a provisioning system)
if not user_dict.get('email'):
# get an email for settings, or default to system
try:
user_dict['email'] = settings.ADMINS[0][1]
except (IndexError, ValueError):
user_dict['email'] = 'root@localhost'
if not (user_dict.get('first_name') or user_dict.get('last_name')):
# some SP require a name
user_dict['first_name'] = user_dict['username']
user_dict['is_staff'] = True
user_dict['is_superuser'] = True
user, created = User.objects.get_or_create(
defaults=user_dict,
username=user_dict['username'])
# create/update user attributes
fields = []
disabled_fields = []
for i, attribute in enumerate(hobo_environment.get('profile', {}).get('fields')):
if attribute['name'] == 'email':
# this field is hardcoded in the user model, don't add
# it as a new attribute, but add it to the fields list,
# so it gets shared as SAML attribute.
fields.append(attribute['name'])
continue
attr, created = Attribute.all_objects.get_or_create(
name=attribute['name'],
defaults={'kind': attribute['kind']})
for key in ('label', 'description', 'asked_on_registration',
'user_editable', 'user_visible', 'required',
'searchable', 'disabled'):
if key in attribute:
setattr(attr, key, attribute[key])
attr.order = i
if attribute['disabled']:
disabled_fields.append(attr.name)
else:
fields.append(attr.name)
attr.save()
# creation of IdpPolicy
policy, created = SPOptionsIdPPolicy.objects.get_or_create(name='Default')
policy.enabled = True
policy.authn_request_signed = False
policy.accepted_name_id_format = ['uuid']
policy.default_name_id_format = 'uuid'
policy.idp_initiated_sso = True
policy.save()
policy_type = ContentType.objects.get_for_model(SPOptionsIdPPolicy)
provider_type = ContentType.objects.get_for_model(LibertyProvider)
# create SAML default policy attributes
names = ['username', 'is_superuser'] + fields + disabled_fields
for name in names:
attribute, created = SAMLAttribute.objects.get_or_create(
name=name, name_format='basic',
attribute_name='django_user_%s' % name,
object_id=policy.id, content_type=policy_type)
attribute.enabled = not (name in disabled_fields)
attribute.save()
# also pass verified attributes
SAMLAttribute.objects.get_or_create(
name='verified_attributes',
name_format='basic',
attribute_name='@verified_attributes@',
object_id=policy.id,
content_type=policy_type)
# create or update Service Providers
services = hobo_environment['services']
retries = 0
provision_target_ous = {}
max_retries = 1 if self.redeploy else 5
while retries < max_retries:
for service in services:
if service.get('$done'):
continue
if not service.get('saml-sp-metadata-url'):
service['$done'] = True
continue
sp_url = service['saml-sp-metadata-url']
metadata_text = None
try:
metadata_response = requests.get(
sp_url, verify=app_settings.A2_VERIFY_SSL, timeout=5)
metadata_response.raise_for_status()
# verify metadata is correct
if self.check_saml_metadata(metadata_response.text):
metadata_text = metadata_response.text
else:
service['$last-error'] = 'metadata is incorrect'
continue
except requests.exceptions.RequestException as e:
service['$last-error'] = str(e)
continue
metadata_text = metadata_response.text
provider, service_created = \
LibertyProvider.objects.get_or_create(
entity_id=sp_url,
protocol_conformance=lasso.PROTOCOL_SAML_2_0)
provider.name = service['title']
provider.slug = service['slug']
provider.federation_source = 'hobo'
provider.metadata = metadata_text
provider.metadata_url = service['saml-sp-metadata-url']
if service.get('variables', {}).get('ou-slug'):
ou, created = get_ou_model().objects.get_or_create(
slug=service['variables']['ou-slug'])
ou.name = service['variables']['ou-label']
ou.save()
else:
# if there are more than one w.c.s. service we will create an
# ou of the same name
ou = get_default_ou()
create_ou = False
if service_created and service['service-id'] == 'wcs':
for s in services:
if s['service-id'] != 'wcs':
continue
if s['slug'] == service['slug']:
continue
if LibertyProvider.objects.filter(
slug=s['slug']).exists():
create_ou = True
break
if create_ou:
ou, created = get_ou_model().objects.get_or_create(
name=service['title'])
if service_created or not provider.ou:
provider.ou = ou
provision_target_ous[provider.ou.id] = provider.ou
provider.save()
if service_created:
service_provider = LibertyServiceProvider(
enabled=True, liberty_provider=provider,
sp_options_policy=policy,
users_can_manage_federations=False)
service_provider.save()
# add a superuser role for the service
Role = get_role_model()
name = _('Superuser of %s') % service['title']
su_role, created = Role.objects.get_or_create(
service=provider, slug='_a2-hobo-superuser',
defaults={'name': name})
if su_role.name != name:
su_role.name = name
su_role.save()
su_role.attributes.get_or_create(name='is_superuser',
kind='string',
value='true')
# pass the new attribute to the service
SAMLAttribute.objects.get_or_create(
name='is_superuser',
name_format='basic',
attribute_name='is_superuser',
object_id=provider.pk,
content_type=provider_type)
SAMLAttribute.objects.get_or_create(
name='role-slug',
name_format='basic',
attribute_name='a2_service_ou_role_uuids',
object_id=provider.pk,
content_type=provider_type)
# load skeleton if service is new
if service.get('template_name'):
# if there are more of the same servie, we will create an
# ou
self.load_skeleton(provider, service['service-id'],
service['template_name'])
service['$done'] = True
if all(service.get('$done') for service in services):
# it's finished no need to continue
break
# wait 5, 10, 20, 40, .. seconds
sleep(self.backoff_factor * (2 ** retries))
retries += 1
if provision_target_ous:
# mass provision roles on new created services
engine = Provisionning()
roles = get_role_model().objects.all()
engine.notify_roles(provision_target_ous, roles, full=True)
for service in services:
if not service.get('$done'):
last_error = service['$last-error']
sp_url = service['saml-sp-metadata-url']
self.stderr.write(self.style.WARNING('Error registering %s: %s\n' % (sp_url, last_error)))
def load_skeleton(self, provider, service_id, template_name,
create_ou=False):
if not getattr(settings, 'HOBO_SKELETONS_DIR', None):
self.logger.debug('no skeleton: no HOBO_SKELETONS_DIR setting')
return
# ex.: /var/lib/authentic2-multitenant/skeletons/communes/wcs/
skeleton_dir = os.path.join(settings.HOBO_SKELETONS_DIR, template_name,
service_id)
if not os.path.exists(skeleton_dir):
self.logger.debug('no skeleton: skeleton dir %r does not exist',
skeleton_dir)
return
self.load_skeleton_roles(skeleton_dir, provider)
def load_skeleton_roles(self, skeleton_dir, provider):
'''Load default roles based on a template'''
roles_filename = os.path.join(skeleton_dir, 'roles.json')
if not os.path.exists(roles_filename):
self.logger.debug('no skeleton roles: roles file %r does not '
'exist', roles_filename)
return
Role = get_role_model()
if Role.objects.filter(ou=provider.ou).exclude(slug__startswith='_').exists():
return
roles = []
for role in serializers.deserialize('json', open(roles_filename)):
assert isinstance(role.object, Role)
# reset id and natural key
role.object.pk = None
role.object.uuid = Role._meta.get_field('uuid').default()
# same ou as provider
role.object.ou = provider.ou
# XXX: attach to service or not ?
roles.append(role.object)
if roles:
Role.objects.bulk_create(roles)
Role.objects.get(uuid=roles[-1].uuid).save()
def check_saml_metadata(self, saml_metadata):
try:
root = ET.fromstring(saml_metadata.encode('utf-8'))
except ET.ParseError:
return False
return root.tag == '{%s}EntityDescriptor' % lasso.SAML2_METADATA_HREF