general: use HTTP API to provision users & groups (#43245)
This commit is contained in:
parent
e5cea48f38
commit
0012761656
|
@ -264,6 +264,7 @@ if 'MIDDLEWARE_CLASSES' in globals():
|
|||
if PROJECT_NAME != 'wcs' and 'authentic2' not in INSTALLED_APPS:
|
||||
MIDDLEWARE_CLASSES = MIDDLEWARE_CLASSES + (
|
||||
'mellon.middleware.PassiveAuthenticationMiddleware',
|
||||
'hobo.provisionning.middleware.ProvisionningMiddleware',
|
||||
)
|
||||
|
||||
if 'authentic2' in INSTALLED_APPS:
|
||||
|
@ -282,6 +283,7 @@ else:
|
|||
if PROJECT_NAME != 'wcs' and 'authentic2' not in INSTALLED_APPS:
|
||||
MIDDLEWARE = MIDDLEWARE + (
|
||||
'mellon.middleware.PassiveAuthenticationMiddleware',
|
||||
'hobo.provisionning.middleware.ProvisionningMiddleware',
|
||||
)
|
||||
|
||||
if 'authentic2' in INSTALLED_APPS:
|
||||
|
|
|
@ -3,6 +3,7 @@ from django.utils.six.moves.urllib.parse import urljoin
|
|||
import threading
|
||||
import copy
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import connection
|
||||
|
@ -12,6 +13,7 @@ from django.utils.encoding import force_text
|
|||
|
||||
from django_rbac.utils import get_role_model, get_ou_model, get_role_parenting_model
|
||||
from hobo.agent.common import notify_agents
|
||||
from hobo.signature import sign_url
|
||||
from authentic2.saml.models import LibertyProvider
|
||||
from authentic2.a2_rbac.models import RoleAttribute
|
||||
from authentic2.models import AttributeValue
|
||||
|
@ -164,7 +166,7 @@ class Provisionning(threading.local):
|
|||
for service, audience in self.get_audience(ou):
|
||||
for user in users:
|
||||
logger.info(u'provisionning user %s to %s', user, audience)
|
||||
notify_agents({
|
||||
self.notify_agents({
|
||||
'@type': 'provision',
|
||||
'issuer': issuer,
|
||||
'audience': [audience],
|
||||
|
@ -181,7 +183,7 @@ class Provisionning(threading.local):
|
|||
continue
|
||||
logger.info(u'provisionning users %s to %s', u', '.join(
|
||||
map(force_text, users)), u', '.join(audience))
|
||||
notify_agents({
|
||||
self.notify_agents({
|
||||
'@type': 'provision',
|
||||
'issuer': issuer,
|
||||
'audience': audience,
|
||||
|
@ -196,7 +198,7 @@ class Provisionning(threading.local):
|
|||
for s, audience in self.get_audience(ou)]
|
||||
logger.info(u'deprovisionning users %s from %s', u', '.join(
|
||||
map(force_text, users)), u', '.join(audience))
|
||||
notify_agents({
|
||||
self.notify_agents({
|
||||
'@type': 'deprovision',
|
||||
'issuer': issuer,
|
||||
'audience': audience,
|
||||
|
@ -249,7 +251,7 @@ class Provisionning(threading.local):
|
|||
|
||||
audience = [entity_id for service, entity_id in self.get_audience(ou)]
|
||||
logger.info(u'%sning roles %s to %s', mode, roles, audience)
|
||||
notify_agents({
|
||||
self.notify_agents({
|
||||
'@type': mode,
|
||||
'audience': audience,
|
||||
'full': full,
|
||||
|
@ -401,3 +403,32 @@ class Provisionning(threading.local):
|
|||
if not reverse:
|
||||
for other_instance in instance.members.all():
|
||||
self.add_saved(other_instance)
|
||||
|
||||
def notify_agents(self, data):
|
||||
if getattr(settings, 'HOBO_HTTP_PROVISIONNING', False):
|
||||
services_by_url = {}
|
||||
for services in settings.KNOWN_SERVICES.values():
|
||||
for service in services.values():
|
||||
if service.get('provisionning-url'):
|
||||
services_by_url[service['saml-sp-metadata-url']] = service
|
||||
audience = data.get('audience')
|
||||
rest_audience = [x for x in audience if x in services_by_url]
|
||||
amqp_audience = audience
|
||||
for audience in rest_audience:
|
||||
service = services_by_url[audience]
|
||||
data['audience'] = [audience]
|
||||
try:
|
||||
response = requests.put(
|
||||
sign_url(service['provisionning-url'] + '?orig=%s' % service['orig'], service['secret']),
|
||||
json=data)
|
||||
response.raise_for_status()
|
||||
except requests.RequestException as e:
|
||||
logger.error(u'error provisionning to %s (%s)', audience, e)
|
||||
else:
|
||||
amqp_audience.remove(audience)
|
||||
data['audience'] = amqp_audience
|
||||
if amqp_audience:
|
||||
logger.info(u'leftover AMQP audience: %s', amqp_audience)
|
||||
|
||||
if data['audience']:
|
||||
notify_agents(data)
|
||||
|
|
|
@ -17,23 +17,16 @@
|
|||
import json
|
||||
import os
|
||||
import sys
|
||||
import random
|
||||
import logging
|
||||
|
||||
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.multitenant.utils import provision_user_groups
|
||||
|
||||
from hobo.agent.common.models import Role
|
||||
from hobo.provisionning.utils import NotificationProcessing, TryAgain
|
||||
|
||||
class TryAgain(Exception):
|
||||
pass
|
||||
|
||||
class Command(BaseCommand):
|
||||
class Command(BaseCommand, NotificationProcessing):
|
||||
requires_system_checks = False
|
||||
|
||||
def add_arguments(self, parser):
|
||||
|
@ -55,135 +48,6 @@ class Command(BaseCommand):
|
|||
with tenant_context(tenant):
|
||||
self.process_notification(tenant, notification)
|
||||
|
||||
@classmethod
|
||||
def check_valid_notification(cls, notification):
|
||||
return isinstance(notification, dict) \
|
||||
and '@type' in notification \
|
||||
and notification['@type'] in ['provision', 'deprovision'] \
|
||||
and 'objects' in notification \
|
||||
and 'audience' in notification \
|
||||
and isinstance(notification['audience'], list) \
|
||||
and isinstance(notification['objects'], dict)
|
||||
|
||||
@classmethod
|
||||
def check_valid_role(cls, o):
|
||||
return 'uuid' in o \
|
||||
and 'name' in o \
|
||||
and 'description' in o
|
||||
|
||||
@classmethod
|
||||
def check_valid_user(cls, o):
|
||||
return 'uuid' in o \
|
||||
and 'is_superuser' 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:
|
||||
try:
|
||||
with atomic():
|
||||
if action == 'provision':
|
||||
assert cls.check_valid_user(o)
|
||||
try:
|
||||
mellon_user = UserSAMLIdentifier.objects.get(
|
||||
issuer=issuer, name_id=o['uuid'])
|
||||
user = mellon_user.user
|
||||
except UserSAMLIdentifier.DoesNotExist:
|
||||
try:
|
||||
user = User.objects.get(username=o['uuid'][:30])
|
||||
except User.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'][:30]
|
||||
user.last_name = o['last_name'][:30]
|
||||
user.email = o['email'][:75]
|
||||
user.username = o['uuid'][:30]
|
||||
user.is_superuser = o['is_superuser']
|
||||
user.is_staff = o['is_superuser']
|
||||
user.save()
|
||||
role_uuids = [role['uuid'] for role in o.get('roles', [])]
|
||||
provision_user_groups(user, role_uuids)
|
||||
elif action == 'deprovision':
|
||||
assert 'uuid' in o
|
||||
uuids.add(o['uuid'])
|
||||
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):
|
||||
logger = logging.getLogger(__name__)
|
||||
uuids = set()
|
||||
for o in data:
|
||||
assert 'uuid' in o
|
||||
uuids.add(o['uuid'])
|
||||
if action == 'provision':
|
||||
assert cls.check_valid_role(o)
|
||||
role_name = o['name']
|
||||
if len(role_name) > 70:
|
||||
role_name = role_name[:70] + '(...)'
|
||||
try:
|
||||
role = Role.objects.get(uuid=o['uuid'])
|
||||
created = False
|
||||
except Role.DoesNotExist:
|
||||
try:
|
||||
with atomic():
|
||||
role, created = Role.objects.get_or_create(
|
||||
name=role_name, defaults={
|
||||
'uuid': o['uuid'],
|
||||
'description': o['description']})
|
||||
except IntegrityError:
|
||||
# Can happen if uuid and name already exist
|
||||
logger.error(u'cannot provision role "%s" (%s)', o['name'], o['uuid'])
|
||||
continue
|
||||
if not created:
|
||||
save = False
|
||||
if role.name != role_name:
|
||||
role.name = role_name
|
||||
save = True
|
||||
if role.uuid != o['uuid']:
|
||||
role.uuid = o['uuid']
|
||||
save = True
|
||||
if role.description != o['description']:
|
||||
role.description = o['description']
|
||||
save = True
|
||||
if role.details != o.get('details', u''):
|
||||
role.details = o.get('details', u'')
|
||||
save = True
|
||||
if save:
|
||||
try:
|
||||
with atomic():
|
||||
role.save()
|
||||
except IntegrityError:
|
||||
# Can happen if uuid and name already exist
|
||||
logger.error(u'cannot provision role "%s" (%s)', o['name'], o['uuid'])
|
||||
continue
|
||||
if full and action == 'provision':
|
||||
for role in Role.objects.exclude(uuid__in=uuids):
|
||||
role.delete()
|
||||
elif action == 'deprovision':
|
||||
for role in Role.objects.filter(uuid__in=uuids):
|
||||
role.delete()
|
||||
|
||||
@classmethod
|
||||
def process_notification(cls, tenant, notification):
|
||||
assert cls.check_valid_notification(notification), \
|
||||
|
@ -197,7 +61,6 @@ class Command(BaseCommand):
|
|||
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']
|
||||
for i in range(20):
|
||||
try:
|
||||
|
|
|
@ -155,6 +155,8 @@ class ServiceBase(models.Model):
|
|||
as_dict['saml-idp-metadata-url'] = self.get_saml_idp_metadata_url()
|
||||
if self.get_backoffice_menu_url():
|
||||
as_dict['backoffice-menu-url'] = self.get_backoffice_menu_url()
|
||||
if self.get_provisionning_url():
|
||||
as_dict['provisionning-url'] = self.get_provisionning_url()
|
||||
return as_dict
|
||||
|
||||
@property
|
||||
|
@ -207,6 +209,9 @@ class ServiceBase(models.Model):
|
|||
def get_backoffice_menu_url(self):
|
||||
return None
|
||||
|
||||
def get_provisionning_url(self):
|
||||
return self.get_base_url_path() + '__provision__/'
|
||||
|
||||
def is_resolvable(self):
|
||||
try:
|
||||
netloc = urlparse(self.base_url).netloc
|
||||
|
@ -298,6 +303,9 @@ class Authentic(ServiceBase):
|
|||
def get_backoffice_menu_url(self):
|
||||
return self.get_base_url_path() + 'manage/menu.json'
|
||||
|
||||
def get_provisionning_url(self):
|
||||
return None
|
||||
|
||||
|
||||
class Wcs(ServiceBase):
|
||||
class Meta:
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import hashlib
|
||||
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from django.db import connection
|
||||
|
@ -21,6 +23,7 @@ from django.utils.six.moves.urllib.parse import urlparse
|
|||
from django.utils.encoding import force_text
|
||||
|
||||
from hobo.middleware.utils import StoreRequestMiddleware
|
||||
from hobo.multitenant.settings_loaders import KnownServices
|
||||
|
||||
|
||||
def get_installed_services():
|
||||
|
@ -35,9 +38,13 @@ def get_operational_services():
|
|||
return [x for x in get_installed_services() if x.is_operational()]
|
||||
|
||||
|
||||
def get_installed_services_dict():
|
||||
from .models import Variable
|
||||
hobo_service = []
|
||||
def get_local_key(url):
|
||||
secret1 = force_text(settings.SECRET_KEY)
|
||||
secret2 = url
|
||||
return KnownServices.shared_secret(secret1, secret2)[:40]
|
||||
|
||||
|
||||
def get_local_hobo_dict():
|
||||
build_absolute_uri = None
|
||||
if hasattr(connection, 'get_tenant') and hasattr(connection.get_tenant(), 'build_absolute_uri'):
|
||||
build_absolute_uri = connection.get_tenant().build_absolute_uri
|
||||
|
@ -45,16 +52,27 @@ def get_installed_services_dict():
|
|||
request = StoreRequestMiddleware.get_request()
|
||||
if request:
|
||||
build_absolute_uri = request.build_absolute_uri
|
||||
if build_absolute_uri:
|
||||
# if there's a known base url hobo can advertise itself.
|
||||
hobo_service = [{
|
||||
'service-id': 'hobo',
|
||||
'title': 'Hobo',
|
||||
'slug': 'hobo',
|
||||
'base_url': build_absolute_uri(reverse('home')),
|
||||
'saml-sp-metadata-url': build_absolute_uri(reverse('mellon_metadata')),
|
||||
'backoffice-menu-url': build_absolute_uri(reverse('menu_json')),
|
||||
}]
|
||||
if not build_absolute_uri:
|
||||
return None
|
||||
# if there's a known base url hobo can advertise itself.
|
||||
return {
|
||||
'secret_key': get_local_key(build_absolute_uri('/')),
|
||||
'service-id': 'hobo',
|
||||
'title': 'Hobo',
|
||||
'slug': 'hobo',
|
||||
'base_url': build_absolute_uri(reverse('home')),
|
||||
'saml-sp-metadata-url': build_absolute_uri(reverse('mellon_metadata')),
|
||||
'backoffice-menu-url': build_absolute_uri(reverse('menu_json')),
|
||||
'provisionning-url': build_absolute_uri('/__provision__/'),
|
||||
}
|
||||
|
||||
|
||||
def get_installed_services_dict():
|
||||
from .models import Variable
|
||||
hobo_service = []
|
||||
hobo_dict = get_local_hobo_dict()
|
||||
if hobo_dict:
|
||||
hobo_service.append(hobo_dict)
|
||||
return {
|
||||
'services': hobo_service + [x.as_dict() for x in get_installed_services()],
|
||||
'variables': {v.name: v.json for v in Variable.objects.filter(service_pk__isnull=True)}
|
||||
|
|
|
@ -84,6 +84,8 @@ class KnownServices(FileBaseSettingsLoader):
|
|||
service_data = {
|
||||
'url': url,
|
||||
'backoffice-menu-url': service.get('backoffice-menu-url'),
|
||||
'provisionning-url': service.get('provisionning-url'),
|
||||
'saml-sp-metadata-url': service.get('saml-sp-metadata-url'),
|
||||
'title': service.get('title'),
|
||||
'orig': orig,
|
||||
'verif_orig': verif_orig,
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
# hobo - portal to configure and deploy applications
|
||||
# Copyright (C) 2015-2020 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import json
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import JsonResponse, HttpResponseBadRequest, HttpResponseForbidden
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
from django.utils.six.moves.urllib.parse import urlparse
|
||||
|
||||
from hobo.provisionning.utils import NotificationProcessing, TryAgain
|
||||
from hobo.rest_authentication import PublikAuthentication, PublikAuthenticationFailed
|
||||
|
||||
|
||||
class ProvisionningMiddleware(MiddlewareMixin, NotificationProcessing):
|
||||
def process_request(self, request):
|
||||
if not (request.method == 'PUT' and request.path == '/__provision__/'):
|
||||
return None
|
||||
if 'hobo.environment' in settings.INSTALLED_APPS:
|
||||
self.hobo_specific_setup()
|
||||
|
||||
try:
|
||||
PublikAuthentication().authenticate(request)
|
||||
except PublikAuthenticationFailed:
|
||||
return HttpResponseForbidden()
|
||||
try:
|
||||
notification = json.loads(force_text(request.body))
|
||||
except ValueError:
|
||||
return HttpResponseBadRequest()
|
||||
if not isinstance(notification, dict) or 'objects' not in notification:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
object_type = notification['objects'].get('@type')
|
||||
issuer = notification.get('issuer')
|
||||
action = notification.get('@type')
|
||||
if not (object_type and action):
|
||||
return HttpResponseBadRequest()
|
||||
full = notification['full'] if 'full' in notification else False
|
||||
|
||||
for i in range(20):
|
||||
try:
|
||||
getattr(self, 'provision_' + object_type)(issuer, action, notification['objects']['data'], full=full)
|
||||
except TryAgain:
|
||||
continue
|
||||
break
|
||||
|
||||
return JsonResponse({'err': 0})
|
||||
|
||||
def hobo_specific_setup(self):
|
||||
# much ado about hobo, this is because it is not deployed like other
|
||||
# services and will not have a hobo.json in its tenant directory, and
|
||||
# will thus be missing settings loaders, etc.
|
||||
from hobo.environment.utils import get_local_hobo_dict
|
||||
known_services = getattr(settings, 'KNOWN_SERVICES', None)
|
||||
local_hobo_dict = get_local_hobo_dict()
|
||||
if not known_services:
|
||||
# hobo in a single deployment instance
|
||||
settings.KNOWN_SERVICES = known_services = {}
|
||||
known_services['hobo'] = {'hobo': local_hobo_dict}
|
||||
known_services['authentic'] = {'idp': {}}
|
||||
if known_services['hobo']['hobo']['provisionning-url'] == local_hobo_dict['provisionning-url']:
|
||||
# hobo in a single deployment instance, or primary hobo in a
|
||||
# multi-instances environment
|
||||
from hobo.environment.models import Authentic
|
||||
from hobo.multitenant.settings_loaders import KnownServices
|
||||
authentic = Authentic.objects.all().first()
|
||||
orig = urlparse(authentic.base_url).netloc.split(':')[0]
|
||||
# create stub settings.KNOWN_SERVICES with just enough to get
|
||||
# authentication passing.
|
||||
idp_service = list(settings.KNOWN_SERVICES['authentic'].values())[0]
|
||||
idp_service['verif_orig'] = orig
|
||||
idp_service['secret_key'] = KnownServices.shared_secret(
|
||||
authentic.secret_key, local_hobo_dict['secret_key'])
|
|
@ -0,0 +1,159 @@
|
|||
# hobo - portal to configure and deploy applications
|
||||
# Copyright (C) 2015-2020 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import random
|
||||
import logging
|
||||
|
||||
from django.db.transaction import atomic
|
||||
from django.db import IntegrityError
|
||||
|
||||
from hobo.multitenant.utils import provision_user_groups
|
||||
from hobo.agent.common.models import Role
|
||||
|
||||
|
||||
class TryAgain(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NotificationProcessing:
|
||||
|
||||
@classmethod
|
||||
def check_valid_notification(cls, notification):
|
||||
return isinstance(notification, dict) \
|
||||
and '@type' in notification \
|
||||
and notification['@type'] in ['provision', 'deprovision'] \
|
||||
and 'objects' in notification \
|
||||
and 'audience' in notification \
|
||||
and isinstance(notification['audience'], list) \
|
||||
and isinstance(notification['objects'], dict)
|
||||
|
||||
@classmethod
|
||||
def check_valid_role(cls, o):
|
||||
return 'uuid' in o \
|
||||
and 'name' in o \
|
||||
and 'description' in o
|
||||
|
||||
@classmethod
|
||||
def check_valid_user(cls, o):
|
||||
return 'uuid' in o \
|
||||
and 'is_superuser' 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:
|
||||
try:
|
||||
with atomic():
|
||||
if action == 'provision':
|
||||
assert cls.check_valid_user(o)
|
||||
try:
|
||||
mellon_user = UserSAMLIdentifier.objects.get(
|
||||
issuer=issuer, name_id=o['uuid'])
|
||||
user = mellon_user.user
|
||||
except UserSAMLIdentifier.DoesNotExist:
|
||||
try:
|
||||
user = User.objects.get(username=o['uuid'][:30])
|
||||
except User.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'][:30]
|
||||
user.last_name = o['last_name'][:30]
|
||||
user.email = o['email'][:75]
|
||||
user.username = o['uuid'][:30]
|
||||
user.is_superuser = o['is_superuser']
|
||||
user.is_staff = o['is_superuser']
|
||||
user.save()
|
||||
role_uuids = [role['uuid'] for role in o.get('roles', [])]
|
||||
provision_user_groups(user, role_uuids)
|
||||
elif action == 'deprovision':
|
||||
assert 'uuid' in o
|
||||
uuids.add(o['uuid'])
|
||||
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):
|
||||
logger = logging.getLogger(__name__)
|
||||
uuids = set()
|
||||
for o in data:
|
||||
assert 'uuid' in o
|
||||
uuids.add(o['uuid'])
|
||||
if action == 'provision':
|
||||
assert cls.check_valid_role(o)
|
||||
role_name = o['name']
|
||||
if len(role_name) > 70:
|
||||
role_name = role_name[:70] + '(...)'
|
||||
try:
|
||||
role = Role.objects.get(uuid=o['uuid'])
|
||||
created = False
|
||||
except Role.DoesNotExist:
|
||||
try:
|
||||
with atomic():
|
||||
role, created = Role.objects.get_or_create(
|
||||
name=role_name, defaults={
|
||||
'uuid': o['uuid'],
|
||||
'description': o['description']})
|
||||
except IntegrityError:
|
||||
# Can happen if uuid and name already exist
|
||||
logger.error(u'cannot provision role "%s" (%s)', o['name'], o['uuid'])
|
||||
continue
|
||||
if not created:
|
||||
save = False
|
||||
if role.name != role_name:
|
||||
role.name = role_name
|
||||
save = True
|
||||
if role.uuid != o['uuid']:
|
||||
role.uuid = o['uuid']
|
||||
save = True
|
||||
if role.description != o['description']:
|
||||
role.description = o['description']
|
||||
save = True
|
||||
if role.details != o.get('details', u''):
|
||||
role.details = o.get('details', u'')
|
||||
save = True
|
||||
if save:
|
||||
try:
|
||||
with atomic():
|
||||
role.save()
|
||||
except IntegrityError:
|
||||
# Can happen if uuid and name already exist
|
||||
logger.error(u'cannot provision role "%s" (%s)', o['name'], o['uuid'])
|
||||
continue
|
||||
if full and action == 'provision':
|
||||
for role in Role.objects.exclude(uuid__in=uuids):
|
||||
role.delete()
|
||||
elif action == 'deprovision':
|
||||
for role in Role.objects.filter(uuid__in=uuids):
|
||||
role.delete()
|
|
@ -2,12 +2,14 @@ LANGUAGE_CODE = 'en-us'
|
|||
BROKER_URL = 'memory://'
|
||||
OZWILLO_SECRET = 'secret'
|
||||
|
||||
INSTALLED_APPS += ('hobo.contrib.ozwillo',)
|
||||
INSTALLED_APPS += ('hobo.contrib.ozwillo', 'hobo.agent.common')
|
||||
|
||||
ALLOWED_HOSTS.append('localhost')
|
||||
|
||||
TEMPLATES[0]['OPTIONS']['debug'] = True
|
||||
|
||||
MIDDLEWARE_CLASSES = ('hobo.middleware.RobotsTxtMiddleware', ) + MIDDLEWARE_CLASSES
|
||||
MIDDLEWARE_CLASSES = MIDDLEWARE_CLASSES + (
|
||||
'hobo.middleware.RobotsTxtMiddleware',
|
||||
'hobo.provisionning.middleware.ProvisionningMiddleware')
|
||||
|
||||
HOBO_MANAGER_HOMEPAGE_URL_VAR = 'portal_agent_url'
|
||||
|
|
|
@ -535,3 +535,61 @@ def test_middleware(notify_agents, app_factory, tenant, settings):
|
|||
assert notify_agents.call_count == 0
|
||||
resp = resp.form.submit().follow()
|
||||
assert notify_agents.call_count == 1
|
||||
|
||||
|
||||
def test_provision_using_http(transactional_db, tenant, settings, caplog):
|
||||
with tenant_context(tenant):
|
||||
# create providers so notification messages have an audience.
|
||||
LibertyProvider.objects.create(ou=None, name='provider', slug='provider',
|
||||
entity_id='http://example.org',
|
||||
protocol_conformance=lasso.PROTOCOL_SAML_2_0)
|
||||
LibertyProvider.objects.create(ou=None, name='provider2', slug='provider2',
|
||||
entity_id='http://example.com',
|
||||
protocol_conformance=lasso.PROTOCOL_SAML_2_0)
|
||||
with patch('hobo.agent.authentic2.provisionning.notify_agents') as notify_agents:
|
||||
call_command('createsuperuser', domain=tenant.domain_url, uuid='coin',
|
||||
username='coin', email='coin@coin.org', interactive=False)
|
||||
assert notify_agents.call_count == 1
|
||||
assert set(notify_agents.call_args[0][0]['audience']) == {'http://example.org', 'http://example.com'}
|
||||
|
||||
settings.HOBO_HTTP_PROVISIONNING = True
|
||||
with patch('hobo.agent.authentic2.provisionning.notify_agents') as notify_agents:
|
||||
call_command('createsuperuser', domain=tenant.domain_url, uuid='coin2',
|
||||
username='coin2', email='coin2@coin.org', interactive=False)
|
||||
assert notify_agents.call_count == 1
|
||||
assert set(notify_agents.call_args[0][0]['audience']) == {'http://example.org', 'http://example.com'}
|
||||
|
||||
settings.HOBO_HTTP_PROVISIONNING = True
|
||||
settings.KNOWN_SERVICES = {
|
||||
'foo': {
|
||||
'bar': {
|
||||
'saml-sp-metadata-url': 'http://example.org',
|
||||
'provisionning-url': 'http://example.org/__provision__/',
|
||||
'orig': 'example.org',
|
||||
'secret': 'xxx',
|
||||
}
|
||||
}
|
||||
}
|
||||
with patch('hobo.agent.authentic2.provisionning.notify_agents') as notify_agents:
|
||||
with patch('hobo.agent.authentic2.provisionning.requests.put') as requests_put:
|
||||
call_command('createsuperuser', domain=tenant.domain_url, uuid='coin2',
|
||||
username='coin2', email='coin2@coin.org', interactive=False)
|
||||
assert notify_agents.call_count == 1
|
||||
assert notify_agents.call_args[0][0]['audience'] == ['http://example.com']
|
||||
assert requests_put.call_count == 1
|
||||
# cannot check audience passed to requests.put as it's the same
|
||||
# dictionary that is altered afterwards and would thus also contain
|
||||
# http://example.com.
|
||||
|
||||
settings.KNOWN_SERVICES['foo']['bar2'] = {
|
||||
'saml-sp-metadata-url': 'http://example.com',
|
||||
'provisionning-url': 'http://example.com/__provision__/',
|
||||
'orig': 'example.com',
|
||||
'secret': 'xxx',
|
||||
}
|
||||
with patch('hobo.agent.authentic2.provisionning.notify_agents') as notify_agents:
|
||||
with patch('hobo.agent.authentic2.provisionning.requests.put') as requests_put:
|
||||
call_command('createsuperuser', domain=tenant.domain_url, uuid='coin2',
|
||||
username='coin2', email='coin2@coin.org', interactive=False)
|
||||
assert notify_agents.call_count == 0
|
||||
assert requests_put.call_count == 2
|
||||
|
|
|
@ -245,7 +245,7 @@ def test_known_services(tenants, settings):
|
|||
assert 'other' in settings.KNOWN_SERVICES['authentic']
|
||||
assert (set(['url', 'backoffice-menu-url', 'title', 'orig',
|
||||
'verif_orig', 'secret', 'template_name', 'variables',
|
||||
'secondary'])
|
||||
'saml-sp-metadata-url', 'provisionning-url', 'secondary'])
|
||||
== set(settings.KNOWN_SERVICES['authentic']['other'].keys()))
|
||||
assert (settings.KNOWN_SERVICES['authentic']['other']['url']
|
||||
== hobo_json['services'][2]['base_url'])
|
||||
|
|
|
@ -139,7 +139,9 @@
|
|||
{
|
||||
"backoffice-menu-url": "https://hobo-instance-name.dev.signalpublik.com/menu.json",
|
||||
"base_url": "https://hobo-instance-name.dev.signalpublik.com/",
|
||||
"provisionning-url": "https://hobo-instance-name.dev.signalpublik.com/__provision__/",
|
||||
"saml-sp-metadata-url": "https://hobo-instance-name.dev.signalpublik.com/accounts/mellon/metadata/",
|
||||
"secret_key": "XXX",
|
||||
"service-id": "hobo",
|
||||
"slug": "hobo",
|
||||
"title": "Hobo"
|
||||
|
@ -150,22 +152,23 @@
|
|||
"id": 1,
|
||||
"saml-idp-metadata-url": "https://connexion-instance-name.dev.signalpublik.com/idp/saml2/metadata",
|
||||
"secondary": false,
|
||||
"secret_key": "k_a)vo)a&8xugbzjl#%^s8vfkm2+#yhz#if4m+xu!qqv=04x9q",
|
||||
"secret_key": "XXX",
|
||||
"service-id": "authentic",
|
||||
"service-label": "Authentic",
|
||||
"slug": "idp",
|
||||
"template_name": "signal-publik",
|
||||
"title": "Connexion",
|
||||
"variables": {},
|
||||
"use_as_idp_for_self": true
|
||||
"use_as_idp_for_self": true,
|
||||
"variables": {}
|
||||
},
|
||||
{
|
||||
"backoffice-menu-url": "https://demarches-instance-name.dev.signalpublik.com/backoffice/menu.json",
|
||||
"base_url": "https://demarches-instance-name.dev.signalpublik.com/",
|
||||
"id": 1,
|
||||
"provisionning-url": "https://demarches-instance-name.dev.signalpublik.com/__provision__/",
|
||||
"saml-sp-metadata-url": "https://demarches-instance-name.dev.signalpublik.com/saml/metadata",
|
||||
"secondary": false,
|
||||
"secret_key": "uhipz^y38a*w#rrnio_-i=+7p47aq#$+dntm*i@nz(y)n57153",
|
||||
"secret_key": "XXX",
|
||||
"service-id": "wcs",
|
||||
"service-label": "w.c.s.",
|
||||
"slug": "eservices",
|
||||
|
@ -177,9 +180,10 @@
|
|||
"backoffice-menu-url": "https://passerelle-instance-name.dev.signalpublik.com/manage/menu.json",
|
||||
"base_url": "https://passerelle-instance-name.dev.signalpublik.com/",
|
||||
"id": 1,
|
||||
"provisionning-url": "https://passerelle-instance-name.dev.signalpublik.com/__provision__/",
|
||||
"saml-sp-metadata-url": "https://passerelle-instance-name.dev.signalpublik.com/accounts/mellon/metadata/",
|
||||
"secondary": false,
|
||||
"secret_key": "vz&g(p1bhzw35iltrrl$^6013*+q80l&l4)b)tsr=+ko__js_v",
|
||||
"secret_key": "XXX",
|
||||
"service-id": "passerelle",
|
||||
"service-label": "Passerelle",
|
||||
"slug": "passerelle",
|
||||
|
@ -191,9 +195,10 @@
|
|||
"backoffice-menu-url": "https://instance-name.dev.signalpublik.com/manage/menu.json",
|
||||
"base_url": "https://instance-name.dev.signalpublik.com/",
|
||||
"id": 1,
|
||||
"provisionning-url": "https://instance-name.dev.signalpublik.com/__provision__/",
|
||||
"saml-sp-metadata-url": "https://instance-name.dev.signalpublik.com/accounts/mellon/metadata/",
|
||||
"secondary": false,
|
||||
"secret_key": "^0!psa-ijq4*va0a4&_)solvils#hig2vtof(%3iy#!6p5!f6e",
|
||||
"secret_key": "XXX",
|
||||
"service-id": "combo",
|
||||
"service-label": "Combo",
|
||||
"slug": "portal",
|
||||
|
@ -205,9 +210,10 @@
|
|||
"backoffice-menu-url": "https://agents-instance-name.dev.signalpublik.com/manage/menu.json",
|
||||
"base_url": "https://agents-instance-name.dev.signalpublik.com/",
|
||||
"id": 2,
|
||||
"provisionning-url": "https://agents-instance-name.dev.signalpublik.com/__provision__/",
|
||||
"saml-sp-metadata-url": "https://agents-instance-name.dev.signalpublik.com/accounts/mellon/metadata/",
|
||||
"secondary": false,
|
||||
"secret_key": "m1&vql=pm-clw)0wcnk=q4g1-#flrus!dui$gr$7ug2%xw@ko$",
|
||||
"secret_key": "XXX",
|
||||
"service-id": "combo",
|
||||
"service-label": "Combo",
|
||||
"slug": "portal-agent",
|
||||
|
@ -216,7 +222,7 @@
|
|||
"variables": {}
|
||||
}
|
||||
],
|
||||
"timestamp": "1558975192.98",
|
||||
"timestamp": "XXXXXXXXXX.XX",
|
||||
"users": [],
|
||||
"variables": {
|
||||
"css_variant": "publik",
|
||||
|
|
Loading…
Reference in New Issue