Projet

Général

Profil

0001-hobo-handle-domain-change-59762.patch

Emmanuel Cazenave, 22 décembre 2021 17:46

Télécharger (17,8 ko)

Voir les différences:

Subject: [PATCH] hobo: handle domain change (#59762)

 tests/test_hobo_sql.py | 294 +++++++++++++++++++++++++++++++++++++++++
 wcs/ctl/check_hobos.py |  68 +++++++---
 2 files changed, 343 insertions(+), 19 deletions(-)
 create mode 100644 tests/test_hobo_sql.py
tests/test_hobo_sql.py
1
import collections
2
import copy
3
import json
4
import os
5
import random
6
import shutil
7
import tempfile
8
import zipfile
9

  
10
import psycopg2
11
import pytest
12
from quixote import cleanup
13

  
14
from wcs.ctl.check_hobos import CmdCheckHobos
15
from wcs.publisher import WcsPublisher
16
from wcs.sql import cleanup_connection
17

  
18
from .utilities import clean_temporary_pub, create_temporary_pub
19

  
20
CONFIG = {
21
    "postgresql": {
22
        "createdb-connection-params": {"database": "postgres", "user": os.environ['USER']},
23
        "database-template-name": "%s",
24
        "user": os.environ['USER'],
25
    }
26
}
27

  
28
WCS_BASE_TENANT = 'wcsteststenant%d' % random.randint(0, 100000)
29
WCS_TENANT = '%s.net' % WCS_BASE_TENANT
30
WCS_DB_NAME = '%s_net' % WCS_BASE_TENANT
31

  
32
NEW_WCS_BASE_TENANT = 'wcsteststenant%d' % random.randint(0, 100000)
33
NEW_WCS_TENANT = '%s.net' % NEW_WCS_BASE_TENANT
34
NEW_WCS_DB_NAME = '%s_net' % NEW_WCS_BASE_TENANT
35

  
36

  
37
HOBO_JSON = {
38
    'services': [
39
        {
40
            'title': 'Hobo',
41
            'slug': 'hobo',
42
            'service-id': 'hobo',
43
            'base_url': 'http://hobo.example.net/',
44
            'saml-sp-metadata-url': 'http://hobo.example.net/accounts/mellon/metadata/',
45
        },
46
        {
47
            'service-id': 'authentic',
48
            'saml-idp-metadata-url': 'http://authentic.example.net/idp/saml2/metadata',
49
            'template_name': '',
50
            'variables': {},
51
            'title': 'Authentic',
52
            'base_url': 'http://authentic.example.net/',
53
            'id': 3,
54
            'slug': 'authentic',
55
            'secret_key': '12345',
56
        },
57
        {
58
            'service-id': 'wcs',
59
            'template_name': 'publik.zip',
60
            'variables': {'xxx': 'HELLO WORLD'},
61
            'title': 'Test wcs',
62
            'saml-sp-metadata-url': 'http://%s/saml/metadata' % WCS_TENANT,
63
            'base_url': 'http://%s/' % WCS_TENANT,
64
            'backoffice-menu-url': 'http://%s/backoffice/menu.json' % WCS_TENANT,
65
            'id': 1,
66
            'secret_key': 'eiue7aa10nt6e9*#jg2bsfvdgl)cr%4(tafibfjx9i$pgnfj#v',
67
            'slug': 'test-wcs',
68
        },
69
        {
70
            'service-id': 'combo',
71
            'template_name': 'portal-agent',
72
            'title': 'Portal Agents',
73
            'base_url': 'http://agents.example.net/',
74
            'secret_key': 'aaa',
75
        },
76
        {
77
            'service-id': 'combo',
78
            'template_name': 'portal-user',
79
            'title': 'Portal',
80
            'base_url': 'http://portal.example.net/',
81
            'secret_key': 'bbb',
82
        },
83
    ],
84
    'timestamp': '1431420355.31',
85
}
86

  
87

  
88
@pytest.fixture
89
def setuptest():
90
    cleanup_connection()
91
    createdb_cfg = CONFIG['postgresql'].get('createdb-connection-params')
92
    conn = psycopg2.connect(**createdb_cfg)
93
    conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
94
    cursor = conn.cursor()
95
    for dbname in (WCS_DB_NAME, NEW_WCS_DB_NAME):
96
        cursor.execute('DROP DATABASE IF EXISTS %s' % dbname)
97

  
98
    pub = create_temporary_pub()
99
    pub.cfg['language'] = {'language': 'en'}
100
    hobo_cmd = CmdCheckHobos()
101
    hobo_cmd.all_services = HOBO_JSON
102
    WcsPublisher.APP_DIR = tempfile.mkdtemp()
103

  
104
    skeleton_dir = os.path.join(WcsPublisher.APP_DIR, 'skeletons')
105
    os.mkdir(skeleton_dir)
106
    with open(os.path.join(skeleton_dir, 'publik.zip'), 'wb') as f:
107
        with zipfile.ZipFile(f, 'w') as z:
108
            z.writestr('config.json', json.dumps(CONFIG))
109
            z.writestr('site-options.cfg', '[options]\npostgresql = true')
110

  
111
    yield pub, hobo_cmd
112

  
113
    clean_temporary_pub()
114
    shutil.rmtree(WcsPublisher.APP_DIR)
115
    cleanup_connection()
116
    for dbname in (WCS_DB_NAME, NEW_WCS_DB_NAME):
117
        cursor.execute('DROP DATABASE IF EXISTS %s' % dbname)
118
    conn.close()
119

  
120

  
121
def database_exists(database):
122
    res = False
123
    cleanup_connection()
124
    createdb_cfg = CONFIG['postgresql'].get('createdb-connection-params')
125
    conn = psycopg2.connect(**createdb_cfg)
126
    conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
127
    cursor = conn.cursor()
128
    cursor.execute("SELECT 1 AS result FROM pg_database WHERE datname='%s'" % database)
129
    if cursor.fetchall():
130
        res = True
131
    conn.close()
132
    return res
133

  
134

  
135
def test_deploy(setuptest):
136
    _, hobo_cmd = setuptest
137
    assert not os.path.exists(os.path.join(WcsPublisher.APP_DIR, 'tenants', WCS_TENANT))
138
    assert not database_exists(WCS_DB_NAME)
139
    assert not os.path.exists(os.path.join(WcsPublisher.APP_DIR, 'tenants', NEW_WCS_TENANT))
140
    assert not database_exists(NEW_WCS_DB_NAME)
141

  
142
    cleanup()
143
    with open(os.path.join(WcsPublisher.APP_DIR, 'hobo.json'), 'w') as fd:
144
        fd.write(json.dumps(HOBO_JSON))
145
    hobo_cmd = CmdCheckHobos()
146
    base_options = {}
147
    sub_options_class = collections.namedtuple('Options', ['ignore_timestamp', 'redeploy', 'extra'])
148
    sub_options = sub_options_class(True, False, None)
149
    hobo_cmd.execute(
150
        base_options,
151
        sub_options,
152
        ['http://%s/' % WCS_TENANT, os.path.join(WcsPublisher.APP_DIR, 'hobo.json')],
153
    )
154
    assert os.path.exists(os.path.join(WcsPublisher.APP_DIR, 'tenants', WCS_TENANT))
155
    assert database_exists(WCS_DB_NAME)
156

  
157
    # deploy a new tenant
158
    with open(os.path.join(WcsPublisher.APP_DIR, 'hobo.json'), 'w') as fd:
159
        hobo_json = copy.deepcopy(HOBO_JSON)
160
        wcs_service = hobo_json['services'][2]
161
        wcs_service['saml-sp-metadata-url'] = ('http://%s/saml/metadata' % WCS_TENANT,)
162
        wcs_service['base_url'] = 'http://%s/' % NEW_WCS_TENANT
163
        wcs_service['backoffice-menu-url'] = 'http://%s/backoffice/menu.json' % NEW_WCS_TENANT
164
        fd.write(json.dumps(hobo_json))
165

  
166
    hobo_cmd.execute(
167
        base_options,
168
        sub_options,
169
        ['http://%s/' % NEW_WCS_TENANT, os.path.join(WcsPublisher.APP_DIR, 'hobo.json')],
170
    )
171
    assert os.path.exists(os.path.join(WcsPublisher.APP_DIR, 'tenants', WCS_TENANT))
172
    assert database_exists(WCS_DB_NAME)
173
    assert os.path.exists(os.path.join(WcsPublisher.APP_DIR, 'tenants', NEW_WCS_TENANT))
174
    assert database_exists(NEW_WCS_DB_NAME)
175

  
176

  
177
def test_deploy_url_change(setuptest):
178
    _, hobo_cmd = setuptest
179
    assert not os.path.exists(os.path.join(WcsPublisher.APP_DIR, 'tenants', WCS_TENANT))
180
    assert not database_exists(WCS_DB_NAME)
181
    assert not os.path.exists(os.path.join(WcsPublisher.APP_DIR, 'tenants', NEW_WCS_TENANT))
182
    assert not database_exists(NEW_WCS_DB_NAME)
183

  
184
    cleanup()
185
    with open(os.path.join(WcsPublisher.APP_DIR, 'hobo.json'), 'w') as fd:
186
        fd.write(json.dumps(HOBO_JSON))
187
    hobo_cmd = CmdCheckHobos()
188
    base_options = {}
189
    sub_options_class = collections.namedtuple('Options', ['ignore_timestamp', 'redeploy', 'extra'])
190
    sub_options = sub_options_class(True, False, None)
191
    hobo_cmd.execute(
192
        base_options,
193
        sub_options,
194
        ['http://%s/' % WCS_TENANT, os.path.join(WcsPublisher.APP_DIR, 'hobo.json')],
195
    )
196
    assert os.path.exists(os.path.join(WcsPublisher.APP_DIR, 'tenants', WCS_TENANT))
197
    assert database_exists(WCS_DB_NAME)
198

  
199
    # domain change request
200
    with open(os.path.join(WcsPublisher.APP_DIR, 'hobo.json'), 'w') as fd:
201
        hobo_json = copy.deepcopy(HOBO_JSON)
202
        wcs_service = hobo_json['services'][2]
203
        wcs_service['legacy_urls'] = [
204
            {
205
                'saml-sp-metadata-url': wcs_service['saml-sp-metadata-url'],
206
                'base_url': wcs_service['base_url'],
207
                'backoffice-menu-url': wcs_service['backoffice-menu-url'],
208
            }
209
        ]
210

  
211
        wcs_service['saml-sp-metadata-url'] = ('http://%s/saml/metadata' % WCS_TENANT,)
212
        wcs_service['base_url'] = 'http://%s/' % NEW_WCS_TENANT
213
        wcs_service['backoffice-menu-url'] = 'http://%s/backoffice/menu.json' % NEW_WCS_TENANT
214
        fd.write(json.dumps(hobo_json))
215

  
216
    hobo_cmd.execute(
217
        base_options,
218
        sub_options,
219
        ['http://%s/' % NEW_WCS_TENANT, os.path.join(WcsPublisher.APP_DIR, 'hobo.json')],
220
    )
221
    assert not os.path.exists(os.path.join(WcsPublisher.APP_DIR, 'tenants', WCS_TENANT))
222
    assert database_exists(WCS_DB_NAME)
223
    assert os.path.exists(os.path.join(WcsPublisher.APP_DIR, 'tenants', NEW_WCS_TENANT))
224
    assert not database_exists(NEW_WCS_DB_NAME)
225

  
226
    publisher = WcsPublisher.create_publisher()
227
    publisher.set_tenant_by_hostname(NEW_WCS_TENANT)
228
    # check that WCS_DB_NAME is used by NEW_WCS_TENANT
229
    assert publisher.cfg['postgresql']['database'] == WCS_DB_NAME
230
    # check that sp configuration is updated
231
    assert publisher.cfg['sp']['saml2_providerid'] == 'http://%s/saml/metadata' % NEW_WCS_TENANT
232

  
233

  
234
def test_deploy_url_change_old_tenant_dir(setuptest):
235
    _, hobo_cmd = setuptest
236
    assert not os.path.exists(os.path.join(WcsPublisher.APP_DIR, 'tenants', WCS_TENANT))
237
    assert not database_exists(WCS_DB_NAME)
238
    assert not os.path.exists(os.path.join(WcsPublisher.APP_DIR, 'tenants', NEW_WCS_TENANT))
239
    assert not database_exists(NEW_WCS_DB_NAME)
240

  
241
    cleanup()
242
    with open(os.path.join(WcsPublisher.APP_DIR, 'hobo.json'), 'w') as fd:
243
        fd.write(json.dumps(HOBO_JSON))
244
    hobo_cmd = CmdCheckHobos()
245
    base_options = {}
246
    sub_options_class = collections.namedtuple('Options', ['ignore_timestamp', 'redeploy', 'extra'])
247
    sub_options = sub_options_class(True, False, None)
248
    hobo_cmd.execute(
249
        base_options,
250
        sub_options,
251
        ['http://%s/' % WCS_TENANT, os.path.join(WcsPublisher.APP_DIR, 'hobo.json')],
252
    )
253
    assert os.path.exists(os.path.join(WcsPublisher.APP_DIR, 'tenants', WCS_TENANT))
254
    assert database_exists(WCS_DB_NAME)
255
    # move tenant to APP_DIR (legacy way)
256
    os.replace(
257
        os.path.join(WcsPublisher.APP_DIR, 'tenants', WCS_TENANT),
258
        os.path.join(WcsPublisher.APP_DIR, WCS_TENANT),
259
    )
260
    assert not os.path.exists(os.path.join(WcsPublisher.APP_DIR, 'tenants', WCS_TENANT))
261

  
262
    # domain change request
263
    with open(os.path.join(WcsPublisher.APP_DIR, 'hobo.json'), 'w') as fd:
264
        hobo_json = copy.deepcopy(HOBO_JSON)
265
        wcs_service = hobo_json['services'][2]
266
        wcs_service['legacy_urls'] = [
267
            {
268
                'saml-sp-metadata-url': wcs_service['saml-sp-metadata-url'],
269
                'base_url': wcs_service['base_url'],
270
                'backoffice-menu-url': wcs_service['backoffice-menu-url'],
271
            }
272
        ]
273

  
274
        wcs_service['saml-sp-metadata-url'] = ('http://%s/saml/metadata' % WCS_TENANT,)
275
        wcs_service['base_url'] = 'http://%s/' % NEW_WCS_TENANT
276
        wcs_service['backoffice-menu-url'] = 'http://%s/backoffice/menu.json' % NEW_WCS_TENANT
277
        fd.write(json.dumps(hobo_json))
278

  
279
    hobo_cmd.execute(
280
        base_options,
281
        sub_options,
282
        ['http://%s/' % NEW_WCS_TENANT, os.path.join(WcsPublisher.APP_DIR, 'hobo.json')],
283
    )
284
    assert not os.path.exists(os.path.join(WcsPublisher.APP_DIR, WCS_TENANT))
285
    assert database_exists(WCS_DB_NAME)
286
    assert os.path.exists(os.path.join(WcsPublisher.APP_DIR, 'tenants', NEW_WCS_TENANT))
287
    assert not database_exists(NEW_WCS_DB_NAME)
288

  
289
    publisher = WcsPublisher.create_publisher()
290
    publisher.set_tenant_by_hostname(NEW_WCS_TENANT)
291
    # check that WCS_DB_NAME is used by NEW_WCS_TENANT
292
    assert publisher.cfg['postgresql']['database'] == WCS_DB_NAME
293
    # check that sp configuration is updated
294
    assert publisher.cfg['sp']['saml2_providerid'] == 'http://%s/saml/metadata' % NEW_WCS_TENANT
wcs/ctl/check_hobos.py
126 126
        if base_url.endswith('/'):  # wcs doesn't expect a trailing slash
127 127
            service['base_url'] = base_url[:-1]
128 128

  
129
        new_site = False
130
        force_spconfig = False
129 131
        try:
130
            pub.set_tenant_by_hostname(self.get_instance_path(service), skip_sql=True)
132
            pub.set_tenant_by_hostname(self.get_instance_path(service.get('base_url')), skip_sql=True)
131 133
        except UnknownTenantError:
132 134
            if not os.path.exists(global_tenants_dir):
133 135
                os.mkdir(global_tenants_dir)
134
            tenant_app_dir = os.path.join(global_tenants_dir, self.get_instance_path(service))
135
            if not os.path.exists(tenant_app_dir):
136
            tenant_app_dir = os.path.join(global_tenants_dir, self.get_instance_path(service.get('base_url')))
137
            # check in legacy_urls for domain change
138
            for legacy_urls in service.get('legacy_urls', []):
139
                legacy_base_url = legacy_urls.get('base_url')
140
                if legacy_base_url.endswith('/'):  # wcs doesn't expect a trailing slash
141
                    legacy_base_url = legacy_base_url[:-1]
142
                legacy_instance_path = self.get_instance_path(legacy_base_url)
143
                try:
144
                    pub.set_tenant_by_hostname(legacy_instance_path, skip_sql=True)
145
                    # rename tenant directory
146
                    for base_dir in (global_app_dir, global_tenants_dir):
147
                        legacy_tenant_dir = os.path.join(base_dir, legacy_instance_path)
148
                        if os.path.exists(legacy_tenant_dir):
149
                            print('rename tenant directory %s to %s' % (legacy_tenant_dir, tenant_app_dir))
150
                            os.rename(legacy_tenant_dir, tenant_app_dir)
151
                            break
152
                    else:
153
                        print('tenant directory not found')
154
                        return
155
                    pub.set_tenant_by_hostname(self.get_instance_path(service.get('base_url')))
156
                    force_spconfig = True
157
                    break
158
                except UnknownTenantError:
159
                    pass
160
            else:
161
                # new tenant
136 162
                print('initializing instance in', tenant_app_dir)
137
            os.mkdir(tenant_app_dir)
138
            pub.set_tenant_by_hostname(self.get_instance_path(service))
139

  
140
            if service.get('template_name'):
141
                skeleton_filepath = os.path.join(global_app_dir, 'skeletons', service.get('template_name'))
142
                if os.path.exists(skeleton_filepath):
143
                    with open(skeleton_filepath, 'rb') as fd:
144
                        pub.import_zip(fd)
145
            new_site = True
163
                os.mkdir(tenant_app_dir)
164
                pub.set_tenant_by_hostname(self.get_instance_path(service.get('base_url')))
165
                if service.get('template_name'):
166
                    skeleton_filepath = os.path.join(
167
                        global_app_dir, 'skeletons', service.get('template_name')
168
                    )
169
                    if os.path.exists(skeleton_filepath):
170
                        with open(skeleton_filepath, 'rb') as fd:
171
                            pub.import_zip(fd)
172
                new_site = True
173

  
146 174
        else:
147 175
            print('updating instance in', pub.app_dir)
148
            new_site = False
149 176

  
150 177
        try:
151 178
            self.configure_site_options(service, pub, ignore_timestamp=sub_options.ignore_timestamp)
......
157 184
        if new_site:
158 185
            self.configure_sql(service, pub)
159 186
        self.update_configuration(service, pub)
160
        self.configure_authentication_methods(service, pub)
187
        self.configure_authentication_methods(service, pub, force_spconfig)
161 188

  
162 189
        self.update_profile(self.all_services.get('profile', {}), pub)
163 190
        # Store hobo.json
......
307 334
                    idp['attribute-mapping'][str(attribute_name)] = str(field_id)
308 335
        pub.write_cfg()
309 336

  
310
    def configure_authentication_methods(self, service, pub):
337
    def configure_authentication_methods(self, service, pub, force_spconfig=False):
311 338
        # look for an identity provider
312 339
        idps = [x for x in self.all_services.get('services', []) if x.get('service-id') == 'authentic']
313 340
        if not pub.cfg.get('identification'):
......
328 355
            return
329 356

  
330 357
        # initialize service provider side
331
        if not pub.cfg['sp'].get('publickey'):
358
        if not pub.cfg['sp'].get('publickey') or force_spconfig:
332 359
            from ..qommon.ident.idp import MethodAdminDirectory
333 360

  
334 361
            spconfig = pub.cfg['sp']
......
386 413
            pub.cfg['saml_identities']['registration-url'] = str('%saccounts/register/' % idp['base_url'])
387 414
            pub.write_cfg()
388 415

  
389
    def get_instance_path(self, service):
390
        parsed_url = urllib.parse.urlsplit(service.get('base_url'))
416
    def get_instance_path(self, base_url):
417
        parsed_url = urllib.parse.urlsplit(base_url)
391 418
        instance_path = parsed_url.netloc
392 419
        if parsed_url.path:
393 420
            instance_path += '+%s' % parsed_url.path.replace('/', '+')
......
534 561

  
535 562
        # determine database name using the instance path
536 563
        domain_table_name = (
537
            self.get_instance_path(service).replace('-', '_').replace('.', '_').replace('+', '_')
564
            self.get_instance_path(service.get('base_url'))
565
            .replace('-', '_')
566
            .replace('.', '_')
567
            .replace('+', '_')
538 568
        )
539 569

  
540 570
        if pub.cfg['postgresql'].get('database-template-name'):
541
-