1 |
|
# hobo - portal to configure and deploy applications
|
2 |
|
# Copyright (C) 2015 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 os
|
18 |
|
import logging
|
19 |
|
import requests
|
20 |
|
import urllib
|
21 |
|
import urlparse
|
22 |
|
import hashlib
|
23 |
|
import json
|
24 |
|
|
25 |
|
from optparse import make_option
|
26 |
|
|
27 |
|
from django.utils.text import slugify
|
28 |
|
from django.core.management.base import BaseCommand
|
29 |
|
from django.utils.translation import ugettext as _
|
30 |
|
from django.conf import settings
|
31 |
|
|
32 |
|
from authentic2.saml.models import LibertyProvider
|
33 |
|
from authentic2.a2_rbac.models import Role, RoleAttribute
|
34 |
|
from authentic2 import app_settings
|
35 |
|
|
36 |
|
|
37 |
|
from hobo import signature
|
38 |
|
from hobo.multitenant.middleware import TenantMiddleware
|
39 |
|
|
40 |
|
from tenant_schemas.utils import tenant_context
|
41 |
|
|
42 |
|
|
43 |
|
class WcsRoleImporter(object):
|
44 |
|
def __init__(self, liberty_provider, key, orig,
|
45 |
|
attribute_name='role-slug', delete=False):
|
46 |
|
self.service = liberty_provider
|
47 |
|
self.slug = liberty_provider.slug
|
48 |
|
self.key = key
|
49 |
|
self.orig = orig
|
50 |
|
self.attribute_name = attribute_name
|
51 |
|
self.delete = delete
|
52 |
|
assert 'saml/metadata' in self.service.entity_id
|
53 |
|
self.wcs_url = self.service.entity_id.split('saml/metadata')[0]
|
54 |
|
self.logger = logging.getLogger('%s.%s' % (__name__,
|
55 |
|
self.__class__.__name__))
|
56 |
|
self.seen_ids = set()
|
57 |
|
|
58 |
|
def import_roles(self):
|
59 |
|
for role_tpl in self.get_roles():
|
60 |
|
self.seen_ids.add(role_tpl.external_id)
|
61 |
|
self.create_role(role_tpl)
|
62 |
|
if self.delete:
|
63 |
|
self.delete_dead_roles()
|
64 |
|
su_role, created = Role.objects.get_or_create(
|
65 |
|
service=self.service, slug='_a2-hobo-superuser',
|
66 |
|
defaults={'name': _('Superuser')})
|
67 |
|
su_role.attributes.get_or_create(name='is_superuser', kind='string',
|
68 |
|
value='true')
|
69 |
|
|
70 |
|
def create_role(self, role_tpl):
|
71 |
|
defaults = {
|
72 |
|
'name': role_tpl.name,
|
73 |
|
# w.c.s. will always provide a slug but for other services we do
|
74 |
|
# not know
|
75 |
|
'slug': role_tpl.slug or slugify(role_tpl.name),
|
76 |
|
}
|
77 |
|
# search role by external id, create if not found
|
78 |
|
role, created = Role.objects.get_or_create(
|
79 |
|
ou=self.service.ou,
|
80 |
|
external_id=role_tpl.external_id,
|
81 |
|
defaults=defaults)
|
82 |
|
RoleAttribute.objects.filter(role=role).delete()
|
83 |
|
role_attribute, ra_created = RoleAttribute.objects.get_or_create(
|
84 |
|
role=role,
|
85 |
|
name=self.attribute_name,
|
86 |
|
kind='string',
|
87 |
|
defaults={
|
88 |
|
'value': role_tpl.external_id
|
89 |
|
})
|
90 |
|
if created:
|
91 |
|
self.logger.info('imported new role %r(%r) from service %s',
|
92 |
|
role.external_id, role.name, self.slug)
|
93 |
|
# update role attribute value if it has changed
|
94 |
|
if not ra_created:
|
95 |
|
if role_attribute.value != role_tpl.external_id:
|
96 |
|
role_attribute.value = role_tpl.external_id
|
97 |
|
role_attribute.save()
|
98 |
|
# update role name if has changed
|
99 |
|
if not created:
|
100 |
|
# Update name and slug if they have changed
|
101 |
|
if role.name != role_tpl.name:
|
102 |
|
role.name = role_tpl.name
|
103 |
|
role.save()
|
104 |
|
# update emails, emails_to_members and details in RA
|
105 |
|
if role_tpl.emails:
|
106 |
|
ra, created = RoleAttribute.objects.get_or_create(
|
107 |
|
role=role, name='emails', kind='json',
|
108 |
|
defaults={'value': json.dumps(role_tpl.emails)})
|
109 |
|
if ra.value != json.dumps(role_tpl.emails):
|
110 |
|
ra.value = json.dumps(role_tpl.emails)
|
111 |
|
ra.save()
|
112 |
|
ra, created = RoleAttribute.objects.get_or_create(
|
113 |
|
role=role, name='emails_to_members', kind='json',
|
114 |
|
defaults={'value': json.dumps(role_tpl.emails_to_members)})
|
115 |
|
if ra.value != json.dumps(role_tpl.emails_to_members):
|
116 |
|
ra.value = json.dumps(role_tpl.emails_to_members)
|
117 |
|
ra.save()
|
118 |
|
if role_tpl.details:
|
119 |
|
value = json.dumps(role_tpl.details)
|
120 |
|
ra, created = RoleAttribute.objects.get_or_create(
|
121 |
|
role=role, name='details', kind='json',
|
122 |
|
defaults={'value': value})
|
123 |
|
if ra.value != value:
|
124 |
|
ra.value = value
|
125 |
|
ra.save()
|
126 |
|
|
127 |
|
def delete_dead_roles(self):
|
128 |
|
'''Deletes service roles whose id is not in self.seen_ids'''
|
129 |
|
qs = Role.objects.filter(service=self.service) \
|
130 |
|
.exclude(external_id__in=list(self.seen_ids))
|
131 |
|
for role in qs:
|
132 |
|
self.logger.info('deleted dead role %r(%r) from service %s',
|
133 |
|
role.external_id, role.slug, self.slug)
|
134 |
|
qs.delete()
|
135 |
|
|
136 |
|
def get_roles(self):
|
137 |
|
'''Get w.c.s. from its roles web-service by sending a signed GET
|
138 |
|
request.
|
139 |
|
'''
|
140 |
|
url = self.wcs_url + 'api/roles?%s' % urllib.urlencode(
|
141 |
|
{'format': 'json', 'orig': self.orig})
|
142 |
|
signed_url = signature.sign_url(url, self.key)
|
143 |
|
response = requests.get(signed_url, verify=app_settings.A2_VERIFY_SSL)
|
144 |
|
if response.status_code == 200:
|
145 |
|
for role in response.json()['data']:
|
146 |
|
new_role = Role(name=role['text'], external_id=str(role['slug']),
|
147 |
|
slug=str(role['slug']))
|
148 |
|
new_role.description = role.get('details', u'')
|
149 |
|
new_role.emails = role.get('emails', [])
|
150 |
|
new_role.emails_to_members = role.get('emails_to_members', False)
|
151 |
|
new_role.details = role.get('details', '')
|
152 |
|
yield new_role
|
153 |
|
else:
|
154 |
|
self.logger.warn('failed to get roles for %s (response: %s)',
|
155 |
|
self.wcs_url, response.status_code)
|
156 |
|
|
157 |
|
|
158 |
|
class Command(BaseCommand):
|
159 |
|
help = "Import W.C.S. roles"
|
160 |
|
|
161 |
|
requires_system_checks = False
|
162 |
|
|
163 |
|
def add_arguments(self, parser):
|
164 |
|
parser.add_argument('--delete', action="store_true", dest='delete')
|
165 |
|
|
166 |
|
def handle(self, *args, **options):
|
167 |
|
# traverse list of tenants
|
168 |
|
for tenant in TenantMiddleware.get_tenants():
|
169 |
|
with tenant_context(tenant):
|
170 |
|
if getattr(settings, 'HOBO_ROLE_EXPORT', True):
|
171 |
|
continue
|
172 |
|
self.handle_tenant(tenant, **options)
|
173 |
|
|
174 |
|
def handle_tenant(self, tenant, **options):
|
175 |
|
# extract informations on deployed w.c.s. instances from hobo.json
|
176 |
|
hobo_json_path = os.path.join(tenant.get_directory(), 'hobo.json')
|
177 |
|
if not os.path.exists(hobo_json_path):
|
178 |
|
print 'skipping %s, no hobo.json found' % tenant
|
179 |
|
return
|
180 |
|
hobo_environment = json.load(open(hobo_json_path))
|
181 |
|
# compute our credentials from our hobo configuration
|
182 |
|
me = [x for x in hobo_environment['services'] if x.get('this')]
|
183 |
|
if not me:
|
184 |
|
print 'skipping %s, self services is not marked' % tenant
|
185 |
|
return
|
186 |
|
me = me[0]
|
187 |
|
orig = urlparse.urlsplit(me['base_url']).netloc.split(':')[0]
|
188 |
|
for service_id in settings.KNOWN_SERVICES:
|
189 |
|
if service_id != 'wcs':
|
190 |
|
continue
|
191 |
|
for service in settings.KNOWN_SERVICES[service_id].values():
|
192 |
|
if not service.get('secret'):
|
193 |
|
continue
|
194 |
|
liberty_provider = LibertyProvider.objects.get(
|
195 |
|
entity_id=service['url'] + 'saml/metadata')
|
196 |
|
importer = WcsRoleImporter(
|
197 |
|
liberty_provider=liberty_provider,
|
198 |
|
key=service['secret'],
|
199 |
|
orig=orig,
|
200 |
|
delete=options.get('delete', False),
|
201 |
|
)
|
202 |
|
importer.import_roles()
|
203 |
|
-
|