0001-general-use-HTTP-API-to-provision-users-groups-43245.patch
debian/debian_config_common.py | ||
---|---|---|
264 | 264 |
if PROJECT_NAME != 'wcs' and 'authentic2' not in INSTALLED_APPS: |
265 | 265 |
MIDDLEWARE_CLASSES = MIDDLEWARE_CLASSES + ( |
266 | 266 |
'mellon.middleware.PassiveAuthenticationMiddleware', |
267 |
'hobo.provisionning.middleware.ProvisionningMiddleware', |
|
267 | 268 |
) |
268 | 269 | |
269 | 270 |
if 'authentic2' in INSTALLED_APPS: |
... | ... | |
282 | 283 |
if PROJECT_NAME != 'wcs' and 'authentic2' not in INSTALLED_APPS: |
283 | 284 |
MIDDLEWARE = MIDDLEWARE + ( |
284 | 285 |
'mellon.middleware.PassiveAuthenticationMiddleware', |
286 |
'hobo.provisionning.middleware.ProvisionningMiddleware', |
|
285 | 287 |
) |
286 | 288 | |
287 | 289 |
if 'authentic2' in INSTALLED_APPS: |
hobo/agent/authentic2/provisionning.py | ||
---|---|---|
3 | 3 |
import threading |
4 | 4 |
import copy |
5 | 5 |
import logging |
6 |
import requests |
|
6 | 7 | |
7 | 8 |
from django.contrib.auth import get_user_model |
8 | 9 |
from django.db import connection |
... | ... | |
12 | 13 | |
13 | 14 |
from django_rbac.utils import get_role_model, get_ou_model, get_role_parenting_model |
14 | 15 |
from hobo.agent.common import notify_agents |
16 |
from hobo.signature import sign_url |
|
15 | 17 |
from authentic2.saml.models import LibertyProvider |
16 | 18 |
from authentic2.a2_rbac.models import RoleAttribute |
17 | 19 |
from authentic2.models import AttributeValue |
... | ... | |
164 | 166 |
for service, audience in self.get_audience(ou): |
165 | 167 |
for user in users: |
166 | 168 |
logger.info(u'provisionning user %s to %s', user, audience) |
167 |
notify_agents({ |
|
169 |
self.notify_agents({
|
|
168 | 170 |
'@type': 'provision', |
169 | 171 |
'issuer': issuer, |
170 | 172 |
'audience': [audience], |
... | ... | |
181 | 183 |
continue |
182 | 184 |
logger.info(u'provisionning users %s to %s', u', '.join( |
183 | 185 |
map(force_text, users)), u', '.join(audience)) |
184 |
notify_agents({ |
|
186 |
self.notify_agents({
|
|
185 | 187 |
'@type': 'provision', |
186 | 188 |
'issuer': issuer, |
187 | 189 |
'audience': audience, |
... | ... | |
196 | 198 |
for s, audience in self.get_audience(ou)] |
197 | 199 |
logger.info(u'deprovisionning users %s from %s', u', '.join( |
198 | 200 |
map(force_text, users)), u', '.join(audience)) |
199 |
notify_agents({ |
|
201 |
self.notify_agents({
|
|
200 | 202 |
'@type': 'deprovision', |
201 | 203 |
'issuer': issuer, |
202 | 204 |
'audience': audience, |
... | ... | |
249 | 251 | |
250 | 252 |
audience = [entity_id for service, entity_id in self.get_audience(ou)] |
251 | 253 |
logger.info(u'%sning roles %s to %s', mode, roles, audience) |
252 |
notify_agents({ |
|
254 |
self.notify_agents({
|
|
253 | 255 |
'@type': mode, |
254 | 256 |
'audience': audience, |
255 | 257 |
'full': full, |
... | ... | |
401 | 403 |
if not reverse: |
402 | 404 |
for other_instance in instance.members.all(): |
403 | 405 |
self.add_saved(other_instance) |
406 | ||
407 |
def notify_agents(self, data): |
|
408 |
if getattr(settings, 'HOBO_HTTP_PROVISIONNING', False): |
|
409 |
services_by_url = {} |
|
410 |
for services in settings.KNOWN_SERVICES.values(): |
|
411 |
for service in services.values(): |
|
412 |
if service.get('provisionning-url'): |
|
413 |
services_by_url[service['saml-sp-metadata-url']] = service |
|
414 |
audience = data.get('audience') |
|
415 |
rest_audience = [x for x in audience if x in services_by_url] |
|
416 |
amqp_audience = audience |
|
417 |
for audience in rest_audience: |
|
418 |
service = services_by_url[audience] |
|
419 |
data['audience'] = [audience] |
|
420 |
try: |
|
421 |
response = requests.put( |
|
422 |
sign_url(service['provisionning-url'] + '?orig=%s' % service['orig'], service['secret']), |
|
423 |
json=data) |
|
424 |
response.raise_for_status() |
|
425 |
except requests.RequestException as e: |
|
426 |
logger.error(u'error provisionning to %s (%s)', audience, e) |
|
427 |
else: |
|
428 |
amqp_audience.remove(audience) |
|
429 |
data['audience'] = amqp_audience |
|
430 |
if amqp_audience: |
|
431 |
logger.info(u'leftover AMQP audience: %s', amqp_audience) |
|
432 | ||
433 |
if data['audience']: |
|
434 |
notify_agents(data) |
hobo/agent/common/management/commands/hobo_notify.py | ||
---|---|---|
17 | 17 |
import json |
18 | 18 |
import os |
19 | 19 |
import sys |
20 |
import random |
|
21 |
import logging |
|
22 | 20 | |
23 | 21 |
from django.core.management.base import BaseCommand |
24 |
from django.db.transaction import atomic |
|
25 |
from django.db import IntegrityError |
|
26 | 22 | |
27 | 23 |
from tenant_schemas.utils import tenant_context |
28 | 24 |
from hobo.multitenant.middleware import TenantMiddleware |
29 |
from hobo.multitenant.utils import provision_user_groups |
|
30 | 25 | |
31 |
from hobo.agent.common.models import Role
|
|
26 |
from hobo.provisionning.utils import NotificationProcessing, TryAgain
|
|
32 | 27 | |
33 |
class TryAgain(Exception): |
|
34 |
pass |
|
35 | 28 | |
36 |
class Command(BaseCommand): |
|
29 |
class Command(BaseCommand, NotificationProcessing):
|
|
37 | 30 |
requires_system_checks = False |
38 | 31 | |
39 | 32 |
def add_arguments(self, parser): |
... | ... | |
55 | 48 |
with tenant_context(tenant): |
56 | 49 |
self.process_notification(tenant, notification) |
57 | 50 | |
58 |
@classmethod |
|
59 |
def check_valid_notification(cls, notification): |
|
60 |
return isinstance(notification, dict) \ |
|
61 |
and '@type' in notification \ |
|
62 |
and notification['@type'] in ['provision', 'deprovision'] \ |
|
63 |
and 'objects' in notification \ |
|
64 |
and 'audience' in notification \ |
|
65 |
and isinstance(notification['audience'], list) \ |
|
66 |
and isinstance(notification['objects'], dict) |
|
67 | ||
68 |
@classmethod |
|
69 |
def check_valid_role(cls, o): |
|
70 |
return 'uuid' in o \ |
|
71 |
and 'name' in o \ |
|
72 |
and 'description' in o |
|
73 | ||
74 |
@classmethod |
|
75 |
def check_valid_user(cls, o): |
|
76 |
return 'uuid' in o \ |
|
77 |
and 'is_superuser' in o \ |
|
78 |
and 'email' in o \ |
|
79 |
and 'first_name' in o \ |
|
80 |
and 'last_name' in o \ |
|
81 |
and 'roles' in o |
|
82 | ||
83 |
@classmethod |
|
84 |
def provision_user(cls, issuer, action, data, full=False): |
|
85 |
from django.contrib.auth import get_user_model |
|
86 |
from mellon.models import UserSAMLIdentifier |
|
87 |
User = get_user_model() |
|
88 | ||
89 | ||
90 |
assert not full # provisionning all users is dangerous, we prefer deprovision |
|
91 |
uuids = set() |
|
92 |
for o in data: |
|
93 |
try: |
|
94 |
with atomic(): |
|
95 |
if action == 'provision': |
|
96 |
assert cls.check_valid_user(o) |
|
97 |
try: |
|
98 |
mellon_user = UserSAMLIdentifier.objects.get( |
|
99 |
issuer=issuer, name_id=o['uuid']) |
|
100 |
user = mellon_user.user |
|
101 |
except UserSAMLIdentifier.DoesNotExist: |
|
102 |
try: |
|
103 |
user = User.objects.get(username=o['uuid'][:30]) |
|
104 |
except User.DoesNotExist: |
|
105 |
# temp user object |
|
106 |
random_uid = str(random.randint(1,10000000000000)) |
|
107 |
user = User.objects.create( |
|
108 |
username=random_uid) |
|
109 |
mellon_user = UserSAMLIdentifier.objects.create( |
|
110 |
user=user, issuer=issuer, name_id=o['uuid']) |
|
111 |
user.first_name = o['first_name'][:30] |
|
112 |
user.last_name = o['last_name'][:30] |
|
113 |
user.email = o['email'][:75] |
|
114 |
user.username = o['uuid'][:30] |
|
115 |
user.is_superuser = o['is_superuser'] |
|
116 |
user.is_staff = o['is_superuser'] |
|
117 |
user.save() |
|
118 |
role_uuids = [role['uuid'] for role in o.get('roles', [])] |
|
119 |
provision_user_groups(user, role_uuids) |
|
120 |
elif action == 'deprovision': |
|
121 |
assert 'uuid' in o |
|
122 |
uuids.add(o['uuid']) |
|
123 |
except IntegrityError: |
|
124 |
raise TryAgain |
|
125 |
if full and action == 'provision': |
|
126 |
for usi in UserSAMLIdentifier.objects.exclude(name_id__in=uuids): |
|
127 |
usi.user.delete() |
|
128 |
elif action == 'deprovision': |
|
129 |
for user in User.objects.filter(saml_identifiers__name_id__in=uuids): |
|
130 |
user.delete() |
|
131 | ||
132 |
@classmethod |
|
133 |
def provision_role(cls, issuer, action, data, full=False): |
|
134 |
logger = logging.getLogger(__name__) |
|
135 |
uuids = set() |
|
136 |
for o in data: |
|
137 |
assert 'uuid' in o |
|
138 |
uuids.add(o['uuid']) |
|
139 |
if action == 'provision': |
|
140 |
assert cls.check_valid_role(o) |
|
141 |
role_name = o['name'] |
|
142 |
if len(role_name) > 70: |
|
143 |
role_name = role_name[:70] + '(...)' |
|
144 |
try: |
|
145 |
role = Role.objects.get(uuid=o['uuid']) |
|
146 |
created = False |
|
147 |
except Role.DoesNotExist: |
|
148 |
try: |
|
149 |
with atomic(): |
|
150 |
role, created = Role.objects.get_or_create( |
|
151 |
name=role_name, defaults={ |
|
152 |
'uuid': o['uuid'], |
|
153 |
'description': o['description']}) |
|
154 |
except IntegrityError: |
|
155 |
# Can happen if uuid and name already exist |
|
156 |
logger.error(u'cannot provision role "%s" (%s)', o['name'], o['uuid']) |
|
157 |
continue |
|
158 |
if not created: |
|
159 |
save = False |
|
160 |
if role.name != role_name: |
|
161 |
role.name = role_name |
|
162 |
save = True |
|
163 |
if role.uuid != o['uuid']: |
|
164 |
role.uuid = o['uuid'] |
|
165 |
save = True |
|
166 |
if role.description != o['description']: |
|
167 |
role.description = o['description'] |
|
168 |
save = True |
|
169 |
if role.details != o.get('details', u''): |
|
170 |
role.details = o.get('details', u'') |
|
171 |
save = True |
|
172 |
if save: |
|
173 |
try: |
|
174 |
with atomic(): |
|
175 |
role.save() |
|
176 |
except IntegrityError: |
|
177 |
# Can happen if uuid and name already exist |
|
178 |
logger.error(u'cannot provision role "%s" (%s)', o['name'], o['uuid']) |
|
179 |
continue |
|
180 |
if full and action == 'provision': |
|
181 |
for role in Role.objects.exclude(uuid__in=uuids): |
|
182 |
role.delete() |
|
183 |
elif action == 'deprovision': |
|
184 |
for role in Role.objects.filter(uuid__in=uuids): |
|
185 |
role.delete() |
|
186 | ||
187 | 51 |
@classmethod |
188 | 52 |
def process_notification(cls, tenant, notification): |
189 | 53 |
assert cls.check_valid_notification(notification), \ |
... | ... | |
197 | 61 |
assert entity_id, 'service has no saml-sp-metadat-url field' |
198 | 62 |
if entity_id not in audience: |
199 | 63 |
return |
200 |
uuids = set() |
|
201 | 64 |
object_type = notification['objects']['@type'] |
202 | 65 |
for i in range(20): |
203 | 66 |
try: |
hobo/environment/models.py | ||
---|---|---|
155 | 155 |
as_dict['saml-idp-metadata-url'] = self.get_saml_idp_metadata_url() |
156 | 156 |
if self.get_backoffice_menu_url(): |
157 | 157 |
as_dict['backoffice-menu-url'] = self.get_backoffice_menu_url() |
158 |
if self.get_provisionning_url(): |
|
159 |
as_dict['provisionning-url'] = self.get_provisionning_url() |
|
158 | 160 |
return as_dict |
159 | 161 | |
160 | 162 |
@property |
... | ... | |
207 | 209 |
def get_backoffice_menu_url(self): |
208 | 210 |
return None |
209 | 211 | |
212 |
def get_provisionning_url(self): |
|
213 |
return self.get_base_url_path() + '__provision__/' |
|
214 | ||
210 | 215 |
def is_resolvable(self): |
211 | 216 |
try: |
212 | 217 |
netloc = urlparse(self.base_url).netloc |
... | ... | |
298 | 303 |
def get_backoffice_menu_url(self): |
299 | 304 |
return self.get_base_url_path() + 'manage/menu.json' |
300 | 305 | |
306 |
def get_provisionning_url(self): |
|
307 |
return None |
|
308 | ||
301 | 309 | |
302 | 310 |
class Wcs(ServiceBase): |
303 | 311 |
class Meta: |
hobo/environment/utils.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 hashlib |
|
18 | ||
17 | 19 |
from django.conf import settings |
18 | 20 |
from django.urls import reverse |
19 | 21 |
from django.db import connection |
... | ... | |
21 | 23 |
from django.utils.encoding import force_text |
22 | 24 | |
23 | 25 |
from hobo.middleware.utils import StoreRequestMiddleware |
26 |
from hobo.multitenant.settings_loaders import KnownServices |
|
24 | 27 | |
25 | 28 | |
26 | 29 |
def get_installed_services(): |
... | ... | |
35 | 38 |
return [x for x in get_installed_services() if x.is_operational()] |
36 | 39 | |
37 | 40 | |
38 |
def get_installed_services_dict(): |
|
39 |
from .models import Variable |
|
40 |
hobo_service = [] |
|
41 |
def get_local_key(url): |
|
42 |
secret1 = force_text(settings.SECRET_KEY) |
|
43 |
secret2 = url |
|
44 |
return KnownServices.shared_secret(secret1, secret2)[:40] |
|
45 | ||
46 | ||
47 |
def get_local_hobo_dict(): |
|
41 | 48 |
build_absolute_uri = None |
42 | 49 |
if hasattr(connection, 'get_tenant') and hasattr(connection.get_tenant(), 'build_absolute_uri'): |
43 | 50 |
build_absolute_uri = connection.get_tenant().build_absolute_uri |
... | ... | |
45 | 52 |
request = StoreRequestMiddleware.get_request() |
46 | 53 |
if request: |
47 | 54 |
build_absolute_uri = request.build_absolute_uri |
48 |
if build_absolute_uri: |
|
49 |
# if there's a known base url hobo can advertise itself. |
|
50 |
hobo_service = [{ |
|
51 |
'service-id': 'hobo', |
|
52 |
'title': 'Hobo', |
|
53 |
'slug': 'hobo', |
|
54 |
'base_url': build_absolute_uri(reverse('home')), |
|
55 |
'saml-sp-metadata-url': build_absolute_uri(reverse('mellon_metadata')), |
|
56 |
'backoffice-menu-url': build_absolute_uri(reverse('menu_json')), |
|
57 |
}] |
|
55 |
if not build_absolute_uri: |
|
56 |
return None |
|
57 |
# if there's a known base url hobo can advertise itself. |
|
58 |
return { |
|
59 |
'secret_key': get_local_key(build_absolute_uri('/')), |
|
60 |
'service-id': 'hobo', |
|
61 |
'title': 'Hobo', |
|
62 |
'slug': 'hobo', |
|
63 |
'base_url': build_absolute_uri(reverse('home')), |
|
64 |
'saml-sp-metadata-url': build_absolute_uri(reverse('mellon_metadata')), |
|
65 |
'backoffice-menu-url': build_absolute_uri(reverse('menu_json')), |
|
66 |
'provisionning-url': build_absolute_uri('/__provision__/'), |
|
67 |
} |
|
68 | ||
69 | ||
70 |
def get_installed_services_dict(): |
|
71 |
from .models import Variable |
|
72 |
hobo_service = [] |
|
73 |
hobo_dict = get_local_hobo_dict() |
|
74 |
if hobo_dict: |
|
75 |
hobo_service.append(hobo_dict) |
|
58 | 76 |
return { |
59 | 77 |
'services': hobo_service + [x.as_dict() for x in get_installed_services()], |
60 | 78 |
'variables': {v.name: v.json for v in Variable.objects.filter(service_pk__isnull=True)} |
hobo/multitenant/settings_loaders.py | ||
---|---|---|
84 | 84 |
service_data = { |
85 | 85 |
'url': url, |
86 | 86 |
'backoffice-menu-url': service.get('backoffice-menu-url'), |
87 |
'provisionning-url': service.get('provisionning-url'), |
|
88 |
'saml-sp-metadata-url': service.get('saml-sp-metadata-url'), |
|
87 | 89 |
'title': service.get('title'), |
88 | 90 |
'orig': orig, |
89 | 91 |
'verif_orig': verif_orig, |
hobo/provisionning/middleware.py | ||
---|---|---|
1 |
# hobo - portal to configure and deploy applications |
|
2 |
# Copyright (C) 2015-2020 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import json |
|
18 | ||
19 |
from django.conf import settings |
|
20 |
from django.http import JsonResponse, HttpResponseBadRequest, HttpResponseForbidden |
|
21 |
from django.utils.encoding import force_text |
|
22 |
from django.utils.deprecation import MiddlewareMixin |
|
23 |
from django.utils.six.moves.urllib.parse import urlparse |
|
24 | ||
25 |
from hobo.provisionning.utils import NotificationProcessing, TryAgain |
|
26 |
from hobo.rest_authentication import PublikAuthentication, PublikAuthenticationFailed |
|
27 | ||
28 | ||
29 |
class ProvisionningMiddleware(MiddlewareMixin, NotificationProcessing): |
|
30 |
def process_request(self, request): |
|
31 |
if not (request.method == 'PUT' and request.path == '/__provision__/'): |
|
32 |
return None |
|
33 |
if 'hobo.environment' in settings.INSTALLED_APPS: |
|
34 |
# much ado about hobo |
|
35 |
from hobo.environment.utils import get_local_hobo_dict |
|
36 |
known_services = getattr(settings, 'KNOWN_SERVICES', None) |
|
37 |
local_hobo_dict = get_local_hobo_dict() |
|
38 |
if not known_services: |
|
39 |
# hobo in a single deployment instance |
|
40 |
settings.KNOWN_SERVICES = known_services = {} |
|
41 |
known_services['hobo'] = {'hobo': local_hobo_dict} |
|
42 |
known_services['authentic'] = {'idp': {}} |
|
43 |
if known_services['hobo']['hobo']['provisionning-url'] == local_hobo_dict['provisionning-url']: |
|
44 |
# hobo in a single deployment instance, or primary hobo in a |
|
45 |
# multi-instances environment |
|
46 |
from hobo.environment.models import Authentic |
|
47 |
from hobo.multitenant.settings_loaders import KnownServices |
|
48 |
authentic = Authentic.objects.all().first() |
|
49 |
orig = urlparse(authentic.base_url).netloc.split(':')[0] |
|
50 |
# create stub settings.KNOWN_SERVICES with just enough to get |
|
51 |
# authentication passing. |
|
52 |
idp_service = list(settings.KNOWN_SERVICES['authentic'].values())[0] |
|
53 |
idp_service['verif_orig'] = orig |
|
54 |
idp_service['secret_key'] = KnownServices.shared_secret( |
|
55 |
authentic.secret_key, local_hobo_dict['secret_key']) |
|
56 |
try: |
|
57 |
PublikAuthentication().authenticate(request) |
|
58 |
except PublikAuthenticationFailed: |
|
59 |
return HttpResponseForbidden() |
|
60 |
try: |
|
61 |
notification = json.loads(force_text(request.body)) |
|
62 |
except ValueError: |
|
63 |
return HttpResponseBadRequest() |
|
64 |
if not isinstance(notification, dict) or 'objects' not in notification: |
|
65 |
return HttpResponseBadRequest() |
|
66 | ||
67 |
object_type = notification['objects'].get('@type') |
|
68 |
issuer = notification.get('issuer') |
|
69 |
action = notification.get('@type') |
|
70 |
if not (object_type and issuer and action): |
|
71 |
return HttpResponseBadRequest() |
|
72 |
full = notification['full'] if 'full' in notification else False |
|
73 | ||
74 |
for i in range(20): |
|
75 |
try: |
|
76 |
getattr(self, 'provision_' + object_type)(issuer, action, notification['objects']['data'], full=full) |
|
77 |
except TryAgain: |
|
78 |
continue |
|
79 |
break |
|
80 | ||
81 |
return JsonResponse({'err': 0}) |
hobo/provisionning/utils.py | ||
---|---|---|
1 |
# hobo - portal to configure and deploy applications |
|
2 |
# Copyright (C) 2015-2020 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import random |
|
18 |
import logging |
|
19 | ||
20 |
from django.db.transaction import atomic |
|
21 |
from django.db import IntegrityError |
|
22 | ||
23 |
from hobo.multitenant.utils import provision_user_groups |
|
24 |
from hobo.agent.common.models import Role |
|
25 | ||
26 | ||
27 |
class TryAgain(Exception): |
|
28 |
pass |
|
29 | ||
30 | ||
31 |
class NotificationProcessing: |
|
32 | ||
33 |
@classmethod |
|
34 |
def check_valid_notification(cls, notification): |
|
35 |
return isinstance(notification, dict) \ |
|
36 |
and '@type' in notification \ |
|
37 |
and notification['@type'] in ['provision', 'deprovision'] \ |
|
38 |
and 'objects' in notification \ |
|
39 |
and 'audience' in notification \ |
|
40 |
and isinstance(notification['audience'], list) \ |
|
41 |
and isinstance(notification['objects'], dict) |
|
42 | ||
43 |
@classmethod |
|
44 |
def check_valid_role(cls, o): |
|
45 |
return 'uuid' in o \ |
|
46 |
and 'name' in o \ |
|
47 |
and 'description' in o |
|
48 | ||
49 |
@classmethod |
|
50 |
def check_valid_user(cls, o): |
|
51 |
return 'uuid' in o \ |
|
52 |
and 'is_superuser' in o \ |
|
53 |
and 'email' in o \ |
|
54 |
and 'first_name' in o \ |
|
55 |
and 'last_name' in o \ |
|
56 |
and 'roles' in o |
|
57 | ||
58 |
@classmethod |
|
59 |
def provision_user(cls, issuer, action, data, full=False): |
|
60 |
from django.contrib.auth import get_user_model |
|
61 |
from mellon.models import UserSAMLIdentifier |
|
62 |
User = get_user_model() |
|
63 | ||
64 |
assert not full # provisionning all users is dangerous, we prefer deprovision |
|
65 |
uuids = set() |
|
66 |
for o in data: |
|
67 |
try: |
|
68 |
with atomic(): |
|
69 |
if action == 'provision': |
|
70 |
assert cls.check_valid_user(o) |
|
71 |
try: |
|
72 |
mellon_user = UserSAMLIdentifier.objects.get( |
|
73 |
issuer=issuer, name_id=o['uuid']) |
|
74 |
user = mellon_user.user |
|
75 |
except UserSAMLIdentifier.DoesNotExist: |
|
76 |
try: |
|
77 |
user = User.objects.get(username=o['uuid'][:30]) |
|
78 |
except User.DoesNotExist: |
|
79 |
# temp user object |
|
80 |
random_uid = str(random.randint(1, 10000000000000)) |
|
81 |
user = User.objects.create( |
|
82 |
username=random_uid) |
|
83 |
mellon_user = UserSAMLIdentifier.objects.create( |
|
84 |
user=user, issuer=issuer, name_id=o['uuid']) |
|
85 |
user.first_name = o['first_name'][:30] |
|
86 |
user.last_name = o['last_name'][:30] |
|
87 |
user.email = o['email'][:75] |
|
88 |
user.username = o['uuid'][:30] |
|
89 |
user.is_superuser = o['is_superuser'] |
|
90 |
user.is_staff = o['is_superuser'] |
|
91 |
user.save() |
|
92 |
role_uuids = [role['uuid'] for role in o.get('roles', [])] |
|
93 |
provision_user_groups(user, role_uuids) |
|
94 |
elif action == 'deprovision': |
|
95 |
assert 'uuid' in o |
|
96 |
uuids.add(o['uuid']) |
|
97 |
except IntegrityError: |
|
98 |
raise TryAgain |
|
99 |
if full and action == 'provision': |
|
100 |
for usi in UserSAMLIdentifier.objects.exclude(name_id__in=uuids): |
|
101 |
usi.user.delete() |
|
102 |
elif action == 'deprovision': |
|
103 |
for user in User.objects.filter(saml_identifiers__name_id__in=uuids): |
|
104 |
user.delete() |
|
105 | ||
106 |
@classmethod |
|
107 |
def provision_role(cls, issuer, action, data, full=False): |
|
108 |
logger = logging.getLogger(__name__) |
|
109 |
uuids = set() |
|
110 |
for o in data: |
|
111 |
assert 'uuid' in o |
|
112 |
uuids.add(o['uuid']) |
|
113 |
if action == 'provision': |
|
114 |
assert cls.check_valid_role(o) |
|
115 |
role_name = o['name'] |
|
116 |
if len(role_name) > 70: |
|
117 |
role_name = role_name[:70] + '(...)' |
|
118 |
try: |
|
119 |
role = Role.objects.get(uuid=o['uuid']) |
|
120 |
created = False |
|
121 |
except Role.DoesNotExist: |
|
122 |
try: |
|
123 |
with atomic(): |
|
124 |
role, created = Role.objects.get_or_create( |
|
125 |
name=role_name, defaults={ |
|
126 |
'uuid': o['uuid'], |
|
127 |
'description': o['description']}) |
|
128 |
except IntegrityError: |
|
129 |
# Can happen if uuid and name already exist |
|
130 |
logger.error(u'cannot provision role "%s" (%s)', o['name'], o['uuid']) |
|
131 |
continue |
|
132 |
if not created: |
|
133 |
save = False |
|
134 |
if role.name != role_name: |
|
135 |
role.name = role_name |
|
136 |
save = True |
|
137 |
if role.uuid != o['uuid']: |
|
138 |
role.uuid = o['uuid'] |
|
139 |
save = True |
|
140 |
if role.description != o['description']: |
|
141 |
role.description = o['description'] |
|
142 |
save = True |
|
143 |
if role.details != o.get('details', u''): |
|
144 |
role.details = o.get('details', u'') |
|
145 |
save = True |
|
146 |
if save: |
|
147 |
try: |
|
148 |
with atomic(): |
|
149 |
role.save() |
|
150 |
except IntegrityError: |
|
151 |
# Can happen if uuid and name already exist |
|
152 |
logger.error(u'cannot provision role "%s" (%s)', o['name'], o['uuid']) |
|
153 |
continue |
|
154 |
if full and action == 'provision': |
|
155 |
for role in Role.objects.exclude(uuid__in=uuids): |
|
156 |
role.delete() |
|
157 |
elif action == 'deprovision': |
|
158 |
for role in Role.objects.filter(uuid__in=uuids): |
|
159 |
role.delete() |
tests/settings.py | ||
---|---|---|
2 | 2 |
BROKER_URL = 'memory://' |
3 | 3 |
OZWILLO_SECRET = 'secret' |
4 | 4 | |
5 |
INSTALLED_APPS += ('hobo.contrib.ozwillo',) |
|
5 |
INSTALLED_APPS += ('hobo.contrib.ozwillo', 'hobo.agent.common')
|
|
6 | 6 | |
7 | 7 |
ALLOWED_HOSTS.append('localhost') |
8 | 8 | |
9 | 9 |
TEMPLATES[0]['OPTIONS']['debug'] = True |
10 | 10 | |
11 |
MIDDLEWARE_CLASSES = ('hobo.middleware.RobotsTxtMiddleware', ) + MIDDLEWARE_CLASSES |
|
11 |
MIDDLEWARE_CLASSES = MIDDLEWARE_CLASSES + ( |
|
12 |
'hobo.middleware.RobotsTxtMiddleware', |
|
13 |
'hobo.provisionning.middleware.ProvisionningMiddleware') |
|
12 | 14 | |
13 | 15 |
HOBO_MANAGER_HOMEPAGE_URL_VAR = 'portal_agent_url' |
tests_authentic/test_provisionning.py | ||
---|---|---|
535 | 535 |
assert notify_agents.call_count == 0 |
536 | 536 |
resp = resp.form.submit().follow() |
537 | 537 |
assert notify_agents.call_count == 1 |
538 | ||
539 | ||
540 |
def test_provision_using_http(transactional_db, tenant, settings, caplog): |
|
541 |
with tenant_context(tenant): |
|
542 |
# create providers so notification messages have an audience. |
|
543 |
LibertyProvider.objects.create(ou=None, name='provider', slug='provider', |
|
544 |
entity_id='http://example.org', |
|
545 |
protocol_conformance=lasso.PROTOCOL_SAML_2_0) |
|
546 |
LibertyProvider.objects.create(ou=None, name='provider2', slug='provider2', |
|
547 |
entity_id='http://example.com', |
|
548 |
protocol_conformance=lasso.PROTOCOL_SAML_2_0) |
|
549 |
with patch('hobo.agent.authentic2.provisionning.notify_agents') as notify_agents: |
|
550 |
call_command('createsuperuser', domain=tenant.domain_url, uuid='coin', |
|
551 |
username='coin', email='coin@coin.org', interactive=False) |
|
552 |
assert notify_agents.call_count == 1 |
|
553 |
assert set(notify_agents.call_args[0][0]['audience']) == {'http://example.org', 'http://example.com'} |
|
554 | ||
555 |
settings.HOBO_HTTP_PROVISIONNING = True |
|
556 |
with patch('hobo.agent.authentic2.provisionning.notify_agents') as notify_agents: |
|
557 |
call_command('createsuperuser', domain=tenant.domain_url, uuid='coin2', |
|
558 |
username='coin2', email='coin2@coin.org', interactive=False) |
|
559 |
assert notify_agents.call_count == 1 |
|
560 |
assert set(notify_agents.call_args[0][0]['audience']) == {'http://example.org', 'http://example.com'} |
|
561 | ||
562 |
settings.HOBO_HTTP_PROVISIONNING = True |
|
563 |
settings.KNOWN_SERVICES = { |
|
564 |
'foo': { |
|
565 |
'bar': { |
|
566 |
'saml-sp-metadata-url': 'http://example.org', |
|
567 |
'provisionning-url': 'http://example.org/__provision__/', |
|
568 |
'orig': 'example.org', |
|
569 |
'secret': 'xxx', |
|
570 |
} |
|
571 |
} |
|
572 |
} |
|
573 |
with patch('hobo.agent.authentic2.provisionning.notify_agents') as notify_agents: |
|
574 |
with patch('hobo.agent.authentic2.provisionning.requests.put') as requests_put: |
|
575 |
call_command('createsuperuser', domain=tenant.domain_url, uuid='coin2', |
|
576 |
username='coin2', email='coin2@coin.org', interactive=False) |
|
577 |
assert notify_agents.call_count == 1 |
|
578 |
assert notify_agents.call_args[0][0]['audience'] == ['http://example.com'] |
|
579 |
assert requests_put.call_count == 1 |
|
580 |
# cannot check audience passed to requests.put as it's the same |
|
581 |
# dictionary that is altered afterwards and would thus also contain |
|
582 |
# http://example.com. |
|
583 | ||
584 |
settings.KNOWN_SERVICES['foo']['bar2'] = { |
|
585 |
'saml-sp-metadata-url': 'http://example.com', |
|
586 |
'provisionning-url': 'http://example.com/__provision__/', |
|
587 |
'orig': 'example.com', |
|
588 |
'secret': 'xxx', |
|
589 |
} |
|
590 |
with patch('hobo.agent.authentic2.provisionning.notify_agents') as notify_agents: |
|
591 |
with patch('hobo.agent.authentic2.provisionning.requests.put') as requests_put: |
|
592 |
call_command('createsuperuser', domain=tenant.domain_url, uuid='coin2', |
|
593 |
username='coin2', email='coin2@coin.org', interactive=False) |
|
594 |
assert notify_agents.call_count == 0 |
|
595 |
assert requests_put.call_count == 2 |
tests_multitenant/test_settings.py | ||
---|---|---|
245 | 245 |
assert 'other' in settings.KNOWN_SERVICES['authentic'] |
246 | 246 |
assert (set(['url', 'backoffice-menu-url', 'title', 'orig', |
247 | 247 |
'verif_orig', 'secret', 'template_name', 'variables', |
248 |
'secondary']) |
|
248 |
'saml-sp-metadata-url', 'provisionning-url', 'secondary'])
|
|
249 | 249 |
== set(settings.KNOWN_SERVICES['authentic']['other'].keys())) |
250 | 250 |
assert (settings.KNOWN_SERVICES['authentic']['other']['url'] |
251 | 251 |
== hobo_json['services'][2]['base_url']) |
tests_schemas/example_env.json | ||
---|---|---|
139 | 139 |
{ |
140 | 140 |
"backoffice-menu-url": "https://hobo-instance-name.dev.signalpublik.com/menu.json", |
141 | 141 |
"base_url": "https://hobo-instance-name.dev.signalpublik.com/", |
142 |
"provisionning-url": "https://hobo-instance-name.dev.signalpublik.com/__provision__/", |
|
142 | 143 |
"saml-sp-metadata-url": "https://hobo-instance-name.dev.signalpublik.com/accounts/mellon/metadata/", |
144 |
"secret_key": "XXX", |
|
143 | 145 |
"service-id": "hobo", |
144 | 146 |
"slug": "hobo", |
145 | 147 |
"title": "Hobo" |
... | ... | |
150 | 152 |
"id": 1, |
151 | 153 |
"saml-idp-metadata-url": "https://connexion-instance-name.dev.signalpublik.com/idp/saml2/metadata", |
152 | 154 |
"secondary": false, |
153 |
"secret_key": "k_a)vo)a&8xugbzjl#%^s8vfkm2+#yhz#if4m+xu!qqv=04x9q",
|
|
155 |
"secret_key": "XXX",
|
|
154 | 156 |
"service-id": "authentic", |
155 | 157 |
"service-label": "Authentic", |
156 | 158 |
"slug": "idp", |
157 | 159 |
"template_name": "signal-publik", |
158 | 160 |
"title": "Connexion", |
159 |
"variables": {},
|
|
160 |
"use_as_idp_for_self": true
|
|
161 |
"use_as_idp_for_self": true,
|
|
162 |
"variables": {}
|
|
161 | 163 |
}, |
162 | 164 |
{ |
163 | 165 |
"backoffice-menu-url": "https://demarches-instance-name.dev.signalpublik.com/backoffice/menu.json", |
164 | 166 |
"base_url": "https://demarches-instance-name.dev.signalpublik.com/", |
165 | 167 |
"id": 1, |
168 |
"provisionning-url": "https://demarches-instance-name.dev.signalpublik.com/__provision__/", |
|
166 | 169 |
"saml-sp-metadata-url": "https://demarches-instance-name.dev.signalpublik.com/saml/metadata", |
167 | 170 |
"secondary": false, |
168 |
"secret_key": "uhipz^y38a*w#rrnio_-i=+7p47aq#$+dntm*i@nz(y)n57153",
|
|
171 |
"secret_key": "XXX",
|
|
169 | 172 |
"service-id": "wcs", |
170 | 173 |
"service-label": "w.c.s.", |
171 | 174 |
"slug": "eservices", |
... | ... | |
177 | 180 |
"backoffice-menu-url": "https://passerelle-instance-name.dev.signalpublik.com/manage/menu.json", |
178 | 181 |
"base_url": "https://passerelle-instance-name.dev.signalpublik.com/", |
179 | 182 |
"id": 1, |
183 |
"provisionning-url": "https://passerelle-instance-name.dev.signalpublik.com/__provision__/", |
|
180 | 184 |
"saml-sp-metadata-url": "https://passerelle-instance-name.dev.signalpublik.com/accounts/mellon/metadata/", |
181 | 185 |
"secondary": false, |
182 |
"secret_key": "vz&g(p1bhzw35iltrrl$^6013*+q80l&l4)b)tsr=+ko__js_v",
|
|
186 |
"secret_key": "XXX",
|
|
183 | 187 |
"service-id": "passerelle", |
184 | 188 |
"service-label": "Passerelle", |
185 | 189 |
"slug": "passerelle", |
... | ... | |
191 | 195 |
"backoffice-menu-url": "https://instance-name.dev.signalpublik.com/manage/menu.json", |
192 | 196 |
"base_url": "https://instance-name.dev.signalpublik.com/", |
193 | 197 |
"id": 1, |
198 |
"provisionning-url": "https://instance-name.dev.signalpublik.com/__provision__/", |
|
194 | 199 |
"saml-sp-metadata-url": "https://instance-name.dev.signalpublik.com/accounts/mellon/metadata/", |
195 | 200 |
"secondary": false, |
196 |
"secret_key": "^0!psa-ijq4*va0a4&_)solvils#hig2vtof(%3iy#!6p5!f6e",
|
|
201 |
"secret_key": "XXX",
|
|
197 | 202 |
"service-id": "combo", |
198 | 203 |
"service-label": "Combo", |
199 | 204 |
"slug": "portal", |
... | ... | |
205 | 210 |
"backoffice-menu-url": "https://agents-instance-name.dev.signalpublik.com/manage/menu.json", |
206 | 211 |
"base_url": "https://agents-instance-name.dev.signalpublik.com/", |
207 | 212 |
"id": 2, |
213 |
"provisionning-url": "https://agents-instance-name.dev.signalpublik.com/__provision__/", |
|
208 | 214 |
"saml-sp-metadata-url": "https://agents-instance-name.dev.signalpublik.com/accounts/mellon/metadata/", |
209 | 215 |
"secondary": false, |
210 |
"secret_key": "m1&vql=pm-clw)0wcnk=q4g1-#flrus!dui$gr$7ug2%xw@ko$",
|
|
216 |
"secret_key": "XXX",
|
|
211 | 217 |
"service-id": "combo", |
212 | 218 |
"service-label": "Combo", |
213 | 219 |
"slug": "portal-agent", |
... | ... | |
216 | 222 |
"variables": {} |
217 | 223 |
} |
218 | 224 |
], |
219 |
"timestamp": "1558975192.98",
|
|
225 |
"timestamp": "XXXXXXXXXX.XX",
|
|
220 | 226 |
"users": [], |
221 | 227 |
"variables": { |
222 | 228 |
"css_variant": "publik", |
223 |
- |