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