agent/authentic2: retry service's metadata retrieval (#35351)

This commit is contained in:
Benjamin Dauvergne 2019-08-09 20:43:35 +02:00
parent 2ad03f8140
commit 0f0043ca53
3 changed files with 139 additions and 89 deletions

View File

@ -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
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(
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:
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()
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'])
# 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

View File

@ -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:

View File

@ -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': [