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