From 31830604fe8624b30e4174cf484c018847545ec6 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Fri, 9 Aug 2019 20:43:35 +0200 Subject: [PATCH] agent/authentic2: retry service's metadata retrieval (#35351) --- .../management/commands/hobo_deploy.py | 213 +++++++++++------- .../common/management/commands/hobo_deploy.py | 1 + tests_authentic/test_hobo_deploy.py | 18 +- 3 files changed, 141 insertions(+), 91 deletions(-) diff --git a/hobo/agent/authentic2/management/commands/hobo_deploy.py b/hobo/agent/authentic2/management/commands/hobo_deploy.py index 4f0c634..4972fe1 100644 --- a/hobo/agent/authentic2/management/commands/hobo_deploy.py +++ b/hobo/agent/authentic2/management/commands/hobo_deploy.py @@ -1,6 +1,8 @@ import requests import logging import os +import time +import xml.etree.ElementTree as ET from authentic2 import app_settings from authentic2.compat_lasso import lasso @@ -27,6 +29,8 @@ 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) @@ -117,93 +121,123 @@ class Command(hobo_deploy.Command): # create or update Service Providers services = hobo_environment['services'] - for service in services: - if not service.get('saml-sp-metadata-url'): - continue - sp_url = service['saml-sp-metadata-url'] - try: - metadata_response = requests.get( - sp_url, verify=app_settings.A2_VERIFY_SSL, timeout=5) - metadata_response.raise_for_status() - except requests.exceptions.RequestException as e: - self.stderr.write(self.style.WARNING( - 'Error registering %s: %r\n' % (sp_url, 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: + retries = 0 + loaded = 0 + 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 + loaded += 1 + 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( - name=service['title']) - if service_created or not provider.ou: - provider.ou = 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']) + 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 + 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 + loaded += 1 + + if len(services) == loaded: + # it's finished no need to continue + break + + # wait 5, 10, 20, 40, .. seconds + time.sleep(self.backoff_factor * (2 ** retries)) + retries += 1 + + 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): @@ -242,3 +276,10 @@ class Command(hobo_deploy.Command): 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) + except ET.ParseError: + return False + return root.tag == '{%s}EntityDescriptor' % lasso.SAML2_METADATA_HREF diff --git a/hobo/agent/common/management/commands/hobo_deploy.py b/hobo/agent/common/management/commands/hobo_deploy.py index ab43ec2..8906ec9 100644 --- a/hobo/agent/common/management/commands/hobo_deploy.py +++ b/hobo/agent/common/management/commands/hobo_deploy.py @@ -44,6 +44,7 @@ class Command(BaseCommand): def handle(self, base_url=None, json_filename=None, ignore_timestamp=None, redeploy=None, *args, **kwargs): + self.redeploy = redeploy if redeploy: for tenant in TenantMiddleware.get_tenants(): try: diff --git a/tests_authentic/test_hobo_deploy.py b/tests_authentic/test_hobo_deploy.py index 41467fb..9762d50 100644 --- a/tests_authentic/test_hobo_deploy.py +++ b/tests_authentic/test_hobo_deploy.py @@ -6,6 +6,8 @@ import shutil import json import mock +from requests import RequestException + from django.core.management import call_command from django.db import connection from hobo.multitenant.middleware import TenantMiddleware @@ -26,9 +28,10 @@ def skeleton_dir(request, settings): return settings.HOBO_SKELETONS_DIR -def test_hobo_deploy(tenant_base, mocker, skeleton_dir): +def test_hobo_deploy(monkeypatch, tenant_base, mocker, skeleton_dir): from django.core.management import call_command from django.conf import settings + from hobo.agent.authentic2.management.commands.hobo_deploy import Command as HoboDeployCommand # Create skeleton roles.json os.makedirs(os.path.join(skeleton_dir, 'commune', 'wcs')) @@ -117,11 +120,16 @@ def test_hobo_deploy(tenant_base, mocker, skeleton_dir): meta2 = meta1.replace('eservices', 'passerelle') meta3 = meta1.replace('eservices', 'clapiers') metadatas = [meta1, meta2, meta3] - side_effect = [] - for meta in metadatas: + monkeypatch.setattr(HoboDeployCommand, 'backoff_factor', 0.0001) + side_effect_iter = iter([meta1, meta2, RequestException(), meta3]) + + def side_effect(*args, **kwargs): + v = next(side_effect_iter) + if isinstance(v, Exception): + raise v m = mock.Mock() - m.text = meta - side_effect.append(m) + m.text = v + return m requests_get.side_effect = side_effect env = { 'users': [ -- 2.23.0.rc1