Use indirection to report error strings
[cascardo/ipsilon.git] / ipsilon / providers / saml2 / admin.py
1 #!/usr/bin/python
2 #
3 # Copyright (C) 2014  Simo Sorce <simo@redhat.com>
4 #
5 # see file 'COPYING' for use and warranty information
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20 import cherrypy
21 from ipsilon.admin.common import AdminPage
22 from ipsilon.admin.common import ADMIN_STATUS_OK
23 from ipsilon.admin.common import ADMIN_STATUS_ERROR
24 from ipsilon.admin.common import ADMIN_STATUS_WARN
25 from ipsilon.providers.saml2.provider import ServiceProvider
26 from ipsilon.providers.saml2.provider import ServiceProviderCreator
27 from ipsilon.providers.saml2.provider import InvalidProviderId
28 import re
29 import requests
30
31
32 VALID_IN_NAME = r'[^\ a-zA-Z0-9]'
33
34
35 class NewSPAdminPage(AdminPage):
36
37     def __init__(self, site, parent):
38         super(NewSPAdminPage, self).__init__(site, form=True)
39         self.parent = parent
40         self.title = 'New Service Provider'
41         self.backurl = parent.url
42         self.url = '%s/new' % (parent.url,)
43
44     def form_new(self, message=None, message_type=None):
45         return self._template('admin/providers/saml2_sp_new.html',
46                               title=self.title,
47                               message=message,
48                               message_type=message_type,
49                               name='saml2_sp_new_form',
50                               backurl=self.backurl, action=self.url)
51
52     def GET(self, *args, **kwargs):
53         return self.form_new()
54
55     def POST(self, *args, **kwargs):
56
57         if self.user.is_admin:
58             # TODO: allow authenticated user to create SPs on their own
59             #       set the owner in that case
60             name = None
61             meta = None
62             if 'content-type' not in cherrypy.request.headers:
63                 self._debug("Invalid request, missing content-type")
64                 message = "Malformed request"
65                 message_type = ADMIN_STATUS_ERROR
66                 return self.form_new(message, message_type)
67             ctype = cherrypy.request.headers['content-type'].split(';')[0]
68             if ctype != 'multipart/form-data':
69                 self._debug("Invalid form type (%s), trying to cope" % (
70                             cherrypy.request.content_type,))
71             for key, value in kwargs.iteritems():
72                 if key == 'name':
73                     if re.search(VALID_IN_NAME, value):
74                         message = "Invalid name!" \
75                                   " Use only numbers and letters"
76                         message_type = ADMIN_STATUS_ERROR
77                         return self.form_new(message, message_type)
78
79                     name = value
80                 elif key == 'metatext':
81                     if len(value) > 0:
82                         meta = value
83                 elif key == 'metafile':
84                     if hasattr(value, 'content_type'):
85                         meta = value.fullvalue()
86                     else:
87                         self._debug("Invalid format for 'meta'")
88                 elif key == 'metaurl':
89                     if len(value) > 0:
90                         try:
91                             r = requests.get(value)
92                             r.raise_for_status()
93                             meta = r.content
94                         except Exception, e:  # pylint: disable=broad-except
95                             self._debug("Failed to fetch metadata: " + repr(e))
96                             message = "Failed to fetch metadata: " + repr(e)
97                             message_type = ADMIN_STATUS_ERROR
98                             return self.form_new(message, message_type)
99
100             if name and meta:
101                 try:
102                     spc = ServiceProviderCreator(self.parent.cfg)
103                     sp = spc.create_from_buffer(name, meta)
104                     sp_page = self.parent.add_sp(name, sp)
105                     message = "SP Successfully added"
106                     message_type = ADMIN_STATUS_OK
107                     return sp_page.form_standard(message, message_type)
108                 except InvalidProviderId, e:
109                     message = str(e)
110                     message_type = ADMIN_STATUS_ERROR
111                 except Exception, e:  # pylint: disable=broad-except
112                     self._debug(repr(e))
113                     message = "Failed to create Service Provider!"
114                     message_type = ADMIN_STATUS_ERROR
115             else:
116                 message = "A name and a metadata file must be provided"
117                 message_type = ADMIN_STATUS_ERROR
118         else:
119             message = "Unauthorized"
120             message_type = ADMIN_STATUS_ERROR
121
122         return self.form_new(message, message_type)
123
124
125 class InvalidValueFormat(Exception):
126     pass
127
128
129 class UnauthorizedUser(Exception):
130     pass
131
132
133 class SPAdminPage(AdminPage):
134
135     def __init__(self, sp, site, parent):
136         super(SPAdminPage, self).__init__(site, form=True)
137         self.parent = parent
138         self.sp = sp
139         self.title = sp.name
140         self.backurl = parent.url
141         self.url = '%s/sp/%s' % (parent.url, sp.name)
142
143     def form_standard(self, message=None, message_type=None, newurl=None):
144         return self._template('admin/providers/saml2_sp.html',
145                               message=message,
146                               message_type=message_type,
147                               title=self.title,
148                               name='saml2_sp_%s_form' % self.sp.name,
149                               backurl=self.backurl, action=self.url,
150                               data=self.sp, newurl=newurl)
151
152     def GET(self, *args, **kwargs):
153         return self.form_standard()
154
155     def change_name(self, key, value):
156
157         if value == self.sp.name:
158             return False
159
160         if self.user.is_admin or self.user.name == self.sp.owner:
161             if re.search(VALID_IN_NAME, value):
162                 err = "Invalid name! Use only numbers and letters"
163                 raise InvalidValueFormat(err)
164
165             self._debug("Replacing %s: %s -> %s" % (key, self.sp.name, value))
166             return {'name': value, 'rename': [self.sp.name, value]}
167         else:
168             raise UnauthorizedUser("Unauthorized to rename Service Provider")
169
170     def change_owner(self, key, value):
171         if value == self.sp.owner:
172             return False
173
174         if self.user.is_admin:
175             self._debug("Replacing %s: %s -> %s" % (key, self.sp.owner, value))
176             return {'owner': value}
177         else:
178             raise UnauthorizedUser("Unauthorized to set owner value")
179
180     def change_default_nameid(self, key, value):
181         if value == self.sp.default_nameid:
182             return False
183
184         if self.user.is_admin:
185             self._debug("Replacing %s: %s -> %s" % (key,
186                                                     self.sp.default_nameid,
187                                                     value))
188             if not self.sp.is_valid_nameid(value):
189                 raise InvalidValueFormat('Invalid default nameid value')
190             return {'default_nameid': value}
191         else:
192             raise UnauthorizedUser("Unauthorized to set default nameid value")
193
194     def change_allowed_nameids(self, key, value):
195         v = set([x.strip() for x in value.split(',')])
196         if v == set(self.sp.allowed_nameids):
197             return False
198
199         if self.user.is_admin:
200             self._debug("Replacing %s: %s -> %s" % (key,
201                                                     self.sp.allowed_nameids,
202                                                     list(v)))
203             for x in v:
204                 if not self.sp.is_valid_nameid(x):
205                     l = ', '.join(self.sp.valid_nameids())
206                     err = 'Invalid nameid [%s]. Available [%s].' % (x, l)
207                     raise InvalidValueFormat(err)
208             return {'allowed_nameids': list(v)}
209         else:
210             raise UnauthorizedUser("Unauthorized to set alowed nameids values")
211
212     def POST(self, *args, **kwargs):
213
214         message = "Nothing was modified."
215         message_type = "info"
216         results = dict()
217
218         try:
219             for key, value in kwargs.iteritems():
220                 if key == 'name':
221                     r = self.change_name(key, value)
222                     if r:
223                         results.update(r)
224                 elif key == 'owner':
225                     r = self.change_owner(key, value)
226                     if r:
227                         results.update(r)
228
229                 elif key == 'default_nameid':
230                     r = self.change_default_nameid(key, value)
231                     if r:
232                         results.update(r)
233
234                 elif key == 'allowed_nameids':
235                     r = self.change_allowed_nameids(key, value)
236                     if r:
237                         results.update(r)
238
239         except InvalidValueFormat, e:
240             message = str(e)
241             message_type = ADMIN_STATUS_WARN
242             return self.form_standard(message, message_type)
243         except UnauthorizedUser, e:
244             message = str(e)
245             message_type = ADMIN_STATUS_ERROR
246             return self.form_standard(message, message_type)
247         except Exception, e:  # pylint: disable=broad-except
248             self._debug("Error: %s" % repr(e))
249             message = "Internal Error"
250             message_type = ADMIN_STATUS_ERROR
251             return self.form_standard(message, message_type)
252
253         if len(results) > 0:
254             try:
255                 if 'name' in results:
256                     self.sp.name = results['name']
257                 if 'owner' in results:
258                     self.sp.owner = results['owner']
259                 if 'default_nameid' in results:
260                     self.sp.default_nameid = results['default_nameid']
261                 if 'allowed_nameids' in results:
262                     self.sp.allowed_nameids = results['allowed_nameids']
263                 self.sp.save_properties()
264                 if 'rename' in results:
265                     rename = results['rename']
266                     self.url = '%s/sp/%s' % (self.parent.url, rename[1])
267                     self.parent.rename_sp(rename[0], rename[1])
268                 message = "Properties successfully changed"
269                 message_type = ADMIN_STATUS_OK
270             except Exception:  # pylint: disable=broad-except
271                 message = "Failed to save data!"
272                 message_type = ADMIN_STATUS_ERROR
273
274         return self.form_standard(message, message_type, self.url)
275
276     def delete(self):
277         self.parent.del_sp(self.sp.name)
278         self.sp.permanently_delete()
279         return self.parent.root()
280     delete.public_function = True
281
282
283 class Saml2AdminPage(AdminPage):
284     def __init__(self, site, config):
285         super(Saml2AdminPage, self).__init__(site)
286         self.name = 'admin'
287         self.cfg = config
288         self.providers = []
289         self.menu = []
290         self.url = None
291         self.sp = AdminPage(self._site)
292
293     def add_sp(self, name, sp):
294         page = SPAdminPage(sp, self._site, self)
295         self.sp.add_subtree(name, page)
296         self.providers.append(sp)
297         return page
298
299     def rename_sp(self, oldname, newname):
300         page = getattr(self.sp, oldname)
301         self.sp.del_subtree(oldname)
302         self.sp.add_subtree(newname, page)
303
304     def del_sp(self, name):
305         try:
306             page = getattr(self.sp, name)
307             self.providers.remove(page.sp)
308             self.sp.del_subtree(name)
309         except Exception, e:  # pylint: disable=broad-except
310             self._debug("Failed to remove provider %s: %s" % (name, str(e)))
311
312     def add_sps(self):
313         if self.cfg.idp:
314             for p in self.cfg.idp.get_providers():
315                 try:
316                     sp = ServiceProvider(self.cfg, p)
317                     self.del_sp(sp.name)
318                     self.add_sp(sp.name, sp)
319                 except Exception, e:  # pylint: disable=broad-except
320                     self._debug("Failed to find provider %s: %s" % (p, str(e)))
321
322     def mount(self, page):
323         self.menu = page.menu
324         self.url = '%s/%s' % (page.url, self.name)
325         self.add_sps()
326         self.add_subtree('new', NewSPAdminPage(self._site, self))
327         page.add_subtree(self.name, self)
328
329     def root(self, *args, **kwargs):
330         return self._template('admin/providers/saml2.html',
331                               title='SAML2 Administration',
332                               providers=self.providers,
333                               baseurl=self.url,
334                               menu=self.menu)