--- /dev/null
+#! /usr/bin/env python
+#
+# Copyright 2011 Roland Hedberg <roland.hedberg@adm.umu.se>
+#
+# $Id$
+
+__author__ = 'rolandh'
+
+import radiusd
+import sys
+from saml2 import soap
+from saml2.client import Saml2Client
+from saml2.s_utils import sid
+from saml2.response import attribute_response
+
+# Where's the configuration
+CONFIG_DIR = "/usr/local/etc/moonshot"
+sys.path.insert(0, CONFIG_DIR)
+
+import config
+
+# Globals
+CLIENT = None
+HTTP = None
+
+
+def eq_len_parts(str, delta=250):
+ res = []
+ n = 0
+ strlen = len(str)
+ while n <= strlen:
+ m = n + delta
+ res.append("".join(str[n:m]))
+ n = m
+ return res
+
+
+def log(level, s):
+ """Log function."""
+ radiusd.radlog(level, 'moonshot.py: ' + s)
+
+
+class LOG(object):
+ def info(self, txt):
+ log(radiusd.L_INFO, txt)
+
+ def error(self, txt):
+ log(radiusd.L_ERR, txt)
+
+#noinspection PyUnusedLocal
+def instantiate(p):
+ """Module Instantiation. 0 for success, -1 for failure.
+ p is a dummy variable here.
+ """
+ global CLIENT
+ global HTTP
+
+ try:
+ CLIENT = Saml2Client(config.DEBUG,
+ identity_cache=config.IDENTITY_CACHE,
+ state_cache=config.STATE_CACHE,
+ config_file=config.CONFIG)
+ except Exception, e:
+ # Report the error and return -1 for failure.
+ # xxx A more advanced module would retry the database.
+ log(radiusd.L_ERR, str(e))
+
+ return -1
+
+ try:
+ HTTP = soap.SOAPClient("") # No default URL
+ except Exception, e:
+ log(radiusd.L_ERR, str(e))
+ return -1
+
+ log(radiusd.L_INFO, 'SP initialized')
+
+ return 0
+
+
+def attribute_query(cls, subject_id, destination, issuer_id=None,
+ attribute=None, sp_name_qualifier=None, name_qualifier=None,
+ nameid_format=None, log=None, sign=False):
+ """ Does a attribute request to an attribute authority, this is
+ by default done over SOAP. Other bindings could be used but are not
+ supported right now.
+
+ :param subject_id: The identifier of the subject
+ :param destination: To whom the query should be sent
+ :param issuer_id: Who is sending this query
+ :param attribute: A dictionary of attributes and values that is asked for
+ :param sp_name_qualifier: The unique identifier of the
+ service provider or affiliation of providers for whom the
+ identifier was generated.
+ :param name_qualifier: The unique identifier of the identity
+ provider that generated the identifier.
+ :param nameid_format: The format of the name ID
+ :param log: Function to use for logging
+ :param sign: Whether the request should be signed or not
+ :return: The Assertion
+ """
+
+ if log is None:
+ log = cls.logger
+
+ session_id = sid()
+ issuer = cls.issuer(issuer_id)
+
+ if not name_qualifier and not sp_name_qualifier:
+ sp_name_qualifier = cls.config.entityid
+
+ request = cls.create_attribute_query(session_id, subject_id,
+ destination, issuer, attribute,
+ sp_name_qualifier,
+ name_qualifier,
+ nameid_format=nameid_format, sign=sign)
+
+ # soapclient = HTTP.send(destination, cls.config.key_file,
+ # cls.config.cert_file)
+
+ try:
+ response = HTTP.send(request, path=destination)
+ except Exception, exc:
+ if log:
+ log.info("SoapClient exception: %s" % (exc,))
+ return None
+
+ if response:
+ try:
+ # synchronous operation
+ return_addr = cls.config.endpoint('assertion_consumer_service')[0]
+ aresp = attribute_response(cls.config, return_addr, log=log)
+ aresp.allow_unsolicited = True
+ aresp.asynchop = False
+ #aresp.debug = True
+ except Exception, exc:
+ if log:
+ log.error("%s", (exc,))
+ return None
+
+ try:
+ _resp = aresp.loads(response, False, HTTP.response).verify()
+ except Exception, err:
+ if log:
+ log.error("%s", (exc,))
+ return None
+ if _resp is None:
+ if log:
+ log.error("Didn't like the response")
+ return None
+
+ return _resp.assertion
+ else:
+ return None
+
+
+def only_allowed_attributes(client, assertion, allowed):
+ res = []
+ _aconvs = client.config.attribute_converters
+
+ for statement in assertion.attribute_statement:
+ for attribute in statement.attribute:
+ if attribute.friendly_name:
+ fname = attribute.friendly_name
+ else:
+ fname = ""
+ for acv in _aconvs:
+ if acv.name_form == attribute.name_form:
+ fname = acv._fro[attribute.name]
+
+ if fname in allowed:
+ res.append(attribute)
+
+ return assertion
+
+
+def post_auth(authData):
+ """ Attribute aggregation after authentication
+ This is the function that is accessible from the freeradius server core.
+
+ :return: A 3-tuple
+ """
+
+ global CLIENT
+ global HTTP
+
+ # Extract the data we need.
+ userName = None
+ serviceName = ""
+ hostName = ""
+ #userPasswd = None
+
+ for t in authData:
+ if t[0] == 'User-Name':
+ userName = t[1][1:-1]
+ elif t[0] == "GSS-Acceptor-Service-Name":
+ serviceName = t[1][1:-1]
+ elif t[0] == "GSS-Acceptor-Host-Name":
+ hostName = t[1][1:-1]
+
+ _srv = "%s:%s" % (serviceName, hostName)
+ log(radiusd.L_DBG, "Working on behalf of: %s" % _srv)
+
+
+ # Find the endpoint to use
+ location = CLIENT.config.attribute_services(
+ config.ATTRIBUTE_AUTHORITY)[0].location
+ log(radiusd.L_DBG, "location: %s" % location)
+
+ # Build and send the attribute query
+ sp_name_qualifier = config.SP_NAME_QUALIFIER
+ name_qualifier = config.NAME_QUALIFIER
+ nameid_format = config.NAMEID_FORMAT
+
+ log(radiusd.L_DBG, "SP_NAME_QUALIFIER: %s" % sp_name_qualifier)
+ log(radiusd.L_DBG, "NAME_QUALIFIER: %s" % name_qualifier)
+ log(radiusd.L_DBG, "NAMEID_FORMAT: %s" % nameid_format)
+
+ _attribute_assertion = attribute_query(CLIENT,
+ userName,
+ location,
+ sp_name_qualifier=sp_name_qualifier,
+ name_qualifier=name_qualifier,
+ nameid_format=nameid_format,
+ issuer_id=CLIENT.issuer(),
+ log=LOG(),
+ sign=config.SIGN)
+
+ if _attribute_assertion is None:
+ return radiusd.RLM_MODULE_FAIL
+
+ if _attribute_assertion is False:
+ log(radiusd.L_DBG, "IdP returned: %s" % HTTP.server.error_description)
+ return radiusd.RLM_MODULE_FAIL
+
+ # remove the subject confirmation if there is one
+ _attribute_assertion.subject.subject_confirmation = []
+ # Only allow attributes that the service should have
+ try:
+ _attribute_assertion = only_allowed_attributes(CLIENT,
+ _attribute_assertion,
+ config.ATTRIBUTE_FILTER[
+ _srv])
+ except KeyError:
+ pass
+
+ log(radiusd.L_DBG, "Assertion: %s" % _attribute_assertion)
+
+ # Log the success
+ log(radiusd.L_DBG, 'user accepted: %s' % (userName, ))
+
+ # We are adding to the RADIUS packet
+ # We need to set an Auth-Type.
+
+ # UKERNA, 25622; attribute ID is 132
+ attr = "SAML-AAA-Assertion"
+ #attr = "UKERNA-Attr-%d" % 132
+ #attr = "Vendor-%d-Attr-%d" % (25622, 132)
+ restup = (tuple([(attr, x) for x in eq_len_parts(
+ "%s" % _attribute_assertion, 248)]))
+
+ return radiusd.RLM_MODULE_UPDATED, restup, None
+
+
+# Test the modules
+if __name__ == '__main__':
+ instantiate(None)
+ # print authorize((('User-Name', '"map"'), ('User-Password', '"abc"')))
+ print post_auth((('User-Name', '"roland"'), ('User-Password', '"one"')))