wcs/wcs/qommon/saml2utils.py

206 lines
7.9 KiB
Python

# w.c.s. - web application for online forms
# Copyright (C) 2005-2010 Entr'ouvert
#
# 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 2 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 <http://www.gnu.org/licenses/>.
import os
from . import force_str
from . import x509utils
def bool2xs(boolean):
'''Convert a boolean value to XSchema boolean representation'''
if boolean is True:
return 'true'
if boolean is False:
return 'false'
raise TypeError()
class Metadata:
__endpoints = {'slo': 'singleLogout', 'ac': 'assertionConsumer'}
def __init__(self, publisher, provider_id, config):
self.publisher = publisher
self.provider_id = provider_id
self.config = config
def get_key_descriptor(self, keytype, key):
'''Format key as an XML Dsig KeyNode content'''
if keytype:
prologue = ' <KeyDescriptor use="%s">' % keytype
else:
prologue = ' <KeyDescriptor>'
if key and 'CERTIF' in key:
naked = x509utils.decapsulate_pem_file(key)
return (
prologue
+ '''
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data><ds:X509Certificate>%s</ds:X509Certificate></ds:X509Data>
</ds:KeyInfo>
</KeyDescriptor>
'''
% naked
)
# FIXME: generate proper RSAKeyValue, but wait for support in Lasso
elif key and 'KEY' in key:
naked = x509utils.get_xmldsig_rsa_key_value(key)
return (
prologue
+ '''
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
%s
</ds:KeyInfo>
</KeyDescriptor>
'''
% naked
)
else:
return ''
def get_key_descriptors(self, signing_pem_key, encryption_pem_key):
signing_pem_key, encryption_pem_key = self.get_new_or_old_keys(signing_pem_key, encryption_pem_key)
sp_key = {
'signing': self.get_key_descriptor('signing', signing_pem_key),
'encryption': self.get_key_descriptor('encryption', encryption_pem_key),
}
if not sp_key['signing'] and sp_key['encryption']:
sp_key = {'signing': '', 'encryption': self.get_key_descriptor('', encryption_pem_key)}
if sp_key['signing'] and not sp_key['encryption']:
sp_key = {'signing': self.get_key_descriptor('', signing_pem_key), 'encryption': ''}
return sp_key
def get_new_or_old_keys(self, signing_pem_key, encryption_pem_key):
'''Return new or earlier version of PEM keys'''
dir = self.publisher.app_dir
if not signing_pem_key and self.config.get('publickey'):
signing_pem_key = open(os.path.join(dir, 'public-key.pem'), 'r').read()
if not encryption_pem_key and self.config.get('encryption_publickey'):
encryption_pem_key = open(os.path.join(dir, 'encryption-public-key.pem'), 'r').read()
return (signing_pem_key, encryption_pem_key)
def get_spsso_descriptor(self, signing_pem_key, encryption_pem_key, endpoints):
signing_pem_key, encryption_pem_key = self.get_new_or_old_keys(signing_pem_key, encryption_pem_key)
authnrequestsigned = bool2xs(self.config.get('authn-request-signed', True))
wantassertionsigned = bool2xs(self.config.get('want-assertion-signed', True))
prologue = ''' <SPSSODescriptor
AuthnRequestsSigned="%s" WantAssertionsSigned="%s"
protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
''' % (
authnrequestsigned,
wantassertionsigned,
)
sp_key = self.get_key_descriptors(signing_pem_key, encryption_pem_key)
config = {}
config.update(self.config)
config.update(self.__endpoints)
config.update(endpoints)
return (
prologue
+ sp_key['signing']
+ sp_key['encryption']
+ '''
<SingleLogoutService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="%(saml2_base_url)s/%(slo)s"
ResponseLocation="%(saml2_base_url)s/%(slo)sReturn" />
<SingleLogoutService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
Location="%(saml2_base_url)s/%(slo)sSOAP" />
<AssertionConsumerService isDefault="true" index="0"
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"
Location="%(saml2_base_url)s/%(ac)sArtifact" />
<AssertionConsumerService index="1"
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="%(saml2_base_url)s/%(ac)sPost" />
<AssertionConsumerService index="3"
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="%(saml2_base_url)s/%(ac)sRedirect" />
</SPSSODescriptor>'''
% config
)
def get_idpsso_descriptor(self, signing_pem_key, encryption_pem_key):
signing_pem_key, encryption_pem_key = self.get_new_or_old_keys(signing_pem_key, encryption_pem_key)
idp_key = self.get_key_descriptors(signing_pem_key, encryption_pem_key)
idp_head = """
<IDPSSODescriptor
WantAuthnRequestsSigned="%s"
protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
""" % bool2xs(
self.config.get('want-authn-request-signed', True)
)
idp_body = (
"""
<ArtifactResolutionService isDefault="true" index="0"
Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
Location="%(saml2_base_soap_url)s/artifact" />
<SingleLogoutService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="%(saml2_base_url)s/singleLogout"
ResponseLocation="%(saml2_base_url)s/singleLogoutReturn" />
<SingleLogoutService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
Location="%(saml2_base_soap_url)s/singleLogoutSOAP" />
<SingleSignOnService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="%(saml2_base_url)s/singleSignOn" />
<SingleSignOnService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="%(saml2_base_url)s/singleSignOn" />
<SingleSignOnService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
Location="%(saml2_base_soap_url)s/singleSignOnSOAP" />
</IDPSSODescriptor>"""
% self.config
)
return idp_head + idp_key['signing'] + idp_key['encryption'] + idp_body
def get_saml2_metadata(
self, signing_pem_key='', encryption_pem_key='', do_idp=False, do_sp=False, endpoints=None
):
endpoints = endpoints or {}
prologue = (
'''<?xml version="1.0"?>
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
entityID="%s">'''
% self.provider_id
)
sp_descriptor = ''
if do_sp:
sp_descriptor = self.get_spsso_descriptor(signing_pem_key, encryption_pem_key, endpoints)
idp_descriptor = ''
if do_idp:
idp_descriptor = self.get_idpsso_descriptor(signing_pem_key, encryption_pem_key)
orga = ''
if self.config.get('organization_name'):
orga = '''<Organization>
<OrganizationName xml:lang="en">%s</OrganizationName>
</Organization>''' % force_str(
self.config['organization_name']
)
epilogue = '</EntityDescriptor>'
return '\n'.join([prologue, sp_descriptor, idp_descriptor, orga, epilogue])