hobo/tests_authentic/test_hobo_deploy.py

759 lines
31 KiB
Python

import json
import os
import shutil
import tempfile
import time
from unittest import mock
import pytest
from authentic2.data_transfer import export_site
from django.core.management import call_command
from django.db import connection
from requests import RequestException
from hobo.multitenant.middleware import TenantMiddleware
os.sys.path.append('%s/tests' % os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from utils import byteify
pytestmark = pytest.mark.django_db
@pytest.fixture
def skeleton_dir(request, settings):
settings.HOBO_SKELETONS_DIR = tempfile.mkdtemp('skeletons')
def fin():
shutil.rmtree(settings.HOBO_SKELETONS_DIR)
request.addfinalizer(fin)
return settings.HOBO_SKELETONS_DIR
def test_hobo_deploy(monkeypatch, tenant_base, mocker, skeleton_dir, tmp_path):
from django.conf import settings
from django.core.management import call_command
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'))
with open(os.path.join(skeleton_dir, 'commune', 'wcs', 'roles.json'), 'w') as roles_json:
json.dump(
[
{
'model': 'a2_rbac.role',
'fields': {
'name': 'Service petite enfance',
'slug': 'service-petite-enfance',
},
},
{
'model': 'a2_rbac.role',
'fields': {
'name': 'Service état-civil',
'slug': 'service-etat-civil',
},
},
],
roles_json,
)
requests_get = mocker.patch('requests.get')
meta1 = '''<?xml version="1.0"?>
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
entityID="http://eservices.example.net/saml/metadata">
<SPSSODescriptor
AuthnRequestsSigned="true" WantAssertionsSigned="true"
protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<KeyValue xmlns="http://www.w3.org/2000/09/xmldsig#">
<RSAKeyValue>
<Modulus>nJpkBznHNbvE+RAC6mU+NPQnIWs8gFNCm6I3FPcUKYpaJbXaurJ4cJgvnaEiqIXPQDcbHxuLeCbYbId9yascWZirvQbh8d/r+Vv+24bPG++9gW+i3Nnz1VW8V+z0b+puHWvM/FjJjBNJgWkI38gaupz47U6/02CtWx00stitiwk=</Modulus>
<Exponent>AQAB</Exponent>
</RSAKeyValue>
</KeyValue>
</ds:KeyInfo>
</KeyDescriptor>
<KeyDescriptor use="encryption">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<KeyValue xmlns="http://www.w3.org/2000/09/xmldsig#">
<RSAKeyValue>
<Modulus>3BxSiAzGvY1Yuqa31L7Zr2WHM/8cn5oX+Q6A2SYgzjuvAgnWyizN8YgW/fHR4G7MtkmZ5RFJLXfcSLwbUfpFHV6KO1ikbgViYuFempM+SWtjqEI7ribm9GaI5kUzHJZBrH3/Q9XAd9/GLLALxurGjbKDeLfc0D+7el26g4sYmA8=</Modulus>
<Exponent>AQAB</Exponent>
</RSAKeyValue>
</KeyValue>
</ds:KeyInfo>
</KeyDescriptor>
<SingleLogoutService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="http://eservices.example.net/saml/singleLogout"
ResponseLocation="http://eservices.example.net/saml/singleLogoutReturn" />
<SingleLogoutService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
Location="http://eservices.example.net/saml/singleLogoutSOAP" />
<ManageNameIDService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="http://eservices.example.net/saml/manageNameId"
ResponseLocation="http://eservices.example.net/saml/manageNameIdReturn" />
<ManageNameIDService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
Location="http://eservices.example.net/saml/manageNameIdSOAP" />
<AssertionConsumerService isDefault="true" index="0"
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"
Location="http://eservices.example.net/saml/assertionConsumerArtifact" />
<AssertionConsumerService index="1"
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="http://eservices.example.net/saml/assertionConsumerPost" />
<AssertionConsumerService index="2"
Binding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS"
Location="http://eservices.example.net/saml/assertionConsumerSOAP" />
<AssertionConsumerService index="3"
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="http://eservices.example.net/saml/assertionConsumerRedirect" />
</SPSSODescriptor>
</EntityDescriptor>'''
meta2 = meta1.replace('eservices', 'passerelle')
meta3 = meta1.replace('eservices', 'clapiers')
meta4 = meta1.replace('eservices', 'portal')
metadatas = [meta1, meta2, meta3, meta4]
monkeypatch.setattr(HoboDeployCommand, 'backoff_factor', 0.0001)
side_effect_iter = iter([meta1, meta2, RequestException(), meta4, meta3])
def side_effect(*args, **kwargs):
for v in side_effect_iter:
if isinstance(v, Exception):
raise v
m = mock.Mock()
m.text = v
return m
requests_get.side_effect = side_effect
env = {
'users': [
{
'username': 'john.doe',
'first_name': 'John',
'last_name': 'Doe',
'email': 'john.doe@example.net',
'password': 'password',
},
],
'profile': {
'fields': [
{
'kind': 'title',
'description': '',
'required': False,
'user_visible': True,
'label': 'Civilité',
'disabled': True,
'user_editable': True,
'asked_on_registration': False,
'name': 'title',
},
{
'kind': 'string',
'description': '',
'required': True,
'user_visible': True,
'label': 'Prénom',
'disabled': False,
'user_editable': True,
'asked_on_registration': True,
'name': 'first_name',
},
{
'kind': 'string',
'description': '',
'required': True,
'user_visible': True,
'label': 'Nom',
'disabled': False,
'user_editable': True,
'asked_on_registration': True,
'name': 'last_name',
},
{
'kind': 'email',
'description': '',
'required': True,
'user_visible': True,
'label': 'Adresse électronique',
'disabled': False,
'user_editable': True,
'asked_on_registration': False,
'name': 'email',
},
{
'kind': 'string',
'description': '',
'required': False,
'user_visible': True,
'label': 'Addresse',
'disabled': False,
'user_editable': True,
'asked_on_registration': False,
'name': 'address',
},
{
'kind': 'string',
'description': '',
'required': False,
'user_visible': True,
'label': 'Code postal',
'disabled': False,
'user_editable': True,
'asked_on_registration': False,
'name': 'zipcode',
},
{
'kind': 'string',
'description': '',
'required': False,
'user_visible': True,
'label': 'Commune',
'disabled': False,
'user_editable': True,
'asked_on_registration': False,
'name': 'city',
},
{
'kind': 'phone_number',
'description': '',
'required': False,
'user_visible': True,
'label': 'Téléphone',
'disabled': False,
'user_editable': True,
'asked_on_registration': False,
'name': 'phone',
},
{
'kind': 'fr_phone_number',
'description': '',
'required': False,
'user_visible': True,
'label': 'Téléphone Français',
'disabled': False,
'user_editable': True,
'asked_on_registration': False,
'name': 'fr_phone',
},
{
'kind': 'string',
'description': '',
'required': False,
'user_visible': True,
'label': 'Mobile',
'disabled': False,
'user_editable': True,
'asked_on_registration': False,
'name': 'mobile',
},
{
'kind': 'string',
'description': '',
'required': False,
'user_visible': True,
'label': 'Pays',
'disabled': True,
'user_editable': True,
'asked_on_registration': False,
'name': 'country',
},
{
'kind': 'birthdate',
'description': '',
'required': False,
'user_visible': True,
'label': 'Date de naissance',
'disabled': True,
'user_editable': True,
'asked_on_registration': False,
'name': 'birthdate',
},
{
'kind': 'boolean',
'description': '',
'required': False,
'user_visible': True,
'label': 'CGU?',
'disabled': False,
'user_editable': False,
'asked_on_registration': True,
'required_on_login': True,
'name': 'cgu',
},
]
},
'variables': {
'hobo_test_variable': True,
'other_variable': 'foo',
},
'services': [
{
'service-id': 'authentic',
'slug': 'test',
'title': 'Test',
'this': True,
'secret_key': '12345',
'base_url': 'http://sso.example.net',
'variables': {
'other_variable': 'bar',
},
},
{
'service-id': 'wcs',
'template_name': 'commune',
'slug': 'montpellier-metropole',
'title': 'Montpellier-Métropole',
'base_url': 'http://eservices.example.net',
'saml-sp-metadata-url': 'http://eservices.example.net/saml/metadata',
},
{
'service-id': 'passerelle',
'slug': 'passerelle',
'title': 'Passerelle',
'base_url': 'http://passerelle.example.net',
'saml-sp-metadata-url': 'http://passerelle.example.net/saml/metadata',
},
{
'service-id': 'wcs',
'template_name': 'commune',
'slug': 'clapiers',
'title': 'Clapiers',
'secondary': True,
'base_url': 'http://clapiers.example.net',
'saml-sp-metadata-url': 'http://clapiers.example.net/saml/metadata',
'variables': {
'ou-label': 'OU label',
'ou-slug': 'ou-slug',
},
},
{
'service-id': 'combo',
'template_name': 'portal-user',
'slug': 'portal',
'title': 'Portail Montpellier-Métropole',
'base_url': 'http://portal.example.net',
'saml-sp-metadata-url': 'http://portal.example.net/saml/metadata',
},
],
}
def hobo_json():
with tempfile.NamedTemporaryFile(mode='w', dir=str(tmp_path), delete=False) as hobo_json:
hobo_json_content = json.dumps(env)
hobo_json.write(hobo_json_content)
return hobo_json.name
with mock.patch('hobo.agent.authentic2.provisionning.notify_agents') as mock_notify, mock.patch(
'hobo.agent.authentic2.management.commands.hobo_deploy.sleep', wraps=time.sleep
) as sleep_mock:
call_command('hobo_deploy', 'http://sso.example.net', hobo_json())
assert (
sleep_mock.call_count == 1
) # there is one retry, as the third service's metadata is temporarily unavailable
# check role mass provisionning to new services
# two wcs => two ous => two audiences
assert mock_notify.call_count == 2
audiences = sorted(arg[0][0]['audience'] for arg in mock_notify.call_args_list)
assert audiences == [
['http://clapiers.example.net/saml/metadata'],
[
'http://eservices.example.net/saml/metadata',
'http://passerelle.example.net/saml/metadata',
'http://portal.example.net/saml/metadata',
],
]
assert [arg[0][0]['@type'] for arg in mock_notify.call_args_list] == ['provision', 'provision']
assert [arg[0][0]['objects']['@type'] for arg in mock_notify.call_args_list] == ['role', 'role']
assert [arg[0][0]['full'] for arg in mock_notify.call_args_list] == [True, True]
from hobo.multitenant.middleware import TenantMiddleware
tenants = list(TenantMiddleware.get_tenants())
assert len(tenants) == 1
tenant = tenants[0]
assert tenant.domain_url == 'sso.example.net'
assert tenant.schema_name == 'sso_example_net'
tenant_directory = tenant.get_directory()
assert tenant_directory == os.path.join(tenant_base, tenant.domain_url)
assert os.path.exists(os.path.join(tenant_directory, 'saml.crt'))
assert os.path.exists(os.path.join(tenant_directory, 'saml.key'))
from tenant_schemas.utils import tenant_context
with tenant_context(tenant):
from authentic2.custom_user.models import User
assert User.objects.count() == 1
user = User.objects.get()
assert user.username == 'john.doe'
assert user.first_name == 'John'
assert user.last_name == 'Doe'
assert user.email == 'john.doe@example.net'
assert user.is_superuser is True
assert user.is_staff is True
from authentic2.models import Attribute
assert Attribute.all_objects.count() == 12
for field in env['profile']['fields']:
if field['name'] != 'email':
at = Attribute.all_objects.get(name=field['name'])
assert at.label == field['label']
assert at.description == field['description']
assert at.kind == field['kind']
if field['disabled']:
assert at.disabled is True
else:
assert at.disabled is False
assert at.asked_on_registration == field['asked_on_registration']
assert at.user_visible == field['user_visible']
assert at.user_editable == field['user_editable']
assert at.required_on_login == field.get('required_on_login', False)
for at in Attribute.all_objects.all():
assert [field for field in env['profile']['fields'] if field['name'] == at.name]
# OIDC checks
from authentic2_idp_oidc.utils import get_jwkset
assert get_jwkset()
# SAML checks
from authentic2.saml.models import LibertyProvider, LibertyServiceProvider, SPOptionsIdPPolicy
assert SPOptionsIdPPolicy.objects.count() == 1
policy = SPOptionsIdPPolicy.objects.get()
assert policy.name == 'Default'
assert policy.enabled is True
assert policy.authn_request_signed is False
assert policy.accepted_name_id_format == ['uuid']
assert policy.default_name_id_format == 'uuid'
assert policy.attributes.count() == 16
assert policy.attributes.filter(enabled=True).count() == 13
assert policy.attributes.filter(enabled=False).count() == 3
assert policy.attributes.filter(name_format='basic').count() == 16
assert (
policy.attributes.filter(name='is_superuser', attribute_name='django_user_is_superuser').count()
== 1
)
assert policy.attributes.filter(name='username', attribute_name='django_user_username').count() == 1
for field in env['profile']['fields']:
assert (
policy.attributes.filter(
name=field['name'], attribute_name='django_user_%s' % field['name']
).count()
== 1
)
assert LibertyProvider.objects.count() == 4
services = tenant.get_hobo_json()['services']
from authentic2.a2_rbac.models import Role
from authentic2.a2_rbac.utils import get_default_ou
other_services = [service for service in services if not service.get('this')]
for meta, service in zip(metadatas, other_services):
provider = LibertyProvider.objects.get(slug=service['slug'])
assert provider.metadata == meta
# Two services loaded roles.json, so there must be 5 roles associated
# to their "ou" excluding their superuser role, 3 admin roles for users,
# roles and services, and 2 loaded roles, petite enfance and état-civil
assert Role.objects.filter(ou__isnull=False, service__isnull=True).count() == 10
for service_id in {s['service-id'] for s in other_services}:
same_services = [s for s in other_services if s['service-id'] == service_id]
for i, service in enumerate(same_services):
assert LibertyProvider.objects.filter(slug=service['slug']).count() == 1
provider = LibertyProvider.objects.get(slug=service['slug'])
if service['slug'] == 'clapiers':
assert provider.name == 'Clapiers (OU label)'
else:
assert provider.name == service['title']
assert provider.federation_source == 'hobo'
assert provider.entity_id == service['saml-sp-metadata-url']
if service.get('template_name') == 'portal-user':
assert provider.ou.home_url == service['base_url']
assert LibertyServiceProvider.objects.filter(liberty_provider=provider).count() == 1
service_provider = LibertyServiceProvider.objects.get(liberty_provider=provider)
assert service_provider.enabled is True
assert service_provider.sp_options_policy == policy
assert service_provider.users_can_manage_federations is False
assert Role.objects.filter(slug='_a2-hobo-superuser', service=provider).count() == 1
su_role = Role.objects.get(slug='_a2-hobo-superuser', service=provider)
assert su_role.attributes.count() == 1
assert (
su_role.attributes.filter(name='is_superuser', kind='string', value='true').count() == 1
)
if i == 0 or service_id != 'wcs':
assert provider.ou == get_default_ou()
else:
assert (
provider.ou.name == service['variables']['ou-label']
if 'variables' in service
else service['title']
)
assert su_role.ou == provider.ou
assert provider.attributes.count() == 2
assert (
provider.attributes.filter(
name='is_superuser', name_format='basic', attribute_name='is_superuser'
).count()
== 1
)
assert (
provider.attributes.filter(
name='role-slug', name_format='basic', attribute_name='a2_service_ou_role_uuids'
).count()
== 1
)
if service.get('template_name'):
assert Role.objects.filter(ou=provider.ou, service__isnull=True).count() == 5
assert (
Role.objects.filter(
ou=provider.ou, service__isnull=True, name='Service petite enfance'
).count()
== 1
)
assert (
Role.objects.filter(
ou=provider.ou, service__isnull=True, name='Service état-civil'
).count()
== 1
)
with mock.patch('hobo.agent.authentic2.provisionning.notify_agents') as mock_notify, mock.patch(
'hobo.agent.authentic2.management.commands.hobo_deploy.sleep', wraps=time.sleep
) as sleep_mock:
call_command('hobo_deploy', redeploy=True)
assert sleep_mock.call_count == 0
# test attribute kind update
with tenant_context(tenant):
assert Attribute.objects.filter(name='mobile', kind='string').count() == 1
field = env['profile']['fields'][9]
assert field['name'] == 'mobile'
field['kind'] = 'phone_number'
side_effect_iter = iter([meta1, meta2, RequestException(), meta4, meta3])
with mock.patch('hobo.agent.authentic2.provisionning.notify_agents') as mock_notify, mock.patch(
'hobo.agent.authentic2.management.commands.hobo_deploy.sleep', wraps=time.sleep
) as sleep_mock:
call_command('hobo_deploy', 'http://sso.example.net', hobo_json(), ignore_timestamp=True)
with tenant_context(tenant):
assert Attribute.objects.filter(name='mobile', kind='string').count() == 0
assert Attribute.objects.filter(name='mobile', kind='phone_number').count() == 1
def test_import_template(db, tenant_base):
def listify(value):
if isinstance(value, dict):
value = list((k, listify(v) or []) for k, v in value.items())
value.sort()
if isinstance(value, list):
value = list(listify(x) for x in value)
value.sort()
return value
def sort_and_remove_uuid(value):
if isinstance(value, dict):
if 'uuid' in value:
value.pop('uuid')
value = {k: sort_and_remove_uuid(v) for k, v in value.items()}
if isinstance(value, list):
value = [sort_and_remove_uuid(elt) for elt in value]
value.sort(key=lambda elt: listify(elt))
return value
call_command('create_tenant', 'authentic.example.net')
tenant = TenantMiddleware.get_tenant_by_hostname('authentic.example.net')
connection.set_tenant(tenant)
call_command('import_template', '--basepath=%s' % os.path.dirname(__file__), 'data_authentic_export_site')
content = open('%s/data_authentic_export_site.json' % os.path.dirname(__file__)).read()
export_ref = sort_and_remove_uuid(export_site())
file_ref = sort_and_remove_uuid(json.loads(content))
assert export_ref == file_ref
def test_hobo_deploy_with_legacy_urls(monkeypatch, tenant_base, mocker, skeleton_dir, tmp_path):
from django.core.management import call_command
from hobo.agent.authentic2.management.commands.hobo_deploy import Command as HoboDeployCommand
requests_get = mocker.patch('requests.get')
meta1 = '''<?xml version="1.0"?>
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
entityID="http://passerelle.example.net/saml/metadata">
<SPSSODescriptor
AuthnRequestsSigned="true" WantAssertionsSigned="true"
protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<KeyValue xmlns="http://www.w3.org/2000/09/xmldsig#">
<RSAKeyValue>
<Modulus>nJpkBznHNbvE+RAC6mU+NPQnIWs8gFNCm6I3FPcUKYpaJbXaurJ4cJgvnaEiqIXPQDcbHxuLeCbYbId9yascWZirvQbh8d/r+Vv+24bPG++9gW+i3Nnz1VW8V+z0b+puHWvM/FjJjBNJgWkI38gaupz47U6/02CtWx00stitiwk=</Modulus>
<Exponent>AQAB</Exponent>
</RSAKeyValue>
</KeyValue>
</ds:KeyInfo>
</KeyDescriptor>
<KeyDescriptor use="encryption">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<KeyValue xmlns="http://www.w3.org/2000/09/xmldsig#">
<RSAKeyValue>
<Modulus>3BxSiAzGvY1Yuqa31L7Zr2WHM/8cn5oX+Q6A2SYgzjuvAgnWyizN8YgW/fHR4G7MtkmZ5RFJLXfcSLwbUfpFHV6KO1ikbgViYuFempM+SWtjqEI7ribm9GaI5kUzHJZBrH3/Q9XAd9/GLLALxurGjbKDeLfc0D+7el26g4sYmA8=</Modulus>
<Exponent>AQAB</Exponent>
</RSAKeyValue>
</KeyValue>
</ds:KeyInfo>
</KeyDescriptor>
<SingleLogoutService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="http://passerelle.example.net/saml/singleLogout"
ResponseLocation="http://passerelle.example.net/saml/singleLogoutReturn" />
<SingleLogoutService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
Location="http://passerelle.example.net/saml/singleLogoutSOAP" />
<ManageNameIDService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="http://passerelle.example.net/saml/manageNameId"
ResponseLocation="http://passerelle.example.net/saml/manageNameIdReturn" />
<ManageNameIDService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
Location="http://passerelle.example.net/saml/manageNameIdSOAP" />
<AssertionConsumerService isDefault="true" index="0"
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"
Location="http://passerelle.example.net/saml/assertionConsumerArtifact" />
<AssertionConsumerService index="1"
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="http://passerelle.example.net/saml/assertionConsumerPost" />
<AssertionConsumerService index="2"
Binding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS"
Location="http://passerelle.example.net/saml/assertionConsumerSOAP" />
<AssertionConsumerService index="3"
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="http://passerelle.example.net/saml/assertionConsumerRedirect" />
</SPSSODescriptor>
</EntityDescriptor>'''
meta2 = meta1.replace('passerelle.example.net', 'new-passerelle.example.net')
monkeypatch.setattr(HoboDeployCommand, 'backoff_factor', 0.0001)
side_effect_iter = iter([meta1, meta2])
def side_effect(*args, **kwargs):
for v in side_effect_iter:
m = mock.Mock()
m.text = v
return m
requests_get.side_effect = side_effect
def hobo_json(env_dict):
with tempfile.NamedTemporaryFile(mode='w', dir=str(tmp_path), delete=False) as hobo_json:
hobo_json_content = json.dumps(env_dict)
hobo_json.write(hobo_json_content)
return hobo_json.name
env = {
'services': [
{
'service-id': 'authentic',
'slug': 'test',
'title': 'Test',
'this': True,
'secret_key': '12345',
'base_url': 'http://sso.example.net',
'variables': {
'other_variable': 'bar',
},
},
{
'service-id': 'passerelle',
'slug': 'passerelle',
'title': 'Passerelle',
'base_url': 'http://passerelle.example.net',
'saml-sp-metadata-url': 'http://passerelle.example.net/saml/metadata',
},
],
'users': [],
'profile': {'fields': []},
}
with mock.patch('hobo.agent.authentic2.provisionning.notify_agents'):
call_command('hobo_deploy', 'http://sso.example.net', hobo_json(env))
from hobo.multitenant.middleware import TenantMiddleware
tenants = list(TenantMiddleware.get_tenants())
assert len(tenants) == 1
tenant = tenants[0]
assert tenant.domain_url == 'sso.example.net'
assert tenant.schema_name == 'sso_example_net'
tenant_directory = tenant.get_directory()
assert tenant_directory == os.path.join(tenant_base, tenant.domain_url)
assert os.path.exists(os.path.join(tenant_directory, 'saml.crt'))
assert os.path.exists(os.path.join(tenant_directory, 'saml.key'))
from tenant_schemas.utils import tenant_context
with tenant_context(tenant):
# SAML checks
from authentic2.saml.models import LibertyProvider
assert LibertyProvider.objects.count() == 1
provider = LibertyProvider.objects.first()
provider_id = provider.pk
assert provider.entity_id == 'http://passerelle.example.net/saml/metadata'
assert provider.metadata == meta1
new_env = {
'services': [
{
'service-id': 'authentic',
'slug': 'test',
'title': 'Test',
'this': True,
'secret_key': '12345',
'base_url': 'http://sso.example.net',
'variables': {
'other_variable': 'bar',
},
},
{
'service-id': 'passerelle',
'slug': 'passerelle',
'title': 'Passerelle',
'base_url': 'http://new-passerelle.example.net',
'saml-sp-metadata-url': 'http://new-passerelle.example.net/saml/metadata',
'legacy_urls': [
{
'base_url': 'http://passerelle.example.net',
'saml-sp-metadata-url': 'http://passerelle.example.net/saml/metadata',
}
],
},
],
'users': [],
'profile': {'fields': []},
}
with mock.patch('hobo.agent.authentic2.provisionning.notify_agents'):
call_command('hobo_deploy', '--ignore-timestamp', 'http://sso.example.net', hobo_json(new_env))
# check that liberty provider is updated
with tenant_context(tenant):
from authentic2.saml.models import LibertyProvider
assert LibertyProvider.objects.count() == 1
provider = LibertyProvider.objects.first()
assert provider.metadata == meta2
assert provider.entity_id == 'http://new-passerelle.example.net/saml/metadata'
assert provider.pk == provider_id