28ea19fc6067ca76d2cac371937a66a18e93a36c
[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 ['Owner', 'Default NameID', 'Allowed NameIDs',
196                                  'Attribute Mapping', 'Allowed Attributes']:
197                         if not self.user.is_admin:
198                             raise UnauthorizedUser(
199                                 "Unauthorized to set %s" % key
200                             )
201
202                 # Make changes in current config
203                 for name, option in conf.iteritems():
204                     value = new_db_values.get(name, False)
205                     # A value of None means remove from the data store
206                     if value is False or value == []:
207                         continue
208                     if name == 'Name':
209                         if not self.sp.is_valid_name(value):
210                             raise InvalidValueFormat(
211                                 'Invalid name! Use only numbers and'
212                                 ' letters'
213                             )
214                         self.sp.name = value
215                         self.url = '%s/sp/%s' % (self.parent.url, value)
216                         self.parent.rename_sp(option.get_value(), value)
217                     elif name == 'User Owner':
218                         self.sp.owner = value
219                     elif name == 'Default NameID':
220                         self.sp.default_nameid = value
221                     elif name == 'Allowed NameIDs':
222                         self.sp.allowed_nameids = value
223                     elif name == 'Attribute Mapping':
224                         self.sp.attribute_mappings = value
225                     elif name == 'Allowed Attributes':
226                         self.sp.allowed_attributes = value
227             except InvalidValueFormat, e:
228                 message = str(e)
229                 message_type = ADMIN_STATUS_WARN
230                 return self.root_with_msg(message, message_type)
231             except UnauthorizedUser, e:
232                 message = str(e)
233                 message_type = ADMIN_STATUS_ERROR
234                 return self.root_with_msg(message, message_type)
235             except Exception as e:  # pylint: disable=broad-except
236                 self.debug("Error: %s" % repr(e))
237                 message = "Internal Error"
238                 message_type = ADMIN_STATUS_ERROR
239                 return self.root_with_msg(message, message_type)
240
241             try:
242                 self.sp.save_properties()
243                 message = "Properties successfully changed"
244                 message_type = ADMIN_STATUS_OK
245             except Exception as e:  # pylint: disable=broad-except
246                 self.error('Failed to save data: %s' % e)
247                 message = "Failed to save data!"
248                 message_type = ADMIN_STATUS_ERROR
249             else:
250                 self.sp.refresh_config()
251
252         return self.root_with_msg(message=message,
253                                   message_type=message_type)
254
255     def delete(self):
256         self.parent.del_sp(self.sp.name)
257         self.sp.permanently_delete()
258         return self.parent.root()
259     delete.public_function = True
260
261
262 class Saml2AdminPage(AdminPage):
263     def __init__(self, site, config):
264         super(Saml2AdminPage, self).__init__(site)
265         self.name = 'admin'
266         self.cfg = config
267         self.providers = []
268         self.menu = []
269         self.url = None
270         self.sp = AdminPage(self._site)
271
272     def add_sp(self, name, sp):
273         page = SPAdminPage(sp, self._site, self)
274         self.sp.add_subtree(name, page)
275         self.providers.append(sp)
276         return page
277
278     def rename_sp(self, oldname, newname):
279         page = getattr(self.sp, oldname)
280         self.sp.del_subtree(oldname)
281         self.sp.add_subtree(newname, page)
282
283     def del_sp(self, name):
284         try:
285             page = getattr(self.sp, name)
286             self.providers.remove(page.sp)
287             self.sp.del_subtree(name)
288         except Exception, e:  # pylint: disable=broad-except
289             self.debug("Failed to remove provider %s: %s" % (name, str(e)))
290
291     def add_sps(self):
292         if self.cfg.idp:
293             for p in self.cfg.idp.get_providers():
294                 try:
295                     sp = ServiceProvider(self.cfg, p)
296                     self.del_sp(sp.name)
297                     self.add_sp(sp.name, sp)
298                 except Exception, e:  # pylint: disable=broad-except
299                     self.debug("Failed to find provider %s: %s" % (p, str(e)))
300
301     def mount(self, page):
302         self.menu = page.menu
303         self.url = '%s/%s' % (page.url, self.name)
304         self.add_sps()
305         self.add_subtree('new', NewSPAdminPage(self._site, self))
306         page.add_subtree(self.name, self)
307
308     def root(self, *args, **kwargs):
309         return self._template('admin/providers/saml2.html',
310                               title='SAML2 Administration',
311                               providers=self.providers,
312                               baseurl=self.url,
313                               menu=self.menu)