Add check for permissions on deleting a SAML2 Service Provider
[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         if (not self.user.is_admin and
258                 self.user.name != self.sp.owner):
259             raise cherrypy.HTTPError(403)
260         self.parent.del_sp(self.sp.name)
261         self.sp.permanently_delete()
262         return self.parent.root()
263     delete.public_function = True
264
265
266 class Saml2AdminPage(AdminPage):
267     def __init__(self, site, config):
268         super(Saml2AdminPage, self).__init__(site)
269         self.name = 'admin'
270         self.cfg = config
271         self.providers = []
272         self.menu = []
273         self.url = None
274         self.sp = AdminPage(self._site)
275
276     def add_sp(self, name, sp):
277         page = SPAdminPage(sp, self._site, self)
278         self.sp.add_subtree(name, page)
279         self.providers.append(sp)
280         return page
281
282     def rename_sp(self, oldname, newname):
283         page = getattr(self.sp, oldname)
284         self.sp.del_subtree(oldname)
285         self.sp.add_subtree(newname, page)
286
287     def del_sp(self, name):
288         try:
289             page = getattr(self.sp, name)
290             self.providers.remove(page.sp)
291             self.sp.del_subtree(name)
292         except Exception, e:  # pylint: disable=broad-except
293             self.debug("Failed to remove provider %s: %s" % (name, str(e)))
294
295     def add_sps(self):
296         if self.cfg.idp:
297             for p in self.cfg.idp.get_providers():
298                 try:
299                     sp = ServiceProvider(self.cfg, p)
300                     self.del_sp(sp.name)
301                     self.add_sp(sp.name, sp)
302                 except Exception, e:  # pylint: disable=broad-except
303                     self.debug("Failed to find provider %s: %s" % (p, str(e)))
304
305     def mount(self, page):
306         self.menu = page.menu
307         self.url = '%s/%s' % (page.url, self.name)
308         self.add_sps()
309         self.add_subtree('new', NewSPAdminPage(self._site, self))
310         page.add_subtree(self.name, self)
311
312     def root(self, *args, **kwargs):
313         return self._template('admin/providers/saml2.html',
314                               title='SAML2 Administration',
315                               providers=self.providers,
316                               baseurl=self.url,
317                               menu=self.menu)