9d06be1f057b1ae26020214f0a94c8b5fd0dc70e
[cascardo/ipsilon.git] / ipsilon / providers / saml2 / admin.py
1 # Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING
2
3 import cherrypy
4 from ipsilon.util import config as pconfig
5 from ipsilon.admin.common import AdminPage
6 from ipsilon.admin.common import ADMIN_STATUS_OK
7 from ipsilon.admin.common import ADMIN_STATUS_ERROR
8 from ipsilon.admin.common import ADMIN_STATUS_WARN
9 from ipsilon.admin.common import get_mapping_list_value
10 from ipsilon.admin.common import get_complex_list_value
11 from ipsilon.providers.saml2.provider import ServiceProvider
12 from ipsilon.providers.saml2.provider import ServiceProviderCreator
13 from ipsilon.providers.saml2.provider import InvalidProviderId
14 from copy import deepcopy
15 import requests
16 import logging
17 import base64
18 from urlparse import urlparse
19
20
21 class NewSPAdminPage(AdminPage):
22
23     def __init__(self, site, parent):
24         super(NewSPAdminPage, self).__init__(site, form=True)
25         self.parent = parent
26         self.title = 'New Service Provider'
27         self.back = parent.url
28         self.url = '%s/new' % (parent.url,)
29
30     def form_new(self, message=None, message_type=None):
31         return self._template('admin/providers/saml2_sp_new.html',
32                               title=self.title,
33                               message=message,
34                               message_type=message_type,
35                               name='saml2_sp_new_form',
36                               back=self.back, action=self.url)
37
38     def GET(self, *args, **kwargs):
39         return self.form_new()
40
41     def POST(self, *args, **kwargs):
42
43         if self.user.is_admin:
44             # TODO: allow authenticated user to create SPs on their own
45             #       set the owner in that case
46             name = None
47             meta = None
48             description = None
49             splink = None
50             visible = False
51             imagefile = None
52             if 'content-type' not in cherrypy.request.headers:
53                 self.debug("Invalid request, missing content-type")
54                 message = "Malformed request"
55                 message_type = ADMIN_STATUS_ERROR
56                 return self.form_new(message, message_type)
57             ctype = cherrypy.request.headers['content-type'].split(';')[0]
58             if ctype != 'multipart/form-data':
59                 self.debug("Invalid form type (%s), trying to cope" % (
60                            cherrypy.request.content_type,))
61             for key, value in kwargs.iteritems():
62                 if key == 'name':
63                     name = value
64                 elif key == 'description':
65                     description = value
66                 elif key == 'splink':
67                     # pylint: disable=unused-variable
68                     (scheme, netloc, path, params, query, frag) = urlparse(
69                         value
70                     )
71                     # minimum URL validation
72                     if (scheme not in ['http', 'https'] or not netloc):
73                         message = "Invalid URL for Service Provider link"
74                         message_type = ADMIN_STATUS_ERROR
75                         return self.form_new(message, message_type)
76                     splink = value
77                 elif key == 'portalvisible' and value.lower() == 'on':
78                     visible = True
79                 elif key == 'imagefile':
80                     if hasattr(value, 'content_type'):
81                         imagefile = value.fullvalue()
82                         if len(imagefile) == 0:
83                             imagefile = None
84                         else:
85                             imagefile = base64.b64encode(imagefile)
86                     else:
87                         self.debug("Invalid format for 'imagefile'")
88                 elif key == 'metatext':
89                     if len(value) > 0:
90                         meta = value
91                 elif key == 'metafile':
92                     if hasattr(value, 'content_type'):
93                         meta = value.fullvalue()
94                     else:
95                         self.debug("Invalid format for 'meta'")
96                 elif key == 'metaurl':
97                     if len(value) > 0:
98                         try:
99                             r = requests.get(value)
100                             r.raise_for_status()
101                             meta = r.content
102                         except Exception, e:  # pylint: disable=broad-except
103                             self.debug("Failed to fetch metadata: " + repr(e))
104                             message = "Failed to fetch metadata: " + repr(e)
105                             message_type = ADMIN_STATUS_ERROR
106                             return self.form_new(message, message_type)
107
108             if name and meta:
109                 try:
110                     spc = ServiceProviderCreator(self.parent.cfg)
111                     sp = spc.create_from_buffer(name, meta, description,
112                                                 visible, imagefile, splink)
113                     sp_page = self.parent.add_sp(name, sp)
114                     message = "SP Successfully added"
115                     message_type = ADMIN_STATUS_OK
116                     return sp_page.root_with_msg(message, message_type)
117                 except InvalidProviderId, e:
118                     message = str(e)
119                     message_type = ADMIN_STATUS_ERROR
120                 except Exception, e:  # pylint: disable=broad-except
121                     self.debug(repr(e))
122                     message = "Failed to create Service Provider!"
123                     message_type = ADMIN_STATUS_ERROR
124             else:
125                 message = "A name and a metadata file must be provided"
126                 message_type = ADMIN_STATUS_ERROR
127         else:
128             message = "Unauthorized"
129             message_type = ADMIN_STATUS_ERROR
130
131         return self.form_new(message, message_type)
132
133
134 class InvalidValueFormat(Exception):
135     pass
136
137
138 class UnauthorizedUser(Exception):
139     pass
140
141
142 class SPAdminPage(AdminPage):
143
144     def __init__(self, sp, site, parent):
145         super(SPAdminPage, self).__init__(site, form=True)
146         self.parent = parent
147         self.sp = sp
148         self.title = sp.name
149         self.url = '%s/sp/%s' % (parent.url, sp.name)
150         self.menu = [parent]
151         self.back = parent.url
152
153     def root_with_msg(self, message=None, message_type=None):
154         return self._template('admin/option_config.html', title=self.title,
155                               menu=self.menu, action=self.url, back=self.back,
156                               message=message, message_type=message_type,
157                               name='saml2_sp_%s_form' % (self.sp.name),
158                               config=self.sp.get_config_obj())
159
160     def GET(self, *args, **kwargs):
161         return self.root_with_msg()
162
163     def POST(self, *args, **kwargs):
164
165         message = "Nothing was modified."
166         message_type = "info"
167         new_db_values = dict()
168
169         conf = self.sp.get_config_obj()
170
171         for name, option in conf.iteritems():
172             if name in kwargs:
173                 value = kwargs[name]
174                 if isinstance(option, pconfig.List):
175                     value = [x.strip() for x in value.split('\n')]
176                     # for normal lists we want unordered comparison
177                     if set(value) == set(option.get_value()):
178                         continue
179                 elif isinstance(option, pconfig.Condition):
180                     value = True
181             else:
182                 if isinstance(option, pconfig.Condition):
183                     value = False
184                 elif isinstance(option, pconfig.Choice):
185                     value = list()
186                     for a in option.get_allowed():
187                         aname = '%s_%s' % (name, a)
188                         if aname in kwargs:
189                             value.append(a)
190                 elif isinstance(option, pconfig.MappingList):
191                     current = deepcopy(option.get_value())
192                     value = get_mapping_list_value(name,
193                                                    current,
194                                                    **kwargs)
195                     # if current value is None do nothing
196                     if value is None:
197                         if option.get_value() is None:
198                             continue
199                         # else pass and let it continue as None
200                 elif isinstance(option, pconfig.ComplexList):
201                     current = deepcopy(option.get_value())
202                     value = get_complex_list_value(name,
203                                                    current,
204                                                    **kwargs)
205                     # if current value is None do nothing
206                     if value is None:
207                         if option.get_value() is None:
208                             continue
209                         # else pass and let it continue as None
210                 else:
211                     continue
212
213             if value != option.get_value():
214                 cherrypy.log.error("Storing %s = %s" %
215                                    (name, value), severity=logging.DEBUG)
216                 new_db_values[name] = value
217
218         if len(new_db_values) != 0:
219             try:
220                 # Validate user can make these changes
221                 for (key, value) in new_db_values.iteritems():
222                     if key == 'Name':
223                         if (not self.user.is_admin and
224                                 self.user.name != self.sp.owner):
225                             raise UnauthorizedUser("Unauthorized to set owner")
226                     elif key in ['User Owner', 'Default NameID',
227                                  'Allowed NameIDs', 'Attribute Mapping',
228                                  'Allowed Attributes', 'Description',
229                                  'Service Provider link',
230                                  'Visible in Portal', 'Image File']:
231                         if not self.user.is_admin:
232                             raise UnauthorizedUser(
233                                 "Unauthorized to set %s" % key
234                             )
235
236                 # Make changes in current config
237                 for name, option in conf.iteritems():
238                     if name not in new_db_values:
239                         continue
240                     value = new_db_values.get(name, False)
241                     # A value of None means remove from the data store
242                     if ((value is False or value == []) and
243                             name != 'Visible in Portal'):
244                         continue
245                     if name == 'Name':
246                         if not self.sp.is_valid_name(value):
247                             raise InvalidValueFormat(
248                                 'Invalid name! Use only numbers and'
249                                 ' letters'
250                             )
251                         self.sp.name = value
252                         self.url = '%s/sp/%s' % (self.parent.url, value)
253                         self.parent.rename_sp(option.get_value(), value)
254                     elif name == 'User Owner':
255                         self.sp.owner = value
256                     elif name == 'Description':
257                         self.sp.description = value
258                     elif name == 'Visible in Portal':
259                         self.sp.visible = value
260                     elif name == 'Service Provider link':
261                         self.sp.splink = value
262                     elif name == 'Default NameID':
263                         self.sp.default_nameid = value
264                     elif name == 'Allowed NameIDs':
265                         self.sp.allowed_nameids = value
266                     elif name == 'Attribute Mapping':
267                         self.sp.attribute_mappings = value
268                     elif name == 'Allowed Attributes':
269                         self.sp.allowed_attributes = value
270                     elif name == 'Image File':
271                         if hasattr(value, 'content_type'):
272                             # pylint: disable=maybe-no-member
273                             blob = value.fullvalue()
274                             if len(blob) > 0:
275                                 self.sp.imagefile = base64.b64encode(blob)
276                         else:
277                             raise InvalidValueFormat(
278                                 'Invalid Image file format'
279                             )
280
281             except InvalidValueFormat, e:
282                 message = str(e)
283                 message_type = ADMIN_STATUS_WARN
284                 return self.root_with_msg(message, message_type)
285             except UnauthorizedUser, e:
286                 message = str(e)
287                 message_type = ADMIN_STATUS_ERROR
288                 return self.root_with_msg(message, message_type)
289             except Exception as e:  # pylint: disable=broad-except
290                 self.debug("Error: %s" % repr(e))
291                 message = "Internal Error"
292                 message_type = ADMIN_STATUS_ERROR
293                 return self.root_with_msg(message, message_type)
294
295             try:
296                 self.sp.save_properties()
297                 message = "Properties successfully changed"
298                 message_type = ADMIN_STATUS_OK
299             except Exception as e:  # pylint: disable=broad-except
300                 self.error('Failed to save data: %s' % e)
301                 message = "Failed to save data!"
302                 message_type = ADMIN_STATUS_ERROR
303             else:
304                 self.sp.refresh_config()
305
306         return self.root_with_msg(message=message,
307                                   message_type=message_type)
308
309     def delete(self):
310         self.parent.del_sp(self.sp.name)
311         self.sp.permanently_delete()
312         return self.parent.root()
313     delete.public_function = True
314
315
316 class Saml2AdminPage(AdminPage):
317     def __init__(self, site, config):
318         super(Saml2AdminPage, self).__init__(site)
319         self.name = 'admin'
320         self.cfg = config
321         self.providers = []
322         self.menu = []
323         self.url = None
324         self.sp = AdminPage(self._site)
325
326     def add_sp(self, name, sp):
327         page = SPAdminPage(sp, self._site, self)
328         self.sp.add_subtree(name, page)
329         self.providers.append(sp)
330         return page
331
332     def rename_sp(self, oldname, newname):
333         page = getattr(self.sp, oldname)
334         self.sp.del_subtree(oldname)
335         self.sp.add_subtree(newname, page)
336
337     def del_sp(self, name):
338         try:
339             page = getattr(self.sp, name)
340             self.providers.remove(page.sp)
341             self.sp.del_subtree(name)
342         except Exception, e:  # pylint: disable=broad-except
343             self.debug("Failed to remove provider %s: %s" % (name, str(e)))
344
345     def add_sps(self):
346         if self.cfg.idp:
347             for p in self.cfg.idp.get_providers():
348                 try:
349                     sp = ServiceProvider(self.cfg, p)
350                     self.del_sp(sp.name)
351                     self.add_sp(sp.name, sp)
352                 except Exception, e:  # pylint: disable=broad-except
353                     self.debug("Failed to find provider %s: %s" % (p, str(e)))
354
355     def mount(self, page):
356         self.menu = page.menu
357         self.url = '%s/%s' % (page.url, self.name)
358         self.add_sps()
359         self.add_subtree('new', NewSPAdminPage(self._site, self))
360         page.add_subtree(self.name, self)
361
362     def root(self, *args, **kwargs):
363         return self._template('admin/providers/saml2.html',
364                               title='SAML2 Administration',
365                               providers=self.providers,
366                               baseurl=self.url,
367                               menu=self.menu)