From 3d26438ae7aa05036fe6b1087088f75f15b6138b Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Wed, 30 Sep 2015 18:03:09 +0200 Subject: [PATCH 3/4] agent/common: add user provisionning and tests for common agent (#8440) --- .../common/management/commands/hobo_notify.py | 66 +++++++- tests_multitenant/settings.py | 4 +- tests_multitenant/test_hobo_notify.py | 169 +++++++++++++++++++++ 3 files changed, 235 insertions(+), 4 deletions(-) diff --git a/hobo/agent/common/management/commands/hobo_notify.py b/hobo/agent/common/management/commands/hobo_notify.py index 9dea18b..89b77e1 100644 --- a/hobo/agent/common/management/commands/hobo_notify.py +++ b/hobo/agent/common/management/commands/hobo_notify.py @@ -16,14 +16,19 @@ import json import sys +import random from django.core.management.base import BaseCommand +from django.db.transaction import atomic +from django.db import IntegrityError from tenant_schemas.utils import tenant_context from hobo.multitenant.middleware import TenantMiddleware from hobo.agent.common.models import Role +class TryAgain(Exception): + pass class Command(BaseCommand): @classmethod @@ -57,7 +62,58 @@ class Command(BaseCommand): and 'description' in o @classmethod - def provision_role(cls, action, data, full=False): + def check_valid_user(cls, o): + return 'uuid' in o \ + and 'email' in o \ + and 'first_name' in o \ + and 'last_name' in o \ + and 'roles' in o + + @classmethod + def provision_user(cls, issuer, action, data, full=False): + from django.contrib.auth import get_user_model + from mellon.models import UserSAMLIdentifier + User = get_user_model() + + + assert not full # provisionning all users is dangerous, we prefer deprovision + uuids = set() + for o in data: + uuids.add(o['uuid']) + try: + with atomic(): + assert cls.check_valid_user(o) + if action == 'provision': + try: + mellon_user = UserSAMLIdentifier.objects.get( + issuer=issuer, name_id=o['uuid']) + user = mellon_user.user + except UserSAMLIdentifier.DoesNotExist: + # temp user object + random_uid = str(random.randint(1,10000000000000)) + user = User.objects.create( + username=random_uid) + mellon_user = UserSAMLIdentifier.objects.create( + user=user, issuer=issuer, name_id=o['uuid']) + user.first_name = o['first_name'] + user.last_name = o['last_name'] + user.email = o['email'] + user.username = o['uuid'][:30] + user.save() + role_uuids = [role['uuid'] for role in o.get('roles', [])] + user.groups = Role.objects.filter(uuid__in=role_uuids) + except IntegrityError: + raise TryAgain + if full and action == 'provision': + for usi in UserSAMLIdentifier.objects.exclude(name_id__in=uuids): + usi.user.delete() + elif action == 'deprovision': + for user in User.objects.filter(saml_identifiers__name_id__in=uuids): + user.delete() + + + @classmethod + def provision_role(cls, issuer, action, data, full=False): uuids = set() for o in data: assert cls.check_valid_role(o) @@ -93,9 +149,15 @@ class Command(BaseCommand): audience = notification['audience'] full = notification['full'] if 'full' in notification else False entity_id = service.get('saml-sp-metadata-url') + issuer = notification.get('issuer') assert entity_id, 'service has no saml-sp-metadat-url field' if entity_id not in audience: return uuids = set() object_type = notification['objects']['@type'] - getattr(cls, 'provision_' + object_type)(action, notification['objects']['data'], full=full) + while True: + try: + getattr(cls, 'provision_' + object_type)(issuer, action, notification['objects']['data'], full=full) + except TryAgain: + continue + break diff --git a/tests_multitenant/settings.py b/tests_multitenant/settings.py index a1b9106..23235b2 100644 --- a/tests_multitenant/settings.py +++ b/tests_multitenant/settings.py @@ -4,14 +4,14 @@ from mock import mock_open, patch LANGUAGE_CODE = 'en-us' -INSTALLED_APPS = ('django.contrib.auth', 'django.contrib.sessions', 'django.contrib.contenttypes') +INSTALLED_APPS = ('django.contrib.auth', 'django.contrib.sessions', 'django.contrib.contenttypes', 'mellon') PROJECT_NAME = 'fake-agent' with patch.object(builtin, 'file', mock_open(read_data='xxx')): execfile(os.path.join(os.path.dirname(__file__), '../debian/debian_config_common.py')) -TENANT_APPS = ('django.contrib.auth','django.contrib.sessions', 'django.contrib.contenttypes', 'hobo.agent.common') +TENANT_APPS = ('django.contrib.auth','django.contrib.sessions', 'django.contrib.contenttypes', 'hobo.agent.common', 'mellon') ROOT_URLCONF = 'hobo.agent.test_urls' CACHES = { diff --git a/tests_multitenant/test_hobo_notify.py b/tests_multitenant/test_hobo_notify.py index 56addac..ec22e2c 100644 --- a/tests_multitenant/test_hobo_notify.py +++ b/tests_multitenant/test_hobo_notify.py @@ -107,3 +107,172 @@ def test_hobo_notify_roles(tenants): Command.process_notification(tenant, notification) assert Group.objects.count() == 0 assert Role.objects.count() == 0 + +def test_provision_users(tenants): + from hobo.agent.common.management.commands.hobo_notify import Command + from tenant_schemas.utils import tenant_context + from django.contrib.auth import get_user_model + from django.contrib.auth.models import Group + from hobo.agent.common.models import Role + + User = get_user_model() + + # provision a role + for tenant in tenants: + with tenant_context(tenant): + notification = { + u'@type': u'provision', + u'audience': [u'%s/saml/metadata' % tenant.get_base_url()], + u'objects': { + u'@type': 'role', + u'data': [ + { + u'uuid': u'12345', + u'name': u'Service petite enfance', + u'slug': u'service-petite-enfance', + u'description': u'Role du service petite enfance %s' % tenant.domain_url, + } + ] + } + } + Command.process_notification(tenant, notification) + assert Group.objects.count() == 1 + assert Role.objects.count() == 1 + role = Role.objects.get() + assert role.uuid == u'12345' + assert role.name == u'Service petite enfance' + assert role.description == u'Role du service petite enfance %s' % tenant.domain_url + + # test user provisionning + for tenant in tenants: + with tenant_context(tenant): + notification = { + u'@type': u'provision', + u'issuer': 'http://idp.example.net/idp/saml/metadata', + u'audience': [u'%s/saml/metadata' % tenant.get_base_url()], + u'objects': { + u'@type': 'user', + u'data': [ + { + u'uuid': u'a' * 32, + u'first_name': u'John', + u'last_name': u'Doe', + u'email': u'john.doe@example.net', + u'roles': [ + { + u'uuid': u'12345', + u'name': u'Service petite enfance', + u'description': u'etc.', + }, + { + u'uuid': u'xyz', + u'name': u'Service état civil', + u'description': u'etc.', + }, + ], + } + ] + } + } + Command.process_notification(tenant, notification) + assert User.objects.count() == 1 + assert Role.objects.count() == 1 + assert Group.objects.count() == 1 + user = User.objects.get() + assert user.username == 'a' * 30 + assert user.first_name == 'John' + assert user.last_name == 'Doe' + assert user.email == 'john.doe@example.net' + assert user.saml_identifiers.count() == 1 + usi = user.saml_identifiers.get() + assert usi.issuer == 'http://idp.example.net/idp/saml/metadata' + assert usi.name_id == 'a' * 32 + assert user.groups.count() == 1 + group = user.groups.get() + assert group.name == 'Service petite enfance' + role = Role.objects.get(group_ptr=group.pk) + assert role.uuid == '12345' + + # test nothing change if run a second time + for tenant in tenants: + with tenant_context(tenant): + notification = { + u'@type': u'provision', + u'issuer': 'http://idp.example.net/idp/saml/metadata', + u'audience': [u'%s/saml/metadata' % tenant.get_base_url()], + u'objects': { + u'@type': 'user', + u'data': [ + { + u'uuid': u'a' * 32, + u'first_name': u'John', + u'last_name': u'Doe', + u'email': u'john.doe@example.net', + u'roles': [ + { + u'uuid': u'12345', + u'name': u'Service petite enfance', + u'description': u'etc.', + }, + { + u'uuid': u'xyz', + u'name': u'Service état civil', + u'description': u'etc.', + }, + ], + } + ] + } + } + Command.process_notification(tenant, notification) + assert User.objects.count() == 1 + assert Role.objects.count() == 1 + assert Group.objects.count() == 1 + user = User.objects.get() + assert user.username == 'a' * 30 + assert user.first_name == 'John' + assert user.last_name == 'Doe' + assert user.email == 'john.doe@example.net' + assert user.saml_identifiers.count() == 1 + usi = user.saml_identifiers.get() + assert usi.issuer == 'http://idp.example.net/idp/saml/metadata' + assert usi.name_id == 'a' * 32 + assert user.groups.count() == 1 + group = user.groups.get() + assert group.name == 'Service petite enfance' + role = Role.objects.get(group_ptr=group.pk) + assert role.uuid == '12345' + + # test deprovision works + for tenant in tenants: + with tenant_context(tenant): + notification = { + u'@type': u'deprovision', + u'issuer': 'http://idp.example.net/idp/saml/metadata', + u'audience': [u'%s/saml/metadata' % tenant.get_base_url()], + u'objects': { + u'@type': 'user', + u'data': [ + { + u'uuid': u'a' * 32, + u'first_name': u'John', + u'last_name': u'Doe', + u'email': u'john.doe@example.net', + u'roles': [ + { + u'uuid': u'12345', + u'name': u'Service petite enfance', + u'description': u'etc.', + }, + { + u'uuid': u'xyz', + u'name': u'Service état civil', + u'description': u'etc.', + }, + ], + } + ] + } + } + Command.process_notification(tenant, notification) + assert User.objects.count() == 0 -- 2.1.4