"Fossies" - the Fresh Open Source Software Archive

Member "keystone-17.0.0/keystone/federation/idp.py" (13 May 2020, 27327 Bytes) of package /linux/misc/openstack/keystone-17.0.0.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "idp.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 16.0.1_vs_17.0.0.

    1 # Licensed under the Apache License, Version 2.0 (the "License"); you may
    2 # not use this file except in compliance with the License. You may obtain
    3 # a copy of the License at
    4 #
    5 #      http://www.apache.org/licenses/LICENSE-2.0
    6 #
    7 # Unless required by applicable law or agreed to in writing, software
    8 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    9 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   10 # License for the specific language governing permissions and limitations
   11 # under the License.
   12 
   13 import datetime
   14 import os
   15 import subprocess  # nosec : see comments in the code below
   16 import uuid
   17 
   18 from oslo_log import log
   19 from oslo_utils import fileutils
   20 from oslo_utils import importutils
   21 from oslo_utils import timeutils
   22 import saml2
   23 from saml2 import client_base
   24 from saml2 import md
   25 from saml2.profile import ecp
   26 from saml2 import saml
   27 from saml2 import samlp
   28 from saml2.schema import soapenv
   29 from saml2 import sigver
   30 xmldsig = importutils.try_import("saml2.xmldsig")
   31 if not xmldsig:
   32     xmldsig = importutils.try_import("xmldsig")
   33 
   34 from keystone.common import utils
   35 import keystone.conf
   36 from keystone import exception
   37 from keystone.i18n import _
   38 
   39 
   40 LOG = log.getLogger(__name__)
   41 CONF = keystone.conf.CONF
   42 
   43 
   44 class SAMLGenerator(object):
   45     """A class to generate SAML assertions."""
   46 
   47     def __init__(self):
   48         self.assertion_id = uuid.uuid4().hex
   49 
   50     def samlize_token(self, issuer, recipient, user, user_domain_name, roles,
   51                       project, project_domain_name, groups,
   52                       expires_in=None):
   53         """Convert Keystone attributes to a SAML assertion.
   54 
   55         :param issuer: URL of the issuing party
   56         :type issuer: string
   57         :param recipient: URL of the recipient
   58         :type recipient: string
   59         :param user: User name
   60         :type user: string
   61         :param user_domain_name: User Domain name
   62         :type user_domain_name: string
   63         :param roles: List of role names
   64         :type roles: list
   65         :param project: Project name
   66         :type project: string
   67         :param project_domain_name: Project Domain name
   68         :type project_domain_name: string
   69         :param groups: List of strings of user groups and domain name, where
   70                        strings are serialized dictionaries.
   71         :type groups: list
   72         :param expires_in: Sets how long the assertion is valid for, in seconds
   73         :type expires_in: int
   74 
   75         :returns: XML <Response> object
   76 
   77         """
   78         expiration_time = self._determine_expiration_time(expires_in)
   79         status = self._create_status()
   80         saml_issuer = self._create_issuer(issuer)
   81         subject = self._create_subject(user, expiration_time, recipient)
   82         attribute_statement = self._create_attribute_statement(
   83             user, user_domain_name, roles, project, project_domain_name,
   84             groups)
   85         authn_statement = self._create_authn_statement(issuer, expiration_time)
   86         signature = self._create_signature()
   87 
   88         assertion = self._create_assertion(saml_issuer, signature,
   89                                            subject, authn_statement,
   90                                            attribute_statement)
   91 
   92         assertion = _sign_assertion(assertion)
   93 
   94         response = self._create_response(saml_issuer, status, assertion,
   95                                          recipient)
   96         return response
   97 
   98     def _determine_expiration_time(self, expires_in):
   99         if expires_in is None:
  100             expires_in = CONF.saml.assertion_expiration_time
  101         now = timeutils.utcnow()
  102         future = now + datetime.timedelta(seconds=expires_in)
  103         return utils.isotime(future, subsecond=True)
  104 
  105     def _create_status(self):
  106         """Create an object that represents a SAML Status.
  107 
  108         <ns0:Status xmlns:ns0="urn:oasis:names:tc:SAML:2.0:protocol">
  109             <ns0:StatusCode
  110               Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
  111         </ns0:Status>
  112 
  113         :returns: XML <Status> object
  114 
  115         """
  116         status = samlp.Status()
  117         status_code = samlp.StatusCode()
  118         status_code.value = samlp.STATUS_SUCCESS
  119         status_code.set_text('')
  120         status.status_code = status_code
  121         return status
  122 
  123     def _create_issuer(self, issuer_url):
  124         """Create an object that represents a SAML Issuer.
  125 
  126         <ns0:Issuer
  127           xmlns:ns0="urn:oasis:names:tc:SAML:2.0:assertion"
  128           Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">
  129           https://acme.com/FIM/sps/openstack/saml20</ns0:Issuer>
  130 
  131         :returns: XML <Issuer> object
  132 
  133         """
  134         issuer = saml.Issuer()
  135         issuer.format = saml.NAMEID_FORMAT_ENTITY
  136         issuer.set_text(issuer_url)
  137         return issuer
  138 
  139     def _create_subject(self, user, expiration_time, recipient):
  140         """Create an object that represents a SAML Subject.
  141 
  142         <ns0:Subject>
  143             <ns0:NameID>
  144                 john@smith.com</ns0:NameID>
  145             <ns0:SubjectConfirmation
  146               Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
  147                 <ns0:SubjectConfirmationData
  148                   NotOnOrAfter="2014-08-19T11:53:57.243106Z"
  149                   Recipient="http://beta.com/Shibboleth.sso/SAML2/POST" />
  150             </ns0:SubjectConfirmation>
  151         </ns0:Subject>
  152 
  153         :returns: XML <Subject> object
  154 
  155         """
  156         name_id = saml.NameID()
  157         name_id.set_text(user)
  158         subject_conf_data = saml.SubjectConfirmationData()
  159         subject_conf_data.recipient = recipient
  160         subject_conf_data.not_on_or_after = expiration_time
  161         subject_conf = saml.SubjectConfirmation()
  162         subject_conf.method = saml.SCM_BEARER
  163         subject_conf.subject_confirmation_data = subject_conf_data
  164         subject = saml.Subject()
  165         subject.subject_confirmation = subject_conf
  166         subject.name_id = name_id
  167         return subject
  168 
  169     def _create_attribute_statement(self, user, user_domain_name, roles,
  170                                     project, project_domain_name,
  171                                     groups):
  172         """Create an object that represents a SAML AttributeStatement.
  173 
  174         <ns0:AttributeStatement>
  175             <ns0:Attribute Name="openstack_user">
  176                 <ns0:AttributeValue
  177                   xsi:type="xs:string">test_user</ns0:AttributeValue>
  178             </ns0:Attribute>
  179             <ns0:Attribute Name="openstack_user_domain">
  180                 <ns0:AttributeValue
  181                   xsi:type="xs:string">Default</ns0:AttributeValue>
  182             </ns0:Attribute>
  183             <ns0:Attribute Name="openstack_roles">
  184                 <ns0:AttributeValue
  185                   xsi:type="xs:string">admin</ns0:AttributeValue>
  186                 <ns0:AttributeValue
  187                   xsi:type="xs:string">member</ns0:AttributeValue>
  188             </ns0:Attribute>
  189             <ns0:Attribute Name="openstack_project">
  190                 <ns0:AttributeValue
  191                   xsi:type="xs:string">development</ns0:AttributeValue>
  192             </ns0:Attribute>
  193             <ns0:Attribute Name="openstack_project_domain">
  194                 <ns0:AttributeValue
  195                   xsi:type="xs:string">Default</ns0:AttributeValue>
  196             </ns0:Attribute>
  197             <ns0:Attribute Name="openstack_groups">
  198                 <ns0:AttributeValue
  199                    xsi:type="xs:string">JSON:{"name":"group1","domain":{"name":"Default"}}
  200                 </ns0:AttributeValue>
  201                 <ns0:AttributeValue
  202                    xsi:type="xs:string">JSON:{"name":"group2","domain":{"name":"Default"}}
  203                 </ns0:AttributeValue>
  204             </ns0:Attribute>
  205 
  206         </ns0:AttributeStatement>
  207 
  208         :returns: XML <AttributeStatement> object
  209 
  210         """
  211         def _build_attribute(attribute_name, attribute_values):
  212             attribute = saml.Attribute()
  213             attribute.name = attribute_name
  214 
  215             for value in attribute_values:
  216                 attribute_value = saml.AttributeValue()
  217                 attribute_value.set_text(value)
  218                 attribute.attribute_value.append(attribute_value)
  219 
  220             return attribute
  221 
  222         user_attribute = _build_attribute('openstack_user', [user])
  223         roles_attribute = _build_attribute('openstack_roles', roles)
  224         project_attribute = _build_attribute('openstack_project', [project])
  225         project_domain_attribute = _build_attribute(
  226             'openstack_project_domain', [project_domain_name])
  227         user_domain_attribute = _build_attribute(
  228             'openstack_user_domain', [user_domain_name])
  229 
  230         attribute_statement = saml.AttributeStatement()
  231         attribute_statement.attribute.append(user_attribute)
  232         attribute_statement.attribute.append(roles_attribute)
  233         attribute_statement.attribute.append(project_attribute)
  234         attribute_statement.attribute.append(project_domain_attribute)
  235         attribute_statement.attribute.append(user_domain_attribute)
  236 
  237         if groups:
  238             groups_attribute = _build_attribute(
  239                 'openstack_groups', groups)
  240             attribute_statement.attribute.append(groups_attribute)
  241         return attribute_statement
  242 
  243     def _create_authn_statement(self, issuer, expiration_time):
  244         """Create an object that represents a SAML AuthnStatement.
  245 
  246         <ns0:AuthnStatement xmlns:ns0="urn:oasis:names:tc:SAML:2.0:assertion"
  247           AuthnInstant="2014-07-30T03:04:25Z" SessionIndex="47335964efb"
  248           SessionNotOnOrAfter="2014-07-30T03:04:26Z">
  249             <ns0:AuthnContext>
  250                 <ns0:AuthnContextClassRef>
  251                   urn:oasis:names:tc:SAML:2.0:ac:classes:Password
  252                 </ns0:AuthnContextClassRef>
  253                 <ns0:AuthenticatingAuthority>
  254                   https://acme.com/FIM/sps/openstack/saml20
  255                 </ns0:AuthenticatingAuthority>
  256             </ns0:AuthnContext>
  257         </ns0:AuthnStatement>
  258 
  259         :returns: XML <AuthnStatement> object
  260 
  261         """
  262         authn_statement = saml.AuthnStatement()
  263         authn_statement.authn_instant = utils.isotime()
  264         authn_statement.session_index = uuid.uuid4().hex
  265         authn_statement.session_not_on_or_after = expiration_time
  266 
  267         authn_context = saml.AuthnContext()
  268         authn_context_class = saml.AuthnContextClassRef()
  269         authn_context_class.set_text(saml.AUTHN_PASSWORD)
  270 
  271         authn_authority = saml.AuthenticatingAuthority()
  272         authn_authority.set_text(issuer)
  273         authn_context.authn_context_class_ref = authn_context_class
  274         authn_context.authenticating_authority = authn_authority
  275 
  276         authn_statement.authn_context = authn_context
  277 
  278         return authn_statement
  279 
  280     def _create_assertion(self, issuer, signature, subject, authn_statement,
  281                           attribute_statement):
  282         """Create an object that represents a SAML Assertion.
  283 
  284         <ns0:Assertion
  285           ID="35daed258ba647ba8962e9baff4d6a46"
  286           IssueInstant="2014-06-11T15:45:58Z"
  287           Version="2.0">
  288             <ns0:Issuer> ... </ns0:Issuer>
  289             <ns1:Signature> ... </ns1:Signature>
  290             <ns0:Subject> ... </ns0:Subject>
  291             <ns0:AuthnStatement> ... </ns0:AuthnStatement>
  292             <ns0:AttributeStatement> ... </ns0:AttributeStatement>
  293         </ns0:Assertion>
  294 
  295         :returns: XML <Assertion> object
  296 
  297         """
  298         assertion = saml.Assertion()
  299         assertion.id = self.assertion_id
  300         assertion.issue_instant = utils.isotime()
  301         assertion.version = '2.0'
  302         assertion.issuer = issuer
  303         assertion.signature = signature
  304         assertion.subject = subject
  305         assertion.authn_statement = authn_statement
  306         assertion.attribute_statement = attribute_statement
  307         return assertion
  308 
  309     def _create_response(self, issuer, status, assertion, recipient):
  310         """Create an object that represents a SAML Response.
  311 
  312         <ns0:Response
  313           Destination="http://beta.com/Shibboleth.sso/SAML2/POST"
  314           ID="c5954543230e4e778bc5b92923a0512d"
  315           IssueInstant="2014-07-30T03:19:45Z"
  316           Version="2.0" />
  317             <ns0:Issuer> ... </ns0:Issuer>
  318             <ns0:Assertion> ... </ns0:Assertion>
  319             <ns0:Status> ... </ns0:Status>
  320         </ns0:Response>
  321 
  322         :returns: XML <Response> object
  323 
  324         """
  325         response = samlp.Response()
  326         response.id = uuid.uuid4().hex
  327         response.destination = recipient
  328         response.issue_instant = utils.isotime()
  329         response.version = '2.0'
  330         response.issuer = issuer
  331         response.status = status
  332         response.assertion = assertion
  333         return response
  334 
  335     def _create_signature(self):
  336         """Create an object that represents a SAML <Signature>.
  337 
  338         This must be filled with algorithms that the signing binary will apply
  339         in order to sign the whole message.
  340         Currently we enforce X509 signing.
  341         Example of the template::
  342 
  343         <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
  344           <SignedInfo>
  345             <CanonicalizationMethod
  346               Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
  347             <SignatureMethod
  348               Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
  349             <Reference URI="#<Assertion ID>">
  350               <Transforms>
  351                 <Transform
  352             Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
  353                <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
  354               </Transforms>
  355              <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
  356              <DigestValue />
  357             </Reference>
  358           </SignedInfo>
  359           <SignatureValue />
  360           <KeyInfo>
  361             <X509Data />
  362           </KeyInfo>
  363         </Signature>
  364 
  365         :returns: XML <Signature> object
  366 
  367         """
  368         canonicalization_method = xmldsig.CanonicalizationMethod()
  369         canonicalization_method.algorithm = xmldsig.ALG_EXC_C14N
  370         signature_method = xmldsig.SignatureMethod(
  371             algorithm=xmldsig.SIG_RSA_SHA1)
  372 
  373         transforms = xmldsig.Transforms()
  374         envelope_transform = xmldsig.Transform(
  375             algorithm=xmldsig.TRANSFORM_ENVELOPED)
  376 
  377         c14_transform = xmldsig.Transform(algorithm=xmldsig.ALG_EXC_C14N)
  378         transforms.transform = [envelope_transform, c14_transform]
  379 
  380         digest_method = xmldsig.DigestMethod(algorithm=xmldsig.DIGEST_SHA1)
  381         digest_value = xmldsig.DigestValue()
  382 
  383         reference = xmldsig.Reference()
  384         reference.uri = '#' + self.assertion_id
  385         reference.digest_method = digest_method
  386         reference.digest_value = digest_value
  387         reference.transforms = transforms
  388 
  389         signed_info = xmldsig.SignedInfo()
  390         signed_info.canonicalization_method = canonicalization_method
  391         signed_info.signature_method = signature_method
  392         signed_info.reference = reference
  393 
  394         key_info = xmldsig.KeyInfo()
  395         key_info.x509_data = xmldsig.X509Data()
  396 
  397         signature = xmldsig.Signature()
  398         signature.signed_info = signed_info
  399         signature.signature_value = xmldsig.SignatureValue()
  400         signature.key_info = key_info
  401 
  402         return signature
  403 
  404 
  405 def _verify_assertion_binary_is_installed():
  406     """Make sure the specified xmlsec binary is installed.
  407 
  408     If the binary specified in configuration isn't installed, make sure we
  409     leave some sort of useful error message for operators since the absense of
  410     it is going to throw an HTTP 500.
  411 
  412     """
  413     try:
  414         # `check_output` just returns the output of whatever is passed in
  415         # (hence the name). We don't really care about where the location of
  416         # the binary exists, though. We just want to make sure it's actually
  417         # installed and if an `CalledProcessError` isn't thrown, it is.
  418         subprocess.check_output(  # nosec : The contents of this command are
  419                                   # coming from either the default
  420                                   # configuration value for
  421                                   # CONF.saml.xmlsec1_binary or an operator
  422                                   # supplied location for that binary. In
  423                                   # either case, it is safe to assume this
  424                                   # input is coming from a trusted source and
  425                                   # not a possible attacker (over the API).
  426             ['/usr/bin/which', CONF.saml.xmlsec1_binary]
  427         )
  428     except subprocess.CalledProcessError:
  429         msg = (
  430             'Unable to locate %(binary)s binary on the system. Check to make '
  431             'sure it is installed.') % {'binary': CONF.saml.xmlsec1_binary}
  432         tr_msg = _(
  433             'Unable to locate %(binary)s binary on the system. Check to '
  434             'make sure it is installed.') % {
  435             'binary': CONF.saml.xmlsec1_binary}
  436         LOG.error(msg)
  437         raise exception.SAMLSigningError(reason=tr_msg)
  438 
  439 
  440 def _sign_assertion(assertion):
  441     """Sign a SAML assertion.
  442 
  443     This method utilizes ``xmlsec1`` binary and signs SAML assertions in a
  444     separate process. ``xmlsec1`` cannot read input data from stdin so the
  445     prepared assertion needs to be serialized and stored in a temporary file.
  446     This file will be deleted immediately after ``xmlsec1`` returns. The signed
  447     assertion is redirected to a standard output and read using
  448     ``subprocess.PIPE`` redirection. A ``saml.Assertion`` class is created from
  449     the signed string again and returned.
  450 
  451     Parameters that are required in the CONF::
  452     * xmlsec_binary
  453     * private key file path
  454     * public key file path
  455     :returns: XML <Assertion> object
  456 
  457     """
  458     # Ensure that the configured certificate paths do not contain any commas,
  459     # before we string format a comma in between them and cause xmlsec1 to
  460     # explode like a thousand fiery supernovas made entirely of unsigned SAML.
  461     for option in ('keyfile', 'certfile'):
  462         if ',' in getattr(CONF.saml, option, ''):
  463             raise exception.UnexpectedError(
  464                 'The configuration value in `keystone.conf [saml] %s` cannot '
  465                 'contain a comma (`,`). Please fix your configuration.' %
  466                 option)
  467 
  468     # xmlsec1 --sign --privkey-pem privkey,cert --id-attr:ID <tag> <file>
  469     certificates = '%(idp_private_key)s,%(idp_public_key)s' % {
  470         'idp_public_key': CONF.saml.certfile,
  471         'idp_private_key': CONF.saml.keyfile,
  472     }
  473 
  474     # Verify that the binary used to create the assertion actually exists on
  475     # the system. If it doesn't, log a warning for operators to go and install
  476     # it. Requests for assertions will fail with HTTP 500s until the package is
  477     # installed, so providing something useful in the logs is about the best we
  478     # can do.
  479     _verify_assertion_binary_is_installed()
  480 
  481     command_list = [
  482         CONF.saml.xmlsec1_binary, '--sign', '--privkey-pem', certificates,
  483         '--id-attr:ID', 'Assertion']
  484 
  485     file_path = None
  486     try:
  487         # NOTE(gyee): need to make the namespace prefixes explicit so
  488         # they won't get reassigned when we wrap the assertion into
  489         # SAML2 response
  490         file_path = fileutils.write_to_tempfile(assertion.to_string(
  491             nspair={'saml': saml2.NAMESPACE,
  492                     'xmldsig': xmldsig.NAMESPACE}))
  493         command_list.append(file_path)
  494         stdout = subprocess.check_output(command_list,  # nosec : The contents
  495                                          # of the command list are coming from
  496                                          # a trusted source because the
  497                                          # executable and arguments all either
  498                                          # come from the config file or are
  499                                          # hardcoded. The command list is
  500                                          # initialized earlier in this function
  501                                          # to a list and it's still a list at
  502                                          # this point in the function. There is
  503                                          # no opportunity for an attacker to
  504                                          # attempt command injection via string
  505                                          # parsing.
  506                                          stderr=subprocess.STDOUT)
  507     except Exception as e:
  508         msg = 'Error when signing assertion, reason: %(reason)s%(output)s'
  509         LOG.error(msg,
  510                   {'reason': e,
  511                    'output': ' ' + e.output if hasattr(e, 'output') else ''})
  512         raise exception.SAMLSigningError(reason=e)
  513     finally:
  514         try:
  515             if file_path:
  516                 os.remove(file_path)
  517         except OSError:  # nosec
  518             # The file is already gone, good.
  519             pass
  520 
  521     return saml2.create_class_from_xml_string(saml.Assertion, stdout)
  522 
  523 
  524 class MetadataGenerator(object):
  525     """A class for generating SAML IdP Metadata."""
  526 
  527     def generate_metadata(self):
  528         """Generate Identity Provider Metadata.
  529 
  530         Generate and format metadata into XML that can be exposed and
  531         consumed by a federated Service Provider.
  532 
  533         :returns: XML <EntityDescriptor> object.
  534         :raises keystone.exception.ValidationError: If the required
  535             config options aren't set.
  536         """
  537         self._ensure_required_values_present()
  538         entity_descriptor = self._create_entity_descriptor()
  539         entity_descriptor.idpsso_descriptor = (
  540             self._create_idp_sso_descriptor())
  541         return entity_descriptor
  542 
  543     def _create_entity_descriptor(self):
  544         ed = md.EntityDescriptor()
  545         ed.entity_id = CONF.saml.idp_entity_id
  546         return ed
  547 
  548     def _create_idp_sso_descriptor(self):
  549 
  550         def get_cert():
  551             try:
  552                 return sigver.read_cert_from_file(CONF.saml.certfile, 'pem')
  553             except (IOError, sigver.CertificateError) as e:
  554                 msg = ('Cannot open certificate %(cert_file)s.'
  555                        'Reason: %(reason)s') % {
  556                     'cert_file': CONF.saml.certfile, 'reason': e}
  557                 tr_msg = _('Cannot open certificate %(cert_file)s.'
  558                            'Reason: %(reason)s') % {
  559                     'cert_file': CONF.saml.certfile, 'reason': e}
  560                 LOG.error(msg)
  561                 raise IOError(tr_msg)
  562 
  563         def key_descriptor():
  564             cert = get_cert()
  565             return md.KeyDescriptor(
  566                 key_info=xmldsig.KeyInfo(
  567                     x509_data=xmldsig.X509Data(
  568                         x509_certificate=xmldsig.X509Certificate(text=cert)
  569                     )
  570                 ), use='signing'
  571             )
  572 
  573         def single_sign_on_service():
  574             idp_sso_endpoint = CONF.saml.idp_sso_endpoint
  575             return md.SingleSignOnService(
  576                 binding=saml2.BINDING_URI,
  577                 location=idp_sso_endpoint)
  578 
  579         def organization():
  580             name = md.OrganizationName(lang=CONF.saml.idp_lang,
  581                                        text=CONF.saml.idp_organization_name)
  582             display_name = md.OrganizationDisplayName(
  583                 lang=CONF.saml.idp_lang,
  584                 text=CONF.saml.idp_organization_display_name)
  585             url = md.OrganizationURL(lang=CONF.saml.idp_lang,
  586                                      text=CONF.saml.idp_organization_url)
  587 
  588             return md.Organization(
  589                 organization_display_name=display_name,
  590                 organization_url=url, organization_name=name)
  591 
  592         def contact_person():
  593             company = md.Company(text=CONF.saml.idp_contact_company)
  594             given_name = md.GivenName(text=CONF.saml.idp_contact_name)
  595             surname = md.SurName(text=CONF.saml.idp_contact_surname)
  596             email = md.EmailAddress(text=CONF.saml.idp_contact_email)
  597             telephone = md.TelephoneNumber(
  598                 text=CONF.saml.idp_contact_telephone)
  599             contact_type = CONF.saml.idp_contact_type
  600 
  601             return md.ContactPerson(
  602                 company=company, given_name=given_name, sur_name=surname,
  603                 email_address=email, telephone_number=telephone,
  604                 contact_type=contact_type)
  605 
  606         def name_id_format():
  607             return md.NameIDFormat(text=saml.NAMEID_FORMAT_TRANSIENT)
  608 
  609         idpsso = md.IDPSSODescriptor()
  610         idpsso.protocol_support_enumeration = samlp.NAMESPACE
  611         idpsso.key_descriptor = key_descriptor()
  612         idpsso.single_sign_on_service = single_sign_on_service()
  613         idpsso.name_id_format = name_id_format()
  614         if self._check_organization_values():
  615             idpsso.organization = organization()
  616         if self._check_contact_person_values():
  617             idpsso.contact_person = contact_person()
  618         return idpsso
  619 
  620     def _ensure_required_values_present(self):
  621         """Ensure idp_sso_endpoint and idp_entity_id have values."""
  622         if CONF.saml.idp_entity_id is None:
  623             msg = _('Ensure configuration option idp_entity_id is set.')
  624             raise exception.ValidationError(msg)
  625         if CONF.saml.idp_sso_endpoint is None:
  626             msg = _('Ensure configuration option idp_sso_endpoint is set.')
  627             raise exception.ValidationError(msg)
  628 
  629     def _check_contact_person_values(self):
  630         """Determine if contact information is included in metadata."""
  631         # Check if we should include contact information
  632         params = [CONF.saml.idp_contact_company,
  633                   CONF.saml.idp_contact_name,
  634                   CONF.saml.idp_contact_surname,
  635                   CONF.saml.idp_contact_email,
  636                   CONF.saml.idp_contact_telephone,
  637                   CONF.saml.idp_contact_type]
  638         for value in params:
  639             if value is None:
  640                 return False
  641 
  642         return True
  643 
  644     def _check_organization_values(self):
  645         """Determine if organization information is included in metadata."""
  646         params = [CONF.saml.idp_organization_name,
  647                   CONF.saml.idp_organization_display_name,
  648                   CONF.saml.idp_organization_url]
  649         for value in params:
  650             if value is None:
  651                 return False
  652         return True
  653 
  654 
  655 class ECPGenerator(object):
  656     """A class for generating an ECP assertion."""
  657 
  658     @staticmethod
  659     def generate_ecp(saml_assertion, relay_state_prefix):
  660         ecp_generator = ECPGenerator()
  661         header = ecp_generator._create_header(relay_state_prefix)
  662         body = ecp_generator._create_body(saml_assertion)
  663         envelope = soapenv.Envelope(header=header, body=body)
  664         return envelope
  665 
  666     def _create_header(self, relay_state_prefix):
  667         relay_state_text = relay_state_prefix + uuid.uuid4().hex
  668         relay_state = ecp.RelayState(actor=client_base.ACTOR,
  669                                      must_understand='1',
  670                                      text=relay_state_text)
  671         header = soapenv.Header()
  672         header.extension_elements = (
  673             [saml2.element_to_extension_element(relay_state)])
  674         return header
  675 
  676     def _create_body(self, saml_assertion):
  677         body = soapenv.Body()
  678         body.extension_elements = (
  679             [saml2.element_to_extension_element(saml_assertion)])
  680         return body