def attribute_query(environ, attribute_requests, subject_id, subject_id_format='urn:esg:openid'): """ Make a query for a set of attributes Returns a dictionary of values for each attribute in the query @param attribute_requests: SAML Attribute instances @param subject_id: The ID of the subject, e.g. openid @param subject_id_format: optional format of the ID, default is openid """ # Grab info from the environ saml_trusted_ca_dir = environ.get('saml_trusted_ca_dir', '') attr_service_uri = environ.get('attr_service_uri', '') client_binding = AttributeQuerySslSOAPBinding() client_binding.sslCACertDir = saml_trusted_ca_dir client_binding.clockSkewTolerance = 1 # 1 second tolerance # Make a new query object query = AttributeQuery() query.version = SAMLVersion(SAMLVersion.VERSION_20) query.id = str(uuid4()) query.issueInstant = datetime.utcnow() query.issuer = Issuer() query.issuer.format = Issuer.X509_SUBJECT query.issuer.value = '/O=STFC/OU=SPBU/CN=test' query.subject = Subject() query.subject.nameID = NameID() query.subject.nameID.format = subject_id_format query.subject.nameID.value = subject_id # Specify what attributes to query for for attribute_request in attribute_requests: query.attributes.append(attribute_request) try: response = client_binding.send(query, uri=attr_service_uri) attribute_results = {} assertion = next(iter(response.assertions or []), None) if assertion: statement = next(iter(assertion.attributeStatements or []), None) if statement: for attribute in statement.attributes: values = [] for attribute_value in attribute.attributeValues: values.append(attribute_value.value) attribute_results[attribute.name] = values return attribute_results except (RequestResponseError, Error) as e: logger.error("Error processing SOAP query for {0}: {1}".format(id, e))
def _attributeQuery(self, thisSection): uri = self.cfgParser.get(thisSection, 'uri') print("Testing %s ..." % thisSection) print("Calling Attribute Service %r ..." % uri) binding = AttributeQuerySslSOAPBinding.fromConfig( self.__class__.CONFIG_FILEPATH, section=thisSection, prefix='attributeQuery.') binding.subjectID = self.cfgParser.get(thisSection, 'subject') response = binding.send(uri=uri) # ESGFResponseElementTree has an extension to support ESG Group/Role # Attribute Value samlResponseElem = ESGFResponseElementTree.toXML(response) print("SAML Response ...") # print(ElementTree.tostring(samlResponseElem)) print("Pretty print SAML Response ...") print(prettyPrint(samlResponseElem)) self.assert_(response.status.statusCode.value==StatusCode.SUCCESS_URI)
def __init__(self, sessionCacheDataDir=None, sessionCacheTimeout=None, sessionCacheAssertionClockSkewTol=1.0): '''Initialise settings for connection to an Attribute Authority @param sessionCacheDataDir: directory for permanent storage of sessions. Sessions are used as a means of optimisation caching Attribute Query results to reduce the number of Attribute Authority web service calls. If set to None, sessions are cached in memory only. @type sessionCacheDataDir: None type / basestring @param sessionCacheTimeout: time in seconds for individual caches' lifetimes. Set to None to set no expiry. @type sessionCacheTimeout: float/int/long/string or None type ''' self.sessionCacheDataDir = sessionCacheDataDir self.sessionCacheTimeout = sessionCacheTimeout self.__sessionCacheAssertionClockSkewTol = \ sessionCacheAssertionClockSkewTol self.__subjectAttributeId = None self.__mappingFilePath = None # Force mapping dict to have string type keys and items _typeCheckers = (lambda val: isinstance(val, basestring), ) * 2 self.__attributeId2AttributeAuthorityMap = VettedDict(*_typeCheckers) self.__attribute_query = AttributeQueryFactory.create() self.__attribute_query_binding = AttributeQuerySslSOAPBinding() self.__cacheSessions = True
def test02SendQuery(self): query_binding = AttributeQuerySslSOAPBinding() attribute_query = AttributeQueryFactory.create() attribute_query.subject.nameID.format = self.__class__.SUBJECT_FORMAT attribute_query.subject.nameID.value = self.__class__.SUBJECT attribute_query.issuerName = '/O=Site A/CN=Authorisation Service' attribute = Attribute() attribute.name = 'urn:ndg:saml:emailaddress' attribute.friendlyName = 'emailAddress' attribute.nameFormat = 'http://www.w3.org/2001/XMLSchema' attribute_query.attributes.append(attribute) query_binding.clockSkewTolerance = 2. query_binding.sslCACertDir = self.__class__.CLIENT_CACERT_DIR query_binding.sslCertFilePath = self.__class__.CLIENT_CERT_FILEPATH query_binding.sslPriKeyFilePath = self.__class__.CLIENT_PRIKEY_FILEPATH query_binding.sslValidDNs = self.__class__.VALID_DNS response = query_binding.send(attribute_query, uri=self.__class__.SERVICE_URI) # Convert back to ElementTree instance read for string output samlResponseElem = ResponseElementTree.toXML(response) print("SAML Response ...") print(ElementTree.tostring(samlResponseElem)) print("Pretty print SAML Response ...") print(prettyPrint(samlResponseElem)) self.assert_(response.status.statusCode.value==StatusCode.SUCCESS_URI)
def initialise(self, app_conf, prefix=DEFAULT_PARAM_PREFIX, **local_conf): """Initialise attributes from the given local configuration settings @param app_conf: application configuration settings - ignored - this method includes this arg to fit Paste middleware / app function signature @type app_conf: dict @param prefix: optional prefix for parameter names included in the local_conf dict - enables these parameters to be filtered from others which don't apply to this middleware @param local_conf: attribute settings to apply @type local_conf: dict """ if prefix is None: prefix = '' prefixLength = len(prefix) queryPrefix = prefix + self.__class__.ATTRIBUTE_QUERY_PARAMS_PREFIX queryPrefixLength = len(queryPrefix) for k in local_conf: # SSL parameters apply to this class and to the attribute query # client. if k.startswith(queryPrefix): paramName = k[queryPrefixLength:] elif k.startswith(prefix): paramName = k[prefixLength:] if paramName in AttributeRequestMiddleware.PARAM_NAMES: setattr(self, paramName, local_conf[k]) self._attributeQueryClient = AttributeQuerySslSOAPBinding() # Parse authorisation decision query options self._attributeQueryClient.parseKeywords(prefix=queryPrefix, **local_conf) sslContext = ssl_context_util.make_ssl_context(self.sslPriKeyFilePath, self.sslCertFilePath, None, self.sslCACertDir, verify_peer=True) self._httpsClientConfig = httpsclient_utils.Configuration( sslContext, False)
def dispatch(self, command): if command == self.__class__.ATTRIBUTE_QUERY_CMD: query = self.create_attribute_query() binding = AttributeQuerySslSOAPBinding() elif command == self.__class__.AUTHZ_DECISION_QUERY_CMD: query = self.create_authz_decision_query() binding = AuthzDecisionQuerySslSOAPBinding() binding.sslCACertDir = self.ca_cert_dir binding.ssl_no_peer_verification = self.ssl_no_peer_verification binding.sslCertFilePath = self.client_cert_filepath binding.sslPriKeyFilePath = self.client_prikey_filepath binding.clockSkewTolerance = self.clock_skew_tolerance response = binding.send(query, uri=self.service_uri) return response
class AttributeRequestMiddleware(NDGSecurityMiddlewareBase): """Makes an attribute request to obtain ESG attributes corresponding to the username stored in the environ. """ DEFAULT_PARAM_PREFIX = 'attr_req.' ATTRIBUTE_QUERY_PARAMS_PREFIX = 'attributeQuery.' ATTRIBUTE_NAME_MAP = { 'urn:esg:first:name': 'firstname', 'urn:esg:last:name': 'lastname', 'urn:esg:email:address': 'email' } # Key in environ for session SESSION_KEY_OPTION_DEFAULT = 'beaker.session.ndg.security' # Key in session for attribute dict SESSION_ATTRIBUTE_KEY_OPTION_DEFAULT = 'openid.ax' # Constants for parsing XDRS document XRDS_NS = 'xri://$xrd*($v*2.0)' XRDS_SERVICE_PATH = ('{%s}XRD/{%s}Service' % (XRDS_NS, XRDS_NS)) XRDS_TYPE_PATH = ('{%s}Type' % XRDS_NS) XRDS_URI_PATH = ('{%s}URI' % XRDS_NS) XRDS_ATTRIBUTE_SERVICE_TYPE = 'urn:esg:security:attribute-service' PARAM_NAMES = ('attributeServiceUrl', 'sslCACertDir', 'sslCertFilePath', 'sslPriKeyFilePath', 'sessionAttributeKey', 'sessionKey') __slots__ = ('_app', '_attributeQueryClient', '_httpsClientConfig') __slots__ += tuple(['__' + i for i in PARAM_NAMES]) del i def __init__(self, app): self._app = app self.__attributeServiceUrl = None self.__sessionAttributeKey = self.__class__.SESSION_ATTRIBUTE_KEY_OPTION_DEFAULT self.__sessionKey = self.__class__.SESSION_KEY_OPTION_DEFAULT def initialise(self, app_conf, prefix=DEFAULT_PARAM_PREFIX, **local_conf): """Initialise attributes from the given local configuration settings @param app_conf: application configuration settings - ignored - this method includes this arg to fit Paste middleware / app function signature @type app_conf: dict @param prefix: optional prefix for parameter names included in the local_conf dict - enables these parameters to be filtered from others which don't apply to this middleware @param local_conf: attribute settings to apply @type local_conf: dict """ if prefix is None: prefix = '' prefixLength = len(prefix) queryPrefix = prefix + self.__class__.ATTRIBUTE_QUERY_PARAMS_PREFIX queryPrefixLength = len(queryPrefix) for k in local_conf: # SSL parameters apply to this class and to the attribute query # client. if k.startswith(queryPrefix): paramName = k[queryPrefixLength:] elif k.startswith(prefix): paramName = k[prefixLength:] if paramName in AttributeRequestMiddleware.PARAM_NAMES: setattr(self, paramName, local_conf[k]) self._attributeQueryClient = AttributeQuerySslSOAPBinding() # Parse authorisation decision query options self._attributeQueryClient.parseKeywords(prefix=queryPrefix, **local_conf) sslContext = ssl_context_util.make_ssl_context(self.sslPriKeyFilePath, self.sslCertFilePath, None, self.sslCACertDir, verify_peer=True) self._httpsClientConfig = httpsclient_utils.Configuration( sslContext, False) @classmethod def filter_app_factory(cls, app, app_conf, **kw): obj = cls(app) obj.initialise(app_conf, **kw) return obj def __call__(self, environ, start_response): """Checks whether the attributes are stored in the session and if not makes an attribute request. """ # Get session. session = environ.get(self.sessionKey) if session is None: raise Exception( 'AttributeRequestMiddleware.__call__: No beaker session key ' '"%s" found in environ' % self.sessionKey) username = environ.get(self.__class__.USERNAME_ENVIRON_KEYNAME) log.debug("Found username: %s", username) if (username and (self.sessionAttributeKey not in session)): attributes = self._getAttributes(username) session[self.sessionAttributeKey] = attributes session.save() return self._app(environ, start_response) @staticmethod def _isHttpUrl(string): """Determines whether a string can be interpreted as a HTTP or HTTPS URL. @type string: basestring @param string: string to test @rtype: bool @return: True if string can be parsed as a URL with a scheme that is HTTP or HTTPS and at least a net location, otherwise False """ parts = urlparse.urlsplit(string) return (parts.scheme in ['http', 'https']) and bool(parts.netloc) def _getAttributeService(self, subject): """ @type subject: basestring @param subject: subject for which the query is to be made @rtype: basestring @return: URL of attribute service """ if not self._isHttpUrl(subject): log.debug( "Subject is not a HTTP URL - not making Yadis request to" " obtain attribute service: %s", subject) return None try: log.debug( "Making Yadis request to obtain attribute service for" " subject %s", subject) xrdsStr = httpsclient_utils.fetch_from_url(subject, self._httpsClientConfig) except Exception, exc: log.error( "Unable to determine attribute service for subject %s: %s", subject, exc.__str__()) return None xrdsEl = ElementTree.XML(xrdsStr) for svcEl in xrdsEl.findall(self.__class__.XRDS_SERVICE_PATH): isAttrService = False for typeEl in svcEl.findall(self.__class__.XRDS_TYPE_PATH): if typeEl.text == self.__class__.XRDS_ATTRIBUTE_SERVICE_TYPE: isAttrService = True if isAttrService: for uriEl in svcEl.findall(self.__class__.XRDS_URI_PATH): attributeServiceUrl = uriEl.text log.debug("Found attribute service URL: %s", attributeServiceUrl) return attributeServiceUrl return None
class CertExtApp(object): """Application to create a X.509 certificate extension containing a SAML assertion for inclusion by MyProxy into an issued certificate """ DEFAULT_QUERY_ATTRIBUTES = ESGFDefaultQueryAttributes.ATTRIBUTES N_DEFAULT_QUERY_ATTRIBUTES = len(DEFAULT_QUERY_ATTRIBUTES) ESG_NAME_ID_FORMAT = ESGFSamlNamespaces.NAMEID_FORMAT CONNECTION_STRING_OPTNAME = 'connectionString' OPENID_SQLQUERY_OPTNAME = 'openIdSqlQuery' ATTRIBUTE_AUTHORITY_URI_OPTNAME = 'attributeAuthorityURI' CONFIG_FILE_OPTNAMES = ( ATTRIBUTE_AUTHORITY_URI_OPTNAME, CONNECTION_STRING_OPTNAME, OPENID_SQLQUERY_OPTNAME, ) ATTRIBUTE_QUERY_ATTRNAME = 'attributeQuery' LEN_ATTRIBUTE_QUERY_ATTRNAME = len(ATTRIBUTE_QUERY_ATTRNAME) __PRIVATE_ATTR_PREFIX = '__' __slots__ = tuple( [__PRIVATE_ATTR_PREFIX + i for i in CONFIG_FILE_OPTNAMES + (ATTRIBUTE_QUERY_ATTRNAME,)] ) del i def __init__(self): self.__attributeAuthorityURI = None self.__connectionString = None self.__openIdSqlQuery = None self.__attributeQuery = AttributeQuerySslSOAPBinding() @classmethod def fromConfigFile(cls, configFilePath, **kw): '''Alternative constructor makes object from config file settings @type configFilePath: basestring @param configFilePath: configuration file path ''' certExtApp = cls() certExtApp.readConfig(configFilePath, **kw) return certExtApp def __call__(self, username): """Main method - create SAML assertion by querying the user's OpenID identifier from the user database and using this to query the Attribute Authority for attributes """ self.__attributeQuery.subjectID = self.queryOpenId(username) response = self.__attributeQuery.send(uri=self.attributeAuthorityURI) try: assertionStr = self.serialiseAssertion(response.assertions[0]) except (IndexError, TypeError): raise CertExtAppRetrieveError("Error accessing assertion from " "Attribute Authority SAML response: " "%s" % traceback.format_exc()) return assertionStr def readConfig(self, cfg, prefix='', section='DEFAULT'): '''Read config file settings @type cfg: basestring /ConfigParser derived type @param cfg: configuration file path or ConfigParser type object @type prefix: basestring @param prefix: prefix for option names e.g. "certExtApp." @type section: baestring @param section: configuration file section from which to extract parameters. ''' if isinstance(cfg, basestring): cfgFilePath = os.path.expandvars(cfg) _cfg = CaseSensitiveConfigParser() _cfg.read(cfgFilePath) elif isinstance(cfg, ConfigParser): _cfg = cfg else: raise AttributeError('Expecting basestring or ConfigParser type ' 'for "cfg" attribute; got %r type' % type(cfg)) prefixLen = len(prefix) for optName, val in _cfg.items(section): if prefix: # Filter attributes based on prefix if optName.startswith(prefix): setattr(self, optName[prefixLen:], val) else: # No prefix set - attempt to set all attributes setattr(self, optName, val) def __setattr__(self, name, value): """Enable setting of AttributeQuerySslSOAPBinding attributes from names starting with attributeQuery.* / attributeQuery_*. Addition for setting these values from ini file """ try: super(CertExtApp, self).__setattr__(name, value) except AttributeError: # Coerce into setting AttributeQuerySslSOAPBinding attributes - # names must start with 'attributeQuery\W' e.g. # attributeQuery.clockSkew or attributeQuery_issuerDN if name.startswith(CertExtApp.ATTRIBUTE_QUERY_ATTRNAME): setattr(self.__attributeQuery, name[CertExtApp.LEN_ATTRIBUTE_QUERY_ATTRNAME+1:], value) else: raise @property def attributeQuery(self): """SAML SOAP Attribute Query client binding object""" return self.__attributeQuery def _getAttributeAuthorityURI(self): return self.__attributeAuthorityURI def _setAttributeAuthorityURI(self, value): if not isinstance(value, basestring): raise TypeError('Expecting string type for "attributeAuthorityURI";' ' got %r instead' % type(value)) self.__attributeAuthorityURI = value attributeAuthorityURI = property(_getAttributeAuthorityURI, _setAttributeAuthorityURI, doc="Attribute Authority SOAP SAML URI") def _getConnectionString(self): return self.__connectionString def _setConnectionString(self, value): if not isinstance(value, basestring): raise TypeError('Expecting string type for "%s" attribute; got %r'% (CertExtApp.CONNECTION_STRING_OPTNAME, type(value))) self.__connectionString = os.path.expandvars(value) connectionString = property(fget=_getConnectionString, fset=_setConnectionString, doc="Database connection string") def _getOpenIdSqlQuery(self): return self.__openIdSqlQuery def _setOpenIdSqlQuery(self, value): if not isinstance(value, basestring): raise TypeError('Expecting string type for "%s" attribute; got %r'% (CertExtApp.OPENID_SQLQUERY_OPTNAME, type(value))) self.__openIdSqlQuery = value openIdSqlQuery = property(fget=_getOpenIdSqlQuery, fset=_setOpenIdSqlQuery, doc="SQL Query for authentication request") def __getstate__(self): '''Specific implementation needed with __slots__''' return dict([(attrName, getattr(self, attrName)) for attrName in CertExtApp.__slots__]) def __setstate__(self, attrDict): '''Specific implementation needed with __slots__''' for attr, val in attrDict.items(): setattr(self, attr, val) def serialiseAssertion(self, assertion): """Convert SAML assertion object into a string""" samlAssertionElem = AssertionElementTree.toXML(assertion) return ElementTree.tostring(samlAssertionElem) def queryOpenId(self, username): """Given a username, query for user OpenID from the user database @type username: basestring @param username: username @rtype: basestring @return: the OpenID identifier corresponding to the input username """ try: dbEngine = create_engine(self.connectionString) except ImportError, e: raise CertExtAppConfigError("Missing database engine for " "SQLAlchemy: %s" % e) connection = dbEngine.connect() try: queryInputs = dict(username=username) query = Template(self.openIdSqlQuery).substitute(queryInputs) result = connection.execute(query) except exc.ProgrammingError: raise CertExtAppSqlError("Error with SQL Syntax: %s" % traceback.format_exc()) finally: connection.close() try: openId = [r for r in result][0][0] except Exception: raise CertExtAppRetrieveError("Error with result set: %s" % traceback.format_exc()) log.debug('Query succeeded for user %r' % username) return openId
def __init__(self): self.__attributeAuthorityURI = None self.__connectionString = None self.__openIdSqlQuery = None self.__attributeQuery = AttributeQuerySslSOAPBinding()