Projet

Général

Profil

0001-hobo_deploy-handle-url-change-on-a-service-58908.patch

Emmanuel Cazenave, 13 décembre 2021 17:41

Télécharger (20,7 ko)

Voir les différences:

Subject: [PATCH 1/2] hobo_deploy: handle url change on a service (#58908)

 .../management/commands/hobo_deploy.py        |  18 +-
 .../common/management/commands/hobo_deploy.py |  13 +-
 .../migrations/0022_legacy_urls.py            |  59 ++++++
 hobo/environment/models.py                    |  25 +++
 tests/test_hobo_deploy.py                     |   4 +-
 tests_authentic/test_hobo_deploy.py           | 178 ++++++++++++++++++
 tests_schemas/legacy_urls_chrono_env.json     |  45 +++++
 tests_schemas/test_hobo_deploy.py             |  25 ++-
 8 files changed, 358 insertions(+), 9 deletions(-)
 create mode 100644 hobo/environment/migrations/0022_legacy_urls.py
 create mode 100644 tests_schemas/legacy_urls_chrono_env.json
hobo/agent/authentic2/management/commands/hobo_deploy.py
155 155
                        continue
156 156
                    metadata_text = metadata_response.text
157 157

  
158
                    provider, service_created = LibertyProvider.objects.get_or_create(
159
                        entity_id=sp_url, protocol_conformance=lasso.PROTOCOL_SAML_2_0
160
                    )
158
                    provider, service_created = None, False
159
                    for legacy_urls in service.get('legacy_urls', []):
160
                        try:
161
                            provider = LibertyProvider.objects.get(
162
                                entity_id=legacy_urls['saml-sp-metadata-url'],
163
                                protocol_conformance=lasso.PROTOCOL_SAML_2_0,
164
                            )
165
                            provider.entity_id = sp_url
166
                            break
167
                        except LibertyProvider.DoesNotExist:
168
                            pass
169
                    if not provider:
170
                        provider, service_created = LibertyProvider.objects.get_or_create(
171
                            entity_id=sp_url, protocol_conformance=lasso.PROTOCOL_SAML_2_0
172
                        )
161 173
                    provider.name = service['title']
162 174
                    provider.slug = service['slug']
163 175
                    provider.federation_source = 'hobo'
hobo/agent/common/management/commands/hobo_deploy.py
74 74
            # early exit, we don't redeploy secondary services
75 75
            return
76 76
        domain = urlparse.urlparse(self.me.get('base_url')).netloc.split(':')[0]
77
        legacy_domain = None
77 78

  
78 79
        try:
79 80
            tenant = TenantMiddleware.get_tenant_by_hostname(domain)
80 81
        except TenantNotFound:
81
            # create tenant for domain
82
            call_command('create_tenant', domain)
82
            # might be a domain change request
83
            for legacy_urls in self.me.get('legacy_urls', []):
84
                old_domain = urlparse.urlparse(legacy_urls['base_url']).netloc.split(':')[0]
85
                try:
86
                    tenant = TenantMiddleware.get_tenant_by_hostname(old_domain)
87
                    legacy_domain = old_domain
88
                    break
89
                except TenantNotFound:
90
                    pass
91
            call_command('create_tenant', domain, legacy_hostname=legacy_domain)
83 92
            tenant = TenantMiddleware.get_tenant_by_hostname(domain)
84 93

  
85 94
        timestamp = hobo_environment.get('timestamp')
hobo/environment/migrations/0022_legacy_urls.py
1
# Generated by Django 2.2.24 on 2021-12-02 13:55
2

  
3
import django.contrib.postgres.fields.jsonb
4
from django.db import migrations
5

  
6

  
7
class Migration(migrations.Migration):
8

  
9
    dependencies = [
10
        ('environment', '0021_base_url_validators'),
11
    ]
12

  
13
    operations = [
14
        migrations.AddField(
15
            model_name='authentic',
16
            name='legacy_urls',
17
            field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, null=True),
18
        ),
19
        migrations.AddField(
20
            model_name='bijoe',
21
            name='legacy_urls',
22
            field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, null=True),
23
        ),
