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

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

313 lines
14 KiB
Python
Raw Normal View History

import logging
import os
import xml.etree.ElementTree as ET
from time import sleep
import requests
from authentic2 import app_settings
from authentic2.a2_rbac.models import OrganizationalUnit, Role
from authentic2.a2_rbac.utils import get_default_ou
from authentic2.compat_lasso import lasso
from authentic2.models import Attribute
from authentic2.saml.models import LibertyProvider, LibertyServiceProvider, SAMLAttribute, SPOptionsIdPPolicy
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.core import serializers
from django.utils.translation import activate
from django.utils.translation import gettext as _
from tenant_schemas.utils import tenant_context
from hobo.agent.authentic2.provisionning import Provisionning
from hobo.agent.common.management.commands import hobo_deploy
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().__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.update_or_create(
name=attribute['name'], defaults={'kind': attribute['kind']}
)
for key in (
'label',
'description',
'asked_on_registration',
'user_editable',
'user_visible',
'required',
'searchable',
'disabled',
'required_on_login',
):
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 = None, False
for legacy_urls in service.get('legacy_urls', []):
try:
provider = LibertyProvider.objects.get(
entity_id=legacy_urls['saml-sp-metadata-url'],
protocol_conformance=lasso.PROTOCOL_SAML_2_0,
)
provider.entity_id = sp_url
break
except LibertyProvider.DoesNotExist:
pass
if not provider:
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']
variables = service.get('variables', {})
if variables.get('ou-slug'):
ou, created = OrganizationalUnit.objects.get_or_create(
slug=service['variables']['ou-slug']
)
ou.name = service['variables']['ou-label']
ou.save()
if service.get('secondary') and variables.get('ou-label'):
# for secondary services include collectivity in label
provider.name = '%s (%s)' % (service['title'], service['variables']['ou-label'])
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 = OrganizationalUnit.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
if service.get('template_name') == 'portal-user':
provider.ou.home_url = service['base_url']
provider.ou.save()
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
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.is_superuser = True
su_role.save()
# 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 = Role.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
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