import shutil
import socket
import sys
+import base64
HTTPDCONFD = '/etc/httpd/conf.d'
path = None
if not args['saml_no_httpd']:
path = os.path.join(SAML2_HTTPDIR, args['hostname'])
+ if os.path.exists(path):
+ raise Exception('Service Provider is already configured')
os.makedirs(path, 0750)
else:
path = os.getcwd()
"Error: [%s]" % e)
raise
+ sp_image = None
+ if args['saml_sp_image']:
+ try:
+ # FIXME: limit size
+ with open(args['saml_sp_image']) as f:
+ sp_image = f.read()
+ sp_image = base64.b64encode(sp_image)
+ except Exception as e: # pylint: disable=broad-except
+ logger.error("Failed to read SP Image file!\n" +
+ "Error: [%s]" % e)
+
# Register the SP
try:
saml2_register_sp(args['saml_idp_url'], args['admin_user'],
admin_password, args['saml_sp_name'],
- sp_metadata)
+ sp_metadata, args['saml_sp_description'],
+ args['saml_sp_visible'], sp_image)
except Exception as e: # pylint: disable=broad-except
logger.error("Failed to register SP with IDP!\n" +
"Error: [%s]" % e)
' configure your Service Provider')
-def saml2_register_sp(url, user, password, sp_name, sp_metadata):
+def saml2_register_sp(url, user, password, sp_name, sp_metadata,
+ sp_description, sp_visible, sp_image):
s = requests.Session()
# Authenticate to the IdP
sp_url = '%s/rest/providers/saml2/SPS/%s' % (url.rstrip('/'), sp_name)
sp_headers = {'Content-type': 'application/x-www-form-urlencoded',
'Referer': sp_url}
- sp_data = urlencode({'metadata': sp_metadata})
+ sp_data = {'metadata': sp_metadata}
+ if sp_description:
+ sp_data['description'] = sp_description
+ if sp_visible:
+ sp_data['visible'] = sp_visible
+ if sp_image:
+ if sp_image:
+ sp_data['imagefile'] = sp_image
+ sp_data = urlencode(sp_data)
r = s.post(sp_url, headers=sp_headers, data=sp_data)
if r.status_code != 201:
def saml2_uninstall():
- try:
- shutil.rmtree(os.path.join(SAML2_HTTPDIR, args['hostname']))
- except Exception, e: # pylint: disable=broad-except
- log_exception(e)
- try:
- os.remove(SAML2_CONFFILE)
- except Exception, e: # pylint: disable=broad-except
- log_exception(e)
+ path = os.path.join(SAML2_HTTPDIR, args['hostname'])
+ if os.path.exists(path):
+ try:
+ shutil.rmtree(path)
+ except Exception, e: # pylint: disable=broad-except
+ log_exception(e)
+
+ if os.path.exists(SAML2_CONFFILE):
+ try:
+ os.remove(SAML2_CONFFILE)
+ except Exception, e: # pylint: disable=broad-except
+ log_exception(e)
def uninstall():
help="SAML NameID format to use")
parser.add_argument('--saml-sp-name', default=None,
help="The SP name to register with the IdP")
+ parser.add_argument('--saml-sp-description', default=None,
+ help="The description of the SP to display on the " +
+ "portal")
+ parser.add_argument('--saml-sp-visible', action='store_false',
+ default=True,
+ help="The SP is visible in the portal")
+ parser.add_argument('--saml-sp-image', default=None,
+ help="Image to display for this SP on the portal")
parser.add_argument('--debug', action='store_true', default=False,
help="Turn on script debugging")
parser.add_argument('--config-profile', default=None,
HTTPDCONFD = '/etc/httpd/conf.d'
BINDIR = '/usr/libexec'
STATICDIR = '/usr/share/ipsilon'
+CACHEDIR = '/var/cache/httpd/ipsilon'
WSGI_SOCKET_PREFIX = None
'sysuser': args['system_user'],
'ipsilondir': BINDIR,
'staticdir': STATICDIR,
+ 'cachedir': CACHEDIR,
'admindb': args['admin_dburi'] or args['database_url'] % {
'datadir': args['data_dir'], 'dbname': 'adminconfig'},
'usersdb': args['users_dburi'] or args['database_url'] % {
from copy import deepcopy
import requests
import logging
+import base64
+from urlparse import urlparse
class NewSPAdminPage(AdminPage):
# set the owner in that case
name = None
meta = None
+ description = None
+ splink = None
+ visible = False
+ imagefile = None
if 'content-type' not in cherrypy.request.headers:
self.debug("Invalid request, missing content-type")
message = "Malformed request"
for key, value in kwargs.iteritems():
if key == 'name':
name = value
+ elif key == 'description':
+ description = value
+ elif key == 'splink':
+ # pylint: disable=unused-variable
+ (scheme, netloc, path, params, query, frag) = urlparse(
+ value
+ )
+ # minimum URL validation
+ if (scheme not in ['http', 'https'] or not netloc):
+ message = "Invalid URL for Service Provider link"
+ message_type = ADMIN_STATUS_ERROR
+ return self.form_new(message, message_type)
+ splink = value
+ elif key == 'portalvisible' and value.lower() == 'on':
+ visible = True
+ elif key == 'imagefile':
+ if hasattr(value, 'content_type'):
+ imagefile = value.fullvalue()
+ if len(imagefile) == 0:
+ imagefile = None
+ else:
+ imagefile = base64.b64encode(imagefile)
+ else:
+ self.debug("Invalid format for 'imagefile'")
elif key == 'metatext':
if len(value) > 0:
meta = value
if name and meta:
try:
spc = ServiceProviderCreator(self.parent.cfg)
- sp = spc.create_from_buffer(name, meta)
+ sp = spc.create_from_buffer(name, meta, description,
+ visible, imagefile, splink)
sp_page = self.parent.add_sp(name, sp)
message = "SP Successfully added"
message_type = ADMIN_STATUS_OK
raise UnauthorizedUser("Unauthorized to set owner")
elif key in ['User Owner', 'Default NameID',
'Allowed NameIDs', 'Attribute Mapping',
- 'Allowed Attributes']:
+ 'Allowed Attributes', 'Description',
+ 'Service Provider link',
+ 'Visible in Portal', 'Image File']:
if not self.user.is_admin:
raise UnauthorizedUser(
"Unauthorized to set %s" % key
# Make changes in current config
for name, option in conf.iteritems():
+ if name not in new_db_values:
+ continue
value = new_db_values.get(name, False)
# A value of None means remove from the data store
- if value is False or value == []:
+ if ((value is False or value == []) and
+ name != 'Visible in Portal'):
continue
if name == 'Name':
if not self.sp.is_valid_name(value):
self.parent.rename_sp(option.get_value(), value)
elif name == 'User Owner':
self.sp.owner = value
+ elif name == 'Description':
+ self.sp.description = value
+ elif name == 'Visible in Portal':
+ self.sp.visible = value
+ elif name == 'Service Provider link':
+ self.sp.splink = value
elif name == 'Default NameID':
self.sp.default_nameid = value
elif name == 'Allowed NameIDs':
self.sp.attribute_mappings = value
elif name == 'Allowed Attributes':
self.sp.allowed_attributes = value
+ elif name == 'Image File':
+ if hasattr(value, 'content_type'):
+ # pylint: disable=maybe-no-member
+ blob = value.fullvalue()
+ if len(blob) > 0:
+ self.sp.imagefile = base64.b64encode(blob)
+ else:
+ raise InvalidValueFormat(
+ 'Invalid Image file format'
+ )
+
except InvalidValueFormat, e:
message = str(e)
message_type = ADMIN_STATUS_WARN
' Only alphanumeric characters [A-Z,a-z,0-9] and spaces are'
' accepted.',
self.name),
+ pconfig.String(
+ 'Description',
+ 'A description of the SP to show on the Portal.',
+ self.description),
+ pconfig.String(
+ 'Service Provider link',
+ 'A link to the Service Provider for the Portal.',
+ self.splink),
+ pconfig.Condition(
+ 'Visible in Portal',
+ 'This SP is visible in the Portal.',
+ self.visible),
+ pconfig.Image(
+ 'Image File',
+ 'Image to display for this SP in the Portal. Scale to '
+ '100x200 for best results.',
+ self.imagefile),
pconfig.Pick(
'Default NameID',
'Default NameID used by Service Providers.',
def name(self, value):
self._staging['name'] = value
+ @property
+ def description(self):
+ return self._properties.get('description', '')
+
+ @description.setter
+ def description(self, value):
+ self._staging['description'] = value
+
+ @property
+ def visible(self):
+ return self._properties.get('visible', True)
+
+ @visible.setter
+ def visible(self, value):
+ self._staging['visible'] = value
+
+ @property
+ def imagefile(self):
+ return self._properties.get('imagefile', '')
+
+ @imagefile.setter
+ def imagefile(self, value):
+ self._staging['imagefile'] = value
+
+ @property
+ def imageurl(self):
+ return pconfig.url_from_image(self._properties['imagefile'])
+
+ @property
+ def splink(self):
+ return self._properties.get('splink', '')
+
+ @splink.setter
+ def splink(self, value):
+ self._staging['splink'] = value
+
@property
def owner(self):
if 'owner' in self._properties:
def __init__(self, config):
self.cfg = config
- def create_from_buffer(self, name, metabuf):
+ def create_from_buffer(self, name, metabuf, description='',
+ visible=True, imagefile='', splink=''):
'''Test and add data'''
if re.search(VALID_IN_NAME, name):
data = self.cfg.get_data(name='id', value=spid)
if len(data) != 0:
raise InvalidProviderId("Provider Already Exists")
- datum = {'id': spid, 'name': name, 'type': 'SP', 'metadata': metabuf}
+ datum = {
+ 'id': spid,
+ 'name': name,
+ 'type': 'SP',
+ 'metadata': metabuf,
+ 'description': description,
+ 'visible': visible,
+ 'imagefile': imagefile,
+ 'splink': splink,
+ }
self.cfg.new_datum(datum)
data = self.cfg.get_data(name='id', value=spid)
if len(args) != 1:
return rest_error(400, 'Invalid arguments. Found %d'
' there should be one.')
+ self.debug('REST POST %s' % kwargs)
name = args[0]
metadata = kwargs.get('metadata')
+ description = kwargs.get('description', '')
+ visible = kwargs.get('visible', True)
+ imagefile = kwargs.get('image', None)
+ splink = kwargs.get('splink', '')
obj = self._site[FACILITY].available[self.parent.plugin_name]
try:
spc = ServiceProviderCreator(obj)
- sp = spc.create_from_buffer(name, metadata)
+ sp = spc.create_from_buffer(name, metadata, description,
+ visible, imagefile, splink)
except (InvalidProviderId, ServerAddProviderFailedError) as e:
self.debug(repr(e))
return rest_error(400, str(e))
# Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING
from ipsilon.util.log import Log
+import os
import json
+import base64
+import imghdr
+import hashlib
+import cherrypy
+
+
+def name_from_image(image):
+ if image is None:
+ return None
+
+ fext = imghdr.what(None, base64.b64decode(image))
+ m = hashlib.sha1()
+ m.update(base64.b64decode(image))
+
+ return '%s.%s' % (m.hexdigest(), fext)
+
+
+def url_from_image(image):
+ if image is None:
+ return None
+
+ return '%s/cache/%s' % (
+ cherrypy.config.get('base.mount', ""),
+ name_from_image(image)
+ )
class Config(Log):
self._str_import_value(value)
+class Image(Option):
+ """
+ An image has two components: the binary blob of the image itself and
+ the SHA1 sum of the image.
+
+ We only need the image blob when writing to the cache file or
+ updating the database.
+
+ For the purposes of the UI we only need the filename which is
+ the SHA1 sum of file type the blob + file type.
+ """
+
+ def __init__(self, name, description, default_value=None, readonly=False):
+ super(Image, self).__init__(name, description, readonly=readonly)
+ self._image = None
+
+ if default_value:
+ self._image = default_value
+
+ self._assigned_value = url_from_image(self._image)
+ self.__write_cache_file()
+
+ def set_value(self, value):
+ if value is None:
+ return None
+
+ if os.path.exists(self.__filename()):
+ try:
+ os.remove(self.__filename())
+ except IOError as e:
+ self.error('Error removing %s: %s' % (self.__filename(), e))
+
+ self._image = base64.b64encode(value)
+ self._assigned_value = url_from_image(value)
+
+ def export_value(self):
+ if self._image is None:
+ return None
+
+ self.__write_cache_file()
+ return base64.b64decode(self._image)
+
+ def import_value(self, value):
+ if value is None:
+ return None
+
+ if os.path.exists(self.__filename()):
+ try:
+ os.remove(self.__filename())
+ except IOError as e:
+ self.error('Error removing %s: %s' % (self.__filename(), e))
+ self._image = base64.b64encode(value)
+ self._assigned_value = url_from_image(self._image)
+ self.__write_cache_file()
+
+ def __filename(self):
+ if self._image is None:
+ return None
+
+ cdir = cherrypy.config.get('cache_dir', '/var/cache/ipsilon')
+
+ return '%s/%s' % (cdir, name_from_image(self._image))
+
+ def __write_cache_file(self):
+ if self._image is None:
+ return None
+
+ if not os.path.exists(self.__filename()):
+ with open(self.__filename(), 'w') as imagefile:
+ imagefile.write(base64.b64decode(self._image))
+
+
class Template(Option):
def __init__(self, name, description, default_template=None,
def __init__(self, name, description, default_value=False,
readonly=False):
+ # The db stores 1/0. Convert the passed-in value if
+ # necessary
+ if default_value in [u'1', 'True', True]:
+ default_value = True
+ else:
+ default_value = False
super(Condition, self).__init__(name, description,
[True, False], default_value,
readonly=readonly)
def import_value(self, value):
- self._assigned_value = value == 'True'
+ self._assigned_value = value
class ConfigHelper(Log):
$(buttonRow).appendTo(ourTable)
}
);
+ $(function() {
+ $("#uploadFile").on("change", function()
+ {
+ var files = !!this.files ? this.files : [];
+ if (!files.length || !window.FileReader) return; // no file selected, or no FileReader support
+
+ if (/^image/.test( files[0].type)){ // only image file
+ var reader = new FileReader(); // instance of the FileReader
+ reader.readAsDataURL(files[0]); // read the local file
+
+ reader.onloadend = function(){ // set image data as background of div
+ $("#imagePreview").css("background-image", "url("+this.result+")");
+ }
+ }
+ });
+ });
</script>
{% endblock %}
{% block main %}
<hr>
<div id="options">
- <form class="form-horizontal" role="form" id="{{ name }}" action="{{ action }}" method="post" enctype="application/x-www-form-urlencoded">
+ <form class="form-horizontal" role="form" id="{{ name }}" action="{{ action }}" method="post" enctype="multipart/form-data"">
{% for k, v in config.iteritems() %}
<div class="form-group">
<label class="col-sm-2" for="{{ v.name }}">{{ v.name }}:</label>
disabled
{%- endif -%}
>
+ {% elif v.__class__.__name__ == 'Image' -%}
+ <!-- FIXME: This is limited to a single instance of Image -->
+ {%- if value %}
+ <img src="{{ value }}"
+ height="100" width="200"
+ >
+ {%- endif -%}
+ <p></p>
+ <input type="file" name="{{ v.name }}"
+ title="{{ v.name }}"
+ accept=".png,.jpg"
+ id="uploadFile"
+ style="display: none;" />
+ <input type="button" value="Select Image..." onclick="document.getElementById('uploadFile').click();" />
+ <p></p>
+ <div id="imagePreview"></div>
{% elif v.__class__.__name__ == 'List' -%}
<textarea class="form-control" name="{{ v.name }}"
{% if v.is_readonly() -%}
Only alphanumeric characters and spaces are accepted"/>
</div>
+ <div class="form-group">
+ <label for="name">Description:</label>
+ <input type="text" class="form-control" name="description" value=""
+ title="A description of the services this Service Provider provides"/>
+ </div>
+
+ <div class="form-group">
+ <label for="name">Visible in IdP Portal: </label>
+ <input type="checkbox" name="portalvisible" checked
+ title="Show this Service Provider in the IdP Portal"/>
+ </div>
+
+ <div class="form-group">
+ <label for="image">Portal image:</label>
+ <input type="file" name="imagefile" id="image"
+ title="Image to display for this Service Provider in the IdP Portal. Scale to 100x200 for best results."
+ accept=".png,.jpg"
+ />
+ </div>
+
+ <div class="form-group">
+ <label for="splink">Link to Service Provider:</label>
+ <input type="text" class="form-control" name="splink" value=""
+ title="Link to the Service Provider"
+ />
+ </div>
+
<div class="form-group">
<label for="metafile">Metadata file:</label>
<input type="file" name="metafile" id="file"