24
        migrations.AddField(
25
            model_name='chrono',
26
            name='legacy_urls',
27
            field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, null=True),
28
        ),
29
        migrations.AddField(
30
            model_name='combo',
31
            name='legacy_urls',
32
            field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, null=True),
33
        ),
34
        migrations.AddField(
35
            model_name='fargo',
36
            name='legacy_urls',
37
            field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, null=True),
38
        ),
39
        migrations.AddField(
40
            model_name='hobo',
41
            name='legacy_urls',
42
            field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, null=True),
43
        ),
44
        migrations.AddField(
45
            model_name='passerelle',
46
            name='legacy_urls',
47
            field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, null=True),
48
        ),
49
        migrations.AddField(
50
            model_name='wcs',
51
            name='legacy_urls',
52
            field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, null=True),
53
        ),
54
        migrations.AddField(
55
            model_name='welco',
56
            name='legacy_urls',
57
            field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, null=True),
58
        ),
59
    ]
hobo/environment/models.py
19 19
import random
20 20
import re
21 21
import socket
22
import time
22 23

  
23 24
import requests
24 25
from django.conf import settings
25 26
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
26 27
from django.contrib.contenttypes.models import ContentType
28
from django.contrib.postgres.fields import JSONField
27 29
from django.core.cache import cache
28 30
from django.core.exceptions import ValidationError
29 31
from django.core.validators import URLValidator
......
104 106
    title = models.CharField(_('Title'), max_length=50)
105 107
    slug = models.SlugField(_('Slug'))
106 108
    base_url = models.CharField(_('Base URL'), max_length=200, validators=[URLValidator()])
109
    legacy_urls = JSONField(null=True, default=list, blank=True)
107 110
    secret_key = models.CharField(_('Secret Key'), max_length=60)
108 111
    template_name = models.CharField(_('Template'), max_length=60, blank=True)
109 112
    secondary = models.BooleanField(_('Secondary Service'), default=False)
......
159 162
            ]
160 163
        )
161 164
        as_dict['base_url'] = self.get_base_url_path()
165
        if self.legacy_urls:
166
            as_dict['legacy_urls'] = self.legacy_urls
162 167
        as_dict['service-id'] = self.Extra.service_id
163 168
        as_dict['service-label'] = force_text(self.Extra.service_label)
164 169
        as_dict['variables'] = dict(((v.name, v.json) for v in self.variables.all()))
......
294 299
            result[name] = value
295 300
        return result
296 301

  
302
    def change_base_url(self, base_url):
303
        service_dict = self.as_dict()
304
        timestamp = datetime.datetime.now()
305
        legacy_urls = {
306
            'base_url': service_dict['base_url'],
307
            'timestamp': str(time.mktime(timestamp.timetuple()) + timestamp.microsecond / 1e6),
308
        }
309
        for url_key in (
310
            'saml-sp-metadata-url',
311
            'saml-idp-metadata-url',
312
            'backoffice-menu-url',
313
            'provisionning-url',
314
        ):
315
            if url_key in service_dict:
316
                legacy_urls[url_key] = service_dict[url_key]
317
        if not self.legacy_urls:
318
            self.legacy_urls = []
319
        self.legacy_urls.insert(0, legacy_urls)
320
        self.base_url = base_url
321

  
297 322

  
298 323
class Authentic(ServiceBase):
299 324
    use_as_idp_for_self = models.BooleanField(verbose_name=_('Use as IdP'), default=False)
tests/test_hobo_deploy.py
152 152
    mocked_get_tenant_by_hostname.side_effect = [TenantNotFound, tenant]
153 153
    with patch('hobo.agent.common.management.commands.hobo_deploy.call_command') as mocked_call_command:
154 154
        command.deploy(base_url, ENVIRONMENT, None)
155
    assert mocked_call_command.mock_calls == [call('create_tenant', 'combo.dev.publik.love')]
155
    assert mocked_call_command.mock_calls == [
156
        call('create_tenant', 'combo.dev.publik.love', legacy_hostname=None)
157
    ]
