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