Projet

Général

Profil

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

Benjamin Dauvergne, 05 octobre 2016 14:03

Télécharger (48,2 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    |  14 ++
 hobo/agent/authentic2/provisionning.py | 352 +++++++++++++++++++++++++++++++++
 hobo/multitenant/apps.py               |   1 +
 hobo/multitenant/models.py             |   4 +-
 tests_authentic/conftest.py            |   7 +-
 tests_authentic/test_hobo_deploy.py    |   2 +-
 tests_authentic/test_provisionning.py  | 311 ++++++++++++++++++++++-------
 tox.ini                                |   6 +-
 10 files changed, 633 insertions(+), 276 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
        return response
14

  
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
        if mode == 'provision':
64
            users = (self.User.objects.filter(id__in=[u.id for u in users])
65
                     .select_related('ou').prefetch_related('attribute_values__attribute'))
66
        else:
67
            self.resolve_ou(users, ous)
68

  
69
        ous = {}
70
        for user in users:
71
            ous.setdefault(user.ou, set()).add(user)
72

  
73
        issuer = unicode(self.get_entity_id())
74
        if mode == 'provision':
75

  
76
            def user_to_json(service, user, user_roles):
77
                data = {}
78
                roles = user.roles_and_parents().prefetch_related('attributes')
79
                data.update({
80
                    'uuid': user.uuid,
81
                    'username': user.username,
82
                    'first_name': user.first_name,
83
                    'last_name': user.last_name,
84
                    'email': user.email,
85
                    'roles': [
86
                        {
87
                            'uuid': role.uuid,
88
                            'name': role.name,
89
                            'slug': role.slug,
90
                        } for role in roles],
91
                })
92
                for av in user.attribute_values.all():
93
                    data[str(av.attribute.name)] = av.to_python()
94
                # check if user is superuser through a role
95
                role_is_superuser = False
96
                if service:
97
                    for role in user_roles.get(user.id, []):
98
                        if role.service_id != service.pk:
99
                            continue
100
                        for attribute in role.attributes.all():
101
                            if attribute.name == 'is_superuser' and attribute.value == 'true':
102
                                role_is_superuser = True
103
                data['is_superuser'] = user.is_superuser or role_is_superuser
104
                return data
105
            # Find roles giving a superuser attribute
106
            # If there is any role of this kind, we do one provisionning message for each user and
107
            # each service.
108
            roles_with_attributes = (self.Role.objects.filter(members=users)
109
                                     .parents(include_self=True)
110
                                     .filter(attributes__name='is_superuser')
111
                                     .exists())
112

  
113
            all_roles = (self.Role.objects.filter(members__in=users).parents()
114
                         .prefetch_related('attributes').distinct())
115
            roles = dict((r.id, r) for r in all_roles)
116
            user_roles = {}
117
            parents = {}
118
            for rp in self.RoleParenting.objects.filter(child=all_roles):
119
                parents.setdefault(rp.child.id, []).append(rp.parent.id)
120
            Through = self.Role.members.through
121
            for u_id, r_id in Through.objects.filter(role__members=users).values_list('user_id',
122
                                                                                      'role_id'):
123
                user_roles.setdefault(u_id, set()).add(roles[r_id])
124
                for p_id in parents.get(r_id, []):
125
                    user_roles[u_id].add(roles[p_id])
126

  
127
            # add role's ous
128
            for user in users:
129
                for r in user_roles.get(user.id, []):
130
                    ous.setdefault(r.ou, set()).add(user)
131

  
132
            if roles_with_attributes:
133
                for ou, users in ous.iteritems():
134
                    for service, audience in self.get_audience(ou):
135
                        for user in users:
136
                            self.logger.info(u'provisionning user %s to %s', user, audience)
137
                            notify_agents({
138
                                '@type': 'provision',
139
                                'issuer': issuer,
140
                                'audience': [audience],
141
                                'full': False,
142
                                'objects': {
143
                                    '@type': 'user',
144
                                    'data': [user_to_json(service, user, user_roles)],
145
                                }
146
                            })
147
            else:
148
                for ou, users in ous.iteritems():
149
                    audience = [a for service, a in self.get_audience(ou)]
150
                    self.logger.info(u'provisionning users %s to %s',
151
                                     u', '.join(map(unicode, users)), u', '.join(audience))
152
                    notify_agents({
153
                        '@type': 'provision',
154
                        'issuer': issuer,
155
                        'audience': audience,
156
                        'full': False,
157
                        'objects': {
158
                            '@type': 'user',
159
                            'data': [user_to_json(None, user, user_roles) for user in users],
160
                        }
161
                    })
162
        elif users:
163
            audience = [audience for ou in self.OU.objects.all()
164
                        for s, audience in self.get_audience(ou)]
165
            self.logger.info(u'deprovisionning users %s from %s', u', '.join(map(unicode, users)),
166
                             u', '.join(audience))
167
            notify_agents({
168
                '@type': 'deprovision',
169
                'issuer': issuer,
170
                'audience': audience,
171
                'full': False,
172
                'objects': {
173
                    '@type': 'user',
174
                    'data': [{
175
                        'uuid': user.uuid,
176
                    } for user in users]
177
                }
178
            })
179

  
180
    def notify_roles(self, ous, roles, mode='provision', full=False):
181
        roles = set([role for role in roles if not role.slug.startswith('_')])
182
        if mode == 'provision':
183
            self.complete_roles(roles)
184

  
185
        if not roles:
186
            return
187

  
188
        self.resolve_ou(roles, ous)
189
        ous = {}
190
        for role in roles:
191
            ous.setdefault(role.ou, []).append(role)
192

  
193
        def helper(ou, roles):
194
            if mode == 'provision':
195
                data = [
196
                    {
197
                        'uuid': role.uuid,
198
                        'name': role.name,
199
                        'slug': role.slug,
200
                        'description': role.description,
201
                        'details': role.details,
202
                        'emails': role.emails,
203
                        'emails_to_members': role.emails_to_members,
204
                    } for role in roles
205
                ]
206
            else:
207
                data = [
208
                    {
209
                        'uuid': role.uuid,
210
                    } for role in roles
211
                ]
212

  
213
            audience = [entity_id for service, entity_id in self.get_audience(ou)]
214
            self.logger.info(u'%sning roles %s to %s', mode, role, audience)
215
            notify_agents({
216
                '@type': mode,
217
                'audience': audience,
218
                'full': full,
219
                'objects': {
220
                    '@type': 'role',
221
                    'data': data,
222
                }
223
            })
224

  
225
        global_roles = set(ous.get(None, []))
226
        for ou, ou_roles in ous.iteritems():
227
            sent_roles = set(ou_roles) | global_roles
228
            helper(ou, sent_roles)
229

  
230
    def provision(self):
231
        if not getattr(settings, 'HOBO_ROLE_EXPORT', True):
232
            return
233
        # exit early if not started
234
        if not hasattr(self.local, 'saved') or not hasattr(self.local, 'deleted'):
235
            return
236

  
237
        t = threading.Thread(target=self.do_provision, kwargs={
238
            'saved': getattr(self.local, 'saved', {}),
239
            'deleted': getattr(self.local, 'deleted', {}),
240
        })
241
        t.start()
242
        self.threads.add(t)
243
        self.clean()
244

  
245
    def do_provision(self, saved, deleted, thread=None):
246
        try:
247
            ous = {ou.id: ou for ou in self.OU.objects.all()}
248
            self.notify_roles(ous, saved.get(self.Role, []))
249
            self.notify_roles(ous, deleted.get(self.Role, []), mode='deprovision')
250
            self.notify_users(ous, saved.get(self.User, []))
251
            self.notify_users(ous, deleted.get(self.User, []), mode='deprovision')
252
        except Exception:
253
            # last step, clear everything
254
            self.logger.exception(u'error in provisionning thread')
255
        finally:
256
            self.threads.discard(threading.current_thread())
257

  
258
    def wait(self):
259
        for thread in list(self.threads):
260
            thread.join()
261

  
262
    def __enter__(self):
263
        self.start()
264

  
265
    def __exit__(self, exc_type, exc_value, exc_tb):
266
        if exc_type is None:
267
            self.provision()
268
            self.wait()
269
        else:
270
            self.clean()
271

  
272
    def get_audience(self, ou):
273
        if ou:
274
            qs = LibertyProvider.objects.filter(ou=ou)
275
        else:
276
            qs = LibertyProvider.objects.filter(ou__isnull=True)
277
        return [(service, service.entity_id) for service in qs]
278

  
279
    def complete_roles(self, roles):
280
        for role in roles:
281
            role.emails = []
282
            role.emails_to_members = False
283
            role.details = u''
284
            for attribute in role.attributes.all():
285
                if (attribute.name in ('emails', 'emails_to_members', 'details')
286
                        and attribute.kind == 'json'):
287
                    setattr(role, attribute.name, json.loads(attribute.value))
288

  
289
    def get_entity_id(self):
290
        tenant = getattr(connection, 'tenant', None)
291
        assert tenant
292
        base_url = tenant.get_base_url()
293
        return urljoin(base_url, reverse('a2-idp-saml-metadata'))
294

  
295
    def pre_save(self, sender, instance, raw, using, update_fields, **kwargs):
296
        # we skip new instances
297
        if not instance.pk:
298
            return
299
        if not isinstance(instance, (self.User, self.Role, RoleAttribute, AttributeValue)):
300
            return
301
        # ignore last_login update on login
302
        if isinstance(instance, self.User) and update_fields == ['last_login']:
303
            return
304
        if isinstance(instance, RoleAttribute):
305
            instance = instance.role
306
        elif isinstance(instance, AttributeValue):
307
            if not isinstance(instance.owner, self.User):
308
                return
309
            instance = instance.owner
310
        self.saved(instance)
311

  
312
    def post_save(self, sender, instance, created, raw, using, update_fields, **kwargs):
313
        # during post_save we only handle new instances
314
        if isinstance(instance, self.RoleParenting):
315
            self.saved(*list(instance.child.all_members()))
316
            return
317
        if not created:
318
            return
319
        if not isinstance(instance, (self.User, self.Role, RoleAttribute, AttributeValue)):
320
            return
321
        if isinstance(instance, RoleAttribute):
322
            instance = instance.role
323
        elif isinstance(instance, AttributeValue):
324
            if not isinstance(instance.owner, self.User):
325
                return
326
            instance = instance.owner
327
        self.saved(instance)
328

  
329
    def pre_delete(self, sender, instance, using, **kwargs):
330
        if isinstance(instance, (self.User, self.Role)):
331
            self.deleted(copy.copy(instance))
332
        elif isinstance(instance, RoleAttribute):
333
            instance = instance.role
334
            self.saved(instance)
335
        elif isinstance(instance, AttributeValue):
336
            if not isinstance(instance.owner, self.User):
337
                return
338
            instance = instance.owner
339
            self.saved(instance)
340
        elif isinstance(instance, self.RoleParenting):
341
            self.saved(*list(instance.child.all_members()))
342

  
343
    def m2m_changed(self, sender, instance, action, reverse, model, pk_set, using, **kwargs):
344
        if not action.startswith('post'):
345
            return
346
        if action.endswith('_clear'):
347
            return
348
        if reverse:
349
            self.saved(instance)
350
        else:
351
            for user in model.objects.filter(pk__in=pk_set):
352
                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_hobo_deploy.py
47 47

  
48 48
    # As a user is created, notify_agents is called, as celery is not running
49 49
    # we just block it
50
    mocker.patch('hobo.agent.authentic2.apps.notify_agents')
50
    mocker.patch('hobo.agent.authentic2.provisionning.notify_agents')
51 51
    requests_get = mocker.patch('requests.get')
52 52
    meta1 = '''<?xml version="1.0"?>
53 53
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
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'] == []
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))
62

  
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()
51 91

  
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'])
52 111

  
53
def test_provision_user(tenant):
54
    import lasso
55
    from authentic2.saml.models import LibertyProvider
56 112

  
57
    with patch('hobo.agent.authentic2.apps.notify_agents') as notify_agents:
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
            role2 = Role.objects.create(name='zob', service=service, ou=get_default_ou())
122
            role2.attributes.create(kind='json', name='emails', value='["zob@example.net"]')
123
            child_role = Role.objects.create(name='child', ou=get_default_ou())
64 124
            notify_agents.reset_mock()
65 125
            User = get_user_model()
66 126
            attribute = Attribute.objects.create(label='Code postal', name='code_postal',
67 127
                                                 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())
128
            with provisionning:
129
                user1 = User.objects.create(username=u'Étienne',
130
                                            email='etienne.dugenou@example.net',
131
                                            first_name=u'Étienne',
132
                                            last_name=u'Dugenou',
133
                                            ou=get_default_ou())
134
                user2 = User.objects.create(username=u'john.doe',
135
                                            email='iohn.doe@example.net',
136
                                            first_name=u'John',
137
                                            last_name=u'Doe',
138
                                            ou=get_default_ou())
139
                role2.members.add(user2)
140
            users = {user.uuid: user for user in [user1, user2]}
73 141
            assert notify_agents.call_count == 1
74 142
            arg = notify_agents.call_args
75 143
            assert arg == call(ANY)
......
88 156
            assert objects['@type'] == 'user'
89 157
            data = objects['data']
90 158
            assert isinstance(data, list)
91
            assert len(data) == 1
159
            assert len(data) == 2
160
            assert len(set([o['uuid'] for o in data])) == 2
92 161
            for o in data:
93 162
                assert set(o.keys()) == set(['uuid', 'username', 'first_name', 'is_superuser',
94 163
                                             'last_name', 'email', 'roles'])
95
                assert o['uuid'] == user.uuid
164
                assert o['uuid'] in users
165
                user = users[o['uuid']]
96 166
                assert o['username'] == user.username
97 167
                assert o['first_name'] == user.first_name
98 168
                assert o['last_name'] == user.last_name
99 169
                assert o['email'] == user.email
100
                assert o['roles'] == []
170
                assert o['roles'] == [{'name': r.name, 'slug': r.slug, 'uuid': r.uuid} for r in
171
                                      user.roles.all()]
101 172
                assert o['is_superuser'] is False
102 173

  
103 174
            notify_agents.reset_mock()
104
            attribute.set_value(user, '13400')
105
            user.is_superuser = True
106
            user.save()
175
            attribute.set_value(user1, '13400')
176
            user1.is_superuser = True
177
            with provisionning:
178
                user1.save()
179
                user2.save()
107 180

  
108
            assert notify_agents.call_count == 2
181
            assert notify_agents.call_count == 1
109 182
            arg = notify_agents.call_args
110 183
            assert arg == call(ANY)
111 184
            arg = arg[0][0]
......
123 196
            assert objects['@type'] == 'user'
124 197
            data = objects['data']
125 198
            assert isinstance(data, list)
126
            assert len(data) == 1
127
            for o in data:
128
                assert set(o.keys()) == set(['code_postal', 'uuid', 'username', 'first_name',
199
            assert len(data) == 2
200
            for o, user in zip(data, [user1, user2]):
201
                assert set(o.keys()) <= set(['code_postal', 'uuid', 'username', 'first_name',
129 202
                                             'is_superuser', 'last_name', 'email', 'roles'])
130 203
                assert o['uuid'] == user.uuid
131 204
                assert o['username'] == user.username
132 205
                assert o['first_name'] == user.first_name
133 206
                assert o['last_name'] == user.last_name
134 207
                assert o['email'] == user.email
208
                assert o['roles'] == [{'name': r.name, 'slug': r.slug, 'uuid': r.uuid} for r in
209
                                      user.roles.all()]
210
                assert 'code_postal' not in o or o['code_postal'] == '13400'
211
                assert o['is_superuser'] is user.is_superuser
212

  
213
            notify_agents.reset_mock()
214
            with provisionning:
215
                AttributeValue.objects.get(attribute=attribute).delete()
216

  
217
            assert notify_agents.call_count == 1
218
            arg = notify_agents.call_args
219
            assert arg == call(ANY)
220
            arg = arg[0][0]
221
            assert isinstance(arg, dict)
222
            assert set(arg.keys()) == set([
223
                'issuer', 'audience', '@type', 'objects', 'full'])
224
            assert arg['issuer'] == \
225
                'http://%s/idp/saml2/metadata' % tenant.domain_url
226
            assert arg['audience'] == ['http://provider.com']
227
            assert arg['@type'] == 'provision'
228
            assert arg['full'] is False
229
            objects = arg['objects']
230
            assert isinstance(objects, dict)
231
            assert set(objects.keys()) == set(['data', '@type'])
232
            assert objects['@type'] == 'user'
233
            data = objects['data']
234
            assert isinstance(data, list)
235
            assert len(data) == 1
236
            for o in data:
237
                assert set(o.keys()) == set(['uuid', 'username', 'first_name',
238
                                             'is_superuser', 'last_name', 'email', 'roles'])
239
                assert o['uuid'] == user1.uuid
240
                assert o['username'] == user1.username
241
                assert o['first_name'] == user1.first_name
242
                assert o['last_name'] == user1.last_name
243
                assert o['email'] == user1.email
135 244
                assert o['roles'] == []
136
                assert o['code_postal'] == '13400'
137 245
                assert o['is_superuser'] is True
138 246

  
247
            user1.is_superuser = False
248
            user1.save()
249

  
139 250
            notify_agents.reset_mock()
140
            AttributeValue.objects.get(attribute=attribute).delete()
251
            with provisionning:
252
                role.members.add(user1)
253
                user2.save()
254

  
255
            assert notify_agents.call_count == 2
256
            for arg in notify_agents.call_args_list:
257
                assert arg == call(ANY)
258
                arg = arg[0][0]
259
                assert isinstance(arg, dict)
260
                assert set(arg.keys()) == set([
261
                    'issuer', 'audience', '@type', 'objects', 'full'])
262
                assert arg['issuer'] == \
263
                    'http://%s/idp/saml2/metadata' % tenant.domain_url
264
                assert arg['audience'] == ['http://provider.com']
265
                assert arg['@type'] == 'provision'
266
                assert arg['full'] is False
267
                objects = arg['objects']
268
                assert isinstance(objects, dict)
269
                assert set(objects.keys()) == set(['data', '@type'])
270
                assert objects['@type'] == 'user'
271
                data = objects['data']
272
                assert isinstance(data, list)
273
                assert len(data) == 1
274
                print data
275
                for o in data:
276
                    assert set(o.keys()) == set(['uuid', 'username', 'first_name',
277
                                                 'is_superuser', 'last_name', 'email', 'roles'])
278
                    assert o['uuid'] in users
279
                    user = users[o['uuid']]
280
                    assert o['uuid'] == user.uuid
281
                    assert o['username'] == user.username
282
                    assert o['first_name'] == user.first_name
283
                    assert o['last_name'] == user.last_name
284
                    assert o['email'] == user.email
285
                    assert o['roles'] == [{'name': r.name, 'slug': r.slug, 'uuid': r.uuid} for r in
286
                                          user.roles.all()]
287
                    assert o['is_superuser'] is (user == user1)
288
            assert len(set(arg[0][0]['objects']['data'][0]['uuid'] for arg in
289
                           notify_agents.call_args_list)) == 2
290

  
291
            notify_agents.reset_mock()
292
            with provisionning:
293
                user1.roles.remove(role)
141 294

  
142 295
            assert notify_agents.call_count == 1
143 296
            arg = notify_agents.call_args
......
161 314
            for o in data:
162 315
                assert set(o.keys()) == set(['uuid', 'username', 'first_name',
163 316
                                             'is_superuser', 'last_name', 'email', 'roles'])
164
                assert o['uuid'] == user.uuid
165
                assert o['username'] == user.username
166
                assert o['first_name'] == user.first_name
167
                assert o['last_name'] == user.last_name
168
                assert o['email'] == user.email
317
                assert o['uuid'] == user1.uuid
318
                assert o['username'] == user1.username
319
                assert o['first_name'] == user1.first_name
320
                assert o['last_name'] == user1.last_name
321
                assert o['email'] == user1.email
169 322
                assert o['roles'] == []
170
                assert o['is_superuser'] is True
323
                assert o['is_superuser'] is False
171 324

  
172
            user.is_superuser = False
173
            user.save()
174 325
            notify_agents.reset_mock()
175
            role.members.add(user)
326
            with provisionning:
327
                user1.roles.add(child_role)
328
                child_role.add_parent(role)
176 329

  
177 330
            assert notify_agents.call_count == 1
178 331
            arg = notify_agents.call_args
......
196 349
            for o in data:
197 350
                assert set(o.keys()) == set(['uuid', 'username', 'first_name',
198 351
                                             'is_superuser', 'last_name', 'email', 'roles'])
199
                assert o['uuid'] == user.uuid
200
                assert o['username'] == user.username
201
                assert o['first_name'] == user.first_name
202
                assert o['last_name'] == user.last_name
203
                assert o['email'] == user.email
204
                assert o['roles'] == [{
205
                    'uuid': role.uuid,
206
                    'name': role.name,
207
                    'slug': role.slug
208
                }]
352
                assert o['uuid'] == user1.uuid
353
                assert o['username'] == user1.username
354
                assert o['first_name'] == user1.first_name
355
                assert o['last_name'] == user1.last_name
356
                assert o['email'] == user1.email
357
                assert len(o['roles']) == 2
358
                for r in o['roles']:
359
                    r1 = {
360
                        'uuid': role.uuid,
361
                        'name': role.name,
362
                        'slug': role.slug
363
                    }
364
                    r2 = {
365
                        'uuid': child_role.uuid,
366
                        'name': child_role.name,
367
                        'slug': child_role.slug
368
                    }
369
                    assert r == r1 or r == r2
370
                assert len(set(r['uuid'] for r in o['roles'])) == 2
209 371
                assert o['is_superuser'] is True
210 372

  
211 373
            notify_agents.reset_mock()
212
            user.roles.remove(role)
374
            with provisionning:
375
                child_role.remove_parent(role)
213 376

  
214 377
            assert notify_agents.call_count == 1
215 378
            arg = notify_agents.call_args
......
233 396
            for o in data:
234 397
                assert set(o.keys()) == set(['uuid', 'username', 'first_name',
235 398
                                             'is_superuser', 'last_name', 'email', 'roles'])
236
                assert o['uuid'] == user.uuid
237
                assert o['username'] == user.username
238
                assert o['first_name'] == user.first_name
239
                assert o['last_name'] == user.last_name
240
                assert o['email'] == user.email
241
                assert o['roles'] == []
399
                assert o['uuid'] == user1.uuid
400
                assert o['username'] == user1.username
401
                assert o['first_name'] == user1.first_name
402
                assert o['last_name'] == user1.last_name
403
                assert o['email'] == user1.email
404
                assert o['roles'] == [{
405
                    'uuid': child_role.uuid,
406
                    'name': child_role.name,
407
                    'slug': child_role.slug
408
                }]
242 409
                assert o['is_superuser'] is False
410

  
243 411
            notify_agents.reset_mock()
244
            user.delete()
412
            with provisionning:
413
                user1.delete()
414
                user2.delete()
245 415
            assert notify_agents.call_count == 1
246 416
            arg = notify_agents.call_args
247 417
            assert arg == call(ANY)
......
260 430
            assert objects['@type'] == 'user'
261 431
            data = objects['data']
262 432
            assert isinstance(data, list)
263
            assert len(data) == 1
433
            assert len(data) == 2
434
            assert len(set([o['uuid'] for o in data])) == 2
264 435
            for o in data:
265 436
                assert set(o.keys()) == set(['uuid'])
266
                assert o['uuid'] == user.uuid
437
                assert o['uuid'] in users
tox.ini
22 22
	passerelle: PASSERELLE_SETTINGS_FILE=tests_passerelle/settings.py
23 23
	passerelle: DJANGO_SETTINGS_MODULE=passerelle.settings
24 24
	coverage: COVERAGE=--junitxml=junit-{envname}.xml --cov-report xml --cov=hobo/ --cov-config .coveragerc
25
	nomigrations: NOMIGRATIONS=--nomigrations
25
	fast: NOMIGRATIONS=--nomigrations
26 26
deps:
27 27
	dj17: django>1.7,<1.8
28 28
	dj18: django>1.8,<1.9
......
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
......
47 47
	./getlasso.sh
48 48
	hobo: py.test {env:COVERAGE:} {env:NOMIGRATIONS:} {posargs:tests/}
49 49
	multitenant: py.test {env:COVERAGE:} {env:NOMIGRATIONS:} {posargs:tests_multitenant/}
50
	authentic: py.test {env:COVERAGE:} {env:NOMIGRATIONS:} {posargs:tests_authentic/}
50
	authentic: py.test {env:FAST:} {env:COVERAGE:} {env:NOMIGRATIONS:} {posargs:tests_authentic/}
51 51
	passerelle: py.test {env:COVERAGE:} {env:NOMIGRATIONS:} {posargs:tests_passerelle/}
52 52
	coverage: mv coverage.xml coverage-{envname}.xml
53
-