156 158
    assert_deployed()
157 159

  
158 160
    # already there (timestamp do not change)
tests_authentic/test_hobo_deploy.py
564 564
    export_ref = sort_and_remove_uuid(export_site())
565 565
    file_ref = sort_and_remove_uuid(json.loads(content))
566 566
    assert export_ref == file_ref
567

  
568

  
569
def test_hobo_deploy_with_legacy_urls(monkeypatch, tenant_base, mocker, skeleton_dir, tmp_path):
570
    from django.core.management import call_command
571

  
572
    from hobo.agent.authentic2.management.commands.hobo_deploy import Command as HoboDeployCommand
573

  
574
    requests_get = mocker.patch('requests.get')
575
    meta1 = '''<?xml version="1.0"?>
576
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
577
    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
578
    xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
579
    entityID="http://passerelle.example.net/saml/metadata">
580
  <SPSSODescriptor
581
    AuthnRequestsSigned="true" WantAssertionsSigned="true"
582
    protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
583
    <KeyDescriptor use="signing">
584
      <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
585
        <KeyValue  xmlns="http://www.w3.org/2000/09/xmldsig#">
586
    <RSAKeyValue>
587
        <Modulus>nJpkBznHNbvE+RAC6mU+NPQnIWs8gFNCm6I3FPcUKYpaJbXaurJ4cJgvnaEiqIXPQDcbHxuLeCbYbId9yascWZirvQbh8d/r+Vv+24bPG++9gW+i3Nnz1VW8V+z0b+puHWvM/FjJjBNJgWkI38gaupz47U6/02CtWx00stitiwk=</Modulus>
588
        <Exponent>AQAB</Exponent>
589
    </RSAKeyValue>
590
</KeyValue>
591
      </ds:KeyInfo>
592
    </KeyDescriptor>
593
    <KeyDescriptor use="encryption">
594
      <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
595
        <KeyValue  xmlns="http://www.w3.org/2000/09/xmldsig#">
596
    <RSAKeyValue>
597
        <Modulus>3BxSiAzGvY1Yuqa31L7Zr2WHM/8cn5oX+Q6A2SYgzjuvAgnWyizN8YgW/fHR4G7MtkmZ5RFJLXfcSLwbUfpFHV6KO1ikbgViYuFempM+SWtjqEI7ribm9GaI5kUzHJZBrH3/Q9XAd9/GLLALxurGjbKDeLfc0D+7el26g4sYmA8=</Modulus>
598
        <Exponent>AQAB</Exponent>
599
    </RSAKeyValue>
600
</KeyValue>
601
      </ds:KeyInfo>
602
    </KeyDescriptor>
603

  
604
    <SingleLogoutService
605
      Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
606
      Location="http://passerelle.example.net/saml/singleLogout"
607
      ResponseLocation="http://passerelle.example.net/saml/singleLogoutReturn" />
608
    <SingleLogoutService
609
      Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
610
      Location="http://passerelle.example.net/saml/singleLogoutSOAP" />
611
    <ManageNameIDService
612
      Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
613
      Location="http://passerelle.example.net/saml/manageNameId"
614
      ResponseLocation="http://passerelle.example.net/saml/manageNameIdReturn" />
615
    <ManageNameIDService
616
      Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
617
      Location="http://passerelle.example.net/saml/manageNameIdSOAP" />
618
    <AssertionConsumerService isDefault="true" index="0"
619
      Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"
620
      Location="http://passerelle.example.net/saml/assertionConsumerArtifact" />
621
    <AssertionConsumerService index="1"
622
      Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
623
      Location="http://passerelle.example.net/saml/assertionConsumerPost" />
624
    <AssertionConsumerService index="2"
625
      Binding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS"
626
      Location="http://passerelle.example.net/saml/assertionConsumerSOAP" />
627
    <AssertionConsumerService index="3"
628
      Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
629
      Location="http://passerelle.example.net/saml/assertionConsumerRedirect" />
630
  </SPSSODescriptor>
631
</EntityDescriptor>'''
632
    meta2 = meta1.replace('passerelle.example.net', 'new-passerelle.example.net')
