Projet

Général

Profil

0001-send-provisionning-messages-after-request-treatment-.patch

Benjamin Dauvergne, 20 septembre 2016 11:31

Télécharger (37,3 ko)

Voir les différences:

Subject: [PATCH] send provisionning messages after request treatment in a
 thread (fixes #9396)

All objects to provision are collected into the Provisionning singleton object
in thread local dictionnaries. When request processing is finished the
ProvisionningMiddleware launch a thread which will send provisionning messages.
 debian/debian_config_common.py         |   4 +
 hobo/agent/authentic2/apps.py          | 208 +---------------------
 hobo/agent/authentic2/middleware.py    |  13 ++
 hobo/agent/authentic2/provisionning.py | 303 +++++++++++++++++++++++++++++++++
 hobo/multitenant/apps.py               |   1 +
 hobo/multitenant/models.py             |   4 +-
 tests_authentic/conftest.py            |   7 +-
 tests_authentic/test_provisionning.py  | 222 ++++++++++++++++++++----
 tox.ini                                |   2 +-
 9 files changed, 526 insertions(+), 238 deletions(-)
 create mode 100644 hobo/agent/authentic2/middleware.py
 create mode 100644 hobo/agent/authentic2/provisionning.py
debian/debian_config_common.py
209 209
    MIDDLEWARE_CLASSES = MIDDLEWARE_CLASSES + (
210 210
        'mellon.middleware.PassiveAuthenticationMiddleware',
211 211
    )
212
else:
213
    MIDDLEWARE_CLASSES = MIDDLEWARE_CLASSES + (
214
        'hobo.agent.authentic2.middleware.ProvisionningMiddleware',
215
    )
212 216

  
213 217
TENANT_SETTINGS_LOADERS = (
214 218
    'hobo.multitenant.settings_loaders.TemplateVars',
hobo/agent/authentic2/apps.py
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import json
18
from urlparse import urljoin
19

  
20 17
from django.apps import AppConfig
21
from django.db.models.signals import post_save, post_delete, pre_delete, m2m_changed
22
from django.conf import settings
23
from django.contrib.auth import get_user_model
24
from django.db import connection
25
from django.core.urlresolvers import reverse
26

  
27
from django_rbac.utils import get_role_model
28

  
29
from hobo.agent.common import notify_agents
30
from authentic2.models import AttributeValue
31
from authentic2.saml.models import LibertyProvider
32
from authentic2.a2_rbac.models import OrganizationalUnit
33

  
34

  
35
def get_ou(role_or_through):
36
    if hasattr(role_or_through, 'ou_id'):
37
        return role_or_through.ou
38
    else:
39
        return role_or_through.role.ou
40

  
41

  
42
def get_audience(role_or_through):
43
    ou = get_ou(role_or_through)
44
    if ou:
45
        qs = LibertyProvider.objects.filter(ou=ou)
46
    else:
47
        qs = LibertyProvider.objects.filter(ou__isnull=True)
48
    return [(service, service.entity_id) for service in qs]
49

  
50

  
51
def get_related_roles(role_or_through):
52
    ou = get_ou(role_or_through)
53
    Role = get_role_model()
54
    qs = Role.objects.filter(admin_scope_id__isnull=True) \
55
        .prefetch_related('attributes')
56
    if ou:
57
        qs = qs.filter(ou=ou)
58
    else:
59
        qs = qs.filter(ou__isnull=True)
60
    for role in qs:
61
        role.emails = []
62
        role.emails_to_members = False
63
        role.details = u''
64
        for attribute in role.attributes.all():
65
            if (attribute.name in ('emails', 'emails_to_members', 'details')
66
                and attribute.kind == 'json'):
67
                setattr(role, attribute.name, json.loads(attribute.value))
68
    return qs
69

  
70

  
71
def notify_roles(sender, instance, **kwargs):
72
    if not getattr(settings, 'HOBO_ROLE_EXPORT', True):
73
        return
74
    if instance.slug.startswith('_'):
75
        return
76
    try:
77
        notify_agents({
78
            '@type': 'provision',
79
            'audience': [audience for service, audience in get_audience(instance)],
80
            'full': True,
81
            'objects': {
82
                '@type': 'role',
83
                'data': [
84
                    {
85
                        'uuid': role.uuid,
86
                        'name': role.name,
87
                        'slug': role.slug,
88
                        'description': role.description,
89
                        'details': role.details,
90
                        'emails': role.emails,
91
                        'emails_to_members': role.emails_to_members,
92
                    } for role in get_related_roles(instance)
93
                ],
94
            }
95
        })
96
    except OrganizationalUnit.DoesNotExist:
97
        pass
98

  
99

  
100
def get_entity_id():
101
    tenant = getattr(connection, 'tenant', None)
102
    assert tenant
103
    base_url = tenant.get_base_url()
104
    return urljoin(base_url, reverse('a2-idp-saml-metadata'))
105

  
106

  
107
def provision_user(sender, instance, **kwargs):
108
    User = get_user_model()
109
    if not isinstance(instance, User):
110
        return
111
    if not getattr(settings, 'HOBO_ROLE_EXPORT', True):
112
        return
113
    data = {}
114
    for av in AttributeValue.objects.with_owner(instance):
115
        data[str(av.attribute.name)] = av.to_python()
116

  
117
    roles = instance.roles_and_parents() \
118
            .prefetch_related('attributes')
119
    is_superuser = instance.is_superuser
120
    data.update({
121
        'uuid': instance.uuid,
122
        'username': instance.username,
123
        'first_name': instance.first_name,
124
        'last_name': instance.last_name,
125
        'email': instance.email,
126
        'roles': [
127
            {
128
                'uuid': role.uuid,
129
                'name': role.name,
130
                'slug': role.slug,
131
            } for role in roles],
132
    })
133

  
134
    for service, audience in get_audience(instance):
135
        role_is_superuser = False
136
        for role in roles:
137
            if role.service_id != service.pk:
138
                continue
139
            for attribute in role.attributes.all():
140
                if attribute.name == 'is_superuser' and attribute.value == 'true':
141
                    role_is_superuser = True
142
        data['is_superuser'] = is_superuser or role_is_superuser
143
        notify_agents({
144
            '@type': 'provision',
145
            'issuer': unicode(get_entity_id()),
146
            'audience': [audience],
147
            'full': False,
148
            'objects': {
149
                '@type': 'user',
150
                'data': [data],
151
            }
152
        })
153

  
154

  
155
def deprovision_user(sender, instance, **kwargs):
156
    User = get_user_model()
157
    if not isinstance(instance, User):
158
        return
159
    if not getattr(settings, 'HOBO_ROLE_EXPORT', True):
160
        return
161
    notify_agents({
162
        '@type': 'deprovision',
163
        'issuer': unicode(get_entity_id()),
164
        'audience': [audience for service, audience in get_audience(instance)],
165
        'full': False,
166
        'objects': {
167
            '@type': 'user',
168
            'data': [
169
                {
170
                    'uuid': instance.uuid,
171
                }
172
            ],
173
        }
174
    })
175

  
176

  
177
def provision_user_on_role_change(sender, action, instance, model, pk_set,
178
                                  reverse, **kwargs):
179
    if not action.startswith('post'):
180
        return
181
    if action.endswith('_clear'):
182
        return
183
    if reverse:
184
        provision_user(sender, instance, **kwargs)
185
    else:
186
        for user in model.objects.filter(pk__in=pk_set):
187
            provision_user(sender, user, **kwargs)
188

  
189

  
190
def provision_user_on_attribute_value_save(sender, instance, **kwargs):
191
    User = get_user_model()
192
    if not isinstance(instance.owner, User):
193
        return
194
    provision_user(User, instance.owner)
195

  
196

  
197
def provision_user_on_attribute_value_delete(sender, instance, **kwargs):
198
    User = get_user_model()
199
    if not isinstance(instance.owner, User):
200
        return
201
    provision_user(User, instance.owner)
18
from django.db.models.signals import pre_save, pre_delete, m2m_changed, post_save
202 19

  
203 20

  
204 21
class Authentic2AgentConfig(AppConfig):
......
207 24
    verbose_name = 'Authentic2 Agent'
208 25

  
209 26
    def ready(self):
210
        Role = get_role_model()
211
        post_save.connect(notify_roles, sender=Role)
212
        post_delete.connect(notify_roles, sender=Role)
213
        post_save.connect(notify_roles, Role)
214
        post_delete.connect(notify_roles, Role)
215
        post_save.connect(notify_roles, Role.members.through)
216
        post_delete.connect(notify_roles, Role.members.through)
217
        post_save.connect(provision_user)
218
        pre_delete.connect(deprovision_user)
219
        post_save.connect(provision_user_on_attribute_value_save, sender=AttributeValue)
220
        post_delete.connect(provision_user_on_attribute_value_delete, sender=AttributeValue)
221
        m2m_changed.connect(provision_user_on_role_change,
222
                            sender=Role.members.through)
223
        settings.A2_MANAGER_ROLE_FORM_CLASS = \
224
            'hobo.agent.authentic2.role_forms.RoleForm'
27
        from . import provisionning
28

  
29
        engine = provisionning.Provisionning()
30
        pre_save.connect(engine.pre_save)
31
        post_save.connect(engine.post_save)
32
        pre_delete.connect(engine.pre_delete)
33
        m2m_changed.connect(engine.m2m_changed)
34
        provisionning.provisionning = engine
hobo/agent/authentic2/middleware.py
1
from .provisionning import provisionning
2

  
3

  
4
class ProvisionningMiddleware(object):
5
    def process_request(self, request):
6
        provisionning.start()
7

  
8
    def process_exception(self, request, exception):
9
        provisionning.clean()
10

  
11
    def process_response(self, request, response):
12
        provisionning.provision()
13

  
hobo/agent/authentic2/provisionning.py
1
import json
2
from urlparse import urljoin
3
import threading
4
import copy
5
import logging
6

  
7
from django.contrib.auth import get_user_model
8
from django.db import connection
9
from django.core.urlresolvers import reverse
10
from django.conf import settings
11

  
12
from django_rbac.utils import get_role_model, get_ou_model, get_role_parenting_model
13
from hobo.agent.common import notify_agents
14
from authentic2.saml.models import LibertyProvider
15
from authentic2.a2_rbac.models import RoleAttribute
16
from authentic2.models import AttributeValue
17

  
18

  
19
class Provisionning(object):
20
    local = threading.local()
21
    threads = set()
22

  
23
    def __init__(self):
24
        self.User = get_user_model()
25
        self.Role = get_role_model()
26
        self.OU = get_ou_model()
27
        self.RoleParenting = get_role_parenting_model()
28
        self.logger = logging.getLogger(__name__)
29

  
30
    def start(self):
31
        self.local.saved = {}
32
        self.local.deleted = {}
33

  
34
    def clean(self):
35
        if hasattr(self.local, 'saved'):
36
            del self.local.saved
37
        if hasattr(self.local, 'deleted'):
38
            del self.local.deleted
39

  
40
    def saved(self, *args):
41
        if not hasattr(self.local, 'saved'):
42
            return
43

  
44
        for instance in args:
45
            klass = self.User if isinstance(instance, self.User) else self.Role
46
            self.local.saved.setdefault(klass, set()).add(instance)
47

  
48
    def deleted(self, *args):
49
        if not hasattr(self.local, 'saved'):
50
            return
51

  
52
        for instance in args:
53
            klass = self.User if isinstance(instance, self.User) else self.Role
54
            self.local.deleted.setdefault(klass, set()).add(instance)
55
            self.local.saved.get(klass, set()).discard(instance)
56

  
57
    def resolve_ou(self, instances, ous):
58
        for instance in instances:
59
            if instance.ou_id in ous:
60
                instance.ou = ous[instance.ou_id]
61

  
62
    def notify_users(self, ous, users, mode='provision'):
63
        self.resolve_ou(users, ous)
64

  
65
        ous = {}
66
        for user in users:
67
            ous.setdefault(user.ou, set()).add(user)
68

  
69
        def user_to_json(service, user):
70
            data = {}
71
            roles = user.roles_and_parents().prefetch_related('attributes')
72
            data.update({
73
                'uuid': user.uuid,
74
                'username': user.username,
75
                'first_name': user.first_name,
76
                'last_name': user.last_name,
77
                'email': user.email,
78
                'roles': [
79
                    {
80
                        'uuid': role.uuid,
81
                        'name': role.name,
82
                        'slug': role.slug,
83
                    } for role in roles],
84
            })
85
            for av in AttributeValue.objects.with_owner(user):
86
                data[str(av.attribute.name)] = av.to_python()
87
            roles = user.roles_and_parents().prefetch_related('attributes')
88
            # check if user is superuser through a role
89
            role_is_superuser = False
90
            for role in roles:
91
                if role.service_id != service.pk:
92
                    continue
93
                for attribute in role.attributes.all():
94
                    if attribute.name == 'is_superuser' and attribute.value == 'true':
95
                        role_is_superuser = True
96
            data['is_superuser'] = user.is_superuser or role_is_superuser
97
            return data
98

  
99
        issuer = unicode(self.get_entity_id())
100
        if mode == 'provision':
101
            for ou, users in ous.iteritems():
102
                for service, audience in self.get_audience(user):
103
                    self.logger.info(u'provisionning user %s to %s', user, audience)
104
                    notify_agents({
105
                        '@type': 'provision',
106
                        'issuer': issuer,
107
                        'audience': [audience],
108
                        'full': False,
109
                        'objects': {
110
                            '@type': 'user',
111
                            'data': [user_to_json(service, user)],
112
                        }
113
                    })
114
        else:
115
            for ou, users in ous.iteritems():
116
                audience = [audience for s, audience in self.get_audience(user)]
117
                self.logger.info(u'deprovisionning users %s from %s', users, audience)
118
                notify_agents({
119
                    '@type': 'deprovision',
120
                    'issuer': issuer,
121
                    'audience': audience,
122
                    'full': False,
123
                    'objects': {
124
                        '@type': 'user',
125
                        'data': [{
126
                            'uuid': user.uuid,
127
                        } for user in users]
128
                    }
129
                })
130

  
131
    def notify_roles(self, ous, roles, mode='provision', full=False):
132
        roles = set([role for role in roles if not role.slug.startswith('_')])
133
        if mode == 'provision':
134
            self.complete_roles(roles)
135

  
136
        if not roles:
137
            return
138

  
139
        self.resolve_ou(roles, ous)
140
        ous = {}
141
        for role in roles:
142
            ous.setdefault(role.ou, []).append(role)
143

  
144
        def helper(ou, roles):
145
            if mode == 'provision':
146
                data = [
147
                    {
148
                        'uuid': role.uuid,
149
                        'name': role.name,
150
                        'slug': role.slug,
151
                        'description': role.description,
152
                        'details': role.details,
153
                        'emails': role.emails,
154
                        'emails_to_members': role.emails_to_members,
155
                    } for role in roles
156
                ]
157
            else:
158
                data = [
159
                    {
160
                        'uuid': role.uuid,
161
                    } for role in roles
162
                ]
163

  
164
            audience = [entity_id for service, entity_id in self.get_audience(ou)]
165
            self.logger.info(u'%sning roles %s to %s', mode, role, audience)
166
            notify_agents({
167
                '@type': mode,
168
                'audience': audience,
169
                'full': full,
170
                'objects': {
171
                    '@type': 'role',
172
                    'data': data,
173
                }
174
            })
175

  
176
        global_roles = set(ous.get(None, []))
177
        for ou, ou_roles in ous.iteritems():
178
            sent_roles = set(ou_roles) | global_roles
179
            helper(ou, sent_roles)
180

  
181
    def provision(self):
182
        if not getattr(settings, 'HOBO_ROLE_EXPORT', True):
183
            return
184
        # exit early if not started
185
        if not hasattr(self.local, 'saved') or not hasattr(self.local, 'deleted'):
186
            return
187

  
188
        t = threading.Thread(target=self.do_provision, kwargs={
189
            'saved': getattr(self.local, 'saved', {}),
190
            'deleted': getattr(self.local, 'deleted', {}),
191
        })
192
        t.start()
193
        self.threads.add(t)
194
        self.clean()
195

  
196
    def do_provision(self, saved, deleted, thread=None):
197
        try:
198
            ous = {ou.id: ou for ou in self.OU.objects.all()}
199
            self.notify_roles(ous, saved.get(self.Role, []))
200
            self.notify_roles(ous, deleted.get(self.Role, []), mode='deprovision')
201
            self.notify_users(ous, saved.get(self.User, []))
202
            self.notify_users(ous, deleted.get(self.User, []), mode='deprovision')
203
        except Exception:
204
            # last step, clear everything
205
            self.logger.exception(u'error in provisionning thread')
206
        finally:
207
            self.threads.discard(threading.current_thread())
208

  
209
    def wait(self):
210
        for thread in list(self.threads):
211
            thread.join()
212

  
213
    def __enter__(self):
214
        self.start()
215

  
216
    def __exit__(self, exc_type, exc_value, exc_tb):
217
        if exc_type is None:
218
            self.provision()
219
            self.wait()
220
        else:
221
            self.clean()
222

  
223
    def get_audience(self, ou):
224
        if ou:
225
            qs = LibertyProvider.objects.filter(ou=ou)
226
        else:
227
            qs = LibertyProvider.objects.filter(ou__isnull=True)
228
        return [(service, service.entity_id) for service in qs]
229

  
230
    def complete_roles(self, roles):
231
        for role in roles:
232
            role.emails = []
233
            role.emails_to_members = False
234
            role.details = u''
235
            for attribute in role.attributes.all():
236
                if (attribute.name in ('emails', 'emails_to_members', 'details')
237
                        and attribute.kind == 'json'):
238
                    setattr(role, attribute.name, json.loads(attribute.value))
239

  
240
    def get_entity_id(self):
241
        tenant = getattr(connection, 'tenant', None)
242
        assert tenant
243
        base_url = tenant.get_base_url()
244
        return urljoin(base_url, reverse('a2-idp-saml-metadata'))
245

  
246
    def pre_save(self, sender, instance, raw, using, update_fields, **kwargs):
247
        # we skip new instances
248
        if not instance.pk:
249
            return
250
        if not isinstance(instance, (self.User, self.Role, RoleAttribute, AttributeValue)):
251
            return
252
        # ignore last_login update on login
253
        if isinstance(instance, self.User) and update_fields == ['last_login']:
254
            return
255
        if isinstance(instance, RoleAttribute):
256
            instance = instance.role
257
        elif isinstance(instance, AttributeValue):
258
            if not isinstance(instance.owner, self.User):
259
                return
260
            instance = instance.owner
261
        self.saved(instance)
262

  
263
    def post_save(self, sender, instance, created, raw, using, update_fields, **kwargs):
264
        # during post_save we only handle new instances
265
        if isinstance(instance, self.RoleParenting):
266
            self.saved(*list(instance.child.all_members()))
267
            return
268
        if not created:
269
            return
270
        if not isinstance(instance, (self.User, self.Role, RoleAttribute, AttributeValue)):
271
            return
272
        if isinstance(instance, RoleAttribute):
273
            instance = instance.role
274
        elif isinstance(instance, AttributeValue):
275
            if not isinstance(instance.owner, self.User):
276
                return
277
            instance = instance.owner
278
        self.saved(instance)
279

  
280
    def pre_delete(self, sender, instance, using, **kwargs):
281
        if isinstance(instance, (self.User, self.Role)):
282
            self.deleted(copy.copy(instance))
283
        elif isinstance(instance, RoleAttribute):
284
            instance = instance.role
285
            self.saved(instance)
286
        elif isinstance(instance, AttributeValue):
287
            if not isinstance(instance.owner, self.User):
288
                return
289
            instance = instance.owner
290
            self.saved(instance)
291
        elif isinstance(instance, self.RoleParenting):
292
            self.saved(*list(instance.child.all_members()))
293

  
294
    def m2m_changed(self, sender, instance, action, reverse, model, pk_set, using, **kwargs):
295
        if not action.startswith('post'):
296
            return
297
        if action.endswith('_clear'):
298
            return
299
        if reverse:
300
            self.saved(instance)
301
        else:
302
            for user in model.objects.filter(pk__in=pk_set):
303
                self.saved(user)
hobo/multitenant/apps.py
19 19
            super(TenantAwareThread, self).run()
20 20
        finally:
21 21
            connection.set_tenant(old_tenant)
22
            connection.close()
22 23

  
23 24

  
24 25
class _Timer(TenantAwareThread):
hobo/multitenant/models.py
65 65
                            "the public schema. Current schema is %s."
66 66
                            % connection.schema_name)
67 67

  
68
        os.rename(self.get_directory(), self.get_directory()+'.invalid')
68
        os.rename(self.get_directory(), self.get_directory() + '.invalid')
69 69

  
70
        if schema_exists(self.schema_name) and (self.auto_drop_schema or force_drop) and not django_is_in_test_mode():
70
        if schema_exists(self.schema_name) and (self.auto_drop_schema or force_drop):
71 71
            cursor = connection.cursor()
72 72
            cursor.execute('DROP SCHEMA %s CASCADE' % self.schema_name)
tests_authentic/conftest.py
5 5

  
6 6
import pytest
7 7

  
8

  
8 9
@pytest.fixture
9 10
def tenant_base(request, settings):
10 11
    base = tempfile.mkdtemp('combo-tenant-base')
11 12
    settings.TENANT_BASE = base
13

  
12 14
    def fin():
13 15
        shutil.rmtree(base)
14 16
    request.addfinalizer(fin)
15 17
    return base
16 18

  
19

  
17 20
@pytest.fixture(scope='function')
18
def tenant(db, request, settings, tenant_base):
21
def tenant(transactional_db, request, settings, tenant_base):
19 22
    from hobo.multitenant.models import Tenant
20 23
    base = tenant_base
24

  
21 25
    @pytest.mark.django_db
22 26
    def make_tenant(name):
23 27
        tenant_dir = os.path.join(base, name)
......
54 58
        t.create_schema()
55 59
        return t
56 60
    tenants = [make_tenant('authentic.example.net')]
61

  
57 62
    def fin():
58 63
        from django.db import connection
59 64
        connection.set_schema_to_public()
tests_authentic/test_provisionning.py
1 1
# -*- coding: utf-8 -*-
2
import json
3

  
2 4
import pytest
5
import lasso
3 6

  
4 7
from mock import patch, call, ANY
5 8

  
......
7 10

  
8 11
from tenant_schemas.utils import tenant_context
9 12

  
10
from authentic2.a2_rbac.models import Role
13
from authentic2.saml.models import LibertyProvider
14
from authentic2.a2_rbac.models import Role, RoleAttribute
11 15
from authentic2.a2_rbac.utils import get_default_ou
12 16
from authentic2.models import Attribute, AttributeValue
17
from hobo.agent.authentic2.provisionning import provisionning
13 18

  
14 19
pytestmark = pytest.mark.django_db
15 20

  
16 21

  
17
def test_provision_role(tenant):
18
    with patch('hobo.agent.authentic2.apps.notify_agents') as notify_agents:
22
def test_provision_role(transactional_db, tenant, caplog):
23
    with patch('hobo.agent.authentic2.provisionning.notify_agents') as notify_agents:
19 24
        with tenant_context(tenant):
20
            role = Role.objects.create(name='coin')
25
            LibertyProvider.objects.create(ou=get_default_ou(), name='provider',
26
                                           entity_id='http://provider.com',
27
                                           protocol_conformance=lasso.PROTOCOL_SAML_2_0)
28
            with provisionning:
29
                role = Role.objects.create(name='coin', ou=get_default_ou())
30

  
21 31
            assert notify_agents.call_count == 1
22 32
            arg = notify_agents.call_args
23 33
            assert arg == call(ANY)
......
25 35
            assert isinstance(arg, dict)
26 36
            assert set(arg.keys()) == set([
27 37
                'audience', '@type', 'objects', 'full'])
28
            assert arg['audience'] == []
38
            assert arg['audience'] == ['http://provider.com']
29 39
            assert arg['@type'] == 'provision'
30
            assert arg['full'] is True
40
            assert arg['full'] is False
31 41
            objects = arg['objects']
32 42
            assert isinstance(objects, dict)
33 43
            assert set(objects.keys()) == set(['data', '@type'])
34 44
            assert objects['@type'] == 'role'
35 45
            data = objects['data']
36 46
            assert isinstance(data, list)
37
            assert len(data) == 2
38
            like_role = 0
39
            for o in data:
40
                assert set(o.keys()) == set(['details', 'emails_to_members',
41
                                             'description', 'uuid', 'name',
42
                                             'slug', 'emails'])
43
                assert o['details'] == ''
44
                assert o['emails_to_members'] is False
45
                assert o['emails'] == []
46
                if o['uuid'] == role.uuid and o['name'] == role.name \
47
                   and o['description'] == role.description \
48
                   and o['slug'] == role.slug:
49
                    like_role += 1
50
            assert like_role == 1
47
            assert len(data) == 1
48
            o = data[0]
49
            assert set(o.keys()) == set(['details', 'emails_to_members',
50
                                         'description', 'uuid', 'name',
51
                                         'slug', 'emails'])
52
            assert o['details'] == ''
53
            assert o['emails_to_members'] is False
54
            assert o['emails'] == []
51 55

  
56
            notify_agents.reset_mock()
57
            emails = ['john.doe@example.com', 'toto@entrouvert.com']
58
            with provisionning:
59
                RoleAttribute.objects.create(
60
                    role=role, name='emails', kind='json',
61
                    value=json.dumps(emails))
52 62

  
53
def test_provision_user(tenant):
54
    import lasso
55
    from authentic2.saml.models import LibertyProvider
63
            assert notify_agents.call_count == 1
64
            arg = notify_agents.call_args
65
            assert arg == call(ANY)
66
            arg = arg[0][0]
67
            assert isinstance(arg, dict)
68
            assert set(arg.keys()) == set([
69
                'audience', '@type', 'objects', 'full'])
70
            assert arg['audience'] == ['http://provider.com']
71
            assert arg['@type'] == 'provision'
72
            assert arg['full'] is False
73
            objects = arg['objects']
74
            assert isinstance(objects, dict)
75
            assert set(objects.keys()) == set(['data', '@type'])
76
            assert objects['@type'] == 'role'
77
            data = objects['data']
78
            assert isinstance(data, list)
79
            assert len(data) == 1
80
            o = data[0]
81
            assert set(o.keys()) == set(['details', 'emails_to_members',
82
                                         'description', 'uuid', 'name',
83
                                         'slug', 'emails'])
84
            assert o['details'] == ''
85
            assert o['emails_to_members'] is False
86
            assert o['emails'] == emails
87

  
88
            notify_agents.reset_mock()
89
            with provisionning:
90
                role.delete()
56 91

  
57
    with patch('hobo.agent.authentic2.apps.notify_agents') as notify_agents:
92
            assert notify_agents.call_count == 1
93
            arg = notify_agents.call_args
94
            assert arg == call(ANY)
95
            arg = arg[0][0]
96
            assert isinstance(arg, dict)
97
            assert set(arg.keys()) == set([
98
                'audience', '@type', 'objects', 'full'])
99
            assert arg['audience'] == ['http://provider.com']
100
            assert arg['@type'] == 'deprovision'
101
            assert arg['full'] is False
102
            objects = arg['objects']
103
            assert isinstance(objects, dict)
104
            assert set(objects.keys()) == set(['data', '@type'])
105
            assert objects['@type'] == 'role'
106
            data = objects['data']
107
            assert isinstance(data, list)
108
            assert len(data) == 1
109
            o = data[0]
110
            assert set(o.keys()) == set(['uuid'])
111

  
112

  
113
def test_provision_user(transactional_db, tenant, caplog):
114
    with patch('hobo.agent.authentic2.provisionning.notify_agents') as notify_agents:
58 115
        with tenant_context(tenant):
59 116
            service = LibertyProvider.objects.create(ou=get_default_ou(), name='provider',
60 117
                                                     entity_id='http://provider.com',
61 118
                                                     protocol_conformance=lasso.PROTOCOL_SAML_2_0)
62 119
            role = Role.objects.create(name='coin', service=service, ou=get_default_ou())
63 120
            role.attributes.create(kind='string', name='is_superuser', value='true')
121
            child_role = Role.objects.create(name='child', ou=get_default_ou())
64 122
            notify_agents.reset_mock()
65 123
            User = get_user_model()
66 124
            attribute = Attribute.objects.create(label='Code postal', name='code_postal',
67 125
                                                 kind='string')
68
            user = User.objects.create(username=u'Étienne',
69
                                       email='etienne.dugenou@example.net',
70
                                       first_name=u'Étienne',
71
                                       last_name=u'Dugenou',
72
                                       ou=get_default_ou())
126
            with provisionning:
127
                user = User.objects.create(username=u'Étienne',
128
                                           email='etienne.dugenou@example.net',
129
                                           first_name=u'Étienne',
130
                                           last_name=u'Dugenou',
131
                                           ou=get_default_ou())
73 132
            assert notify_agents.call_count == 1
74 133
            arg = notify_agents.call_args
75 134
            assert arg == call(ANY)
......
103 162
            notify_agents.reset_mock()
104 163
            attribute.set_value(user, '13400')
105 164
            user.is_superuser = True
106
            user.save()
165
            with provisionning:
166
                user.save()
107 167

  
108
            assert notify_agents.call_count == 2
168
            assert notify_agents.call_count == 1
109 169
            arg = notify_agents.call_args
110 170
            assert arg == call(ANY)
111 171
            arg = arg[0][0]
......
137 197
                assert o['is_superuser'] is True
138 198

  
139 199
            notify_agents.reset_mock()
140
            AttributeValue.objects.get().delete()
200
            with provisionning:
201
                AttributeValue.objects.get().delete()
141 202

  
142 203
            assert notify_agents.call_count == 1
143 204
            arg = notify_agents.call_args
......
171 232

  
172 233
            user.is_superuser = False
173 234
            user.save()
235

  
174 236
            notify_agents.reset_mock()
175
            role.members.add(user)
237
            with provisionning:
238
                role.members.add(user)
176 239

  
177 240
            assert notify_agents.call_count == 1
178 241
            arg = notify_agents.call_args
......
209 272
                assert o['is_superuser'] is True
210 273

  
211 274
            notify_agents.reset_mock()
212
            user.roles.remove(role)
275
            with provisionning:
276
                user.roles.remove(role)
213 277

  
214 278
            assert notify_agents.call_count == 1
215 279
            arg = notify_agents.call_args
......
240 304
                assert o['email'] == user.email
241 305
                assert o['roles'] == []
242 306
                assert o['is_superuser'] is False
307

  
308
            notify_agents.reset_mock()
309
            with provisionning:
310
                user.roles.add(child_role)
311
                child_role.add_parent(role)
312

  
313
            assert notify_agents.call_count == 1
314
            arg = notify_agents.call_args
315
            assert arg == call(ANY)
316
            arg = arg[0][0]
317
            assert isinstance(arg, dict)
318
            assert set(arg.keys()) == set([
319
                'issuer', 'audience', '@type', 'objects', 'full'])
320
            assert arg['issuer'] == \
321
                'http://%s/idp/saml2/metadata' % tenant.domain_url
322
            assert arg['audience'] == ['http://provider.com']
323
            assert arg['@type'] == 'provision'
324
            assert arg['full'] is False
325
            objects = arg['objects']
326
            assert isinstance(objects, dict)
327
            assert set(objects.keys()) == set(['data', '@type'])
328
            assert objects['@type'] == 'user'
329
            data = objects['data']
330
            assert isinstance(data, list)
331
            assert len(data) == 1
332
            for o in data:
333
                assert set(o.keys()) == set(['uuid', 'username', 'first_name',
334
                                             'is_superuser', 'last_name', 'email', 'roles'])
335
                assert o['uuid'] == user.uuid
336
                assert o['username'] == user.username
337
                assert o['first_name'] == user.first_name
338
                assert o['last_name'] == user.last_name
339
                assert o['email'] == user.email
340
                assert len(o['roles']) == 2
341
                for r in o['roles']:
342
                    r1 = {
343
                        'uuid': role.uuid,
344
                        'name': role.name,
345
                        'slug': role.slug
346
                    }
347
                    r2 = {
348
                        'uuid': child_role.uuid,
349
                        'name': child_role.name,
350
                        'slug': child_role.slug
351
                    }
352
                    assert r == r1 or r == r2
353
                assert len(set(r['uuid'] for r in o['roles'])) == 2
354
                assert o['is_superuser'] is True
355

  
356
            notify_agents.reset_mock()
357
            with provisionning:
358
                child_role.remove_parent(role)
359

  
360
            assert notify_agents.call_count == 1
361
            arg = notify_agents.call_args
362
            assert arg == call(ANY)
363
            arg = arg[0][0]
364
            assert isinstance(arg, dict)
365
            assert set(arg.keys()) == set([
366
                'issuer', 'audience', '@type', 'objects', 'full'])
367
            assert arg['issuer'] == \
368
                'http://%s/idp/saml2/metadata' % tenant.domain_url
369
            assert arg['audience'] == ['http://provider.com']
370
            assert arg['@type'] == 'provision'
371
            assert arg['full'] is False
372
            objects = arg['objects']
373
            assert isinstance(objects, dict)
374
            assert set(objects.keys()) == set(['data', '@type'])
375
            assert objects['@type'] == 'user'
376
            data = objects['data']
377
            assert isinstance(data, list)
378
            assert len(data) == 1
379
            for o in data:
380
                assert set(o.keys()) == set(['uuid', 'username', 'first_name',
381
                                             'is_superuser', 'last_name', 'email', 'roles'])
382
                assert o['uuid'] == user.uuid
383
                assert o['username'] == user.username
384
                assert o['first_name'] == user.first_name
385
                assert o['last_name'] == user.last_name
386
                assert o['email'] == user.email
387
                assert o['roles'] == [{
388
                    'uuid': child_role.uuid,
389
                    'name': child_role.name,
390
                    'slug': child_role.slug
391
                }]
392
                assert o['is_superuser'] is False
393

  
243 394
            notify_agents.reset_mock()
244
            user.delete()
395
            with provisionning:
396
                user.delete()
245 397
            assert notify_agents.call_count == 1
246 398
            arg = notify_agents.call_args
247 399
            assert arg == call(ANY)
tox.ini
31 31
	pytest-cov
32 32
	pytest-django
33 33
	pytest-mock
34
	pytest-capturelog
34 35
	coverage
35 36
	raven
36 37
	cssselect
37 38
	WebTest
38 39
	django-mellon
39
	multitenant: pytest-capturelog
40 40
	multitenant,passerelle: celery
41 41
	authentic: http://git.entrouvert.org/authentic.git/snapshot/authentic-master.tar.gz
42 42
	passerelle: http://git.entrouvert.org/passerelle.git/snapshot/passerelle-master.tar.gz
43
-