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