Projet

Général

Profil

0001-agent-add-resync_users-common-command-45276.patch

Paul Marillonnet, 30 septembre 2021 15:44

Télécharger (9,32 ko)

Voir les différences:

Subject: [PATCH] agent: add resync_users common command (#45276)

 .../management/commands/resync_users.py       |  85 ++++++++++++++
 tests/settings.py                             |   6 +
 tests/test_resync_users.py                    | 107 ++++++++++++++++++
 3 files changed, 198 insertions(+)
 create mode 100644 hobo/agent/common/management/commands/resync_users.py
 create mode 100644 tests/test_resync_users.py
hobo/agent/common/management/commands/resync_users.py
1
# hobo - portal to configure and deploy applications
2
# Copyright (C) 2015-2021  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import datetime
18
import logging
19

  
20
import requests
21
from django.conf import settings
22
from django.contrib.auth import get_user_model
23
from django.core.management.base import BaseCommand
24
from mellon.models import UserSAMLIdentifier
25

  
26
from hobo.signature import sign_url
27

  
28

  
29
class Command(BaseCommand):
30
    def handle(self, *args, **options):
31
        logger = logging.getLogger(__name__)
32
        User = get_user_model()
33
        self.verbosity = int(options['verbosity'])
34

  
35
        base_idp_url = settings.KNOWN_SERVICES['authentic'].get('idp', {}).get('url', None)
36
        if not base_idp_url:
37
            self.stderr.write('resync_users: no base api url known, required for users resynchronization')
38
            return
39
        secret = settings.KNOWN_SERVICES['authentic'].get('other', {}).get('secret', None)
40
        if not secret:
41
            self.stderr.write(
42
                'resync_users: no secret is known for authentic instance %s, yet required for users resynchronization',
43
                base_idp_url,
44
            )
45
            return
46

  
47
        url = base_idp_url + 'api/users/synchronization/'
48

  
49
        # check all existing users
50
        def chunks(l, n):
51
            for i in range(0, len(l), n):
52
                yield l[i : i + n]
53

  
54
        unknown_uuids = []
55
        for usersamlids in chunks(UserSAMLIdentifier.objects.all(), 100):
56
            uuids = [x.name_id for x in usersamlids]
57
            resp = requests.post(sign_url(url, key=secret), json={'known_uuids': uuids})
58
            unknown_uuids.extend(resp.json().get('unknown_uuids'))
59

  
60
        for usersamlid in UserSAMLIdentifier.objects.filter(name_id__in=unknown_uuids):
61
            if self.verbosity > 0:
62
                self.stdout.write('deleted user %s with uuid %s' % (usersamlid.user, usersamlid.name_id))
63
            usersamlid.user.delete()
64

  
65
        # update recently modified users
66
        url = settings.A2_SOURCE_API_BASE_URL + 'users/?modified__gt=%s' % (
67
            datetime.datetime.now() - datetime.timedelta(seconds=120)
68
        ).strftime('%Y-%m-%dT%H:%M:%S')
69
        resp = requests.get(sign_url(url, key=secret))
70
        for user_dict in resp.json()['results']:
71
            try:
72
                user = User.objects.get(email=user_dict['email'])
73
            except User.DoesNotExist:
74
                continue
75
            modified = []
76
            for attr in ['first_name', 'last_name', 'username']:
77
                if attr in user_dict:
78
                    modified.append(attr)
79
                    setattr(user, attr, user_dict.get(attr))
80
            if modified and self.verbosity > 0:
81
                self.stdout.write(
82
                    'modified user %s with new attributes %s'
83
                    % (user, dict((attr, user_dict.get(attr)) for attr in modified))
84
                )
85
            user.save()
tests/settings.py
24 24
        },
25 25
    }