633
    monkeypatch.setattr(HoboDeployCommand, 'backoff_factor', 0.0001)
634

  
635
    side_effect_iter = iter([meta1, meta2])
636

  
637
    def side_effect(*args, **kwargs):
638
        for v in side_effect_iter:
639
            m = mock.Mock()
640
            m.text = v
641
            return m
642

  
643
    requests_get.side_effect = side_effect
644

  
645
    def hobo_json(env_dict):
646
        with tempfile.NamedTemporaryFile(mode='w', dir=str(tmp_path), delete=False) as hobo_json:
647
            hobo_json_content = json.dumps(env_dict)
648
            hobo_json.write(hobo_json_content)
649
            return hobo_json.name
650

  
651
    env = {
652
        'services': [
653
            {
654
                'service-id': 'authentic',
655
                'slug': 'test',
656
                'title': 'Test',
657
                'this': True,
658
                'secret_key': '12345',
659
                'base_url': 'http://sso.example.net',
660
                'variables': {
661
                    'other_variable': 'bar',
662
                },
663
            },
664
            {
665
                'service-id': 'passerelle',
666
                'slug': 'passerelle',
667
                'title': u'Passerelle',
668
                'base_url': 'http://passerelle.example.net',
669
                'saml-sp-metadata-url': 'http://passerelle.example.net/saml/metadata',
670
            },
671
        ],
672
        'users': [],
673
        'profile': {'fields': []},
674
    }
675

  
676
    with mock.patch('hobo.agent.authentic2.provisionning.notify_agents'):
677
        call_command('hobo_deploy', 'http://sso.example.net', hobo_json(env))
678

  
679
    from hobo.multitenant.middleware import TenantMiddleware
680

  
681
    tenants = list(TenantMiddleware.get_tenants())
682
    assert len(tenants) == 1
683
    tenant = tenants[0]
684
    assert tenant.domain_url == 'sso.example.net'
685
    assert tenant.schema_name == 'sso_example_net'
686
    tenant_directory = tenant.get_directory()
687
    assert tenant_directory == os.path.join(tenant_base, tenant.domain_url)
688
    assert os.path.exists(os.path.join(tenant_directory, 'saml.crt'))
689
    assert os.path.exists(os.path.join(tenant_directory, 'saml.key'))
690

  
691
    from tenant_schemas.utils import tenant_context
692

  
693
    with tenant_context(tenant):
694
        # SAML checks
695
        from authentic2.saml.models import LibertyProvider
696

  
697
        assert LibertyProvider.objects.count() == 1
698
        provider = LibertyProvider.objects.first()
699
        provider_id = provider.pk
700
        assert provider.entity_id == 'http://passerelle.example.net/saml/metadata'
701
        assert provider.metadata == meta1
702

  
703
    new_env = {
704
        'services': [
705
            {
706
                'service-id': 'authentic',
707
                'slug': 'test',
708
                'title': 'Test',
709
                'this': True,
710
                'secret_key': '12345',
711
                'base_url': 'http://sso.example.net',
712
                'variables': {
713
                    'other_variable': 'bar',
714
                },
715
            },
716
            {
717
                'service-id': 'passerelle',
718
                'slug': 'passerelle',
719
                'title': u'Passerelle',
720
                'base_url': 'http://new-passerelle.example.net',
721
                'saml-sp-metadata-url': 'http://new-passerelle.example.net/saml/metadata',
722
                'legacy_urls': [
723
                    {
724
                        'base_url': 'http://passerelle.example.net',
725
                        'saml-sp-metadata-url': 'http://passerelle.example.net/saml/metadata',
726
                    }
727
                ],
728
            },
729
        ],
730
        'users': [],
731
        'profile': {'fields': []},
732
    }
733

  
734
    with mock.patch('hobo.agent.authentic2.provisionning.notify_agents'):
735
        call_command('hobo_deploy', '--ignore-timestamp', 'http://sso.example.net', hobo_json(new_env))
