1 # Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING
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
18 from urlparse import urlparse
21 class NewSPAdminPage(AdminPage):
23 def __init__(self, site, parent):
24 super(NewSPAdminPage, self).__init__(site, form=True)
26 self.title = 'New Service Provider'
27 self.back = parent.url
28 self.url = '%s/new' % (parent.url,)
30 def form_new(self, message=None, message_type=None):
31 return self._template('admin/providers/saml2_sp_new.html',
34 message_type=message_type,
35 name='saml2_sp_new_form',
36 back=self.back, action=self.url)
38 def GET(self, *args, **kwargs):
39 return self.form_new()
41 def POST(self, *args, **kwargs):
43 if self.user.is_admin:
44 # TODO: allow authenticated user to create SPs on their own
45 # set the owner in that case
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():
64 elif key == 'description':
67 # pylint: disable=unused-variable
68 (scheme, netloc, path, params, query, frag) = urlparse(
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)
77 elif key == 'portalvisible' and value.lower() == 'on':
79 elif key == 'imagefile':
80 if hasattr(value, 'content_type'):
81 imagefile = value.fullvalue()
82 if len(imagefile) == 0:
85 imagefile = base64.b64encode(imagefile)
87 self.debug("Invalid format for 'imagefile'")
88 elif key == 'metatext':
91 elif key == 'metafile':
92 if hasattr(value, 'content_type'):
93 meta = value.fullvalue()
95 self.debug("Invalid format for 'meta'")
96 elif key == 'metaurl':
99 r = requests.get(value)
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)
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:
119 message_type = ADMIN_STATUS_ERROR
120 except Exception, e: # pylint: disable=broad-except
122 message = "Failed to create Service Provider!"
123 message_type = ADMIN_STATUS_ERROR
125 message = "A name and a metadata file must be provided"
126 message_type = ADMIN_STATUS_ERROR
128 message = "Unauthorized"
129 message_type = ADMIN_STATUS_ERROR
131 return self.form_new(message, message_type)
134 class InvalidValueFormat(Exception):
138 class UnauthorizedUser(Exception):
142 class SPAdminPage(AdminPage):
144 def __init__(self, sp, site, parent):
145 super(SPAdminPage, self).__init__(site, form=True)
149 self.url = '%s/sp/%s' % (parent.url, sp.name)
151 self.back = parent.url
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())
160 def GET(self, *args, **kwargs):
161 return self.root_with_msg()
163 def POST(self, *args, **kwargs):
165 message = "Nothing was modified."
166 message_type = "info"
167 new_db_values = dict()
169 conf = self.sp.get_config_obj()
171 for name, option in conf.iteritems():
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()):
179 elif isinstance(option, pconfig.Condition):
182 if isinstance(option, pconfig.Condition):
184 elif isinstance(option, pconfig.Choice):
186 for a in option.get_allowed():
187 aname = '%s_%s' % (name, a)
190 elif isinstance(option, pconfig.MappingList):
191 current = deepcopy(option.get_value())
192 value = get_mapping_list_value(name,
195 # if current value is None do nothing
197 if option.get_value() is None:
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,
205 # if current value is None do nothing
207 if option.get_value() is None:
209 # else pass and let it continue as None
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
218 if len(new_db_values) != 0:
220 # Validate user can make these changes
221 for (key, value) in new_db_values.iteritems():
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
236 # Make changes in current config
237 for name, option in conf.iteritems():
238 if name not in new_db_values:
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'):
246 if not self.sp.is_valid_name(value):
247 raise InvalidValueFormat(
248 'Invalid name! Use only numbers and'
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()
275 self.sp.imagefile = base64.b64encode(blob)
277 raise InvalidValueFormat(
278 'Invalid Image file format'
281 except InvalidValueFormat, e:
283 message_type = ADMIN_STATUS_WARN
284 return self.root_with_msg(message, message_type)
285 except UnauthorizedUser, 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)
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
304 self.sp.refresh_config()
306 return self.root_with_msg(message=message,
307 message_type=message_type)
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
319 class Saml2AdminPage(AdminPage):
320 def __init__(self, site, config):
321 super(Saml2AdminPage, self).__init__(site)
327 self.sp = AdminPage(self._site)
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)
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)
340 def del_sp(self, name):
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)))
350 for p in self.cfg.idp.get_providers():
352 sp = ServiceProvider(self.cfg, p)
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)))
358 def mount(self, page):
359 self.menu = page.menu
360 self.url = '%s/%s' % (page.url, self.name)
362 self.add_subtree('new', NewSPAdminPage(self._site, self))
363 page.add_subtree(self.name, self)
365 def root(self, *args, **kwargs):
366 return self._template('admin/providers/saml2.html',
367 title='SAML2 Administration',
368 providers=self.providers,