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