26 26
}
27

  
28
KNOWN_SERVICES = {
29
    'authentic': {'idp': {'url': 'https://authentic.dev.publik.null/'}, 'other': {'secret': 'xyz'}},
30
}
31

  
32
A2_SOURCE_API_BASE_URL = 'https://authentic.dev.publik.love/api/'
tests/test_resync_users.py
1
# hobo - portal to configure and deploy applications
2
# Copyright (C) 2015-2021  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import json
18
import os
19
import sys
20

  
21
import pytest
22
from django.contrib.auth import get_user_model
23
from mellon.models import Issuer, UserSAMLIdentifier
24
from mock import Mock, call, patch
25
from requests import Response
26

  
27
from hobo.agent.common.management.commands.hobo_deploy import Command as DeployCommand
28
from hobo.agent.common.management.commands.hobo_deploy import CommandError, replace_file
29
from hobo.agent.common.management.commands.resync_users import Command as ResyncUsersCommand
30
from hobo.agent.hobo.management.commands.hobo_deploy import Command as HoboDeployCommand
31
from hobo.environment.models import Combo, Hobo, Variable, Wcs
32
from hobo.multitenant.middleware import TenantNotFound
33

  
34
User = get_user_model()
35

  
36
pytestmark = pytest.mark.django_db
37

  
38

  
39
@patch('hobo.agent.common.management.commands.hobo_deploy.TenantMiddleware.get_tenant_by_hostname')
40
def test_resync_users(mocked_get_tenant_by_hostname, db, tmpdir, settings):
41
    command = DeployCommand()
42
    command.deploy_specifics = Mock()
43
    tenant = Mock()
44
    tenant.get_directory = Mock(return_value=str(tmpdir))
45
    combo_base_url = 'https://combo.dev.publik.love/'
46
    wcs_base_url = 'https://wcs.dev.publik.love/'
47
    tenant_hobo_json = os.path.join(str(tmpdir), 'hobo.json')
48

  
49
    ENVIRONMENT = {
50
        'services': [
51
            {
52
                'service-id': 'combo',
53
                'base_url': combo_base_url,
54
                'id': 1,
55
            },
56
            {
57
                'service-id': 'wcs',
58
                'base_url': wcs_base_url,
59
                'id': 1,
60
            },
61
        ]
62
    }
63

  
64
    command.deploy_specifics.reset_mock()
65
    mocked_get_tenant_by_hostname.reset_mock()
66
    mocked_get_tenant_by_hostname.side_effect = [TenantNotFound, tenant]
67
    with patch('hobo.agent.common.management.commands.hobo_deploy.call_command') as mocked_call_command:
68
        command.deploy(combo_base_url, ENVIRONMENT, None)
69
    assert mocked_call_command.mock_calls == [call('create_tenant', 'combo.dev.publik.love')]
70
    issuer = Issuer.objects.create(entity_id='My Service', slug='my-service')
71
    for user_data in [
72
        ('john', 'doe', 'john.doe', '123'),
73
        ('will', 'smith', 'will.smith', '456'),
74
        ('emily', 'joe', 'emily.joe', '789'),
75
        ('alan', 'brown', 'alan.brown', '012'),
76
        ('bob', 'miller', 'bob.miller', '345'),
77
        ('helen', 'jones', 'helen.jones', '678'),
78
    ]:
79
        user = User.objects.create(
80
            first_name=user_data[0],
81
            last_name=user_data[1],
82
            username=user_data[2],
83
            email='%s@nowhere.null' % user_data[2],
84
        )
85
        UserSAMLIdentifier.objects.create(user=user, issuer=issuer, name_id=user_data[3])
86
        user.save()
87
    users = User.objects.all()
88
    assert len(users) == 6
89

  
90
    with patch('hobo.agent.common.management.commands.resync_users.requests.post') as mocked_post:
91
        mocked_post.return_value.ok = True
92
        mocked_post.return_value.json.return_value = {'unknown_uuids': ['456', '789']}
93
        with patch('hobo.agent.common.management.commands.resync_users.requests.get') as mocked_get:
94
            mocked_get.return_value.ok = True
95
            mocked_get.return_value.json.return_value = {
96
                'results': [
97
                    {
98
                        'email': 'john.doe@nowhere.null',
99
                        'first_name': 'johnny',
100
                    }
101
                ]
102
            }
103
            resync_users_command = ResyncUsersCommand()
104
            resync_users_command.handle(verbosity=1)
105
    users = User.objects.all()
106
    assert len(users) == 4
107
    assert users.get(email='john.doe@nowhere.null').first_name == 'johnny'
0
-