X-Git-Url: http://git.cascardo.eti.br/?a=blobdiff_plain;f=ipsilon%2Fproviders%2Fsaml2%2Fprovider.py;h=6d46ad22798746d6288ce6436e28781d83d29d83;hb=992492e0f2f526cd07e63b03cd70a81fca6a94ec;hp=d3ed5daad9e0ea43aad9643e332de387dd43c572;hpb=8f6f3b2226d66a085fffa521dea1cf31c42e896f;p=cascardo%2Fipsilon.git diff --git a/ipsilon/providers/saml2/provider.py b/ipsilon/providers/saml2/provider.py old mode 100755 new mode 100644 index d3ed5da..6d46ad2 --- a/ipsilon/providers/saml2/provider.py +++ b/ipsilon/providers/saml2/provider.py @@ -1,38 +1,16 @@ -#!/usr/bin/python -# -# Copyright (C) 2014 Simo Sorce -# -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . +# Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING from ipsilon.providers.common import ProviderException -import cherrypy +from ipsilon.util import config as pconfig +from ipsilon.util.config import ConfigHelper +from ipsilon.tools.saml2metadata import SAML2_NAMEID_MAP, NSMAP +from ipsilon.util.log import Log +from lxml import etree import lasso +import re -NAMEID_MAP = { - 'email': lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL, - 'encrypted': lasso.SAML2_NAME_IDENTIFIER_FORMAT_ENCRYPTED, - 'entity': lasso.SAML2_NAME_IDENTIFIER_FORMAT_ENTITY, - 'kerberos': lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS, - 'persistent': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT, - 'transient': lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT, - 'unspecified': lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED, - 'windows': lasso.SAML2_NAME_IDENTIFIER_FORMAT_WINDOWS, - 'x509': lasso.SAML2_NAME_IDENTIFIER_FORMAT_X509, -} +VALID_IN_NAME = r'[^\ a-zA-Z0-9]' class InvalidProviderId(ProviderException): @@ -40,13 +18,13 @@ class InvalidProviderId(ProviderException): def __init__(self, code): message = 'Invalid Provider ID: %s' % code super(InvalidProviderId, self).__init__(message) - self._debug(message) + self.debug(message) class NameIdNotAllowed(Exception): - def __init__(self): - message = 'The specified Name ID is not allowed' + def __init__(self, nid): + message = 'Name ID [%s] is not allowed' % nid super(NameIdNotAllowed, self).__init__(message) self.message = message @@ -54,9 +32,15 @@ class NameIdNotAllowed(Exception): return repr(self.message) -class ServiceProvider(object): +class ServiceProviderConfig(ConfigHelper): + def __init__(self): + super(ServiceProviderConfig, self).__init__() + + +class ServiceProvider(ServiceProviderConfig): def __init__(self, config, provider_id): + super(ServiceProvider, self).__init__() self.cfg = config data = self.cfg.get_data(name='id', value=provider_id) if len(data) != 1: @@ -64,6 +48,68 @@ class ServiceProvider(object): idval = data.keys()[0] data = self.cfg.get_data(idval=idval) self._properties = data[idval] + self._staging = dict() + self.load_config() + self.logout_mechs = [] + xmldoc = etree.XML(str(data[idval]['metadata'])) + logout = xmldoc.xpath('//md:EntityDescriptor' + '/md:SPSSODescriptor' + '/md:SingleLogoutService', + namespaces=NSMAP) + for service in logout: + self.logout_mechs.append(service.values()[0]) + + def load_config(self): + self.new_config( + self.provider_id, + pconfig.String( + 'Name', + 'A nickname used to easily identify the Service Provider.' + ' 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.', + SAML2_NAMEID_MAP.keys(), + self.default_nameid), + pconfig.Choice( + 'Allowed NameIDs', + 'Allowed NameIDs for this Service Provider.', + SAML2_NAMEID_MAP.keys(), + self.allowed_nameids), + pconfig.String( + 'User Owner', + 'The user that owns this Service Provider', + self.owner), + pconfig.MappingList( + 'Attribute Mapping', + 'Defines how to map attributes before returning them to' + ' the SP. Setting this overrides the global values.', + self.attribute_mappings), + pconfig.ComplexList( + 'Allowed Attributes', + 'Defines a list of allowed attributes, applied after mapping.' + ' Setting this overrides the global values.', + self.allowed_attributes), + ) @property def provider_id(self): @@ -73,13 +119,71 @@ class ServiceProvider(object): def name(self): return self._properties['name'] + @name.setter + 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 allowed_namedids(self): - if 'allowed nameid' in self._properties: - return self._properties['allowed nameid'] + 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: + return self._properties['owner'] + else: + return '' + + @owner.setter + def owner(self, value): + self._staging['owner'] = value + + @property + def allowed_nameids(self): + if 'allowed nameids' in self._properties: + allowed = self._properties['allowed nameids'] + return [x.strip() for x in allowed.split(',')] else: return self.cfg.default_allowed_nameids + @allowed_nameids.setter + def allowed_nameids(self, value): + if not isinstance(value, list): + raise ValueError("Must be a list") + self._staging['allowed nameids'] = ','.join(value) + @property def default_nameid(self): if 'default nameid' in self._properties: @@ -87,25 +191,179 @@ class ServiceProvider(object): else: return self.cfg.default_nameid + @default_nameid.setter + def default_nameid(self, value): + self._staging['default nameid'] = value + + @property + def attribute_mappings(self): + if 'attribute mappings' in self._properties: + attr_map = pconfig.MappingList('temp', 'temp', None) + attr_map.import_value(str(self._properties['attribute mappings'])) + return attr_map.get_value() + else: + return None + + @attribute_mappings.setter + def attribute_mappings(self, attr_map): + if isinstance(attr_map, pconfig.MappingList): + value = attr_map.export_value() + else: + temp = pconfig.MappingList('temp', 'temp', None) + temp.set_value(attr_map) + value = temp.export_value() + self._staging['attribute mappings'] = value + + @property + def allowed_attributes(self): + if 'allowed_attributes' in self._properties: + attr_map = pconfig.ComplexList('temp', 'temp', None) + attr_map.import_value(str(self._properties['allowed_attributes'])) + return attr_map.get_value() + else: + return None + + @allowed_attributes.setter + def allowed_attributes(self, attr_map): + if isinstance(attr_map, pconfig.ComplexList): + value = attr_map.export_value() + else: + temp = pconfig.ComplexList('temp', 'temp', None) + temp.set_value(attr_map) + value = temp.export_value() + self._staging['allowed_attributes'] = value + + def save_properties(self): + data = self.cfg.get_data(name='id', value=self.provider_id) + if len(data) != 1: + raise InvalidProviderId('Could not find SP data') + idval = data.keys()[0] + data = dict() + data[idval] = self._staging + self.cfg.save_data(data) + data = self.cfg.get_data(idval=idval) + self._properties = data[idval] + self._staging = dict() + + def refresh_config(self): + """ + Create a new config object for displaying in the UI based on + the current set of properties. + """ + del self._config + self.load_config() + def get_valid_nameid(self, nip): - self._debug('Requested NameId [%s]' % (nip.format,)) - if nip.format is None: - return NAMEID_MAP[self.default_nameid] - elif nip.format == lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED: - return NAMEID_MAP[self.default_nameid] + if nip is None or nip.format is None: + self.debug('No NameId requested, returning default [%s]' + % SAML2_NAMEID_MAP[self.default_nameid]) + return SAML2_NAMEID_MAP[self.default_nameid] else: - allowed = self.allowed_namedids - self._debug('Allowed NameIds %s' % (repr(allowed))) + self.debug('Requested NameId [%s]' % (nip.format,)) + allowed = self.allowed_nameids + self.debug('Allowed NameIds %s' % (repr(allowed))) for nameid in allowed: - if nip.format == NAMEID_MAP[nameid]: + if nip.format == SAML2_NAMEID_MAP[nameid]: return nip.format raise NameIdNotAllowed(nip.format) - def _debug(self, fact): - if cherrypy.config.get('debug', False): - cherrypy.log(fact) + def permanently_delete(self): + data = self.cfg.get_data(name='id', value=self.provider_id) + if len(data) != 1: + raise InvalidProviderId('Could not find SP data') + idval = data.keys()[0] + self.cfg.del_datum(idval) def normalize_username(self, username): if 'strip domain' in self._properties: return username.split('@', 1)[0] return username + + def is_valid_name(self, value): + if re.search(VALID_IN_NAME, value): + return False + return True + + def is_valid_nameid(self, value): + if value in SAML2_NAMEID_MAP: + return True + return False + + def valid_nameids(self): + return SAML2_NAMEID_MAP.keys() + + +class ServiceProviderCreator(object): + + def __init__(self, config): + self.cfg = config + + def create_from_buffer(self, name, metabuf, description='', + visible=True, imagefile='', splink=''): + '''Test and add data''' + + if re.search(VALID_IN_NAME, name): + raise InvalidProviderId("Name must contain only " + "numbers and letters") + + test = lasso.Server() + test.addProviderFromBuffer(lasso.PROVIDER_ROLE_SP, metabuf) + newsps = test.get_providers() + if len(newsps) != 1: + raise InvalidProviderId("Metadata must contain one Provider") + + spid = newsps.keys()[0] + 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, + 'description': description, + 'visible': visible, + 'imagefile': imagefile, + 'splink': splink, + } + self.cfg.new_datum(datum) + + data = self.cfg.get_data(name='id', value=spid) + if len(data) != 1: + raise InvalidProviderId("Internal Error") + idval = data.keys()[0] + data = self.cfg.get_data(idval=idval) + sp = data[idval] + self.cfg.idp.add_provider(sp) + + return ServiceProvider(self.cfg, spid) + + +class IdentityProvider(Log): + def __init__(self, config, sessionfactory): + self.server = lasso.Server(config.idp_metadata_file, + config.idp_key_file, + None, + config.idp_certificate_file) + self.server.role = lasso.PROVIDER_ROLE_IDP + self.sessionfactory = sessionfactory + + def add_provider(self, sp): + self.server.addProviderFromBuffer(lasso.PROVIDER_ROLE_SP, + sp['metadata']) + self.debug('Added SP %s' % sp['name']) + + def get_login_handler(self, dump=None): + if dump: + return lasso.Login.newFromDump(self.server, dump) + else: + return lasso.Login(self.server) + + def get_providers(self): + return self.server.get_providers() + + def get_logout_handler(self, dump=None): + if dump: + return lasso.Logout.newFromDump(self.server, dump) + else: + return lasso.Logout(self.server)