general: use HTTP API to provision users & groups (#43245)

This commit is contained in:
Frédéric Péters 2020-05-25 21:15:26 +02:00
parent e5cea48f38
commit 0012761656
13 changed files with 403 additions and 167 deletions

View File

@ -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:

View File

@ -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)

View File

@ -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:

View File

@ -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:

View File

@ -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)}

View File

@ -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,

View File

View File

@ -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'])

159
hobo/provisionning/utils.py Normal file
View File

@ -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()

View File

@ -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'

View File

@ -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

View File

@ -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'])

View File

@ -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",