736
    # check that liberty provider is updated
737
    with tenant_context(tenant):
738
        from authentic2.saml.models import LibertyProvider
739

  
740
        assert LibertyProvider.objects.count() == 1
741
        provider = LibertyProvider.objects.first()
742
        assert provider.metadata == meta2
743
        assert provider.entity_id == 'http://new-passerelle.example.net/saml/metadata'
744
        assert provider.pk == provider_id
tests_schemas/legacy_urls_chrono_env.json
1
{
2
    "services": [
3
	{
4
	    "service-id": "chrono",
5
	    "base_url": "https://new-chrono.dev.publik.love/",
6
	    "slug": "agendas",
7
	    "title": "CHRONO",
8
	    "secret_key": "123",
9
	    "template_name": "import_me",
10
            "legacy_urls": [
11
                {
12
                    "base_url": "https://chrono.dev.publik.love/"
13
                }
14
            ]
15
	},
16
	{
17
	    "service-id": "wcs",
18
	    "base_url": "https://wcs.dev.publik.love/",
19
	    "slug": "eservices",
20
	    "title": "WCS"
21
	},
22
	{
23
	    "service-id": "hobo",
24
	    "base_url": "https://hobo.dev.publik.love/",
25
	    "slug": "hobo",
26
	    "title": "HOBO",
27
	    "secret_key": "123"
28
	},
29
	{
30
	    "service-id": "combo",
31
	    "base_url": "https://combo.dev.publik.love/",
32
	    "slug": "portal",
33
	    "title": "COMBO",
34
	    "secret_key": "123",
35
	    "template_name": "import_me"
36
	},
37
	{
38
	    "service-id": "authentic",
39
	    "base_url": "https://authentic.dev.publik.love/",
40
	    "slug": "idp",
41
	    "title": "A2",
42
	    "secret_key": "123"
43
	}
44
    ]
45
}
tests_schemas/test_hobo_deploy.py
1 1
import os
2 2

  
3 3
import mock
4
import pytest
4 5
from django.core.management import call_command, get_commands, load_command_class
5 6
from tenant_schemas.utils import tenant_context
6 7

  
7 8
from hobo.environment.models import Variable
8
from hobo.multitenant.middleware import TenantMiddleware
9
from hobo.multitenant.middleware import TenantMiddleware, TenantNotFound
9 10

  
10 11

  
11 12
def assert_deployed(domain):
......
26 27
    command = load_command_class('hobo.agent.common', 'hobo_deploy')
27 28
    domain = 'chrono.dev.publik.love'
28 29

  
29
    def my_call_command(command, parameter):
30
    def my_call_command(command, parameter, **kwargs):
30 31
        if command == 'import_template':
31 32
            my_call_command.import_template_was_called = True
32 33
            return
33
        call_command(command, parameter)
34
        call_command(command, parameter, **kwargs)
34 35

  
35 36
    mocked_get_commands.return_value = ['import_template']
36 37
    mocked_call_command.side_effect = my_call_command
......
77 78
# fails to simulate call from bijoe agent, that overload deploy_specifics()
78 79
# $ bijoe-mange hobo-deploy
79 80
# here, because this code is not implemented here
81

  
82

  
83
def test_deploy_with_legacy_urls(db):
84
    command = load_command_class('hobo.agent.common', 'hobo_deploy')
85
    domain = 'chrono.dev.publik.love'
86
    command.handle('https://%s/' % domain, 'tests_schemas/env.json')
87
    assert_deployed(domain)
88
    tenant_directory = TenantMiddleware.get_tenant_by_hostname(domain).get_directory()
89

  
90
    # change domain
91
    new_domain = 'new-chrono.dev.publik.love'
92
    command.handle('https://%s/' % new_domain, 'tests_schemas/legacy_urls_chrono_env.json')
93
    assert_deployed(new_domain)
94

  
95
    # check old tenant is gone
96
    with pytest.raises(TenantNotFound):
97
        TenantMiddleware.get_tenant_by_hostname(domain)
98
    assert not os.path.exists(tenant_directory)
80
-